Repository: itcharge/AlgoNote Branch: main Commit: 02499d0df8a3 Files: 1514 Total size: 4.0 MB Directory structure: gitextract_dhdwtz4s/ ├── .github/ │ └── workflows/ │ └── sync.yml ├── .gitignore ├── LICENSE ├── README.md ├── codes/ │ └── python/ │ ├── 01_array/ │ │ ├── array_maxheap.py │ │ ├── array_sort_bubble_sort.py │ │ ├── array_sort_bucket_sort.py │ │ ├── array_sort_counting_sort.py │ │ ├── array_sort_insertion_sort.py │ │ ├── array_sort_maxheap_sort.py │ │ ├── array_sort_merge_sort.py │ │ ├── array_sort_minheap_sort.py │ │ ├── array_sort_quick_sort.py │ │ ├── array_sort_radix_sort.py │ │ ├── array_sort_selection_sort.py │ │ └── array_sort_shell_sort.py │ ├── 02_linked_list/ │ │ ├── linked_list.py │ │ ├── linked_list_bubble_sort.py │ │ ├── linked_list_bucket_sort.py │ │ ├── linked_list_counting_sort.py │ │ ├── linked_list_insertion_sort.py │ │ ├── linked_list_merge_sort.py │ │ ├── linked_list_quick_sort.py │ │ ├── linked_list_radix_sort.py │ │ └── linked_list_section_sort.py │ ├── 03_stack_queue_hash_table/ │ │ ├── queue_circularSequential_queue.py │ │ ├── queue_deque.py │ │ ├── queue_link_queue.py │ │ ├── queue_priority_queue.py │ │ ├── queue_sequential_queue.py │ │ ├── stack_link_stack.py │ │ ├── stack_monotone_stack.py │ │ └── stack_sequential_stack.py │ ├── 04_string/ │ │ ├── string_Strcmp.py │ │ ├── string_boyer_moore.py │ │ ├── string_brute_force.py │ │ ├── string_horspool.py │ │ ├── string_kmp.py │ │ ├── string_rabin_karp.py │ │ ├── string_sunday.py │ │ └── string_trie.py │ ├── 05_tree/ │ │ ├── tree_binaryindexed_tree.py │ │ ├── tree_dynamicSegmentTree_update_interval_1.py │ │ ├── tree_dynamicSegmentTree_update_interval_2.py │ │ ├── tree_segmentTree_update_interval_1.py │ │ ├── tree_segmentTree_update_interval_2.py │ │ ├── tree_segmentTree_update_point.py │ │ ├── tree_unionFind.py │ │ ├── tree_unionFind_QuickFind.py │ │ ├── tree_unionFind_QuickUnion.py │ │ ├── tree_unionFind_UnoinByRank.py │ │ └── tree_unionFind_UnoinBySize.py │ ├── 06_graph/ │ │ ├── Graph-Adjacency-List.py │ │ ├── Graph-Adjacency-Matrix.py │ │ ├── Graph-BFS.py │ │ ├── Graph-Bellman-Ford.py │ │ ├── Graph-DFS.py │ │ ├── Graph-Edgeset-Array.py │ │ ├── Graph-Hash-Table.py │ │ ├── Graph-Kruskal.py │ │ ├── Graph-Linked-Forward-Star.py │ │ ├── Graph-Prim.py │ │ ├── Graph-Topological-Sorting-DFS.py │ │ └── Graph-Topological-Sorting-Kahn.py │ └── 08_dynamic_programming/ │ ├── Digit-DP.py │ ├── Pack-2DCostPack.py │ ├── Pack-CompletePack.py │ ├── Pack-GroupPack.py │ ├── Pack-MixedPack.py │ ├── Pack-MultiplePack.py │ ├── Pack-ProblemVariants.py │ └── Pack-ZeroOnePack.py └── docs/ ├── 00_preface/ │ ├── 00_01_preface.md │ ├── 00_02_data_structures_algorithms.md │ ├── 00_03_algorithm_complexity.md │ ├── 00_04_leetcode_guide.md │ ├── 00_05_solutions_list.md │ ├── 00_06_categories_list.md │ ├── 00_07_interview_100_list.md │ ├── 00_08_interview_200_list.md │ └── index.md ├── 01_array/ │ ├── 01_01_array_basic.md │ ├── 01_02_array_sort.md │ ├── 01_03_array_bubble_sort.md │ ├── 01_04_array_selection_sort.md │ ├── 01_05_array_insertion_sort.md │ ├── 01_06_array_shell_sort.md │ ├── 01_07_array_merge_sort.md │ ├── 01_08_array_quick_sort.md │ ├── 01_09_array_heap_sort.md │ ├── 01_10_array_counting_sort.md │ ├── 01_11_array_bucket_sort.md │ ├── 01_12_array_radix_sort.md │ ├── 01_13_array_binary_search_01.md │ ├── 01_14_array_binary_search_02.md │ ├── 01_15_array_two_pointers.md │ ├── 01_16_array_sliding_window.md │ └── index.md ├── 02_linked_list/ │ ├── 02_01_linked_list_basic.md │ ├── 02_02_linked_list_sort.md │ ├── 02_03_linked_list_bubble_sort.md │ ├── 02_04_linked_list_selection_sort.md │ ├── 02_05_linked_list_insertion_sort.md │ ├── 02_06_linked_list_merge_sort.md │ ├── 02_07_linked_list_quick_sort.md │ ├── 02_08_linked_list_counting_sort.md │ ├── 02_09_linked_list_bucket_sort.md │ ├── 02_10_linked_list_radix_sort.md │ ├── 02_11_linked_list_two_pointers.md │ └── index.md ├── 03_stack_queue_hash_table/ │ ├── 03_01_stack_basic.md │ ├── 03_02_monotone_stack.md │ ├── 03_03_queue_basic.md │ ├── 03_04_priority_queue.md │ ├── 03_05_bidirectional_queue.md │ ├── 03_06_hash_table.md │ └── index.md ├── 04_string/ │ ├── 04_01_string_basic.md │ ├── 04_02_string_brute_force.md │ ├── 04_03_string_rabin_karp.md │ ├── 04_04_string_kmp.md │ ├── 04_05_string_boyer_moore.md │ ├── 04_06_string_horspool.md │ ├── 04_07_string_sunday.md │ ├── 04_08_trie.md │ ├── 04_09_ac_automaton.md │ ├── 04_10_suffix_array.md │ └── index.md ├── 05_tree/ │ ├── 05_01_tree_basic.md │ ├── 05_02_binary_tree_traverse.md │ ├── 05_03_binary_tree_reduction.md │ ├── 05_04_binary_search_tree.md │ ├── 05_05_segment_tree_01.md │ ├── 05_06_segment_tree_02.md │ ├── 05_07_binary_indexed_tree.md │ ├── 05_08_union_find.md │ └── index.md ├── 06_graph/ │ ├── 06_01_graph_basic.md │ ├── 06_02_graph_structure.md │ ├── 06_03_graph_dfs.md │ ├── 06_04_graph_bfs.md │ ├── 06_05_graph_topological_sorting.md │ ├── 06_06_graph_minimum_spanning_tree.md │ ├── 06_07_graph_shortest_path_01.md │ ├── 06_08_graph_shortest_path_02.md │ ├── 06_09_graph_multi_source_shortest_path.md │ ├── 06_10_graph_the_second_shortest_path.md │ ├── 06_11_graph_bipartite_basic.md │ ├── 06_12_graph_bipartite_matching.md │ └── index.md ├── 07_algorithm/ │ ├── 07_01_enumeration_algorithm.md │ ├── 07_02_recursive_algorithm.md │ ├── 07_03_divide_and_conquer_algorithm.md │ ├── 07_04_backtracking_algorithm.md │ ├── 07_05_greedy_algorithm.md │ ├── 07_06_bit_operation.md │ └── index.md ├── 08_dynamic_programming/ │ ├── 08_01_dynamic_programming_basic.md │ ├── 08_02_memoization_search.md │ ├── 08_03_linear_dp_01.md │ ├── 08_04_linear_dp_02.md │ ├── 08_05_linear_dp_03.md │ ├── 08_06_knapsack_problem_01.md │ ├── 08_07_knapsack_problem_02.md │ ├── 08_08_knapsack_problem_03.md │ ├── 08_09_knapsack_problem_04.md │ ├── 08_10_knapsack_problem_05.md │ ├── 08_11_interval_dp.md │ ├── 08_12_tree_dp.md │ ├── 08_13_state_compression_dp.md │ ├── 08_14_counting_dp.md │ ├── 08_15_digit_dp.md │ ├── 08_16_probability_dp.md │ └── index.md ├── README.md ├── others/ │ ├── index.md │ ├── todo_list.md │ └── update_time.md └── solutions/ ├── 0001-0099/ │ ├── 3sum-closest.md │ ├── 3sum.md │ ├── 4sum.md │ ├── add-binary.md │ ├── add-two-numbers.md │ ├── binary-tree-inorder-traversal.md │ ├── climbing-stairs.md │ ├── combination-sum-ii.md │ ├── combination-sum.md │ ├── combinations.md │ ├── container-with-most-water.md │ ├── count-and-say.md │ ├── decode-ways.md │ ├── divide-two-integers.md │ ├── edit-distance.md │ ├── find-first-and-last-position-of-element-in-sorted-array.md │ ├── find-the-index-of-the-first-occurrence-in-a-string.md │ ├── first-missing-positive.md │ ├── generate-parentheses.md │ ├── gray-code.md │ ├── group-anagrams.md │ ├── index.md │ ├── insert-interval.md │ ├── integer-to-roman.md │ ├── interleaving-string.md │ ├── jump-game-ii.md │ ├── jump-game.md │ ├── largest-rectangle-in-histogram.md │ ├── length-of-last-word.md │ ├── letter-combinations-of-a-phone-number.md │ ├── longest-common-prefix.md │ ├── longest-palindromic-substring.md │ ├── longest-substring-without-repeating-characters.md │ ├── longest-valid-parentheses.md │ ├── maximal-rectangle.md │ ├── maximum-subarray.md │ ├── median-of-two-sorted-arrays.md │ ├── merge-intervals.md │ ├── merge-k-sorted-lists.md │ ├── merge-sorted-array.md │ ├── merge-two-sorted-lists.md │ ├── minimum-path-sum.md │ ├── minimum-window-substring.md │ ├── multiply-strings.md │ ├── n-queens-ii.md │ ├── n-queens.md │ ├── next-permutation.md │ ├── palindrome-number.md │ ├── partition-list.md │ ├── permutation-sequence.md │ ├── permutations-ii.md │ ├── permutations.md │ ├── plus-one.md │ ├── powx-n.md │ ├── recover-binary-search-tree.md │ ├── regular-expression-matching.md │ ├── remove-duplicates-from-sorted-array-ii.md │ ├── remove-duplicates-from-sorted-array.md │ ├── remove-duplicates-from-sorted-list-ii.md │ ├── remove-duplicates-from-sorted-list.md │ ├── remove-element.md │ ├── remove-nth-node-from-end-of-list.md │ ├── restore-ip-addresses.md │ ├── reverse-integer.md │ ├── reverse-linked-list-ii.md │ ├── reverse-nodes-in-k-group.md │ ├── roman-to-integer.md │ ├── rotate-image.md │ ├── rotate-list.md │ ├── scramble-string.md │ ├── search-a-2d-matrix.md │ ├── search-in-rotated-sorted-array-ii.md │ ├── search-in-rotated-sorted-array.md │ ├── search-insert-position.md │ ├── set-matrix-zeroes.md │ ├── simplify-path.md │ ├── sort-colors.md │ ├── spiral-matrix-ii.md │ ├── spiral-matrix.md │ ├── sqrtx.md │ ├── string-to-integer-atoi.md │ ├── subsets-ii.md │ ├── subsets.md │ ├── substring-with-concatenation-of-all-words.md │ ├── sudoku-solver.md │ ├── swap-nodes-in-pairs.md │ ├── text-justification.md │ ├── trapping-rain-water.md │ ├── two-sum.md │ ├── unique-binary-search-trees-ii.md │ ├── unique-binary-search-trees.md │ ├── unique-paths-ii.md │ ├── unique-paths.md │ ├── valid-number.md │ ├── valid-parentheses.md │ ├── valid-sudoku.md │ ├── validate-binary-search-tree.md │ ├── wildcard-matching.md │ ├── word-search.md │ └── zigzag-conversion.md ├── 0100-0199/ │ ├── balanced-binary-tree.md │ ├── best-time-to-buy-and-sell-stock-ii.md │ ├── best-time-to-buy-and-sell-stock-iii.md │ ├── best-time-to-buy-and-sell-stock-iv.md │ ├── best-time-to-buy-and-sell-stock.md │ ├── binary-search-tree-iterator.md │ ├── binary-tree-level-order-traversal-ii.md │ ├── binary-tree-level-order-traversal.md │ ├── binary-tree-maximum-path-sum.md │ ├── binary-tree-postorder-traversal.md │ ├── binary-tree-preorder-traversal.md │ ├── binary-tree-right-side-view.md │ ├── binary-tree-upside-down.md │ ├── binary-tree-zigzag-level-order-traversal.md │ ├── candy.md │ ├── clone-graph.md │ ├── compare-version-numbers.md │ ├── construct-binary-tree-from-inorder-and-postorder-traversal.md │ ├── construct-binary-tree-from-preorder-and-inorder-traversal.md │ ├── convert-sorted-array-to-binary-search-tree.md │ ├── convert-sorted-list-to-binary-search-tree.md │ ├── copy-list-with-random-pointer.md │ ├── distinct-subsequences.md │ ├── dungeon-game.md │ ├── evaluate-reverse-polish-notation.md │ ├── excel-sheet-column-number.md │ ├── excel-sheet-column-title.md │ ├── factorial-trailing-zeroes.md │ ├── find-minimum-in-rotated-sorted-array-ii.md │ ├── find-minimum-in-rotated-sorted-array.md │ ├── find-peak-element.md │ ├── flatten-binary-tree-to-linked-list.md │ ├── fraction-to-recurring-decimal.md │ ├── gas-station.md │ ├── house-robber.md │ ├── index.md │ ├── insertion-sort-list.md │ ├── intersection-of-two-linked-lists.md │ ├── largest-number.md │ ├── linked-list-cycle-ii.md │ ├── linked-list-cycle.md │ ├── longest-consecutive-sequence.md │ ├── longest-substring-with-at-most-two-distinct-characters.md │ ├── lru-cache.md │ ├── majority-element.md │ ├── max-points-on-a-line.md │ ├── maximum-depth-of-binary-tree.md │ ├── maximum-gap.md │ ├── maximum-product-subarray.md │ ├── min-stack.md │ ├── minimum-depth-of-binary-tree.md │ ├── missing-ranges.md │ ├── number-of-1-bits.md │ ├── one-edit-distance.md │ ├── palindrome-partitioning-ii.md │ ├── palindrome-partitioning.md │ ├── pascals-triangle-ii.md │ ├── pascals-triangle.md │ ├── path-sum-ii.md │ ├── path-sum.md │ ├── populating-next-right-pointers-in-each-node-ii.md │ ├── populating-next-right-pointers-in-each-node.md │ ├── read-n-characters-given-read4-ii-call-multiple-times.md │ ├── read-n-characters-given-read4.md │ ├── reorder-list.md │ ├── repeated-dna-sequences.md │ ├── reverse-bits.md │ ├── reverse-words-in-a-string-ii.md │ ├── reverse-words-in-a-string.md │ ├── rotate-array.md │ ├── same-tree.md │ ├── single-number-ii.md │ ├── single-number.md │ ├── sort-list.md │ ├── sum-root-to-leaf-numbers.md │ ├── surrounded-regions.md │ ├── symmetric-tree.md │ ├── triangle.md │ ├── two-sum-ii-input-array-is-sorted.md │ ├── two-sum-iii-data-structure-design.md │ ├── valid-palindrome.md │ ├── word-break-ii.md │ ├── word-break.md │ ├── word-ladder-ii.md │ └── word-ladder.md ├── 0200-0299/ │ ├── 3sum-smaller.md │ ├── add-digits.md │ ├── alien-dictionary.md │ ├── basic-calculator-ii.md │ ├── basic-calculator.md │ ├── best-meeting-point.md │ ├── binary-tree-longest-consecutive-sequence.md │ ├── binary-tree-paths.md │ ├── bitwise-and-of-numbers-range.md │ ├── bulls-and-cows.md │ ├── closest-binary-search-tree-value-ii.md │ ├── closest-binary-search-tree-value.md │ ├── combination-sum-iii.md │ ├── contains-duplicate-ii.md │ ├── contains-duplicate-iii.md │ ├── contains-duplicate.md │ ├── count-complete-tree-nodes.md │ ├── count-primes.md │ ├── count-univalue-subtrees.md │ ├── course-schedule-ii.md │ ├── course-schedule.md │ ├── delete-node-in-a-linked-list.md │ ├── design-add-and-search-words-data-structure.md │ ├── different-ways-to-add-parentheses.md │ ├── encode-and-decode-strings.md │ ├── expression-add-operators.md │ ├── factor-combinations.md │ ├── find-median-from-data-stream.md │ ├── find-the-celebrity.md │ ├── find-the-duplicate-number.md │ ├── first-bad-version.md │ ├── flatten-2d-vector.md │ ├── flip-game-ii.md │ ├── flip-game.md │ ├── game-of-life.md │ ├── graph-valid-tree.md │ ├── group-shifted-strings.md │ ├── h-index-ii.md │ ├── h-index.md │ ├── happy-number.md │ ├── house-robber-ii.md │ ├── implement-queue-using-stacks.md │ ├── implement-stack-using-queues.md │ ├── implement-trie-prefix-tree.md │ ├── index.md │ ├── inorder-successor-in-bst.md │ ├── integer-to-english-words.md │ ├── invert-binary-tree.md │ ├── isomorphic-strings.md │ ├── kth-largest-element-in-an-array.md │ ├── kth-smallest-element-in-a-bst.md │ ├── lowest-common-ancestor-of-a-binary-search-tree.md │ ├── lowest-common-ancestor-of-a-binary-tree.md │ ├── majority-element-ii.md │ ├── maximal-square.md │ ├── meeting-rooms-ii.md │ ├── meeting-rooms.md │ ├── minimum-size-subarray-sum.md │ ├── missing-number.md │ ├── move-zeroes.md │ ├── nim-game.md │ ├── number-of-digit-one.md │ ├── number-of-islands.md │ ├── paint-fence.md │ ├── paint-house-ii.md │ ├── paint-house.md │ ├── palindrome-linked-list.md │ ├── palindrome-permutation-ii.md │ ├── palindrome-permutation.md │ ├── peeking-iterator.md │ ├── perfect-squares.md │ ├── power-of-two.md │ ├── product-of-array-except-self.md │ ├── rectangle-area.md │ ├── remove-linked-list-elements.md │ ├── reverse-linked-list.md │ ├── search-a-2d-matrix-ii.md │ ├── serialize-and-deserialize-binary-tree.md │ ├── shortest-palindrome.md │ ├── shortest-word-distance-ii.md │ ├── shortest-word-distance-iii.md │ ├── shortest-word-distance.md │ ├── single-number-iii.md │ ├── sliding-window-maximum.md │ ├── strobogrammatic-number-ii.md │ ├── strobogrammatic-number-iii.md │ ├── strobogrammatic-number.md │ ├── summary-ranges.md │ ├── the-skyline-problem.md │ ├── ugly-number-ii.md │ ├── ugly-number.md │ ├── unique-word-abbreviation.md │ ├── valid-anagram.md │ ├── verify-preorder-sequence-in-binary-search-tree.md │ ├── walls-and-gates.md │ ├── wiggle-sort.md │ ├── word-pattern-ii.md │ ├── word-pattern.md │ ├── word-search-ii.md │ └── zigzag-iterator.md ├── 0300-0399/ │ ├── additive-number.md │ ├── android-unlock-patterns.md │ ├── best-time-to-buy-and-sell-stock-with-cooldown.md │ ├── binary-tree-vertical-order-traversal.md │ ├── bomb-enemy.md │ ├── bulb-switcher.md │ ├── burst-balloons.md │ ├── coin-change.md │ ├── combination-sum-iv.md │ ├── count-numbers-with-unique-digits.md │ ├── count-of-range-sum.md │ ├── count-of-smaller-numbers-after-self.md │ ├── counting-bits.md │ ├── create-maximum-number.md │ ├── data-stream-as-disjoint-intervals.md │ ├── decode-string.md │ ├── design-hit-counter.md │ ├── design-phone-directory.md │ ├── design-snake-game.md │ ├── design-tic-tac-toe.md │ ├── design-twitter.md │ ├── elimination-game.md │ ├── evaluate-division.md │ ├── find-k-pairs-with-smallest-sums.md │ ├── find-leaves-of-binary-tree.md │ ├── find-the-difference.md │ ├── first-unique-character-in-a-string.md │ ├── flatten-nested-list-iterator.md │ ├── generalized-abbreviation.md │ ├── guess-number-higher-or-lower-ii.md │ ├── guess-number-higher-or-lower.md │ ├── house-robber-iii.md │ ├── increasing-triplet-subsequence.md │ ├── index.md │ ├── insert-delete-getrandom-o1-duplicates-allowed.md │ ├── insert-delete-getrandom-o1.md │ ├── integer-break.md │ ├── integer-replacement.md │ ├── intersection-of-two-arrays-ii.md │ ├── intersection-of-two-arrays.md │ ├── is-subsequence.md │ ├── kth-smallest-element-in-a-sorted-matrix.md │ ├── largest-bst-subtree.md │ ├── largest-divisible-subset.md │ ├── lexicographical-numbers.md │ ├── line-reflection.md │ ├── linked-list-random-node.md │ ├── logger-rate-limiter.md │ ├── longest-absolute-file-path.md │ ├── longest-increasing-path-in-a-matrix.md │ ├── longest-increasing-subsequence.md │ ├── longest-substring-with-at-least-k-repeating-characters.md │ ├── longest-substring-with-at-most-k-distinct-characters.md │ ├── max-sum-of-rectangle-no-larger-than-k.md │ ├── maximum-product-of-word-lengths.md │ ├── maximum-size-subarray-sum-equals-k.md │ ├── mini-parser.md │ ├── minimum-height-trees.md │ ├── moving-average-from-data-stream.md │ ├── nested-list-weight-sum-ii.md │ ├── nested-list-weight-sum.md │ ├── number-of-connected-components-in-an-undirected-graph.md │ ├── number-of-islands-ii.md │ ├── odd-even-linked-list.md │ ├── palindrome-pairs.md │ ├── patching-array.md │ ├── perfect-rectangle.md │ ├── plus-one-linked-list.md │ ├── power-of-four.md │ ├── power-of-three.md │ ├── random-pick-index.md │ ├── range-addition.md │ ├── range-sum-query-2d-immutable.md │ ├── range-sum-query-2d-mutable.md │ ├── range-sum-query-immutable.md │ ├── range-sum-query-mutable.md │ ├── ransom-note.md │ ├── rearrange-string-k-distance-apart.md │ ├── reconstruct-itinerary.md │ ├── remove-duplicate-letters.md │ ├── remove-invalid-parentheses.md │ ├── reverse-string.md │ ├── reverse-vowels-of-a-string.md │ ├── rotate-function.md │ ├── russian-doll-envelopes.md │ ├── self-crossing.md │ ├── shortest-distance-from-all-buildings.md │ ├── shuffle-an-array.md │ ├── smallest-rectangle-enclosing-black-pixels.md │ ├── sort-transformed-array.md │ ├── sparse-matrix-multiplication.md │ ├── sum-of-two-integers.md │ ├── super-pow.md │ ├── super-ugly-number.md │ ├── top-k-frequent-elements.md │ ├── utf-8-validation.md │ ├── valid-perfect-square.md │ ├── verify-preorder-serialization-of-a-binary-tree.md │ ├── water-and-jug-problem.md │ ├── wiggle-sort-ii.md │ └── wiggle-subsequence.md ├── 0400-0499/ │ ├── 132-pattern.md │ ├── 4sum-ii.md │ ├── add-strings.md │ ├── add-two-numbers-ii.md │ ├── all-oone-data-structure.md │ ├── arithmetic-slices-ii-subsequence.md │ ├── arithmetic-slices.md │ ├── arranging-coins.md │ ├── assign-cookies.md │ ├── battleships-in-a-board.md │ ├── binary-watch.md │ ├── can-i-win.md │ ├── circular-array-loop.md │ ├── concatenated-words.md │ ├── construct-quad-tree.md │ ├── construct-the-rectangle.md │ ├── convert-a-number-to-hexadecimal.md │ ├── convert-binary-search-tree-to-sorted-doubly-linked-list.md │ ├── convex-polygon.md │ ├── count-the-repetitions.md │ ├── delete-node-in-a-bst.md │ ├── diagonal-traverse.md │ ├── encode-n-ary-tree-to-binary-tree.md │ ├── encode-string-with-shortest-length.md │ ├── find-all-anagrams-in-a-string.md │ ├── find-all-duplicates-in-an-array.md │ ├── find-all-numbers-disappeared-in-an-array.md │ ├── find-permutation.md │ ├── find-right-interval.md │ ├── fizz-buzz.md │ ├── flatten-a-multilevel-doubly-linked-list.md │ ├── frog-jump.md │ ├── generate-random-point-in-a-circle.md │ ├── hamming-distance.md │ ├── heaters.md │ ├── implement-rand10-using-rand7.md │ ├── index.md │ ├── island-perimeter.md │ ├── k-th-smallest-in-lexicographical-order.md │ ├── largest-palindrome-product.md │ ├── lfu-cache.md │ ├── license-key-formatting.md │ ├── longest-palindrome.md │ ├── longest-repeating-character-replacement.md │ ├── magical-string.md │ ├── matchsticks-to-square.md │ ├── max-consecutive-ones-ii.md │ ├── max-consecutive-ones.md │ ├── maximum-xor-of-two-numbers-in-an-array.md │ ├── minimum-genetic-mutation.md │ ├── minimum-moves-to-equal-array-elements-ii.md │ ├── minimum-moves-to-equal-array-elements.md │ ├── minimum-number-of-arrows-to-burst-balloons.md │ ├── minimum-unique-word-abbreviation.md │ ├── n-ary-tree-level-order-traversal.md │ ├── next-greater-element-i.md │ ├── non-decreasing-subsequences.md │ ├── non-overlapping-intervals.md │ ├── nth-digit.md │ ├── number-complement.md │ ├── number-of-boomerangs.md │ ├── number-of-segments-in-a-string.md │ ├── ones-and-zeroes.md │ ├── optimal-account-balancing.md │ ├── pacific-atlantic-water-flow.md │ ├── partition-equal-subset-sum.md │ ├── path-sum-iii.md │ ├── poor-pigs.md │ ├── predict-the-winner.md │ ├── queue-reconstruction-by-height.md │ ├── random-point-in-non-overlapping-rectangles.md │ ├── reconstruct-original-digits-from-english.md │ ├── remove-k-digits.md │ ├── repeated-substring-pattern.md │ ├── reverse-pairs.md │ ├── robot-room-cleaner.md │ ├── sentence-screen-fitting.md │ ├── sequence-reconstruction.md │ ├── serialize-and-deserialize-bst.md │ ├── serialize-and-deserialize-n-ary-tree.md │ ├── sliding-window-median.md │ ├── smallest-good-base.md │ ├── sort-characters-by-frequency.md │ ├── split-array-largest-sum.md │ ├── string-compression.md │ ├── strong-password-checker.md │ ├── sum-of-left-leaves.md │ ├── target-sum.md │ ├── teemo-attacking.md │ ├── ternary-expression-parser.md │ ├── the-maze-iii.md │ ├── the-maze.md │ ├── third-maximum-number.md │ ├── total-hamming-distance.md │ ├── trapping-rain-water-ii.md │ ├── unique-substrings-in-wraparound-string.md │ ├── valid-word-abbreviation.md │ ├── valid-word-square.md │ ├── validate-ip-address.md │ ├── word-squares.md │ └── zuma-game.md ├── 0500-0599/ │ ├── 01-matrix.md │ ├── array-nesting.md │ ├── array-partition.md │ ├── base-7.md │ ├── beautiful-arrangement.md │ ├── binary-tree-longest-consecutive-sequence-ii.md │ ├── binary-tree-tilt.md │ ├── boundary-of-binary-tree.md │ ├── brick-wall.md │ ├── coin-change-ii.md │ ├── complex-number-multiplication.md │ ├── construct-binary-tree-from-string.md │ ├── contiguous-array.md │ ├── continuous-subarray-sum.md │ ├── convert-bst-to-greater-tree.md │ ├── delete-operation-for-two-strings.md │ ├── design-in-memory-file-system.md │ ├── detect-capital.md │ ├── diameter-of-binary-tree.md │ ├── distribute-candies.md │ ├── encode-and-decode-tinyurl.md │ ├── erect-the-fence.md │ ├── fibonacci-number.md │ ├── find-bottom-left-tree-value.md │ ├── find-largest-value-in-each-tree-row.md │ ├── find-mode-in-binary-search-tree.md │ ├── find-the-closest-palindrome.md │ ├── fraction-addition-and-subtraction.md │ ├── freedom-trail.md │ ├── index.md │ ├── inorder-successor-in-bst-ii.md │ ├── ipo.md │ ├── k-diff-pairs-in-an-array.md │ ├── keyboard-row.md │ ├── kill-process.md │ ├── logical-or-of-two-binary-grids-represented-as-quad-trees.md │ ├── lonely-pixel-i.md │ ├── lonely-pixel-ii.md │ ├── longest-harmonious-subsequence.md │ ├── longest-line-of-consecutive-one-in-matrix.md │ ├── longest-palindromic-subsequence.md │ ├── longest-uncommon-subsequence-i.md │ ├── longest-uncommon-subsequence-ii.md │ ├── longest-word-in-dictionary-through-deleting.md │ ├── maximum-depth-of-n-ary-tree.md │ ├── maximum-vacation-days.md │ ├── minesweeper.md │ ├── minimum-absolute-difference-in-bst.md │ ├── minimum-index-sum-of-two-lists.md │ ├── minimum-time-difference.md │ ├── most-frequent-subtree-sum.md │ ├── n-ary-tree-postorder-traversal.md │ ├── n-ary-tree-preorder-traversal.md │ ├── next-greater-element-ii.md │ ├── next-greater-element-iii.md │ ├── number-of-provinces.md │ ├── optimal-division.md │ ├── out-of-boundary-paths.md │ ├── output-contest-matches.md │ ├── perfect-number.md │ ├── permutation-in-string.md │ ├── random-flip-matrix.md │ ├── random-pick-with-weight.md │ ├── range-addition-ii.md │ ├── relative-ranks.md │ ├── remove-boxes.md │ ├── reshape-the-matrix.md │ ├── reverse-string-ii.md │ ├── reverse-words-in-a-string-iii.md │ ├── shortest-unsorted-continuous-subarray.md │ ├── single-element-in-a-sorted-array.md │ ├── split-array-with-equal-sum.md │ ├── split-concatenated-strings.md │ ├── squirrel-simulation.md │ ├── student-attendance-record-i.md │ ├── student-attendance-record-ii.md │ ├── subarray-sum-equals-k.md │ ├── subtree-of-another-tree.md │ ├── super-washing-machines.md │ ├── tag-validator.md │ ├── the-maze-ii.md │ ├── valid-square.md │ └── word-abbreviation.md ├── 0600-0699/ │ ├── 2-keys-keyboard.md │ ├── 24-game.md │ ├── 4-keys-keyboard.md │ ├── add-bold-tag-in-string.md │ ├── add-one-row-to-tree.md │ ├── average-of-levels-in-binary-tree.md │ ├── baseball-game.md │ ├── beautiful-arrangement-ii.md │ ├── binary-number-with-alternating-bits.md │ ├── bulb-switcher-ii.md │ ├── can-place-flowers.md │ ├── coin-path.md │ ├── construct-string-from-binary-tree.md │ ├── count-binary-substrings.md │ ├── course-schedule-iii.md │ ├── cut-off-trees-for-golf-event.md │ ├── decode-ways-ii.md │ ├── degree-of-an-array.md │ ├── design-circular-deque.md │ ├── design-circular-queue.md │ ├── design-compressed-string-iterator.md │ ├── design-excel-sum-formula.md │ ├── design-log-storage-system.md │ ├── design-search-autocomplete-system.md │ ├── dota2-senate.md │ ├── employee-importance.md │ ├── equal-tree-partition.md │ ├── exclusive-time-of-functions.md │ ├── falling-squares.md │ ├── find-duplicate-file-in-system.md │ ├── find-duplicate-subtrees.md │ ├── find-k-closest-elements.md │ ├── find-the-derangement-of-an-array.md │ ├── image-smoother.md │ ├── implement-magic-dictionary.md │ ├── index.md │ ├── k-empty-slots.md │ ├── k-inverse-pairs-array.md │ ├── knight-probability-in-chessboard.md │ ├── longest-continuous-increasing-subsequence.md │ ├── longest-univalue-path.md │ ├── map-sum-pairs.md │ ├── max-area-of-island.md │ ├── maximum-average-subarray-i.md │ ├── maximum-average-subarray-ii.md │ ├── maximum-binary-tree.md │ ├── maximum-distance-in-arrays.md │ ├── maximum-length-of-pair-chain.md │ ├── maximum-product-of-three-numbers.md │ ├── maximum-sum-of-3-non-overlapping-subarrays.md │ ├── maximum-swap.md │ ├── maximum-width-of-binary-tree.md │ ├── merge-two-binary-trees.md │ ├── minimum-factorization.md │ ├── next-closest-time.md │ ├── non-decreasing-array.md │ ├── non-negative-integers-without-consecutive-ones.md │ ├── number-of-distinct-islands.md │ ├── number-of-longest-increasing-subsequence.md │ ├── palindromic-substrings.md │ ├── partition-to-k-equal-sum-subsets.md │ ├── path-sum-iv.md │ ├── print-binary-tree.md │ ├── redundant-connection-ii.md │ ├── redundant-connection.md │ ├── remove-9.md │ ├── repeated-string-match.md │ ├── replace-words.md │ ├── robot-return-to-origin.md │ ├── second-minimum-node-in-a-binary-tree copy.md │ ├── second-minimum-node-in-a-binary-tree.md │ ├── set-mismatch.md │ ├── shopping-offers.md │ ├── smallest-range-covering-elements-from-k-lists.md │ ├── solve-the-equation.md │ ├── split-array-into-consecutive-subsequences.md │ ├── stickers-to-spell-word.md │ ├── strange-printer.md │ ├── sum-of-square-numbers.md │ ├── task-scheduler.md │ ├── top-k-frequent-words copy.md │ ├── top-k-frequent-words.md │ ├── trim-a-binary-search-tree.md │ ├── two-sum-iv-input-is-a-bst.md │ ├── valid-palindrome-ii.md │ ├── valid-parenthesis-string.md │ └── valid-triangle-number.md ├── 0700-0799/ │ ├── 1-bit-and-2-bit-characters.md │ ├── accounts-merge.md │ ├── all-paths-from-source-to-target.md │ ├── asteroid-collision.md │ ├── basic-calculator-iv.md │ ├── best-time-to-buy-and-sell-stock-with-transaction-fee.md │ ├── binary-search.md │ ├── bold-words-in-string.md │ ├── champagne-tower.md │ ├── cheapest-flights-within-k-stops.md │ ├── cherry-pickup.md │ ├── contain-virus.md │ ├── count-different-palindromic-subsequences.md │ ├── couples-holding-hands.md │ ├── cracking-the-safe.md │ ├── custom-sort-string.md │ ├── daily-temperatures.md │ ├── delete-and-earn.md │ ├── design-hashmap.md │ ├── design-hashset.md │ ├── design-linked-list.md │ ├── domino-and-tromino-tiling.md │ ├── escape-the-ghosts.md │ ├── find-k-th-smallest-pair-distance.md │ ├── find-pivot-index.md │ ├── find-smallest-letter-greater-than-target.md │ ├── flood-fill.md │ ├── global-and-local-inversions.md │ ├── index.md │ ├── insert-into-a-binary-search-tree.md │ ├── insert-into-a-sorted-circular-linked-list.md │ ├── is-graph-bipartite.md │ ├── jewels-and-stones.md │ ├── k-th-smallest-prime-fraction.md │ ├── k-th-symbol-in-grammar.md │ ├── kth-largest-element-in-a-stream.md │ ├── largest-number-at-least-twice-of-others.md │ ├── largest-plus-sign.md │ ├── letter-case-permutation.md │ ├── longest-word-in-dictionary.md │ ├── max-chunks-to-make-sorted-ii.md │ ├── max-chunks-to-make-sorted.md │ ├── maximum-length-of-repeated-subarray.md │ ├── min-cost-climbing-stairs.md │ ├── minimum-ascii-delete-sum-for-two-strings.md │ ├── minimum-distance-between-bst-nodes.md │ ├── minimum-window-subsequence.md │ ├── monotone-increasing-digits.md │ ├── my-calendar-i.md │ ├── my-calendar-ii.md │ ├── my-calendar-iii.md │ ├── network-delay-time.md │ ├── number-of-atoms.md │ ├── number-of-matching-subsequences.md │ ├── number-of-subarrays-with-bounded-maximum.md │ ├── open-the-lock.md │ ├── parse-lisp-expression.md │ ├── partition-labels.md │ ├── prefix-and-suffix-search.md │ ├── preimage-size-of-factorial-zeroes-function.md │ ├── prime-number-of-set-bits-in-binary-representation.md │ ├── pyramid-transition-matrix.md │ ├── rabbits-in-forest.md │ ├── random-pick-with-blacklist.md │ ├── range-module.md │ ├── reach-a-number.md │ ├── reaching-points.md │ ├── remove-comments.md │ ├── reorganize-string.md │ ├── rotate-string.md │ ├── rotated-digits.md │ ├── search-in-a-binary-search-tree.md │ ├── search-in-a-sorted-array-of-unknown-size.md │ ├── self-dividing-numbers.md │ ├── set-intersection-size-at-least-two.md │ ├── shortest-completing-word.md │ ├── sliding-puzzle.md │ ├── smallest-rotation-with-highest-score.md │ ├── split-linked-list-in-parts.md │ ├── subarray-product-less-than-k.md │ ├── swap-adjacent-in-lr-string.md │ ├── swim-in-rising-water.md │ ├── to-lower-case.md │ ├── toeplitz-matrix.md │ ├── transform-to-chessboard.md │ └── valid-tic-tac-toe-state.md ├── 0800-0899/ │ ├── advantage-shuffle.md │ ├── all-nodes-distance-k-in-binary-tree.md │ ├── all-possible-full-binary-trees.md │ ├── ambiguous-coordinates.md │ ├── backspace-string-compare.md │ ├── binary-gap.md │ ├── binary-tree-pruning.md │ ├── binary-trees-with-factors.md │ ├── bitwise-ors-of-subarrays.md │ ├── boats-to-save-people.md │ ├── bricks-falling-when-hit.md │ ├── buddy-strings.md │ ├── bus-routes.md │ ├── car-fleet.md │ ├── card-flipping-game.md │ ├── chalkboard-xor-game.md │ ├── consecutive-numbers-sum.md │ ├── construct-binary-tree-from-preorder-and-postorder-traversal.md │ ├── count-unique-characters-of-all-substrings-of-a-given-string.md │ ├── decoded-string-at-index.md │ ├── exam-room.md │ ├── expressive-words.md │ ├── fair-candy-swap.md │ ├── find-and-replace-in-string.md │ ├── find-and-replace-pattern.md │ ├── find-eventual-safe-states.md │ ├── flipping-an-image.md │ ├── friends-of-appropriate-ages.md │ ├── goat-latin.md │ ├── groups-of-special-equivalent-strings.md │ ├── guess-the-word.md │ ├── hand-of-straights.md │ ├── image-overlap.md │ ├── increasing-order-search-tree.md │ ├── index.md │ ├── k-similar-strings.md │ ├── keys-and-rooms.md │ ├── koko-eating-bananas.md │ ├── largest-sum-of-averages.md │ ├── largest-triangle-area.md │ ├── leaf-similar-trees.md │ ├── lemonade-change.md │ ├── length-of-longest-fibonacci-subsequence.md │ ├── linked-list-components.md │ ├── longest-mountain-in-array.md │ ├── loud-and-rich.md │ ├── magic-squares-in-grid.md │ ├── making-a-large-island.md │ ├── masking-personal-information.md │ ├── max-increase-to-keep-city-skyline.md │ ├── maximize-distance-to-closest-person.md │ ├── maximum-frequency-stack.md │ ├── middle-of-the-linked-list.md │ ├── minimum-cost-to-hire-k-workers.md │ ├── minimum-number-of-refueling-stops.md │ ├── minimum-swaps-to-make-sequences-increasing.md │ ├── mirror-reflection.md │ ├── monotonic-array.md │ ├── most-common-word.md │ ├── most-profit-assigning-work.md │ ├── new-21-game.md │ ├── nth-magical-number.md │ ├── number-of-lines-to-write-string.md │ ├── orderly-queue.md │ ├── peak-index-in-a-mountain-array.md │ ├── positions-of-large-groups.md │ ├── possible-bipartition.md │ ├── prime-palindrome.md │ ├── profitable-schemes.md │ ├── projection-area-of-3d-shapes.md │ ├── push-dominoes.md │ ├── race-car.md │ ├── reachable-nodes-in-subdivided-graph.md │ ├── rectangle-area-ii.md │ ├── rectangle-overlap.md │ ├── reordered-power-of-2.md │ ├── score-after-flipping-matrix.md │ ├── score-of-parentheses.md │ ├── shifting-letters copy.md │ ├── shifting-letters.md │ ├── short-encoding-of-words.md │ ├── shortest-distance-to-a-character.md │ ├── shortest-path-to-get-all-keys.md │ ├── shortest-path-visiting-all-nodes.md │ ├── shortest-subarray-with-sum-at-least-k.md │ ├── similar-rgb-color.md │ ├── similar-string-groups.md │ ├── smallest-subtree-with-all-the-deepest-nodes.md │ ├── soup-servings.md │ ├── spiral-matrix-iii.md │ ├── split-array-into-fibonacci-sequence.md │ ├── split-array-with-same-average.md │ ├── stone-game.md │ ├── subdomain-visit-count.md │ ├── sum-of-distances-in-tree.md │ ├── sum-of-subsequence-widths.md │ ├── super-egg-drop.md │ ├── surface-area-of-3d-shapes.md │ ├── transpose-matrix.md │ ├── uncommon-words-from-two-sentences.md │ ├── unique-morse-code-words.md │ └── walking-robot-simulation.md ├── 0900-0999/ │ ├── 3sum-with-multiplicity.md │ ├── add-to-array-form-of-integer.md │ ├── array-of-doubled-pairs.md │ ├── available-captures-for-rook.md │ ├── bag-of-tokens.md │ ├── beautiful-array.md │ ├── binary-subarrays-with-sum.md │ ├── binary-tree-cameras.md │ ├── broken-calculator.md │ ├── cat-and-mouse.md │ ├── check-completeness-of-a-binary-tree.md │ ├── complete-binary-tree-inserter.md │ ├── cousins-in-binary-tree.md │ ├── delete-columns-to-make-sorted-ii.md │ ├── delete-columns-to-make-sorted-iii.md │ ├── delete-columns-to-make-sorted.md │ ├── di-string-match.md │ ├── distinct-subsequences-ii.md │ ├── distribute-coins-in-binary-tree.md │ ├── equal-rational-numbers.md │ ├── find-the-shortest-superstring.md │ ├── find-the-town-judge.md │ ├── flip-binary-tree-to-match-preorder-traversal.md │ ├── flip-equivalent-binary-trees.md │ ├── flip-string-to-monotone-increasing.md │ ├── fruit-into-baskets.md │ ├── index.md │ ├── interval-list-intersections.md │ ├── k-closest-points-to-origin.md │ ├── knight-dialer.md │ ├── largest-component-size-by-common-factor.md │ ├── largest-perimeter-triangle.md │ ├── largest-time-for-given-digits.md │ ├── least-operators-to-express-number.md │ ├── long-pressed-name.md │ ├── longest-turbulent-subarray.md │ ├── maximum-binary-tree-ii.md │ ├── maximum-sum-circular-subarray.md │ ├── maximum-width-ramp.md │ ├── minimize-malware-spread-ii.md │ ├── minimize-malware-spread.md │ ├── minimum-add-to-make-parentheses-valid.md │ ├── minimum-area-rectangle-ii.md │ ├── minimum-area-rectangle.md │ ├── minimum-cost-for-tickets.md │ ├── minimum-falling-path-sum.md │ ├── minimum-increment-to-make-array-unique.md │ ├── minimum-number-of-k-consecutive-bit-flips.md │ ├── most-stones-removed-with-same-row-or-column.md │ ├── n-repeated-element-in-size-2n-array.md │ ├── number-of-music-playlists.md │ ├── number-of-recent-calls.md │ ├── number-of-squareful-arrays.md │ ├── numbers-at-most-n-given-digit-set.md │ ├── numbers-with-same-consecutive-differences.md │ ├── odd-even-jump.md │ ├── online-election.md │ ├── online-stock-span.md │ ├── pancake-sorting.md │ ├── partition-array-into-disjoint-intervals.md │ ├── powerful-integers.md │ ├── prison-cells-after-n-days.md │ ├── range-sum-of-bst.md │ ├── regions-cut-by-slashes.md │ ├── reorder-data-in-log-files.md │ ├── reveal-cards-in-increasing-order.md │ ├── reverse-only-letters.md │ ├── rle-iterator.md │ ├── rotting-oranges.md │ ├── satisfiability-of-equality-equations.md │ ├── shortest-bridge.md │ ├── smallest-range-i.md │ ├── smallest-range-ii.md │ ├── smallest-string-starting-from-leaf.md │ ├── snakes-and-ladders.md │ ├── sort-an-array.md │ ├── sort-array-by-parity-ii.md │ ├── sort-array-by-parity.md │ ├── squares-of-a-sorted-array.md │ ├── stamping-the-sequence.md │ ├── string-without-aaa-or-bbb.md │ ├── subarray-sums-divisible-by-k.md │ ├── subarrays-with-k-different-integers.md │ ├── sum-of-even-numbers-after-queries.md │ ├── sum-of-subarray-minimums.md │ ├── super-palindromes.md │ ├── tallest-billboard.md │ ├── three-equal-parts.md │ ├── time-based-key-value-store.md │ ├── triples-with-bitwise-and-equal-to-zero.md │ ├── unique-email-addresses.md │ ├── unique-paths-iii.md │ ├── univalued-binary-tree.md │ ├── valid-mountain-array.md │ ├── valid-permutations-for-di-sequence.md │ ├── validate-stack-sequences.md │ ├── verifying-an-alien-dictionary.md │ ├── vertical-order-traversal-of-a-binary-tree.md │ ├── vowel-spellchecker.md │ ├── word-subsets.md │ └── x-of-a-kind-in-a-deck-of-cards.md ├── 1000-1099/ │ ├── best-sightseeing-pair.md │ ├── binary-search-tree-to-greater-sum-tree.md │ ├── camelcase-matching.md │ ├── capacity-to-ship-packages-within-d-days.md │ ├── coloring-a-border.md │ ├── complement-of-base-10-integer.md │ ├── construct-binary-search-tree-from-preorder-traversal.md │ ├── divisor-game.md │ ├── duplicate-zeros.md │ ├── find-common-characters.md │ ├── find-in-mountain-array.md │ ├── grumpy-bookstore-owner.md │ ├── height-checker.md │ ├── index-pairs-of-a-string.md │ ├── index.md │ ├── last-stone-weight-ii.md │ ├── letter-tile-possibilities.md │ ├── max-consecutive-ones-iii.md │ ├── maximize-sum-of-array-after-k-negations.md │ ├── minimum-cost-to-merge-stones.md │ ├── minimum-score-triangulation-of-polygon.md │ ├── number-of-enclaves.md │ ├── numbers-with-repeated-digits.md │ ├── recover-a-tree-from-preorder-traversal.md │ ├── remove-all-adjacent-duplicates-in-string.md │ ├── remove-outermost-parentheses.md │ ├── robot-bounded-in-circle.md │ ├── shortest-path-in-binary-matrix.md │ ├── smallest-subsequence-of-distinct-characters.md │ ├── stream-of-characters.md │ ├── two-city-scheduling.md │ ├── two-sum-less-than-k.md │ ├── uncrossed-lines.md │ └── valid-boomerang.md ├── 1100-1199/ │ ├── corporate-flight-bookings.md │ ├── defanging-an-ip-address.md │ ├── delete-nodes-and-return-forest.md │ ├── diet-plan-performance.md │ ├── distance-between-bus-stops.md │ ├── distribute-candies-to-people.md │ ├── find-k-length-substrings-with-no-repeated-characters.md │ ├── index.md │ ├── longest-common-subsequence.md │ ├── maximum-level-sum-of-a-binary-tree.md │ ├── minimum-swaps-to-group-all-1s-together.md │ ├── n-th-tribonacci-number.md │ ├── number-of-dice-rolls-with-target-sum.md │ ├── parallel-courses.md │ └── relative-sort-array.md ├── 1200-1299/ │ ├── airplane-seat-assignment-probability.md │ ├── check-if-it-is-a-straight-line.md │ ├── count-vowels-permutation.md │ ├── divide-array-in-sets-of-k-consecutive-numbers.md │ ├── find-elements-in-a-contaminated-binary-tree.md │ ├── get-equal-substrings-within-budget.md │ ├── index.md │ ├── meeting-scheduler.md │ ├── minimum-cost-to-move-chips-to-the-same-position.md │ ├── minimum-swaps-to-make-strings-equal.md │ ├── minimum-time-visiting-all-points.md │ ├── number-of-closed-islands.md │ ├── reconstruct-a-2-row-binary-matrix.md │ ├── search-suggestions-system.md │ ├── smallest-string-with-swaps.md │ ├── subtract-the-product-and-sum-of-digits-of-an-integer.md │ └── tree-diameter.md ├── 1300-1399/ │ ├── all-elements-in-two-binary-search-trees.md │ ├── angle-between-hands-of-a-clock.md │ ├── closest-divisors.md │ ├── convert-integer-to-the-sum-of-two-no-zero-integers.md │ ├── decompress-run-length-encoded-list.md │ ├── design-a-stack-with-increment-operation.md │ ├── index.md │ ├── maximum-students-taking-exam.md │ ├── minimum-number-of-steps-to-make-two-strings-anagram.md │ ├── number-of-operations-to-make-network-connected.md │ ├── number-of-sub-arrays-of-size-k-and-average-greater-than-or-equal-to-threshold.md │ ├── number-of-substrings-containing-all-three-characters.md │ ├── print-words-vertically.md │ ├── reduce-array-size-to-the-half.md │ ├── sum-of-mutated-array-closest-to-target.md │ └── xor-queries-of-a-subarray.md ├── 1400-1499/ │ ├── average-salary-excluding-the-minimum-and-maximum-salary.md │ ├── consecutive-characters.md │ ├── construct-k-palindrome-strings.md │ ├── form-largest-integer-with-digits-that-add-up-to-target.md │ ├── index.md │ ├── longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit.md │ ├── longest-subarray-of-1s-after-deleting-one-element.md │ ├── maximum-number-of-vowels-in-a-substring-of-given-length.md │ ├── maximum-points-you-can-obtain-from-cards.md │ ├── maximum-score-after-splitting-a-string.md │ ├── minimum-number-of-days-to-make-m-bouquets.md │ ├── number-of-students-doing-homework-at-a-given-time.md │ ├── path-crossing.md │ ├── rearrange-words-in-a-sentence.md │ ├── running-sum-of-1d-array.md │ ├── simplified-fractions.md │ ├── string-matching-in-an-array.md │ ├── subrectangle-queries.md │ └── xor-operation-in-an-array.md ├── 1500-1599/ │ ├── can-make-arithmetic-progression-from-sequence.md │ ├── count-good-triplets.md │ ├── count-odd-numbers-in-an-interval-range.md │ ├── index.md │ ├── maximum-length-of-subarray-with-positive-product.md │ ├── maximum-number-of-coins-you-can-get.md │ ├── min-cost-to-connect-all-points.md │ ├── minimum-cost-to-connect-two-groups-of-points.md │ ├── minimum-cost-to-cut-a-stick.md │ ├── minimum-operations-to-make-array-equal.md │ ├── reformat-date.md │ ├── special-positions-in-a-binary-matrix.md │ ├── split-a-string-into-the-max-number-of-unique-substrings.md │ └── thousand-separator.md ├── 1600-1699/ │ ├── count-sorted-vowel-strings.md │ ├── count-subtrees-with-max-distance-between-cities.md │ ├── design-parking-system.md │ ├── determine-if-two-strings-are-close.md │ ├── find-valid-matrix-given-row-and-column-sums.md │ ├── get-maximum-in-generated-array.md │ ├── index.md │ ├── maximum-erasure-value.md │ ├── maximum-nesting-depth-of-the-parentheses.md │ ├── minimum-deletions-to-make-character-frequencies-unique.md │ ├── minimum-operations-to-reduce-x-to-zero.md │ ├── number-of-distinct-substrings-in-a-string.md │ ├── path-with-minimum-effort.md │ └── richest-customer-wealth.md ├── 1700-1799/ │ ├── calculate-money-in-leetcode-bank.md │ ├── check-if-one-string-swap-can-make-strings-equal.md │ ├── decode-xored-array.md │ ├── find-center-of-star-graph.md │ ├── find-nearest-point-that-has-the-same-x-or-y-coordinate.md │ ├── index.md │ ├── latest-time-by-replacing-hidden-digits.md │ ├── longest-nice-substring.md │ ├── maximum-absolute-sum-of-any-subarray.md │ ├── maximum-number-of-balls-in-a-box.md │ ├── maximum-units-on-a-truck.md │ └── tuple-with-same-product.md ├── 1800-1899/ │ ├── check-if-all-the-integers-in-a-range-are-covered.md │ ├── index.md │ ├── longest-word-with-all-prefixes.md │ ├── maximum-ice-cream-bars.md │ ├── minimize-maximum-pair-sum-in-array.md │ ├── minimum-operations-to-make-the-array-increasing.md │ ├── minimum-xor-sum-of-two-arrays.md │ ├── redistribute-characters-to-make-all-strings-equal.md │ ├── replace-all-digits-with-characters.md │ ├── sign-of-the-product-of-an-array.md │ ├── sorting-the-sentence.md │ └── substrings-of-size-three-with-distinct-characters.md ├── 1900-1999/ │ ├── add-minimum-number-of-rungs.md │ ├── check-if-all-characters-have-equal-number-of-occurrences.md │ ├── concatenation-of-array.md │ ├── count-square-sum-triples.md │ ├── eliminate-maximum-number-of-monsters.md │ ├── find-the-middle-index-in-array.md │ ├── index.md │ ├── largest-odd-number-in-string.md │ ├── maximum-compatibility-score-sum.md │ ├── minimum-difference-between-highest-and-lowest-of-k-scores.md │ ├── minimum-number-of-work-sessions-to-finish-the-tasks.md │ ├── the-number-of-good-subsets.md │ └── unique-length-3-palindromic-subsequences.md ├── 2000-2099/ │ ├── final-value-of-variable-after-performing-operations.md │ ├── index.md │ ├── number-of-pairs-of-strings-with-concatenation-equal-to-target.md │ └── parallel-courses-iii.md ├── 2100-2199/ │ ├── find-substring-with-given-hash-value.md │ ├── index.md │ └── maximum-and-sum-of-array.md ├── 2200-2299/ │ ├── add-two-integers.md │ ├── count-integers-in-intervals.md │ ├── count-lattice-points-inside-a-circle.md │ ├── index.md │ └── longest-path-with-different-adjacent-characters.md ├── 2300-2399/ │ ├── count-special-integers.md │ └── index.md ├── 2400-2499/ │ ├── index.md │ └── number-of-common-factors.md ├── 2500-2599/ │ ├── difference-between-maximum-and-minimum-price-sum.md │ ├── index.md │ └── number-of-ways-to-earn-points.md ├── 2700-2799/ │ ├── count-of-integers.md │ └── index.md ├── LCR/ │ ├── 0H97ZC.md │ ├── 0on3uN.md │ ├── 0ynMMM.md │ ├── 1fGaJU.md │ ├── 21dk04.md │ ├── 2AoeFn.md │ ├── 2VG8Kg.md │ ├── 2bCMpM.md │ ├── 3Etpl5.md │ ├── 3u1WK4.md │ ├── 4sjJUc.md │ ├── 4ueAj6.md │ ├── 569nqc.md │ ├── 6eUYwP.md │ ├── 7LpjUW.md │ ├── 7WHec2.md │ ├── 7WqeDu.md │ ├── 7p8L0Z.md │ ├── 8Zf90G.md │ ├── A1NYOS.md │ ├── D0F0SV.md │ ├── FortPu.md │ ├── Gu0c2T.md │ ├── GzCJIP.md │ ├── H8086Q.md │ ├── IDBivT.md │ ├── JFETK5.md │ ├── LGjMqU.md │ ├── LwUNpT.md │ ├── M1oyTv.md │ ├── M99OJA.md │ ├── N6YdxV.md │ ├── NUPfPr.md │ ├── NYBBNL.md │ ├── NaqhDT.md │ ├── O4NDxx.md │ ├── OrIXps.md │ ├── P5rCT8.md │ ├── PzWKhm.md │ ├── Q91FMA.md │ ├── QA2IGt.md │ ├── QC3q1f.md │ ├── QTMn0o.md │ ├── Qv1Da2.md │ ├── RQku0D.md │ ├── SLwz0R.md │ ├── SsGoHC.md │ ├── TVdhkn.md │ ├── UHnkqh.md │ ├── US1pGT.md │ ├── UhWRSj.md │ ├── VvJkup.md │ ├── WGki4K.md │ ├── WNC0Lk.md │ ├── WhsWhI.md │ ├── XagZNi.md │ ├── XltzEq.md │ ├── YaVDxD.md │ ├── Ygoe9J.md │ ├── ZL6zAn.md │ ├── ZVAVXX.md │ ├── a7VOhD.md │ ├── aMhZSa.md │ ├── aseY1I.md │ ├── bLyHh0.md │ ├── ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof.md │ ├── ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof.md │ ├── ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof.md │ ├── bao-han-minhan-shu-de-zhan-lcof.md │ ├── bu-ke-pai-zhong-de-shun-zi-lcof.md │ ├── bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof.md │ ├── c32eOV.md │ ├── chou-shu-lcof.md │ ├── cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof.md │ ├── cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof.md │ ├── cong-shang-dao-xia-da-yin-er-cha-shu-lcof.md │ ├── cong-wei-dao-tou-da-yin-lian-biao-lcof.md │ ├── dKk3P7.md │ ├── da-yin-cong-1dao-zui-da-de-nwei-shu-lcof.md │ ├── di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof.md │ ├── diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof.md │ ├── dui-cheng-de-er-cha-shu-lcof.md │ ├── dui-lie-de-zui-da-zhi-lcof.md │ ├── er-cha-shu-de-jing-xiang-lcof.md │ ├── er-cha-shu-de-shen-du-lcof.md │ ├── er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof.md │ ├── er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof.md │ ├── er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof.md │ ├── er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof.md │ ├── er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof.md │ ├── er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof.md │ ├── er-jin-zhi-zhong-1de-ge-shu-lcof.md │ ├── er-wei-shu-zu-zhong-de-cha-zhao-lcof.md │ ├── fan-zhuan-dan-ci-shun-xu-lcof.md │ ├── fan-zhuan-lian-biao-lcof.md │ ├── fei-bo-na-qi-shu-lie-lcof.md │ ├── fpTFWP.md │ ├── fu-za-lian-biao-de-fu-zhi-lcof.md │ ├── g5c51o.md │ ├── gaM7Ch.md │ ├── gou-jian-cheng-ji-shu-zu-lcof.md │ ├── gu-piao-de-zui-da-li-run-lcof.md │ ├── h54YBf.md │ ├── hPov7L.md │ ├── he-bing-liang-ge-pai-xu-de-lian-biao-lcof.md │ ├── he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof.md │ ├── he-wei-sde-liang-ge-shu-zi-lcof.md │ ├── hua-dong-chuang-kou-de-zui-da-zhi-lcof.md │ ├── iIQa4I.md │ ├── iSwD2y.md │ ├── index.md │ ├── jBjn9C.md │ ├── jC7MId.md │ ├── jJ0w9p.md │ ├── ji-qi-ren-de-yun-dong-fan-wei-lcof.md │ ├── jian-sheng-zi-lcof.md │ ├── ju-zhen-zhong-de-lu-jing-lcof.md │ ├── kLl5u1.md │ ├── kTOapQ.md │ ├── lMSNwu.md │ ├── li-wu-de-zui-da-jie-zhi-lcof.md │ ├── lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof.md │ ├── lian-xu-zi-shu-zu-de-zui-da-he-lcof.md │ ├── liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof.md │ ├── lwyVBB.md │ ├── ms70jA.md │ ├── nZZqjQ.md │ ├── om3reC.md │ ├── opLdQZ.md │ ├── pOCWxh.md │ ├── ping-heng-er-cha-shu-lcof.md │ ├── qIsx9U.md │ ├── qJnOS7.md │ ├── qing-wa-tiao-tai-jie-wen-ti-lcof.md │ ├── qiu-12n-lcof.md │ ├── que-shi-de-shu-zi-lcof.md │ ├── sfvd7V.md │ ├── shan-chu-lian-biao-de-jie-dian-lcof.md │ ├── shu-de-zi-jie-gou-lcof.md │ ├── shu-ju-liu-zhong-de-zhong-wei-shu-lcof.md │ ├── shu-zhi-de-zheng-shu-ci-fang-lcof.md │ ├── shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof.md │ ├── shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof.md │ ├── shu-zu-zhong-de-ni-xu-dui-lcof.md │ ├── shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof.md │ ├── shu-zu-zhong-zhong-fu-de-shu-zi-lcof.md │ ├── shun-shi-zhen-da-yin-ju-zhen-lcof.md │ ├── ti-huan-kong-ge-lcof.md │ ├── tvdfij.md │ ├── uUsW3B.md │ ├── vEAB3K.md │ ├── vlzXQL.md │ ├── vvXgSW.md │ ├── w3tCBm.md │ ├── w6cpku.md │ ├── wtcaE1.md │ ├── xoh6Oh.md │ ├── xu-lie-hua-er-cha-shu-lcof.md │ ├── xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof.md │ ├── xx4gT2.md │ ├── yong-liang-ge-zhan-shi-xian-dui-lie-lcof.md │ ├── yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof.md │ ├── z1R5dt.md │ ├── zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof.md │ ├── zhan-de-ya-ru-dan-chu-xu-lie-lcof.md │ ├── zhong-jian-er-cha-shu-lcof.md │ ├── zi-fu-chuan-de-pai-lie-lcof.md │ ├── zlDJc7.md │ ├── zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof.md │ ├── zui-xiao-de-kge-shu-lcof.md │ └── zuo-xuan-zhuan-zi-fu-chuan-lcof.md ├── index.md └── interviews/ ├── bracket-lcci.md ├── calculator-lcci.md ├── color-fill-lcci.md ├── eight-queens-lcci.md ├── factorial-zeros-lcci.md ├── first-common-ancestor-lcci.md ├── group-anagrams-lcci.md ├── implement-queue-using-stacks-lcci.md ├── index.md ├── intersection-of-two-linked-lists-lcci.md ├── kth-node-from-end-of-list-lcci.md ├── legal-binary-search-tree-lcci.md ├── linked-list-cycle-lcci.md ├── longest-word-lcci.md ├── min-stack-lcci.md ├── minimum-height-tree-lcci.md ├── multi-search-lcci.md ├── number-of-2s-in-range-lcci.md ├── palindrome-linked-list-lcci.md ├── paths-with-sum-lcci.md ├── permutation-i-lcci.md ├── permutation-ii-lcci.md ├── power-set-lcci.md ├── rotate-matrix-lcci.md ├── smallest-k-lcci.md ├── sorted-matrix-search-lcci.md ├── sorted-merge-lcci.md ├── successor-lcci.md ├── sum-lists-lcci.md ├── words-frequency-lcci.md └── zero-matrix-lcci.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/sync.yml ================================================ name: Mirror to Gitee Repo on: [ push ] # Ensures that only one mirror task will run at a time. concurrency: group: git-mirror jobs: git-mirror: runs-on: ubuntu-latest steps: - uses: wearerequired/git-mirror-action@v1 env: SSH_PRIVATE_KEY: ${{ secrets.SYNC_GITEE_PRI_KEY }} with: source-repo: "git@github.com:itcharge/AlgoNote.git" destination-repo: "git@gitee.com:itcharge/AlgoNote.git" ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # Custom Temp .idea .DS_Store ================================================ FILE: LICENSE ================================================ Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public : wiki.creativecommons.org/Considerations_for_licensees Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. - Section 1 – Definitions. - a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. - b. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. - c. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. - d. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. - e. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. - f. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. - g. Licensor means the individual(s) or entity(ies) granting rights under this Public License. - h. NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. - i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. - j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. - k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. - Section 2 – Scope. - a. License grant. - 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: - A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and - B. produce and reproduce, but not Share, Adapted Material for NonCommercial purposes only. - 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. - 3. Term. The term of this Public License is specified in Section 6(a). - 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. - 5. Downstream recipients. - A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. - B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. - 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). - b. Other rights. - 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. - 2. Patent and trademark rights are not licensed under this Public License. - 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. - Section 3 – License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. - a. Attribution. - 1. If You Share the Licensed Material, You must: - A. retain the following if it is supplied by the Licensor with the Licensed Material: - i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); - ii. a copyright notice; - iii. a notice that refers to this Public License; - iv. a notice that refers to the disclaimer of warranties; - v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; - B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and - C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. For the avoidance of doubt, You do not have permission under this Public License to Share Adapted Material. - 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. - 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. - Section 4 – Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: - a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only and provided You do not Share Adapted Material; - b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and - c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. - Section 5 – Disclaimer of Warranties and Limitation of Liability. - a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. - b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. - c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. - Section 6 – Term and Termination. - a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. - b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: - 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or - 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. - c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. - d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. - Section 7 – Other Terms and Conditions. - a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. - b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. - Section 8 – Interpretation. - a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. - b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. - c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. - d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the "Licensor." The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. ================================================ FILE: README.md ================================================
alt text

算法通关手册

GitHub stars GitHub forks Language GitHub Project

📚 从零开始的「算法与数据结构」学习教程

一本系统讲解算法与数据结构、涵盖 LeetCode 题解的中文学习手册

在线阅读 PDF 下载
如果觉得本项目对你有帮助,欢迎点亮 🌟 Star,支持一下! ## 1. 本书简介 本书不仅仅只是一本算法题解书,更是一本算法与数据结构基础知识的讲解书。 - 超详细的 **「算法与数据结构」** 基础讲解教程,**「LeetCode 1000+ 道」** 经典题目详细解析。 - 本项目易于理解,没有大跨度的思维跳跃,项目中使用大量图示、例子来帮助理解。 - 本项目先从基础的数据结构和算法开始讲解,再针对不同分类的数据结构和算法,进行具体题目的讲解分析。让读者可以通过「算法基础理论学习」和「编程实战学习」相结合的方式,彻底的掌握算法知识。 - 本项目从各大知名互联网公司面试算法题中整理汇总了 **「LeetCode 200 道高频面试题」**,帮助面试者更有针对性的准备面试。 ### 1.1 目标读者 - 拥有 Python 编程基础或其他编程语言基础的编程爱好者 - 对 LeetCode 刷题感兴趣或准备算法面试的面试人员 - 对算法感兴趣的计算机专业学生或程序员 - 想要提升编程思维和问题解决能力的开发者 ### 1.2 内容结构 本书采用算法与数据结构相结合的方法,把内容分为如下几个主要部分: - **0. 序言**:介绍数据结构与算法的基础知识、算法复杂度、LeetCode 的入门和攻略,为后面的学习打好基础。 - **1. 数组**:讲解数组的基本概念、数组的基本操作。 - **2. 链表**:讲解链表的基本概念、操作和应用,包括单链表、双向链表、循环链表等。 - **3. 栈、队列、哈希表**:详细介绍栈、队列、哈希表这三种数据结构,包括它们的基本概念、实现方式、应用场景以及相关的经典算法题。 - **4. 字符串**:讲解字符串的基本操作、单字符串匹配算法、多字符串匹配算法,以及字符串相关的经典算法题。 - **5. 树结构**:介绍树的基本概念、二叉树、二叉搜索树、线段树、树状数组、并查集等数据结构。 - **6. 图论**:讲解图的基本概念、表示方法、遍历算法和经典应用。 - **7. 基础算法**:介绍基本的算法思想。包括枚举、递归、分治、回溯、贪心以及位运算。 - **8. 动态规划**:介绍动态规划的基础知识、各种动态规划题型的解法。 - **9. 附加内容**:作为全书的扩展模块。 - **10. 题目解析**:讲解 LeetCode 上刷过的所有题目,可按照对应题号进行检索和学习。 ### 1.3 使用说明 - 本电子书左侧提供了完整的章节目录导航,可直接点击跳转至相应内容。 - 本电子书右上角配有搜索栏,便于快速查找所需章节和题解文章。 - 本电子书集成了 giscus 评论系统,欢迎在页面底部评论区留言(需 GitHub 账号登录)。 - 建议按章节顺序系统学习,逐步掌握各知识点;也可根据兴趣自由选择章节阅读。 - 每篇内容末尾设有练习题,建议及时完成以加深理解、巩固所学。 ## 2. 相关说明 ### 2.1 关于作者 我是一名 iOS / macOS 的开发程序员,研究生毕业于北航软件学院。曾在大学期间学习过算法知识,并参加过 3 年的 ACM 比赛, 但水平有限,未能取得理想成绩。但是这 3 年的 ACM 经历,给我最大的收获是锻炼了自己的逻辑思维和解决实际问题的能力,这种能力为我今后的工作、学习打下了坚实的基础。 我从 2021 年 03 月 30 日开始每日在 LeetCode 刷题,到目前为止日已经刷了 1800+ 道题目,并且完成了 1000+ 道题解。努力向着 1500+、2000+ 道题解前进。 ### 2.2 互助与勘误 限于本人的水平和经验,书中一定不乏纰漏和谬误之处。恳切希望读者给予批评指正。这将有利于我改进和提高,以帮助更多的读者。如果您对本书有任何评论和建议,或者遇到问题需要帮助,可在每页评论区留言,或者致信作者邮箱 [i@itcharge.cn](mailto:i@itcharge.cn),我将不胜感激。 ### 2.3 版权说明 - 本书采用 [知识署名—非商业性使用—禁止演绎(BY-NC-ND)4.0 协议国际许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode.zh-Hans) 进行许可。 - 本书题解中的所有题目版权均归 [LeetCode](https://leetcode.com/) 和 [力扣中国](https://leetcode.cn/) 所有。 ### 2.4 致谢 在本书构思与写作阶段,很多朋友给我提出了有益的意见和建议。这些意见和建议令我受益匪浅。感谢在本书著作准备过程中,帮助过我的朋友,以及一起陪我刷题打卡的朋友,还有提供宝贵意见的读者。谢谢诸位。 ================================================ FILE: codes/python/01_array/array_maxheap.py ================================================ class MaxHeap: def __init__(self): self.max_heap = [] def peek(self) -> int: # 大顶堆为空 if not self.max_heap: return None # 返回堆顶元素 return self.max_heap[0] def push(self, val: int): # 将新元素添加到堆的末尾 self.max_heap.append(val) size = len(self.max_heap) # 从新插入的元素节点开始,进行上移调整 self.__shift_up(size - 1) def __shift_up(self, i: int): while (i - 1) // 2 >= 0 and self.max_heap[i] > self.max_heap[(i - 1) // 2]: self.max_heap[i], self.max_heap[(i - 1) // 2] = self.max_heap[(i - 1) // 2], self.max_heap[i] i = (i - 1) // 2 def pop(self) -> int: # 堆为空 if not self.max_heap: raise IndexError("堆为空") size = len(self.max_heap) self.max_heap[0], self.max_heap[size - 1] = self.max_heap[size - 1], self.max_heap[0] # 删除堆顶元素 val = self.max_heap.pop() # 节点数减 1 size -= 1 self.__shift_down(0, size) # 返回堆顶元素 return val def __shift_down(self, i: int, n: int): while 2 * i + 1 < n: # 左右子节点编号 left, right = 2 * i + 1, 2 * i + 2 # 找出左右子节点中的较大值节点编号 if 2 * i + 2 >= n: # 右子节点编号超出范围(只有左子节点 larger = left else: # 左子节点、右子节点都存在 if self.max_heap[left] >= self.max_heap[right]: larger = left else: larger = right # 将当前节点值与其较大的子节点进行比较 if self.max_heap[i] < self.max_heap[larger]: # 如果当前节点值小于其较大的子节点,则将它们交换 self.max_heap[i], self.max_heap[larger] = self.max_heap[larger], self.max_heap[i] i = larger else: # 如果当前节点值大于等于于其较大的子节点,此时结束 break class Solution: def maxHeapOperations(self): max_heap = MaxHeap() max_heap.push(3) print(max_heap.peek()) max_heap.push(2) print(max_heap.peek()) max_heap.push(4) print(max_heap.peek()) max_heap.pop() print(max_heap.peek()) print(Solution().maxHeapOperations()) ================================================ FILE: codes/python/01_array/array_sort_bubble_sort.py ================================================ class Solution: def bubbleSort(self, nums: [int]) -> [int]: # 第 i 趟「冒泡」 for i in range(len(nums) - 1): flag = False # 是否发生交换的标志位 # 对数组未排序区间 [0, n - i - 1] 的元素执行「冒泡」 for j in range(len(nums) - i - 1): # 相邻两个元素进行比较,如果前者大于后者,则交换位置 if nums[j] > nums[j + 1]: nums[j], nums[j + 1] = nums[j + 1], nums[j] flag = True if not flag: # 此趟遍历未交换任何元素,直接跳出 break return nums def sortArray(self, nums: [int]) -> [int]: return self.bubbleSort(nums) print(Solution().sortArray([5, 2, 3, 6, 1, 4])) ================================================ FILE: codes/python/01_array/array_sort_bucket_sort.py ================================================ class Solution: def insertionSort(self, nums: [int]) -> [int]: # 遍历无序区间 for i in range(1, len(nums)): temp = nums[i] j = i # 从右至左遍历有序区间 while j > 0 and nums[j - 1] > temp: # 将有序区间中插入位置右侧的元素依次右移一位 nums[j] = nums[j - 1] j -= 1 # 将该元素插入到适当位置 nums[j] = temp return nums def bucketSort(self, nums: [int], bucket_size=5) -> [int]: # 计算待排序序列中最大值元素 nums_max、最小值元素 nums_min nums_min, nums_max = min(nums), max(nums) # 定义桶的个数为 (最大值元素 - 最小值元素) // 每个桶的大小 + 1 bucket_count = (nums_max - nums_min) // bucket_size + 1 # 定义桶数组 buckets buckets = [[] for _ in range(bucket_count)] # 遍历待排序数组元素,将每个元素根据大小分配到对应的桶中 for num in nums: buckets[(num - nums_min) // bucket_size].append(num) # 对每个非空桶内的元素单独排序,排序之后,按照区间顺序依次合并到 res 数组中 res = [] for bucket in buckets: self.insertionSort(bucket) res.extend(bucket) # 返回结果数组 return res def sortArray(self, nums: [int]) -> [int]: return self.bucketSort(nums) print(Solution().sortArray([39, 49, 8, 13, 22, 15, 10, 30, 5, 44])) ================================================ FILE: codes/python/01_array/array_sort_counting_sort.py ================================================ class Solution: def countingSort(self, nums: [int]) -> [int]: # 计算待排序数组中最大值元素 nums_max 和最小值元素 nums_min nums_min, nums_max = min(nums), max(nums) # 定义计数数组 counts,大小为 最大值元素 - 最小值元素 + 1 size = nums_max - nums_min + 1 counts = [0 for _ in range(size)] # 统计值为 num 的元素出现的次数 for num in nums: counts[num - nums_min] += 1 # 生成累积计数数组 for i in range(1, size): counts[i] += counts[i - 1] # 反向填充目标数组 res = [0 for _ in range(len(nums))] for i in range(len(nums) - 1, -1, -1): num = nums[i] # 根据累积计数数组,将 num 放在数组对应位置 res[counts[num - nums_min] - 1] = num # 将 num 的对应放置位置减 1,从而得到下个元素 num 的放置位置 counts[nums[i] - nums_min] -= 1 return res def sortArray(self, nums: [int]) -> [int]: return self.countingSort(nums) print(Solution().sortArray([3, 0, 4, 2, 5, 1, 3, 1, 4, 5])) ================================================ FILE: codes/python/01_array/array_sort_insertion_sort.py ================================================ class Solution: def insertionSort(self, nums: [int]) -> [int]: # 遍历无序区间 for i in range(1, len(nums)): temp = nums[i] j = i # 从右至左遍历有序区间 while j > 0 and nums[j - 1] > temp: # 将有序区间中插入位置右侧的所有元素依次右移一位 nums[j] = nums[j - 1] j -= 1 # 将该元素插入到适当位置 nums[j] = temp return nums def sortArray(self, nums: [int]) -> [int]: return self.insertionSort(nums) print(Solution().sortArray([5, 2, 3, 6, 1, 4])) ================================================ FILE: codes/python/01_array/array_sort_maxheap_sort.py ================================================ class MaxHeap: def __init__(self): self.max_heap = [] def peek(self) -> int: # 大顶堆为空 if not self.max_heap: return None # 返回堆顶元素 return self.max_heap[0] def push(self, val: int): # 将新元素添加到堆的末尾 self.max_heap.append(val) size = len(self.max_heap) # 从新插入的元素节点开始,进行上移调整 self.__shift_up(size - 1) def __shift_up(self, i: int): while (i - 1) // 2 >= 0 and self.max_heap[i] > self.max_heap[(i - 1) // 2]: self.max_heap[i], self.max_heap[(i - 1) // 2] = self.max_heap[(i - 1) // 2], self.max_heap[i] i = (i - 1) // 2 def pop(self) -> int: # 堆为空 if not self.max_heap: raise IndexError("堆为空") size = len(self.max_heap) self.max_heap[0], self.max_heap[size - 1] = self.max_heap[size - 1], self.max_heap[0] # 删除堆顶元素 val = self.max_heap.pop() # 节点数减 1 size -= 1 self.__shift_down(0, size) # 返回堆顶元素 return val def __shift_down(self, i: int, n: int): while 2 * i + 1 < n: # 左右子节点编号 left, right = 2 * i + 1, 2 * i + 2 # 找出左右子节点中的较大值节点编号 if 2 * i + 2 >= n: # 右子节点编号超出范围(只有左子节点 larger = left else: # 左子节点、右子节点都存在 if self.max_heap[left] >= self.max_heap[right]: larger = left else: larger = right # 将当前节点值与其较大的子节点进行比较 if self.max_heap[i] < self.max_heap[larger]: # 如果当前节点值小于其较大的子节点,则将它们交换 self.max_heap[i], self.max_heap[larger] = self.max_heap[larger], self.max_heap[i] i = larger else: # 如果当前节点值大于等于于其较大的子节点,此时结束 break def __buildMaxHeap(self, nums: [int]): size = len(nums) # 先将数组 nums 的元素按顺序添加到 max_heap 中 for i in range(size): self.max_heap.append(nums[i]) # 从最后一个非叶子节点开始,进行下移调整 for i in range((size - 2) // 2, -1, -1): self.__shift_down(i, size) def maxHeapSort(self, nums: [int]) -> [int]: # 根据数组 nums 建立初始堆 self.__buildMaxHeap(nums) size = len(self.max_heap) for i in range(size - 1, -1, -1): # 交换根节点与当前堆的最后一个节点 self.max_heap[0], self.max_heap[i] = self.max_heap[i], self.max_heap[0] # 从根节点开始,对当前堆进行下移调整 self.__shift_down(0, i) # 返回排序后的数组 return self.max_heap class Solution: def maxHeapSort(self, nums: [int]) -> [int]: return MaxHeap().maxHeapSort(nums) def sortArray(self, nums: [int]) -> [int]: return self.maxHeapSort(nums) print(Solution().sortArray([10, 25, 6, 8, 7, 1, 20, 23, 16, 19, 17, 3, 18, 14])) ================================================ FILE: codes/python/01_array/array_sort_merge_sort.py ================================================ class Solution: # 合并过程 def merge(self, left_nums: [int], right_nums: [int]): nums = [] left_i, right_i = 0, 0 while left_i < len(left_nums) and right_i < len(right_nums): # 将两个有序子数组中较小元素依次插入到结果数组中 if left_nums[left_i] < right_nums[right_i]: nums.append(left_nums[left_i]) left_i += 1 else: nums.append(right_nums[right_i]) right_i += 1 # 如果左子数组有剩余元素,则将其插入到结果数组中 while left_i < len(left_nums): nums.append(left_nums[left_i]) left_i += 1 # 如果右子数组有剩余元素,则将其插入到结果数组中 while right_i < len(right_nums): nums.append(right_nums[right_i]) right_i += 1 # 返回合并后的结果数组 return nums # 分解过程 def mergeSort(self, nums: [int]) -> [int]: # 数组元素个数小于等于 1 时,直接返回原数组 if len(nums) <= 1: return nums mid = len(nums) // 2 # 将数组从中间位置分为左右两个数组 left_nums = self.mergeSort(nums[0: mid]) # 递归将左子数组进行分解和排序 right_nums = self.mergeSort(nums[mid:]) # 递归将右子数组进行分解和排序 return self.merge(left_nums, right_nums) # 把当前数组组中有序子数组逐层向上,进行两两合并 def sortArray(self, nums: [int]) -> [int]: return self.mergeSort(nums) print(Solution().sortArray([0, 5, 7, 3, 1, 6, 8, 4])) ================================================ FILE: codes/python/01_array/array_sort_minheap_sort.py ================================================ class MinHeap: def __init__(self): self.min_heap = [] def peek(self) -> int: # 大顶堆为空 if not self.min_heap: return None # 返回堆顶元素 return self.min_heap[0] def push(self, val: int): # 将新元素添加到堆的末尾 self.min_heap.append(val) size = len(self.min_heap) # 从新插入的元素节点开始,进行上移调整 self.__shift_up(size - 1) def __shift_up(self, i: int): while (i - 1) // 2 >= 0 and self.min_heap[i] < self.min_heap[(i - 1) // 2]: self.min_heap[i], self.min_heap[(i - 1) // 2] = self.min_heap[(i - 1) // 2], self.min_heap[i] i = (i - 1) // 2 def pop(self) -> int: # 堆为空 if not self.min_heap: raise IndexError("堆为空") size = len(self.min_heap) self.min_heap[0], self.min_heap[size - 1] = self.min_heap[size - 1], self.min_heap[0] # 删除堆顶元素 val = self.min_heap.pop() # 节点数减 1 size -= 1 self.__shift_down(0, size) # 返回堆顶元素 return val def __shift_down(self, i: int, n: int): while 2 * i + 1 < n: # 左右子节点编号 left, right = 2 * i + 1, 2 * i + 2 # 找出左右子节点中的较大值节点编号 if 2 * i + 2 >= n: # 右子节点编号超出范围(只有左子节点 larger = left else: # 左子节点、右子节点都存在 if self.min_heap[left] <= self.min_heap[right]: larger = left else: larger = right # 将当前节点值与其较小的子节点进行比较 if self.min_heap[i] > self.min_heap[larger]: # 如果当前节点值小于其较大的子节点,则将它们交换 self.min_heap[i], self.min_heap[larger] = self.min_heap[larger], self.min_heap[i] i = larger else: # 如果当前节点值大于等于于其较大的子节点,此时结束 break def __buildMinHeap(self, nums: [int]): size = len(nums) # 先将数组 nums 的元素按顺序添加到 min_heap 中 for i in range(size): self.min_heap.append(nums[i]) # 从最后一个非叶子节点开始,进行下移调整 for i in range((size - 2) // 2, -1, -1): self.__shift_down(i, size) def minHeapSort(self, nums: [int]) -> [int]: # 根据数组 nums 建立初始堆 self.__buildMinHeap(nums) size = len(self.min_heap) for i in range(size - 1, -1, -1): # 交换根节点与当前堆的最后一个节点 self.min_heap[0], self.min_heap[i] = self.min_heap[i], self.min_heap[0] # 从根节点开始,对当前堆进行下移调整 self.__shift_down(0, i) # 返回排序后的数组 return self.min_heap class Solution: def minHeapSort(self, nums: [int]) -> [int]: return MinHeap().minHeapSort(nums) def sortArray(self, nums: [int]) -> [int]: return self.minHeapSort(nums) print(Solution().sortArray([10, 25, 6, 8, 7, 1, 20, 23, 16, 19, 17, 3, 18, 14])) ================================================ FILE: codes/python/01_array/array_sort_quick_sort.py ================================================ import random class Solution: # 随机哨兵划分:从 nums[low: high + 1] 中随机挑选一个基准数,并进行移位排序 def randomPartition(self, nums: [int], low: int, high: int) -> int: # 随机挑选一个基准数 i = random.randint(low, high) # 将基准数与最低位互换 nums[i], nums[low] = nums[low], nums[i] # 以最低位为基准数,然后将数组中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上 return self.partition(nums, low, high) # 哨兵划分:以第 1 位元素 nums[low] 为基准数,然后将比基准数小的元素移动到基准数左侧,将比基准数大的元素移动到基准数右侧,最后将基准数放到正确位置上 def partition(self, nums: [int], low: int, high: int) -> int: # 以第 1 位元素为基准数 pivot = nums[low] i, j = low, high while i < j: # 从右向左找到第 1 个小于基准数的元素 while i < j and nums[j] >= pivot: j -= 1 # 从左向右找到第 1 个大于基准数的元素 while i < j and nums[i] <= pivot: i += 1 # 交换元素 nums[i], nums[j] = nums[j], nums[i] # 将基准数放到正确位置上 nums[j], nums[low] = nums[low], nums[j] return j def quickSort(self, nums: [int], low: int, high: int) -> [int]: if low < high: # 按照基准数的位置,将数组划分为左右两个子数组 pivot_i = self.partition(nums, low, high) # 对左右两个子数组分别进行递归快速排序 self.quickSort(nums, low, pivot_i - 1) self.quickSort(nums, pivot_i + 1, high) return nums def sortArray(self, nums: [int]) -> [int]: return self.quickSort(nums, 0, len(nums) - 1) print(Solution().sortArray([4, 7, 5, 2, 6, 1, 3])) ================================================ FILE: codes/python/01_array/array_sort_radix_sort.py ================================================ class Solution: def radixSort(self, nums: [int]) -> [int]: # 桶的大小为所有元素的最大位数 size = len(str(max(nums))) # 从最低位(个位)开始,逐位遍历每一位 for i in range(size): # 定义长度为 10 的桶数组 buckets,每个桶分别代表 0 ~ 9 中的 1 个数字。 buckets = [[] for _ in range(10)] # 遍历数组元素,按照每个元素当前位上的数字,将元素放入对应数字的桶中。 for num in nums: buckets[num // (10 ** i) % 10].append(num) # 清空原始数组 nums.clear() # 按照桶的顺序依次取出对应元素,重新加入到原始数组中。 for bucket in buckets: for num in bucket: nums.append(num) # 完成排序,返回结果数组 return nums def sortArray(self, nums: [int]) -> [int]: return self.radixSort(nums) print(Solution().sortArray([692, 924, 969, 503, 871, 704, 542, 436])) ================================================ FILE: codes/python/01_array/array_sort_selection_sort.py ================================================ class Solution: def selectionSort(self, nums: [int]) -> [int]: for i in range(len(nums) - 1): # 记录未排序区间中最小值的位置 min_i = i for j in range(i + 1, len(nums)): if nums[j] < nums[min_i]: min_i = j # 如果找到最小值的位置,将 i 位置上元素与最小值位置上的元素进行交换 if i != min_i: nums[i], nums[min_i] = nums[min_i], nums[i] return nums def sortArray(self, nums: [int]) -> [int]: return self.selectionSort(nums) print(Solution().sortArray([5, 2, 3, 6, 1, 4])) ================================================ FILE: codes/python/01_array/array_sort_shell_sort.py ================================================ class Solution: def shellSort(self, nums: [int]) -> [int]: size = len(nums) gap = size // 2 # 按照 gap 分组 while gap > 0: # 对每组元素进行插入排序 for i in range(gap, size): # temp 为每组中无序数组第 1 个元素 temp = nums[i] j = i # 从右至左遍历每组中的有序数组元素 while j >= gap and nums[j - gap] > temp: # 将每组有序数组中插入位置右侧的元素依次在组中右移一位 nums[j] = nums[j - gap] j -= gap # 将该元素插入到适当位置 nums[j] = temp # 缩小 gap 间隔 gap = gap // 2 return nums def sortArray(self, nums: [int]) -> [int]: return self.shellSort(nums) print(Solution().sortArray([7, 2, 6, 8, 0, 4, 1, 5, 9, 3])) ================================================ FILE: codes/python/02_linked_list/linked_list.py ================================================ class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class LinkedList: def __init__(self): self.head = None # 根据 data 初始化一个新链表 def create(self, data): if not data: return self.head = ListNode(data[0]) cur = self.head for i in range(1, len(data)): node = ListNode(data[i]) cur.next = node cur = cur.next # 获取线性链表长度 def length(self): count = 0 cur = self.head while cur: count += 1 cur = cur.next return count # 查找元素:在链表中查找值为 val 的元素 def find(self, val): cur = self.head while cur: if val == cur.val: return cur cur = cur.next return None # 链表头部插入元素 def insertFront(self, val): node = ListNode(val) node.next = self.head self.head = node # 链表尾部插入元素 def insertRear(self, val): node = ListNode(val) cur = self.head while cur.next: cur = cur.next cur.next = node # 链表中间插入元素 def insertInside(self, index, val): count = 0 cur = self.head while cur and count < index - 1: count += 1 cur = cur.next if not cur: return 'Error' node = ListNode(val) node.next = cur.next cur.next = node # 改变元素:将链表中第 i 个元素值改为 val def change(self, index, val): count = 0 cur = self.head while cur and count < index: count += 1 cur = cur.next if not cur: return 'Error' cur.val = val # 链表头部删除元素 def removeFront(self): if self.head: self.head = self.head.next # 链表尾部删除元素 def removeRear(self): if not self.head or not self.head.next: return 'Error' cur = self.head while cur.next.next: cur = cur.next cur.next = None # 链表中间删除元素 def removeInside(self, index): count = 0 cur = self.head while cur.next and count < index - 1: count += 1 cur = cur.next if not cur: return 'Error' del_node = cur.next cur.next = del_node.next ================================================ FILE: codes/python/02_linked_list/linked_list_bubble_sort.py ================================================ class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class Solution: def bubbleSort(self, head: ListNode): node_i = head tail = None # 外层循环次数为 链表长度 次 while node_i: node_j = head while node_j and node_j.next != tail: if node_j.val > node_j.next.val: # 交换两个节点的值 node_j.val, node_j.next.val = node_j.next.val, node_j.val node_j = node_j.next # 尾指针向前移动 1 位,此时尾指针右侧为排好序的链表 tail = node_j node_i = node_i.next return head def sortLinkedList(self, head: ListNode): return self.bubbleSort(head) ================================================ FILE: codes/python/02_linked_list/linked_list_bucket_sort.py ================================================ class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class Solution: # 将链表节点值 val 添加到对应桶 buckets[index] 中 def insertion(self, buckets, index, val): if not buckets[index]: buckets[index] = ListNode(val) return node = ListNode(val) node.next = buckets[index] buckets[index] = node # 归并环节 def merge(self, left, right): dummy_head = ListNode(-1) cur = dummy_head while left and right: if left.val <= right.val: cur.next = left left = left.next else: cur.next = right right = right.next cur = cur.next if left: cur.next = left elif right: cur.next = right return dummy_head.next # 归并排序 def mergeSort(self, head: ListNode): # 分割环节 if not head or not head.next: return head # 快慢指针找到中心链节点 slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next # 断开左右链节点 left_head, right_head = head, slow.next slow.next = None # 归并操作 return self.merge(self.mergeSort(left_head), self.mergeSort(right_head)) def bucketSort(self, head: ListNode, bucket_size=5): if not head: return head # 找出链表中最大值 list_max 和最小值 list_min list_min, list_max = float('inf'), float('-inf') cur = head while cur: if cur.val < list_min: list_min = cur.val if cur.val > list_max: list_max = cur.val cur = cur.next # 计算桶的个数,并定义桶 bucket_count = (list_max - list_min) // bucket_size + 1 buckets = [None for _ in range(bucket_count)] # 将链表节点值依次添加到对应桶中 cur = head while cur: index = (cur.val - list_min) // bucket_size self.insertion(buckets, index, cur.val) cur = cur.next dummy_head = ListNode(-1) cur = dummy_head # 将元素依次出桶,并拼接成有序链表 for bucket_head in buckets: bucket_cur = self.mergeSort(bucket_head) while bucket_cur: cur.next = bucket_cur cur = cur.next bucket_cur = bucket_cur.next return dummy_head.next def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.bucketSort(head) ================================================ FILE: codes/python/02_linked_list/linked_list_counting_sort.py ================================================ class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class Solution: def countingSort(self, head: ListNode): if not head: return head list_min, list_max = float('inf'), float('-inf') cur = head while cur: if cur.val < list_min: list_min = cur.val if cur.val > list_max: list_max = cur.val cur = cur.next size = list_max - list_min + 1 counts = [0 for _ in range(size)] cur = head while cur: counts[cur.val - list_min] += 1 cur = cur.next dummy_head = ListNode(-1) cur = dummy_head for i in range(size): while counts[i]: cur.next = ListNode(i + list_min) counts[i] -= 1 cur = cur.next return dummy_head.next def sortLinkedList(self, head: ListNode): return self.countingSort(head, None) ================================================ FILE: codes/python/02_linked_list/linked_list_insertion_sort.py ================================================ class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class Solution: def insertionSort(self, head: ListNode): if not head and not head.next: return head dummy_head = ListNode(-1) dummy_head.next = head sorted_list = head cur = head.next while cur: if sorted_list.val <= cur.val: # 将 cur 插入到 sorted_list 之后 sorted_list = sorted_list.next else: prev = dummy_head while prev.next.val <= cur.val: prev = prev.next # 将 cur 到链表中间 sorted_list.next = cur.next cur.next = prev.next prev.next = curr cur = sorted_list.next return dummy_head.next def sortLinkedList(self, head: ListNode): return self.insertionSort(head) ================================================ FILE: codes/python/02_linked_list/linked_list_merge_sort.py ================================================ class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class Solution: def merge(self, left, right): # 归并环节 dummy_head = ListNode(-1) cur = dummy_head while left and right: if left.val <= right.val: cur.next = left left = left.next else: cur.next = right right = right.next cur = cur.next if left: cur.next = left elif right: cur.next = right return dummy_head.next def mergeSort(self, head: ListNode): # 分割环节 if not head or not head.next: return head # 快慢指针找到中心链节点 slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next # 断开左右链节点 left_head, right_head = head, slow.next slow.next = None # 归并操作 return self.merge(self.mergeSort(left_head), self.mergeSort(right_head)) def sortLinkedList(self, head: ListNode): return self.mergeSort(head) ================================================ FILE: codes/python/02_linked_list/linked_list_quick_sort.py ================================================ class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class Solution: def partition(self, left: ListNode, right: ListNode): # 左闭右开,区间没有元素或者只有一个元素,直接返回第一个节点 if left == right or left.next == right: return left # 选择头节点为基准节点 pivot = left.val # 使用 node_i, node_j 双指针,保证 node_i 之前的节点值都小于基准节点值,node_i 与 node_j 之间的节点值都大于等于基准节点值 node_i, node_j = left, left.next while node_j != right: # 发现一个小与基准值的元素 if node_j.val < pivot: # node_i 之前节点都小于基准值,所以 node_i 向右移动一位(此时 node_i 节点值大于等于基准节点值) node_i = node_i.next # 将小于基准值的元素与当前 node_i 换位,换位后可以保证 node_i 之前的节点都小于基准节点值 node_i.val, node_j.val = node_j.val, node_i.val node_j = node_j.next # 将基准节点放到正确位置上 node_i.val, left.val = left.val, node_i.val return node_i def quickSort(self, left: ListNode, right: ListNode): if left == right or left.next == right: return left pi = self.partition(left, right) self.quickSort(left, pi) self.quickSort(pi.next, right) return left def sortLinkedList(self, head: ListNode): if not head or not head.next: return head return self.quickSort(head, None) ================================================ FILE: codes/python/02_linked_list/linked_list_radix_sort.py ================================================ class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class Solution: def radixSort(self, head: ListNode): # 计算位数最长的位数 size = 0 cur = head while cur: val_len = len(str(cur.val)) if val_len > size: size = val_len cur = cur.next # 从个位到高位遍历位数 for i in range(size): buckets = [[] for _ in range(10)] cur = head while cur: # 以每个节点对应位数上的数字为索引,将节点值放入到对应桶中 buckets[cur.val // (10 ** i) % 10].append(cur.val) cur = cur.next # 生成新的链表 dummy_head = ListNode(-1) cur = dummy_head for bucket in buckets: for num in bucket: cur.next = ListNode(num) cur = cur.next head = dummy_head.next return head def sortLinkedList(self, head: ListNode): return self.radixSort(head) ================================================ FILE: codes/python/02_linked_list/linked_list_section_sort.py ================================================ class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class Solution: def sectionSort(self, head: ListNode): node_i = head # node_i 为当前未排序链表的第一个链节点 while node_i and node_i.next: # min_node 为未排序链表中的值最小节点 min_node = node_i node_j = node_i.next while node_j: if node_j.val < min_node.val: min_node = node_j node_j = node_j.next # 交换值最小节点与未排序链表中第一个节点的值 if node_i != min_node: node_i.val, min_node.val = min_node.val, node_i.val node_i = node_i.next return head def sortLinkedList(self, head: ListNode): return self.sectionSort(head) ================================================ FILE: codes/python/03_stack_queue_hash_table/queue_circularSequential_queue.py ================================================ class Queue: # 初始化空队列 def __init__(self, size=100): self.size = size + 1 self.queue = [None for _ in range(size + 1)] self.front = 0 self.rear = 0 # 判断队列是否为空 def is_empty(self): return self.front == self.rear # 判断队列是否已满 def is_full(self): return (self.rear + 1) % self.size == self.front # 入队操作 def enqueue(self, value): if self.is_full(): raise Exception('Queue is full') else: self.rear = (self.rear + 1) % self.size self.queue[self.rear] = value # 出队操作 def dequeue(self): if self.is_empty(): raise Exception('Queue is empty') else: self.queue[self.front] = None self.front = (self.front + 1) % self.size return self.queue[self.front] # 获取队头元素 def front_value(self): if self.is_empty(): raise Exception('Queue is empty') else: value = self.queue[(self.front + 1) % self.size] return value # 获取队尾元素 def rear_value(self): if self.is_empty(): raise Exception('Queue is empty') else: value = self.queue[self.rear] return value queue = Queue(size=2) queue.enqueue(1) #print(queue.front_value()) #print(queue.rear_value()) #queue.dequeue() queue.enqueue(2) queue.enqueue(3) #queue.dequeue() print(queue.front_value()) print(queue.rear_value()) #queue.dequeue() #print(queue.front_value()) #print(queue.rear_value()) ================================================ FILE: codes/python/03_stack_queue_hash_table/queue_deque.py ================================================ class Node: """双向链表节点""" def __init__(self, value): self.value = value # 节点值 self.prev = None # 指向前一个节点的指针 self.next = None # 指向后一个节点的指针 class Deque: """双向队列实现 - 链式存储""" def __init__(self): """初始化空双向队列""" # 创建哨兵节点 self.head = Node(0) # 头哨兵节点 self.tail = Node(0) # 尾哨兵节点 self.head.next = self.tail self.tail.prev = self.head self.size = 0 # 队列大小 def is_empty(self): """判断队列是否为空""" return self.size == 0 def get_size(self): """获取队列大小""" return self.size def push_front(self, value): """队头入队""" new_node = Node(value) # 在头哨兵节点后插入新节点 new_node.next = self.head.next new_node.prev = self.head self.head.next.prev = new_node self.head.next = new_node self.size += 1 def push_back(self, value): """队尾入队""" new_node = Node(value) # 在尾哨兵节点前插入新节点 new_node.prev = self.tail.prev new_node.next = self.tail self.tail.prev.next = new_node self.tail.prev = new_node self.size += 1 def pop_front(self): """队头出队""" if self.is_empty(): raise Exception('Deque is empty') # 删除头哨兵节点后的第一个节点 node = self.head.next self.head.next = node.next node.next.prev = self.head self.size -= 1 return node.value def pop_back(self): """队尾出队""" if self.is_empty(): raise Exception('Deque is empty') # 删除尾哨兵节点前的第一个节点 node = self.tail.prev self.tail.prev = node.prev node.prev.next = self.tail self.size -= 1 return node.value def peek_front(self): """查看队头元素""" if self.is_empty(): raise Exception('Deque is empty') return self.head.next.value def peek_back(self): """查看队尾元素""" if self.is_empty(): raise Exception('Deque is empty') return self.tail.prev.value class ArrayDeque: """双向队列实现 - 顺序存储""" def __init__(self, capacity=100): """初始化双向队列""" self.capacity = capacity self.queue = [None] * capacity self.front = 0 # 队头指针 self.rear = 0 # 队尾指针 self.size = 0 # 队列大小 def is_empty(self): """判断队列是否为空""" return self.size == 0 def is_full(self): """判断队列是否已满""" return self.size == self.capacity def get_size(self): """获取队列大小""" return self.size def push_front(self, value): """队头入队""" if self.is_full(): raise Exception('Deque is full') # 队头指针向前移动 self.front = (self.front - 1) % self.capacity self.queue[self.front] = value self.size += 1 def push_back(self, value): """队尾入队""" if self.is_full(): raise Exception('Deque is full') self.queue[self.rear] = value # 队尾指针向后移动 self.rear = (self.rear + 1) % self.capacity self.size += 1 def pop_front(self): """队头出队""" if self.is_empty(): raise Exception('Deque is empty') value = self.queue[self.front] # 队头指针向后移动 self.front = (self.front + 1) % self.capacity self.size -= 1 return value def pop_back(self): """队尾出队""" if self.is_empty(): raise Exception('Deque is empty') # 队尾指针向前移动 self.rear = (self.rear - 1) % self.capacity value = self.queue[self.rear] self.size -= 1 return value def peek_front(self): """查看队头元素""" if self.is_empty(): raise Exception('Deque is empty') return self.queue[self.front] def peek_back(self): """查看队尾元素""" if self.is_empty(): raise Exception('Deque is empty') return self.queue[(self.rear - 1) % self.capacity] # 测试代码 if __name__ == "__main__": print("=== 链式存储双向队列测试 ===") deque = Deque() # 测试队尾入队 deque.push_back(1) deque.push_back(2) deque.push_back(3) print(f"队尾入队后,队列大小: {deque.get_size()}") print(f"队头元素: {deque.peek_front()}") print(f"队尾元素: {deque.peek_back()}") # 测试队头入队 deque.push_front(0) print(f"队头入队后,队列大小: {deque.get_size()}") print(f"队头元素: {deque.peek_front()}") print(f"队尾元素: {deque.peek_back()}") # 测试出队操作 print(f"队头出队: {deque.pop_front()}") print(f"队尾出队: {deque.pop_back()}") print(f"出队后,队列大小: {deque.get_size()}") print("\n=== 顺序存储双向队列测试 ===") array_deque = ArrayDeque(capacity=5) # 测试队尾入队 array_deque.push_back(1) array_deque.push_back(2) array_deque.push_back(3) print(f"队尾入队后,队列大小: {array_deque.get_size()}") print(f"队头元素: {array_deque.peek_front()}") print(f"队尾元素: {array_deque.peek_back()}") # 测试队头入队 array_deque.push_front(0) print(f"队头入队后,队列大小: {array_deque.get_size()}") print(f"队头元素: {array_deque.peek_front()}") print(f"队尾元素: {array_deque.peek_back()}") # 测试出队操作 print(f"队头出队: {array_deque.pop_front()}") print(f"队尾出队: {array_deque.pop_back()}") print(f"出队后,队列大小: {array_deque.get_size()}") # 测试循环队列特性 print("\n=== 循环队列特性测试 ===") array_deque.push_back(4) array_deque.push_back(5) array_deque.push_front(6) print(f"队列大小: {array_deque.get_size()}") print(f"是否已满: {array_deque.is_full()}") # 清空队列 while not array_deque.is_empty(): print(f"出队: {array_deque.pop_front()}") print(f"队列是否为空: {array_deque.is_empty()}") ================================================ FILE: codes/python/03_stack_queue_hash_table/queue_link_queue.py ================================================ class Node: def __init__(self, value): self.value = value self.next = None class Queue: # 初始化空队列 def __init__(self): head = Node(0) self.front = head self.rear = head # 判断队列是否为空 def is_empty(self): return self.front == self.rear # 入队操作 def enqueue(self, value): node = Node(value) self.rear.next = node self.rear = node # 出队操作 def dequeue(self): if self.is_empty(): raise Exception('Queue is empty') else: node = self.front.next self.front.next = node.next if self.rear == node: self.rear = self.front value = node.value del node return value # 获取队头元素 def front_value(self): if self.is_empty(): raise Exception('Queue is empty') else: return self.front.next.value # 获取队尾元素 def rear_value(self): if self.is_empty(): raise Exception('Queue is empty') else: return self.rear.value queue = Queue() queue.enqueue(1) print(queue.front_value()) print(queue.rear_value()) queue.dequeue() queue.enqueue(2) queue.enqueue(3) queue.dequeue() print(queue.front_value()) print(queue.rear_value()) queue.dequeue() print(queue.front_value()) print(queue.rear_value()) ================================================ FILE: codes/python/03_stack_queue_hash_table/queue_priority_queue.py ================================================ class Heapq: # 堆调整方法:调整为大顶堆 def heapAdjust(self, nums: [int], index: int, end: int): left = index * 2 + 1 right = left + 1 while left <= end: # 当前节点为非叶子结点 max_index = index if nums[left] > nums[max_index]: max_index = left if right <= end and nums[right] > nums[max_index]: max_index = right if index == max_index: # 如果不用交换,则说明已经交换结束 break nums[index], nums[max_index] = nums[max_index], nums[index] # 继续调整子树 index = max_index left = index * 2 + 1 right = left + 1 # 将数组构建为二叉堆 def heapify(self, nums: [int]): size = len(nums) # (size - 2) // 2 是最后一个非叶节点,叶节点不用调整 for i in range((size - 2) // 2, -1, -1): # 调用调整堆函数 self.heapAdjust(nums, i, size - 1) # 入队操作 def heappush(self, nums: list, value): nums.append(value) size = len(nums) i = size - 1 # 寻找插入位置 while (i - 1) // 2 >= 0: cur_root = (i - 1) // 2 # value 小于当前根节点,则插入到当前位置 if nums[cur_root] > value: break # 继续向上查找 nums[i] = nums[cur_root] i = cur_root # 找到插入位置或者到达根位置,将其插入 nums[i] = value # 出队操作 def heappop(self, nums: list) -> int: size = len(nums) nums[0], nums[-1] = nums[-1], nums[0] # 得到最大值(堆顶元素)然后调整堆 top = nums.pop() if size > 0: self.heapAdjust(nums, 0, size - 2) return top # 升序堆排序 def heapSort(self, nums: [int]): self.heapify(nums) size = len(nums) for i in range(size): nums[0], nums[size - i - 1] = nums[size - i - 1], nums[0] self.heapAdjust(nums, 0, size - i - 2) return nums nums = [49, 38, 65, 97, 76, 13, 27, 49] heap = Heapq() # 1. 创建堆,并进行堆排序 heap.heapSort(nums) heap.heapify(nums) # 2. 测试 heappop() rst = heap.heappop(nums) print(rst) rst = heap.heappop(nums) print(rst) rst = heap.heappop(nums) print(rst) rst = heap.heappop(nums) print(rst) rst = heap.heappop(nums) print(rst) rst = heap.heappop(nums) print(rst) # 3. 测试 heappush() nums = [49, 38, 65, 97, 76, 13, 27, 49] heapList = [] for num in nums: heap.heappush(heapList, num) print(heapList) # 4. 堆排序 rst = heap.heapSort(heapList) print(heapList) ================================================ FILE: codes/python/03_stack_queue_hash_table/queue_sequential_queue.py ================================================ class Queue: # 初始化空队列 def __init__(self, size=100): self.size = size self.queue = [None for _ in range(size)] self.front = -1 self.rear = -1 # 判断队列是否为空 def is_empty(self): return self.front == self.rear # 判断队列是否已满 def is_full(self): return self.rear + 1 == self.size # 入队操作 def enqueue(self, value): if self.is_full(): raise Exception('Queue is full') else: self.rear += 1 self.queue[self.rear] = value # 出队操作 def dequeue(self): if self.is_empty(): raise Exception('Queue is empty') else: self.queue[self.front] = None self.front += 1 return self.queue[self.front] # 获取队头元素 def front_value(self): if self.is_empty(): raise Exception('Queue is empty') else: return self.queue[self.front + 1] # 获取队尾元素 def rear_value(self): if self.is_empty(): raise Exception('Queue is empty') else: return self.queue[self.rear] queue = Queue(size=2) queue.enqueue(1) print(queue.front_value()) print(queue.rear_value()) #queue.dequeue() queue.enqueue(2) queue.enqueue(3) #queue.dequeue() print(queue.front_value()) print(queue.rear_value()) #queue.dequeue() #print(queue.front_value()) #print(queue.rear_value()) ================================================ FILE: codes/python/03_stack_queue_hash_table/stack_link_stack.py ================================================ class Node: def __init__(self, value): self.value = value self.next = None class Stack: # 初始化空栈 def __init__(self): self.top = None # 判断栈是否为空 def is_empty(self): return self.top == None # 入栈操作 def push(self, value): cur = Node(value) cur.next = self.top self.top = cur # 出栈操作 def pop(self): if self.is_empty(): raise Exception('Stack is empty') else: cur = self.top self.top = self.top.next del cur # 获取栈顶元素 def peek(self): if self.is_empty(): raise Exception('Stack is empty') else: return self.top.value stack = Stack() for i in range(5): stack.push(i) for i in range(3): stack.pop() print(stack.peek()) ================================================ FILE: codes/python/03_stack_queue_hash_table/stack_monotone_stack.py ================================================ import random def monotoneStack(nums): print(str(nums)) stack = [] for num in nums: while stack and num <= stack[-1]: top = stack[-1] stack.pop() print(str(top) + " 出栈 " + str(stack)) stack.append(num) print(str(num) + " 入栈 " + str(stack)) def monotoneIncreasingStack(nums): stack = [] for num in nums: while stack and num >= stack[-1]: top = stack[-1] stack.pop() print(str(top) + " 出栈 " + str(stack)) stack.append(num) print(str(num) + " 入栈 " + str(stack)) def monotoneDecreasingStack(nums): stack = [] for num in nums: while stack and num <= stack[-1]: top = stack[-1] stack.pop() print(str(top) + " 出栈 " + str(stack)) stack.append(num) print(str(num) + " 入栈 " + str(stack)) nums = [] for i in range(8): nums.append(random.randint(1, 9)) print(nums) #nums = [4, 3, 2, 5, 7, 4, 6, 8] monotoneIncreasingStack(nums) ================================================ FILE: codes/python/03_stack_queue_hash_table/stack_sequential_stack.py ================================================ #!/usr/bin/env python3 class Stack: # 初始化空栈 def __init__(self, size=100): self.stack = [] self.size = size self.top = -1 # 判断栈是否为空 def is_empty(self): return self.top == -1 # 判断栈是否已满 def is_full(self): return self.top + 1 == self.size # 入栈操作 def push(self, value): if self.is_full(): raise Exception('Stack is full') else: self.stack.append(value) self.top += 1 # 出栈操作 def pop(self): if self.is_empty(): raise Exception('Stack is empty') else: self.top -= 1 self.stack.pop() # 获取栈顶元素 def peek(self): if self.is_empty(): raise Exception('Stack is empty') else: return self.stack[self.top] S = Stack(10) for i in range(5): S.push(i) for i in range(3): S.pop() print(S.peek()) ================================================ FILE: codes/python/04_string/string_Strcmp.py ================================================ def strcmp(str1, str2): index1, index2 = 0, 0 while index1 < len(str1) and index2 < len(str2): if ord(str1[index1]) == ord(str2[index2]): index1 += 1 index2 += 1 elif ord(str1[index1]) < ord(str2[index2]): return -1 else: return 1 if len(str1) < len(str2): return -1 elif len(str1) > len(str2): return 1 else: return 0 print(strcmp("123", "123")) print(strcmp("123", "124")) print(strcmp("123", "1235")) print(strcmp("223", "123")) print(strcmp("13", "123")) print(strcmp("132", "123")) print(strcmp("1322", "1325")) ================================================ FILE: codes/python/04_string/string_boyer_moore.py ================================================ # BM 匹配算法 def boyerMoore(T: str, p: str) -> int: n, m = len(T), len(p) bc_table = generateBadCharTable(p) # 生成坏字符位置表 gs_list = generageGoodSuffixList(p) # 生成好后缀规则后移位数表 i = 0 while i <= n - m: j = m - 1 while j > -1 and T[i + j] == p[j]: # 进行后缀匹配,跳出循环说明出现坏字符 j -= 1 if j < 0: return i # 匹配完成,返回模式串 p 在文本串 T 中的位置 bad_move = j - bc_table.get(T[i + j], -1) # 坏字符规则下的后移位数 good_move = gs_list[j] # 好后缀规则下的后移位数 i += max(bad_move, good_move) # 取两种规则下后移位数的最大值进行移动 return -1 # 生成坏字符位置表 # bc_table[bad_char] 表示坏字符在模式串中最后一次出现的位置 def generateBadCharTable(p: str): bc_table = dict() for i in range(len(p)): bc_table[p[i]] = i # 更新坏字符在模式串中最后一次出现的位置 return bc_table # 生成好后缀规则后移位数表 # gs_list[j] 表示在 j 下标处遇到坏字符时,可根据好规则向右移动的距离 def generageGoodSuffixList(p: str): # 好后缀规则后移位数表 # 情况 1: 模式串中有子串匹配上好后缀 # 情况 2: 模式串中无子串匹配上好后缀,但有最长前缀匹配好后缀的后缀 # 情况 3: 模式串中无子串匹配上好后缀,也找不到前缀匹配 m = len(p) gs_list = [m for _ in range(m)] # 情况 3:初始化时假设全部为情况 3 suffix = generageSuffixArray(p) # 生成 suffix 数组 j = 0 # j 为好后缀前的坏字符位置 for i in range(m - 1, -1, -1): # 情况 2:从最长的前缀开始检索 if suffix[i] == i + 1: # 匹配到前缀,即 p[0...i] == p[m-1-i...m-1] while j < m - 1 - i: if gs_list[j] == m: gs_list[j] = m - 1 - i # 更新在 j 处遇到坏字符可向后移动位数 j += 1 for i in range(m - 1): # 情况 1:匹配到子串 p[i-s...i] == p[m-1-s, m-1] gs_list[m - 1 - suffix[i]] = m - 1 - i # 更新在好后缀的左端点处遇到坏字符可向后移动位数 return gs_list # 生成 suffix 数组 # suffix[i] 表示为以下标 i 为结尾的子串与模式串后缀匹配的最大长度 def generageSuffixArray(p: str): m = len(p) suffix = [m for _ in range(m)] # 初始化时假设匹配的最大长度为 m for i in range(m - 2, -1, -1): # 子串末尾从 m - 2 开始 start = i # start 为子串开始位置 while start >= 0 and p[start] == p[m - 1 - i + start]: start -= 1 # 进行后缀匹配,start 为匹配到的子串开始位置 suffix[i] = i - start # 更新以下标 i 为结尾的子串与模式串后缀匹配的最大长度 return suffix print(boyerMoore("abbcfdddbddcaddebc", "aaaaa")) print(boyerMoore("", "")) ================================================ FILE: codes/python/04_string/string_brute_force.py ================================================ def bruteForce(T: str, p: str) -> int: n, m = len(T), len(p) i, j = 0, 0 # i 表示文本串 T 的当前位置,j 表示模式串 p 的当前位置 while i < n and j < m: # i 或 j 其中一个到达尾部时停止搜索 if T[i] == p[j]: # 如果相等,则继续进行下一个字符匹配 i += 1 j += 1 else: i = i - (j - 1) # 如果匹配失败则将 i 移动到上次匹配开始位置的下一个位置 j = 0 # 匹配失败 j 回退到模式串开始位置 if j == m: return i - j # 匹配成功,返回匹配的开始位置 else: return -1 # 匹配失败,返回 -1 print(bruteForce("abcdeabc", "bcd")) ================================================ FILE: codes/python/04_string/string_horspool.py ================================================ # horspool 算法,T 为文本串,p 为模式串 def horspool(T: str, p: str) -> int: n, m = len(T), len(p) bc_table = generateBadCharTable(p) # 生成后移位数表 i = 0 while i <= n - m: j = m - 1 while j > -1 and T[i + j] == p[j]: # 进行后缀匹配,跳出循环说明出现坏字符 j -= 1 if j < 0: return i # 匹配完成,返回模式串 p 在文本串 T 中的位置 i += bc_table.get(T[i + m - 1], m) # 通过后移位数表,向右进行进行快速移动 return -1 # 匹配失败 # 生成后移位数表 # bc_table[bad_char] 表示遇到坏字符可以向右移动的距离 def generateBadCharTable(p: str): m = len(p) bc_table = dict() for i in range(m - 1): # 迭代到 m - 2 bc_table[p[i]] = m - 1 - i # 更新遇到坏字符可向右移动的距离 return bc_table print(horspool("abbcfdddbddcaddebc", "aaaaa")) print(horspool("abbcfdddbddcaddebc", "bcf")) print(horspool("aaaaa", "bba")) print(horspool("mississippi", "issi")) print(horspool("ababbbbaaabbbaaa", "bbbb")) ================================================ FILE: codes/python/04_string/string_kmp.py ================================================ # KMP 匹配算法,T 为文本串,p 为模式串 def kmp(T: str, p: str) -> int: n, m = len(T), len(p) next = generateNext(p) # 生成 next 数组 j = 0 # j 为模式串中当前匹配的位置 for i in range(n): # i 为文本串中当前匹配的位置 while j > 0 and T[i] != p[j]: # 如果模式串匹配不成功, 将模式串进行回退, j == 0 时停止回退 j = next[j - 1] if T[i] == p[j]: # 当前模式串前缀匹配成功,令 j += 1,继续匹配 j += 1 if j == m: # 当前模式串完全匹配成功,返回匹配开始位置 return i - j + 1 return -1 # 匹配失败,返回 -1 # 生成 next 数组 # next[j] 表示下标 j 之前的模式串 p 中,最长相等前后缀的长度 def generateNext(p: str): m = len(p) next = [0 for _ in range(m)] # 初始化数组元素全部为 0 left = 0 # left 表示前缀串开始所在的下标位置 for right in range(1, m): # right 表示后缀串开始所在的下标位置 while left > 0 and p[left] != p[right]: # 匹配不成功, left 进行回退, left == 0 时停止回退 left = next[left - 1] # left 进行回退操作 if p[left] == p[right]: # 匹配成功,找到相同的前后缀,先让 left += 1,此时 left 为前缀长度 left += 1 next[right] = left # 记录前缀长度,更新 next[right], 结束本次循环, right += 1 return next print(kmp("abbcfdddbddcaddebc", "ABCABCD")) print(kmp("abbcfdddbddcaddebc", "bcf")) print(kmp("aaaaa", "bba")) print(kmp("mississippi", "issi")) print(kmp("ababbbbaaabbbaaa", "bbbb")) ================================================ FILE: codes/python/04_string/string_rabin_karp.py ================================================ # T 为文本串,p 为模式串,d 为字符集的字符种类数,q 为质数 def rabinKarp(T: str, p: str, d, q) -> int: n, m = len(T), len(p) if n < m: return -1 hash_p, hash_t = 0, 0 for i in range(m): hash_p = (hash_p * d + ord(p[i])) % q # 计算模式串 p 的哈希值 hash_t = (hash_t * d + ord(T[i])) % q # 计算文本串 T 中第一个子串的哈希值 power = pow(d, m - 1) % q # power 用于移除字符哈希时 for i in range(n - m + 1): if hash_p == hash_t: # 检查模式串 p 的哈希值和子串的哈希值 match = True # 如果哈希值相等,验证模式串和子串每个字符是否完全相同(避免哈希冲突) for j in range(m): if T[i + j] != p[j]: match = False # 模式串和子串某个字符不相等,验证失败,跳出循环 break if match: # 如果模式串和子串每个字符是否完全相同,返回匹配开始位置 return i if i < n - m: # 计算下一个相邻子串的哈希值 hash_t = (hash_t - power * ord(T[i])) % q # 移除字符 T[i] hash_t = (hash_t * d + ord(T[i + m])) % q # 增加字符 T[i + m] hash_t = (hash_t + q) % q # 确保 hash_t >= 0 return -1 print(rabinKarp("aaaaa", "bba", 256, 101)) ================================================ FILE: codes/python/04_string/string_sunday.py ================================================ # sunday 算法,T 为文本串,p 为模式串 def sunday(T: str, p: str) -> int: n, m = len(T), len(p) bc_table = generateBadCharTable(p) # 生成后移位数表 i = 0 while i <= n - m: j = 0 if T[i: i + m] == p: return i # 匹配完成,返回模式串 p 在文本串 T 的位置 if i + m >= n: return -1 i += bc_table.get(T[i + m], m + 1) # 通过后移位数表,向右进行进行快速移动 return -1 # 匹配失败 # 生成后移位数表 # bc_table[bad_char] 表示遇到坏字符可以向右移动的距离 def generateBadCharTable(p: str): m = len(p) bc_table = dict() for i in range(m): # 迭代到最后一个位置 m - 1 bc_table[p[i]] = m - i # 更新遇到坏字符可向右移动的距离 return bc_table print(sunday("abbcfdddbddcaddebc", "aaaaa")) print(sunday("abbcfdddbddcaddebc", "bcf")) print(sunday("aaaaa", "bba")) print(sunday("mississippi", "issi")) print(sunday("ababbbbaaabbbaaa", "bbbb")) ================================================ FILE: codes/python/04_string/string_trie.py ================================================ class Node: # 字符节点 def __init__(self): # 初始化字符节点 self.children = dict() # 初始化子节点 self.isEnd = False # isEnd 用于标记单词结束 class Trie: # 字典树 # 初始化字典树 def __init__(self): # 初始化字典树 self.root = Node() # 初始化根节点(根节点不保存字符) # 向字典树中插入一个单词 def insert(self, word: str) -> None: cur = self.root for ch in word: # 遍历单词中的字符 if ch not in cur.children: # 如果当前节点的子节点中,不存在键为 ch 的节点 cur.children[ch] = Node() # 建立一个节点,并将其保存到当前节点的子节点 cur = cur.children[ch] # 令当前节点指向新建立的节点,继续处理下一个字符 cur.isEnd = True # 单词处理完成时,将当前节点标记为单词结束 # 查找字典树中是否存在一个单词 def search(self, word: str) -> bool: cur = self.root for ch in word: # 遍历单词中的字符 if ch not in cur.children: # 如果当前节点的子节点中,不存在键为 ch 的节点 return False # 直接返回 False cur = cur.children[ch] # 令当前节点指向新建立的节点,然后继续查找下一个字符 return cur is not None and cur.isEnd # 判断当前节点是否为空,并且是否有单词结束标记 # 查找字典树中是否存在一个前缀 def startsWith(self, prefix: str) -> bool: cur = self.root for ch in prefix: # 遍历前缀中的字符 if ch not in cur.children: # 如果当前节点的子节点中,不存在键为 ch 的节点 return False # 直接返回 False cur = cur.children[ch] # 令当前节点指向新建立的节点,然后继续查找下一个字符 return cur is not None # 判断当前节点是否为空,不为空则查找成功 ================================================ FILE: codes/python/05_tree/tree_binaryindexed_tree.py ================================================ class BinaryIndexTree: def __init__(self, n): self.size = n self.tree = [0 for _ in range(n + 1)] def lowbit(self, index): return index & (-index) def update(self, index, delta): while index <= self.size: self.tree[index] += delta index += self.lowbit(index) def query(self, index): res = 0 while index > 0: res += self.tree[index] index -= self.lowbit(index) return res ================================================ FILE: codes/python/05_tree/tree_dynamicSegmentTree_update_interval_1.py ================================================ # 线段树的节点类 class SegTreeNode: def __init__(self, left=-1, right=-1, val=False, lazy_tag=None, leftNode=None, rightNode=None): self.left = left # 区间左边界 self.right = right # 区间右边界 self.mid = left + (right - left) // 2 self.leftNode = leftNode # 区间左节点 self.rightNode = rightNode # 区间右节点 self.val = val # 节点值(区间值) self.lazy_tag = lazy_tag # 区间问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, function): self.tree = SegTreeNode(0, int(1e9)) self.function = function # function 是一个函数,左右区间的聚合方法 # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.__update_point(i, val, self.tree) # 区间更新接口:将区间为 [q_left, q_right] 上的元素值修改为 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, self.tree) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, self.tree) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self, length): nums = [0 for _ in range(length)] for i in range(length): nums[i] = self.query_interval(i, i) return nums # 以下为内部实现方法 # 单点更新实现方法:将 nums[i] 更改为 val。node 节点的区间为 [node.left, node.right] def __update_point(self, i, val, node): if node.left == node.right: node.val = val # 叶子节点,节点值修改为 val return if i <= node.mid: # 在左子树中更新节点值 self.__update_point(i, val, node.leftNode) else: # 在右子树中更新节点值 self.__update_point(i, val, node.rightNode) self.__pushup(node) # 向上更新节点的区间值 # 区间更新实现方法 def __update_interval(self, q_left, q_right, val, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 node.lazy_tag = val # 将当前节点的延迟标记增加 val interval_size = (node.right - node.left + 1) # 当前节点所在区间大小 node.val = val * interval_size # 当前节点所在区间每个元素值改为 val return if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 if q_left <= node.mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, node.leftNode) if q_right > node.mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, node.rightNode) self.__pushup(node) # 区间查询实现方法:在线段树的 [left, right] 区间范围中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return node.val # 直接返回节点值 if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= node.mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, node.leftNode) if q_right > node.mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, node.rightNode) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:更新 node 节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, node): if node.leftNode and node.rightNode: node.val = self.function(node.leftNode.val, node.rightNode.val) # 向下更新实现方法:更新 node 节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, node): if node.leftNode is None: node.leftNode = SegTreeNode(node.left, node.mid) if node.rightNode is None: node.rightNode = SegTreeNode(node.mid + 1, node.right) lazy_tag = node.lazy_tag if node.lazy_tag is None: return node.leftNode.lazy_tag = lazy_tag # 更新左子节点懒惰标记 left_size = (node.leftNode.right - node.leftNode.left + 1) node.leftNode.val = lazy_tag * left_size # 更新左子节点值 node.rightNode.lazy_tag = lazy_tag # 更新右子节点懒惰标记 right_size = (node.rightNode.right - node.rightNode.left + 1) node.rightNode.val = lazy_tag * right_size # 更新右子节点值 node.lazy_tag = None # 更新当前节点的懒惰标记 # 线段树使用方法 class Solution: # 初始化线段树 def __init__(self): self.STree = SegmentTree(lambda x, y: x + y) # 将区间 [left, right] 上的元素值全部修改为 val def update(self, left: int, right: int, val) -> None: self.STree.update_interval(left, right, val) # 查询区间 [left, right] 的区间值 def sumRange(self, left: int, right: int) -> int: return self.STree.query_interval(left, right) ================================================ FILE: codes/python/05_tree/tree_dynamicSegmentTree_update_interval_2.py ================================================ # 线段树的节点类 class SegTreeNode: def __init__(self, left=-1, right=-1, val=0, lazy_tag=None, leftNode=None, rightNode=None): self.left = left # 区间左边界 self.right = right # 区间右边界 self.mid = left + (right - left) // 2 self.leftNode = leftNode # 区间左节点 self.rightNode = rightNode # 区间右节点 self.val = val # 节点值(区间值) self.lazy_tag = lazy_tag # 区间问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, function): self.tree = SegTreeNode(0, int(1e9)) self.function = function # function 是一个函数,左右区间的聚合方法 # 单点更新,将 nums[i] 更改为 val def update_point(self, i, val): self.__update_point(i, val, self.tree) # 区间更新,将区间为 [q_left, q_right] 上的元素值修改为 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, self.tree) # 区间查询,查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, self.tree) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self, length): nums = [0 for _ in range(length)] for i in range(length): nums[i] = self.query_interval(i, i) return nums # 以下为内部实现方法 # 单点更新,将 nums[i] 更改为 val。node 节点的区间为 [node.left, node.right] def __update_point(self, i, val, node): if node.left == node.right: node.val = val # 叶子节点,节点值修改为 val return if i <= node.mid: # 在左子树中更新节点值 self.__update_point(i, val, node.leftNode) else: # 在右子树中更新节点值 self.__update_point(i, val, node.rightNode) self.__pushup(node) # 向上更新节点的区间值 # 区间更新 def __update_interval(self, q_left, q_right, val, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 if node.lazy_tag is not None: node.lazy_tag += val # 将当前节点的延迟标记增加 val else: node.lazy_tag = val # 将当前节点的延迟标记增加 val interval_size = (node.right - node.left + 1) # 当前节点所在区间大小 node.val += val * interval_size # 当前节点所在区间每个元素值增加 val return if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 if q_left <= node.mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, node.leftNode) if q_right > node.mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, node.rightNode) self.__pushup(node) # 区间查询,在线段树的 [left, right] 区间范围中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return node.val # 直接返回节点值 if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= node.mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, node.leftNode) if q_right > node.mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, node.rightNode) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新 node 节点区间值,节点的区间值等于该节点左右子节点元素值的聚合计算结果 def __pushup(self, node): if node.leftNode and node.rightNode: node.val = self.function(node.leftNode.val, node.rightNode.val) # 向下更新 node 节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, node): if node.leftNode is None: node.leftNode = SegTreeNode(node.left, node.mid) if node.rightNode is None: node.rightNode = SegTreeNode(node.mid + 1, node.right) lazy_tag = node.lazy_tag if node.lazy_tag is None: return if node.leftNode.lazy_tag is not None: node.leftNode.lazy_tag += lazy_tag # 更新左子节点懒惰标记 else: node.leftNode.lazy_tag = lazy_tag # 更新左子节点懒惰标记 left_size = (node.leftNode.right - node.leftNode.left + 1) node.leftNode.val += lazy_tag * left_size # 左子节点每个元素值增加 lazy_tag if node.rightNode.lazy_tag is not None: node.rightNode.lazy_tag += lazy_tag # 更新右子节点懒惰标记 else: node.rightNode.lazy_tag = lazy_tag # 更新右子节点懒惰标记 right_size = (node.rightNode.right - node.rightNode.left + 1) node.rightNode.val += lazy_tag * right_size # 右子节点每个元素值增加 lazy_tag node.lazy_tag = None # 更新当前节点的懒惰标记 # 线段树使用方法 class Solution: # 初始化线段树 def __init__(self): self.STree = SegmentTree(lambda x, y: x + y) # 将区间 [left, right] 上的元素值全部修改为 val def update(self, left: int, right: int, val) -> None: self.STree.update_interval(left, right, val) # 查询区间 [left, right] 的区间值 def sumRange(self, left: int, right: int) -> int: return self.STree.query_interval(left, right) ================================================ FILE: codes/python/05_tree/tree_segmentTree_update_interval_1.py ================================================ # 线段树的节点类 class SegTreeNode: def __init__(self, val=0): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值) self.lazy_tag = None # 区间和问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, nums, function): self.size = len(nums) self.tree = [SegTreeNode() for _ in range(4 * self.size)] # 维护 SegTreeNode 数组 self.nums = nums # 原始数据 self.function = function # function 是一个函数,左右区间的聚合方法 if self.size > 0: self.__build(0, 0, self.size - 1) # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.nums[i] = val self.__update_point(i, val, 0) # 区间更新接口:将区间为 [q_left, q_right] 上的元素值修改为 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, 0) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, 0) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self): for i in range(self.size): self.nums[i] = self.query_interval(i, i) return self.nums # 以下为内部实现方法 # 构建线段树实现方法:节点的存储下标为 index,节点的区间为 [left, right] def __build(self, index, left, right): self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,节点值为对应位置的元素值 self.tree[index].val = self.nums[left] return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.__build(left_index, left, mid) # 递归创建左子树 self.__build(right_index, mid + 1, right) # 递归创建右子树 self.__pushup(index) # 向上更新节点的区间值 # 单点更新实现方法:将 nums[i] 更改为 val,节点的存储下标为 index def __update_point(self, i, val, index): left = self.tree[index].left right = self.tree[index].right if left == right: self.tree[index].val = val # 叶子节点,节点值修改为 val return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if i <= mid: # 在左子树中更新节点值 self.__update_point(i, val, left_index) else: # 在右子树中更新节点值 self.__update_point(i, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间更新实现方法 def __update_interval(self, q_left, q_right, val, index): left = self.tree[index].left right = self.tree[index].right if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 self.tree[index].lazy_tag = val # 将当前节点的延迟标记为区间值 interval_size = (right - left + 1) # 当前节点所在区间大小 self.tree[index].val = interval_size * val # 当前节点所在区间每个元素值改为 val return self.__pushdown(index) # 向下更新节点的区间值 mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if q_left <= mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, left_index) if q_right > mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间查询实现方法:在线段树中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return self.tree[index].val # 直接返回节点值 if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(index) mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, left_index) if q_right > mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, right_index) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:更新下标为 index 的节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, index): left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.tree[index].val = self.function(self.tree[left_index].val, self.tree[right_index].val) # 向下更新实现方法:更新下标为 index 的节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, index): lazy_tag = self.tree[index].lazy_tag if lazy_tag is None: return left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.tree[left_index].lazy_tag = lazy_tag # 更新左子节点懒惰标记 left_size = (self.tree[left_index].right - self.tree[left_index].left + 1) self.tree[left_index].val = lazy_tag * left_size # 更新左子节点值 self.tree[right_index].lazy_tag = lazy_tag # 更新右子节点懒惰标记 right_size = (self.tree[right_index].right - self.tree[right_index].left + 1) self.tree[right_index].val = lazy_tag * right_size # 更新右子节点值 self.tree[index].lazy_tag = None # 更新当前节点的懒惰标记 class Solution: # 根据原始数组 nums 初始化线段树 def __init__(self, nums: List[int]): self.STree = SegmentTree(nums, lambda x, y: x + y) # 将数组 nums 将区间为 [left, right] 上的元素值全部修改为 val def update(self, left: int, right: int, val) -> None: self.STree.update_interval(left, right, val) # 查询区间为 [left, right] 的区间值 def sumRange(self, left: int, right: int) -> int: return self.STree.query_interval(left, right) ================================================ FILE: codes/python/05_tree/tree_segmentTree_update_interval_2.py ================================================ # 线段树的节点类 class SegTreeNode: def __init__(self, val=0): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值) self.lazy_tag = None # 区间和问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, nums, function): self.size = len(nums) self.tree = [SegTreeNode() for _ in range(4 * self.size)] # 维护 SegTreeNode 数组 self.nums = nums # 原始数据 self.function = function # function 是一个函数,左右区间的聚合方法 if self.size > 0: self.__build(0, 0, self.size - 1) # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.nums[i] = val self.__update_point(i, val, 0) # 区间更新接口:将区间为 [q_left, q_right] 上的所有元素值加上 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, 0) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, 0) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self): for i in range(self.size): self.nums[i] = self.query_interval(i, i) return self.nums # 以下为内部实现方法 # 构建线段树实现方法:节点的存储下标为 index,节点的区间为 [left, right] def __build(self, index, left, right): self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,节点值为对应位置的元素值 self.tree[index].val = self.nums[left] return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.__build(left_index, left, mid) # 递归创建左子树 self.__build(right_index, mid + 1, right) # 递归创建右子树 self.__pushup(index) # 向上更新节点的区间值 # 单点更新实现方法:将 nums[i] 更改为 val,节点的存储下标为 index def __update_point(self, i, val, index): left = self.tree[index].left right = self.tree[index].right if left == right: self.tree[index].val = val # 叶子节点,节点值修改为 val return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if i <= mid: # 在左子树中更新节点值 self.__update_point(i, val, left_index) else: # 在右子树中更新节点值 self.__update_point(i, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间更新实现方法 def __update_interval(self, q_left, q_right, val, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 if self.tree[index].lazy_tag is not None: self.tree[index].lazy_tag += val # 将当前节点的延迟标记增加 val else: self.tree[index].lazy_tag = val # 将当前节点的延迟标记增加 val interval_size = (right - left + 1) # 当前节点所在区间大小 self.tree[index].val += val * interval_size # 当前节点所在区间每个元素值增加 val return if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return self.__pushdown(index) # 向下更新节点的区间值 mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if q_left <= mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, left_index) if q_right > mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间查询实现方法:在线段树中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return self.tree[index].val # 直接返回节点值 if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(index) mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, left_index) if q_right > mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, right_index) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:更新下标为 index 的节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, index): left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.tree[index].val = self.function(self.tree[left_index].val, self.tree[right_index].val) # 向下更新实现方法:更新下标为 index 的节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, index): lazy_tag = self.tree[index].lazy_tag if lazy_tag is None: return left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if self.tree[left_index].lazy_tag is not None: self.tree[left_index].lazy_tag += lazy_tag # 更新左子节点懒惰标记 else: self.tree[left_index].lazy_tag = lazy_tag left_size = (self.tree[left_index].right - self.tree[left_index].left + 1) self.tree[left_index].val += lazy_tag * left_size # 左子节点每个元素值增加 lazy_tag if self.tree[right_index].lazy_tag is not None: self.tree[right_index].lazy_tag += lazy_tag # 更新右子节点懒惰标记 else: self.tree[right_index].lazy_tag = lazy_tag right_size = (self.tree[right_index].right - self.tree[right_index].left + 1) self.tree[right_index].val += lazy_tag * right_size # 右子节点每个元素值增加 lazy_tag self.tree[index].lazy_tag = None # 更新当前节点的懒惰标记 class Solution: # 根据原始数组 nums 初始化线段树 def __init__(self, nums: List[int]): self.STree = SegmentTree(nums, lambda x, y: x + y) # 将数组 nums 将区间为 [left, right] 上每个元素值增加 val def addVal(self, left: int, right: int, val) -> None: self.STree.update_interval(left, right, val) # 查询区间为 [left, right] 的区间值 def sumRange(self, left: int, right: int) -> int: return self.STree.query_interval(left, right) ================================================ FILE: codes/python/05_tree/tree_segmentTree_update_point.py ================================================ # 线段树的节点类 class SegTreeNode: def __init__(self, val=0): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值) # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, nums, function): self.size = len(nums) self.tree = [SegTreeNode() for _ in range(4 * self.size)] # 维护 SegTreeNode 数组 self.nums = nums # 原始数据 self.function = function # function 是一个函数,左右区间的聚合方法 if self.size > 0: self.__build(0, 0, self.size - 1) # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.nums[i] = val self.__update_point(i, val, 0) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, 0) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self): for i in range(self.size): self.nums[i] = self.query_interval(i, i) return self.nums # 以下为内部实现方法 # 构建线段树实现方法:节点的存储下标为 index,节点的区间为 [left, right] def __build(self, index, left, right): self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,节点值为对应位置的元素值 self.tree[index].val = self.nums[left] return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.__build(left_index, left, mid) # 递归创建左子树 self.__build(right_index, mid + 1, right) # 递归创建右子树 self.__pushup(index) # 向上更新节点的区间值 # 单点更新实现方法:将 nums[i] 更改为 val。节点的存储下标为 index,节点的区间为 [left, right] def __update_point(self, i, val, index): left = self.tree[index].left right = self.tree[index].right if left == right: self.tree[index].val = val # 叶子节点,节点值修改为 val return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if i <= mid: # 在左子树中更新节点值 self.__update_point(i, val, left_index) else: # 在右子树中更新节点值 self.__update_point(i, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间查询实现方法:在线段树的 [left, right] 区间范围中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return self.tree[index].val # 直接返回节点值 if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, left_index) if q_right > mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, right_index) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:下标为 index 的节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, index): left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.tree[index].val = self.function(self.tree[left_index].val, self.tree[right_index].val) # 线段树使用方法 class Solution: # 根据原始数组 nums 初始化线段树 def __init__(self, nums: List[int]): self.STree = SegmentTree(nums, lambda x, y: x + y) # 将 nums[index] 更改为 val def update(self, index: int, val: int) -> None: self.STree.update_point(index, val) # 查询区间为 [left, right] 的区间值 def sumRange(self, left: int, right: int) -> int: return self.STree.query_interval(left, right) ================================================ FILE: codes/python/05_tree/tree_unionFind.py ================================================ class UnionFind: def __init__(self, n): # 初始化 self.fa = [i for i in range(n)] # 每个元素的集合编号初始化为数组 fa 的下标索引 def find(self, x): # 查找元素根节点的集合编号内部实现方法 while self.fa[x] != x: # 递归查找元素的父节点,直到根节点 self.fa[x] = self.fa[self.fa[x]] # 隔代压缩优化 x = self.fa[x] return x # 返回元素根节点的集合编号 def union(self, x, y): # 合并操作:令其中一个集合的树根节点指向另一个集合的树根节点 root_x = self.find(x) root_y = self.find(y) if root_x == root_y: # x 和 y 的根节点集合编号相同,说明 x 和 y 已经同属于一个集合 return False self.fa[root_x] = root_y # x 的根节点连接到 y 的根节点上,成为 y 的根节点的子节点 return True def is_connected(self, x, y): # 查询操作:判断 x 和 y 是否同属于一个集合 return self.find(x) == self.find(y) ================================================ FILE: codes/python/05_tree/tree_unionFind_QuickFind.py ================================================ class UnionFind: def __init__(self, n): # 初始化:将每个元素的集合编号初始化为数组下标索引 self.ids = [i for i in range(n)] def find(self, x): # 查找元素所属集合编号内部实现方法 return self.ids[x] def union(self, x, y): # 合并操作:将集合 x 和集合 y 合并成一个集合 x_id = self.find(x) y_id = self.find(y) if x_id == y_id: # x 和 y 已经同属于一个集合 return False for i in range(len(self.ids)): # 将两个集合的集合编号改为一致 if self.ids[i] == y_id: self.ids[i] = x_id return True def is_connected(self, x, y): # 查询操作:判断 x 和 y 是否同属于一个集合 return self.find(x) == self.find(y) ================================================ FILE: codes/python/05_tree/tree_unionFind_QuickUnion.py ================================================ class UnionFind: def __init__(self, n): # 初始化:将每个元素的集合编号初始化为数组 fa 的下标索引 self.fa = [i for i in range(n)] def find(self, x): # 查找元素根节点的集合编号内部实现方法 while x != self.fa[x]: # 递归查找元素的父节点,直到根节点 x = self.fa[x] return x # 返回元素根节点的集合编号 def union(self, x, y): # 合并操作:令其中一个集合的树根节点指向另一个集合的树根节点 root_x = self.find(x) root_y = self.find(y) if root_x == root_y: # x 和 y 的根节点集合编号相同,说明 x 和 y 已经同属于一个集合 return False self.fa[root_x] = root_y # x 的根节点连接到 y 的根节点上,成为 y 的根节点的子节点 return True def is_connected(self, x, y): # 查询操作:判断 x 和 y 是否同属于一个集合 return self.find(x) == self.find(y) ================================================ FILE: codes/python/05_tree/tree_unionFind_UnoinByRank.py ================================================ class UnionFind: def __init__(self, n): # 初始化 self.fa = [i for i in range(n)] # 每个元素的集合编号初始化为数组 fa 的下标索引 self.rank = [1 for i in range(n)] # 每个元素的深度初始化为 1 def find(self, x): # 查找元素根节点的集合编号内部实现方法 while self.fa[x] != x: # 递归查找元素的父节点,直到根节点 self.fa[x] = self.fa[self.fa[x]] # 隔代压缩优化 x = self.fa[x] return x # 返回元素根节点的集合编号 def union(self, x, y): # 合并操作:令其中一个集合的树根节点指向另一个集合的树根节点 root_x = self.find(x) root_y = self.find(y) if root_x == root_y: # x 和 y 的根节点集合编号相同,说明 x 和 y 已经同属于一个集合 return False if self.rank[root_x] < self.rank[root_y]: # x 的根节点对应的树的深度 小于 y 的根节点对应的树的深度 self.fa[root_x] = root_y # x 的根节点连接到 y 的根节点上,成为 y 的根节点的子节点 elif self.rank[root_y] > self.rank[root_y]: # x 的根节点对应的树的深度 大于 y 的根节点对应的树的深度 self.fa[root_y] = root_x # y 的根节点连接到 x 的根节点上,成为 x 的根节点的子节点 else: # x 的根节点对应的树的深度 等于 y 的根节点对应的树的深度 self.fa[root_x] = root_y # 向任意一方合并即可 self.rank[root_y] += 1 # 因为层数相同,被合并的树必然层数会 +1 return True def is_connected(self, x, y): # 查询操作:判断 x 和 y 是否同属于一个集合 return self.find(x) == self.find(y) ================================================ FILE: codes/python/05_tree/tree_unionFind_UnoinBySize.py ================================================ class UnionFind: def __init__(self, n): # 初始化 self.fa = [i for i in range(n)] # 每个元素的集合编号初始化为数组 fa 的下标索引 self.size = [1 for i in range(n)] # 每个元素的集合个数初始化为 1 def find(self, x): # 查找元素根节点的集合编号内部实现方法 while self.fa[x] != x: # 递归查找元素的父节点,直到根节点 self.fa[x] = self.fa[self.fa[x]] # 隔代压缩优化 x = self.fa[x] return x # 返回元素根节点的集合编号 def union(self, x, y): # 合并操作:令其中一个集合的树根节点指向另一个集合的树根节点 root_x = self.find(x) root_y = self.find(y) if root_x == root_y: # x 和 y 的根节点集合编号相同,说明 x 和 y 已经同属于一个集合 return False if self.size[root_x] < self.size[root_y]: # x 对应的集合元素个数 小于 y 对应的集合元素个数 self.fa[root_x] = root_y # x 的根节点连接到 y 的根节点上,成为 y 的根节点的子节点 self.size[root_y] += self.size[root_x] # y 的根节点对应的集合元素个数 累加上 x 的根节点对应的集合元素个数 elif self.size[root_x] > self.size[root_y]: # x 对应的集合元素个数 大于 y 对应的集合元素个数 self.fa[root_y] = root_x # y 的根节点连接到 x 的根节点上,成为 x 的根节点的子节点 self.size[root_x] += self.size[root_y] # x 的根节点对应的集合元素个数 累加上 y 的根节点对应的集合元素个数 else: # x 对应的集合元素个数 小于 y 对应的集合元素个数 self.fa[root_x] = root_y # 向任意一方合并即可 self.size[root_y] += self.size[root_x] return True def is_connected(self, x, y): # 查询操作:判断 x 和 y 是否同属于一个集合 return self.find(x) == self.find(y) ================================================ FILE: codes/python/06_graph/Graph-Adjacency-List.py ================================================ class EdgeNode: # 边信息类 def __init__(self, vj, val): self.vj = vj # 边的终点 self.val = val # 边的权值 self.next = None # 下一条边 class VertexNode: # 顶点信息类 def __init__(self, vi): self.vi = vi # 边的起点 self.head = None # 下一个邻接点 class Graph: def __init__(self, ver_count): self.ver_count = ver_count self.vertices = [] for vi in range(ver_count): vertex = VertexNode(vi) self.vertices.append(vertex) # 判断顶点 v 是否有效 def __valid(self, v): return 0 <= v <= self.ver_count # 图的创建操作,edges 为边信息 def creatGraph(self, edges=[]): for vi, vj, val in edges: self.add_edge(vi, vj, val) # 向图的邻接表中添加边:vi - vj,权值为 val def add_edge(self, vi, vj, val): if not self.__valid(vi) or not self.__valid(vj): raise ValueError(str(vi) + ' or ' + str(vj) + " is not a valid vertex.") vertex = self.vertices[vi] edge = EdgeNode(vj, val) edge.next = vertex.head vertex.head = edge # 获取 vi - vj 边的权值 def get_edge(self, vi, vj): if not self.__valid(vi) or not self.__valid(vj): raise ValueError(str(vi) + ' or ' + str(vj) + " is not a valid vertex.") vertex = self.vertices[vi] cur_edge = vertex.head while cur_edge: if cur_edge.vj == vj: return cur_edge.val cur_edge = cur_edge.next return None # 根据邻接表打印图的边 def printGraph(self): for vertex in self.vertices: cur_edge = vertex.head while cur_edge: print(str(vertex.vi) + ' - ' + str(cur_edge.vj) + ' : ' + str(cur_edge.val)) cur_edge = cur_edge.next graph = Graph(7) edges = [[1, 2, 5],[1, 5, 6],[2, 4, 7],[4, 3, 9],[3, 1, 2],[5, 6, 8],[6, 4, 3]] graph.creatGraph(edges) print(graph.get_edge(3, 4)) graph.printGraph() ================================================ FILE: codes/python/06_graph/Graph-Adjacency-Matrix.py ================================================ class Graph: # 基本图类,采用邻接矩阵表示 # 图的初始化操作,ver_count 为顶点个数 def __init__(self, ver_count): self.ver_count = ver_count # 顶点个数 self.adj_matrix = [[None for _ in range(ver_count)] for _ in range(ver_count)] # 邻接矩阵 # 判断顶点 v 是否有效 def __valid(self, v): return 0 <= v <= self.ver_count # 图的创建操作,edges 为边信息 def creatGraph(self, edges=[]): for vi, vj, val in edges: self.add_edge(vi, vj, val) # 向图的邻接矩阵中添加边:vi - vj,权值为 val def add_edge(self, vi, vj, val): if not self.__valid(vi) or not self.__valid(vj): raise ValueError(str(vi) + ' or ' + str(vj) + " is not a valid vertex.") self.adj_matrix[vi][vj] = val # 获取 vi - vj 边的权值 def get_edge(self, vi, vj): if not self.__valid(vi) or not self.__valid(vj): raise ValueError(str(vi) + ' or ' + str(vj) + " is not a valid vertex.") return self.adj_matrix[vi][vj] # 根据邻接矩阵打印图的边 def printGraph(self): for vi in range(self.ver_count): for vj in range(self.ver_count): val = self.get_edge(vi, vj) if val: print(str(vi) + ' - ' + str(vj) + ' : ' + str(val)) graph = Graph(5) edges = [[1, 2, 5],[2, 1, 5],[1, 3, 30],[3, 1, 30],[2, 3, 14],[3, 2, 14],[2, 4, 26], [4, 2, 26]] graph.creatGraph(edges) print(graph.get_edge(3, 4)) graph.printGraph() ================================================ FILE: codes/python/06_graph/Graph-BFS.py ================================================ import collections class Solution: def bfs(self, graph, u): visited = set() # 使用 visited 标记访问过的节点 queue = collections.deque([]) # 使用 queue 存放临时节点 visited.add(u) # 将起始节点 u 标记为已访问 queue.append(u) # 将起始节点 u 加入队列中 while queue: # 队列不为空 u = queue.popleft() # 取出队头节点 u print(u) # 访问节点 u for v in graph[u]: # 遍历节点 u 的所有未访问邻接节点 v if v not in visited: # 节点 v 未被访问 visited.add(v) # 将节点 v 标记为已访问 queue.append(v) # 将节点 v 加入队列中 graph = { "0": ["1", "2"], "1": ["0", "2", "3"], "2": ["0", "1", "3", "4"], "3": ["1", "2", "4", "5"], "4": ["2", "3"], "5": ["3", "6"], "6": [] } # 基于队列实现的广度优先搜索 Solution().bfs(graph, "0") ================================================ FILE: codes/python/06_graph/Graph-Bellman-Ford.py ================================================ class Solution: def bellmanFord(self, graph, source): size = len(graph) dist = dict() for vi in graph: dist[vi] = float('inf') dist[source] = 0 for i in range(size - 1): for vi in graph: for vj in graph[vi]: if dist[vj] > graph[vi][vj] + dist[vi]: dist[vj] = graph[vi][vj] + dist[vi] for vi in graph: for vj in graph[vi]: if dist[vj] > dist[vi] + graph[vi][vj]: return None return dist graph = { 'a': {'b': -1, 'c': 4}, 'b': {'c': 2, 'd': 3, 'e': 2}, 'c': {}, 'd': {'b': 3, 'c': 5}, 'e': {'d': -3} } dist = Solution().bellmanFord(graph, 'a') print(dist) ================================================ FILE: codes/python/06_graph/Graph-DFS.py ================================================ class Solution: def dfs_recursive(self, graph, u, visited): print(u) # 访问节点 visited.add(u) # 节点 u 标记其已访问 for v in graph[u]: if v not in visited: # 节点 v 未访问过 # 深度优先搜索遍历节点 self.dfs_recursive(graph, v, visited) def dfs_stack(self, graph, u): print(u) # 访问节点 u visited, stack = set(), [] # 使用 visited 标记访问过的节点, 使用栈 stack 存放临时节点 stack.append([u, 0]) # 将起始节点 u 以及节点 u 的下一个邻接节点下标放入栈中,下一次将遍历 graph[u][0] visited.add(u) # 将起始节点 u 标记为已访问 while stack: u, i = stack.pop() # 取出节点 u,以及节点 u 下一个将要访问的邻接节点下标 i if i < len(graph[u]): v = graph[u][i] # 取出邻接节点 v stack.append([u, i + 1])# 将节点 u 以及节点 u 的下一个邻接节点下标 i + 1 放入栈中,下一次将遍历 graph[u][i + 1] if v not in visited: # 节点 v 未访问过 print(v) # 访问节点 v stack.append([v, 0])# 将节点 v 以及节点 v 的下一个邻接节点下标 0 放入栈中,下一次将遍历 graph[v][0] visited.add(v) # 将节点 v 标记为已访问 graph = { "A": ["B", "C"], "B": ["A", "C", "D"], "C": ["A", "B", "D", "E"], "D": ["B", "C", "E", "F"], "E": ["C", "D"], "F": ["D", "G"], "G": [] } # 基于递归实现的深度优先搜索 visited = set() Solution().dfs_recursive(graph, "A", visited) # 基于堆栈实现的深度优先搜索 Solution().dfs_stack(graph, "A") ================================================ FILE: codes/python/06_graph/Graph-Edgeset-Array.py ================================================ class EdgeNode: # 边信息类 def __init__(self, vi, vj, val): self.vi = vi # 边的起点 self.vj = vj # 边的终点 self.val = val # 边的权值 class Graph: # 基本图类,采用边集数组表示 def __init__(self): self.edges = [] # 边数组 # 图的创建操作,edges 为边信息 def creatGraph(self, edges=[]): for vi, vj, val in edges: self.add_edge(vi, vj, val) # 向图的边数组中添加边:vi - vj,权值为 val def add_edge(self, vi, vj, val): edge = EdgeNode(vi, vj, val) # 创建边节点 self.edges.append(edge) # 将边节点添加到边数组中 # 获取 vi - vj 边的权值 def get_edge(self, vi, vj): for edge in self.edges: if vi == edge.vi and vj == edge.vj: val = edge.val return val return None # 根据边数组打印图 def printGraph(self): for edge in self.edges: print(str(edge.vi) + ' - ' + str(edge.vj) + ' : ' + str(edge.val)) graph = Graph() edges = [[1, 2, 5],[1, 5, 6],[2, 4, 7],[4, 3, 9],[3, 1, 2],[5, 6, 8],[6, 4, 3]] graph.creatGraph(edges) print(graph.get_edge(3, 4)) graph.printGraph() ================================================ FILE: codes/python/06_graph/Graph-Hash-Table.py ================================================ class VertexNode: # 顶点信息类 def __init__(self, vi): self.vi = vi # 顶点 self.adj_edges = dict() # 顶点的邻接边 class Graph: def __init__(self): self.vertices = dict() # 顶点 # 图的创建操作,edges 为边信息 def creatGraph(self, edges=[]): for vi, vj, val in edges: self.add_edge(vi, vj, val) # 向图中添加节点 def add_vertex(self, vi): vertex = VertexNode(vi) self.vertices[vi] = vertex # 向图的邻接表中添加边:vi - vj,权值为 val def add_edge(self, vi, vj, val): if vi not in self.vertices: self.add_vertex(vi) if vj not in self.vertices: self.add_vertex(vj) self.vertices[vi].adj_edges[vj] = val # 获取 vi - vj 边的权值 def get_edge(self, vi, vj): if vi in self.vertices and vj in self.vertices[vi].adj_edges: return self.vertices[vi].adj_edges[vj] return None # 根据邻接表打印图的边 def printGraph(self): for vi in self.vertices: for vj in self.vertices[vi].adj_edges: print(str(vi) + ' - ' + str(vj) + ' : ' + str(self.vertices[vi].adj_edges[vj])) graph = Graph() edges = [[1, 2, 5],[1, 5, 6],[2, 4, 7],[4, 3, 9],[3, 1, 2],[5, 6, 8],[6, 4, 3]] graph.creatGraph(edges) print(graph.get_edge(3, 4)) graph.printGraph() ================================================ FILE: codes/python/06_graph/Graph-Kruskal.py ================================================ class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] self.count = n def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.count -= 1 def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def Kruskal(self, edges, size): union_find = UnionFind(size) edges.sort(key=lambda x: x[2]) res, cnt = 0, 1 for x, y, dist in edges: if union_find.is_connected(x, y): continue ans += dist cnt += 1 union_find.union(x, y) if cnt == size - 1: return ans return ans def minCostConnectPoints(self, points: List[List[int]]) -> int: size = len(points) edges = [] for i in range(size): xi, yi = points[i] for j in range(i + 1, size): xj, yj = points[j] dist = abs(xi - xj) + abs(yi - yj) edges.append([i, j, dist]) ans = Solution().Kruskal(edges, size) return ans ================================================ FILE: codes/python/06_graph/Graph-Linked-Forward-Star.py ================================================ class EdgeNode: # 边信息类 def __init__(self, vj, val): self.vj = vj # 边的终点 self.val = val # 边的权值 self.next = None # 下一条边 class Graph: def __init__(self, ver_count, edge_count): self.ver_count = ver_count # 顶点个数 self.edge_count = edge_count # 边个数 self.head = [-1 for _ in range(ver_count)] # 头节点数组 self.edges = [] # 边集数组 # 判断顶点 v 是否有效 def __valid(self, v): return 0 <= v <= self.ver_count # 图的创建操作,edges 为边信息 def creatGraph(self, edges=[]): for i in range(len(edges)): vi, vj, val = edges[i] self.add_edge(i, vi, vj, val) # 向图的边集数组中添加边:vi - vj,权值为 val def add_edge(self, index, vi, vj, val): if not self.__valid(vi) or not self.__valid(vj): raise ValueError(str(vi) + ' or ' + str(vj) + " is not a valid vertex.") edge = EdgeNode(vj, val) # 构造边节点 edge.next = self.head[vi] # 边节点的 next 指向原来首指针 self.edges.append(edge) # 边集数组添加该边 self.head[vi] = index # 首指针指向新加边所在边集数组的下标 # 获取 vi - vj 边的权值 def get_edge(self, vi, vj): if not self.__valid(vi) or not self.__valid(vj): raise ValueError(str(vi) + ' or ' + str(vj) + " is not a valid vertex.") index = self.head[vi] # 得到顶点 vi 相连的第一条边在边集数组的下标 while index != -1: # index == -1 时说明 vi 相连的边遍历完了 if vj == self.edges[index].vj: # 找到了 vi - vj 边 return self.edges[index].val # 返回 vi - vj 边的权值 index = self.edges[index].next # 取顶点 vi 相连的下一条边在边集数组的下标 return None # 没有找到 vi - vj 边 # 根据链式前向星打印图的边 def printGraph(self): for vi in range(self.ver_count): # 遍历顶点 vi index = self.head[vi] # 得到顶点 vi 相连的第一条边在边集数组的下标 while index != -1: # index == -1 时说明 vi 相连的边遍历完了 print(str(vi) + ' - ' + str(self.edges[index].vj) + ' : ' + str(self.edges[index].val)) index = self.edges[index].next # 取顶点 vi 相连的下一条边在边集数组的下标 graph = Graph(7, 7) edges = [[1, 2, 5],[1, 5, 6],[2, 4, 7],[4, 3, 9],[3, 1, 2],[5, 6, 8],[6, 4, 3]] graph.creatGraph(edges) print(graph.get_edge(4, 3)) print(graph.get_edge(4, 5)) graph.printGraph() ================================================ FILE: codes/python/06_graph/Graph-Prim.py ================================================ class Solution: # graph 为图的邻接矩阵,start 为起始顶点 def Prim(self, graph, start): size = len(graph) vis = set() dist = [float('inf') for _ in range(size)] ans = 0 # 最小生成树的边权和 dist[start] = 0 # 初始化起始顶点到起始顶点的边权值为 0 for i in range(1, size): # 初始化起始顶点到其他顶点的边权值 dist[i] = graph[start][i] vis.add(start) # 将 start 顶点标记为已访问 for _ in range(size - 1): min_dis = float('inf') min_dis_pos = -1 for i in range(size): if i not in vis and dist[i] < min_dis: min_dis = dist[i] min_dis_pos = i if min_dis_pos == -1: # 没有顶点可以加入 MST,图 G 不连通 return -1 ans += min_dis # 将顶点加入 MST,并将边权值加入到答案中 vis.add(min_dis_pos) for i in range(size): if i not in vis and dist[i] > graph[min_dis_pos][i]: dist[i] = graph[min_dis_pos][i] return ans points = [[0,0]] graph = dict() size = len(points) for i in range(size): x1, y1 = points[i] for j in range(size): x2, y2 = points[j] dist = abs(x2 - x1) + abs(y2 - y1) if i not in graph: graph[i] = dict() if j not in graph: graph[j] = dict() graph[i][j] = dist graph[j][i] = dist print(Solution().Prim(graph)) ================================================ FILE: codes/python/06_graph/Graph-Topological-Sorting-DFS.py ================================================ import collections class Solution: # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) def topologicalSortingDFS(self, graph: dict): visited = set() # 记录当前顶点是否被访问过 onStack = set() # 记录同一次深搜时,当前顶点是否被访问过 order = [] # 用于存储拓扑序列 hasCycle = False # 用于判断是否存在环 def dfs(u): nonlocal hasCycle if u in onStack: # 同一次深度优先搜索时,当前顶点被访问过,说明存在环 hasCycle = True if u in visited or hasCycle: # 当前节点被访问或者有环时直接返回 return visited.add(u) # 标记节点被访问 onStack.add(u) # 标记本次深搜时,当前顶点被访问 for v in graph[u]: # 遍历顶点 u 的邻接顶点 v dfs(v) # 递归访问节点 v order.append(u) # 后序遍历顺序访问节点 u onStack.remove(u) # 取消本次深搜时的 顶点访问标记 for u in graph: if u not in visited: dfs(u) # 递归遍历未访问节点 u if hasCycle: # 判断是否存在环 return [] # 存在环,无法构成拓扑序列 order.reverse() # 将后序遍历转为拓扑排序顺序 return order # 返回拓扑序列 def findOrder(self, n: int, edges): # 构建图 graph = dict() for i in range(n): graph[i] = [] for v, u in edges: graph[u].append(v) return self.topologicalSortingDFS(graph) print(Solution().findOrder(2, [[1,0]])) print(Solution().findOrder(4, [[1,0],[2,0],[3,1],[3,2]])) print(Solution().findOrder(1, [])) ================================================ FILE: codes/python/06_graph/Graph-Topological-Sorting-Kahn.py ================================================ import collections class Solution: # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) def topologicalSortingKahn(self, graph: dict): indegrees = {u: 0 for u in graph} # indegrees 用于记录所有节点入度 for u in graph: for v in graph[u]: indegrees[v] += 1 # 统计所有节点入度 # 将入度为 0 的顶点存入集合 S 中 S = collections.deque([u for u in indegrees if indegrees[u] == 0]) order = [] # order 用于存储拓扑序列 while S: u = S.pop() # 从集合中选择一个没有前驱的顶点 0 order.append(u) # 将其输出到拓扑序列 order 中 for v in graph[u]: # 遍历顶点 u 的邻接顶点 v indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 S.append(v) # 将其放入集合 S 中 if len(indegrees) != len(order): # 还有顶点未遍历(存在环),无法构成拓扑序列 return [] return order # 返回拓扑序列 def findOrder(self, n: int, edges): # 构建图 graph = dict() for i in range(n): graph[i] = [] for u, v in edges: graph[u].append(v) return self.topologicalSortingKahn(graph) print(Solution().findOrder(2, [[1,0]])) print(Solution().findOrder(4, [[1,0],[2,0],[3,1],[3,2]])) print(Solution().findOrder(1, [])) ================================================ FILE: codes/python/08_dynamic_programming/Digit-DP.py ================================================ class Solution: def digitDP(self, n: int) -> int: s = str(n) @cache # pos: 第 pos 个数位 # state: 之前选过的数字集合。 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 # isNum: 表示 pos 前面的数位是否填了数字。 # 如果为真,则当前位不可跳过;如果为假,则当前位可跳过。 def dfs(pos, state, isLimit, isNum): if pos == len(s): # isNum 为 True,则表示当前方案符合要求 return int(isNum) ans = 0 if not isNum: # 如果 isNumb 为 False,则可以跳过当前数位 ans = dfs(pos + 1, state, False, False) # 如果前一位没有填写数字,则最小可选择数字为 0,否则最少为 1(不能含有前导 0)。 minX = 0 if isNum else 1 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s[pos]) if isLimit else 9 # 枚举可选择的数字 for x in range(minX, maxX + 1): # x 不在选择的数字集合中,即之前没有选择过 x if (state >> x) & 1 == 0: ans += dfs(pos + 1, state | (1 << x), isLimit and x == maxX, True) return ans return dfs(0, 0, True, False) ================================================ FILE: codes/python/08_dynamic_programming/Pack-2DCostPack.py ================================================ class Solution: # 思路 1:动态规划 + 三维基本思路 def twoDCostPackMethod1(self, weight: [int], volume: [int], value: [int], W: int, V: int): size = len(weight) dp = [[[0 for _ in range(V + 1)] for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 组物品 for i in range(1, N + 1): # 枚举背包装载重量 for w in range(W + 1): # 枚举背包装载容量 for v in range(V + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1] or v < volume[i - 1]: # dp[i][w][v] 取「前 i - 1 件物品装入装载重量为 w、装载容量为 v 的背包中的最大价值」 dp[i][w][v] = dp[i - 1][w][v] else: # dp[i][w][v] 取所有 dp[w - weight[i - 1]][v - volume[i - 1]] + value[i - 1] 中最大值 dp[i][w][v] = max(dp[i - 1][w][v], dp[i - 1][w - weight[i - 1]][v - volume[i - 1]] + value[i - 1]) return dp[size][W][V] # 思路 2:动态规划 + 滚动数组优化 def twoDCostPackMethod2(self, weight: [int], volume: [int], value: [int], W: int, V: int): size = len(weight) dp = [[0 for _ in range(V + 1)] for _ in range(W + 1)] # 枚举前 i 组物品 for i in range(1, N + 1): # 逆序枚举背包装载重量 for w in range(W, weight[i - 1] - 1, -1): # 逆序枚举背包装载容量 for v in range(V, volume[i - 1] - 1, -1): # dp[w][v] 取所有 dp[w - weight[i - 1]][v - volume[i - 1]] + value[i - 1] 中最大值 dp[w][v] = max(dp[w][v], dp[w - weight[i - 1]][v - volume[i - 1]] + value[i - 1]) return dp[W][V] ================================================ FILE: codes/python/08_dynamic_programming/Pack-CompletePack.py ================================================ class Solution: # 思路 1:动态规划 + 二维基本思路 def completePackMethod1(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 枚举第 i - 1 种物品能取个数 for k in range(w // weight[i - 1] + 1): # dp[i][w] 取所有 dp[i - 1][w - k * weight[i - 1] + k * value[i - 1] 中最大值 dp[i][w] = max(dp[i][w], dp[i - 1][w - k * weight[i - 1]] + k * value[i - 1]) return dp[size][W] # 思路 2:动态规划 + 状态转移方程优化 def completePackMethod2(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1]: # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 dp[i][w] = dp[i - 1][w] else: # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」与「前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品所得的最大价值」两者中的最大值 dp[i][w] = max(dp[i - 1][w], dp[i][w - weight[i - 1]] + value[i - 1]) return dp[size][W] # 思路 3:动态规划 + 滚动数组优化 def completePackMethod3(self, weight: [int], value: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 正序枚举背包装载重量 for w in range(weight[i - 1], W + 1): # dp[w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」与「前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1]) return dp[W] ================================================ FILE: codes/python/08_dynamic_programming/Pack-GroupPack.py ================================================ class Solution: # 思路 1:动态规划 + 二维基本思路 def groupPackMethod1(self, group_count: [int], weight: [[int]], value: [[int]], W: int): size = len(group_count) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 组物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 枚举第 i - 1 组物品能取个数 dp[i][w] = dp[i - 1][w] for k in range(group_count[i - 1]): if w >= weight[i - 1][k]: # dp[i][w] 取所有 dp[i - 1][w - weight[i - 1][k]] + value[i - 1][k] 中最大值 dp[i][w] = max(dp[i][w], dp[i - 1][w - weight[i - 1][k]] + value[i - 1][k]) return dp[size][W] # 思路 2:动态规划 + 滚动数组优化 def groupPackMethod2(self, group_count: [int], weight: [[int]], value: [[int]], W: int): size = len(group_count) dp = [0 for _ in range(W + 1)] # 枚举前 i 组物品 for i in range(1, size + 1): # 逆序枚举背包装载重量 for w in range(W, -1, -1): # 枚举第 i - 1 组物品能取个数 for k in range(group_count[i - 1]): if w >= weight[i - 1][k]: # dp[w] 取所有 dp[w - weight[i - 1][k]] + value[i - 1][k] 中最大值 dp[w] = max(dp[w], dp[w - weight[i - 1][k]] + value[i - 1][k]) return dp[W] ================================================ FILE: codes/python/08_dynamic_programming/Pack-MixedPack.py ================================================ class Solution: def mixedPackMethod1(self, weight: [int], value: [int], count: [int], W: int): weight_new, value_new, count_new = [], [], [] # 二进制优化 for i in range(len(weight)): cnt = count[i] # 多重背包问题,转为 0-1 背包问题 if cnt > 0: k = 1 while k <= cnt: cnt -= k weight_new.append(weight[i] * k) value_new.append(value[i] * k) count_new.append(1) k *= 2 if cnt > 0: weight_new.append(weight[i] * cnt) value_new.append(value[i] * cnt) count_new.append(1) # 0-1 背包问题,直接添加 elif cnt == -1: weight_new.append(weight[i]) value_new.append(value[i]) count_new.append(1) # 完全背包问题,标记并添加 else: weight_new.append(weight[i]) value_new.append(value[i]) count_new.append(0) dp = [0 for _ in range(W + 1)] size = len(weight_new) # 枚举前 i 种物品 for i in range(1, size + 1): # 0-1 背包问题 if count_new[i - 1] == 1: # 逆序枚举背包装载重量(避免状态值错误) for w in range(W, weight_new[i - 1] - 1, -1): # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight_new[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight_new[i - 1]] + value_new[i - 1]) # 完全背包问题 else: # 正序枚举背包装载重量 for w in range(weight_new[i - 1], W + 1): # dp[w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」与「前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight_new[i - 1]] + value_new[i - 1]) return dp[W] ================================================ FILE: codes/python/08_dynamic_programming/Pack-MultiplePack.py ================================================ class Solution: # 思路 1:动态规划 + 二维基本思路 def multiplePackMethod1(self, weight: [int], value: [int], count: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 枚举第 i - 1 种物品能取个数 for k in range(min(count[i - 1], w // weight[i - 1]) + 1): # dp[i][w] 取所有 dp[i - 1][w - k * weight[i - 1] + k * value[i - 1] 中最大值 dp[i][w] = max(dp[i][w], dp[i - 1][w - k * weight[i - 1]] + k * value[i - 1]) return dp[size][W] # 思路 2:动态规划 + 滚动数组优化 def multiplePackMethod2(self, weight: [int], value: [int], count: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 逆序枚举背包装载重量(避免状态值错误) for w in range(W, weight[i - 1] - 1, -1): # 枚举第 i - 1 种物品能取个数 for k in range(min(count[i - 1], w // weight[i - 1]) + 1): # dp[w] 取所有 dp[w - k * weight[i - 1]] + k * value[i - 1] 中最大值 dp[w] = max(dp[w], dp[w - k * weight[i - 1]] + k * value[i - 1]) return dp[W] # 思路 3:动态规划 + 二进制优化 def multiplePackMethod3(self, weight: [int], value: [int], count: [int], W: int): weight_new, value_new = [], [] # 二进制优化 for i in range(len(weight)): cnt = count[i] k = 1 while k <= cnt: cnt -= k weight_new.append(weight[i] * k) value_new.append(value[i] * k) k *= 2 if cnt > 0: weight_new.append(weight[i] * cnt) value_new.append(value[i] * cnt) dp = [0 for _ in range(W + 1)] size = len(weight_new) # 枚举前 i 种物品 for i in range(1, size + 1): # 逆序枚举背包装载重量(避免状态值错误) for w in range(W, weight_new[i - 1] - 1, -1): # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight_new[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight_new[i - 1]] + value_new[i - 1]) return dp[W] ================================================ FILE: codes/python/08_dynamic_programming/Pack-ProblemVariants.py ================================================ class Solution: # 1. 求恰好装满背包的最大价值 # 0-1 背包问题 求恰好装满背包的最大价值 def zeroOnePackJustFillUp(self, weight: [int], value: [int], W: int): size = len(weight) dp = [float('-inf') for _ in range(W + 1)] dp[0] = 0 # 枚举前 i 种物品 for i in range(1, size + 1): # 逆序枚举背包装载重量(避免状态值错误) for w in range(W, weight[i - 1] - 1, -1): # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1]) if dp[W] == float('-inf'): return -1 return dp[W] # 完全背包问题 求恰好装满背包的最大价值 def completePackJustFillUp(self, weight: [int], value: [int], W: int): size = len(weight) dp = [float('-inf') for _ in range(W + 1)] dp[0] = 0 # 枚举前 i 种物品 for i in range(1, size + 1): # 正序枚举背包装载重量 for w in range(weight[i - 1], W + 1): # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1]) if dp[W] == float('-inf'): return -1 return dp[W] # 2. 求方案总数 # 0-1 背包问题 求方案总数 def zeroOnePackNumbers(self, weight: [int], value: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] dp[0] = 1 # 枚举前 i 种物品 for i in range(1, size + 1): # 逆序枚举背包装载重量 for w in range(W, weight[i - 1] - 1, -1): # dp[w] = 前 i - 1 件物品装入载重为 w 的背包中的方案数 + 前 i 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 件物品的方案数 dp[w] = dp[w] + dp[w - weight[i - 1]] return dp[W] # 完全背包问题求方案总数 def completePackNumbers(self, weight: [int], value: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] dp[0] = 1 # 枚举前 i 种物品 for i in range(1, size + 1): # 正序枚举背包装载重量 for w in range(weight[i - 1], W + 1): # dp[w] = 前 i - 1 种物品装入载重为 w 的背包中的方案数 + 前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品的方案数 dp[w] = dp[w] + dp[w - weight[i - 1]] return dp[W] # 3. 求最优方案数 # 0-1 背包问题 求最优方案数 思路 1 def zeroOnePackMaxProfitNumbers1(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] op = [[1 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1]: # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 dp[i][w] = dp[i - 1][w] op[i][w] = op[i - 1][w] else: # 选择第 i - 1 件物品获得价值更高 if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] # 在之前方案基础上添加了第 i - 1 件物品,因此方案数量不变 op[i][w] = op[i - 1][w - weight[i - 1]] # 两种方式获得价格相等 elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w] # 方案数 = 不使用第 i - 1 件物品的方案数 + 使用第 i - 1 件物品的方案数 op[i][w] = op[i - 1][w] + op[i - 1][w - weight[i - 1]] # 不选择第 i - 1 件物品获得价值最高 else: dp[i][w] = dp[i - 1][w] # 不选择第 i - 1 件物品,与之前方案数相等 op[i][w] = op[i - 1][w] return op[size][W] # 0-1 背包问题求最优方案数 思路 2 def zeroOnePackMaxProfitNumbers2(self, weight: [int], value: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] op = [1 for _ in range(W + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W, weight[i - 1] - 1, -1): # 选择第 i - 1 件物品获得价值更高 if dp[w] < dp[w - weight[i - 1]] + value[i - 1]: dp[w] = dp[w - weight[i - 1]] + value[i - 1] # 在之前方案基础上添加了第 i - 1 件物品,因此方案数量不变 op[w] = op[w - weight[i - 1]] # 两种方式获得价格相等 elif dp[w] == dp[w - weight[i - 1]] + value[i - 1]: # 方案数 = 不使用第 i - 1 件物品的方案数 + 使用第 i - 1 件物品的方案数 op[w] = op[w] + op[w - weight[i - 1]] return op[W] # 完全背包问题求最优方案数 思路 1 def completePackMaxProfitNumbers1(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] op = [[1 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1]: # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 dp[i][w] = dp[i - 1][w] op[i][w] = op[i - 1][w] else: # 选择第 i - 1 件物品获得价值更高 if dp[i - 1][w] < dp[i][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i][w - weight[i - 1]] + value[i - 1] # 在之前方案基础上添加了 1 件第 i - 1 种物品,因此方案数量不变 op[i][w] = op[i][w - weight[i - 1]] # 两种方式获得价格相等 elif dp[i - 1][w] == dp[i][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w] # 方案数 = 不使用第 i - 1 种物品的方案数 + 使用 1 件第 i - 1 种物品的方案数 op[i][w] = op[i - 1][w] + op[i][w - weight[i - 1]] # 不选择第 i - 1 件物品获得价值最高 else: dp[i][w] = dp[i - 1][w] # 不选择第 i - 1 种物品,与之前方案数相等 op[i][w] = op[i - 1][w] return dp[size][W] # 完全背包问题求最优方案数 思路 2 def completePackMaxProfitNumbers2(self, weight: [int], value: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] op = [1 for _ in range(W + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(weight[i - 1], W + 1): # 选择第 i - 1 件物品获得价值更高 if dp[w] < dp[w - weight[i - 1]] + value[i - 1]: dp[w] = dp[w - weight[i - 1]] + value[i - 1] # 在之前方案基础上添加了 1 件第 i - 1 种物品,因此方案数量不变 op[w] = op[w - weight[i - 1]] # 两种方式获得价格相等 elif dp[w] == dp[w - weight[i - 1]] + value[i - 1]: # 方案数 = 不使用第 i - 1 种物品的方案数 + 使用 1 件第 i - 1 种物品的方案数 op[w] = op[w] + op[w - weight[i - 1]] return dp[size][W] # 4. 求具体方案 # 0-1 背包问题求具体方案 def zeroOnePackPrintPath(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] path = [[False for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1]: # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 dp[i][w] = dp[i - 1][w] path[i][w] = False else: # 选择第 i - 1 件物品获得价值更高 if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] # 取状态转移式第二项:在之前方案基础上添加了第 i - 1 件物品 path[i][w] = True # 两种方式获得价格相等 elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w] # 取状态转移式第二项:尽量使用第 i - 1 件物品 path[i][w] = True # 不选择第 i - 1 件物品获得价值最高 else: dp[i][w] = dp[i - 1][w] # 取状态转移式第一项:不选择第 i - 1 件物品 path[i][w] = False res = [] i, w = size, W while i >= 1 and w >= 0: if path[i][w]: res.append(str(i - 1)) w -= weight[i - 1] i -= 1 return " ".join(res[::-1]) # 0-1 背包问题求具体方案,要求最小序输出 def zeroOnePackPrintPathMinOrder(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] path = [[False for _ in range(W + 1)] for _ in range(size + 1)] weight.reverse() value.reverse() # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1]: # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 dp[i][w] = dp[i - 1][w] path[i][w] = False else: # 选择第 i - 1 件物品获得价值更高 if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] # 取状态转移式第二项:在之前方案基础上添加了第 i - 1 件物品 path[i][w] = True # 两种方式获得价格相等 elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w] # 取状态转移式第二项:尽量使用第 i - 1 件物品 path[i][w] = True # 不选择第 i - 1 件物品获得价值最高 else: dp[i][w] = dp[i - 1][w] # 取状态转移式第一项:不选择第 i - 1 件物品 path[i][w] = False res = [] i, w = size, W while i >= 1 and w >= 0: if path[i][w]: res.append(str(size - i)) w -= weight[i - 1] i -= 1 return " ".join(res) ================================================ FILE: codes/python/08_dynamic_programming/Pack-ZeroOnePack.py ================================================ class Solution: # 思路 1:动态规划 + 二维基本思路 def zeroOnePackMethod1(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1]: # dp[i][w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」 dp[i][w] = dp[i - 1][w] else: # dp[i][w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weight[i - 1]] + value[i - 1]) return dp[size][W] # 思路 2:动态规划 + 滚动数组优化 def zeroOnePackMethod2(self, weight: [int], value: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 逆序枚举背包装载重量(避免状态值错误) for w in range(W, weight[i - 1] - 1, -1): # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1]) return dp[W] ================================================ FILE: docs/00_preface/00_01_preface.md ================================================ ## 1. 创作历程 ### 1.1 创作起因 写一本通俗易懂的算法书一直是我的心愿,这个想法已经在我心中埋藏了七年之久。至今我仍记得大学时立下的 flag:**要把所学的算法知识系统整理,编写成书,署上自己的名字,并分享给所有热爱算法的朋友们。** 然而,毕业后由于工作繁忙,这个计划被一再搁置。直到 2021 年 3 月,在朋友的建议下,我们组建了一个算法学习群,并制定了为期三个月(2021 年 4 月 ~ 6 月)的刷题打卡计划,规定连续两天不刷题就会被移出群。 最初有 39 人参与,不过最终坚持下来的只有 13 人。虽然计划未能全部完成,但这三个月的坚持让我重新找回了学习算法的乐趣,也养成了刷题的习惯。工作之余,我总会习惯性地打开 LeetCode 刷题、写题解,收获满满。 后来,我们又重新组建了算法交流群。群里的伙伴越来越多,大家每天刷题、写题解、讨论思路、交流心得,甚至还一起参加周赛、双周赛,赛后还会分享做题心得。 ### 1.2 输出是最好的学习方法 在刷题和撰写题解的过程中,我逐步整理了算法与数据结构的基础知识,最终汇聚成了这个开源项目。随后,我又学习搭建了电子书网站,方便大家随时在线阅读。 在这个过程中,我深刻体会到一个重要秘诀:**「输出」是最有效的学习方式**,这正是费曼学习法的真实写照。 只有真正理解了某个概念,才能用简明易懂的语言表达出来,让他人也能明白。如果自己尚未吃透,就很难讲清楚。为此,我广泛阅读了各类算法书籍和高质量博客,不断思考和总结,力求把复杂的知识用更简单通俗的方式表达出来。 刷题过程中,我与许多朋友和群里的小伙伴们积极探讨算法知识,大家互相交流、分享见解,也会帮助我发现不足并提出改进建议。这些宝贵反馈如同专业老师批改作业,不仅不断推动内容的完善,也让我对算法有了更加深入的理解。 就这样,从 2021 年 7 月到 2022 年 7 月,经过一年的坚持,我在 LeetCode 上完成了 1000 多道题目,然后系统总结了算法与数据结构知识,最终完成了这本 **「算法通关手册」**。 ## 2. 为什么要学习算法和数据结构 ### 2.1 算法是程序员的底层能力 **算法和数据结构** 是计算机程序设计的核心理论基础,但在实际开发中,许多程序员往往忽视了它们的重要性。日常工作中,我们更多依赖成熟的框架和封装良好的接口来完成 CRUD 操作,极少需要自己从零实现底层的数据结构和算法。 此外,编程语言和开发框架的迭代速度极快。以前端为例,React 还没完全掌握,Vue 又流行起来了;刚研究完 Vue 2.0,Vue 3.0 又已发布。新技术层出不穷,学习都来不及,很难专门抽出时间去深入研究算法。 不可否认,语言、技术和框架固然重要,但它们背后的计算机算法与理论才是根本。无论技术如何更迭,始终不变的是底层的算法和理论基础,比如:**数据结构**、**算法**、**编译原理**、**计算机网络**、**计算机体系结构** 等。掌握这些核心理论,才能灵活应对各种技术变化,深入理解系统设计原理和框架思想,快速上手新技术,并有效提升工作效率。 **学习数据结构与算法的关键,在于领悟其思想和精髓,掌握解决实际问题的方法。** ### 2.2 算法是技术面试的必考内容 在互联网行业的技术面试中,**算法与数据结构** 几乎是所有公司必考的核心内容。许多知名互联网公司倾向于以 LeetCode 等平台上的算法题作为考察标准,要求面试者不仅要分析问题、阐述解题思路,还需要评估算法的时间复杂度和空间复杂度。通过这些题目的考察,面试官能够有效判断候选人解决实际问题的能力和思维深度。 LeetCode 等平台的算法题已成为行业通用标准,许多公司会直接选用或稍作改编作为面试题。系统地练习这些题目,不仅能提升解决实际问题的能力,也能让你在面试中遇到类似问题时更加自信、从容。 学习算法应当循序渐进,从基础数据结构入手,逐步掌握常见的算法思想。每当学习一个新概念,都要通过实际题目加以巩固,长期积累下来,才能建立起完善的算法知识体系。 本书「算法通关手册」旨在帮助读者系统学习算法知识,既包含基础理论讲解,也有大量实战题目分析。通过理论与实践相结合,读者能够真正掌握算法精髓,提升解决问题的能力。无论是备战面试,还是提升编程能力,这本书都将为你带来切实的帮助。 ================================================ FILE: docs/00_preface/00_02_data_structures_algorithms.md ================================================ ![程序=算法+数据结构](https://qcdn.itcharge.cn/images/202109092112373.png) > 数据结构是程序的骨架,而算法则是程序的灵魂。 **《算法 + 数据结构 = 程序》** 是 Pascal 语言之父 [Niklaus Emil Wirth](https://zh.wikipedia.org/wiki/尼克劳斯·维尔特) 所著的经典著作,其书名也成为计算机科学领域广为流传的名言。这句话深刻揭示了算法与数据结构在程序设计中的密切关系。 在正式学习之前,我们首先要搞清楚:什么是算法?什么是数据结构?为什么要学习它们? 简而言之,**「算法」是解决问题的方法或步骤**。如果把问题看作一个函数,算法就是将输入转化为输出的过程。**「数据结构」则是数据在计算机中的组织方式及其相关操作**。**「程序」就是算法和数据结构的具体实现**。 如果把「程序设计」比作烹饪,「数据结构」就像是食材和调料,「算法」则是不同的烹饪方法或菜谱。不同的食材、调料和烹饪方式可以组合出千变万化的菜肴。同样的原料,不同的人做出来,味道也会大不相同。 为什么要学习算法和数据结构呢? 还是以做菜为例。做菜讲究「色香味俱全」,而程序设计则追求为问题选择最合适的「数据结构」,并采用更高效、占用资源更少的「算法」。 学习算法和数据结构,能够帮助我们在编程时从时间复杂度和空间复杂度等角度优化解决方案,提升逻辑思维能力,写出高质量的代码,从而增强编程能力,获得更好的职业发展。 当然,正如掌握了食材和烹饪方法并不代表就能做出美味佳肴,学会了算法和数据结构也不意味着就能写出优秀的程序。这需要不断实践、思考和持续学习,才能真正成长为一名优秀的 ~~厨师~~(程序员)。 ## 1. 数据结构 > **数据结构(Data Structure)**:具有特定结构特征的数据元素的集合。 简而言之,「数据结构」 就是数据的组织结构,用来组织、存储数据。 进一步来说,数据结构关注的是数据的逻辑结构、物理结构及其相互关系,并针对这些结构定义相应的操作和算法,确保经过操作后数据依然保持原有的结构特性。 数据结构的核心作用在于提升计算机资源的利用效率。例如,操作系统想要查找硬盘中「Microsoft Word」的存储位置,如果采用全盘扫描,则效率极低;而通过「B+ 树」索引,可以迅速定位到 `Microsoft Word`,进而快速获取其文件信息和磁盘位置。 学习数据结构,能够帮助我们理解和掌握数据在计算机中的组织与存储方式,从而为高效编程打下坚实基础。 --- 通常,我们可以从 **「逻辑结构」** 和 **「物理结构」** 两个维度对数据结构进行分类。 ### 1.1 数据的逻辑结构 > **逻辑结构(Logical Structure)**:指数据元素之间的相互关系。 根据数据元素之间的关系,数据的逻辑结构通常分为以下四类: 1. 集合结构 2. 线性结构 3. 树形结构 4. 图形结构 #### 1.1.1 集合结构 > **集合结构**:数据元素属于同一个集合,彼此之间没有其他关系。 集合结构中的数据元素是无序且互不相同的,每个元素在集合中只出现一次。这种结构与数学中的「集合」概念非常相似。 ![集合结构](https://qcdn.itcharge.cn/images/20240509150647.png) #### 1.1.2 线性结构 > **线性结构**:数据元素之间存在严格的「一对一」关系。 在线性结构中,除第一个元素和最后一个元素外,每个数据元素都仅与前后各一个元素相邻。常见的线性结构有:数组、链表,以及基于它们实现的栈、队列等。此外,哈希表在底层实现时也常依赖数组或链表等线性结构。 ![线性结构](https://qcdn.itcharge.cn/images/20240509150709.png) #### 1.1.3 树形结构 > **树形结构**:数据元素之间存在「一对多」的层次关系。 树形结构最典型的例子是二叉树,其基本形式包括根节点、左子树和右子树,而每个子树又可以递归地包含自己的子树。除了二叉树之外,树形结构还包括多叉树、字典树等多种类型,广泛用于表达具有层级关系的数据。 ![树形结构](https://qcdn.itcharge.cn/images/20240509150724.png) #### 1.1.4 图形结构 > **图形结构**:数据元素之间存在「多对多」的关系。 图形结构是一种比树形结构更为复杂的非线性结构,常用于描述对象之间任意的关联关系。在图结构中,数据元素被称为 **「顶点」**(或 **「结点」**),顶点之间通过 **「边」**(可以是直线或曲线)相连,形成复杂的网络。 与树形结构不同,图形结构允许任意两个顶点之间建立连接,顶点之间的邻接关系没有限制。常见的图结构类型包括:无向图、有向图、连通图等。 ![图形结构](https://qcdn.itcharge.cn/images/20240509150831.png) ### 1.2 数据的物理结构 > **物理结构(Physical Structure)**:指数据的逻辑结构在计算机中的具体存储方式。 在计算机中,常见的物理存储结构主要有两种:**顺序存储结构** 和 **链式存储结构**,它们被广泛应用于各类数据结构的实现。 #### 1.2.1 顺序存储结构 > **顺序存储结构(Sequential Storage Structure)**:指将所有数据元素依次存放在一块地址连续的存储空间中,元素之间的逻辑关系通过它们在物理存储上的相对位置来体现。 ![顺序存储结构](https://qcdn.itcharge.cn/images/20240509150846.png) 在顺序存储结构中,逻辑上相邻的数据元素在物理地址上也紧密相邻。 顺序存储结构的优点在于:结构简单、易于理解,并且能够高效利用存储空间。其缺点主要包括:必须预先分配一块连续的存储空间,灵活性较差;在插入、删除等需要移动大量元素的操作时,时间效率较低。 #### 2. 链式存储结构 > **链式存储结构(Linked Storage Structure)**:指将数据元素存放在内存中的任意存储单元,这些单元可以是连续的,也可以是不连续的。 ![链式存储结构](https://qcdn.itcharge.cn/images/20240509150902.png) 在链式存储结构中,逻辑上相邻的数据元素在物理地址上不要求相邻,实际存储位置是随机的。通常,每个数据元素及其相关信息被组合成一个「链结点」。每个链结点除了存放数据本身外,还包含一个「指针(或引用)」,用于指向下一个逻辑上相邻的链结点。也就是说,数据元素之间的逻辑关系是通过指针来连接和体现的。 链式存储结构的主要优点在于:无需预先分配一整块连续的存储空间,能够根据需要动态申请和释放内存,避免空间浪费;在插入、删除等操作时,通常只需修改指针,效率较高。其缺点是:每个链结点除了存储数据外,还需额外存储指针信息,因此整体空间开销相较于顺序存储结构更大。 ## 2. 算法 > **算法(Algorithm)**:解决特定问题求解步骤的准确而完整的描述,在计算机中表现为一系列指令的集合,算法代表着用系统的方法描述解决问题的策略机制。 简而言之,**「算法」** 就是解决问题的具体方法和步骤。 进一步来说,算法是一系列有序的运算步骤,能够为某一类计算问题提供通用的解决方案。对于任意合法输入,算法都能按照既定步骤逐步执行,最终得到正确的输出。算法本身与具体的编程语言无关,可以用 **自然语言、编程语言(如 Python、C、C++、Java 等)**,也可以用 **伪代码或流程图** 等多种方式进行描述。 下面通过几个例子来直观理解什么是算法。 - 示例 1: > **问题描述**: > > - 如何从上海前往北京? > > **解决方法**: > > 1. 选择乘坐飞机,速度最快但费用最高。 > 2. 选择长途汽车,费用最低但耗时最长。 > 3. 选择高铁或火车,时间和费用都较为适中。 - 示例 2: > **问题描述**: > > - 如何计算 $1 + 2 + 3 + \dots + 100$ 的和? > > **解决方法**: > > 1. 依次用计算器从 $1$ 加到 $100$,最终得到 $5050$。 > 2. 利用高斯求和公式:**和 = (首项 + 末项) × 项数 ÷ 2**,直接计算得 $\frac{(1+100) \times 100}{2} = 5050$。 - 示例 3: > **问题描述**: > > - 如何将一个包含 $n$ 个整数的数组按升序排列? > > **解决方法**: > > 1. 使用冒泡排序对数组进行升序排序。 > 2. 也可以选择插入排序、归并排序、快速排序等其他排序算法实现升序排列。 上述三个示例中的解决方法都属于算法。从上海到北京的出行方案是一种算法,对 $1$ 到 $100$ 求和的方法是一种算法,对数组排序的方法同样是一种算法。可以看出,对于同一个问题,往往存在多种不同的算法可供选择。 ### 2.1 算法的基本特性 算法本质上是一组有序的运算步骤,用于解决特定的问题。除此之外,**算法** 还必须具备以下五个基本特性: 1. **输入**:算法需要接收外部提供的信息作为处理对象,这些信息称为输入。一个算法可以有零个、一个或多个输入。例如,示例 $1$ 的输入是出发地和目的地(如上海、北京),示例 $3$ 的输入是由 $n$ 个整数构成的数组,而示例 $2$ 针对的是固定问题,可以视为没有输入。 2. **输出**:算法的执行结果必须有明确的输出,即至少有一个输出结果。比如,示例 $1$ 的输出是最终选择的交通方式,示例 $2$ 的输出是求和的结果,示例 $3$ 的输出是排好序的数组。 3. **有穷性**:算法必须在有限的步骤内终止,并且能够在合理的时间内完成。如果算法无法在有限时间内结束,就不能称为有效的算法。例如,如果五一假期从上海到北京旅游,三天都没决定交通方式,计划就无法实现,这样的「算法」显然不合理。 4. **确定性**:算法中的每一步操作都必须有明确、唯一的含义,不能存在歧义。也就是说,任何人在相同输入下执行算法,得到的中间过程和最终结果都应一致。 5. **可行性**:算法的每一步都必须是可执行的,即在现有条件下能够通过有限次数的操作实现,并且可以被计算机程序实现并运行,最终得到正确的结果。 ### 2.2 算法追求的目标 研究算法的核心目的,是让我们以更高效的方式解决问题。对于同一个问题,往往存在多种算法可选,而不同算法的「代价」也各不相同。一般来说,优秀的算法应当重点追求以下两个目标: 1. **更少的运行时间(更低的时间复杂度)** 2. **更小的内存占用(更低的空间复杂度)** 举例来说,假设计算机执行一条指令需要 $1$ 纳秒。如果某算法需 $100$ 纳秒,另一算法只需 $3$ 纳秒,在不考虑内存消耗的前提下,显然后者更优。再比如,如果某算法只需 $3$ 字节内存,另一算法需 $100$ 字节,在不考虑运行时间的情况下,前者更优。 实际应用中,算法设计往往需要在运行时间和空间占用之间权衡。理想情况下,算法既快又省空间,但现实中常常需要根据具体需求做出取舍。例如,当程序运行速度要求较高时,可以适当增加空间消耗以换取更快的执行速度;反之,如果设备内存有限且对速度要求不高,则可以选择更节省空间的算法,即使牺牲一些运行时间。 除了运行时间和空间占用,优秀的算法还应具备以下基本特性: 1. **正确性**:算法能准确满足问题需求,程序运行无语法错误,能通过典型测试,达到预期目标。 2. **可读性**:算法结构清晰,命名规范,注释恰当,便于理解、维护和后续修改。 3. **健壮性**:算法能合理应对非法输入或异常操作,具备良好的容错能力。 这三点是算法的基本要求,所有算法都必须满足。而我们评价一个算法是否优秀,通常最看重的还是其运行时间和空间占用两个方面。 ## 3. 总结 ### 3.1 数据结构 数据结构通常分为 **逻辑结构** 和 **物理结构** 两大类。 - **逻辑结构**:描述数据元素之间的关系,主要包括:**集合结构**、**线性结构**、**树形结构** 和 **图形结构**。 - **物理结构**:指数据在计算机中的实际存储方式,主要有:**顺序存储结构** 和 **链式存储结构**。 逻辑结构强调数据元素之间的相互关系,而物理结构则关注这些关系在计算机内的具体实现。例如,线性表中的「栈」在逻辑上属于线性结构,元素之间是一对一的关系(除首尾元素外,每个元素有唯一前驱和后继)。在物理实现上,栈可以采用顺序存储(即 **顺序栈**,元素在内存中连续存放),也可以采用链式存储(即 **链式栈**,元素在内存中不一定连续,通过指针连接)。 ### 3.2 算法 **算法** 是解决特定问题的有序操作步骤的集合。 一个算法应具备以下五个基本特性:**输入**、**输出**、**有穷性**、**确定性**、**可行性**。 优秀的算法通常追求以下目标:**正确性**、**可读性**、**健壮性**、**更低的时间复杂度(运行时间更短)** 和 **更低的空间复杂度(占用内存更小)**。 ## 参考资料 - 【文章】[数据结构与算法 · 看云](https://www.kancloud.cn/zxliu/algorithm/2088786) - 【书籍】大话数据结构——程杰 著 - 【书籍】趣学算法——陈小玉 著 - 【书籍】计算机程序设计艺术(第一卷)基本算法(第三版)——苏运霖 译 - 【书籍】算法艺术与信息学竞赛——刘汝佳、黄亮 著 ================================================ FILE: docs/00_preface/00_03_algorithm_complexity.md ================================================ ## 1. 算法复杂度简介 > **算法复杂度(Algorithm complexity)**:用于衡量算法在输入规模为 $n$ 时所需的时间和空间资源。 这里的 **问题规模 $n$**,指的是算法输入的数据量。不同类型的算法,$n$ 的具体含义也有所不同: - 排序算法中,$n$ 表示待排序元素的数量; - 查找算法中,$n$ 表示查找范围的大小(如数组长度、字符串长度等); - 图论算法中,$n$ 可以指节点数或边数,具体视问题而定; - 二进制相关算法中,$n$ 通常指二进制的位数。 一般来说,输入规模越大,算法的计算成本也会随之增加;而当输入规模相近时,计算成本也会比较接近。 「算法分析」的核心目标是优化算法,使其 **运行时间更短**、**内存占用更小**。分析算法时,主要从运行时间和空间使用两个方面入手。常见的分析方法有两种: - **事后统计**:将不同算法分别实现并运行,通过实际测量运行时间和内存占用来比较优劣。 - **预先估算**:在算法设计阶段,根据算法的步骤,理论上估算其运行时间和空间消耗,并进行比较。 实际应用中,我们更倾向于采用预先估算的方法,因为事后统计不仅工作量大,而且同一算法在不同编程语言和硬件环境下的表现差异较大。 采用预先估算时,我们通常不考虑编程语言、计算机运行速度等外部因素,关注的是算法随问题规模增长时的资源消耗趋势。 ## 2. 时间复杂度 ### 2.1 时间复杂度简介 > **时间复杂度(Time Complexity)**:用于衡量算法在输入规模为 $n$ 时的运行时间,通常记作 $T(n)$。 时间复杂度的本质,是统计算法中 **基本操作** 的执行次数。也就是说,时间复杂度与算法中基本操作的数量成正比。 - **基本操作**:指的是在常数时间内可以完成的语句,其执行时间与操作数的大小无关。 举例来说,两个小整数相加,所需时间不会因为数字位数的不同而变化,因此属于基本操作。但如果操作数非常大,运算时间会随位数增加而增长,这时整体加法就不再是基本操作,应将每一位的加法视为基本操作。 下面通过一个具体例子来演示时间复杂度的计算方法。 ```python def find_max(arr): max_val = arr[0] # 1 次操作 for i in range(len(arr)): # n 次循环 if arr[i] > max_val: # n 次比较 max_val = arr[i] # 最多 n 次赋值 return max_val # 1 次操作 ``` 在上述例子中,基本操作总共执行了 $1 + n + n + n + 1 = 3 \times n + 2$ 次,因此可以用 $f(n) = 3 \times n + 2$ 表示其操作次数。 时间复杂度分析如下: - 当 $n$ 足够大时,$3n$ 是主要影响项,常数 $2$ 可以忽略不计。 - 由于我们关注的是随规模增长的趋势,常数系数 $3$ 也可以省略。 - 因此,该算法的时间复杂度为 $O(n)$。这里的 $O$ 表示渐近符号,强调 $f(n)$ 与 $n$ 成正比。 所谓「算法执行时间的增长趋势」,实际上就是用类似 $O$ 这样的渐近符号,来简洁地描述算法随输入规模变化时的资源消耗情况。 ### 2.2 渐近符号 时间复杂度通常记作 $T(n) = O(f(n))$,称为 **渐近时间复杂度(Asymptotic Time Complexity)**,用于描述当问题规模 $n$ 趋近于无穷大时,算法运行时间的增长趋势。我们常用渐近符号(如 $O$、$\Omega$、$\Theta$ 等)来表达这种增长关系。渐近时间复杂度只关注主导项,忽略常数和低阶项,从而简洁地反映算法的本质效率。 > **渐近符号(Asymptotic Symbol)**:一类数学符号,用于描述函数(如算法运行时间或空间)随输入规模增长时的变化速度。在算法分析中,常用的渐近符号有大 $O$(上界)、大 $\Omega$(下界)、大 $\Theta$(紧确界),它们帮助我们以统一的方式比较不同算法的效率。 ![渐近符号关系图](https://qcdn.itcharge.cn/images/202109092356694.png) #### 2.2.1 渐近上界符号 $O$ > **渐近上界符号 $O$**:用于描述算法运行时间的上限,通常反映算法在最坏情况下的性能。 **数学定义**:设 $T(n)$ 和 $f(n)$ 为两个函数,如果存在正常数 $c$ 和 $n_0$,使得对所有 $n \geq n_0$,都有 $T(n) \leq c \cdot f(n)$,则称 $T(n) = O(f(n))$。 **直观理解**:$T(n) = O(f(n))$ 表示「算法的运行时间至多为 $f(n)$ 的某个常数倍」,即不会比 $f(n)$ 增长得更快。 > **示例**: > > - 如果 $T(n) = 3 \times n^2 + 2 \times n + 1$,则 $T(n) = O(n^2)$。 > - 如果 $T(n) = 2 \times n + 5$,则 $T(n) = O(n)$。 > - 如果 $T(n) = 100$,则 $T(n) = O(1)$。 #### 2.2.2 渐近下界符号 $\Omega$ > **渐近下界符号 $\Omega$**:用于描述算法运行时间的下界,通常反映算法在最优情况下的性能。 **数学定义**:设 $T(n)$ 和 $f(n)$ 为两个函数,如果存在正常数 $c > 0$ 和 $n_0$,使得对所有 $n \geq n_0$,都有 $T(n) \geq c \cdot f(n)$,则称 $T(n) = \Omega(f(n))$。 **直观理解**:$T(n) = \Omega(f(n))$ 表示「算法的运行时间至少不会低于 $f(n)$ 的某个常数倍」,即增长速度不慢于 $f(n)$。 > **示例**: > > - 如果 $T(n) = 3 \times n^2 + 2 \times n + 1$,则 $T(n) = \Omega(n^2)$。 > - 如果 $T(n) = 2 \times n + 5$,则 $T(n) = \Omega(n)$。 > - 如果 $T(n) = n^3$,则 $T(n) = \Omega(n^2)$。 #### 2.2.3 渐近紧确界符号 $\Theta$ > **渐近紧确界符号 $\Theta$**:用于描述算法运行时间的精确数量级,即算法在最好和最坏情况下的增长速度都与 $f(n)$ 保持一致。 **数学定义**:设 $T(n)$ 和 $f(n)$ 为两个函数,如果存在正常数 $c_1, c_2 > 0$ 及 $n_0$,使得对所有 $n \geq n_0$,都有 $c_1 \cdot f(n) \leq T(n) \leq c_2 \cdot f(n)$,则称 $T(n) = \Theta(f(n))$。 **直观理解**:$T(n) = \Theta(f(n))$ 表示「算法运行时间与 $f(n)$ 同阶」,即上下界都为 $f(n)$ 的常数倍。 > **示例**: > > - 如果 $T(n) = 3 \times n^2 + 2 \times n + 1$,则 $T(n) = \Theta(n^2)$。 > - 如果 $T(n) = 2 \times n + 5$,则 $T(n) = \Theta(n)$。 > - 如果 $T(n) = n \log n + n$,则 $T(n) = \Theta(n \log n)$。 ### 2.3 时间复杂度计算 渐近符号用于描述函数的上界、下界,以及算法执行时间随问题规模增长的趋势。 在分析时间复杂度时,我们通常使用 $O$ 符号来表示算法的上界,因为实际应用中更关注算法在最坏情况下的表现。 那么,如何具体计算时间复杂度呢? 一般来说,计算时间复杂度可以分为以下几个步骤: 1. **确定基本操作**:找出算法中执行次数最多的语句,通常是最内层循环的核心操作。 2. **估算执行次数**:只关注基本操作的最高阶项,忽略常数系数和低阶项。 3. **用大 O 符号表示**:将上一步得到的数量级用 $O$ 符号表示出来。 在计算时间复杂度时,还需注意以下两条常用原则: > **加法原则**:多个代码块顺序执行时,总时间复杂度等于其中最大的那一个。 即如果 $T_1(n) = O(f_1(n))$,$T_2(n) = O(f_2(n))$,$T(n) = T_1(n) + T_2(n)$,则 $T(n) = O(\max(f_1(n), f_2(n)))$。 > **乘法原则**:循环嵌套时,总时间复杂度等于各层复杂度的乘积。 即如果 $T_1(n) = O(f_1(n))$,$T_2(n) = O(f_2(n))$,$T(n) = T_1(n) \times T_2(n)$,则 $T(n) = O(f_1(n) \times f_2(n))$。 下面通过具体实例来说明各种常见时间复杂度的计算方法。 #### 2.3.1 常数时间 $O(1)$ 没有循环和递归的算法,时间复杂度通常为 $O(1)$。 ```python def get_first_element(arr): return arr[0] # 直接返回第一个元素 def add_two_numbers(a, b): return a + b # 简单的加法运算 ``` 上述代码中,每个函数都只执行常数次操作,时间复杂度为 $O(1)$。 #### 2.3.2 线性时间 $O(n)$ 单层循环遍历 $n$ 个元素的算法,时间复杂度为 $O(n)$。 ```python def find_max(arr): max_val = arr[0] for num in arr: # 遍历数组中的每个元素 if num > max_val: max_val = num return max_val def sum_array(arr): total = 0 for num in arr: # 遍历数组中的每个元素 total += num return total ``` 上述代码中,每个函数都只遍历数组一次,时间复杂度为 $O(n)$。 #### 2.3.3 平方时间 $O(n^2)$ 两层嵌套循环,每层执行 $n$ 次操作的算法,时间复杂度为 $O(n^2)$。 ```python def bubble_sort(arr): n = len(arr) for i in range(n): # 外层循环 for j in range(n - 1): # 内层循环 if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] def find_all_pairs(arr): pairs = [] for i in range(len(arr)): # 外层循环 for j in range(len(arr)): # 内层循环 pairs.append((arr[i], arr[j])) return pairs ``` 上述代码中,每个函数都包含两层嵌套循环,总操作次数为 $n^2$,时间复杂度为 $O(n^2)$。 #### 2.3.4 对数时间 $O(\log n)$ 每次操作将问题规模缩小一半的算法,如「二分查找」和「分治算法」,时间复杂度为 $O(\log n)$。 ```python def binary_search(arr, target): left, right = 0, len(arr) - 1 while left <= right: mid = (left + right) // 2 if arr[mid] == target: return mid elif arr[mid] < target: left = mid + 1 else: right = mid - 1 return -1 def power_of_two(n): count = 0 while n > 1: n = n // 2 # 每次除以2 count += 1 return count ``` 上述代码中,每次将问题规模缩小一半,循环次数为 $\log_2 n$,时间复杂度为 $O(\log n)$。 #### 2.3.5 线性对数时间 $O(n \log n)$ 线性对数一般出现在排序算法中,例如「快速排序」、「归并排序」、「堆排序」等,时间复杂度为 $O(n \log n)$。 ```python def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left = merge_sort(arr[:mid]) # 递归处理左半部分 right = merge_sort(arr[mid:]) # 递归处理右半部分 return merge(left, right) # 合并两个有序数组 def merge(left, right): result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return result ``` 上述代码中,`merge_sort` 函数采用了分治思想,每次递归将数组一分为二,递归深度为 $\log_2 n$ 层,每层处理 $n$ 个元素,整体的时间复杂度为 $O(n \log n)$。 #### 2.3.6 指数时间 $O(2^n)$ 指数时间复杂度 $O(2^n)$ 通常出现在每一步都存在两种选择、递归分支成倍增长的算法中,如递归斐波那契、子集枚举等,时间复杂度为 $O(2^n)$。 ```python def fibonacci_recursive(n): if n <= 1: return n return fibonacci_recursive(n-1) + fibonacci_recursive(n-2) def generate_subsets(arr): def backtrack(start, current): result.append(current[:]) for i in range(start, len(arr)): current.append(arr[i]) backtrack(i + 1, current) current.pop() result = [] backtrack(0, []) return result ``` 上述代码中,`fibonacci_recursive` 函数每次递归都会分裂成两个子问题,递归树的节点总数为 $2^n$,因此时间复杂度为 $O(2^n)$。`generate_subsets` 函数通过回溯法枚举所有子集,每个元素有选或不选两种选择,子集总数为 $2^n$,所以整体时间复杂度也是 $O(2^n)$。 #### 2.3.7 阶乘时间 $O(n!)$ 阶乘时间 $O(n!)$ 通常出现在需要枚举所有排列或组合的算法中,如全排列、旅行商问题暴力解法等。随着输入规模 $n$ 的增加,算法的执行次数以阶乘级别增长,计算量极大,几乎 无法处理较大的输入规模。 ```python def generate_permutations(arr): def backtrack(start): if start == len(arr): result.append(arr[:]) return for i in range(start, len(arr)): arr[start], arr[i] = arr[i], arr[start] # 交换 backtrack(start + 1) # 递归 arr[start], arr[i] = arr[i], arr[start] # 恢复 result = [] backtrack(0) return result ``` 上述代码中,`generate_permutations` 函数通过回溯法枚举所有排列。每一层递归会将当前位置与后续每个元素交换,递归深度为 $n$ 层。第 1 层有 $n$ 种选择,第 2 层有 $n - 1$ 种选择,依此类推,总共 $n!$ 种排列,因此时间复杂度为 $O(n!)$。 #### 2.3.8 时间复杂度对比 常见时间复杂度从小到大排序:$O(1)$ < $O(\log n)$ < $O(n)$ < $O(n \log n)$ < $O(n^2)$ < $O(n^3)$ < $O(2^n)$ < $O(n!)$ < $O(n^n)$ | 时间复杂度 | 输入规模 $n=10$ | $n=100$ | $n=1000$ | 实际应用 | |------------|---------------|-------|--------|----------| | $O(1)$ | $1$ | $1$ | $1$ | 数组访问、哈希表查找 | | $O(\log n)$| $3$ | $7$ | $10$ | 二分查找、平衡树操作 | | $O(n)$ | $10$ | $100$ | $1000$ | 线性搜索、数组遍历 | | $O(n \log n)$ | $33$ | $664$ | $9966$ | 快速排序、归并排序 | | $O(n^2)$ | $100$ | $10000$ | $1000000$ | 冒泡排序、选择排序 | | $O(2^n)$ | $1024$ | $1.3 \times 10^{30}$ | $1.1 \times 10^{301}$ | 递归斐波那契 | | $O(n!)$ | $3628800$ | $9.3 \times 10^{157}$ | $4.0 \times 10^{2567}$ | 全排列 | ### 2.4 最佳、最坏、平均时间复杂度 由于同一算法在不同输入下的表现可能差异很大,我们通常从三个角度分析时间复杂度: - **最佳时间复杂度**:最理想输入下的时间复杂度 - **最坏时间复杂度**:最差输入下的时间复杂度 - **平均时间复杂度**:随机输入下的期望时间复杂度 **示例**:在数组中查找目标值 ```python def find(nums, val): for i in range(len(nums)): if nums[i] == val: return i return -1 ``` - **最佳情况**:目标值在数组开头,时间复杂度 $O(1)$ - **最坏情况**:目标值不存在,需要遍历整个数组,时间复杂度 $O(n)$ - **平均情况**:假设目标值等概率出现在任意位置,平均时间复杂度 $O(n)$ > **实际应用**:通常使用 **最坏时间复杂度** 作为算法性能的衡量标准,因为它能保证算法在任何输入下的性能上限。只有在不同情况下的时间复杂度存在量级差异时,才需要区分三种情况。 ## 3. 空间复杂度 ### 3.1 空间复杂度简介 > **空间复杂度(Space Complexity)**:在问题的输入规模为 $n$ 的条件下,算法所占用的空间大小,可以记作为 $S(n)$。一般将 **算法的辅助空间** 作为衡量空间复杂度的标准。 空间复杂度的渐近符号表示方法与时间复杂度相同,可以表示为 $S(n) = O(f(n))$,表示算法空间占用随问题规模 $n$ 的增长趋势。 相对于算法的时间复杂度计算来说,算法的空间复杂度更容易计算。空间复杂度的计算主要包括局部变量占用的存储空间和递归栈空间两个部分。 ### 3.2 空间复杂度计算 空间复杂度的计算主要考虑算法运行过程中额外占用的空间,包括局部变量和递归栈空间。 #### 3.2.1 常数空间 $O(1)$ ```python def algorithm(n): a = 1 b = 2 res = a * b + n return res ``` 上述代码中,只使用了固定数量的变量,因此空间复杂度为 $O(1)$。 #### 3.2.2 线性空间 $O(n)$ ```python def algorithm(n): if n <= 0: return 1 return n * algorithm(n - 1) ``` 上述代码中,递归深度为 $n$,需要 $O(n)$ 的栈空间。 #### 3.2.3 常见空间复杂度 常见空间复杂度从小到大排序:$O(1)$ < $O(\log n)$ < $O(n)$ < $O(n^2)$ < $O(2^n)$ ## 4. 总结 **「算法复杂度」** 包括 **「时间复杂度」** 和 **「空间复杂度」**,用于衡量算法在输入规模 $n$ 增大时的资源消耗情况。通常使用**渐近符号**(如 $O$ 符号)来描述算法复杂度的增长趋势。 常见的时间复杂度有:$O(1)$、$O(\log n)$、$O(n)$、$O(n \log n)$、$O(n^2)$、$O(n^3)$、$O(2^n)$、$O(n!)$。 常见的空间复杂度有:$O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$。 ## 参考资料 - 【书籍】数据结构(C++ 语言版)- 邓俊辉 著 - 【书籍】算法导论 第三版(中文版)- 殷建平等 译 - 【书籍】算法艺术与信息学竞赛 - 刘汝佳、黄亮 著 - 【书籍】数据结构(C 语言版)- 严蔚敏 著 - 【书籍】趣学算法 - 陈小玉 著 - 【文章】[复杂度分析 - 数据结构与算法之美 王争](https://time.geekbang.org/column/intro/126) - 【文章】[算法复杂度(时间复杂度+空间复杂度)](https://www.biancheng.net/algorithm/complexity.html) - 【文章】[算法基础 - 复杂度 - OI Wiki](https://oi-wiki.org/basic/complexity/) - 【文章】[图解算法数据结构 - 算法复杂度 - LeetBook - 力扣](https://leetcode.cn/leetbook/read/illustration-of-algorithm/r84gmi/) ================================================ FILE: docs/00_preface/00_04_leetcode_guide.md ================================================ ## 1. LeetCode 是什么 **「LeetCode」** 是一个在线编程评测平台,主要包含算法、数据库、Shell、多线程等题目,其中以算法题目为主。LeetCode 上有 $3000+$ 道编程问题,支持 $16+$ 种编程语言,还有一个活跃的社区用于技术交流。我们可以通过解决 LeetCode 题库中的问题来练习编程技能,以及提高算法能力。 许多知名互联网公司在面试时会考察 LeetCode 题目,要求面试者分析问题、编写代码,并分析算法的时间复杂度和空间复杂度。通过 LeetCode 刷题,充分准备算法知识,对获得好的工作机会很有帮助。 ## 2. LeetCode 新手入门 ### 2.1 LeetCode 注册 1. 打开 LeetCode 中文主页,链接:[力扣(LeetCode)官网](https://leetcode.cn/)。 2. 输入手机号,获取验证码。 3. 输入验证码之后,点击「登录 / 注册」,就注册好了。 ![LeetCode 注册页面](https://qcdn.itcharge.cn/images/20210901155409.png) ### 2.2 LeetCode 题库 「[题库](https://leetcode.cn/problemset/algorithms/)」是 LeetCode 上最直接的练习入口,在这里可以根据题目的标签、难度、状态进行刷题。也可以按照随机一题开始刷题。 ![LeetCode 题库页面](https://qcdn.itcharge.cn/images/20210901155423.png) #### 2.2.1 题目标签 LeetCode 的题目涉及了许多算法和数据结构。有贪心,搜索,动态规划,链表,二叉树,哈希表等等,可以通过选择对应标签进行专项刷题,同时也可以看到对应专题的完成度情况。 ![LeetCode 题目标签](https://qcdn.itcharge.cn/images/20210901155435.png) #### 2.2.2 题目列表 LeetCode 提供了题目的搜索过滤功能。可以筛选相关题单、不同难易程度、题目完成状态、不同标签的题目。还可以根据题目编号、题解数目、通过率、难度、出现频率等进行排序。 ![LeetCode 题目列表](https://qcdn.itcharge.cn/images/20210901155450.png) #### 2.2.3 当前进度 当前进度提供了一个直观的进度展示。在这里可以看到自己的练习概况。进度会自动展现当前的做题情况。也可以点击「[进度设置](https://leetcode.cn/session/)」创建新的进度,在这里还可以修改、删除相关的进度。 ![LeetCode 当前进度](https://qcdn.itcharge.cn/images/20210901155500.png) #### 2.2.4 题目详情 从题目列表点击进入,就可以看到这道题目的内容描述和代码编辑器。在这里还可以查看相关的题解和自己的提交记录。 ![LeetCode 题目详情](https://qcdn.itcharge.cn/images/20210901155529.png) ### 2.3 LeetCode 刷题语言 面试时考察的是算法基本功,对于语言的选择没有限制。建议使用熟悉的语言或语法简洁的语言刷题。 相对于 Python 而言,C、C++ 语法比较复杂,在做题的时候除了要思考思路,还得考虑语法,不太利于刷题。Python 等语言更简洁,能让你专注于算法思路本身,提高刷题效率。当然,算法竞赛选手通常使用 C++,已经成为传统了。 > 人生苦短,我用 Python。 ### 2.4 LeetCode 刷题流程 在「2.2.1 题目标签」中我们介绍了题目的相关情况。 ![LeetCode 题目详情](https://qcdn.itcharge.cn/images/20210901155529.png) 可以看到左侧区域为题目内容描述区域,还可以看到题目的内容描述和一些示例数据。而右侧是代码编辑区域,代码编辑区域里边默认显示了待实现的方法。 我们需要在代码编辑器中根据方法给定的参数实现对应的算法,并返回题目要求的结果。然后还要经过「执行代码」测试结果,点击「提交」后,显示执行结果为「**通过**」时,才算完成一道题目。 ![LeetCode 提交记录](https://qcdn.itcharge.cn/images/20210901155545.png) 总结一下我们的刷题流程为: 1. 在 LeetCode 题库中选择一道自己想要解决的题目。 2. 查看题目左侧的题目描述,理解题目要求。 3. 思考解决思路,并在右侧代码编辑区域实现对应的方法,并返回题目要求的结果。 4. 如果实在想不出解决思路,可以查看题目相关的题解,努力理解他人的解题思路和代码。 5. 点击「执行代码」按钮测试结果。 - 如果输出结果与预期结果不符,则回到第 3 步重新思考解决思路,并改写代码。 6. 如果输出结果与预期符合,则点击「提交」按钮。 - 如果执行结果显示「编译出错」、「解答错误」、「执行出错」、「超出时间限制」、「超出内存限制」等情况,则需要回到第 3 步重新思考解决思路,或者思考特殊数据,并改写代码。 7. 如果执行结果显示「通过」,恭喜你通过了这道题目。 接下来我们将通过「[2235. 两整数相加 - 力扣(LeetCode)](https://leetcode.cn/problems/add-two-integers/)」这道题目来讲解如何在 LeetCode 上刷题。 ### 2.5 LeetCode 第一题 #### 2.5.1 题目链接 - [2235. 两整数相加 - 力扣(LeetCode)](https://leetcode.cn/problems/add-two-integers/) #### 2.5.2 题目大意 **描述**:给定两个整数 $num1$ 和 $num2$。 **要求**:返回这两个整数的和。 **说明**: - $-100 \le num1, num2 \le 100$。 **示例**: - 示例 1: ```python 输入:num1 = 12, num2 = 5 输出:17 解释:num1 是 12,num2 是 5,它们的和是 12 + 5 = 17,因此返回 17。 ``` - 示例 2: ```python 输入:num1 = -10, num2 = 4 输出:-6 解释:num1 + num2 = -6,因此返回 -6。 ``` #### 2.5.3 解题思路 ##### 思路 1:直接计算 1. 直接计算整数 $num1$ 与 $num2$ 的和,返回 $num1 + num2$ 即可。 ##### 思路 1:代码 ```python class Solution: def sum(self, num1: int, num2: int) -> int: return num1 + num2 ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。 - **空间复杂度**:$O(1)$。 理解了上面这道题的题意,就可以试着自己编写代码,并尝试提交通过。如果提交结果显示「通过」,那么恭喜你完成了 LeetCode 上的第一题。虽然只是一道题,但这意味着刷题计划的开始!希望你能坚持下去,得到应有的收获。 ## 3. LeetCode 刷题攻略 ### 3.1 LeetCode 前期准备 如果你是算法和数据结构的新手,建议在刷 LeetCode 之前先学习一下基础知识,这样刷题时会更顺利。 基础知识包括: - **数据结构**:数组、字符串、链表、树(如二叉树)等。 - **算法**:分治、贪心、回溯、动态规划等。 这个阶段推荐看一些经典的算法基础书来进行学习。这里推荐一下我看过的感觉不错的算法书: - 【书籍】[算法(第 4 版)- 谢路云 译](https://book.douban.com/subject/19952400/) - 【书籍】[大话数据结构 - 程杰 著](https://book.douban.com/subject/6424904/) - 【书籍】[趣学算法 - 陈小玉 著](https://book.douban.com/subject/27109832/) - 【书籍】[算法图解 - 袁国忠 译](https://book.douban.com/subject/26979890/) - 【书籍】[算法竞赛入门经典(第 2 版) - 刘汝佳 著](https://book.douban.com/subject/25902102/) - 【书籍】[数据结构与算法分析 - 冯舜玺 译](https://book.douban.com/subject/1139426/) - 【书籍】[算法导论(原书第 3 版) - 殷建平 / 徐云 / 王刚 / 刘晓光 / 苏明 / 邹恒明 / 王宏志 译](https://book.douban.com/subject/20432061/) 当然,也可以直接看我写的「算法通关手册」,欢迎指正和提出建议,万分感谢。 - 「算法通关手册」GitHub 地址:[https://github.com/ITCharge/AlgoNote](https://github.com/ITCharge/AlgoNote) - 「算法通关手册」电子书网站地址:[https://algo.itcharge.cn](https://algo.itcharge.cn) ### 3.2 LeetCode 刷题顺序 讲个笑话,从前有个人以为 LeetCode 的题目是按照难易程度排序的,所以他从 [1. 两数之和](https://leetcode.cn/problems/two-sum) 开始刷题,结果他卡在了 [4. 寻找两个正序数组的中位数](https://leetcode.cn/problems/median-of-two-sorted-arrays) 这道困难题上。 LeetCode 的题目序号并不是按难易程度排序的,不建议按序号顺序刷题。新手建议从「简单」难度开始,熟练后再刷中等难度题目,最后考虑面试题或难题。 其实 LeetCode 官方网站上就有整理好的题目不错的刷题清单。链接为:[https://leetcode.cn/leetbook/](https://leetcode.cn/leetbook/)。可以先刷这里边的题目卡片。我这里也做了一个整理。 推荐刷题顺序和目录如下: [1. 初级算法](https://leetcode.cn/leetbook/detail/top-interview-questions-easy/)、[2. 数组类算法](https://leetcode.cn/leetbook/detail/all-about-array/)、[3. 数组和字符串](https://leetcode.cn/leetbook/detail/array-and-string/)、[4. 链表类算法](https://leetcode.cn/leetbook/detail/linked-list/)、[5. 哈希表](https://leetcode.cn/leetbook/detail/hash-table/)、[6. 队列 & 栈](https://leetcode.cn/leetbook/detail/queue-stack/)、[7. 递归](https://leetcode.cn/leetbook/detail/recursion/)、[8. 二分查找](https://leetcode.cn/leetbook/detail/binary-search/)、[9. 二叉树](https://leetcode.cn/leetbook/detail/data-structure-binary-tree/)、[10. 中级算法](https://leetcode.cn/leetbook/detail/top-interview-questions-medium/)、[11. 高级算法](https://leetcode.cn/leetbook/detail/top-interview-questions-hard/)、[12. 算法面试题汇总](https://leetcode.cn/leetbook/detail/top-interview-questions/)。 当然还可以通过官方推出的「[学习计划 - 力扣](https://leetcode.cn/study-plan/)」按计划每天刷题。 或者直接按照我整理的分类刷题列表进行刷题: - LeetCode 分类刷题列表:[点击打开「LeetCode 分类刷题列表」](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md) 正在准备面试、没有太多时间刷题的小伙伴,可以按照我总结的「LeetCode 面试最常考 100 题」、「LeetCode 面试最常考 200 题」进行刷题。 > **说明**:「LeetCode 面试最常考 100 题」、「LeetCode 面试最常考 200 题」是笔者根据「[CodeTop 企业题库](https://codetop.cc/home)」按频度从高到低进行筛选,并且去除了一部分 LeetCode 上没有的题目和重复题目后得到的题目清单。 - [LeetCode 面试最常考 100 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_07_interview_100_list.md) - [LeetCode 面试最常考 200 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_08_interview_200_list.md) ### 3.3 LeetCode 刷题技巧 下面分享一下我在刷题过程中用到的刷题技巧。简单来说,可以分为 $5$ 条: > 1. 五分钟思考法 > 2. 重复刷题 > 3. 按专题分类刷题 > 4. 写解题报告 > 5. 坚持刷题 #### 3.3.1 五分钟思考法 > **五分钟思考法**:如果一道题如果 $5$ 分钟之内有思路,就立即动手写代码解题。如果 $5$ 分钟之后还没有思路,就直接去看题解。然后根据题解的思路,自己去实现代码。如果发现自己看了题解也无法实现代码,就认真阅读题解的代码,并理解代码的逻辑。 其实,刷算法题的过程和背英语单词很相似。 刚开始学英语时,先从最基础的字母学起,不必纠结每个字母的由来。接着学习简单的单词,也不用深究单词的含义,先记住再说。掌握了基础词汇后,再逐步学习词组、短句、长句,最后阅读文章。 背单词不是看一遍就能记住,而是需要不断重复练习、反复记忆来加深印象。 刷算法题也是如此。零基础时,不要纠结为什么自己想不出解法,或者为什么没想到更高效的方法。遇到没有思路的题目时,直接去看题解区的高赞解答,尽快积累经验,帮助自己快速入门。 #### 3.3.2 重复刷题 > **重复刷题**:遇见不会的题,多刷几遍,不断加深理解。 刷算法题经常是做完一遍后,隔一段时间就忘记了,看到之前做过的题目也未必能立刻想起解题思路。所以,刷题并不是做完一遍就结束了,还需要定期回顾和复习。 此外,一道题往往有多种解法和不同的优化思路。第一次做时可能只想到一种方法,等到第二遍、第三遍时,可能会发现新的解法或更优的实现。 因此,建议对不会的题目多刷几遍,通过反复练习不断加深理解和记忆。 #### 3.3.3 按专题分类刷题 > **按专题分类刷题**:按照不同专题分类刷题,既可以巩固刚学完的算法知识,还可以提高刷题效率。 按专题分类刷题有两个好处: 1. **巩固知识**:刚学完某个算法时,可能对里边的相关知识理解的不够透彻,或者说可能会遗漏一些关键知识点,这时候可以通过刷对应题目的方式来帮助我们巩固刚学完的算法知识。 2. **提高效率**:同类题目所用到的算法知识其实是相同或者相似的,同一种解题思路可以运用到多道题目中。通过不断求解同一类算法专题下的题目,可以大大的提升我们的刷题速度。 #### 3.3.4 写解题报告 > **写解题报告**:如果能够用简洁清晰的语言让别人听懂这道题目的思路,那就说明你真正理解了这道题的解法。 写解题报告是很有用的技巧。如果你能用通俗易懂的语言写出解题思路,说明你真正理解了这道题。这相当于「费曼学习法」,也能减少重复刷题的时间。 #### 3.3.5 坚持刷题 > **坚持刷题**:算法刷题没有捷径,只有不断的刷题、总结,再刷题,再总结。 千万不要相信「3天精通数据结构」这类速成学习宣传。学习算法需要不断积累,反复理解算法思想,并通过刷题来应用知识。 根据我的个人经验,能坚持每天刷题并掌握基础算法知识的人总是少数。但如果你选择了学习算法,希望在达成目标前能坚持下去,通过「刻意练习」把刷题变成兴趣。 ## 4. 总结 LeetCode 是一个在线编程练习平台,主要用于提升算法和编程能力。新手可以从简单题目开始,逐步学习数据结构和算法。 刷题技巧包括:五分钟思考法、重复刷题、按专题分类刷题、写解题报告、坚持刷题。最重要的是坚持,通过不断练习和总结来掌握算法知识。 ## 练习题目 - [2235. 两整数相加](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/add-two-integers.md) - [1929. 数组串联](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/concatenation-of-array.md) ## 参考资料 - 【文章】[What is LeetCode? - Quora](https://www.quora.com/What-is-Leetcode) - 【文章】[LeetCode 帮助中心 - 力扣(LeetCode)](https://support.leetcode-cn.com/hc/) - 【回答】[刷 leetcode 使用 python 还是 c++? - 知乎](https://www.zhihu.com/question/319448129) - 【回答】[刷完 LeetCode 是什么水平?能拿到什么水平的 offer? - 知乎](https://www.zhihu.com/question/32019460) ================================================ FILE: docs/00_preface/00_05_solutions_list.md ================================================ # LeetCode 题解(已完成 1303 道) ### 第 1 ~ 99 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0001. 两数之和](https://leetcode.cn/problems/two-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/two-sum.md) | 数组、哈希表 | 简单 | | [0002. 两数相加](https://leetcode.cn/problems/add-two-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/add-two-numbers.md) | 递归、链表、数学 | 中等 | | [0003. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-substring-without-repeating-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0004. 寻找两个正序数组的中位数](https://leetcode.cn/problems/median-of-two-sorted-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/median-of-two-sorted-arrays.md) | 数组、二分查找、分治 | 困难 | | [0005. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-palindromic-substring.md) | 双指针、字符串、动态规划 | 中等 | | [0006. Z 字形变换](https://leetcode.cn/problems/zigzag-conversion/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/zigzag-conversion.md) | 字符串 | 中等 | | [0007. 整数反转](https://leetcode.cn/problems/reverse-integer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-integer.md) | 数学 | 中等 | | [0008. 字符串转换整数 (atoi)](https://leetcode.cn/problems/string-to-integer-atoi/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/string-to-integer-atoi.md) | 字符串 | 中等 | | [0009. 回文数](https://leetcode.cn/problems/palindrome-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/palindrome-number.md) | 数学 | 简单 | | [0010. 正则表达式匹配](https://leetcode.cn/problems/regular-expression-matching/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/regular-expression-matching.md) | 递归、字符串、动态规划 | 困难 | | [0011. 盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/container-with-most-water.md) | 贪心、数组、双指针 | 中等 | | [0012. 整数转罗马数字](https://leetcode.cn/problems/integer-to-roman/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/integer-to-roman.md) | 哈希表、数学、字符串 | 中等 | | [0013. 罗马数字转整数](https://leetcode.cn/problems/roman-to-integer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/roman-to-integer.md) | 哈希表、数学、字符串 | 简单 | | [0014. 最长公共前缀](https://leetcode.cn/problems/longest-common-prefix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-common-prefix.md) | 字典树、数组、字符串 | 简单 | | [0015. 三数之和](https://leetcode.cn/problems/3sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum.md) | 数组、双指针、排序 | 中等 | | [0016. 最接近的三数之和](https://leetcode.cn/problems/3sum-closest/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum-closest.md) | 数组、双指针、排序 | 中等 | | [0017. 电话号码的字母组合](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/letter-combinations-of-a-phone-number.md) | 哈希表、字符串、回溯 | 中等 | | [0018. 四数之和](https://leetcode.cn/problems/4sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/4sum.md) | 数组、双指针、排序 | 中等 | | [0019. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-nth-node-from-end-of-list.md) | 链表、双指针 | 中等 | | [0020. 有效的括号](https://leetcode.cn/problems/valid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-parentheses.md) | 栈、字符串 | 简单 | | [0021. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-two-sorted-lists.md) | 递归、链表 | 简单 | | [0022. 括号生成](https://leetcode.cn/problems/generate-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/generate-parentheses.md) | 字符串、动态规划、回溯 | 中等 | | [0023. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) | 链表、分治、堆(优先队列)、归并排序 | 困难 | | [0024. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/swap-nodes-in-pairs.md) | 递归、链表 | 中等 | | [0025. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-nodes-in-k-group.md) | 递归、链表 | 困难 | | [0026. 删除有序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-array.md) | 数组、双指针 | 简单 | | [0027. 移除元素](https://leetcode.cn/problems/remove-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-element.md) | 数组、双指针 | 简单 | | [0028. 找出字符串中第一个匹配项的下标](https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-the-index-of-the-first-occurrence-in-a-string.md) | 双指针、字符串、字符串匹配 | 简单 | | [0029. 两数相除](https://leetcode.cn/problems/divide-two-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/divide-two-integers.md) | 位运算、数学 | 中等 | | [0030. 串联所有单词的子串](https://leetcode.cn/problems/substring-with-concatenation-of-all-words/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/substring-with-concatenation-of-all-words.md) | 哈希表、字符串、滑动窗口 | 困难 | | [0031. 下一个排列](https://leetcode.cn/problems/next-permutation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/next-permutation.md) | 数组、双指针 | 中等 | | [0032. 最长有效括号](https://leetcode.cn/problems/longest-valid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-valid-parentheses.md) | 栈、字符串、动态规划 | 困难 | | [0033. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-in-rotated-sorted-array.md) | 数组、二分查找 | 中等 | | [0034. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-first-and-last-position-of-element-in-sorted-array.md) | 数组、二分查找 | 中等 | | [0035. 搜索插入位置](https://leetcode.cn/problems/search-insert-position/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-insert-position.md) | 数组、二分查找 | 简单 | | [0036. 有效的数独](https://leetcode.cn/problems/valid-sudoku/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-sudoku.md) | 数组、哈希表、矩阵 | 中等 | | [0037. 解数独](https://leetcode.cn/problems/sudoku-solver/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sudoku-solver.md) | 数组、哈希表、回溯、矩阵 | 困难 | | [0038. 外观数列](https://leetcode.cn/problems/count-and-say/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/count-and-say.md) | 字符串 | 中等 | | [0039. 组合总和](https://leetcode.cn/problems/combination-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combination-sum.md) | 数组、回溯 | 中等 | | [0040. 组合总和 II](https://leetcode.cn/problems/combination-sum-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combination-sum-ii.md) | 数组、回溯 | 中等 | | [0041. 缺失的第一个正数](https://leetcode.cn/problems/first-missing-positive/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/first-missing-positive.md) | 数组、哈希表 | 困难 | | [0042. 接雨水](https://leetcode.cn/problems/trapping-rain-water/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/trapping-rain-water.md) | 栈、数组、双指针、动态规划、单调栈 | 困难 | | [0043. 字符串相乘](https://leetcode.cn/problems/multiply-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/multiply-strings.md) | 数学、字符串、模拟 | 中等 | | [0044. 通配符匹配](https://leetcode.cn/problems/wildcard-matching/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/wildcard-matching.md) | 贪心、递归、字符串、动态规划 | 困难 | | [0045. 跳跃游戏 II](https://leetcode.cn/problems/jump-game-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/jump-game-ii.md) | 贪心、数组、动态规划 | 中等 | | [0046. 全排列](https://leetcode.cn/problems/permutations/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutations.md) | 数组、回溯 | 中等 | | [0047. 全排列 II](https://leetcode.cn/problems/permutations-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutations-ii.md) | 数组、回溯、排序 | 中等 | | [0048. 旋转图像](https://leetcode.cn/problems/rotate-image/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/rotate-image.md) | 数组、数学、矩阵 | 中等 | | [0049. 字母异位词分组](https://leetcode.cn/problems/group-anagrams/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/group-anagrams.md) | 数组、哈希表、字符串、排序 | 中等 | | [0050. Pow(x, n)](https://leetcode.cn/problems/powx-n/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/powx-n.md) | 递归、数学 | 中等 | | [0051. N 皇后](https://leetcode.cn/problems/n-queens/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/n-queens.md) | 数组、回溯 | 困难 | | [0052. N 皇后 II](https://leetcode.cn/problems/n-queens-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/n-queens-ii.md) | 回溯 | 困难 | | [0053. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximum-subarray.md) | 数组、分治、动态规划 | 中等 | | [0054. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/spiral-matrix.md) | 数组、矩阵、模拟 | 中等 | | [0055. 跳跃游戏](https://leetcode.cn/problems/jump-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/jump-game.md) | 贪心、数组、动态规划 | 中等 | | [0056. 合并区间](https://leetcode.cn/problems/merge-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-intervals.md) | 数组、排序 | 中等 | | [0057. 插入区间](https://leetcode.cn/problems/insert-interval/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/insert-interval.md) | 数组 | 中等 | | [0058. 最后一个单词的长度](https://leetcode.cn/problems/length-of-last-word/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/length-of-last-word.md) | 字符串 | 简单 | | [0059. 螺旋矩阵 II](https://leetcode.cn/problems/spiral-matrix-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/spiral-matrix-ii.md) | 数组、矩阵、模拟 | 中等 | | [0060. 排列序列](https://leetcode.cn/problems/permutation-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutation-sequence.md) | 递归、数学 | 困难 | | [0061. 旋转链表](https://leetcode.cn/problems/rotate-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/rotate-list.md) | 链表、双指针 | 中等 | | [0062. 不同路径](https://leetcode.cn/problems/unique-paths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths.md) | 数学、动态规划、组合数学 | 中等 | | [0063. 不同路径 II](https://leetcode.cn/problems/unique-paths-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths-ii.md) | 数组、动态规划、矩阵 | 中等 | | [0064. 最小路径和](https://leetcode.cn/problems/minimum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/minimum-path-sum.md) | 数组、动态规划、矩阵 | 中等 | | [0065. 有效数字](https://leetcode.cn/problems/valid-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-number.md) | 字符串 | 困难 | | [0066. 加一](https://leetcode.cn/problems/plus-one/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/plus-one.md) | 数组、数学 | 简单 | | [0067. 二进制求和](https://leetcode.cn/problems/add-binary/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/add-binary.md) | 位运算、数学、字符串、模拟 | 简单 | | [0068. 文本左右对齐](https://leetcode.cn/problems/text-justification/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/text-justification.md) | 数组、字符串、模拟 | 困难 | | [0069. x 的平方根](https://leetcode.cn/problems/sqrtx/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sqrtx.md) | 数学、二分查找 | 简单 | | [0070. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/climbing-stairs.md) | 记忆化搜索、数学、动态规划 | 简单 | | [0071. 简化路径](https://leetcode.cn/problems/simplify-path/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/simplify-path.md) | 栈、字符串 | 中等 | | [0072. 编辑距离](https://leetcode.cn/problems/edit-distance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/edit-distance.md) | 字符串、动态规划 | 中等 | | [0073. 矩阵置零](https://leetcode.cn/problems/set-matrix-zeroes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/set-matrix-zeroes.md) | 数组、哈希表、矩阵 | 中等 | | [0074. 搜索二维矩阵](https://leetcode.cn/problems/search-a-2d-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-a-2d-matrix.md) | 数组、二分查找、矩阵 | 中等 | | [0075. 颜色分类](https://leetcode.cn/problems/sort-colors/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sort-colors.md) | 数组、双指针、排序 | 中等 | | [0076. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/minimum-window-substring.md) | 哈希表、字符串、滑动窗口 | 困难 | | [0077. 组合](https://leetcode.cn/problems/combinations/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combinations.md) | 回溯 | 中等 | | [0078. 子集](https://leetcode.cn/problems/subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets.md) | 位运算、数组、回溯 | 中等 | | [0079. 单词搜索](https://leetcode.cn/problems/word-search/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/word-search.md) | 深度优先搜索、数组、字符串、回溯、矩阵 | 中等 | | [0080. 删除有序数组中的重复项 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-array-ii.md) | 数组、双指针 | 中等 | | [0081. 搜索旋转排序数组 II](https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-in-rotated-sorted-array-ii.md) | 数组、二分查找 | 中等 | | [0082. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-list-ii.md) | 链表、双指针 | 中等 | | [0083. 删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-list.md) | 链表 | 简单 | | [0084. 柱状图中最大的矩形](https://leetcode.cn/problems/largest-rectangle-in-histogram/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/largest-rectangle-in-histogram.md) | 栈、数组、单调栈 | 困难 | | [0085. 最大矩形](https://leetcode.cn/problems/maximal-rectangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximal-rectangle.md) | 栈、数组、动态规划、矩阵、单调栈 | 困难 | | [0086. 分隔链表](https://leetcode.cn/problems/partition-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/partition-list.md) | 链表、双指针 | 中等 | | [0087. 扰乱字符串](https://leetcode.cn/problems/scramble-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/scramble-string.md) | 字符串、动态规划 | 困难 | | [0088. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-sorted-array.md) | 数组、双指针、排序 | 简单 | | [0089. 格雷编码](https://leetcode.cn/problems/gray-code/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/gray-code.md) | 位运算、数学、回溯 | 中等 | | [0090. 子集 II](https://leetcode.cn/problems/subsets-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets-ii.md) | 位运算、数组、回溯 | 中等 | | [0091. 解码方法](https://leetcode.cn/problems/decode-ways/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/decode-ways.md) | 字符串、动态规划 | 中等 | | [0092. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-linked-list-ii.md) | 链表 | 中等 | | [0093. 复原 IP 地址](https://leetcode.cn/problems/restore-ip-addresses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/restore-ip-addresses.md) | 字符串、回溯 | 中等 | | [0094. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/binary-tree-inorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0095. 不同的二叉搜索树 II](https://leetcode.cn/problems/unique-binary-search-trees-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-binary-search-trees-ii.md) | 树、二叉搜索树、动态规划、回溯、二叉树 | 中等 | | [0096. 不同的二叉搜索树](https://leetcode.cn/problems/unique-binary-search-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-binary-search-trees.md) | 树、二叉搜索树、数学、动态规划、二叉树 | 中等 | | [0097. 交错字符串](https://leetcode.cn/problems/interleaving-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/interleaving-string.md) | 字符串、动态规划 | 中等 | | [0098. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/validate-binary-search-tree.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0099. 恢复二叉搜索树](https://leetcode.cn/problems/recover-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/recover-binary-search-tree.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | ### 第 100 ~ 199 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0100. 相同的树](https://leetcode.cn/problems/same-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/same-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0101. 对称二叉树](https://leetcode.cn/problems/symmetric-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/symmetric-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-level-order-traversal.md) | 树、广度优先搜索、二叉树 | 中等 | | [0103. 二叉树的锯齿形层序遍历](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-zigzag-level-order-traversal.md) | 树、广度优先搜索、二叉树 | 中等 | | [0104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/construct-binary-tree-from-preorder-and-inorder-traversal.md) | 树、数组、哈希表、分治、二叉树 | 中等 | | [0106. 从中序与后序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/construct-binary-tree-from-inorder-and-postorder-traversal.md) | 树、数组、哈希表、分治、二叉树 | 中等 | | [0107. 二叉树的层序遍历 II](https://leetcode.cn/problems/binary-tree-level-order-traversal-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-level-order-traversal-ii.md) | 树、广度优先搜索、二叉树 | 中等 | | [0108. 将有序数组转换为二叉搜索树](https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/convert-sorted-array-to-binary-search-tree.md) | 树、二叉搜索树、数组、分治、二叉树 | 简单 | | [0109. 有序链表转换二叉搜索树](https://leetcode.cn/problems/convert-sorted-list-to-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/convert-sorted-list-to-binary-search-tree.md) | 树、二叉搜索树、链表、分治、二叉树 | 中等 | | [0110. 平衡二叉树](https://leetcode.cn/problems/balanced-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/balanced-binary-tree.md) | 树、深度优先搜索、二叉树 | 简单 | | [0111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/minimum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0112. 路径总和](https://leetcode.cn/problems/path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/path-sum.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0113. 路径总和 II](https://leetcode.cn/problems/path-sum-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/path-sum-ii.md) | 树、深度优先搜索、回溯、二叉树 | 中等 | | [0114. 二叉树展开为链表](https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/flatten-binary-tree-to-linked-list.md) | 栈、树、深度优先搜索、链表、二叉树 | 中等 | | [0115. 不同的子序列](https://leetcode.cn/problems/distinct-subsequences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/distinct-subsequences.md) | 字符串、动态规划 | 困难 | | [0116. 填充每个节点的下一个右侧节点指针](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/populating-next-right-pointers-in-each-node.md) | 树、深度优先搜索、广度优先搜索、链表、二叉树 | 中等 | | [0117. 填充每个节点的下一个右侧节点指针 II](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/populating-next-right-pointers-in-each-node-ii.md) | 树、深度优先搜索、广度优先搜索、链表、二叉树 | 中等 | | [0118. 杨辉三角](https://leetcode.cn/problems/pascals-triangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/pascals-triangle.md) | 数组、动态规划 | 简单 | | [0119. 杨辉三角 II](https://leetcode.cn/problems/pascals-triangle-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/pascals-triangle-ii.md) | 数组、动态规划 | 简单 | | [0120. 三角形最小路径和](https://leetcode.cn/problems/triangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/triangle.md) | 数组、动态规划 | 中等 | | [0121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock.md) | 数组、动态规划 | 简单 | | [0122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-ii.md) | 贪心、数组、动态规划 | 中等 | | [0123. 买卖股票的最佳时机 III](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-iii.md) | 数组、动态规划 | 困难 | | [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [0125. 验证回文串](https://leetcode.cn/problems/valid-palindrome/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/valid-palindrome.md) | 双指针、字符串 | 简单 | | [0126. 单词接龙 II](https://leetcode.cn/problems/word-ladder-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/word-ladder-ii.md) | 广度优先搜索、哈希表、字符串、回溯 | 困难 | | [0127. 单词接龙](https://leetcode.cn/problems/word-ladder/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/word-ladder.md) | 广度优先搜索、哈希表、字符串 | 困难 | | [0128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/longest-consecutive-sequence.md) | 并查集、数组、哈希表 | 中等 | | [0129. 求根节点到叶节点数字之和](https://leetcode.cn/problems/sum-root-to-leaf-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sum-root-to-leaf-numbers.md) | 树、深度优先搜索、二叉树 | 中等 | | [0130. 被围绕的区域](https://leetcode.cn/problems/surrounded-regions/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/surrounded-regions.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0131. 分割回文串](https://leetcode.cn/problems/palindrome-partitioning/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/palindrome-partitioning.md) | 字符串、动态规划、回溯 | 中等 | | [0132. 分割回文串 II](https://leetcode.cn/problems/palindrome-partitioning-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/palindrome-partitioning-ii.md) | 字符串、动态规划 | 困难 | | [0133. 克隆图](https://leetcode.cn/problems/clone-graph/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/clone-graph.md) | 深度优先搜索、广度优先搜索、图、哈希表 | 中等 | | [0134. 加油站](https://leetcode.cn/problems/gas-station/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/gas-station.md) | 贪心、数组 | 中等 | | [0135. 分发糖果](https://leetcode.cn/problems/candy/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/candy.md) | 贪心、数组 | 困难 | | [0136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) | 位运算、数组 | 简单 | | [0137. 只出现一次的数字 II](https://leetcode.cn/problems/single-number-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number-ii.md) | 位运算、数组 | 中等 | | [0138. 随机链表的复制](https://leetcode.cn/problems/copy-list-with-random-pointer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/copy-list-with-random-pointer.md) | 哈希表、链表 | 中等 | | [0139. 单词拆分](https://leetcode.cn/problems/word-break/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/word-break.md) | 字典树、记忆化搜索、数组、哈希表、字符串、动态规划 | 中等 | | [0140. 单词拆分 II](https://leetcode.cn/problems/word-break-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/word-break-ii.md) | 字典树、记忆化搜索、数组、哈希表、字符串、动态规划、回溯 | 困难 | | [0141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle.md) | 哈希表、链表、双指针 | 简单 | | [0142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle-ii.md) | 哈希表、链表、双指针 | 中等 | | [0143. 重排链表](https://leetcode.cn/problems/reorder-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reorder-list.md) | 栈、递归、链表、双指针 | 中等 | | [0144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-preorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0145. 二叉树的后序遍历](https://leetcode.cn/problems/binary-tree-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-postorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0146. LRU 缓存](https://leetcode.cn/problems/lru-cache/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/lru-cache.md) | 设计、哈希表、链表、双向链表 | 中等 | | [0147. 对链表进行插入排序](https://leetcode.cn/problems/insertion-sort-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/insertion-sort-list.md) | 链表、排序 | 中等 | | [0148. 排序链表](https://leetcode.cn/problems/sort-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md) | 链表、双指针、分治、排序、归并排序 | 中等 | | [0149. 直线上最多的点数](https://leetcode.cn/problems/max-points-on-a-line/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/max-points-on-a-line.md) | 几何、数组、哈希表、数学 | 困难 | | [0150. 逆波兰表达式求值](https://leetcode.cn/problems/evaluate-reverse-polish-notation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/evaluate-reverse-polish-notation.md) | 栈、数组、数学 | 中等 | | [0151. 反转字符串中的单词](https://leetcode.cn/problems/reverse-words-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reverse-words-in-a-string.md) | 双指针、字符串 | 中等 | | [0152. 乘积最大子数组](https://leetcode.cn/problems/maximum-product-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-product-subarray.md) | 数组、动态规划 | 中等 | | [0153. 寻找旋转排序数组中的最小值](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array.md) | 数组、二分查找 | 中等 | | [0154. 寻找旋转排序数组中的最小值 II](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array-ii.md) | 数组、二分查找 | 困难 | | [0155. 最小栈](https://leetcode.cn/problems/min-stack/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/min-stack.md) | 栈、设计 | 中等 | | [0156. 上下翻转二叉树](https://leetcode.cn/problems/binary-tree-upside-down/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-upside-down.md) | 树、深度优先搜索、二叉树 | 中等 | | [0157. 用 Read4 读取 N 个字符](https://leetcode.cn/problems/read-n-characters-given-read4/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/read-n-characters-given-read4.md) | 数组、交互、模拟 | 简单 | | [0158. 用 Read4 读取 N 个字符 II - 多次调用](https://leetcode.cn/problems/read-n-characters-given-read4-ii-call-multiple-times/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/read-n-characters-given-read4-ii-call-multiple-times.md) | 数组、交互、模拟 | 困难 | | [0159. 至多包含两个不同字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-most-two-distinct-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/longest-substring-with-at-most-two-distinct-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0160. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/intersection-of-two-linked-lists.md) | 哈希表、链表、双指针 | 简单 | | [0161. 相隔为 1 的编辑距离](https://leetcode.cn/problems/one-edit-distance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/one-edit-distance.md) | 双指针、字符串 | 中等 | | [0162. 寻找峰值](https://leetcode.cn/problems/find-peak-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-peak-element.md) | 数组、二分查找 | 中等 | | [0163. 缺失的区间](https://leetcode.cn/problems/missing-ranges/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/missing-ranges.md) | 数组 | 简单 | | [0164. 最大间距](https://leetcode.cn/problems/maximum-gap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-gap.md) | 数组、桶排序、基数排序、排序 | 中等 | | [0165. 比较版本号](https://leetcode.cn/problems/compare-version-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/compare-version-numbers.md) | 双指针、字符串 | 中等 | | [0166. 分数到小数](https://leetcode.cn/problems/fraction-to-recurring-decimal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/fraction-to-recurring-decimal.md) | 哈希表、数学、字符串 | 中等 | | [0167. 两数之和 II - 输入有序数组](https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/two-sum-ii-input-array-is-sorted.md) | 数组、双指针、二分查找 | 中等 | | [0168. Excel 表列名称](https://leetcode.cn/problems/excel-sheet-column-title/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/excel-sheet-column-title.md) | 数学、字符串 | 简单 | | [0169. 多数元素](https://leetcode.cn/problems/majority-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/majority-element.md) | 数组、哈希表、分治、计数、排序 | 简单 | | [0170. 两数之和 III - 数据结构设计](https://leetcode.cn/problems/two-sum-iii-data-structure-design/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/two-sum-iii-data-structure-design.md) | 设计、数组、哈希表、双指针、数据流 | 简单 | | [0171. Excel 表列序号](https://leetcode.cn/problems/excel-sheet-column-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/excel-sheet-column-number.md) | 数学、字符串 | 简单 | | [0172. 阶乘后的零](https://leetcode.cn/problems/factorial-trailing-zeroes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/factorial-trailing-zeroes.md) | 数学 | 中等 | | [0173. 二叉搜索树迭代器](https://leetcode.cn/problems/binary-search-tree-iterator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-search-tree-iterator.md) | 栈、树、设计、二叉搜索树、二叉树、迭代器 | 中等 | | [0174. 地下城游戏](https://leetcode.cn/problems/dungeon-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/dungeon-game.md) | 数组、动态规划、矩阵 | 困难 | | [0179. 最大数](https://leetcode.cn/problems/largest-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/largest-number.md) | 贪心、数组、字符串、排序 | 中等 | | [0186. 反转字符串中的单词 II](https://leetcode.cn/problems/reverse-words-in-a-string-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reverse-words-in-a-string-ii.md) | 双指针、字符串 | 中等 | | [0187. 重复的DNA序列](https://leetcode.cn/problems/repeated-dna-sequences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/repeated-dna-sequences.md) | 位运算、哈希表、字符串、滑动窗口、哈希函数、滚动哈希 | 中等 | | [0188. 买卖股票的最佳时机 IV](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-iv.md) | 数组、动态规划 | 困难 | | [0189. 轮转数组](https://leetcode.cn/problems/rotate-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/rotate-array.md) | 数组、数学、双指针 | 中等 | | [0190. 颠倒二进制位](https://leetcode.cn/problems/reverse-bits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reverse-bits.md) | 位运算、分治 | 简单 | | [0191. 位1的个数](https://leetcode.cn/problems/number-of-1-bits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/number-of-1-bits.md) | 位运算、分治 | 简单 | | [0198. 打家劫舍](https://leetcode.cn/problems/house-robber/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/house-robber.md) | 数组、动态规划 | 中等 | | [0199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-right-side-view.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | ### 第 200 ~ 299 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-islands.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0201. 数字范围按位与](https://leetcode.cn/problems/bitwise-and-of-numbers-range/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/bitwise-and-of-numbers-range.md) | 位运算 | 中等 | | [0202. 快乐数](https://leetcode.cn/problems/happy-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/happy-number.md) | 哈希表、数学、双指针 | 简单 | | [0203. 移除链表元素](https://leetcode.cn/problems/remove-linked-list-elements/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/remove-linked-list-elements.md) | 递归、链表 | 简单 | | [0204. 计数质数](https://leetcode.cn/problems/count-primes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/count-primes.md) | 数组、数学、枚举、数论 | 中等 | | [0205. 同构字符串](https://leetcode.cn/problems/isomorphic-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/isomorphic-strings.md) | 哈希表、字符串 | 简单 | | [0206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/reverse-linked-list.md) | 递归、链表 | 简单 | | [0207. 课程表](https://leetcode.cn/problems/course-schedule/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/course-schedule.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [0208. 实现 Trie (前缀树)](https://leetcode.cn/problems/implement-trie-prefix-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-trie-prefix-tree.md) | 设计、字典树、哈希表、字符串 | 中等 | | [0209. 长度最小的子数组](https://leetcode.cn/problems/minimum-size-subarray-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/minimum-size-subarray-sum.md) | 数组、二分查找、前缀和、滑动窗口 | 中等 | | [0210. 课程表 II](https://leetcode.cn/problems/course-schedule-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/course-schedule-ii.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [0211. 添加与搜索单词 - 数据结构设计](https://leetcode.cn/problems/design-add-and-search-words-data-structure/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/design-add-and-search-words-data-structure.md) | 深度优先搜索、设计、字典树、字符串 | 中等 | | [0212. 单词搜索 II](https://leetcode.cn/problems/word-search-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/word-search-ii.md) | 字典树、数组、字符串、回溯、矩阵 | 困难 | | [0213. 打家劫舍 II](https://leetcode.cn/problems/house-robber-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/house-robber-ii.md) | 数组、动态规划 | 中等 | | [0214. 最短回文串](https://leetcode.cn/problems/shortest-palindrome/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/shortest-palindrome.md) | 字符串、字符串匹配、哈希函数、滚动哈希 | 困难 | | [0215. 数组中的第K个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/kth-largest-element-in-an-array.md) | 数组、分治、快速选择、排序、堆(优先队列) | 中等 | | [0216. 组合总和 III](https://leetcode.cn/problems/combination-sum-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/combination-sum-iii.md) | 数组、回溯 | 中等 | | [0217. 存在重复元素](https://leetcode.cn/problems/contains-duplicate/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate.md) | 数组、哈希表、排序 | 简单 | | [0218. 天际线问题](https://leetcode.cn/problems/the-skyline-problem/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/the-skyline-problem.md) | 树状数组、线段树、数组、分治、有序集合、排序、扫描线、堆(优先队列) | 困难 | | [0219. 存在重复元素 II](https://leetcode.cn/problems/contains-duplicate-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate-ii.md) | 数组、哈希表、滑动窗口 | 简单 | | [0220. 存在重复元素 III](https://leetcode.cn/problems/contains-duplicate-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate-iii.md) | 数组、桶排序、有序集合、排序、滑动窗口 | 困难 | | [0221. 最大正方形](https://leetcode.cn/problems/maximal-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/maximal-square.md) | 数组、动态规划、矩阵 | 中等 | | [0222. 完全二叉树的节点个数](https://leetcode.cn/problems/count-complete-tree-nodes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/count-complete-tree-nodes.md) | 位运算、树、二分查找、二叉树 | 简单 | | [0223. 矩形面积](https://leetcode.cn/problems/rectangle-area/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/rectangle-area.md) | 几何、数学 | 中等 | | [0224. 基本计算器](https://leetcode.cn/problems/basic-calculator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/basic-calculator.md) | 栈、递归、数学、字符串 | 困难 | | [0225. 用队列实现栈](https://leetcode.cn/problems/implement-stack-using-queues/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-stack-using-queues.md) | 栈、设计、队列 | 简单 | | [0226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/invert-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0227. 基本计算器 II](https://leetcode.cn/problems/basic-calculator-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/basic-calculator-ii.md) | 栈、数学、字符串 | 中等 | | [0228. 汇总区间](https://leetcode.cn/problems/summary-ranges/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/summary-ranges.md) | 数组 | 简单 | | [0229. 多数元素 II](https://leetcode.cn/problems/majority-element-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/majority-element-ii.md) | 数组、哈希表、计数、排序 | 中等 | | [0230. 二叉搜索树中第 K 小的元素](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/kth-smallest-element-in-a-bst.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0231. 2 的幂](https://leetcode.cn/problems/power-of-two/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/power-of-two.md) | 位运算、递归、数学 | 简单 | | [0232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-queue-using-stacks.md) | 栈、设计、队列 | 简单 | | [0233. 数字 1 的个数](https://leetcode.cn/problems/number-of-digit-one/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-digit-one.md) | 递归、数学、动态规划 | 困难 | | [0234. 回文链表](https://leetcode.cn/problems/palindrome-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/palindrome-linked-list.md) | 栈、递归、链表、双指针 | 简单 | | [0235. 二叉搜索树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/lowest-common-ancestor-of-a-binary-search-tree.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/lowest-common-ancestor-of-a-binary-tree.md) | 树、深度优先搜索、二叉树 | 中等 | | [0237. 删除链表中的节点](https://leetcode.cn/problems/delete-node-in-a-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/delete-node-in-a-linked-list.md) | 链表 | 中等 | | [0238. 除自身以外数组的乘积](https://leetcode.cn/problems/product-of-array-except-self/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/product-of-array-except-self.md) | 数组、前缀和 | 中等 | | [0239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/sliding-window-maximum.md) | 队列、数组、滑动窗口、单调队列、堆(优先队列) | 困难 | | [0240. 搜索二维矩阵 II](https://leetcode.cn/problems/search-a-2d-matrix-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/search-a-2d-matrix-ii.md) | 数组、二分查找、分治、矩阵 | 中等 | | [0241. 为运算表达式设计优先级](https://leetcode.cn/problems/different-ways-to-add-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/different-ways-to-add-parentheses.md) | 递归、记忆化搜索、数学、字符串、动态规划 | 中等 | | [0242. 有效的字母异位词](https://leetcode.cn/problems/valid-anagram/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/valid-anagram.md) | 哈希表、字符串、排序 | 简单 | | [0243. 最短单词距离](https://leetcode.cn/problems/shortest-word-distance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/shortest-word-distance.md) | 数组、字符串 | 简单 | | [0244. 最短单词距离 II](https://leetcode.cn/problems/shortest-word-distance-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/shortest-word-distance-ii.md) | 设计、数组、哈希表、双指针、字符串 | 中等 | | [0245. 最短单词距离 III](https://leetcode.cn/problems/shortest-word-distance-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/shortest-word-distance-iii.md) | 数组、字符串 | 中等 | | [0246. 中心对称数](https://leetcode.cn/problems/strobogrammatic-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/strobogrammatic-number.md) | 哈希表、双指针、字符串 | 简单 | | [0247. 中心对称数 II](https://leetcode.cn/problems/strobogrammatic-number-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/strobogrammatic-number-ii.md) | 递归、数组、字符串 | 中等 | | [0248. 中心对称数 III](https://leetcode.cn/problems/strobogrammatic-number-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/strobogrammatic-number-iii.md) | 递归、数组、字符串 | 困难 | | [0249. 移位字符串分组](https://leetcode.cn/problems/group-shifted-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/group-shifted-strings.md) | 数组、哈希表、字符串 | 中等 | | [0250. 统计同值子树](https://leetcode.cn/problems/count-univalue-subtrees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/count-univalue-subtrees.md) | 树、深度优先搜索、二叉树 | 中等 | | [0251. 展开二维向量](https://leetcode.cn/problems/flatten-2d-vector/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/flatten-2d-vector.md) | 设计、数组、双指针、迭代器 | 中等 | | [0252. 会议室](https://leetcode.cn/problems/meeting-rooms/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/meeting-rooms.md) | 数组、排序 | 简单 | | [0253. 会议室 II](https://leetcode.cn/problems/meeting-rooms-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/meeting-rooms-ii.md) | 贪心、数组、双指针、前缀和、排序、堆(优先队列) | 中等 | | [0254. 因子的组合](https://leetcode.cn/problems/factor-combinations/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/factor-combinations.md) | 回溯 | 中等 | | [0255. 验证二叉搜索树的前序遍历序列](https://leetcode.cn/problems/verify-preorder-sequence-in-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/verify-preorder-sequence-in-binary-search-tree.md) | 栈、树、二叉搜索树、递归、数组、二叉树、单调栈 | 中等 | | [0256. 粉刷房子](https://leetcode.cn/problems/paint-house/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/paint-house.md) | 数组、动态规划 | 中等 | | [0257. 二叉树的所有路径](https://leetcode.cn/problems/binary-tree-paths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/binary-tree-paths.md) | 树、深度优先搜索、字符串、回溯、二叉树 | 简单 | | [0258. 各位相加](https://leetcode.cn/problems/add-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/add-digits.md) | 数学、数论、模拟 | 简单 | | [0259. 较小的三数之和](https://leetcode.cn/problems/3sum-smaller/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/3sum-smaller.md) | 数组、双指针、二分查找、排序 | 中等 | | [0260. 只出现一次的数字 III](https://leetcode.cn/problems/single-number-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/single-number-iii.md) | 位运算、数组 | 中等 | | [0261. 以图判树](https://leetcode.cn/problems/graph-valid-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/graph-valid-tree.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0263. 丑数](https://leetcode.cn/problems/ugly-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/ugly-number.md) | 数学 | 简单 | | [0264. 丑数 II](https://leetcode.cn/problems/ugly-number-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/ugly-number-ii.md) | 哈希表、数学、动态规划、堆(优先队列) | 中等 | | [0265. 粉刷房子 II](https://leetcode.cn/problems/paint-house-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/paint-house-ii.md) | 数组、动态规划 | 困难 | | [0266. 回文排列](https://leetcode.cn/problems/palindrome-permutation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/palindrome-permutation.md) | 位运算、哈希表、字符串 | 简单 | | [0267. 回文排列 II](https://leetcode.cn/problems/palindrome-permutation-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/palindrome-permutation-ii.md) | 哈希表、字符串、回溯 | 中等 | | [0268. 丢失的数字](https://leetcode.cn/problems/missing-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/missing-number.md) | 位运算、数组、哈希表、数学、二分查找、排序 | 简单 | | [0269. 火星词典](https://leetcode.cn/problems/alien-dictionary/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/alien-dictionary.md) | 深度优先搜索、广度优先搜索、图、拓扑排序、数组、字符串 | 困难 | | [0270. 最接近的二叉搜索树值](https://leetcode.cn/problems/closest-binary-search-tree-value/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/closest-binary-search-tree-value.md) | 树、深度优先搜索、二叉搜索树、二分查找、二叉树 | 简单 | | [0271. 字符串的编码与解码](https://leetcode.cn/problems/encode-and-decode-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/encode-and-decode-strings.md) | 设计、数组、字符串 | 中等 | | [0272. 最接近的二叉搜索树值 II](https://leetcode.cn/problems/closest-binary-search-tree-value-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/closest-binary-search-tree-value-ii.md) | 栈、树、深度优先搜索、二叉搜索树、双指针、二叉树、堆(优先队列) | 困难 | | [0273. 整数转换英文表示](https://leetcode.cn/problems/integer-to-english-words/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/integer-to-english-words.md) | 递归、数学、字符串 | 困难 | | [0274. H 指数](https://leetcode.cn/problems/h-index/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/h-index.md) | 数组、计数排序、排序 | 中等 | | [0275. H 指数 II](https://leetcode.cn/problems/h-index-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/h-index-ii.md) | 数组、二分查找 | 中等 | | [0276. 栅栏涂色](https://leetcode.cn/problems/paint-fence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/paint-fence.md) | 动态规划 | 中等 | | [0277. 搜寻名人](https://leetcode.cn/problems/find-the-celebrity/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/find-the-celebrity.md) | 图、双指针、交互 | 中等 | | [0278. 第一个错误的版本](https://leetcode.cn/problems/first-bad-version/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/first-bad-version.md) | 二分查找、交互 | 简单 | | [0279. 完全平方数](https://leetcode.cn/problems/perfect-squares/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/perfect-squares.md) | 广度优先搜索、数学、动态规划 | 中等 | | [0280. 摆动排序](https://leetcode.cn/problems/wiggle-sort/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/wiggle-sort.md) | 贪心、数组、排序 | 中等 | | [0281. 锯齿迭代器](https://leetcode.cn/problems/zigzag-iterator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/zigzag-iterator.md) | 设计、队列、数组、迭代器 | 中等 | | [0282. 给表达式添加运算符](https://leetcode.cn/problems/expression-add-operators/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/expression-add-operators.md) | 数学、字符串、回溯 | 困难 | | [0283. 移动零](https://leetcode.cn/problems/move-zeroes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/move-zeroes.md) | 数组、双指针 | 简单 | | [0284. 窥视迭代器](https://leetcode.cn/problems/peeking-iterator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/peeking-iterator.md) | 设计、数组、迭代器 | 中等 | | [0285. 二叉搜索树中的中序后继](https://leetcode.cn/problems/inorder-successor-in-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/inorder-successor-in-bst.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0286. 墙与门](https://leetcode.cn/problems/walls-and-gates/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/walls-and-gates.md) | 广度优先搜索、数组、矩阵 | 中等 | | [0287. 寻找重复数](https://leetcode.cn/problems/find-the-duplicate-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/find-the-duplicate-number.md) | 位运算、数组、双指针、二分查找 | 中等 | | [0288. 单词的唯一缩写](https://leetcode.cn/problems/unique-word-abbreviation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/unique-word-abbreviation.md) | 设计、数组、哈希表、字符串 | 中等 | | [0289. 生命游戏](https://leetcode.cn/problems/game-of-life/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/game-of-life.md) | 数组、矩阵、模拟 | 中等 | | [0290. 单词规律](https://leetcode.cn/problems/word-pattern/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/word-pattern.md) | 哈希表、字符串 | 简单 | | [0291. 单词规律 II](https://leetcode.cn/problems/word-pattern-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/word-pattern-ii.md) | 哈希表、字符串、回溯 | 中等 | | [0292. Nim 游戏](https://leetcode.cn/problems/nim-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/nim-game.md) | 脑筋急转弯、数学、博弈 | 简单 | | [0293. 翻转游戏](https://leetcode.cn/problems/flip-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/flip-game.md) | 字符串 | 简单 | | [0294. 翻转游戏 II](https://leetcode.cn/problems/flip-game-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/flip-game-ii.md) | 记忆化搜索、数学、动态规划、回溯、博弈 | 中等 | | [0295. 数据流的中位数](https://leetcode.cn/problems/find-median-from-data-stream/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/find-median-from-data-stream.md) | 设计、双指针、数据流、排序、堆(优先队列) | 困难 | | [0296. 最佳的碰头地点](https://leetcode.cn/problems/best-meeting-point/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/best-meeting-point.md) | 数组、数学、矩阵、排序 | 困难 | | [0297. 二叉树的序列化与反序列化](https://leetcode.cn/problems/serialize-and-deserialize-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/serialize-and-deserialize-binary-tree.md) | 树、深度优先搜索、广度优先搜索、设计、字符串、二叉树 | 困难 | | [0298. 二叉树最长连续序列](https://leetcode.cn/problems/binary-tree-longest-consecutive-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/binary-tree-longest-consecutive-sequence.md) | 树、深度优先搜索、二叉树 | 中等 | | [0299. 猜数字游戏](https://leetcode.cn/problems/bulls-and-cows/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/bulls-and-cows.md) | 哈希表、字符串、计数 | 中等 | ### 第 300 ~ 399 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-subsequence.md) | 数组、二分查找、动态规划 | 中等 | | [0301. 删除无效的括号](https://leetcode.cn/problems/remove-invalid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/remove-invalid-parentheses.md) | 广度优先搜索、字符串、回溯 | 困难 | | [0302. 包含全部黑色像素的最小矩形](https://leetcode.cn/problems/smallest-rectangle-enclosing-black-pixels/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/smallest-rectangle-enclosing-black-pixels.md) | 深度优先搜索、广度优先搜索、数组、二分查找、矩阵 | 困难 | | [0303. 区域和检索 - 数组不可变](https://leetcode.cn/problems/range-sum-query-immutable/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-immutable.md) | 设计、数组、前缀和 | 简单 | | [0304. 二维区域和检索 - 矩阵不可变](https://leetcode.cn/problems/range-sum-query-2d-immutable/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-2d-immutable.md) | 设计、数组、矩阵、前缀和 | 中等 | | [0305. 岛屿数量 II](https://leetcode.cn/problems/number-of-islands-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/number-of-islands-ii.md) | 并查集、数组、哈希表 | 困难 | | [0306. 累加数](https://leetcode.cn/problems/additive-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/additive-number.md) | 字符串、回溯 | 中等 | | [0307. 区域和检索 - 数组可修改](https://leetcode.cn/problems/range-sum-query-mutable/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-mutable.md) | 设计、树状数组、线段树、数组、分治 | 中等 | | [0308. 二维区域和检索 - 矩阵可修改](https://leetcode.cn/problems/range-sum-query-2d-mutable/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-2d-mutable.md) | 设计、树状数组、线段树、数组、矩阵 | 中等 | | [0309. 买卖股票的最佳时机含冷冻期](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/best-time-to-buy-and-sell-stock-with-cooldown.md) | 数组、动态规划 | 中等 | | [0310. 最小高度树](https://leetcode.cn/problems/minimum-height-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/minimum-height-trees.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [0311. 稀疏矩阵的乘法](https://leetcode.cn/problems/sparse-matrix-multiplication/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sparse-matrix-multiplication.md) | 数组、哈希表、矩阵 | 中等 | | [0312. 戳气球](https://leetcode.cn/problems/burst-balloons/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/burst-balloons.md) | 数组、动态规划 | 困难 | | [0313. 超级丑数](https://leetcode.cn/problems/super-ugly-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/super-ugly-number.md) | 数组、数学、动态规划 | 中等 | | [0314. 二叉树的垂直遍历](https://leetcode.cn/problems/binary-tree-vertical-order-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/binary-tree-vertical-order-traversal.md) | 树、深度优先搜索、广度优先搜索、哈希表、二叉树、排序 | 中等 | | [0315. 计算右侧小于当前元素的个数](https://leetcode.cn/problems/count-of-smaller-numbers-after-self/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-of-smaller-numbers-after-self.md) | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 | | [0316. 去除重复字母](https://leetcode.cn/problems/remove-duplicate-letters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/remove-duplicate-letters.md) | 栈、贪心、字符串、单调栈 | 中等 | | [0317. 离建筑物最近的距离](https://leetcode.cn/problems/shortest-distance-from-all-buildings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/shortest-distance-from-all-buildings.md) | 广度优先搜索、数组、矩阵 | 困难 | | [0318. 最大单词长度乘积](https://leetcode.cn/problems/maximum-product-of-word-lengths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/maximum-product-of-word-lengths.md) | 位运算、数组、字符串 | 中等 | | [0319. 灯泡开关](https://leetcode.cn/problems/bulb-switcher/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/bulb-switcher.md) | 脑筋急转弯、数学 | 中等 | | [0320. 列举单词的全部缩写](https://leetcode.cn/problems/generalized-abbreviation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/generalized-abbreviation.md) | 位运算、字符串、回溯 | 中等 | | [0321. 拼接最大数](https://leetcode.cn/problems/create-maximum-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/create-maximum-number.md) | 栈、贪心、数组、双指针、单调栈 | 困难 | | [0322. 零钱兑换](https://leetcode.cn/problems/coin-change/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) | 广度优先搜索、数组、动态规划 | 中等 | | [0323. 无向图中连通分量的数目](https://leetcode.cn/problems/number-of-connected-components-in-an-undirected-graph/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/number-of-connected-components-in-an-undirected-graph.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0324. 摆动排序 II](https://leetcode.cn/problems/wiggle-sort-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/wiggle-sort-ii.md) | 贪心、数组、分治、快速选择、排序 | 中等 | | [0325. 和等于 k 的最长子数组长度](https://leetcode.cn/problems/maximum-size-subarray-sum-equals-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/maximum-size-subarray-sum-equals-k.md) | 数组、哈希表、前缀和 | 中等 | | [0326. 3 的幂](https://leetcode.cn/problems/power-of-three/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/power-of-three.md) | 递归、数学 | 简单 | | [0327. 区间和的个数](https://leetcode.cn/problems/count-of-range-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-of-range-sum.md) | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 | | [0328. 奇偶链表](https://leetcode.cn/problems/odd-even-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/odd-even-linked-list.md) | 链表 | 中等 | | [0329. 矩阵中的最长递增路径](https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-path-in-a-matrix.md) | 深度优先搜索、广度优先搜索、图、拓扑排序、记忆化搜索、数组、动态规划、矩阵 | 困难 | | [0330. 按要求补齐数组](https://leetcode.cn/problems/patching-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/patching-array.md) | 贪心、数组 | 困难 | | [0331. 验证二叉树的前序序列化](https://leetcode.cn/problems/verify-preorder-serialization-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/verify-preorder-serialization-of-a-binary-tree.md) | 栈、树、字符串、二叉树 | 中等 | | [0332. 重新安排行程](https://leetcode.cn/problems/reconstruct-itinerary/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reconstruct-itinerary.md) | 深度优先搜索、图、欧拉回路 | 困难 | | [0333. 最大二叉搜索子树](https://leetcode.cn/problems/largest-bst-subtree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/largest-bst-subtree.md) | 树、深度优先搜索、二叉搜索树、动态规划、二叉树 | 中等 | | [0334. 递增的三元子序列](https://leetcode.cn/problems/increasing-triplet-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/increasing-triplet-subsequence.md) | 贪心、数组 | 中等 | | [0335. 路径交叉](https://leetcode.cn/problems/self-crossing/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/self-crossing.md) | 几何、数组、数学 | 困难 | | [0336. 回文对](https://leetcode.cn/problems/palindrome-pairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/palindrome-pairs.md) | 字典树、数组、哈希表、字符串 | 困难 | | [0337. 打家劫舍 III](https://leetcode.cn/problems/house-robber-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/house-robber-iii.md) | 树、深度优先搜索、动态规划、二叉树 | 中等 | | [0338. 比特位计数](https://leetcode.cn/problems/counting-bits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/counting-bits.md) | 位运算、动态规划 | 简单 | | [0339. 嵌套列表加权和](https://leetcode.cn/problems/nested-list-weight-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/nested-list-weight-sum.md) | 深度优先搜索、广度优先搜索 | 中等 | | [0340. 至多包含 K 个不同字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-most-k-distinct-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-substring-with-at-most-k-distinct-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0341. 扁平化嵌套列表迭代器](https://leetcode.cn/problems/flatten-nested-list-iterator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/flatten-nested-list-iterator.md) | 栈、树、深度优先搜索、设计、队列、迭代器 | 中等 | | [0342. 4的幂](https://leetcode.cn/problems/power-of-four/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/power-of-four.md) | 位运算、递归、数学 | 简单 | | [0343. 整数拆分](https://leetcode.cn/problems/integer-break/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/integer-break.md) | 数学、动态规划 | 中等 | | [0344. 反转字符串](https://leetcode.cn/problems/reverse-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-string.md) | 双指针、字符串 | 简单 | | [0345. 反转字符串中的元音字母](https://leetcode.cn/problems/reverse-vowels-of-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-vowels-of-a-string.md) | 双指针、字符串 | 简单 | | [0346. 数据流中的移动平均值](https://leetcode.cn/problems/moving-average-from-data-stream/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/moving-average-from-data-stream.md) | 设计、队列、数组、数据流 | 简单 | | [0347. 前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/top-k-frequent-elements.md) | 数组、哈希表、分治、桶排序、计数、快速选择、排序、堆(优先队列) | 中等 | | [0348. 设计井字棋](https://leetcode.cn/problems/design-tic-tac-toe/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-tic-tac-toe.md) | 设计、数组、哈希表、矩阵、模拟 | 中等 | | [0349. 两个数组的交集](https://leetcode.cn/problems/intersection-of-two-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays.md) | 数组、哈希表、双指针、二分查找、排序 | 简单 | | [0350. 两个数组的交集 II](https://leetcode.cn/problems/intersection-of-two-arrays-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays-ii.md) | 数组、哈希表、双指针、二分查找、排序 | 简单 | | [0351. 安卓系统手势解锁](https://leetcode.cn/problems/android-unlock-patterns/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/android-unlock-patterns.md) | 位运算、动态规划、回溯、状态压缩 | 中等 | | [0352. 将数据流变为多个不相交区间](https://leetcode.cn/problems/data-stream-as-disjoint-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/data-stream-as-disjoint-intervals.md) | 设计、二分查找、有序集合 | 困难 | | [0353. 贪吃蛇](https://leetcode.cn/problems/design-snake-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-snake-game.md) | 设计、队列、数组、哈希表、模拟 | 中等 | | [0354. 俄罗斯套娃信封问题](https://leetcode.cn/problems/russian-doll-envelopes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/russian-doll-envelopes.md) | 数组、二分查找、动态规划、排序 | 困难 | | [0355. 设计推特](https://leetcode.cn/problems/design-twitter/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-twitter.md) | 设计、哈希表、链表、堆(优先队列) | 中等 | | [0356. 直线镜像](https://leetcode.cn/problems/line-reflection/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/line-reflection.md) | 数组、哈希表、数学 | 中等 | | [0357. 统计各位数字都不同的数字个数](https://leetcode.cn/problems/count-numbers-with-unique-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-numbers-with-unique-digits.md) | 数学、动态规划、回溯 | 中等 | | [0358. K 距离间隔重排字符串](https://leetcode.cn/problems/rearrange-string-k-distance-apart/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/rearrange-string-k-distance-apart.md) | 贪心、哈希表、字符串、计数、排序、堆(优先队列) | 困难 | | [0359. 日志速率限制器](https://leetcode.cn/problems/logger-rate-limiter/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/logger-rate-limiter.md) | 设计、哈希表、数据流 | 简单 | | [0360. 有序转化数组](https://leetcode.cn/problems/sort-transformed-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sort-transformed-array.md) | 数组、数学、双指针、排序 | 中等 | | [0361. 轰炸敌人](https://leetcode.cn/problems/bomb-enemy/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/bomb-enemy.md) | 数组、动态规划、矩阵 | 中等 | | [0362. 敲击计数器](https://leetcode.cn/problems/design-hit-counter/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-hit-counter.md) | 设计、队列、数组、二分查找、数据流 | 中等 | | [0363. 矩形区域不超过 K 的最大数值和](https://leetcode.cn/problems/max-sum-of-rectangle-no-larger-than-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/max-sum-of-rectangle-no-larger-than-k.md) | 数组、二分查找、矩阵、有序集合、前缀和 | 困难 | | [0364. 嵌套列表加权和 II](https://leetcode.cn/problems/nested-list-weight-sum-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/nested-list-weight-sum-ii.md) | 栈、深度优先搜索、广度优先搜索 | 中等 | | [0365. 水壶问题](https://leetcode.cn/problems/water-and-jug-problem/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/water-and-jug-problem.md) | 深度优先搜索、广度优先搜索、数学 | 中等 | | [0366. 寻找二叉树的叶子节点](https://leetcode.cn/problems/find-leaves-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/find-leaves-of-binary-tree.md) | 树、深度优先搜索、二叉树 | 中等 | | [0367. 有效的完全平方数](https://leetcode.cn/problems/valid-perfect-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/valid-perfect-square.md) | 数学、二分查找 | 简单 | | [0368. 最大整除子集](https://leetcode.cn/problems/largest-divisible-subset/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/largest-divisible-subset.md) | 数组、数学、动态规划、排序 | 中等 | | [0369. 给单链表加一](https://leetcode.cn/problems/plus-one-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/plus-one-linked-list.md) | 链表、数学 | 中等 | | [0370. 区间加法](https://leetcode.cn/problems/range-addition/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-addition.md) | 数组、前缀和 | 中等 | | [0371. 两整数之和](https://leetcode.cn/problems/sum-of-two-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sum-of-two-integers.md) | 位运算、数学 | 中等 | | [0372. 超级次方](https://leetcode.cn/problems/super-pow/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/super-pow.md) | 数学、分治 | 中等 | | [0373. 查找和最小的 K 对数字](https://leetcode.cn/problems/find-k-pairs-with-smallest-sums/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/find-k-pairs-with-smallest-sums.md) | 数组、堆(优先队列) | 中等 | | [0374. 猜数字大小](https://leetcode.cn/problems/guess-number-higher-or-lower/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/guess-number-higher-or-lower.md) | 二分查找、交互 | 简单 | | [0375. 猜数字大小 II](https://leetcode.cn/problems/guess-number-higher-or-lower-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/guess-number-higher-or-lower-ii.md) | 数学、动态规划、博弈 | 中等 | | [0376. 摆动序列](https://leetcode.cn/problems/wiggle-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/wiggle-subsequence.md) | 贪心、数组、动态规划 | 中等 | | [0377. 组合总和 Ⅳ](https://leetcode.cn/problems/combination-sum-iv/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/combination-sum-iv.md) | 数组、动态规划 | 中等 | | [0378. 有序矩阵中第 K 小的元素](https://leetcode.cn/problems/kth-smallest-element-in-a-sorted-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/kth-smallest-element-in-a-sorted-matrix.md) | 数组、二分查找、矩阵、排序、堆(优先队列) | 中等 | | [0379. 电话目录管理系统](https://leetcode.cn/problems/design-phone-directory/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-phone-directory.md) | 设计、队列、数组、哈希表、链表 | 中等 | | [0380. O(1) 时间插入、删除和获取随机元素](https://leetcode.cn/problems/insert-delete-getrandom-o1/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/insert-delete-getrandom-o1.md) | 设计、数组、哈希表、数学、随机化 | 中等 | | [0381. O(1) 时间插入、删除和获取随机元素 - 允许重复](https://leetcode.cn/problems/insert-delete-getrandom-o1-duplicates-allowed/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/insert-delete-getrandom-o1-duplicates-allowed.md) | 设计、数组、哈希表、数学、随机化 | 困难 | | [0382. 链表随机节点](https://leetcode.cn/problems/linked-list-random-node/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/linked-list-random-node.md) | 水塘抽样、链表、数学、随机化 | 中等 | | [0383. 赎金信](https://leetcode.cn/problems/ransom-note/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/ransom-note.md) | 哈希表、字符串、计数 | 简单 | | [0384. 打乱数组](https://leetcode.cn/problems/shuffle-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/shuffle-an-array.md) | 设计、数组、数学、随机化 | 中等 | | [0385. 迷你语法分析器](https://leetcode.cn/problems/mini-parser/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/mini-parser.md) | 栈、深度优先搜索、字符串 | 中等 | | [0386. 字典序排数](https://leetcode.cn/problems/lexicographical-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/lexicographical-numbers.md) | 深度优先搜索、字典树 | 中等 | | [0387. 字符串中的第一个唯一字符](https://leetcode.cn/problems/first-unique-character-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/first-unique-character-in-a-string.md) | 队列、哈希表、字符串、计数 | 简单 | | [0388. 文件的最长绝对路径](https://leetcode.cn/problems/longest-absolute-file-path/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-absolute-file-path.md) | 栈、深度优先搜索、字符串 | 中等 | | [0389. 找不同](https://leetcode.cn/problems/find-the-difference/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/find-the-difference.md) | 位运算、哈希表、字符串、排序 | 简单 | | [0390. 消除游戏](https://leetcode.cn/problems/elimination-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/elimination-game.md) | 递归、数学 | 中等 | | [0391. 完美矩形](https://leetcode.cn/problems/perfect-rectangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/perfect-rectangle.md) | 几何、数组、哈希表、数学、扫描线 | 困难 | | [0392. 判断子序列](https://leetcode.cn/problems/is-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/is-subsequence.md) | 双指针、字符串、动态规划 | 简单 | | [0393. UTF-8 编码验证](https://leetcode.cn/problems/utf-8-validation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/utf-8-validation.md) | 位运算、数组 | 中等 | | [0394. 字符串解码](https://leetcode.cn/problems/decode-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/decode-string.md) | 栈、递归、字符串 | 中等 | | [0395. 至少有 K 个重复字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-substring-with-at-least-k-repeating-characters.md) | 哈希表、字符串、分治、滑动窗口 | 中等 | | [0396. 旋转函数](https://leetcode.cn/problems/rotate-function/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/rotate-function.md) | 数组、数学、动态规划 | 中等 | | [0397. 整数替换](https://leetcode.cn/problems/integer-replacement/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/integer-replacement.md) | 贪心、位运算、记忆化搜索、动态规划 | 中等 | | [0398. 随机数索引](https://leetcode.cn/problems/random-pick-index/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/random-pick-index.md) | 水塘抽样、哈希表、数学、随机化 | 中等 | | [0399. 除法求值](https://leetcode.cn/problems/evaluate-division/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/evaluate-division.md) | 深度优先搜索、广度优先搜索、并查集、图、数组、字符串、最短路 | 中等 | ### 第 400 ~ 499 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0400. 第 N 位数字](https://leetcode.cn/problems/nth-digit/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/nth-digit.md) | 数学、二分查找 | 中等 | | [0401. 二进制手表](https://leetcode.cn/problems/binary-watch/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/binary-watch.md) | 位运算、回溯 | 简单 | | [0402. 移掉 K 位数字](https://leetcode.cn/problems/remove-k-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/remove-k-digits.md) | 栈、贪心、字符串、单调栈 | 中等 | | [0403. 青蛙过河](https://leetcode.cn/problems/frog-jump/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/frog-jump.md) | 数组、动态规划 | 困难 | | [0404. 左叶子之和](https://leetcode.cn/problems/sum-of-left-leaves/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sum-of-left-leaves.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0405. 数字转换为十六进制数](https://leetcode.cn/problems/convert-a-number-to-hexadecimal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/convert-a-number-to-hexadecimal.md) | 位运算、数学、字符串 | 简单 | | [0406. 根据身高重建队列](https://leetcode.cn/problems/queue-reconstruction-by-height/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/queue-reconstruction-by-height.md) | 树状数组、线段树、数组、排序 | 中等 | | [0407. 接雨水 II](https://leetcode.cn/problems/trapping-rain-water-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/trapping-rain-water-ii.md) | 广度优先搜索、数组、矩阵、堆(优先队列) | 困难 | | [0408. 有效单词缩写](https://leetcode.cn/problems/valid-word-abbreviation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/valid-word-abbreviation.md) | 双指针、字符串 | 简单 | | [0409. 最长回文串](https://leetcode.cn/problems/longest-palindrome/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/longest-palindrome.md) | 贪心、哈希表、字符串 | 简单 | | [0410. 分割数组的最大值](https://leetcode.cn/problems/split-array-largest-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/split-array-largest-sum.md) | 贪心、数组、二分查找、动态规划、前缀和 | 困难 | | [0411. 最短独占单词缩写](https://leetcode.cn/problems/minimum-unique-word-abbreviation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-unique-word-abbreviation.md) | 位运算、数组、字符串、回溯 | 困难 | | [0412. Fizz Buzz](https://leetcode.cn/problems/fizz-buzz/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/fizz-buzz.md) | 数学、字符串、模拟 | 简单 | | [0413. 等差数列划分](https://leetcode.cn/problems/arithmetic-slices/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/arithmetic-slices.md) | 数组、动态规划、滑动窗口 | 中等 | | [0414. 第三大的数](https://leetcode.cn/problems/third-maximum-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/third-maximum-number.md) | 数组、排序 | 简单 | | [0415. 字符串相加](https://leetcode.cn/problems/add-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-strings.md) | 数学、字符串、模拟 | 简单 | | [0416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/partition-equal-subset-sum.md) | 数组、动态规划 | 中等 | | [0417. 太平洋大西洋水流问题](https://leetcode.cn/problems/pacific-atlantic-water-flow/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/pacific-atlantic-water-flow.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 中等 | | [0418. 屏幕可显示句子的数量](https://leetcode.cn/problems/sentence-screen-fitting/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sentence-screen-fitting.md) | 数组、字符串、动态规划 | 中等 | | [0419. 棋盘上的战舰](https://leetcode.cn/problems/battleships-in-a-board/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/battleships-in-a-board.md) | 深度优先搜索、数组、矩阵 | 中等 | | [0420. 强密码检验器](https://leetcode.cn/problems/strong-password-checker/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/strong-password-checker.md) | 贪心、字符串、堆(优先队列) | 困难 | | [0421. 数组中两个数的最大异或值](https://leetcode.cn/problems/maximum-xor-of-two-numbers-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/maximum-xor-of-two-numbers-in-an-array.md) | 位运算、字典树、数组、哈希表 | 中等 | | [0422. 有效的单词方块](https://leetcode.cn/problems/valid-word-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/valid-word-square.md) | 数组、矩阵 | 简单 | | [0423. 从英文中重建数字](https://leetcode.cn/problems/reconstruct-original-digits-from-english/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/reconstruct-original-digits-from-english.md) | 哈希表、数学、字符串 | 中等 | | [0424. 替换后的最长重复字符](https://leetcode.cn/problems/longest-repeating-character-replacement/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/longest-repeating-character-replacement.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0425. 单词方块](https://leetcode.cn/problems/word-squares/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/word-squares.md) | 字典树、数组、字符串、回溯 | 困难 | | [0426. 将二叉搜索树转化为排序的双向链表](https://leetcode.cn/problems/convert-binary-search-tree-to-sorted-doubly-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/convert-binary-search-tree-to-sorted-doubly-linked-list.md) | 栈、树、深度优先搜索、二叉搜索树、链表、二叉树、双向链表 | 中等 | | [0427. 建立四叉树](https://leetcode.cn/problems/construct-quad-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/construct-quad-tree.md) | 树、数组、分治、矩阵 | 中等 | | [0428. 序列化和反序列化 N 叉树](https://leetcode.cn/problems/serialize-and-deserialize-n-ary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/serialize-and-deserialize-n-ary-tree.md) | 树、深度优先搜索、广度优先搜索、字符串 | 困难 | | [0429. N 叉树的层序遍历](https://leetcode.cn/problems/n-ary-tree-level-order-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/n-ary-tree-level-order-traversal.md) | 树、广度优先搜索 | 中等 | | [0430. 扁平化多级双向链表](https://leetcode.cn/problems/flatten-a-multilevel-doubly-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/flatten-a-multilevel-doubly-linked-list.md) | 深度优先搜索、链表、双向链表 | 中等 | | [0431. 将 N 叉树编码为二叉树](https://leetcode.cn/problems/encode-n-ary-tree-to-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/encode-n-ary-tree-to-binary-tree.md) | 树、深度优先搜索、广度优先搜索、设计、二叉树 | 困难 | | [0432. 全 O(1) 的数据结构](https://leetcode.cn/problems/all-oone-data-structure/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/all-oone-data-structure.md) | 设计、哈希表、链表、双向链表 | 困难 | | [0433. 最小基因变化](https://leetcode.cn/problems/minimum-genetic-mutation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-genetic-mutation.md) | 广度优先搜索、哈希表、字符串 | 中等 | | [0434. 字符串中的单词数](https://leetcode.cn/problems/number-of-segments-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/number-of-segments-in-a-string.md) | 字符串 | 简单 | | [0435. 无重叠区间](https://leetcode.cn/problems/non-overlapping-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/non-overlapping-intervals.md) | 贪心、数组、动态规划、排序 | 中等 | | [0436. 寻找右区间](https://leetcode.cn/problems/find-right-interval/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-right-interval.md) | 数组、二分查找、排序 | 中等 | | [0437. 路径总和 III](https://leetcode.cn/problems/path-sum-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/path-sum-iii.md) | 树、深度优先搜索、二叉树 | 中等 | | [0438. 找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-all-anagrams-in-a-string.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0439. 三元表达式解析器](https://leetcode.cn/problems/ternary-expression-parser/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/ternary-expression-parser.md) | 栈、递归、字符串 | 中等 | | [0440. 字典序的第K小数字](https://leetcode.cn/problems/k-th-smallest-in-lexicographical-order/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/k-th-smallest-in-lexicographical-order.md) | 字典树 | 困难 | | [0441. 排列硬币](https://leetcode.cn/problems/arranging-coins/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/arranging-coins.md) | 数学、二分查找 | 简单 | | [0442. 数组中重复的数据](https://leetcode.cn/problems/find-all-duplicates-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-all-duplicates-in-an-array.md) | 数组、哈希表 | 中等 | | [0443. 压缩字符串](https://leetcode.cn/problems/string-compression/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/string-compression.md) | 双指针、字符串 | 中等 | | [0444. 序列重建](https://leetcode.cn/problems/sequence-reconstruction/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sequence-reconstruction.md) | 图、拓扑排序、数组 | 中等 | | [0445. 两数相加 II](https://leetcode.cn/problems/add-two-numbers-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-two-numbers-ii.md) | 栈、链表、数学 | 中等 | | [0446. 等差数列划分 II - 子序列](https://leetcode.cn/problems/arithmetic-slices-ii-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/arithmetic-slices-ii-subsequence.md) | 数组、动态规划 | 困难 | | [0447. 回旋镖的数量](https://leetcode.cn/problems/number-of-boomerangs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/number-of-boomerangs.md) | 数组、哈希表、数学 | 中等 | | [0448. 找到所有数组中消失的数字](https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-all-numbers-disappeared-in-an-array.md) | 数组、哈希表 | 简单 | | [0449. 序列化和反序列化二叉搜索树](https://leetcode.cn/problems/serialize-and-deserialize-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/serialize-and-deserialize-bst.md) | 树、深度优先搜索、广度优先搜索、设计、二叉搜索树、字符串、二叉树 | 中等 | | [0450. 删除二叉搜索树中的节点](https://leetcode.cn/problems/delete-node-in-a-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/delete-node-in-a-bst.md) | 树、二叉搜索树、二叉树 | 中等 | | [0451. 根据字符出现频率排序](https://leetcode.cn/problems/sort-characters-by-frequency/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sort-characters-by-frequency.md) | 哈希表、字符串、桶排序、计数、排序、堆(优先队列) | 中等 | | [0452. 用最少数量的箭引爆气球](https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-number-of-arrows-to-burst-balloons.md) | 贪心、数组、排序 | 中等 | | [0453. 最小操作次数使数组元素相等](https://leetcode.cn/problems/minimum-moves-to-equal-array-elements/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-moves-to-equal-array-elements.md) | 数组、数学 | 中等 | | [0454. 四数相加 II](https://leetcode.cn/problems/4sum-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/4sum-ii.md) | 数组、哈希表 | 中等 | | [0455. 分发饼干](https://leetcode.cn/problems/assign-cookies/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/assign-cookies.md) | 贪心、数组、双指针、排序 | 简单 | | [0456. 132 模式](https://leetcode.cn/problems/132-pattern/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/132-pattern.md) | 栈、数组、二分查找、有序集合、单调栈 | 中等 | | [0457. 环形数组是否存在循环](https://leetcode.cn/problems/circular-array-loop/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/circular-array-loop.md) | 数组、哈希表、双指针 | 中等 | | [0458. 可怜的小猪](https://leetcode.cn/problems/poor-pigs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/poor-pigs.md) | 数学、动态规划、组合数学 | 困难 | | [0459. 重复的子字符串](https://leetcode.cn/problems/repeated-substring-pattern/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/repeated-substring-pattern.md) | 字符串、字符串匹配 | 简单 | | [0460. LFU 缓存](https://leetcode.cn/problems/lfu-cache/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/lfu-cache.md) | 设计、哈希表、链表、双向链表 | 困难 | | [0461. 汉明距离](https://leetcode.cn/problems/hamming-distance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/hamming-distance.md) | 位运算 | 简单 | | [0462. 最小操作次数使数组元素相等 II](https://leetcode.cn/problems/minimum-moves-to-equal-array-elements-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-moves-to-equal-array-elements-ii.md) | 数组、数学、排序 | 中等 | | [0463. 岛屿的周长](https://leetcode.cn/problems/island-perimeter/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/island-perimeter.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 简单 | | [0464. 我能赢吗](https://leetcode.cn/problems/can-i-win/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/can-i-win.md) | 位运算、记忆化搜索、数学、动态规划、状态压缩、博弈 | 中等 | | [0465. 最优账单平衡](https://leetcode.cn/problems/optimal-account-balancing/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/optimal-account-balancing.md) | 位运算、数组、动态规划、回溯、状态压缩 | 困难 | | [0466. 统计重复个数](https://leetcode.cn/problems/count-the-repetitions/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/count-the-repetitions.md) | 字符串、动态规划 | 困难 | | [0467. 环绕字符串中唯一的子字符串](https://leetcode.cn/problems/unique-substrings-in-wraparound-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/unique-substrings-in-wraparound-string.md) | 字符串、动态规划 | 中等 | | [0468. 验证IP地址](https://leetcode.cn/problems/validate-ip-address/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/validate-ip-address.md) | 字符串 | 中等 | | [0469. 凸多边形](https://leetcode.cn/problems/convex-polygon/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/convex-polygon.md) | 几何、数组、数学 | 中等 | | [0470. 用 Rand7() 实现 Rand10()](https://leetcode.cn/problems/implement-rand10-using-rand7/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/implement-rand10-using-rand7.md) | 数学、拒绝采样、概率与统计、随机化 | 中等 | | [0471. 编码最短长度的字符串](https://leetcode.cn/problems/encode-string-with-shortest-length/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/encode-string-with-shortest-length.md) | 字符串、动态规划 | 困难 | | [0472. 连接词](https://leetcode.cn/problems/concatenated-words/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/concatenated-words.md) | 深度优先搜索、字典树、数组、字符串、动态规划、排序 | 困难 | | [0473. 火柴拼正方形](https://leetcode.cn/problems/matchsticks-to-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/matchsticks-to-square.md) | 位运算、数组、动态规划、回溯、状态压缩 | 中等 | | [0474. 一和零](https://leetcode.cn/problems/ones-and-zeroes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/ones-and-zeroes.md) | 数组、字符串、动态规划 | 中等 | | [0475. 供暖器](https://leetcode.cn/problems/heaters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/heaters.md) | 数组、双指针、二分查找、排序 | 中等 | | [0476. 数字的补数](https://leetcode.cn/problems/number-complement/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/number-complement.md) | 位运算 | 简单 | | [0477. 汉明距离总和](https://leetcode.cn/problems/total-hamming-distance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/total-hamming-distance.md) | 位运算、数组、数学 | 中等 | | [0478. 在圆内随机生成点](https://leetcode.cn/problems/generate-random-point-in-a-circle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/generate-random-point-in-a-circle.md) | 几何、数学、拒绝采样、随机化 | 中等 | | [0479. 最大回文数乘积](https://leetcode.cn/problems/largest-palindrome-product/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/largest-palindrome-product.md) | 数学、枚举 | 困难 | | [0480. 滑动窗口中位数](https://leetcode.cn/problems/sliding-window-median/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sliding-window-median.md) | 数组、哈希表、滑动窗口、堆(优先队列) | 困难 | | [0481. 神奇字符串](https://leetcode.cn/problems/magical-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/magical-string.md) | 双指针、字符串 | 中等 | | [0482. 密钥格式化](https://leetcode.cn/problems/license-key-formatting/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/license-key-formatting.md) | 字符串 | 简单 | | [0483. 最小好进制](https://leetcode.cn/problems/smallest-good-base/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/smallest-good-base.md) | 数学、二分查找 | 困难 | | [0484. 寻找排列](https://leetcode.cn/problems/find-permutation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-permutation.md) | 栈、贪心、数组、字符串 | 中等 | | [0485. 最大连续 1 的个数](https://leetcode.cn/problems/max-consecutive-ones/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/max-consecutive-ones.md) | 数组 | 简单 | | [0486. 预测赢家](https://leetcode.cn/problems/predict-the-winner/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/predict-the-winner.md) | 递归、数组、数学、动态规划、博弈 | 中等 | | [0487. 最大连续1的个数 II](https://leetcode.cn/problems/max-consecutive-ones-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/max-consecutive-ones-ii.md) | 数组、动态规划、滑动窗口 | 中等 | | [0488. 祖玛游戏](https://leetcode.cn/problems/zuma-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/zuma-game.md) | 栈、广度优先搜索、记忆化搜索、字符串、动态规划 | 困难 | | [0489. 扫地机器人](https://leetcode.cn/problems/robot-room-cleaner/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/robot-room-cleaner.md) | 回溯、交互 | 困难 | | [0490. 迷宫](https://leetcode.cn/problems/the-maze/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/the-maze.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 中等 | | [0491. 非递减子序列](https://leetcode.cn/problems/non-decreasing-subsequences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/non-decreasing-subsequences.md) | 位运算、数组、哈希表、回溯 | 中等 | | [0492. 构造矩形](https://leetcode.cn/problems/construct-the-rectangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/construct-the-rectangle.md) | 数学 | 简单 | | [0493. 翻转对](https://leetcode.cn/problems/reverse-pairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/reverse-pairs.md) | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 | | [0494. 目标和](https://leetcode.cn/problems/target-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/target-sum.md) | 数组、动态规划、回溯 | 中等 | | [0495. 提莫攻击](https://leetcode.cn/problems/teemo-attacking/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/teemo-attacking.md) | 数组、模拟 | 简单 | | [0496. 下一个更大元素 I](https://leetcode.cn/problems/next-greater-element-i/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/next-greater-element-i.md) | 栈、数组、哈希表、单调栈 | 简单 | | [0497. 非重叠矩形中的随机点](https://leetcode.cn/problems/random-point-in-non-overlapping-rectangles/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/random-point-in-non-overlapping-rectangles.md) | 水塘抽样、数组、数学、二分查找、有序集合、前缀和、随机化 | 中等 | | [0498. 对角线遍历](https://leetcode.cn/problems/diagonal-traverse/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/diagonal-traverse.md) | 数组、矩阵、模拟 | 中等 | | [0499. 迷宫 III](https://leetcode.cn/problems/the-maze-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/the-maze-iii.md) | 深度优先搜索、广度优先搜索、图、数组、字符串、矩阵、最短路、堆(优先队列) | 困难 | ### 第 500 ~ 599 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0500. 键盘行](https://leetcode.cn/problems/keyboard-row/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/keyboard-row.md) | 数组、哈希表、字符串 | 简单 | | [0501. 二叉搜索树中的众数](https://leetcode.cn/problems/find-mode-in-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/find-mode-in-binary-search-tree.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 简单 | | [0502. IPO](https://leetcode.cn/problems/ipo/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/ipo.md) | 贪心、数组、排序、堆(优先队列) | 困难 | | [0503. 下一个更大元素 II](https://leetcode.cn/problems/next-greater-element-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/next-greater-element-ii.md) | 栈、数组、单调栈 | 中等 | | [0504. 七进制数](https://leetcode.cn/problems/base-7/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/base-7.md) | 数学、字符串 | 简单 | | [0505. 迷宫 II](https://leetcode.cn/problems/the-maze-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/the-maze-ii.md) | 深度优先搜索、广度优先搜索、图、数组、矩阵、最短路、堆(优先队列) | 中等 | | [0506. 相对名次](https://leetcode.cn/problems/relative-ranks/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/relative-ranks.md) | 数组、排序、堆(优先队列) | 简单 | | [0507. 完美数](https://leetcode.cn/problems/perfect-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/perfect-number.md) | 数学 | 简单 | | [0508. 出现次数最多的子树元素和](https://leetcode.cn/problems/most-frequent-subtree-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/most-frequent-subtree-sum.md) | 树、深度优先搜索、哈希表、二叉树 | 中等 | | [0509. 斐波那契数](https://leetcode.cn/problems/fibonacci-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/fibonacci-number.md) | 递归、记忆化搜索、数学、动态规划 | 简单 | | [0510. 二叉搜索树中的中序后继 II](https://leetcode.cn/problems/inorder-successor-in-bst-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/inorder-successor-in-bst-ii.md) | 树、二叉搜索树、二叉树 | 中等 | | [0513. 找树左下角的值](https://leetcode.cn/problems/find-bottom-left-tree-value/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/find-bottom-left-tree-value.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0514. 自由之路](https://leetcode.cn/problems/freedom-trail/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/freedom-trail.md) | 深度优先搜索、广度优先搜索、字符串、动态规划 | 困难 | | [0515. 在每个树行中找最大值](https://leetcode.cn/problems/find-largest-value-in-each-tree-row/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/find-largest-value-in-each-tree-row.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-palindromic-subsequence.md) | 字符串、动态规划 | 中等 | | [0517. 超级洗衣机](https://leetcode.cn/problems/super-washing-machines/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/super-washing-machines.md) | 贪心、数组 | 困难 | | [0518. 零钱兑换 II](https://leetcode.cn/problems/coin-change-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/coin-change-ii.md) | 数组、动态规划 | 中等 | | [0519. 随机翻转矩阵](https://leetcode.cn/problems/random-flip-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/random-flip-matrix.md) | 水塘抽样、哈希表、数学、随机化 | 中等 | | [0520. 检测大写字母](https://leetcode.cn/problems/detect-capital/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/detect-capital.md) | 字符串 | 简单 | | [0521. 最长特殊序列 Ⅰ](https://leetcode.cn/problems/longest-uncommon-subsequence-i/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-uncommon-subsequence-i.md) | 字符串 | 简单 | | [0522. 最长特殊序列 II](https://leetcode.cn/problems/longest-uncommon-subsequence-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-uncommon-subsequence-ii.md) | 数组、哈希表、双指针、字符串、排序 | 中等 | | [0523. 连续的子数组和](https://leetcode.cn/problems/continuous-subarray-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/continuous-subarray-sum.md) | 数组、哈希表、数学、前缀和 | 中等 | | [0524. 通过删除字母匹配到字典里最长单词](https://leetcode.cn/problems/longest-word-in-dictionary-through-deleting/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-word-in-dictionary-through-deleting.md) | 数组、双指针、字符串、排序 | 中等 | | [0525. 连续数组](https://leetcode.cn/problems/contiguous-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/contiguous-array.md) | 数组、哈希表、前缀和 | 中等 | | [0526. 优美的排列](https://leetcode.cn/problems/beautiful-arrangement/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/beautiful-arrangement.md) | 位运算、数组、动态规划、回溯、状态压缩 | 中等 | | [0527. 单词缩写](https://leetcode.cn/problems/word-abbreviation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/word-abbreviation.md) | 贪心、字典树、数组、字符串、排序 | 困难 | | [0528. 按权重随机选择](https://leetcode.cn/problems/random-pick-with-weight/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/random-pick-with-weight.md) | 数组、数学、二分查找、前缀和、随机化 | 中等 | | [0529. 扫雷游戏](https://leetcode.cn/problems/minesweeper/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/minesweeper.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 中等 | | [0530. 二叉搜索树的最小绝对差](https://leetcode.cn/problems/minimum-absolute-difference-in-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/minimum-absolute-difference-in-bst.md) | 树、深度优先搜索、广度优先搜索、二叉搜索树、二叉树 | 简单 | | [0531. 孤独像素 I](https://leetcode.cn/problems/lonely-pixel-i/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/lonely-pixel-i.md) | 数组、哈希表、矩阵 | 中等 | | [0532. 数组中的 k-diff 数对](https://leetcode.cn/problems/k-diff-pairs-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/k-diff-pairs-in-an-array.md) | 数组、哈希表、双指针、二分查找、排序 | 中等 | | [0533. 孤独像素 II](https://leetcode.cn/problems/lonely-pixel-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/lonely-pixel-ii.md) | 数组、哈希表、矩阵 | 中等 | | [0535. TinyURL 的加密与解密](https://leetcode.cn/problems/encode-and-decode-tinyurl/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/encode-and-decode-tinyurl.md) | 设计、哈希表、字符串、哈希函数 | 中等 | | [0536. 从字符串生成二叉树](https://leetcode.cn/problems/construct-binary-tree-from-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/construct-binary-tree-from-string.md) | 栈、树、深度优先搜索、字符串、二叉树 | 中等 | | [0537. 复数乘法](https://leetcode.cn/problems/complex-number-multiplication/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/complex-number-multiplication.md) | 数学、字符串、模拟 | 中等 | | [0538. 把二叉搜索树转换为累加树](https://leetcode.cn/problems/convert-bst-to-greater-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/convert-bst-to-greater-tree.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0539. 最小时间差](https://leetcode.cn/problems/minimum-time-difference/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/minimum-time-difference.md) | 数组、数学、字符串、排序 | 中等 | | [0540. 有序数组中的单一元素](https://leetcode.cn/problems/single-element-in-a-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/single-element-in-a-sorted-array.md) | 数组、二分查找 | 中等 | | [0541. 反转字符串 II](https://leetcode.cn/problems/reverse-string-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/reverse-string-ii.md) | 双指针、字符串 | 简单 | | [0542. 01 矩阵](https://leetcode.cn/problems/01-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/01-matrix.md) | 广度优先搜索、数组、动态规划、矩阵 | 中等 | | [0543. 二叉树的直径](https://leetcode.cn/problems/diameter-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/diameter-of-binary-tree.md) | 树、深度优先搜索、二叉树 | 简单 | | [0544. 输出比赛匹配对](https://leetcode.cn/problems/output-contest-matches/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/output-contest-matches.md) | 递归、字符串、模拟 | 中等 | | [0545. 二叉树的边界](https://leetcode.cn/problems/boundary-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/boundary-of-binary-tree.md) | 树、深度优先搜索、二叉树 | 中等 | | [0546. 移除盒子](https://leetcode.cn/problems/remove-boxes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/remove-boxes.md) | 记忆化搜索、数组、动态规划 | 困难 | | [0547. 省份数量](https://leetcode.cn/problems/number-of-provinces/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/number-of-provinces.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0548. 将数组分割成和相等的子数组](https://leetcode.cn/problems/split-array-with-equal-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/split-array-with-equal-sum.md) | 数组、哈希表、前缀和 | 困难 | | [0549. 二叉树最长连续序列 II](https://leetcode.cn/problems/binary-tree-longest-consecutive-sequence-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/binary-tree-longest-consecutive-sequence-ii.md) | 树、深度优先搜索、二叉树 | 中等 | | [0551. 学生出勤记录 I](https://leetcode.cn/problems/student-attendance-record-i/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/student-attendance-record-i.md) | 字符串 | 简单 | | [0552. 学生出勤记录 II](https://leetcode.cn/problems/student-attendance-record-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/student-attendance-record-ii.md) | 动态规划 | 困难 | | [0553. 最优除法](https://leetcode.cn/problems/optimal-division/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/optimal-division.md) | 数组、数学、动态规划 | 中等 | | [0554. 砖墙](https://leetcode.cn/problems/brick-wall/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/brick-wall.md) | 数组、哈希表 | 中等 | | [0555. 分割连接字符串](https://leetcode.cn/problems/split-concatenated-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/split-concatenated-strings.md) | 贪心、数组、字符串 | 中等 | | [0556. 下一个更大元素 III](https://leetcode.cn/problems/next-greater-element-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/next-greater-element-iii.md) | 数学、双指针、字符串 | 中等 | | [0557. 反转字符串中的单词 III](https://leetcode.cn/problems/reverse-words-in-a-string-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/reverse-words-in-a-string-iii.md) | 双指针、字符串 | 简单 | | [0558. 四叉树交集](https://leetcode.cn/problems/logical-or-of-two-binary-grids-represented-as-quad-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/logical-or-of-two-binary-grids-represented-as-quad-trees.md) | 树、分治 | 中等 | | [0559. N 叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-n-ary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/maximum-depth-of-n-ary-tree.md) | 树、深度优先搜索、广度优先搜索 | 简单 | | [0560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subarray-sum-equals-k.md) | 数组、哈希表、前缀和 | 中等 | | [0561. 数组拆分](https://leetcode.cn/problems/array-partition/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/array-partition.md) | 贪心、数组、计数排序、排序 | 简单 | | [0562. 矩阵中最长的连续1线段](https://leetcode.cn/problems/longest-line-of-consecutive-one-in-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-line-of-consecutive-one-in-matrix.md) | 数组、动态规划、矩阵 | 中等 | | [0563. 二叉树的坡度](https://leetcode.cn/problems/binary-tree-tilt/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/binary-tree-tilt.md) | 树、深度优先搜索、二叉树 | 简单 | | [0564. 寻找最近的回文数](https://leetcode.cn/problems/find-the-closest-palindrome/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/find-the-closest-palindrome.md) | 数学、字符串 | 困难 | | [0565. 数组嵌套](https://leetcode.cn/problems/array-nesting/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/array-nesting.md) | 深度优先搜索、数组 | 中等 | | [0566. 重塑矩阵](https://leetcode.cn/problems/reshape-the-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/reshape-the-matrix.md) | 数组、矩阵、模拟 | 简单 | | [0567. 字符串的排列](https://leetcode.cn/problems/permutation-in-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/permutation-in-string.md) | 哈希表、双指针、字符串、滑动窗口 | 中等 | | [0568. 最大休假天数](https://leetcode.cn/problems/maximum-vacation-days/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/maximum-vacation-days.md) | 数组、动态规划、矩阵 | 困难 | | [0572. 另一棵树的子树](https://leetcode.cn/problems/subtree-of-another-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subtree-of-another-tree.md) | 树、深度优先搜索、二叉树、字符串匹配、哈希函数 | 简单 | | [0573. 松鼠模拟](https://leetcode.cn/problems/squirrel-simulation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/squirrel-simulation.md) | 数组、数学 | 中等 | | [0575. 分糖果](https://leetcode.cn/problems/distribute-candies/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/distribute-candies.md) | 数组、哈希表 | 简单 | | [0576. 出界的路径数](https://leetcode.cn/problems/out-of-boundary-paths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/out-of-boundary-paths.md) | 动态规划 | 中等 | | [0581. 最短无序连续子数组](https://leetcode.cn/problems/shortest-unsorted-continuous-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/shortest-unsorted-continuous-subarray.md) | 栈、贪心、数组、双指针、排序、单调栈 | 中等 | | [0582. 杀掉进程](https://leetcode.cn/problems/kill-process/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/kill-process.md) | 树、深度优先搜索、广度优先搜索、数组、哈希表 | 中等 | | [0583. 两个字符串的删除操作](https://leetcode.cn/problems/delete-operation-for-two-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/delete-operation-for-two-strings.md) | 字符串、动态规划 | 中等 | | [0587. 安装栅栏](https://leetcode.cn/problems/erect-the-fence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/erect-the-fence.md) | 几何、数组、数学 | 困难 | | [0588. 设计内存文件系统](https://leetcode.cn/problems/design-in-memory-file-system/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/design-in-memory-file-system.md) | 设计、字典树、哈希表、字符串、排序 | 困难 | | [0589. N 叉树的前序遍历](https://leetcode.cn/problems/n-ary-tree-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/n-ary-tree-preorder-traversal.md) | 栈、树、深度优先搜索 | 简单 | | [0590. N 叉树的后序遍历](https://leetcode.cn/problems/n-ary-tree-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/n-ary-tree-postorder-traversal.md) | 栈、树、深度优先搜索 | 简单 | | [0591. 标签验证器](https://leetcode.cn/problems/tag-validator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/tag-validator.md) | 栈、字符串 | 困难 | | [0592. 分数加减运算](https://leetcode.cn/problems/fraction-addition-and-subtraction/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/fraction-addition-and-subtraction.md) | 数学、字符串、模拟 | 中等 | | [0593. 有效的正方形](https://leetcode.cn/problems/valid-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/valid-square.md) | 几何、数学 | 中等 | | [0594. 最长和谐子序列](https://leetcode.cn/problems/longest-harmonious-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-harmonious-subsequence.md) | 数组、哈希表、计数、排序、滑动窗口 | 简单 | | [0598. 区间加法 II](https://leetcode.cn/problems/range-addition-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/range-addition-ii.md) | 数组、数学 | 简单 | | [0599. 两个列表的最小索引总和](https://leetcode.cn/problems/minimum-index-sum-of-two-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/minimum-index-sum-of-two-lists.md) | 数组、哈希表、字符串 | 简单 | ### 第 600 ~ 699 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0600. 不含连续1的非负整数](https://leetcode.cn/problems/non-negative-integers-without-consecutive-ones/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/non-negative-integers-without-consecutive-ones.md) | 动态规划 | 困难 | | [0604. 迭代压缩字符串](https://leetcode.cn/problems/design-compressed-string-iterator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-compressed-string-iterator.md) | 设计、数组、字符串、迭代器 | 简单 | | [0605. 种花问题](https://leetcode.cn/problems/can-place-flowers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/can-place-flowers.md) | 贪心、数组 | 简单 | | [0606. 根据二叉树创建字符串](https://leetcode.cn/problems/construct-string-from-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/construct-string-from-binary-tree.md) | 树、深度优先搜索、字符串、二叉树 | 中等 | | [0609. 在系统中查找重复文件](https://leetcode.cn/problems/find-duplicate-file-in-system/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/find-duplicate-file-in-system.md) | 数组、哈希表、字符串 | 中等 | | [0611. 有效三角形的个数](https://leetcode.cn/problems/valid-triangle-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/valid-triangle-number.md) | 贪心、数组、双指针、二分查找、排序 | 中等 | | [0616. 给字符串添加加粗标签](https://leetcode.cn/problems/add-bold-tag-in-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/add-bold-tag-in-string.md) | 字典树、数组、哈希表、字符串、字符串匹配 | 中等 | | [0617. 合并二叉树](https://leetcode.cn/problems/merge-two-binary-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/merge-two-binary-trees.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0621. 任务调度器](https://leetcode.cn/problems/task-scheduler/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/task-scheduler.md) | 贪心、数组、哈希表、计数、排序、堆(优先队列) | 中等 | | [0622. 设计循环队列](https://leetcode.cn/problems/design-circular-queue/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-circular-queue.md) | 设计、队列、数组、链表 | 中等 | | [0623. 在二叉树中增加一行](https://leetcode.cn/problems/add-one-row-to-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/add-one-row-to-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0624. 数组列表中的最大距离](https://leetcode.cn/problems/maximum-distance-in-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-distance-in-arrays.md) | 贪心、数组 | 中等 | | [0625. 最小因式分解](https://leetcode.cn/problems/minimum-factorization/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/minimum-factorization.md) | 贪心、数学 | 中等 | | [0628. 三个数的最大乘积](https://leetcode.cn/problems/maximum-product-of-three-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-product-of-three-numbers.md) | 数组、数学、排序 | 简单 | | [0629. K 个逆序对数组](https://leetcode.cn/problems/k-inverse-pairs-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/k-inverse-pairs-array.md) | 动态规划 | 困难 | | [0630. 课程表 III](https://leetcode.cn/problems/course-schedule-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/course-schedule-iii.md) | 贪心、数组、排序、堆(优先队列) | 困难 | | [0631. 设计 Excel 求和公式](https://leetcode.cn/problems/design-excel-sum-formula/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-excel-sum-formula.md) | 图、设计、拓扑排序、数组、哈希表、字符串、矩阵 | 困难 | | [0632. 最小区间](https://leetcode.cn/problems/smallest-range-covering-elements-from-k-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/smallest-range-covering-elements-from-k-lists.md) | 贪心、数组、哈希表、排序、滑动窗口、堆(优先队列) | 困难 | | [0633. 平方数之和](https://leetcode.cn/problems/sum-of-square-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/sum-of-square-numbers.md) | 数学、双指针、二分查找 | 中等 | | [0634. 寻找数组的错位排列](https://leetcode.cn/problems/find-the-derangement-of-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/find-the-derangement-of-an-array.md) | 数学、动态规划、组合数学 | 中等 | | [0635. 设计日志存储系统](https://leetcode.cn/problems/design-log-storage-system/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-log-storage-system.md) | 设计、哈希表、字符串、有序集合 | 中等 | | [0636. 函数的独占时间](https://leetcode.cn/problems/exclusive-time-of-functions/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/exclusive-time-of-functions.md) | 栈、数组 | 中等 | | [0637. 二叉树的层平均值](https://leetcode.cn/problems/average-of-levels-in-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/average-of-levels-in-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0638. 大礼包](https://leetcode.cn/problems/shopping-offers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/shopping-offers.md) | 位运算、记忆化搜索、数组、动态规划、回溯、状态压缩 | 中等 | | [0639. 解码方法 II](https://leetcode.cn/problems/decode-ways-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/decode-ways-ii.md) | 字符串、动态规划 | 困难 | | [0640. 求解方程](https://leetcode.cn/problems/solve-the-equation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/solve-the-equation.md) | 数学、字符串、模拟 | 中等 | | [0641. 设计循环双端队列](https://leetcode.cn/problems/design-circular-deque/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-circular-deque.md) | 设计、队列、数组、链表 | 中等 | | [0642. 设计搜索自动补全系统](https://leetcode.cn/problems/design-search-autocomplete-system/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-search-autocomplete-system.md) | 深度优先搜索、设计、字典树、字符串、数据流、排序、堆(优先队列) | 困难 | | [0643. 子数组最大平均数 I](https://leetcode.cn/problems/maximum-average-subarray-i/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-average-subarray-i.md) | 数组、滑动窗口 | 简单 | | [0644. 子数组最大平均数 II](https://leetcode.cn/problems/maximum-average-subarray-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-average-subarray-ii.md) | 数组、二分查找、前缀和 | 困难 | | [0645. 错误的集合](https://leetcode.cn/problems/set-mismatch/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/set-mismatch.md) | 位运算、数组、哈希表、排序 | 简单 | | [0646. 最长数对链](https://leetcode.cn/problems/maximum-length-of-pair-chain/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-length-of-pair-chain.md) | 贪心、数组、动态规划、排序 | 中等 | | [0647. 回文子串](https://leetcode.cn/problems/palindromic-substrings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/palindromic-substrings.md) | 双指针、字符串、动态规划 | 中等 | | [0648. 单词替换](https://leetcode.cn/problems/replace-words/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/replace-words.md) | 字典树、数组、哈希表、字符串 | 中等 | | [0649. Dota2 参议院](https://leetcode.cn/problems/dota2-senate/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/dota2-senate.md) | 贪心、队列、字符串 | 中等 | | [0650. 两个键的键盘](https://leetcode.cn/problems/2-keys-keyboard/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/2-keys-keyboard.md) | 数学、动态规划 | 中等 | | [0651. 四个键的键盘](https://leetcode.cn/problems/4-keys-keyboard/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/4-keys-keyboard.md) | 数学、动态规划 | 中等 | | [0652. 寻找重复的子树](https://leetcode.cn/problems/find-duplicate-subtrees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/find-duplicate-subtrees.md) | 树、深度优先搜索、哈希表、二叉树 | 中等 | | [0653. 两数之和 IV - 输入二叉搜索树](https://leetcode.cn/problems/two-sum-iv-input-is-a-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/two-sum-iv-input-is-a-bst.md) | 树、深度优先搜索、广度优先搜索、二叉搜索树、哈希表、双指针、二叉树 | 简单 | | [0654. 最大二叉树](https://leetcode.cn/problems/maximum-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-binary-tree.md) | 栈、树、数组、分治、二叉树、单调栈 | 中等 | | [0655. 输出二叉树](https://leetcode.cn/problems/print-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/print-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0656. 成本最小路径](https://leetcode.cn/problems/coin-path/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/coin-path.md) | 数组、动态规划 | 困难 | | [0657. 机器人能否返回原点](https://leetcode.cn/problems/robot-return-to-origin/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/robot-return-to-origin.md) | 字符串、模拟 | 简单 | | [0658. 找到 K 个最接近的元素](https://leetcode.cn/problems/find-k-closest-elements/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/find-k-closest-elements.md) | 数组、双指针、二分查找、排序、滑动窗口、堆(优先队列) | 中等 | | [0659. 分割数组为连续子序列](https://leetcode.cn/problems/split-array-into-consecutive-subsequences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/split-array-into-consecutive-subsequences.md) | 贪心、数组、哈希表、堆(优先队列) | 中等 | | [0660. 移除 9](https://leetcode.cn/problems/remove-9/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/remove-9.md) | 数学 | 困难 | | [0661. 图片平滑器](https://leetcode.cn/problems/image-smoother/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/image-smoother.md) | 数组、矩阵 | 简单 | | [0662. 二叉树最大宽度](https://leetcode.cn/problems/maximum-width-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-width-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0663. 均匀树划分](https://leetcode.cn/problems/equal-tree-partition/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/equal-tree-partition.md) | 树、深度优先搜索、二叉树 | 中等 | | [0664. 奇怪的打印机](https://leetcode.cn/problems/strange-printer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/strange-printer.md) | 字符串、动态规划 | 困难 | | [0665. 非递减数列](https://leetcode.cn/problems/non-decreasing-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/non-decreasing-array.md) | 数组 | 中等 | | [0666. 路径总和 IV](https://leetcode.cn/problems/path-sum-iv/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/path-sum-iv.md) | 树、深度优先搜索、数组、哈希表、二叉树 | 中等 | | [0667. 优美的排列 II](https://leetcode.cn/problems/beautiful-arrangement-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/beautiful-arrangement-ii.md) | 数组、数学 | 中等 | | [0669. 修剪二叉搜索树](https://leetcode.cn/problems/trim-a-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/trim-a-binary-search-tree.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0670. 最大交换](https://leetcode.cn/problems/maximum-swap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-swap.md) | 贪心、数学 | 中等 | | [0671. 二叉树中第二小的节点](https://leetcode.cn/problems/second-minimum-node-in-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/second-minimum-node-in-a-binary-tree.md) | 树、深度优先搜索、二叉树 | 简单 | | [0672. 灯泡开关 Ⅱ](https://leetcode.cn/problems/bulb-switcher-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/bulb-switcher-ii.md) | 位运算、深度优先搜索、广度优先搜索、数学 | 中等 | | [0673. 最长递增子序列的个数](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/number-of-longest-increasing-subsequence.md) | 树状数组、线段树、数组、动态规划 | 中等 | | [0674. 最长连续递增序列](https://leetcode.cn/problems/longest-continuous-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/longest-continuous-increasing-subsequence.md) | 数组 | 简单 | | [0675. 为高尔夫比赛砍树](https://leetcode.cn/problems/cut-off-trees-for-golf-event/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/cut-off-trees-for-golf-event.md) | 广度优先搜索、数组、矩阵、堆(优先队列) | 困难 | | [0676. 实现一个魔法字典](https://leetcode.cn/problems/implement-magic-dictionary/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/implement-magic-dictionary.md) | 深度优先搜索、设计、字典树、哈希表、字符串 | 中等 | | [0677. 键值映射](https://leetcode.cn/problems/map-sum-pairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/map-sum-pairs.md) | 设计、字典树、哈希表、字符串 | 中等 | | [0678. 有效的括号字符串](https://leetcode.cn/problems/valid-parenthesis-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/valid-parenthesis-string.md) | 栈、贪心、字符串、动态规划 | 中等 | | [0679. 24 点游戏](https://leetcode.cn/problems/24-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/24-game.md) | 数组、数学、回溯 | 困难 | | [0680. 验证回文串 II](https://leetcode.cn/problems/valid-palindrome-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/valid-palindrome-ii.md) | 贪心、双指针、字符串 | 简单 | | [0681. 最近时刻](https://leetcode.cn/problems/next-closest-time/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/next-closest-time.md) | 哈希表、字符串、回溯、枚举 | 中等 | | [0682. 棒球比赛](https://leetcode.cn/problems/baseball-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/baseball-game.md) | 栈、数组、模拟 | 简单 | | [0683. K 个关闭的灯泡](https://leetcode.cn/problems/k-empty-slots/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/k-empty-slots.md) | 树状数组、线段树、队列、数组、有序集合、滑动窗口、单调队列、堆(优先队列) | 困难 | | [0684. 冗余连接](https://leetcode.cn/problems/redundant-connection/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/redundant-connection.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0685. 冗余连接 II](https://leetcode.cn/problems/redundant-connection-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/redundant-connection-ii.md) | 深度优先搜索、广度优先搜索、并查集、图 | 困难 | | [0686. 重复叠加字符串匹配](https://leetcode.cn/problems/repeated-string-match/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/repeated-string-match.md) | 字符串、字符串匹配 | 中等 | | [0687. 最长同值路径](https://leetcode.cn/problems/longest-univalue-path/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/longest-univalue-path.md) | 树、深度优先搜索、二叉树 | 中等 | | [0688. 骑士在棋盘上的概率](https://leetcode.cn/problems/knight-probability-in-chessboard/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/knight-probability-in-chessboard.md) | 动态规划 | 中等 | | [0689. 三个无重叠子数组的最大和](https://leetcode.cn/problems/maximum-sum-of-3-non-overlapping-subarrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-sum-of-3-non-overlapping-subarrays.md) | 数组、动态规划、前缀和、滑动窗口 | 困难 | | [0690. 员工的重要性](https://leetcode.cn/problems/employee-importance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/employee-importance.md) | 树、深度优先搜索、广度优先搜索、数组、哈希表 | 中等 | | [0691. 贴纸拼词](https://leetcode.cn/problems/stickers-to-spell-word/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/stickers-to-spell-word.md) | 位运算、记忆化搜索、数组、哈希表、字符串、动态规划、回溯、状态压缩 | 困难 | | [0692. 前K个高频单词](https://leetcode.cn/problems/top-k-frequent-words/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/top-k-frequent-words.md) | 字典树、数组、哈希表、字符串、桶排序、计数、排序、堆(优先队列) | 中等 | | [0693. 交替位二进制数](https://leetcode.cn/problems/binary-number-with-alternating-bits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/binary-number-with-alternating-bits.md) | 位运算 | 简单 | | [0694. 不同岛屿的数量](https://leetcode.cn/problems/number-of-distinct-islands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/number-of-distinct-islands.md) | 深度优先搜索、广度优先搜索、并查集、哈希表、哈希函数 | 中等 | | [0695. 岛屿的最大面积](https://leetcode.cn/problems/max-area-of-island/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/max-area-of-island.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0696. 计数二进制子串](https://leetcode.cn/problems/count-binary-substrings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/count-binary-substrings.md) | 双指针、字符串 | 简单 | | [0697. 数组的度](https://leetcode.cn/problems/degree-of-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/degree-of-an-array.md) | 数组、哈希表 | 简单 | | [0698. 划分为k个相等的子集](https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/partition-to-k-equal-sum-subsets.md) | 位运算、记忆化搜索、数组、动态规划、回溯、状态压缩 | 中等 | | [0699. 掉落的方块](https://leetcode.cn/problems/falling-squares/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/falling-squares.md) | 线段树、数组、有序集合 | 困难 | ### 第 700 ~ 799 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0700. 二叉搜索树中的搜索](https://leetcode.cn/problems/search-in-a-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/search-in-a-binary-search-tree.md) | 树、二叉搜索树、二叉树 | 简单 | | [0701. 二叉搜索树中的插入操作](https://leetcode.cn/problems/insert-into-a-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/insert-into-a-binary-search-tree.md) | 树、二叉搜索树、二叉树 | 中等 | | [0702. 搜索长度未知的有序数组](https://leetcode.cn/problems/search-in-a-sorted-array-of-unknown-size/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/search-in-a-sorted-array-of-unknown-size.md) | 数组、二分查找、交互 | 中等 | | [0703. 数据流中的第 K 大元素](https://leetcode.cn/problems/kth-largest-element-in-a-stream/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/kth-largest-element-in-a-stream.md) | 树、设计、二叉搜索树、二叉树、数据流、堆(优先队列) | 简单 | | [0704. 二分查找](https://leetcode.cn/problems/binary-search/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/binary-search.md) | 数组、二分查找 | 简单 | | [0705. 设计哈希集合](https://leetcode.cn/problems/design-hashset/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/design-hashset.md) | 设计、数组、哈希表、链表、哈希函数 | 简单 | | [0706. 设计哈希映射](https://leetcode.cn/problems/design-hashmap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/design-hashmap.md) | 设计、数组、哈希表、链表、哈希函数 | 简单 | | [0707. 设计链表](https://leetcode.cn/problems/design-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/design-linked-list.md) | 设计、链表 | 中等 | | [0708. 循环有序列表的插入](https://leetcode.cn/problems/insert-into-a-sorted-circular-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/insert-into-a-sorted-circular-linked-list.md) | 链表 | 中等 | | [0709. 转换成小写字母](https://leetcode.cn/problems/to-lower-case/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/to-lower-case.md) | 字符串 | 简单 | | [0710. 黑名单中的随机数](https://leetcode.cn/problems/random-pick-with-blacklist/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/random-pick-with-blacklist.md) | 数组、哈希表、数学、二分查找、排序、随机化 | 困难 | | [0712. 两个字符串的最小ASCII删除和](https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/minimum-ascii-delete-sum-for-two-strings.md) | 字符串、动态规划 | 中等 | | [0713. 乘积小于 K 的子数组](https://leetcode.cn/problems/subarray-product-less-than-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/subarray-product-less-than-k.md) | 数组、二分查找、前缀和、滑动窗口 | 中等 | | [0714. 买卖股票的最佳时机含手续费](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/best-time-to-buy-and-sell-stock-with-transaction-fee.md) | 贪心、数组、动态规划 | 中等 | | [0715. Range 模块](https://leetcode.cn/problems/range-module/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/range-module.md) | 设计、线段树、有序集合 | 困难 | | [0717. 1 比特与 2 比特字符](https://leetcode.cn/problems/1-bit-and-2-bit-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/1-bit-and-2-bit-characters.md) | 数组 | 简单 | | [0718. 最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/maximum-length-of-repeated-subarray.md) | 数组、二分查找、动态规划、滑动窗口、哈希函数、滚动哈希 | 中等 | | [0719. 找出第 K 小的数对距离](https://leetcode.cn/problems/find-k-th-smallest-pair-distance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/find-k-th-smallest-pair-distance.md) | 数组、双指针、二分查找、排序 | 困难 | | [0720. 词典中最长的单词](https://leetcode.cn/problems/longest-word-in-dictionary/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/longest-word-in-dictionary.md) | 字典树、数组、哈希表、字符串、排序 | 中等 | | [0721. 账户合并](https://leetcode.cn/problems/accounts-merge/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/accounts-merge.md) | 深度优先搜索、广度优先搜索、并查集、数组、哈希表、字符串、排序 | 中等 | | [0722. 删除注释](https://leetcode.cn/problems/remove-comments/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/remove-comments.md) | 数组、字符串 | 中等 | | [0724. 寻找数组的中心下标](https://leetcode.cn/problems/find-pivot-index/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/find-pivot-index.md) | 数组、前缀和 | 简单 | | [0725. 分隔链表](https://leetcode.cn/problems/split-linked-list-in-parts/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/split-linked-list-in-parts.md) | 链表 | 中等 | | [0726. 原子的数量](https://leetcode.cn/problems/number-of-atoms/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/number-of-atoms.md) | 栈、哈希表、字符串、排序 | 困难 | | [0727. 最小窗口子序列](https://leetcode.cn/problems/minimum-window-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/minimum-window-subsequence.md) | 字符串、动态规划、滑动窗口 | 困难 | | [0728. 自除数](https://leetcode.cn/problems/self-dividing-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/self-dividing-numbers.md) | 数学 | 简单 | | [0729. 我的日程安排表 I](https://leetcode.cn/problems/my-calendar-i/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/my-calendar-i.md) | 设计、线段树、数组、二分查找、有序集合 | 中等 | | [0730. 统计不同回文子序列](https://leetcode.cn/problems/count-different-palindromic-subsequences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/count-different-palindromic-subsequences.md) | 字符串、动态规划 | 困难 | | [0731. 我的日程安排表 II](https://leetcode.cn/problems/my-calendar-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/my-calendar-ii.md) | 设计、线段树、数组、二分查找、有序集合、前缀和 | 中等 | | [0732. 我的日程安排表 III](https://leetcode.cn/problems/my-calendar-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/my-calendar-iii.md) | 设计、线段树、二分查找、有序集合、前缀和 | 困难 | | [0733. 图像渲染](https://leetcode.cn/problems/flood-fill/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/flood-fill.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 简单 | | [0735. 小行星碰撞](https://leetcode.cn/problems/asteroid-collision/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/asteroid-collision.md) | 栈、数组、模拟 | 中等 | | [0736. Lisp 语法解析](https://leetcode.cn/problems/parse-lisp-expression/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/parse-lisp-expression.md) | 栈、递归、哈希表、字符串 | 困难 | | [0738. 单调递增的数字](https://leetcode.cn/problems/monotone-increasing-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/monotone-increasing-digits.md) | 贪心、数学 | 中等 | | [0739. 每日温度](https://leetcode.cn/problems/daily-temperatures/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/daily-temperatures.md) | 栈、数组、单调栈 | 中等 | | [0740. 删除并获得点数](https://leetcode.cn/problems/delete-and-earn/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/delete-and-earn.md) | 数组、哈希表、动态规划 | 中等 | | [0741. 摘樱桃](https://leetcode.cn/problems/cherry-pickup/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/cherry-pickup.md) | 数组、动态规划、矩阵 | 困难 | | [0743. 网络延迟时间](https://leetcode.cn/problems/network-delay-time/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/network-delay-time.md) | 深度优先搜索、广度优先搜索、图、最短路、堆(优先队列) | 中等 | | [0744. 寻找比目标字母大的最小字母](https://leetcode.cn/problems/find-smallest-letter-greater-than-target/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/find-smallest-letter-greater-than-target.md) | 数组、二分查找 | 简单 | | [0745. 前缀和后缀搜索](https://leetcode.cn/problems/prefix-and-suffix-search/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/prefix-and-suffix-search.md) | 设计、字典树、数组、哈希表、字符串 | 困难 | | [0746. 使用最小花费爬楼梯](https://leetcode.cn/problems/min-cost-climbing-stairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/min-cost-climbing-stairs.md) | 数组、动态规划 | 简单 | | [0747. 至少是其他数字两倍的最大数](https://leetcode.cn/problems/largest-number-at-least-twice-of-others/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/largest-number-at-least-twice-of-others.md) | 数组、排序 | 简单 | | [0748. 最短补全词](https://leetcode.cn/problems/shortest-completing-word/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/shortest-completing-word.md) | 数组、哈希表、字符串 | 简单 | | [0749. 隔离病毒](https://leetcode.cn/problems/contain-virus/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/contain-virus.md) | 深度优先搜索、广度优先搜索、数组、矩阵、模拟 | 困难 | | [0752. 打开转盘锁](https://leetcode.cn/problems/open-the-lock/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/open-the-lock.md) | 广度优先搜索、数组、哈希表、字符串 | 中等 | | [0753. 破解保险箱](https://leetcode.cn/problems/cracking-the-safe/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/cracking-the-safe.md) | 深度优先搜索、图、欧拉回路 | 困难 | | [0754. 到达终点数字](https://leetcode.cn/problems/reach-a-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/reach-a-number.md) | 数学、二分查找 | 中等 | | [0756. 金字塔转换矩阵](https://leetcode.cn/problems/pyramid-transition-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/pyramid-transition-matrix.md) | 位运算、深度优先搜索、广度优先搜索、哈希表、字符串 | 中等 | | [0757. 设置交集大小至少为2](https://leetcode.cn/problems/set-intersection-size-at-least-two/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/set-intersection-size-at-least-two.md) | 贪心、数组、排序 | 困难 | | [0758. 字符串中的加粗单词](https://leetcode.cn/problems/bold-words-in-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/bold-words-in-string.md) | 字典树、数组、哈希表、字符串、字符串匹配 | 中等 | | [0762. 二进制表示中质数个计算置位](https://leetcode.cn/problems/prime-number-of-set-bits-in-binary-representation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/prime-number-of-set-bits-in-binary-representation.md) | 位运算、数学 | 简单 | | [0763. 划分字母区间](https://leetcode.cn/problems/partition-labels/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/partition-labels.md) | 贪心、哈希表、双指针、字符串 | 中等 | | [0764. 最大加号标志](https://leetcode.cn/problems/largest-plus-sign/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/largest-plus-sign.md) | 数组、动态规划 | 中等 | | [0765. 情侣牵手](https://leetcode.cn/problems/couples-holding-hands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/couples-holding-hands.md) | 贪心、深度优先搜索、广度优先搜索、并查集、图 | 困难 | | [0766. 托普利茨矩阵](https://leetcode.cn/problems/toeplitz-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/toeplitz-matrix.md) | 数组、矩阵 | 简单 | | [0767. 重构字符串](https://leetcode.cn/problems/reorganize-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/reorganize-string.md) | 贪心、哈希表、字符串、计数、排序、堆(优先队列) | 中等 | | [0768. 最多能完成排序的块 II](https://leetcode.cn/problems/max-chunks-to-make-sorted-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/max-chunks-to-make-sorted-ii.md) | 栈、贪心、数组、排序、单调栈 | 困难 | | [0769. 最多能完成排序的块](https://leetcode.cn/problems/max-chunks-to-make-sorted/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/max-chunks-to-make-sorted.md) | 栈、贪心、数组、排序、单调栈 | 中等 | | [0770. 基本计算器 IV](https://leetcode.cn/problems/basic-calculator-iv/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/basic-calculator-iv.md) | 栈、递归、哈希表、数学、字符串 | 困难 | | [0771. 宝石与石头](https://leetcode.cn/problems/jewels-and-stones/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/jewels-and-stones.md) | 哈希表、字符串 | 简单 | | [0773. 滑动谜题](https://leetcode.cn/problems/sliding-puzzle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/sliding-puzzle.md) | 广度优先搜索、记忆化搜索、数组、动态规划、回溯、矩阵 | 困难 | | [0775. 全局倒置与局部倒置](https://leetcode.cn/problems/global-and-local-inversions/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/global-and-local-inversions.md) | 数组、数学 | 中等 | | [0777. 在 LR 字符串中交换相邻字符](https://leetcode.cn/problems/swap-adjacent-in-lr-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/swap-adjacent-in-lr-string.md) | 双指针、字符串 | 中等 | | [0778. 水位上升的泳池中游泳](https://leetcode.cn/problems/swim-in-rising-water/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/swim-in-rising-water.md) | 深度优先搜索、广度优先搜索、并查集、数组、二分查找、矩阵、堆(优先队列) | 困难 | | [0779. 第K个语法符号](https://leetcode.cn/problems/k-th-symbol-in-grammar/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/k-th-symbol-in-grammar.md) | 位运算、递归、数学 | 中等 | | [0780. 到达终点](https://leetcode.cn/problems/reaching-points/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/reaching-points.md) | 数学 | 困难 | | [0781. 森林中的兔子](https://leetcode.cn/problems/rabbits-in-forest/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rabbits-in-forest.md) | 贪心、数组、哈希表、数学 | 中等 | | [0782. 变为棋盘](https://leetcode.cn/problems/transform-to-chessboard/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/transform-to-chessboard.md) | 位运算、数组、数学、矩阵 | 困难 | | [0783. 二叉搜索树节点最小距离](https://leetcode.cn/problems/minimum-distance-between-bst-nodes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/minimum-distance-between-bst-nodes.md) | 树、深度优先搜索、广度优先搜索、二叉搜索树、二叉树 | 简单 | | [0784. 字母大小写全排列](https://leetcode.cn/problems/letter-case-permutation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/letter-case-permutation.md) | 位运算、字符串、回溯 | 中等 | | [0785. 判断二分图](https://leetcode.cn/problems/is-graph-bipartite/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/is-graph-bipartite.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0786. 第 K 个最小的质数分数](https://leetcode.cn/problems/k-th-smallest-prime-fraction/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/k-th-smallest-prime-fraction.md) | 数组、双指针、二分查找、排序、堆(优先队列) | 中等 | | [0787. K 站中转内最便宜的航班](https://leetcode.cn/problems/cheapest-flights-within-k-stops/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/cheapest-flights-within-k-stops.md) | 深度优先搜索、广度优先搜索、图、动态规划、最短路、堆(优先队列) | 中等 | | [0788. 旋转数字](https://leetcode.cn/problems/rotated-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotated-digits.md) | 数学、动态规划 | 中等 | | [0789. 逃脱阻碍者](https://leetcode.cn/problems/escape-the-ghosts/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/escape-the-ghosts.md) | 数组、数学 | 中等 | | [0790. 多米诺和托米诺平铺](https://leetcode.cn/problems/domino-and-tromino-tiling/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/domino-and-tromino-tiling.md) | 动态规划 | 中等 | | [0791. 自定义字符串排序](https://leetcode.cn/problems/custom-sort-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/custom-sort-string.md) | 哈希表、字符串、排序 | 中等 | | [0792. 匹配子序列的单词数](https://leetcode.cn/problems/number-of-matching-subsequences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/number-of-matching-subsequences.md) | 字典树、数组、哈希表、字符串、二分查找、动态规划、排序 | 中等 | | [0793. 阶乘函数后 K 个零](https://leetcode.cn/problems/preimage-size-of-factorial-zeroes-function/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/preimage-size-of-factorial-zeroes-function.md) | 数学、二分查找 | 困难 | | [0794. 有效的井字游戏](https://leetcode.cn/problems/valid-tic-tac-toe-state/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/valid-tic-tac-toe-state.md) | 数组、矩阵 | 中等 | | [0795. 区间子数组个数](https://leetcode.cn/problems/number-of-subarrays-with-bounded-maximum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/number-of-subarrays-with-bounded-maximum.md) | 数组、双指针 | 中等 | | [0796. 旋转字符串](https://leetcode.cn/problems/rotate-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotate-string.md) | 字符串、字符串匹配 | 简单 | | [0797. 所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/all-paths-from-source-to-target.md) | 深度优先搜索、广度优先搜索、图、回溯 | 中等 | | [0798. 得分最高的最小轮调](https://leetcode.cn/problems/smallest-rotation-with-highest-score/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/smallest-rotation-with-highest-score.md) | 数组、前缀和 | 困难 | | [0799. 香槟塔](https://leetcode.cn/problems/champagne-tower/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/champagne-tower.md) | 动态规划 | 中等 | ### 第 800 ~ 899 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0800. 相似 RGB 颜色](https://leetcode.cn/problems/similar-rgb-color/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/similar-rgb-color.md) | 数学、字符串、枚举 | 简单 | | [0801. 使序列递增的最小交换次数](https://leetcode.cn/problems/minimum-swaps-to-make-sequences-increasing/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/minimum-swaps-to-make-sequences-increasing.md) | 数组、动态规划 | 困难 | | [0802. 找到最终的安全状态](https://leetcode.cn/problems/find-eventual-safe-states/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/find-eventual-safe-states.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [0803. 打砖块](https://leetcode.cn/problems/bricks-falling-when-hit/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/bricks-falling-when-hit.md) | 并查集、数组、矩阵 | 困难 | | [0804. 唯一摩尔斯密码词](https://leetcode.cn/problems/unique-morse-code-words/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/unique-morse-code-words.md) | 数组、哈希表、字符串 | 简单 | | [0805. 数组的均值分割](https://leetcode.cn/problems/split-array-with-same-average/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/split-array-with-same-average.md) | 位运算、数组、数学、动态规划、状态压缩 | 困难 | | [0806. 写字符串需要的行数](https://leetcode.cn/problems/number-of-lines-to-write-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/number-of-lines-to-write-string.md) | 数组、字符串 | 简单 | | [0807. 保持城市天际线](https://leetcode.cn/problems/max-increase-to-keep-city-skyline/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/max-increase-to-keep-city-skyline.md) | 贪心、数组、矩阵 | 中等 | | [0808. 分汤](https://leetcode.cn/problems/soup-servings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/soup-servings.md) | 数学、动态规划、概率与统计 | 中等 | | [0809. 情感丰富的文字](https://leetcode.cn/problems/expressive-words/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/expressive-words.md) | 数组、双指针、字符串 | 中等 | | [0810. 黑板异或游戏](https://leetcode.cn/problems/chalkboard-xor-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/chalkboard-xor-game.md) | 位运算、脑筋急转弯、数组、数学、博弈 | 困难 | | [0811. 子域名访问计数](https://leetcode.cn/problems/subdomain-visit-count/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/subdomain-visit-count.md) | 数组、哈希表、字符串、计数 | 中等 | | [0812. 最大三角形面积](https://leetcode.cn/problems/largest-triangle-area/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/largest-triangle-area.md) | 几何、数组、数学 | 简单 | | [0813. 最大平均值和的分组](https://leetcode.cn/problems/largest-sum-of-averages/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/largest-sum-of-averages.md) | 数组、动态规划、前缀和 | 中等 | | [0814. 二叉树剪枝](https://leetcode.cn/problems/binary-tree-pruning/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/binary-tree-pruning.md) | 树、深度优先搜索、二叉树 | 中等 | | [0815. 公交路线](https://leetcode.cn/problems/bus-routes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/bus-routes.md) | 广度优先搜索、数组、哈希表 | 困难 | | [0816. 模糊坐标](https://leetcode.cn/problems/ambiguous-coordinates/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/ambiguous-coordinates.md) | 字符串、回溯、枚举 | 中等 | | [0817. 链表组件](https://leetcode.cn/problems/linked-list-components/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/linked-list-components.md) | 数组、哈希表、链表 | 中等 | | [0818. 赛车](https://leetcode.cn/problems/race-car/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/race-car.md) | 动态规划 | 困难 | | [0819. 最常见的单词](https://leetcode.cn/problems/most-common-word/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/most-common-word.md) | 数组、哈希表、字符串、计数 | 简单 | | [0820. 单词的压缩编码](https://leetcode.cn/problems/short-encoding-of-words/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/short-encoding-of-words.md) | 字典树、数组、哈希表、字符串 | 中等 | | [0821. 字符的最短距离](https://leetcode.cn/problems/shortest-distance-to-a-character/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-distance-to-a-character.md) | 数组、双指针、字符串 | 简单 | | [0822. 翻转卡片游戏](https://leetcode.cn/problems/card-flipping-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/card-flipping-game.md) | 数组、哈希表 | 中等 | | [0823. 带因子的二叉树](https://leetcode.cn/problems/binary-trees-with-factors/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/binary-trees-with-factors.md) | 数组、哈希表、动态规划、排序 | 中等 | | [0824. 山羊拉丁文](https://leetcode.cn/problems/goat-latin/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/goat-latin.md) | 字符串 | 简单 | | [0825. 适龄的朋友](https://leetcode.cn/problems/friends-of-appropriate-ages/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/friends-of-appropriate-ages.md) | 数组、双指针、二分查找、排序 | 中等 | | [0826. 安排工作以达到最大收益](https://leetcode.cn/problems/most-profit-assigning-work/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/most-profit-assigning-work.md) | 贪心、数组、双指针、二分查找、排序 | 中等 | | [0827. 最大人工岛](https://leetcode.cn/problems/making-a-large-island/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/making-a-large-island.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 困难 | | [0828. 统计子串中的唯一字符](https://leetcode.cn/problems/count-unique-characters-of-all-substrings-of-a-given-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/count-unique-characters-of-all-substrings-of-a-given-string.md) | 哈希表、字符串、动态规划 | 困难 | | [0829. 连续整数求和](https://leetcode.cn/problems/consecutive-numbers-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/consecutive-numbers-sum.md) | 数学、枚举 | 困难 | | [0830. 较大分组的位置](https://leetcode.cn/problems/positions-of-large-groups/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/positions-of-large-groups.md) | 字符串 | 简单 | | [0831. 隐藏个人信息](https://leetcode.cn/problems/masking-personal-information/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/masking-personal-information.md) | 字符串 | 中等 | | [0832. 翻转图像](https://leetcode.cn/problems/flipping-an-image/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/flipping-an-image.md) | 位运算、数组、双指针、矩阵、模拟 | 简单 | | [0833. 字符串中的查找与替换](https://leetcode.cn/problems/find-and-replace-in-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/find-and-replace-in-string.md) | 数组、哈希表、字符串、排序 | 中等 | | [0834. 树中距离之和](https://leetcode.cn/problems/sum-of-distances-in-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/sum-of-distances-in-tree.md) | 树、深度优先搜索、图、动态规划 | 困难 | | [0835. 图像重叠](https://leetcode.cn/problems/image-overlap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/image-overlap.md) | 数组、矩阵 | 中等 | | [0836. 矩形重叠](https://leetcode.cn/problems/rectangle-overlap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/rectangle-overlap.md) | 几何、数学 | 简单 | | [0837. 新 21 点](https://leetcode.cn/problems/new-21-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/new-21-game.md) | 数学、动态规划、滑动窗口、概率与统计 | 中等 | | [0838. 推多米诺](https://leetcode.cn/problems/push-dominoes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/push-dominoes.md) | 双指针、字符串、动态规划 | 中等 | | [0839. 相似字符串组](https://leetcode.cn/problems/similar-string-groups/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/similar-string-groups.md) | 深度优先搜索、广度优先搜索、并查集、数组、哈希表、字符串 | 困难 | | [0840. 矩阵中的幻方](https://leetcode.cn/problems/magic-squares-in-grid/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/magic-squares-in-grid.md) | 数组、哈希表、数学、矩阵 | 中等 | | [0841. 钥匙和房间](https://leetcode.cn/problems/keys-and-rooms/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/keys-and-rooms.md) | 深度优先搜索、广度优先搜索、图 | 中等 | | [0842. 将数组拆分成斐波那契序列](https://leetcode.cn/problems/split-array-into-fibonacci-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/split-array-into-fibonacci-sequence.md) | 字符串、回溯 | 中等 | | [0843. 猜猜这个单词](https://leetcode.cn/problems/guess-the-word/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/guess-the-word.md) | 数组、数学、字符串、博弈、交互 | 困难 | | [0844. 比较含退格的字符串](https://leetcode.cn/problems/backspace-string-compare/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/backspace-string-compare.md) | 栈、双指针、字符串、模拟 | 简单 | | [0845. 数组中的最长山脉](https://leetcode.cn/problems/longest-mountain-in-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/longest-mountain-in-array.md) | 数组、双指针、动态规划、枚举 | 中等 | | [0846. 一手顺子](https://leetcode.cn/problems/hand-of-straights/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/hand-of-straights.md) | 贪心、数组、哈希表、排序 | 中等 | | [0847. 访问所有节点的最短路径](https://leetcode.cn/problems/shortest-path-visiting-all-nodes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-path-visiting-all-nodes.md) | 位运算、广度优先搜索、图、动态规划、状态压缩 | 困难 | | [0848. 字母移位](https://leetcode.cn/problems/shifting-letters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shifting-letters.md) | 数组、字符串、前缀和 | 中等 | | [0849. 到最近的人的最大距离](https://leetcode.cn/problems/maximize-distance-to-closest-person/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/maximize-distance-to-closest-person.md) | 数组 | 中等 | | [0850. 矩形面积 II](https://leetcode.cn/problems/rectangle-area-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/rectangle-area-ii.md) | 线段树、数组、有序集合、扫描线 | 困难 | | [0851. 喧闹和富有](https://leetcode.cn/problems/loud-and-rich/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/loud-and-rich.md) | 深度优先搜索、图、拓扑排序、数组 | 中等 | | [0852. 山脉数组的峰顶索引](https://leetcode.cn/problems/peak-index-in-a-mountain-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/peak-index-in-a-mountain-array.md) | 数组、二分查找 | 中等 | | [0853. 车队](https://leetcode.cn/problems/car-fleet/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/car-fleet.md) | 栈、数组、排序、单调栈 | 中等 | | [0854. 相似度为 K 的字符串](https://leetcode.cn/problems/k-similar-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/k-similar-strings.md) | 广度优先搜索、哈希表、字符串 | 困难 | | [0855. 考场就座](https://leetcode.cn/problems/exam-room/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/exam-room.md) | 设计、有序集合、堆(优先队列) | 中等 | | [0856. 括号的分数](https://leetcode.cn/problems/score-of-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/score-of-parentheses.md) | 栈、字符串 | 中等 | | [0857. 雇佣 K 名工人的最低成本](https://leetcode.cn/problems/minimum-cost-to-hire-k-workers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/minimum-cost-to-hire-k-workers.md) | 贪心、数组、排序、堆(优先队列) | 困难 | | [0858. 镜面反射](https://leetcode.cn/problems/mirror-reflection/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/mirror-reflection.md) | 几何、数学、数论 | 中等 | | [0859. 亲密字符串](https://leetcode.cn/problems/buddy-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/buddy-strings.md) | 哈希表、字符串 | 简单 | | [0860. 柠檬水找零](https://leetcode.cn/problems/lemonade-change/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/lemonade-change.md) | 贪心、数组 | 简单 | | [0861. 翻转矩阵后的得分](https://leetcode.cn/problems/score-after-flipping-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/score-after-flipping-matrix.md) | 贪心、位运算、数组、矩阵 | 中等 | | [0862. 和至少为 K 的最短子数组](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-subarray-with-sum-at-least-k.md) | 队列、数组、二分查找、前缀和、滑动窗口、单调队列、堆(优先队列) | 困难 | | [0863. 二叉树中所有距离为 K 的结点](https://leetcode.cn/problems/all-nodes-distance-k-in-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/all-nodes-distance-k-in-binary-tree.md) | 树、深度优先搜索、广度优先搜索、哈希表、二叉树 | 中等 | | [0864. 获取所有钥匙的最短路径](https://leetcode.cn/problems/shortest-path-to-get-all-keys/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-path-to-get-all-keys.md) | 位运算、广度优先搜索、数组、矩阵 | 困难 | | [0865. 具有所有最深节点的最小子树](https://leetcode.cn/problems/smallest-subtree-with-all-the-deepest-nodes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/smallest-subtree-with-all-the-deepest-nodes.md) | 树、深度优先搜索、广度优先搜索、哈希表、二叉树 | 中等 | | [0866. 回文质数](https://leetcode.cn/problems/prime-palindrome/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/prime-palindrome.md) | 数学、数论 | 中等 | | [0867. 转置矩阵](https://leetcode.cn/problems/transpose-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/transpose-matrix.md) | 数组、矩阵、模拟 | 简单 | | [0868. 二进制间距](https://leetcode.cn/problems/binary-gap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/binary-gap.md) | 位运算 | 简单 | | [0869. 重新排序得到 2 的幂](https://leetcode.cn/problems/reordered-power-of-2/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/reordered-power-of-2.md) | 哈希表、数学、计数、枚举、排序 | 中等 | | [0870. 优势洗牌](https://leetcode.cn/problems/advantage-shuffle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/advantage-shuffle.md) | 贪心、数组、双指针、排序 | 中等 | | [0871. 最低加油次数](https://leetcode.cn/problems/minimum-number-of-refueling-stops/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/minimum-number-of-refueling-stops.md) | 贪心、数组、动态规划、堆(优先队列) | 困难 | | [0872. 叶子相似的树](https://leetcode.cn/problems/leaf-similar-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/leaf-similar-trees.md) | 树、深度优先搜索、二叉树 | 简单 | | [0873. 最长的斐波那契子序列的长度](https://leetcode.cn/problems/length-of-longest-fibonacci-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/length-of-longest-fibonacci-subsequence.md) | 数组、哈希表、动态规划 | 中等 | | [0874. 模拟行走机器人](https://leetcode.cn/problems/walking-robot-simulation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/walking-robot-simulation.md) | 数组、哈希表、模拟 | 中等 | | [0875. 爱吃香蕉的珂珂](https://leetcode.cn/problems/koko-eating-bananas/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/koko-eating-bananas.md) | 数组、二分查找 | 中等 | | [0876. 链表的中间结点](https://leetcode.cn/problems/middle-of-the-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/middle-of-the-linked-list.md) | 链表、双指针 | 简单 | | [0877. 石子游戏](https://leetcode.cn/problems/stone-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/stone-game.md) | 数组、数学、动态规划、博弈 | 中等 | | [0878. 第 N 个神奇数字](https://leetcode.cn/problems/nth-magical-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/nth-magical-number.md) | 数学、二分查找 | 困难 | | [0879. 盈利计划](https://leetcode.cn/problems/profitable-schemes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/profitable-schemes.md) | 数组、动态规划 | 困难 | | [0880. 索引处的解码字符串](https://leetcode.cn/problems/decoded-string-at-index/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/decoded-string-at-index.md) | 栈、字符串 | 中等 | | [0881. 救生艇](https://leetcode.cn/problems/boats-to-save-people/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/boats-to-save-people.md) | 贪心、数组、双指针、排序 | 中等 | | [0882. 细分图中的可到达节点](https://leetcode.cn/problems/reachable-nodes-in-subdivided-graph/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/reachable-nodes-in-subdivided-graph.md) | 图、最短路、堆(优先队列) | 困难 | | [0883. 三维形体投影面积](https://leetcode.cn/problems/projection-area-of-3d-shapes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/projection-area-of-3d-shapes.md) | 几何、数组、数学、矩阵 | 简单 | | [0884. 两句话中的不常见单词](https://leetcode.cn/problems/uncommon-words-from-two-sentences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/uncommon-words-from-two-sentences.md) | 哈希表、字符串、计数 | 简单 | | [0885. 螺旋矩阵 III](https://leetcode.cn/problems/spiral-matrix-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/spiral-matrix-iii.md) | 数组、矩阵、模拟 | 中等 | | [0886. 可能的二分法](https://leetcode.cn/problems/possible-bipartition/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/possible-bipartition.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0887. 鸡蛋掉落](https://leetcode.cn/problems/super-egg-drop/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/super-egg-drop.md) | 数学、二分查找、动态规划 | 困难 | | [0888. 公平的糖果交换](https://leetcode.cn/problems/fair-candy-swap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/fair-candy-swap.md) | 数组、哈希表、二分查找、排序 | 简单 | | [0889. 根据前序和后序遍历构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/construct-binary-tree-from-preorder-and-postorder-traversal.md) | 树、数组、哈希表、分治、二叉树 | 中等 | | [0890. 查找和替换模式](https://leetcode.cn/problems/find-and-replace-pattern/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/find-and-replace-pattern.md) | 数组、哈希表、字符串 | 中等 | | [0891. 子序列宽度之和](https://leetcode.cn/problems/sum-of-subsequence-widths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/sum-of-subsequence-widths.md) | 数组、数学、排序 | 困难 | | [0892. 三维形体的表面积](https://leetcode.cn/problems/surface-area-of-3d-shapes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/surface-area-of-3d-shapes.md) | 几何、数组、数学、矩阵 | 简单 | | [0893. 特殊等价字符串组](https://leetcode.cn/problems/groups-of-special-equivalent-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/groups-of-special-equivalent-strings.md) | 数组、哈希表、字符串、排序 | 中等 | | [0894. 所有可能的真二叉树](https://leetcode.cn/problems/all-possible-full-binary-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/all-possible-full-binary-trees.md) | 树、递归、记忆化搜索、动态规划、二叉树 | 中等 | | [0895. 最大频率栈](https://leetcode.cn/problems/maximum-frequency-stack/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/maximum-frequency-stack.md) | 栈、设计、哈希表、有序集合 | 困难 | | [0896. 单调数列](https://leetcode.cn/problems/monotonic-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/monotonic-array.md) | 数组 | 简单 | | [0897. 递增顺序搜索树](https://leetcode.cn/problems/increasing-order-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/increasing-order-search-tree.md) | 栈、树、深度优先搜索、二叉搜索树、二叉树 | 简单 | | [0898. 子数组按位或操作](https://leetcode.cn/problems/bitwise-ors-of-subarrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/bitwise-ors-of-subarrays.md) | 位运算、数组、动态规划 | 中等 | | [0899. 有序队列](https://leetcode.cn/problems/orderly-queue/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/orderly-queue.md) | 数学、字符串、排序 | 困难 | ### 第 900 ~ 999 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0900. RLE 迭代器](https://leetcode.cn/problems/rle-iterator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/rle-iterator.md) | 设计、数组、计数、迭代器 | 中等 | | [0901. 股票价格跨度](https://leetcode.cn/problems/online-stock-span/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/online-stock-span.md) | 栈、设计、数据流、单调栈 | 中等 | | [0902. 最大为 N 的数字组合](https://leetcode.cn/problems/numbers-at-most-n-given-digit-set/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/numbers-at-most-n-given-digit-set.md) | 数组、数学、字符串、二分查找、动态规划 | 困难 | | [0903. DI 序列的有效排列](https://leetcode.cn/problems/valid-permutations-for-di-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/valid-permutations-for-di-sequence.md) | 字符串、动态规划、前缀和 | 困难 | | [0904. 水果成篮](https://leetcode.cn/problems/fruit-into-baskets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/fruit-into-baskets.md) | 数组、哈希表、滑动窗口 | 中等 | | [0905. 按奇偶排序数组](https://leetcode.cn/problems/sort-array-by-parity/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-array-by-parity.md) | 数组、双指针、排序 | 简单 | | [0906. 超级回文数](https://leetcode.cn/problems/super-palindromes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/super-palindromes.md) | 数学、字符串、枚举 | 困难 | | [0907. 子数组的最小值之和](https://leetcode.cn/problems/sum-of-subarray-minimums/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sum-of-subarray-minimums.md) | 栈、数组、动态规划、单调栈 | 中等 | | [0908. 最小差值 I](https://leetcode.cn/problems/smallest-range-i/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/smallest-range-i.md) | 数组、数学 | 简单 | | [0909. 蛇梯棋](https://leetcode.cn/problems/snakes-and-ladders/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/snakes-and-ladders.md) | 广度优先搜索、数组、矩阵 | 中等 | | [0910. 最小差值 II](https://leetcode.cn/problems/smallest-range-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/smallest-range-ii.md) | 贪心、数组、数学、排序 | 中等 | | [0911. 在线选举](https://leetcode.cn/problems/online-election/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/online-election.md) | 设计、数组、哈希表、二分查找 | 中等 | | [0912. 排序数组](https://leetcode.cn/problems/sort-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) | 数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序 | 中等 | | [0913. 猫和老鼠](https://leetcode.cn/problems/cat-and-mouse/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/cat-and-mouse.md) | 图、拓扑排序、记忆化搜索、数学、动态规划、博弈 | 困难 | | [0914. 卡牌分组](https://leetcode.cn/problems/x-of-a-kind-in-a-deck-of-cards/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/x-of-a-kind-in-a-deck-of-cards.md) | 数组、哈希表、数学、计数、数论 | 简单 | | [0915. 分割数组](https://leetcode.cn/problems/partition-array-into-disjoint-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/partition-array-into-disjoint-intervals.md) | 数组 | 中等 | | [0916. 单词子集](https://leetcode.cn/problems/word-subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/word-subsets.md) | 数组、哈希表、字符串 | 中等 | | [0917. 仅仅反转字母](https://leetcode.cn/problems/reverse-only-letters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/reverse-only-letters.md) | 双指针、字符串 | 简单 | | [0918. 环形子数组的最大和](https://leetcode.cn/problems/maximum-sum-circular-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/maximum-sum-circular-subarray.md) | 队列、数组、分治、动态规划、单调队列 | 中等 | | [0919. 完全二叉树插入器](https://leetcode.cn/problems/complete-binary-tree-inserter/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/complete-binary-tree-inserter.md) | 树、广度优先搜索、设计、二叉树 | 中等 | | [0920. 播放列表的数量](https://leetcode.cn/problems/number-of-music-playlists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/number-of-music-playlists.md) | 数学、动态规划、组合数学 | 困难 | | [0921. 使括号有效的最少添加](https://leetcode.cn/problems/minimum-add-to-make-parentheses-valid/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-add-to-make-parentheses-valid.md) | 栈、贪心、字符串 | 中等 | | [0922. 按奇偶排序数组 II](https://leetcode.cn/problems/sort-array-by-parity-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-array-by-parity-ii.md) | 数组、双指针、排序 | 简单 | | [0923. 三数之和的多种可能](https://leetcode.cn/problems/3sum-with-multiplicity/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/3sum-with-multiplicity.md) | 数组、哈希表、双指针、计数、排序 | 中等 | | [0924. 尽量减少恶意软件的传播](https://leetcode.cn/problems/minimize-malware-spread/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimize-malware-spread.md) | 深度优先搜索、广度优先搜索、并查集、图、数组、哈希表 | 困难 | | [0925. 长按键入](https://leetcode.cn/problems/long-pressed-name/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/long-pressed-name.md) | 双指针、字符串 | 简单 | | [0926. 将字符串翻转到单调递增](https://leetcode.cn/problems/flip-string-to-monotone-increasing/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/flip-string-to-monotone-increasing.md) | 字符串、动态规划 | 中等 | | [0927. 三等分](https://leetcode.cn/problems/three-equal-parts/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/three-equal-parts.md) | 数组、数学 | 困难 | | [0928. 尽量减少恶意软件的传播 II](https://leetcode.cn/problems/minimize-malware-spread-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimize-malware-spread-ii.md) | 深度优先搜索、广度优先搜索、并查集、图、数组、哈希表 | 困难 | | [0929. 独特的电子邮件地址](https://leetcode.cn/problems/unique-email-addresses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/unique-email-addresses.md) | 数组、哈希表、字符串 | 简单 | | [0930. 和相同的二元子数组](https://leetcode.cn/problems/binary-subarrays-with-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/binary-subarrays-with-sum.md) | 数组、哈希表、前缀和、滑动窗口 | 中等 | | [0931. 下降路径最小和](https://leetcode.cn/problems/minimum-falling-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-falling-path-sum.md) | 数组、动态规划、矩阵 | 中等 | | [0932. 漂亮数组](https://leetcode.cn/problems/beautiful-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/beautiful-array.md) | 数组、数学、分治 | 中等 | | [0933. 最近的请求次数](https://leetcode.cn/problems/number-of-recent-calls/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/number-of-recent-calls.md) | 设计、队列、数据流 | 简单 | | [0934. 最短的桥](https://leetcode.cn/problems/shortest-bridge/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/shortest-bridge.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 中等 | | [0935. 骑士拨号器](https://leetcode.cn/problems/knight-dialer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/knight-dialer.md) | 动态规划 | 中等 | | [0936. 戳印序列](https://leetcode.cn/problems/stamping-the-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/stamping-the-sequence.md) | 栈、贪心、队列、字符串 | 困难 | | [0937. 重新排列日志文件](https://leetcode.cn/problems/reorder-data-in-log-files/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/reorder-data-in-log-files.md) | 数组、字符串、排序 | 中等 | | [0938. 二叉搜索树的范围和](https://leetcode.cn/problems/range-sum-of-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/range-sum-of-bst.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 简单 | | [0939. 最小面积矩形](https://leetcode.cn/problems/minimum-area-rectangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-area-rectangle.md) | 几何、数组、哈希表、数学、排序 | 中等 | | [0940. 不同的子序列 II](https://leetcode.cn/problems/distinct-subsequences-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/distinct-subsequences-ii.md) | 字符串、动态规划 | 困难 | | [0941. 有效的山脉数组](https://leetcode.cn/problems/valid-mountain-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/valid-mountain-array.md) | 数组 | 简单 | | [0942. 增减字符串匹配](https://leetcode.cn/problems/di-string-match/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/di-string-match.md) | 贪心、数组、双指针、字符串 | 简单 | | [0943. 最短超级串](https://leetcode.cn/problems/find-the-shortest-superstring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/find-the-shortest-superstring.md) | 位运算、数组、字符串、动态规划、状态压缩 | 困难 | | [0944. 删列造序](https://leetcode.cn/problems/delete-columns-to-make-sorted/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/delete-columns-to-make-sorted.md) | 数组、字符串 | 简单 | | [0945. 使数组唯一的最小增量](https://leetcode.cn/problems/minimum-increment-to-make-array-unique/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-increment-to-make-array-unique.md) | 贪心、数组、计数、排序 | 中等 | | [0946. 验证栈序列](https://leetcode.cn/problems/validate-stack-sequences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/validate-stack-sequences.md) | 栈、数组、模拟 | 中等 | | [0947. 移除最多的同行或同列石头](https://leetcode.cn/problems/most-stones-removed-with-same-row-or-column/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/most-stones-removed-with-same-row-or-column.md) | 深度优先搜索、并查集、图、哈希表 | 中等 | | [0948. 令牌放置](https://leetcode.cn/problems/bag-of-tokens/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/bag-of-tokens.md) | 贪心、数组、双指针、排序 | 中等 | | [0949. 给定数字能组成的最大时间](https://leetcode.cn/problems/largest-time-for-given-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/largest-time-for-given-digits.md) | 数组、字符串、回溯、枚举 | 中等 | | [0950. 按递增顺序显示卡牌](https://leetcode.cn/problems/reveal-cards-in-increasing-order/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/reveal-cards-in-increasing-order.md) | 队列、数组、排序、模拟 | 中等 | | [0951. 翻转等价二叉树](https://leetcode.cn/problems/flip-equivalent-binary-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/flip-equivalent-binary-trees.md) | 树、深度优先搜索、二叉树 | 中等 | | [0952. 按公因数计算最大组件大小](https://leetcode.cn/problems/largest-component-size-by-common-factor/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/largest-component-size-by-common-factor.md) | 并查集、数组、哈希表、数学、数论 | 困难 | | [0953. 验证外星语词典](https://leetcode.cn/problems/verifying-an-alien-dictionary/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/verifying-an-alien-dictionary.md) | 数组、哈希表、字符串 | 简单 | | [0954. 二倍数对数组](https://leetcode.cn/problems/array-of-doubled-pairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/array-of-doubled-pairs.md) | 贪心、数组、哈希表、排序 | 中等 | | [0955. 删列造序 II](https://leetcode.cn/problems/delete-columns-to-make-sorted-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/delete-columns-to-make-sorted-ii.md) | 贪心、数组、字符串 | 中等 | | [0956. 最高的广告牌](https://leetcode.cn/problems/tallest-billboard/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/tallest-billboard.md) | 数组、动态规划 | 困难 | | [0957. N 天后的牢房](https://leetcode.cn/problems/prison-cells-after-n-days/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/prison-cells-after-n-days.md) | 位运算、数组、哈希表、数学 | 中等 | | [0958. 二叉树的完全性检验](https://leetcode.cn/problems/check-completeness-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/check-completeness-of-a-binary-tree.md) | 树、广度优先搜索、二叉树 | 中等 | | [0959. 由斜杠划分区域](https://leetcode.cn/problems/regions-cut-by-slashes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/regions-cut-by-slashes.md) | 深度优先搜索、广度优先搜索、并查集、数组、哈希表、矩阵 | 中等 | | [0960. 删列造序 III](https://leetcode.cn/problems/delete-columns-to-make-sorted-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/delete-columns-to-make-sorted-iii.md) | 数组、字符串、动态规划 | 困难 | | [0961. 在长度 2N 的数组中找出重复 N 次的元素](https://leetcode.cn/problems/n-repeated-element-in-size-2n-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/n-repeated-element-in-size-2n-array.md) | 数组、哈希表 | 简单 | | [0962. 最大宽度坡](https://leetcode.cn/problems/maximum-width-ramp/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/maximum-width-ramp.md) | 栈、数组、双指针、单调栈 | 中等 | | [0963. 最小面积矩形 II](https://leetcode.cn/problems/minimum-area-rectangle-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-area-rectangle-ii.md) | 几何、数组、哈希表、数学 | 中等 | | [0964. 表示数字的最少运算符](https://leetcode.cn/problems/least-operators-to-express-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/least-operators-to-express-number.md) | 记忆化搜索、数学、动态规划 | 困难 | | [0965. 单值二叉树](https://leetcode.cn/problems/univalued-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/univalued-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0966. 元音拼写检查器](https://leetcode.cn/problems/vowel-spellchecker/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/vowel-spellchecker.md) | 数组、哈希表、字符串 | 中等 | | [0967. 连续差相同的数字](https://leetcode.cn/problems/numbers-with-same-consecutive-differences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/numbers-with-same-consecutive-differences.md) | 广度优先搜索、回溯 | 中等 | | [0968. 监控二叉树](https://leetcode.cn/problems/binary-tree-cameras/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/binary-tree-cameras.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [0969. 煎饼排序](https://leetcode.cn/problems/pancake-sorting/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/pancake-sorting.md) | 贪心、数组、双指针、排序 | 中等 | | [0970. 强整数](https://leetcode.cn/problems/powerful-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/powerful-integers.md) | 哈希表、数学、枚举 | 中等 | | [0971. 翻转二叉树以匹配先序遍历](https://leetcode.cn/problems/flip-binary-tree-to-match-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/flip-binary-tree-to-match-preorder-traversal.md) | 树、深度优先搜索、二叉树 | 中等 | | [0972. 相等的有理数](https://leetcode.cn/problems/equal-rational-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/equal-rational-numbers.md) | 数学、字符串 | 困难 | | [0973. 最接近原点的 K 个点](https://leetcode.cn/problems/k-closest-points-to-origin/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/k-closest-points-to-origin.md) | 几何、数组、数学、分治、快速选择、排序、堆(优先队列) | 中等 | | [0974. 和可被 K 整除的子数组](https://leetcode.cn/problems/subarray-sums-divisible-by-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/subarray-sums-divisible-by-k.md) | 数组、哈希表、前缀和 | 中等 | | [0975. 奇偶跳](https://leetcode.cn/problems/odd-even-jump/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/odd-even-jump.md) | 栈、数组、动态规划、有序集合、排序、单调栈 | 困难 | | [0976. 三角形的最大周长](https://leetcode.cn/problems/largest-perimeter-triangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/largest-perimeter-triangle.md) | 贪心、数组、数学、排序 | 简单 | | [0977. 有序数组的平方](https://leetcode.cn/problems/squares-of-a-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/squares-of-a-sorted-array.md) | 数组、双指针、排序 | 简单 | | [0978. 最长湍流子数组](https://leetcode.cn/problems/longest-turbulent-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/longest-turbulent-subarray.md) | 数组、动态规划、滑动窗口 | 中等 | | [0979. 在二叉树中分配硬币](https://leetcode.cn/problems/distribute-coins-in-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/distribute-coins-in-binary-tree.md) | 树、深度优先搜索、二叉树 | 中等 | | [0980. 不同路径 III](https://leetcode.cn/problems/unique-paths-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/unique-paths-iii.md) | 位运算、数组、回溯、矩阵 | 困难 | | [0981. 基于时间的键值存储](https://leetcode.cn/problems/time-based-key-value-store/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/time-based-key-value-store.md) | 设计、哈希表、字符串、二分查找 | 中等 | | [0982. 按位与为零的三元组](https://leetcode.cn/problems/triples-with-bitwise-and-equal-to-zero/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/triples-with-bitwise-and-equal-to-zero.md) | 位运算、数组、哈希表 | 困难 | | [0983. 最低票价](https://leetcode.cn/problems/minimum-cost-for-tickets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-cost-for-tickets.md) | 数组、动态规划 | 中等 | | [0984. 不含 AAA 或 BBB 的字符串](https://leetcode.cn/problems/string-without-aaa-or-bbb/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/string-without-aaa-or-bbb.md) | 贪心、字符串 | 中等 | | [0985. 查询后的偶数和](https://leetcode.cn/problems/sum-of-even-numbers-after-queries/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sum-of-even-numbers-after-queries.md) | 数组、模拟 | 中等 | | [0986. 区间列表的交集](https://leetcode.cn/problems/interval-list-intersections/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/interval-list-intersections.md) | 数组、双指针、扫描线 | 中等 | | [0987. 二叉树的垂序遍历](https://leetcode.cn/problems/vertical-order-traversal-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/vertical-order-traversal-of-a-binary-tree.md) | 树、深度优先搜索、广度优先搜索、哈希表、二叉树、排序 | 困难 | | [0988. 从叶结点开始的最小字符串](https://leetcode.cn/problems/smallest-string-starting-from-leaf/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/smallest-string-starting-from-leaf.md) | 树、深度优先搜索、字符串、回溯、二叉树 | 中等 | | [0989. 数组形式的整数加法](https://leetcode.cn/problems/add-to-array-form-of-integer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/add-to-array-form-of-integer.md) | 数组、数学 | 简单 | | [0990. 等式方程的可满足性](https://leetcode.cn/problems/satisfiability-of-equality-equations/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/satisfiability-of-equality-equations.md) | 并查集、图、数组、字符串 | 中等 | | [0991. 坏了的计算器](https://leetcode.cn/problems/broken-calculator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/broken-calculator.md) | 贪心、数学 | 中等 | | [0992. K 个不同整数的子数组](https://leetcode.cn/problems/subarrays-with-k-different-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/subarrays-with-k-different-integers.md) | 数组、哈希表、计数、滑动窗口 | 困难 | | [0993. 二叉树的堂兄弟节点](https://leetcode.cn/problems/cousins-in-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/cousins-in-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0994. 腐烂的橘子](https://leetcode.cn/problems/rotting-oranges/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/rotting-oranges.md) | 广度优先搜索、数组、矩阵 | 中等 | | [0995. K 连续位的最小翻转次数](https://leetcode.cn/problems/minimum-number-of-k-consecutive-bit-flips/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-number-of-k-consecutive-bit-flips.md) | 位运算、队列、数组、前缀和、滑动窗口 | 困难 | | [0996. 平方数组的数目](https://leetcode.cn/problems/number-of-squareful-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/number-of-squareful-arrays.md) | 位运算、数组、哈希表、数学、动态规划、回溯、状态压缩 | 困难 | | [0997. 找到小镇的法官](https://leetcode.cn/problems/find-the-town-judge/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/find-the-town-judge.md) | 图、数组、哈希表 | 简单 | | [0998. 最大二叉树 II](https://leetcode.cn/problems/maximum-binary-tree-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/maximum-binary-tree-ii.md) | 树、二叉树 | 中等 | | [0999. 可以被一步捕获的棋子数](https://leetcode.cn/problems/available-captures-for-rook/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/available-captures-for-rook.md) | 数组、矩阵、模拟 | 简单 | ### 第 1000 ~ 1099 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1000. 合并石头的最低成本](https://leetcode.cn/problems/minimum-cost-to-merge-stones/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/minimum-cost-to-merge-stones.md) | 数组、动态规划、前缀和 | 困难 | | [1002. 查找共用字符](https://leetcode.cn/problems/find-common-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/find-common-characters.md) | 数组、哈希表、字符串 | 简单 | | [1004. 最大连续1的个数 III](https://leetcode.cn/problems/max-consecutive-ones-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/max-consecutive-ones-iii.md) | 数组、二分查找、前缀和、滑动窗口 | 中等 | | [1005. K 次取反后最大化的数组和](https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/maximize-sum-of-array-after-k-negations.md) | 贪心、数组、排序 | 简单 | | [1008. 前序遍历构造二叉搜索树](https://leetcode.cn/problems/construct-binary-search-tree-from-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/construct-binary-search-tree-from-preorder-traversal.md) | 栈、树、二叉搜索树、数组、二叉树、单调栈 | 中等 | | [1009. 十进制整数的反码](https://leetcode.cn/problems/complement-of-base-10-integer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/complement-of-base-10-integer.md) | 位运算 | 简单 | | [1011. 在 D 天内送达包裹的能力](https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/capacity-to-ship-packages-within-d-days.md) | 数组、二分查找 | 中等 | | [1012. 至少有 1 位重复的数字](https://leetcode.cn/problems/numbers-with-repeated-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/numbers-with-repeated-digits.md) | 数学、动态规划 | 困难 | | [1014. 最佳观光组合](https://leetcode.cn/problems/best-sightseeing-pair/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/best-sightseeing-pair.md) | 数组、动态规划 | 中等 | | [1020. 飞地的数量](https://leetcode.cn/problems/number-of-enclaves/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/number-of-enclaves.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [1021. 删除最外层的括号](https://leetcode.cn/problems/remove-outermost-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/remove-outermost-parentheses.md) | 栈、字符串 | 简单 | | [1023. 驼峰式匹配](https://leetcode.cn/problems/camelcase-matching/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/camelcase-matching.md) | 字典树、数组、双指针、字符串、字符串匹配 | 中等 | | [1025. 除数博弈](https://leetcode.cn/problems/divisor-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/divisor-game.md) | 脑筋急转弯、数学、动态规划、博弈 | 简单 | | [1028. 从先序遍历还原二叉树](https://leetcode.cn/problems/recover-a-tree-from-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/recover-a-tree-from-preorder-traversal.md) | 树、深度优先搜索、字符串、二叉树 | 困难 | | [1029. 两地调度](https://leetcode.cn/problems/two-city-scheduling/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/two-city-scheduling.md) | 贪心、数组、排序 | 中等 | | [1032. 字符流](https://leetcode.cn/problems/stream-of-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/stream-of-characters.md) | 设计、字典树、数组、字符串、数据流 | 困难 | | [1034. 边界着色](https://leetcode.cn/problems/coloring-a-border/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/coloring-a-border.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 中等 | | [1035. 不相交的线](https://leetcode.cn/problems/uncrossed-lines/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/uncrossed-lines.md) | 数组、动态规划 | 中等 | | [1037. 有效的回旋镖](https://leetcode.cn/problems/valid-boomerang/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/valid-boomerang.md) | 几何、数组、数学 | 简单 | | [1038. 从二叉搜索树到更大和树](https://leetcode.cn/problems/binary-search-tree-to-greater-sum-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/binary-search-tree-to-greater-sum-tree.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [1039. 多边形三角剖分的最低得分](https://leetcode.cn/problems/minimum-score-triangulation-of-polygon/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/minimum-score-triangulation-of-polygon.md) | 数组、动态规划 | 中等 | | [1041. 困于环中的机器人](https://leetcode.cn/problems/robot-bounded-in-circle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/robot-bounded-in-circle.md) | 数学、字符串、模拟 | 中等 | | [1047. 删除字符串中的所有相邻重复项](https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/remove-all-adjacent-duplicates-in-string.md) | 栈、字符串 | 简单 | | [1049. 最后一块石头的重量 II](https://leetcode.cn/problems/last-stone-weight-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/last-stone-weight-ii.md) | 数组、动态规划 | 中等 | | [1051. 高度检查器](https://leetcode.cn/problems/height-checker/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/height-checker.md) | 数组、计数排序、排序 | 简单 | | [1052. 爱生气的书店老板](https://leetcode.cn/problems/grumpy-bookstore-owner/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/grumpy-bookstore-owner.md) | 数组、滑动窗口 | 中等 | | [1065. 字符串的索引对](https://leetcode.cn/problems/index-pairs-of-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/index-pairs-of-a-string.md) | 字典树、数组、字符串、排序 | 简单 | | [1079. 活字印刷](https://leetcode.cn/problems/letter-tile-possibilities/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/letter-tile-possibilities.md) | 哈希表、字符串、回溯、计数 | 中等 | | [1081. 不同字符的最小子序列](https://leetcode.cn/problems/smallest-subsequence-of-distinct-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/smallest-subsequence-of-distinct-characters.md) | 栈、贪心、字符串、单调栈 | 中等 | | [1089. 复写零](https://leetcode.cn/problems/duplicate-zeros/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/duplicate-zeros.md) | 数组、双指针 | 简单 | | [1091. 二进制矩阵中的最短路径](https://leetcode.cn/problems/shortest-path-in-binary-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/shortest-path-in-binary-matrix.md) | 广度优先搜索、数组、矩阵 | 中等 | | [1095. 山脉数组中查找目标值](https://leetcode.cn/problems/find-in-mountain-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/find-in-mountain-array.md) | 数组、二分查找、交互 | 困难 | | [1099. 小于 K 的两数之和](https://leetcode.cn/problems/two-sum-less-than-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/two-sum-less-than-k.md) | 数组、双指针、二分查找、排序 | 简单 | ### 第 1100 ~ 1199 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1100. 长度为 K 的无重复字符子串](https://leetcode.cn/problems/find-k-length-substrings-with-no-repeated-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/find-k-length-substrings-with-no-repeated-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [1103. 分糖果 II](https://leetcode.cn/problems/distribute-candies-to-people/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/distribute-candies-to-people.md) | 数学、模拟 | 简单 | | [1108. IP 地址无效化](https://leetcode.cn/problems/defanging-an-ip-address/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/defanging-an-ip-address.md) | 字符串 | 简单 | | [1109. 航班预订统计](https://leetcode.cn/problems/corporate-flight-bookings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/corporate-flight-bookings.md) | 数组、前缀和 | 中等 | | [1110. 删点成林](https://leetcode.cn/problems/delete-nodes-and-return-forest/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/delete-nodes-and-return-forest.md) | 树、深度优先搜索、数组、哈希表、二叉树 | 中等 | | [1122. 数组的相对排序](https://leetcode.cn/problems/relative-sort-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/relative-sort-array.md) | 数组、哈希表、计数排序、排序 | 简单 | | [1136. 并行课程](https://leetcode.cn/problems/parallel-courses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/parallel-courses.md) | 图、拓扑排序 | 中等 | | [1137. 第 N 个泰波那契数](https://leetcode.cn/problems/n-th-tribonacci-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/n-th-tribonacci-number.md) | 记忆化搜索、数学、动态规划 | 简单 | | [1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/longest-common-subsequence.md) | 字符串、动态规划 | 中等 | | [1151. 最少交换次数来组合所有的 1](https://leetcode.cn/problems/minimum-swaps-to-group-all-1s-together/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/minimum-swaps-to-group-all-1s-together.md) | 数组、滑动窗口 | 中等 | | [1155. 掷骰子等于目标和的方法数](https://leetcode.cn/problems/number-of-dice-rolls-with-target-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/number-of-dice-rolls-with-target-sum.md) | 动态规划 | 中等 | | [1161. 最大层内元素和](https://leetcode.cn/problems/maximum-level-sum-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/maximum-level-sum-of-a-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [1176. 健身计划评估](https://leetcode.cn/problems/diet-plan-performance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/diet-plan-performance.md) | 数组、滑动窗口 | 简单 | | [1184. 公交站间的距离](https://leetcode.cn/problems/distance-between-bus-stops/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/distance-between-bus-stops.md) | 数组 | 简单 | ### 第 1200 ~ 1299 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1202. 交换字符串中的元素](https://leetcode.cn/problems/smallest-string-with-swaps/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/smallest-string-with-swaps.md) | 深度优先搜索、广度优先搜索、并查集、数组、哈希表、字符串、排序 | 中等 | | [1208. 尽可能使字符串相等](https://leetcode.cn/problems/get-equal-substrings-within-budget/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/get-equal-substrings-within-budget.md) | 字符串、二分查找、前缀和、滑动窗口 | 中等 | | [1217. 玩筹码](https://leetcode.cn/problems/minimum-cost-to-move-chips-to-the-same-position/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/minimum-cost-to-move-chips-to-the-same-position.md) | 贪心、数组、数学 | 简单 | | [1220. 统计元音字母序列的数目](https://leetcode.cn/problems/count-vowels-permutation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/count-vowels-permutation.md) | 动态规划 | 困难 | | [1227. 飞机座位分配概率](https://leetcode.cn/problems/airplane-seat-assignment-probability/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/airplane-seat-assignment-probability.md) | 脑筋急转弯、数学、动态规划、概率与统计 | 中等 | | [1229. 安排会议日程](https://leetcode.cn/problems/meeting-scheduler/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/meeting-scheduler.md) | 数组、双指针、排序 | 中等 | | [1232. 缀点成线](https://leetcode.cn/problems/check-if-it-is-a-straight-line/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/check-if-it-is-a-straight-line.md) | 几何、数组、数学 | 简单 | | [1245. 树的直径](https://leetcode.cn/problems/tree-diameter/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/tree-diameter.md) | 树、深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [1247. 交换字符使得字符串相同](https://leetcode.cn/problems/minimum-swaps-to-make-strings-equal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/minimum-swaps-to-make-strings-equal.md) | 贪心、数学、字符串 | 中等 | | [1253. 重构 2 行二进制矩阵](https://leetcode.cn/problems/reconstruct-a-2-row-binary-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/reconstruct-a-2-row-binary-matrix.md) | 贪心、数组、矩阵 | 中等 | | [1254. 统计封闭岛屿的数目](https://leetcode.cn/problems/number-of-closed-islands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/number-of-closed-islands.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [1261. 在受污染的二叉树中查找元素](https://leetcode.cn/problems/find-elements-in-a-contaminated-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/find-elements-in-a-contaminated-binary-tree.md) | 树、深度优先搜索、广度优先搜索、设计、哈希表、二叉树 | 中等 | | [1266. 访问所有点的最小时间](https://leetcode.cn/problems/minimum-time-visiting-all-points/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/minimum-time-visiting-all-points.md) | 几何、数组、数学 | 简单 | | [1268. 搜索推荐系统](https://leetcode.cn/problems/search-suggestions-system/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/search-suggestions-system.md) | 字典树、数组、字符串、二分查找、排序、堆(优先队列) | 中等 | | [1281. 整数的各位积和之差](https://leetcode.cn/problems/subtract-the-product-and-sum-of-digits-of-an-integer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/subtract-the-product-and-sum-of-digits-of-an-integer.md) | 数学 | 简单 | | [1296. 划分数组为连续数字的集合](https://leetcode.cn/problems/divide-array-in-sets-of-k-consecutive-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/divide-array-in-sets-of-k-consecutive-numbers.md) | 贪心、数组、哈希表、排序 | 中等 | ### 第 1300 ~ 1399 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1300. 转变数组后最接近目标值的数组和](https://leetcode.cn/problems/sum-of-mutated-array-closest-to-target/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/sum-of-mutated-array-closest-to-target.md) | 数组、二分查找、排序 | 中等 | | [1305. 两棵二叉搜索树中的所有元素](https://leetcode.cn/problems/all-elements-in-two-binary-search-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/all-elements-in-two-binary-search-trees.md) | 树、深度优先搜索、二叉搜索树、二叉树、排序 | 中等 | | [1310. 子数组异或查询](https://leetcode.cn/problems/xor-queries-of-a-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/xor-queries-of-a-subarray.md) | 位运算、数组、前缀和 | 中等 | | [1313. 解压缩编码列表](https://leetcode.cn/problems/decompress-run-length-encoded-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/decompress-run-length-encoded-list.md) | 数组 | 简单 | | [1317. 将整数转换为两个无零整数的和](https://leetcode.cn/problems/convert-integer-to-the-sum-of-two-no-zero-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/convert-integer-to-the-sum-of-two-no-zero-integers.md) | 数学 | 简单 | | [1319. 连通网络的操作次数](https://leetcode.cn/problems/number-of-operations-to-make-network-connected/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/number-of-operations-to-make-network-connected.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [1324. 竖直打印单词](https://leetcode.cn/problems/print-words-vertically/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/print-words-vertically.md) | 数组、字符串、模拟 | 中等 | | [1338. 数组大小减半](https://leetcode.cn/problems/reduce-array-size-to-the-half/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/reduce-array-size-to-the-half.md) | 贪心、数组、哈希表、排序、堆(优先队列) | 中等 | | [1343. 大小为 K 且平均值大于等于阈值的子数组数目](https://leetcode.cn/problems/number-of-sub-arrays-of-size-k-and-average-greater-than-or-equal-to-threshold/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/number-of-sub-arrays-of-size-k-and-average-greater-than-or-equal-to-threshold.md) | 数组、滑动窗口 | 中等 | | [1344. 时钟指针的夹角](https://leetcode.cn/problems/angle-between-hands-of-a-clock/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/angle-between-hands-of-a-clock.md) | 数学 | 中等 | | [1347. 制造字母异位词的最小步骤数](https://leetcode.cn/problems/minimum-number-of-steps-to-make-two-strings-anagram/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/minimum-number-of-steps-to-make-two-strings-anagram.md) | 哈希表、字符串、计数 | 中等 | | [1349. 参加考试的最大学生数](https://leetcode.cn/problems/maximum-students-taking-exam/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/maximum-students-taking-exam.md) | 位运算、数组、动态规划、状态压缩、矩阵 | 困难 | | [1358. 包含所有三种字符的子字符串数目](https://leetcode.cn/problems/number-of-substrings-containing-all-three-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/number-of-substrings-containing-all-three-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [1362. 最接近的因数](https://leetcode.cn/problems/closest-divisors/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/closest-divisors.md) | 数学 | 中等 | | [1381. 设计一个支持增量操作的栈](https://leetcode.cn/problems/design-a-stack-with-increment-operation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/design-a-stack-with-increment-operation.md) | 栈、设计、数组 | 中等 | ### 第 1400 ~ 1499 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1400. 构造 K 个回文字符串](https://leetcode.cn/problems/construct-k-palindrome-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/construct-k-palindrome-strings.md) | 贪心、哈希表、字符串、计数 | 中等 | | [1408. 数组中的字符串匹配](https://leetcode.cn/problems/string-matching-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/string-matching-in-an-array.md) | 数组、字符串、字符串匹配 | 简单 | | [1422. 分割字符串的最大得分](https://leetcode.cn/problems/maximum-score-after-splitting-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/maximum-score-after-splitting-a-string.md) | 字符串、前缀和 | 简单 | | [1423. 可获得的最大点数](https://leetcode.cn/problems/maximum-points-you-can-obtain-from-cards/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/maximum-points-you-can-obtain-from-cards.md) | 数组、前缀和、滑动窗口 | 中等 | | [1438. 绝对差不超过限制的最长连续子数组](https://leetcode.cn/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit.md) | 队列、数组、有序集合、滑动窗口、单调队列、堆(优先队列) | 中等 | | [1446. 连续字符](https://leetcode.cn/problems/consecutive-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/consecutive-characters.md) | 字符串 | 简单 | | [1447. 最简分数](https://leetcode.cn/problems/simplified-fractions/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/simplified-fractions.md) | 数学、字符串、数论 | 中等 | | [1449. 数位成本和为目标值的最大数字](https://leetcode.cn/problems/form-largest-integer-with-digits-that-add-up-to-target/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/form-largest-integer-with-digits-that-add-up-to-target.md) | 数组、动态规划 | 困难 | | [1450. 在既定时间做作业的学生人数](https://leetcode.cn/problems/number-of-students-doing-homework-at-a-given-time/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/number-of-students-doing-homework-at-a-given-time.md) | 数组 | 简单 | | [1451. 重新排列句子中的单词](https://leetcode.cn/problems/rearrange-words-in-a-sentence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/rearrange-words-in-a-sentence.md) | 字符串、排序 | 中等 | | [1456. 定长子串中元音的最大数目](https://leetcode.cn/problems/maximum-number-of-vowels-in-a-substring-of-given-length/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/maximum-number-of-vowels-in-a-substring-of-given-length.md) | 字符串、滑动窗口 | 中等 | | [1476. 子矩形查询](https://leetcode.cn/problems/subrectangle-queries/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/subrectangle-queries.md) | 设计、数组、矩阵 | 中等 | | [1480. 一维数组的动态和](https://leetcode.cn/problems/running-sum-of-1d-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/running-sum-of-1d-array.md) | 数组、前缀和 | 简单 | | [1482. 制作 m 束花所需的最少天数](https://leetcode.cn/problems/minimum-number-of-days-to-make-m-bouquets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/minimum-number-of-days-to-make-m-bouquets.md) | 数组、二分查找 | 中等 | | [1486. 数组异或操作](https://leetcode.cn/problems/xor-operation-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/xor-operation-in-an-array.md) | 位运算、数学 | 简单 | | [1491. 去掉最低工资和最高工资后的工资平均值](https://leetcode.cn/problems/average-salary-excluding-the-minimum-and-maximum-salary/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/average-salary-excluding-the-minimum-and-maximum-salary.md) | 数组、排序 | 简单 | | [1493. 删掉一个元素以后全为 1 的最长子数组](https://leetcode.cn/problems/longest-subarray-of-1s-after-deleting-one-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/longest-subarray-of-1s-after-deleting-one-element.md) | 数组、动态规划、滑动窗口 | 中等 | | [1496. 判断路径是否相交](https://leetcode.cn/problems/path-crossing/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/path-crossing.md) | 哈希表、字符串 | 简单 | ### 第 1500 ~ 1599 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1502. 判断能否形成等差数列](https://leetcode.cn/problems/can-make-arithmetic-progression-from-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/can-make-arithmetic-progression-from-sequence.md) | 数组、排序 | 简单 | | [1507. 转变日期格式](https://leetcode.cn/problems/reformat-date/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/reformat-date.md) | 字符串 | 简单 | | [1523. 在区间范围内统计奇数数目](https://leetcode.cn/problems/count-odd-numbers-in-an-interval-range/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/count-odd-numbers-in-an-interval-range.md) | 数学 | 简单 | | [1534. 统计好三元组](https://leetcode.cn/problems/count-good-triplets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/count-good-triplets.md) | 数组、枚举 | 简单 | | [1547. 切棍子的最小成本](https://leetcode.cn/problems/minimum-cost-to-cut-a-stick/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-cost-to-cut-a-stick.md) | 数组、动态规划、排序 | 困难 | | [1551. 使数组中所有元素相等的最小操作数](https://leetcode.cn/problems/minimum-operations-to-make-array-equal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-operations-to-make-array-equal.md) | 数学 | 中等 | | [1556. 千位分隔数](https://leetcode.cn/problems/thousand-separator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/thousand-separator.md) | 字符串 | 简单 | | [1561. 你可以获得的最大硬币数目](https://leetcode.cn/problems/maximum-number-of-coins-you-can-get/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/maximum-number-of-coins-you-can-get.md) | 贪心、数组、数学、博弈、排序 | 中等 | | [1567. 乘积为正数的最长子数组长度](https://leetcode.cn/problems/maximum-length-of-subarray-with-positive-product/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/maximum-length-of-subarray-with-positive-product.md) | 贪心、数组、动态规划 | 中等 | | [1582. 二进制矩阵中的特殊位置](https://leetcode.cn/problems/special-positions-in-a-binary-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/special-positions-in-a-binary-matrix.md) | 数组、矩阵 | 简单 | | [1584. 连接所有点的最小费用](https://leetcode.cn/problems/min-cost-to-connect-all-points/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/min-cost-to-connect-all-points.md) | 并查集、图、数组、最小生成树 | 中等 | | [1593. 拆分字符串使唯一子字符串的数目最大](https://leetcode.cn/problems/split-a-string-into-the-max-number-of-unique-substrings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/split-a-string-into-the-max-number-of-unique-substrings.md) | 哈希表、字符串、回溯 | 中等 | | [1595. 连通两组点的最小成本](https://leetcode.cn/problems/minimum-cost-to-connect-two-groups-of-points/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-cost-to-connect-two-groups-of-points.md) | 位运算、数组、动态规划、状态压缩、矩阵 | 困难 | ### 第 1600 ~ 1699 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1603. 设计停车系统](https://leetcode.cn/problems/design-parking-system/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/design-parking-system.md) | 设计、计数、模拟 | 简单 | | [1605. 给定行和列的和求可行矩阵](https://leetcode.cn/problems/find-valid-matrix-given-row-and-column-sums/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/find-valid-matrix-given-row-and-column-sums.md) | 贪心、数组、矩阵 | 中等 | | [1614. 括号的最大嵌套深度](https://leetcode.cn/problems/maximum-nesting-depth-of-the-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/maximum-nesting-depth-of-the-parentheses.md) | 栈、字符串 | 简单 | | [1617. 统计子树中城市之间最大距离](https://leetcode.cn/problems/count-subtrees-with-max-distance-between-cities/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/count-subtrees-with-max-distance-between-cities.md) | 位运算、树、动态规划、状态压缩、枚举 | 困难 | | [1631. 最小体力消耗路径](https://leetcode.cn/problems/path-with-minimum-effort/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/path-with-minimum-effort.md) | 深度优先搜索、广度优先搜索、并查集、数组、二分查找、矩阵、堆(优先队列) | 中等 | | [1641. 统计字典序元音字符串的数目](https://leetcode.cn/problems/count-sorted-vowel-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/count-sorted-vowel-strings.md) | 数学、动态规划、组合数学 | 中等 | | [1646. 获取生成数组中的最大值](https://leetcode.cn/problems/get-maximum-in-generated-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/get-maximum-in-generated-array.md) | 数组、模拟 | 简单 | | [1647. 字符频次唯一的最小删除次数](https://leetcode.cn/problems/minimum-deletions-to-make-character-frequencies-unique/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/minimum-deletions-to-make-character-frequencies-unique.md) | 贪心、哈希表、字符串、排序 | 中等 | | [1657. 确定两个字符串是否接近](https://leetcode.cn/problems/determine-if-two-strings-are-close/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/determine-if-two-strings-are-close.md) | 哈希表、字符串、计数、排序 | 中等 | | [1658. 将 x 减到 0 的最小操作数](https://leetcode.cn/problems/minimum-operations-to-reduce-x-to-zero/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/minimum-operations-to-reduce-x-to-zero.md) | 数组、哈希表、二分查找、前缀和、滑动窗口 | 中等 | | [1672. 最富有客户的资产总量](https://leetcode.cn/problems/richest-customer-wealth/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/richest-customer-wealth.md) | 数组、矩阵 | 简单 | | [1695. 删除子数组的最大得分](https://leetcode.cn/problems/maximum-erasure-value/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/maximum-erasure-value.md) | 数组、哈希表、滑动窗口 | 中等 | | [1698. 字符串的不同子字符串个数](https://leetcode.cn/problems/number-of-distinct-substrings-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/number-of-distinct-substrings-in-a-string.md) | 字典树、字符串、后缀数组、哈希函数、滚动哈希 | 中等 | ### 第 1700 ~ 1799 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1710. 卡车上的最大单元数](https://leetcode.cn/problems/maximum-units-on-a-truck/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/maximum-units-on-a-truck.md) | 贪心、数组、排序 | 简单 | | [1716. 计算力扣银行的钱](https://leetcode.cn/problems/calculate-money-in-leetcode-bank/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/calculate-money-in-leetcode-bank.md) | 数学 | 简单 | | [1720. 解码异或后的数组](https://leetcode.cn/problems/decode-xored-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/decode-xored-array.md) | 位运算、数组 | 简单 | | [1726. 同积元组](https://leetcode.cn/problems/tuple-with-same-product/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/tuple-with-same-product.md) | 数组、哈希表、计数 | 中等 | | [1736. 替换隐藏数字得到的最晚时间](https://leetcode.cn/problems/latest-time-by-replacing-hidden-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/latest-time-by-replacing-hidden-digits.md) | 贪心、字符串 | 简单 | | [1742. 盒子中小球的最大数量](https://leetcode.cn/problems/maximum-number-of-balls-in-a-box/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/maximum-number-of-balls-in-a-box.md) | 哈希表、数学、计数 | 简单 | | [1749. 任意子数组和的绝对值的最大值](https://leetcode.cn/problems/maximum-absolute-sum-of-any-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/maximum-absolute-sum-of-any-subarray.md) | 数组、动态规划 | 中等 | | [1763. 最长的美好子字符串](https://leetcode.cn/problems/longest-nice-substring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/longest-nice-substring.md) | 位运算、哈希表、字符串、分治、滑动窗口 | 简单 | | [1779. 找到最近的有相同 X 或 Y 坐标的点](https://leetcode.cn/problems/find-nearest-point-that-has-the-same-x-or-y-coordinate/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/find-nearest-point-that-has-the-same-x-or-y-coordinate.md) | 数组 | 简单 | | [1790. 仅执行一次字符串交换能否使两个字符串相等](https://leetcode.cn/problems/check-if-one-string-swap-can-make-strings-equal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/check-if-one-string-swap-can-make-strings-equal.md) | 哈希表、字符串、计数 | 简单 | | [1791. 找出星型图的中心节点](https://leetcode.cn/problems/find-center-of-star-graph/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/find-center-of-star-graph.md) | 图 | 简单 | ### 第 1800 ~ 1899 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1822. 数组元素积的符号](https://leetcode.cn/problems/sign-of-the-product-of-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/sign-of-the-product-of-an-array.md) | 数组、数学 | 简单 | | [1827. 最少操作使数组递增](https://leetcode.cn/problems/minimum-operations-to-make-the-array-increasing/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/minimum-operations-to-make-the-array-increasing.md) | 贪心、数组 | 简单 | | [1833. 雪糕的最大数量](https://leetcode.cn/problems/maximum-ice-cream-bars/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/maximum-ice-cream-bars.md) | 贪心、数组、计数排序、排序 | 中等 | | [1844. 将所有数字用字符替换](https://leetcode.cn/problems/replace-all-digits-with-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/replace-all-digits-with-characters.md) | 字符串 | 简单 | | [1858. 包含所有前缀的最长单词](https://leetcode.cn/problems/longest-word-with-all-prefixes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/longest-word-with-all-prefixes.md) | 深度优先搜索、字典树、数组、字符串 | 中等 | | [1859. 将句子排序](https://leetcode.cn/problems/sorting-the-sentence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/sorting-the-sentence.md) | 字符串、排序 | 简单 | | [1876. 长度为三且各字符不同的子字符串](https://leetcode.cn/problems/substrings-of-size-three-with-distinct-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/substrings-of-size-three-with-distinct-characters.md) | 哈希表、字符串、计数、滑动窗口 | 简单 | | [1877. 数组中最大数对和的最小值](https://leetcode.cn/problems/minimize-maximum-pair-sum-in-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/minimize-maximum-pair-sum-in-array.md) | 贪心、数组、双指针、排序 | 中等 | | [1879. 两个数组最小的异或值之和](https://leetcode.cn/problems/minimum-xor-sum-of-two-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/minimum-xor-sum-of-two-arrays.md) | 位运算、数组、动态规划、状态压缩 | 困难 | | [1893. 检查是否区域内所有整数都被覆盖](https://leetcode.cn/problems/check-if-all-the-integers-in-a-range-are-covered/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/check-if-all-the-integers-in-a-range-are-covered.md) | 数组、哈希表、前缀和 | 简单 | | [1897. 重新分配字符使所有字符串都相等](https://leetcode.cn/problems/redistribute-characters-to-make-all-strings-equal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/redistribute-characters-to-make-all-strings-equal.md) | 哈希表、字符串、计数 | 简单 | ### 第 1900 ~ 1999 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1903. 字符串中的最大奇数](https://leetcode.cn/problems/largest-odd-number-in-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/largest-odd-number-in-string.md) | 贪心、数学、字符串 | 简单 | | [1921. 消灭怪物的最大数量](https://leetcode.cn/problems/eliminate-maximum-number-of-monsters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/eliminate-maximum-number-of-monsters.md) | 贪心、数组、排序 | 中等 | | [1925. 统计平方和三元组的数目](https://leetcode.cn/problems/count-square-sum-triples/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/count-square-sum-triples.md) | 数学、枚举 | 简单 | | [1929. 数组串联](https://leetcode.cn/problems/concatenation-of-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/concatenation-of-array.md) | 数组、模拟 | 简单 | | [1930. 长度为 3 的不同回文子序列](https://leetcode.cn/problems/unique-length-3-palindromic-subsequences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/unique-length-3-palindromic-subsequences.md) | 位运算、哈希表、字符串、前缀和 | 中等 | | [1936. 新增的最少台阶数](https://leetcode.cn/problems/add-minimum-number-of-rungs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/add-minimum-number-of-rungs.md) | 贪心、数组 | 中等 | | [1941. 检查是否所有字符出现次数相同](https://leetcode.cn/problems/check-if-all-characters-have-equal-number-of-occurrences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/check-if-all-characters-have-equal-number-of-occurrences.md) | 哈希表、字符串、计数 | 简单 | | [1947. 最大兼容性评分和](https://leetcode.cn/problems/maximum-compatibility-score-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/maximum-compatibility-score-sum.md) | 位运算、数组、动态规划、回溯、状态压缩 | 中等 | | [1984. 学生分数的最小差值](https://leetcode.cn/problems/minimum-difference-between-highest-and-lowest-of-k-scores/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/minimum-difference-between-highest-and-lowest-of-k-scores.md) | 数组、排序、滑动窗口 | 简单 | | [1986. 完成任务的最少工作时间段](https://leetcode.cn/problems/minimum-number-of-work-sessions-to-finish-the-tasks/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/minimum-number-of-work-sessions-to-finish-the-tasks.md) | 位运算、数组、动态规划、回溯、状态压缩 | 中等 | | [1991. 找到数组的中间位置](https://leetcode.cn/problems/find-the-middle-index-in-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/find-the-middle-index-in-array.md) | 数组、前缀和 | 简单 | | [1994. 好子集的数目](https://leetcode.cn/problems/the-number-of-good-subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/the-number-of-good-subsets.md) | 位运算、数组、哈希表、数学、动态规划、状态压缩、计数、数论 | 困难 | ### 第 2000 ~ 2099 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [2011. 执行操作后的变量值](https://leetcode.cn/problems/final-value-of-variable-after-performing-operations/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/final-value-of-variable-after-performing-operations.md) | 数组、字符串、模拟 | 简单 | | [2023. 连接后等于目标字符串的字符串对](https://leetcode.cn/problems/number-of-pairs-of-strings-with-concatenation-equal-to-target/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/number-of-pairs-of-strings-with-concatenation-equal-to-target.md) | 数组、哈希表、字符串、计数 | 中等 | | [2050. 并行课程 III](https://leetcode.cn/problems/parallel-courses-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/parallel-courses-iii.md) | 图、拓扑排序、数组、动态规划 | 困难 | ### 第 2100 ~ 2199 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [2156. 查找给定哈希值的子串](https://leetcode.cn/problems/find-substring-with-given-hash-value/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/find-substring-with-given-hash-value.md) | 字符串、滑动窗口、哈希函数、滚动哈希 | 困难 | | [2172. 数组的最大与和](https://leetcode.cn/problems/maximum-and-sum-of-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/maximum-and-sum-of-array.md) | 位运算、数组、动态规划、状态压缩 | 困难 | ### 第 2200 ~ 2299 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [2235. 两整数相加](https://leetcode.cn/problems/add-two-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/add-two-integers.md) | 数学 | 简单 | | [2246. 相邻字符不同的最长路径](https://leetcode.cn/problems/longest-path-with-different-adjacent-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/longest-path-with-different-adjacent-characters.md) | 树、深度优先搜索、图、拓扑排序、数组、字符串 | 困难 | | [2249. 统计圆内格点数目](https://leetcode.cn/problems/count-lattice-points-inside-a-circle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/count-lattice-points-inside-a-circle.md) | 几何、数组、哈希表、数学、枚举 | 中等 | | [2276. 统计区间中的整数数目](https://leetcode.cn/problems/count-integers-in-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/count-integers-in-intervals.md) | 设计、线段树、有序集合 | 困难 | ### 第 2300 ~ 2399 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [2376. 统计特殊整数](https://leetcode.cn/problems/count-special-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2300-2399/count-special-integers.md) | 数学、动态规划 | 困难 | ### 第 2400 ~ 2499 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [2427. 公因子的数目](https://leetcode.cn/problems/number-of-common-factors/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2400-2499/number-of-common-factors.md) | 数学、枚举、数论 | 简单 | ### 第 2500 ~ 2599 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [2538. 最大价值和与最小价值和的差值](https://leetcode.cn/problems/difference-between-maximum-and-minimum-price-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2500-2599/difference-between-maximum-and-minimum-price-sum.md) | 树、深度优先搜索、数组、动态规划 | 困难 | | [2585. 获得分数的方法数](https://leetcode.cn/problems/number-of-ways-to-earn-points/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2500-2599/number-of-ways-to-earn-points.md) | 数组、动态规划 | 困难 | ### 第 2700 ~ 2799 题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [2719. 统计整数数目](https://leetcode.cn/problems/count-of-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2700-2799/count-of-integers.md) | 数学、字符串、动态规划 | 困难 | ### LCR 系列 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [LCR 001. 两数相除](https://leetcode.cn/problems/xoh6Oh/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/xoh6Oh.md) | 数学 | 简单 | | [LCR 002. 二进制求和](https://leetcode.cn/problems/JFETK5/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/JFETK5.md) | 位运算、数学、字符串、模拟 | 简单 | | [LCR 003. 比特位计数](https://leetcode.cn/problems/w3tCBm/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/w3tCBm.md) | 位运算、动态规划 | 简单 | | [LCR 004. 只出现一次的数字 II](https://leetcode.cn/problems/WGki4K/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/WGki4K.md) | 位运算、数组 | 中等 | | [LCR 005. 最大单词长度乘积](https://leetcode.cn/problems/aseY1I/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/aseY1I.md) | 位运算、数组、字符串 | 中等 | | [LCR 006. 两数之和 II - 输入有序数组](https://leetcode.cn/problems/kLl5u1/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/kLl5u1.md) | 数组、双指针、二分查找 | 简单 | | [LCR 007. 三数之和](https://leetcode.cn/problems/1fGaJU/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/1fGaJU.md) | 数组、双指针、排序 | 中等 | | [LCR 008. 长度最小的子数组](https://leetcode.cn/problems/2VG8Kg/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/2VG8Kg.md) | 数组、二分查找、前缀和、滑动窗口 | 中等 | | [LCR 009. 乘积小于 K 的子数组](https://leetcode.cn/problems/ZVAVXX/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ZVAVXX.md) | 数组、滑动窗口 | 中等 | | [LCR 010. 和为 K 的子数组](https://leetcode.cn/problems/QTMn0o/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/QTMn0o.md) | 数组、哈希表、前缀和 | 中等 | | [LCR 011. 连续数组](https://leetcode.cn/problems/A1NYOS/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/A1NYOS.md) | 数组、哈希表、前缀和 | 中等 | | [LCR 012. 寻找数组的中心下标](https://leetcode.cn/problems/tvdfij/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/tvdfij.md) | 数组、前缀和 | 简单 | | [LCR 013. 二维区域和检索 - 矩阵不可变](https://leetcode.cn/problems/O4NDxx/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/O4NDxx.md) | 设计、数组、矩阵、前缀和 | 中等 | | [LCR 016. 无重复字符的最长子串](https://leetcode.cn/problems/wtcaE1/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/wtcaE1.md) | 哈希表、字符串、滑动窗口 | 中等 | | [LCR 017. 最小覆盖子串](https://leetcode.cn/problems/M1oyTv/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/M1oyTv.md) | 哈希表、字符串、滑动窗口 | 困难 | | [LCR 018. 验证回文串](https://leetcode.cn/problems/XltzEq/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/XltzEq.md) | 双指针、字符串 | 简单 | | [LCR 019. 验证回文串 II](https://leetcode.cn/problems/RQku0D/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/RQku0D.md) | 贪心、双指针、字符串 | 简单 | | [LCR 020. 回文子串](https://leetcode.cn/problems/a7VOhD/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/a7VOhD.md) | 字符串、动态规划 | 中等 | | [LCR 021. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/SLwz0R/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/SLwz0R.md) | 链表、双指针 | 中等 | | [LCR 022. 环形链表 II](https://leetcode.cn/problems/c32eOV/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/c32eOV.md) | 哈希表、链表、双指针 | 中等 | | [LCR 023. 相交链表](https://leetcode.cn/problems/3u1WK4/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/3u1WK4.md) | 哈希表、链表、双指针 | 简单 | | [LCR 024. 反转链表](https://leetcode.cn/problems/UHnkqh/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/UHnkqh.md) | 递归、链表 | 简单 | | [LCR 025. 两数相加 II](https://leetcode.cn/problems/lMSNwu/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/lMSNwu.md) | 栈、链表、数学 | 中等 | | [LCR 026. 重排链表](https://leetcode.cn/problems/LGjMqU/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/LGjMqU.md) | 栈、递归、链表、双指针 | 中等 | | [LCR 027. 回文链表](https://leetcode.cn/problems/aMhZSa/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/aMhZSa.md) | 栈、递归、链表、双指针 | 简单 | | [LCR 028. 扁平化多级双向链表](https://leetcode.cn/problems/Qv1Da2/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/Qv1Da2.md) | 深度优先搜索、链表、双向链表 | 中等 | | [LCR 029. 循环有序列表的插入](https://leetcode.cn/problems/4ueAj6/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/4ueAj6.md) | 链表 | 中等 | | [LCR 030. O(1) 时间插入、删除和获取随机元素](https://leetcode.cn/problems/FortPu/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/FortPu.md) | 设计、数组、哈希表、数学、随机化 | 中等 | | [LCR 031. LRU 缓存](https://leetcode.cn/problems/OrIXps/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/OrIXps.md) | 设计、哈希表、链表、双向链表 | 中等 | | [LCR 032. 有效的字母异位词](https://leetcode.cn/problems/dKk3P7/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/dKk3P7.md) | 哈希表、字符串、排序 | 简单 | | [LCR 033. 字母异位词分组](https://leetcode.cn/problems/sfvd7V/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/sfvd7V.md) | 数组、哈希表、字符串、排序 | 中等 | | [LCR 034. 验证外星语词典](https://leetcode.cn/problems/lwyVBB/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/lwyVBB.md) | 数组、哈希表、字符串 | 简单 | | [LCR 035. 最小时间差](https://leetcode.cn/problems/569nqc/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/569nqc.md) | 数组、数学、字符串、排序 | 中等 | | [LCR 036. 逆波兰表达式求值](https://leetcode.cn/problems/8Zf90G/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/8Zf90G.md) | 栈、数组、数学 | 中等 | | [LCR 037. 行星碰撞](https://leetcode.cn/problems/XagZNi/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/XagZNi.md) | 栈、数组、模拟 | 中等 | | [LCR 038. 每日温度](https://leetcode.cn/problems/iIQa4I/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/iIQa4I.md) | 栈、数组、单调栈 | 中等 | | [LCR 039. 柱状图中最大的矩形](https://leetcode.cn/problems/0ynMMM/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/0ynMMM.md) | 栈、数组、单调栈 | 困难 | | [LCR 041. 数据流中的移动平均值](https://leetcode.cn/problems/qIsx9U/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/qIsx9U.md) | 设计、队列、数组、数据流 | 简单 | | [LCR 042. 最近的请求次数](https://leetcode.cn/problems/H8086Q/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/H8086Q.md) | 设计、队列、数据流 | 简单 | | [LCR 043. 完全二叉树插入器](https://leetcode.cn/problems/NaqhDT/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/NaqhDT.md) | 树、广度优先搜索、设计、二叉树 | 中等 | | [LCR 044. 在每个树行中找最大值](https://leetcode.cn/problems/hPov7L/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/hPov7L.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [LCR 045. 找树左下角的值](https://leetcode.cn/problems/LwUNpT/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/LwUNpT.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [LCR 046. 二叉树的右视图](https://leetcode.cn/problems/WNC0Lk/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/WNC0Lk.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [LCR 047. 二叉树剪枝](https://leetcode.cn/problems/pOCWxh/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/pOCWxh.md) | 树、深度优先搜索、二叉树 | 中等 | | [LCR 048. 二叉树的序列化与反序列化](https://leetcode.cn/problems/h54YBf/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/h54YBf.md) | 树、深度优先搜索、广度优先搜索、设计、字符串、二叉树 | 困难 | | [LCR 049. 求根节点到叶节点数字之和](https://leetcode.cn/problems/3Etpl5/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/3Etpl5.md) | 树、深度优先搜索、二叉树 | 中等 | | [LCR 050. 路径总和 III](https://leetcode.cn/problems/6eUYwP/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/6eUYwP.md) | 树、深度优先搜索、二叉树 | 中等 | | [LCR 051. 二叉树中的最大路径和](https://leetcode.cn/problems/jC7MId/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/jC7MId.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [LCR 052. 递增顺序搜索树](https://leetcode.cn/problems/NYBBNL/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/NYBBNL.md) | 栈、树、深度优先搜索、二叉搜索树、二叉树 | 简单 | | [LCR 053. 二叉搜索树中的中序后继](https://leetcode.cn/problems/P5rCT8/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/P5rCT8.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [LCR 054. 把二叉搜索树转换为累加树](https://leetcode.cn/problems/w6cpku/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/w6cpku.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [LCR 055. 二叉搜索树迭代器](https://leetcode.cn/problems/kTOapQ/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/kTOapQ.md) | 栈、树、设计、二叉搜索树、二叉树、迭代器 | 中等 | | [LCR 056. 两数之和 IV - 输入二叉搜索树](https://leetcode.cn/problems/opLdQZ/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/opLdQZ.md) | 数组、滑动窗口 | 简单 | | [LCR 057. 存在重复元素 III](https://leetcode.cn/problems/7WqeDu/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/7WqeDu.md) | 数组、桶排序、有序集合、排序、滑动窗口 | 中等 | | [LCR 059. 数据流中的第 K 大元素](https://leetcode.cn/problems/jBjn9C/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/jBjn9C.md) | 树、设计、二叉搜索树、二叉树、数据流、堆(优先队列) | 简单 | | [LCR 060. 前 K 个高频元素](https://leetcode.cn/problems/g5c51o/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/g5c51o.md) | 数组、哈希表、分治、桶排序、计数、快速选择、排序、堆(优先队列) | 中等 | | [LCR 062. 实现 Trie (前缀树)](https://leetcode.cn/problems/QC3q1f/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/QC3q1f.md) | 设计、字典树、哈希表、字符串 | 中等 | | [LCR 063. 单词替换](https://leetcode.cn/problems/UhWRSj/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/UhWRSj.md) | 字典树、数组、哈希表、字符串 | 中等 | | [LCR 064. 实现一个魔法字典](https://leetcode.cn/problems/US1pGT/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/US1pGT.md) | 深度优先搜索、设计、字典树、哈希表、字符串 | 中等 | | [LCR 065. 单词的压缩编码](https://leetcode.cn/problems/iSwD2y/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/iSwD2y.md) | 字典树、数组、哈希表、字符串 | 中等 | | [LCR 066. 键值映射](https://leetcode.cn/problems/z1R5dt/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/z1R5dt.md) | 设计、字典树、哈希表、字符串 | 中等 | | [LCR 067. 数组中两个数的最大异或值](https://leetcode.cn/problems/ms70jA/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ms70jA.md) | 位运算、字典树、数组、哈希表 | 中等 | | [LCR 068. 搜索插入位置](https://leetcode.cn/problems/N6YdxV/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/N6YdxV.md) | 数组、二分查找 | 简单 | | [LCR 072. x 的平方根](https://leetcode.cn/problems/jJ0w9p/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/jJ0w9p.md) | 数学、二分查找 | 简单 | | [LCR 073. 爱吃香蕉的狒狒](https://leetcode.cn/problems/nZZqjQ/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/nZZqjQ.md) | 数组、二分查找 | 中等 | | [LCR 074. 合并区间](https://leetcode.cn/problems/SsGoHC/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/SsGoHC.md) | 数组、排序 | 中等 | | [LCR 075. 数组的相对排序](https://leetcode.cn/problems/0H97ZC/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/0H97ZC.md) | 数组、哈希表、计数排序、排序 | 简单 | | [LCR 076. 数组中的第 K 个最大元素](https://leetcode.cn/problems/xx4gT2/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/xx4gT2.md) | 数组、分治、快速选择、排序、堆(优先队列) | 中等 | | [LCR 077. 排序链表](https://leetcode.cn/problems/7WHec2/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/7WHec2.md) | 链表、双指针、分治、排序、归并排序 | 中等 | | [LCR 078. 合并 K 个升序链表](https://leetcode.cn/problems/vvXgSW/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/vvXgSW.md) | 链表、分治、堆(优先队列)、归并排序 | 困难 | | [LCR 079. 子集](https://leetcode.cn/problems/TVdhkn/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/TVdhkn.md) | 位运算、数组、回溯 | 中等 | | [LCR 080. 组合](https://leetcode.cn/problems/uUsW3B/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/uUsW3B.md) | 数组、回溯 | 中等 | | [LCR 081. 组合总和](https://leetcode.cn/problems/Ygoe9J/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/Ygoe9J.md) | 数组、回溯 | 中等 | | [LCR 082. 组合总和 II](https://leetcode.cn/problems/4sjJUc/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/4sjJUc.md) | 数组、回溯 | 中等 | | [LCR 083. 全排列](https://leetcode.cn/problems/VvJkup/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/VvJkup.md) | 数组、回溯 | 中等 | | [LCR 084. 全排列 II](https://leetcode.cn/problems/7p8L0Z/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/7p8L0Z.md) | 数组、回溯 | 中等 | | [LCR 085. 括号生成](https://leetcode.cn/problems/IDBivT/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/IDBivT.md) | 字符串、动态规划、回溯 | 中等 | | [LCR 086. 分割回文串](https://leetcode.cn/problems/M99OJA/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/M99OJA.md) | 深度优先搜索、广度优先搜索、图、哈希表 | 中等 | | [LCR 087. 复原 IP 地址](https://leetcode.cn/problems/0on3uN/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/0on3uN.md) | 字符串、回溯 | 中等 | | [LCR 088. 使用最小花费爬楼梯](https://leetcode.cn/problems/GzCJIP/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/GzCJIP.md) | 数组、动态规划 | 简单 | | [LCR 089. 打家劫舍](https://leetcode.cn/problems/Gu0c2T/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/Gu0c2T.md) | 数组、动态规划 | 中等 | | [LCR 090. 打家劫舍 II](https://leetcode.cn/problems/PzWKhm/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/PzWKhm.md) | 数组、动态规划 | 中等 | | [LCR 093. 最长的斐波那契子序列的长度](https://leetcode.cn/problems/Q91FMA/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/Q91FMA.md) | 数组、哈希表、动态规划 | 中等 | | [LCR 095. 最长公共子序列](https://leetcode.cn/problems/qJnOS7/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/qJnOS7.md) | 字符串、动态规划 | 中等 | | [LCR 097. 不同的子序列](https://leetcode.cn/problems/21dk04/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/21dk04.md) | 字符串、动态规划 | 困难 | | [LCR 098. 不同路径](https://leetcode.cn/problems/2AoeFn/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/2AoeFn.md) | 数学、动态规划、组合数学 | 中等 | | [LCR 101. 分割等和子集](https://leetcode.cn/problems/NUPfPr/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/NUPfPr.md) | 数学、字符串、模拟 | 简单 | | [LCR 102. 目标和](https://leetcode.cn/problems/YaVDxD/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/YaVDxD.md) | 数组、动态规划、回溯 | 中等 | | [LCR 103. 零钱兑换](https://leetcode.cn/problems/gaM7Ch/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/gaM7Ch.md) | 广度优先搜索、数组、动态规划 | 中等 | | [LCR 104. 组合总和 Ⅳ](https://leetcode.cn/problems/D0F0SV/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/D0F0SV.md) | 数组、动态规划 | 中等 | | [LCR 105. 岛屿的最大面积](https://leetcode.cn/problems/ZL6zAn/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ZL6zAn.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [LCR 106. 判断二分图](https://leetcode.cn/problems/vEAB3K/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/vEAB3K.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [LCR 107. 01 矩阵](https://leetcode.cn/problems/2bCMpM/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/2bCMpM.md) | 广度优先搜索、数组、动态规划、矩阵 | 中等 | | [LCR 108. 单词接龙](https://leetcode.cn/problems/om3reC/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/om3reC.md) | 广度优先搜索、哈希表、字符串 | 困难 | | [LCR 109. 打开转盘锁](https://leetcode.cn/problems/zlDJc7/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zlDJc7.md) | 广度优先搜索、数组、哈希表、字符串 | 中等 | | [LCR 111. 除法求值](https://leetcode.cn/problems/vlzXQL/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/vlzXQL.md) | 深度优先搜索、广度优先搜索、并查集、图、数组、最短路 | 中等 | | [LCR 112. 矩阵中的最长递增路径](https://leetcode.cn/problems/fpTFWP/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/fpTFWP.md) | 深度优先搜索、广度优先搜索、图、拓扑排序、记忆化搜索、数组、动态规划、矩阵 | 困难 | | [LCR 113. 课程表 II](https://leetcode.cn/problems/QA2IGt/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/QA2IGt.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [LCR 116. 省份数量](https://leetcode.cn/problems/bLyHh0/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/bLyHh0.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [LCR 118. 冗余连接](https://leetcode.cn/problems/7LpjUW/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/7LpjUW.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [LCR 119. 最长连续序列](https://leetcode.cn/problems/WhsWhI/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/WhsWhI.md) | 并查集、数组、哈希表 | 中等 | | [LCR 120. 寻找文件副本](https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-zhong-fu-de-shu-zi-lcof.md) | 数组、哈希表、排序 | 简单 | | [LCR 121. 寻找目标值 - 二维数组](https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-wei-shu-zu-zhong-de-cha-zhao-lcof.md) | 数组、二分查找、分治、矩阵 | 中等 | | [LCR 122. 路径加密](https://leetcode.cn/problems/ti-huan-kong-ge-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ti-huan-kong-ge-lcof.md) | 字符串 | 简单 | | [LCR 123. 图书整理 I](https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/cong-wei-dao-tou-da-yin-lian-biao-lcof.md) | 栈、递归、链表、双指针 | 简单 | | [LCR 124. 推理二叉树](https://leetcode.cn/problems/zhong-jian-er-cha-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zhong-jian-er-cha-shu-lcof.md) | 树、数组、哈希表、分治、二叉树 | 中等 | | [LCR 125. 图书整理 II](https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/yong-liang-ge-zhan-shi-xian-dui-lie-lcof.md) | 栈、设计、队列 | 简单 | | [LCR 126. 斐波那契数](https://leetcode.cn/problems/fei-bo-na-qi-shu-lie-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/fei-bo-na-qi-shu-lie-lcof.md) | 记忆化搜索、数学、动态规划 | 简单 | | [LCR 127. 跳跃训练](https://leetcode.cn/problems/qing-wa-tiao-tai-jie-wen-ti-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/qing-wa-tiao-tai-jie-wen-ti-lcof.md) | 记忆化搜索、数学、动态规划 | 简单 | | [LCR 128. 库存管理 I](https://leetcode.cn/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof.md) | 数组、二分查找 | 简单 | | [LCR 129. 字母迷宫](https://leetcode.cn/problems/ju-zhen-zhong-de-lu-jing-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ju-zhen-zhong-de-lu-jing-lcof.md) | 数组、字符串、回溯、矩阵 | 中等 | | [LCR 130. 衣橱整理](https://leetcode.cn/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ji-qi-ren-de-yun-dong-fan-wei-lcof.md) | 深度优先搜索、广度优先搜索、动态规划 | 中等 | | [LCR 131. 砍竹子 I](https://leetcode.cn/problems/jian-sheng-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/jian-sheng-zi-lcof.md) | 数学、动态规划 | 中等 | | [LCR 133. 位 1 的个数](https://leetcode.cn/problems/er-jin-zhi-zhong-1de-ge-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-jin-zhi-zhong-1de-ge-shu-lcof.md) | 位运算 | 简单 | | [LCR 134. Pow(x, n)](https://leetcode.cn/problems/shu-zhi-de-zheng-shu-ci-fang-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zhi-de-zheng-shu-ci-fang-lcof.md) | 递归、数学 | 中等 | | [LCR 135. 报数](https://leetcode.cn/problems/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof.md) | 数组、数学 | 简单 | | [LCR 136. 删除链表的节点](https://leetcode.cn/problems/shan-chu-lian-biao-de-jie-dian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shan-chu-lian-biao-de-jie-dian-lcof.md) | 链表 | 简单 | | [LCR 139. 训练计划 I](https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof.md) | 数组、双指针、排序 | 简单 | | [LCR 140. 训练计划 II](https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof.md) | 链表、双指针 | 简单 | | [LCR 141. 训练计划 III](https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/fan-zhuan-lian-biao-lcof.md) | 递归、链表 | 简单 | | [LCR 142. 训练计划 IV](https://leetcode.cn/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/he-bing-liang-ge-pai-xu-de-lian-biao-lcof.md) | 递归、链表 | 简单 | | [LCR 143. 子结构判断](https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-de-zi-jie-gou-lcof.md) | 树、深度优先搜索、二叉树 | 中等 | | [LCR 144. 翻转二叉树](https://leetcode.cn/problems/er-cha-shu-de-jing-xiang-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-shu-de-jing-xiang-lcof.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [LCR 145. 判断对称二叉树](https://leetcode.cn/problems/dui-cheng-de-er-cha-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/dui-cheng-de-er-cha-shu-lcof.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [LCR 146. 螺旋遍历二维数组](https://leetcode.cn/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shun-shi-zhen-da-yin-ju-zhen-lcof.md) | 数组、矩阵、模拟 | 简单 | | [LCR 147. 最小栈](https://leetcode.cn/problems/bao-han-minhan-shu-de-zhan-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/bao-han-minhan-shu-de-zhan-lcof.md) | 栈、设计 | 简单 | | [LCR 148. 验证图书取出顺序](https://leetcode.cn/problems/zhan-de-ya-ru-dan-chu-xu-lie-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zhan-de-ya-ru-dan-chu-xu-lie-lcof.md) | 栈、数组、模拟 | 中等 | | [LCR 149. 彩灯装饰记录 I](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/cong-shang-dao-xia-da-yin-er-cha-shu-lcof.md) | 树、广度优先搜索、二叉树 | 中等 | | [LCR 150. 彩灯装饰记录 II](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof.md) | 树、广度优先搜索、二叉树 | 简单 | | [LCR 151. 彩灯装饰记录 III](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof.md) | 树、广度优先搜索、二叉树 | 中等 | | [LCR 152. 验证二叉搜索树的后序遍历序列](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof.md) | 栈、树、二叉搜索树、递归、数组、二叉树、单调栈 | 中等 | | [LCR 153. 二叉树中和为目标值的路径](https://leetcode.cn/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof.md) | 树、深度优先搜索、回溯、二叉树 | 中等 | | [LCR 154. 复杂链表的复制](https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/fu-za-lian-biao-de-fu-zhi-lcof.md) | 哈希表、链表 | 中等 | | [LCR 155. 将二叉搜索树转化为排序的双向链表](https://leetcode.cn/problems/er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof.md) | 栈、树、深度优先搜索、二叉搜索树、链表、二叉树、双向链表 | 中等 | | [LCR 156. 序列化与反序列化二叉树](https://leetcode.cn/problems/xu-lie-hua-er-cha-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/xu-lie-hua-er-cha-shu-lcof.md) | 树、深度优先搜索、广度优先搜索、设计、字符串、二叉树 | 困难 | | [LCR 157. 套餐内商品的排列顺序](https://leetcode.cn/problems/zi-fu-chuan-de-pai-lie-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zi-fu-chuan-de-pai-lie-lcof.md) | 字符串、回溯 | 中等 | | [LCR 158. 库存管理 II](https://leetcode.cn/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof.md) | 数组、哈希表、分治、计数、排序 | 简单 | | [LCR 159. 库存管理 III](https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zui-xiao-de-kge-shu-lcof.md) | 数组、分治、快速选择、排序、堆(优先队列) | 简单 | | [LCR 160. 数据流中的中位数](https://leetcode.cn/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-ju-liu-zhong-de-zhong-wei-shu-lcof.md) | 设计、双指针、数据流、排序、堆(优先队列) | 困难 | | [LCR 161. 连续天数的最高销售额](https://leetcode.cn/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/lian-xu-zi-shu-zu-de-zui-da-he-lcof.md) | 数组、分治、动态规划 | 简单 | | [LCR 163. 找到第 k 位数字](https://leetcode.cn/problems/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof.md) | 数学、二分查找 | 中等 | | [LCR 164. 破解闯关密码](https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof.md) | 贪心、字符串、排序 | 中等 | | [LCR 165. 解密数字](https://leetcode.cn/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof.md) | 字符串、动态规划 | 中等 | | [LCR 166. 珠宝的最高价值](https://leetcode.cn/problems/li-wu-de-zui-da-jie-zhi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/li-wu-de-zui-da-jie-zhi-lcof.md) | 数组、动态规划、矩阵 | 中等 | | [LCR 167. 招式拆解 I](https://leetcode.cn/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof.md) | 哈希表、字符串、滑动窗口 | 中等 | | [LCR 168. 丑数](https://leetcode.cn/problems/chou-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/chou-shu-lcof.md) | 哈希表、数学、动态规划、堆(优先队列) | 中等 | | [LCR 169. 招式拆解 II](https://leetcode.cn/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof.md) | 队列、哈希表、字符串、计数 | 简单 | | [LCR 170. 交易逆序对的总数](https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-de-ni-xu-dui-lcof.md) | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 | | [LCR 171. 训练计划 V](https://leetcode.cn/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof.md) | 哈希表、链表、双指针 | 简单 | | [LCR 172. 统计目标成绩的出现次数](https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof.md) | 数组、二分查找 | 简单 | | [LCR 173. 点名](https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/que-shi-de-shu-zi-lcof.md) | 位运算、数组、哈希表、数学、二分查找 | 简单 | | [LCR 174. 寻找二叉搜索树中的目标节点](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 简单 | | [LCR 175. 计算二叉树的深度](https://leetcode.cn/problems/er-cha-shu-de-shen-du-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-shu-de-shen-du-lcof.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [LCR 176. 判断是否为平衡二叉树](https://leetcode.cn/problems/ping-heng-er-cha-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ping-heng-er-cha-shu-lcof.md) | 树、深度优先搜索、二叉树 | 简单 | | [LCR 177. 撞色搭配](https://leetcode.cn/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof.md) | 位运算、数组 | 中等 | | [LCR 179. 查找总价格为目标值的两个商品](https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/he-wei-sde-liang-ge-shu-zi-lcof.md) | 数组、双指针、二分查找 | 简单 | | [LCR 180. 文件组合](https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof.md) | 数学、双指针、枚举 | 简单 | | [LCR 181. 字符串中的单词反转](https://leetcode.cn/problems/fan-zhuan-dan-ci-shun-xu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/fan-zhuan-dan-ci-shun-xu-lcof.md) | 双指针、字符串 | 简单 | | [LCR 182. 动态口令](https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zuo-xuan-zhuan-zi-fu-chuan-lcof.md) | 数学、双指针、字符串 | 简单 | | [LCR 183. 望远镜中最高的海拔](https://leetcode.cn/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/hua-dong-chuang-kou-de-zui-da-zhi-lcof.md) | 队列、数组、滑动窗口、单调队列、堆(优先队列) | 困难 | | [LCR 184. 设计自助结算系统](https://leetcode.cn/problems/dui-lie-de-zui-da-zhi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/dui-lie-de-zui-da-zhi-lcof.md) | 设计、队列、单调队列 | 中等 | | [LCR 186. 文物朝代判断](https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/bu-ke-pai-zhong-de-shun-zi-lcof.md) | 数组、排序 | 简单 | | [LCR 187. 破冰游戏](https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof.md) | 递归、数学 | 简单 | | [LCR 188. 买卖芯片的最佳时机](https://leetcode.cn/problems/gu-piao-de-zui-da-li-run-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/gu-piao-de-zui-da-li-run-lcof.md) | 数组、动态规划 | 中等 | | [LCR 189. 设计机械累加器](https://leetcode.cn/problems/qiu-12n-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/qiu-12n-lcof.md) | 位运算、递归、脑筋急转弯 | 中等 | | [LCR 190. 加密运算](https://leetcode.cn/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof.md) | 位运算、数学 | 简单 | | [LCR 191. 按规则计算统计结果](https://leetcode.cn/problems/gou-jian-cheng-ji-shu-zu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/gou-jian-cheng-ji-shu-zu-lcof.md) | 数组、前缀和 | 中等 | | [LCR 192. 把字符串转换成整数 (atoi)](https://leetcode.cn/problems/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof.md) | 字符串 | 中等 | | [LCR 193. 二叉搜索树的最近公共祖先](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 简单 | | [LCR 194. 二叉树的最近公共祖先](https://leetcode.cn/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof.md) | 树、深度优先搜索、二叉树 | 简单 | ### 面试题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [面试题 01.07. 旋转矩阵](https://leetcode.cn/problems/rotate-matrix-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/rotate-matrix-lcci.md) | 数组、数学、矩阵 | 中等 | | [面试题 01.08. 零矩阵](https://leetcode.cn/problems/zero-matrix-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/zero-matrix-lcci.md) | 数组、哈希表、矩阵 | 中等 | | [面试题 02.02. 返回倒数第 k 个节点](https://leetcode.cn/problems/kth-node-from-end-of-list-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/kth-node-from-end-of-list-lcci.md) | 链表、双指针 | 简单 | | [面试题 02.05. 链表求和](https://leetcode.cn/problems/sum-lists-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/sum-lists-lcci.md) | 递归、链表、数学 | 中等 | | [面试题 02.06. 回文链表](https://leetcode.cn/problems/palindrome-linked-list-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/palindrome-linked-list-lcci.md) | 栈、递归、链表、双指针 | 简单 | | [面试题 02.07. 链表相交](https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/intersection-of-two-linked-lists-lcci.md) | 哈希表、链表、双指针 | 简单 | | [面试题 02.08. 环路检测](https://leetcode.cn/problems/linked-list-cycle-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/linked-list-cycle-lcci.md) | 哈希表、链表、双指针 | 中等 | | [面试题 03.02. 栈的最小值](https://leetcode.cn/problems/min-stack-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/min-stack-lcci.md) | 栈、设计 | 简单 | | [面试题 03.04. 化栈为队](https://leetcode.cn/problems/implement-queue-using-stacks-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/implement-queue-using-stacks-lcci.md) | 栈、设计、队列 | 简单 | | [面试题 04.02. 最小高度树](https://leetcode.cn/problems/minimum-height-tree-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/minimum-height-tree-lcci.md) | 树、二叉搜索树、数组、分治、二叉树 | 简单 | | [面试题 04.05. 合法二叉搜索树](https://leetcode.cn/problems/legal-binary-search-tree-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/legal-binary-search-tree-lcci.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [面试题 04.06. 后继者](https://leetcode.cn/problems/successor-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/successor-lcci.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [面试题 04.08. 首个共同祖先](https://leetcode.cn/problems/first-common-ancestor-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/first-common-ancestor-lcci.md) | 树、深度优先搜索、二叉树 | 中等 | | [面试题 04.12. 求和路径](https://leetcode.cn/problems/paths-with-sum-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/paths-with-sum-lcci.md) | 树、深度优先搜索、二叉树 | 中等 | | [面试题 08.04. 幂集](https://leetcode.cn/problems/power-set-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/power-set-lcci.md) | 位运算、数组、回溯 | 中等 | | [面试题 08.07. 无重复字符串的排列组合](https://leetcode.cn/problems/permutation-i-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/permutation-i-lcci.md) | 字符串、回溯 | 中等 | | [面试题 08.08. 有重复字符串的排列组合](https://leetcode.cn/problems/permutation-ii-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/permutation-ii-lcci.md) | 字符串、回溯 | 中等 | | [面试题 08.09. 括号](https://leetcode.cn/problems/bracket-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/bracket-lcci.md) | 字符串、动态规划、回溯 | 中等 | | [面试题 08.10. 颜色填充](https://leetcode.cn/problems/color-fill-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/color-fill-lcci.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 简单 | | [面试题 08.12. 八皇后](https://leetcode.cn/problems/eight-queens-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/eight-queens-lcci.md) | 数组、回溯 | 困难 | | [面试题 10.01. 合并排序的数组](https://leetcode.cn/problems/sorted-merge-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/sorted-merge-lcci.md) | 数组、双指针、排序 | 简单 | | [面试题 10.02. 变位词组](https://leetcode.cn/problems/group-anagrams-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/group-anagrams-lcci.md) | 数组、哈希表、字符串、排序 | 中等 | | [面试题 10.09. 排序矩阵查找](https://leetcode.cn/problems/sorted-matrix-search-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/sorted-matrix-search-lcci.md) | 数组、二分查找、分治、矩阵 | 中等 | | [面试题 16.02. 单词频率](https://leetcode.cn/problems/words-frequency-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/words-frequency-lcci.md) | 设计、字典树、数组、哈希表、字符串 | 中等 | | [面试题 16.05. 阶乘尾数](https://leetcode.cn/problems/factorial-zeros-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/factorial-zeros-lcci.md) | 数学 | 简单 | | [面试题 16.26. 计算器](https://leetcode.cn/problems/calculator-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/calculator-lcci.md) | 栈、数学、字符串 | 中等 | | [面试题 17.06. 2出现的次数](https://leetcode.cn/problems/number-of-2s-in-range-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/number-of-2s-in-range-lcci.md) | 递归、数学、动态规划 | 困难 | | [面试题 17.14. 最小K个数](https://leetcode.cn/problems/smallest-k-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/smallest-k-lcci.md) | 数组、分治、快速选择、排序、堆(优先队列) | 中等 | | [面试题 17.15. 最长单词](https://leetcode.cn/problems/longest-word-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/longest-word-lcci.md) | 字典树、数组、哈希表、字符串 | 中等 | | [面试题 17.17. 多次搜索](https://leetcode.cn/problems/multi-search-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/multi-search-lcci.md) | 字典树、数组、哈希表、字符串、字符串匹配、滑动窗口 | 中等 | ================================================ FILE: docs/00_preface/00_06_categories_list.md ================================================ # LeetCode 题解(按分类排序,推荐刷题列表 ★★★) ## 第 1 章 数组 ### 数组基础题目 #### 数组操作题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0189. 轮转数组](https://leetcode.cn/problems/rotate-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/rotate-array.md) | 数组、数学、双指针 | 中等 | | [0066. 加一](https://leetcode.cn/problems/plus-one/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/plus-one.md) | 数组、数学 | 简单 | | [0724. 寻找数组的中心下标](https://leetcode.cn/problems/find-pivot-index/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/find-pivot-index.md) | 数组、前缀和 | 简单 | | [0485. 最大连续 1 的个数](https://leetcode.cn/problems/max-consecutive-ones/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/max-consecutive-ones.md) | 数组 | 简单 | | [0238. 除自身以外数组的乘积](https://leetcode.cn/problems/product-of-array-except-self/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/product-of-array-except-self.md) | 数组、前缀和 | 中等 | #### 二维数组题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0498. 对角线遍历](https://leetcode.cn/problems/diagonal-traverse/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/diagonal-traverse.md) | 数组、矩阵、模拟 | 中等 | | [0048. 旋转图像](https://leetcode.cn/problems/rotate-image/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/rotate-image.md) | 数组、数学、矩阵 | 中等 | | [0073. 矩阵置零](https://leetcode.cn/problems/set-matrix-zeroes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/set-matrix-zeroes.md) | 数组、哈希表、矩阵 | 中等 | | [0054. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/spiral-matrix.md) | 数组、矩阵、模拟 | 中等 | | [0059. 螺旋矩阵 II](https://leetcode.cn/problems/spiral-matrix-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/spiral-matrix-ii.md) | 数组、矩阵、模拟 | 中等 | | [0289. 生命游戏](https://leetcode.cn/problems/game-of-life/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/game-of-life.md) | 数组、矩阵、模拟 | 中等 | ### 数组排序算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0912. 排序数组](https://leetcode.cn/problems/sort-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) | 数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序 | 中等 | | [LCR 164. 破解闯关密码](https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof.md) | 贪心、字符串、排序 | 中等 | | [0283. 移动零](https://leetcode.cn/problems/move-zeroes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/move-zeroes.md) | 数组、双指针 | 简单 | | [0215. 数组中的第K个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/kth-largest-element-in-an-array.md) | 数组、分治、快速选择、排序、堆(优先队列) | 中等 | | [0075. 颜色分类](https://leetcode.cn/problems/sort-colors/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sort-colors.md) | 数组、双指针、排序 | 中等 | | [0506. 相对名次](https://leetcode.cn/problems/relative-ranks/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/relative-ranks.md) | 数组、排序、堆(优先队列) | 简单 | | [0088. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-sorted-array.md) | 数组、双指针、排序 | 简单 | | [LCR 170. 交易逆序对的总数](https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-de-ni-xu-dui-lcof.md) | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 | | [0315. 计算右侧小于当前元素的个数](https://leetcode.cn/problems/count-of-smaller-numbers-after-self/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-of-smaller-numbers-after-self.md) | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 | | [0169. 多数元素](https://leetcode.cn/problems/majority-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/majority-element.md) | 数组、哈希表、分治、计数、排序 | 简单 | | [LCR 159. 库存管理 III](https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zui-xiao-de-kge-shu-lcof.md) | 数组、分治、快速选择、排序、堆(优先队列) | 简单 | | [1122. 数组的相对排序](https://leetcode.cn/problems/relative-sort-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/relative-sort-array.md) | 数组、哈希表、计数排序、排序 | 简单 | | [0220. 存在重复元素 III](https://leetcode.cn/problems/contains-duplicate-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate-iii.md) | 数组、桶排序、有序集合、排序、滑动窗口 | 困难 | | [0164. 最大间距](https://leetcode.cn/problems/maximum-gap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-gap.md) | 数组、桶排序、基数排序、排序 | 中等 | | [0561. 数组拆分](https://leetcode.cn/problems/array-partition/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/array-partition.md) | 贪心、数组、计数排序、排序 | 简单 | | [0217. 存在重复元素](https://leetcode.cn/problems/contains-duplicate/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate.md) | 数组、哈希表、排序 | 简单 | | [0136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) | 位运算、数组 | 简单 | | [0056. 合并区间](https://leetcode.cn/problems/merge-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-intervals.md) | 数组、排序 | 中等 | | [0179. 最大数](https://leetcode.cn/problems/largest-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/largest-number.md) | 贪心、数组、字符串、排序 | 中等 | | [0384. 打乱数组](https://leetcode.cn/problems/shuffle-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/shuffle-an-array.md) | 设计、数组、数学、随机化 | 中等 | ### 二分查找题目 #### 二分下标题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0704. 二分查找](https://leetcode.cn/problems/binary-search/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/binary-search.md) | 数组、二分查找 | 简单 | | [0374. 猜数字大小](https://leetcode.cn/problems/guess-number-higher-or-lower/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/guess-number-higher-or-lower.md) | 二分查找、交互 | 简单 | | [0035. 搜索插入位置](https://leetcode.cn/problems/search-insert-position/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-insert-position.md) | 数组、二分查找 | 简单 | | [0034. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-first-and-last-position-of-element-in-sorted-array.md) | 数组、二分查找 | 中等 | | [0167. 两数之和 II - 输入有序数组](https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/two-sum-ii-input-array-is-sorted.md) | 数组、双指针、二分查找 | 中等 | | [0153. 寻找旋转排序数组中的最小值](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array.md) | 数组、二分查找 | 中等 | | [0154. 寻找旋转排序数组中的最小值 II](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array-ii.md) | 数组、二分查找 | 困难 | | [0033. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-in-rotated-sorted-array.md) | 数组、二分查找 | 中等 | | [0081. 搜索旋转排序数组 II](https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-in-rotated-sorted-array-ii.md) | 数组、二分查找 | 中等 | | [0278. 第一个错误的版本](https://leetcode.cn/problems/first-bad-version/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/first-bad-version.md) | 二分查找、交互 | 简单 | | [0162. 寻找峰值](https://leetcode.cn/problems/find-peak-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-peak-element.md) | 数组、二分查找 | 中等 | | [0852. 山脉数组的峰顶索引](https://leetcode.cn/problems/peak-index-in-a-mountain-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/peak-index-in-a-mountain-array.md) | 数组、二分查找 | 中等 | | [1095. 山脉数组中查找目标值](https://leetcode.cn/problems/find-in-mountain-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/find-in-mountain-array.md) | 数组、二分查找、交互 | 困难 | | [0744. 寻找比目标字母大的最小字母](https://leetcode.cn/problems/find-smallest-letter-greater-than-target/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/find-smallest-letter-greater-than-target.md) | 数组、二分查找 | 简单 | | [0004. 寻找两个正序数组的中位数](https://leetcode.cn/problems/median-of-two-sorted-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/median-of-two-sorted-arrays.md) | 数组、二分查找、分治 | 困难 | | [0074. 搜索二维矩阵](https://leetcode.cn/problems/search-a-2d-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-a-2d-matrix.md) | 数组、二分查找、矩阵 | 中等 | | [0240. 搜索二维矩阵 II](https://leetcode.cn/problems/search-a-2d-matrix-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/search-a-2d-matrix-ii.md) | 数组、二分查找、分治、矩阵 | 中等 | #### 二分答案题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0069. x 的平方根](https://leetcode.cn/problems/sqrtx/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sqrtx.md) | 数学、二分查找 | 简单 | | [0287. 寻找重复数](https://leetcode.cn/problems/find-the-duplicate-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/find-the-duplicate-number.md) | 位运算、数组、双指针、二分查找 | 中等 | | [0050. Pow(x, n)](https://leetcode.cn/problems/powx-n/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/powx-n.md) | 递归、数学 | 中等 | | [0367. 有效的完全平方数](https://leetcode.cn/problems/valid-perfect-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/valid-perfect-square.md) | 数学、二分查找 | 简单 | | [1300. 转变数组后最接近目标值的数组和](https://leetcode.cn/problems/sum-of-mutated-array-closest-to-target/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/sum-of-mutated-array-closest-to-target.md) | 数组、二分查找、排序 | 中等 | | [0400. 第 N 位数字](https://leetcode.cn/problems/nth-digit/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/nth-digit.md) | 数学、二分查找 | 中等 | #### 复杂的二分查找问题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0875. 爱吃香蕉的珂珂](https://leetcode.cn/problems/koko-eating-bananas/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/koko-eating-bananas.md) | 数组、二分查找 | 中等 | | [0410. 分割数组的最大值](https://leetcode.cn/problems/split-array-largest-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/split-array-largest-sum.md) | 贪心、数组、二分查找、动态规划、前缀和 | 困难 | | [0209. 长度最小的子数组](https://leetcode.cn/problems/minimum-size-subarray-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/minimum-size-subarray-sum.md) | 数组、二分查找、前缀和、滑动窗口 | 中等 | | [0658. 找到 K 个最接近的元素](https://leetcode.cn/problems/find-k-closest-elements/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/find-k-closest-elements.md) | 数组、双指针、二分查找、排序、滑动窗口、堆(优先队列) | 中等 | | [0270. 最接近的二叉搜索树值](https://leetcode.cn/problems/closest-binary-search-tree-value/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/closest-binary-search-tree-value.md) | 树、深度优先搜索、二叉搜索树、二分查找、二叉树 | 简单 | | [0702. 搜索长度未知的有序数组](https://leetcode.cn/problems/search-in-a-sorted-array-of-unknown-size/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/search-in-a-sorted-array-of-unknown-size.md) | 数组、二分查找、交互 | 中等 | | [0349. 两个数组的交集](https://leetcode.cn/problems/intersection-of-two-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays.md) | 数组、哈希表、双指针、二分查找、排序 | 简单 | | [0350. 两个数组的交集 II](https://leetcode.cn/problems/intersection-of-two-arrays-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays-ii.md) | 数组、哈希表、双指针、二分查找、排序 | 简单 | | [0287. 寻找重复数](https://leetcode.cn/problems/find-the-duplicate-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/find-the-duplicate-number.md) | 位运算、数组、双指针、二分查找 | 中等 | | [0719. 找出第 K 小的数对距离](https://leetcode.cn/problems/find-k-th-smallest-pair-distance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/find-k-th-smallest-pair-distance.md) | 数组、双指针、二分查找、排序 | 困难 | | [0259. 较小的三数之和](https://leetcode.cn/problems/3sum-smaller/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/3sum-smaller.md) | 数组、双指针、二分查找、排序 | 中等 | | [1011. 在 D 天内送达包裹的能力](https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/capacity-to-ship-packages-within-d-days.md) | 数组、二分查找 | 中等 | | [1482. 制作 m 束花所需的最少天数](https://leetcode.cn/problems/minimum-number-of-days-to-make-m-bouquets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/minimum-number-of-days-to-make-m-bouquets.md) | 数组、二分查找 | 中等 | ### 双指针题目 #### 对撞指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0167. 两数之和 II - 输入有序数组](https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/two-sum-ii-input-array-is-sorted.md) | 数组、双指针、二分查找 | 中等 | | [0344. 反转字符串](https://leetcode.cn/problems/reverse-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-string.md) | 双指针、字符串 | 简单 | | [0345. 反转字符串中的元音字母](https://leetcode.cn/problems/reverse-vowels-of-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-vowels-of-a-string.md) | 双指针、字符串 | 简单 | | [0125. 验证回文串](https://leetcode.cn/problems/valid-palindrome/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/valid-palindrome.md) | 双指针、字符串 | 简单 | | [0011. 盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/container-with-most-water.md) | 贪心、数组、双指针 | 中等 | | [0611. 有效三角形的个数](https://leetcode.cn/problems/valid-triangle-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/valid-triangle-number.md) | 贪心、数组、双指针、二分查找、排序 | 中等 | | [0015. 三数之和](https://leetcode.cn/problems/3sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum.md) | 数组、双指针、排序 | 中等 | | [0016. 最接近的三数之和](https://leetcode.cn/problems/3sum-closest/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum-closest.md) | 数组、双指针、排序 | 中等 | | [0018. 四数之和](https://leetcode.cn/problems/4sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/4sum.md) | 数组、双指针、排序 | 中等 | | [0259. 较小的三数之和](https://leetcode.cn/problems/3sum-smaller/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/3sum-smaller.md) | 数组、双指针、二分查找、排序 | 中等 | | [0658. 找到 K 个最接近的元素](https://leetcode.cn/problems/find-k-closest-elements/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/find-k-closest-elements.md) | 数组、双指针、二分查找、排序、滑动窗口、堆(优先队列) | 中等 | | [1099. 小于 K 的两数之和](https://leetcode.cn/problems/two-sum-less-than-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/two-sum-less-than-k.md) | 数组、双指针、二分查找、排序 | 简单 | | [0075. 颜色分类](https://leetcode.cn/problems/sort-colors/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sort-colors.md) | 数组、双指针、排序 | 中等 | | [0360. 有序转化数组](https://leetcode.cn/problems/sort-transformed-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sort-transformed-array.md) | 数组、数学、双指针、排序 | 中等 | | [0977. 有序数组的平方](https://leetcode.cn/problems/squares-of-a-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/squares-of-a-sorted-array.md) | 数组、双指针、排序 | 简单 | | [0881. 救生艇](https://leetcode.cn/problems/boats-to-save-people/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/boats-to-save-people.md) | 贪心、数组、双指针、排序 | 中等 | | [0042. 接雨水](https://leetcode.cn/problems/trapping-rain-water/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/trapping-rain-water.md) | 栈、数组、双指针、动态规划、单调栈 | 困难 | | [0443. 压缩字符串](https://leetcode.cn/problems/string-compression/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/string-compression.md) | 双指针、字符串 | 中等 | #### 快慢指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0026. 删除有序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-array.md) | 数组、双指针 | 简单 | | [0080. 删除有序数组中的重复项 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-array-ii.md) | 数组、双指针 | 中等 | | [0027. 移除元素](https://leetcode.cn/problems/remove-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-element.md) | 数组、双指针 | 简单 | | [0283. 移动零](https://leetcode.cn/problems/move-zeroes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/move-zeroes.md) | 数组、双指针 | 简单 | | [0845. 数组中的最长山脉](https://leetcode.cn/problems/longest-mountain-in-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/longest-mountain-in-array.md) | 数组、双指针、动态规划、枚举 | 中等 | | [0088. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-sorted-array.md) | 数组、双指针、排序 | 简单 | | [0719. 找出第 K 小的数对距离](https://leetcode.cn/problems/find-k-th-smallest-pair-distance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/find-k-th-smallest-pair-distance.md) | 数组、双指针、二分查找、排序 | 困难 | | [0334. 递增的三元子序列](https://leetcode.cn/problems/increasing-triplet-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/increasing-triplet-subsequence.md) | 贪心、数组 | 中等 | | [0978. 最长湍流子数组](https://leetcode.cn/problems/longest-turbulent-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/longest-turbulent-subarray.md) | 数组、动态规划、滑动窗口 | 中等 | | [LCR 139. 训练计划 I](https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof.md) | 数组、双指针、排序 | 简单 | #### 分离双指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0350. 两个数组的交集 II](https://leetcode.cn/problems/intersection-of-two-arrays-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays-ii.md) | 数组、哈希表、双指针、二分查找、排序 | 简单 | | [0925. 长按键入](https://leetcode.cn/problems/long-pressed-name/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/long-pressed-name.md) | 双指针、字符串 | 简单 | | [0844. 比较含退格的字符串](https://leetcode.cn/problems/backspace-string-compare/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/backspace-string-compare.md) | 栈、双指针、字符串、模拟 | 简单 | | [1229. 安排会议日程](https://leetcode.cn/problems/meeting-scheduler/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/meeting-scheduler.md) | 数组、双指针、排序 | 中等 | | [0415. 字符串相加](https://leetcode.cn/problems/add-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-strings.md) | 数学、字符串、模拟 | 简单 | | [0392. 判断子序列](https://leetcode.cn/problems/is-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/is-subsequence.md) | 双指针、字符串、动态规划 | 简单 | ### 滑动窗口题目 #### 固定长度窗口题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1343. 大小为 K 且平均值大于等于阈值的子数组数目](https://leetcode.cn/problems/number-of-sub-arrays-of-size-k-and-average-greater-than-or-equal-to-threshold/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/number-of-sub-arrays-of-size-k-and-average-greater-than-or-equal-to-threshold.md) | 数组、滑动窗口 | 中等 | | [0643. 子数组最大平均数 I](https://leetcode.cn/problems/maximum-average-subarray-i/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-average-subarray-i.md) | 数组、滑动窗口 | 简单 | | [1052. 爱生气的书店老板](https://leetcode.cn/problems/grumpy-bookstore-owner/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/grumpy-bookstore-owner.md) | 数组、滑动窗口 | 中等 | | [1423. 可获得的最大点数](https://leetcode.cn/problems/maximum-points-you-can-obtain-from-cards/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/maximum-points-you-can-obtain-from-cards.md) | 数组、前缀和、滑动窗口 | 中等 | | [1456. 定长子串中元音的最大数目](https://leetcode.cn/problems/maximum-number-of-vowels-in-a-substring-of-given-length/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/maximum-number-of-vowels-in-a-substring-of-given-length.md) | 字符串、滑动窗口 | 中等 | | [0567. 字符串的排列](https://leetcode.cn/problems/permutation-in-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/permutation-in-string.md) | 哈希表、双指针、字符串、滑动窗口 | 中等 | | [1100. 长度为 K 的无重复字符子串](https://leetcode.cn/problems/find-k-length-substrings-with-no-repeated-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/find-k-length-substrings-with-no-repeated-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [1151. 最少交换次数来组合所有的 1](https://leetcode.cn/problems/minimum-swaps-to-group-all-1s-together/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/minimum-swaps-to-group-all-1s-together.md) | 数组、滑动窗口 | 中等 | | [1176. 健身计划评估](https://leetcode.cn/problems/diet-plan-performance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/diet-plan-performance.md) | 数组、滑动窗口 | 简单 | | [0438. 找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-all-anagrams-in-a-string.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0995. K 连续位的最小翻转次数](https://leetcode.cn/problems/minimum-number-of-k-consecutive-bit-flips/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-number-of-k-consecutive-bit-flips.md) | 位运算、队列、数组、前缀和、滑动窗口 | 困难 | | [0683. K 个关闭的灯泡](https://leetcode.cn/problems/k-empty-slots/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/k-empty-slots.md) | 树状数组、线段树、队列、数组、有序集合、滑动窗口、单调队列、堆(优先队列) | 困难 | | [0220. 存在重复元素 III](https://leetcode.cn/problems/contains-duplicate-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate-iii.md) | 数组、桶排序、有序集合、排序、滑动窗口 | 困难 | | [0239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/sliding-window-maximum.md) | 队列、数组、滑动窗口、单调队列、堆(优先队列) | 困难 | | [0480. 滑动窗口中位数](https://leetcode.cn/problems/sliding-window-median/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sliding-window-median.md) | 数组、哈希表、滑动窗口、堆(优先队列) | 困难 | #### 不定长度窗口题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0674. 最长连续递增序列](https://leetcode.cn/problems/longest-continuous-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/longest-continuous-increasing-subsequence.md) | 数组 | 简单 | | [0485. 最大连续 1 的个数](https://leetcode.cn/problems/max-consecutive-ones/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/max-consecutive-ones.md) | 数组 | 简单 | | [0487. 最大连续1的个数 II](https://leetcode.cn/problems/max-consecutive-ones-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/max-consecutive-ones-ii.md) | 数组、动态规划、滑动窗口 | 中等 | | [0076. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/minimum-window-substring.md) | 哈希表、字符串、滑动窗口 | 困难 | | [0718. 最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/maximum-length-of-repeated-subarray.md) | 数组、二分查找、动态规划、滑动窗口、哈希函数、滚动哈希 | 中等 | | [0209. 长度最小的子数组](https://leetcode.cn/problems/minimum-size-subarray-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/minimum-size-subarray-sum.md) | 数组、二分查找、前缀和、滑动窗口 | 中等 | | [1004. 最大连续1的个数 III](https://leetcode.cn/problems/max-consecutive-ones-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/max-consecutive-ones-iii.md) | 数组、二分查找、前缀和、滑动窗口 | 中等 | | [1658. 将 x 减到 0 的最小操作数](https://leetcode.cn/problems/minimum-operations-to-reduce-x-to-zero/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/minimum-operations-to-reduce-x-to-zero.md) | 数组、哈希表、二分查找、前缀和、滑动窗口 | 中等 | | [0424. 替换后的最长重复字符](https://leetcode.cn/problems/longest-repeating-character-replacement/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/longest-repeating-character-replacement.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0003. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-substring-without-repeating-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [1695. 删除子数组的最大得分](https://leetcode.cn/problems/maximum-erasure-value/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/maximum-erasure-value.md) | 数组、哈希表、滑动窗口 | 中等 | | [1208. 尽可能使字符串相等](https://leetcode.cn/problems/get-equal-substrings-within-budget/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/get-equal-substrings-within-budget.md) | 字符串、二分查找、前缀和、滑动窗口 | 中等 | | [1493. 删掉一个元素以后全为 1 的最长子数组](https://leetcode.cn/problems/longest-subarray-of-1s-after-deleting-one-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/longest-subarray-of-1s-after-deleting-one-element.md) | 数组、动态规划、滑动窗口 | 中等 | | [0727. 最小窗口子序列](https://leetcode.cn/problems/minimum-window-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/minimum-window-subsequence.md) | 字符串、动态规划、滑动窗口 | 困难 | | [0159. 至多包含两个不同字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-most-two-distinct-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/longest-substring-with-at-most-two-distinct-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0340. 至多包含 K 个不同字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-most-k-distinct-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-substring-with-at-most-k-distinct-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0795. 区间子数组个数](https://leetcode.cn/problems/number-of-subarrays-with-bounded-maximum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/number-of-subarrays-with-bounded-maximum.md) | 数组、双指针 | 中等 | | [0992. K 个不同整数的子数组](https://leetcode.cn/problems/subarrays-with-k-different-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/subarrays-with-k-different-integers.md) | 数组、哈希表、计数、滑动窗口 | 困难 | | [0713. 乘积小于 K 的子数组](https://leetcode.cn/problems/subarray-product-less-than-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/subarray-product-less-than-k.md) | 数组、二分查找、前缀和、滑动窗口 | 中等 | | [0904. 水果成篮](https://leetcode.cn/problems/fruit-into-baskets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/fruit-into-baskets.md) | 数组、哈希表、滑动窗口 | 中等 | | [1358. 包含所有三种字符的子字符串数目](https://leetcode.cn/problems/number-of-substrings-containing-all-three-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/number-of-substrings-containing-all-three-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0467. 环绕字符串中唯一的子字符串](https://leetcode.cn/problems/unique-substrings-in-wraparound-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/unique-substrings-in-wraparound-string.md) | 字符串、动态规划 | 中等 | | [1438. 绝对差不超过限制的最长连续子数组](https://leetcode.cn/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit.md) | 队列、数组、有序集合、滑动窗口、单调队列、堆(优先队列) | 中等 | ## 第 2 章 链表 ### 链表基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0707. 设计链表](https://leetcode.cn/problems/design-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/design-linked-list.md) | 设计、链表 | 中等 | | [0083. 删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-list.md) | 链表 | 简单 | | [0082. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-list-ii.md) | 链表、双指针 | 中等 | | [0206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/reverse-linked-list.md) | 递归、链表 | 简单 | | [0092. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-linked-list-ii.md) | 链表 | 中等 | | [0025. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-nodes-in-k-group.md) | 递归、链表 | 困难 | | [0203. 移除链表元素](https://leetcode.cn/problems/remove-linked-list-elements/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/remove-linked-list-elements.md) | 递归、链表 | 简单 | | [0328. 奇偶链表](https://leetcode.cn/problems/odd-even-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/odd-even-linked-list.md) | 链表 | 中等 | | [0234. 回文链表](https://leetcode.cn/problems/palindrome-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/palindrome-linked-list.md) | 栈、递归、链表、双指针 | 简单 | | [0430. 扁平化多级双向链表](https://leetcode.cn/problems/flatten-a-multilevel-doubly-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/flatten-a-multilevel-doubly-linked-list.md) | 深度优先搜索、链表、双向链表 | 中等 | | [0138. 随机链表的复制](https://leetcode.cn/problems/copy-list-with-random-pointer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/copy-list-with-random-pointer.md) | 哈希表、链表 | 中等 | | [0061. 旋转链表](https://leetcode.cn/problems/rotate-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/rotate-list.md) | 链表、双指针 | 中等 | ### 链表排序题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0148. 排序链表](https://leetcode.cn/problems/sort-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md) | 链表、双指针、分治、排序、归并排序 | 中等 | | [0021. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-two-sorted-lists.md) | 递归、链表 | 简单 | | [0023. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) | 链表、分治、堆(优先队列)、归并排序 | 困难 | | [0147. 对链表进行插入排序](https://leetcode.cn/problems/insertion-sort-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/insertion-sort-list.md) | 链表、排序 | 中等 | ### 链表双指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle.md) | 哈希表、链表、双指针 | 简单 | | [0142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle-ii.md) | 哈希表、链表、双指针 | 中等 | | [0160. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/intersection-of-two-linked-lists.md) | 哈希表、链表、双指针 | 简单 | | [0019. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-nth-node-from-end-of-list.md) | 链表、双指针 | 中等 | | [0876. 链表的中间结点](https://leetcode.cn/problems/middle-of-the-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/middle-of-the-linked-list.md) | 链表、双指针 | 简单 | | [LCR 140. 训练计划 II](https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof.md) | 链表、双指针 | 简单 | | [0143. 重排链表](https://leetcode.cn/problems/reorder-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reorder-list.md) | 栈、递归、链表、双指针 | 中等 | | [0002. 两数相加](https://leetcode.cn/problems/add-two-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/add-two-numbers.md) | 递归、链表、数学 | 中等 | | [0445. 两数相加 II](https://leetcode.cn/problems/add-two-numbers-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-two-numbers-ii.md) | 栈、链表、数学 | 中等 | ## 第 3 章 栈、队列、哈希表 ### 栈基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1047. 删除字符串中的所有相邻重复项](https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/remove-all-adjacent-duplicates-in-string.md) | 栈、字符串 | 简单 | | [0155. 最小栈](https://leetcode.cn/problems/min-stack/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/min-stack.md) | 栈、设计 | 中等 | | [0020. 有效的括号](https://leetcode.cn/problems/valid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-parentheses.md) | 栈、字符串 | 简单 | | [0227. 基本计算器 II](https://leetcode.cn/problems/basic-calculator-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/basic-calculator-ii.md) | 栈、数学、字符串 | 中等 | | [0150. 逆波兰表达式求值](https://leetcode.cn/problems/evaluate-reverse-polish-notation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/evaluate-reverse-polish-notation.md) | 栈、数组、数学 | 中等 | | [0232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-queue-using-stacks.md) | 栈、设计、队列 | 简单 | | [LCR 125. 图书整理 II](https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/yong-liang-ge-zhan-shi-xian-dui-lie-lcof.md) | 栈、设计、队列 | 简单 | | [0394. 字符串解码](https://leetcode.cn/problems/decode-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/decode-string.md) | 栈、递归、字符串 | 中等 | | [0032. 最长有效括号](https://leetcode.cn/problems/longest-valid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-valid-parentheses.md) | 栈、字符串、动态规划 | 困难 | | [0946. 验证栈序列](https://leetcode.cn/problems/validate-stack-sequences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/validate-stack-sequences.md) | 栈、数组、模拟 | 中等 | | [LCR 123. 图书整理 I](https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/cong-wei-dao-tou-da-yin-lian-biao-lcof.md) | 栈、递归、链表、双指针 | 简单 | | [0071. 简化路径](https://leetcode.cn/problems/simplify-path/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/simplify-path.md) | 栈、字符串 | 中等 | ### 单调栈题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0739. 每日温度](https://leetcode.cn/problems/daily-temperatures/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/daily-temperatures.md) | 栈、数组、单调栈 | 中等 | | [0496. 下一个更大元素 I](https://leetcode.cn/problems/next-greater-element-i/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/next-greater-element-i.md) | 栈、数组、哈希表、单调栈 | 简单 | | [0503. 下一个更大元素 II](https://leetcode.cn/problems/next-greater-element-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/next-greater-element-ii.md) | 栈、数组、单调栈 | 中等 | | [0901. 股票价格跨度](https://leetcode.cn/problems/online-stock-span/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/online-stock-span.md) | 栈、设计、数据流、单调栈 | 中等 | | [0853. 车队](https://leetcode.cn/problems/car-fleet/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/car-fleet.md) | 栈、数组、排序、单调栈 | 中等 | | [1762. 能看到海景的建筑物](https://leetcode.cn/problems/buildings-with-an-ocean-view/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/buildings-with-an-ocean-view.md) | 栈、数组、单调栈 | 中等 | | [0084. 柱状图中最大的矩形](https://leetcode.cn/problems/largest-rectangle-in-histogram/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/largest-rectangle-in-histogram.md) | 栈、数组、单调栈 | 困难 | | [0316. 去除重复字母](https://leetcode.cn/problems/remove-duplicate-letters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/remove-duplicate-letters.md) | 栈、贪心、字符串、单调栈 | 中等 | | [0042. 接雨水](https://leetcode.cn/problems/trapping-rain-water/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/trapping-rain-water.md) | 栈、数组、双指针、动态规划、单调栈 | 困难 | | [0085. 最大矩形](https://leetcode.cn/problems/maximal-rectangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximal-rectangle.md) | 栈、数组、动态规划、矩阵、单调栈 | 困难 | | [0862. 和至少为 K 的最短子数组](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-subarray-with-sum-at-least-k.md) | 队列、数组、二分查找、前缀和、滑动窗口、单调队列、堆(优先队列) | 困难 | ### 队列基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0622. 设计循环队列](https://leetcode.cn/problems/design-circular-queue/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-circular-queue.md) | 设计、队列、数组、链表 | 中等 | | [0346. 数据流中的移动平均值](https://leetcode.cn/problems/moving-average-from-data-stream/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/moving-average-from-data-stream.md) | 设计、队列、数组、数据流 | 简单 | | [0225. 用队列实现栈](https://leetcode.cn/problems/implement-stack-using-queues/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-stack-using-queues.md) | 栈、设计、队列 | 简单 | ### 优先队列题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0703. 数据流中的第 K 大元素](https://leetcode.cn/problems/kth-largest-element-in-a-stream/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/kth-largest-element-in-a-stream.md) | 树、设计、二叉搜索树、二叉树、数据流、堆(优先队列) | 简单 | | [0347. 前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/top-k-frequent-elements.md) | 数组、哈希表、分治、桶排序、计数、快速选择、排序、堆(优先队列) | 中等 | | [0451. 根据字符出现频率排序](https://leetcode.cn/problems/sort-characters-by-frequency/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sort-characters-by-frequency.md) | 哈希表、字符串、桶排序、计数、排序、堆(优先队列) | 中等 | | [0973. 最接近原点的 K 个点](https://leetcode.cn/problems/k-closest-points-to-origin/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/k-closest-points-to-origin.md) | 几何、数组、数学、分治、快速选择、排序、堆(优先队列) | 中等 | | [1296. 划分数组为连续数字的集合](https://leetcode.cn/problems/divide-array-in-sets-of-k-consecutive-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/divide-array-in-sets-of-k-consecutive-numbers.md) | 贪心、数组、哈希表、排序 | 中等 | | [0239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/sliding-window-maximum.md) | 队列、数组、滑动窗口、单调队列、堆(优先队列) | 困难 | | [0295. 数据流的中位数](https://leetcode.cn/problems/find-median-from-data-stream/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/find-median-from-data-stream.md) | 设计、双指针、数据流、排序、堆(优先队列) | 困难 | | [0023. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) | 链表、分治、堆(优先队列)、归并排序 | 困难 | | [0218. 天际线问题](https://leetcode.cn/problems/the-skyline-problem/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/the-skyline-problem.md) | 树状数组、线段树、数组、分治、有序集合、排序、扫描线、堆(优先队列) | 困难 | ### 哈希表题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0705. 设计哈希集合](https://leetcode.cn/problems/design-hashset/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/design-hashset.md) | 设计、数组、哈希表、链表、哈希函数 | 简单 | | [0706. 设计哈希映射](https://leetcode.cn/problems/design-hashmap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/design-hashmap.md) | 设计、数组、哈希表、链表、哈希函数 | 简单 | | [0217. 存在重复元素](https://leetcode.cn/problems/contains-duplicate/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate.md) | 数组、哈希表、排序 | 简单 | | [0219. 存在重复元素 II](https://leetcode.cn/problems/contains-duplicate-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate-ii.md) | 数组、哈希表、滑动窗口 | 简单 | | [0220. 存在重复元素 III](https://leetcode.cn/problems/contains-duplicate-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate-iii.md) | 数组、桶排序、有序集合、排序、滑动窗口 | 困难 | | [1941. 检查是否所有字符出现次数相同](https://leetcode.cn/problems/check-if-all-characters-have-equal-number-of-occurrences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/check-if-all-characters-have-equal-number-of-occurrences.md) | 哈希表、字符串、计数 | 简单 | | [0136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) | 位运算、数组 | 简单 | | [0383. 赎金信](https://leetcode.cn/problems/ransom-note/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/ransom-note.md) | 哈希表、字符串、计数 | 简单 | | [0349. 两个数组的交集](https://leetcode.cn/problems/intersection-of-two-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays.md) | 数组、哈希表、双指针、二分查找、排序 | 简单 | | [0350. 两个数组的交集 II](https://leetcode.cn/problems/intersection-of-two-arrays-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays-ii.md) | 数组、哈希表、双指针、二分查找、排序 | 简单 | | [0036. 有效的数独](https://leetcode.cn/problems/valid-sudoku/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-sudoku.md) | 数组、哈希表、矩阵 | 中等 | | [0001. 两数之和](https://leetcode.cn/problems/two-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/two-sum.md) | 数组、哈希表 | 简单 | | [0015. 三数之和](https://leetcode.cn/problems/3sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum.md) | 数组、双指针、排序 | 中等 | | [0018. 四数之和](https://leetcode.cn/problems/4sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/4sum.md) | 数组、双指针、排序 | 中等 | | [0454. 四数相加 II](https://leetcode.cn/problems/4sum-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/4sum-ii.md) | 数组、哈希表 | 中等 | | [0041. 缺失的第一个正数](https://leetcode.cn/problems/first-missing-positive/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/first-missing-positive.md) | 数组、哈希表 | 困难 | | [0128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/longest-consecutive-sequence.md) | 并查集、数组、哈希表 | 中等 | | [0202. 快乐数](https://leetcode.cn/problems/happy-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/happy-number.md) | 哈希表、数学、双指针 | 简单 | | [0242. 有效的字母异位词](https://leetcode.cn/problems/valid-anagram/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/valid-anagram.md) | 哈希表、字符串、排序 | 简单 | | [0205. 同构字符串](https://leetcode.cn/problems/isomorphic-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/isomorphic-strings.md) | 哈希表、字符串 | 简单 | | [0442. 数组中重复的数据](https://leetcode.cn/problems/find-all-duplicates-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-all-duplicates-in-an-array.md) | 数组、哈希表 | 中等 | | [LCR 186. 文物朝代判断](https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/bu-ke-pai-zhong-de-shun-zi-lcof.md) | 数组、排序 | 简单 | | [0268. 丢失的数字](https://leetcode.cn/problems/missing-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/missing-number.md) | 位运算、数组、哈希表、数学、二分查找、排序 | 简单 | | [LCR 120. 寻找文件副本](https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-zhong-fu-de-shu-zi-lcof.md) | 数组、哈希表、排序 | 简单 | | [0451. 根据字符出现频率排序](https://leetcode.cn/problems/sort-characters-by-frequency/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sort-characters-by-frequency.md) | 哈希表、字符串、桶排序、计数、排序、堆(优先队列) | 中等 | | [0049. 字母异位词分组](https://leetcode.cn/problems/group-anagrams/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/group-anagrams.md) | 数组、哈希表、字符串、排序 | 中等 | | [0599. 两个列表的最小索引总和](https://leetcode.cn/problems/minimum-index-sum-of-two-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/minimum-index-sum-of-two-lists.md) | 数组、哈希表、字符串 | 简单 | | [0387. 字符串中的第一个唯一字符](https://leetcode.cn/problems/first-unique-character-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/first-unique-character-in-a-string.md) | 队列、哈希表、字符串、计数 | 简单 | | [0447. 回旋镖的数量](https://leetcode.cn/problems/number-of-boomerangs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/number-of-boomerangs.md) | 数组、哈希表、数学 | 中等 | | [0149. 直线上最多的点数](https://leetcode.cn/problems/max-points-on-a-line/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/max-points-on-a-line.md) | 几何、数组、哈希表、数学 | 困难 | | [0359. 日志速率限制器](https://leetcode.cn/problems/logger-rate-limiter/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/logger-rate-limiter.md) | 设计、哈希表、数据流 | 简单 | | [0811. 子域名访问计数](https://leetcode.cn/problems/subdomain-visit-count/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/subdomain-visit-count.md) | 数组、哈希表、字符串、计数 | 中等 | ## 第 4 章 字符串 ### 字符串基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0125. 验证回文串](https://leetcode.cn/problems/valid-palindrome/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/valid-palindrome.md) | 双指针、字符串 | 简单 | | [0005. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-palindromic-substring.md) | 双指针、字符串、动态规划 | 中等 | | [0003. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-substring-without-repeating-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0344. 反转字符串](https://leetcode.cn/problems/reverse-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-string.md) | 双指针、字符串 | 简单 | | [0557. 反转字符串中的单词 III](https://leetcode.cn/problems/reverse-words-in-a-string-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/reverse-words-in-a-string-iii.md) | 双指针、字符串 | 简单 | | [0049. 字母异位词分组](https://leetcode.cn/problems/group-anagrams/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/group-anagrams.md) | 数组、哈希表、字符串、排序 | 中等 | | [0415. 字符串相加](https://leetcode.cn/problems/add-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-strings.md) | 数学、字符串、模拟 | 简单 | | [0151. 反转字符串中的单词](https://leetcode.cn/problems/reverse-words-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reverse-words-in-a-string.md) | 双指针、字符串 | 中等 | | [0043. 字符串相乘](https://leetcode.cn/problems/multiply-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/multiply-strings.md) | 数学、字符串、模拟 | 中等 | | [0014. 最长公共前缀](https://leetcode.cn/problems/longest-common-prefix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-common-prefix.md) | 字典树、数组、字符串 | 简单 | ### 单模式串匹配题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0028. 找出字符串中第一个匹配项的下标](https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-the-index-of-the-first-occurrence-in-a-string.md) | 双指针、字符串、字符串匹配 | 简单 | | [0459. 重复的子字符串](https://leetcode.cn/problems/repeated-substring-pattern/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/repeated-substring-pattern.md) | 字符串、字符串匹配 | 简单 | | [0686. 重复叠加字符串匹配](https://leetcode.cn/problems/repeated-string-match/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/repeated-string-match.md) | 字符串、字符串匹配 | 中等 | | [1668. 最大重复子字符串](https://leetcode.cn/problems/maximum-repeating-substring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/maximum-repeating-substring.md) | 字符串、动态规划、字符串匹配 | 简单 | | [0796. 旋转字符串](https://leetcode.cn/problems/rotate-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotate-string.md) | 字符串、字符串匹配 | 简单 | | [1408. 数组中的字符串匹配](https://leetcode.cn/problems/string-matching-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/string-matching-in-an-array.md) | 数组、字符串、字符串匹配 | 简单 | | [2156. 查找给定哈希值的子串](https://leetcode.cn/problems/find-substring-with-given-hash-value/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/find-substring-with-given-hash-value.md) | 字符串、滑动窗口、哈希函数、滚动哈希 | 困难 | ### 字典树题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0208. 实现 Trie (前缀树)](https://leetcode.cn/problems/implement-trie-prefix-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-trie-prefix-tree.md) | 设计、字典树、哈希表、字符串 | 中等 | | [0677. 键值映射](https://leetcode.cn/problems/map-sum-pairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/map-sum-pairs.md) | 设计、字典树、哈希表、字符串 | 中等 | | [0648. 单词替换](https://leetcode.cn/problems/replace-words/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/replace-words.md) | 字典树、数组、哈希表、字符串 | 中等 | | [0642. 设计搜索自动补全系统](https://leetcode.cn/problems/design-search-autocomplete-system/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-search-autocomplete-system.md) | 深度优先搜索、设计、字典树、字符串、数据流、排序、堆(优先队列) | 困难 | | [0211. 添加与搜索单词 - 数据结构设计](https://leetcode.cn/problems/design-add-and-search-words-data-structure/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/design-add-and-search-words-data-structure.md) | 深度优先搜索、设计、字典树、字符串 | 中等 | | [0421. 数组中两个数的最大异或值](https://leetcode.cn/problems/maximum-xor-of-two-numbers-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/maximum-xor-of-two-numbers-in-an-array.md) | 位运算、字典树、数组、哈希表 | 中等 | | [0212. 单词搜索 II](https://leetcode.cn/problems/word-search-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/word-search-ii.md) | 字典树、数组、字符串、回溯、矩阵 | 困难 | | [0425. 单词方块](https://leetcode.cn/problems/word-squares/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/word-squares.md) | 字典树、数组、字符串、回溯 | 困难 | | [0336. 回文对](https://leetcode.cn/problems/palindrome-pairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/palindrome-pairs.md) | 字典树、数组、哈希表、字符串 | 困难 | | [1023. 驼峰式匹配](https://leetcode.cn/problems/camelcase-matching/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/camelcase-matching.md) | 字典树、数组、双指针、字符串、字符串匹配 | 中等 | | [0676. 实现一个魔法字典](https://leetcode.cn/problems/implement-magic-dictionary/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/implement-magic-dictionary.md) | 深度优先搜索、设计、字典树、哈希表、字符串 | 中等 | | [0440. 字典序的第K小数字](https://leetcode.cn/problems/k-th-smallest-in-lexicographical-order/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/k-th-smallest-in-lexicographical-order.md) | 字典树 | 困难 | ## 第 5 章 树 ### 二叉树的遍历题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-preorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0094. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/binary-tree-inorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0145. 二叉树的后序遍历](https://leetcode.cn/problems/binary-tree-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-postorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-level-order-traversal.md) | 树、广度优先搜索、二叉树 | 中等 | | [0103. 二叉树的锯齿形层序遍历](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-zigzag-level-order-traversal.md) | 树、广度优先搜索、二叉树 | 中等 | | [0107. 二叉树的层序遍历 II](https://leetcode.cn/problems/binary-tree-level-order-traversal-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-level-order-traversal-ii.md) | 树、广度优先搜索、二叉树 | 中等 | | [0104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/minimum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [0101. 对称二叉树](https://leetcode.cn/problems/symmetric-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/symmetric-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0112. 路径总和](https://leetcode.cn/problems/path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/path-sum.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0113. 路径总和 II](https://leetcode.cn/problems/path-sum-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/path-sum-ii.md) | 树、深度优先搜索、回溯、二叉树 | 中等 | | [0236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/lowest-common-ancestor-of-a-binary-tree.md) | 树、深度优先搜索、二叉树 | 中等 | | [0199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-right-side-view.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/invert-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0958. 二叉树的完全性检验](https://leetcode.cn/problems/check-completeness-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/check-completeness-of-a-binary-tree.md) | 树、广度优先搜索、二叉树 | 中等 | | [0572. 另一棵树的子树](https://leetcode.cn/problems/subtree-of-another-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subtree-of-another-tree.md) | 树、深度优先搜索、二叉树、字符串匹配、哈希函数 | 简单 | | [0100. 相同的树](https://leetcode.cn/problems/same-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/same-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0116. 填充每个节点的下一个右侧节点指针](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/populating-next-right-pointers-in-each-node.md) | 树、深度优先搜索、广度优先搜索、链表、二叉树 | 中等 | | [0117. 填充每个节点的下一个右侧节点指针 II](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/populating-next-right-pointers-in-each-node-ii.md) | 树、深度优先搜索、广度优先搜索、链表、二叉树 | 中等 | | [0297. 二叉树的序列化与反序列化](https://leetcode.cn/problems/serialize-and-deserialize-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/serialize-and-deserialize-binary-tree.md) | 树、深度优先搜索、广度优先搜索、设计、字符串、二叉树 | 困难 | | [0114. 二叉树展开为链表](https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/flatten-binary-tree-to-linked-list.md) | 栈、树、深度优先搜索、链表、二叉树 | 中等 | ### 二叉树的还原题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/construct-binary-tree-from-preorder-and-inorder-traversal.md) | 树、数组、哈希表、分治、二叉树 | 中等 | | [0106. 从中序与后序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/construct-binary-tree-from-inorder-and-postorder-traversal.md) | 树、数组、哈希表、分治、二叉树 | 中等 | | [0889. 根据前序和后序遍历构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/construct-binary-tree-from-preorder-and-postorder-traversal.md) | 树、数组、哈希表、分治、二叉树 | 中等 | ### 二叉搜索树题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0098. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/validate-binary-search-tree.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0173. 二叉搜索树迭代器](https://leetcode.cn/problems/binary-search-tree-iterator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-search-tree-iterator.md) | 栈、树、设计、二叉搜索树、二叉树、迭代器 | 中等 | | [0700. 二叉搜索树中的搜索](https://leetcode.cn/problems/search-in-a-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/search-in-a-binary-search-tree.md) | 树、二叉搜索树、二叉树 | 简单 | | [0701. 二叉搜索树中的插入操作](https://leetcode.cn/problems/insert-into-a-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/insert-into-a-binary-search-tree.md) | 树、二叉搜索树、二叉树 | 中等 | | [0450. 删除二叉搜索树中的节点](https://leetcode.cn/problems/delete-node-in-a-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/delete-node-in-a-bst.md) | 树、二叉搜索树、二叉树 | 中等 | | [0703. 数据流中的第 K 大元素](https://leetcode.cn/problems/kth-largest-element-in-a-stream/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/kth-largest-element-in-a-stream.md) | 树、设计、二叉搜索树、二叉树、数据流、堆(优先队列) | 简单 | | [LCR 174. 寻找二叉搜索树中的目标节点](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 简单 | | [0230. 二叉搜索树中第 K 小的元素](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/kth-smallest-element-in-a-bst.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0235. 二叉搜索树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/lowest-common-ancestor-of-a-binary-search-tree.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0426. 将二叉搜索树转化为排序的双向链表](https://leetcode.cn/problems/convert-binary-search-tree-to-sorted-doubly-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/convert-binary-search-tree-to-sorted-doubly-linked-list.md) | 栈、树、深度优先搜索、二叉搜索树、链表、二叉树、双向链表 | 中等 | | [0108. 将有序数组转换为二叉搜索树](https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/convert-sorted-array-to-binary-search-tree.md) | 树、二叉搜索树、数组、分治、二叉树 | 简单 | | [0110. 平衡二叉树](https://leetcode.cn/problems/balanced-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/balanced-binary-tree.md) | 树、深度优先搜索、二叉树 | 简单 | ### 线段树题目 #### 单点更新题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0303. 区域和检索 - 数组不可变](https://leetcode.cn/problems/range-sum-query-immutable/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-immutable.md) | 设计、数组、前缀和 | 简单 | | [0307. 区域和检索 - 数组可修改](https://leetcode.cn/problems/range-sum-query-mutable/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-mutable.md) | 设计、树状数组、线段树、数组、分治 | 中等 | | [0354. 俄罗斯套娃信封问题](https://leetcode.cn/problems/russian-doll-envelopes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/russian-doll-envelopes.md) | 数组、二分查找、动态规划、排序 | 困难 | #### 区间更新题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0370. 区间加法](https://leetcode.cn/problems/range-addition/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-addition.md) | 数组、前缀和 | 中等 | | [1109. 航班预订统计](https://leetcode.cn/problems/corporate-flight-bookings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/corporate-flight-bookings.md) | 数组、前缀和 | 中等 | | [1450. 在既定时间做作业的学生人数](https://leetcode.cn/problems/number-of-students-doing-homework-at-a-given-time/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/number-of-students-doing-homework-at-a-given-time.md) | 数组 | 简单 | | [0673. 最长递增子序列的个数](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/number-of-longest-increasing-subsequence.md) | 树状数组、线段树、数组、动态规划 | 中等 | | [1310. 子数组异或查询](https://leetcode.cn/problems/xor-queries-of-a-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/xor-queries-of-a-subarray.md) | 位运算、数组、前缀和 | 中等 | | [1851. 包含每个查询的最小区间](https://leetcode.cn/problems/minimum-interval-to-include-each-query/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/minimum-interval-to-include-each-query.md) | 数组、二分查找、排序、扫描线、堆(优先队列) | 困难 | #### 区间合并题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0729. 我的日程安排表 I](https://leetcode.cn/problems/my-calendar-i/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/my-calendar-i.md) | 设计、线段树、数组、二分查找、有序集合 | 中等 | | [0731. 我的日程安排表 II](https://leetcode.cn/problems/my-calendar-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/my-calendar-ii.md) | 设计、线段树、数组、二分查找、有序集合、前缀和 | 中等 | | [0732. 我的日程安排表 III](https://leetcode.cn/problems/my-calendar-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/my-calendar-iii.md) | 设计、线段树、二分查找、有序集合、前缀和 | 困难 | #### 扫描线问题题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0218. 天际线问题](https://leetcode.cn/problems/the-skyline-problem/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/the-skyline-problem.md) | 树状数组、线段树、数组、分治、有序集合、排序、扫描线、堆(优先队列) | 困难 | | [0391. 完美矩形](https://leetcode.cn/problems/perfect-rectangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/perfect-rectangle.md) | 几何、数组、哈希表、数学、扫描线 | 困难 | | [0850. 矩形面积 II](https://leetcode.cn/problems/rectangle-area-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/rectangle-area-ii.md) | 线段树、数组、有序集合、扫描线 | 困难 | ### 树状数组题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0303. 区域和检索 - 数组不可变](https://leetcode.cn/problems/range-sum-query-immutable/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-immutable.md) | 设计、数组、前缀和 | 简单 | | [0307. 区域和检索 - 数组可修改](https://leetcode.cn/problems/range-sum-query-mutable/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-mutable.md) | 设计、树状数组、线段树、数组、分治 | 中等 | | [0315. 计算右侧小于当前元素的个数](https://leetcode.cn/problems/count-of-smaller-numbers-after-self/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-of-smaller-numbers-after-self.md) | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 | | [1450. 在既定时间做作业的学生人数](https://leetcode.cn/problems/number-of-students-doing-homework-at-a-given-time/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/number-of-students-doing-homework-at-a-given-time.md) | 数组 | 简单 | | [0354. 俄罗斯套娃信封问题](https://leetcode.cn/problems/russian-doll-envelopes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/russian-doll-envelopes.md) | 数组、二分查找、动态规划、排序 | 困难 | | [0673. 最长递增子序列的个数](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/number-of-longest-increasing-subsequence.md) | 树状数组、线段树、数组、动态规划 | 中等 | | [1310. 子数组异或查询](https://leetcode.cn/problems/xor-queries-of-a-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/xor-queries-of-a-subarray.md) | 位运算、数组、前缀和 | 中等 | | [1893. 检查是否区域内所有整数都被覆盖](https://leetcode.cn/problems/check-if-all-the-integers-in-a-range-are-covered/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/check-if-all-the-integers-in-a-range-are-covered.md) | 数组、哈希表、前缀和 | 简单 | ### 并查集题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0990. 等式方程的可满足性](https://leetcode.cn/problems/satisfiability-of-equality-equations/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/satisfiability-of-equality-equations.md) | 并查集、图、数组、字符串 | 中等 | | [0547. 省份数量](https://leetcode.cn/problems/number-of-provinces/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/number-of-provinces.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0684. 冗余连接](https://leetcode.cn/problems/redundant-connection/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/redundant-connection.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [1319. 连通网络的操作次数](https://leetcode.cn/problems/number-of-operations-to-make-network-connected/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/number-of-operations-to-make-network-connected.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0765. 情侣牵手](https://leetcode.cn/problems/couples-holding-hands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/couples-holding-hands.md) | 贪心、深度优先搜索、广度优先搜索、并查集、图 | 困难 | | [0399. 除法求值](https://leetcode.cn/problems/evaluate-division/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/evaluate-division.md) | 深度优先搜索、广度优先搜索、并查集、图、数组、字符串、最短路 | 中等 | | [0959. 由斜杠划分区域](https://leetcode.cn/problems/regions-cut-by-slashes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/regions-cut-by-slashes.md) | 深度优先搜索、广度优先搜索、并查集、数组、哈希表、矩阵 | 中等 | | [1631. 最小体力消耗路径](https://leetcode.cn/problems/path-with-minimum-effort/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/path-with-minimum-effort.md) | 深度优先搜索、广度优先搜索、并查集、数组、二分查找、矩阵、堆(优先队列) | 中等 | | [0778. 水位上升的泳池中游泳](https://leetcode.cn/problems/swim-in-rising-water/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/swim-in-rising-water.md) | 深度优先搜索、广度优先搜索、并查集、数组、二分查找、矩阵、堆(优先队列) | 困难 | | [1202. 交换字符串中的元素](https://leetcode.cn/problems/smallest-string-with-swaps/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/smallest-string-with-swaps.md) | 深度优先搜索、广度优先搜索、并查集、数组、哈希表、字符串、排序 | 中等 | | [0947. 移除最多的同行或同列石头](https://leetcode.cn/problems/most-stones-removed-with-same-row-or-column/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/most-stones-removed-with-same-row-or-column.md) | 深度优先搜索、并查集、图、哈希表 | 中等 | | [0803. 打砖块](https://leetcode.cn/problems/bricks-falling-when-hit/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/bricks-falling-when-hit.md) | 并查集、数组、矩阵 | 困难 | | [0128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/longest-consecutive-sequence.md) | 并查集、数组、哈希表 | 中等 | ## 第 6 章 图论 ### 深度优先搜索题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0797. 所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/all-paths-from-source-to-target.md) | 深度优先搜索、广度优先搜索、图、回溯 | 中等 | | [0200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-islands.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0695. 岛屿的最大面积](https://leetcode.cn/problems/max-area-of-island/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/max-area-of-island.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0133. 克隆图](https://leetcode.cn/problems/clone-graph/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/clone-graph.md) | 深度优先搜索、广度优先搜索、图、哈希表 | 中等 | | [0494. 目标和](https://leetcode.cn/problems/target-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/target-sum.md) | 数组、动态规划、回溯 | 中等 | | [0144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-preorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0094. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/binary-tree-inorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0145. 二叉树的后序遍历](https://leetcode.cn/problems/binary-tree-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-postorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0589. N 叉树的前序遍历](https://leetcode.cn/problems/n-ary-tree-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/n-ary-tree-preorder-traversal.md) | 栈、树、深度优先搜索 | 简单 | | [0590. N 叉树的后序遍历](https://leetcode.cn/problems/n-ary-tree-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/n-ary-tree-postorder-traversal.md) | 栈、树、深度优先搜索 | 简单 | | [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [0199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-right-side-view.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0543. 二叉树的直径](https://leetcode.cn/problems/diameter-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/diameter-of-binary-tree.md) | 树、深度优先搜索、二叉树 | 简单 | | [0662. 二叉树最大宽度](https://leetcode.cn/problems/maximum-width-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-width-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0958. 二叉树的完全性检验](https://leetcode.cn/problems/check-completeness-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/check-completeness-of-a-binary-tree.md) | 树、广度优先搜索、二叉树 | 中等 | | [0572. 另一棵树的子树](https://leetcode.cn/problems/subtree-of-another-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subtree-of-another-tree.md) | 树、深度优先搜索、二叉树、字符串匹配、哈希函数 | 简单 | | [0100. 相同的树](https://leetcode.cn/problems/same-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/same-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/minimum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0841. 钥匙和房间](https://leetcode.cn/problems/keys-and-rooms/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/keys-and-rooms.md) | 深度优先搜索、广度优先搜索、图 | 中等 | | [0129. 求根节点到叶节点数字之和](https://leetcode.cn/problems/sum-root-to-leaf-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sum-root-to-leaf-numbers.md) | 树、深度优先搜索、二叉树 | 中等 | | [0323. 无向图中连通分量的数目](https://leetcode.cn/problems/number-of-connected-components-in-an-undirected-graph/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/number-of-connected-components-in-an-undirected-graph.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0684. 冗余连接](https://leetcode.cn/problems/redundant-connection/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/redundant-connection.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0802. 找到最终的安全状态](https://leetcode.cn/problems/find-eventual-safe-states/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/find-eventual-safe-states.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [0785. 判断二分图](https://leetcode.cn/problems/is-graph-bipartite/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/is-graph-bipartite.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0886. 可能的二分法](https://leetcode.cn/problems/possible-bipartition/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/possible-bipartition.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0323. 无向图中连通分量的数目](https://leetcode.cn/problems/number-of-connected-components-in-an-undirected-graph/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/number-of-connected-components-in-an-undirected-graph.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0130. 被围绕的区域](https://leetcode.cn/problems/surrounded-regions/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/surrounded-regions.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0417. 太平洋大西洋水流问题](https://leetcode.cn/problems/pacific-atlantic-water-flow/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/pacific-atlantic-water-flow.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 中等 | | [1020. 飞地的数量](https://leetcode.cn/problems/number-of-enclaves/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/number-of-enclaves.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [1254. 统计封闭岛屿的数目](https://leetcode.cn/problems/number-of-closed-islands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/number-of-closed-islands.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [1034. 边界着色](https://leetcode.cn/problems/coloring-a-border/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/coloring-a-border.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 中等 | | [LCR 130. 衣橱整理](https://leetcode.cn/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ji-qi-ren-de-yun-dong-fan-wei-lcof.md) | 深度优先搜索、广度优先搜索、动态规划 | 中等 | | [0529. 扫雷游戏](https://leetcode.cn/problems/minesweeper/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/minesweeper.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 中等 | ### 广度优先搜索题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0797. 所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/all-paths-from-source-to-target.md) | 深度优先搜索、广度优先搜索、图、回溯 | 中等 | | [0286. 墙与门](https://leetcode.cn/problems/walls-and-gates/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/walls-and-gates.md) | 广度优先搜索、数组、矩阵 | 中等 | | [0200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-islands.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0752. 打开转盘锁](https://leetcode.cn/problems/open-the-lock/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/open-the-lock.md) | 广度优先搜索、数组、哈希表、字符串 | 中等 | | [0279. 完全平方数](https://leetcode.cn/problems/perfect-squares/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/perfect-squares.md) | 广度优先搜索、数学、动态规划 | 中等 | | [0133. 克隆图](https://leetcode.cn/problems/clone-graph/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/clone-graph.md) | 深度优先搜索、广度优先搜索、图、哈希表 | 中等 | | [0733. 图像渲染](https://leetcode.cn/problems/flood-fill/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/flood-fill.md) | 深度优先搜索、广度优先搜索、数组、矩阵 | 简单 | | [0542. 01 矩阵](https://leetcode.cn/problems/01-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/01-matrix.md) | 广度优先搜索、数组、动态规划、矩阵 | 中等 | | [0322. 零钱兑换](https://leetcode.cn/problems/coin-change/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) | 广度优先搜索、数组、动态规划 | 中等 | | [0323. 无向图中连通分量的数目](https://leetcode.cn/problems/number-of-connected-components-in-an-undirected-graph/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/number-of-connected-components-in-an-undirected-graph.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [LCR 130. 衣橱整理](https://leetcode.cn/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ji-qi-ren-de-yun-dong-fan-wei-lcof.md) | 深度优先搜索、广度优先搜索、动态规划 | 中等 | | [0199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-right-side-view.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0662. 二叉树最大宽度](https://leetcode.cn/problems/maximum-width-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-width-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0958. 二叉树的完全性检验](https://leetcode.cn/problems/check-completeness-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/check-completeness-of-a-binary-tree.md) | 树、广度优先搜索、二叉树 | 中等 | | [0572. 另一棵树的子树](https://leetcode.cn/problems/subtree-of-another-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subtree-of-another-tree.md) | 树、深度优先搜索、二叉树、字符串匹配、哈希函数 | 简单 | | [0100. 相同的树](https://leetcode.cn/problems/same-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/same-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/minimum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [LCR 151. 彩灯装饰记录 III](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof.md) | 树、广度优先搜索、二叉树 | 中等 | ### 图的拓扑排序题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0207. 课程表](https://leetcode.cn/problems/course-schedule/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/course-schedule.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [0210. 课程表 II](https://leetcode.cn/problems/course-schedule-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/course-schedule-ii.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [1136. 并行课程](https://leetcode.cn/problems/parallel-courses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/parallel-courses.md) | 图、拓扑排序 | 中等 | | [2050. 并行课程 III](https://leetcode.cn/problems/parallel-courses-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/parallel-courses-iii.md) | 图、拓扑排序、数组、动态规划 | 困难 | | [0802. 找到最终的安全状态](https://leetcode.cn/problems/find-eventual-safe-states/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/find-eventual-safe-states.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [0851. 喧闹和富有](https://leetcode.cn/problems/loud-and-rich/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/loud-and-rich.md) | 深度优先搜索、图、拓扑排序、数组 | 中等 | ### 图的最小生成树题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1584. 连接所有点的最小费用](https://leetcode.cn/problems/min-cost-to-connect-all-points/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/min-cost-to-connect-all-points.md) | 并查集、图、数组、最小生成树 | 中等 | | [1631. 最小体力消耗路径](https://leetcode.cn/problems/path-with-minimum-effort/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/path-with-minimum-effort.md) | 深度优先搜索、广度优先搜索、并查集、数组、二分查找、矩阵、堆(优先队列) | 中等 | | [0778. 水位上升的泳池中游泳](https://leetcode.cn/problems/swim-in-rising-water/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/swim-in-rising-water.md) | 深度优先搜索、广度优先搜索、并查集、数组、二分查找、矩阵、堆(优先队列) | 困难 | ### 单源最短路径题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0407. 接雨水 II](https://leetcode.cn/problems/trapping-rain-water-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/trapping-rain-water-ii.md) | 广度优先搜索、数组、矩阵、堆(优先队列) | 困难 | | [0743. 网络延迟时间](https://leetcode.cn/problems/network-delay-time/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/network-delay-time.md) | 深度优先搜索、广度优先搜索、图、最短路、堆(优先队列) | 中等 | | [0787. K 站中转内最便宜的航班](https://leetcode.cn/problems/cheapest-flights-within-k-stops/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/cheapest-flights-within-k-stops.md) | 深度优先搜索、广度优先搜索、图、动态规划、最短路、堆(优先队列) | 中等 | | [1631. 最小体力消耗路径](https://leetcode.cn/problems/path-with-minimum-effort/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/path-with-minimum-effort.md) | 深度优先搜索、广度优先搜索、并查集、数组、二分查找、矩阵、堆(优先队列) | 中等 | | [1786. 从第一个节点出发到最后一个节点的受限路径数](https://leetcode.cn/problems/number-of-restricted-paths-from-first-to-last-node/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/number-of-restricted-paths-from-first-to-last-node.md) | 图、拓扑排序、动态规划、最短路、堆(优先队列) | 中等 | ### 多源最短路径题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0815. 公交路线](https://leetcode.cn/problems/bus-routes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/bus-routes.md) | 广度优先搜索、数组、哈希表 | 困难 | | [1162. 地图分析](https://leetcode.cn/problems/as-far-from-land-as-possible/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/as-far-from-land-as-possible.md) | 广度优先搜索、数组、动态规划、矩阵 | 中等 | ### 次短路径题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [2045. 到达目的地的第二短时间](https://leetcode.cn/problems/second-minimum-time-to-reach-destination/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/second-minimum-time-to-reach-destination.md) | 广度优先搜索、图、最短路 | 困难 | ### 差分约束系统题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0995. K 连续位的最小翻转次数](https://leetcode.cn/problems/minimum-number-of-k-consecutive-bit-flips/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-number-of-k-consecutive-bit-flips.md) | 位运算、队列、数组、前缀和、滑动窗口 | 困难 | | [1109. 航班预订统计](https://leetcode.cn/problems/corporate-flight-bookings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/corporate-flight-bookings.md) | 数组、前缀和 | 中等 | ### 二分图基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0785. 判断二分图](https://leetcode.cn/problems/is-graph-bipartite/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/is-graph-bipartite.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | ### 二分图最大匹配题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [LCP 04. 覆盖](https://leetcode.cn/problems/broken-board-dominoes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCP/broken-board-dominoes.md) | 位运算、图、数组、动态规划、状态压缩 | 困难 | | [1947. 最大兼容性评分和](https://leetcode.cn/problems/maximum-compatibility-score-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/maximum-compatibility-score-sum.md) | 位运算、数组、动态规划、回溯、状态压缩 | 中等 | | [1595. 连通两组点的最小成本](https://leetcode.cn/problems/minimum-cost-to-connect-two-groups-of-points/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-cost-to-connect-two-groups-of-points.md) | 位运算、数组、动态规划、状态压缩、矩阵 | 困难 | ## 第 7 章 基础算法 ### 枚举算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0001. 两数之和](https://leetcode.cn/problems/two-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/two-sum.md) | 数组、哈希表 | 简单 | | [0204. 计数质数](https://leetcode.cn/problems/count-primes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/count-primes.md) | 数组、数学、枚举、数论 | 中等 | | [2427. 公因子的数目](https://leetcode.cn/problems/number-of-common-factors/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2400-2499/number-of-common-factors.md) | 数学、枚举、数论 | 简单 | | [1925. 统计平方和三元组的数目](https://leetcode.cn/problems/count-square-sum-triples/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/count-square-sum-triples.md) | 数学、枚举 | 简单 | | [1450. 在既定时间做作业的学生人数](https://leetcode.cn/problems/number-of-students-doing-homework-at-a-given-time/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/number-of-students-doing-homework-at-a-given-time.md) | 数组 | 简单 | | [1620. 网络信号最好的坐标](https://leetcode.cn/problems/coordinate-with-maximum-network-quality/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/coordinate-with-maximum-network-quality.md) | 数组、枚举 | 中等 | | [LCR 180. 文件组合](https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof.md) | 数学、双指针、枚举 | 简单 | | [0800. 相似 RGB 颜色](https://leetcode.cn/problems/similar-rgb-color/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/similar-rgb-color.md) | 数学、字符串、枚举 | 简单 | | [0221. 最大正方形](https://leetcode.cn/problems/maximal-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/maximal-square.md) | 数组、动态规划、矩阵 | 中等 | | [0560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subarray-sum-equals-k.md) | 数组、哈希表、前缀和 | 中等 | ### 递归算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0344. 反转字符串](https://leetcode.cn/problems/reverse-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-string.md) | 双指针、字符串 | 简单 | | [0024. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/swap-nodes-in-pairs.md) | 递归、链表 | 中等 | | [0118. 杨辉三角](https://leetcode.cn/problems/pascals-triangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/pascals-triangle.md) | 数组、动态规划 | 简单 | | [0119. 杨辉三角 II](https://leetcode.cn/problems/pascals-triangle-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/pascals-triangle-ii.md) | 数组、动态规划 | 简单 | | [0206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/reverse-linked-list.md) | 递归、链表 | 简单 | | [0092. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-linked-list-ii.md) | 链表 | 中等 | | [0021. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-two-sorted-lists.md) | 递归、链表 | 简单 | | [0509. 斐波那契数](https://leetcode.cn/problems/fibonacci-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/fibonacci-number.md) | 递归、记忆化搜索、数学、动态规划 | 简单 | | [0070. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/climbing-stairs.md) | 记忆化搜索、数学、动态规划 | 简单 | | [0104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [0226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/invert-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0050. Pow(x, n)](https://leetcode.cn/problems/powx-n/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/powx-n.md) | 递归、数学 | 中等 | | [0779. 第K个语法符号](https://leetcode.cn/problems/k-th-symbol-in-grammar/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/k-th-symbol-in-grammar.md) | 位运算、递归、数学 | 中等 | | [0095. 不同的二叉搜索树 II](https://leetcode.cn/problems/unique-binary-search-trees-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-binary-search-trees-ii.md) | 树、二叉搜索树、动态规划、回溯、二叉树 | 中等 | | [LCR 187. 破冰游戏](https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof.md) | 递归、数学 | 简单 | ### 分治算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0004. 寻找两个正序数组的中位数](https://leetcode.cn/problems/median-of-two-sorted-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/median-of-two-sorted-arrays.md) | 数组、二分查找、分治 | 困难 | | [0023. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) | 链表、分治、堆(优先队列)、归并排序 | 困难 | | [0053. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximum-subarray.md) | 数组、分治、动态规划 | 中等 | | [0241. 为运算表达式设计优先级](https://leetcode.cn/problems/different-ways-to-add-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/different-ways-to-add-parentheses.md) | 递归、记忆化搜索、数学、字符串、动态规划 | 中等 | | [0169. 多数元素](https://leetcode.cn/problems/majority-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/majority-element.md) | 数组、哈希表、分治、计数、排序 | 简单 | | [0050. Pow(x, n)](https://leetcode.cn/problems/powx-n/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/powx-n.md) | 递归、数学 | 中等 | | [0014. 最长公共前缀](https://leetcode.cn/problems/longest-common-prefix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-common-prefix.md) | 字典树、数组、字符串 | 简单 | | [LCR 152. 验证二叉搜索树的后序遍历序列](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof.md) | 栈、树、二叉搜索树、递归、数组、二叉树、单调栈 | 中等 | ### 回溯算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0046. 全排列](https://leetcode.cn/problems/permutations/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutations.md) | 数组、回溯 | 中等 | | [0047. 全排列 II](https://leetcode.cn/problems/permutations-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutations-ii.md) | 数组、回溯、排序 | 中等 | | [0037. 解数独](https://leetcode.cn/problems/sudoku-solver/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sudoku-solver.md) | 数组、哈希表、回溯、矩阵 | 困难 | | [0022. 括号生成](https://leetcode.cn/problems/generate-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/generate-parentheses.md) | 字符串、动态规划、回溯 | 中等 | | [0017. 电话号码的字母组合](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/letter-combinations-of-a-phone-number.md) | 哈希表、字符串、回溯 | 中等 | | [0784. 字母大小写全排列](https://leetcode.cn/problems/letter-case-permutation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/letter-case-permutation.md) | 位运算、字符串、回溯 | 中等 | | [0039. 组合总和](https://leetcode.cn/problems/combination-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combination-sum.md) | 数组、回溯 | 中等 | | [0040. 组合总和 II](https://leetcode.cn/problems/combination-sum-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combination-sum-ii.md) | 数组、回溯 | 中等 | | [0078. 子集](https://leetcode.cn/problems/subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets.md) | 位运算、数组、回溯 | 中等 | | [0090. 子集 II](https://leetcode.cn/problems/subsets-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets-ii.md) | 位运算、数组、回溯 | 中等 | | [0473. 火柴拼正方形](https://leetcode.cn/problems/matchsticks-to-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/matchsticks-to-square.md) | 位运算、数组、动态规划、回溯、状态压缩 | 中等 | | [1593. 拆分字符串使唯一子字符串的数目最大](https://leetcode.cn/problems/split-a-string-into-the-max-number-of-unique-substrings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/split-a-string-into-the-max-number-of-unique-substrings.md) | 哈希表、字符串、回溯 | 中等 | | [1079. 活字印刷](https://leetcode.cn/problems/letter-tile-possibilities/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/letter-tile-possibilities.md) | 哈希表、字符串、回溯、计数 | 中等 | | [0093. 复原 IP 地址](https://leetcode.cn/problems/restore-ip-addresses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/restore-ip-addresses.md) | 字符串、回溯 | 中等 | | [0079. 单词搜索](https://leetcode.cn/problems/word-search/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/word-search.md) | 深度优先搜索、数组、字符串、回溯、矩阵 | 中等 | | [0679. 24 点游戏](https://leetcode.cn/problems/24-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/24-game.md) | 数组、数学、回溯 | 困难 | ### 贪心算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0455. 分发饼干](https://leetcode.cn/problems/assign-cookies/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/assign-cookies.md) | 贪心、数组、双指针、排序 | 简单 | | [0860. 柠檬水找零](https://leetcode.cn/problems/lemonade-change/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/lemonade-change.md) | 贪心、数组 | 简单 | | [0056. 合并区间](https://leetcode.cn/problems/merge-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-intervals.md) | 数组、排序 | 中等 | | [0435. 无重叠区间](https://leetcode.cn/problems/non-overlapping-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/non-overlapping-intervals.md) | 贪心、数组、动态规划、排序 | 中等 | | [0452. 用最少数量的箭引爆气球](https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-number-of-arrows-to-burst-balloons.md) | 贪心、数组、排序 | 中等 | | [0055. 跳跃游戏](https://leetcode.cn/problems/jump-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/jump-game.md) | 贪心、数组、动态规划 | 中等 | | [0045. 跳跃游戏 II](https://leetcode.cn/problems/jump-game-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/jump-game-ii.md) | 贪心、数组、动态规划 | 中等 | | [0122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-ii.md) | 贪心、数组、动态规划 | 中等 | | [0561. 数组拆分](https://leetcode.cn/problems/array-partition/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/array-partition.md) | 贪心、数组、计数排序、排序 | 简单 | | [1710. 卡车上的最大单元数](https://leetcode.cn/problems/maximum-units-on-a-truck/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/maximum-units-on-a-truck.md) | 贪心、数组、排序 | 简单 | | [1217. 玩筹码](https://leetcode.cn/problems/minimum-cost-to-move-chips-to-the-same-position/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/minimum-cost-to-move-chips-to-the-same-position.md) | 贪心、数组、数学 | 简单 | | [1247. 交换字符使得字符串相同](https://leetcode.cn/problems/minimum-swaps-to-make-strings-equal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/minimum-swaps-to-make-strings-equal.md) | 贪心、数学、字符串 | 中等 | | [1400. 构造 K 个回文字符串](https://leetcode.cn/problems/construct-k-palindrome-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/construct-k-palindrome-strings.md) | 贪心、哈希表、字符串、计数 | 中等 | | [0921. 使括号有效的最少添加](https://leetcode.cn/problems/minimum-add-to-make-parentheses-valid/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-add-to-make-parentheses-valid.md) | 栈、贪心、字符串 | 中等 | | [1029. 两地调度](https://leetcode.cn/problems/two-city-scheduling/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/two-city-scheduling.md) | 贪心、数组、排序 | 中等 | | [1605. 给定行和列的和求可行矩阵](https://leetcode.cn/problems/find-valid-matrix-given-row-and-column-sums/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/find-valid-matrix-given-row-and-column-sums.md) | 贪心、数组、矩阵 | 中等 | | [0135. 分发糖果](https://leetcode.cn/problems/candy/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/candy.md) | 贪心、数组 | 困难 | | [0134. 加油站](https://leetcode.cn/problems/gas-station/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/gas-station.md) | 贪心、数组 | 中等 | | [0053. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximum-subarray.md) | 数组、分治、动态规划 | 中等 | | [0376. 摆动序列](https://leetcode.cn/problems/wiggle-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/wiggle-subsequence.md) | 贪心、数组、动态规划 | 中等 | | [0738. 单调递增的数字](https://leetcode.cn/problems/monotone-increasing-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/monotone-increasing-digits.md) | 贪心、数学 | 中等 | | [0402. 移掉 K 位数字](https://leetcode.cn/problems/remove-k-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/remove-k-digits.md) | 栈、贪心、字符串、单调栈 | 中等 | | [0861. 翻转矩阵后的得分](https://leetcode.cn/problems/score-after-flipping-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/score-after-flipping-matrix.md) | 贪心、位运算、数组、矩阵 | 中等 | | [0670. 最大交换](https://leetcode.cn/problems/maximum-swap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-swap.md) | 贪心、数学 | 中等 | ### 位运算题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0504. 七进制数](https://leetcode.cn/problems/base-7/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/base-7.md) | 数学、字符串 | 简单 | | [0405. 数字转换为十六进制数](https://leetcode.cn/problems/convert-a-number-to-hexadecimal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/convert-a-number-to-hexadecimal.md) | 位运算、数学、字符串 | 简单 | | [0190. 颠倒二进制位](https://leetcode.cn/problems/reverse-bits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reverse-bits.md) | 位运算、分治 | 简单 | | [1009. 十进制整数的反码](https://leetcode.cn/problems/complement-of-base-10-integer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/complement-of-base-10-integer.md) | 位运算 | 简单 | | [0191. 位1的个数](https://leetcode.cn/problems/number-of-1-bits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/number-of-1-bits.md) | 位运算、分治 | 简单 | | [0371. 两整数之和](https://leetcode.cn/problems/sum-of-two-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sum-of-two-integers.md) | 位运算、数学 | 中等 | | [0089. 格雷编码](https://leetcode.cn/problems/gray-code/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/gray-code.md) | 位运算、数学、回溯 | 中等 | | [0201. 数字范围按位与](https://leetcode.cn/problems/bitwise-and-of-numbers-range/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/bitwise-and-of-numbers-range.md) | 位运算 | 中等 | | [0338. 比特位计数](https://leetcode.cn/problems/counting-bits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/counting-bits.md) | 位运算、动态规划 | 简单 | | [0136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) | 位运算、数组 | 简单 | | [0137. 只出现一次的数字 II](https://leetcode.cn/problems/single-number-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number-ii.md) | 位运算、数组 | 中等 | | [0260. 只出现一次的数字 III](https://leetcode.cn/problems/single-number-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/single-number-iii.md) | 位运算、数组 | 中等 | | [0268. 丢失的数字](https://leetcode.cn/problems/missing-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/missing-number.md) | 位运算、数组、哈希表、数学、二分查找、排序 | 简单 | | [1349. 参加考试的最大学生数](https://leetcode.cn/problems/maximum-students-taking-exam/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/maximum-students-taking-exam.md) | 位运算、数组、动态规划、状态压缩、矩阵 | 困难 | | [0645. 错误的集合](https://leetcode.cn/problems/set-mismatch/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/set-mismatch.md) | 位运算、数组、哈希表、排序 | 简单 | | [0078. 子集](https://leetcode.cn/problems/subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets.md) | 位运算、数组、回溯 | 中等 | | [0090. 子集 II](https://leetcode.cn/problems/subsets-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets-ii.md) | 位运算、数组、回溯 | 中等 | ## 第 8 章 动态规划 ### 动态规划基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0509. 斐波那契数](https://leetcode.cn/problems/fibonacci-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/fibonacci-number.md) | 递归、记忆化搜索、数学、动态规划 | 简单 | | [0070. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/climbing-stairs.md) | 记忆化搜索、数学、动态规划 | 简单 | | [0062. 不同路径](https://leetcode.cn/problems/unique-paths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths.md) | 数学、动态规划、组合数学 | 中等 | ### 记忆化搜索题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1137. 第 N 个泰波那契数](https://leetcode.cn/problems/n-th-tribonacci-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/n-th-tribonacci-number.md) | 记忆化搜索、数学、动态规划 | 简单 | | [0375. 猜数字大小 II](https://leetcode.cn/problems/guess-number-higher-or-lower-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/guess-number-higher-or-lower-ii.md) | 数学、动态规划、博弈 | 中等 | | [0494. 目标和](https://leetcode.cn/problems/target-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/target-sum.md) | 数组、动态规划、回溯 | 中等 | | [0576. 出界的路径数](https://leetcode.cn/problems/out-of-boundary-paths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/out-of-boundary-paths.md) | 动态规划 | 中等 | | [0087. 扰乱字符串](https://leetcode.cn/problems/scramble-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/scramble-string.md) | 字符串、动态规划 | 困难 | | [0403. 青蛙过河](https://leetcode.cn/problems/frog-jump/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/frog-jump.md) | 数组、动态规划 | 困难 | | [0552. 学生出勤记录 II](https://leetcode.cn/problems/student-attendance-record-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/student-attendance-record-ii.md) | 动态规划 | 困难 | | [0913. 猫和老鼠](https://leetcode.cn/problems/cat-and-mouse/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/cat-and-mouse.md) | 图、拓扑排序、记忆化搜索、数学、动态规划、博弈 | 困难 | | [0329. 矩阵中的最长递增路径](https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-path-in-a-matrix.md) | 深度优先搜索、广度优先搜索、图、拓扑排序、记忆化搜索、数组、动态规划、矩阵 | 困难 | ### 线性 DP 题目 #### 单串线性 DP 问题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-subsequence.md) | 数组、二分查找、动态规划 | 中等 | | [0673. 最长递增子序列的个数](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/number-of-longest-increasing-subsequence.md) | 树状数组、线段树、数组、动态规划 | 中等 | | [0354. 俄罗斯套娃信封问题](https://leetcode.cn/problems/russian-doll-envelopes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/russian-doll-envelopes.md) | 数组、二分查找、动态规划、排序 | 困难 | | [0053. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximum-subarray.md) | 数组、分治、动态规划 | 中等 | | [0152. 乘积最大子数组](https://leetcode.cn/problems/maximum-product-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-product-subarray.md) | 数组、动态规划 | 中等 | | [0918. 环形子数组的最大和](https://leetcode.cn/problems/maximum-sum-circular-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/maximum-sum-circular-subarray.md) | 队列、数组、分治、动态规划、单调队列 | 中等 | | [0198. 打家劫舍](https://leetcode.cn/problems/house-robber/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/house-robber.md) | 数组、动态规划 | 中等 | | [0213. 打家劫舍 II](https://leetcode.cn/problems/house-robber-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/house-robber-ii.md) | 数组、动态规划 | 中等 | | [0740. 删除并获得点数](https://leetcode.cn/problems/delete-and-earn/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/delete-and-earn.md) | 数组、哈希表、动态规划 | 中等 | | [1388. 3n 块披萨](https://leetcode.cn/problems/pizza-with-3n-slices/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/pizza-with-3n-slices.md) | 贪心、数组、动态规划、堆(优先队列) | 困难 | | [0873. 最长的斐波那契子序列的长度](https://leetcode.cn/problems/length-of-longest-fibonacci-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/length-of-longest-fibonacci-subsequence.md) | 数组、哈希表、动态规划 | 中等 | | [1027. 最长等差数列](https://leetcode.cn/problems/longest-arithmetic-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/longest-arithmetic-subsequence.md) | 数组、哈希表、二分查找、动态规划 | 中等 | | [1055. 形成字符串的最短路径](https://leetcode.cn/problems/shortest-way-to-form-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/shortest-way-to-form-string.md) | 贪心、双指针、字符串、二分查找 | 中等 | | [0368. 最大整除子集](https://leetcode.cn/problems/largest-divisible-subset/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/largest-divisible-subset.md) | 数组、数学、动态规划、排序 | 中等 | | [0032. 最长有效括号](https://leetcode.cn/problems/longest-valid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-valid-parentheses.md) | 栈、字符串、动态规划 | 困难 | | [0413. 等差数列划分](https://leetcode.cn/problems/arithmetic-slices/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/arithmetic-slices.md) | 数组、动态规划、滑动窗口 | 中等 | | [0091. 解码方法](https://leetcode.cn/problems/decode-ways/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/decode-ways.md) | 字符串、动态规划 | 中等 | | [0639. 解码方法 II](https://leetcode.cn/problems/decode-ways-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/decode-ways-ii.md) | 字符串、动态规划 | 困难 | | [0132. 分割回文串 II](https://leetcode.cn/problems/palindrome-partitioning-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/palindrome-partitioning-ii.md) | 字符串、动态规划 | 困难 | | [1220. 统计元音字母序列的数目](https://leetcode.cn/problems/count-vowels-permutation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/count-vowels-permutation.md) | 动态规划 | 困难 | | [0338. 比特位计数](https://leetcode.cn/problems/counting-bits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/counting-bits.md) | 位运算、动态规划 | 简单 | | [0801. 使序列递增的最小交换次数](https://leetcode.cn/problems/minimum-swaps-to-make-sequences-increasing/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/minimum-swaps-to-make-sequences-increasing.md) | 数组、动态规划 | 困难 | | [0871. 最低加油次数](https://leetcode.cn/problems/minimum-number-of-refueling-stops/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/minimum-number-of-refueling-stops.md) | 贪心、数组、动态规划、堆(优先队列) | 困难 | | [0045. 跳跃游戏 II](https://leetcode.cn/problems/jump-game-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/jump-game-ii.md) | 贪心、数组、动态规划 | 中等 | | [0813. 最大平均值和的分组](https://leetcode.cn/problems/largest-sum-of-averages/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/largest-sum-of-averages.md) | 数组、动态规划、前缀和 | 中等 | | [0887. 鸡蛋掉落](https://leetcode.cn/problems/super-egg-drop/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/super-egg-drop.md) | 数学、二分查找、动态规划 | 困难 | | [0256. 粉刷房子](https://leetcode.cn/problems/paint-house/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/paint-house.md) | 数组、动态规划 | 中等 | | [0265. 粉刷房子 II](https://leetcode.cn/problems/paint-house-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/paint-house-ii.md) | 数组、动态规划 | 困难 | | [1473. 粉刷房子 III](https://leetcode.cn/problems/paint-house-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/paint-house-iii.md) | 数组、动态规划 | 困难 | | [0975. 奇偶跳](https://leetcode.cn/problems/odd-even-jump/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/odd-even-jump.md) | 栈、数组、动态规划、有序集合、排序、单调栈 | 困难 | | [0403. 青蛙过河](https://leetcode.cn/problems/frog-jump/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/frog-jump.md) | 数组、动态规划 | 困难 | | [1478. 安排邮筒](https://leetcode.cn/problems/allocate-mailboxes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/allocate-mailboxes.md) | 数组、数学、动态规划、排序 | 困难 | | [1230. 抛掷硬币](https://leetcode.cn/problems/toss-strange-coins/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/toss-strange-coins.md) | 数组、数学、动态规划、概率与统计 | 中等 | | [0410. 分割数组的最大值](https://leetcode.cn/problems/split-array-largest-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/split-array-largest-sum.md) | 贪心、数组、二分查找、动态规划、前缀和 | 困难 | | [1751. 最多可以参加的会议数目 II](https://leetcode.cn/problems/maximum-number-of-events-that-can-be-attended-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/maximum-number-of-events-that-can-be-attended-ii.md) | 数组、二分查找、动态规划、排序 | 困难 | | [1787. 使所有区间的异或结果为零](https://leetcode.cn/problems/make-the-xor-of-all-segments-equal-to-zero/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/make-the-xor-of-all-segments-equal-to-zero.md) | 位运算、数组、动态规划 | 困难 | | [0121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock.md) | 数组、动态规划 | 简单 | | [0122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-ii.md) | 贪心、数组、动态规划 | 中等 | | [0123. 买卖股票的最佳时机 III](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-iii.md) | 数组、动态规划 | 困难 | | [0188. 买卖股票的最佳时机 IV](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-iv.md) | 数组、动态规划 | 困难 | | [0309. 买卖股票的最佳时机含冷冻期](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/best-time-to-buy-and-sell-stock-with-cooldown.md) | 数组、动态规划 | 中等 | | [0714. 买卖股票的最佳时机含手续费](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/best-time-to-buy-and-sell-stock-with-transaction-fee.md) | 贪心、数组、动态规划 | 中等 | #### 双串线性 DP 问题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/longest-common-subsequence.md) | 字符串、动态规划 | 中等 | | [0712. 两个字符串的最小ASCII删除和](https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/minimum-ascii-delete-sum-for-two-strings.md) | 字符串、动态规划 | 中等 | | [0718. 最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/maximum-length-of-repeated-subarray.md) | 数组、二分查找、动态规划、滑动窗口、哈希函数、滚动哈希 | 中等 | | [0583. 两个字符串的删除操作](https://leetcode.cn/problems/delete-operation-for-two-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/delete-operation-for-two-strings.md) | 字符串、动态规划 | 中等 | | [0072. 编辑距离](https://leetcode.cn/problems/edit-distance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/edit-distance.md) | 字符串、动态规划 | 中等 | | [0044. 通配符匹配](https://leetcode.cn/problems/wildcard-matching/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/wildcard-matching.md) | 贪心、递归、字符串、动态规划 | 困难 | | [0010. 正则表达式匹配](https://leetcode.cn/problems/regular-expression-matching/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/regular-expression-matching.md) | 递归、字符串、动态规划 | 困难 | | [0097. 交错字符串](https://leetcode.cn/problems/interleaving-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/interleaving-string.md) | 字符串、动态规划 | 中等 | | [0115. 不同的子序列](https://leetcode.cn/problems/distinct-subsequences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/distinct-subsequences.md) | 字符串、动态规划 | 困难 | | [0087. 扰乱字符串](https://leetcode.cn/problems/scramble-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/scramble-string.md) | 字符串、动态规划 | 困难 | #### 矩阵线性 DP 问题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0118. 杨辉三角](https://leetcode.cn/problems/pascals-triangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/pascals-triangle.md) | 数组、动态规划 | 简单 | | [0119. 杨辉三角 II](https://leetcode.cn/problems/pascals-triangle-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/pascals-triangle-ii.md) | 数组、动态规划 | 简单 | | [0120. 三角形最小路径和](https://leetcode.cn/problems/triangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/triangle.md) | 数组、动态规划 | 中等 | | [0064. 最小路径和](https://leetcode.cn/problems/minimum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/minimum-path-sum.md) | 数组、动态规划、矩阵 | 中等 | | [0174. 地下城游戏](https://leetcode.cn/problems/dungeon-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/dungeon-game.md) | 数组、动态规划、矩阵 | 困难 | | [0221. 最大正方形](https://leetcode.cn/problems/maximal-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/maximal-square.md) | 数组、动态规划、矩阵 | 中等 | | [0931. 下降路径最小和](https://leetcode.cn/problems/minimum-falling-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-falling-path-sum.md) | 数组、动态规划、矩阵 | 中等 | | [0576. 出界的路径数](https://leetcode.cn/problems/out-of-boundary-paths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/out-of-boundary-paths.md) | 动态规划 | 中等 | | [0085. 最大矩形](https://leetcode.cn/problems/maximal-rectangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximal-rectangle.md) | 栈、数组、动态规划、矩阵、单调栈 | 困难 | | [0363. 矩形区域不超过 K 的最大数值和](https://leetcode.cn/problems/max-sum-of-rectangle-no-larger-than-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/max-sum-of-rectangle-no-larger-than-k.md) | 数组、二分查找、矩阵、有序集合、前缀和 | 困难 | | [面试题 17.24. 最大子矩阵](https://leetcode.cn/problems/max-submatrix-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/max-submatrix-lcci.md) | 数组、动态规划、矩阵、前缀和 | 困难 | | [1444. 切披萨的方案数](https://leetcode.cn/problems/number-of-ways-of-cutting-a-pizza/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/number-of-ways-of-cutting-a-pizza.md) | 记忆化搜索、数组、动态规划、矩阵、前缀和 | 困难 | #### 无串线性 DP 问题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1137. 第 N 个泰波那契数](https://leetcode.cn/problems/n-th-tribonacci-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/n-th-tribonacci-number.md) | 记忆化搜索、数学、动态规划 | 简单 | | [0650. 两个键的键盘](https://leetcode.cn/problems/2-keys-keyboard/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/2-keys-keyboard.md) | 数学、动态规划 | 中等 | | [0264. 丑数 II](https://leetcode.cn/problems/ugly-number-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/ugly-number-ii.md) | 哈希表、数学、动态规划、堆(优先队列) | 中等 | | [0279. 完全平方数](https://leetcode.cn/problems/perfect-squares/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/perfect-squares.md) | 广度优先搜索、数学、动态规划 | 中等 | | [0343. 整数拆分](https://leetcode.cn/problems/integer-break/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/integer-break.md) | 数学、动态规划 | 中等 | ### 背包问题题目 #### 0-1 背包问题题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/partition-equal-subset-sum.md) | 数组、动态规划 | 中等 | | [0494. 目标和](https://leetcode.cn/problems/target-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/target-sum.md) | 数组、动态规划、回溯 | 中等 | | [1049. 最后一块石头的重量 II](https://leetcode.cn/problems/last-stone-weight-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/last-stone-weight-ii.md) | 数组、动态规划 | 中等 | #### 完全背包问题题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0279. 完全平方数](https://leetcode.cn/problems/perfect-squares/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/perfect-squares.md) | 广度优先搜索、数学、动态规划 | 中等 | | [0322. 零钱兑换](https://leetcode.cn/problems/coin-change/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) | 广度优先搜索、数组、动态规划 | 中等 | | [0518. 零钱兑换 II](https://leetcode.cn/problems/coin-change-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/coin-change-ii.md) | 数组、动态规划 | 中等 | | [0139. 单词拆分](https://leetcode.cn/problems/word-break/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/word-break.md) | 字典树、记忆化搜索、数组、哈希表、字符串、动态规划 | 中等 | | [0377. 组合总和 Ⅳ](https://leetcode.cn/problems/combination-sum-iv/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/combination-sum-iv.md) | 数组、动态规划 | 中等 | | [0638. 大礼包](https://leetcode.cn/problems/shopping-offers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/shopping-offers.md) | 位运算、记忆化搜索、数组、动态规划、回溯、状态压缩 | 中等 | | [1449. 数位成本和为目标值的最大数字](https://leetcode.cn/problems/form-largest-integer-with-digits-that-add-up-to-target/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/form-largest-integer-with-digits-that-add-up-to-target.md) | 数组、动态规划 | 困难 | #### 多重背包问题题目 #### 分组背包问题题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1155. 掷骰子等于目标和的方法数](https://leetcode.cn/problems/number-of-dice-rolls-with-target-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/number-of-dice-rolls-with-target-sum.md) | 动态规划 | 中等 | | [2585. 获得分数的方法数](https://leetcode.cn/problems/number-of-ways-to-earn-points/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2500-2599/number-of-ways-to-earn-points.md) | 数组、动态规划 | 困难 | #### 多维背包问题题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0474. 一和零](https://leetcode.cn/problems/ones-and-zeroes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/ones-and-zeroes.md) | 数组、字符串、动态规划 | 中等 | | [0879. 盈利计划](https://leetcode.cn/problems/profitable-schemes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/profitable-schemes.md) | 数组、动态规划 | 困难 | | [1995. 统计特殊四元组](https://leetcode.cn/problems/count-special-quadruplets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/count-special-quadruplets.md) | 数组、哈希表、枚举 | 简单 | ### 区间 DP 题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0486. 预测赢家](https://leetcode.cn/problems/predict-the-winner/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/predict-the-winner.md) | 递归、数组、数学、动态规划、博弈 | 中等 | | [0312. 戳气球](https://leetcode.cn/problems/burst-balloons/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/burst-balloons.md) | 数组、动态规划 | 困难 | | [0877. 石子游戏](https://leetcode.cn/problems/stone-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/stone-game.md) | 数组、数学、动态规划、博弈 | 中等 | | [1000. 合并石头的最低成本](https://leetcode.cn/problems/minimum-cost-to-merge-stones/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/minimum-cost-to-merge-stones.md) | 数组、动态规划、前缀和 | 困难 | | [1547. 切棍子的最小成本](https://leetcode.cn/problems/minimum-cost-to-cut-a-stick/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-cost-to-cut-a-stick.md) | 数组、动态规划、排序 | 困难 | | [0664. 奇怪的打印机](https://leetcode.cn/problems/strange-printer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/strange-printer.md) | 字符串、动态规划 | 困难 | | [1039. 多边形三角剖分的最低得分](https://leetcode.cn/problems/minimum-score-triangulation-of-polygon/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/minimum-score-triangulation-of-polygon.md) | 数组、动态规划 | 中等 | | [0546. 移除盒子](https://leetcode.cn/problems/remove-boxes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/remove-boxes.md) | 记忆化搜索、数组、动态规划 | 困难 | | [0375. 猜数字大小 II](https://leetcode.cn/problems/guess-number-higher-or-lower-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/guess-number-higher-or-lower-ii.md) | 数学、动态规划、博弈 | 中等 | | [0678. 有效的括号字符串](https://leetcode.cn/problems/valid-parenthesis-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/valid-parenthesis-string.md) | 栈、贪心、字符串、动态规划 | 中等 | | [0005. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-palindromic-substring.md) | 双指针、字符串、动态规划 | 中等 | | [0516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-palindromic-subsequence.md) | 字符串、动态规划 | 中等 | | [0730. 统计不同回文子序列](https://leetcode.cn/problems/count-different-palindromic-subsequences/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/count-different-palindromic-subsequences.md) | 字符串、动态规划 | 困难 | | [2104. 子数组范围和](https://leetcode.cn/problems/sum-of-subarray-ranges/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/sum-of-subarray-ranges.md) | 栈、数组、单调栈 | 中等 | ### 树形 DP 题目 #### 固定根的树形 DP 题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0543. 二叉树的直径](https://leetcode.cn/problems/diameter-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/diameter-of-binary-tree.md) | 树、深度优先搜索、二叉树 | 简单 | | [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [1245. 树的直径](https://leetcode.cn/problems/tree-diameter/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/tree-diameter.md) | 树、深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [2246. 相邻字符不同的最长路径](https://leetcode.cn/problems/longest-path-with-different-adjacent-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/longest-path-with-different-adjacent-characters.md) | 树、深度优先搜索、图、拓扑排序、数组、字符串 | 困难 | | [0687. 最长同值路径](https://leetcode.cn/problems/longest-univalue-path/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/longest-univalue-path.md) | 树、深度优先搜索、二叉树 | 中等 | | [0337. 打家劫舍 III](https://leetcode.cn/problems/house-robber-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/house-robber-iii.md) | 树、深度优先搜索、动态规划、二叉树 | 中等 | | [0333. 最大二叉搜索子树](https://leetcode.cn/problems/largest-bst-subtree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/largest-bst-subtree.md) | 树、深度优先搜索、二叉搜索树、动态规划、二叉树 | 中等 | | [1617. 统计子树中城市之间最大距离](https://leetcode.cn/problems/count-subtrees-with-max-distance-between-cities/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/count-subtrees-with-max-distance-between-cities.md) | 位运算、树、动态规划、状态压缩、枚举 | 困难 | | [2538. 最大价值和与最小价值和的差值](https://leetcode.cn/problems/difference-between-maximum-and-minimum-price-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2500-2599/difference-between-maximum-and-minimum-price-sum.md) | 树、深度优先搜索、数组、动态规划 | 困难 | | [1569. 将子数组重新排序得到同一个二叉搜索树的方案数](https://leetcode.cn/problems/number-of-ways-to-reorder-array-to-get-same-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/number-of-ways-to-reorder-array-to-get-same-bst.md) | 树、并查集、二叉搜索树、记忆化搜索、数组、数学、分治、动态规划、二叉树、组合数学 | 困难 | | [1372. 二叉树中的最长交错路径](https://leetcode.cn/problems/longest-zigzag-path-in-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/longest-zigzag-path-in-a-binary-tree.md) | 树、深度优先搜索、动态规划、二叉树 | 中等 | | [1373. 二叉搜索子树的最大键值和](https://leetcode.cn/problems/maximum-sum-bst-in-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/maximum-sum-bst-in-binary-tree.md) | 树、深度优先搜索、二叉搜索树、动态规划、二叉树 | 困难 | | [0968. 监控二叉树](https://leetcode.cn/problems/binary-tree-cameras/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/binary-tree-cameras.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [1273. 删除树节点](https://leetcode.cn/problems/delete-tree-nodes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/delete-tree-nodes.md) | 树、深度优先搜索、广度优先搜索、数组 | 中等 | | [1519. 子树中标签相同的节点数](https://leetcode.cn/problems/number-of-nodes-in-the-sub-tree-with-the-same-label/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/number-of-nodes-in-the-sub-tree-with-the-same-label.md) | 树、深度优先搜索、广度优先搜索、哈希表、计数 | 中等 | #### 不定根的树形 DP 题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0310. 最小高度树](https://leetcode.cn/problems/minimum-height-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/minimum-height-trees.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [0834. 树中距离之和](https://leetcode.cn/problems/sum-of-distances-in-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/sum-of-distances-in-tree.md) | 树、深度优先搜索、图、动态规划 | 困难 | | [2581. 统计可能的树根数目](https://leetcode.cn/problems/count-number-of-possible-root-nodes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2500-2599/count-number-of-possible-root-nodes.md) | 树、深度优先搜索、数组、哈希表、动态规划 | 困难 | ### 状态压缩 DP 题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1879. 两个数组最小的异或值之和](https://leetcode.cn/problems/minimum-xor-sum-of-two-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/minimum-xor-sum-of-two-arrays.md) | 位运算、数组、动态规划、状态压缩 | 困难 | | [2172. 数组的最大与和](https://leetcode.cn/problems/maximum-and-sum-of-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/maximum-and-sum-of-array.md) | 位运算、数组、动态规划、状态压缩 | 困难 | | [1947. 最大兼容性评分和](https://leetcode.cn/problems/maximum-compatibility-score-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/maximum-compatibility-score-sum.md) | 位运算、数组、动态规划、回溯、状态压缩 | 中等 | | [1595. 连通两组点的最小成本](https://leetcode.cn/problems/minimum-cost-to-connect-two-groups-of-points/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-cost-to-connect-two-groups-of-points.md) | 位运算、数组、动态规划、状态压缩、矩阵 | 困难 | | [1494. 并行课程 II](https://leetcode.cn/problems/parallel-courses-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/parallel-courses-ii.md) | 位运算、图、动态规划、状态压缩 | 困难 | | [1655. 分配重复整数](https://leetcode.cn/problems/distribute-repeating-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/distribute-repeating-integers.md) | 位运算、数组、动态规划、回溯、状态压缩 | 困难 | | [1986. 完成任务的最少工作时间段](https://leetcode.cn/problems/minimum-number-of-work-sessions-to-finish-the-tasks/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/minimum-number-of-work-sessions-to-finish-the-tasks.md) | 位运算、数组、动态规划、回溯、状态压缩 | 中等 | | [1434. 每个人戴不同帽子的方案数](https://leetcode.cn/problems/number-of-ways-to-wear-different-hats-to-each-other/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/number-of-ways-to-wear-different-hats-to-each-other.md) | 位运算、数组、动态规划、状态压缩 | 困难 | | [1799. N 次操作后的最大分数和](https://leetcode.cn/problems/maximize-score-after-n-operations/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/maximize-score-after-n-operations.md) | 位运算、数组、数学、动态规划、回溯、状态压缩、数论 | 困难 | | [1681. 最小不兼容性](https://leetcode.cn/problems/minimum-incompatibility/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/minimum-incompatibility.md) | 位运算、数组、动态规划、状态压缩 | 困难 | | [0526. 优美的排列](https://leetcode.cn/problems/beautiful-arrangement/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/beautiful-arrangement.md) | 位运算、数组、动态规划、回溯、状态压缩 | 中等 | | [0351. 安卓系统手势解锁](https://leetcode.cn/problems/android-unlock-patterns/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/android-unlock-patterns.md) | 位运算、动态规划、回溯、状态压缩 | 中等 | | [0464. 我能赢吗](https://leetcode.cn/problems/can-i-win/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/can-i-win.md) | 位运算、记忆化搜索、数学、动态规划、状态压缩、博弈 | 中等 | | [0847. 访问所有节点的最短路径](https://leetcode.cn/problems/shortest-path-visiting-all-nodes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-path-visiting-all-nodes.md) | 位运算、广度优先搜索、图、动态规划、状态压缩 | 困难 | | [0638. 大礼包](https://leetcode.cn/problems/shopping-offers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/shopping-offers.md) | 位运算、记忆化搜索、数组、动态规划、回溯、状态压缩 | 中等 | | [1994. 好子集的数目](https://leetcode.cn/problems/the-number-of-good-subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/the-number-of-good-subsets.md) | 位运算、数组、哈希表、数学、动态规划、状态压缩、计数、数论 | 困难 | | [1349. 参加考试的最大学生数](https://leetcode.cn/problems/maximum-students-taking-exam/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/maximum-students-taking-exam.md) | 位运算、数组、动态规划、状态压缩、矩阵 | 困难 | | [0698. 划分为k个相等的子集](https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/partition-to-k-equal-sum-subsets.md) | 位运算、记忆化搜索、数组、动态规划、回溯、状态压缩 | 中等 | | [0943. 最短超级串](https://leetcode.cn/problems/find-the-shortest-superstring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/find-the-shortest-superstring.md) | 位运算、数组、字符串、动态规划、状态压缩 | 困难 | | [0691. 贴纸拼词](https://leetcode.cn/problems/stickers-to-spell-word/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/stickers-to-spell-word.md) | 位运算、记忆化搜索、数组、哈希表、字符串、动态规划、回溯、状态压缩 | 困难 | | [0982. 按位与为零的三元组](https://leetcode.cn/problems/triples-with-bitwise-and-equal-to-zero/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/triples-with-bitwise-and-equal-to-zero.md) | 位运算、数组、哈希表 | 困难 | ### 计数 DP 题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0062. 不同路径](https://leetcode.cn/problems/unique-paths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths.md) | 数学、动态规划、组合数学 | 中等 | | [0063. 不同路径 II](https://leetcode.cn/problems/unique-paths-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths-ii.md) | 数组、动态规划、矩阵 | 中等 | | [0343. 整数拆分](https://leetcode.cn/problems/integer-break/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/integer-break.md) | 数学、动态规划 | 中等 | | [0096. 不同的二叉搜索树](https://leetcode.cn/problems/unique-binary-search-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-binary-search-trees.md) | 树、二叉搜索树、数学、动态规划、二叉树 | 中等 | | [1259. 不相交的握手](https://leetcode.cn/problems/handshakes-that-dont-cross/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/handshakes-that-dont-cross.md) | 数学、动态规划 | 困难 | | [0790. 多米诺和托米诺平铺](https://leetcode.cn/problems/domino-and-tromino-tiling/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/domino-and-tromino-tiling.md) | 动态规划 | 中等 | | [0070. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/climbing-stairs.md) | 记忆化搜索、数学、动态规划 | 简单 | | [0746. 使用最小花费爬楼梯](https://leetcode.cn/problems/min-cost-climbing-stairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/min-cost-climbing-stairs.md) | 数组、动态规划 | 简单 | | [0509. 斐波那契数](https://leetcode.cn/problems/fibonacci-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/fibonacci-number.md) | 递归、记忆化搜索、数学、动态规划 | 简单 | | [1137. 第 N 个泰波那契数](https://leetcode.cn/problems/n-th-tribonacci-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/n-th-tribonacci-number.md) | 记忆化搜索、数学、动态规划 | 简单 | ### 数位 DP 题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [2376. 统计特殊整数](https://leetcode.cn/problems/count-special-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2300-2399/count-special-integers.md) | 数学、动态规划 | 困难 | | [0357. 统计各位数字都不同的数字个数](https://leetcode.cn/problems/count-numbers-with-unique-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-numbers-with-unique-digits.md) | 数学、动态规划、回溯 | 中等 | | [1012. 至少有 1 位重复的数字](https://leetcode.cn/problems/numbers-with-repeated-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/numbers-with-repeated-digits.md) | 数学、动态规划 | 困难 | | [0902. 最大为 N 的数字组合](https://leetcode.cn/problems/numbers-at-most-n-given-digit-set/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/numbers-at-most-n-given-digit-set.md) | 数组、数学、字符串、二分查找、动态规划 | 困难 | | [0788. 旋转数字](https://leetcode.cn/problems/rotated-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotated-digits.md) | 数学、动态规划 | 中等 | | [0600. 不含连续1的非负整数](https://leetcode.cn/problems/non-negative-integers-without-consecutive-ones/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/non-negative-integers-without-consecutive-ones.md) | 动态规划 | 困难 | | [0233. 数字 1 的个数](https://leetcode.cn/problems/number-of-digit-one/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-digit-one.md) | 递归、数学、动态规划 | 困难 | | [2719. 统计整数数目](https://leetcode.cn/problems/count-of-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2700-2799/count-of-integers.md) | 数学、字符串、动态规划 | 困难 | | [0248. 中心对称数 III](https://leetcode.cn/problems/strobogrammatic-number-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/strobogrammatic-number-iii.md) | 递归、数组、字符串 | 困难 | | [1088. 易混淆数 II](https://leetcode.cn/problems/confusing-number-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/confusing-number-ii.md) | 数学、回溯 | 困难 | | [1067. 范围内的数字计数](https://leetcode.cn/problems/digit-count-in-range/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/digit-count-in-range.md) | 数学、动态规划 | 困难 | | [1742. 盒子中小球的最大数量](https://leetcode.cn/problems/maximum-number-of-balls-in-a-box/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/maximum-number-of-balls-in-a-box.md) | 哈希表、数学、计数 | 简单 | | [面试题 17.06. 2出现的次数](https://leetcode.cn/problems/number-of-2s-in-range-lcci/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/number-of-2s-in-range-lcci.md) | 递归、数学、动态规划 | 困难 | ### 概率 DP 题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0688. 骑士在棋盘上的概率](https://leetcode.cn/problems/knight-probability-in-chessboard/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/knight-probability-in-chessboard.md) | 动态规划 | 中等 | | [0808. 分汤](https://leetcode.cn/problems/soup-servings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/soup-servings.md) | 数学、动态规划、概率与统计 | 中等 | | [0837. 新 21 点](https://leetcode.cn/problems/new-21-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/new-21-game.md) | 数学、动态规划、滑动窗口、概率与统计 | 中等 | | [1230. 抛掷硬币](https://leetcode.cn/problems/toss-strange-coins/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/toss-strange-coins.md) | 数组、数学、动态规划、概率与统计 | 中等 | | [1467. 两个盒子中球的颜色数相同的概率](https://leetcode.cn/problems/probability-of-a-two-boxes-having-the-same-number-of-distinct-balls/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/probability-of-a-two-boxes-having-the-same-number-of-distinct-balls.md) | 数组、数学、动态规划、回溯、组合数学、概率与统计 | 困难 | | [1227. 飞机座位分配概率](https://leetcode.cn/problems/airplane-seat-assignment-probability/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/airplane-seat-assignment-probability.md) | 脑筋急转弯、数学、动态规划、概率与统计 | 中等 | | [1377. T 秒后青蛙的位置](https://leetcode.cn/problems/frog-position-after-t-seconds/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/frog-position-after-t-seconds.md) | 树、深度优先搜索、广度优先搜索、图 | 困难 | | [LCR 185. 统计结果概率](https://leetcode.cn/problems/nge-tou-zi-de-dian-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/nge-tou-zi-de-dian-shu-lcof.md) | 数学、动态规划、概率与统计 | 中等 | ================================================ FILE: docs/00_preface/00_07_interview_100_list.md ================================================ # LeetCode 面试最常考 100 题(按分类排序) ## 第 1 章 数组 ### 数组基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0054. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/spiral-matrix.md) | 数组、矩阵、模拟 | 中等 | | [0048. 旋转图像](https://leetcode.cn/problems/rotate-image/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/rotate-image.md) | 数组、数学、矩阵 | 中等 | ### 排序算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0912. 排序数组](https://leetcode.cn/problems/sort-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) | 数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序 | 中等 | | [0215. 数组中的第K个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/kth-largest-element-in-an-array.md) | 数组、分治、快速选择、排序、堆(优先队列) | 中等 | | [0088. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-sorted-array.md) | 数组、双指针、排序 | 简单 | | [0169. 多数元素](https://leetcode.cn/problems/majority-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/majority-element.md) | 数组、哈希表、分治、计数、排序 | 简单 | | [0136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) | 位运算、数组 | 简单 | | [0056. 合并区间](https://leetcode.cn/problems/merge-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-intervals.md) | 数组、排序 | 中等 | | [0179. 最大数](https://leetcode.cn/problems/largest-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/largest-number.md) | 贪心、数组、字符串、排序 | 中等 | ### 二分查找题目 #### 二分下标题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0704. 二分查找](https://leetcode.cn/problems/binary-search/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/binary-search.md) | 数组、二分查找 | 简单 | | [0034. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-first-and-last-position-of-element-in-sorted-array.md) | 数组、二分查找 | 中等 | | [0153. 寻找旋转排序数组中的最小值](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array.md) | 数组、二分查找 | 中等 | | [0033. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-in-rotated-sorted-array.md) | 数组、二分查找 | 中等 | | [0162. 寻找峰值](https://leetcode.cn/problems/find-peak-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-peak-element.md) | 数组、二分查找 | 中等 | | [0004. 寻找两个正序数组的中位数](https://leetcode.cn/problems/median-of-two-sorted-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/median-of-two-sorted-arrays.md) | 数组、二分查找、分治 | 困难 | | [0240. 搜索二维矩阵 II](https://leetcode.cn/problems/search-a-2d-matrix-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/search-a-2d-matrix-ii.md) | 数组、二分查找、分治、矩阵 | 中等 | #### 二分答案题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0069. x 的平方根](https://leetcode.cn/problems/sqrtx/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sqrtx.md) | 数学、二分查找 | 简单 | ### 双指针题目 #### 对撞指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0015. 三数之和](https://leetcode.cn/problems/3sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum.md) | 数组、双指针、排序 | 中等 | #### 快慢指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0283. 移动零](https://leetcode.cn/problems/move-zeroes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/move-zeroes.md) | 数组、双指针 | 简单 | | [0088. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-sorted-array.md) | 数组、双指针、排序 | 简单 | #### 分离双指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0415. 字符串相加](https://leetcode.cn/problems/add-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-strings.md) | 数学、字符串、模拟 | 简单 | ### 滑动窗口题目 #### 固定长度窗口题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/sliding-window-maximum.md) | 队列、数组、滑动窗口、单调队列、堆(优先队列) | 困难 | #### 不定长度窗口题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0003. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-substring-without-repeating-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0076. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/minimum-window-substring.md) | 哈希表、字符串、滑动窗口 | 困难 | | [0718. 最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/maximum-length-of-repeated-subarray.md) | 数组、二分查找、动态规划、滑动窗口、哈希函数、滚动哈希 | 中等 | ## 第 2 章 链表 ### 链表基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0083. 删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-list.md) | 链表 | 简单 | | [0082. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-list-ii.md) | 链表、双指针 | 中等 | | [0206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/reverse-linked-list.md) | 递归、链表 | 简单 | | [0092. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-linked-list-ii.md) | 链表 | 中等 | | [0025. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-nodes-in-k-group.md) | 递归、链表 | 困难 | | [0234. 回文链表](https://leetcode.cn/problems/palindrome-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/palindrome-linked-list.md) | 栈、递归、链表、双指针 | 简单 | ### 链表排序题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0148. 排序链表](https://leetcode.cn/problems/sort-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md) | 链表、双指针、分治、排序、归并排序 | 中等 | | [0021. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-two-sorted-lists.md) | 递归、链表 | 简单 | | [0023. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) | 链表、分治、堆(优先队列)、归并排序 | 困难 | ### 链表双指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle.md) | 哈希表、链表、双指针 | 简单 | | [0142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle-ii.md) | 哈希表、链表、双指针 | 中等 | | [0160. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/intersection-of-two-linked-lists.md) | 哈希表、链表、双指针 | 简单 | | [0019. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-nth-node-from-end-of-list.md) | 链表、双指针 | 中等 | | [LCR 140. 训练计划 II](https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof.md) | 链表、双指针 | 简单 | | [0143. 重排链表](https://leetcode.cn/problems/reorder-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reorder-list.md) | 栈、递归、链表、双指针 | 中等 | | [0002. 两数相加](https://leetcode.cn/problems/add-two-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/add-two-numbers.md) | 递归、链表、数学 | 中等 | ## 第 3 章 栈、队列、哈希表 ### 栈基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0155. 最小栈](https://leetcode.cn/problems/min-stack/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/min-stack.md) | 栈、设计 | 中等 | | [0020. 有效的括号](https://leetcode.cn/problems/valid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-parentheses.md) | 栈、字符串 | 简单 | | [0227. 基本计算器 II](https://leetcode.cn/problems/basic-calculator-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/basic-calculator-ii.md) | 栈、数学、字符串 | 中等 | | [0232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-queue-using-stacks.md) | 栈、设计、队列 | 简单 | | [0394. 字符串解码](https://leetcode.cn/problems/decode-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/decode-string.md) | 栈、递归、字符串 | 中等 | | [0032. 最长有效括号](https://leetcode.cn/problems/longest-valid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-valid-parentheses.md) | 栈、字符串、动态规划 | 困难 | ### 单调栈题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0042. 接雨水](https://leetcode.cn/problems/trapping-rain-water/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/trapping-rain-water.md) | 栈、数组、双指针、动态规划、单调栈 | 困难 | ### 队列基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0225. 用队列实现栈](https://leetcode.cn/problems/implement-stack-using-queues/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-stack-using-queues.md) | 栈、设计、队列 | 简单 | ### 优先队列题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0023. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) | 链表、分治、堆(优先队列)、归并排序 | 困难 | | [0239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/sliding-window-maximum.md) | 队列、数组、滑动窗口、单调队列、堆(优先队列) | 困难 | ### 哈希表题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0001. 两数之和](https://leetcode.cn/problems/two-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/two-sum.md) | 数组、哈希表 | 简单 | | [0015. 三数之和](https://leetcode.cn/problems/3sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum.md) | 数组、双指针、排序 | 中等 | | [0041. 缺失的第一个正数](https://leetcode.cn/problems/first-missing-positive/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/first-missing-positive.md) | 数组、哈希表 | 困难 | | [0128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/longest-consecutive-sequence.md) | 并查集、数组、哈希表 | 中等 | | [0136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) | 位运算、数组 | 简单 | ## 第 4 章 字符串 ### 字符串基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0003. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-substring-without-repeating-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0005. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-palindromic-substring.md) | 双指针、字符串、动态规划 | 中等 | | [0415. 字符串相加](https://leetcode.cn/problems/add-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-strings.md) | 数学、字符串、模拟 | 简单 | | [0151. 反转字符串中的单词](https://leetcode.cn/problems/reverse-words-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reverse-words-in-a-string.md) | 双指针、字符串 | 中等 | | [0043. 字符串相乘](https://leetcode.cn/problems/multiply-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/multiply-strings.md) | 数学、字符串、模拟 | 中等 | | [0014. 最长公共前缀](https://leetcode.cn/problems/longest-common-prefix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-common-prefix.md) | 字典树、数组、字符串 | 简单 | ## 第 5 章 树 ### 二叉树的遍历题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-preorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0094. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/binary-tree-inorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-level-order-traversal.md) | 树、广度优先搜索、二叉树 | 中等 | | [0103. 二叉树的锯齿形层序遍历](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-zigzag-level-order-traversal.md) | 树、广度优先搜索、二叉树 | 中等 | | [0236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/lowest-common-ancestor-of-a-binary-tree.md) | 树、深度优先搜索、二叉树 | 中等 | | [0104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0112. 路径总和](https://leetcode.cn/problems/path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/path-sum.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0113. 路径总和 II](https://leetcode.cn/problems/path-sum-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/path-sum-ii.md) | 树、深度优先搜索、回溯、二叉树 | 中等 | | [0101. 对称二叉树](https://leetcode.cn/problems/symmetric-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/symmetric-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [0199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-right-side-view.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/invert-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | ### 二叉树的还原题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/construct-binary-tree-from-preorder-and-inorder-traversal.md) | 树、数组、哈希表、分治、二叉树 | 中等 | ### 二叉搜索树题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0098. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/validate-binary-search-tree.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0110. 平衡二叉树](https://leetcode.cn/problems/balanced-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/balanced-binary-tree.md) | 树、深度优先搜索、二叉树 | 简单 | ### 并查集题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/longest-consecutive-sequence.md) | 并查集、数组、哈希表 | 中等 | ## 第 6 章 图论 ### 深度优先搜索题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-islands.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0695. 岛屿的最大面积](https://leetcode.cn/problems/max-area-of-island/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/max-area-of-island.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-preorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0094. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/binary-tree-inorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0129. 求根节点到叶节点数字之和](https://leetcode.cn/problems/sum-root-to-leaf-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sum-root-to-leaf-numbers.md) | 树、深度优先搜索、二叉树 | 中等 | | [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [0199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-right-side-view.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0543. 二叉树的直径](https://leetcode.cn/problems/diameter-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/diameter-of-binary-tree.md) | 树、深度优先搜索、二叉树 | 简单 | | [0662. 二叉树最大宽度](https://leetcode.cn/problems/maximum-width-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-width-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | ### 广度优先搜索题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-islands.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0322. 零钱兑换](https://leetcode.cn/problems/coin-change/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) | 广度优先搜索、数组、动态规划 | 中等 | | [0199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-right-side-view.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0662. 二叉树最大宽度](https://leetcode.cn/problems/maximum-width-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-width-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | ## 第 7 章 基础算法 ### 枚举算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0001. 两数之和](https://leetcode.cn/problems/two-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/two-sum.md) | 数组、哈希表 | 简单 | | [0078. 子集](https://leetcode.cn/problems/subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets.md) | 位运算、数组、回溯 | 中等 | | [0221. 最大正方形](https://leetcode.cn/problems/maximal-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/maximal-square.md) | 数组、动态规划、矩阵 | 中等 | ### 递归算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0024. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/swap-nodes-in-pairs.md) | 递归、链表 | 中等 | | [0206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/reverse-linked-list.md) | 递归、链表 | 简单 | | [0092. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-linked-list-ii.md) | 链表 | 中等 | | [0021. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-two-sorted-lists.md) | 递归、链表 | 简单 | | [0070. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/climbing-stairs.md) | 记忆化搜索、数学、动态规划 | 简单 | | [0104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [0226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/invert-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | ### 分治算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0053. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximum-subarray.md) | 数组、分治、动态规划 | 中等 | | [0023. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) | 链表、分治、堆(优先队列)、归并排序 | 困难 | | [0004. 寻找两个正序数组的中位数](https://leetcode.cn/problems/median-of-two-sorted-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/median-of-two-sorted-arrays.md) | 数组、二分查找、分治 | 困难 | | [0169. 多数元素](https://leetcode.cn/problems/majority-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/majority-element.md) | 数组、哈希表、分治、计数、排序 | 简单 | | [0014. 最长公共前缀](https://leetcode.cn/problems/longest-common-prefix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-common-prefix.md) | 字典树、数组、字符串 | 简单 | ### 回溯算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0046. 全排列](https://leetcode.cn/problems/permutations/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutations.md) | 数组、回溯 | 中等 | | [0022. 括号生成](https://leetcode.cn/problems/generate-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/generate-parentheses.md) | 字符串、动态规划、回溯 | 中等 | | [0078. 子集](https://leetcode.cn/problems/subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets.md) | 位运算、数组、回溯 | 中等 | | [0039. 组合总和](https://leetcode.cn/problems/combination-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combination-sum.md) | 数组、回溯 | 中等 | | [0093. 复原 IP 地址](https://leetcode.cn/problems/restore-ip-addresses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/restore-ip-addresses.md) | 字符串、回溯 | 中等 | ### 贪心算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0053. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximum-subarray.md) | 数组、分治、动态规划 | 中等 | | [0056. 合并区间](https://leetcode.cn/problems/merge-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-intervals.md) | 数组、排序 | 中等 | | [0122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-ii.md) | 贪心、数组、动态规划 | 中等 | ### 位运算题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) | 位运算、数组 | 简单 | ## 第 8 章 动态规划 ### 动态规划题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0070. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/climbing-stairs.md) | 记忆化搜索、数学、动态规划 | 简单 | | [0121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock.md) | 数组、动态规划 | 简单 | | [0322. 零钱兑换](https://leetcode.cn/problems/coin-change/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) | 广度优先搜索、数组、动态规划 | 中等 | | [0300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-subsequence.md) | 数组、二分查找、动态规划 | 中等 | | [1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/longest-common-subsequence.md) | 字符串、动态规划 | 中等 | | [0718. 最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/maximum-length-of-repeated-subarray.md) | 数组、二分查找、动态规划、滑动窗口、哈希函数、滚动哈希 | 中等 | | [0064. 最小路径和](https://leetcode.cn/problems/minimum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/minimum-path-sum.md) | 数组、动态规划、矩阵 | 中等 | | [0072. 编辑距离](https://leetcode.cn/problems/edit-distance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/edit-distance.md) | 字符串、动态规划 | 中等 | | [0032. 最长有效括号](https://leetcode.cn/problems/longest-valid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-valid-parentheses.md) | 栈、字符串、动态规划 | 困难 | | [0221. 最大正方形](https://leetcode.cn/problems/maximal-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/maximal-square.md) | 数组、动态规划、矩阵 | 中等 | | [0062. 不同路径](https://leetcode.cn/problems/unique-paths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths.md) | 数学、动态规划、组合数学 | 中等 | | [0152. 乘积最大子数组](https://leetcode.cn/problems/maximum-product-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-product-subarray.md) | 数组、动态规划 | 中等 | | [0198. 打家劫舍](https://leetcode.cn/problems/house-robber/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/house-robber.md) | 数组、动态规划 | 中等 | ## 补充题目 #### 设计数据结构题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0146. LRU 缓存](https://leetcode.cn/problems/lru-cache/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/lru-cache.md) | 设计、哈希表、链表、双向链表 | 中等 | #### 模拟题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0008. 字符串转换整数 (atoi)](https://leetcode.cn/problems/string-to-integer-atoi/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/string-to-integer-atoi.md) | 字符串 | 中等 | | [0165. 比较版本号](https://leetcode.cn/problems/compare-version-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/compare-version-numbers.md) | 双指针、字符串 | 中等 | | [0468. 验证IP地址](https://leetcode.cn/problems/validate-ip-address/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/validate-ip-address.md) | 字符串 | 中等 | #### 思维锻炼题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0031. 下一个排列](https://leetcode.cn/problems/next-permutation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/next-permutation.md) | 数组、双指针 | 中等 | | [0470. 用 Rand7() 实现 Rand10()](https://leetcode.cn/problems/implement-rand10-using-rand7/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/implement-rand10-using-rand7.md) | 数学、拒绝采样、概率与统计、随机化 | 中等 | ## 参考资料 - 【清单】[CodeTop 企业题库](https://codetop.cc/home) ================================================ FILE: docs/00_preface/00_08_interview_200_list.md ================================================ # LeetCode 面试最常考 200 题(按分类排序) ## 第 1 章 数组 ### 数组基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0189. 轮转数组](https://leetcode.cn/problems/rotate-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/rotate-array.md) | 数组、数学、双指针 | 中等 | | [0498. 对角线遍历](https://leetcode.cn/problems/diagonal-traverse/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/diagonal-traverse.md) | 数组、矩阵、模拟 | 中等 | | [0048. 旋转图像](https://leetcode.cn/problems/rotate-image/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/rotate-image.md) | 数组、数学、矩阵 | 中等 | | [0054. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/spiral-matrix.md) | 数组、矩阵、模拟 | 中等 | | [0059. 螺旋矩阵 II](https://leetcode.cn/problems/spiral-matrix-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/spiral-matrix-ii.md) | 数组、矩阵、模拟 | 中等 | ### 排序算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0912. 排序数组](https://leetcode.cn/problems/sort-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) | 数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序 | 中等 | | [0283. 移动零](https://leetcode.cn/problems/move-zeroes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/move-zeroes.md) | 数组、双指针 | 简单 | | [0215. 数组中的第K个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/kth-largest-element-in-an-array.md) | 数组、分治、快速选择、排序、堆(优先队列) | 中等 | | [0075. 颜色分类](https://leetcode.cn/problems/sort-colors/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sort-colors.md) | 数组、双指针、排序 | 中等 | | [0088. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-sorted-array.md) | 数组、双指针、排序 | 简单 | | [LCR 170. 交易逆序对的总数](https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-de-ni-xu-dui-lcof.md) | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 | | [0169. 多数元素](https://leetcode.cn/problems/majority-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/majority-element.md) | 数组、哈希表、分治、计数、排序 | 简单 | | [LCR 159. 库存管理 III](https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zui-xiao-de-kge-shu-lcof.md) | 数组、分治、快速选择、排序、堆(优先队列) | 简单 | | [0164. 最大间距](https://leetcode.cn/problems/maximum-gap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-gap.md) | 数组、桶排序、基数排序、排序 | 中等 | | [0136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) | 位运算、数组 | 简单 | | [0056. 合并区间](https://leetcode.cn/problems/merge-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-intervals.md) | 数组、排序 | 中等 | | [0179. 最大数](https://leetcode.cn/problems/largest-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/largest-number.md) | 贪心、数组、字符串、排序 | 中等 | | [0384. 打乱数组](https://leetcode.cn/problems/shuffle-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/shuffle-an-array.md) | 设计、数组、数学、随机化 | 中等 | | [LCR 164. 破解闯关密码](https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof.md) | 贪心、字符串、排序 | 中等 | ### 二分查找题目 #### 二分下标题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0704. 二分查找](https://leetcode.cn/problems/binary-search/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/binary-search.md) | 数组、二分查找 | 简单 | | [0034. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-first-and-last-position-of-element-in-sorted-array.md) | 数组、二分查找 | 中等 | | [0153. 寻找旋转排序数组中的最小值](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array.md) | 数组、二分查找 | 中等 | | [0154. 寻找旋转排序数组中的最小值 II](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array-ii.md) | 数组、二分查找 | 困难 | | [0033. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-in-rotated-sorted-array.md) | 数组、二分查找 | 中等 | | [0162. 寻找峰值](https://leetcode.cn/problems/find-peak-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-peak-element.md) | 数组、二分查找 | 中等 | | [0004. 寻找两个正序数组的中位数](https://leetcode.cn/problems/median-of-two-sorted-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/median-of-two-sorted-arrays.md) | 数组、二分查找、分治 | 困难 | | [0074. 搜索二维矩阵](https://leetcode.cn/problems/search-a-2d-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-a-2d-matrix.md) | 数组、二分查找、矩阵 | 中等 | | [0240. 搜索二维矩阵 II](https://leetcode.cn/problems/search-a-2d-matrix-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/search-a-2d-matrix-ii.md) | 数组、二分查找、分治、矩阵 | 中等 | #### 二分答案题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0069. x 的平方根](https://leetcode.cn/problems/sqrtx/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sqrtx.md) | 数学、二分查找 | 简单 | | [0287. 寻找重复数](https://leetcode.cn/problems/find-the-duplicate-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/find-the-duplicate-number.md) | 位运算、数组、双指针、二分查找 | 中等 | | [0050. Pow(x, n)](https://leetcode.cn/problems/powx-n/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/powx-n.md) | 递归、数学 | 中等 | | [0400. 第 N 位数字](https://leetcode.cn/problems/nth-digit/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/nth-digit.md) | 数学、二分查找 | 中等 | #### 复杂的二分查找问题 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0209. 长度最小的子数组](https://leetcode.cn/problems/minimum-size-subarray-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/minimum-size-subarray-sum.md) | 数组、二分查找、前缀和、滑动窗口 | 中等 | | [0349. 两个数组的交集](https://leetcode.cn/problems/intersection-of-two-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays.md) | 数组、哈希表、双指针、二分查找、排序 | 简单 | ### 双指针题目 #### 对撞指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0611. 有效三角形的个数](https://leetcode.cn/problems/valid-triangle-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/valid-triangle-number.md) | 贪心、数组、双指针、二分查找、排序 | 中等 | | [0015. 三数之和](https://leetcode.cn/problems/3sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum.md) | 数组、双指针、排序 | 中等 | | [0016. 最接近的三数之和](https://leetcode.cn/problems/3sum-closest/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum-closest.md) | 数组、双指针、排序 | 中等 | | [0125. 验证回文串](https://leetcode.cn/problems/valid-palindrome/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/valid-palindrome.md) | 双指针、字符串 | 简单 | | [0011. 盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/container-with-most-water.md) | 贪心、数组、双指针 | 中等 | | [0075. 颜色分类](https://leetcode.cn/problems/sort-colors/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sort-colors.md) | 数组、双指针、排序 | 中等 | | [LCR 139. 训练计划 I](https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof.md) | 数组、双指针、排序 | 简单 | | [0443. 压缩字符串](https://leetcode.cn/problems/string-compression/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/string-compression.md) | 双指针、字符串 | 中等 | #### 快慢指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0026. 删除有序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-array.md) | 数组、双指针 | 简单 | | [0283. 移动零](https://leetcode.cn/problems/move-zeroes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/move-zeroes.md) | 数组、双指针 | 简单 | | [0088. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-sorted-array.md) | 数组、双指针、排序 | 简单 | #### 分离双指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0415. 字符串相加](https://leetcode.cn/problems/add-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-strings.md) | 数学、字符串、模拟 | 简单 | ### 滑动窗口题目 #### 固定长度窗口题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/sliding-window-maximum.md) | 队列、数组、滑动窗口、单调队列、堆(优先队列) | 困难 | #### 不定长度窗口题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0003. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-substring-without-repeating-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0076. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/minimum-window-substring.md) | 哈希表、字符串、滑动窗口 | 困难 | | [0718. 最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/maximum-length-of-repeated-subarray.md) | 数组、二分查找、动态规划、滑动窗口、哈希函数、滚动哈希 | 中等 | | [0209. 长度最小的子数组](https://leetcode.cn/problems/minimum-size-subarray-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/minimum-size-subarray-sum.md) | 数组、二分查找、前缀和、滑动窗口 | 中等 | | [0862. 和至少为 K 的最短子数组](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-subarray-with-sum-at-least-k.md) | 队列、数组、二分查找、前缀和、滑动窗口、单调队列、堆(优先队列) | 困难 | | [1004. 最大连续1的个数 III](https://leetcode.cn/problems/max-consecutive-ones-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/max-consecutive-ones-iii.md) | 数组、二分查找、前缀和、滑动窗口 | 中等 | ## 02. 链表 ### 链表基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0083. 删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-list.md) | 链表 | 简单 | | [0082. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-list-ii.md) | 链表、双指针 | 中等 | | [0206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/reverse-linked-list.md) | 递归、链表 | 简单 | | [0092. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-linked-list-ii.md) | 链表 | 中等 | | [0025. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-nodes-in-k-group.md) | 递归、链表 | 困难 | | [0328. 奇偶链表](https://leetcode.cn/problems/odd-even-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/odd-even-linked-list.md) | 链表 | 中等 | | [0234. 回文链表](https://leetcode.cn/problems/palindrome-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/palindrome-linked-list.md) | 栈、递归、链表、双指针 | 简单 | | [0138. 随机链表的复制](https://leetcode.cn/problems/copy-list-with-random-pointer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/copy-list-with-random-pointer.md) | 哈希表、链表 | 中等 | | [0061. 旋转链表](https://leetcode.cn/problems/rotate-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/rotate-list.md) | 链表、双指针 | 中等 | ### 链表排序题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0148. 排序链表](https://leetcode.cn/problems/sort-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md) | 链表、双指针、分治、排序、归并排序 | 中等 | | [0021. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-two-sorted-lists.md) | 递归、链表 | 简单 | | [0023. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) | 链表、分治、堆(优先队列)、归并排序 | 困难 | ### 链表双指针题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle.md) | 哈希表、链表、双指针 | 简单 | | [0142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle-ii.md) | 哈希表、链表、双指针 | 中等 | | [0160. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/intersection-of-two-linked-lists.md) | 哈希表、链表、双指针 | 简单 | | [0019. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-nth-node-from-end-of-list.md) | 链表、双指针 | 中等 | | [LCR 140. 训练计划 II](https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof.md) | 链表、双指针 | 简单 | | [0143. 重排链表](https://leetcode.cn/problems/reorder-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reorder-list.md) | 栈、递归、链表、双指针 | 中等 | | [0002. 两数相加](https://leetcode.cn/problems/add-two-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/add-two-numbers.md) | 递归、链表、数学 | 中等 | | [0445. 两数相加 II](https://leetcode.cn/problems/add-two-numbers-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-two-numbers-ii.md) | 栈、链表、数学 | 中等 | ## 第 3 章 栈、队列、哈希表 ### 栈基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [1047. 删除字符串中的所有相邻重复项](https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/remove-all-adjacent-duplicates-in-string.md) | 栈、字符串 | 简单 | | [0155. 最小栈](https://leetcode.cn/problems/min-stack/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/min-stack.md) | 栈、设计 | 中等 | | [0020. 有效的括号](https://leetcode.cn/problems/valid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-parentheses.md) | 栈、字符串 | 简单 | | [0224. 基本计算器](https://leetcode.cn/problems/basic-calculator/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/basic-calculator.md) | 栈、递归、数学、字符串 | 困难 | | [0227. 基本计算器 II](https://leetcode.cn/problems/basic-calculator-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/basic-calculator-ii.md) | 栈、数学、字符串 | 中等 | | [0232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-queue-using-stacks.md) | 栈、设计、队列 | 简单 | | [LCR 125. 图书整理 II](https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/yong-liang-ge-zhan-shi-xian-dui-lie-lcof.md) | 栈、设计、队列 | 简单 | | [0394. 字符串解码](https://leetcode.cn/problems/decode-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/decode-string.md) | 栈、递归、字符串 | 中等 | | [0032. 最长有效括号](https://leetcode.cn/problems/longest-valid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-valid-parentheses.md) | 栈、字符串、动态规划 | 困难 | | [0739. 每日温度](https://leetcode.cn/problems/daily-temperatures/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/daily-temperatures.md) | 栈、数组、单调栈 | 中等 | | [0071. 简化路径](https://leetcode.cn/problems/simplify-path/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/simplify-path.md) | 栈、字符串 | 中等 | ### 单调栈题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0739. 每日温度](https://leetcode.cn/problems/daily-temperatures/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/daily-temperatures.md) | 栈、数组、单调栈 | 中等 | | [0503. 下一个更大元素 II](https://leetcode.cn/problems/next-greater-element-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/next-greater-element-ii.md) | 栈、数组、单调栈 | 中等 | | [0042. 接雨水](https://leetcode.cn/problems/trapping-rain-water/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/trapping-rain-water.md) | 栈、数组、双指针、动态规划、单调栈 | 困难 | | [0085. 最大矩形](https://leetcode.cn/problems/maximal-rectangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximal-rectangle.md) | 栈、数组、动态规划、矩阵、单调栈 | 困难 | ### 队列基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0225. 用队列实现栈](https://leetcode.cn/problems/implement-stack-using-queues/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-stack-using-queues.md) | 栈、设计、队列 | 简单 | ### 优先队列题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0347. 前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/top-k-frequent-elements.md) | 数组、哈希表、分治、桶排序、计数、快速选择、排序、堆(优先队列) | 中等 | | [0239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/sliding-window-maximum.md) | 队列、数组、滑动窗口、单调队列、堆(优先队列) | 困难 | | [0295. 数据流的中位数](https://leetcode.cn/problems/find-median-from-data-stream/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/find-median-from-data-stream.md) | 设计、双指针、数据流、排序、堆(优先队列) | 困难 | | [0023. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) | 链表、分治、堆(优先队列)、归并排序 | 困难 | ### 哈希表题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0001. 两数之和](https://leetcode.cn/problems/two-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/two-sum.md) | 数组、哈希表 | 简单 | | [0015. 三数之和](https://leetcode.cn/problems/3sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum.md) | 数组、双指针、排序 | 中等 | | [0041. 缺失的第一个正数](https://leetcode.cn/problems/first-missing-positive/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/first-missing-positive.md) | 数组、哈希表 | 困难 | | [0128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/longest-consecutive-sequence.md) | 并查集、数组、哈希表 | 中等 | | [0136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) | 位运算、数组 | 简单 | | [0242. 有效的字母异位词](https://leetcode.cn/problems/valid-anagram/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/valid-anagram.md) | 哈希表、字符串、排序 | 简单 | | [0442. 数组中重复的数据](https://leetcode.cn/problems/find-all-duplicates-in-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-all-duplicates-in-an-array.md) | 数组、哈希表 | 中等 | | [LCR 186. 文物朝代判断](https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/bu-ke-pai-zhong-de-shun-zi-lcof.md) | 数组、排序 | 简单 | | [0268. 丢失的数字](https://leetcode.cn/problems/missing-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/missing-number.md) | 位运算、数组、哈希表、数学、二分查找、排序 | 简单 | | [LCR 120. 寻找文件副本](https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-zhong-fu-de-shu-zi-lcof.md) | 数组、哈希表、排序 | 简单 | ## 第 4 章 字符串 ### 字符串基础题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0125. 验证回文串](https://leetcode.cn/problems/valid-palindrome/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/valid-palindrome.md) | 双指针、字符串 | 简单 | | [0005. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-palindromic-substring.md) | 双指针、字符串、动态规划 | 中等 | | [0003. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-substring-without-repeating-characters.md) | 哈希表、字符串、滑动窗口 | 中等 | | [0344. 反转字符串](https://leetcode.cn/problems/reverse-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-string.md) | 双指针、字符串 | 简单 | | [0557. 反转字符串中的单词 III](https://leetcode.cn/problems/reverse-words-in-a-string-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/reverse-words-in-a-string-iii.md) | 双指针、字符串 | 简单 | | [0415. 字符串相加](https://leetcode.cn/problems/add-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-strings.md) | 数学、字符串、模拟 | 简单 | | [0151. 反转字符串中的单词](https://leetcode.cn/problems/reverse-words-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reverse-words-in-a-string.md) | 双指针、字符串 | 中等 | | [0043. 字符串相乘](https://leetcode.cn/problems/multiply-strings/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/multiply-strings.md) | 数学、字符串、模拟 | 中等 | | [0014. 最长公共前缀](https://leetcode.cn/problems/longest-common-prefix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-common-prefix.md) | 字典树、数组、字符串 | 简单 | ### 单模式串匹配题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0459. 重复的子字符串](https://leetcode.cn/problems/repeated-substring-pattern/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/repeated-substring-pattern.md) | 字符串、字符串匹配 | 简单 | ### 字典树题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0208. 实现 Trie (前缀树)](https://leetcode.cn/problems/implement-trie-prefix-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-trie-prefix-tree.md) | 设计、字典树、哈希表、字符串 | 中等 | | [0440. 字典序的第K小数字](https://leetcode.cn/problems/k-th-smallest-in-lexicographical-order/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/k-th-smallest-in-lexicographical-order.md) | 字典树 | 困难 | ## 第 5 章 树 ### 二叉树的遍历题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-preorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0094. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/binary-tree-inorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0145. 二叉树的后序遍历](https://leetcode.cn/problems/binary-tree-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-postorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-level-order-traversal.md) | 树、广度优先搜索、二叉树 | 中等 | | [0103. 二叉树的锯齿形层序遍历](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-zigzag-level-order-traversal.md) | 树、广度优先搜索、二叉树 | 中等 | | [0104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/minimum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [0101. 对称二叉树](https://leetcode.cn/problems/symmetric-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/symmetric-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0112. 路径总和](https://leetcode.cn/problems/path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/path-sum.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0113. 路径总和 II](https://leetcode.cn/problems/path-sum-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/path-sum-ii.md) | 树、深度优先搜索、回溯、二叉树 | 中等 | | [0236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/lowest-common-ancestor-of-a-binary-tree.md) | 树、深度优先搜索、二叉树 | 中等 | | [0199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-right-side-view.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/invert-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0958. 二叉树的完全性检验](https://leetcode.cn/problems/check-completeness-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/check-completeness-of-a-binary-tree.md) | 树、广度优先搜索、二叉树 | 中等 | | [0572. 另一棵树的子树](https://leetcode.cn/problems/subtree-of-another-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subtree-of-another-tree.md) | 树、深度优先搜索、二叉树、字符串匹配、哈希函数 | 简单 | | [0100. 相同的树](https://leetcode.cn/problems/same-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/same-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0297. 二叉树的序列化与反序列化](https://leetcode.cn/problems/serialize-and-deserialize-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/serialize-and-deserialize-binary-tree.md) | 树、深度优先搜索、广度优先搜索、设计、字符串、二叉树 | 困难 | | [0114. 二叉树展开为链表](https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/flatten-binary-tree-to-linked-list.md) | 栈、树、深度优先搜索、链表、二叉树 | 中等 | ### 二叉树的还原题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/construct-binary-tree-from-preorder-and-inorder-traversal.md) | 树、数组、哈希表、分治、二叉树 | 中等 | | [0106. 从中序与后序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/construct-binary-tree-from-inorder-and-postorder-traversal.md) | 树、数组、哈希表、分治、二叉树 | 中等 | ### 二叉搜索树题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0098. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/validate-binary-search-tree.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0450. 删除二叉搜索树中的节点](https://leetcode.cn/problems/delete-node-in-a-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/delete-node-in-a-bst.md) | 树、二叉搜索树、二叉树 | 中等 | | [LCR 174. 寻找二叉搜索树中的目标节点](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 简单 | | [0230. 二叉搜索树中第 K 小的元素](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/kth-smallest-element-in-a-bst.md) | 树、深度优先搜索、二叉搜索树、二叉树 | 中等 | | [0426. 将二叉搜索树转化为排序的双向链表](https://leetcode.cn/problems/convert-binary-search-tree-to-sorted-doubly-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/convert-binary-search-tree-to-sorted-doubly-linked-list.md) | 栈、树、深度优先搜索、二叉搜索树、链表、二叉树、双向链表 | 中等 | | [0110. 平衡二叉树](https://leetcode.cn/problems/balanced-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/balanced-binary-tree.md) | 树、深度优先搜索、二叉树 | 简单 | ### 并查集题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/longest-consecutive-sequence.md) | 并查集、数组、哈希表 | 中等 | ## 第 6 章 图论 ### 深度优先搜索题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-islands.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0695. 岛屿的最大面积](https://leetcode.cn/problems/max-area-of-island/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/max-area-of-island.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-preorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0094. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/binary-tree-inorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0145. 二叉树的后序遍历](https://leetcode.cn/problems/binary-tree-postorder-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-postorder-traversal.md) | 栈、树、深度优先搜索、二叉树 | 简单 | | [0129. 求根节点到叶节点数字之和](https://leetcode.cn/problems/sum-root-to-leaf-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sum-root-to-leaf-numbers.md) | 树、深度优先搜索、二叉树 | 中等 | | [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [0199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-right-side-view.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0543. 二叉树的直径](https://leetcode.cn/problems/diameter-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/diameter-of-binary-tree.md) | 树、深度优先搜索、二叉树 | 简单 | | [0662. 二叉树最大宽度](https://leetcode.cn/problems/maximum-width-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-width-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0958. 二叉树的完全性检验](https://leetcode.cn/problems/check-completeness-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/check-completeness-of-a-binary-tree.md) | 树、广度优先搜索、二叉树 | 中等 | | [0572. 另一棵树的子树](https://leetcode.cn/problems/subtree-of-another-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subtree-of-another-tree.md) | 树、深度优先搜索、二叉树、字符串匹配、哈希函数 | 简单 | | [0100. 相同的树](https://leetcode.cn/problems/same-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/same-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/minimum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | ### 广度优先搜索题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-islands.md) | 深度优先搜索、广度优先搜索、并查集、数组、矩阵 | 中等 | | [0322. 零钱兑换](https://leetcode.cn/problems/coin-change/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) | 广度优先搜索、数组、动态规划 | 中等 | | [0207. 课程表](https://leetcode.cn/problems/course-schedule/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/course-schedule.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [0199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-right-side-view.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0662. 二叉树最大宽度](https://leetcode.cn/problems/maximum-width-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-width-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 中等 | | [0958. 二叉树的完全性检验](https://leetcode.cn/problems/check-completeness-of-a-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/check-completeness-of-a-binary-tree.md) | 树、广度优先搜索、二叉树 | 中等 | | [0572. 另一棵树的子树](https://leetcode.cn/problems/subtree-of-another-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subtree-of-another-tree.md) | 树、深度优先搜索、二叉树、字符串匹配、哈希函数 | 简单 | | [0100. 相同的树](https://leetcode.cn/problems/same-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/same-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/minimum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [LCR 151. 彩灯装饰记录 III](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof.md) | 树、广度优先搜索、二叉树 | 中等 | ### 图的拓扑排序题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0210. 课程表 II](https://leetcode.cn/problems/course-schedule-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/course-schedule-ii.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | ## 第 7 章 基础算法 ### 枚举算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0001. 两数之和](https://leetcode.cn/problems/two-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/two-sum.md) | 数组、哈希表 | 简单 | | [0078. 子集](https://leetcode.cn/problems/subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets.md) | 位运算、数组、回溯 | 中等 | | [0221. 最大正方形](https://leetcode.cn/problems/maximal-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/maximal-square.md) | 数组、动态规划、矩阵 | 中等 | | [0560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subarray-sum-equals-k.md) | 数组、哈希表、前缀和 | 中等 | ### 递归算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0024. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/swap-nodes-in-pairs.md) | 递归、链表 | 中等 | | [0206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/reverse-linked-list.md) | 递归、链表 | 简单 | | [0092. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-linked-list-ii.md) | 链表 | 中等 | | [0021. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-two-sorted-lists.md) | 递归、链表 | 简单 | | [0509. 斐波那契数](https://leetcode.cn/problems/fibonacci-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/fibonacci-number.md) | 递归、记忆化搜索、数学、动态规划 | 简单 | | [0070. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/climbing-stairs.md) | 记忆化搜索、数学、动态规划 | 简单 | | [0104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-depth-of-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) | 树、深度优先搜索、动态规划、二叉树 | 困难 | | [0226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/invert-binary-tree.md) | 树、深度优先搜索、广度优先搜索、二叉树 | 简单 | | [LCR 187. 破冰游戏](https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof.md) | 递归、数学 | 简单 | ### 分治算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0004. 寻找两个正序数组的中位数](https://leetcode.cn/problems/median-of-two-sorted-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/median-of-two-sorted-arrays.md) | 数组、二分查找、分治 | 困难 | | [0023. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) | 链表、分治、堆(优先队列)、归并排序 | 困难 | | [0053. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximum-subarray.md) | 数组、分治、动态规划 | 中等 | | [0169. 多数元素](https://leetcode.cn/problems/majority-element/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/majority-element.md) | 数组、哈希表、分治、计数、排序 | 简单 | | [0014. 最长公共前缀](https://leetcode.cn/problems/longest-common-prefix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-common-prefix.md) | 字典树、数组、字符串 | 简单 | | [LCR 152. 验证二叉搜索树的后序遍历序列](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof.md) | 栈、树、二叉搜索树、递归、数组、二叉树、单调栈 | 中等 | ### 回溯算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0046. 全排列](https://leetcode.cn/problems/permutations/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutations.md) | 数组、回溯 | 中等 | | [0047. 全排列 II](https://leetcode.cn/problems/permutations-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutations-ii.md) | 数组、回溯、排序 | 中等 | | [0037. 解数独](https://leetcode.cn/problems/sudoku-solver/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sudoku-solver.md) | 数组、哈希表、回溯、矩阵 | 困难 | | [0022. 括号生成](https://leetcode.cn/problems/generate-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/generate-parentheses.md) | 字符串、动态规划、回溯 | 中等 | | [0078. 子集](https://leetcode.cn/problems/subsets/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets.md) | 位运算、数组、回溯 | 中等 | | [0039. 组合总和](https://leetcode.cn/problems/combination-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combination-sum.md) | 数组、回溯 | 中等 | | [0040. 组合总和 II](https://leetcode.cn/problems/combination-sum-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combination-sum-ii.md) | 数组、回溯 | 中等 | | [0093. 复原 IP 地址](https://leetcode.cn/problems/restore-ip-addresses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/restore-ip-addresses.md) | 字符串、回溯 | 中等 | | [0079. 单词搜索](https://leetcode.cn/problems/word-search/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/word-search.md) | 深度优先搜索、数组、字符串、回溯、矩阵 | 中等 | | [0679. 24 点游戏](https://leetcode.cn/problems/24-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/24-game.md) | 数组、数学、回溯 | 困难 | ### 贪心算法题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0053. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximum-subarray.md) | 数组、分治、动态规划 | 中等 | | [0056. 合并区间](https://leetcode.cn/problems/merge-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-intervals.md) | 数组、排序 | 中等 | | [0122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-ii.md) | 贪心、数组、动态规划 | 中等 | | [0055. 跳跃游戏](https://leetcode.cn/problems/jump-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/jump-game.md) | 贪心、数组、动态规划 | 中等 | | [0402. 移掉 K 位数字](https://leetcode.cn/problems/remove-k-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/remove-k-digits.md) | 栈、贪心、字符串、单调栈 | 中等 | | [0135. 分发糖果](https://leetcode.cn/problems/candy/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/candy.md) | 贪心、数组 | 困难 | | [0134. 加油站](https://leetcode.cn/problems/gas-station/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/gas-station.md) | 贪心、数组 | 中等 | | [0670. 最大交换](https://leetcode.cn/problems/maximum-swap/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-swap.md) | 贪心、数学 | 中等 | ### 位运算题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) | 位运算、数组 | 简单 | | [0191. 位1的个数](https://leetcode.cn/problems/number-of-1-bits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/number-of-1-bits.md) | 位运算、分治 | 简单 | | [0268. 丢失的数字](https://leetcode.cn/problems/missing-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/missing-number.md) | 位运算、数组、哈希表、数学、二分查找、排序 | 简单 | ## 第 8 章 动态规划 ### 动态规划题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0070. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/climbing-stairs.md) | 记忆化搜索、数学、动态规划 | 简单 | | [0509. 斐波那契数](https://leetcode.cn/problems/fibonacci-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/fibonacci-number.md) | 递归、记忆化搜索、数学、动态规划 | 简单 | | [0121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock.md) | 数组、动态规划 | 简单 | | [0322. 零钱兑换](https://leetcode.cn/problems/coin-change/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) | 广度优先搜索、数组、动态规划 | 中等 | | [0518. 零钱兑换 II](https://leetcode.cn/problems/coin-change-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/coin-change-ii.md) | 数组、动态规划 | 中等 | | [0300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-subsequence.md) | 数组、二分查找、动态规划 | 中等 | | [1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/longest-common-subsequence.md) | 字符串、动态规划 | 中等 | | [0718. 最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/maximum-length-of-repeated-subarray.md) | 数组、二分查找、动态规划、滑动窗口、哈希函数、滚动哈希 | 中等 | | [0064. 最小路径和](https://leetcode.cn/problems/minimum-path-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/minimum-path-sum.md) | 数组、动态规划、矩阵 | 中等 | | [0072. 编辑距离](https://leetcode.cn/problems/edit-distance/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/edit-distance.md) | 字符串、动态规划 | 中等 | | [0032. 最长有效括号](https://leetcode.cn/problems/longest-valid-parentheses/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-valid-parentheses.md) | 栈、字符串、动态规划 | 困难 | | [0221. 最大正方形](https://leetcode.cn/problems/maximal-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/maximal-square.md) | 数组、动态规划、矩阵 | 中等 | | [0062. 不同路径](https://leetcode.cn/problems/unique-paths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths.md) | 数学、动态规划、组合数学 | 中等 | | [0063. 不同路径 II](https://leetcode.cn/problems/unique-paths-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths-ii.md) | 数组、动态规划、矩阵 | 中等 | | [0152. 乘积最大子数组](https://leetcode.cn/problems/maximum-product-subarray/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-product-subarray.md) | 数组、动态规划 | 中等 | | [0198. 打家劫舍](https://leetcode.cn/problems/house-robber/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/house-robber.md) | 数组、动态规划 | 中等 | | [0213. 打家劫舍 II](https://leetcode.cn/problems/house-robber-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/house-robber-ii.md) | 数组、动态规划 | 中等 | | [0091. 解码方法](https://leetcode.cn/problems/decode-ways/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/decode-ways.md) | 字符串、动态规划 | 中等 | | [0010. 正则表达式匹配](https://leetcode.cn/problems/regular-expression-matching/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/regular-expression-matching.md) | 递归、字符串、动态规划 | 困难 | | [0678. 有效的括号字符串](https://leetcode.cn/problems/valid-parenthesis-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/valid-parenthesis-string.md) | 栈、贪心、字符串、动态规划 | 中等 | | [0045. 跳跃游戏 II](https://leetcode.cn/problems/jump-game-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/jump-game-ii.md) | 贪心、数组、动态规划 | 中等 | | [0673. 最长递增子序列的个数](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/number-of-longest-increasing-subsequence.md) | 树状数组、线段树、数组、动态规划 | 中等 | | [0139. 单词拆分](https://leetcode.cn/problems/word-break/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/word-break.md) | 字典树、记忆化搜索、数组、哈希表、字符串、动态规划 | 中等 | | [0044. 通配符匹配](https://leetcode.cn/problems/wildcard-matching/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/wildcard-matching.md) | 贪心、递归、字符串、动态规划 | 困难 | | [0120. 三角形最小路径和](https://leetcode.cn/problems/triangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/triangle.md) | 数组、动态规划 | 中等 | | [0096. 不同的二叉搜索树](https://leetcode.cn/problems/unique-binary-search-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-binary-search-trees.md) | 树、二叉搜索树、数学、动态规划、二叉树 | 中等 | | [0887. 鸡蛋掉落](https://leetcode.cn/problems/super-egg-drop/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/super-egg-drop.md) | 数学、二分查找、动态规划 | 困难 | | [0097. 交错字符串](https://leetcode.cn/problems/interleaving-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/interleaving-string.md) | 字符串、动态规划 | 中等 | | [0516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-palindromic-subsequence.md) | 字符串、动态规划 | 中等 | ### 记忆化搜索题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0329. 矩阵中的最长递增路径](https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-path-in-a-matrix.md) | 深度优先搜索、广度优先搜索、图、拓扑排序、记忆化搜索、数组、动态规划、矩阵 | 困难 | ## 补充题目 #### 设计数据结构题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0146. LRU 缓存](https://leetcode.cn/problems/lru-cache/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/lru-cache.md) | 设计、哈希表、链表、双向链表 | 中等 | | [0460. LFU 缓存](https://leetcode.cn/problems/lfu-cache/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/lfu-cache.md) | 设计、哈希表、链表、双向链表 | 困难 | #### 数学题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0007. 整数反转](https://leetcode.cn/problems/reverse-integer/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-integer.md) | 数学 | 中等 | | [0009. 回文数](https://leetcode.cn/problems/palindrome-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/palindrome-number.md) | 数学 | 简单 | | [LCR 187. 破冰游戏](https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof.md) | 递归、数学 | 简单 | | [0168. Excel 表列名称](https://leetcode.cn/problems/excel-sheet-column-title/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/excel-sheet-column-title.md) | 数学、字符串 | 简单 | | [0400. 第 N 位数字](https://leetcode.cn/problems/nth-digit/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/nth-digit.md) | 数学、二分查找 | 中等 | #### 模拟题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0008. 字符串转换整数 (atoi)](https://leetcode.cn/problems/string-to-integer-atoi/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/string-to-integer-atoi.md) | 字符串 | 中等 | | [0165. 比较版本号](https://leetcode.cn/problems/compare-version-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/compare-version-numbers.md) | 双指针、字符串 | 中等 | | [0468. 验证IP地址](https://leetcode.cn/problems/validate-ip-address/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/validate-ip-address.md) | 字符串 | 中等 | | [0086. 分隔链表](https://leetcode.cn/problems/partition-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/partition-list.md) | 链表、双指针 | 中等 | #### 前缀和 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subarray-sum-equals-k.md) | 数组、哈希表、前缀和 | 中等 | #### 思维锻炼题目 | 标题 | 题解 | 标签 | 难度 | | :--- | :--- | :--- | :--- | | [0031. 下一个排列](https://leetcode.cn/problems/next-permutation/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/next-permutation.md) | 数组、双指针 | 中等 | | [0556. 下一个更大元素 III](https://leetcode.cn/problems/next-greater-element-iii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/next-greater-element-iii.md) | 数学、双指针、字符串 | 中等 | | [0470. 用 Rand7() 实现 Rand10()](https://leetcode.cn/problems/implement-rand10-using-rand7/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/implement-rand10-using-rand7.md) | 数学、拒绝采样、概率与统计、随机化 | 中等 | ## 参考资料 - 【清单】[CodeTop 企业题库](https://codetop.cc/home) ================================================ FILE: docs/00_preface/index.md ================================================ ![](https://qcdn.itcharge.cn/images/20250923152426.png) ::: tip 引 言 数据结构如同星辰,算法宛若清风,星光与微风交织,点亮智慧的夜空。 愿本书与你同行,助你轻装前行,照亮属于你的算法之路。 ::: ## 本章内容 - [0.1 前言](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_01_preface.md) - [0.2 算法与数据结构](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_02_data_structures_algorithms.md) - [0.3 算法复杂度](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_03_algorithm_complexity.md) - [0.4 LeetCode 入门与攻略](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_04_leetcode_guide.md) - [0.5 LeetCode 题解(字典序排序,850+ 道题解)](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_05_solutions_list.md) - [0.6 LeetCode 题解(按分类排序,推荐刷题列表 ★★★)](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md) - [0.7 LeetCode 面试最常考 100 题(按分类排序)](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_07_interview_100_list.md) - [0.8 LeetCode 面试最常考 200 题(按分类排序)](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_08_interview_200_list.md) ================================================ FILE: docs/01_array/01_01_array_basic.md ================================================ ## 1. 数组简介 ### 1.1 数组定义 > **数组(Array)**:一种线性表数据结构,利用一段连续的内存空间,存储一组相同类型的数据。 简而言之,**「数组」** 是线性表顺序存储结构的典型代表。 以整数数组为例,其存储方式如下图所示: ![数组](https://qcdn.itcharge.cn/images/202405091955166.png) 如上图,假设数组包含 $n$ 个元素,每个元素都有唯一的下标索引,范围从 $0$ 到 $n - 1$。每个下标对应一个数据元素。 可以看出,数组在计算机中本质上是一段连续的内存区域。每个元素都占用相同大小的存储单元,这些单元都有自己的内存地址,并且在物理内存中是依次排列的。 我们可以从两个角度理解数组的定义: > 1. **线性表**:线性表是一种数据元素顺序排列、类型相同的数据结构,每个元素最多只有前驱和后继两个相邻元素。数组正是线性表的一种典型实现,此外,栈、队列、链表等也属于线性表结构。 > 2. **连续的内存空间**:线性表有「顺序存储」和「链式存储」两种方式。顺序存储结构要求内存空间连续,相邻元素在物理内存中紧挨着。数组采用的正是顺序存储结构,且所有元素类型一致。 综合这两个角度,数组可以看作是采用「顺序存储结构」实现的「线性表」。 ### 1.2 如何随机访问数据元素 数组最显著的特点是:**支持随机访问**。也就是说,可以通过下标直接定位并访问任意一个元素。 那么,计算机是如何实现通过下标高效访问数组元素的呢? 实际上,数组在内存中被分配为一段连续的空间,第一个元素的地址称为 **「首地址」**。每个元素都有唯一的下标和对应的内存地址。计算机在访问数组元素时,会利用下标通过 **「寻址公式」** 快速计算出目标元素的内存地址,从而实现高效访问。 寻址公式为:**下标 $i$ 的元素地址 = 首地址 + $i$ × 单个元素占用的字节数** ### 1.3 多维数组 前面介绍的是只有一个维度的数组,称为一维数组,其每个数据元素都通过单一的下标进行访问。但在实际应用中,许多数据具有二维或多维结构,一维数组已无法满足需求,因此引入了多维数组的概念。 以二维数组为例,其结构如下图所示: ![二维数组](https://qcdn.itcharge.cn/images/202405091957859.png) 二维数组由 $m$ 行 $n$ 列的数据元素组成,本质上可以理解为「数组的数组」,即每个元素本身也是一个数组。第一维表示行,第二维表示列。在内存中,二维数组通常采用行优先或列优先的存储方式。 二维数组常被视为矩阵,可以用于处理如矩阵转置、矩阵加法、矩阵乘法等相关问题。 ### 1.4 不同编程语言中数组的实现 在不同的编程语言中,数组的数据结构实现存在一定差异。 C / C++ 语言中的数组实现最贴合数据结构中对数组的定义:它们使用一块连续的内存空间来存储相同类型的数据元素。无论是基本数据类型,还是结构体、对象,在数组中都以连续方式排列。例如: ```C++ int arr[3][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}}; ``` Java 中的数组同样用于存储相同类型的数据,并且在底层实现中也是连续存储的。但在多维数组的情况下,Java 允许创建不规则数组(jagged array),即每个嵌套数组的长度可以不同。例如: ```Java int[][] arr = new int[3][]; arr[0] = new int[]{1, 2, 3}; arr[1] = new int[]{4, 5}; arr[2] = new int[]{6, 7, 8, 9}; ``` 在原生 Python 中,并不存在严格意义上的「数组」这一数据结构,而是提供了一种名为「列表(list)」的容器类型,功能类似于 Java 中的 ArrayList。我们通常将列表作为 Python 中的数组来使用。与传统数组不同,Python 的列表不仅可以存储不同类型的数据元素,长度也可以动态变化,并且支持丰富的内置方法。例如: ```python arr = ['python', 'java', ['asp', 'php'], 'c'] ``` ## 2. 数组的基本操作 数组的基本操作主要包括增、删、改、查四类,下面我们分别介绍数组在这四种操作下的实现方式。 ### 2.1 访问元素 > **访问数组中第 $index$ 个元素**: > > 1. 首先检查下标 $index$ 是否在合法范围内,即 $0 \le index \le len(nums) - 1$,超出该范围属于非法访问。 > 2. 如果下标合法,则可直接通过下标获取对应元素的值。 > 3. 如果下标不合法,则抛出异常或返回特殊值。 ```python # 从数组 nums 中读取下标为 i 的数据元素值 def get_element(nums: list[int], index: int): """获取数组中指定下标的元素值""" if 0 <= index < len(nums): return nums[index] else: raise IndexError(f"数组下标 {index} 超出范围 [0, {len(nums)-1}]") # 示例用法 arr = [0, 5, 2, 3, 7, 1, 6] print(get_element(arr, 3)) # 输出: 3 ``` 「访问数组元素」的操作不依赖于数组中元素个数,因此,「访问数组元素」的时间复杂度为 $O(1)$。 ### 2.2 查找元素 > **查找数组中元素值为 $val$ 的位置**: > > 1. 遍历数组,将目标值 $val$ 与每个元素进行比较。 > 2. 找到匹配元素时返回其下标。 > 3. 遍历完未找到时返回特殊值(如 $-1$)。 ```python def find_element(nums: list[int], val: int): """查找数组中元素值为 val 的位置""" for i in range(len(nums)): if nums[i] == val: return i return -1 # 示例用法 arr = [0, 5, 2, 3, 7, 1, 6] print(find_element(arr, 5)) # 输出: 1 print(find_element(arr, 9)) # 输出: -1 (未找到) ``` 当数组无序时,查找元素只能通过将 $val$ 与数组中的每个元素依次比较,这种方式称为线性查找。由于需要遍历整个数组,线性查找的时间复杂度为 $O(n)$。 ### 2.3 插入元素 > **在数组第 $index$ 个位置插入值 $val$**: > > 1. 检查 $index$ 是否在 $0 \le index \le len(nums)$ 范围内。 > 2. 扩展数组长度,为新元素腾出空间。 > 3. 将 $index$ 及其后的元素整体向后移动一位。 > 4. 在 $index$ 位置插入 $val$。 ![插入元素](https://qcdn.itcharge.cn/images/20210916224032.png) ```python def insert_element(nums: list[int], index: int, val: int): """在指定位置插入元素""" # 检查 index 是否在有效范围内 if 0 <= index <= len(nums): # 扩展数组长度,在末尾添加一个占位元素 nums.append(0) # 将 index 及其后的元素整体向后移动一位 for i in range(len(nums) - 1, index, -1): nums[i] = nums[i - 1] # 在 index 位置插入 val nums[index] = val return True else: # 索引不在范围内,返回错误 return False # 示例用法 arr = [0, 5, 2, 3, 7, 1, 6] result = insert_element(arr, 2, 4) print(f"插入结果: {result}") # 输出: 插入结果: True print(f"插入后数组: {arr}") # 输出: [0, 5, 4, 2, 3, 7, 1, 6] ``` 「在数组中间位置插入元素」的操作中,由于移动元素的操作次数跟元素个数有关,因此,「在数组中间位置插入元素」的最坏和平均时间复杂度都是 $O(n)$。 ### 2.4 改变元素 > **将数组中第 $index$ 个元素值改为 $val$**: > > 1. 检查 $index$ 是否在 $0 \le index \le len(nums) - 1$ 范围内。 > 2. 将第 $index$ 个元素值赋值为 $val$。 ![改变元素](https://qcdn.itcharge.cn/images/20210916224722.png) ```python def change_element(nums: list[int], index: int, val: int): """修改数组中指定位置的元素值""" if 0 <= index < len(nums): nums[index] = val return True else: return False # 索引超出范围 # 示例用法 arr = [0, 5, 2, 3, 7, 1, 6] result = change_element(arr, 2, 4) print(f"修改结果: {result}") # 输出: 修改结果: True print(f"修改后数组: {arr}") # 输出: [0, 5, 4, 3, 7, 1, 6] ``` 「改变元素」操作与访问元素类似,都是通过下标直接定位,无需遍历数组,操作时间与数组长度无关,因此其时间复杂度为 $O(1)$。 ### 2.5 删除元素 > **删除数组中第 $index$ 个位置的元素**: > > 1. 检查下标 $index$ 是否在合法范围内,即 $0 \le index < len(nums)$。 > 2. 将 $index + 1$ 位置及其后的元素整体向前移动一位。 > 3. 删除最后一个元素(或更新数组长度)。 ![删除元素](https://qcdn.itcharge.cn/images/20210916234013.png) ```python def delete_element(nums: list[int], index: int): """删除数组中指定位置的元素""" if 0 <= index < len(nums): # 将 index 后的元素整体向前移动一位 for i in range(index, len(nums) - 1): nums[i] = nums[i + 1] # 删除最后一个元素(或更新数组长度) nums.pop() return True else: return False # 索引超出范围 # 示例用法 arr = [0, 5, 2, 3, 7, 1, 6] result = delete_element(arr, 2) print(f"删除结果: {result}") # 输出: 删除结果: True print(f"删除后数组: {arr}") # 输出: [0, 5, 3, 7, 1, 6] ``` 「删除元素」需要移动后续元素,移动次数与数组长度相关,因此时间复杂度为 $O(n)$。 ## 3. 总结 数组是一种基础且重要的数据结构,采用连续的内存空间来存储同类型的数据。其最大优势在于支持随机访问,可以通过下标高效地定位和访问任意元素。 数组的访问和修改操作时间复杂度为 $O(1)$,而插入和删除操作由于需要移动元素,时间复杂度为 $O(n)$。 ## 4. 练习题目 - [0066. 加一](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/plus-one.md) - [0724. 寻找数组的中心下标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/find-pivot-index.md) - [0189. 轮转数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/rotate-array.md) - [0048. 旋转图像](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/rotate-image.md) - [0054. 螺旋矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/spiral-matrix.md) - [0498. 对角线遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/diagonal-traverse.md) - [数组基础题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%95%B0%E7%BB%84%E5%9F%BA%E7%A1%80%E9%A2%98%E7%9B%AE) ## 参考资料 - 【文章】[数据结构中的数组和不同语言中数组的区别 - CSDN 博客](https://blog.csdn.net/sinat_14913533/article/details/102763573) - 【文章】[数组理论基础 - 代码随想录](https://programmercarl.com/数组理论基础.html#数组理论基础) - 【文章】[Python 与 Java 中容器对比:List - 知乎](https://zhuanlan.zhihu.com/p/120312437) - 【文章】[什么是数组 - 漫画算法 - 小灰的算法之旅 - 力扣](https://leetcode.cn/leetbook/read/journey-of-algorithm/5ozchs/) - 【文章】[数组 - 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/intro/100017301) - 【书籍】数据结构教程 第 2 版 - 唐发根 著 - 【书籍】数据结构与算法 Python 语言描述 - 裘宗燕 著 ================================================ FILE: docs/01_array/01_02_array_sort.md ================================================ 数组排序是计算机科学中最基本的问题之一。排序算法有很多种,每种算法都有其特点和适用场景。 ## 1. 排序算法的分类 排序算法可以按照不同的标准进行分类: 1. 按照时间复杂度分类 - **简单排序算法**:时间复杂度为 $O(n^2)$,如冒泡排序、选择排序、插入排序 - **高级排序算法**:时间复杂度为 $O(n \log n)$,如快速排序、归并排序、堆排序 - **线性排序算法**:时间复杂度为 $O(n)$,如计数排序、桶排序、基数排序 2. 按照空间复杂度分类 - **原地排序算法**:空间复杂度为 $O(1)$,如冒泡排序、选择排序、插入排序、快速排序、堆排序 - **非原地排序算法**:空间复杂度为 $O(n)$ 或更高,如归并排序、计数排序、桶排序、基数排序 3. 按照稳定性分类 - **稳定排序算法**:相等元素的相对顺序在排序后保持不变,如冒泡排序、插入排序、归并排序、计数排序、桶排序、基数排序 - **不稳定排序算法**:相等元素的相对顺序在排序后可能改变,如选择排序、快速排序、堆排序 ## 2. 排序算法的评价指标 评价一个排序算法的好坏,主要从以下几个方面考虑: 1. **时间复杂度**:算法执行所需的时间,包括最好情况、最坏情况和平均情况 2. **空间复杂度**:算法执行所需的额外空间(不包括输入数据本身) 3. **稳定性**:相等元素的相对顺序是否保持不变 4. **原地性**:是否需要在原数组之外开辟额外空间 ## 3. 常见排序算法 常见的排序算法包括: 1. **冒泡排序**:通过相邻元素比较和交换,将最大元素逐步「冒泡」到数组末尾 2. **选择排序**:每次从未排序区间选择最小元素,放到已排序区间末尾 3. **插入排序**:将未排序区间的元素插入到已排序区间的合适位置 4. **希尔排序**:先按一定间隔分组进行插入排序,逐步缩小间隔,最后整体进行插入排序 5. **归并排序**:将数组分成两半,分别排序后合并 6. **快速排序**:选择一个基准元素,将数组分为两部分,递归排序 7. **堆排序**:利用堆这种数据结构进行排序 8. **计数排序**:统计每个元素出现的次数,按顺序输出 9. **桶排序**:将元素分到有限数量的桶中,对每个桶单独排序 10. **基数排序**:按照元素的位数进行排序 ## 4. 排序算法的选择策略 在实际应用中,选择合适的排序算法需要考虑以下因素: 1. 数据规模 - **小规模数据**(n < 50):可以使用简单排序算法,如插入排序 - **中等规模数据**(50 ≤ n < 1000):推荐使用快速排序或归并排序 - **大规模数据**(n ≥ 1000):优先考虑快速排序、归并排序或堆排序 2. 数据特征 - **基本有序**:插入排序效率较高 - **数据分布均匀**:快速排序效率较高 - **数据范围较小**:计数排序或桶排序可能更高效 - **数据有大量重复**:三路快速排序或计数排序更合适 3. 环境约束 - **空间限制**:选择原地排序算法 - **稳定性要求**:选择稳定排序算法 - **硬件环境**:考虑缓存友好性和并行化潜力 4. 实际应用场景 - **系统排序**:通常使用快速排序或归并排序的混合算法 - **外部排序**:使用归并排序 - **实时系统**:优先考虑时间复杂度稳定的算法 - **内存受限环境**:选择空间复杂度低的算法 ## 5. 总结 排序算法的核心目标是将数据按指定顺序排列。常见排序算法各有优缺点,选择时需结合数据规模、数据特性和实际需求。一般来说,小规模数据可用插入、冒泡等简单算法;大规模或高性能场景优先考虑快速排序、归并排序等高效算法。如果有稳定性或空间限制等特殊要求,应优先选择满足条件的算法。理解各种排序的原理和适用场景,有助于在实际开发中做出最优选择。 ================================================ FILE: docs/01_array/01_03_array_bubble_sort.md ================================================ ## 1. 冒泡排序算法思想 > **冒泡排序(Bubble Sort)基本思想**: > > 通过相邻元素的比较与交换,将较大的元素逐步「冒泡」到数组末尾,较小的元素自然「下沉」到数组开头。 冒泡排序的名字来源于这个过程:就像水中的气泡从底部向上浮到水面一样,较大的元素会逐步移动到数组的末尾。 **我们使用「冒泡」的方式来模拟一下这个过程**: 1. 将数组元素想象成大小不同的「泡泡」,值越大的元素「泡泡」越大。 2. 从左到右依次比较相邻的两个元素。 3. 如果左侧元素大于右侧元素,则交换位置。 4. 每完成一趟遍历,最大的元素就会「浮」到最右侧。 ::: tabs#bubble @tab <1> ![冒泡排序 1](https://qcdn.itcharge.cn/images/202308152226863.png) @tab <2> ![冒泡排序 2](https://qcdn.itcharge.cn/images/202308152227763.png) @tab <3> ![冒泡排序 3](https://qcdn.itcharge.cn/images/202308152227002.png) @tab <4> ![冒泡排序 4](https://qcdn.itcharge.cn/images/202308152227621.png) @tab <5> ![冒泡排序 5](https://qcdn.itcharge.cn/images/202308152227175.png) @tab <6> ![冒泡排序 6](https://qcdn.itcharge.cn/images/202308152227578.png) @tab <7> ![冒泡排序 7](https://qcdn.itcharge.cn/images/202308152228488.png) ::: ## 2. 冒泡排序算法步骤 对于长度为 $n$ 的数组,冒泡排序的步骤如下: 1. 第 $1$ 趟冒泡:对前 $n$ 个元素依次比较相邻元素,将较大的元素向右交换,最终使最大值移动到数组末尾(第 $n$ 个位置)。 1. 比较第 $1$ 个和第 $2$ 个元素,如果前者大于后者则交换。 2. 比较第 $2$ 个和第 $3$ 个元素,如果前者大于后者则交换。 3. 以此类推,直到比较第 $n - 1$ 个和第 $n$ 个元素。 4. 完成后,最大元素已位于末尾。 2. 第 $2$ 趟冒泡:对前 $n-1$ 个元素重复上述过程,将次大值移动到倒数第二个位置(第 $n-1$ 个位置)。 1. 比较第 $1$ 个和第 $2$ 个元素,如果前者大于后者则交换。 2. 比较第 $2$ 个和第 $3$ 个元素,如果前者大于后者则交换。 3. 以此类推,直到比较第 $n-2$ 个和第 $n-1$ 个元素。 4. 完成后,次大元素已位于倒数第二位。 3. 持续进行上述冒泡过程,每一趟比较的元素个数递减,直到某一趟未发生任何交换,说明数组已完全有序,排序结束。 以数组 $[5, 2, 3, 6, 1, 4]$ 为例,演示一下冒泡排序的算法步骤。 ![冒泡排序的算法步骤](https://qcdn.itcharge.cn/images/20230816154510.png) ## 3. 冒泡排序代码实现 ```python class Solution: def bubbleSort(self, nums: [int]) -> [int]: """冒泡排序算法实现""" n = len(nums) # 外层循环控制趟数,每一趟将当前未排序区间的最大值「冒泡」到末尾 for i in range(n - 1): swapped = False # 记录本趟是否发生过交换 # 内层循环负责相邻元素两两比较,将较大值后移 for j in range(n - i - 1): # 如果前一个元素大于后一个元素,则交换 if nums[j] > nums[j + 1]: nums[j], nums[j + 1] = nums[j + 1], nums[j] swapped = True # 发生了交换 # 如果本趟没有发生任何交换,说明数组已经有序,可以提前结束 if not swapped: break return nums # 返回排序后的数组 def sortArray(self, nums: [int]) -> [int]: """排序数组的接口,调用冒泡排序""" return self.bubbleSort(nums) ``` ## 4. 冒泡排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n)$ | 数组已有序,只需一趟遍历 | | **最坏时间复杂度** | $O(n^2)$ | 数组逆序,需要 $n$ 趟遍历 | | **平均时间复杂度** | $O(n^2)$ | 一般情况下的复杂度 | | **空间复杂度** | $O(1)$ | 原地排序,只使用常数空间 | | **稳定性** | 稳定 | 相等元素相对位置不变 | **适用场景**: - 数据量较小($n < 50$) - 数据基本有序 ## 5. 总结 冒泡排序是最简单的排序算法之一,通过相邻元素比较交换实现排序。虽然实现简单,但效率较低。 - **优点**:实现简单,稳定排序,空间复杂度低。 - **缺点**:时间复杂度高,交换次数多。 ## 练习题目 - [0912. 排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md)(冒泡排序会超时,仅作练习) - [0283. 移动零](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/move-zeroes.md)(冒泡排序会超时,仅作练习) - [排序算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ## 参考资料 - 【文章】[11.3. 冒泡排序 - Hello 算法](https://www.hello-algo.com/chapter_sorting/bubble_sort/) ================================================ FILE: docs/01_array/01_04_array_selection_sort.md ================================================ ## 1. 选择排序算法思想 > **选择排序(Selection Sort)基本思想**: > > 将数组分为两个区间:左侧为已排序区间,右侧为未排序区间。每趟从未排序区间中选择最小的元素,放到已排序区间的末尾。 选择排序是一种简单直观的排序算法,实现简单,易于理解。 ## 2. 选择排序算法步骤 假设数组长度为 $n$,选择排序的算法步骤如下: 1. **初始状态**:已排序区间为空,未排序区间为 $[0, n - 1]$。 2. **第 $i$ 趟选择**($i$ 从 $1$ 开始): 1. 在未排序区间 $[i - 1, n - 1]$ 中找到最小元素的位置 $min\_i$。 2. 将位置 $i - 1$ 的元素与位置 $min\_i$ 的元素交换。 3. 此时 $[0, i - 1]$ 为已排序区间,$[i, n - 1]$ 为未排序区间。 3. **重复步骤 2**,直到未排序区间为空,排序完成。 以数组 $[5, 2, 3, 6, 1, 4]$ 为例,演示一下选择排序的算法步骤。 ::: tabs#selectionSort @tab <1> ![选择排序 1](https://qcdn.itcharge.cn/images/20230816155042.png) @tab <2> ![选择排序 2](https://qcdn.itcharge.cn/images/20230816155017.png) @tab <3> ![选择排序 3](https://qcdn.itcharge.cn/images/20230816154955.png) @tab <4> ![选择排序 4](https://qcdn.itcharge.cn/images/20230816154924.png) @tab <5> ![选择排序 5](https://qcdn.itcharge.cn/images/20230816154859.png) @tab <6> ![选择排序 6](https://qcdn.itcharge.cn/images/20230816154836.png) @tab <7> ![选择排序 7](https://qcdn.itcharge.cn/images/20230816153324.png) ::: ## 3. 选择排序代码实现 ```python class Solution: def selectionSort(self, nums: [int]) -> [int]: n = len(nums) for i in range(n - 1): # 找到未排序区间中最小元素的位置 min_i = i for j in range(i + 1, n): if nums[j] < nums[min_i]: min_i = j # 交换元素 if i != min_i: nums[i], nums[min_i] = nums[min_i], nums[i] return nums def sortArray(self, nums: [int]) -> [int]: return self.selectionSort(nums) ``` ## 4. 选择排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n^2)$ | 无论数组状态如何,都需要 $\frac{n(n-1)}{2}$ 次比较 | | **最坏时间复杂度** | $O(n^2)$ | 无论数组状态如何,都需要 $\frac{n(n-1)}{2}$ 次比较 | | **平均时间复杂度** | $O(n^2)$ | 选择排序的时间复杂度与数据状态无关 | | **空间复杂度** | $O(1)$ | 原地排序,只使用常数空间 | | **稳定性** | 不稳定 | 交换操作可能改变相等元素的相对顺序 | **适用场景**: - 数据量较小($n < 50$) - 对空间复杂度要求严格的场景 ## 5. 总结 选择排序是一种简单直观的排序算法,通过不断选择未排序区间的最小元素来构建有序序列。 - **优点**:实现简单,空间复杂度低,交换次数少 - **缺点**:时间复杂度高,不适合大规模数据 ## 练习题目 - [0912. 排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md)(选择排序会超时,仅作练习) - [排序算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/01_array/01_05_array_insertion_sort.md ================================================ ## 1. 插入排序算法思想 > **插入排序(Insertion Sort)基本思想**: > > 将数组分为有序区间和无序区间,每次从无序区间取出一个元素插入到有序区间的正确位置。 插入排序通过逐步构建有序序列来实现排序,每次插入后有序区间保持有序。 ## 2. 插入排序算法步骤 假设数组长度为 $n$,算法步骤如下: 1. **初始化**:有序区间为 $[0, 0]$,无序区间为 $[1, n - 1]$ 2. **第 $i$ 趟插入**($i$ 从 $1$ 到 $n - 1$): - 取出无序区间第一个元素 $nums[i]$ - 从右到左遍历有序区间,将大于 $nums[i]$ 的元素右移一位 - 找到合适位置后插入 $nums[i]$ - 有序区间扩展为 $[0, i]$,无序区间变为 $[i + 1, n - 1]$ 以数组 $[5, 2, 3, 6, 1, 4]$ 为例,演示一下插入排序的算法步骤。 ![插入排序的算法步骤](http://qcdn.itcharge.cn/images/20230816175619.png) ## 3. 插入排序代码实现 ```python class Solution: def insertionSort(self, nums: [int]) -> [int]: # 遍历无序区间 for i in range(1, len(nums)): temp = nums[i] j = i # 从右至左遍历有序区间 while j > 0 and nums[j - 1] > temp: # 将大于temp的元素右移 nums[j] = nums[j - 1] j -= 1 # 插入到正确位置 nums[j] = temp return nums def sortArray(self, nums: [int]) -> [int]: return self.insertionSort(nums) ``` ## 4. 插入排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n)$ | 数组已有序,每个元素只需比较一次 | | **最坏时间复杂度** | $O(n^2)$ | 数组逆序,每个元素需要比较 $i-1$ 次 | | **平均时间复杂度** | $O(n^2)$ | 一般情况下的复杂度 | | **空间复杂度** | $O(1)$ | 原地排序,只使用常数空间 | | **稳定性** | 稳定 | 相等元素相对位置不变 | **适用场景**: - 数据量较小($n < 50$) - 数据基本有序 - 在线排序(数据逐个到达) ## 5. 总结 插入排序是一种简单直观的排序算法,通过逐步构建有序序列实现排序。 - **优点**:实现简单,稳定排序,空间复杂度低,对基本有序数据效率高 - **缺点**:时间复杂度高,不适合大规模数据 ## 练习题目 - [0912. 排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md)(插入排序会超时,仅作练习) - [排序算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/01_array/01_06_array_shell_sort.md ================================================ ## 1. 希尔排序算法思想 > **希尔排序(Shell Sort)基本思想**: > > 通过设定不同的间隔(gap),将数组分组进行插入排序,然后逐步缩小间隔直至为 $1$,最终完成整个数组的排序。 ## 2. 希尔排序算法步骤 假设数组长度为 $n$,算法步骤如下: 1. 设定初始间隔 `gap = n / 2`。 2. 按间隔将数组分组,对每组进行插入排序。 3. 缩小间隔 `gap = gap / 2`。 4. 重复步骤 $2 \sim 3$,直到 `gap = 1`。 5. 最后对整个数组进行一次插入排序。 以数组 $[7, 2, 6, 8, 0, 4, 1, 5, 9, 3]$ 为例,演示一下希尔排序的算法步骤。 ::: tabs#shellSort @tab <1> ![希尔排序 1](https://qcdn.itcharge.cn/images/202308162132060.png) @tab <2> ![希尔排序 2](https://qcdn.itcharge.cn/images/202308162132189.png) @tab <3> ![希尔排序 3](https://qcdn.itcharge.cn/images/202308162132870.png) @tab <4> ![希尔排序 4](https://qcdn.itcharge.cn/images/202308162132322.png) @tab <5> ![希尔排序 5](https://qcdn.itcharge.cn/images/202308162132881.png) @tab <6> ![希尔排序 6](https://qcdn.itcharge.cn/images/202308162132386.png) @tab <7> ![希尔排序 7](https://qcdn.itcharge.cn/images/202308162132898.png) ::: ## 3. 希尔排序代码实现 ```python class Solution: def shellSort(self, nums: [int]) -> [int]: size = len(nums) gap = size // 2 # 初始间隔设为数组长度的一半 # 不断缩小gap,直到gap为0 while gap > 0: # 从gap位置开始,对每个元素进行组内插入排序 for i in range(gap, size): temp = nums[i] # 记录当前待插入的元素 j = i # 在组内进行插入排序,将比 temp 大的元素向后移动 while j >= gap and nums[j - gap] > temp: nums[j] = nums[j - gap] # 元素后移 j -= gap # 向前跳 gap 步 nums[j] = temp # 插入到正确位置 # 缩小 gap,通常取 gap 的一半 gap //= 2 return nums # 返回排序后的数组 def sortArray(self, nums: [int]) -> [int]: """排序接口,调用shellSort方法""" return self.shellSort(nums) ``` ## 4. 希尔排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n)$ | 当数组已有序时 | | **最坏时间复杂度** | $O(n^2)$ | 使用普通间隔序列时 | | **平均时间复杂度** | $O(n^{1.3})$ ~ $O(n^{1.5})$ | 取决于间隔序列选择,如果选取得当接近于 $O(n \log n)$ | | **空间复杂度** | $O(1)$ | 原地排序,只使用常数空间 | | **稳定性** | 不稳定 | 不同组间的相等元素可能改变相对顺序 | **补充说明:** - 希尔排序的时间复杂度高度依赖于间隔序列的选择。 - 当采用常见的 `gap = gap // 2` 间隔序列时,排序过程大约需要 $\log_2 n$ 趟,每一趟的操作类似于分组插入排序。 - 每一趟的排序时间复杂度约为 $O(n)$,但随着 gap 的减小,实际操作次数逐步减少。 - 综合来看,希尔排序的整体时间复杂度通常介于 $O(n \log n)$ 和 $O(n^2)$ 之间,如果间隔序列选择得当,性能可接近 $O(n \log n)$。 **适用场景**: - 中等规模数据($50 \leq n \leq 1000$) - 对插入排序的改进需求 - 对稳定性要求不高的场景 ## 5. 总结 希尔排序是插入排序的改进版本,通过分组排序减少数据移动次数,提高排序效率。 - **优点**:比插入排序更快,空间复杂度低,适合中等规模数据 - **缺点**:时间复杂度不稳定,不稳定排序,间隔序列选择影响性能 ## 练习题目 - [0912. 排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) - [0506. 相对名次](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/relative-ranks.md) - [排序算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/01_array/01_07_array_merge_sort.md ================================================ ## 1. 归并排序算法思想 > **归并排序(Merge Sort)基本思想**: > > 利用分治法,将数组递归地一分为二,直至每个子数组只包含一个元素。随后,将这些有序子数组两两合并,最终得到一个整体有序的数组。 ## 2. 归并排序算法步骤 假设数组的元素个数为 $n$ 个,则归并排序的算法步骤如下: 1. **分解过程**:递归地将当前数组平分为两部分,直到每个子数组只包含一个元素为止。 1. 找到数组的中间位置 $mid$,将数组划分为左、右两个子数组 $left\_nums$ 和 $right\_nums$。 2. 分别对 $left\_nums$ 和 $right\_nums$ 递归执行分解操作。 3. 最终将原数组拆分为 $n$ 个长度为 $1$ 的有序子数组。 2. **归并过程**:从长度为 $1$ 的有序子数组开始,逐步将相邻的有序子数组两两合并,最终合并为一个长度为 $n$ 的有序数组。 1. 新建数组 $nums$ 用于存放合并后的有序结果。 2. 设置两个指针 $left\_i$ 和 $right\_i$,分别指向 $left\_nums$ 和 $right\_nums$ 的起始位置。 3. 比较两个指针所指元素,将较小者加入结果数组 $nums$,并将对应指针后移一位。 4. 重复上述操作,直到某一指针到达对应子数组末尾。 5. 将另一个子数组剩余的所有元素依次加入结果数组 $nums$。 6. 返回合并后的有序数组 $nums$。 以数组 $[0, 5, 7, 3, 1, 6, 8, 4]$ 为例,演示一下归并排序的算法步骤。 ![归并排序的算法步骤](http://qcdn.itcharge.cn/images/20230817103814.png) ## 3. 归并排序代码实现 ```python class Solution: # 合并过程 def merge(self, left_nums: [int], right_nums: [int]): nums = [] left_i, right_i = 0, 0 # 合并两个有序子数组 while left_i < len(left_nums) and right_i < len(right_nums): if left_nums[left_i] <= right_nums[right_i]: nums.append(left_nums[left_i]) left_i += 1 else: nums.append(right_nums[right_i]) right_i += 1 # 如果左子数组有剩余元素,则将其插入到结果数组中 while left_i < len(left_nums): nums.append(left_nums[left_i]) left_i += 1 # 如果右子数组有剩余元素,则将其插入到结果数组中 while right_i < len(right_nums): nums.append(right_nums[right_i]) right_i += 1 # 返回合并后的结果数组 return nums # 分解过程 def mergeSort(self, nums: [int]) -> [int]: # 数组元素个数小于等于 1 时,直接返回原数组 if len(nums) <= 1: return nums mid = len(nums) // 2 # 将数组从中间位置分为左右两个数组 left_nums = self.mergeSort(nums[0: mid]) # 递归将左子数组进行分解和排序 right_nums = self.mergeSort(nums[mid:]) # 递归将右子数组进行分解和排序 return self.merge(left_nums, right_nums) # 把当前数组组中有序子数组逐层向上,进行两两合并 def sortArray(self, nums: [int]) -> [int]: return self.mergeSort(nums) ``` ## 4. 归并排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n \log n)$ | 无论数组状态如何,都需要 $\log n$ 次分解和 $n$ 次合并 | | **最坏时间复杂度** | $O(n \log n)$ | 无论数组状态如何,都需要 $\log n$ 次分解和 $n$ 次合并 | | **平均时间复杂度** | $O(n \log n)$ | 归并排序的时间复杂度与数据状态无关 | | **空间复杂度** | $O(n)$ | 需要额外的辅助数组来存储合并结果 | | **稳定性** | 稳定 | 合并过程中相等元素的相对顺序保持不变 | **补充说明:** - 归并排序采用分治策略,将数组递归地分成两半,每次分解的时间复杂度为 $O(1)$,分解次数为 $\log n$。 - 合并过程的时间复杂度为 $O(n)$,因为需要遍历两个子数组的所有元素。 - 总的时间复杂度为 $O(n \log n)$,这是基于比较的排序算法的理论下界。 **适用场景:** - 大规模数据排序($n > 1000$) - 对稳定性有要求的场景 - 外部排序(数据无法全部加载到内存) - 链表排序 ## 5. 总结 归并排序是一种高效稳定的排序算法,采用分治策略将数组递归分解后合并排序。 - **优点**: - 时间复杂度稳定,始终为 $O(n \log n)$ - 稳定排序,相等元素相对位置不变 - 适合大规模数据排序 - 可用于外部排序和链表排序 - **缺点**: - 空间复杂度较高,需要 $O(n)$ 额外空间 - 对于小规模数据,常数因子较大 - 不是原地排序算法 ## 练习题目 - [0912. 排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) - [0088. 合并两个有序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-sorted-array.md) - [LCR 170. 交易逆序对的总数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-de-ni-xu-dui-lcof.md) - [0315. 计算右侧小于当前元素的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-of-smaller-numbers-after-self.md) - [排序算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/01_array/01_08_array_quick_sort.md ================================================ ## 1. 快速排序算法思想 > **快速排序(Quick Sort)基本思想**: > > 采用分治策略,选择一个基准元素,将数组分为两部分:小于基准的元素放在左侧,大于基准的元素放在右侧。然后递归地对左右两部分进行排序,最终得到有序数组。 ## 2. 快速排序算法步骤 快速排序的核心是 **分区操作**,具体步骤如下: 1. **选择基准**:从数组中选择一个元素作为基准值(通常选择第一个元素) 2. **分区操作**: - 使用双指针法,左指针从数组开始,右指针从数组末尾 - 右指针向左移动,找到第一个小于基准值的元素 - 左指针向右移动,找到第一个大于基准值的元素 - 交换这两个元素 - 重复上述过程,直到左右指针相遇 - 将基准值放到正确位置(左右指针相遇处) 3. **递归排序**:对基准值左右的两个子数组分别进行快速排序 以数组 $[4, 7, 5, 2, 6, 1, 3]$ 为例,先来演示一下快速排序的分区操作过程。 ::: tabs#partition @tab <1> ![分区操作 1](https://qcdn.itcharge.cn/images/20230818175908.png) @tab <2> ![分区操作 2](https://qcdn.itcharge.cn/images/20230818175922.png) @tab <3> ![分区操作 3](https://qcdn.itcharge.cn/images/20230818175952.png) @tab <4> ![分区操作 4](https://qcdn.itcharge.cn/images/20230818180001.png) @tab <5> ![分区操作 5](https://qcdn.itcharge.cn/images/20230818180009.png) @tab <6> ![分区操作 6](https://qcdn.itcharge.cn/images/20230818180019.png) @tab <7> ![分区操作 7](https://qcdn.itcharge.cn/images/20230818180027.png) ::: 完成一次分区后,数组被分为三部分:左子数组、基准值、右子数组。然后递归地对左右子数组进行排序。 ![快速排序算法步骤](https://qcdn.itcharge.cn/images/20230818153642.png) ## 3. 快速排序代码实现 ```python import random class Solution: def randomPartition(self, nums: [int], low: int, high: int) -> int: # 随机选择基准值,避免最坏情况 i = random.randint(low, high) # 将基准数与最低位互换 nums[i], nums[low] = nums[low], nums[i] # 随机将基准数移到首位,后续进行分区操作 return self.partition(nums, low, high) # 哨兵划分法(Hoare 法):以 nums[low] 作为基准值 # 左右指针分别从区间两端向中间收缩 # 使比基准值小的元素都移动到基准值左侧 # 使比基准值大的元素都移动到基准值右侧 # 循环后将基准值放入最终的位置,并返回该位置索引 def partition(self, nums: [int], low: int, high: int) -> int: pivot = nums[low] # 选取基准值(当前区间第一个元素) i, j = low, high while i < j: # 从右向左找小于基准值的元素 while i < j and nums[j] >= pivot: j -= 1 # 从左向右找大于基准值的元素 while i < j and nums[i] <= pivot: i += 1 # 交换元素 nums[i], nums[j] = nums[j], nums[i] # 将基准值放到正确位置 nums[i], nums[low] = nums[low], nums[i] # 返回基准数的索引 return i def quickSort(self, nums: [int], low: int, high: int) -> [int]: if low < high: # 分区并获取基准值位置 pivot_i = self.randomPartition(nums, low, high) # 递归排序左右子数组 self.quickSort(nums, low, pivot_i - 1) self.quickSort(nums, pivot_i + 1, high) return nums def sortArray(self, nums: [int]) -> [int]: return self.quickSort(nums, 0, len(nums) - 1) ``` ## 4. 快速排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n \log n)$ | 每次都能将数组平均分成两半 | | **最坏时间复杂度** | $O(n^2)$ | 每次选择的基准值都是极值(如已排序数组) | | **平均时间复杂度** | $O(n \log n)$ | 随机选择基准值时的期望复杂度 | | **空间复杂度** | $O(\log n)$ | 递归栈空间,最坏情况下为 $O(n)$ | | **稳定性** | 不稳定 | 交换操作可能改变相等元素的相对位置 | **适用场景**: - 大规模数据排序($n \geq 1000$) - 对平均性能要求高的场景 - 数据分布相对均匀的情况 **优化策略**: - 随机选择基准值,避免最坏情况 - 三数取中法选择基准值 - 小数组使用插入排序 - 处理重复元素时使用三路快排 ## 5. 总结 快速排序是一种高效的排序算法,采用分治策略,通过分区操作将数组分成两部分,然后递归排序。 - **优点**: - 平均情况下效率高,时间复杂度为 $O(n \log n)$ - 原地排序,空间复杂度低 - 缓存友好,局部性良好 - 实际应用中常数因子较小 - **缺点**: - 不稳定排序 - 最坏情况下性能较差,时间复杂度为 $O(n^2)$ - 对于小数组,其他算法可能更快 - 递归调用可能导致栈溢出 快速排序是许多编程语言内置排序函数的实现基础,在实际应用中非常广泛。通过合理的优化策略,可以显著提高其性能和稳定性。 ## 练习题目 - [0912. 排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) - [0169. 多数元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/majority-element.md) - [排序算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ## 参考资料 - 【文章】[快速排序 - OI Wiki](https://oi-wiki.org/basic/quick-sort/) ================================================ FILE: docs/01_array/01_09_array_heap_sort.md ================================================ ## 1. 堆结构 「堆排序(Heap sort)」是一种基于「堆结构」实现的高效排序算法。在介绍堆排序之前,我们先来了解什么是堆结构。 ### 1.1 堆的定义 > **堆(Heap)**:一种特殊的完全二叉树,具有以下性质之一: > > - **大顶堆(Max Heap)**:任意节点值 ≥ 其子节点值 > - **小顶堆(Min Heap)**:任意节点值 ≤ 其子节点值 ![堆结构](https://qcdn.itcharge.cn/images/20230823133321.png) ### 1.2 堆的存储结构 堆的逻辑结构是一棵完全二叉树,如下图所示: ![堆的逻辑结构](https://qcdn.itcharge.cn/images/202405092006120.png) 在实际编程中,堆通常采用数组进行存储。使用数组表示堆时,节点与数组索引之间的对应关系如下: - 如果某节点的下标为 $i$,则其左孩子的下标为 $2 \times i + 1$,右孩子的下标为 $2 \times i + 2$; - 如果某节点的下标为 $i$,则其父节点的下标为 $\lfloor \frac{i - 1}{2} \rfloor$。 如下图所示,顺序存储结构(数组)可以高效地表示堆: ![使用顺序存储结构(数组)表示堆](https://qcdn.itcharge.cn/images/202405092007823.png) ### 1.3 堆的基本操作 #### 1.3.1 创建空堆 > **创建空堆**:初始化一个空的堆结构,为后续的堆操作做准备。 创建空堆是堆操作的基础,只需要初始化一个空数组即可。在实际应用中,我们通常创建一个类来封装堆的各种操作。 ```python class MaxHeap: def __init__(self): # 创建空的大顶堆 self.max_heap = [] ``` **时间复杂度**:$O(1)$。空堆创建只需要初始化一个空数组,操作非常简单高效。 #### 1.3.2 访问堆顶元素 > **访问堆顶元素**:获取堆中最大(或最小)的元素,即根节点的值。 在大顶堆中,堆顶元素就是整个堆中的最大值;在小顶堆中,堆顶元素就是整个堆中的最小值。由于堆顶元素总是存储在数组的第一个位置,因此访问操作非常高效。 ```python def peek(self) -> int: # 检查堆是否为空 if not self.max_heap: return None # 返回堆顶元素(数组第一个元素) return self.max_heap[0] ``` **时间复杂度**:$O(1)$。由于堆顶元素始终位于数组索引 0 的位置,访问操作只需要一次数组索引操作,不依赖于堆的大小。 #### 1.3.3 向堆中插入元素 > **向堆中插入元素**:将新元素添加到堆中,并通过调整保持堆的性质。 向堆中插入元素需要两个步骤: 1. **添加元素**:将新元素添加到堆的末尾,保持完全二叉树的结构 2. **上移调整**:从新元素开始向上调整,直到满足堆的性质 **上移调整(Shift Up)过程**: - 将新插入的节点与其父节点比较 - 如果新节点值大于父节点值,则交换它们 - 重复此过程,直到新节点不再大于其父节点或到达根节点 下面我们通过图示步骤来演示一下向堆中插入元素的过程。 ::: tabs#heapPush @tab <1> ![向堆中插入元素1](https://qcdn.itcharge.cn/images/20230831111022.png) @tab <2> ![向堆中插入元素2](https://qcdn.itcharge.cn/images/20230831111036.png) @tab <3> ![向堆中插入元素3](https://qcdn.itcharge.cn/images/20230831111052.png) @tab <4> ![向堆中插入元素4](https://qcdn.itcharge.cn/images/20230831111103.png) @tab <5> ![向堆中插入元素5](https://qcdn.itcharge.cn/images/20230831112321.png) @tab <6> ![向堆中插入元素6](https://qcdn.itcharge.cn/images/20230831112328.png) @tab <7> ![向堆中插入元素7](https://qcdn.itcharge.cn/images/20230831134124.png) ::: ```python def push(self, val: int): # 将新元素添加到堆的末尾 self.max_heap.append(val) # 从新元素开始进行上移调整 self.__shift_up(len(self.max_heap) - 1) def __shift_up(self, i: int): # 上移调整:将节点与其父节点比较并交换 while (i - 1) // 2 >= 0 and self.max_heap[i] > self.max_heap[(i - 1) // 2]: # 交换当前节点与父节点 self.max_heap[i], self.max_heap[(i - 1) // 2] = self.max_heap[(i - 1) // 2], self.max_heap[i] # 继续向上调整 i = (i - 1) // 2 ``` **时间复杂度**:$O(\log n)$。在最坏情况下,新插入的元素需要从堆的底部移动到顶部,移动的距离等于堆的高度 $\log n$。 #### 1.3.4 删除堆顶元素 > **删除堆顶元素**:移除堆中的最大(或最小)元素,并重新调整堆结构。 删除堆顶元素需要三个步骤: 1. **交换元素**:将堆顶元素与末尾元素交换 2. **删除元素**:移除末尾元素(原堆顶元素) 3. **下移调整**:从新的堆顶开始向下调整,直到满足堆的性质 **下移调整(Shift Down)过程**: - 将新的堆顶元素与其较大的子节点比较 - 如果堆顶元素小于较大子节点,则交换它们 - 重复此过程,直到堆顶元素不再小于其子节点或到达叶子节点 这个过程称为「下移调整」,因为新的堆顶元素会逐步向堆的下方移动,直到找到合适的位置。 下面我们通过图示步骤来演示一下删除堆顶元素的过程。 ::: tabs#heapPop @tab <1> ![删除堆顶元素 1](https://qcdn.itcharge.cn/images/20230831134148.png) @tab <2> ![删除堆顶元素 2](https://qcdn.itcharge.cn/images/20230831134156.png) @tab <3> ![删除堆顶元素 3](https://qcdn.itcharge.cn/images/20230831134205.png) @tab <4> ![删除堆顶元素 4](https://qcdn.itcharge.cn/images/20230831134214.png) @tab <5> ![删除堆顶元素 5](https://qcdn.itcharge.cn/images/20230831134221.png) @tab <6> ![删除堆顶元素 6](https://qcdn.itcharge.cn/images/20230831134229.png) @tab <7> ![删除堆顶元素 7](https://qcdn.itcharge.cn/images/20230831134237.png) ::: ```python def pop(self) -> int: # 检查堆是否为空 if not self.max_heap: raise IndexError("堆为空") # 交换堆顶元素与末尾元素 size = len(self.max_heap) self.max_heap[0], self.max_heap[size - 1] = self.max_heap[size - 1], self.max_heap[0] # 删除末尾元素(原堆顶元素) val = self.max_heap.pop() # 如果堆不为空,进行下移调整 if self.max_heap: self.__shift_down(0, len(self.max_heap)) # 返回被删除的堆顶元素 return val def __shift_down(self, i: int, n: int): # 下移调整:将节点与其较大的子节点比较并交换 while 2 * i + 1 < n: # 计算左右子节点索引 left, right = 2 * i + 1, 2 * i + 2 # 找出较大的子节点 larger = left if right < n and self.max_heap[right] > self.max_heap[left]: larger = right # 如果当前节点小于较大子节点,则交换 if self.max_heap[i] < self.max_heap[larger]: self.max_heap[i], self.max_heap[larger] = self.max_heap[larger], self.max_heap[i] i = larger else: break ``` **时间复杂度**:$O(\log n)$。在最坏情况下,新的堆顶元素需要从堆的顶部移动到底部,移动的距离等于堆的高度 $\log n$。 ## 2. 堆排序 ### 2.1 堆排序算法思想 > **堆排序(Heap sort)基本思想**: > > 利用堆的特性,将数组构建成大顶堆,然后重复取出堆顶元素(最大值)并调整堆结构,最终得到有序数组。 ### 2.2 堆排序算法步骤 堆排序分为两个主要阶段: **第一阶段:构建初始大顶堆** 1. 将原始数组视为完全二叉树 2. 从最后一个非叶子节点开始,自底向上进行下移调整 3. 将数组转换为大顶堆 ::: tabs#heapSortBuildMaxHeap @tab <1> ![构建初始大顶堆 1](https://qcdn.itcharge.cn/images/20230831151620.png) @tab <2> ![构建初始大顶堆 2](https://qcdn.itcharge.cn/images/20230831151641.png) @tab <3> ![构建初始大顶堆 3](https://qcdn.itcharge.cn/images/20230831151703.png) @tab <4> ![构建初始大顶堆 4](https://qcdn.itcharge.cn/images/20230831151715.png) @tab <5> ![构建初始大顶堆 5](https://qcdn.itcharge.cn/images/20230831151725.png) @tab <6> ![构建初始大顶堆 6](https://qcdn.itcharge.cn/images/20230831151735.png) @tab <7> ![构建初始大顶堆 7](https://qcdn.itcharge.cn/images/20230831151749.png) ::: **第二阶段:重复提取最大值** 1. 交换堆顶元素与当前末尾元素 2. 堆长度减 $1$,末尾元素已排好序 3. 对新的堆顶元素进行下移调整,恢复堆的性质 4. 重复步骤 $1 \sim 3$,直到堆的大小为 $1$ ::: tabs#heapSortExchangeVal @tab <1> ![交换元素,调整堆 1](https://qcdn.itcharge.cn/images/20230831162335.png) @tab <2> ![交换元素,调整堆 2](https://qcdn.itcharge.cn/images/20230831162346.png) @tab <3> ![交换元素,调整堆 3](https://qcdn.itcharge.cn/images/20230831162359.png) @tab <4> ![交换元素,调整堆 4](https://qcdn.itcharge.cn/images/20230831162408.png) @tab <5> ![交换元素,调整堆 5](https://qcdn.itcharge.cn/images/20230831162416.png) @tab <6> ![交换元素,调整堆 6](https://qcdn.itcharge.cn/images/20230831162424.png) @tab <7> ![交换元素,调整堆 7](https://qcdn.itcharge.cn/images/20230831162431.png) @tab <8> ![交换元素,调整堆 8](https://qcdn.itcharge.cn/images/20230831162440.png) @tab <9> ![交换元素,调整堆 9](https://qcdn.itcharge.cn/images/20230831162449.png) @tab <10> ![交换元素,调整堆 10](https://qcdn.itcharge.cn/images/20230831162457.png) @tab <11> ![交换元素,调整堆 11](https://qcdn.itcharge.cn/images/20230831162505.png) @tab <12> ![交换元素,调整堆 12](https://qcdn.itcharge.cn/images/20230831162512.png) ::: ### 2.3 堆排序代码实现 ```python class MaxHeap: def __init__(self): self.max_heap = [] def __buildMaxHeap(self, nums: [int]): # 将数组元素复制到堆中 self.max_heap = nums.copy() size = len(nums) # 从最后一个非叶子节点开始,自底向上构建堆 for i in range((size - 2) // 2, -1, -1): self.__shift_down(i, size) def maxHeapSort(self, nums: [int]) -> [int]: # 第一阶段:构建初始大顶堆 self.__buildMaxHeap(nums) size = len(self.max_heap) # 第二阶段:重复提取最大值 for i in range(size - 1, -1, -1): # 交换堆顶元素与当前末尾元素 self.max_heap[0], self.max_heap[i] = self.max_heap[i], self.max_heap[0] # 对新的堆顶元素进行下移调整,堆的大小为 i self.__shift_down(0, i) # 返回排序后的数组 return self.max_heap def __shift_down(self, i: int, n: int): # 下移调整:将节点与其较大的子节点比较并交换 while 2 * i + 1 < n: left, right = 2 * i + 1, 2 * i + 2 # 找出较大的子节点 larger = left if right < n and self.max_heap[right] > self.max_heap[left]: larger = right # 如果当前节点小于较大子节点,则交换 if self.max_heap[i] < self.max_heap[larger]: self.max_heap[i], self.max_heap[larger] = self.max_heap[larger], self.max_heap[i] i = larger else: break class Solution: def sortArray(self, nums: [int]) -> [int]: return MaxHeap().maxHeapSort(nums) ``` ### 2.4 堆排序算法分析 **时间复杂度分析**: 堆排序的时间复杂度由两个主要步骤组成: 1. **构建初始堆**:$O(n)$ - 从最后一个非叶子节点开始,自底向上进行下移调整 - 对于第 $i$ 层的节点,最多需要下移 $(\log n - i)$ 层 - 第 $i$ 层有 $2^i$ 个节点 - 总调整次数:$\sum_{i=0}^{\log n - 1} 2^i \cdot (\log n - i) = O(n)$ 2. **重复提取最大值**:$O(n \log n)$ - 需要进行 $n$ 次提取操作 - 每次提取后需要下移调整,最坏情况下需要 $O(\log n)$ 时间 - 总时间复杂度:$n \times O(\log n) = O(n \log n)$ **总时间复杂度**:$O(n) + O(n \log n) = O(n \log n)$ | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n \log n)$ | 无论数组状态如何,都需要构建堆和提取元素 | | **最坏时间复杂度** | $O(n \log n)$ | 无论数组状态如何,都需要构建堆和提取元素 | | **平均时间复杂度** | $O(n \log n)$ | 堆排序的时间复杂度与数据状态无关 | | **空间复杂度** | $O(1)$ | 原地排序,只使用常数空间 | | **稳定性** | 不稳定 | 调整堆的过程中可能改变相等元素的相对顺序 | **适用场景**: - 大规模数据排序 - 内存受限的环境 - 需要稳定时间复杂度的场景 - 需要保证最坏情况下性能的场景 ## 3. 总结 堆排序是一种基于堆数据结构的排序算法,利用堆的特性实现高效排序。 **核心思想**: - 将数组构建成大顶堆,堆顶元素始终是最大值 - 重复取出堆顶元素并调整堆结构,最终得到有序数组 **算法步骤**: 1. **构建初始堆**:将数组转换为大顶堆 2. **重复提取**:交换堆顶与末尾元素,调整堆结构,逐步得到有序数组 - **优点**: - 时间复杂度稳定,始终为 $O(n \log n)$ - 空间复杂度低,为 $O(1)$ - 适合处理大规模数据 - 原地排序,不需要额外空间 - **缺点**: - 不稳定排序 - 常数因子较大,实际应用中可能比快速排序稍慢 - 对缓存不友好,访问模式不够局部化 堆排序是一种同时具备 $O(n \log n)$ 时间复杂度和 $O(1)$ 空间复杂度的比较排序算法,在内存受限或需要稳定时间复杂度的场景下具有重要价值。 ## 练习题目 - [0912. 排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) - [0215. 数组中的第K个最大元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/kth-largest-element-in-an-array.md) - [LCR 159. 库存管理 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zui-xiao-de-kge-shu-lcof.md) - [排序算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/01_array/01_10_array_counting_sort.md ================================================ ## 1. 计数排序算法思想 > **计数排序(Counting Sort)基本思想**: > > 统计数组中每个元素出现的次数,然后根据统计信息将元素按顺序放置到正确位置,实现排序。 ## 2. 计数排序算法步骤 1. **确定数值范围**:找出数组中的最大值和最小值,计算数值范围。 2. **创建计数数组**:创建一个大小为数值范围的数组,用于统计每个元素出现的次数。 3. **统计元素频次**:遍历原数组,统计每个元素出现的次数。 4. **计算累积频次**:将计数数组转换为累积频次数组,表示每个元素在排序后数组中的位置。 5. **逆序填充结果**:逆序遍历原数组,根据累积频次将元素放入正确位置。 以数组 $[3, 0, 4, 2, 5, 1, 3, 1, 4, 5]$ 为例,演示一下计数排序的算法步骤。 ![计数排序的算法步骤](https://qcdn.itcharge.cn/images/20230822135634.png) ## 3. 计数排序代码实现 ```python class Solution: def countingSort(self, nums: [int]) -> [int]: # 确定数值范围 nums_min, nums_max = min(nums), max(nums) size = nums_max - nums_min + 1 counts = [0 for _ in range(size)] # 统计每个元素出现的次数 for num in nums: counts[num - nums_min] += 1 # 计算累积频次(每个元素出现的次数) for i in range(1, size): counts[i] += counts[i - 1] # 逆序填充结果数组 res = [0 for _ in range(len(nums))] for i in range(len(nums) - 1, -1, -1): num = nums[i] # 根据累积计数数组,将 num 放在数组对应位置 res[counts[num - nums_min] - 1] = num counts[num - nums_min] -= 1 return res def sortArray(self, nums: [int]) -> [int]: return self.countingSort(nums) ``` ## 4. 计数排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n + k)$ | $n$ 为元素个数,$k$ 为数值范围,无论数组状态如何,都需要统计和填充操作 | | **最坏时间复杂度** | $O(n + k)$ | 无论数组状态如何,都需要统计和填充操作 | | **平均时间复杂度** | $O(n + k)$ | 计数排序的时间复杂度与数据状态无关 | | **空间复杂度** | $O(k)$ | 需要额外的计数数组,大小取决于数值范围 | | **稳定性** | 稳定 | 逆序填充保持相等元素的相对顺序 | **适用场景**: - 整数排序 - 数值范围较小($k$ 远小于 $n$) - 对稳定性有要求的场景 ## 5. 总结 计数排序是一种非比较排序算法,通过统计元素频次实现排序。它特别适合数值范围较小的整数排序。 - **优点**:时间复杂度稳定,稳定排序,适合小范围整数排序 - **缺点**:空间复杂度与数值范围相关,不适合大范围数值 ## 练习题目 - [0912. 排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) - [1122. 数组的相对排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/relative-sort-array.md) - [排序算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/01_array/01_11_array_bucket_sort.md ================================================ ## 1. 桶排序算法思想 > **桶排序(Bucket Sort)**: > > 将待排序元素分散到多个桶中,对每个桶单独排序后合并。 ## 2. 桶排序算法步骤 1. **确定桶的数量**:根据待排序数组的数值范围,将其划分为 $k$ 个桶,每个桶对应一个特定的区间。 2. **元素分配**:遍历数组,将每个元素根据其数值映射到所属的桶中。 3. **桶内排序**:对每个非空桶分别进行排序(可选用插入排序、归并排序、快速排序等算法)。 4. **合并结果**:按桶的顺序依次合并所有已排序的桶,得到最终有序数组。 以数组 $[39, 49, 8, 13, 22, 15, 10, 30, 5, 44]$ 为例,演示一下桶排序的算法步骤。 ![桶排序的算法步骤](https://qcdn.itcharge.cn/images/20230822153701.png) ## 3. 桶排序代码实现 ```python class Solution: def insertionSort(self, nums: [int]) -> [int]: # 遍历无序区间 for i in range(1, len(nums)): temp = nums[i] j = i # 从右至左遍历有序区间 while j > 0 and nums[j - 1] > temp: # 将有序区间中插入位置右侧的元素依次右移一位 nums[j] = nums[j - 1] j -= 1 # 将该元素插入到适当位置 nums[j] = temp return nums def bucketSort(self, nums: [int], bucket_size=5) -> [int]: # 计算数据范围 nums_min, nums_max = min(nums), max(nums) bucket_count = (nums_max - nums_min) // bucket_size + 1 # 定义桶数组 buckets buckets = [[] for _ in range(bucket_count)] # 遍历待排序数组元素,将每个元素根据大小分配到对应的桶中 for num in nums: buckets[(num - nums_min) // bucket_size].append(num) # 排序并合并 res = [] for bucket in buckets: self.insertionSort(bucket) res.extend(bucket) # 返回结果数组 return res def sortArray(self, nums: [int]) -> [int]: return self.bucketSort(nums) ``` ## 4. 桶排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n)$ | 数据分布均匀,每个桶内元素数量相近 | | **最坏时间复杂度** | $O(n^2)$ | 数据集中在少数桶中,桶内排序复杂度高 | | **平均时间复杂度** | $O(n + k)$ | $k$ 为桶的数量,数据分布较均匀时接近 $O(n)$ | | **空间复杂度** | $O(n + m)$ | 需要额外空间存储桶,$m$ 为桶的数量 | | **稳定性** | 稳定 | 取决于桶内排序算法,通常使用稳定排序 | **适用场景**: - 数据分布均匀 - 外部排序 - 数据范围已知且有限 ## 5. 总结 桶排序是一种分布式排序算法,通过将数据分散到多个桶中,对每个桶单独排序后合并实现排序。 - **优点**:数据分布均匀时效率高,适合外部排序,可并行处理 - **缺点**:需要额外空间,数据分布不均匀时效率下降,对数据范围有要求 ## 练习题目 - [0912. 排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) - [0220. 存在重复元素 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate-iii.md) - [0164. 最大间距](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-gap.md) - [排序算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/01_array/01_12_array_radix_sort.md ================================================ ## 1. 基数排序算法思想 > **基数排序(Radix Sort)基本思想**: > > 按照数字的每一位进行排序,从最低位到最高位,逐位比较。 ## 2. 基数排序算法步骤 基数排序算法可以采用「最低位优先法(Least Significant Digit First)」或者「最高位优先法(Most Significant Digit first)」。最常用的是「最低位优先法」。 下面我们以最低位优先法为例,讲解一下算法步骤。 1. **确定最大位数**:遍历数组元素,找到数组中最大值的位数。 2. **从最低位(个位)开始,到最高位为止,逐位对每一位进行排序**: 1. 创建 10 个桶(每个桶分别代表 $0 \sim 9$ 中的一个数字)。 2. 按照每个元素当前位上的数字,将元素放入对应桶中。 3. 清空原始数组,然后按照桶的顺序依次取出对应元素,重新加入到数组中。 我们以 $[692, 924, 969, 503, 871, 704, 542, 436]$ 为例,演示一下基数排序的算法步骤。 ![基数排序的算法步骤](https://qcdn.itcharge.cn/images/20230822171758.png) ## 3. 基数排序代码实现 ```python class Solution: def radixSort(self, nums: [int]) -> [int]: # 获取最大位数 size = len(str(max(nums))) # 从个位开始逐位排序 for i in range(size): # 创建 10 个桶,每个桶分别代表 0 ~ 9 中的 1 个数字 buckets = [[] for _ in range(10)] # 按当前位数字分桶 for num in nums: buckets[num // (10 ** i) % 10].append(num) # 重新收集 nums.clear() for bucket in buckets: for num in bucket: nums.append(num) # 完成排序,返回结果数组 return nums def sortArray(self, nums: [int]) -> [int]: return self.radixSort(nums) ``` ## 4. 基数排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n \times k)$ | 所有数字位数相同,$k$ 为最大位数 | | **最坏时间复杂度** | $O(n \times k)$ | 所有数字位数相同,$k$ 为最大位数 | | **平均时间复杂度** | $O(n \times k)$ | 基数排序的时间复杂度与数据状态无关 | | **空间复杂度** | $O(n + k)$ | 需要 $n$ 个元素的存储空间和 $k$ 个桶 | | **稳定性** | 稳定 | 桶排序保证相等元素的相对位置不变 | **适用场景**: - 整数排序,位数不多($k$ 较小) - 数据范围大但位数固定 - 电话号码、身份证号等固定位数数据 ## 5. 总结 基数排序是一种非比较排序算法,通过按位分配和收集实现排序。 - **优点**:时间复杂度与数据范围无关,稳定排序,适合固定位数数据 - **缺点**:空间复杂度较高,只适用于整数排序 ## 练习题目 - [0912. 排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) - [0164. 最大间距](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-gap.md) - [0561. 数组拆分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/array-partition.md) - [排序算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/01_array/01_13_array_binary_search_01.md ================================================ ## 1. 二分查找算法介绍 ### 1.1 算法简介 > **二分查找算法(Binary Search Algorithm)**,又称折半查找或对数查找,是一种在有序数组中高效定位目标元素的方法。其核心思想是每次将查找区间缩小一半,从而快速锁定目标位置。 ### 1.2 算法步骤 1. **初始化**:确定待查找的有序数据集合(如数组或列表),并确保元素已按升序或降序排列。 2. **设置查找区间**:定义查找的左右边界,初始时 $left$ 指向数组起始位置,$right$ 指向数组末尾位置。 3. **计算中间下标**:通过 $mid = \lfloor (left + right) / 2 \rfloor$ 计算当前查找区间的中间下标 $mid$。 4. **比较并缩小区间**:将目标值 $target$ 与 $nums[mid]$ 比较: 1. 如果 $target == nums[mid]$,则找到目标,返回 $mid$。 2. 如果 $target < nums[mid]$,目标在左半区间,更新右边界 $right = mid - 1$。 3. 如果 $target > nums[mid]$,目标在右半区间,更新左边界 $left = mid + 1$。 5. 重复步骤 $3 \sim 4$,直到找到目标元素(返回 $mid$),或查找区间为空($left > right$),此时返回 $-1$,表示目标不存在。 我们以在有序数组 $[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]$ 中查找目标元素 $6$ 为例,二分查找的具体过程如下: 1. **设置查找区间**:左边界 $left = 0$(数组起始位置),右边界 $right = 10$(数组末尾位置),查找区间为 $[0, 10]$。 2. **第一次取中间元素**:$mid = (0 + 10) \div 2 = 5$,$nums[5] = 5$。 3. **比较目标值与中间元素**:$6 > 5$,目标值在右半区间,更新左边界 $left = 6$,查找区间变为 $[6, 10]$。 4. **第二次取中间元素**:$mid = (6 + 10) \div 2 = 8$,$nums[8] = 8$。 5. **再次比较**:$6 < 8$,目标值在左半区间,更新右边界 $right = 7$,查找区间变为 $[6, 7]$。 6. **第三次取中间元素**:$mid = (6 + 7) \div 2 = 6$,$nums[6] = 6$。 7. **找到目标值**:$6 == 6$,查找成功,返回下标 $6$,算法结束。 可以看到,对于一个长度为 $10$ 的有序数组,使用二分查找仅需 $3$ 次比较就能定位目标元素;而如果采用顺序遍历,最坏情况下则需要 $10$ 次比较才能找到目标。这充分体现了二分查找在有序数据中的高效性。 下面展示了二分查找算法在有序数组中查找目标元素 $6$ 的完整过程,详细说明了每一步如何更新查找区间、选择中间元素,并最终定位到目标元素的位置。 ::: tabs#BinarySearch @tab <1> ![二分查找算法 1](https://qcdn.itcharge.cn/images/20230907144632.png) @tab <2> ![二分查找算法 2](https://qcdn.itcharge.cn/images/20230906133742.png) @tab <3> ![二分查找算法 3](https://qcdn.itcharge.cn/images/20230906133758.png) @tab <4> ![二分查找算法 4](https://qcdn.itcharge.cn/images/20230906133809.png) @tab <5> ![二分查找算法 5](https://qcdn.itcharge.cn/images/20230906133820.png) @tab <6> ![二分查找算法 6](https://qcdn.itcharge.cn/images/20230906133830.png) @tab <7> ![二分查找算法 7](https://qcdn.itcharge.cn/images/20230906133839.png) @tab <8> ![二分查找算法 8](https://qcdn.itcharge.cn/images/20230906133848.png) ::: ### 1.3 二分查找算法思想 二分查找算法体现了经典的 **「减而治之」** 思想。 所谓 **「减」**,就是每一步都通过条件判断,排除掉一部分一定不包含目标元素的区间,从而缩小问题规模;**「治」**,则是在缩小后的区间内继续解决剩下的子问题。也就是说,二分查找的核心在于:**每次查找都排除掉不可能存在目标的区间,仅在可能存在目标的区间内继续查找**。 通过不断缩小查找区间,问题规模逐步减小。由于区间有限,经过有限次迭代,最终要么找到目标元素,要么确定目标不存在于数组中。 ## 2. 简单二分查找 下面通过一个简单的例子来讲解下二分查找的思路和代码。 - 题目链接:[704. 二分查找](https://leetcode.cn/problems/binary-search/) ### 2.1 题目大意 **描述**:给定一个升序的数组 $nums$,和一个目标值 $target$。 **要求**:返回 $target$ 在数组中的位置,如果找不到,则返回 $-1$。 **说明**: - 你可以假设 $nums$ 中的所有元素是不重复的。 - $n$ 将在 $[1, 10000]$ 之间。 - $nums$ 的每个元素都将在 $[-9999, 9999]$之间。 **示例**: ```python 输入: nums = [-1,0,3,5,9,12], target = 9 输出: 4 解释: 9 出现在 nums 中并且下标为 4 输入: nums = [-1,0,3,5,9,12], target = 2 输出: -1 解释: 2 不存在 nums 中因此返回 -1 ``` ### 2.2 解题思路 #### 思路 1:二分查找 1. 初始化左右边界,令 $left = 0$,$right = len(nums) - 1$,即查找区间为 $[left, right]$(左闭右闭)。 2. 在每轮循环中,计算中间位置 $mid$,比较 $nums[mid]$ 与目标值 $target$: 1. 如果 $nums[mid] == target$,直接返回 $mid$。 2. 如果 $nums[mid] < target$,则目标值只可能在右半区间,将 $left$ 更新为 $mid + 1$。 3. 如果 $nums[mid] > target$,则目标值只可能在左半区间,将 $right$ 更新为 $mid - 1$。 3. 当 $left > right$ 时,说明查找区间已为空,目标值不存在,返回 $-1$。 #### 思路 1:代码 ```python class Solution: def search(self, nums: List[int], target: int) -> int: left, right = 0, len(nums) - 1 # 循环查找区间为 [left, right],直到区间为空 while left <= right: # 计算中间位置,防止溢出可写为 left + (right - left) // 2 mid = (left + right) // 2 # 命中目标,直接返回下标 if nums[mid] == target: return mid # 目标在右半区间,收缩左边界 elif nums[mid] < target: left = mid + 1 # 目标在左半区间,收缩右边界 else: right = mid - 1 # 查找失败,返回 -1 return -1 ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(1)$。 ## 3. 总结 ### 3.1 核心要点 **二分查找** 是一种在 **有序数组** 中高效查找目标元素的算法,其核心思想是 **每次将查找区间缩小一半**,从而快速定位目标位置。 ### 3.2 算法特点 - **时间复杂度**:$O(\log n)$,比线性查找 $O(n)$ 更高效。 - **空间复杂度**:$O(1)$,只需要常数级别的额外空间。 - **适用条件**:数据必须是有序的(升序或降序)。 - **核心思想**:减而治之,每次排除一半不可能的区域。 ### 3.3 实现要点 1. **区间定义**:使用左闭右闭区间 `[left, right]`。 2. **中间计算**:`mid = (left + right) // 2` 或 `mid = left + (right - left) // 2`(防溢出)。 3. **边界更新**: - 目标在右半区间:`left = mid + 1` - 目标在左半区间:`right = mid - 1` 4. **终止条件**:`left > right` 时查找失败。 ### 3.4 应用场景 - 在有序数组中查找特定元素 - 查找插入位置 - 寻找边界值 - 数值范围查询 二分查找是算法学习中的基础算法,掌握其思想和实现对于解决更复杂的查找问题具有重要意义。 ## 练习题目 - [0704. 二分查找](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/binary-search.md) - [0374. 猜数字大小](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/guess-number-higher-or-lower.md) - [0035. 搜索插入位置](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-insert-position.md) - [0167. 两数之和 II - 输入有序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/two-sum-ii-input-array-is-sorted.md) - [二分查找题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/01_array/01_14_array_binary_search_02.md ================================================ ## 1. 二分查找细节 在上一节中,我们已经掌握了二分查找的基本思路和实现代码。然而,在实际解题过程中,二分查找还涉及许多关键细节,常见的有以下几个方面: 1. **区间的开闭选择**:查找区间应采用左闭右闭 $[left, right]$,还是左闭右开 $[left, right)$? 2. **$mid$ 的计算方式**:是 $mid = \lfloor \frac{left + right}{2} \rfloor$,还是 $mid = \lfloor \frac{left + right + 1}{2} \rfloor$? 3. **循环终止条件**:应使用 $left \le right$ 还是 $left < right$? 4. **区间收缩方式**:如 $left = mid + 1$、$right = mid - 1$、$left = mid$、$right = mid$ 等,应该如何选择? 接下来将针对这些细节逐一分析说明。 ## 2. 区间的开闭问题 在二分查找中,区间的开闭方式决定了查找范围的边界取值。常见的有两种: - **左闭右闭区间 $[left, right]$**:初始化时,$left = 0$,$right = len(nums) - 1$。此时 $left$ 和 $right$ 都指向有效元素,区间两端的元素都包含在查找范围内。 - **左闭右开区间 $[left, right)$**:初始化时,$left = 0$,$right = len(nums)$。$left$ 指向第一个元素,$right$ 指向最后一个元素的下一个位置,查找范围包含左端点但不包含右端点。 虽然两种区间写法都可以实现二分查找,但在实际编码和边界处理时,左闭右开区间往往更容易出错,需要额外关注边界条件,逻辑也更复杂。因此,**强烈推荐统一采用「左闭右闭区间」的写法**,这样更易于理解和维护,出错概率更低。 ## 3. $mid$ 的取值问题 在实际应用二分查找时,$mid$ 的取值通常有两种常见写法: 1. `mid = (left + right) // 2` 2. `mid = (left + right + 1) // 2` 这里的 `//` 表示向下取整。如果当前查找区间元素个数为奇数,这两种写法都会得到区间正中间的下标。 当区间元素个数为偶数时,`mid = (left + right) // 2` 会取到中间偏左的下标,而 `mid = (left + right + 1) // 2` 则会取到中间偏右的下标。 ::: tabs#mid @tab <1> ![mid 取值问题 1](https://qcdn.itcharge.cn/images/20230906153359.png) @tab <2> ![mid 取值问题 2](https://qcdn.itcharge.cn/images/20230906153409.png) ::: 将这两个公式分别应用到 [704. 二分查找](https://leetcode.cn/problems/binary-search/) 的代码中,会发现它们都能通过题目测试。这是为什么? 原因在于,二分查找的核心思想是:每次根据中间元素的值,决定下一步在哪个区间继续查找。实际上,中间元素的位置不必严格取区间正中,偏左、偏右,甚至取区间的三分之一、五分之一等位置都可以,例如 `mid = (left + right) * 1 // 5` 也是可行的。 不过,通常取区间中点能在平均意义下获得最优效率,且实现最为简洁。因此,实际编码时大多数情况下会选择第一个公式。但在某些特定场景下,需要用到第二个公式,具体会在「5.2 排除法」部分详细说明。 除了上述两种写法,我们还常见如下两种等价公式: 1. `mid = left + (right - left) // 2` 2. `mid = left + (right - left + 1) // 2` 这两种写法本质上与前面的公式等价,只是通过减法避免了整型溢出的问题(虽然 Python 不会溢出,但其他语言可能会)。当 $left + right$ 不会超过整型最大值时,哪种写法都可以;但如果有溢出风险,推荐使用后一种写法。 因此,为了统一和简化二分查找的实现,建议采用如下写法: 1. `mid = left + (right - left) // 2` 2. `mid = left + (right - left + 1) // 2` ## 4. 出界条件的判断 在二分查找的实现中,`while` 循环的边界判断主要有两种常见写法: 1. `left <= right` 2. `left < right` 那么,实际编码时应如何选择呢?我们可以从循环终止的条件来分析: - 当使用 `left <= right` 作为循环条件时,如果目标元素不存在,循环会在 `left > right` 时终止,即 $[right + 1, right]$,此时查找区间已为空,无需再判断,直接返回 $-1$ 即可。例如区间 $[3, 2]$,左边界大于右边界,查找结束。 - 当使用 `left < right` 作为循环条件时,如果目标元素不存在,循环会在 `left == right` 时终止,即 $[right, right]$,此时区间内还剩下一个元素。此时不能直接返回 $-1$,因为最后一个元素可能就是目标值。例如区间 $[2, 2]$,$nums[2]$ 可能等于 $target$,直接返回 $-1$ 会遗漏正确答案。 如果选择 `left < right`,则需要在循环结束后额外判断 $nums[left]$ 是否等于目标值: ```python # ... while left < right: # ... return left if nums[left] == target else -1 ``` 另外,采用 `while left < right` 作为循环条件的一个优点是,循环结束时必然有 `left == right`,此时只需判断一个位置,无需区分返回 $left$ 还是 $right$,简化了后续处理。 ## 5. 搜索区间范围的选择 在选择二分查找的区间更新方式时,常见有三种写法: 1. `left = mid + 1`,`right = mid - 1` 2. `left = mid + 1`,`right = mid` 3. `left = mid`,`right = mid - 1` 那么,究竟该如何确定具体的区间更新方式呢? 这正是二分查找中最容易出错的地方,区间更新不当容易导致死循环或结果错误。 本质上,这与二分查找的两种核心思路和三种区间写法密切相关: - 思路一:「直接法」—— 在循环体内一旦找到目标元素立即返回。 - 思路二:「排除法」—— 每次循环排除目标元素一定不存在的区间。 下面我们将详细介绍这两种思路的具体实现和适用场景。 ## 5.1 直接法思路 > **直接法思想**:在循环过程中,一旦找到目标元素,立即返回其下标。 这种方法实现简单,实际上我们在前文「1.13 二分查找(一)- 2. 简单二分查找」中已经用过。下面简要回顾其核心思路与代码: #### 思路 1:直接法 1. 初始化左右边界,令 $left = 0$,$right = len(nums) - 1$,即查找区间为 $[left, right]$(左闭右闭)。 2. 在每轮循环中,计算中间位置 $mid$,比较 $nums[mid]$ 与目标值 $target$: 1. 如果 $nums[mid] == target$,直接返回 $mid$。 2. 如果 $nums[mid] < target$,则目标值只可能在右半区间,将 $left$ 更新为 $mid + 1$。 3. 如果 $nums[mid] > target$,则目标值只可能在左半区间,将 $right$ 更新为 $mid - 1$。 3. 当 $left > right$ 时,说明查找区间已为空,目标值不存在,返回 $-1$。 #### 思路 1:代码 ```python class Solution: def search(self, nums: List[int], target: int) -> int: left, right = 0, len(nums) - 1 # 循环查找区间为 [left, right],直到区间为空 while left <= right: # 计算中间位置,防止溢出可写为 left + (right - left) // 2 mid = (left + right) // 2 # 命中目标,直接返回下标 if nums[mid] == target: return mid # 目标在右半区间,收缩左边界 elif nums[mid] < target: left = mid + 1 # 目标在左半区间,收缩右边界 else: right = mid - 1 # 查找失败,返回 -1 return -1 ``` #### 思路 1:细节 - 这种思路是在一旦循环体中找到元素就直接返回。 - 循环可以继续的条件是 `left <= right`。 - 如果一旦退出循环,则说明这个区间内一定不存在目标元素。 ### 5.2 排除法 思路 > **排除法思想**:每轮循环都优先排除掉一定不包含目标元素的区间,仅在可能存在目标的区间内继续查找。 #### 思路 2:排除法 1. 初始化左右边界 $left = 0$,$right = len(nums) - 1$,查找区间为 $[left, right]$(左闭右闭)。 2. 每次计算中间位置 $mid$,比较 $nums[mid]$ 与 $target$,优先排除掉目标元素一定不存在的区间。 3. 在剩余的区间内继续查找,重复上述过程。 4. 当区间收缩到只剩一个元素时,判断该元素是否为目标值。 基于排除法,可以实现两种常见写法: #### 思路 2:代码 1 ```python class Solution: def search(self, nums: List[int], target: int) -> int: left, right = 0, len(nums) - 1 # 在闭区间 [left, right] 内查找 target while left < right: # 计算中间位置,向下取整 mid = left + (right - left) // 2 # 如果 nums[mid] 小于目标值,排除 [left, mid] 区间,继续在 [mid + 1, right] 查找 if nums[mid] < target: left = mid + 1 # 否则目标值可能在 [left, mid] 区间,收缩右边界 else: right = mid # 循环结束后,left == right,判断该位置是否为目标值 return left if nums[left] == target else -1 ``` #### 思路 2:代码 2 ```python class Solution: def search(self, nums: List[int], target: int) -> int: left, right = 0, len(nums) - 1 # 在闭区间 [left, right] 内查找 target while left < right: # 计算中间位置,向上取整,防止死循环 mid = left + (right - left + 1) // 2 # 如果 nums[mid] > target,说明目标只可能在 [left, mid - 1] 区间 if nums[mid] > target: right = mid - 1 # 否则,目标在 [mid, right] 区间(包括 mid) else: left = mid # 循环结束后,left == right,判断该位置是否为目标值 return left if nums[left] == target else -1 ``` #### 思路 2:细节 - 循环条件采用 `left < right`,这样循环结束时必然有 `left == right`,无需再区分返回 $left$ 还是 $right$,只需判断 $nums[left]$ 是否为目标值即可。 - 在循环体内,先比较目标值与中间元素的大小,优先排除目标值不可能存在的区间,然后在剩余区间继续查找。 - 排除目标值不可能存在的区间后,`else` 分支通常直接取剩余的另一半区间,无需额外判断。例如,如果排除 $[left, mid]$,则剩余区间为 $[mid + 1, right]$;如果排除 $[mid, right]$,则剩余区间为 $[left, mid-1]$。 - 为避免死循环,当区间被划分为 $[left, mid-1]$ 和 $[mid, right]$ 时,**$mid$ 需要向上取整**,即 `mid = left + (right - left + 1) // 2`。因为当区间只剩两个元素($right = left + 1$)时,如果 $mid$ 向下取整,`left = mid` 会导致区间不变,陷入死循环。 - 例如 $left = 5$,$right = 6$,如果 $mid = 5$,执行 $left = mid$ 后区间仍为 $[5, 6]$,无法收缩,导致死循环。 - 如果 $mid$ 向上取整,$mid = 6$,执行 $left = mid$ 后区间变为 $[6, 6]$,循环得以终止。 - 边界设置可记忆为:只要出现 `left = mid`,就要让 $mid$ 向上取整。具体配对如下: - `left = mid + 1`、`right = mid` 搭配 `mid = left + (right - left) // 2`。 - `right = mid - 1`、`left = mid` 搭配 `mid = left + (right - left + 1) // 2`。 ### 4.3 两种思路适用范围 - **直接法**:因为判断语句是 `left <= right`,有时候要考虑返回是 $left$ 还是 $right$。循环体内有 3 个分支,并且一定有一个分支用于退出循环或者直接返回。这种思路适合解决简单题目。即要查找的元素性质简单,数组中都是非重复元素,且 `==`、`>`、`<` 的情况非常好写的时候。 - **排除法**:更加符合二分查找算法的减治思想。每次排除目标元素一定不存在的区间,达到减少问题规模的效果。然后在可能存在的区间内继续查找目标元素。这种思路适合解决复杂题目。比如查找一个数组里可能不存在的元素,找边界问题,可以使用这种思路。 ## 5. 总结 二分查找的细节问题包括区间开闭、mid取值、循环条件和搜索范围的选择。 **区间开闭**:建议使用左闭右闭区间,这样逻辑更简单,减少出错可能。 **mid取值**:通常使用 `mid = left + (right - left) // 2`,防止整型溢出。在某些情况下,如 `left = mid` 时,需向上取整,避免死循环。 **循环条件**: - `left <= right`:适用于直接法,循环结束时如果未找到目标,直接返回 $-1$。 - `left < right`:适用于排除法,循环结束时需额外判断 `nums[left]` 是否为目标值。 **搜索范围选择**: - 直接法:`left = mid + 1` 或 `right = mid - 1`,明确缩小范围。 - 排除法:根据情况选择 `left = mid + 1` 或 `right = mid`,以及 `right = mid - 1` 或 `left = mid`,确保每次排除无效区间。 **两种思路**: - **直接法**:简单直接,适合查找明确存在的元素。 - **排除法**:更通用,适合复杂问题,如边界查找或不确定元素是否存在的情况。 掌握这些细节能更灵活地应用二分查找,避免常见错误。 ## 练习题目 - [0278. 第一个错误的版本](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/first-bad-version.md) - [0069. x 的平方根](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sqrtx.md) - [1011. 在 D 天内送达包裹的能力](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/capacity-to-ship-packages-within-d-days.md) - [0033. 搜索旋转排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-in-rotated-sorted-array.md) - [0153. 寻找旋转排序数组中的最小值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array.md) - [二分查找题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E9%A2%98%E7%9B%AE) ## 参考资料 - 【博文】[Learning-Algorithms-with-Leetcode - 第 3.1 节 二分查找算法](https://www.yuque.com/liweiwei1419/algo/wkmtx4) - 【博文】[二分法的细节加细节 你真的应该搞懂!!!_小马的博客](https://blog.csdn.net/xiao_jj_jj/article/details/106018702) - 【课程】[零起步学算法 - LeetBook - 二分查找的基本思想:减而治之](https://leetcode.cn/leetbook/read/learning-algorithms-with-leetcode/xsz9zc/) - 【题解】[二分查找算法细节详解,顺便写了首诗 - LeetCode](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/solution/er-fen-cha-zhao-suan-fa-xi-jie-xiang-jie-by-labula/) ================================================ FILE: docs/01_array/01_15_array_two_pointers.md ================================================ ## 1. 双指针简介 > **双指针(Two Pointers)**:在遍历序列时,同时用两个指针协同访问元素,以高效解决问题。常见类型有三种:同序列相向移动的「对撞指针」、同序列同向移动的「快慢指针」、以及分别指向不同序列的「分离双指针」。 在处理数组区间类问题时,传统的暴力解法时间复杂度通常为 $O(n^2)$,而双指针方法能够利用区间的「单调性」特征,将时间复杂度优化至 $O(n)$。 ## 2. 对撞指针 > **对撞指针**:即用两个指针 $left$ 和 $right$,分别指向序列的首尾,$left$ 向右、$right$ 向左移动,直到两指针相遇($left == right$)或满足特定条件。 ![对撞指针](https://qcdn.itcharge.cn/images/202405092155032.png) ### 2.1 对撞指针求解步骤 1. 初始化两个指针 $left = 0$,$right = len(nums) - 1$。 2. 循环中根据条件移动指针:如果满足某条件,$left$ 右移;如果满足另一条件,$right$ 左移。 3. 循环至两指针相遇或满足终止条件。 ### 2.2 对撞指针通用模板 ```python # 初始化左右指针,分别指向数组的首尾 left, right = 0, len(nums) - 1 # 当左指针小于右指针时循环 while left < right: # 如果满足题目要求的特殊条件,直接返回结果 if 满足要求的特殊条件: return 符合条件的值 # 如果满足某一条件,左指针右移,缩小区间 elif 一定条件 1: left += 1 # 如果满足另一条件,右指针左移,缩小区间 elif 一定条件 2: right -= 1 # 如果循环结束还未找到,返回未找到或对应值 return 没找到 或 找到对应值 ``` ### 2.3 对撞指针适用场景 对撞指针常用于有序数组或字符串,典型应用包括: - 查找有序数组中特定元素组合,如二分查找、两数之和等。 - 字符串或数组反转,如反转字符串、判断回文、颠倒二进制等。 下面通过具体例子演示对撞指针的用法。 ### 2.4 经典例题:两数之和 II - 输入有序数组 #### 2.4.1 题目链接 - [167. 两数之和 II - 输入有序数组 - 力扣(LeetCode)](https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/) #### 2.4.2 题目大意 **描述**:给定一个下标从 $1$ 开始计数、升序排列的整数数组:$numbers$ 和一个目标值 $target$。 **要求**:从数组中找出满足相加之和等于 $target$ 的两个数,并返回两个数在数组中下的标值。 **说明**: - $2 \le numbers.length \le 3 * 10^4$。 - $-1000 \le numbers[i] \le 1000$。 - $numbers$ 按非递减顺序排列。 - $-1000 \le target \le 1000$。 - 仅存在一个有效答案。 **示例**: - 示例 1: ```python 输入:numbers = [2,7,11,15], target = 9 输出:[1,2] 解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。 ``` - 示例 2: ```python 输入:numbers = [2,3,4], target = 6 输出:[1,3] 解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。 ``` #### 2.4.3 解题思路 ##### 思路 1:暴力枚举 可以直接使用两重循环,枚举所有可能的两数组合,判断其和是否等于目标值 $target$。具体做法如下: 1. 外层循环遍历数组的每一个元素 $i$。 2. 内层循环遍历 $i$ 之后的每一个元素 $j$。 3. 判断 $numbers[i] + numbers[j]$ 是否等于 $target$,如果相等则返回 $[i + 1, j + 1]$(题目下标从 1 开始)。 4. 如果遍历结束仍未找到,返回 $[-1, -1]$。 ##### 思路 1:代码 ```python class Solution: def twoSum(self, numbers: List[int], target: int) -> List[int]: size = len(numbers) for i in range(size): for j in range(i + 1, size): if numbers[i] + numbers[j] == target: return [i + 1, j + 1] return [-1, -1] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。外层循环 $O(n)$,内层循环最坏情况下 $O(n)$,因此总时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(1)$。只使用了常数级别的额外空间。 ##### 思路 2:对撞指针 可以考虑使用对撞指针来减少时间复杂度。具体做法如下: 1. 使用两个指针 $left$,$right$。$left$ 指向数组第一个值最小的元素位置,$right$ 指向数组值最大元素位置。 2. 判断两个位置上的元素的和与目标值的关系。 1. 如果元素和等于目标值,则返回两个元素位置。 2. 如果元素和大于目标值,则让 $right$ 左移,继续检测。 3. 如果元素和小于目标值,则让 $left$ 右移,继续检测。 3. 直到 $left$ 和 $right$ 移动到相同位置停止检测。 4. 如果最终仍没找到,则返回 $[-1, -1]$。 ##### 思路 2:代码 ```python class Solution: def twoSum(self, numbers: List[int], target: int) -> List[int]: left = 0 right = len(numbers) - 1 while left < right: total = numbers[left] + numbers[right] if total == target: return [left + 1, right + 1] elif total < target: left += 1 else: right -= 1 return [-1, -1] ``` ##### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。只用到了常数空间存放若干变量。 ## 3. 快慢指针 > **快慢指针**:指两个指针从同一侧出发,步长不同,快指针(fast)移动更快,慢指针(slow)移动较慢。它们以不同速度遍历序列,直到快指针到达末尾、两指针相遇或满足特定条件时停止。 ![快慢指针](https://qcdn.itcharge.cn/images/202405092156465.png) ### 3.1 快慢指针求解步骤 1. 初始化两个指针 $slow = 0$,$fast = 1$,分别指向第一个和第二个元素。 2. 循环中根据条件移动指针:满足条件时 $slow += 1$,否则 $fast += 1$。 3. 当 $fast$ 到达数组末尾、两指针相遇或满足其他条件时结束循环。 ### 3.2 快慢指针通用模板 ```python # 初始化慢指针 slow 和快指针 fast slow = 0 fast = 1 # 当 fast 没有遍历到数组末尾时循环 while fast 未遍历到数组末尾: # 如果满足特定条件(如去重时 nums[fast] != nums[slow]) if 满足特定条件: slow += 1 # 慢指针右移一位,准备接收新元素 # 根据实际需求,可能需要将 fast 指向的元素赋值给 slow 指向的位置 # 例如:nums[slow] = nums[fast] fast += 1 # 快指针继续向右遍历 # 返回最终结果(如新数组长度 slow + 1 或处理后的数组等) return 最终结果 ``` ### 3.3 快慢指针的应用场景 快慢指针主要用于解决数组元素的移动、删除等问题,以及链表中的环检测、长度统计等操作。链表相关的双指针技巧将在后续链表章节详细介绍。 接下来,我们通过具体例题,演示快慢指针的实际用法。 ### 3.4 经典例题:删除有序数组中的重复项 #### 3.4.1 题目链接 - [26. 删除有序数组中的重复项 - 力扣(LeetCode)](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/) #### 3.4.2 题目大意 **描述**:给定一个有序数组 $nums$。 **要求**:删除数组 $nums$ 中的重复元素,使每个元素只出现一次。并输出去除重复元素之后数组的长度。 **说明**: - 不能使用额外的数组空间,在原地修改数组,并在使用 $O(1)$ 额外空间的条件下完成。 **示例**: - 示例 1: ```python 输入:nums = [1,1,2] 输出:2, nums = [1,2,_] 解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。 ``` - 示例 2: ```python 输入:nums = [0,0,1,1,1,2,2,3,3,4] 输出:5, nums = [0,1,2,3,4] 解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。 ``` #### 3.4.3 解题思路 ##### 思路 1:快慢指针 有序数组中,重复元素必然相邻。我们可以用双指针原地去重: 1. 用两个指针 $slow$ 和 $fast$,初始 $slow = 0$,$fast = 1$。 2. 遍历数组,如果 $nums[fast] \neq nums[slow]$,则 $slow$ 右移一位,并将 $nums[fast]$ 赋值到 $nums[slow]$。 3. 每轮 $fast$ 右移一位,直到遍历结束。 4. 最终返回 $slow + 1$,即去重后数组长度。 ##### 思路 1:代码 ```python class Solution: def removeDuplicates(self, nums: List[int]) -> int: # 数组为空或只有一个元素,直接返回长度 if len(nums) <= 1: return len(nums) # slow 指针指向去重后数组的最后一个元素 # fast 指针用于遍历整个数组 slow, fast = 0, 1 while fast < len(nums): # 如果当前 fast 指向的元素和 slow 指向的元素不同 # 说明遇到了新的不重复元素 if nums[slow] != nums[fast]: slow += 1 # slow 前进一位 nums[slow] = nums[fast] # 将新元素赋值到 slow 位置 # 无论是否赋值,fast 都要前进一位 fast += 1 # 返回去重后数组的长度(下标从0开始,所以要+1) return slow + 1 ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ## 4. 分离双指针 > **分离双指针**:指的是分别在两个不同数组上各设置一个指针,两个指针独立地在各自数组中移动,以协同完成特定任务。 ![分离双指针](https://qcdn.itcharge.cn/images/202405092157828.png) ### 4.1 分离双指针求解步骤 1. 定义两个指针 $left\_1$ 和 $left\_2$,分别指向两个数组的起始位置(均为 $0$)。 2. 根据条件,如果需要,两个指针同时右移:$left\_1 += 1$,$left\_2 += 1$。 3. 如果只需移动第一个数组指针,则 $left\_1 += 1$。 4. 如果只需移动第二个数组指针,则 $left\_2 += 1$。 5. 当任一指针遍历到数组末尾或满足终止条件时,结束循环。 ### 4.2 分离双指针通用模板 ```python # 初始化两个指针,分别指向两个数组的起始位置 left_1, left_2 = 0, 0 # 当两个指针都未遍历到各自数组末尾时,循环进行比较 while left_1 < len(nums1) and left_2 < len(nums2): if 满足条件 1: # 通常表示两个指针指向的元素相等 # 此时可以将该元素加入结果集(如交集),并同时移动两个指针 left_1 += 1 left_2 += 1 elif 满足条件 2: # 通常表示第一个数组当前元素较小 # 只移动第一个指针,继续比较下一个元素 left_1 += 1 elif 满足条件 3: # 通常表示第二个数组当前元素较小 # 只移动第二个指针,继续比较下一个元素 left_2 += 1 ``` ### 4.3 分离双指针适用场景 分离双指针主要应用于有序数组的合并、交集、并集等问题,能够高效地同时遍历两个数组,协同完成元素的比较与处理。 下面通过具体例子,详细讲解分离双指针的实际用法。 ### 4.4 经典例题:两个数组的交集 #### 4.4.1 题目链接 - [349. 两个数组的交集 - 力扣(LeetCode)](https://leetcode.cn/problems/intersection-of-two-arrays/) #### 4.4.2 题目大意 **描述**:给定两个数组 $nums1$ 和 $nums2$。 **要求**:返回两个数组的交集。重复元素只计算一次。 **说明**: - $1 \le nums1.length, nums2.length \le 1000$。 - $0 \le nums1[i], nums2[i] \le 1000$。 **示例**: - 示例 1: ```python 输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2] 示例 2: ``` - 示例 2: ```python 输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[9,4] 解释:[4,9] 也是可通过的 ``` #### 4.4.3 解题思路 ##### 思路 1:分离双指针 1. 先对 $nums1$ 和 $nums2$ 排序。 2. 用两个指针 $left\_1$、$left\_2$ 分别从两个数组头部开始遍历。 3. 如果 $nums1[left\_1] == nums2[left\_2]$,将该元素(去重)加入结果,并同时右移 $left\_1$、$left\_2$。 4. 如果 $nums1[left\_1] < nums2[left\_2]$,则 $left\_1$ 右移。 5. 如果 $nums1[left\_1] > nums2[left\_2]$,则 $left\_2$ 右移。 6. 遍历结束后返回结果数组。 ##### 思路 1:代码 ```python class Solution: def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]: nums1.sort() # 对 nums1 进行排序 nums2.sort() # 对 nums2 进行排序 left_1 = 0 # 指向 nums1 的指针 left_2 = 0 # 指向 nums2 的指针 res = [] # 优化:由于数组已排序,结果去重只需判断上一个加入的元素即可 while left_1 < len(nums1) and left_2 < len(nums2): if nums1[left_1] == nums2[left_2]: # 只有 res 为空或当前元素与上一个加入的元素不同才加入结果,避免重复 if not res or nums1[left_1] != res[-1]: res.append(nums1[left_1]) left_1 += 1 left_2 += 1 elif nums1[left_1] < nums2[left_2]: left_1 += 1 else: # nums1[left_1] > nums2[left_2] left_2 += 1 return res ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(m \log m + n \log n)$,其中 $m$ 和 $n$ 分别为两个数组的长度。排序用时 $O(m \log m + n \log n)$,双指针遍历用时 $O(m + n)$,因此总的时间复杂度为 $O(m \log m + n \log n)$。 - **空间复杂度**:$O(\min(m, n))$。 ## 5. 双指针总结 双指针主要分为三类:「对撞指针」、「快慢指针」和「分离双指针」。 - **对撞指针**:两个指针分别从序列两端向中间移动,常用于查找有序数组中满足特定条件的元素对、字符串反转等场景。 - **快慢指针**:两个指针从同一端出发,步长不同,常用于数组元素的移动、删除,或链表中的环检测、长度统计等问题。 - **分离双指针**:两个指针分别遍历不同的数组或链表,适合处理有序数组的合并、交集、并集等问题。 双指针算法能够显著降低时间复杂度,通常可将暴力解法的 $O(n^2)$ 优化为 $O(n)$。其核心在于利用数据的有序性或问题的单调性,通过灵活移动指针,快速排除不符合条件的情况,从而减少无效计算。熟练掌握双指针技巧,可以高效解决大量数组和链表相关的问题。 ## 练习题目 - [0344. 反转字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-string.md) - [0345. 反转字符串中的元音字母](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-vowels-of-a-string.md) - [0015. 三数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum.md) - [0027. 移除元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-element.md) - [0080. 删除有序数组中的重复项 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-array-ii.md) - [0925. 长按键入](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/long-pressed-name.md) - [双指针题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8F%8C%E6%8C%87%E9%92%88%E9%A2%98%E7%9B%AE) ## 参考资料 - 【博文】[双指针算法之快慢指针 (yanyusoul.com)](https://yanyusoul.com/blog/cs/algorithms_fast-slow-points/) - 【博文】[双指针算法各类基础题型总结 - 掘金](https://juejin.cn/post/6855129006451687431) - 【博文】[双指针 - 力扣加加 - 努力做西湖区最好的算法题解](https://leetcode-solution-leetcode-pp.gitbook.io/leetcode-solution/91/two-pointers#zuo-you-duan-dian-zhi-zhen) - 【博文】[LeetCode分类专题(四)——双指针和滑动窗口1 - iwehdio - 博客园](https://www.cnblogs.com/iwehdio/p/14434988.html) - 【博文】[双指针算法各类基础题型总结 - 掘金](https://juejin.cn/post/6855129006451687431) ================================================ FILE: docs/01_array/01_16_array_sliding_window.md ================================================ ## 1. 滑动窗口算法简介 在计算机网络中,滑动窗口协议(Sliding Window Protocol)是传输层进行流控的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的发送速度,从而达到防止发送方发送速度过快而导致自己被淹没的目的。我们所要讲解的滑动窗口算法也是利用了同样的特性。 > **滑动窗口算法(Sliding Window)**:在数组 / 字符串上维护一个固定或可变长度的窗口,通过滑动和缩放窗口,动态维护区间内的最优解。 - **滑动**:窗口整体向一个方向移动,通常是向右。 - **缩放**:窗口长度可变时,可以通过移动左指针缩小窗口,或移动右指针扩大窗口。 滑动窗口本质上是双指针(快慢指针)的一种应用,可以理解为用两个指针维护一个区间,动态调整区间范围以满足题目要求。 ![滑动窗口](https://qcdn.itcharge.cn/images/202405092203225.png) ## 2. 滑动窗口的应用场景 滑动窗口常用于查找满足某些条件的连续子区间,能将嵌套循环优化为单循环,大幅降低时间复杂度。常见题型包括: - **固定长度窗口**:窗口大小固定,通常用于统计或查找长度为 $k$ 的区间性质。 - **可变长度窗口**:窗口大小不固定,常用于查找满足条件的最长/最短区间。 下面分别介绍这两类滑动窗口的应用。 ## 3. 固定长度滑动窗口 > **固定长度滑动窗口算法(Fixed Length Sliding Window)**:在数组 / 字符串上维护一个长度固定的窗口,通过不断向右滑动窗口,实时更新窗口内的数据,并根据题目要求动态维护最优解。 ![固定长度滑动窗口](https://qcdn.itcharge.cn/images/202405092204712.png) ### 3.1 固定长度滑动窗口算法步骤 假设窗口大小为 $window\_size$,步骤如下: 1. 定义两个指针 $left$ 和 $right$,初始都指向序列起始位置($left = 0, right = 0$),区间 $[left, right]$ 表示当前窗口。 2. 不断右移 $right$,将元素加入窗口(如 `window.append(nums[right])`)。 3. 当窗口长度达到 $window\_size$(即 `right - left + 1 >= window_size`)时: - 判断窗口内元素是否满足题目要求,如果满足则更新答案。 - 右移 $left$(`left += 1`),保持窗口长度不变。 4. 重复上述过程,直到 $right$ 遍历完整个数组。 ### 3.2 固定长度滑动窗口代码模板 ```python left = 0 # 窗口左边界 right = 0 # 窗口右边界 while right < len(nums): # 将当前元素加入窗口 window.append(nums[right]) # 判断当前窗口长度是否达到 window_size if right - left + 1 >= window_size: # 在窗口长度达到要求时,进行答案的统计或更新 # ... 这里根据题目需求维护/更新答案 # 移除窗口最左侧元素,窗口向右滑动 window.popleft() left += 1 # 左指针右移,缩小窗口长度,保持窗口长度为 window_size # 右指针右移,扩大窗口 right += 1 ``` 下面我们通过具体例题,详细说明如何利用固定长度滑动窗口方法高效解决相关问题。 ### 3.3 经典例题:大小为 K 且平均值大于等于阈值的子数组数目 #### 3.3.1 题目链接 - [1343. 大小为 K 且平均值大于等于阈值的子数组数目 - 力扣(LeetCode)](https://leetcode.cn/problems/number-of-sub-arrays-of-size-k-and-average-greater-than-or-equal-to-threshold/) #### 3.3.2 题目大意 **描述**:给定一个整数数组 $arr$ 和两个整数 $k$ 和 $threshold$ 。 **要求**:返回长度为 $k$ 且平均值大于等于 $threshold$ 的子数组数目。 **说明**: - $1 \le arr.length \le 10^5$。 - $1 \le arr[i] \le 10^4$。 - $1 \le k \le arr.length$。 - $0 \le threshold \le 10^4$。 **示例**: - 示例 1: ```python 输入:arr = [2,2,2,2,5,5,5,8], k = 3, threshold = 4 输出:3 解释:子数组 [2,5,5],[5,5,5] 和 [5,5,8] 的平均值分别为 4,5 和 6 。其他长度为 3 的子数组的平均值都小于 4 (threshold 的值)。 ``` - 示例 2: ```python 输入:arr = [11,13,17,23,29,31,7,5,2,3], k = 3, threshold = 5 输出:6 解释:前 6 个长度为 3 的子数组平均值都大于 5 。注意平均值不是整数。 ``` #### 3.3.3 解题思路 ##### 思路 1:滑动窗口(固定长度) 本题是典型的定长滑动窗口问题,窗口大小为 $k$。具体做法如下: 1. 用 $window\_sum$ 维护当前窗口内元素和,$ans$ 统计满足条件的子数组个数。 2. 使用两个指针 $left$、$right$,初始都为 $0$。 3. 每次将 $arr[right]$ 加入 $window\_sum$,$right$ 右移。 4. 当窗口长度达到 $k$(即 `right - left + 1 >= k`)时,判断窗口平均值是否大于等于 $threshold$,满足则 $ans + 1$。 5. 然后将 $arr[left]$ 移出窗口,$left$ 右移,保证窗口长度始终为 $k$。 6. 重复上述过程直到遍历完整个数组,最后返回 $ans$。 ##### 思路 1:代码 ```python class Solution: def numOfSubarrays(self, arr: List[int], k: int, threshold: int) -> int: left = 0 # 窗口左边界 right = 0 # 窗口右边界 window_sum = 0 # 当前窗口内元素的和 ans = 0 # 满足条件的子数组个数 while right < len(arr): window_sum += arr[right] # 将右边界元素加入窗口和 # 当窗口长度达到k时,判断是否满足条件 if right - left + 1 >= k: # 判断当前窗口的平均值是否大于等于 threshold if window_sum >= k * threshold: ans += 1 # 满足条件,计数加一 window_sum -= arr[left] # 移除左边界元素,准备滑动窗口 left += 1 # 左边界右移 right += 1 # 右边界右移,扩大窗口 return ans # 返回满足条件的子数组个数 ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ## 4. 不定长度滑动窗口 > **不定长滑动窗口(Sliding Window)**:在数组 / 字符串上用两个指针动态维护一个可变长度的窗口,通过左右移动指针灵活调整窗口范围,实时维护最优解。 ![不定长度滑动窗口](https://qcdn.itcharge.cn/images/202405092206553.png) ### 4.1 不定长度滑动窗口算法步骤 1. 定义左右指针 $left$、$right$,初始都为 $0$,区间 $[left, right]$ 表示当前窗口。 2. 将 $s[right]$ 加入窗口(如 `window.add(s[right])`),然后 $right += 1$,扩大窗口。 3. 当窗口不满足条件时,不断移除 $s[left]$(如 `window.popleft()`),并 $left += 1$,缩小窗口,直到重新满足条件。 4. 重复上述过程,直到 $right$ 遍历完整个序列。 ### 4.2 不定长度滑动窗口代码模板 ```python # 初始化左右指针,均指向数组起始位置 left = 0 right = 0 # 主循环,右指针遍历整个数组 while right < len(nums): # 将当前右指针指向的元素加入窗口 window.append(nums[right]) # 当窗口不满足题目要求时,缩小窗口(移动左指针) while 窗口需要缩小: # 此处可根据题意维护/更新答案 window.popleft() # 移除左边界元素 left += 1 # 左指针右移,缩小窗口 # 此处可根据题意维护/更新答案(如记录最大/最小窗口等) # 右指针右移,扩大窗口 right += 1 ``` ### 4.3 经典例题:无重复字符的最长子串 #### 4.3.1 题目链接 - [3. 无重复字符的最长子串 - 力扣(LeetCode)](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) #### 4.3.2 题目大意 **描述**:给定一个字符串 $s$。 **要求**:找出其中不含有重复字符的最长子串的长度。 **说明**: - $0 \le s.length \le 5 * 10^4$。 - $s$ 由英文字母、数字、符号和空格组成。 **示例**: - 示例 1: ```python 输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 ``` - 示例 2: ```python 输入: s = "bbbbb" 输出: 1 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 ``` #### 4.3.3 解题思路 ##### 思路 1:滑动窗口(不定长度) 使用滑动窗口(哈希表 $window$ 记录窗口内每个字符出现的次数)来维护一个不含重复字符的子串。 1. 初始化两个指针 $left$ 和 $right$,分别作为滑动窗口的左右边界,初始都为 $0$。 2. 右指针 $right$ 向右移动,每次将 $s[right]$ 加入 window,并统计其出现次数。 3. 如果当前字符 $s[right]$ 在窗口中的出现次数大于 $1$(即 $window[s[right]] > 1$),说明出现重复字符。此时不断右移左指针 $left$,并相应减少 $window[s[left]]$ 的计数,直到窗口内 $s[right]$ 只出现一次,保证窗口内无重复字符。 4. 每次窗口合法(无重复字符)时,更新最长子串长度的答案。 5. 重复上述过程,直到 $right$ 遍历完整个字符串。 6. 最终返回无重复字符的最长子串长度。 ##### 思路 1:代码 ```python class Solution: def lengthOfLongestSubstring(self, s: str) -> int: left = 0 # 滑动窗口左边界 right = 0 # 滑动窗口右边界 window = dict() # 记录窗口内每个字符出现的次数 ans = 0 # 记录最长无重复子串的长度 while right < len(s): # 将当前字符加入窗口,统计出现次数 if s[right] not in window: window[s[right]] = 1 else: window[s[right]] += 1 # 如果当前字符出现次数大于1,说明有重复,需要收缩左边界 while window[s[right]] > 1: window[s[left]] -= 1 # 左边界字符出现次数减少 left += 1 # 左边界右移,缩小窗口 # 更新最长无重复子串的长度 ans = max(ans, right - left + 1) right += 1 # 右边界右移,扩大窗口 return ans # 返回结果 ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(| \sum |)$。其中 $\sum$ 表示字符集,$| \sum |$ 表示字符集的大小。 ## 5. 总结 滑动窗口算法高效解决数组或字符串的连续区间问题。 - **固定长度窗口**:窗口大小固定,常用于统计定长子区间的和、均值等。 - **不定长度窗口**:窗口大小可变,适合查找最长/最短满足条件的子区间,如最长无重复子串。 滑动窗口通过动态调整左右边界,避免重复遍历,将时间复杂度从 $O(n^2)$ 降至 $O(n)$。 使用时需注意: - 窗口的起始位置 - 何时扩展或收缩窗口 - 如何及时更新答案 - 边界情况处理 熟练掌握滑动窗口,可高效应对各类区间类问题。 ## 练习题目 - [0643. 子数组最大平均数 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-average-subarray-i.md) - [0674. 最长连续递增序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/longest-continuous-increasing-subsequence.md) - [1004. 最大连续1的个数 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/max-consecutive-ones-iii.md) - [滑动窗口题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%A2%98%E7%9B%AE) ## 参考资料 - 【答案】[TCP 协议的滑动窗口具体是怎样控制流量的? - 知乎](https://www.zhihu.com/question/32255109/answer/68558623) - 【博文】[滑动窗口算法基本原理与实践 - huansky - 博客园](https://www.cnblogs.com/huansky/p/13488234.html) - 【博文】[滑动窗口(Sliding Window)- lucifer.ren](https://lucifer.ren/leetcode/thinkings/slide-window.html) ================================================ FILE: docs/01_array/index.md ================================================ ![](https://qcdn.itcharge.cn/images/20250923140234.png) ::: tip 引 言 数组如同连续砖块砌成的墙。 每一块砖石紧密相依,井然有序,宛如时光的流转,悄然记录着数据的点滴。 ::: ## 本章内容 - [1.1 数组基础](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_01_array_basic.md) - [1.2 数组排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_02_array_sort.md) - [1.3 数组冒泡排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_03_array_bubble_sort.md) - [1.4 数组选择排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_04_array_selection_sort.md) - [1.5 数组插入排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array//01_05_array_insertion_sort.md) - [1.6 数组希尔排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_06_array_shell_sort.md) - [1.7 数组归并排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_07_array_merge_sort.md) - [1.8 数组快速排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_08_array_quick_sort.md) - [1.9 数组堆排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_09_array_heap_sort.md) - [1.10 数组计数排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_10_array_counting_sort.md) - [1.11 数组桶排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_11_array_bucket_sort.md) - [1.12 数组基数排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_12_array_radix_sort.md) - [1.13 数组二分查找(一)](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_13_array_binary_search_01.md) - [1.14 数组二分查找(二)](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_14_array_binary_search_02.md) - [1.15 数组双指针](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_15_array_two_pointers.md) - [1.16 数组滑动窗口](https://github.com/ITCharge/AlgoNote/tree/main/docs/01_array/01_16_array_sliding_window.md) ================================================ FILE: docs/02_linked_list/02_01_linked_list_basic.md ================================================ ## 1. 链表简介 ### 1.1 链表定义 > **链表(Linked List)**:一种线性表数据结构,通过一组任意(可连续或不连续)的存储单元,存储同类型数据。 简而言之,**链表** 是线性表的链式存储实现。 以单链表为例,其结构如下图: ![链表](https://qcdn.itcharge.cn/images/202405092229936.png) 如上图所示,链表通过指针将一组任意的存储单元串联起来。每个数据元素及其所在的存储单元构成一个「链节点」。为了将所有节点连接成链,每个链节点除了存放数据元素本身,还需要额外存储一个指向其直接后继节点的指针,称为「后继指针 $next$」。 在链表结构中,数据元素之间的逻辑顺序由指针维护。虽然逻辑上相邻的数据元素在物理内存中可以相邻,也可以完全不相邻,因此链表在物理存储上的分布是非连续、随机的。 链表的优缺点如下: - **优点**:链表无需预先分配存储空间,按需动态申请,能够有效避免空间浪费;在插入、删除等操作上,链表通常比数组更高效,尤其是在需要频繁修改数据结构时表现突出。 - **缺点**:链表除了存储数据本身外,还需额外存储指针信息,因此整体空间开销大于数组;同时,链表不支持随机访问,查找元素时需要从头遍历,效率较低。 下面介绍除单链表外的其他链表类型。 ### 1.2 双向链表 > **双向链表(Doubly Linked List)**:链表的一种,也称为双链表。每个节点包含两个指针,分别指向其直接前驱和直接后继节点。 - **双向链表的特点**:可以从任意节点高效地访问其前驱和后继节点,支持双向遍历,插入和删除操作更加灵活。 ![双向链表](https://qcdn.itcharge.cn/images/202405092230869.png) ### 1.3 循环链表 > **循环链表(Circular Linked List)**:一种特殊的链表结构,其最后一个节点的指针指向头节点,从而使整个链表首尾相连,形成一个闭环。 - **循环链表的特点**:无论从哪个节点出发,都可以遍历到链表中的任意节点,实现了节点间的循环访问。 ![循环链表](https://qcdn.itcharge.cn/images/202405092230094.png) 下面我们将以最基础的「单链表」为例,详细讲解链表的基本操作。 ## 2. 链表的基本操作 在数据结构中,常见的基本操作包括增、删、改、查四类,链表的操作同样主要围绕这四个方面展开。下面我们详细介绍链表的基本操作。 ### 2.1 链表的结构定义 链表由若干链节点通过 $next$ 指针依次连接而成。通常我们会先定义一个简单的「链节点类」,再基于此实现完整的「链表类」。 - **链节点类(ListNode)**:包含成员变量 $val$(存储数据元素的值)和 $next$(指向下一个节点的指针)。 - **链表类(LinkedList)**:包含一个链节点变量 $head$,用于表示链表的头节点。 创建空链表时,只需将头节点 $head$ 设为「空指针」。在 Python 中可用 $None$ 表示,其他语言中常用 $NULL$、$nil$、$0$ 等。 **链节点与链表结构的代码实现如下:** ```python # 链节点类 class ListNode: def __init__(self, val=0, next=None): self.val = val # 节点的值 self.next = next # 指向下一个节点 class LinkedList: def __init__(self): self.head = None # 链表头指针,初始为 None ``` ### 2.2 创建链表 > **创建链表**:根据给定的线性表数据,依次生成链表节点,并将它们顺序连接起来,构成完整的链表。 具体步骤如下: 1. 取出线性表的第 $1$ 个元素,创建链表头节点。 2. 依次遍历剩余元素,每获取一个数据元素,就新建一个节点,并将其连接到当前链表的尾部。 3. 所有元素插入完成后,返回头节点。 **创建链表** 的实现代码如下: ```python # 根据 data 列表初始化一个新链表 def create(self, data): if not data: # 如果输入数据为空,直接返回,不创建链表 return # 创建头节点,并将 head 指向头节点 self.head = ListNode(data[0]) cur = self.head # cur 用于指向当前链表的尾节点 # 依次遍历 data 中剩余的元素,逐个创建新节点并连接到链表尾部 for i in range(1, len(data)): node = ListNode(data[i]) # 创建新节点 cur.next = node # 将新节点连接到当前尾节点 cur = cur.next # cur 指向新的尾节点,准备连接下一个节点 ``` 「创建链表」的操作需要遍历所有数据元素,时间复杂度为 $O(n)$,其中 $n$ 为线性表的长度。 ### 2.3 链表长度 > **链表长度**:通过一个指针变量 $cur$ 沿着链表的 $next$ 指针逐个遍历节点,并用计数器 $count$ 统计节点数量,最终得到链表长度。 具体步骤如下: 1. 令指针 $cur$ 指向链表头节点(第 $1$ 个节点)。 2. 沿着 $next$ 指针遍历链表,每访问一个节点,计数器 $count$ 加 $1$。 3. 当 $cur$ 变为 $None$(即遍历到链表末尾)时,遍历结束,此时 $count$ 即为链表长度,返回该值。 **「求链表长度」** 的实现代码如下: ```python # 获取线性链表长度 def length(self): count = 0 # 初始化计数器,记录节点个数 cur = self.head # 从链表头节点开始遍历 while cur: # 只要当前节点不为 None,就继续遍历 count += 1 # 每遍历到一个节点,计数器加 1 cur = cur.next # 指针后移,指向下一个节点 return count # 返回计数器的值,即链表长度 ``` 「求链表长度」的操作需要遍历链表的所有节点,操作次数为 $n$,因此时间复杂度为 $O(n)$,其中 $n$ 为链表长度。 ### 2.4 查找节点 > **链表中查找值为 $val$ 的节点**:从头节点 $head$ 开始,依次遍历链表,查找值等于 $val$ 的节点。如果找到,返回该节点;否则返回 $None$。 具体步骤如下: 1. 定义指针变量 $cur$,初始指向链表的头节点。 2. 沿着链表的 $next$ 指针依次遍历每个节点: - 如果当前节点 $cur$ 的值等于 $val$,则查找成功,返回该节点。 - 否则,$cur$ 指向下一个节点,继续查找。 3. 如果遍历完整个链表仍未找到,说明链表中不存在值为 $val$ 的节点,返回 $None$。 **「链表中查找值为 $val$ 的节点」** 的实现代码如下: ```python # 链表中查找值为 val 的节点 def find(self, val): cur = self.head # 从链表头节点开始遍历 while cur: # 只要当前节点不为 None,就继续遍历 if val == cur.val: # 如果当前节点的值等于目标值,查找成功 return cur # 返回当前节点 cur = cur.next # 指针后移,指向下一个节点 # 遍历完整个链表都没有找到目标值,返回 None return None ``` 「链表中查找值为 $val$ 的节点」需要遍历链表的所有节点,因此其时间复杂度为 $O(n)$,其中 $n$ 表示链表的长度。 ### 2.5 插入节点 - **插入节点**:在链表的第 $i$ 个位置前插入一个值为 $val$ 的新节点。 具体步骤如下: 1. 定义指针变量 $cur$,初始指向链表头节点,同时定义计数器 $count$,初始值为 $0$。 2. 沿着链表的 $next$ 指针遍历,$cur$ 每指向一个节点,$count$ 加 $1$。 3. 当 $count$ 等于 $index - 1$ 时,$cur$ 正好指向第 $index - 1$ 个节点(即新节点的前驱节点),此时停止遍历。 4. 创建一个新节点 $node$,其值为 $val$。 5. 将 $node.next$ 指向 $cur.next$,即新节点的后继为原本的第 $index$ 个节点。 6. 将 $cur.next$ 指向 $node$,完成插入操作。 > 注意:如果 $index = 1$,即在头节点前插入,需要特殊处理(如使用虚拟头节点或单独判断)。 ![插入节点](https://qcdn.itcharge.cn/images/202405092232900.png) **「插入节点」** 的实现代码如下: ```python # 插入节点 def insertInside(self, index, val): # 头部插入(index == 1) if index == 1: node = ListNode(val) node.next = self.head self.head = node return count = 0 cur = self.head # 遍历链表,找到第 index - 1 个节点(即新节点的前驱节点) while cur and count < index - 1: cur = cur.next count += 1 # 如果遍历到链表末尾还没找到前驱节点,说明 index 越界,插入失败 if not cur: return 'Error' node = ListNode(val) # 尾部插入(index 指向最后一个节点的下一个位置) if cur.next is None: cur.next = node else: node.next = cur.next cur.next = node ``` 「插入节点」操作需要将指针 $cur$ 从链表头部遍历到第 $i$ 个节点的前一个位置,平均时间复杂度为 $O(n)$,因此整体的时间复杂度为 $O(n)$。 ### 2.6 改变节点 > **将链表中第 $i$ 个节点的值修改为 $val$**:只需遍历到第 $i$ 个节点,然后直接修改该节点的值。具体步骤如下: > 1. 定义指针变量 $cur$ 指向链表头节点,并设置计数器 $count$,初始为 $0$。 2. 沿着 $next$ 指针遍历链表,每遍历一个节点,$count$ 加 $1$。 3. 当 $count$ 等于 $index$ 时,$cur$ 正好指向第 $i$ 个节点,停止遍历。 4. 直接将 $cur$ 的值设为 $val$。 **「将链表中第 $i$ 个节点的值修改为 $val$」** 的实现代码如下: ```python # 改变元素:将链表中第 i 个元素值改为 val def change(self, index, val): # 初始化计数器 count 和指针 cur,cur 指向链表头节点 count = 0 cur = self.head # 遍历链表,直到找到第 index 个节点 while cur and count < index: count += 1 cur = cur.next # 如果 cur 为空,说明 index 越界,返回错误 if not cur: return 'Error' # 修改第 index 个节点的值为 val cur.val = val ``` 要将链表中第 $i$ 个节点的值修改为 $val$,需要从链表头节点出发,遍历到第 $i$ 个节点,然后进行赋值操作。由于遍历链表的时间复杂度为 $O(n)$,因此该操作的整体时间复杂度为 $O(n)$。 ### 2.7 删除元素 > **删除元素**:删除链表中第 $i$ 个节点。 具体步骤如下: 1. 使用指针变量 $cur$ 遍历至第 $i - 1$ 个节点(即待删除节点的前驱)。 2. 将 $cur$ 的 $next$ 指针指向第 $i$ 个节点的下一个节点,从而跳过并移除第 $i$ 个节点。 ![删除元素](https://qcdn.itcharge.cn/images/202405092233332.png) **「删除元素」** 的实现代码如下: ```python # 链表删除元素 def removeInside(self, index): # 初始化计数器 count 和指针 cur,cur 指向链表头节点 count = 0 cur = self.head # 遍历链表,cur 移动到第 index - 1 个节点(即待删除节点的前驱) while cur.next and count < index - 1: count += 1 cur = cur.next # 如果 cur 为空,说明 index 越界,返回错误 if not cur: return 'Error' # del_node 指向待删除的节点 del_node = cur.next # 将 cur 的 next 指针指向 del_node 的下一个节点,实现删除 cur.next = del_node.next ``` 「删除元素」操作需要将指针 $cur$ 从链表头节点遍历至第 $i$ 个节点的前一个节点,因此其时间复杂度为 $O(n)$。 ## 3. 总结 ### 3.1 链表特点 链表是一种**链式存储**的线性表数据结构,具有以下核心特征: - **存储方式**:通过指针连接任意存储单元,物理存储非连续 - **节点结构**:每个节点包含数据域和指针域 - **访问方式**:只能顺序访问,不支持随机访问 ### 3.2 链表类型 | 类型 | 特点 | 适用场景 | |------|------|----------| | **单链表** | 每个节点只有一个后继指针 | 基础链表操作 | | **双向链表** | 每个节点有前驱和后继指针 | 需要双向遍历 | | **循环链表** | 尾节点指向头节点形成环 | 循环访问场景 | ### 3.3 基本操作复杂度 | 操作 | 时间复杂度 | 空间复杂度 | 说明 | |------|------------|------------|------| | **查找** | $O(n)$ | $O(1)$ | 需要遍历到目标位置 | | **插入** | $O(n)$ | $O(1)$ | 找到插入位置后操作简单 | | **删除** | $O(n)$ | $O(1)$ | 找到删除位置后操作简单 | | **修改** | $O(n)$ | $O(1)$ | 需要遍历到目标位置 | ### 3.4 链表 vs 数组 | 特性 | 链表 | 数组 | |------|------|------| | **存储方式** | 链式存储,非连续 | 顺序存储,连续 | | **随机访问** | 不支持,$O(n)$ | 支持,$O(1)$ | | **插入删除** | 高效,$O(1)$(已知位置) | 需要移动元素,$O(n)$ | | **空间开销** | 额外指针开销 | 无额外开销 | | **内存分配** | 动态分配 | 静态分配 | ### 3.5 应用场景 - **频繁插入删除**:链表在插入删除操作上比数组更高效 - **动态内存管理**:适合内存大小不确定的场景 - **实现其他数据结构**:栈、队列、哈希表等的基础 - **算法优化**:某些算法中链表结构能提供更好的性能 ## 练习题目 - [0707. 设计链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/design-linked-list.md) - [0206. 反转链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/reverse-linked-list.md) - [0203. 移除链表元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/remove-linked-list-elements.md) - [0328. 奇偶链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/odd-even-linked-list.md) - [0234. 回文链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/palindrome-linked-list.md) - [0138. 随机链表的复制](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/copy-list-with-random-pointer.md) - [链表基础题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%93%BE%E8%A1%A8%E5%9F%BA%E7%A1%80%E9%A2%98%E7%9B%AE) ## 参考资料 - 【文章】[链表理论基础 - 代码随想录](https://programmercarl.com/链表理论基础.html#链表理论基础) - 【文章】[什么是链表 - 漫画算法 - 小灰的算法之旅 - 力扣](https://leetcode.cn/leetbook/read/journey-of-algorithm/5ozchs/) - 【文章】[链表 - 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/41013) - 【书籍】数据结构教程 第 2 版 - 唐发根 著 - 【书籍】数据结构与算法 Python 语言描述 - 裘宗燕 著 ================================================ FILE: docs/02_linked_list/02_02_linked_list_sort.md ================================================ ## 1. 链表排序简介 链表排序相比数组排序具有独特的挑战性。由于链表 **不支持随机访问**,只能通过 $next$ 指针顺序遍历,这使得某些排序算法在链表上的实现更加复杂。 ### 1.1 算法适用性分析 | 适用性 | 排序算法 | 说明 | |--------|----------|------| | **完全适合** | 冒泡排序、选择排序、插入排序、归并排序、快速排序 | 天然适合链表结构 | | **需要额外空间** | 计数排序、桶排序、基数排序 | 需要辅助数组,但算法逻辑适合 | | **不适合** | 希尔排序 | 依赖随机访问,链表无法高效实现 | | **不推荐** | 堆排序 | 完全二叉树结构用链表存储效率低 | ### 1.2 关键限制因素 **希尔排序的限制**:希尔排序需要访问序列中第 $i + gap$ 个元素,链表无法直接跳转,必须从头遍历,时间复杂度从 $O(1)$ 退化为 $O(n)$。 **堆排序的限制**:堆排序基于完全二叉树结构,数组可以通过下标 $O(1)$ 访问父子节点,而链表需要遍历查找,效率极低。 ## 2. 常见链表排序算法 链表排序算法可分为 **比较排序** 和 **非比较排序** 两大类,每种算法都有其适用场景和性能特点。 ### 2.1 比较排序算法 | 算法 | 核心思想 | 时间复杂度 | 空间复杂度 | 特点 | |------|----------|------------|------------|------| | **冒泡排序** | 相邻节点比较交换 | $O(n^2)$ | $O(1)$ | 实现简单,适合小规模数据 | | **选择排序** | 选择最小节点交换 | $O(n^2)$ | $O(1)$ | 交换次数少,适合交换成本高的场景 | | **插入排序** | 逐个插入到有序序列 | $O(n^2)$ | $O(1)$ | 链表上表现优异,适合部分有序数据 | | **归并排序** | 分治合并 | $O(n \log n)$ | $O(1)$ | 链表上的最优选择,稳定高效 | | **快速排序** | 基准分割递归 | $O(n \log n)$ | $O(1)$ | 平均性能好,但最坏情况 $O(n^2)$ | ### 2.2 非比较排序算法 | 算法 | 核心思想 | 时间复杂度 | 空间复杂度 | 适用条件 | |------|----------|------------|------------|----------| | **计数排序** | 统计频率重建 | $O(n + k)$ | $O(k)$ | 值域范围小,$k$ 为值域大小 | | **桶排序** | 分桶排序合并 | $O(n)$ | $O(n + m)$ | 数据分布均匀,$m$ 为桶数 | | **基数排序** | 按位分桶排序 | $O(n \times d)$ | $O(n + k)$ | 数字位数 $d$ 较小 | ### 2.3 算法选择建议 - **小规模数据**:选择冒泡排序或插入排序 - **大规模数据**:优先考虑归并排序 - **特定场景**:根据数据特征选择相应的非比较排序 ## 参考资料 - 【文章】[单链表的冒泡排序_zhao_miao的博客 - CSDN博客](https://blog.csdn.net/zhao_miao/article/details/81708454) - 【文章】[链表排序总结(全)(C++)- 阿祭儿 - CSDN博客](https://blog.csdn.net/qq_32523711/article/details/107402873) - 【题解】[快排、冒泡、选择排序实现列表排序 - 排序链表 - 力扣](https://leetcode.cn/problems/sort-list/solution/kuai-pai-mou-pao-xuan-ze-pai-xu-shi-xian-ula7/) - 【题解】[归并排序+快速排序 - 排序链表 - 力扣](https://leetcode.cn/problems/sort-list/solution/gui-bing-pai-xu-kuai-su-pai-xu-by-datacruiser/) - 【题解】[排序链表(递归+迭代)详解 - 排序链表 - 力扣](https://leetcode.cn/problems/sort-list/solution/pai-xu-lian-biao-di-gui-die-dai-xiang-jie-by-cherr/) - 【题解】[Sort List (归并排序链表) - 排序链表 - 力扣](https://leetcode.cn/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/) ================================================ FILE: docs/02_linked_list/02_03_linked_list_bubble_sort.md ================================================ ## 1. 链表冒泡排序算法思想 > **链表冒泡排序基本思想**: > > **通过相邻节点比较和交换,将最大值逐步「冒泡」到链表末尾**。 与数组冒泡排序类似,但需要处理链表的指针操作。 ## 2. 链表冒泡排序算法步骤 链表冒泡排序的算法步骤如下: 1. **外层循环**:控制排序轮数,每轮将当前最大值「冒泡」到末尾 2. **内层循环**:比较相邻节点,必要时交换值 3. **尾指针优化**:每轮结束后,末尾已排序部分不再参与比较 ``` 初始状态:head → 4 → 2 → 1 → 3 → null ↑ node_i, node_j 第1轮内循环: - 比较 4 和 2:4 > 2,交换 → 2 → 4 → 1 → 3 → null - 比较 4 和 1:4 > 1,交换 → 2 → 1 → 4 → 3 → null - 比较 4 和 3:4 > 3,交换 → 2 → 1 → 3 → 4 → null 第1轮结束:tail 指向 4,4 已排好序 第2轮内循环:只在 2 → 1 → 3 范围内比较 ... ``` ## 3. 链表冒泡排序实现代码 ```python class Solution: def bubbleSort(self, head: ListNode): if not head or not head.next: return head # 外层循环:控制排序轮数 node_i = head tail = None # 尾指针,右侧为已排序部分 while node_i: node_j = head # 内层循环指针 # 内层循环:比较相邻节点 while node_j and node_j.next != tail: if node_j.val > node_j.next.val: # 交换相邻节点的值 node_j.val, node_j.next.val = node_j.next.val, node_j.val node_j = node_j.next # 更新尾指针,右侧已排好序 tail = node_j node_i = node_i.next return head def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.bubbleSort(head) ``` ## 4. 链表冒泡排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n)$ | 链表已有序,配合「提前终止」优化仅需一趟 | | **最坏时间复杂度** | $O(n^2)$ | 链表逆序,多轮遍历与相邻节点交换 | | **平均时间复杂度** | $O(n^2)$ | 一般情况下需要二重循环比较交换 | | **空间复杂度** | $O(1)$ | 原地排序,仅使用常数个指针变量 | | **稳定性** | 稳定 | 相等元素的相对次序保持不变 | **适用场景**: - **小规模数据**:节点数量 < 100 - **教学演示**:理解排序算法原理 - **特殊要求**:需要稳定排序且空间受限 ## 5. 总结 链表中的冒泡排序是最简单的链表排序之一,通过相邻节点比较交换实现排序。虽然实现简单,但效率较低。 - **优点**:实现简单,稳定排序,空间复杂度低 - **缺点**:时间复杂度高,交换次数多 ## 练习题目 - [0148. 排序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md) (链表冒泡排序会超时,仅做练习) - [链表排序题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%93%BE%E8%A1%A8%E6%8E%92%E5%BA%8F%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/02_linked_list/02_04_linked_list_selection_sort.md ================================================ ## 1. 链表选择排序算法思想 > **链表选择排序基本思想**: > > 在未排序部分中找到最小元素,然后将其放到已排序部分的末尾。 ## 2. 链表选择排序算法步骤 1. **初始化**:使用两个指针 `node_i` 和 `node_j`。`node_i` 指向当前未排序部分的第一个节点,同时也用于控制外循环。 2. **寻找最小值**:在未排序部分中,使用 `min_node` 记录值最小的节点。初始时假设 `node_i` 为最小值节点。 3. **比较交换**:遍历未排序部分,比较每个节点的值。如果发现更小的值,则更新 `min_node`。 4. **交换操作**:一趟排序结束后,如果 `min_node` 不等于 `node_i`,则交换两个节点的值。 5. **移动指针**:将 `node_i` 向右移动一位,继续处理剩余未排序部分。 6. **终止条件**:当 `node_i` 为 `None` 或 `node_i.next` 为 `None` 时,排序完成。 ## 3. 链表选择排序实现代码 ```python class Solution: def selectionSort(self, head: ListNode): node_i = head # 外层循环:遍历每个节点 while node_i and node_i.next: # 假设当前节点为最小值节点 min_node = node_i node_j = node_i.next # 内层循环:在未排序部分寻找最小值 while node_j: if node_j.val < min_node.val: min_node = node_j node_j = node_j.next # 如果找到更小的值,则交换 if node_i != min_node: node_i.val, min_node.val = min_node.val, node_i.val # 移动到下一个节点 node_i = node_i.next return head def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.selectionSort(head) ``` ## 4. 链表选择排序算法复杂度分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n^2)$ | 无论初始顺序如何,都需 $O(n^2)$ 次比较 | | **最坏时间复杂度** | $O(n^2)$ | 无论初始顺序如何,都需 $O(n^2)$ 次比较 | | **平均时间复杂度** | $O(n^2)$ | 比较次数与数据状态无关 | | **空间复杂度** | $O(1)$ | 原地排序,仅使用常数额外空间 | | **稳定性** | 不稳定 | 可能改变相等节点的相对次序 | **适用场景**: - **小规模数据**:节点数量较少的场景(如 < 100) - **对空间复杂度严格**:仅使用常数额外空间的需求 - **交换代价高的场景**:选择排序交换次数少(最多 \(n-1\) 次) ## 5. 总结 链表中的选择排序是一种简单直观的链表排序算法,通过在未排序部分中选择最小节点并将其放到已排序部分的末尾来完成排序。虽然实现简单,但效率较低。 - **优点**:实现简单,空间复杂度低,交换次数少 - **缺点**:时间复杂度高,不稳定,不适合大规模数据 ## 练习题目 - [0148. 排序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md)(链表选择排序会超时,仅做练习) - [链表排序题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%93%BE%E8%A1%A8%E6%8E%92%E5%BA%8F%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/02_linked_list/02_05_linked_list_insertion_sort.md ================================================ ## 1. 链表插入排序基本思想 > **链表插入排序基本思想**: > > 将链表分为已排序部分和未排序部分,逐个将未排序部分的节点插入到已排序部分的正确位置**。 链表插入排序的算法步骤如下: 1. **初始化**: - 创建哑节点 `dummy_head`,指向链表头 `head` - 设置 `sorted_tail` 为已排序部分的尾节点,初始为 `head` - 设置 `cur` 为当前待插入节点,初始为 `head.next` 2. **插入过程**: - 如果 `cur.val >= sorted_tail.val`:说明 `cur` 已经在正确位置,将 `sorted_tail` 后移 - 如果 `cur.val < sorted_tail.val`:需要将 `cur` 插入到已排序部分的合适位置 - 初始化 `prev = dummy_head`,用于在已排序部分中寻找插入位置 - 使用 `while prev.next.val <= cur.val` 循环,让 `prev` 移动到第一个大于 `cur.val` 的节点的前一个位置 - 执行插入操作: - `sorted_tail.next = cur.next`:从原位置移除 `cur` - `cur.next = prev.next`:`cur` 指向 `prev` 的下一个节点 - `prev.next = cur`:`prev` 指向 `cur`,完成插入 3. **更新指针**: - 更新 `cur` 为下一个待插入节点:`cur = sorted_tail.next` - 重复步骤2,直到所有节点都处理完毕 ### 关键理解 - 已排序部分始终是有序的 - 每次插入后,已排序部分长度+1,未排序部分长度-1 - 插入操作需要维护链表的前后连接关系 - **`prev` 的作用**:在已排序部分中寻找插入位置,它始终指向要插入位置的前一个节点 - **`prev` 的移动规律**:通过 `prev.next.val <= cur.val` 的条件,`prev` 会移动到第一个大于 `cur.val` 的节点的前一个位置 ## 2. 链表插入排序实现代码 ```python class Solution: def insertionSort(self, head: ListNode): if not head or not head.next: return head # 创建哑节点,简化边界情况处理 dummy_head = ListNode(-1) dummy_head.next = head # sorted_tail: 已排序部分的尾节点 # cur: 当前待插入的节点 sorted_tail = head cur = head.next while cur: if sorted_tail.val <= cur.val: # cur 已经在正确位置,扩展已排序部分 sorted_tail = sorted_tail.next else: # 需要插入 cur 到已排序部分的合适位置 prev = dummy_head # 找到插入位置:第一个大于 cur.val 的节点的前一个位置 while prev.next.val <= cur.val: prev = prev.next # 执行插入操作 sorted_tail.next = cur.next # 从原位置移除 cur cur.next = prev.next # cur 指向下一个节点 prev.next = cur # 前一个节点指向 cur # 移动到下一个待插入节点 cur = sorted_tail.next return dummy_head.next def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.insertionSort(head) ``` ## 3. 链表插入排序算法复杂度分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n)$ | 链表基本有序,仅线性扫描与少量移动 | | **最坏时间复杂度** | $O(n^2)$ | 链表逆序,每次插入需线性查找插入位置 | | **平均时间复杂度** | $O(n^2)$ | 一般情况下多次线性插入 | | **空间复杂度** | $O(1)$ | 原地排序,仅使用常数个指针变量 | | **稳定性** | 稳定 | 相等节点的相对次序保持不变 | **适用场景**: - 链表长度较小(通常 < 1000) - 链表基本有序的情况 - 需要稳定排序的场景 - 作为教学或理解插入排序原理的练习 ## 4. 总结 链表插入排序通过将未排序节点插入到已排序部分的正确位置完成排序。对近乎有序的链表表现较好,但总体效率不高。 - **优点**:实现简单,稳定排序,空间复杂度低,适合近乎有序数据 - **缺点**:平均/最坏时间复杂度高,不适合大规模数据 ## 练习题目 - [0148. 排序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md)(链表插入排序会超时,仅做练习) - [0147. 对链表进行插入排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/insertion-sort-list.md) - [链表排序题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%93%BE%E8%A1%A8%E6%8E%92%E5%BA%8F%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/02_linked_list/02_06_linked_list_merge_sort.md ================================================ ## 1. 链表归并算法基本思想 > **链表归并排序基本思想**: > > **采用分治策略,将链表递归分割为更小的子链表,然后两两归并得到有序链表**。 链表归并排序的算法步骤如下: 1. **分割阶段**:找到链表的中间节点,将链表从中间断开,并递归进行分割。 1. 使用快慢指针法,`fast = head.next`、`slow = head`,让 `fast` 每次移动 2 步,`slow` 移动 1 步,当 `fast` 到达链表末尾时,`slow` 即为链表的中间节点。 2. 从中间位置将链表分为左右两个子链表 `left_head` 和 `right_head`,并从中间位置断开,即 `slow.next = None`。 3. 对左右两个子链表分别进行递归分割,直到每个子链表中只包含一个节点。 2. **归并阶段**:将递归分割后的子链表进行两两归并,完成一遍归并后每个子链表长度加倍。重复进行归并操作,直到得到完整的排序链表。 1. 使用哑节点 `dummy_head` 构造一个头节点,并使用 `cur` 指向 `dummy_head` 用于遍历。 2. 比较两个子链表头节点 `left` 和 `right` 的值大小。将较小的头节点加入到合并后的链表中,并向后移动该链表的头节点指针。 3. 重复上一步操作,直到其中一个子链表为空。 4. 将剩余非空的子链表直接连接到合并后的链表末尾。 5. 返回哑节点的下一个节点 `dummy_head.next` 作为合并后的头节点。 ## 2. 链表归并排序实现代码 ```python class Solution: def merge(self, left, right): # 归并阶段 dummy_head = ListNode(-1) cur = dummy_head while left and right: if left.val <= right.val: cur.next = left left = left.next else: cur.next = right right = right.next cur = cur.next if left: cur.next = left elif right: cur.next = right return dummy_head.next def mergeSort(self, head: ListNode): # 分割阶段 if not head or not head.next: return head # 快慢指针找到中间节点 slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next # 断开左右子链表 left_head, right_head = head, slow.next slow.next = None # 归并操作 return self.merge(self.mergeSort(left_head), self.mergeSort(right_head)) def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.mergeSort(head) ``` ## 3. 链表归并排序算法复杂度分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n \log n)$ | 每次二分出约 $\log n$ 层;每层线性合并 $n$ 个节点 | | **最坏时间复杂度** | $O(n \log n)$ | 任何输入都经历 $\log n$ 层 × 每层 $O(n)$ 合并 | | **平均时间复杂度** | $O(n \log n)$ | 期望同上:$\log n$ 层 × 每层 $O(n)$ | | **空间复杂度** | $O(\log n)$ | 递归需要约 $\log n$ 层调用栈(迭代自底向上可降到 $O(1)$) | | **稳定性** | 稳定 | 归并时相等元素优先取左侧,原相对顺序保留 | ## 4. 总结 链表归并排序采用分治策略,将链表递归拆分后再两两归并得到有序链表,整体效率高且稳定。 - **优点**:时间复杂度 $O(n\log n)$,稳定排序,适合大规模链表;无需随机访问,天然适配链表结构 - **缺点**:递归实现需要 $O(\log n)$ 栈空间,实现相对复杂,常数因子较高 ## 练习题目 - [0148. 排序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md) - [0021. 合并两个有序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-two-sorted-lists.md) - [0023. 合并 K 个升序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) - [链表排序题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%93%BE%E8%A1%A8%E6%8E%92%E5%BA%8F%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/02_linked_list/02_07_linked_list_quick_sort.md ================================================ ## 1. 链表快速排序基本思想 > **链表快速排序基本思想**: > > 通过选择基准值(pivot)将链表分割为两部分,使得左部分所有节点的值都小于基准值,右部分所有节点的值都大于等于基准值,然后递归地对左右两部分进行排序,最终实现整个链表的有序排列。 链表快速排序的核心思想是**分治策略**,具体算法步骤如下: 1. **选择基准值**:从链表中选择一个基准值 `pivot`,通常选择头节点的值作为基准值。 2. **分割链表**:通过快慢指针(`node_i`、`node_j`)遍历链表,将链表分割为两部分: - 左部分:所有节点值都小于基准值 - 右部分:所有节点值都大于等于基准值 3. **递归排序**:对分割后的左右两部分分别递归执行快速排序 4. **合并结果**:当子链表长度小于等于1时,递归结束,最终得到有序链表 ## 2. 链表快速排序实现代码 ```python class Solution: def partition(self, left: ListNode, right: ListNode): """ 分割函数:将链表分割为两部分 left: 左边界节点(包含) right: 右边界节点(不包含) 返回:基准值节点的最终位置 """ # 边界条件:区间没有元素或者只有一个元素,直接返回第一个节点 if left == right or left.next == right: return left # 选择头节点为基准节点 pivot = left.val # 使用快慢指针进行分割 # node_i: 指向小于基准值的最后一个节点 # node_j: 遍历指针,寻找小于基准值的节点 node_i, node_j = left, left.next while node_j != right: # 发现一个小于基准值的元素 if node_j.val < pivot: # 将 node_i 向右移动一位 node_i = node_i.next # 交换 node_i 和 node_j 的值,保证 node_i 之前的节点都小于基准值 node_i.val, node_j.val = node_j.val, node_i.val node_j = node_j.next # 将基准节点放到正确位置上(node_i 位置) node_i.val, left.val = left.val, node_i.val return node_i def quickSort(self, left: ListNode, right: ListNode): """ 快速排序主函数 left: 左边界节点(包含) right: 右边界节点(不包含) """ # 递归终止条件:区间长度小于等于 1 if left == right or left.next == right: return left # 分割链表,获取基准值位置 pi = self.partition(left, right) # 递归排序左半部分 self.quickSort(left, pi) # 递归排序右半部分 self.quickSort(pi.next, right) return left def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: """ 链表排序入口函数 """ # 边界条件检查 if not head or not head.next: return head # 调用快速排序 return self.quickSort(head, None) ``` ## 3. 链表快速排序算法复杂度分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n \log n)$ | 每次等分为两半,递归层数约 $\log n$ | | **最坏时间复杂度** | $O(n^2)$ | 已有序/逆序或重复值多,划分极端不均 | | **平均时间复杂度** | $O(n \log n)$ | 期望情况下划分较均匀 | | **空间复杂度** | $O(\log n)$ | 递归调用栈深度(原地就地分区,无额外数组) | | **稳定性** | 不稳定 | 相等节点的相对顺序可能改变 | ## 4. 总结 链表快速排序通过分治与就地分区实现排序,平均效率高,但极端情况下退化明显,且不稳定。 - **优点**:平均时间复杂度 $O(n\log n)$,就地分区、空间开销小 - **缺点**:最坏时间复杂度 $O(n^2)$,不稳定,对枢轴选择敏感 ## 练习题目 - [0148. 排序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md)(链表快速排序会超时,仅做练习) - [链表排序题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%93%BE%E8%A1%A8%E6%8E%92%E5%BA%8F%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/02_linked_list/02_08_linked_list_counting_sort.md ================================================ ## 1. 链表计数排序基本思想 > **计数排序(Counting Sort)基本思想**: > > 统计每个元素在序列中出现的次数,然后根据统计结果将元素放回正确的位置。 对于链表结构,计数排序的基本思想是: 1. **统计阶段**:遍历链表,统计每个数值出现的次数 2. **重构阶段**:根据统计结果,重新构建有序链表 ## 2. 链表计数排序算法步骤 1. **初始化阶段** - 使用 `cur` 指针遍历一遍链表 - 找出链表中最大值 `list_max` 和最小值 `list_min` - 计算数值范围:`size = list_max - list_min + 1` 2. **计数统计阶段** - 创建大小为 `size` 的计数数组 `counts`,初始化为 0 - 再次遍历链表,统计每个数值出现的次数 - 将计数结果存储在 `counts[cur.val - list_min]` 中 3. **重构链表阶段** - 建立哑节点 `dummy_head` 作为新链表的头节点 - 使用 `cur` 指针指向 `dummy_head` - 从小到大遍历计数数组 `counts` - 对于每个非零计数,创建相应数量的节点,值为 `i + list_min` - 将新节点插入到当前指针位置,并移动指针 ## 3. 链表计数排序代码实现 ```python class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class Solution: def countingSort(self, head: ListNode) -> ListNode: # 边界条件检查 if not head or not head.next: return head # 步骤1: 找出链表中最大值和最小值 list_min, list_max = float('inf'), float('-inf') cur = head while cur: if cur.val < list_min: list_min = cur.val if cur.val > list_max: list_max = cur.val cur = cur.next # 计算数值范围 size = list_max - list_min + 1 # 步骤2: 创建计数数组并统计每个数值的出现次数 counts = [0 for _ in range(size)] cur = head while cur: # 将数值映射到计数数组的索引 index = cur.val - list_min counts[index] += 1 cur = cur.next # 步骤3: 重构有序链表 dummy_head = ListNode(-1) # 哑节点,简化头节点操作 cur = dummy_head # 遍历计数数组,按顺序重构链表 for i in range(size): # 对于每个非零计数,创建相应数量的节点 while counts[i] > 0: # 创建新节点,值为 i + list_min new_node = ListNode(i + list_min) cur.next = new_node cur = cur.next counts[i] -= 1 return dummy_head.next def sortList(self, head: ListNode) -> ListNode: """ 排序链表的主函数 Args: head: 待排序链表的头节点 Returns: 排序后的链表头节点 """ return self.countingSort(head) ``` ## 4. 链表计数排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n + k)$ | 遍历链表 n 次 + 遍历计数数组 k 次 | | **最坏时间复杂度** | $O(n + k)$ | 与初始顺序无关,始终为 n + k | | **平均时间复杂度** | $O(n + k)$ | 其中 n 为链表长度,k 为值域大小(max - min + 1) | | **空间复杂度** | $O(k)$ | 需要大小为 k 的计数数组 | | **稳定性** | 稳定 | 重构按数值顺序写回,保留相对次序 | ## 5. 总结 链表计数排序通过「统计次数 + 重构链表」完成排序,在值域小且为整数的场景下接近线性时间。 - **优点**:时间近线性(当 k 较小)、实现简单、稳定排序 - **缺点**:额外空间 $O(k)$,对值域敏感,不适合大值域或稀疏分布 ## 练习题目 - [0148. 排序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md) - [链表排序题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%93%BE%E8%A1%A8%E6%8E%92%E5%BA%8F%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/02_linked_list/02_09_linked_list_bucket_sort.md ================================================ ## 1. 链表桶排序算法思想 > **桶排序基本思想**: > > 将数据分散到若干个有序的桶中,每个桶内再单独排序,最后按顺序合并所有桶。 ## 2. 链表桶排序算法步骤 1. **确定数据范围**:遍历链表找出最大值和最小值。 2. **计算桶数量**:根据数据范围和桶大小确定桶的个数。 3. **分配元素到桶**:将每个元素放入对应的桶中。 4. **桶内排序**:对每个桶内的元素进行排序(使用归并排序等)。 5. **合并结果**:按桶的顺序将所有元素合并成有序链表。 ## 3. 链表桶排序代码实现 ```python class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class Solution: def insertion(self, buckets, index, val): """ 将元素插入到指定桶中(头插法) Args: buckets: 桶数组 index: 桶的索引 val: 要插入的值 """ if not buckets[index]: # 如果桶为空,直接创建新节点 buckets[index] = ListNode(val) return # 头插法:新节点插入到桶的头部 node = ListNode(val) node.next = buckets[index] buckets[index] = node def merge(self, left, right): """ 归并两个有序链表 Args: left: 左链表头节点 right: 右链表头节点 Returns: 合并后的有序链表头节点 """ dummy_head = ListNode(-1) # 虚拟头节点 cur = dummy_head # 比较两个链表的节点值,选择较小的加入结果链表 while left and right: if left.val <= right.val: cur.next = left left = left.next else: cur.next = right right = right.next cur = cur.next # 处理剩余节点 if left: cur.next = left elif right: cur.next = right return dummy_head.next def mergeSort(self, head): """ 对链表进行归并排序 Args: head: 链表头节点 Returns: 排序后的链表头节点 """ # 递归终止条件:空链表或单节点 if not head or not head.next: return head # 快慢指针找到链表中间位置 slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next # 分割链表为左右两部分 left_head, right_head = head, slow.next slow.next = None # 递归排序左右两部分,然后归并 return self.merge(self.mergeSort(left_head), self.mergeSort(right_head)) def bucketSort(self, head, bucket_size=5): """ 链表桶排序主函数 Args: head: 待排序的链表头节点 bucket_size: 每个桶的大小,默认5 Returns: 排序后的链表头节点 """ if not head: return head # 第一步:找出链表中的最大值和最小值 list_min, list_max = float('inf'), float('-inf') cur = head while cur: list_min = min(list_min, cur.val) list_max = max(list_max, cur.val) cur = cur.next # 第二步:计算桶的数量并初始化桶数组 bucket_count = (list_max - list_min) // bucket_size + 1 buckets = [None for _ in range(bucket_count)] # 第三步:将链表元素分配到对应的桶中 cur = head while cur: # 计算元素应该放入哪个桶 index = (cur.val - list_min) // bucket_size self.insertion(buckets, index, cur.val) cur = cur.next # 第四步:对每个桶内的元素排序,然后合并 dummy_head = ListNode(-1) cur = dummy_head for bucket_head in buckets: if bucket_head: # 对桶内元素进行归并排序 sorted_bucket = self.mergeSort(bucket_head) # 将排序后的桶内元素添加到结果链表 while sorted_bucket: cur.next = sorted_bucket cur = cur.next sorted_bucket = sorted_bucket.next return dummy_head.next def sortList(self, head): """ 排序链表接口函数 Args: head: 待排序的链表头节点 Returns: 排序后的链表头节点 """ return self.bucketSort(head) ``` ## 4. 链表桶排序算法复杂度分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n)$ | 元素均匀分布,各桶很少元素,桶内排序代价小 | | **最坏时间复杂度** | $O(n^2)$ | 大量元素落入同一桶,桶内排序退化 | | **平均时间复杂度** | $O(n + k)$ | 遍历元素 n 次 + 遍历 k 个桶(含桶内排序) | | **空间复杂度** | $O(n + k)$ | 需要桶数组与桶内临时节点空间 | | **稳定性** | 稳定 | 桶内使用稳定排序并按桶序合并,保留相对次序 | ## 5. 总结 链表桶排序将元素按值域分配到多个桶中,分别排序后再合并。适合值域已知且分布较均匀的场景。 - **优点**:分布均匀且桶参数合理时可接近线性;可用稳定桶内排序;并行友好 - **缺点**:对分布与桶大小/数量敏感;额外空间 $O(n+k)$;最坏情况可能退化 ## 练习题目 - [0148. 排序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md) - [链表排序题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%93%BE%E8%A1%A8%E6%8E%92%E5%BA%8F%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/02_linked_list/02_10_linked_list_radix_sort.md ================================================ ## 1. 链表基数排序算法思想 > **基数排序算法思想**: > > 从最低位(个位)开始,按照每一位的数字将节点分配到对应的桶中,然后按顺序重新连接 ## 2. 链表基数排序算法步骤 1. **确定最大位数**:遍历链表找出最大数字的位数。 2. **按位排序**:从个位开始,依次处理每一位。 3. **分配桶**:根据当前位的数字(0-9)将节点分配到 10 个桶中。 4. **重新连接**:按桶的顺序重新连接链表。 5. **重复处理**:处理完所有位后完成排序。 ## 3. 链表基数排序代码实现 ```python class Solution: def radixSort(self, head: ListNode): # 1. 计算最大数字的位数 size = 0 cur = head while cur: val_len = len(str(cur.val)) size = max(size, val_len) cur = cur.next # 2. 从个位到最高位依次排序 for i in range(size): # 创建 10 个桶(对应数字 0-9) buckets = [[] for _ in range(10)] cur = head # 3. 按当前位数字分配到对应桶 while cur: # 获取第 i 位数字:先除以 10^i,再对 10 取余 digit = (cur.val // (10 ** i)) % 10 buckets[digit].append(cur.val) cur = cur.next # 4. 按桶的顺序重新构建链表 dummy_head = ListNode(-1) cur = dummy_head for bucket in buckets: for num in bucket: cur.next = ListNode(num) cur = cur.next head = dummy_head.next return head def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.radixSort(head) ``` ## 4. 链表基数排序算法分析 | 指标 | 复杂度 | 说明 | |------|--------|------| | **最佳时间复杂度** | $O(n \times k)$ | 每一位都需遍历所有节点,总共 $k$ 位,每位操作 $O(n)$,整体 $O(nk)$ | | **最坏时间复杂度** | $O(n \times k)$ | 同上;与初始顺序无关 | | **平均时间复杂度** | $O(n \times k)$ | 同上;$n$ 为节点数,$k$ 为最大位数 | | **空间复杂度** | $O(n + k)$ | 需要 $n$ 个临时节点/数组及 $k$ 个桶 | | **稳定性** | 稳定 | 按桶顺序收集,保持相等值相对次序 | ## 5. 总结 链表基数排序按位进行「分桶 + 收集」,在位数较小、整数键的场景下效率稳定。 - **优点**:时间复杂度与数据有序度无关,稳定排序,适合固定位数整数 - **缺点**:空间开销较高,仅适用于整数或可映射到位的键 ## 练习题目 - [链表排序题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%93%BE%E8%A1%A8%E6%8E%92%E5%BA%8F%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/02_linked_list/02_11_linked_list_two_pointers.md ================================================ ## 1. 双指针简介 双指针是链表问题中非常常用且高效的技巧,通过两个指针的配合移动,能够巧妙地解决许多复杂问题。 > **双指针(Two Pointers)**:指在遍历链表时同时使用两个指针,根据移动方式主要分为以下两类: > > - **快慢指针**:两个指针从同一起点出发,移动速度不同,常用于检测环、寻找中点、定位倒数第 n 个节点等。 > - **起点不一致**:快指针先行若干步,之后快慢指针同步移动,常用于定位倒数第 n 个节点。 > - **步长不一致**:快指针每次移动两步,慢指针每次移动一步,常用于判断链表是否有环、寻找中点等。 > - **分离双指针**:两个指针分别在不同链表或不同起点上独立移动,常用于合并两个有序链表、比较链表节点等场景。 双指针法能够有效降低时间复杂度,减少空间消耗,是解决链表相关问题(如查找、删除、合并等)时非常高效且常用的技巧。 ## 2. 快慢指针(起点不一致) > **起点不一致的快慢指针**:快指针先走 n 步,然后两个指针同时移动,快指针到达末尾时,慢指针正好在目标位置。 ### 2.1 求解步骤 1. **初始化**:两个指针 $slow$、$fast$ 都指向头节点。 2. **快指针先行**:快指针先移动 n 步。 3. **同步移动**:两个指针同时移动,直到快指针到达末尾(`fast == None`)。 4. **结果**:慢指针正好指向倒数第 n 个节点。 ### 2.2 代码模板 ```python def findNthFromEnd(head, n): slow = fast = head # 快指针先走 n 步 for _ in range(n): fast = fast.next # 两个指针同时移动 while fast: slow = slow.next fast = fast.next return slow # 慢指针指向倒数第 n 个节点 ``` ### 2.3 适用场景 - 找到链表中倒数第 k 个节点 - 删除链表倒数第 N 个节点 - 其他需要定位倒数位置的问题 ### 2.4 经典例题:删除链表的倒数第 N 个结点 #### 2.4.1 题目链接 - [19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) #### 2.4.2 题目大意 **描述**:给定一个链表的头节点 `head`。 **要求**:删除链表的倒数第 `n` 个节点,并且返回链表的头节点。 **说明**: - 要求使用一次遍历实现。 - 链表中结点的数目为 `sz`。 - $1 \le sz \le 30$。 - $0 \le Node.val \le 100$。 - $1 \le n \le sz$。 **示例**: ![](https://assets.leetcode.com/uploads/2020/10/03/remove_ex1.jpg) ```python 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] 输入:head = [1], n = 1 输出:[] ``` #### 2.4.3 解题思路 ##### 思路 1:快慢指针 常规思路是遍历一遍链表,求出链表长度,再遍历一遍到对应位置,删除该位置上的节点。 如果用一次遍历实现的话,可以使用快慢指针。让快指针先走 `n` 步,然后快慢指针、慢指针再同时走,每次一步,这样等快指针遍历到链表尾部的时候,慢指针就刚好遍历到了倒数第 `n` 个节点位置。将该位置上的节点删除即可。 ##### 思路 1:代码 ```python class Solution: def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode: # 创建虚拟头节点,简化边界情况处理 dummy = ListNode(0, head) slow = dummy fast = head # 快指针先走 n 步 for _ in range(n): fast = fast.next # 两个指针同时移动 while fast: slow = slow.next fast = fast.next # 删除目标节点(slow.next 是要删除的节点) slow.next = slow.next.next return dummy.next ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,只遍历一次 - **空间复杂度**:O(1),只使用常数额外空间 ## 3. 快慢指针(步长不一致) > **步长不一致的快慢指针**:两个指针从同一起点出发,慢指针每次走 1 步,快指针每次走 2 步。 ### 3.1 求解步骤 1. **初始化**:两个指针都指向头节点。 2. **不同步长**:慢指针每次移动 1 步,快指针每次移动 2 步。 3. **终止条件**:快指针到达末尾或无法继续移动。 4. **应用场景**:找中点、检测环、找交点等。 ### 3.2 代码模板 ```python def fastSlowPointer(head): slow = fast = head # 快指针每次走 2 步,慢指针每次走 1 步 while fast and fast.next: slow = slow.next # 慢指针移动 1 步 fast = fast.next.next # 快指针移动 2 步 return slow # 慢指针指向中点或环的入口 ``` ### 3.3 适用场景 - 寻找链表的中点 - 检测链表是否有环 - 找到两个链表的交点 - 其他需要定位中间位置的问题 ### 3.4 经典例题:链表的中间结点 #### 3.4.1 题目链接 - [876. 链表的中间结点 - 力扣(LeetCode)](https://leetcode.cn/problems/middle-of-the-linked-list/) #### 3.4.2 题目大意 **描述**:给定一个单链表的头节点 `head`。 **要求**:返回链表的中间节点。如果有两个中间节点,则返回第二个中间节点。 **说明**: - 给定链表的结点数介于 `1` 和 `100` 之间。 **示例**: ```python 输入:[1,2,3,4,5] 输出:此列表中的结点 3 (序列化形式:[3,4,5]) 解释:返回的结点值为 3 。 注意,我们返回了一个 ListNode 类型的对象 ans,这样: ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL. 输入:[1,2,3,4,5,6] 输出:此列表中的结点 4 (序列化形式:[4,5,6]) 解释:由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。 ``` #### 3.4.3 解题思路 ##### 思路 1:单指针 先遍历一遍链表,统计一下节点个数为 `n`,再遍历到 `n / 2` 的位置,返回中间节点。 ##### 思路 1:代码 ```python class Solution: def middleNode(self, head: ListNode) -> ListNode: # 第一次遍历:计算链表长度 count = 0 curr = head while curr: count += 1 curr = curr.next # 第二次遍历:找到中间位置 curr = head for _ in range(count // 2): curr = curr.next return curr ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ##### 思路 2:快慢指针 使用步长不一致的快慢指针进行一次遍历找到链表的中间节点。具体做法如下: 1. 使用两个指针 `slow`、`fast`。`slow`、`fast` 都指向链表的头节点。 2. 在循环体中将快、慢指针同时向右移动。其中慢指针每次移动 `1` 步,即 `slow = slow.next`。快指针每次移动 `2` 步,即 `fast = fast.next.next`。 3. 等到快指针移动到链表尾部(即 `fast == Node`)时跳出循环体,此时 `slow` 指向链表中间位置。 4. 返回 `slow` 指针。 ##### 思路 2:代码 ```python class Solution: def middleNode(self, head: ListNode) -> ListNode: slow = fast = head # 快指针每次走 2 步,慢指针每次走 1 步 # 当快指针到达末尾时,慢指针正好在中点 while fast and fast.next: slow = slow.next # 慢指针移动 1 步 fast = fast.next.next # 快指针移动 2 步 return slow ``` ##### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ## 4. 分离双指针 > **分离双指针**:两个指针分别在不同的链表中移动,常用于合并、比较等操作。 ### 4.1 求解步骤 1. **初始化**:两个指针分别指向两个链表的头节点。 2. **条件移动**:根据具体问题决定何时移动哪个指针。 3. **终止条件**:其中一个链表遍历完毕或满足特定条件。 4. **应用场景**:有序链表合并、链表比较等。 ### 4.2 代码模板 ```python def separateTwoPointers(list1, list2): p1, p2 = list1, list2 while p1 and p2: if condition1: # 两个指针同时移动 p1 = p1.next p2 = p2.next elif condition2: # 只移动第一个指针 p1 = p1.next else: # 只移动第二个指针 p2 = p2.next return result ``` ### 4.3 适用场景 - 合并两个有序链表 - 比较两个链表 - 找到两个链表的交点 - 其他需要同时处理两个链表的问题 ### 4.4 经典例题:合并两个有序链表 #### 4.4.1 题目链接 - [21. 合并两个有序链表 - 力扣(LeetCode)](https://leetcode.cn/problems/merge-two-sorted-lists/) #### 4.4.2 题目大意 **描述**:给定两个升序链表的头节点 `list1` 和 `list2`。 **要求**:将其合并为一个升序链表。 **说明**: - 两个链表的节点数目范围是 $[0, 50]$。 - $-100 \le Node.val \le 100$。 - `list1` 和 `list2` 均按 **非递减顺序** 排列 **示例**: ![](https://assets.leetcode.com/uploads/2020/10/03/merge_ex1.jpg) ```python 输入:list1 = [1,2,4], list2 = [1,3,4] 输出:[1,1,2,3,4,4] 输入:list1 = [], list2 = [] 输出:[] ``` #### 4.4.3 解题思路 ##### 思路 1:归并排序 利用归并排序的思想,具体步骤如下: 1. 使用哑节点 `dummy_head` 构造一个头节点,并使用 `curr` 指向 `dummy_head` 用于遍历。 2. 然后判断 `list1` 和 `list2` 头节点的值,将较小的头节点加入到合并后的链表中。并向后移动该链表的头节点指针。 3. 然后重复上一步操作,直到两个链表中出现链表为空的情况。 4. 将剩余链表链接到合并后的链表中。 5. 将哑节点 `dummy_dead` 的下一个链节点 `dummy_head.next` 作为合并后有序链表的头节点返回。 ##### 思路 1:代码 ```python class Solution: def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: # 创建虚拟头节点,简化操作 dummy = ListNode(-1) curr = dummy # 分离双指针:分别遍历两个链表 while list1 and list2: if list1.val <= list2.val: # 选择 list1 的当前节点 curr.next = list1 list1 = list1.next else: # 选择 list2 的当前节点 curr.next = list2 list2 = list2.next curr = curr.next # 处理剩余节点 curr.next = list1 if list1 else list2 return dummy.next ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ## 练习题目 - [0141. 环形链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle.md) - [0142. 环形链表 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle-ii.md) - [0019. 删除链表的倒数第 N 个结点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-nth-node-from-end-of-list.md) - [链表双指针题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/02_linked_list/index.md ================================================ ![](https://qcdn.itcharge.cn/images/20250923140251.png) ::: tip 引 言 链表如同夜空中串联的星星。 节点相连,如繁星串珠,静静铺展成一条灵动的数据长河。 ::: ## 本章内容 - [2.1 链表基础](https://github.com/ITCharge/AlgoNote/tree/main/docs/02_linked_list/02_01_linked_list_basic.md) - [2.2 链表排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/02_linked_list/02_02_linked_list_sort.md) - [2.3 链表冒泡排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/02_linked_list/02_03_linked_list_bubble_sort.md) - [2.4 链表选择排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/02_linked_list/02_04_linked_list_selection_sort.md) - [2.5 链表插入排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/02_linked_list/02_05_linked_list_insertion_sort.md) - [2.6 链表归并排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/02_linked_list/02_06_linked_list_merge_sort.md) - [2.7 链表快速排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/02_linked_list/02_07_linked_list_quick_sort.md) - [2.8 链表计数排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/02_linked_list/02_08_linked_list_counting_sort.md) - [2.9 链表桶排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/02_linked_list/02_09_linked_list_bucket_sort.md) - [2.10 链表基数排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/02_linked_list/02_10_linked_list_radix_sort.md) - [2.11 链表双指针](https://github.com/ITCharge/AlgoNote/tree/main/docs/02_linked_list/02_11_linked_list_two_pointers.md) ================================================ FILE: docs/03_stack_queue_hash_table/03_01_stack_basic.md ================================================ ## 1. 栈简介 > **栈(Stack)**:也叫做「堆栈」,一种线性表数据结构,只允许在表的一端进行插入和删除操作。 ### 1.1 基本概念 我们可以把栈想象成一摞叠放的盘子: - **栈顶(top)**:可以插入和删除元素的一端,就像盘子堆的最上面。 - **栈底(bottom)**:固定不动的一端,不能进行操作,就像盘子堆的最下面。 - **空栈**:栈中没有任何元素时,称为空栈。 ### 1.2 核心特性 栈的操作遵循 **后进先出(LIFO)** 的原则: - 最后放入栈的元素,最先被取出。 - 就像叠盘子,最后放上去的盘子,总是最先被拿走。 ### 1.3 基本操作 栈的常见操作有: - **入栈(Push)**:在栈顶加入一个新元素。 - **出栈(Pop)**:移除并返回栈顶的元素。 - **查看栈顶(Peek)**:只查看栈顶元素,但不移除。 下图展示了栈的结构和操作方式: ![栈结构](https://qcdn.itcharge.cn/images/202405092243204.png) ## 2. 栈的实现方式 与线性表类似,栈常见的存储方式有两种:**「顺序栈」** 和 **「链式栈」**。 - **顺序栈**:采用一段连续的存储空间(如数组)依次存放从栈底到栈顶的元素,并通过指针 $top$ 标记当前栈顶元素在数组中的位置。 - **链式栈**:采用单链表实现,每次新元素都插入到链表头部,$top$ 始终指向链表的头节点,即栈顶元素的位置。 ### 2.1 顺序栈(数组实现) 栈的最常见实现方式是利用数组来构建顺序存储结构。在 Python 中,可以直接使用列表(list)来实现顺序栈。 这种基于顺序存储的栈结构,通常被称为 **「顺序栈」**。 #### 2.1.1 顺序栈的基本描述 ![栈的顺序存储](https://qcdn.itcharge.cn/images/202405092243306.png) 我们约定 $self.top$ 指向当前栈顶元素的位置。 - **初始化空栈**:用列表创建空栈,设置栈的最大容量 $self.size$,并将栈顶指针 $self.top$ 设为 $-1$,即 $self.top = -1$。 - **判断栈空**:如果 $self.top == -1$,则栈为空,返回 $True$,否则返回 $False$。 - **判断栈满**:如果 $self.top == self.size - 1$,则栈已满,返回 $True$,否则返回 $False$。 - **入栈(push)**:先判断栈是否已满,如果已满则抛出异常。未满时,将新元素添加到 $self.stack$ 末尾,并将 $self.top$ 加 $1$。 - **出栈(pop)**:先判断栈是否为空,如果为空则抛出异常。不为空时,删除 $self.stack$ 末尾元素,并将 $self.top$ 减 $1$。 - **获取栈顶元素(peek)**:先判断栈是否为空,如果为空则抛出异常。不为空时,返回 $self.stack[self.top]$,即栈顶元素。 #### 2.1.2 顺序栈的实现代码 ```python class Stack: # 初始化空栈 def __init__(self, size=100): self.stack = [] # 存储元素的数组 self.size = size # 栈的最大容量 self.top = -1 # 栈顶指针,-1表示空栈 def is_empty(self): """判断栈是否为空""" return self.top == -1 def is_full(self): """判断栈是否已满""" return self.top + 1 == self.size def push(self, value): """入栈操作""" if self.is_full(): raise Exception('栈已满') self.stack.append(value) self.top += 1 def pop(self): """出栈操作""" if self.is_empty(): raise Exception('栈为空') value = self.stack.pop() self.top -= 1 return value def peek(self): """查看栈顶元素""" if self.is_empty(): raise Exception('栈为空') return self.stack[self.top] ``` - **时间复杂度**:入栈、出栈、查看栈顶均为 O(1) ### 2.2 链式栈(链表实现) ![栈的链式存储](https://qcdn.itcharge.cn/images/202509292200369.png) 顺序栈在存储空间上存在一定局限性:当栈满或需要扩容时,往往需要移动大量元素,效率较低。为了解决这一问题,可以采用链式存储结构实现栈。在 Python 中,我们通常通过自定义链表节点 $Node$ 来实现链式栈。采用链式存储结构的栈被称为 **「链式栈」**。 #### 2.2.1 链式栈的基本描述 约定 $self.top$ 始终指向栈顶元素。 - **初始化空栈**:将栈顶指针 $self.top$ 设为 $None$,表示栈为空。 - **判断栈是否为空**:如果 $self.top == None$,则栈为空,返回 $True$,否则返回 $False$。 - **入栈(push)**:新建一个值为 $value$ 的链表节点,将其插入到链表头部,并更新 $self.top$ 指向该新节点。 - **出栈(pop)**:先判断栈是否为空,如果为空则抛出异常。否则,记录当前栈顶节点,$self.top$ 指向下一个节点,并返回原栈顶节点的值。 - **获取栈顶元素(peek)**:先判断栈是否为空,如果为空则抛出异常。否则,返回 $self.top.value$。 #### 2.2.2 链式栈的实现代码 ```python class Node: """链表节点""" def __init__(self, value): self.value = value # 节点值 self.next = None # 指向下一个节点的指针 class Stack: def __init__(self): """初始化空栈""" self.top = None # 栈顶指针,指向链表头节点 def is_empty(self): """判断栈是否为空""" return self.top is None def push(self, value): """入栈操作 - 在链表头部插入新节点""" new_node = Node(value) new_node.next = self.top self.top = new_node def pop(self): """出栈操作 - 删除链表头节点""" if self.is_empty(): raise Exception('栈为空') value = self.top.value self.top = self.top.next return value def peek(self): """查看栈顶元素""" if self.is_empty(): raise Exception('栈为空') return self.top.value ``` - **时间复杂度**:入栈、出栈、查看栈顶均为 O(1) ### 2.3 两种实现方式对比 | 特性 | 顺序栈 | 链式栈 | |------|--------|--------| | 空间利用率 | 固定大小,可能浪费 | 按需分配,无浪费 | | 扩容操作 | 需要重新分配空间 | 无需扩容 | | 内存碎片 | 较少 | 可能产生碎片 | | 实现复杂度 | 简单 | 相对复杂 | ## 3. 栈的经典应用 ### 3.1 经典例题:括号匹配问题 #### 3.1.1 题目链接 - [20. 有效的括号 - 力扣(LeetCode)](https://leetcode.cn/problems/valid-parentheses/) #### 3.1.2 题目大意 **描述**:给定一个只包括 `'('`,`')'`,`'{'`,`'}'`,`'['`,`']'` 的字符串 $s$。 **要求**:判断字符串 $s$ 是否有效(即括号是否匹配)。 **说明**: - 有效字符串需满足: 1. 左括号必须用相同类型的右括号闭合。 2. 左括号必须以正确的顺序闭合。 **示例**: ```python 输入:s = "()" 输出:True 输入:s = "()[]{}" 输出:True ``` #### 3.2.3 解题思路 ##### 思路 1:栈 括号匹配问题是「栈」结构的经典应用场景。我们可以利用栈高效地判断括号是否匹配,具体思路如下: 1. 首先判断字符串长度是否为偶数。由于括号必须成对出现,如果长度为奇数,则一定无法完全匹配,直接返回 $False$。 2. 使用栈 $stack$ 存放尚未匹配的左括号。遍历字符串 $s$ 的每个字符,按如下规则处理: 1. 如果遇到左括号,则将其压入栈中。 2. 如果遇到右括号,检查栈顶元素是否为对应类型的左括号: 1. 如果匹配,则弹出栈顶元素,继续遍历。 2. 如果不匹配或栈已空,说明括号不合法,直接返回 $False$。 3. 遍历结束后,检查栈是否为空: 1. 如果栈为空,说明所有括号均已正确配对,返回 $True$。 2. 如果栈不为空,说明仍有未配对的左括号,返回 $False$。 ##### 思路 1:代码 ```python class Solution: def isValid(self, s: str) -> bool: # 如果字符串长度为奇数,必然无法完全配对,直接返回 False if len(s) % 2 == 1: return False stack = list() # 用于存放未配对的左括号 for ch in s: # 如果是左括号,直接入栈 if ch == '(' or ch == '[' or ch == '{': stack.append(ch) # 如果是右括号,需要判断栈顶是否为对应的左括号 elif ch == ')': # 栈非空且栈顶为对应的左括号,弹出 if len(stack) != 0 and stack[-1] == '(': stack.pop() else: # 不匹配或栈空,返回 False return False elif ch == ']': if len(stack) != 0 and stack[-1] == '[': stack.pop() else: return False elif ch == '}': if len(stack) != 0 and stack[-1] == '{': stack.pop() else: return False # 遍历结束后,栈为空说明全部配对成功 if len(stack) == 0: return True else: # 栈不为空,说明有未配对的左括号 return False ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ### 3.2 经典例题:表达式求值问题 #### 3.2.1 题目链接 - [227. 基本计算器 II - 力扣(LeetCode)](https://leetcode.cn/problems/basic-calculator-ii/) #### 3.2.2 题目大意 **描述**:给定一个字符串表达式 $s$,表达式中所有整数为非负整数,运算符只有 `+`、`-`、`*`、`/`,没有括号。 **要求**:实现一个基本计算器来计算并返回它的值。 **说明**: - $1 \le s.length \le 3 * 10^5$。 - $s$ 由整数和算符(`+`、`-`、`*`、`/`)组成,中间由一些空格隔开。 - $s$ 表示一个有效表达式。 - 表达式中的所有整数都是非负整数,且在范围 $[0, 2^{31} - 1]$ 内。 - 题目数据保证答案是一个 32-bit 整数。 **示例**: ```python 输入:s = "3+2*2" 输出:7 输入:s = " 3/2 " 输出:1 ``` #### 3.2.3 解题思路 ##### 思路 1:栈 在表达式计算中,乘除运算优先于加减运算。我们可以优先处理乘除,将结果暂存,再统一处理加减。 具体实现时,可以借助一个栈来保存每一步的中间结果。遇到正数直接入栈,遇到负数则取相反数入栈。这样,最终的计算结果就是栈中所有元素的和。 详细步骤如下: 1. 遍历字符串 $s$,用变量 $op$ 记录当前数字前的运算符,初始为 `+`。 2. 当遇到数字时,连续读取完整数字 $num$,根据 $op$ 的类型进行如下处理: 1. 如果 $op$ 为 `+`,将 $num$ 入栈。 2. 如果 $op$ 为 `-`,将 $-num$ 入栈。 3. 如果 $op$ 为 `*`,弹出栈顶元素 $top$,计算 $top \times num$,将结果入栈。 4. 如果 $op$ 为 `/`,弹出栈顶元素 $top$,计算 $int(top / num)$,将结果入栈。 3. 如果遇到运算符 `+`、`-`、`*`、`/`,则更新 $op$。 4. 最后,将栈中所有数字求和,返回结果。 ##### 思路 1:代码 ```python class Solution: def calculate(self, s: str) -> int: size = len(s) stack = [] # 用于存储每一步的中间结果 op = '+' # 记录上一个运算符,初始为加号 index = 0 while index < size: if s[index] == ' ': # 跳过空格 index += 1 continue if s[index].isdigit(): # 解析多位数字 num = ord(s[index]) - ord('0') while index + 1 < size and s[index+1].isdigit(): index += 1 num = 10 * num + ord(s[index]) - ord('0') # 根据上一个运算符进行处理 if op == '+': stack.append(num) # 加号直接入栈 elif op == '-': stack.append(-num) # 减号取相反数入栈 elif op == '*': top = stack.pop() # 乘法弹出栈顶元素 stack.append(top * num) # 计算后入栈 elif op == '/': top = stack.pop() # 除法弹出栈顶元素 # Python 的 int() 向零取整,符合题意 stack.append(int(top / num)) elif s[index] in "+-*/": # 更新当前运算符 op = s[index] index += 1 # 栈中所有元素求和即为最终结果 return sum(stack) ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ## 练习题目 - [0155. 最小栈](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/min-stack.md) - [0020. 有效的括号](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-parentheses.md) - [0227. 基本计算器 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/basic-calculator-ii.md) - [0150. 逆波兰表达式求值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/evaluate-reverse-polish-notation.md) - [0394. 字符串解码](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/decode-string.md) - [0946. 验证栈序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/validate-stack-sequences.md) - [栈基础题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%A0%88%E5%9F%BA%E7%A1%80%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】数据结构与算法 Python 语言描述 - 裘宗燕 著 - 【书籍】数据结构教程 第 3 版 - 唐发根 著 - 【书籍】大话数据结构 程杰 著 - 【文章】[栈 - 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/41222) ================================================ FILE: docs/03_stack_queue_hash_table/03_02_monotone_stack.md ================================================ ## 1. 单调栈简介 > **单调栈(Monotone Stack)**:在栈「先进后出」规则的基础上,要求从 **栈顶** 到 **栈底** 的元素单调递增或单调递减。 > > - **单调递增栈**:从栈顶到栈底元素单调递增 > - **单调递减栈**:从栈顶到栈底元素单调递减 ### 1.1 单调递增栈 > **单调递增栈**:每次新元素进栈时,如果它比栈顶元素小,直接入栈;如果比栈顶元素大或相等,就把栈顶及以上所有小于等于它的元素依次弹出,直到栈顶比它大或栈空,再将新元素入栈。 > > 这样可以保证:栈从栈顶到栈底的元素是递增的,且每个元素左侧第一个比它大的元素都能被快速找到。 单调递增栈的操作流程: - 当前元素 $x$,如果 $x$ < 栈顶元素,直接入栈。 - 否则,不断弹出栈顶小于等于 $x$ 的元素,直到栈顶比 $x$ 大或栈空,然后将 $x$ 入栈。 下面以数组 $[2, 7, 5, 4, 6, 3, 4, 2]$ 为例,演示「单调递增栈」的入栈与出栈过程: - 数组:$[2, 7, 5, 4, 6, 3, 4, 2]$,从左到右依次遍历。 | 步骤 | 当前元素 | 操作 | 栈状态(左为栈底) | 说明 | | :--: | :------: | ------------------------- | ------------------- | -------------------------------------- | | 1 | 2 | 2 入栈 | [2] | 2 左侧无更大元素 | | 2 | 7 | 2 出栈,7 入栈 | [7] | 7 左侧无更大元素 | | 3 | 5 | 5 入栈 | [7, 5] | 5 左侧第一个更大元素为 7 | | 4 | 4 | 4 入栈 | [7, 5, 4] | 4 左侧第一个更大元素为 5 | | 5 | 6 | 4 出栈,5 出栈,6 入栈 | [7, 6] | 6 左侧第一个更大元素为 7 | | 6 | 3 | 3 入栈 | [7, 6, 3] | 3 左侧第一个更大元素为 6 | | 7 | 4 | 3 出栈,4 入栈 | [7, 6, 4] | 4 左侧第一个更大元素为 6 | | 8 | 2 | 2 入栈 | [7, 6, 4, 2] | 2 左侧第一个更大元素为 4 | 最终,栈中元素为 $[7, 6, 4, 2]$。从栈顶(右)到栈底(左)为 $2, 4, 6, 7$,满足递增,符合单调递增栈的定义。 以第 5 步为例,图示如下: ![「单调递增栈」的入栈与出栈过程](https://qcdn.itcharge.cn/images/20220107101219.png) ### 1.2 单调递减栈 > **单调递减栈**:每次新元素进栈时,只有当它比栈顶元素大时才能直接入栈;如果小于或等于栈顶元素,则需要先将栈中所有大于等于当前元素的元素依次弹出,直到栈顶元素小于当前元素或栈为空,再将新元素入栈。 > > 这样可以保证:栈中始终只保留比当前入栈元素小的值,并且从栈顶到栈底的元素是单调递减的。 单调递减栈的操作流程如下: - 假设当前待入栈元素为 $x$,如果 $x$ > 栈顶元素,则直接入栈。 - 否则,从栈顶开始,依次弹出所有大于等于 $x$ 的元素,直到遇到一个小于 $x$ 的元素或栈为空,然后将 $x$ 入栈。 下面以数组 $[4, 3, 2, 5, 7, 4, 6, 8]$ 为例,演示「单调递减栈」的入栈与出栈过程: - 数组:$[4, 3, 2, 5, 7, 4, 6, 8]$,从左到右依次遍历。 | 步骤 | 当前元素 | 操作 | 栈状态(左为栈底) | 说明 | | :--: | :------: | --------------------- | ------------------- | ------------------------------------- | | 1 | 4 | 4 入栈 | [4] | 4 左侧无更小元素 | | 2 | 3 | 4 出栈,3 入栈 | [3] | 3 左侧无更小元素 | | 3 | 2 | 3 出栈,2 入栈 | [2] | 2 左侧无更小元素 | | 4 | 5 | 5 入栈 | [2, 5] | 5 左侧第一个更小元素为 2 | | 5 | 7 | 7 入栈 | [2, 5, 7] | 7 左侧第一个更小元素为 5 | | 6 | 4 | 7 出栈,5 出栈,4 入栈| [2, 4] | 4 左侧第一个更小元素为 2 | | 7 | 6 | 6 入栈 | [2, 4, 6] | 6 左侧第一个更小元素为 4 | | 8 | 8 | 8 入栈 | [2, 4, 6, 8] | 8 左侧第一个更小元素为 6 | 最终,栈中元素为 $[2, 4, 6, 8]$。从栈顶(右)到栈底(左)为 $8, 6, 4, 2$,满足递减,符合单调递减栈的定义。 以第 6 步为例,图示如下: ![「单调递减栈」的入栈与出栈过程](https://qcdn.itcharge.cn/images/20220107102446.png) ## 2. 单调栈适用场景 单调栈常用于 $O(n)$ 时间复杂度内高效解决「最近更大/更小元素」类问题,主要包括以下四种典型场景: - 查找左侧第一个比当前元素更大 / 更小的元素。 - 查找右侧第一个比当前元素更大 / 更小的元素。 具体解法如下: ### 2.1 查找左侧第一个比当前元素大的元素 - 从左到右遍历数组,维护单调递增栈(栈顶到栈底递增): - 当前元素入栈时,栈顶元素即为其左侧第一个更大元素; - 如果栈为空,则左侧不存在更大元素。 ### 2.2 查找左侧第一个比当前元素小的元素 - 从左到右遍历数组,维护单调递减栈(栈顶到栈底递减): - 当前元素入栈时,栈顶元素即为其左侧第一个更小元素; - 如果栈为空,则左侧不存在更小元素。 ### 2.3 查找右侧第一个比当前元素大的元素 - 从左到右遍历数组,维护单调递增栈: - 当前元素将栈中比自己小的元素弹出,被弹出的元素的右侧第一个更大元素即为当前元素; - 如果某元素未被弹出,则右侧不存在更大元素。 - 或者,从右到左遍历,入栈时栈顶即为右侧第一个更大元素。 ### 2.4 查找右侧第一个比当前元素小的元素 - 从左到右遍历数组,维护单调递减栈: - 当前元素将栈中比自己大的元素弹出,被弹出的元素的右侧第一个更小元素即为当前元素; - 如果某元素未被弹出,则右侧不存在更小元素。 - 或者,从右到左遍历,入栈时栈顶即为右侧第一个更小元素。 上述四类问题可以归纳为以下通用规则: - 查「更大」用单调递增栈,查「更小」用单调递减栈; - 查「左侧」看元素入栈时的栈顶; - 查「右侧」看元素出栈时触发它的当前元素; - 遍历方向通常为从左到右(部分场景可从右到左)。 ## 3. 单调栈模板 以从左到右遍历元素为例,介绍一下构造单调递增栈和单调递减栈的模板。 ### 3.1 单调递增栈模板 ```python def monotoneIncreasingStack(nums): stack = [] left for i, num in enumerate(nums): while stack and num >= stack[-1]: stack.pop() if stack: stack.append(num) ``` ### 3.2 单调递减栈模板 ```python def monotoneDecreasingStack(nums): stack = [] for num in nums: while stack and num <= stack[-1]: stack.pop() stack.append(num) ``` 等号的去留(>= 或 >,<= 或 <)决定是否保留相等元素,按题意调整。 ## 4. 单调栈的经典应用 ### 4.1 经典例题:下一个更大元素 I #### 4.1.1 题目链接 - [0496. 下一个更大元素 I](https://leetcode.cn/problems/next-greater-element-i/) #### 4.1.2 题目大意 给定两个没有重复元素的数组 $nums1$ 和 $nums2$ ,其中 $nums1$ 是 $nums2$ 的子集。 要求:找出 $nums1$ 中每个元素在 $nums2$ 中的下一个比其大的值。 - $nums1$ 中数字 $x$ 的下一个更大元素是指:$x$ 在 $nums2$ 中对应位置的右边的第一个比 $x$ 大的元素。如果不存在,对应位置输出 $-1$。 #### 4.1.3 解题思路 第一种思路是根据题意直接暴力求解。遍历 $nums1$ 中的每一个元素。对于 $nums1$ 的每一个元素 $nums1[i]$,再遍历一遍 $nums2$,查找 $nums2$ 中对应位置右边第一个比 $nums1[i]$ 大的元素。这种解法的时间复杂度是 $O(n^2)$。 第二种思路是使用单调递增栈。因为 $nums1$ 是 $nums2$ 的子集,所以我们可以先遍历一遍 $nums2$,并构造单调递增栈,求出 $nums2$ 中每个元素右侧下一个更大的元素。然后将其存储到哈希表中。然后再遍历一遍 $nums1$,从哈希表中取出对应结果,存放到答案数组中。这种解法的时间复杂度是 $O(n)$。具体做法如下: - 使用数组 $res$ 存放答案。使用 $stack$ 表示单调递增栈。使用哈希表 $num\_map$ 用于存储 $nums2$ 中下一个比当前元素大的数值,映射关系为 **当前元素值:下一个比当前元素大的数值**。 - 遍历数组 $nums2$,对于当前元素: - 如果当前元素值较小,则直接让当前元素值入栈。 - 如果当前元素值较大,则一直出栈,直到当前元素值小于栈顶元素。 - 出栈时,出栈元素是第一个大于当前元素值的元素。则将其映射到 $num\_map$ 中。 - 遍历完数组 $nums2$,建立好所有元素下一个更大元素的映射关系之后,再遍历数组 $nums1$。 - 从 $num\_map$ 中取出对应的值,将其加入到答案数组中。 - 最终输出答案数组 $res$。 #### 4.1.4 代码 ```python class Solution: def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]: res = [] stack = [] num_map = dict() for num in nums2: while stack and num > stack[-1]: num_map[stack[-1]] = num stack.pop() stack.append(num) for num in nums1: res.append(num_map.get(num, -1)) return res ``` ### 4.2 每日温度 #### 4.2.1 题目链接 - [739. 每日温度 - 力扣(LeetCode)](https://leetcode.cn/problems/daily-temperatures/) #### 4.2.2 题目大意 **描述**:给定一个列表 $temperatures$,$temperatures[i]$ 表示第 $i$ 天的气温。 **要求**:输出一个列表,列表上每个位置代表「如果要观测到更高的气温,至少需要等待的天数」。如果之后的气温不再升高,则用 $0$ 来代替。 **说明**: - $1 \le temperatures.length \le 10^5$。 - $30 \le temperatures[i] \le 100$。 **示例**: ```python 输入: temperatures = [73,74,75,71,69,72,76,73] 输出: [1,1,4,2,1,1,0,0] 输入: temperatures = [30,40,50,60] 输出: [1,1,1,0] ``` #### 4.2.3 解题思路 题目的意思实际上就是给定一个数组,每个位置上有整数值。对于每个位置,在该位置右侧找到第一个比当前元素更大的元素。求「该元素」与「右侧第一个比当前元素更大的元素」之间的距离,将所有距离保存为数组返回结果。 最简单的思路是对于每个温度值,向后依次进行搜索,找到比当前温度更高的值。 更好的方式使用「单调递增栈」,栈中保存元素的下标。 ##### 思路 1:单调栈 1. 首先,将答案数组 $ans$ 全部赋值为 0。然后遍历数组每个位置元素。 2. 如果栈为空,则将当前元素的下标入栈。 3. 如果栈不为空,且当前数字大于栈顶元素对应数字,则栈顶元素出栈,并计算下标差。 4. 此时当前元素就是栈顶元素的下一个更高值,将其下标差存入答案数组 $ans$ 中保存起来,判断栈顶元素。 5. 直到当前数字小于或等于栈顶元素,则停止出栈,将当前元素下标入栈。 6. 最后输出答案数组 $ans$。 ##### 思路 1:代码 ```python class Solution: def dailyTemperatures(self, T: List[int]) -> List[int]: n = len(T) stack = [] ans = [0 for _ in range(n)] for i in range(n): while stack and T[i] > T[stack[-1]]: index = stack.pop() ans[index] = (i-index) stack.append(i) return ans ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ## 练习题目 - [0496. 下一个更大元素 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/next-greater-element-i.md) - [0739. 每日温度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/daily-temperatures.md) - [0316. 去除重复字母](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/remove-duplicate-letters.md) - [单调栈题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8D%95%E8%B0%83%E6%A0%88%E9%A2%98%E7%9B%AE) ## 参考资料 - 【博文】[动画:什么是单调栈?_- 吴师兄学编程](https://www.cxyxiaowu.com/450.html) - 【博文】[单调栈 - OI Wiki](https://oi-wiki.org/ds/monotonous-stack/) - 【博文】[单调栈解题模板秒杀八道题 - lucifer 的网络博客](https://lucifer.ren/blog/2020/11/03/monotone-stack/) - 【博文】[理解单调栈与单调队列 - Hopefully Sky 的博客](https://blog.csdn.net/fuzhongmin05/article/details/118090554) ================================================ FILE: docs/03_stack_queue_hash_table/03_03_queue_basic.md ================================================ ## 1. 队列简介 > **队列(Queue)**:一种线性表数据结构,遵循「先进先出(FIFO)」原则,只允许在一端插入元素(队尾),在另一端删除元素(队头)。 ### 1.1 基本概念 - **队尾(rear)**:允许插入元素的一端 - **队头(front)**:允许删除元素的一端 - **空队**:没有任何数据元素的队列 ### 1.2 核心特性 队列的操作遵循 **先进先出(FIFO)** 的原则: - 最先进入队列的元素,最先被取出。 - 类似于排队买票,先到的人先买票,后到的人只能排在队尾等待。 ### 1.3 基本操作 - **入队(enqueue)**:在队尾插入元素 - **出队(dequeue)**:从队头删除元素 下图展示了队列结构和操作方式。 ![队列结构](https://qcdn.itcharge.cn/images/202405092254785.png) ## 2. 队列的实现方式 与线性表类似,队列常见的存储方式有两种:**顺序存储** 和 **链式存储**。 - **顺序存储队列**:使用一段连续的存储空间(如数组),依次存放队列中从队头到队尾的元素。通过指针 $front$ 标记队头元素的位置,$rear$ 标记队尾元素的位置。 - **链式存储队列**:采用单链表实现,元素按插入顺序依次链接。$front$ 指向链表的头节点(即队头元素),$rear$ 指向链表的尾节点(即队尾元素)。 需要注意的是,$front$ 和 $rear$ 的具体指向方式可能因实现细节而异。为简化算法或代码,有时 $front$ 会指向队头元素的前一个位置,$rear$ 也可能指向队尾元素的下一个位置。具体以实际实现为准。 ### 2.1 顺序存储队列 队列最常见的实现方式是利用数组来构建顺序存储结构。在 Python 中,可以直接使用列表(list)来实现顺序存储队列。 #### 2.1.1 顺序存储队列的基本描述 ![顺序存储队列](https://qcdn.itcharge.cn/images/202405092254909.png) 为简化实现,我们约定:队头指针 $self.front$ 指向队头元素的前一个位置,队尾指针 $self.rear$ 指向队尾元素所在位置。 - **初始化空队列**:创建空队列 $self.queue$,设置队列容量 $self.size$,并令 $self.front = self.rear = -1$。 - **判断队列是否为空**:如果 $self.front$ 与 $self.rear$ 相等,则队列为空。 - **判断队列是否已满**:如果 $self.rear == self.size - 1$,则队列已满。 - **入队操作**:先判断队列是否已满,如果未满,则 $self.rear$ 右移一位,将新元素赋值到 $self.queue[self.rear]$。 - **出队操作**:先判断队列是否为空,如果不为空,则 $self.front$ 右移一位,返回 $self.queue[self.front]$。 - **获取队头元素**:先判断队列是否为空,如果不为空,则返回 $self.queue[self.front + 1]$。 - **获取队尾元素**:先判断队列是否为空,如果不为空,则返回 $self.queue[self.rear]$。 #### 2.1.2 顺序存储队列的实现代码 ```python class Queue: """ 顺序存储队列实现(非循环队列) front 指向队头元素的前一个位置,rear 指向队尾元素所在位置 """ def __init__(self, size=100): """ 初始化空队列 :param size: 队列最大容量 """ self.size = size self.queue = [None for _ in range(size)] # 存储队列元素的数组 self.front = -1 # 队头指针,指向队头元素的前一个位置 self.rear = -1 # 队尾指针,指向队尾元素所在位置 def is_empty(self): """ 判断队列是否为空 :return: 如果队列为空返回 True,否则返回 False """ return self.front == self.rear def is_full(self): """ 判断队列是否已满 :return: 如果队列已满返回 True,否则返回 False """ return self.rear + 1 == self.size def enqueue(self, value): """ 入队操作:在队尾插入元素 :param value: 待插入的元素 :raises Exception: 队列已满时抛出异常 """ if self.is_full(): raise Exception('Queue is full') self.rear += 1 self.queue[self.rear] = value def dequeue(self): """ 出队操作:从队头删除元素并返回 :return: 队头元素 :raises Exception: 队列为空时抛出异常 """ if self.is_empty(): raise Exception('Queue is empty') self.front += 1 return self.queue[self.front] def front_value(self): """ 获取队头元素(不删除) :return: 队头元素 :raises Exception: 队列为空时抛出异常 """ if self.is_empty(): raise Exception('Queue is empty') return self.queue[self.front + 1] def rear_value(self): """ 获取队尾元素(不删除) :return: 队尾元素 :raises Exception: 队列为空时抛出异常 """ if self.is_empty(): raise Exception('Queue is empty') return self.queue[self.rear] ``` ### 2.2 顺序存储循环队列 在上一节的顺序队列实现中,队列满时($self.rear == self.size - 1$)就无法再插入新元素,即使前面有空位也无法利用,导致「假溢出」问题。 为解决这个问题,常用两种方法: - **方法一:每次出队后整体前移元素** 这样可以利用前面的空位,但每次出队都要移动所有元素,效率低,时间复杂度 $O(n)$,不推荐。 - **方法二:循环移动** 将队列的首尾视为相连,通过取模运算实现指针的循环移动,从而充分利用存储空间。采用循环队列后,所有基本操作的时间复杂度均为 $O(1)$,高效且无「假溢出」问题。 循环队列的实现要点如下: - 设 $self.size$ 为循环队列的最大容量,队头指针 $self.front$ 指向队头元素前一个位置,队尾指针 $self.rear$ 指向队尾元素。 - **入队**:$self.rear = (self.rear + 1) \mod self.size$,在新位置插入元素。 - **出队**:$self.front = (self.front + 1) \mod self.size$,并返回该位置元素。 > **注意**: > 初始化时 $self.front == self.rear$,表示队列为空。 > 但队列满时也可能出现 $self.front == self.rear$,因此需要区分队空和队满。 常见区分队空和队满的方法: - **方法 1**:增加计数变量 $self.count$,记录队列元素个数。 - **方法 2**:增加标记变量 $self.tag$,区分最近一次操作是入队还是出队。 - **方法 3(常用)**:特意空出一个位置,约定「队头指针在队尾指针的下一位置」为队满。即: - 队满:$(self.rear + 1) \mod self.size == self.front$ - 队空:$self.front == self.rear$ #### 2.2.1 顺序存储循环队列的基本描述 以方法 3 为例,循环队列的操作如下: - **初始化**:队列大小为 $self.size + 1$,$self.front = self.rear = 0$。 - **判空**:$self.front == self.rear$ - **判满**:$(self.rear + 1) \mod self.size == self.front$ - **入队**:判断队满,未满则 $self.rear$ 循环前进一位,插入元素。 - **出队**:判断队空,非空则 $self.front$ 循环前进一位,返回该元素。 - **获取队头元素**:$self.queue[(self.front + 1) \mod self.size]$ - **获取队尾元素**:$self.queue[self.rear]$ ![顺序存储循环队列](https://qcdn.itcharge.cn/images/202405092254537.png) #### 2.2.2 顺序存储循环队列的实现代码 ```python class Queue: """ 顺序存储循环队列实现 front 指向队头元素的前一个位置,rear 指向队尾元素所在位置 """ def __init__(self, size=100): """ 初始化空队列 :param size: 队列最大容量(实际可用容量为 size) """ self.size = size + 1 # 实际分配空间多一个,用于区分队满和队空 self.queue = [None for _ in range(self.size)] # 存储队列元素 self.front = 0 # 队头指针,指向队头元素的前一个位置 self.rear = 0 # 队尾指针,指向队尾元素所在位置 def is_empty(self): """ 判断队列是否为空 :return: True 表示队列为空,False 表示非空 """ return self.front == self.rear def is_full(self): """ 判断队列是否已满 :return: True 表示队列已满,False 表示未满 """ return (self.rear + 1) % self.size == self.front def enqueue(self, value): """ 入队操作:在队尾插入元素 :param value: 要插入的元素 :raises Exception: 队列已满时抛出异常 """ if self.is_full(): raise Exception('Queue is full') # rear 指针循环前进一位 self.rear = (self.rear + 1) % self.size self.queue[self.rear] = value def dequeue(self): """ 出队操作:从队头删除元素并返回 :return: 队头元素的值 :raises Exception: 队列为空时抛出异常 """ if self.is_empty(): raise Exception('Queue is empty') # front 指针循环前进一位 self.front = (self.front + 1) % self.size value = self.queue[self.front] self.queue[self.front] = None # 可选:清除引用,便于垃圾回收 return value def front_value(self): """ 获取队头元素 :return: 队头元素的值 :raises Exception: 队列为空时抛出异常 """ if self.is_empty(): raise Exception('Queue is empty') return self.queue[(self.front + 1) % self.size] def rear_value(self): """ 获取队尾元素 :return: 队尾元素的值 :raises Exception: 队列为空时抛出异常 """ if self.is_empty(): raise Exception('Queue is empty') return self.queue[self.rear] ``` ### 2.3 链式存储队列 当队列需要频繁插入和删除元素时,链式存储结构比顺序存储结构更高效。因此,队列常用链表实现。 链式队列的实现思路如下: 1. 用单链表表示队列,每个节点存储一个元素。 2. 用指针 $front$ 指向队头元素的前一个位置,$rear$ 指向队尾元素。 3. 只允许在队头删除元素(出队),在队尾插入元素(入队)。 #### 2.3.1 链式存储队列的基本描述 ![链式存储队列](https://qcdn.itcharge.cn/images/202405092255125.png) 约定:$self.front$ 指向队头元素前一个位置,$self.rear$ 指向队尾元素。 - **初始化空队列**:创建头节点 $self.head$,令 $self.front = self.rear = self.head$。 - **队列判空**:如果 $self.front == self.rear$,队列为空。 - **入队**:新建节点,插入链表末尾,$self.rear$ 指向新节点。 - **出队**:如果队列为空则抛出异常,否则取 $self.front.next$ 的值,$self.front$ 前进一位。如果出队后 $self.front.next$ 为空,$self.rear = self.front$。 - **获取队头元素**:队列非空时,返回 $self.front.next.value$。 - **获取队尾元素**:队列非空时,返回 $self.rear.value$。 #### 2.3.2 链式存储队列的实现代码 ```python class Node: """ 链表节点类 """ def __init__(self, value): self.value = value # 节点存储的值 self.next = None # 指向下一个节点的指针 class Queue: """ 链式队列实现 """ def __init__(self): """ 初始化空队列,创建一个头结点(哨兵节点),front和rear都指向头结点 """ head = Node(0) # 哨兵节点,不存储有效数据 self.front = head # front指向队头元素的前一个节点 self.rear = head # rear指向队尾节点 def is_empty(self): """ 判断队列是否为空 :return: 如果队列为空返回 True,否则返回 False """ return self.front == self.rear def enqueue(self, value): """ 入队操作,在队尾插入新节点 :param value: 要插入的元素值 """ node = Node(value) # 创建新节点 self.rear.next = node # 当前队尾节点的next指向新节点 self.rear = node # rear指针后移,指向新节点 def dequeue(self): """ 出队操作,删除队头元素 :return: 队头元素的值 :raises Exception: 队列为空时抛出异常 """ if self.is_empty(): raise Exception('Queue is empty') node = self.front.next # 队头节点(第一个有效节点) self.front.next = node.next # front的next指向下一个节点 if self.rear == node: # 如果出队后队列为空,rear回退到front self.rear = self.front value = node.value # 取出队头元素的值 del node # 释放节点(可省略,Python自动垃圾回收) return value def front_value(self): """ 获取队头元素的值 :return: 队头元素的值 :raises Exception: 队列为空时抛出异常 """ if self.is_empty(): raise Exception('Queue is empty') return self.front.next.value # front.next为队头节点 def rear_value(self): """ 获取队尾元素的值 :return: 队尾元素的值 :raises Exception: 队列为空时抛出异常 """ if self.is_empty(): raise Exception('Queue is empty') return self.rear.value # rear为队尾节点 ``` ## 3. 队列的应用 队列作为最常用的基础数据结构之一,在算法和实际开发中有着极其广泛的应用。无论是生活中的排队买票、银行业务办理,还是计算机系统内部的任务调度,队列都扮演着不可或缺的角色。其在计算机领域的典型应用主要体现在以下两个方面: 1. **缓解主机与外部设备之间的速度差异** - 例如,主机输出数据的速度远快于打印机的打印速度。如果直接将数据传递给打印机,打印机无法及时处理,容易造成数据丢失。为此,通常会设置一个打印缓冲队列,将待打印的数据按顺序写入队列,打印机则按照先进先出的顺序依次取出数据进行打印。这样既保证了数据的有序输出,也提升了主机的工作效率。 2. **解决多用户环境下的系统资源竞争** - 在多终端的计算机系统中,多个用户可能同时请求使用 CPU。操作系统会根据请求到达的先后顺序,将这些请求排成一个队列,每次优先分配 CPU 给队头的用户。当该用户的程序运行结束或时间片用完后,将其移出队列,再将 CPU 分配给新的队头用户。这样既保证了多用户的公平性,也确保了 CPU 的高效利用。 - 此外,像 Linux 的环形缓冲区、高性能队列 Disruptor,以及 iOS 多线程中的 GCD、NSOperationQueue 等,底层都大量采用了队列结构来实现高效的数据和任务管理。 ## 练习题目 - [0622. 设计循环队列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-circular-queue.md) - [0346. 数据流中的移动平均值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/moving-average-from-data-stream.md) - [0225. 用队列实现栈](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-stack-using-queues.md) - [队列基础题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%98%9F%E5%88%97%E5%9F%BA%E7%A1%80%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】数据结构与算法 Python 语言描述 - 裘宗燕 著 - 【书籍】数据结构教程 第 3 版 - 唐发根 著 - 【书籍】大话数据结构 程杰 著 - 【文章】[数据结构之 python 实现队列的链式存储 - 不服输的南瓜的博客](https://blog.csdn.net/weixin_40283816/article/details/87952682) - 【文章】[顺序存储的循环队列判空判满判长_- ccxcuixia](https://blog.csdn.net/baidu_41304382/article/details/108091899) - 【文章】[队列 - 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/41330) ================================================ FILE: docs/03_stack_queue_hash_table/03_04_priority_queue.md ================================================ ## 1. 优先队列简介 > **优先队列(Priority Queue)**:是一种为每个元素分配优先级的特殊队列结构。每次访问或移除元素时,总是优先处理优先级最高的元素。 优先队列与普通队列的核心区别在于 **出队顺序**: - 普通队列按照「先进先出(First In, First Out)」原则,元素按入队顺序依次出队。 - 优先队列则根据元素的优先级决定出队顺序,优先级高的元素先出队,优先级低的元素后出队,遵循 **「优先级高者先出」** 的规则,与入队顺序无关。 下图展示了优先队列的结构示意: ![优先队列](https://qcdn.itcharge.cn/images/202405092258900.png) 优先队列在实际开发和算法设计中有着广泛的应用,常见场景包括: - **数据压缩**:如赫夫曼编码算法中,频率最低的节点优先合并。 - **最短路径搜索**:如 Dijkstra 算法,优先扩展当前距离最小的节点。 - **最小生成树构建**:如 Prim 算法,优先选择权值最小的边。 - **任务调度**:根据任务优先级动态分配执行顺序。 - **事件驱动仿真**:如排队系统,优先处理最早到达或优先级最高的事件。 - **Top-K 问题**:如查找第 k 大(小)元素、实时维护前 K 个高频元素等。 主流编程语言均内置了优先队列相关的数据结构。例如 Java 的 `PriorityQueue`,C++ 的 `priority_queue`,Python 可通过 `heapq` 模块实现优先队列。接下来将详细介绍优先队列的实现方式。 ## 2. 优先队列的实现方式 优先队列的基本操作与普通队列类似,主要包括 **「入队」** 和 **「出队」**,但在出队时会优先移除优先级最高的元素。 优先队列的实现方式主要有三种:**数组(顺序存储)**、**链表(链式存储)** 和 **二叉堆结构**。其中,最常用且高效的是基于二叉堆的实现。下面简要对比三种方案: - **数组(顺序存储)**:入队时直接将元素插入数组末尾,时间复杂度为 $O(1)$;出队时需遍历整个数组以找到优先级最高的元素并删除,时间复杂度为 $O(n)$。 - **链表(链式存储)**:链表内元素按优先级有序排列,入队时需找到合适插入位置,时间复杂度为 $O(n)$;出队时直接移除链表头节点,时间复杂度为 $O(1)$。 - **二叉堆结构**:通过二叉堆维护优先级顺序,入队操作(插入新元素)和出队操作(弹出优先级最高元素)均为 $O(\log n)$,效率较高。 三种实现方式的时间复杂度对比如下: | 实现方式 | 入队操作 | 出队操作(取优先级最高元素) | |----------|----------|------------------------------| | 二叉堆 | $O(\log n)$ | $O(\log n)$ | | 数组 | $O(1)$ | $O(n)$ | | 链表 | $O(n)$ | $O(1)$ | 综上,二叉堆是实现优先队列的主流高效方案。接下来将详细介绍基于二叉堆的优先队列实现。 ## 3. 二叉堆实现的优先队列 ### 3.1 二叉堆的定义 二叉堆是一种完全二叉树,分为两类: - **大顶堆**:每个节点值 ≥ 子节点值 - **小顶堆**:每个节点值 ≤ 子节点值 ### 3.2 二叉堆的基本操作 二叉堆的核心操作有两个: - **堆调整(heapAdjust)**:从某个节点出发,自上而下比较并交换,使以该节点为根的子树满足堆性质(如大顶堆则父节点 ≥ 子节点),直到整个堆有序。 - **建堆(heapify)**:从最后一个非叶子节点开始,依次向前对每个节点执行堆调整,最终将数组整体调整为二叉堆。 ### 3.3 优先队列的基本操作 优先队列主要有两种操作: - **入队(heappush)**:将新元素加到数组末尾,然后从下往上调整,恢复堆结构。 - **出队(heappop)**:将堆顶元素与末尾元素交换,弹出末尾元素,再对新堆顶自上而下调整,恢复堆结构。 ### 3.4 手写二叉堆实现优先队列 手写二叉堆实现优先队列,常用方法包括: - `heapAdjust`:调整堆结构 - `heapify`:建堆 - `heappush`:入队 - `heappop`:出队 - `heapSort`:堆排序 ```python class Heapq: # 堆调整方法:将以 index 为根的子树调整为大顶堆 def heapAdjust(self, nums: list, index: int, end: int): """ nums: 堆数组 index: 当前需要调整的根节点下标 end: 堆的最后一个元素下标 """ left = index * 2 + 1 # 左子节点下标 right = left + 1 # 右子节点下标 while left <= end: max_index = index # 假设当前根节点最大 # 比较左子节点 if nums[left] > nums[max_index]: max_index = left # 比较右子节点(注意要先判断是否越界) if right <= end and nums[right] > nums[max_index]: max_index = right if index == max_index: # 如果根节点就是最大值,调整结束 break # 交换根节点与最大子节点 nums[index], nums[max_index] = nums[max_index], nums[index] # 继续调整被交换下去的子树 index = max_index left = index * 2 + 1 right = left + 1 # 建堆:将数组整体调整为大顶堆 def heapify(self, nums: list): size = len(nums) # 从最后一个非叶子节点开始,依次向前调整 for i in range((size - 2) // 2, -1, -1): self.heapAdjust(nums, i, size - 1) # 入队操作:插入新元素到堆中 def heappush(self, nums: list, value): """ nums: 堆数组 value: 待插入的新元素 """ nums.append(value) # 先将新元素加到末尾 i = len(nums) - 1 # 新元素下标 # 自下向上调整,恢复堆结构 while i > 0: parent = (i - 1) // 2 # 父节点下标 if nums[parent] >= value: # 父节点比新元素大,插入到当前位置 break # 父节点下移 nums[i] = nums[parent] i = parent nums[i] = value # 插入到最终位置 # 出队操作:弹出堆顶元素(最大值) def heappop(self, nums: list) -> int: """ nums: 堆数组 return: 堆顶元素 """ size = len(nums) if size == 0: raise IndexError("heappop from empty heap") # 交换堆顶和末尾元素 nums[0], nums[-1] = nums[-1], nums[0] top = nums.pop() # 弹出最大值 if size > 1: # 重新调整堆 self.heapAdjust(nums, 0, size - 2) return top # 堆排序:原地将数组升序排序 def heapSort(self, nums: list): """ nums: 待排序数组 return: 升序排序后的数组 """ self.heapify(nums) # 先建堆 size = len(nums) # 依次将堆顶元素(最大值)交换到末尾,缩小堆范围 for i in range(size - 1, 0, -1): nums[0], nums[i] = nums[i], nums[0] # 堆顶与末尾交换 self.heapAdjust(nums, 0, i - 1) # 调整剩余部分为大顶堆 return nums ``` ### 3.5 使用 heapq 模块实现优先队列 Python 标准库中的 `heapq` 模块实现了高效的最小堆(小顶堆),可用于构建优先队列。其核心操作如下: - `heapq.heappush(heap, item)`:将元素 `item` 压入堆 `heap` 中,保持堆结构。 - `heapq.heappop(heap)`:弹出并返回堆中的最小元素。 **注意事项**: - `heapq` 默认是小顶堆,即每次弹出的是最小值。 - 如果需实现「大顶堆」(每次弹出最大优先级元素),可将优先级取负数存入堆中。 - 为保证当优先级相同时元素的入队顺序,通常可额外存储一个自增索引。 下面是一个基于 `heapq` 实现的优先队列类,支持自定义优先级,且保证稳定性: ```python import heapq class PriorityQueue: def __init__(self): # 初始化一个空堆和自增索引 self.queue = [] self.index = 0 def push(self, item, priority): """ 入队操作,将元素 item 按照优先级 priority 压入堆中。 为实现大顶堆,优先级取负数;index 保证相同优先级时的稳定性。 """ heapq.heappush(self.queue, (-priority, self.index, item)) self.index += 1 def pop(self): """ 出队操作,弹出并返回优先级最高的元素(大顶堆)。 """ if not self.queue: raise IndexError("pop from empty priority queue") return heapq.heappop(self.queue)[-1] ``` ## 5. 经典例题:滑动窗口最大值 ### 5.1.1 题目链接 - [239. 滑动窗口最大值 - 力扣(LeetCode)](https://leetcode.cn/problems/sliding-window-maximum/) ### 5.1.2 题目大意 **描述**:给定一个整数数组 $nums$,再给定一个整数 $k$,表示为大小为 $k$ 的滑动窗口从数组的最左侧移动到数组的最右侧。我们只能看到滑动窗口内的 $k$ 个数字,滑动窗口每次只能向右移动一位。 **要求**:返回滑动窗口中的最大值。 **说明**: - $1 \le nums.length \le 10^5$。 - $-10^4 \le nums[i] \le 10^4$。 - $1 \le k \le nums.length$。 **示例**: ```python 输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值 --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7 输入:nums = [1], k = 1 输出:[1] ``` ### 5.1.3 解题思路 如果采用暴力解法,需要用两重循环遍历每个滑动窗口,时间复杂度为 $O(n \times k)$,在本题数据范围下会超时。 可以利用优先队列(堆)高效求解: ##### 思路 1:优先队列 1. 首先,将前 $k$ 个元素以 (值, 索引) 形式加入优先队列(大顶堆),以值为优先级。 2. 从第 $k$ 个元素开始,依次将当前元素及其索引压入堆中。 3. 每次插入后,检查堆顶元素的索引是否已滑出窗口(即 $q[0][1] \le i - k$),如果是则不断弹出堆顶,直到堆顶索引在窗口范围内。 4. 此时堆顶元素即为当前窗口最大值,将其加入结果数组。 5. 重复上述过程,直到遍历完整个数组,最后返回结果数组。 ##### 思路 1:代码 ```python class Solution: def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: size = len(nums) q = [(-nums[i], i) for i in range(k)] heapq.heapify(q) res = [-q[0][0]] for i in range(k, size): heapq.heappush(q, (-nums[i], i)) while q[0][1] <= i - k: heapq.heappop(q) res.append(-q[0][0]) return res ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(k)$。 ## 练习题目 - [0215. 数组中的第K个最大元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/kth-largest-element-in-an-array.md) - [0347. 前 K 个高频元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/top-k-frequent-elements.md) - [0451. 根据字符出现频率排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sort-characters-by-frequency.md) - [优先队列题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BC%98%E5%85%88%E9%98%9F%E5%88%97%E9%A2%98%E7%9B%AE) ## 参考资料 - 【博文】[浅入浅出数据结构(15)—— 优先队列(堆) - NSpt - 博客园](https://www.cnblogs.com/mm93/p/7481782.html) - 【博文】[堆(Heap)和优先队列(Priority Queue) - 简书](https://www.jianshu.com/p/859e5fb89eb7) - 【博文】[漫画:什么是优先队列?- 吴师兄学编程](https://www.cxyxiaowu.com/5417.html) - 【博文】[Python3,手写一个堆及其简易功能,并实现优先队列,最小堆任务调度等 - pythonstrat 的博客](https://blog.csdn.net/pythonstrat/article/details/119378788) - 【文档】[实现一个优先级队列 - python3-cookbook 3.0.0 文档](https://python3-cookbook.readthedocs.io/zh_CN/latest/c01/p05_implement_a_priority_queue.html) - 【文档】[heapq - 堆队列算法 - Python 3.10.1 文档](https://docs.python.org/zh-cn/3/library/heapq.html) - 【题解】[239. 滑动窗口最大值 (优先队列&单调栈) - 滑动窗口最大值 - 力扣](https://leetcode.cn/problems/sliding-window-maximum/solution/239-hua-dong-chuang-kou-zui-da-zhi-you-x-9qur/) ================================================ FILE: docs/03_stack_queue_hash_table/03_05_bidirectional_queue.md ================================================ ## 1. 双向队列简介 > **双向队列(Deque,Double-Ended Queue)**:一种线性表数据结构,允许在队列的两端进行插入和删除操作,既可以从队头入队/出队,也可以从队尾入队/出队。 ### 1.1 基本概念 双向队列可以看作是栈和队列的结合体,具有以下特点: - **队头(front)**:队列的前端,可以进行插入和删除操作 - **队尾(rear)**:队列的后端,也可以进行插入和删除操作 - **空队列**:没有任何数据元素的双向队列 ### 1.2 核心特性 双向队列的操作遵循 **双端操作** 的原则: - 可以在队列的两端进行插入和删除操作 - 既具有栈的"后进先出"特性,又具有队列的"先进先出"特性 - 提供了比普通队列更灵活的操作方式 ### 1.3 基本操作 双向队列支持以下基本操作: - **队头入队(push_front)**:在队头插入元素 - **队头出队(pop_front)**:从队头删除并返回元素 - **队尾入队(push_back)**:在队尾插入元素 - **队尾出队(pop_back)**:从队尾删除并返回元素 - **查看队头元素(peek_front)**:查看队头元素但不删除 - **查看队尾元素(peek_back)**:查看队尾元素但不删除 ## 2. 双向队列的实现方式 双向队列可以通过 **顺序存储** 和 **链式存储** 两种方式实现。由于双向队列需要在两端进行操作,链式存储通常更加高效和灵活。 ### 2.1 链式存储双向队列 链式存储是双向队列最常用的实现方式,使用双向链表结构,每个节点都有指向前后节点的指针。 #### 2.1.1 链式存储双向队列的基本描述 我们使用双向链表实现双向队列: - **节点结构**:每个节点包含数据域和两个指针域(prev 和 next) - **头尾指针**:维护指向队头节点和队尾节点的指针 - **哨兵节点**:可以使用哨兵节点简化边界处理 #### 2.1.2 链式存储双向队列的实现代码 ```python class Node: """双向链表节点""" def __init__(self, value): self.value = value # 节点值 self.prev = None # 指向前一个节点的指针 self.next = None # 指向后一个节点的指针 class Deque: """双向队列实现""" def __init__(self): """初始化空双向队列""" # 创建哨兵节点 self.head = Node(0) # 头哨兵节点 self.tail = Node(0) # 尾哨兵节点 self.head.next = self.tail self.tail.prev = self.head self.size = 0 # 队列大小 def is_empty(self): """判断队列是否为空""" return self.size == 0 def get_size(self): """获取队列大小""" return self.size def push_front(self, value): """队头入队""" new_node = Node(value) # 在头哨兵节点后插入新节点 new_node.next = self.head.next new_node.prev = self.head self.head.next.prev = new_node self.head.next = new_node self.size += 1 def push_back(self, value): """队尾入队""" new_node = Node(value) # 在尾哨兵节点前插入新节点 new_node.prev = self.tail.prev new_node.next = self.tail self.tail.prev.next = new_node self.tail.prev = new_node self.size += 1 def pop_front(self): """队头出队""" if self.is_empty(): raise Exception('Deque is empty') # 删除头哨兵节点后的第一个节点 node = self.head.next self.head.next = node.next node.next.prev = self.head self.size -= 1 return node.value def pop_back(self): """队尾出队""" if self.is_empty(): raise Exception('Deque is empty') # 删除尾哨兵节点前的第一个节点 node = self.tail.prev self.tail.prev = node.prev node.prev.next = self.tail self.size -= 1 return node.value def peek_front(self): """查看队头元素""" if self.is_empty(): raise Exception('Deque is empty') return self.head.next.value def peek_back(self): """查看队尾元素""" if self.is_empty(): raise Exception('Deque is empty') return self.tail.prev.value ``` - **时间复杂度**:所有操作均为 O(1) ### 2.2 顺序存储双向队列 顺序存储双向队列可以使用数组实现,但需要处理循环队列的问题以避免"假溢出"。 #### 2.2.1 顺序存储双向队列的基本描述 使用循环数组实现双向队列: - **数组结构**:使用固定大小的数组存储元素 - **头尾指针**:维护队头和队尾的位置 - **循环处理**:通过取模运算实现循环队列 #### 2.2.2 顺序存储双向队列的实现代码 ```python class Deque: """顺序存储双向队列实现""" def __init__(self, capacity=100): """初始化双向队列""" self.capacity = capacity self.queue = [None] * capacity self.front = 0 # 队头指针 self.rear = 0 # 队尾指针 self.size = 0 # 队列大小 def is_empty(self): """判断队列是否为空""" return self.size == 0 def is_full(self): """判断队列是否已满""" return self.size == self.capacity def get_size(self): """获取队列大小""" return self.size def push_front(self, value): """队头入队""" if self.is_full(): raise Exception('Deque is full') # 队头指针向前移动 self.front = (self.front - 1) % self.capacity self.queue[self.front] = value self.size += 1 def push_back(self, value): """队尾入队""" if self.is_full(): raise Exception('Deque is full') self.queue[self.rear] = value # 队尾指针向后移动 self.rear = (self.rear + 1) % self.capacity self.size += 1 def pop_front(self): """队头出队""" if self.is_empty(): raise Exception('Deque is empty') value = self.queue[self.front] # 队头指针向后移动 self.front = (self.front + 1) % self.capacity self.size -= 1 return value def pop_back(self): """队尾出队""" if self.is_empty(): raise Exception('Deque is empty') # 队尾指针向前移动 self.rear = (self.rear - 1) % self.capacity value = self.queue[self.rear] self.size -= 1 return value def peek_front(self): """查看队头元素""" if self.is_empty(): raise Exception('Deque is empty') return self.queue[self.front] def peek_back(self): """查看队尾元素""" if self.is_empty(): raise Exception('Deque is empty') return self.queue[(self.rear - 1) % self.capacity] ``` - **时间复杂度**:所有操作均为 $O(1)$ ### 2.3 两种实现方式对比 | 特性 | 链式存储 | 顺序存储 | |------|----------|----------| | 空间利用率 | 按需分配,无浪费 | 固定大小,可能浪费 | | 扩容操作 | 无需扩容 | 需要重新分配空间 | | 内存碎片 | 可能产生碎片 | 较少 | | 实现复杂度 | 相对复杂 | 简单 | | 缓存性能 | 较差 | 较好 | ## 3. 经典例题:设计循环双端队列 ### 3.1 题目链接 - [641. 设计循环双端队列 - 力扣(LeetCode)](https://leetcode.cn/problems/design-circular-deque/) ### 3.2 题目大意 **描述**:设计实现双端队列。 **要求**:实现 `MyCircularDeque` 类: - `MyCircularDeque(int k)`:构造函数,双端队列最大为 $k$。 - `boolean insertFront()`:将一个元素添加到双端队列头部。如果操作成功返回 $true$,否则返回 $false$。 - `boolean insertLast()`:将一个元素添加到双端队列尾部。如果操作成功返回 $true$,否则返回 $false$。 - `boolean deleteFront()`:从双端队列头部删除一个元素。如果操作成功返回 $true$,否则返回 $false$。 - `boolean deleteLast()`:从双端队列尾部删除一个元素。如果操作成功返回 $true$,否则返回 $false$。 - `int getFront()`:从双端队列头部获得一个元素。如果双端队列为空,返回 $-1$。 - `int getRear()`:获得双端队列的最后一个元素。如果双端队列为空,返回 $-1$。 - `boolean isEmpty()`:如果双端队列为空,则返回 $true$,否则返回 $false$。 - `boolean isFull()`:如果双端队列满了,则返回 $true$,否则返回 $false$。 ### 3.3 解题思路 ##### 思路 1:数组实现 使用数组实现循环双端队列,通过头尾指针和取模运算实现循环操作。 ##### 思路 1:代码 ```python class MyCircularDeque: def __init__(self, k: int): """初始化双端队列""" self.capacity = k self.queue = [0] * k self.front = 0 # 队头指针 self.rear = 0 # 队尾指针 self.size = 0 # 队列大小 def insertFront(self, value: int) -> bool: """在队头插入元素""" if self.isFull(): return False # 队头指针向前移动 self.front = (self.front - 1) % self.capacity self.queue[self.front] = value self.size += 1 return True def insertLast(self, value: int) -> bool: """在队尾插入元素""" if self.isFull(): return False self.queue[self.rear] = value # 队尾指针向后移动 self.rear = (self.rear + 1) % self.capacity self.size += 1 return True def deleteFront(self) -> bool: """删除队头元素""" if self.isEmpty(): return False # 队头指针向后移动 self.front = (self.front + 1) % self.capacity self.size -= 1 return True def deleteLast(self) -> bool: """删除队尾元素""" if self.isEmpty(): return False # 队尾指针向前移动 self.rear = (self.rear - 1) % self.capacity self.size -= 1 return True def getFront(self) -> int: """获取队头元素""" if self.isEmpty(): return -1 return self.queue[self.front] def getRear(self) -> int: """获取队尾元素""" if self.isEmpty(): return -1 return self.queue[(self.rear - 1) % self.capacity] def isEmpty(self) -> bool: """判断队列是否为空""" return self.size == 0 def isFull(self) -> bool: """判断队列是否已满""" return self.size == self.capacity ``` ##### 思路 1:复杂度分析 - **时间复杂度**:所有操作均为 $O(1)$ - **空间复杂度**:$O(k)$,其中 $k$ 为队列的容量,因为底层用定长数组实现 ## 4. 总结 ### 4.1 优点 - **操作灵活**:支持在队列两端进行插入和删除操作 - **效率高**:所有基本操作的时间复杂度均为 O(1) - **功能强大**:可以模拟栈和队列的行为 - **应用广泛**:在滑动窗口、单调队列等算法中发挥重要作用 ### 4.2 缺点 - **实现复杂**:相比普通队列,实现逻辑更加复杂 - **内存开销**:链式实现需要额外的指针空间 - **缓存性能**:链式实现在缓存性能上不如数组实现 ### 4.3 适用场景 - **滑动窗口问题**:如滑动窗口最大值、最小值等 - **单调队列**:维护单调递增或递减序列 - **双端操作**:需要在序列两端频繁操作的场景 - **算法优化**:某些算法的时间复杂度优化 ## 练习题目 - [0239. 滑动窗口最大值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/sliding-window-maximum.md) - [0641. 设计循环双端队列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-circular-deque.md) - [0862. 和至少为 K 的最短子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-subarray-with-sum-at-least-k.md) - [0901. 股票价格跨度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/online-stock-span.md) - [双向队列基础题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E5%9F%BA%E7%A1%80%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】数据结构与算法 Python 语言描述 - 裘宗燕 著 - 【书籍】数据结构教程 第 3 版 - 唐发根 著 - 【书籍】大话数据结构 程杰 著 - 【文章】[双端队列 - 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/41330) - 【文章】[Python collections.deque 详解 - 菜鸟教程](https://www.runoob.com/python3/python3-collections-deque.html) ================================================ FILE: docs/03_stack_queue_hash_table/03_06_hash_table.md ================================================ ## 1. 哈希表简介 > **哈希表(Hash Table)**,又称散列表,是一种能通过关键码(Key)直接访问数据的结构。 > > 哈希表利用「键 $key$」和「哈希函数 $Hash(key)$」将关键码映射到表中的某个位置,从而实现高效的查找和存储。这个映射过程由「哈希函数(散列函数)」完成,存储数据的数组称为「哈希表(散列表)」。 哈希表的核心思想是:通过哈希函数将键 $key$ 映射到表的某个区块,实现高效的数据插入与查找。其基本操作流程如下: - **插入关键码**:利用哈希函数计算关键字应存放的区块索引,然后将数据存入该区块。 - **查找关键码**:用相同的哈希函数定位区块,再在该区块中查找目标数据。 如下图所示为哈希表的原理示意: ![哈希表](https://qcdn.itcharge.cn/images/202405092317578.png) 以图中为例,假设哈希函数为 $Hash(key) = key // 1000$($//$ 表示整除),则哈希表的插入和查找过程如下: - **插入**:如 $0138$,通过 $Hash(0138) = 0$,应存入第 $0$ 区块。 - **查找**: - 查找 $2321$,$Hash(2321) = 2$,在第 $2$ 区块中找到 $2321$。 - 查找 $3214$,$Hash(3214) = 3$,在第 $3$ 区块未找到,说明 $3214$ 不在哈希表中。 哈希表在实际生活中也有广泛应用,例如「查字典」: 查找 **「赞」** 这个字时,我们根据拼音索引 `zan` 查到页码 $599$,然后翻到第 $599$ 页即可查看解释。 ![查字典](https://qcdn.itcharge.cn/images/20220111174223.png) 在这个例子中: - 存放拼音与页码的表,相当于 **哈希表**。 - **「赞」** 的拼音 `zan`,相当于哈希表的 **关键字 $key$**。 - 通过拼音查页码的过程,相当于 **哈希函数 $Hash(key)$** 的作用。 - 查到的页码 $599$,相当于哈希表中的 **哈希地址 $value$**。 ## 2. 哈希函数 > **哈希函数(Hash Function)**:是一种将元素的关键字映射为哈希表存储位置的函数。 哈希函数是哈希表设计的核心。一个优秀的哈希函数通常应具备以下特点: - 计算简单高效,便于实现; - 能将关键字均匀分布到哈希表的各个位置,减少冲突; - 输出哈希值为固定长度; - 如果 $Hash(key1) \ne Hash(key2)$,则 $key1$ 和 $key2$ 必然不同; - 如果 $Hash(key1) = Hash(key2)$,则 $key1$ 和 $key2$ 可能相同,也可能不同(即可能发生哈希冲突)。 在实际应用中,关键字类型可能为数字、字符串、浮点数、大整数,甚至多种类型的组合。通常会先将各种类型的关键字转换为整数,再通过哈希函数映射到哈希表中。 针对整数类型关键字,常见的哈希函数设计方法包括:直接定址法、除留余数法、平方取中法、基数转换法、数字分析法、折叠法、随机数法、乘积法、点积法等。下面介绍几种常用方法: ### 2.1 直接定址法 - **直接定址法**:直接取关键字本身或其线性函数作为哈希地址,即 $Hash(key) = key$ 或 $Hash(key) = a \times key + b$,其中 $a$、$b$ 为常数。 该方法计算极为简单,且不会产生冲突,适用于关键字分布连续的场景。如果关键字分布稀疏,则会造成空间浪费。 例如,统计 $1 \sim 100$ 岁人口,年龄为关键字,哈希函数取关键字本身: | 年龄 | 1 | 2 | 3 | ... | 25 | 26 | 27 | ... | 100 | | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | :--: | | 人数 | 3000 | 2000 | 5000 | ... | 1050 | ... | ... | ... | ... | 假如想要查询 $25$ 岁人数,直接访问第 $25$ 项即可。 ### 2.2 除留余数法 - **除留余数法**:设哈希表长度为 $m$,选取不大于 $m$ 的质数 $p$,用 $Hash(key) = key \mod p$ 计算哈希地址。 这是最常用的哈希函数之一。关键在于 $p$ 的选择,通常取质数或与 $m$ 接近的数,以减少冲突。 例如,将 $[432, 5, 128, 193, 92, 111, 88]$ 存入长度为 $11$ 的哈希表,哈希地址如下: | 索引 | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | | :--: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | | 数据 | 88 | 111 | | 432 | 92 | 5 | 193 | | 128 | | | ### 2.3 平方取中法 - **平方取中法**:先对关键字平方,再取中间若干位作为哈希地址。例如 $Hash(key) = (key \times key) // 100 \mod 1000$,即先平方,去掉末尾两位,再取中间三位。 该方法能有效利用关键字的各位信息,使哈希地址分布更均匀,减少冲突。 ### 2.4 基数转换法 - **基数转换法**:将关键字视为某一进制数,转换为另一进制后,选取部分位作为哈希地址。 - 例如,将关键字视为 $13$ 进制,转换为 $10$ 进制后作为哈希地址。 以 $343246$ 为例,计算如下: $343246_{13} = 3 \times 13^5 + 4 \times 13^4 + 3 \times 13^3 + 2 \times 13^2 + 4 \times 13^1 + 6 \times 13^0 = 1235110_{10}$ ## 3. 哈希冲突 > **哈希冲突(Hash Collision)**:指不同的关键字经过同一个哈希函数后,得到相同的哈希地址,即 $key1 \ne key2$,但 $Hash(key1) == Hash(key2)$,这种现象称为哈希冲突。 理想情况下,哈希函数能够实现一一映射,每个关键字($key$)都对应唯一的存储位置($value$),无需处理冲突。但在实际应用中,即使哈希函数设计得再好,也难以完全避免不同关键字映射到同一地址的情况,即哈希冲突不可避免。 因此,必须采用一定的策略来解决哈希冲突。常见的哈希冲突解决方法主要有两大类:**开放地址法(Open Addressing)** 和 **链地址法(Chaining)**。 ### 3.1 开放地址法 > **开放地址法(Open Addressing)**:当哈希冲突发生时,通过探查哈希表中的其他「空地址」来存放冲突元素,直到找到空位为止。 具体做法是:当发生冲突时,按照如下公式计算下一个可用地址:$H(i) = (Hash(key) + F(i)) \mod m$,其中 $i = 1, 2, 3, ..., n$,$n \le m-1$。 - $H(i)$ 表示第 $i$ 次探查得到的地址。每次冲突时,依次尝试新的地址,直到找到空位。 - $Hash(key)$ 是哈希函数,$m$ 是哈希表长度,取模保证地址在表内。 - $F(i)$ 是探查增量,常见方式有: - 线性探查:$F(i) = i$,即每次向后顺序查找。 - 二次探查:$F(i) = \pm i^2$,即以二次方步长向前后查找。 - 伪随机探查:$F(i)$ 为伪随机数序列。 举例说明:假设哈希表长度为 $11$,哈希函数 $Hash(key) = key \mod 11$,已存入 $28$、$49$、$18$,现插入 $38$,其哈希地址为 $5$,发生冲突。分别采用三种方法处理: - 线性探查:$H(1) = (5 + 1) \mod 11 = 6$(冲突),$H(2) = (5 + 2) \mod 11 = 7$(冲突),$H(3) = (5 + 3) \mod 11 = 8$(空位),将 $38$ 存入 $8$。 - 二次探查:$H(1) = (5 + 1^2) \mod 11 = 6$(冲突),$H(2) = (5 - 1^2) \mod 11 = 4$(空位),将 $38$ 存入 $4$。 - 伪随机探查:假设伪随机数为 $9$,$H(1) = (5+9)\mod 11 = 3$(空位),将 $38$ 存入 $3$。 如下图所示: ![开放地址法](https://qcdn.itcharge.cn/images/202405092318809.png) ### 3.2 链地址法 > **链地址法(Chaining)**:将哈希地址相同的元素以链表的形式存储在同一个槽位中。 链地址法是实际应用中更常用的哈希冲突解决方案。与开放地址法相比,链地址法实现更为简洁灵活。 假设哈希表长度为 $m$,哈希函数输出范围为 $[0, m - 1]$,则哈希表可以看作是 $m$ 个链表头指针组成的数组 $T$。 - 插入时,先通过 $Hash(key)$ 计算哈希地址 $i$,再将元素以链表节点的形式插入 $T[i]$ 指向的链表中。通常插入到表头,时间复杂度为 $O(1)$。 - 查询时,同样先计算哈希地址 $i$,然后遍历 $T[i]$ 链表,查找目标关键字。查找复杂度与链表长度 $k$ 成正比,即 $O(k)$。如果哈希函数分布均匀,$k \approx n/m$,$n$ 为元素总数。 举例:将 $keys = [88, 60, 65, 69, 90, 39, 07, 06, 14, 44, 52, 70, 21, 45, 19, 32]$ 依次插入哈希表,哈希函数 $Hash(key) = key \mod 13$,表长 $m=13$。采用链地址法,插入结果如下图: ![链地址法](https://qcdn.itcharge.cn/images/202405092319327.png) 与开放地址法相比,链地址法虽然需要额外的链表节点存储空间,但能有效降低插入和查找时的平均查找长度。因为链地址法只需比较哈希地址相同的元素,而开放地址法可能需要探查多个不同地址的元素。 ## 4. 哈希表总结 本文系统梳理了哈希表的基本原理与核心技术要点,内容涵盖哈希表的定义、哈希函数的设计、哈希冲突的本质及主流冲突解决策略。 - **哈希表(Hash Table)**:一种通过哈希函数将关键字 $key$ 映射到存储位置,实现高效查找、插入和删除的数据结构。 - **哈希函数(Hash Function)**:用于将关键字转换为哈希表索引的函数,其优劣直接影响哈希表的性能和冲突概率。 - **哈希冲突(Hash Collision)**:指不同关键字经过哈希函数后映射到同一地址,是哈希表设计中不可避免的问题。 哈希表的设计与实现主要关注两个核心问题: 1. **哈希函数的构建**:常见方法包括直接定址法、除留余数法、平方取中法、基数转换法、数字分析法、折叠法、随机数法、乘积法、点积法等。合理选择和设计哈希函数有助于均匀分布关键字,降低冲突概率。 2. **哈希冲突的解决**:主流方法有两类,开放地址法和链地址法。 - **开放地址法**:通过探查其他空槽位存放冲突元素,常见探查方式有线性探查、二次探查和伪随机探查等。 - **链地址法**:将哈希地址相同的元素以链表形式存储在同一槽位,结构灵活,实际应用更为广泛。 合理的哈希函数设计与高效的冲突解决策略,是哈希表高性能的关键所在。 ## 练习题目 - [0217. 存在重复元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate.md) - [0219. 存在重复元素 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate-ii.md) - [0036. 有效的数独](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-sudoku.md) - [0349. 两个数组的交集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays.md) - [0350. 两个数组的交集 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays-ii.md) - [0706. 设计哈希映射](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/design-hashmap.md) - [哈希表题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%93%88%E5%B8%8C%E8%A1%A8%E9%A2%98%E7%9B%AE) ## 参考资料 - 【博文】[哈希算法及 python 字典的实现 – Tim's Path](https://xiaoxubeii.github.io/articles/hash/) - 【文章】[哈希表 - LeetBook - 力扣(LeetCode)](https://leetcode.cn/leetbook/read/hash-table/xh8uld/) - 【文章】[漫画算法 - 小灰的算法之旅 - LeetBook - 力扣(LeetCode)](https://leetcode.cn/leetbook/read/journey-of-algorithm/5o13c3/) - 【博文】[面试官:哈希表都不知道,你是怎么看懂 HashMap 的? - 掘金](https://juejin.cn/post/6876105622274703368) - 【博文】[散列表 | Frank's Blog](https://frankfang.cn/article/202104852) - 【博文】[哈希表 - OI Wiki](https://oi-wiki.org/ds/hash/) - 【博文】[散列表(上)- 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/64233) - 【书籍】数据结构(C 语言版)- 严蔚敏 著 - 【书籍】数据结构教程(第 3 版)- 唐发根 著 - 【书籍】数据结构与算法 Python 语言描述 - 裘宗燕 著 ================================================ FILE: docs/03_stack_queue_hash_table/index.md ================================================ ![](https://qcdn.itcharge.cn/images/20250923140308.png) ::: tip 引 言 栈如杯中叠盘,盘盘相扣,取放皆自上,后进先出。 队列如河中行舟,前舟先行,后舟继发,川流不息。 哈希表如图书馆索引卡,万卷藏于指尖,瞬息之间,尽览群书。 ::: ## 本章内容 - [3.1 栈基础](https://github.com/ITCharge/AlgoNote/tree/main/docs/03_stack_queue_hash_table/03_01_stack_basic.md) - [3.2 单调栈](https://github.com/ITCharge/AlgoNote/tree/main/docs/03_stack_queue_hash_table/03_02_monotone_stack.md) - [3.3 队列基础](https://github.com/ITCharge/AlgoNote/tree/main/docs/03_stack_queue_hash_table/03_03_queue_basic.md) - [3.4 优先队列](https://github.com/ITCharge/AlgoNote/tree/main/docs/03_stack_queue_hash_table/03_04_priority_queue.md) - [3.5 双端队列](https://github.com/ITCharge/AlgoNote/tree/main/docs/03_stack_queue_hash_table/03_05_bidirectional_queue.md) - [3.6 哈希表](https://github.com/ITCharge/AlgoNote/tree/main/docs/03_stack_queue_hash_table/03_06_hash_table.md) ================================================ FILE: docs/04_string/04_01_string_basic.md ================================================ ## 1. 字符串简介 > **字符串(String)**:简称为串,由零个或多个字符组成的有限序列,常记为 $s = a_1a_2…a_n(0 \le n \lneqq \infty)$。 ### 1.1 字符串常见概念 - **字符串名称**:如 $s$。 - **字符串的值**:字符序列 $a_1a_2…a_n$,通常用双引号括起来。 - **字符变量**:字符串中每个位置的字符($a_i$),可以是字母、数字或其他字符,$i$ 表示其位置。 - **字符串长度**:字符个数 $n$。 - **空串**:长度为 $0$ 的字符串,记为 `""`。 - **子串**:字符串中任意连续字符组成的序列。特殊子串包括 **前缀**(从第 $0$ 位起,长度为 $k$)和 **后缀**(以 $n - 1$ 结尾,长度为 $k$)。 - **主串**:包含子串的字符串。 举个例子来说明一下: ```python str = "Hello World" ``` 在示例代码中,$str$ 是一个字符串的变量名称,`Hello World` 则是该字符串的值,字符串的长度为 $11$。该字符串的表示如下图所示: ![字符串](https://qcdn.itcharge.cn/images/20240511114722.png) 字符串与数组相似,都使用 **名称[下标]** 方式访问元素。 ### 1.2 字符串的特点 - 数据元素都是字符,结构简单但规模可能很大 - 常作为整体处理,操作对象是整个字符串或子串 - 经常需要处理多个字符串间的操作(连接、比较等) ### 1.3 字符串问题分类 根据字符串的特点,我们可以将字符串问题分为以下几种: - 字符串匹配问题 - 子串相关问题 - 前缀 / 后缀相关问题 - 回文串相关问题 - 子序列相关问题 ## 2. 字符串的比较 ### 2.1 字符串的比较操作 数字之间的大小比较非常直观,例如 $1 < 2$。而字符串的大小比较则稍显复杂,其本质是根据字符在字符串中的排列顺序和字符编码来决定的。 以 `str1 = "abc"` 和 `str2 = "acc"` 为例,二者的首字母都是 $a$,但第二个字母 $b$ 比 $c$ 靠前,因此 $b < c$,所以 `"abc" < "acc"`,即 $str1 < str2$。 字符串的比较实际上是逐字符比较其「字符编码」——即每个字符在字符集中的编号。 判断两个字符串是否相等,需要满足以下两个条件: 1. 两个字符串的长度相等; 2. 两个字符串对应位置上的每个字符都相同。 如果要比较两个字符串的大小,可以按照如下规则进行: - 从第 $0$ 个字符开始,依次比较对应位置上的字符编码: - 如果 $str1[i]$ 的字符编码等于 $str2[i]$,则继续比较下一位; - 如果 $str1[i]$ 的字符编码小于 $str2[i]$,则 $str1 < str2$,如 `"abc" < "acc"`; - 如果 $str1[i]$ 的字符编码大于 $str2[i]$,则 $str1 > str2$,如 `"bcd" > "bad"`。 - 如果某一字符串已比较到末尾,另一个字符串还有剩余字符: - 如果 $len(str1) < len(str2)$,则 $str1 < str2$,如 `"abc" < "abcde"`; - 如果 $len(str1) > len(str2)$,则 $str1 > str2$,如 `"abcde" > "abc"`。 - 如果所有字符都相等且长度也相同,则 $str1 == str2$,如 `"abcd" == "abcd"`。 基于上述规则,可以实现一个 `strcmp` 方法,约定如下返回值: - $str1 < str2$ 时,返回 $-1$; - $str1 == str2$ 时,返回 $0$; - $str1 > str2$ 时,返回 $1$。 `strcmp` 方法的实现如下: ```python def strcmp(str1, str2): """ 比较两个字符串的大小。 返回值: -1:str1 < str2 0:str1 == str2 1:str1 > str2 """ # 逐字符比较 i = 0 while i < len(str1) and i < len(str2): c1 = ord(str1[i]) c2 = ord(str2[i]) if c1 < c2: return -1 # str1 当前字符小于 str2 elif c1 > c2: return 1 # str1 当前字符大于 str2 i += 1 # 如果前面都相等,比较长度 if len(str1) < len(str2): return -1 # str1较短 elif len(str1) > len(str2): return 1 # str1较长 else: return 0 # 完全相等 ``` 其实,判断字符串大小最直观的例子就是「查英语词典」。在词典中,前面的单词总是比后面的单词小。我们可以把词典里的每个单词看作一个字符串,查找单词的过程本质上就是按照字符串大小进行比较和排序。 ### 2.2 字符串的字符编码 前面我们提到了字符编码,这里简要介绍几种常见的字符串字符编码标准。 最早,计算机采用 ASCII 编码来表示字符。ASCII 编码表包含 $127$ 个字符,涵盖了英文字母(大小写)、数字及常用符号。每个字符对应唯一的编码值,例如大写字母 $A$ 的编码是 $65$,小写字母 $a$ 的编码是 $97$。 然而,ASCII 编码只能满足英文等西方语言的需求,无法表示中文等其他语言字符。为支持中文,我国先后制定了 GB2312、GBK、GB18030 等中文编码标准,将常用汉字纳入编码体系。但全球有上百种语言,各国标准不一,导致编码冲突频发。为解决这一问题,国际上推出了统一的 Unicode 编码标准。 Unicode 能够为世界上所有文字和符号分配唯一编码。实际存储和传输时,Unicode 最常用的实现方式是 UTF-8 编码。UTF-8 会根据字符的不同,将每个 Unicode 字符编码为 $1 \sim 6$ 个字节:常用英文字母通常占 $1$ 个字节,汉字一般占 $3$ 个字节。 ## 3. 字符串的存储结构 字符串的存储结构与线性表类似,主要分为「顺序存储结构」和「链式存储结构」两类。 ### 3.1 字符串的顺序存储结构 顺序存储结构是指用一组地址连续的存储单元,依次存放字符串中的各个字符。通常为每个字符串变量分配一个固定长度的存储空间,常见实现方式是定长数组。 如下图所示: ![字符串的顺序存储](https://qcdn.itcharge.cn/images/20240511114747.png) 在顺序存储结构中,每个字符都有唯一的下标索引,索引从 $0$ 开始,到 $\text{字符串长度} - 1$ 结束。每个下标对应一个字符元素。 顺序存储的字符串支持随机访问,可以通过下标直接定位和访问任意字符,效率高,操作便捷。 ### 3.2 字符串的链式存储结构 链式存储结构是用线性链表来存储字符串。每个链节点包含一个用于存放字符的 $data$ 字段,以及指向下一个节点的指针 $next$,从而将所有字符串联起来。 链式存储结构中,每个节点可以存放一个或多个字符。常见的做法是每个节点存放 $1$ 个或 $4$ 个字符,以减少空间浪费。当节点存放 $4$ 个字符时,如果字符串长度不是 $4$ 的倍数,最后一个节点未用满的部分可用 `#` 或其他特殊字符补齐。 如下图所示: ![字符串的链式存储](https://qcdn.itcharge.cn/images/20240511114804.png) 链式存储结构通过指针将分散的存储单元连接起来,逻辑上形成一个完整的字符串。其优点是插入、删除操作灵活,但随机访问效率较低。 ### 3.3 各语言中的字符串实现 - **C 语言**:字符串以字符数组形式存储,并以空字符 `\0` 结尾标识结束。相关操作函数在 `string.h` 头文件中。 - **C++ 语言**:既支持 C 风格字符串,也提供了功能更强的 `string` 类,极大简化了字符串操作。 - **Java 语言**:标准库中提供了 `String` 类,专门用于字符串处理。 - **Python 语言**:字符串由 `str` 类型对象表示,属于不可变类型。即一旦创建,字符串的内容和长度都无法更改或删除。 ## 4. 字符串匹配问题 > **字符串匹配(String Matching)**,又称模式匹配(Pattern Matching),指的是在一个主串 $T$(文本串)中查找某个子串 $p$(模式串)的位置。 字符串匹配是字符串处理领域中最核心的问题之一。根据需要查找的模式串数量,字符串匹配问题可分为「单模式串匹配」和「多模式串匹配」两大类。 ### 4.1 单模式串匹配问题 > **单模式匹配问题(Single Pattern Matching)**:给定一个文本串 $T = t_1t_2...t_n$ 和一个模式串 $p = p_1p_2...p_m$,要求找出 $p$ 在 $T$ 中所有出现的位置。 #### 4.1.1 问题描述 单模式串匹配问题是字符串匹配的基础问题,其形式化定义如下: - **输入**:文本串 $T = T[0...n-1]$,模式串 $p = p[0...m-1]$ - **输出**:所有满足 $T[i...i+m-1] = p[0...m-1]$ 的位置 $i$ - **目标**:高效地找到所有匹配位置 #### 4.1.2 主要算法介绍 针对单模式串匹配,常见的算法可根据其在文本中搜索模式串的方式分为以下几类: **朴素算法** - **Brute Force 算法(暴力匹配算法)** - **时间复杂度**:$O(n \times m)$ - **空间复杂度**:$O(1)$ - **特点**:简单直观,但效率较低 - **适用场景**:模式串较短或对性能要求不高的场景 **基于前缀搜索的方法** - **KMP 算法(Knuth-Morris-Pratt 算法)** - **时间复杂度**:$O(n + m)$ - **空间复杂度**:$O(m)$ - **特点**:利用失配信息避免重复比较,从前向后逐个读取文本字符 - **适用场景**:对性能要求较高的场景 **基于后缀搜索的方法** - **Boyer Moore 算法** - **时间复杂度**:平均 $O(n/m)$,最坏 $O(n \times m)$ - **空间复杂度**:$O(k)$($k$ 为字符集大小) - **特点**:从右到左比较,跳跃能力强 - **适用场景**:模式串较长,字符集较小的场景 - **Horspool 算法** - **时间复杂度**:平均 $O(n)$,最坏 $O(n \times m)$ - **空间复杂度**:$O(k)$ - **特点**:BM算法的简化版本,实现简单 - **适用场景**:需要简单实现的场景 - **Sunday 算法** - **时间复杂度**:平均 $O(n)$,最坏 $O(n \times m)$ - **空间复杂度**:$O(k)$ - **特点**:从左到右比较,跳跃能力强 - **适用场景**:需要从左到右匹配的场景 **基于子串搜索的方法** - **Rabin Karp 算法** - **时间复杂度**:平均 $O(n + m)$,最坏 $O(n \times m)$ - **空间复杂度**:$O(1)$ - **特点**:使用滚动哈希,平均性能较好 - **适用场景**:需要处理多个模式串或对哈希冲突不敏感的场景 #### 4.1.3 算法复杂度对比 | 算法 | 预处理时间 | 匹配时间 | 空间复杂度 | 特点 | |------|------------|----------|------------|------| | Brute Force | $O(1)$ | $O(n \times m)$ | $O(1)$ | 简单直观 | | Rabin Karp | $O(m)$ | 平均 $O(n)$,最坏 $O(n \times m)$ | $O(1)$ | 滚动哈希 | | KMP | $O(m)$ | $O(n)$ | $O(m)$ | 失配信息 | | Boyer Moore | $O(m + k)$ | 平均 $O(n/m)$,最坏 $O(n \times m)$ | $O(k)$ | 启发式跳跃 | | Horspool | $O(m + k)$ | 平均 $O(n)$,最坏 $O(n \times m)$ | $O(k)$ | BM简化版 | | Sunday | $O(m + k)$ | 平均 $O(n)$,最坏 $O(n \times m)$ | $O(k)$ | 从左到右 | ### 4.2 多模式串匹配问题 > **多模式匹配问题(Multi Pattern Matching)**:给定一个文本串 $T = t_1t_2...t_n$,以及一组模式串 $P = \{p^1, p^2, ..., p^r\}$,其中每个模式串 $p^i$ 是由有限字母表组成的字符串 $p^i = p^i_1p^i_2...p^i_{m_i}$。目标是在文本串 $T$ 中找出集合 $P$ 中所有模式串 $p^i$ 的全部出现位置。 #### 4.2.1 问题描述 **输入**:一个长度为 $n$ 的文本串 $T$,和若干模式串组成的集合 $P = \{p^{(1)}, p^{(2)}, ..., p^{(r)}\}$,每个模式串长度为 $m_i$。 **输出**:返回每个模式串 $p^{(i)}$ 在 $T$ 中所有出现的位置下标。 **目标**:高效地一次性找出所有模式串在文本串中的全部匹配位置。 简而言之,多模式串匹配问题要求:给定一个文本串和多个模式串,找出每个模式串在文本串中所有出现的位置。 #### 4.2.2 主要算法介绍 针对多模式串匹配,最朴素的做法是对每个模式串分别进行 $r$ 次单模式匹配(如 Brute Force、KMP 等),但这种方法在模式串数量较多时效率极低。为此,实际应用中常用更高效的多模式串匹配算法,主要包括以下三类: 多模式串匹配的高效算法主要可以归纳为三大类:前缀结构类、后缀结构类和哈希类。它们各自利用不同的数据结构和思想,实现对多个模式串的高效匹配。下面对主流方法进行梳理与融合说明: **前缀结构类方法** - **字典树(Trie)** - **时间复杂度**:构建 $O(k)$,单次查找 $O(m)$ - **空间复杂度**:$O(k)$ - **特点**:支持高效的前缀匹配和批量字符串检索,结构直观,易于实现 - **适用场景**:前缀匹配、字符串集合检索、词频统计等 - **AC 自动机** - **时间复杂度**:构建 $O(k)$,匹配 $O(n + k)$ - **空间复杂度**:$O(k)$ - **特点**:在字典树基础上引入失败指针,结合 KMP 失配思想,可一次遍历文本高效匹配所有模式串 - **适用场景**:多模式串精确匹配,如敏感词过滤、病毒特征检测等 **后缀结构类方法** - **后缀数组** - **时间复杂度**:构建 $O(n \log n)$,查找 $O(m + \log n)$ - **空间复杂度**:$O(n)$ - **特点**:支持后缀的快速排序和二分查找,适合子串定位和重复子串分析,实现相对简单 - **适用场景**:子串查找、最长重复子串、最长公共子串等 - **后缀树** - **时间复杂度**:构建 $O(n)$(理论),实际实现较复杂 - **空间复杂度**:$O(n)$,但常数较大 - **特点**:支持复杂的字符串分析,能高效解决多种字符串问题,但实现难度高、空间消耗大 - **适用场景**:复杂的字符串分析、需要多种子串关系查询的场景(实际多用后缀数组替代) **哈希与子串搜索类方法** - **Rabin-Karp 算法** - **时间复杂度**:平均 $O(n + m)$,最坏 $O(n \times m)$ - **空间复杂度**:$O(1)$ - **特点**:利用滚动哈希批量比对多个模式串,平均性能较好,但哈希冲突时退化 - **适用场景**:需要处理多个模式串、对哈希冲突不敏感的场景 综上,实际应用中多模式串匹配常用 AC 自动机(高效且适用范围广)、字典树(适合前缀类问题)、后缀数组(适合后缀和子串分析)以及 Rabin-Karp(适合哈希批量比对)等方法。选择哪种算法,需根据具体问题规模、模式串数量、匹配需求等因素综合考虑。 #### 4.2.3 算法复杂度对比 | 算法 | 预处理时间 | 匹配时间 | 空间复杂度 | 稳定性 | 适用场景 | |--------------|--------------------|--------------------|-------------|----------|----------------------------| | 字典树 | $O(k)$ | $O(m)$ | $O(k)$ | 稳定 | 前缀匹配、字符串集合操作 | | AC 自动机 | $O(k)$ | $O(n + k)$ | $O(k)$ | 稳定 | 多模式串精确匹配 | | 后缀数组 | $O(n \log n)$ | $O(m + \log n)$ | $O(n)$ | 稳定 | 子串匹配、后缀相关操作 | 其中: - $n$ 表示文本串长度 - $m$ 表示单个模式串长度 - $k$ 表示所有模式串的总长度 需要特别指出的是,绝大多数高效的多模式串匹配算法都依赖于一种基础数据结构: **字典树(Trie Tree)**。其中,著名的 **Aho-Corasick 自动机(AC 自动机)算法**,正是将「KMP 算法」与 「字典树」结构结合的产物,也是目前多模式串匹配中最常用、最有效的算法之一。 因此,学习多模式匹配算法时,重点应掌握 **字典树** 及 **AC 自动机算法** 的原理与实现。 ## 练习题目 - [0125. 验证回文串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/valid-palindrome.md) - [0344. 反转字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-string.md) - [0557. 反转字符串中的单词 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/reverse-words-in-a-string-iii.md) - [字符串基础题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%9F%BA%E7%A1%80%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】数据结构(C 语言版)- 严蔚敏 著 - 【书籍】大话数据结构 - 程杰 著 - 【书籍】数据结构教程(第 3 版)唐发根 著 - 【书籍】数据结构与算法 Python 语言描述 - 裘宗燕 著 - 【书籍】柔性字符串匹配 - 中科院计算所网络信息安全研究组 译 - 【博文】[字符串和编码 - 廖雪峰的官方网站 ](https://www.liaoxuefeng.com/wiki/1016959663602400/1017075323632896) - 【文章】[数组和字符串 - LeetBook - 力扣](https://leetcode.cn/leetbook/read/array-and-string/c9lnm/) - 【文章】[字符串部分简介 - OI Wiki](https://oi-wiki.org/string/) - 【文章】[解密 Python 中字符串的底层实现,以及相关操作 - 古明地盆 - 博客园](https://www.cnblogs.com/traditional/p/13455962.html) ================================================ FILE: docs/04_string/04_02_string_brute_force.md ================================================ ## 1. Brute Force 算法介绍 > **Brute Force 算法**:简称为 BF 算法,也可以叫做「朴素匹配算法」。 > > - **Brute Force 算法核心思想**:将模式串 $p$ 依次与文本串 $T$ 的每个起点对齐,从左到右逐字符比对;相等则继续,不等则把对齐起点右移一位,直到匹配成功或遍历完文本。 ![朴素匹配算法](https://qcdn.itcharge.cn/images/20240511154456.png) ## 2. Brute Force 算法步骤 1. 设文本串 $T$ 长度为 $n$,模式串 $p$ 长度为 $m$。 2. 从 $T$ 的每个起点 $0..n - m$ 依次与 $p$ 对齐,逐字符比较:如果相等则继续,不相等则起点右移一位、$p$ 归零。 3. 如果某次对齐能把 $p$ 的全部字符匹配完,则返回该起点;否则无解。 ## 3. Brute Force 算法代码实现 ```python def bruteForce(T: str, p: str) -> int: n, m = len(T), len(p) i, j = 0, 0 # i 表示文本串 T 的当前位置,j 表示模式串 p 的当前位置 while i < n and j < m: # i 或 j 其中一个到达尾部时停止搜索 if T[i] == p[j]: # 如果相等,则继续进行下一个字符匹配 i += 1 j += 1 else: i = i - (j - 1) # 如果匹配失败则将 i 移动到上次匹配开始位置的下一个位置 j = 0 # 匹配失败 j 回退到模式串开始位置 if j == m: return i - j # 匹配成功,返回匹配的开始位置 else: return -1 # 匹配失败,返回 -1 ``` ## 4. Brute Force 算法分析 BF 简单直观,但因不匹配时会完全回退、重新对齐,存在大量重复比较,效率较低。 | 指标 | 复杂度 | 说明 | | ------------ | -------------- | ------------------------------------ | | 最好时间复杂度 | $O(m)$ | 首个起点即匹配成功 | | 最坏时间复杂度 | $O(n \times m)$ | 每次都需回退,全部比较 | | 平均时间复杂度 | $O(n \times m)$ | 一般情况下的复杂度 | | 空间复杂度 | $O(1)$ | 原地匹配,无需额外空间 | - 大量回溯导致重复比较,是 BF 变慢的根源。 - 当文本或模式较长时,更应考虑 KMP、BM、Sunday 等改进算法。 ## 5. 总结 Brute Force(BF)算法通过将模式串与文本串每个可能的起点逐字符对齐比较,遇到不匹配时起点右移、模式串重头开始。该算法实现简单,空间复杂度为 $O(1)$,但时间复杂度较高:最好情况下为 $O(m)$(首位即匹配),平均和最坏情况下为 $O(n\times m)$,适合小规模或一次性匹配场景。 - **优点**:实现简单、无需预处理、适合小规模或一次性匹配。 - **缺点**:回溯多、效率低,不适合长文本/长模式或多次匹配场景。 ## 练习题目 - [0028. 找出字符串中第一个匹配项的下标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-the-index-of-the-first-occurrence-in-a-string.md) - [0459. 重复的子字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/repeated-substring-pattern.md) - [0686. 重复叠加字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/repeated-string-match.md) - [0796. 旋转字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotate-string.md) - [1408. 数组中的字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/string-matching-in-an-array.md) - [2156. 查找给定哈希值的子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/find-substring-with-given-hash-value.md) - [单模式串匹配题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8D%95%E6%A8%A1%E5%BC%8F%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】数据结构与算法 Python 语言描述 - 裘宗燕 著 - 【文章】[动画:什么是 BF 算法 ?- 吴师兄学编程](https://www.cxyxiaowu.com/560.html) - 【文章】[BF 算法(普通模式匹配算法)及 C 语言实现 - 数据结构与算法教程](http://data.biancheng.net/view/12.html) - 【文章】[字符串匹配基础(上)- 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/71187) ================================================ FILE: docs/04_string/04_03_string_rabin_karp.md ================================================ ## 1. Rabin Karp 算法介绍 > **Rabin Karp(RK)算法**:由 Michael Oser Rabin 与 Richard Manning Karp 于 1987 年提出,是一种利用哈希快速筛查匹配起点的单模式串匹配算法。 > > - **Rabin Karp 算法核心思想**:给定文本串 $T$ 与模式串 $p$,先计算 $p$ 的哈希值,再对 $T$ 的所有长度为 $m=|p|$ 的子串高效计算哈希。借助「滚动哈希」在 $O(1)$ 时间更新相邻子串的哈希,用哈希相等作为快速筛选,仅在相等时再逐字符比对以排除哈希冲突。 ## 2. Rabin Karp 算法步骤 ### 2.1 Rabin Karp 算法整体流程 1. 设 $n=|T|$、$m=|p|$。 2. 计算模式串哈希 $H(p)$。 3. 计算文本首个长度为 $m$ 的子串 $T_{[0,m-1]}$ 的哈希 $H(T_{[0,m-1]})$,并用滚动哈希依次得到其余 $n - m$ 个相邻子串的哈希。 4. 逐一比较 $H(T_{[i,i+m-1]})$ 与 $H(p)$: - 如果不相等,跳过; - 如果相等,逐字符核验:完全相同则返回起点 $i$,否则继续。 5. 全部位置检查后仍未匹配,返回 $-1$。 ### 2.2 滚动哈希算法 实现 RK 的关键是 **滚动哈希**:使相邻子串哈希的更新从 $O(m)$ 降为 $O(1)$,显著提升效率。 滚动哈希采用 **Rabin fingerprint** 思想:把子串视作 $d$ 进制多项式,基于上一个子串的哈希在 $O(1)$ 时间得到下一个子串的哈希。 下面我们用一个例子来解释一下这种算法思想。 设字符集大小为 $d$,用 $d$ 进制多项式哈希表示子串。 举个例子,假如字符串只包含 $a \sim z$ 这 $26$ 个小写字母,那么我们就可以用 $26$ 进制数来表示一个字符串,$a$ 表示为 $0$,$b$ 表示为 $1$,以此类推,$z$ 就用 $25$ 表示。 例如 `"cat"` 的哈希可表示为: $$\begin{aligned} Hash(cat) &= c \times 26^2 + a \times 26^1 + t \times 26^0 \cr &= 2 \times 26^2 + 0 \times 26^1 + 19 \times 26^0 \cr &= 1371 \end{aligned}$$ 这种多项式哈希的特点是:相邻子串的哈希可由上一个快速推得。 如果 $cat$ 的相邻子串为 `"ate"`,直接计算其哈希: $$\begin{aligned} Hash(ate) &= a \times 26^2 + t \times 26^1 + e \times 26^0 \cr &= 0 \times 26^2 + 19 \times 26^1 + 4 \times 26^0 \cr &= 498 \end{aligned}$$ 如果利用上一个子串 `"cat"` 的哈希滚动更新: $$\begin{aligned} Hash(ate) &= (Hash(cat) - c \times 26^2) \times 26 + e \times 26^0 \cr &= (1371 - 2 \times 26^2) \times 26 + 4 \times 26^0 \cr &= 498 \end{aligned}$$ 可以看出,这两种方式计算出的哈希值是相同的。但是第二种计算方式不需要再遍历子串,只需要进行一位字符的计算即可得出整个子串的哈希值。这样每次计算子串哈希值的时间复杂度就降到了 $O(1)$。然后我们就可以通过滚动哈希算法快速计算出子串的哈希值了。 将上述规律形式化如下。 给定文本串 $T$ 与模式串 $p$,设 $n=|T|$、$m=|p|$、字符集大小为 $d$,则: - 模式串:$H(p)=\sum\limits_{k=0}^{m-1} p_k\, d^{m-1-k}$; - 文本首子串:$H(T_{[0,m-1]})=\sum\limits_{k=0}^{m-1} T_k\, d^{m-1-k}$; - 滚动关系:$H(T_{[i+1,i+m]})=\big(H(T_{[i,i+m-1]})-T_i\, d^{m-1}\big)\, d+T_{i+m}$。 为避免溢出与降低冲突,计算时通常对大质数 $q$ 取模(模数宜大且为质数)。 ## 3. Rabin–Karp 代码实现 ```python # T: 文本串,p: 模式串,d: 字符集大小(基数),q: 模数(质数) def rabinKarp(T: str, p: str, d: int, q: int) -> int: n, m = len(T), len(p) if m == 0: return 0 if n < m: return -1 hash_p, hash_t = 0, 0 # 计算 H(p) 与首个子串的哈希 for i in range(m): hash_p = (hash_p * d + ord(p[i])) % q hash_t = (hash_t * d + ord(T[i])) % q # 使用 pow 的三参形式避免中间溢出 power = pow(d, m - 1, q) # d^(m-1) % q,用于移除最高位字符 for i in range(n - m + 1): if hash_p == hash_t: # 避免冲突:逐字符核验 match = True for j in range(m): if T[i + j] != p[j]: match = False break if match: return i if i < n - m: # 滚动更新到下一个子串 hash_t = (hash_t - power * ord(T[i])) % q # 去掉最高位字符 hash_t = (hash_t * d + ord(T[i + m])) % q # 加入新字符 return -1 ``` ## 4. 复杂度与性质 | 指标 | 复杂度 | 说明 | | ------------ | -------------- | ------------------------------------ | | 最好时间复杂度 | $O(n-m+1)$ | 无哈希冲突时,仅需 $n-m+1$ 次哈希对比,均为 $O(1)$,无需逐字符校验 | | 最坏时间复杂度 | $O(m(n-m+1))\approx O(nm)$ | 每次哈希均冲突,需 $n-m+1$ 次逐字符全量比对,每次 $O(m)$ | | 平均时间复杂度 | $O(n-m+1)$ | 期望哈希冲突极少,绝大多数位置仅哈希对比,均摊 $O(1)$ | | 空间复杂度 | $O(1)$ | 仅需常数变量存储哈希值与辅助参数 | 说明:与 BF 相比,RK 通过哈希筛选把大多数不匹配位置在 $O(1)$ 内排除;但哈希冲突会触发逐字符校验,致使最坏复杂度退化。 ## 5. 总结 Rabin-Karp(RK)算法通过将模式串和文本子串转化为哈希值,利用「滚动哈希」快速筛查匹配位置,大幅减少无效字符比较。其平均时间复杂度远优于朴素算法,适合大文本和多模式串场景,但哈希冲突时需回退逐字符比对,最坏情况下复杂度与朴素法相同。合理选择哈希参数可有效降低冲突概率,是一种高效且易于扩展的字符串匹配算法。 - **优点**: - 滚动哈希使子串哈希更新为 $O(1)$,平均性能优于 BF; - 易于扩展到多模式串场景(统一维护多哈希)。 - **缺点**: - 存在哈希冲突,最坏复杂度可退化至 $O(nm)$; - 需合理选择基数 $d$ 与大质数模 $q$,以降低冲突概率。 ## 练习题目 - [0028. 找出字符串中第一个匹配项的下标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-the-index-of-the-first-occurrence-in-a-string.md) - [0459. 重复的子字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/repeated-substring-pattern.md) - [0686. 重复叠加字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/repeated-string-match.md) - [0796. 旋转字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotate-string.md) - [1408. 数组中的字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/string-matching-in-an-array.md) - [2156. 查找给定哈希值的子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/find-substring-with-given-hash-value.md) - [单模式串匹配题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8D%95%E6%A8%A1%E5%BC%8F%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】数据结构与算法 Python 语言描述 - 裘宗燕 著 - 【文章】[字符串匹配基础(上)- 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/71187) - 【文章】[字符串匹配算法 - Rabin Karp 算法 - coolcao 的小站](https://coolcao.com/2020/08/20/rabin-karp/) - 【问答】[string - Python: Rabin-Karp algorithm hashing - Stack Overflow](https://stackoverflow.com/questions/22216948/python-rabin-karp-algorithm-hashing) ================================================ FILE: docs/04_string/04_04_string_kmp.md ================================================ ## 1. KMP 算法介绍 > **KMP 算法**(全称 **Knuth-Morris-Pratt 算法**):由 Donald Knuth、James H. Morris 和 Vaughan Pratt 三位学者于 1977 年联合提出,并以他们的名字命名。 > > - **KMP 算法核心思想**:在字符串匹配过程中,当文本串 $T$ 的某个字符与模式串 $p$ 发生不匹配时,充分利用已匹配的前缀信息,通过预处理得到的「部分匹配表」(即 next 数组),避免文本指针的回退,从而高效地减少不必要的比较次数,实现快速匹配。 ### 1.1 朴素匹配算法的缺陷 在朴素匹配算法(Brute Force)中,匹配过程使用指针 $i$ 和 $j$ 分别指向文本串 $T$ 和模式串 $p$ 当前比较的字符。当遇到 $T$ 和 $p$ 的字符不匹配时,$j$ 会回到模式串的起始位置,$i$ 则回退到上一次匹配起点的下一个字符,重新开始新一轮匹配,如下图所示。 ![朴素匹配算法](https://qcdn.itcharge.cn/images/20240511154456.png) 也就是说,每当以 $T[i]$ 为起点的匹配失败后,算法会直接尝试从 $T[i + 1]$ 作为新起点继续匹配。实际上,这种做法导致指针 $i$ 可能频繁回退,造成大量重复比较。 那么,有没有一种算法能够让 $i$ 始终向右移动,无需回退,从而提升匹配效率呢? ### 1.2 KMP 算法的改进 KMP 算法的核心在于:每次匹配失败时,能够利用已匹配的信息,跳过那些必然无法匹配的位置,从而显著减少无效的比较次数,实现高效匹配。 具体来说,每次失配时,我们已经知道:**主串的某一段子串等于模式串的某一前缀**。也就是说,如果在下标 $j$ 处失配,说明 $T[i: i + j] == p[0: j]$,即主串从 $i$ 开始的前 $j$ 个字符和模式串的前 $j$ 个字符完全相同。 那么,这一信息如何帮助我们加速匹配呢? 以图中例子为例,假设在第 $5$ 个字符处失配,即 $T[i: i + 5]$ 与 $p[0: 5]$ 完全相同(如 `"ABCAB" == "ABCAB"`),但第 $6$ 个字符不匹配。进一步观察,模式串的前 $5$ 个字符中,前 $2$ 位前缀和后 $2$ 位后缀相同(即 `"AB" == "AB"`)。 因此,我们可以得出:主串子串的后 $2$ 位($T[i + 3: i + 5]$)和模式串的前 $2$ 位($p[0: 2]$)是相同的,这部分已经比较过,无需重复。于是,我们可以直接将主串的 $T[i + 5]$ 与模式串的 $p[2]$ 对齐,继续匹配。这样,主串指针 $i$ 始终向右移动,无需回退,只需调整模式串指针 $j$。 ![KMP 匹配算法移动过程 1](https://qcdn.itcharge.cn/images/20240511155900.png) KMP 算法正是基于这种思想,对模式串 $p$ 进行预处理,构建出一个 **「部分匹配表」**(即 next 数组)。每当失配发生时,主串指针 $i$ 不回退,而是根据 next 数组中 $next[j - 1]$ 的值,直接将模式串指针 $j$ 移动到合适的位置,跳过无效的比较。 例如,上述例子中,模式串在 $j = 5$ 处失配,$next[4] = 2$,因此我们将 $j$ 移动到 $2$,让 $T[i + 5]$ 直接对齐 $p[2]$,继续匹配,无需回退主串指针 $i$。 ### 1.3 next 数组 前文提到的「部分匹配表」又称为「前缀表」,在 KMP 算法中用 $next$ 数组来表示。$next[j]$ 的含义是:**记录子串 $p[0: j + 1]$(包含下标 $j$)中,最长的相等前后缀的长度**。 换句话说,$next[j]$ 就是:**在 $p[0: j + 1]$ 这个子串中,既是前缀又是后缀的最长子串的长度(但不能包含整个子串本身)**。 举例说明,设 $p = "ABCABCD"$,其 $next$ 数组为: - $next[0] = 0$,因为 `"A"` 没有相同的前后缀。 - $next[1] = 0$,因为 `"AB"` 没有相同的前后缀。 - $next[2] = 0$,因为 `"ABC"` 没有相同的前后缀。 - $next[3] = 1$,因为 `"ABCA"` 的前后缀 `"A"` 相同,长度为 $1$。 - $next[4] = 2$,因为 `"ABCAB"` 的前后缀 `"AB"` 相同,长度为 $2$。 - $next[5] = 3$,因为 `"ABCABC"` 的前后缀 `"ABC"` 相同,长度为 $3$。 - $next[6] = 0$,因为 `"ABCABCD"` 没有相同的前后缀。 同理,`"ABCABDEF"` 的前缀表为 $[0, 0, 0, 1, 2, 0, 0, 0]$,`"AABAAAB"` 的前缀表为 $[0, 1, 0, 1, 2, 2, 3]$,`"ABCDABD"` 的前缀表为 $[0, 0, 0, 0, 1, 2, 0]$。 在前面的例子中,当 $p[5]$ 与 $T[i + 5]$ 匹配失败,根据 $next[4] = 2$,我们可以直接将 $T[i + 5]$ 与 $p[2]$ 对齐,继续匹配,如下图所示: ![KMP 匹配算法移动过程 2](https://qcdn.itcharge.cn/images/20240511161310.png) **那么,这样移动的原理是什么?** 实际上,这正是前缀表的作用。具体来说: 假设在第 $j$ 个字符处失配,即 $T[i: i + j] == p[0: j]$,但 $T[i + j] \ne p[j]$。此时,如果 $p[0: k] == p[j - k:j]$,且 $k$ 最大,则 $T[i + j - k: i + j]$ 与 $p[0: k]$ 已经相等,无需重复比较。 因此,我们可以直接将 $T[i + j]$ 与 $p[k]$ 对齐,继续匹配。这里的 $k$ 就是 $next[j - 1]$ 的值。 简而言之,$next$ 数组帮助我们在失配时,快速定位到模式串中下一个可能匹配的位置,从而避免主串指针回退,大幅提升匹配效率。 ## 2. KMP 算法步骤 ### 2.1 next 数组的构造 $next$ 数组的构建其实很直观:它记录了模式串每个前缀(不包含当前位置)中,最长的「相等前后缀」长度。这样一旦失配,我们就能直接跳到下一个可能的匹配位置,避免重复比较。 具体步骤如下: - 假设模式串为 $p$,我们用两个指针:$left$ 表示当前已知的最长相等前后缀的长度,$right$ 表示当前正在处理的字符下标。初始时 $left = 0$,$right = 1$。 - 比较 $p[left]$ 和 $p[right]$: - 如果 $p[left] == p[right]$,说明前后缀可以继续延长。此时 $left$ 加 $1$,将 $next[right]$ 设为 $left$,然后 $right$ 右移一位。这样,$next[right]$ 就记录了当前最长的相等前后缀长度,方便失配时快速跳转。 - 如果 $p[left] \ne p[right]$,说明当前前后缀不相等。此时 $left$ 回退到 $next[left - 1]$,即尝试寻找更短的相等前后缀,直到 $left = 0$ 或再次匹配成功为止。$right$ 不动,继续比较。 - 重复上述过程,直到 $right$ 遍历完整个模式串。 最终,$next[j]$ 就表示子串 $p[0: j+1]$ 的最长相等前后缀的长度。这个数组就是 KMP 算法高效跳转的关键。 ### 2.2 KMP 算法整体流程 1. 先根据模式串 $p$ 构建其前缀表(即 $next$ 数组)。 2. 设置两个指针:$i$ 指向文本串 $T$ 的当前位置,$j$ 指向模式串 $p$ 的当前位置,初始均为 $0$。 3. 遍历文本串 $T$: - 如果 $T[i] == p[j]$,则 $i$ 和 $j$ 同时右移一位,继续比较下一个字符。 - 如果 $T[i] \ne p[j]$ 且 $j > 0$,则将 $j$ 回退到 $next[j - 1]$,即利用前缀表跳过无效匹配,无需回退 $i$。 - 如果 $T[i] \ne p[j]$ 且 $j == 0$,则 $i$ 右移一位,$j$ 保持为 $0$。 4. 当 $j$ 等于模式串长度 $m$ 时,说明已找到完整匹配,返回匹配的起始下标 $i - m + 1$。 5. 如果遍历完整个文本串仍未找到完整匹配,则返回 $-1$。 该流程通过 $next$ 数组高效跳转,避免了主串指针的回退,大幅提升了匹配效率。 ## 3. KMP 算法代码实现 ```python # 生成 next 数组 # next[j] 表示子串 p[0: j+1] 的最长相等前后缀的长度 def generateNext(p: str): m = len(p) next = [0 for _ in range(m)] # 初始化 next 数组,全部为 0 left = 0 # left 表示当前已知的最长相等前后缀的长度 for right in range(1, m): # right 表示当前考察的字符下标 # 如果前后缀不相等,尝试回退 left 到更短的前后缀 while left > 0 and p[left] != p[right]: left = next[left - 1] # 回退到上一个最长相等前后缀 # 如果前后缀相等,最长相等前后缀长度加一 if p[left] == p[right]: left += 1 next[right] = left # 记录当前最长相等前后缀长度 return next # KMP 匹配算法,T 为文本串,p 为模式串 def kmp(T: str, p: str) -> int: """ 返回模式串 p 在文本串 T 中首次出现的位置(下标),如果不存在则返回 -1 """ n, m = len(T), len(p) if m == 0: return 0 # 空模式串视为匹配在开头 next = generateNext(p) # 生成 next 数组 j = 0 # j 为模式串当前匹配到的位置 for i in range(n): # i 为文本串当前匹配到的位置 # 如果当前字符不匹配,且 j > 0,则回退 j 到 next[j-1] while j > 0 and T[i] != p[j]: j = next[j - 1] # 如果当前字符匹配,j 向右移动 if T[i] == p[j]: j += 1 # 如果模式串全部匹配,返回匹配起始下标 if j == m: return i - m + 1 return -1 # 未找到匹配,返回 -1 # 测试用例 print(kmp("abbcfdddbddcaddebc", "ABCABCD")) # 不存在,返回 -1 print(kmp("abbcfdddbddcaddebc", "bcf")) # 返回 2 print(kmp("aaaaa", "bba")) # 不存在,返回 -1 print(kmp("mississippi", "issi")) # 返回 1 print(kmp("ababbbbaaabbbaaa", "bbbb")) # 返回 3 ``` ## 4. KMP 算法分析 | 指标 | 复杂度 | 说明 | | ------------ | -------------- | ------------------------------------------------------------ | | 最好时间复杂度 | $O(n + m)$ | 构造前缀表 $O(m)$,匹配阶段无回退 $O(n)$,总计 $O(n + m)$ | | 最坏时间复杂度 | $O(n + m)$ | 无论文本和模式内容如何,均为 $O(n + m)$ | | 平均时间复杂度 | $O(n + m)$ | 平均情况下同样为 $O(n + m)$ | | 空间复杂度 | $O(m)$ | 仅需存储模式串的前缀表(next 数组) | - 构造前缀表($next$)阶段的时间复杂度为 $O(m)$,其中 $m$ 是模式串 $p$ 的长度。 - 匹配阶段根据前缀表调整位置,文本串指针 $i$ 不回退,时间复杂度为 $O(n)$,其中 $n$ 是文本串 $T$ 的长度。 - 因此整体时间复杂度为 $O(n + m)$,空间复杂度为 $O(m)$。与朴素匹配的 $O(n \times m)$ 相比,有显著提升。 ## 5. 总结 KMP 算法通过预处理模式串的前缀信息,实现文本串指针不回退的高效匹配,是经典的线性时间字符串查找算法。 - **优点**: - 匹配阶段线性时间,文本指针不回退,效率稳定。 - 仅依赖模式串的前缀表,额外空间开销小($O(m)$)。 - **缺点**: - 实现与理解相对复杂,调试成本高于朴素算法。 - 仅适用于精确匹配;包含通配符、编辑距离等需求需用其他算法(如 Aho–Corasick、DP、后缀结构等)。 ## 练习题目 - [0028. 找出字符串中第一个匹配项的下标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-the-index-of-the-first-occurrence-in-a-string.md) - [0459. 重复的子字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/repeated-substring-pattern.md) - [0686. 重复叠加字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/repeated-string-match.md) - [0796. 旋转字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotate-string.md) - [1408. 数组中的字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/string-matching-in-an-array.md) - [2156. 查找给定哈希值的子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/find-substring-with-given-hash-value.md) - [单模式串匹配题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8D%95%E6%A8%A1%E5%BC%8F%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】柔性字符串匹配 - 中科院计算所网络信息安全研究组 译 - 【书籍】ACM-ICPC 程序设计系列 - 算法设计与实现 - 陈宇 吴昊 主编 - 【博文】[从头到尾彻底理解 KMP - 结构之法 算法之道 - CSDN博客](https://blog.csdn.net/v_JULY_v/article/details/7041827?spm=1001.2014.3001.5502) - 【博文】[字符串匹配的 KMP 算法 - 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2013/05/Knuth–Morris–Pratt_algorithm.html) - 【题解】[多图预警 - 详解 KMP 算法 - 实现 strStr() - 力扣](https://leetcode.cn/problems/implement-strstr/solution/duo-tu-yu-jing-xiang-jie-kmp-suan-fa-by-w3c9c/) - 【题解】[「代码随想录」KMP算法详解 - 实现 strStr() - 力扣](https://leetcode.cn/problems/implement-strstr/solution/dai-ma-sui-xiang-lu-kmpsuan-fa-xiang-jie-mfbs/) ================================================ FILE: docs/04_string/04_05_string_boyer_moore.md ================================================ ## 1. Boyer Moore 算法介绍 > **Boyer Moore 算法(BM 算法)**:由 Robert S. Boyer 和 J Strother Moore 于 1977 年提出,是一种高效的字符串搜索算法,实际应用中通常比 KMP 算法快 3~5 倍。 > > - **BM 算法核心思想**:先对模式串 $p$ 预处理,生成辅助表。在匹配过程中,如果文本串 $T$ 某字符与模式串 $p$ 不匹配,通过启发式规则,直接跳过不可能匹配的位置,将模式串整体向后滑动多位。 BM 算法的关键在于两种启发式移动规则:**坏字符规则(Bad Character Rule)** 和 **好后缀规则(Good Suffix Rule)**。 这两种规则的计算只依赖于模式串 $p$,与文本串 $T$ 无关。预处理时分别生成对应的后移表,匹配时每次取两者中较大的后移位数进行滑动。 需要注意,BM 算法滑动模式串时仍是从左到右,但每次字符比较是从右到左(即从后缀开始)。 下面将详细介绍 BM 算法的两种启发式规则:「坏字符规则」和「好后缀规则」。 ## 2. Boyer Moore 算法启发规则 ### 2.1 坏字符规则 > **坏字符规则(Bad Character Rule)**:当文本串 $T$ 和模式串 $p$ 从右往左比较时,如果遇到第一个不匹配的字符(称为 **坏字符**),可以利用该字符快速决定模式串的滑动距离。 移动位数分两种情况: - **情况 1:坏字符在模式串 $p$ 中出现过** - 将模式串中最后一次出现该坏字符的位置与文本串中的坏字符对齐。 - **移动位数 = 坏字符在模式串的失配位置 - 坏字符在模式串中最后一次出现的位置** ![情况 1:坏字符出现在模式串 p 中](https://qcdn.itcharge.cn/images/20240511164026.png) - **情况 2:坏字符未在模式串 $p$ 中出现** - 直接将模式串整体向右移动一位。 - **移动位数 = 坏字符在模式串的失配位置 + 1** ![情况 2:坏字符没有出现在模式串 p 中](https://qcdn.itcharge.cn/images/20240511164048.png) ### 2.2 好后缀规则 > **好后缀规则(Good Suffix Rule)**:当从右往左比较时,遇到不匹配,已匹配的部分称为 **好后缀**。此时可以利用好后缀信息,让模式串整体向右跳跃移动,加快匹配。 好后缀规则分为三种情况: - **情况 1:模式串中存在与好后缀相同的子串** - 直接将该子串与好后缀对齐(如果有多个,选最右侧的)。 - **移动位数 = 好后缀最后一个字符在模式串中的位置 - 匹配子串最后一个字符的位置** ![情况 1:模式串中有子串匹配上好后缀](https://qcdn.itcharge.cn/images/20240511164101.png) - **情况 2:模式串中没有子串匹配好后缀,但有前缀等于好后缀的后缀** - 找到最长的前缀与好后缀的后缀相等,将其对齐。 - **移动位数 = 好后缀后缀最后一个字符在模式串中的位置 - 最长前缀最后一个字符的位置** ![情况 2:模式串中无子串匹配上好后缀, 但有最长前缀匹配好后缀的后缀](https://qcdn.itcharge.cn/images/20240511164112.png) - **情况 3:既无子串匹配好后缀,也无前缀匹配** - 直接将模式串整体右移一整段。 - **移动位数 = 模式串长度** ![情况 3:模式串中无子串匹配上好后缀,也找不到前缀匹配](https://qcdn.itcharge.cn/images/20240511164124.png) ## 3. Boyer Moore 算法匹配过程示例 下面我们以 J Strother Moore 教授的经典例子,详细演示 BM 算法的匹配流程,帮助大家更直观地理解 **「坏字符规则」** 和 **「好后缀规则」** 的实际应用。 ::: tabs#Boyer-Moore @tab <1> 假设文本串为 `"HERE IS A SIMPLE EXAMPLE"`,模式串为 `"EXAMPLE"`,如下图所示。 ![Boyer Moore 算法步骤 1](https://qcdn.itcharge.cn/images/20220127164130.png) @tab <2> 首先,将模式串与文本串的起始位置对齐,从模式串的末尾开始逐个字符向前比较。 ![Boyer Moore 算法步骤 2](https://qcdn.itcharge.cn/images/20220127164140.png) 此时,`'S'` 与 `'E'` 不匹配。`'S'` 就是「坏字符(Bad Character)」,位于模式串的第 $6$ 位。由于 `'S'` 在模式串 `"EXAMPLE"` 中未出现(即最后一次出现的位置为 $-1$),根据坏字符规则,模式串可以直接向右移动 $6 - (-1) = 7$ 位,使得模式串的首字符与文本串中 `'S'` 的下一位对齐。 @tab <3> 将模式串向右移动 $7$ 位后,再次从模式串尾部开始比较,发现 `'P'` 与 `'E'` 不匹配,此时 `'P'` 是坏字符。 ![Boyer Moore 算法步骤 3](https://qcdn.itcharge.cn/images/20220127164151.png) 此时,`'P'` 在模式串中的失配位置为第 $6$ 位,且在模式串中最后一次出现的位置为 $4$(下标从 $0$ 开始)。 @tab <4> 根据坏字符规则,模式串向右移动 $6 - 4 = 2$ 位,使文本串中的 `'P'` 与模式串中的 `'P'` 对齐。 ![Boyer Moore 算法步骤 4](https://qcdn.itcharge.cn/images/20220127164202.png) @tab <5> 继续从模式串尾部逐位比较。首先比较文本串的 `'E'` 和模式串的 `'E'`,二者匹配,此时 `"E"` 为好后缀,位于模式串的第 $6$ 位。 ![Boyer Moore 算法步骤 5](https://qcdn.itcharge.cn/images/20220127164212.png) @tab <6> 继续比较前一位,文本串的 `'L'` 与模式串的 `'L'` 匹配,此时 `"LE"` 为好后缀,位于模式串的第 $6$ 位。 ![Boyer Moore 算法步骤 6](https://qcdn.itcharge.cn/images/20220127164222.png) @tab <7> 继续比较前一位,文本串的 `'P'` 与模式串的 `'P'` 匹配,此时 `"PLE"` 为好后缀,位于模式串的第 $6$ 位。 ![Boyer Moore 算法步骤 7](https://qcdn.itcharge.cn/images/20220127164232.png) @tab <8> 继续比较前一位,文本串的 `'M'` 与模式串的 `'M'` 匹配,此时 `"MPLE"` 为好后缀,位于模式串的第 $6$ 位。 ![Boyer Moore 算法步骤 8](https://qcdn.itcharge.cn/images/20220127164241.png) @tab <9> 继续比较前一位,文本串的 `'I'` 与模式串的 `'A'` 不匹配。 ![Boyer Moore 算法步骤 9-1](https://qcdn.itcharge.cn/images/20220127164251.png) 此时,如果仅用坏字符规则,模式串应向右移动 $2 - (-1) = 3$ 位。但根据好后缀规则,可以获得更优的移动距离。 对于好后缀 `"MPLE"`,其后缀 `"PLE"`、`"LE"`、`"E"` 中,只有 `"E"` 与模式串前缀 `"E"` 匹配,属于好后缀规则的第二种情况。好后缀 `"E"` 的最后一个字符在模式串中的位置为 $6$,最长前缀 `"E"` 的最后一个字符在位置 $0$,因此模式串可以直接向右移动 $6 - 0 = 6$ 位。 ![Boyer Moore 算法步骤 9-2](https://qcdn.itcharge.cn/images/20220127164301.png) @tab <10> 再次从模式串尾部开始逐位比较。 此时,`'P'` 与 `'E'` 不匹配,`'P'` 是坏字符。根据坏字符规则,模式串向右移动 $6 - 4 = 2$ 位。 ![Boyer Moore 算法步骤 10](https://qcdn.itcharge.cn/images/20220127164312.png) @tab <11> 继续从模式串尾部逐位比较,发现模式串全部匹配,搜索结束,返回模式串在文本串中的起始位置。 ::: ## 4. Boyer Moore 算法步骤 BM 算法的整体流程如下: 1. 计算文本串 $T$ 的长度 $n$ 和模式串 $p$ 的长度 $m$。 2. 对模式串 $p$ 进行预处理,分别生成坏字符表 $bc\_table$ 和好后缀规则后移位数表 $gs\_table$。 3. 将模式串 $p$ 的头部与文本串 $T$ 的当前位置 $i$ 对齐,初始 $i = 0$。每次从模式串的末尾($j = m - 1$)开始向前逐位比较: - 如果 $T[i + j]$ 与 $p[j]$ 相等,则继续向前比较下一个字符。 - 如果模式串所有字符均匹配,则返回当前匹配的起始位置 $i$。 - 如果 $T[i + j]$ 与 $p[j]$ 不相等: - 分别根据坏字符表和好后缀表,计算坏字符移动距离 $bad\_move$ 和好后缀移动距离 $good\_move$。 - 取两者的最大值作为本轮的实际移动距离,即 $i += \max(bad\_move,\, good\_move)$,然后继续下一轮匹配。 4. 如果模式串移动到文本串末尾仍未找到匹配,则返回 $-1$。 该流程充分利用了坏字符和好后缀两种规则,实现了高效的字符串匹配。 ## 5. Boyer Moore 算法代码实现 BM 算法的匹配过程本身实现相对简单,真正的难点主要集中在预处理阶段,尤其是「坏字符位置表」和「好后缀规则后移位数表」的构建。其中,「好后缀规则后移位数表」的实现尤为复杂。接下来我们将分别详细讲解这两部分的实现方法。 ### 5.1 生成坏字符位置表代码实现 坏字符位置表的构建非常直观,具体步骤如下: - 创建一个哈希表 $bc\_table$,用于记录每个字符在模式串中最后一次出现的位置,即 $bc\_table[bad\_char]$ 表示坏字符 $bad\_char$ 在模式串中的最右下标。 - 遍历模式串 $p$,将每个字符 $p[i]$ 及其下标 $i$ 存入哈希表。如果某字符在模式串中多次出现,则后出现的下标会覆盖前面的值,确保记录的是最右侧的位置。 在 BM 算法匹配过程中,如果 $bad\_char$ 不在 $bc\_table$ 中,则视为其最右位置为 $-1$;如果存在,则直接取 $bc\_table[bad\_char]$。据此即可计算模式串本轮应向右移动的距离。 坏字符位置表的实现代码如下: ```python # 生成坏字符位置表 # bc_table[bad_char] 表示坏字符 bad_char 在模式串中最后一次出现的位置 def generateBadCharTable(p: str): """ 构建坏字符位置表。 输入: p: 模式串 输出: bc_table: 字典,key 为字符,value 为该字符在模式串中最后一次出现的下标 """ bc_table = dict() # 初始化坏字符表 # 遍历模式串,将每个字符及其下标记录到表中 for i, ch in enumerate(p): bc_table[ch] = i # 如果字符多次出现,保留最后一次出现的位置 # 返回坏字符表 return bc_table ``` ### 5.2 生成好后缀规则后移位数表代码实现 为了生成好后缀规则的后移位数表,首先需要构建一个后缀数组 $suffix$。$suffix[i]$ 表示以 $i$ 结尾的子串(即 $p[0:i+1]$)与模式串后缀的最大匹配长度,即最大的 $k$ 使得 $p[i-k+1:i+1] == p[m-k:m]$。 下面是 $suffix$ 数组的构建代码: ```python # 生成 suffix 数组 # suffix[i] 表示以 i 结尾的子串(p[0:i+1])与模式串后缀的最大匹配长度 def generateSuffixArray(p: str): """ 构建 suffix 数组。 输入: p: 模式串 输出: suffix: 列表,suffix[i] 表示以 i 结尾的子串与模式串后缀的最大匹配长度 """ m = len(p) suffix = [0 for _ in range(m)] # 初始化为 0,表示尚未匹配 suffix[m - 1] = m # 最后一个字符的后缀必然和自身完全匹配,长度为 m # 从倒数第二个字符开始向前遍历 for i in range(m - 2, -1, -1): j = i # j 指向当前子串的起始位置 # 比较 p[j] 与 p[m-1-(i-j)],即从后缀和子串末尾同时向前比较 while j >= 0 and p[j] == p[m - 1 - (i - j)]: j -= 1 # 以 i 结尾的子串与模式串后缀的最大匹配长度为 i - j suffix[i] = i - j return suffix ``` 有了 $suffix$ 数组后,我们可以基于它构建好后缀规则的后移位数表 $gs\_list$。该表用一个数组表示,其中 $gs\_list[j]$ 表示在模式串第 $j$ 位遇到坏字符时,根据好后缀规则可以向右移动的距离。 根据「2.2 好后缀规则」的分析,好后缀的移动分为三种情况: - 情况 1:模式串中存在与好后缀完全相同的子串。 - 情况 2:模式串中不存在匹配好后缀的子串,但存在前缀与好后缀的后缀相等。 - 情况 3:既无匹配子串,也无匹配前缀。 实际上,情况 2 和情况 3 可以合并处理(情况 3 可视为最长前缀长度为 $0$ 的特殊情况)。当某个坏字符同时满足多种情况时,应优先选择移动距离最小的方案,以避免遗漏可能的匹配。例如,如果既有匹配子串又有匹配前缀,应优先采用匹配子串的移动方式。 具体构建 $gs\_list$ 的步骤如下: - 首先,假设所有位置均为情况 3,即 $gs\_list[i] = m$。 - 然后,利用后缀和前缀的匹配关系,更新情况 2 下的移动距离:$gs\_list[j] = m - 1 - i$,其中 $j$ 是好后缀前的坏字符位置,$i$ 是最长前缀的末尾下标,$m - 1 - i$ 为可移动的距离。 - 最后,处理情况 1:对于好后缀的左端点($m - 1 - suffix[i]$ 处)遇到坏字符时,更新其可移动距离为 $gs\_list[m - 1 - suffix[i]] = m - 1 - i$。 下面是生成好后缀规则后移位数表 $gs\_list$ 的代码: ```python # 生成好后缀规则后移位数表 # gs_list[j] 表示在模式串下标 j 处遇到坏字符时,根据好后缀规则可以向右移动的距离 def generateGoodSuffixList(p: str): """ 构建好后缀规则的后移位数表 gs_list。 输入: p: 模式串 输出: gs_list: 列表,gs_list[j] 表示在 j 处遇到坏字符时可向右移动的距离 """ m = len(p) gs_list = [m for _ in range(m)] # 情况3:默认全部初始化为 m,表示完全不匹配时的最大移动 suffix = generateSuffixArray(p) # 生成后缀数组 # 处理情况 2:寻找最长的前缀与好后缀的后缀相等 # j 表示好后缀前的坏字符位置 j = 0 # 从后往前遍历,i 表示前缀的结尾下标 for i in range(m - 1, -1, -1): # 如果 suffix[i] == i + 1,说明 p[0: i+1] == p[m-1-i: m],即前缀和后缀相等 if suffix[i] == i + 1: # 对于所有 j < m-1-i 的位置,如果还未被更新,则设置为 m-1-i while j < m - 1 - i: if gs_list[j] == m: gs_list[j] = m - 1 - i # 更新移动距离 j += 1 # 处理情况 1:模式串中存在与好后缀完全相同的子串 # i 表示好后缀的右端点 for i in range(m - 1): # m-1-suffix[i] 是好后缀的左端点 # m-1-i 是可移动的距离 gs_list[m - 1 - suffix[i]] = m - 1 - i # 更新在好后缀左端点遇到坏字符时的移动距离 return gs_list ``` ### 5.3 Boyer Moore 算法整体代码实现 ```python # Boyer-Moore 字符串匹配算法实现 def boyerMoore(T: str, p: str) -> int: """ Boyer-Moore 算法主函数,返回模式串 p 在文本串 T 中首次出现的位置,如果无则返回 -1。 """ n, m = len(T), len(p) if m == 0: return 0 if n == 0 else -1 # 约定空模式串匹配空文本串返回 0,否则 -1 if n < m: return -1 bc_table = generateBadCharTable(p) # 生成坏字符表 gs_list = generateGoodSuffixList(p) # 生成好后缀表 i = 0 while i <= n - m: j = m - 1 # 从模式串末尾向前比较 while j >= 0 and T[i + j] == p[j]: j -= 1 if j < 0: return i # 匹配成功,返回起始下标 # 坏字符规则:j - bc_table.get(T[i + j], -1) bad_move = j - bc_table.get(T[i + j], -1) # 好后缀规则:gs_list[j] good_move = gs_list[j] # 取两者最大值进行滑动 i += max(bad_move, good_move) return -1 def generateBadCharTable(p: str): """ 生成坏字符表:记录每个字符在模式串中最后一次出现的位置。 """ bc_table = dict() for i, ch in enumerate(p): bc_table[ch] = i # 只保留最后一次出现的位置 return bc_table def generateGoodSuffixList(p: str): """ 生成好后缀规则的后移位数表 gs_list。 gs_list[j] 表示在模式串下标 j 处遇到坏字符时,根据好后缀规则可以向右移动的距离。 """ m = len(p) gs_list = [m for _ in range(m)] # 默认全部为情况 3:最大移动 m suffix = generateSuffixArray(p) # 生成后缀数组 # 处理情况 2:寻找最长的前缀与好后缀的后缀相等 j = 0 for i in range(m - 1, -1, -1): if suffix[i] == i + 1: while j < m - 1 - i: if gs_list[j] == m: gs_list[j] = m - 1 - i # 更新移动距离 j += 1 # 处理情况 1:模式串中存在与好后缀完全相同的子串 for i in range(m - 1): # m-1-suffix[i] 是好后缀的左端点 gs_list[m - 1 - suffix[i]] = m - 1 - i return gs_list def generateSuffixArray(p: str): """ 生成后缀数组 suffix。 suffix[i] 表示以 i 结尾的子串与模式串后缀的最大匹配长度。 """ m = len(p) suffix = [m for _ in range(m)] # 初始化为 0,表示尚未匹配 suffix[m - 1] = m # 最后一个字符的后缀长度为 m for i in range(m - 2, -1, -1): j = i # 从 i 向前与模式串后缀比较 while j >= 0 and p[j] == p[m - 1 - i + j]: j -= 1 suffix[i] = i - j return suffix # 测试用例 print(boyerMoore("abbcfdddbddcaddebc", "aaaaa")) # -1 print(boyerMoore("", "")) # 0 print(boyerMoore("HERE IS A SIMPLE EXAMPLE", "EXAMPLE")) # 17 print(boyerMoore("abcabcabcabc", "abcabc")) # 0 ``` ## 6. Boyer Moore 算法分析 | 指标 | 复杂度 | 说明 | | ------------ | -------------- | ------------------------------------------------------------ | | 最好时间复杂度 | $O(n / m)$ | 每次匹配时,模式串 $p$ 中不存在与文本串 $T$ 中第一个匹配的字符,滑动距离最大,比较次数最少。| | 最坏时间复杂度 | $O(m \times n)$| 文本串 $T$ 中有大量重复字符,且模式串 $p$ 由 $m-1$ 个相同字符和一个不同字符组成,导致每次只能滑动一位。| | 平均时间复杂度 | 介于 $O(n / m)$ 与 $O(m \times n)$ 之间 | 实际应用中通常远优于最坏情况,接近最好情况。| | 预处理时间复杂度 | $O(m + \sigma)$ | 生成坏字符表和好后缀表,$\sigma$ 为字符集大小。| | 空间复杂度 | $O(m + \sigma)$ | 需存储坏字符表($\sigma$)和好后缀表($m$)。| - 其中 $n$ 为文本串长度,$m$ 为模式串长度,$\sigma$ 为字符集大小。 - 当模式串 $p$ 是非周期性的,在最坏情况下,BM 算法最多需要进行 $3n$ 次字符比较操作。 ## 7. 总结 Boyer-Moore(BM)算法通过「坏字符规则」和「好后缀规则」两种启发式策略,实现模式串的高效跳跃移动,是实际应用中性能最优的单模式串匹配算法之一。 - **优点**: - **实际性能优异**:在大多数实际应用中,BM 算法通常比 KMP 算法快 3~5 倍 - **跳跃能力强**:通过坏字符和好后缀规则,能够跳过大量不可能匹配的位置 - **从右到左比较**:充分利用模式串信息,减少不必要的字符比较 - **启发式策略**:两种规则互补,最大化跳跃距离 - **缺点**: - **实现复杂**:特别是好后缀规则的预处理部分,理解和实现难度较高 - **最坏情况退化**:在特定输入下可能退化到 $O(m \times n)$ 复杂度 - **空间开销**:需要存储坏字符表和好后缀表,空间复杂度为 $O(m + \sigma)$ - **预处理开销**:需要预先构建两个辅助表,不适合单次匹配场景 ## 练习题目 - [0028. 找出字符串中第一个匹配项的下标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-the-index-of-the-first-occurrence-in-a-string.md) - [0459. 重复的子字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/repeated-substring-pattern.md) - [0686. 重复叠加字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/repeated-string-match.md) - [0796. 旋转字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotate-string.md) - [1408. 数组中的字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/string-matching-in-an-array.md) - [2156. 查找给定哈希值的子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/find-substring-with-given-hash-value.md) - [单模式串匹配题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8D%95%E6%A8%A1%E5%BC%8F%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】柔性字符串匹配 - 中科院计算所网络信息安全研究组 译 - 【文章】[不用找了,学习 BM 算法,这篇就够了(思路+详注代码)- BoCong-Deng 的博客](https://blog.csdn.net/DBC_121/article/details/105569440) - 【文章】[字符串匹配的 Boyer-Moore 算法 - 阮一峰的网络日志](https://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html) - 【文章】[ bm 算法好后缀 java 实现 - 长笛小号的博客 - CSDN博客](https://blog.csdn.net/weixin_29217235/article/details/114488027) - 【文章】[BM算法详解 - 简单爱_wxg - 博客园](https://www.cnblogs.com/wxgblogs/p/5701101.html) - 【文章】[grep 之字符串搜索算法 Boyer-Moore 由浅入深 - Alexia(minmin) - 博客园](https://www.cnblogs.com/lanxuezaipiao/p/3452579.html) - 【文章】[字符串匹配基础(中)- 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/71525) - 【代码】[BM算法 附有解释 - 实现 strStr() - 力扣](https://leetcode.cn/problems/implement-strstr/solution/bmsuan-fa-fu-you-jie-shi-by-wen-198/) ================================================ FILE: docs/04_string/04_06_string_horspool.md ================================================ ## 1. Horspool 算法介绍 > **Horspool 算法**:由 Nigel Horspool 教授于 1980 年提出,是对 Boyer Moore 算法的简化版,用于在字符串中查找子串。 > > - **Horspool 算法核心思想**:先对模式串 $p$ 预处理,生成移动表。匹配时,从模式串末尾开始比较,遇到不匹配时,根据移动表跳过尽可能多的位置,加快查找速度。 Horspool 算法本质上继承了 Boyer-Moore 的思想,但只保留了「坏字符规则」并加以简化。当文本串 $T$ 某字符与模式串 $p$ 不匹配时,模式串可以根据以下两种情况快速右移: - **情况 1:$T[i + m - 1]$(文本串当前窗口的最后一个字符)在模式串 $p$ 中出现过** - 将该字符在模式串中最后一次出现的位置与模式串末尾对齐。 - **右移位数 = 模式串长度 - 1 - 该字符在模式串中最后一次出现的位置** ![Horspool 算法情况 1](https://qcdn.itcharge.cn/images/20240511165106.png) - **情况 2:$T[i + m - 1]$ 没有在模式串 $p$ 中出现** - 直接将模式串整体右移一整个长度。 - **右移位数 = 模式串长度** ![Horspool 算法情况 2](https://qcdn.itcharge.cn/images/20240511165122.png) ## 2. Horspool 算法步骤 Horspool 算法流程如下: 1. 设文本串 $T$ 长度为 $n$,模式串 $p$ 长度为 $m$。 2. 预处理模式串 $p$,生成后移位数表 $bc\_table$。 3. 从文本串起始位置 $i = 0$ 开始,将模式串与文本串对齐,比较方式如下: - 从模式串末尾 $j = m - 1$ 开始,依次向前比较 $T[i + j]$ 与 $p[j]$。 - 如果全部字符匹配,返回 $i$,即匹配起始位置。 - 如果遇到不匹配,查找 $T[i + m - 1]$ 在 $bc\_table$ 中的值,右移相应距离(如果未出现则右移 $m$)。 4. 如果遍历完文本串仍未找到匹配,返回 $-1$。 ## 3. Horspool 算法代码实现 ### 3.1 后移位数表代码实现 后移位数表的生成非常简单,类似于 Boyer-Moore 算法的坏字符表: - 用一个哈希表 $bc\_table$,记录每个字符在模式串中可向右移动的距离。 - 遍历模式串 $p$,对每个字符 $p[i]$,将 $m - 1 - i$ 作为其移动距离存入表中。如果字符重复,保留最右侧的距离。 匹配时,如果 $T[i + m - 1]$ 不在表中,则右移 $m$;如果在表中,则右移 $bc\_table[T[i + m - 1]]$。 后移位数表代码如下: ```python # 生成后移位数表 # bc_table[bad_char] 表示遇到坏字符时可以向右移动的距离 def generateBadCharTable(p: str): """ 构建 Horspool 算法的后移位数表。 输入: p: 模式串 输出: bc_table: 字典,key 为字符,value 为遇到该字符时可向右移动的距离 """ m = len(p) bc_table = dict() # 只处理模式串的前 m - 1 个字符(最后一个字符不需要处理) for i in range(m - 1): # i 从 0 到 m - 2 # 对于每个字符 p[i],记录其对应的移动距离 # 移动距离 = 模式串长度 - 1 - 当前字符下标 bc_table[p[i]] = m - 1 - i # 如果字符重复出现,保留最右侧(下标最大)的距离 return bc_table ``` ### 3.2 Horspool 算法整体代码实现 ```python # Horspool 算法实现,T 为文本串,p 为模式串 def horspool(T: str, p: str) -> int: """ Horspool 字符串匹配算法。 返回模式串 p 在文本串 T 中首次出现的位置,如果无则返回 -1。 """ n, m = len(T), len(p) if m == 0: return 0 if n == 0 else -1 # 约定:空模式串匹配空文本串返回 0,否则返回 -1 if n < m: return -1 # 模式串比文本串长,必不匹配 bc_table = generateBadCharTable(p) # 生成后移位数表 i = 0 while i <= n - m: j = m - 1 # 从模式串末尾向前逐位比较 while j >= 0 and T[i + j] == p[j]: j -= 1 if j < 0: return i # 匹配成功,返回起始下标 # 取文本串当前窗口最右字符,查表决定滑动距离 shift_char = T[i + m - 1] shift = bc_table.get(shift_char, m) # 如果未出现则右移 m 位 i += shift return -1 # 匹配失败,未找到 # 生成 Horspool 算法的后移位数表 # bc_table[bad_char] 表示遇到坏字符 bad_char 时可以向右移动的距离 def generateBadCharTable(p: str): """ 构建 Horspool 算法的后移位数表。 输入: p: 模式串 输出: bc_table: 字典,key 为字符,value 为遇到该字符时可向右移动的距离 """ m = len(p) bc_table = dict() # 只处理模式串的前 m - 1 个字符(最后一个字符不处理) for i in range(m - 1): # i 从 0 到 m - 2 # 对于每个字符 p[i],记录其对应的移动距离 # 移动距离 = 模式串长度 - 1 - 当前字符下标 bc_table[p[i]] = m - 1 - i # 如果字符重复出现,保留最右侧(下标最大)的距离 return bc_table # 测试用例 print(horspool("abbcfdddbddcaddebc", "aaaaa")) # -1,未匹配 print(horspool("abbcfdddbddcaddebc", "bcf")) # 2,匹配成功 ``` ## 4. Horspool 算法分析 | 指标 | 复杂度 | 说明 | | ------------ | ---------------- | ------------------------------------------------------------ | | 最好时间复杂度 | $O(n)$ | 模式串字符分布均匀,坏字符表能实现最大跳跃,比较次数最少。 | | 最坏时间复杂度 | $O(n \times m)$ | 模式串字符高度重复且与文本不匹配时,每次只能滑动一位。 | | 平均时间复杂度 | $O(n)$ | 实际应用中通常接近最好情况,比较次数较少。 | | 空间复杂度 | $O(m + \sigma)$ | 主要用于存储坏字符表,$m$ 为模式串长度,$\sigma$ 为字符集大小。 | - $n$ 为文本串长度,$m$ 为模式串长度,$\sigma$ 为字符集大小。 - Horspool 算法在大多数实际场景下效率较高,但极端情况下可能退化为 $O(n \times m)$。 - 空间消耗主要体现在坏字符表的构建上。 ## 4. 总结 Horspool 算法是一种基于坏字符规则的高效字符串匹配算法,通过预处理模式串构建坏字符表,实现快速跳跃以提升匹配效率,适用于大多数实际场景。 - **优点**: - 实现简单,代码量少,易于理解。 - 平均性能优良,适合大多数实际应用场景。 - 只需构建坏字符表,预处理开销小。 - **缺点**: - 最坏情况下时间复杂度较高,可能退化为 $O(n \times m)$。 - 只利用坏字符规则,跳跃能力不如 BM 算法。 - 不适合极端重复或特殊构造的模式串。 ## 练习题目 - [0028. 找出字符串中第一个匹配项的下标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-the-index-of-the-first-occurrence-in-a-string.md) - [0459. 重复的子字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/repeated-substring-pattern.md) - [0686. 重复叠加字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/repeated-string-match.md) - [0796. 旋转字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotate-string.md) - [1408. 数组中的字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/string-matching-in-an-array.md) - [2156. 查找给定哈希值的子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/find-substring-with-given-hash-value.md) - [单模式串匹配题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8D%95%E6%A8%A1%E5%BC%8F%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】柔性字符串匹配 - 中科院计算所网络信息安全研究组 译 - 【博文】[字符串模式匹配算法:BM、Horspool、Sunday、KMP、KR、AC算法 - schips - 博客园](https://www.cnblogs.com/schips/p/11098041.html) ================================================ FILE: docs/04_string/04_07_string_sunday.md ================================================ ## 1. Sunday 算法介绍 > **Sunday 算法**:Sunday 算法是一种高效的字符串查找算法,由 Daniel M. Sunday 于 1990 年提出,专门用于在主串中查找子串的位置。 > > - **核心思想**:对于给定的文本串 $T$ 和模式串 $p$,Sunday 算法首先对模式串 $p$ 进行预处理,生成一个「后移位数表」。在匹配过程中,每当发现不匹配时,算法会根据文本串中参与本轮匹配的末尾字符的「下一个字符」,决定模式串应向右滑动的距离,从而尽可能跳过无效的比较,加快匹配速度。 Sunday 算法的思想与 Boyer-Moore 算法类似,但 Sunday 算法始终从左到右进行匹配。当匹配失败时,Sunday 算法关注的是文本串 $T$ 当前匹配窗口末尾的下一个字符 $T[i + m]$,并据此决定模式串的滑动距离,实现快速跳跃。 具体来说,遇到不匹配时有两种情况: - **情况 1:$T[i + m]$ 出现在模式串 $p$ 中** - 此时,将模式串 $p$ 向右移动,使其最后一次出现 $T[i + m]$ 的位置与 $T[i + m]$ 对齐。 - **向右移动的位数 = 模式串中 $T[i + m]$ 最右侧出现的位置到末尾的距离** - 说明:$T[i + m]$ 即为当前匹配窗口末尾的下一个字符。 ![Sunday 算法情况 1](https://qcdn.itcharge.cn/images/20240511165526.png) - **情况 2:$T[i + m]$ 未出现在模式串 $p$ 中** - 此时,直接将模式串整体向右移动 $m + 1$ 位。 - **向右移动的位数 = 模式串长度 $m + 1$** ![Sunday 算法情况 2](https://qcdn.itcharge.cn/images/20240511165540.png) ## 2. Sunday 算法步骤 Sunday 算法的具体流程如下: - 设文本串 $T$ 长度为 $n$,模式串 $p$ 长度为 $m$。 - 首先对模式串 $p$ 进行预处理,生成后移位数表 $bc\_table$。 - 令 $i = 0$,表示当前模式串 $p$ 的起始位置与文本串 $T$ 的第 $i$ 位对齐。 - 在每一轮匹配中,从头开始比较 $T[i + j]$ 与 $p[j]$($j$ 从 $0$ 到 $m-1$): - 如果所有字符均匹配,则返回当前匹配的起始位置 $i$。 - 如果出现不匹配,或未全部匹配完毕,则检查 $T[i + m]$(即当前匹配窗口末尾的下一个字符): - 如果 $T[i + m]$ 存在于后移位数表中,则将 $i$ 增加 $bc\_table[T[i + m]]$,即将模式串向右滑动相应距离。 - 如果 $T[i + m]$ 不存在于后移位数表中,则将 $i$ 增加 $m + 1$,即整体右移 $m + 1$ 位。 - 如果遍历完整个文本串仍未找到匹配,则返回 $-1$。 ## 3. Sunday 算法代码实现 ### 3.1 后移位数表代码实现 后移位数表的实现非常简洁,与 Horspool 算法类似。具体思路如下: - 使用一个哈希表 $bc\_table$,其中 $bc\_table[bad\_char]$ 表示遇到该字符时,模式串可以向右移动的距离。 - 遍历模式串 $p$,将每个字符 $p[i]$ 作为键,其对应的移动距离 $m - i$ 作为值存入字典。如果字符重复出现,则以最右侧(下标最大的)位置为准,覆盖之前的值。这样,哈希表中存储的就是每个字符在模式串中最右侧出现时可向右移动的距离。 在 Sunday 算法匹配过程中,如果 $T[i + m]$ 不在 $bc\_table$ 中,则默认移动 $m + 1$ 位,即将模式串整体右移到当前匹配窗口末尾的下一个字符之后。如果 $T[i + m]$ 存在于表中,则移动距离为 $bc\_table[T[i + m]]$。这样即可高效计算每次滑动的步长。 后移位数表的代码如下: ```python # 生成 Sunday 算法的后移位数表 # bc_table[bad_char] 表示遇到坏字符 bad_char 时,模式串可以向右移动的距离 def generateBadCharTable(p: str): """ 构建 Sunday 算法的后移位数表。 输入: p: 模式串 输出: bc_table: 字典,key 为字符,value 为遇到该字符时可向右移动的距离 """ m = len(p) bc_table = dict() # 遍历模式串的每一个字符(包括最后一个字符) for i in range(m): # 对于每个字符 p[i],记录其对应的移动距离 # 移动距离 = 模式串长度 - 当前字符下标 bc_table[p[i]] = m - i # 如果字符重复出现,保留最右侧(下标最大)的距离 return bc_table ``` ### 3.2 Sunday 算法整体代码实现 ```python # Sunday 算法实现,T 为文本串,p 为模式串 def sunday(T: str, p: str) -> int: """ Sunday 算法主函数,返回模式串 p 在文本串 T 中首次出现的位置,如果未匹配则返回 -1。 参数: T: 文本串 p: 模式串 返回: int: 第一个匹配位置的下标,未匹配返回 -1 """ n, m = len(T), len(p) if m == 0: return 0 # 空模式串视为匹配在开头 bc_table = generateBadCharTable(p) # 生成后移位数表 i = 0 # i 表示当前窗口在文本串中的起始下标 while i <= n - m: # 逐字符比较当前窗口是否与模式串完全匹配 j = 0 while j < m and T[i + j] == p[j]: j += 1 if j == m: return i # 匹配成功,返回起始下标 # 检查窗口末尾的下一个字符,决定滑动距离 if i + m >= n: return -1 # 已到文本串末尾,未匹配 next_char = T[i + m] # 当前窗口末尾的下一个字符 # 如果 next_char 在后移位数表中,滑动对应距离,否则滑动 m+1 shift = bc_table.get(next_char, m + 1) i += shift return -1 # 未找到匹配 # 生成 Sunday 算法的后移位数表 # bc_table[bad_char] 表示遇到坏字符 bad_char 时,模式串可以向右移动的距离 def generateBadCharTable(p: str): """ 构建 Sunday 算法的后移位数表。 参数: p: 模式串 返回: dict: 字典,key 为字符,value 为遇到该字符时可向右移动的距离 """ m = len(p) bc_table = dict() # 遍历模式串每个字符(包括最后一个字符) for i in range(m): # 记录每个字符在模式串中最右侧出现时可向右移动的距离 bc_table[p[i]] = m - i return bc_table # 测试用例 print(sunday("abbcfdddbddcaddebc", "aaaaa")) # 输出: -1,未匹配 print(sunday("abbcfdddbddcaddebc", "bcf")) # 输出: 2,匹配成功 ``` ## 4. Sunday 算法分析 | 指标 | 复杂度 | 说明 | | ------------ | ---------------- | ------------------------------------------------------------ | | 最好时间复杂度 | $O(n)$ | 模式串字符分布均匀,后移位数表能实现最大跳跃,比较次数最少。 | | 最坏时间复杂度 | $O(n \times m)$ | 模式串字符高度重复且与文本不匹配时,每次只能滑动一位。 | | 平均时间复杂度 | $O(n)$ | 实际应用中通常接近最好情况,比较次数较少。 | | 空间复杂度 | $O(m + \sigma)$ | 主要用于存储后移位数表,$m$ 为模式串长度,$\sigma$ 为字符集大小。 | - $n$ 为文本串长度,$m$ 为模式串长度,$\sigma$ 为字符集大小。 - Sunday 算法在大多数实际场景下效率较高,但极端情况下可能退化为 $O(n \times m)$。 - 空间消耗主要体现在后移位数表的构建上。 ## 5. 总结 Sunday 算法是一种高效的字符串匹配算法,通过利用窗口末尾字符的后移位数表,实现大步跳跃式匹配,提升了实际查找效率,适用于大多数文本搜索场景。 - **优点**: - 实现简单,易于理解和编码。 - 平均性能优良,实际应用中匹配效率高。 - 只需构建一次后移位数表,预处理开销小。 - 跳跃能力强,适合大多数实际文本搜索场景。 - **缺点**: - 最坏情况下时间复杂度较高,可能退化为 $O(n \times m)$。 - 只利用窗口末尾字符的信息,未充分利用更多启发式规则(如 BM 算法的好后缀规则)。 - 对极端重复或特殊构造的模式串不够友好,跳跃能力有限。 ## 参考资料 - 【书籍】柔性字符串匹配 - 中科院计算所网络信息安全研究组 译 - 【博文】[字符串模式匹配算法:BM、Horspool、Sunday、KMP、KR、AC算法 - schips - 博客园](https://www.cnblogs.com/schips/p/11098041.html) - 【博文】[字符串匹配——Sunday 算法 - Switch 的博客 - CSDN 博客](https://blog.csdn.net/q547550831/article/details/51860017) ## 练习题目 - [0028. 找出字符串中第一个匹配项的下标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-the-index-of-the-first-occurrence-in-a-string.md) - [0459. 重复的子字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/repeated-substring-pattern.md) - [0686. 重复叠加字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/repeated-string-match.md) - [0796. 旋转字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotate-string.md) - [1408. 数组中的字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/string-matching-in-an-array.md) - [2156. 查找给定哈希值的子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/find-substring-with-given-hash-value.md) - [单模式串匹配题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8D%95%E6%A8%A1%E5%BC%8F%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/04_string/04_08_trie.md ================================================ ## 1. 字典树介绍 > **字典树(Trie)**,又称前缀树,是一种高效存储和查找字符串集合的树形结构。可以把它想象成一本「分层字典」:每个单词从根节点出发,按字母顺序一层层分支,直到单词结尾。具有相同前缀的单词会在树上共用同一条路径,就像家族树中有共同祖先的亲戚一样,这样能大幅提升查找和前缀匹配的效率。 下图展示了一棵字典树,包含 `"a"`、`"abc"`、`"acb"`、`"acc"`、`"ach"`、`"b"`、`"chb"` 这 7 个单词。 ![字典树](https://qcdn.itcharge.cn/images/20240511165918.png) 在图中,边表示字符,从根节点到某节点的路径即为一个单词。例如 $1 \rightarrow 2 \rightarrow 6 \rightarrow 10$ 表示 `"acc"`。每个单词的结尾节点通常会有一个结束标记 $end$(红色节点),用于区分完整单词。 字典树的本质是利用字符串的公共前缀,将相同前缀的单词合并存储,从而加快查询速度,减少重复比较,利用空间换时间。 **字典树的基本性质:** - 根节点不存字符,其他每个节点只存一个字符。 - 从根到某节点的路径组成该节点对应的字符串。 - 每个节点的所有子节点字符都不相同。 ## 2. 字典树的基本操作 字典树常见的基本操作包括 **创建**、**插入**、**查找** 和 **删除**。其中,删除操作在实际应用中较少用到,因此本节主要聚焦于字典树的创建、插入和查找。 ### 2.1 字典树的结构 #### 2.1.1 字典树节点的定义 我们先明确字典树节点的结构。 字典树本质上是一棵多叉树,即每个节点可以拥有多个子节点。实现多叉结构时,常见的方式有两种:使用数组或哈希表。下面分别介绍这两种实现方式。 - 当字符串仅包含小写英文字母时,可以用长度为 $26$ 的数组来存储每个节点的所有子节点。例如: ```python class Node: # 字符节点 def __init__(self): # 初始化字符节点 # children 是长度为 26 的数组,分别对应 'a'~'z' 的子节点 self.children = [None for _ in range(26)] # 初始化所有子节点为 None self.isEnd = False # isEnd 用于标记该节点是否为某个单词的结尾 ``` 上述代码中,$self.children$ 采用数组结构,表示该节点的全部子节点;$isEnd$ 用于标记该节点是否为某个单词的结尾。 在插入单词时,需要将每个字符转换为对应的数字索引,然后在长度为 $26$ 的数组中定位并创建相应的子节点。 - 如果字符集不仅包含小写字母,还包括大写字母或其他字符,则可使用哈希表来存储当前节点的所有子节点,具体实现如下: ```python class Node: # 字符节点 def __init__(self): # 初始化字符节点 self.children = dict() # 用哈希表存储所有子节点,key 为字符,value 为 Node 实例 self.isEnd = False # 标记该节点是否为某个单词的结尾 # 例如:children['a'] 表示以当前节点为父节点,字符为 'a' 的子节点 ``` 在上述代码中,$self.children$ 采用哈希表结构,用于存储该节点的所有子节点,$isEnd$ 用于标记该节点是否为某个单词的结尾。插入单词时,可以根据单词的每个字符,动态创建对应的字符节点,并将其加入哈希表,便于高效查找和插入。 为统一实现和便于维护,本文后续所有代码均采用哈希表来管理节点的子节点。 #### 2.1.2 字典树的基本结构 在明确了字典树节点的结构后,我们进一步定义字典树的整体结构。字典树在初始化时会创建一个根节点,该根节点不存储任何字符。所有的插入和查找操作均从根节点出发。下面是字典树的基本结构代码: ```python class Trie: # 字典树(前缀树) def __init__(self): """ 初始化字典树,创建一个根节点。 根节点不存储任何字符,仅作为所有单词的公共起点。 """ self.root = Node() # 初始化根节点(根节点不保存字符) ``` ### 2.2 字典树的创建与插入操作 字典树的「创建」是指将字符串数组中的所有字符串依次插入到字典树中;而「插入」操作则是将单个字符串加入到字典树的过程。 #### 2.2.1 字典树的插入操作 在介绍字典树的批量创建前,先说明单个单词的插入流程: - 从根节点出发,依次遍历单词的每个字符 $ch$(根节点本身不存储字符)。 - 如果当前节点的子节点中不存在字符 $ch$,则新建一个节点 `cur.children[ch] = Node()`,并将当前指针移动到新节点。 - 如果当前节点的子节点中已存在字符 $ch$,则直接将当前指针移动到该子节点。 - 当所有字符遍历完毕后,将当前节点标记为单词结尾(即 `isEnd = True`)。 ```python # 向字典树中插入一个单词 def insert(self, word: str) -> None: """ 将一个单词插入到字典树中。 参数: word (str): 需要插入的单词 """ cur = self.root # 从根节点开始 for ch in word: # 遍历单词中的每个字符 # 如果当前节点的子节点中不存在字符 ch,则新建一个节点 if ch not in cur.children: cur.children[ch] = Node() # 创建新节点并加入子节点字典 # 移动到下一个字符节点,继续插入 cur = cur.children[ch] # 单词所有字符插入完成后,将当前节点标记为单词结尾 cur.isEnd = True ``` #### 2.2.2 字典树的创建操作 字典树的创建过程比较简单,通常包括以下步骤: - 先实例化一个字典树对象,如 `trie = Trie()`。 - 遍历单词列表,将每个单词依次插入到字典树中。 ```python # 创建一个字典树实例 trie = Trie() # 遍历单词列表,将每个单词插入到字典树中 for word in words: trie.insert(word) ``` ### 2.3 字典树的查找操作 #### 2.3.1 字典树的查找单词操作 在字典树中查找某个单词是否存在的过程与插入操作类似,具体步骤如下: - 从根节点出发,依次遍历单词的每个字符 $ch$。 - 如果当前节点的子节点中不存在字符 $ch$,则说明该单词不在字典树中,直接返回 $False$。 - 如果存在字符 $ch$,则将当前指针移动到对应的子节点,继续查找下一个字符。 - 当所有字符遍历完毕后,检查当前节点是否被标记为单词结尾(`isEnd = True`)。如果是,则说明字典树中存在该单词,返回 $True$;否则返回 $False$。 ```python # 查找字典树中是否存在一个单词 def search(self, word: str) -> bool: """ 在字典树中查找指定单词是否存在。 参数: word (str): 需要查找的单词 返回: bool: 如果单词存在于字典树中,返回 True;否则返回 False """ cur = self.root # 从根节点开始 for ch in word: # 遍历单词中的每个字符 if ch not in cur.children: # 如果当前节点的子节点中不存在该字符 return False # 说明单词不存在,直接返回 False cur = cur.children[ch] # 移动到对应的子节点,继续查找下一个字符 return cur.isEnd # 所有字符查找完毕,判断当前节点是否为单词结尾标记 ``` #### 2.3.2 字典树的查找前缀操作 在字典树中查找某个前缀是否存在,其过程与查找完整单词类似。不同之处在于,查找前缀时只需依次判断每个字符是否存在于相应的子节点中,无需判断最后节点是否为单词结尾标记。只要前缀的所有字符都能顺利匹配,即可认为该前缀存在于字典树中。 ```python # 查找字典树中是否存在一个前缀 def startsWith(self, prefix: str) -> bool: """ 在字典树中查找指定前缀是否存在。 参数: prefix (str): 需要查找的前缀字符串 返回: bool: 如果前缀存在于字典树中,返回 True;否则返回 False """ cur = self.root # 从根节点开始 for ch in prefix: # 遍历前缀中的每个字符 if ch not in cur.children: # 如果当前节点的子节点中不存在该字符 return False # 说明前缀不存在,直接返回 False cur = cur.children[ch] # 移动到对应的子节点,继续查找下一个字符 return True # 所有字符查找完毕,前缀存在于字典树中 ``` ## 3. 字典树的实现代码 ```python class Node: # 字符节点(Trie 树的节点) def __init__(self): self.children = dict() # 子节点字典,key 为字符,value 为 Node 对象 self.isEnd = False # 是否为单词结尾标记 class Trie: # 字典树(Trie) def __init__(self): """ 初始化字典树,创建一个空的根节点(根节点不保存字符) """ self.root = Node() def insert(self, word: str) -> None: """ 向字典树中插入一个单词 参数: word (str): 要插入的单词 """ cur = self.root # 从根节点开始 for ch in word: # 遍历单词中的每个字符 if ch not in cur.children: # 如果当前节点没有ch这个子节点 cur.children[ch] = Node() # 新建一个子节点 cur = cur.children[ch] # 移动到子节点,继续处理下一个字符 cur.isEnd = True # 单词插入完成,标记结尾 def search(self, word: str) -> bool: """ 查找字典树中是否存在一个完整单词 参数: word (str): 要查找的单词 返回: bool: 存在返回True,否则返回False """ cur = self.root # 从根节点开始 for ch in word: # 遍历单词中的每个字符 if ch not in cur.children: # 如果没有对应的子节点 return False # 单词不存在 cur = cur.children[ch] # 移动到子节点 return cur.isEnd # 判断是否为单词结尾 def startsWith(self, prefix: str) -> bool: """ 查找字典树中是否存在某个前缀 参数: prefix (str): 要查找的前缀 返回: bool: 存在返回True,否则返回False """ cur = self.root # 从根节点开始 for ch in prefix: # 遍历前缀中的每个字符 if ch not in cur.children: # 如果没有对应的子节点 return False # 前缀不存在 cur = cur.children[ch] # 移动到子节点 return True # 前缀存在 ``` ## 4. 字典树的算法分析 | 指标 | 复杂度 | 说明 | |------------------|-------------------------------|--------------------------------------------------------------| | 插入一个单词 | 时间:$O(n)$
空间:$O(d^n)$(数组实现)
空间:$O(n)$(哈希表实现) | $n$ 为单词长度,$d$ 为字符集大小。数组实现空间消耗大,哈希表实现更节省空间。 | | 查找一个单词 | 时间:$O(n)$
空间:$O(1)$ | $n$ 为单词长度,仅遍历单词长度,空间为常数。 | | 查找一个前缀 | 时间:$O(m)$
空间:$O(1)$ | $m$ 为前缀长度,仅遍历前缀长度,空间为常数。 | ## 5. 字典树的应用 字典树一个典型的应用场景就是:在搜索引擎中输入部分内容之后,搜索引擎就会自动弹出一些关联的相关搜索内容。我们可以从中直接选择自己想要搜索的内容,而不用将所有内容都输入进去。这个功能从一定程度上节省了我们的搜索时间。 例如下图,当我们输入「字典树」后,底下会出现一些以「字典树」为前缀的相关搜索内容。 ![字典树的应用](https://qcdn.itcharge.cn/images/20220210134829.png) 这个功能实现的基本原理就是字典树。当然,像 Google、必应、百度这样的搜索引擎,在这个功能能的背后肯定做了大量的改进和优化,但它的底层最基本的原理就是「字典树」这种数据结构。 除此之外,我们可以把字典树的应用分为以下几种: - **字符串检索**:事先将已知的⼀些字符串(字典)的有关信息存储到字典树⾥, 查找⼀些字符串是否出现过、出现的频率。 - **前缀统计**:统计⼀个串所有前缀单词的个数,只需统计从根节点到叶子节点路径上单词出现的个数,也可以判断⼀个单词是否为另⼀个单词的前缀。 - **最长公共前缀问题**:利用字典树求解多个字符串的最长公共前缀问题。将⼤量字符串都存储到⼀棵字典树上时, 可以快速得到某些字符串的公共前缀。对所有字符串都建⽴字典树,两个串的最长公共前缀的长度就是它们所在节点最近公共祖先的长度,于是转变为最近公共祖先问题。 - **字符串排序**:利⽤字典树进⾏串排序。例如,给定多个互不相同的仅由⼀个单词构成的英⽂名,将它们按字典序从⼩到⼤输出。采⽤数组⽅式创建字典树,字典树中每个节点的所有⼦节点都是按照其字母⼤⼩排序的。然后对字典树进⾏先序遍历,输出的相应字符串就是按字典序排序的结果。 ## 6. 总结 字典树(Trie)是一种高效存储和查找字符串集合的树形数据结构,通过利用字符串的公共前缀来减少重复比较,实现快速的前缀匹配和字符串检索。 **优点:** - **查找效率高**:查找单词和前缀的时间复杂度均为 $O(n)$,其中 $n$ 为字符串长度,比暴力匹配快很多 - **前缀匹配优秀**:能够快速判断一个字符串是否为另一个字符串的前缀,这在搜索引擎自动补全等场景中非常有用 - **空间共享**:具有相同前缀的单词共享路径,相比单独存储每个单词,能节省大量空间 - **支持动态操作**:可以动态插入、删除字符串,适合需要频繁更新的字符串集合 **缺点:** - **空间消耗较大**:每个节点都需要存储子节点信息,对于稀疏的字符串集合,空间利用率不高 - **实现复杂度**:相比简单的哈希表或数组,字典树的实现和维护更加复杂 - **字符集限制**:使用数组实现时,字符集大小会影响空间复杂度,大字符集会显著增加内存消耗 - **缓存不友好**:树形结构在内存中的分布可能不够连续,对 CPU 缓存不够友好 ## 练习题目 - [0208. 实现 Trie (前缀树)](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-trie-prefix-tree.md) - [0677. 键值映射](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/map-sum-pairs.md) - [1023. 驼峰式匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/camelcase-matching.md) - [0211. 添加与搜索单词 - 数据结构设计](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/design-add-and-search-words-data-structure.md) - [0648. 单词替换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/replace-words.md) - [0676. 实现一个魔法字典](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/implement-magic-dictionary.md) - [字典树题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%AD%97%E5%85%B8%E6%A0%91%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】算法训练营 陈小玉 著 - 【书籍】ACM-ICPC 程序设计系列 算法设计与实现 陈宇 吴昊 主编 - 【博文】[Trie 树 - 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/72414) - 【博文】[一文搞懂字典树](https://segmentfault.com/a/1190000040801084) - 【博文】[字典树 (Trie) - OI Wiki](https://oi-wiki.org/string/trie/) ================================================ FILE: docs/04_string/04_09_ac_automaton.md ================================================ ## 1. AC 自动机简介 > **AC 自动机(Aho-Corasick Automaton)**:由 Alfred V. Aho 和 Margaret J. Corasick 于 1975 年在贝尔实验室提出,是最著名的多模式匹配算法之一。 > > - **AC 自动机核心思想**:以 **字典树(Trie)** 为基础,结合 **KMP 算法的失配指针思想**,构建一个能够同时匹配多个模式串的有限状态自动机。当在文本串中匹配失败时,通过失配指针快速跳转到下一个可能匹配的状态,避免重复比较,实现高效的多模式匹配。 ### 1.1 多模式匹配的难点 在实际应用中,常常需要在文本中一次性查找多个模式串(如敏感词过滤、病毒检测、DNA 序列分析等)。 传统的单模式匹配算法(如 KMP、Boyer-Moore)需要对每个模式串分别进行匹配,时间复杂度为 $O(n \times m \times k)$,其中 $n$ 为文本长度,$m$ 为模式串平均长度,$k$ 为模式串数量,整体效率较低。 如果只使用字典树(Trie),虽然能够共享前缀,但每次匹配失败都必须回到根节点重新开始,无法实现高效跳转,最坏情况下复杂度也接近 $O(n \times m)$。 ### 1.2 AC 自动机高效匹配原理 AC 自动机能够高效解决多模式匹配问题,其核心思想是:将所有模式串构建为一棵字典树(Trie),并为每个节点设置失配指针(fail 指针),结合 KMP 算法的失配机制,实现对文本串的一次扫描即可同时匹配多个模式串。 AC 自动机的主要流程如下: 1. **构建字典树(Trie)**:将所有模式串插入字典树,充分利用公共前缀,节省空间和比较次数。 2. **构建失配指针(fail 指针)**:借鉴 KMP 算法思想,为字典树中每个节点添加失配指针。失配指针指向当前节点对应字符串的最长可用后缀节点,实现匹配失败时的快速跳转,避免重复比较。 3. **一次扫描文本串进行匹配**:只需从头到尾扫描一遍文本串,利用字典树和失配指针的协同作用,即可高效找到所有模式串的出现位置。 AC 自动机的时间复杂度为 $O(n + m + k)$,其中 $n$ 为文本串长度,$m$ 为所有模式串的总长度,$k$ 为匹配到的模式串数量。相比传统的多模式串逐一匹配方法(如 $O(n \times m \times k)$),AC 自动机大幅提升了匹配效率。 ## 2. AC 自动机原理 下面用一个简单例子来直观理解 AC 自动机的原理。 > **例子**:给定 5 个模式串:`say`、`she`、`shr`、`he`、`her`,文本串为 `yasherhs`。 > > **目标**:找出文本中所有出现的模式串及其位置。 ### 2.1 构建字典树(Trie) 我们先把所有模式串插入到一棵字典树中。字典树就像一棵「分叉的路」,每个节点代表一个字符,从根到某节点的路径,就是一个字符串。 以这 5 个模式串为例,字典树结构如下: ``` root / \ s h / \ | a h e / / \ \ y e r r ``` ### 2.2 构造失配指针 失配指针(fail 指针)是 AC 自动机的关键。它借鉴 KMP 算法的思想,为每个节点指向其「最长可用后缀」在字典树中的节点,实现失配时的快速跳转。 #### 2.2.1 失配指针的定义 对于字典树中的任意节点,其失配指针指向该节点对应字符串的 **最长真后缀** 在字典树中的节点。 - **真后缀**:字符串的真后缀是指该字符串的后缀,但不等于字符串本身。 #### 2.2.2 构造规则 失配指针的构造遵循以下规则: 1. **根节点**:失配指针为 `null`。 2. **根节点的子节点**:失配指针都指向根节点。 3. **其他节点**:从父节点的失配指针开始查找,如果找到对应字符的子节点,则指向该子节点;否则继续向上查找,直到找到或到达根节点。 #### 2.2.3 构造示例 以模式串 `["say", "she", "shr", "he", "her"]` 为例: ``` root / \ s h / \ | a h e / / \ \ y e r r ``` **失配指针构造过程**: - `s` → `root`(根节点子节点指向根节点) - `h` → `root`(根节点子节点指向根节点) - `sa` → `root`(根节点没有 `a` 子节点) - `sh` → `h`(根节点有 `h` 子节点) - `say` → `root`(根节点没有 `y` 子节点) - `she` → `he`(`h` 节点有 `e` 子节点) - `shr` → `root`(`h` 和根节点都没有 `r` 子节点) - `he` → `root`(根节点没有 `e` 子节点) - `her` → `root`(`root` 没有 `r` 子节点) #### 2.2.4 失配指针的作用 失配指针的主要作用是: 1. **快速跳转**:匹配失败时,不需要回到根节点重新开始。 2. **避免重复比较**:利用已匹配的部分信息,避免重复比较。 3. **保证匹配连续性**:确保跳转后当前匹配的字符串仍是某个模式串的前缀。 ### 2.3 文本串匹配过程 有了字典树和失配指针,我们就可以进行高效的文本串匹配了。 #### 2.3.1 匹配算法流程 1. **初始化**:从根节点开始。 2. **字符匹配**:对于文本串中的每个字符: - 如果当前节点有对应字符的子节点,移动到该子节点。 - 否则,沿着失配指针向上查找,直到找到匹配的子节点或到达根节点。 3. **模式串检测**:每到达一个节点,检查该节点是否为某个模式串的结尾。 4. **输出匹配结果**:如果找到匹配的模式串,记录其位置和内容。 #### 2.3.2 匹配过程示例 以文本串 `yasherhs` 为例,演示匹配过程: | 字符 | 当前节点 | 操作 | 当前路径 | 匹配结果 | |------|----------|------|----------|----------| | `y` | 根节点 | 无匹配,保持根节点 | - | - | | `a` | 根节点 | 无匹配,保持根节点 | - | - | | `s` | 根节点 | 移动到 `s` 节点 | `s` | - | | `h` | `s` 节点 | 移动到 `sh` 节点 | `sh` | - | | `e` | `sh` 节点 | 移动到 `she` 节点 | `she` | **找到 `she`、`he`**(沿 fail 链) | | `r` | `she` 节点 | 失配,沿 fail 跳到 `he`,再转移到 `her` 节点 | `her` | **找到 `her`** | | `h` | `her` 节点 | 失配,跳到根节点,再移动到 `h` 节点 | `h` | - | | `s` | `h` 节点 | 失配,跳到根节点,再移动到 `s` 节点 | `s` | - | **最终结果**:在文本串 `yasherhs` 中找到模式串 `she`(位置 3-5)、`he`(位置 4-5)、`her`(位置 4-6)。 ## 3. AC 自动机代码实现 ```python class TrieNode: def __init__(self): self.children = {} # 子节点,key 为字符,value 为 TrieNode self.fail = None # 失配指针,指向当前节点最长可用后缀的节点 self.is_end = False # 是否为某个模式串的结尾 self.word = "" # 如果是结尾,存储完整的单词 class AC_Automaton: def __init__(self): self.root = TrieNode() # 初始化根节点 def add_word(self, word): """ 向Trie树中插入一个模式串 """ node = self.root for char in word: if char not in node.children: node.children[char] = TrieNode() # 新建子节点 node = node.children[char] node.is_end = True # 标记单词结尾 node.word = word # 存储完整单词 def build_fail_pointers(self): """ 构建失配指针(fail指针),采用BFS广度优先遍历 """ from collections import deque queue = deque() # 1. 根节点的所有子节点的 fail 指针都指向根节点 for child in self.root.children.values(): child.fail = self.root queue.append(child) # 2. 广度优先遍历,依次为每个节点建立 fail 指针 while queue: current = queue.popleft() for char, child in current.children.items(): # 从当前节点的 fail 指针开始,向上寻找有无相同字符的子节点 fail = current.fail while fail and char not in fail.children: fail = fail.fail # 如果找到了,child的fail指针指向该节点,否则指向根节点 child.fail = fail.children[char] if fail and char in fail.children else self.root queue.append(child) def search(self, text): """ 在文本text中查找所有模式串出现的位置 返回所有匹配到的模式串(可重复) """ result = [] node = self.root for idx, char in enumerate(text): # 如果当前节点没有该字符的子节点,则沿fail指针向上跳转 while node is not self.root and char not in node.children: node = node.fail # 如果有该字符的子节点,则转移到该子节点 if char in node.children: node = node.children[char] # 否则仍然停留在根节点 # 检查当前节点以及沿fail链上的所有节点是否为单词结尾 temp = node while temp is not self.root: if temp.is_end: result.append(temp.word) # 记录匹配到的模式串 temp = temp.fail return result ``` ## 4. AC 自动机算法分析 | 指标 | 复杂度 | 说明 | | ------------ | ---------------- | ------------------------------------------------------------ | | 构建字典树 | $O(m)$ | $m$ 为所有模式串的总长度 | | 构建失配指针 | $O(m)$ | 使用 BFS 遍历所有节点,每个节点最多被访问一次 | | 文本串匹配 | $O(n + k)$ | $n$ 为文本串长度,$k$ 为匹配到的模式串数量 | | **总体时间复杂度** | **$O(n + m + k)$** | 线性时间复杂度,非常高效 | | **空间复杂度** | $O(m)$ | 包含字典树和失配指针的存储,$m$ 为所有模式串的总长度 | ## 6. 总结 AC 自动机是一种高效的多模式匹配算法,它巧妙地结合了字典树和 KMP 算法的思想,实现了在文本串中快速查找多个模式串的功能。 **核心思想**: - 使用字典树组织所有模式串,共享公共前缀 - 借鉴 KMP 算法的失配指针思想,实现快速状态跳转 - 通过一次扫描文本串,找到所有匹配的模式串 虽然 AC 自动机的实现相对复杂,但在需要多模式匹配的场景下,它提供了最优的时间复杂度,是处理多模式匹配问题的首选算法。 ## 练习题目 - [0208. 实现 Trie (前缀树)](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-trie-prefix-tree.md) - [0677. 键值映射](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/map-sum-pairs.md) - [1023. 驼峰式匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/camelcase-matching.md) - [0211. 添加与搜索单词 - 数据结构设计](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/design-add-and-search-words-data-structure.md) - [0648. 单词替换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/replace-words.md) - [0676. 实现一个魔法字典](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/implement-magic-dictionary.md) - [多模式串匹配题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%A4%9A%E6%A8%A1%E5%BC%8F%E4%B8%B2%E5%8C%B9%E9%85%8D%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】算法训练营 陈小玉 著 - 【书籍】ACM-ICPC 程序设计系列 算法设计与实现 陈宇 吴昊 主编 - 【博文】[AC自动机 - OI Wiki](https://oi-wiki.org/string/ac-automaton/) - 【博文】[AC自动机算法详解 - 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/72810) - 【博文】[AC自动机算法详解 - 算法竞赛进阶指南](https://www.acwing.com/blog/content/405/) - 【博文】[AC自动机算法原理与实现 - 算法笔记](https://www.algorithm-notes.org/string/ac-automaton/) ================================================ FILE: docs/04_string/04_10_suffix_array.md ================================================ ## 1. 后缀数组简介 > **后缀数组(Suffix Array)**:是一种高效处理字符串后缀相关问题的数据结构。它将字符串的所有后缀按字典序排序,并记录每个后缀在原串中的起始位置,便于实现高效的子串查找、最长重复子串、最长公共子串等操作。 后缀数组常与 **LCP(Longest Common Prefix)数组** 配合使用,进一步提升字符串处理的效率。 --- ## 2. 基本原理与定义 给定一个长度为 $n$ 的字符串 $S$,其后缀数组 $SA$ 是一个长度为 $n$ 的整数数组,$SA[i]$ 表示 $S$ 的第 $i$ 小后缀在原串中的起始下标。 例如: > $S = "banana"$ > > $S$ 的所有后缀及其下标: > - 0: banana > - 1: anana > - 2: nana > - 3: ana > - 4: na > - 5: a > > 按字典序排序后: > 1. a (5) > 2. ana (3) > 3. anana (1) > 4. banana (0) > 5. na (4) > 6. nana (2) > > 所以 $SA = [5, 3, 1, 0, 4, 2]$ --- ## 3. 后缀数组的构建方法 后缀数组的构建有多种方法,常见的有: - **朴素排序法**:直接生成所有后缀并排序,时间复杂度 $O(n^2 \log n)$,适合短串。 - **倍增算法**:利用基数排序思想,时间复杂度 $O(n \log n)$。 - **DC3/Skew 算法**:线性时间 $O(n)$ 构建,适合大数据量。 ### 3.1 朴素法(适合理解原理) ```python # 朴素法构建后缀数组 S = "banana" suffixes = [(S[i:], i) for i in range(len(S))] suffixes.sort() SA = [idx for (suf, idx) in suffixes] print(SA) # 输出: [5, 3, 1, 0, 4, 2] ``` ### 3.2 倍增算法(常用高效实现) ```python def build_suffix_array(s): n = len(s) k = 1 rank = [ord(c) for c in s] tmp = [0] * n sa = list(range(n)) while True: sa.sort(key=lambda x: (rank[x], rank[x + k] if x + k < n else -1)) tmp[sa[0]] = 0 for i in range(1, n): tmp[sa[i]] = tmp[sa[i-1]] + \ ((rank[sa[i]] != rank[sa[i-1]]) or (rank[sa[i]+k] if sa[i]+k < n else -1) != (rank[sa[i-1]+k] if sa[i-1]+k < n else -1)) rank = tmp[:] if rank[sa[-1]] == n-1: break k <<= 1 return sa # 示例 S = "banana" print(build_suffix_array(S)) # 输出: [5, 3, 1, 0, 4, 2] ``` --- ## 4. LCP(最长公共前缀)数组 > **LCP 数组**:LCP[i] 表示 $SA[i]$ 和 $SA[i-1]$ 所指向的两个后缀的最长公共前缀长度。 LCP 数组常用于: - 快速查找最长重复子串 - 计算不同子串个数 - 字符串压缩等 ### LCP 数组的构建 ```python def build_lcp(s, sa): n = len(s) rank = [0] * n for i in range(n): rank[sa[i]] = i h = 0 lcp = [0] * n for i in range(n): if rank[i] == 0: lcp[0] = 0 else: j = sa[rank[i] - 1] while i + h < n and j + h < n and s[i + h] == s[j + h]: h += 1 lcp[rank[i]] = h if h > 0: h -= 1 return lcp # 示例 S = "banana" SA = build_suffix_array(S) LCP = build_lcp(S, SA) print(LCP) # 输出: [0, 1, 3, 0, 0, 2] ``` ## 5. 算法复杂度分析 - **朴素法**:$O(n^2 \log n)$ - **倍增法**:$O(n \log n)$ - **DC3/Skew**:$O(n)$ - **LCP 构建**:$O(n)$ ## 参考资料 - 《算法竞赛进阶指南》—— 胡策 - 《算法竞赛入门经典》—— 刘汝佳 - [OI Wiki - 后缀数组](https://oi-wiki.org/string/sa/) ================================================ FILE: docs/04_string/index.md ================================================ ![](https://qcdn.itcharge.cn/images/20250923140327.png) ::: tip 引 言 字符串如同夜色中流淌的音符。 如清风拂过琴弦,谱写数据的旋律与梦想。 ::: ## 本章内容 - [4.1 字符串基础](https://github.com/ITCharge/AlgoNote/tree/main/docs/04_string/04_01_string_basic.md) - [4.2 Brute Force 算法](https://github.com/ITCharge/AlgoNote/tree/main/docs/04_string/04_02_string_brute_force.md) - [4.3 Rabin Karp 算法](https://github.com/ITCharge/AlgoNote/tree/main/docs/04_string/04_03_string_rabin_karp.md) - [4.4 KMP 算法](https://github.com/ITCharge/AlgoNote/tree/main/docs/04_string/04_04_string_kmp.md) - [4.5 Boyer Moore 算法](https://github.com/ITCharge/AlgoNote/tree/main/docs/04_string/04_05_string_boyer_moore.md) - [4.6 Horspool 算法](https://github.com/ITCharge/AlgoNote/tree/main/docs/04_string/04_06_string_horspool.md) - [4.7 Sunday 算法](https://github.com/ITCharge/AlgoNote/tree/main/docs/04_string/04_07_string_sunday.md) - [4.8 字典树](https://github.com/ITCharge/AlgoNote/tree/main/docs/04_string/04_08_trie.md) - [4.9 AC 自动机](https://github.com/ITCharge/AlgoNote/tree/main/docs/04_string/04_09_ac_automaton.md) - [4.10 后缀数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/04_string/04_10_suffix_array.md) ================================================ FILE: docs/05_tree/05_01_tree_basic.md ================================================ ## 1. 树 ### 1.1 树的定义 > **树(Tree)**:由 $n \ge 0$ 个节点及其相互之间的关系组成的有限集合。当 $n = 0$ 时称为空树,$n > 0$ 时称为非空树。 之所以称为「树」,是因为这种数据结构的形态类似于一棵倒挂的树:根节点在上,叶子节点在下。如下图所示: ![树](https://qcdn.itcharge.cn/images/20240511171215.png) 树结构具备以下基本特性: - 仅有一个没有前驱的节点,称为 **根节点(Root)**。 - 除根节点外,其余每个节点都且仅有一个直接前驱节点。 - 每个节点(包括根节点)可以有零个或多个后继节点。 - 当 $n > 1$ 时,除根节点外的其余节点可分为 $m\ (m > 0)$ 个互不相交的有限集合 $T_1, T_2, ..., T_m$,每个集合本身又是一棵树,称为根的 **子树(SubTree)**。 如下图所示,红色节点 $A$ 是根节点,除根节点外,存在 $3$ 棵互不相交的子树:$T_1(B, E, H, I, G)$、$T_2(C)$、$T_3(D, F, G, K)$。 ![树与子树](https://qcdn.itcharge.cn/images/20240511171233.png) ### 1.2 树的相关术语 下面介绍树结构中的常用基本术语。 #### 1.2.1 节点分类 - **树的节点**:由一个数据元素和若干指向其子树的分支组成。节点拥有的子树数量称为 **节点的度**。 - **叶子节点(终端节点)**:度为 $0$ 的节点。例如图中 $C$、$H$、$I$、$G$、$F$、$K$。 - **分支节点(非终端节点)**:度大于 $0$ 的节点。例如图中 $A$、$B$、$D$、$E$、$G$。 - **树的度**:树中所有节点的最大度数。例如图中树的度为 $3$。 ![节点分类](https://qcdn.itcharge.cn/images/202509292157162.png) #### 1.2.2 节点间关系 - **孩子节点(子节点)**:某节点的子树的根节点。例如图中 $B$ 是 $A$ 的孩子节点。 - **父节点**:拥有子节点的节点称为其子节点的父节点。例如图中 $B$ 是 $E$ 的父节点。 - **兄弟节点**:同一父节点的不同子节点互为兄弟。例如图中 $F$、$G$ 互为兄弟节点。 ![节点间关系](https://qcdn.itcharge.cn/images/20240511171311.png) #### 1.2.3 其他常用术语 - **节点的层次**:从根节点开始,根为第 $1$ 层,根的子节点为第 $2$ 层,依此类推。 - **树的深度(高度)**:树中节点的最大层数。例如图中树的深度为 $4$。 - **堂兄弟节点**:父节点在同一层的节点互为堂兄弟。例如图中 $J$、$K$ 互为堂兄弟节点。 - **路径**:树中两个节点之间经过的节点序列。例如 $E$ 到 $G$ 的路径为 $E - B - A - D - G$。 - **路径长度**:路径上经过的边数。例如 $E$ 到 $G$ 的路径长度为 $4$。 - **节点的祖先**:从该节点到根节点路径上所有节点。例如 $H$ 的祖先为 $E$、$B$、$A$。 - **节点的子孙**:以该节点为根的子树中所有节点。例如 $D$ 的子孙为 $F$、$G$、$K$。 ![树的其他术语](https://qcdn.itcharge.cn/images/20240511171325.png) ### 1.3 树的分类 树按照节点子树之间是否可以交换位置,可以分为两大类:**有序树** 和 **无序树**。 - **有序树**:每个节点的子树有严格的左右次序,子树之间的位置不可随意交换。例如二叉树就是典型的有序树。 - **无序树**:每个节点的子树之间没有顺序要求,子树可以任意交换位置。 简而言之,有序树强调子树的排列顺序,结构唯一;无序树则只关注连接关系,不关心子树的排列顺序。 ## 2. 二叉树 ### 2.1 二叉树的定义 > **二叉树(Binary Tree)**:是一种有序树,其中每个节点的度最多为 $2$。每个节点的两个分支分别称为 **左子树** 和 **右子树**,且左右子树的顺序不可交换。 如下图所示是一棵典型的二叉树: ![二叉树](https://qcdn.itcharge.cn/images/20240511171342.png) 二叉树还可以递归地定义为: - **空树**:即不包含任何节点的树。 - **非空树**:由一个根节点和两棵互不相交的子树 $T_1$、$T_2$ 组成,$T_1$ 称为左子树,$T_2$ 称为右子树,且 $T_1$、$T_2$ 本身也都是二叉树。 简而言之,二叉树是一种每个节点最多有两个子树(左子树和右子树)的有序树,且左右子树的位置不可交换。换句话说,二叉树中每个节点的度都不超过 2。 二叉树在结构上可以分为以下 $5$ 种基本形态,如下图所示: ![二叉树的形态](https://qcdn.itcharge.cn/images/20220218164839.png) ### 2.2 二叉树的基本性质 二叉树作为最常用的树形结构之一,具有以下基本性质: 1. **第 $i$ 层的最大节点数**:在二叉树中,第 $i$ 层最多有 $2^{i-1}$ 个节点($i \geq 1$)。 2. **深度为 $k$ 的二叉树的最大节点数**:$2^k - 1$。 3. **任意一棵非空二叉树的第 $k$ 层至多有 $2^{k-1}$ 个节点**。 4. **节点数为 $n$ 的二叉树的最小深度**:$\lceil \log_2(n+1) \rceil$。 5. **叶子节点数与度为 $2$ 的节点数关系**:二叉树中叶子节点数 $n_0$ 恰好比度为 $2$ 的节点数 $n_2$ 多 $1$,即 $n_0 = n_2 + 1$。 6. **二叉树的边数**:$n - 1$,其中 $n$ 为节点数。 这些性质对于分析二叉树的结构、空间复杂度和算法效率具有重要意义。 ### 2.3 特殊的二叉树 下面介绍几类常见的特殊二叉树。 #### 2.3.1 满二叉树 > **满二叉树(Full Binary Tree)**:指所有非叶子节点均有左右两个子节点,且所有叶子节点都集中在同一层的二叉树。 满二叉树具有以下特征: - 叶子节点全部位于最底层。 - 所有非叶子节点的度均为 $2$。 - 在相同深度的二叉树中,满二叉树的节点数和叶子节点数均为最大。 如果对满二叉树的节点自上而下、从左到右依次编号,根节点编号为 $1$,则深度为 $k$ 的满二叉树最后一个节点的编号为 $2^k - 1$。 如下图所示,展示了满二叉树与非满二叉树的示例: ![满二叉树与非满二叉树](https://qcdn.itcharge.cn/images/20220218173007.png) #### 2.3.2 完全二叉树 > **完全二叉树(Complete Binary Tree)**:一种特殊的二叉树,要求除了最后一层外,每一层的节点数都达到最大,且最后一层的所有节点都连续排列在最左侧。 完全二叉树的主要特征如下: - 叶子节点只可能出现在最后两层。 - 最底层的叶子节点必须依次排列在最左侧。 - 倒数第二层如果有叶子节点,则这些节点必须集中在右侧。 - 如果某节点的度为 $1$,则该节点只能有左孩子,不存在只有右孩子的情况。 - 在节点数相同的二叉树中,完全二叉树的深度最小。 完全二叉树也可以通过节点编号来定义:从根节点开始,按层次自上而下、从左到右依次编号(根为 $1$)。如果一棵深度为 $k$、节点数为 $n$ 的二叉树,其每个节点与深度为 $k$ 的满二叉树中编号 $1$ 到 $n$ 的节点一一对应,则该树为完全二叉树。 下面通过示例图进行说明: ![完全二叉树与非完全二叉树](https://qcdn.itcharge.cn/images/20220218174000.png) #### 2.3.3 二叉搜索树 > **二叉搜索树(Binary Search Tree, BST)**,又称二叉查找树、有序二叉树或排序二叉树,是一种特殊的二叉树结构。其定义如下: > > - 对于任意一个节点,如果其左子树非空,则左子树所有节点的值均小于该节点的值; > - 对于任意一个节点,如果其右子树非空,则右子树所有节点的值均大于该节点的值; > - 左右子树本身也都是二叉搜索树; > - 空树也被视为二叉搜索树。 二叉搜索树的结构保证了对任意节点的中序遍历结果是递增有序的。下图展示了三棵典型的二叉搜索树: ![二叉搜索树](https://qcdn.itcharge.cn/images/20240511171406.png) #### 2.3.4 平衡二叉搜索树 > **平衡二叉搜索树(Balanced Binary Search Tree, BBST)**:是一类结构上保持平衡的二叉搜索树。其核心特性是任意节点的左右子树高度差的绝对值不超过 $1$,且左右子树本身也都是平衡二叉搜索树。通过这种结构,平衡二叉搜索树能够保证插入、查找和删除操作的时间复杂度均为 $O(\log n)$。最早提出的平衡二叉搜索树是 **AVL 树(Adelson-Velsky and Landis Tree)**。 AVL 树的主要性质如下: - 空树是一棵 AVL 树。 - 如果二叉树 $T$ 是 AVL 树,则 $T$ 的左右子树也都是 AVL 树,且满足 $|h(\text{left}) - h(\text{right})| \le 1$,其中 $h(\text{left})$ 和 $h(\text{right})$ 分别为左、右子树的高度。 - AVL 树的高度始终保持在 $O(\log n)$。 下图中,前两棵树为平衡二叉搜索树,最后一棵树不是平衡二叉搜索树,因为其某节点的左右子树高度差超过 $1$。 ![平衡二叉树与非平衡二叉树](https://qcdn.itcharge.cn/images/20220221103552.png) ### 2.4 二叉树的存储结构 二叉树常见的存储方式有两种:**「顺序存储结构」**和**「链式存储结构」**。下面分别介绍这两种方式。 #### 2.4.1 二叉树的顺序存储结构 顺序存储结构通常使用一维数组来保存二叉树的所有节点。节点在数组中的位置按照完全二叉树的层序编号排列:自上而下、从左到右依次存放。如果某个节点不存在,则在数组对应位置填充「空节点」。 例如,堆排序和优先队列中的二叉堆结构就是采用顺序存储结构实现的。 以节点值为 $[1, 2, 3, 4, 5, 6, 7]$ 的完全二叉树为例,其顺序存储结构如下图所示。 ![二叉树的顺序存储结构](https://qcdn.itcharge.cn/images/20240511171423.png) 通过顺序存储结构,节点之间的关系可以通过下标直接计算得到: - 如果某节点(非叶子节点)下标为 $i$,则其左孩子下标为 $2 \times i + 1$,右孩子下标为 $2 \times i + 2$。 - 如果某节点(非根节点)下标为 $i$,则其父节点下标为 $(i - 1) // 2$($//$ 表示整除)。 顺序存储结构非常适合 **完全二叉树**(尤其是满二叉树),因为可以充分利用数组空间,节点排列紧凑。但对于一般二叉树,如果存在大量空节点,则会造成空间浪费。此外,顺序存储结构不利于二叉树的插入和删除操作,灵活性较差。当二叉树结构和规模经常变化时,更推荐使用链式存储结构。 #### 2.4.2 二叉树的链式存储结构 在链式存储结构中,二叉树的每个节点通常包含三个部分:一个数据域 $val$ 用于存放节点的值,两个指针域 $left$ 和 $right$ 分别指向左、右子节点。当某个子节点不存在时,相应的指针为 None(或 null)。这种结构如下图所示: ![二叉链节点](https://qcdn.itcharge.cn/images/20240511171434.png) 其对应的代码实现如下: ```python class TreeNode: """ 二叉树节点定义(链式存储结构) 属性: val: 节点存储的值 left: 指向左子节点的指针(无左子节点时为 None) right: 指向右子节点的指针(无右子节点时为 None) """ def __init__(self, val=0, left=None, right=None): self.val = val # 节点的值 self.left = left # 左子节点指针 self.right = right # 右子节点指针 ``` 以节点值为 $[1, 2, 3, 4, 5, 6, 7]$ 的完全二叉树为例,其链式存储结构如下图所示。 ![二叉树的链式存储结构](https://qcdn.itcharge.cn/images/20240511171446.png) 链式存储结构具有高度的灵活性和便利性。其节点数量仅受限于系统可用内存,且通常比顺序存储结构更节省空间(指针域的空间开销与节点数成线性关系)。此外,链式结构便于进行插入、删除等操作,因此在实际应用中,二叉树大多采用链式存储结构。 ## 3. 总结 树是一种层次化的非线性数据结构,由节点和边组成,具有一个根节点和若干子树。二叉树是树的一种特殊形式,每个节点最多有两个子节点(左子树和右子树)。 **二叉树的核心特性:** - **层次性**:二叉树以根节点为起点,节点自上而下、由左至右分层排列,形成清晰的层级结构。 - **递归性**:每个节点的左右子树本身也是二叉树,天然适合递归定义与处理。 - **有序性**:每个节点的左、右子树位置固定,不能随意交换,保证结构的唯一性和有序性。 **二叉树常见类型:** - **满二叉树**:除叶子节点外,每个节点都有两个子节点 - **完全二叉树**:除最后一层外,其他层都被填满,最后一层从左到右填充 - **二叉搜索树**:左子树所有节点值小于根节点,右子树所有节点值大于根节点 - **平衡二叉树**:左右子树高度差不超过1,保证操作效率 **二叉树存储方式:** - **顺序存储**:用数组存储,适合完全二叉树,空间利用率高 - **链式存储**:用指针连接,灵活性好,便于动态操作 ## 参考链接 - 【书籍】数据结构教程 第 3 版 - 唐发根 著 - 【书籍】大话数据结构 程杰 著 - 【书籍】算法训练营 陈小玉 著 - 【博文】[二叉树理论基础 - 代码随想录](https://programmercarl.com/二叉树理论基础.html) - 【博文】[二叉树基础 - 袁厨的算法小屋](https://github.com/chefyuan/algorithm-base/blob/main/animation-simulation/二叉树/二叉树基础.md) ================================================ FILE: docs/05_tree/05_02_binary_tree_traverse.md ================================================ ## 1. 二叉树的遍历简介 > **二叉树的遍历**:是指从根节点出发,按照特定顺序依次访问二叉树中的所有节点,确保每个节点被且仅被访问一次。 在实际应用中,常常需要按照一定的顺序访问二叉树的每个节点,以便查找特定节点或处理全部节点。例如,可以依次输出节点的值、统计满足某条件的节点数量等。这里的「访问」通常指对节点执行某种操作。 根据二叉树的递归结构是由根节点、左子树和右子树组成的,只要依次遍历这三部分,就能遍历整棵二叉树。 按照遍历顺序的不同,二叉树的遍历方式主要分为两大类: - **深度优先遍历(DFS)**:根据节点访问顺序的不同,理论上有 $6$ 种遍历方式。如果约定先遍历左子树再遍历右子树,常用的有 $3$ 种:**前序遍历**、**中序遍历**、**后序遍历**。 - **广度优先遍历(BFS)**:按照层次自上而下、每层从左到右依次访问所有节点,称为 **层序遍历**。 这些遍历方式为二叉树的各种操作和算法奠定了基础。 ## 2. 二叉树前序遍历 > **二叉树前序遍历(Preorder Traversal)**:是指按照「根节点 → 左子树 → 右子树」的顺序依次访问二叉树的所有节点。 具体规则如下: - 如果二叉树为空,直接返回; - 如果二叉树非空,则: > 1. 访问根节点; > 2. 递归前序遍历左子树; > 3. 递归前序遍历右子树。 前序遍历本质上是一个递归过程。无论遍历哪一棵子树,始终遵循「先访问根节点,再遍历左子树,最后遍历右子树」的顺序。 如下图所示,该二叉树的前序遍历结果为:$A - B - D - H - I - E - C - F - J - G - K$。 ![二叉树的前序遍历](https://qcdn.itcharge.cn/images/20240511171628.png) ### 2.1 二叉树前序遍历的递归实现 二叉树前序遍历递归实现的基本步骤: 1. 如果当前节点为空,直接返回; 2. 访问当前节点(根节点); 3. 递归遍历左子树; 4. 递归遍历右子树。 前序遍历的递归实现代码如下: ```python class Solution: def preorderTraversal(self, root: TreeNode) -> List[int]: """ 二叉树的前序遍历(递归实现) 参数: root: TreeNode,二叉树的根节点 返回: List[int],前序遍历的节点值列表 """ res = [] # 用于存储遍历结果 def preorder(node): if not node: return # 递归终止条件:节点为空 res.append(node.val) # 1. 访问根节点 preorder(node.left) # 2. 递归遍历左子树 preorder(node.right) # 3. 递归遍历右子树 preorder(root) # 从根节点开始递归 return res ``` ### 2.2 二叉树前序遍历的非递归实现 递归实现前序遍历时,实际上是借助系统调用栈来完成的。我们同样可以用一个显式栈 $stack$ 来手动模拟递归过程,实现前序遍历。 前序遍历的访问顺序为:根节点 → 左子树 → 右子树。由于栈具有「后进先出」的特性,为了保证遍历顺序正确,入栈时应先将右子节点压入,再将左子节点压入,这样弹出时会先访问左子树,再访问右子树。 具体实现步骤如下: 1. 如果二叉树为空,直接返回。 2. 初始化一个栈,将根节点压入栈中。 3. 当栈不为空时,重复以下操作: 1. 弹出栈顶节点 $node$,访问该节点。 2. 如果 $node$ 的右子节点存在,则将其压入栈中。 3. 如果 $node$ 的左子节点存在,则将其压入栈中。 这样即可实现前序遍历的非递归(显式栈)写法。 ```python class Solution: def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]: """ 二叉树的前序遍历(非递归/显式栈实现) 参数: root: Optional[TreeNode],二叉树的根节点 返回: List[int],前序遍历的节点值列表 """ if not root: # 特判:二叉树为空,直接返回空列表 return [] res = [] # 用于存储遍历结果 stack = [root] # 初始化栈,根节点先入栈 while stack: # 当栈不为空时循环 node = stack.pop() # 弹出栈顶节点 res.append(node.val) # 访问当前节点(根节点) # 注意:先右后左,保证左子树先被遍历 if node.right: # 如果右子节点存在,先将其入栈 stack.append(node.right) if node.left: # 如果左子节点存在,再将其入栈 stack.append(node.left) return res # 返回前序遍历结果 ``` ## 3. 二叉树中序遍历 > **二叉树中序遍历(Inorder Traversal)** 的基本规则如下: > > - 如果二叉树为空,直接返回。 > - 如果二叉树非空,则依次执行: > 1. 递归遍历左子树(中序方式); > 2. 访问当前根节点; > 3. 递归遍历右子树(中序方式)。 中序遍历本质上是一个递归过程。无论遍历哪一棵子树,始终遵循「先左子树,后根节点,最后右子树」的顺序。每到一个节点,先深入其左子树,左子树遍历完毕后访问该节点本身,最后再遍历其右子树。 如下图所示,该二叉树的中序遍历结果为:$H - D - I - B - E - A - F - J - C - K - G$。 ![二叉树的中序遍历](https://qcdn.itcharge.cn/images/20240511171643.png) ### 3.1 二叉树中序遍历的递归实现 二叉树的序遍历递归实现的基本步骤: 1. 如果当前节点为空,直接返回。 2. 递归遍历左子树。 3. 访问当前节点。 4. 递归遍历右子树。 对应的递归实现代码如下: ```python class Solution: def inorderTraversal(self, root: TreeNode) -> List[int]: """ 二叉树中序遍历(递归实现) 参数: root: TreeNode,二叉树的根节点 返回: List[int],中序遍历的节点值列表 """ res = [] # 用于存储遍历结果 def inorder(node): if not node: return # 递归终止条件:节点为空 inorder(node.left) # 递归遍历左子树 res.append(node.val) # 访问当前节点 inorder(node.right) # 递归遍历右子树 inorder(root) # 从根节点开始递归 return res # 返回中序遍历结果 ``` ### 3.2 二叉树中序遍历的非递归实现 我们可以通过显式维护一个栈 $stack$,来模拟递归实现的中序遍历过程。 与前序遍历不同,中序遍历要求在访问根节点前,必须先遍历完其左子树。因此,**只有在左子树全部入栈后,当前节点才能出栈并被访问**。 具体做法是:从根节点出发,不断将当前节点压入栈中,并向左移动,直到没有左子节点为止。此时弹出栈顶节点,访问该节点,然后转向其右子树,重复上述过程。这样可以确保遍历顺序严格按照「左-根-右」进行。 中序遍历的非递归(显式栈)实现步骤如下: 1. 如果二叉树为空,直接返回。 2. 初始化一个空栈。 3. 当当前节点不为空或栈不为空时,重复以下操作: 1. 如果当前节点不为空,不断将其压入栈,并向左移动,直到左子节点为空。 2. 如果当前节点为空,说明已到达最左侧,弹出栈顶节点 $node$,访问该节点,然后将当前节点指向 $node$ 的右子节点,继续上述循环。 二叉树中序遍历的非递归实现代码如下: ```python class Solution: def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]: """ 二叉树中序遍历(非递归/显式栈实现) 参数: root: Optional[TreeNode],二叉树的根节点 返回: List[int],中序遍历的节点值列表 """ res = [] # 用于存储遍历结果 stack = [] # 显式栈,用于模拟递归过程 cur = root # 当前遍历的节点指针 while cur or stack: # 只要当前节点不为空或栈不为空就继续 # 不断向左子树深入,将沿途节点全部入栈 while cur: stack.append(cur) # 当前节点入栈 cur = cur.left # 继续遍历左子树 # 此时已到达最左侧,弹出栈顶节点 node = stack.pop() # 弹出最左侧节点 res.append(node.val) # 访问该节点(中序遍历的「根」) cur = node.right # 转向右子树,继续上述过程 return res ``` ## 4. 二叉树后序遍历 > **二叉树后序遍历(Postorder Traversal)** 的基本规则如下: > > - 如果二叉树为空,直接返回。 > - 如果二叉树非空,则依次执行: > 1. 递归遍历左子树(后序方式)。 > 2. 递归遍历右子树(后序方式)。 > 3. 访问根节点。 后序遍历的本质是递归地先处理左子树,再处理右子树,最后处理根节点。无论遍历到哪一棵子树,始终遵循「左-右-根」的顺序。 如下图所示,该二叉树的后序遍历结果为:$H - I - D - E - B - J - F - K - G - C - A$。 ![二叉树的后序遍历](https://qcdn.itcharge.cn/images/20240511171658.png) ### 4.1 二叉树后序遍历的递归实现 后序遍历递归实现的核心思想是:对于每个节点,先处理其左子树,再处理右子树,最后访问节点本身。具体步骤如下: 1. 如果当前节点为空,直接返回。 2. 递归遍历左子树。 3. 递归遍历右子树。 4. 访问当前节点(即处理节点值)。 下面是二叉树后序遍历的递归实现代码: ```python class Solution: def postorderTraversal(self, root: TreeNode) -> List[int]: """ 二叉树后序遍历(递归实现) 参数: root: TreeNode,二叉树的根节点 返回: List[int],后序遍历的节点值列表 """ res = [] # 用于存储遍历结果 def postorder(node): if not node: return # 递归遍历左子树 postorder(node.left) # 递归遍历右子树 postorder(node.right) # 访问当前节点 res.append(node.val) postorder(root) return res ``` ### 4.2 二叉树后序遍历的非递归实现 后序遍历可以通过显式栈 $stack$ 来模拟递归过程。与前序和中序遍历不同,后序遍历要求在左右子树都访问完成后,才能访问根节点。因此,必须确保:**当前节点在其左右孩子节点都访问完毕之前不能出栈**。 后序遍历的非递归实现可以通过如下方式优化理解: - 从根节点出发,将其依次压入栈中,并不断向左深入,直到到达最左侧节点。 - 每次弹出栈顶节点,判断其右子树是否已被访问: - 如果已访问,则访问该节点; - 如果未访问,则将该节点重新压入栈,并转而遍历其右子树。 具体步骤如下: 1. 如果二叉树为空,直接返回。 2. 初始化一个空栈 $stack$,并用 $prev$ 记录上一个访问的节点。 3. 当当前节点不为空或栈不为空时,循环执行: 1. 不断将当前节点压入栈,并向左移动,直到最左侧节点。 2. 弹出栈顶节点 $node$。 3. 如果 $node$ 没有右子树,或右子树已被访问,则访问 $node$,更新 $prev$,并将当前节点设为空。 4. 否则,将 $node$ 重新压回栈,转而遍历其右子树。 这样即可实现二叉树的后序遍历非递归写法。 ```python class Solution: def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]: """ 二叉树后序遍历(非递归/显式栈实现) 参数: root: Optional[TreeNode],二叉树的根节点 返回: List[int],后序遍历的节点值列表 """ res = [] # 用于存储遍历结果 stack = [] # 显式栈,用于模拟递归过程 prev = None # 记录上一个访问的节点,用于判断右子树是否已访问 while root or stack: # 只要当前节点不为空或栈不为空就继续遍历 # 一直向左走,将所有左子节点入栈 while root: stack.append(root) # 当前节点入栈 root = root.left # 继续遍历左子树 node = stack.pop() # 弹出栈顶节点,准备访问或遍历其右子树 # 判断是否可以访问当前节点 # 1. 没有右子树 # 2. 右子树已经访问过(即上一次访问的节点是当前节点的右子节点) if not node.right or node.right == prev: res.append(node.val) # 访问当前节点 prev = node # 更新上一次访问的节点 root = None # 当前节点已访问,重置root,防止重复入栈 else: # 右子树还未访问,当前节点重新入栈,转而遍历右子树 stack.append(node) root = node.right return res ``` ## 5. 二叉树层序遍历 > **二叉树层序遍历**(Level Order Traversal)的基本规则为:指按照从上到下、从左到右的顺序,逐层依次访问二叉树的所有节点。 > > - 如果二叉树为空,直接返回。 > - 如果二叉树非空,则: > 1. 先访问第 $1$ 层(根节点); > 2. 再访问第 $2$ 层的所有节点; > 3. 依次类推,直到访问到最底层的所有节点。 层序遍历本质上是一种广度优先搜索(BFS)过程。遍历时,先访问每一层的所有节点,再进入下一层,并且同一层的节点总是从左到右依次访问。 如下图所示,该二叉树的层序遍历结果为:$A - B - C - D - E - F - G - H - I - J - K$。 ![二叉树的层序遍历](https://qcdn.itcharge.cn/images/20240511175431.png) 层序遍历通常借助队列(Queue)来实现。具体流程如下: 1. 如果二叉树为空,直接返回。 2. 将根节点加入队列。 3. 当队列不为空时,重复以下操作: 1. 记录当前队列长度 $s_i$(即当前层的节点数)。 2. 依次从队列中取出这 $s_i$ 个节点,访问它们,并将它们的左右子节点(如存在)加入队列。 4. 队列为空时,遍历结束。 二叉树层序遍历的代码实现如下: ```python class Solution: def levelOrder(self, root: TreeNode) -> List[List[int]]: """ 二叉树层序遍历(广度优先搜索,BFS) 返回每一层的节点值组成的二维列表 """ if not root: return [] # 空树直接返回空列表 from collections import deque # 推荐使用 deque 提高队列效率 queue = deque([root]) # 初始化队列,根节点入队 order = [] # 用于存储最终结果 while queue: level = [] # 存储当前层的节点值 size = len(queue) # 当前层的节点数量 for _ in range(size): curr = queue.popleft() # 弹出队首节点 level.append(curr.val) # 访问当前节点 if curr.left: queue.append(curr.left) # 左子节点入队 if curr.right: queue.append(curr.right) # 右子节点入队 if level: order.append(level) # 当前层结果加入总结果 return order ``` ## 6. 总结 ### 6.1 算法特点对比 | 遍历方式 | 访问顺序 | 递归实现 | 非递归实现 | 空间复杂度 | 时间复杂度 | |---------|---------|---------|-----------|-----------|-----------| | **前序遍历** | 根 → 左 → 右 | 简单直观 | 使用栈,先右后左入栈 | O(h) | O(n) | | **中序遍历** | 左 → 根 → 右 | 简单直观 | 使用栈,先左后右 | O(h) | O(n) | | **后序遍历** | 左 → 右 → 根 | 简单直观 | 使用栈,需要标记访问状态 | O(h) | O(n) | | **层序遍历** | 按层从左到右 | 不适用 | 使用队列,BFS思想 | O(w) | O(n) | > 注:h 为树的高度,w 为树的最大宽度,n 为节点总数 ### 6.2 优缺点分析 #### 前序遍历 - **优点**: - 递归实现简单直观,易于理解 - 适合需要先处理根节点再处理子节点的场景 - 常用于树的复制、序列化等操作 - **缺点**: - 非递归实现需要特别注意入栈顺序 - 对于深度很大的树,递归可能导致栈溢出 #### 中序遍历 - **优点**: - 对于二叉搜索树,中序遍历得到有序序列 - 递归实现逻辑清晰 - 适合需要按顺序处理节点的场景 - **缺点**: - 非递归实现相对复杂 - 需要理解"左-根-右"的访问时机 #### 后序遍历 - **优点**: - 适合需要先处理子节点再处理父节点的场景 - 常用于树的删除、后序表达式计算等 - 递归实现简单 - **缺点**: - 非递归实现最复杂,需要额外的访问状态标记 - 理解难度较高 #### 层序遍历 - **优点**: - 直观反映树的层次结构 - 适合需要按层处理节点的场景 - 非递归实现相对简单 - **缺点**: - 不适用于递归实现 - 空间复杂度可能较高(对于宽树) ## 练习题目 - [0144. 二叉树的前序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-preorder-traversal.md) - [0094. 二叉树的中序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/binary-tree-inorder-traversal.md) - [0145. 二叉树的后序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-postorder-traversal.md) - [0102. 二叉树的层序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-level-order-traversal.md) - [0104. 二叉树的最大深度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-depth-of-binary-tree.md) - [0112. 路径总和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/path-sum.md) - [二叉树的遍历题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%81%8D%E5%8E%86%E9%A2%98%E7%9B%AE) ## 参考资料 1. 【书籍】数据结构教程 第 3 版 - 唐发根 著 2. 【书籍】大话数据结构 程杰 著 3. 【书籍】算法训练营 陈小玉 著 3. 【题解】[LeetCode 二叉树前序遍历(递归法 + 非递归法)- 二叉树的前序遍历 - 力扣](https://leetcode.cn/problems/binary-tree-preorder-traversal/solution/acm-xuan-shou-tu-jie-leetcode-er-cha-shu-pqpz/) 3. 【题解】[二叉树遍历通解(递归和迭代解法)- 完全模拟递归 - 二叉树的后序遍历 - 力扣](https://leetcode.cn/problems/binary-tree-postorder-traversal/solution/bian-li-tong-jie-by-long_wotu/) 3. 【题解】[迭代后序遍历 - 二叉树的后序遍历 - 力扣](https://leetcode.cn/problems/binary-tree-postorder-traversal/solution/die-dai-hou-xu-bian-li-by-wang-mo-ji-98ob/) ================================================ FILE: docs/05_tree/05_03_binary_tree_reduction.md ================================================ ## 1. 二叉树的还原简介 > **二叉树的还原**:指通过已知的二叉树遍历序列,重建出原始的二叉树结构。 我们知道,对于一棵非空二叉树,其前序、中序、后序遍历序列都是唯一的。但反过来,如果只给出某一种遍历序列,是否能唯一确定这棵二叉树呢?答案是否定的。 ### 1.1 单一遍历序列的还原能力 - **前序遍历**:第一个节点必为根节点,但无法区分后续节点属于左子树还是右子树,因此仅凭前序序列无法还原二叉树。 - **中序遍历**:虽然根节点能将中序序列分为左右子树,但无法确定根节点是谁,因此仅凭中序序列也无法还原二叉树。 - **后序遍历**:最后一个节点必为根节点,但同样无法判断其他节点的归属,仅凭后序序列也无法还原二叉树。 ### 1.2 两种遍历序列的组合情况 - **前序 + 中序**:前序序列确定根节点,中序序列确定左右子树的范围。递归分割子序列,可以唯一还原原二叉树。 - **中序 + 后序**:后序序列确定根节点,中序序列确定左右子树的范围,方法与前序+中序类似,也能唯一还原二叉树。 - **中序 + 层序**:通过层序遍历确定每个子树的根节点,再结合中序遍历分割左右子树,也可以唯一还原二叉树。 ### 1.3 不能唯一还原的特殊情况 - **前序 + 后序**:仅有前序和后序遍历序列时,无法唯一确定二叉树结构。因为缺少中序信息,无法区分左右子树的分界。例如,如果存在度为 $1$ 的节点,无法判断该节点是左子树还是右子树。 - **特殊说明**:只有当二叉树中每个节点的度均为 $2$ 或 $0$(即满二叉树)时,前序和后序遍历序列才能唯一确定二叉树。如果存在度为 $1$ 的节点,则无法唯一还原。 **结论**: - 已知「前序+中序」、「中序+后序」、「中序+层序」任意一组遍历序列,可以唯一还原一棵二叉树。 - 仅有「前序+后序」遍历序列,通常无法唯一还原二叉树,除非二叉树为满二叉树。 ## 2. 利用前序与中序遍历序列重建二叉树 - **描述**:给定一棵二叉树的前序遍历序列和中序遍历序列。 - **目标**:重建出原始的二叉树结构。 - **说明**:树中所有节点值均不重复。 ### 2.1 实现思路与步骤 前序遍历顺序为:根节点 → 左子树 → 右子树; 中序遍历顺序为:左子树 → 根节点 → 右子树。 基于上述规律,可以通过以下方式递归重建二叉树: 1. 前序遍历序列的第一个元素即为当前子树的根节点。 2. 在中序遍历序列中查找该根节点的位置 $inorder[k]$,据此将中序序列分为左、右子树两部分,并确定左右子树的节点数量。 3. 利用左右子树节点数量,将前序遍历序列切分为左、右子树对应的部分。 4. 递归构建当前根节点的左、右子树,直到子树为空(序列长度为0)为止。 简要流程如下: - 取前序序列首元素作为根节点。 - 在中序序列中定位根节点,分割出左、右子树的中序区间。 - 根据左子树节点数,切分前序序列为左、右子树区间。 - 递归处理左右子树,直至区间为空。 ### 2.2 代码实现 ```python class Solution: def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: """ 根据前序遍历和中序遍历序列重建二叉树 参数: preorder: List[int],二叉树的前序遍历序列 inorder: List[int],二叉树的中序遍历序列 返回: TreeNode,重建后的二叉树根节点 """ def createTree(preorder, inorder, n): """ 递归构建二叉树 参数: preorder: 当前子树的前序遍历序列 inorder: 当前子树的中序遍历序列 n: 当前子树的节点数 返回: TreeNode,当前子树的根节点 """ if n == 0: return None # 递归终止条件:子树节点数为 0 # 在中序遍历中查找根节点位置 k = 0 while preorder[0] != inorder[k]: k += 1 # 创建根节点 node = TreeNode(inorder[k]) # 递归构建左子树 node.left = createTree(preorder[1: k + 1], inorder[0: k], k) # 递归构建右子树 node.right = createTree(preorder[k + 1:], inorder[k + 1:], n - k - 1) return node # 从整棵树的前序和中序序列开始递归构建 return createTree(preorder, inorder, len(inorder)) ``` ## 3. 利用中序与后序遍历序列重建二叉树 - **描述**:给定一棵二叉树的中序遍历序列和后序遍历序列。 - **目标**:重建出原始的二叉树结构。 - **说明**:树中所有节点值均不重复。 ### 3.1 实现思路与步骤 - 中序遍历顺序:左子树 → 根节点 → 右子树 - 后序遍历顺序:左子树 → 右子树 → 根节点 利用后序遍历的最后一个元素可以确定当前子树的根节点。再在中序遍历序列中定位该根节点,从而划分出左、右子树的中序区间,并据此确定左右子树的节点数量。递归地对左右子树重复上述过程,直到区间为空。 具体步骤如下: 1. 后序遍历序列的最后一个元素 $postorder[n-1]$ 为当前子树的根节点。 2. 在中序遍历序列中查找该根节点的位置 $inorder[k]$,据此将中序序列分为左、右子树区间,并确定左右子树的节点数。 3. 利用左右子树的节点数,将后序遍历序列划分为左、右子树对应的区间。 4. 构建当前根节点,并递归构建其左、右子树,直到区间为空为止。 ### 3.2 代码实现 ```python class Solution: def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode: """ 根据中序遍历和后序遍历序列重建二叉树 参数: inorder: List[int],二叉树的中序遍历序列 postorder: List[int],二叉树的后序遍历序列 返回: TreeNode,重建后的二叉树根节点 """ def createTree(inorder, postorder, n): """ 递归构建二叉树 参数: inorder: 当前子树的中序遍历序列 postorder: 当前子树的后序遍历序列 n: 当前子树的节点数 返回: TreeNode,当前子树的根节点 """ if n == 0: return None # 递归终止条件:子树节点数为0,返回空节点 # 后序遍历的最后一个元素为当前子树的根节点 root_val = postorder[n - 1] # 在中序遍历中查找根节点的位置 k = 0 while inorder[k] != root_val: k += 1 # 创建根节点 node = TreeNode(root_val) # 递归构建左子树 # 左子树的中序区间:inorder[0:k] # 左子树的后序区间:postorder[0:k] node.left = createTree(inorder[0:k], postorder[0:k], k) # 递归构建右子树 # 右子树的中序区间:inorder[k+1:n] # 右子树的后序区间:postorder[k:n-1] node.right = createTree(inorder[k+1:n], postorder[k:n-1], n - k - 1) return node # 从整棵树的中序和后序序列开始递归构建 return createTree(inorder, postorder, len(postorder)) ``` ## 4. 利用前序与后序遍历序列构造二叉树 如前所述,**仅通过二叉树的前序和后序遍历序列,无法唯一确定一棵二叉树。** 但如果不要求唯一性,只需构造出任意一棵符合条件的二叉树,是可以实现的。 - **描述**:给定一棵二叉树的前序遍历和后序遍历序列。 - **目标**:重建并返回该二叉树。 - **说明**:假设树中节点值各不相同。如果存在多个可行答案,返回其中任意一个即可。 ### 4.1 实现思路与步骤 我们可以假定前序遍历序列的第二个元素为左子树的根节点,进而递归划分左右子树。具体步骤如下: 1. 前序遍历的第一个元素 $preorder[0]$ 是当前子树的根节点。 2. 前序遍历的第二个元素 $preorder[1]$ 是左子树的根节点。我们在后序遍历中查找该节点的位置 $postorder[k]$,该位置左侧为左子树,右侧为右子树。 3. 由 $k$ 可确定左子树的节点数量,从而划分前序和后序序列的左右子树部分。 4. 递归构建当前节点的左、右子树,直到子树为空。 ### 4.2 代码实现 ```python class Solution: def constructFromPrePost(self, preorder: List[int], postorder: List[int]) -> TreeNode: """ 根据前序和后序遍历序列构造二叉树(不唯一) 参数: preorder: List[int],二叉树的前序遍历序列 postorder: List[int],二叉树的后序遍历序列 返回: TreeNode,重建后的二叉树根节点 """ def createTree(preorder, postorder, n): if n == 0: return None # 递归终止条件:子树节点数为0,返回空节点 # 前序遍历的第一个元素为当前子树的根节点 root_val = preorder[0] node = TreeNode(root_val) if n == 1: return node # 只有一个节点,直接返回 # 前序遍历的第二个元素为左子树的根节点 left_root_val = preorder[1] # 在后序遍历中查找左子树根节点的位置 k = 0 while postorder[k] != left_root_val: k += 1 # k 为左子树在 postorder 中的结尾索引,左子树节点数为 k+1 # 划分左右子树的前序和后序区间 # 左子树:preorder[1:k+2], postorder[0:k+1] # 右子树:preorder[k+2:], postorder[k+1:n-1] node.left = createTree(preorder[1:k+2], postorder[0:k+1], k+1) node.right = createTree(preorder[k+2:], postorder[k+1:n-1], n-k-1) return node # 从整棵树的前序和后序序列开始递归构建 return createTree(preorder, postorder, len(preorder)) ``` ## 5. 总结 ### 5.1 核心要点 二叉树的还原是数据结构中的重要问题,其核心在于 **利用遍历序列的特性来重建树结构**。 **关键规律**: - **前序遍历**:根节点 → 左子树 → 右子树 - **中序遍历**:左子树 → 根节点 → 右子树 - **后序遍历**:左子树 → 右子树 → 根节点 ### 5.2 还原能力对比 | 遍历序列组合 | 能否唯一还原 | 说明 | |-------------|-------------|------| | 前序 + 中序 | 可以 | 前序确定根,中序确定左右子树范围 | | 中序 + 后序 | 可以 | 后序确定根,中序确定左右子树范围 | | 中序 + 层序 | 可以 | 层序确定根,中序确定左右子树范围 | | 前序 + 后序 | 不能 | 缺少中序信息,无法区分左右子树 | 二叉树的还原是理解树结构遍历特性的重要应用,掌握「前序+中序」和「中序+后序」的还原方法,就能解决大部分二叉树构造问题。 ## 练习题目 - [0105. 从前序与中序遍历序列构造二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/construct-binary-tree-from-preorder-and-inorder-traversal.md) - [0106. 从中序与后序遍历序列构造二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/construct-binary-tree-from-inorder-and-postorder-traversal.md) - [0889. 根据前序和后序遍历构造二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/construct-binary-tree-from-preorder-and-postorder-traversal.md) - [二叉树的还原题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E8%BF%98%E5%8E%9F%E9%A2%98%E7%9B%AE) ## 参考资料 1. 【书籍】数据结构教程 第 3 版 - 唐发根 著 2. 【书籍】算法训练营 陈小玉 著 3. 【博文】[二叉树的构造系列 - 知乎](https://zhuanlan.zhihu.com/p/346336665) 4. 【评论】[889. 根据前序和后序遍历构造二叉树 - 力扣)](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal/comments/) ================================================ FILE: docs/05_tree/05_04_binary_search_tree.md ================================================ ## 1. 二叉搜索树简介 > **二叉搜索树(Binary Search Tree, BST)**,又称二叉查找树、有序二叉树或排序二叉树,是一种特殊的二叉树结构,满足以下性质: > > - 对于任意节点,如果其左子树非空,则左子树所有节点的值均 **小于** 该节点的值; > - 对于任意节点,如果其右子树非空,则右子树所有节点的值均 **大于** 该节点的值; > - 任意节点的左右子树也都分别是二叉搜索树(递归定义)。 下图展示了三棵典型的二叉搜索树: ![二叉搜索树](https://qcdn.itcharge.cn/images/20240511171406.png) 二叉搜索树的核心特性是:**左子树所有节点值 < 根节点值 < 右子树所有节点值**。 基于这一特性,如果对二叉搜索树进行中序遍历,得到的节点值序列一定是递增的。例如,某棵二叉搜索树的中序遍历结果如下图所示。 ## 2. 二叉搜索树的查找 > **二叉搜索树查找**:即在二叉搜索树中定位值为 $val$ 的节点。 ### 2.1 查找算法思路 基于二叉搜索树的性质,查找过程可以高效地缩小范围。每次比较后,只需决定向左子树还是右子树继续查找,从而大大提升查找效率。具体步骤如下: 1. 如果当前二叉搜索树为空,查找失败,返回空指针 $None$。 2. 如果当前节点不为空,将待查找值 $val$ 与当前节点值 $root.val$ 比较: - 如果 $val == root.val$,查找成功,返回该节点。 - 如果 $val < root.val$,递归查找左子树。 - 如果 $val > root.val$,递归查找右子树。 ### 2.2 查找算法代码实现 ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val # 节点值 self.left = left # 左子节点 self.right = right # 右子节点 class Solution: def searchBST(self, root: TreeNode, val: int) -> TreeNode: """ 在二叉搜索树中查找值为 val 的节点 参数: root: TreeNode,二叉搜索树的根节点 val: int,待查找的目标值 返回: TreeNode,值为 val 的节点,如果未找到则返回 None """ if not root: return None # 空树或查找失败,返回 None if val == root.val: return root # 找到目标节点,返回 elif val < root.val: # 目标值小于当前节点值,递归查找左子树 return self.searchBST(root.left, val) else: # 目标值大于当前节点值,递归查找右子树 return self.searchBST(root.right, val) ``` ### 2.3 二叉搜索树的查找算法分析 | 指标 | 复杂度 | 说明 | |--------------|------------------|--------------------------------------------------------------| | 最优时间 | $O(\log_2 n)$ | 树接近完全平衡,高度为 $h = \log_2 n$,每次查找缩小一半范围 | | 最坏时间 | $O(n)$ | 树退化为单链表,需遍历所有节点 | | 平均时间 | $O(\log_2 n)$ | 随机插入情况下,平均查找长度约为 $\log_2 n$ | | 空间复杂度 | $O(1)$ | 递归实现时为 $O(h)$,迭代实现为 $O(1)$,$h$ 为树高 | ## 3. 二叉搜索树的插入 > **二叉搜索树的插入**:在二叉搜索树中插入一个值为 $val$ 的节点(假设当前树中不存在 $val$)。 ### 3.1 插入算法步骤 二叉搜索树的插入过程与查找类似,具体如下: 1. 如果当前树为空,直接创建值为 $val$ 的节点,作为根节点返回。 2. 如果当前树非空,将 $val$ 与当前节点 $root.val$ 比较: - 如果 $val < root.val$,递归插入到左子树。 - 如果 $val > root.val$,递归插入到右子树。 > **注意**:二叉搜索树不允许重复节点。如果 $val$ 已存在于树中,则不插入,直接返回原树。 ### 3.2 插入算法代码实现 ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val # 节点值 self.left = left # 左子节点 self.right = right # 右子节点 class Solution: def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode: """ 在二叉搜索树中插入一个值为 val 的节点 参数: root: TreeNode,二叉搜索树的根节点 val: int,待插入的节点值 返回: TreeNode,插入后的二叉搜索树根节点 """ if root is None: # 当前子树为空,直接创建新节点并返回 return TreeNode(val) if val < root.val: # 待插入值小于当前节点值,递归插入到左子树 root.left = self.insertIntoBST(root.left, val) elif val > root.val: # 待插入值大于当前节点值,递归插入到右子树 root.right = self.insertIntoBST(root.right, val) # 如果 val == root.val,不插入(不允许重复),直接返回原树 return root ``` ## 4. 二叉搜索树的创建 > **二叉搜索树的创建**:根据给定数组中的元素,依次插入,构建出一棵二叉搜索树。 ### 4.1 创建算法步骤 二叉搜索树的创建通常从一棵空树开始,依次将数组中的每个元素插入到树中,最终形成完整的二叉搜索树。具体步骤如下: 1. 初始化根节点为空。 2. 遍历数组,将每个元素 $nums[i]$ 依次插入到当前的二叉搜索树中。 3. 所有元素插入完成后,返回二叉搜索树的根节点。 ### 4.2 创建代码实现 ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val # 节点值 self.left = left # 左子节点 self.right = right # 右子节点 class Solution: def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode: """ 在二叉搜索树中插入一个值为 val 的节点 参数: root: TreeNode,二叉搜索树的根节点 val: int,待插入的节点值 返回: TreeNode,插入后的二叉搜索树根节点 """ if root is None: # 当前子树为空,直接创建新节点并返回 return TreeNode(val) if val < root.val: # 待插入值小于当前节点值,递归插入到左子树 root.left = self.insertIntoBST(root.left, val) elif val > root.val: # 待插入值大于当前节点值,递归插入到右子树 root.right = self.insertIntoBST(root.right, val) # 如果 val == root.val,不插入(不允许重复),直接返回原树 return root def buildBST(self, nums) -> TreeNode: """ 根据给定数组 nums 创建一棵二叉搜索树 参数: nums: List[int],待插入的节点值数组 返回: TreeNode,构建好的二叉搜索树根节点 """ root = None # 初始化根节点为空 for num in nums: root = self.insertIntoBST(root, num) # 依次插入每个元素 return root ``` ## 5. 二叉搜索树的删除 > **二叉搜索树的删除**:即在二叉搜索树中删除值为 $val$ 的节点。 ### 5.1 删除操作算法步骤 在二叉搜索树中删除节点时,首先需要定位到目标节点,然后根据其子树情况分为三种情形: 1. **左子树为空**:用其右子树替代被删除节点的位置。 2. **右子树为空**:用其左子树替代被删除节点的位置。 3. **左右子树均不为空**:利用二叉搜索树的有序性,可用「直接前驱」或「直接后继」节点的值替换当前节点,然后递归删除前驱或后继节点。 - **直接前驱**:即左子树中值最大的节点(左子树最右侧节点)。 - **直接后继**:即右子树中值最小的节点(右子树最左侧节点)。 具体删除步骤如下: 1. 如果当前节点为空,直接返回。 2. 如果当前节点值大于 $val$,递归在左子树中查找并删除,更新 $root.left$。 3. 如果当前节点值小于 $val$,递归在右子树中查找并删除,更新 $root.right$。 4. 如果当前节点值等于 $val$,即找到目标节点,分三种情况处理: 1. 如果左子树为空,返回右子树(右子树顶替当前节点)。 2. 如果右子树为空,返回左子树(左子树顶替当前节点)。 3. 如果左右子树均不为空,将左子树整体接到右子树的最左侧节点下,然后返回右子树作为新的子树根节点。 ### 5.2 二叉搜索树的删除代码实现 ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right class Solution: def deleteNode(self, root: TreeNode, val: int) -> TreeNode: """ 在二叉搜索树中删除值为 val 的节点,并返回新的根节点 参数: root: TreeNode,当前子树的根节点 val: int,待删除的节点值 返回: TreeNode,删除节点后的新根节点 """ if not root: # 递归终止条件:未找到目标节点,直接返回 return None if val < root.val: # 待删除值小于当前节点,递归去左子树删除 root.left = self.deleteNode(root.left, val) return root elif val > root.val: # 待删除值大于当前节点,递归去右子树删除 root.right = self.deleteNode(root.right, val) return root else: # 找到目标节点,分三种情况处理 if not root.left: # 情况 1:左子树为空,直接返回右子树 return root.right elif not root.right: # 情况 2:右子树为空,直接返回左子树 return root.left else: # 情况 3:左右子树均不为空 # 找到右子树的最左节点(即后继节点) successor = root.right while successor.left: successor = successor.left # 用后继节点的值替换当前节点 root.val = successor.val # 在右子树中递归删除后继节点 root.right = self.deleteNode(root.right, successor.val) return root ``` ## 6. 总结 ### 6.1 核心特性 二叉搜索树(BST)是一种 **有序的二叉树结构**,其核心特性是: - **左子树所有节点值 < 根节点值 < 右子树所有节点值** - **中序遍历结果是有序的**(递增序列) - **每个节点的左右子树也都是二叉搜索树** ### 6.2 基本操作算法分析 | 操作 | 最优时间 | 最坏时间 | 平均时间 | 空间复杂度 | |------|----------|----------|----------|------------| | 查找 | O(log n) | O(n) | O(log n) | O(1) | | 插入 | O(log n) | O(n) | O(log n) | O(1) | | 删除 | O(log n) | O(n) | O(log n) | O(1) | **说明**:最优情况是树接近完全平衡,最坏情况是树退化为单链表。 ### 6.3 算法特点 - **优点**: - 查找、插入、删除效率高(平均 O(log n)) - 支持范围查询和有序遍历 - 实现相对简单,易于理解 - **缺点**: - 插入顺序影响树的高度和性能 - 不平衡时可能退化为链表($O(n)$ 复杂度) - 需要额外的平衡机制(如 AVL 树、红黑树) ## 练习题目 - [0700. 二叉搜索树中的搜索](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/search-in-a-binary-search-tree.md) - [0701. 二叉搜索树中的插入操作](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/insert-into-a-binary-search-tree.md) - [0450. 删除二叉搜索树中的节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/delete-node-in-a-bst.md) - [0098. 验证二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/validate-binary-search-tree.md) - [0108. 将有序数组转换为二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/convert-sorted-array-to-binary-search-tree.md) - [0235. 二叉搜索树的最近公共祖先](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/lowest-common-ancestor-of-a-binary-search-tree.md) - [二叉搜索树题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】算法训练营 陈小玉 著 - 【书籍】算法竞赛入门经典:训练指南 - 刘汝佳,陈锋 著 - 【书籍】算法竞赛进阶指南 - 李煜东 著 - 【博文】[7.4 二叉搜索树 - Hello 算法](https://www.hello-algo.com/chapter_tree/binary_search_tree/) ================================================ FILE: docs/05_tree/05_05_segment_tree_01.md ================================================ ## 1. 线段树简介 ### 1.1 线段树的定义 > **线段树(Segment Tree)**:一种用于高效处理区间查询和区间修改的二叉树结构。它将一个区间不断二分,每个节点管理一个区间,叶子节点对应单个元素,内部节点则代表其子区间的合并结果。这样可以在 $O(\log n)$ 时间内完成区间相关操作。 线段树就像「区间的管家」:每个节点专管一个区间 $[left, right]$,叶子节点只负责一个元素($left = right$),非叶子节点则把区间一分为二,左孩子管 $[left, mid]$,右孩子管 $[mid+1, right]$,其中 $mid = (left + right) // 2$。整棵树自顶向下分工明确,根节点总揽全局。无论是单点修改、区间修改还是区间查询,都能在 $O(\log n)$ 时间内完成,非常适合处理大规模区间数据。 下图展示了区间 $[0, 7]$ 的线段树结构: ![线段树结构](https://qcdn.itcharge.cn/images/20240511173328.png) ### 1.2 线段树的特点 线段树的核心特点如下: 1. 每个节点对应一个区间。 2. 根节点管理整个区间(如 $[1, n]$)。 3. 叶子节点对应单个元素区间($[x, x]$)。 4. 每个内部节点 $[left, right]$ 的左子节点为 $[left, mid]$,右子节点为 $[mid + 1, right]$,其中 $mid = (left + right) // 2$(向下取整)。 ## 2. 线段树的构建 ### 2.1 线段树的存储结构 在二叉树中,我们常见的存储方式有「链式存储」和「顺序存储」。线段树同样可以采用这两种方式实现,但由于其结构接近完全二叉树,使用「顺序存储结构」(即数组)更加高效和简洁。 线段树的数组存储编号规则如下: - 根节点编号为 $0$。 - 如果某节点编号为 $i$,则其左孩子编号为 $2 \times i + 1$,右孩子编号为 $2 \times i + 2$。 - 如果某节点编号为 $i$(且 $i > 0$),其父节点编号为 $(i - 1) // 2$。 这样,我们可以用一个数组来存储整棵线段树。那么数组的大小如何确定呢? - 理想情况下,$n$ 个叶子节点构成的线段树是一棵满二叉树,总节点数为 $2 \times n - 1$。因此,数组大小取 $2 \times n$ 足够。 - 但实际上,为了适配任意长度的区间,线段树的深度为 $\lceil \log_2 n \rceil$,最坏情况下节点总数约为 $2^{\lceil \log_2 n \rceil + 1} - 1$,可近似为 $4 \times n$。因此,通常分配 $4 \times n$ 大小的数组即可保证安全。 ### 2.2 线段树的构建方法 ![线段树父子节点下标关系](https://qcdn.itcharge.cn/images/20240511173417.png) 如上图所示,编号为 $i$ 的节点,其左右孩子编号分别为 $2 \times i + 1$ 和 $2 \times i + 2$。因此,线段树的构建非常适合递归实现。具体步骤如下: 1. 如果当前区间为叶子节点($left == right$),则节点值为对应元素值。 2. 如果为非叶子节点,递归构建左、右子树。 3. 当前节点的区间值(如区间和、最大值、最小值等)由左右子节点的值合并得到。 线段树的构建实现代码如下: ```python # 线段树的节点类 class TreeNode: def __init__(self, val=0): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值,如区间和、区间最大值等) self.lazy_tag = None # 区间延迟更新标记(如区间加法、区间赋值等懒惰标记) # 线段树类 class SegmentTree: def __init__(self, nums, function): """ :param nums: 原始数据数组 :param function: 区间聚合函数(如 sum, max, min 等) """ self.size = len(nums) # 线段树最多需要 4 * n 个节点,使用数组存储 self.tree = [TreeNode() for _ in range(4 * self.size)] self.nums = nums self.function = function if self.size > 0: self.__build(0, 0, self.size - 1) def __build(self, index, left, right): """ 递归构建线段树 :param index: 当前节点在数组中的下标 :param left: 当前节点管理的区间左端点 :param right: 当前节点管理的区间右端点 """ self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,直接赋值为原数组对应元素 self.tree[index].val = self.nums[left] return mid = left + (right - left) // 2 left_index = index * 2 + 1 # 左子节点下标 right_index = index * 2 + 2 # 右子节点下标 self.__build(left_index, left, mid) # 构建左子树 self.__build(right_index, mid + 1, right) # 构建右子树 self.__pushup(index) # 更新当前节点的区间值 def __pushup(self, index): """ 向上更新当前节点的区间值 :param index: 当前节点在数组中的下标 """ left_index = index * 2 + 1 # 左子节点下标 right_index = index * 2 + 2 # 右子节点下标 # 当前节点的区间值由左右子节点的区间值聚合得到 self.tree[index].val = self.function( self.tree[left_index].val, self.tree[right_index].val ) ``` 这里的 `function` 参数用于指定线段树在区间合并时所采用的聚合函数。根据具体题目需求,可以灵活传入如求和(sum)、取最大值(max)、取最小值(min)等常见操作,实现不同类型的区间查询。 ## 3. 线段树的基本操作 线段树的基本操作包括:单点更新、区间查询和区间更新。下面依次介绍。 ### 3.1 单点更新 > **单点更新**:将 $nums[i]$ 修改为 $val$。 递归实现思路如下: 1. 如果当前节点为叶子节点($left == right$),直接更新其值。 2. 否则,判断 $i$ 属于左子树还是右子树,递归更新对应子树。 3. 更新完后,向上合并,重新计算当前节点的区间值。 单点更新的代码如下: ```python def update_point(self, i, val): """ 单点更新:将原数组 nums[i] 的值修改为 val,并同步更新线段树 :param i: 需要更新的元素下标 :param val: 新的值 """ self.nums[i] = val # 更新原数组 self.__update_point(i, val, 0, 0, self.size - 1) # 从根节点递归更新线段树 def __update_point(self, i, val, index, left, right): """ 递归实现单点更新 :param i: 需要更新的元素下标 :param val: 新的值 :param index: 当前节点在线段树数组中的下标 :param left: 当前节点管理的区间左端点 :param right: 当前节点管理的区间右端点 """ # 如果到达叶子节点,直接更新节点值 if self.tree[index].left == self.tree[index].right: self.tree[index].val = val # 叶子节点,节点值修改为 val return mid = left + (right - left) // 2 # 计算区间中点 left_index = index * 2 + 1 # 左子节点的下标 right_index = index * 2 + 2 # 右子节点的下标 # 判断 i 属于左子树还是右子树,递归更新 if i <= mid: self.__update_point(i, val, left_index, left, mid) # 在左子树中更新 else: self.__update_point(i, val, right_index, mid + 1, right) # 在右子树中更新 self.__pushup(index) # 向上更新当前节点的区间值 ``` ### 3.2 区间查询 > **区间查询**:即查询区间 $[q\_left, q\_right]$ 上的区间聚合值(如区间和、区间最值等)。 区间查询通常采用递归方式实现,具体流程如下: 1. 如果查询区间 $[q\_left, q\_right]$ 完全覆盖当前节点区间 $[left, right]$(即 $left \ge q\_left$ 且 $right \le q\_right$),直接返回该节点的区间值。 2. 如果查询区间 $[q\_left, q\_right]$ 与当前节点区间 $[left, right]$ 无交集(即 $right < q\_left$ 或 $left > q\_right$),返回 $0$(或聚合运算的单位元)。 3. 如果两区间有交集,则递归查询左右子区间,并将结果合并: - 如果 $q\_left \le mid$,递归查询左子区间 $[left, mid]$,记为 $res\_left$。 - 如果 $q\_right > mid$,递归查询右子区间 $[mid+1, right]$,记为 $res\_right$。 - 最终返回 $res\_left$ 与 $res\_right$ 的聚合结果。 线段树区间查询的代码如下: ```python # 区间查询,查询区间 [q_left, q_right] 的区间聚合值 def query_interval(self, q_left, q_right): """ 查询区间 [q_left, q_right] 的区间聚合值(如区间和、区间最值等) :param q_left: 查询区间左端点 :param q_right: 查询区间右端点 :return: 区间 [q_left, q_right] 的聚合值 """ return self.__query_interval(q_left, q_right, 0, 0, self.size - 1) # 区间查询的递归实现 def __query_interval(self, q_left, q_right, index, left, right): """ 递归查询线段树节点 [left, right] 区间与查询区间 [q_left, q_right] 的交集部分的聚合值 :param q_left: 查询区间左端点 :param q_right: 查询区间右端点 :param index: 当前节点在线段树数组中的下标 :param left: 当前节点管理的区间左端点 :param right: 当前节点管理的区间右端点 :return: 区间 [q_left, q_right] 与 [left, right] 的交集部分的聚合值 """ # 情况 1:当前节点区间被查询区间完全覆盖,直接返回节点值 if left >= q_left and right <= q_right: return self.tree[index].val # 情况 2:当前节点区间与查询区间无交集,返回单位元(如区间和为 0,区间最小值为正无穷等) if right < q_left or left > q_right: return 0 # 情况 3:当前节点区间与查询区间有部分重叠,递归查询左右子区间 self.__pushdown(index) # 下推懒惰标记,保证子节点信息正确 mid = left + (right - left) // 2 # 计算区间中点 left_index = index * 2 + 1 # 左子节点下标 right_index = index * 2 + 2 # 右子节点下标 res_left = 0 # 左子树查询结果初始化 res_right = 0 # 右子树查询结果初始化 if q_left <= mid: # 查询区间与左子区间有交集 res_left = self.__query_interval(q_left, q_right, left_index, left, mid) if q_right > mid: # 查询区间与右子区间有交集 res_right = self.__query_interval(q_left, q_right, right_index, mid + 1, right) return self.function(res_left, res_right) # 合并左右子树结果并返回 ``` ### 3.3 区间更新 > **区间更新**:即将区间 $[q\_left, q\_right]$ 内所有元素批量修改为 $val$。 #### 3.3.1 延迟标记(懒惰标记) 线段树的区间更新如果每次都递归到所有被覆盖的叶子节点,复杂度会退化为 $O(n)$。为避免无用的重复更新,线段树引入了 **延迟标记**(懒惰标记):当某个节点区间 $[left, right]$ 被更新区间 $[q\_left, q\_right]$ 完全覆盖时,只需直接更新该节点的值,并打上延迟标记,表示其子节点尚未被真正更新。只有在后续递归访问到子节点时,才将更新操作「下推」到子节点。 这样,区间更新和区间查询的时间复杂度都能保持 $O(\log_2 n)$。 区间更新的主要步骤如下: 1. 如果 $[q\_left, q\_right]$ 完全覆盖当前节点区间 $[left, right]$,则直接更新当前节点的值,并设置延迟标记。 2. 如果 $[q\_left, q\_right]$ 与 $[left, right]$ 无交集,直接返回。 3. 如果有部分重叠,先将当前节点的延迟标记下推到子节点(如果有),然后递归更新左右子区间,最后更新当前节点的值。 #### 3.3.2 下推延迟标记 当节点有延迟标记时,需要将该标记下推到左右子节点,具体做法: 1. 将左子节点的值和懒惰标记更新为 $val$。 2. 将右子节点的值和懒惰标记更新为 $val$。 3. 清除当前节点的懒惰标记。 这样可以保证每个节点的更新操作只在必要时才真正执行,极大提升效率。 #### 3.3.3 区间赋值操作(延迟标记) 使用延迟标记实现区间赋值的代码如下: ```python def update_interval(self, q_left, q_right, val): """ 对区间 [q_left, q_right] 进行区间赋值操作,将该区间内所有元素修改为 val """ self.__update_interval(q_left, q_right, val, 0, 0, self.size - 1) def __update_interval(self, q_left, q_right, val, index, left, right): """ 递归实现区间赋值更新 参数说明: q_left, q_right: 待更新的目标区间 val: 赋值的目标值 index: 当前节点在线段树数组中的下标 left, right: 当前节点所表示的区间范围 """ # 情况 1:当前节点区间被 [q_left, q_right] 完全覆盖,直接更新并打懒惰标记 if left >= q_left and right <= q_right: interval_size = (right - left + 1) # 当前区间长度 self.tree[index].val = interval_size * val # 区间所有元素赋值为 val self.tree[index].lazy_tag = val # 打上懒惰标记 return # 情况 2:当前节点区间与 [q_left, q_right] 无交集,直接返回 if right < q_left or left > q_right: return # 情况 3:部分重叠,先下推懒惰标记,再递归更新左右子区间 self.__pushdown(index) mid = left + (right - left) // 2 # 区间中点 left_index = index * 2 + 1 # 左子节点下标 right_index = index * 2 + 2 # 右子节点下标 if q_left <= mid: # 左子区间有交集 self.__update_interval(q_left, q_right, val, left_index, left, mid) if q_right > mid: # 右子区间有交集 self.__update_interval(q_left, q_right, val, right_index, mid + 1, right) self.__pushup(index) # 回溯时更新当前节点的值 def __pushdown(self, index): """ 将当前节点的懒惰标记下推到左右子节点,并更新子节点的值 """ lazy_tag = self.tree[index].lazy_tag if lazy_tag is None: return left_index = index * 2 + 1 # 左子节点下标 right_index = index * 2 + 2 # 右子节点下标 # 更新左子节点的懒惰标记和值 self.tree[left_index].lazy_tag = lazy_tag left_size = self.tree[left_index].right - self.tree[left_index].left + 1 self.tree[left_index].val = lazy_tag * left_size # 更新右子节点的懒惰标记和值 self.tree[right_index].lazy_tag = lazy_tag right_size = self.tree[right_index].right - self.tree[right_index].left + 1 self.tree[right_index].val = lazy_tag * right_size # 清除当前节点的懒惰标记 self.tree[index].lazy_tag = None ``` ### 3.3.4 区间加减操作(延迟标记) 有些题目要求将区间 $[q\_left, q\_right]$ 内每个元素在原有基础上增加或减少 $val$,而不是直接赋值为 $val$。 针对这种情况,我们需要重新定义「延迟标记」的含义:即表示当前区间整体增加了 $val$,但该操作尚未下传到子区间。相应地,代码实现也要相应调整以支持区间加减操作的延迟更新。 以下是基于延迟标记的区间加减操作代码: ```python # 区间更新,将区间 [q_left, q_right] 上的所有元素增加 val def update_interval(self, q_left, q_right, val): """ 对区间 [q_left, q_right] 内的所有元素增加 val """ self.__update_interval(q_left, q_right, val, 0, 0, self.size - 1) def __update_interval(self, q_left, q_right, val, index, left, right): """ 递归实现区间加法更新 参数: q_left, q_right: 待更新的区间范围 val: 增加的值 index: 当前节点在线段树数组中的下标 left, right: 当前节点所表示的区间范围 """ # 情况 1:当前节点区间被 [q_left, q_right] 完全覆盖,直接打懒惰标记并更新区间和 if left >= q_left and right <= q_right: interval_size = right - left + 1 # 当前节点区间长度 if self.tree[index].lazy_tag is not None: self.tree[index].lazy_tag += val # 累加懒惰标记 else: self.tree[index].lazy_tag = val # 新建懒惰标记 self.tree[index].val += val * interval_size # 区间和增加 return # 情况2:当前节点区间与 [q_left, q_right] 无交集,直接返回 if right < q_left or left > q_right: return # 情况3:部分重叠,先下推懒惰标记,再递归更新左右子区间 self.__pushdown(index) mid = left + (right - left) // 2 left_index = index * 2 + 1 right_index = index * 2 + 2 if q_left <= mid: self.__update_interval(q_left, q_right, val, left_index, left, mid) if q_right > mid: self.__update_interval(q_left, q_right, val, right_index, mid + 1, right) self.__pushup(index) # 回溯时更新当前节点的区间和 def __pushdown(self, index): """ 将当前节点的懒惰标记下推到左右子节点,并同步更新子节点的区间和 """ lazy_tag = self.tree[index].lazy_tag if lazy_tag is None: return left_index = index * 2 + 1 right_index = index * 2 + 2 # 处理左子节点 if self.tree[left_index].lazy_tag is not None: self.tree[left_index].lazy_tag += lazy_tag else: self.tree[left_index].lazy_tag = lazy_tag left_size = self.tree[left_index].right - self.tree[left_index].left + 1 self.tree[left_index].val += lazy_tag * left_size # 处理右子节点 if self.tree[right_index].lazy_tag is not None: self.tree[right_index].lazy_tag += lazy_tag else: self.tree[right_index].lazy_tag = lazy_tag right_size = self.tree[right_index].right - self.tree[right_index].left + 1 self.tree[right_index].val += lazy_tag * right_size # 清除当前节点的懒惰标记 self.tree[index].lazy_tag = None ``` ## 4. 总结 ### 4.1 核心要点 - 线段树通过对区间反复二分,让每个节点维护一个子区间的聚合信息(如和、最值)。 - 使用数组顺序存储,容量通常取约 $4 \times n$,查询 / 更新沿树高进行。 - 引入懒惰标记后,区间更新无需遍历到所有叶子,保持对数级复杂度。 - 聚合函数可配置(sum/max/min/自定义),需满足可结合性以支持自底向上合并。 ### 4.2 复杂度分析 | 操作 | 最优时间 | 最坏时间 | 平均时间 | 空间复杂度 | 稳定性 | |------|----------|----------|----------|------------|--------| | 构建 | $O(n)$ | $O(n)$ | $O(n)$ | $O(n)$ | 不涉及 | | 单点更新 | $O(\log n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$(递归为 $O(\log n)$ 栈) | 不涉及 | | 区间查询 | $O(\log n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$(递归为 $O(\log n)$ 栈) | 不涉及 | | 区间更新(含懒标) | $O(\log n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$(递归为 $O(\log n)$ 栈) | 不涉及 | 说明:如果不使用懒惰标记,区间更新在最坏情况下会退化为 $O(n)$。 ### 4.3 算法特点 - **优点**: - 区间查询与区间更新效率高(均为 $O(\log n)$)。 - 适配多种聚合函数,扩展性强。 - 支持动态数据的在线维护。 - **缺点**: - 实现复杂度与常数因子较大,代码易错。 - 对聚合函数有约束(需可结合),不适合不可结合的运算。 - 多维线段树实现复杂,内存与常数进一步增大;对于简单前缀和/仅单点更新的场景,树状数组往往更简洁高效。 ## 练习题目 - [线段树题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E7%BA%BF%E6%AE%B5%E6%A0%91%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】ACM-ICPC 程序设计系列 - 算法设计与实现 - 陈宇 吴昊 主编 - 【书籍】算法训练营 陈小玉 著 - 【博文】[史上最详细的线段树教程 - 知乎](https://zhuanlan.zhihu.com/p/34150142) - 【博文】[线段树 Segment Tree 实战 - halfrost](https://halfrost.com/segment_tree/) - 【博文】[线段树 - OI Wiki](https://oi-wiki.org/ds/seg/) - 【博文】[线段树的 python 实现 - 年糕的博客 - CSDN博客](https://blog.csdn.net/qq_33935895/article/details/102806357) - 【博文】[线段树 从入门到进阶 - Dijkstra·Liu - 博客园](https://www.cnblogs.com/dijkstra2003/p/9676729.html) ================================================ FILE: docs/05_tree/05_06_segment_tree_02.md ================================================ ## 1. 线段树常见题型 线段树是一种高效的数据结构,常用于处理区间相关的查询与修改。以下是线段树常见的几类题型及其简要说明: ### 1.1 区间最大 / 最小值查询(RMQ) > **RMQ(Range Maximum / Minimum Query)问题**:给定长度为 $n$ 的数组 $nums$,多次询问区间 $[q_{left}, q_{right}]$ 内的最大值或最小值。 假设有 $q$ 次查询,朴素算法每次需遍历区间,整体时间复杂度为 $O(q \times n)$;而采用线段树后,每次查询仅需 $O(\log n)$,总复杂度降为 $O(q \times \log n)$,大大提升了效率。 ### 1.2 单点更新与区间查询 > **单点更新与区间查询问题**: > > 1. 支持对数组中某一元素进行修改(单点更新)。 > 2. 支持查询任意区间 $[q_{left}, q_{right}]$ 的聚合值(如区间和、最大/最小值等)。 这类问题直接使用「5.5 线段树(一)」中的「3.1 单点更新」和「3.2 区间查询」即可解决。 ### 1.3 区间更新与区间查询 > **区间更新与区间查询问题**: > > 1. 支持对某一连续区间的所有元素进行批量修改(区间更新)。 > 2. 支持查询任意区间 $[q_{left}, q_{right}]$ 的聚合值(如区间和、最大/最小值等)。 此类问题直接使用「5.5 线段树(一)」中的「3.3 区间更新」与「3.2 区间查询」即可解决。 ### 1.4 区间合并与区间查询 > **区间合并与区间查询问题**: > > 1. 支持对某一连续区间的所有元素进行批量修改(区间更新)。 > 2. 支持查询区间 $[q_{left}, q_{right}]$ 内,满足特定条件的连续最长子区间(如最长连续 1、最长连续递增/递减等)。 这类问题在解决时,需在「5.5 线段树(一)」中「3.3 区间更新」和「3.2 区间查询」的基础上,扩展每个节点维护的信息。例如,节点需额外记录区间内的前缀/后缀/最大连续长度等统计量。在向上合并时,需根据左右子节点的这些信息进行合并计算,从而支持高效的区间合并与查询操作。 ### 1.5 扫描线问题 > **扫描线问题**:通过模拟一条虚拟的扫描线(通常为垂直或水平线)在平面上移动,动态处理与其相交的几何对象,从而高效解决如图形面积、周长等几何统计问题。 > > 核心思想是:让扫描线从一端出发,依次经过所有关键事件点(如矩形的边界),每到一个事件点时,更新与扫描线相交的区间集合,并据此统计所需信息。随着扫描线的推进,所有对象都被处理,最终得到完整解答。 这类问题往往涉及大范围的坐标区间,因此通常需要对坐标进行离散化(如将 $y$ 坐标映射为 $0, 1, 2, \ldots$),以便用线段树等数据结构高效维护区间信息。具体做法是:将每条竖线(或水平线)的端点作为区间边界,利用线段树动态维护区间的覆盖情况(如 $x$ 坐标、左/右边界等),在扫描过程中实时合并区间并统计相关量(如总覆盖长度、重叠次数等)。 ## 2. 线段树的拓展 ### 2.1 动态开点线段树 在某些场景下,线段树需要维护的区间范围极大(如 $[1, 10^9]$),但实际被访问和修改的节点却非常有限。 如果仍采用传统的数组实现方式,则需要分配 $4 \times n$ 的空间,导致空间浪费严重,效率低下。 为了解决这一问题,可以采用 **动态开点** 的线段树实现思路: - 初始时仅创建一个根节点,表示整个区间。 - 只有在访问或修改到某个子区间时,才动态地为该区间分配节点。 这种方式极大地节省了空间,仅为实际需要的区间分配内存,适合处理稀疏访问、超大区间的问题。 动态开点线段树的基本实现如下: ```python # 动态开点线段树节点类 class TreeNode: def __init__(self, left=-1, right=-1, val=0): self.left = left # 区间左边界 self.right = right # 区间右边界 self.mid = left + (right - left) // 2 # 区间中点 self.leftNode = None # 左子节点 self.rightNode = None # 右子节点 self.val = val # 区间聚合值 self.lazy_tag = None # 懒惰标记(延迟更新) # 动态开点线段树 class SegmentTree: def __init__(self, function): self.tree = TreeNode(0, int(1e9)) # 根节点,维护区间 [0, 1e9] self.function = function # 区间聚合函数(如 sum, max, min) def __pushup(self, node): """ 向上更新当前节点的区间值,由左右子节点聚合得到 """ leftNode = node.leftNode rightNode = node.rightNode if leftNode and rightNode: node.val = self.function(leftNode.val, rightNode.val) elif leftNode: node.val = leftNode.val elif rightNode: node.val = rightNode.val # 如果左右子节点都不存在,val 保持不变 def update_point(self, i, val): """ 单点更新:将下标 i 的元素修改为 val """ self.__update_point(i, val, self.tree) def __update_point(self, i, val, node): """ 递归实现单点更新 """ if node.left == node.right: node.val = val # 叶子节点,直接赋值 node.lazy_tag = None # 清除懒惰标记 return self.__pushdown(node) # 下推懒惰标记,保证更新正确 if i <= node.mid: if not node.leftNode: node.leftNode = TreeNode(node.left, node.mid) self.__update_point(i, val, node.leftNode) else: if not node.rightNode: node.rightNode = TreeNode(node.mid + 1, node.right) self.__update_point(i, val, node.rightNode) self.__pushup(node) # 向上更新 def query_interval(self, q_left, q_right): """ 区间查询:[q_left, q_right] 区间的聚合值 """ return self.__query_interval(q_left, q_right, self.tree) def __query_interval(self, q_left, q_right, node): """ 递归实现区间查询 """ if node.left > q_right or node.right < q_left: # 当前节点区间与查询区间无交集 return 0 if node.left >= q_left and node.right <= q_right: # 当前节点区间被查询区间完全覆盖 return node.val self.__pushdown(node) # 下推懒惰标记 res_left = 0 res_right = 0 if q_left <= node.mid: if not node.leftNode: node.leftNode = TreeNode(node.left, node.mid) res_left = self.__query_interval(q_left, q_right, node.leftNode) if q_right > node.mid: if not node.rightNode: node.rightNode = TreeNode(node.mid + 1, node.right) res_right = self.__query_interval(q_left, q_right, node.rightNode) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 def update_interval(self, q_left, q_right, val): """ 区间更新:将 [q_left, q_right] 区间内所有元素增加 val """ self.__update_interval(q_left, q_right, val, self.tree) def __update_interval(self, q_left, q_right, val, node): """ 递归实现区间更新(区间加法) """ if node.left > q_right or node.right < q_left: # 当前节点区间与更新区间无交集 return if node.left >= q_left and node.right <= q_right: # 当前节点区间被更新区间完全覆盖 interval_size = node.right - node.left + 1 if node.lazy_tag is not None: node.lazy_tag += val else: node.lazy_tag = val node.val += val * interval_size return self.__pushdown(node) # 下推懒惰标记 if q_left <= node.mid: if not node.leftNode: node.leftNode = TreeNode(node.left, node.mid) self.__update_interval(q_left, q_right, val, node.leftNode) if q_right > node.mid: if not node.rightNode: node.rightNode = TreeNode(node.mid + 1, node.right) self.__update_interval(q_left, q_right, val, node.rightNode) self.__pushup(node) # 向上更新 def __pushdown(self, node): """ 懒惰标记下推:将当前节点的延迟更新传递给左右子节点 """ if node.lazy_tag is None: return # 动态创建左右子节点 if not node.leftNode: node.leftNode = TreeNode(node.left, node.mid) if not node.rightNode: node.rightNode = TreeNode(node.mid + 1, node.right) # 更新左子节点 left_size = node.leftNode.right - node.leftNode.left + 1 if node.leftNode.lazy_tag is not None: node.leftNode.lazy_tag += node.lazy_tag else: node.leftNode.lazy_tag = node.lazy_tag node.leftNode.val += node.lazy_tag * left_size # 更新右子节点 right_size = node.rightNode.right - node.rightNode.left + 1 if node.rightNode.lazy_tag is not None: node.rightNode.lazy_tag += node.lazy_tag else: node.rightNode.lazy_tag = node.lazy_tag node.rightNode.val += node.lazy_tag * right_size node.lazy_tag = None # 清除当前节点的懒惰标记 ``` ## 练习题目 - [线段树题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E7%BA%BF%E6%AE%B5%E6%A0%91%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】ACM-ICPC 程序设计系列 - 算法设计与实现 - 陈宇 吴昊 主编 - 【书籍】算法训练营 陈小玉 著 - 【博文】[史上最详细的线段树教程 - 知乎](https://zhuanlan.zhihu.com/p/34150142) - 【博文】[线段树 Segment Tree 实战 - halfrost](https://halfrost.com/segment_tree/) - 【博文】[线段树 - OI Wiki](https://oi-wiki.org/ds/seg/) - 【博文】[线段树的 python 实现 - 年糕的博客 - CSDN博客](https://blog.csdn.net/qq_33935895/article/details/102806357) - 【博文】[线段树 从入门到进阶 - Dijkstra·Liu - 博客园](https://www.cnblogs.com/dijkstra2003/p/9676729.html) ================================================ FILE: docs/05_tree/05_07_binary_indexed_tree.md ================================================ ## 1. 树状数组简介 ### 1.1 树状数组的定义 > **树状数组(Binary Indexed Tree)**:也因其发明者命名为 Fenwick 树,最早 Peter M. Fenwick 于 1994 年以 A New Data Structure for Cumulative Frequency Tables 为题发表在 SOFTWARE PRACTICE AND EXPERIENCE。其初衷是解决数据压缩里的累积频率(Cumulative Frequency)的计算问题,现多用于高效计算数列的前缀和,区间和。它可以以 $O(\log n)$ 的时间得到任意前缀 $\sum_{i=1}^{j}A[i], 1 \le j \le n$,并同时支持在 $O(\log n)$ 时间内支持动态单点值的修改。空间复杂度为 $O(n)$。 ### 1.2 树状数组的原理 树状数组的核心思想是利用二进制数的特性,将前缀和分解成多个子区间的和。具体来说: 1. 每个节点存储的是以该节点为根的子树中所有节点的和 2. 对于任意一个数 $x$,其二进制表示中最低位的 1 所在的位置决定了该节点在树中的层级 3. 通过位运算可以快速找到需要更新的节点和需要求和的区间 例如,对于数组 $[1, 2, 3, 4, 5]$,其树状数组的结构如下: ``` [15] / \ [3] [12] / \ / \ [1] [2] [3] [9] / \ [4] [5] ``` ## 2. 树状数组的基本操作 ### 2.1 树状数组的建立 ```python class BinaryIndexedTree: def __init__(self, n): self.n = n self.tree = [0] * (n + 1) def lowbit(self, x): return x & (-x) def build(self, arr): for i in range(len(arr)): self.update(i + 1, arr[i]) ``` ### 2.2 树状数组的修改 ```python def update(self, index, val): while index <= self.n: self.tree[index] += val index += self.lowbit(index) ``` ### 2.3 树状数组的求和 ```python def query(self, index): res = 0 while index > 0: res += self.tree[index] index -= self.lowbit(index) return res ``` ## 3. 树状数组的应用 ### 3.1 单点更新 + 区间求值 这是树状数组最基本的应用,可以高效地: 1. 修改某个位置的值 2. 查询任意区间的和 时间复杂度: - 单点更新:$O(\log n)$ - 区间查询:$O(\log n)$ 具体实现: ```python class BinaryIndexedTree: def __init__(self, n): self.n = n self.tree = [0] * (n + 1) def lowbit(self, x): return x & (-x) def update(self, index, val): while index <= self.n: self.tree[index] += val index += self.lowbit(index) def query(self, index): res = 0 while index > 0: res += self.tree[index] index -= self.lowbit(index) return res def query_range(self, left, right): return self.query(right) - self.query(left - 1) # 使用示例 def example_single_point_update(): # 初始化数组 [1, 2, 3, 4, 5] arr = [1, 2, 3, 4, 5] n = len(arr) bit = BinaryIndexedTree(n) # 构建树状数组 for i in range(n): bit.update(i + 1, arr[i]) # 单点更新:将第3个元素加2 bit.update(3, 2) # arr[2] += 2 # 查询区间和:查询[2,4]的和 sum_range = bit.query_range(2, 4) print(f"区间[2,4]的和为:{sum_range}") # 输出:区间[2,4]的和为:11 ``` ### 3.2 区间更新 + 单点求值 通过差分数组的思想,可以实现: 1. 区间更新:将区间 $[l, r]$ 的所有值加上 $val$ 2. 单点查询:查询某个位置的值 时间复杂度: - 区间更新:$O(\log n)$ - 单点查询:$O(\log n)$ 具体实现: ```python class RangeUpdateBIT: def __init__(self, n): self.n = n self.tree = [0] * (n + 1) def lowbit(self, x): return x & (-x) def update(self, index, val): while index <= self.n: self.tree[index] += val index += self.lowbit(index) def query(self, index): res = 0 while index > 0: res += self.tree[index] index -= self.lowbit(index) return res def range_update(self, left, right, val): # 在left位置加上val self.update(left, val) # 在right+1位置减去val self.update(right + 1, -val) # 使用示例 def example_range_update(): # 初始化数组 [0, 0, 0, 0, 0] n = 5 bit = RangeUpdateBIT(n) # 区间更新:[2,4]区间所有元素加3 bit.range_update(2, 4, 3) # 单点查询:查询第3个元素的值 value = bit.query(3) print(f"第3个元素的值为:{value}") # 输出:第3个元素的值为:3 ``` ### 3.3 求逆序对数 利用树状数组可以高效求解数组中的逆序对数量: 1. 对数组进行离散化处理 2. 从后向前遍历,统计每个数前面比它大的数的个数 3. 将统计结果累加得到逆序对总数 时间复杂度:$O(n \log n)$ 具体实现: ```python class InversionCountBIT: def __init__(self, n): self.n = n self.tree = [0] * (n + 1) def lowbit(self, x): return x & (-x) def update(self, index, val): while index <= self.n: self.tree[index] += val index += self.lowbit(index) def query(self, index): res = 0 while index > 0: res += self.tree[index] index -= self.lowbit(index) return res def count_inversions(self, arr): # 离散化处理 sorted_arr = sorted(set(arr)) rank = {val: i + 1 for i, val in enumerate(sorted_arr)} # 从后向前遍历,统计逆序对 count = 0 for i in range(len(arr) - 1, -1, -1): # 查询当前数前面比它大的数的个数 count += self.query(rank[arr[i]] - 1) # 更新当前数的出现次数 self.update(rank[arr[i]], 1) return count # 使用示例 def example_inversion_count(): # 测试数组 [5, 2, 6, 1, 3] arr = [5, 2, 6, 1, 3] n = len(arr) bit = InversionCountBIT(n) # 计算逆序对数量 inversions = bit.count_inversions(arr) print(f"数组中的逆序对数量为:{inversions}") # 输出:数组中的逆序对数量为:6 ``` ## 参考资料 - 【书籍】ACM-ICPC 程序设计系列 - 算法设计与实现 - 陈宇 吴昊 主编 - 【书籍】算法训练营 陈小玉 著 - 【博文】[聊聊树状数组 Binary Indexed Tree - halfrost](https://halfrost.com/binary_indexed_tree/) - 【博文】[树状数组学习笔记 - AcWing](https://www.acwing.com/blog/content/80/) ================================================ FILE: docs/05_tree/05_08_union_find.md ================================================ ## 1. 并查集简介 ### 1.1 并查集的定义 > **并查集(Union Find)**:一种高效的数据结构,常用于处理若干不相交集合(Disjoint Sets)的合并与查询操作。不相交集合指的是元素互不重叠的集合族。 > > 并查集主要支持两类核心操作: > > - **合并(Union)**:将两个不同的集合合并为一个集合。 > - **查找(Find)**:确定某个元素属于哪个集合,通常返回该集合的「代表元素」。 简而言之,并查集用于高效地管理集合的合并与成员归属查询。 - 并查集中的「集」指的是不相交的集合,即元素互不重复、互不重叠的若干集合。 - 并查集中的「并」指的是集合的并集操作,即将两个不同的集合合并为一个更大的集合。合并操作如下: ```python {1, 3, 5, 7} U {2, 4, 6, 8} = {1, 2, 3, 4, 5, 6, 7, 8} ``` - 并查集中的「查」操作,主要用于判断两个元素是否属于同一个集合。 如果只是判断某个元素是否在集合中,直接用 Python 的 `set` 类型即可。但如果要 **高效判断两个元素是否属于同一集合**,`set` 就不适合了,因为它只能判断单个元素是否存在,无法快速判断两个元素是否在同一个集合里,往往需要遍历所有集合,效率很低。此时,就需要用专门的并查集结构,才能高效地支持集合的合并和连通性查询。 基于上述需求,并查集通常支持以下核心操作接口: - **合并 `union(x, y)`**:将包含元素 $x$ 和 $y$ 的两个集合合并为一个集合。 - **查找 `find(x)`**:查找元素 $x$ 所在集合的代表元素(根节点)。 - **连通性判断 `is_connected(x, y)`**:判断元素 $x$ 和 $y$ 是否属于同一个集合。 ### 1.2 并查集的两种实现思路 并查集常见的两种实现方式分别侧重于不同操作的效率:一种是「快速查询」——基于数组结构,另一种是「快速合并」——基于森林结构。 #### 1.2.1 快速查询:基于数组实现 当我们更关注查询操作的效率时,可以采用基于数组的实现方式。 在这种实现中,使用一个数组来表示每个元素所属的集合。数组的下标代表元素本身,数组的值($id$)表示该元素所在集合的编号。具体操作如下: - **初始化**:将每个元素的集合编号设为其自身的下标,即每个元素自成一个集合。 - **合并操作**:将一个集合中的所有元素的 $id$ 修改为另一个集合的 $id$,从而实现集合的合并。这样,合并后同一集合内所有元素的 $id$ 都相同。 - **查找操作**:直接比较两个元素的 $id$ 是否相同,如果相同则属于同一集合,否则属于不同集合。 举例说明,假设有集合 $\left\{ 0 \right\}, \left\{ 1 \right\}, \left\{ 2 \right\}, \left\{ 3 \right\}, \left\{ 4 \right\}, \left\{ 5 \right\}, \left\{ 6 \right\}, \left\{ 7 \right\}$,初始化如下: ![基于数组实现:初始化操作](https://qcdn.itcharge.cn/images/20240513150949.png) 如上图所示,数组下标即为元素编号,初始时每个元素单独成集。 经过若干次合并操作后,例如合并成 $\left\{ 0 \right\}, \left\{ 1, 2, 3 \right\}, \left\{ 4 \right\}, \left\{5, 6\right\}, \left\{ 7 \right\}$,结果如下: ![基于数组实现:合并操作](https://qcdn.itcharge.cn/images/20240513151310.png) 可以看到,$1$、$2$、$3$ 的 $id$ 相同,说明它们属于同一集合;$5$ 和 $6$ 也同理。 这种实现方式下,查询操作的时间复杂度为 $O(1)$,但合并操作的时间复杂度为 $O(n)$(每次合并都需遍历整个数组)。因此,虽然查询极快,但合并效率较低,实际应用中较少采用。 - 基于「快速查询」思路的并查集代码如下: ```python class UnionFind: def __init__(self, n): """ 初始化并查集,将每个元素的集合编号初始化为其自身下标。 :param n: 元素总数 """ self.ids = [i for i in range(n)] # ids[i] 表示元素 i 所在集合的编号 def find(self, x): """ 查找元素 x 所在集合的编号。 :param x: 元素编号 :return: x 所在集合的编号 """ return self.ids[x] def union(self, x, y): """ 合并包含元素 x 和 y 的两个集合。 :param x: 元素 x :param y: 元素 y :return: 如果 x 和 y 原本就在同一集合,返回 False;否则合并并返回 True """ x_id = self.find(x) y_id = self.find(y) if x_id == y_id: # x 和 y 已经在同一个集合,无需合并 return False # 遍历所有元素,将属于 y_id 集合的元素编号改为 x_id,实现合并 for i in range(len(self.ids)): if self.ids[i] == y_id: self.ids[i] = x_id return True def is_connected(self, x, y): """ 判断元素 x 和 y 是否属于同一个集合。 :param x: 元素 x :param y: 元素 y :return: 如果属于同一集合返回 True,否则返回 False """ return self.find(x) == self.find(y) ``` #### 1.2.2 快速合并:基于森林实现 在「快速查询」的实现方式中,合并操作效率较低,因此我们需要优化合并操作的性能。 为此,可以采用「森林结构」来实现并查集。具体做法是:用若干棵树(即森林)来表示所有集合,每棵树代表一个集合,树中的每个节点对应一个元素,树的根节点即为该集合的代表元素。 > **注意**:与常规树结构(父节点指向子节点)不同,基于森林的并查集中,每个节点都指向其父节点。 我们可以用一个数组 $fa$ 来维护森林结构,其中 $fa[x]$ 表示元素 $x$ 的父节点编号。也就是说,$x$ 通过 $fa[x]$ 指向其父节点。 - **初始化**:令 $fa[x] = x$,即每个元素自成一个集合,自己是自己的根节点。 - **合并操作**:将两个集合的根节点相连,例如令 $fa[root1] = root2$,即把 $root1$ 所在集合合并到 $root2$ 所在集合。 - **查找操作**:从某个元素出发,沿着 $fa$ 数组不断查找其父节点,直到找到根节点。如果两个元素的根节点相同,则它们属于同一集合,否则属于不同集合。 举例说明,假设有集合 $\left\{0\right\}, \left\{1\right\}, \left\{2\right\}, \left\{3\right\}, \left\{4\right\}, \left\{5\right\}, \left\{6\right\}, \left\{7\right\}$,初始化时如下图: ![基于森林实现:初始化操作](https://qcdn.itcharge.cn/images/20240513151548.png) 从上图中可以看出:$fa$ 数组的每个下标索引值对应一个元素的集合编号,代表着每个元素属于一个集合。 接下来,依次执行 `union(4, 5)`、`union(6, 7)`、`union(4, 7)`,最终集合变为 $\left\{0\right\}, \left\{1\right\}, \left\{2\right\}, \left\{3\right\}, \left\{4, 5, 6, 7\right\}$,具体步骤如下: ::: tabs#union @tab <1> - 合并 $(4, 5)$:将 $4$ 的根节点指向 $5$,即 $fa[4] = 5$。 ![基于森林实现:合并操作 1](https://qcdn.itcharge.cn/images/20240513154015.png) @tab <2> - 合并 $(6, 7)$:将 $6$ 的根节点指向 $7$,即 $fa[6] = 7$。 ![基于森林实现:合并操作 2](https://qcdn.itcharge.cn/images/20240513154022.png) @tab <3> - 合并 $(4, 7)$:将 $4$ 的根节点(即 $fa[4] = 5$)指向 $7$,即 $fa[fa[4]] = fa[5] = 7$。 ![基于森林实现:合并操作 3](https://qcdn.itcharge.cn/images/20240513154030.png) ::: 可以看到,经过上述合并后,$4$、$5$、$6$、$7$ 的根节点编号都为 $7$,说明它们已经属于同一个集合。 - 基于「快速合并」思想的并查集代码如下: ```python class UnionFind: def __init__(self, n): """ 初始化并查集,将每个元素的父节点初始化为自身 :param n: 元素个数 """ self.fa = [i for i in range(n)] # fa[x] 表示 x 的父节点,初始时每个节点自成一个集合 def find(self, x): """ 查找元素 x 所在集合的根节点(代表元) :param x: 待查找的元素 :return: x 所在集合的根节点编号 """ # 循环查找父节点,直到找到根节点(fa[x] == x) while self.fa[x] != x: x = self.fa[x] return x def union(self, x, y): """ 合并 x 和 y 所在的两个集合 :param x: 元素 x :param y: 元素 y :return: 如果 x 和 y 原本属于同一集合,返回 False;否则合并并返回 True """ root_x = self.find(x) root_y = self.find(y) if root_x == root_y: # x 和 y 已经在同一个集合中,无需合并 return False self.fa[root_x] = root_y # 将 x 的根节点连接到 y 的根节点 return True def is_connected(self, x, y): """ 判断 x 和 y 是否属于同一个集合 :param x: 元素 x :param y: 元素 y :return: 如果属于同一集合返回 True,否则返回 False """ return self.find(x) == self.find(y) ``` ## 2. 路径压缩 当集合规模较大或树结构极度不平衡时,单纯依赖「快速合并」的并查集实现效率较低。在最坏情况下,树会退化为一条链,此时单次查找操作的时间复杂度为 $O(n)$,如下图所示: ![并查集最坏情况](https://qcdn.itcharge.cn/images/20240513154732.png) 为了提升效率、避免上述最坏情况,常用的优化手段是「路径压缩」。 > **路径压缩(Path Compression)**:在查找根节点的过程中,将路径上经过的所有节点尽量直接挂到根节点下,从而显著降低树的高度,提高后续操作的效率。 路径压缩主要有两种常见实现方式:一种是「隔代压缩」,另一种是「完全压缩」。 ### 2.1 隔代压缩 > **隔代压缩**:在查找操作时,每次将当前节点直接连接到其父节点的父节点(即跳过一层),通过不断重复这一过程,有效降低树的高度,从而提升并查集的查找效率。 如下图所示,展示了隔代压缩的过程: ![路径压缩:隔代压缩](https://qcdn.itcharge.cn/images/20240513154745.png) 隔代压缩的查找代码如下: ```python def find(self, x): """ 查找元素 x 所在集合的根节点(带隔代路径压缩) :param x: 待查找的元素 :return: x 所在集合的根节点编号 """ while self.fa[x] != x: # 将 x 的父节点直接指向其祖父节点,实现隔代压缩 self.fa[x] = self.fa[self.fa[x]] x = self.fa[x] # 继续向上查找 return x # 返回根节点编号 ``` ### 2.2 完全压缩 > **完全压缩**:在查找操作时,将从当前节点到根节点路径上的所有节点的父节点都直接指向根节点,从而极大地降低树的高度。这样,后续对这些节点的查找都能一步到达根节点,显著提升效率。 与「隔代压缩」相比,「完全压缩」能够更彻底地扁平化树结构。如下图所示: ![路径压缩:完全压缩](https://qcdn.itcharge.cn/images/20240513154759.png) 完全压缩的查找代码如下: ```python def find(self, x): """ 查找元素 x 所在集合的根节点(带完全路径压缩) :param x: 待查找的元素 :return: x 所在集合的根节点编号 """ if self.fa[x] != x: # 如果 x 不是根节点,递归查找其父节点 self.fa[x] = self.find(self.fa[x]) # 路径压缩:将 x 直接连接到根节点 return self.fa[x] # 返回根节点编号 ``` ## 3. 按秩合并 虽然路径压缩能够有效降低树的高度,但它只在查找操作时生效,且仅影响当前查找路径上的节点。因此,如果仅依赖路径压缩,整个并查集的结构仍可能出现较高的树。为进一步优化并查集的结构,常用的另一种方法是「按秩合并」。 > **按秩合并(Union By Rank)**:在每次合并操作时,总是将「秩」较小的树的根节点连接到「秩」较大的树的根节点下。 这里的「秩」可以有两种常见定义:一种是树的深度,另一种是集合的大小(即节点个数)。无论采用哪种定义,秩的信息都只需记录在每棵树的根节点上。 按秩合并主要有两种实现方式:一种是「按深度合并」,另一种是「按大小合并」。 ### 3.1 按深度合并 > **按深度合并(Union By Rank)**:每次合并时,将「深度」较小的树的根节点指向「深度」较大的树的根节点。 具体做法是,使用一个数组 $rank$ 记录每个根节点对应的树的深度(非根节点的 $rank$ 值无实际意义,仅根节点有效)。 初始化时,所有元素的 $rank$ 值设为 $1$。合并时,比较两个集合根节点的 $rank$,将 $rank$ 较小的根节点指向 $rank$ 较大的根节点。如果两棵树深度相同,任选一方作为新根,并将其 $rank$ 加 $1$。 如下图所示为「按深度合并」的示意: ![按秩合并:按深度合并](https://qcdn.itcharge.cn/images/20240513154814.png) 按深度合并的实现代码如下: ```python class UnionFind: def __init__(self, n): """ 初始化并查集 :param n: 元素个数 """ self.fa = [i for i in range(n)] # fa[i] 表示元素 i 的父节点,初始时每个元素自成一个集合 self.rank = [1 for _ in range(n)] # rank[i] 表示以 i 为根的树的深度,初始为 1 def find(self, x): """ 查找元素 x 所在集合的根节点(带路径压缩,隔代压缩) :param x: 待查找的元素 :return: x 所在集合的根节点编号 """ while self.fa[x] != x: # 如果 x 不是根节点,继续查找其父节点 self.fa[x] = self.fa[self.fa[x]]# 路径压缩:将 x 直接连接到祖父节点,实现隔代压缩 x = self.fa[x] return x # 返回根节点编号 def union(self, x, y): """ 合并操作:将 x 和 y 所在的集合合并 :param x: 元素 x :param y: 元素 y :return: 如果合并成功返回 True,如果已在同一集合返回 False """ root_x = self.find(x) root_y = self.find(y) if root_x == root_y: # x 和 y 已经在同一个集合 return False # 按秩合并:将深度较小的树合并到深度较大的树下 if self.rank[root_x] < self.rank[root_y]: self.fa[root_x] = root_y # x 的根节点连接到 y 的根节点 elif self.rank[root_x] > self.rank[root_y]: self.fa[root_y] = root_x # y 的根节点连接到 x 的根节点 else: self.fa[root_x] = root_y # 深度相同,任选一方作为新根 self.rank[root_y] += 1 # 新根的深度加 1 return True def is_connected(self, x, y): """ 查询操作:判断 x 和 y 是否属于同一个集合 :param x: 元素 x :param y: 元素 y :return: 如果属于同一集合返回 True,否则返回 False """ return self.find(x) == self.find(y) ``` ### 3.2 按大小合并 > **按大小合并(Union By Size)**:此处的「大小」指的是集合中节点的数量。每次合并时,总是将节点数较少的集合的根节点指向节点数较多的集合的根节点,从而有效控制树的高度。 具体做法是,使用一个数组 $size$ 记录每个根节点所代表集合的节点个数(对于非根节点,$size$ 的值无实际意义,仅根节点的 $size$ 有效)。 初始化时,所有元素各自为一个集合,因此 $size$ 均为 $1$。合并操作时,先分别找到两个元素的根节点,比较它们的 $size$,将较小集合的根节点连接到较大集合的根节点,并更新新根节点的 $size$。 如下图所示为按大小合并的示意: ![按秩合并:按大小合并](https://qcdn.itcharge.cn/images/20240513154835.png) 按大小合并的实现代码如下: ```python class UnionFind: def __init__(self, n): """ 初始化并查集 :param n: 元素个数 """ self.fa = [i for i in range(n)] # fa[i] 表示元素 i 的父节点,初始时每个元素自成一个集合 self.size = [1 for _ in range(n)] # size[i] 表示以 i 为根的集合的元素个数,初始为 1 def find(self, x): """ 查找元素 x 所在集合的根节点(带隔代路径压缩) :param x: 待查找的元素 :return: x 所在集合的根节点编号 """ while self.fa[x] != x: self.fa[x] = self.fa[self.fa[x]] # 隔代路径压缩,将 x 直接连接到祖父节点 x = self.fa[x] return x def union(self, x, y): """ 合并操作:将 x 和 y 所在的集合合并(按集合大小合并) :param x: 元素 x :param y: 元素 y :return: 如果合并成功返回 True,如果已在同一集合返回 False """ root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return False # x 和 y 已经在同一个集合,无需合并 # 按集合大小合并:小集合合并到大集合 if self.size[root_x] < self.size[root_y]: self.fa[root_x] = root_y self.size[root_y] += self.size[root_x] elif self.size[root_x] > self.size[root_y]: self.fa[root_y] = root_x self.size[root_x] += self.size[root_y] else: # 集合大小相等,任选一方作为新根 self.fa[root_x] = root_y self.size[root_y] += self.size[root_x] return True def is_connected(self, x, y): """ 查询操作:判断 x 和 y 是否属于同一个集合 :param x: 元素 x :param y: 元素 y :return: 如果属于同一集合返回 True,否则返回 False """ return self.find(x) == self.find(y) ``` ### 3.3 按秩合并的注意点 很多同学会疑惑:再路径压缩时,为什么不用更新 $rank$ 或 $size$? 其实,路径压缩后,$rank$ 和 $size$ 已经不再代表真实的树高或集合大小。它们只是合并时用来比较「谁大谁小」的辅助标记,只在合并操作时起作用。 换句话说,我们不需要关心每个节点的真实深度或集合元素个数,只要 $rank$ 或 $size$ 能正确反映两个集合的相对大小即可。 此外,路径压缩只会让树变矮,$rank$ 或 $size$ 只会增加,不会减少。因此,它们足以作为合并时的比较依据,无需在路径压缩时维护真实值。 ## 4. 并查集的算法分析 - **时间复杂度**:在同时使用「路径压缩」和「按秩合并」优化后,合并(union)和查找(find)操作的均摊时间复杂度非常接近 $O(1)$。更精确地说,$m$ 次操作的总时间复杂度为 $O(m \times \alpha(n))$,其中 $\alpha(n)$ 是阿克曼函数的反函数,增长极其缓慢,实际应用中可视为常数。 - **空间复杂度**:主要由数组 $fa$(父节点数组)构成,如果采用「按秩合并」优化,还需额外的 $rank$ 或 $size$ 数组。整体空间复杂度为 $O(n)$,其中 $n$ 为元素个数。 ## 5. 并查集的推荐实现方式 结合实际刷题和主流经验,推荐并查集的实现策略如下:优先采用「隔代压缩」优化,一般情况下无需引入「按秩合并」。 这种做法的优势在于代码简洁、易于实现,同时性能表现也非常优秀。只有在遇到性能瓶颈时,再考虑引入「按秩合并」进一步优化。 此外,如果题目需要支持查询集合数量或集合内元素个数等功能,可根据具体需求对实现进行适当扩展。 ::: tabs#bubble @tab <1> 采用「隔代压缩」且不使用「按秩合并」的并查集实现代码: ```python class UnionFind: def __init__(self, n): # 初始化 self.fa = [i for i in range(n)] # 每个元素的集合编号初始化为数组 fa 的下标索引 def find(self, x): # 查找元素根节点的集合编号内部实现方法 while self.fa[x] != x: # 递归查找元素的父节点,直到根节点 self.fa[x] = self.fa[self.fa[x]] # 隔代压缩优化 x = self.fa[x] return x # 返回元素根节点的集合编号 def union(self, x, y): # 合并操作:令其中一个集合的树根节点指向另一个集合的树根节点 root_x = self.find(x) root_y = self.find(y) if root_x == root_y: # x 和 y 的根节点集合编号相同,说明 x 和 y 已经同属于一个集合 return False self.fa[root_x] = root_y # x 的根节点连接到 y 的根节点上,成为 y 的根节点的子节点 return True def is_connected(self, x, y): # 查询操作:判断 x 和 y 是否同属于一个集合 return self.find(x) == self.find(y) ``` @tab <2> 使用「隔代压缩」,使用「按秩合并」的并查集最终实现代码: ```python class UnionFind: def __init__(self, n): # 初始化 self.fa = [i for i in range(n)] # 每个元素的集合编号初始化为数组 fa 的下标索引 self.rank = [1 for i in range(n)] # 每个元素的深度初始化为 1 def find(self, x): # 查找元素根节点的集合编号内部实现方法 while self.fa[x] != x: # 递归查找元素的父节点,直到根节点 self.fa[x] = self.fa[self.fa[x]] # 隔代压缩优化 x = self.fa[x] return x # 返回元素根节点的集合编号 def union(self, x, y): # 合并操作:令其中一个集合的树根节点指向另一个集合的树根节点 root_x = self.find(x) root_y = self.find(y) if root_x == root_y: # x 和 y 的根节点集合编号相同,说明 x 和 y 已经同属于一个集合 return False if self.rank[root_x] < self.rank[root_y]: # x 的根节点对应的树的深度 小于 y 的根节点对应的树的深度 self.fa[root_x] = root_y # x 的根节点连接到 y 的根节点上,成为 y 的根节点的子节点 elif self.rank[root_x] > self.rank[root_y]: # x 的根节点对应的树的深度 大于 y 的根节点对应的树的深度 self.fa[root_y] = root_x # y 的根节点连接到 x 的根节点上,成为 x 的根节点的子节点 else: # x 的根节点对应的树的深度 等于 y 的根节点对应的树的深度 self.fa[root_x] = root_y # 向任意一方合并即可 self.rank[root_y] += 1 # 因为层数相同,被合并的树必然层数会 +1 return True def is_connected(self, x, y): # 查询操作:判断 x 和 y 是否同属于一个集合 return self.find(x) == self.find(y) ``` ::: ## 6. 并查集的应用 并查集通常用来求解不同元素之间的关系问题,比如判断两个人是否是亲戚关系、两个点之间时候存在至少一条路径连接。或者用来求解集合的个数、集合中元素的个数等等。 ### 6.1 等式方程的可满足性 #### 6.1.1 题目链接 - [990. 等式方程的可满足性 - 力扣(LeetCode)](https://leetcode.cn/problems/satisfiability-of-equality-equations/) #### 6.1.2 题目大意 **描述**:给定一个由字符串方程组成的数组 $equations$,每个字符串方程 $equations[i]$ 的长度为 $4$,有以下两种形式组成:`a==b` 或 `a!=b`。$a$ 和 $b$ 是小写字母,表示单字母变量名。 **要求**:判断所有的字符串方程是否能同时满足,如果能同时满足,返回 $True$,否则返回 $False$。 **说明**: - $1 \le equations.length \le 500$。 - $equations[i].length == 4$。 - $equations[i][0]$ 和 $equations[i][3]$ 是小写字母。 - $equations[i][1]$ 要么是 `'='`,要么是 `'!'`。 - $equations[i][2]$ 是 `'='`。 **示例**: ```python 输入:["a==b","b!=a"] 输出:False 解释:如果我们指定,a = 1 且 b = 1 那么可以满足第一个方程,但无法满足第二个方程。 没有办法分配变量同时满足这两个方程。 ``` #### 6.1.3 解题思路 由于字符串方程仅包含 `==` 或 `!=` 两种形式,我们可以将所有等式(`==`)的变量归为同一个集合,然后再检查所有不等式(`!=`)的变量是否被错误地划分到了同一集合中。如果出现这种情况,则说明方程组无法同时满足。 具体步骤如下: - 首先遍历所有等式方程,将等式两侧的变量通过并查集合并到同一个集合中。 - 然后遍历所有不等式方程,判断不等式两侧的变量是否已经在同一个集合中。如果在同一集合,则说明存在矛盾,返回 $False$;如果所有不等式都检查无冲突,则返回 $True$。 #### 6.1.4 代码 ```python class UnionFind: def __init__(self, n): # 初始化 self.fa = [i for i in range(n)] # 每个元素的集合编号初始化为数组 fa 的下标索引 def find(self, x): # 查找元素根节点的集合编号内部实现方法 while self.fa[x] != x: # 递归查找元素的父节点,直到根节点 self.fa[x] = self.fa[self.fa[x]] # 隔代压缩优化 x = self.fa[x] return x # 返回元素根节点的集合编号 def union(self, x, y): # 合并操作:令其中一个集合的树根节点指向另一个集合的树根节点 root_x = self.find(x) root_y = self.find(y) if root_x == root_y: # x 和 y 的根节点集合编号相同,说明 x 和 y 已经同属于一个集合 return False self.fa[root_x] = root_y # x 的根节点连接到 y 的根节点上,成为 y 的根节点的子节点 return True def is_connected(self, x, y): # 查询操作:判断 x 和 y 是否同属于一个集合 return self.find(x) == self.find(y) class Solution: def equationsPossible(self, equations: List[str]) -> bool: union_find = UnionFind(26) for eqation in equations: if eqation[1] == "=": index1 = ord(eqation[0]) - 97 index2 = ord(eqation[3]) - 97 union_find.union(index1, index2) for eqation in equations: if eqation[1] == "!": index1 = ord(eqation[0]) - 97 index2 = ord(eqation[3]) - 97 if union_find.is_connected(index1, index2): return False return True ``` ## 练习题目 - [0990. 等式方程的可满足性](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/satisfiability-of-equality-equations.md) - [1202. 交换字符串中的元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/smallest-string-with-swaps.md) - [0947. 移除最多的同行或同列石头](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/most-stones-removed-with-same-row-or-column.md) - [0547. 省份数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/number-of-provinces.md) - [0684. 冗余连接](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/redundant-connection.md) - [0765. 情侣牵手](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/couples-holding-hands.md) - [并查集题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%B9%B6%E6%9F%A5%E9%9B%86%E9%A2%98%E7%9B%AE) ## 参考资料 - 【博文】[并查集 - OI Wiki](https://oi-wiki.org/ds/dsu/) - 【博文】[并查集 - LeetBook - 力扣](https://leetcode.cn/leetbook/detail/disjoint-set/) - 【博文】[并查集概念及用法分析 - 掘金](https://juejin.cn/post/6844903954774491149) - 【博文】[数据结构之并查集 - 端碗吹水的技术博客](https://blog.51cto.com/zero01/2609695) - 【博文】[并查集复杂度 - OI Wiki](https://oi-wiki.org/ds/dsu-complexity/) - 【题解】[使用并查集处理不相交集合问题(Java、Python) - 等式方程的可满足性 - 力扣](https://leetcode.cn/problems/satisfiability-of-equality-equations/solution/shi-yong-bing-cha-ji-chu-li-bu-xiang-jiao-ji-he-we/) - 【书籍】算法训练营 - 陈小玉 著 - 【书籍】算法 第 4 版 - 谢路云 译 - 【书籍】算法竞赛进阶指南 - 李煜东 著 - 【书籍】算法竞赛入门经典:训练指南 - 刘汝佳,陈锋 著 ================================================ FILE: docs/05_tree/index.md ================================================ ![](https://qcdn.itcharge.cn/images/20250923140359.png) ::: tip 引 言 树如同大地上拔节生长的脉络。 枝叶纵横,层层递进,信息在脉络间流转,万物由此相连。 ::: ## 本章内容 - [5.1 树与二叉树基础](https://github.com/ITCharge/AlgoNote/tree/main/docs/05_tree/05_01_tree_basic.md) - [5.2 二叉树的遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/05_tree/05_02_binary_tree_traverse.md) - [5.3 二叉树的还原](https://github.com/ITCharge/AlgoNote/tree/main/docs/05_tree/05_03_binary_tree_reduction.md) - [5.4 二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/05_tree/05_04_binary_search_tree.md) - [5.5 线段树(一)](https://github.com/ITCharge/AlgoNote/tree/main/docs/05_tree/05_05_segment_tree_01.md) - [5.6 线段树(二)](https://github.com/ITCharge/AlgoNote/tree/main/docs/05_tree/05_06_segment_tree_02.md) - [5.7 树状数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/05_tree/05_07_binary_indexed_tree.md) - [5.8 并查集](https://github.com/ITCharge/AlgoNote/tree/main/docs/05_tree/05_08_union_find.md) ================================================ FILE: docs/06_graph/06_01_graph_basic.md ================================================ ## 1. 图的定义 > **图(Graph)**:由顶点集合 $V$ 和边集合 $E$(即顶点之间的连接关系)组成的数据结构,通常记作 $G = (V, E)$。 - **顶点(Vertex)**:图的基本单元,表示对象或节点。顶点集合 $V$ 是有限且非空的,包含 $n > 0$ 个顶点。通常用圆圈表示顶点。 - **边(Edge)**:连接两个顶点的线段,表示它们之间的关系。边可记为 $e = \langle u, v \rangle$,表示从 $u$ 到 $v$ 的一条边,其中 $u$ 是起点,$v$ 是终点。 ![图](https://qcdn.itcharge.cn/images/20220307145142.png) - **子图(Sub Graph)**:如果图 $G' = (V', E')$ 满足 $V' \subseteq V$ 且 $E' \subseteq E$,则 $G'$ 是 $G$ 的子图。也就是说,子图由原图部分顶点和边组成,且边的两个端点都属于 $V'$。特别地,$G$ 本身也是它的一个子图。下图展示了图 $G$ 及其子图 $G'$。 ![子图](https://qcdn.itcharge.cn/images/20220317163120.png) ## 2. 图的分类 ### 2.1 无向图与有向图 按边是否有方向,可以将图分为「无向图」和「有向图」。 - **无向图(Undirected Graph)**:边没有方向,常用于表示朋友关系、城市间双向道路等。无向图的边由两个顶点组成的无序对表示,如下图左侧中的顶点 $(v_1, v_2)$。 - **有向图(Directed Graph)**:边有方向,常用于表示任务流程、依赖关系等。有向图的边(弧)由两个顶点组成的有序对表示,如下图右侧中的顶点 $\langle v_1, v_2 \rangle$,其中 $v_1$ 为弧尾,$v_2$ 为弧头。 ![无向图与有向图的边](https://qcdn.itcharge.cn/images/20220307160017.png) 如果图有 $n$ 个顶点,则: - **无向图最大边数**:$\frac{n \times (n - 1)}{2}$。达到最大边数的无向图称为 **完全无向图**。 - **有向图最大边数**:$n \times (n - 1)$。达到最大边数的有向图称为 **完全有向图**。 下图左为 $4$ 个顶点的完全无向图,右为 $4$ 个顶点的完全有向图: ![完全无向图与完全有向图](https://qcdn.itcharge.cn/images/20220308151436.png) **顶点的度** 是图的一个重要概念: - **无向图中,顶点的度**:与该顶点相连的边的数量,记为 $TD(v_i)$。 - 如上图左,$v_3$ 的度为 $3$。 - **有向图中,顶点的度** 分为: - **出度(Out Degree)**:以该顶点为起点的边数,记为 $OD(v_i)$。 - **入度(In Degree)**:以该顶点为终点的边数,记为 $ID(v_i)$。 - 顶点总度:$TD(v_i) = OD(v_i) + ID(v_i)$。 - 如上图右,$v_3$ 的出度为 $3$,入度为 $3$,总度为 $6$。 ### 2.2 环形图和无环图 > **路径**:路径是图论中的核心概念。对于图 $G = (V, E)$,如果存在顶点序列 $v_{i_0}, v_{i_1}, v_{i_2}, \ldots, v_{i_m}$,使得任意相邻顶点对之间都存在边相连(无向图为 $(v_{i_{k-1}}, v_{i_k}) \in E$,有向图为 $\langle v_{i_{k-1}}, v_{i_k} \rangle \in E$,$1 \leq k \leq m$),则称该顶点序列为从 $v_{i_0}$ 到 $v_{i_m}$ 的一条路径。其中,$v_{i_0}$ 为起点,$v_{i_m}$ 为终点。 简而言之,如果从顶点 $v_{i_0}$ 出发,经过若干顶点和边能够到达顶点 $v_{i_m}$,则称 $v_{i_0}$ 与 $v_{i_m}$ 之间存在一条路径,所经过的顶点序列即为这条路径。 - **环(Cycle)**:如果一条路径的起点和终点重合(即 $v_{i_0} = v_{i_m}$),则称该路径为「环」或「回路」。 - **简单路径**:路径上所有顶点均不重复出现(除非是环的首尾重合),称为「简单路径」。 根据图中是否存在环,可以将图分为「环形图」和「无环图」: - **环形图(Cyclic Graph)**:如果图中至少存在一条环(Cycle),则称该图为环形图或含环图。 - **无环图(Acyclic Graph)**:如果图中不存在任何环,则称该图为无环图。 对于有向图,如果不存在环,则称为「有向无环图(Directed Acyclic Graph, DAG)」。DAG 结构在动态规划、最短路径、数据压缩等算法中有着广泛应用。 下图展示了四类典型图结构:无向无环图、无向环形图、有向无环图和有向环形图。其中有向环形图中,顶点 $v_1$、$v_2$、$v_3$ 及其相连的边构成了一个环。 ![环形图和无环图](https://qcdn.itcharge.cn/images/20220317115641.png) ### 2.3 连通图与非连通图 #### 2.3.1 连通无向图 在无向图中,如果从顶点 $v_i$ 能通过一条路径到达顶点 $v_j$,则称 $v_i$ 和 $v_j$ 连通。 - **连通无向图**:任意两个顶点之间都有路径相连的无向图。 - **非连通无向图**:存在至少一对顶点之间没有路径相连的无向图。 下图左侧为连通无向图,$v_1$ 能与所有其他顶点连通;右侧为非连通无向图,$v_1$ 只能与 $v_2$、$v_3$、$v_4$ 连通,无法到达 $v_5$、$v_6$。 ![连通无向图与非连通无向图](https://qcdn.itcharge.cn/images/20220317163249.png) #### 2.3.2 无向图的连通分量 在无向图中,整体可能不是连通的,但其中的某些子图是连通的,这些子图称为「连通子图」。如果一个连通子图无法再被包含于更大的连通子图中,则称其为「连通分量」,即无向图中的极大连通子图。 - **连通子图**:无向图的一个子图,且该子图是连通的。 - **极大连通子图**:连通子图中,如果不存在包含它的更大的连通子图,则称为极大连通子图。 - **连通分量**:无向图中的极大连通子图,即为该图的连通分量。 举例来说,上图右侧的非连通无向图中,顶点 $v_1$、$v_2$、$v_3$、$v_4$ 及其相连的边构成了一个连通子图,且无法再扩展为更大的连通子图,因此它是原图的一个连通分量。同理,顶点 $v_5$、$v_6$ 及其相连的边也构成了另一个连通分量。 #### 2.3.3 强连通有向图 在有向图中,如果顶点 $v_i$ 能到达 $v_j$,且 $v_j$ 也能到达 $v_i$,则称 $v_i$ 和 $v_j$ 是「强连通」的。 - **强连通有向图**:任意两个顶点都能互相到达的有向图。 - **非强连通有向图**:存在至少一对顶点不能互相到达的有向图。 下图左为强连通有向图,任意两点可互达;右图中 $v_7$ 无法到达其他顶点,因此不是强连通有向图。 ![强连通有向图与非强连通有向图](https://qcdn.itcharge.cn/images/20220317133500.png) #### 2.3.4 有向图的强连通分量 在有向图中,「强连通分量」指的是:图中某个极大子图,子图内任意两个顶点都可以互相到达(即强连通),并且无法再加入其他顶点使其仍然强连通。 简要定义如下: - **强连通子图**:有向图的一个子图,子图内任意两点互相可达。 - **极大强连通子图**:不能再加入其他顶点的强连通子图。 - **强连通分量**:有向图中的极大强连通子图。 举例说明:上图右侧的有向图不是强连通图(如 $v_7$ 无法到达其他顶点),但 $v_1$、$v_2$、 $v_3$、$v_4$、$v_5$、$v_6$ 及其边构成了一个强连通分量(即左侧图)。而 $v_7$ 自身也单独构成一个强连通分量。 ### 2.4 带权图 有些图不仅表示顶点之间是否有关系,还需要描述这种关系的「强度」或「代价」,这就是「权」。权值可以表示距离、时间、费用等。 - **带权图**:每条边都带有权值的图。权值一般为非负数,有时也可以为负数。 - **网络**:带权且连通的无向图称为网络。 下图展示了一个带权图的例子: ![带权图](https://qcdn.itcharge.cn/images/20220317135207.png) ### 2.5 稠密图与稀疏图 图按边的多少可分为「稠密图」和「稀疏图」,这一划分没有严格的界限,仅为便于理解。 - **稠密图(Dense Graph)**:边数接近完全图的图,即大多数顶点之间都有边相连。 - **稀疏图(Sparse Graph)**:边数远少于完全图的图(常见如 $e < n \times \log_2 n$),大部分顶点之间没有直接连接。 ## 3. 总结 图是计算机科学中最重要的数据结构之一,由顶点和边组成,用于表示对象间的关系。本文介绍了图的基本概念和分类: ### 核心概念 - **顶点(Vertex)**:图的基本单元,表示对象或节点 - **边(Edge)**:连接顶点的线段,表示对象间的关系 - **路径**:顶点序列,表示从一个顶点到另一个顶点的遍历过程 ### 主要分类 1. **按方向性**:无向图(双向关系)vs 有向图(单向关系) 2. **按连通性**:连通图(任意两点可达)vs 非连通图(存在孤立部分) 3. **按环结构**:环形图(存在回路)vs 无环图(无回路,如DAG) 4. **按权值**:带权图(边有权重)vs 无权图(边无权重) 5. **按密度**:稠密图(边多)vs 稀疏图(边少) ### 重要特性 - **度**:无向图中顶点的连接数,有向图中分为入度和出度 - **连通分量**:无向图中的极大连通子图 - **强连通分量**:有向图中任意两点互相可达的极大子图 理解这些基础概念是学习图算法(如 DFS、BFS、最短路径、最小生成树等)的重要前提。 ## 参考资料 - 【书籍】ACM-ICPC 程序设计系列 - 图论及应用 \- 陈宇 吴昊 主编 - 【书籍】数据结构教程 第 3 版 - 唐发根 著 - 【书籍】大话数据结构 - 程杰 著 - 【书籍】算法训练营 - 陈小玉 著 - 【书籍】Python 数据结构与算法分析 第 2 版 - 布拉德利·米勒 戴维·拉努姆 著 - 【博文】[图的基础知识 | 小浩算法](https://www.geekxh.com/1.99.其他补充题目/50.html) - 【博文】[链式前向星及其简单应用 | Malash's Blog](https://malash.me/200910/linked-forward-star/) ================================================ FILE: docs/06_graph/06_02_graph_structure.md ================================================ ## 1. 图的存储结构 图是一种由顶点和边构成的复杂数据结构,通常包含若干(有限个)顶点,任意两个顶点之间都可能通过边相连。在实现图的存储时,关键在于如何高效地表示顶点与边之间的关系。 常见的图存储方式主要分为「顺序存储结构」和「链式存储结构」两大类。顺序存储结构包括邻接矩阵和边集数组;链式存储结构则有邻接表、链式前向星、十字链表、邻接多重表等。 下面将详细介绍几种常用的图存储结构。文中约定:$n$ 表示顶点数,$m$ 表示边数,$TD(v_i)$ 表示顶点 $v_i$ 的度数。 ### 1.1 邻接矩阵 #### 1.1.1 邻接矩阵的原理描述 > **邻接矩阵(Adjacency Matrix)**:通过一个二维数组 $adj$ 来表示顶点之间的连接关系。 > > - 对于无权图,如果 $adj[i][j] == 1$,表示顶点 $v_i$ 与 $v_j$ 之间有边;如果 $adj[i][j] = 0$,则表示两者之间无边。 > - 对于带权图,如果 $adj[i][j] == w$ 且 $w \ne \infty$,则表示 $v_i$ 到 $v_j$ 有一条权值为 $w$ 的边;如果 $adj[i][j] = \infty$,则表示两者之间无边。 下图左侧为一个无向图,右侧为其对应的邻接矩阵结构示意: ![邻接矩阵](https://qcdn.itcharge.cn/images/20220317144826.png) 邻接矩阵的主要特点如下: - **优点**:结构简单,便于实现;可以快速判断任意两个顶点之间是否存在边,也能直接获取边的权值。 - **缺点**:初始化和遍历效率较低,空间占用大且利用率不高,无法表示重边,顶点的增删操作不便。当顶点数量较大(如 $n > 10^5$)时,采用空间复杂度为 $n \times n$ 的二维数组存储邻接矩阵在实际应用中难以实现。 #### 1.1.2 邻接矩阵的代码实现 ```python class Graph: # 邻接矩阵实现的图 def __init__(self, ver_count, directed=False, inf=float('inf')): self.n = ver_count # 顶点数量 n self.directed = directed # 是否为有向图 self.inf = inf # 无边时的填充值(带权图用 ∞ 表示无边) # 邻接矩阵,采用 1..n 顶点编号;0 号行列弃用,便于直观 self.adj = [[inf] * (ver_count + 1) for _ in range(ver_count + 1)] for i in range(1, ver_count + 1): self.adj[i][i] = 0 # 自环距离为 0(无权图也可视作 0) def add_edge(self, vi, vj, w=1): # 添加边 vi -> vj,权重默认为 1 self.adj[vi][vj] = w if not self.directed: # 无向图需要对称赋值 self.adj[vj][vi] = w def get_edge(self, vi, vj): # 查询边权,不存在返回 None if self.adj[vi][vj] != self.inf: return self.adj[vi][vj] return None def printMatrix(self): # 打印邻接矩阵,∞ 表示无边 for i in range(1, self.n + 1): row = [self.adj[i][j] if self.adj[i][j] != self.inf else '∞' for j in range(1, self.n + 1)] print(' '.join(map(str, row))) # 示例:构建一个有向带权图,并进行查询与打印 graph = Graph(6, directed=True) edges = [(1, 2, 5), (1, 5, 6), (2, 4, 7), (4, 3, 9), (3, 1, 2), (5, 6, 8), (6, 4, 3)] for u, v, w in edges: graph.add_edge(u, v, w) print(graph.get_edge(4, 3)) # 输出 9(存在边 4->3,权重 9) print(graph.get_edge(4, 5)) # 输出 None(不存在边 4->5) graph.printMatrix() # 打印 6x6 邻接矩阵 ``` #### 1.1.3 邻接矩阵的算法分析 - **时间复杂度**: - **初始化**:$O(n^2)$。需要为 $n$ 个顶点分配 $n \times n$ 的二维数组空间。 - **查询、添加或删除一条边**:$O(1)$。通过下标即可直接访问和修改边的信息。 - **获取某个顶点的所有邻接边**:$O(n)$。需遍历该顶点所在的整行或整列。 - **遍历整张图**:$O(n^2)$。需要访问整个邻接矩阵。 - **空间复杂度**:$O(n^2)$。无论实际边数多少,均需分配 $n \times n$ 的空间。 ### 1.2 邻接表 #### 1.2.1 邻接表的原理描述 > **邻接表(Adjacency List)**:一种结合顺序存储与链式存储的图结构。它主要由两部分组成:一是用于存放所有顶点信息的数组,二是用于存放每个顶点所有邻接边的链表。 在邻接表中,每个顶点 $v_i$ 都对应一个链表,链表中的每个节点表示一条从 $v_i$ 出发的边,节点中存储该边所指向的顶点及相关信息。因此,如果图有 $n$ 个顶点,则邻接表由 $n$ 个链表组成,每个链表分别记录对应顶点的所有邻接边。 每个顶点在邻接表中都有一个表头节点(即「顶点节点」),该节点包含顶点本身的信息和指向其第一条邻接边的指针。这样可以高效地访问和管理每个顶点的所有邻接边。 为了便于随机访问任意顶点的邻接链表,通常将所有顶点节点以数组形式顺序存储,数组下标即为顶点在图中的编号。 下图左侧为一个有向图,右侧为其对应的邻接表结构示意: ![邻接表](https://qcdn.itcharge.cn/images/20220317154531.png) #### 1.2.2 邻接表的代码实现 ```python class EdgeNode: # 边结点:存储终点、权值与下一条边 def __init__(self, vj, val): self.vj = vj # 边的终点 self.val = val # 边的权值(无权图可默认 1) self.next = None # 指向下一条同起点的边 class VertexNode: # 顶点结点:存储顶点编号与其第一条邻接边 def __init__(self, vi): self.vi = vi # 顶点编号 self.head = None # 指向该顶点的第一条邻接边 class Graph: # 邻接表实现的图 def __init__(self, ver_count, directed=False): self.n = ver_count # 顶点数量 n self.directed = directed # 是否为有向图 # 使用 1..n 的顶点编号,0 号位置空置,便于直观 self.vertices = [None] + [VertexNode(i) for i in range(1, ver_count + 1)] def _valid(self, v): # 顶点合法性检查 return 1 <= v <= self.n def add_edge(self, vi, vj, val=1): # 添加边 vi -> vj,权重默认 1 if not self._valid(vi) or not self._valid(vj): raise ValueError("invalid vertex: {} or {}".format(vi, vj)) edge = EdgeNode(vj, val) # 头插法加入邻接链表 edge.next = self.vertices[vi].head self.vertices[vi].head = edge if not self.directed: # 无向图需要加反向边 rev = EdgeNode(vi, val) rev.next = self.vertices[vj].head self.vertices[vj].head = rev def get_edge(self, vi, vj): # 查询 vi -> vj 的边权,如果无边返回 None if not self._valid(vi) or not self._valid(vj): raise ValueError("invalid vertex: {} or {}".format(vi, vj)) cur = self.vertices[vi].head while cur: if cur.vj == vj: return cur.val cur = cur.next return None def neighbors(self, vi): # 遍历顶点 vi 的所有邻接边 (vj, val) cur = self.vertices[vi].head while cur: yield cur.vj, cur.val cur = cur.next def printGraph(self): # 打印所有边 for vi in range(1, self.n + 1): cur = self.vertices[vi].head while cur: print(str(vi) + ' - ' + str(cur.vj) + ' : ' + str(cur.val)) cur = cur.next # 示例:构建有向带权图并查询/打印 graph = Graph(6, directed=True) edges = [(1, 2, 5), (1, 5, 6), (2, 4, 7), (4, 3, 9), (3, 1, 2), (5, 6, 8), (6, 4, 3)] for u, v, w in edges: graph.add_edge(u, v, w) print(graph.get_edge(4, 3)) # 9 print(graph.get_edge(4, 5)) # None(无此边) graph.printGraph() ``` #### 1.2.3 邻接表的算法分析 - **时间复杂度分析**: - **图的初始化与创建**:$O(n + m)$,其中 $n$ 表示顶点数,$m$ 表示边数。因为需要为每个顶点分配空间,并依次插入每条边。 - **查询是否存在 $v_i$ 到 $v_j$ 的边**:$O(TD(v_i))$,其中 $TD(v_i)$ 表示顶点 $v_i$ 的出度。由于邻接表只存储与 $v_i$ 直接相连的边,因此需要遍历 $v_i$ 的所有邻接点,最坏情况下需遍历 $v_i$ 的全部出边。 - **遍历某个顶点的所有边**:$O(TD(v_i))$,即与该顶点相连的所有边都需访问一遍,效率较高。 - **遍历整张图的所有边**:$O(n + m)$。遍历所有顶点,每个顶点的邻接表总共包含 $m$ 条边,因此整体遍历代价为 $O(n + m)$。 - **空间复杂度分析**:$O(n + m)$,邻接表需要为每个顶点分配一个链表头结点($O(n)$),并为每条边分配一个边节点($O(m)$),因此总空间复杂度为 $O(n + m)$。相比邻接矩阵,邻接表在稀疏图中能显著节省空间。 ### 1.3 链式前向星 #### 1.3.1 链式前向星的原理描述 > **链式前向星(Linked Forward Star)**,又称静态邻接表,是一种以静态链表实现邻接表的高效图存储结构。它将边集数组与邻接表结合,能够高效地访问某个节点的所有邻接点,并且空间开销极小。 链式前向星采用静态链表的方式存储边信息,是目前建图和遍历效率极高的存储方法之一。 其核心由两类数据结构组成: - **边集数组** $edges$:$edges[i]$ 表示第 $i$ 条边,包含以下信息:$edges[i].vj$ 为该边的终点,$edges[i].val$ 为该边的权值,$edges[i].next$ 指向与该边起点相同的下一条边在 $edges$ 数组中的下标。 - **头节点数组** $head$:$head[i]$ 存储以顶点 $i$ 为起点的第一条边在 $edges$ 数组中的下标。 链式前向星的核心思想是通过 $head$ 数组记录每个顶点的第一条出边在 $edges$ 数组中的下标,并利用 $edges$ 数组中每条边的 $next$ 字段,将同一起点的所有出边以静态链表的形式串联起来,从而高效地关联顶点 $v_i$ 及其所有出边。 如下图所示,左侧为一个有向图,右侧为其对应的链式前向星结构。 ![链式前向星](https://qcdn.itcharge.cn/images/20220317161217.png) 以遍历顶点 $v_1$ 的所有出边为例,步骤如下: - 首先,通过 `index = head[1] = 1`,找到以 $v_1$ 为起点的第一条边在 `edges` 数组中的下标,即 `edges[1]`,对应边 $\langle v_1, v_5 \rangle$,权值为 6。 - 然后,根据 `index = edges[1].next = 0`,找到第二条边 `edges[0]`,即 $\langle v_1, v_2 \rangle$,权值为 5。 - 最后,`index = edges[0].next = -1`,表示没有更多的出边,遍历结束。 #### 1.3.2 链式前向星的代码实现 ```python class EdgeNode: """边信息类,存储终点、权值和下一条边的下标""" def __init__(self, vj, val, next_idx): self.vj = vj # 边的终点 self.val = val # 边的权值 self.next = next_idx # 下一条边在边集数组中的下标 class Graph: """链式前向星图结构""" def __init__(self, ver_count): self.n = ver_count # 顶点个数 self.head = [-1] * self.n # 头节点数组,head[i]为顶点i的第一条出边下标 self.edges = [] # 边集数组 def _valid(self, v): """判断顶点编号是否合法(0 ~ n - 1)""" return 0 <= v < self.n def add_edge(self, vi, vj, val): """ 添加一条边 vi -> vj,权值为 val vi, vj 均为 0 ~ n - 1 的顶点编号 """ if not self._valid(vi) or not self._valid(vj): raise ValueError(f"{vi} 或 {vj} 不是有效顶点编号") # 新边的 next 指向 vi 原来的第一条出边 edge = EdgeNode(vj, val, self.head[vi]) self.edges.append(edge) self.head[vi] = len(self.edges) - 1 # head[vi] 指向新加边的下标 def build(self, edge_list): """批量建图,edge_list 为 [(vi, vj, val), ...],顶点编号从 1 开始""" for vi, vj, val in edge_list: # 由于输入的顶点编号是从 1 开始,内部实现是从 0 开始,所以需要减 1 self.add_edge(vi - 1, vj - 1, val) def get_edge(self, vi, vj): """ 查询 vi -> vj的边权,vi, vj 为 1-based 编号 返回权值或 None """ vi -= 1 vj -= 1 if not self._valid(vi) or not self._valid(vj): raise ValueError(f"{vi + 1} 或 {vj + 1} 不是有效顶点编号") idx = self.head[vi] while idx != -1: edge = self.edges[idx] if edge.vj == vj: return edge.val idx = edge.next return None def printGraph(self): """打印所有边,顶点编号输出为 1-based""" for vi in range(self.n): idx = self.head[vi] while idx != -1: edge = self.edges[idx] print(f"{vi+1} - {edge.vj+1} : {edge.val}") idx = edge.next # 示例:构建有向带权图并查询 / 打印 graph = Graph(7) # 顶点编号 1 ~ 7 edges = [ [1, 2, 5], [1, 5, 6], [2, 4, 7], [4, 3, 9], [3, 1, 2], [5, 6, 8], [6, 4, 3] ] graph.build(edges) print(graph.get_edge(4, 3)) # 输出 9 print(graph.get_edge(4, 5)) # 输出 None(无此边) graph.printGraph() ``` #### 1.3.3 链式前向星的算法分析 - **时间复杂度**: - **图的初始化和创建操作**:$O(n + m)$,其中 $n$ 表示顶点数,$m$ 表示边数。初始化时需要为每个顶点分配空间,并依次插入每条边,因此总耗时与顶点和边的数量成线性关系。 - **查询是否存在 $v_i$ 到 $v_j$ 的边**:$O(TD(v_i))$,其中 $TD(v_i)$ 表示顶点 $v_i$ 的出度。因为链式前向星存储结构需要遍历 $v_i$ 的所有出边才能判断是否存在到 $v_j$ 的边,最坏情况下需要遍历 $v_i$ 的所有出边。 - **遍历某个点的所有边**:$O(TD(v_i))$。由于每个顶点的出边在链表中连续存储,遍历时只需顺序访问即可,效率较高。 - **遍历整张图的所有边**:$O(n + m)$。遍历所有顶点并依次访问每个顶点的所有出边,总共访问 $n$ 个顶点和 $m$ 条边,整体复杂度为线性。 - **空间复杂度**:$O(n + m)$。需要为每个顶点分配一个头指针($O(n)$),并为每条边分配一个边节点($O(m)$),因此总空间消耗与顶点数和边数之和成正比。 ### 1.4 哈希表实现邻接表 #### 1.4.1 哈希表实现邻接表的原理描述 在 Python 中,可以利用哈希表(即字典)高效地实现邻接表结构。具体做法是:使用一个字典存储所有顶点信息,字典的键为顶点编号,值为该顶点的邻接边集合(同样用一个字典表示)。每个顶点对应的邻接边字典,其键为相邻顶点的编号,值为对应边的权重。这样既方便查询某一顶点的所有出边,也便于获取任意一条边的权重。 #### 1.4.2 哈希表实现邻接表的代码实现 ```python class Graph: """哈希表实现的邻接表图结构""" def __init__(self, ver_count, directed=False): self.n = ver_count # 顶点数量 self.directed = directed # 是否为有向图 # 使用字典存储邻接表,键为顶点编号,值为邻接边字典 # 邻接边字典:键为相邻顶点编号,值为边权重 self.adj = {i: {} for i in range(1, ver_count + 1)} def _valid(self, v): """判断顶点编号是否合法(1 ~ n)""" return 1 <= v <= self.n def add_edge(self, vi, vj, val=1): """ 添加一条边 vi -> vj,权值为 val vi, vj 为 1-based 顶点编号 """ if not self._valid(vi) or not self._valid(vj): raise ValueError(f"顶点编号 {vi} 或 {vj} 超出范围 [1, {self.n}]") # 添加边 vi -> vj self.adj[vi][vj] = val # 如果是无向图,添加反向边 if not self.directed: self.adj[vj][vi] = val def build(self, edge_list): """ 批量建图 edge_list: [(vi, vj, val), ...] 边列表,顶点编号从1开始 """ for vi, vj, val in edge_list: self.add_edge(vi, vj, val) def get_edge(self, vi, vj): """ 查询 vi -> vj 的边权 返回权值,如果不存在该边则返回 None """ if not self._valid(vi) or not self._valid(vj): raise ValueError(f"顶点编号 {vi} 或 {vj} 超出范围 [1, {self.n}]") return self.adj[vi].get(vj, None) def has_edge(self, vi, vj): """ 判断是否存在 vi -> vj 的边 返回 True 或 False """ if not self._valid(vi) or not self._valid(vj): return False return vj in self.adj[vi] def neighbors(self, vi): """ 遍历顶点 vi 的所有邻接边 返回生成器,每次产生 (邻接顶点, 边权) """ if not self._valid(vi): raise ValueError(f"顶点编号 {vi} 超出范围 [1, {self.n}]") for vj, val in self.adj[vi].items(): yield vj, val def get_degree(self, vi): """ 获取顶点 vi 的出度 """ if not self._valid(vi): raise ValueError(f"顶点编号 {vi} 超出范围 [1, {self.n}]") return len(self.adj[vi]) def print_graph(self): """打印所有边""" for vi in range(1, self.n + 1): for vj, val in self.adj[vi].items(): print(f"{vi} -> {vj} : {val}") # 示例:构建有向带权图并测试各种操作 # 创建有向图 graph = Graph(6, directed=True) # 添加边 edges = [(1, 2, 5), (1, 5, 6), (2, 4, 7), (4, 3, 9), (3, 1, 2), (5, 6, 8), (6, 4, 3)] graph.build(edges) print("=== 图的基本信息 ===") print(f"顶点数: {graph.n}") print(f"是否为有向图: {graph.directed}") print("\n=== 边的查询操作 ===") print(f"边 4->3 的权重: {graph.get_edge(4, 3)}") # 输出: 9 print(f"边 4->5 的权重: {graph.get_edge(4, 5)}") # 输出: None print(f"是否存在边 1->2: {graph.has_edge(1, 2)}") # 输出: True print(f"是否存在边 2->1: {graph.has_edge(2, 1)}") # 输出: False print("\n=== 邻接点遍历 ===") for vi in range(1, 7): neighbors = list(graph.neighbors(vi)) print(f"顶点 {vi} 的邻接点: {neighbors}, 出度: {graph.get_degree(vi)}") print("\n=== 所有边 ===") graph.print_graph() ``` #### 1.4.3 哈希表实现邻接表的简单构建 在实际刷题或竞赛过程中,如果只是需要临时构建一张图用于算法实现,通常会采用最简单直接的方式来初始化邻接表。例如,直接用 Python 的字典(dict)或列表(list)来存储邻接关系,无需封装成类,也不必实现完整的接口。这样可以大大简化代码量,便于快速调试和提交。 ```python # 构建图 n = 6 # 顶点数 edges = [(1, 2, 5), (1, 5, 6), (2, 4, 7), (4, 3, 9), (3, 1, 2), (5, 6, 8), (6, 4, 3)] # 初始化邻接表 graph = {i: {} for i in range(1, n + 1)} # 添加边 for vi, vj, val in edges: graph[vi][vj] = val # 查询边 print(graph[4].get(3, None)) # 输出: 9 print(graph[4].get(5, None)) # 输出: None # 遍历邻接点 for vi in range(1, n + 1): print(f"顶点 {vi} 的邻接点: {list(graph[vi].keys())}") ``` #### 1.4.4 哈希表实现邻接表的算法分析 - **时间复杂度**: - **图的初始化与构建**:$O(n + m)$,其中 $n$ 表示顶点数,$m$ 表示边数。每个顶点的邻接表初始化为 $O(n)$,每条边的插入操作为 $O(1)$,总共 $m$ 条边,因此整体为 $O(n + m)$。 - **边的存在性查询**:$O(1)$。判断是否存在从 $v_i$ 到 $v_j$ 的边,利用哈希表查找,平均时间复杂度为 $O(1)$,即常数时间即可完成。 - **遍历某一顶点的所有邻接边**:遍历顶点 $v_i$ 的所有出边,时间复杂度为 $O(\mathrm{TD}(v_i))$,其中 $\mathrm{TD}(v_i)$ 表示顶点 $v_i$ 的出度(即邻接点个数)。这是因为只需顺序访问该顶点邻接表中的所有元素。 - **遍历整张图的所有边**:遍历所有顶点及其邻接表,整体时间复杂度为 $O(n + m)$。$O(n)$ 用于访问所有顶点,$O(m)$ 用于访问所有边。 - **空间复杂度**:$O(n + m)$,其中 $O(n)$ 用于存储所有顶点的邻接表结构,$O(m)$ 用于存储所有边的信息。相比邻接矩阵,邻接表在稀疏图($m \ll n^2$)时能显著节省空间。 ## 2. 常见图论问题 常见的图论问题主要有:**遍历问题**、**连通性问题**、**生成树问题**、**最短路径问题**、**网络流问题**、**二分图问题** 等。 ### 2.1 图的遍历问题 > **图的遍历**:从某个顶点出发,按照特定顺序访问图中所有节点且仅访问一次。 遍历是解决连通性、拓扑排序、关键路径等问题的基础。常用方法有: - **深度优先搜索(DFS)**:沿一条路径尽可能深入,无法继续时回退。 - **广度优先搜索(BFS)**:一层一层访问邻接点,逐层推进。 ### 2.2 图的连通性问题 无向图常见连通性问题有:**连通分量**、**点双连通分量(割点)**、**边双连通分量(桥)**、**全局最小割**等。 - **连通分量**:无向图中,每个连通分量是内部任意两点都连通、且无法再扩展的极大子图。常用并查集或 DFS/BFS 求解。 - **点双连通分量(割点)**:无向图中,去掉任意一个顶点后,分量内其他顶点仍连通的极大子图。割点是指删除后会增加连通分量数的顶点。常用 DFS 求解。 - **边双连通分量(桥)**:无向图中,去掉任意一条边后,分量内其他顶点仍连通的极大子图。桥是指删除后会增加连通分量数的边。也可用 DFS 求解。 - **全局最小割**:将无向图顶点分为两个不相交集合,使连接这两个集合的边权和最小。常用于网络可靠性分析,常见算法有 Stoer-Wagner 等。 有向图常见连通性问题有:**强连通分量**、**最小点基**、**最小权点基**、**2-SAT 问题** 等。 - **强连通分量**:有向图中,强连通分量指任意两点互相可达、且无法再扩展的极大子图。常用 Kosaraju 或 Tarjan 算法快速求解。 - **最小点基**:在有向图中,选出最少的顶点,使得从这些点能到达所有顶点。常用于控制系统、可达性分析等。 - **最小权点基**:在最小点基的基础上,要求选中顶点的权值和最小,常见于带权有向图的优化问题。 - **2-SAT 问题**:2-SAT 是每个约束只包含两个变量的布尔可满足性问题(如 $x \lor \lnot y$)。可用强连通分量算法在线性时间判断有无解,并给出解。 ### 2.3 图的生成树问题 > **生成树**:连通图的一个包含所有顶点的极小连通子图,且本身是一棵树。 主要问题包括:**最小生成树**、**次小生成树**、**有向图的最小树形图**。 - **最小生成树**:带权无向图中,所有生成树中边权和最小的那棵树。 - **次小生成树**:权值仅次于最小生成树的另一棵生成树。 - **最小树形图**:带权有向图中,以某顶点为根,所有点可达且边权和最小的生成树。 ### 2.4 图的最短路径问题 > **最短路径问题**:在带权图中,寻找两点间权值和最小的路径。 按源点数量可分为: - **单源最短路径**:一个顶点到其他所有顶点的最短路径。 - **多源最短路径**:任意两点间的最短路径。 此外,还有 **k 最短路径问题**(如次短路径、第三短路径等),以及与 **差分约束系统** 相关的问题。 ### 2.5 图的网络流问题 > **网络流**:在带权有向图(网络)中,研究从源点 $s$ 到汇点 $t$ 的流量分配问题。每条边有容量限制,除源点和汇点外,其他点流入等于流出。 常见问题有: - **最大流**:求从源点到汇点的最大可流量。 - **最小费用最大流**:在最大流的基础上,使总费用最小。 - **最小割**:删去若干边使网络不连通,且被删边容量和最小。 ### 2.6 二分图问题 > **二分图**:一种无向图,可以把所有顶点分成两个互不重叠的集合,使得每条边都连接着来自不同集合的两个顶点,也就是说,同一个集合内的顶点之间没有边相连。 常见问题有: - **最大匹配**:匹配边数最多的匹配。 - **最大权匹配**:匹配边权和最大的匹配。 - **多重匹配**:每个点可多次匹配,但有上限。 ## 参考资料 - 【书籍】ACM-ICPC 程序设计系列 - 图论及应用 \- 陈宇 吴昊 主编 - 【书籍】数据结构教程 第 3 版 - 唐发根 著 - 【书籍】大话数据结构 - 程杰 著 - 【书籍】算法训练营 - 陈小玉 著 - 【书籍】Python 数据结构与算法分析 第 2 版 - 布拉德利·米勒 戴维·拉努姆 著 - 【博文】[图的基础知识 | 小浩算法](https://www.geekxh.com/1.99.其他补充题目/50.html) - 【博文】[链式前向星及其简单应用 | Malash's Blog](https://malash.me/200910/linked-forward-star/) - 【博文】[图论部分简介 - OI Wiki](https://oi-wiki.org/graph/) ================================================ FILE: docs/06_graph/06_03_graph_dfs.md ================================================ ## 1. 深度优先搜索简介 > **深度优先搜索算法(Depth First Search,简称 DFS)**:是一种用于遍历或搜索树、图等结构的经典算法。其核心思想是「沿一条路径尽可能深入」,遇到无法继续的节点时再回溯到上一个分叉点,继续探索其他路径,直到遍历完整个结构或找到目标为止。 深度优先的含义,就是每次优先选择一条路径一直走到底,只有在无法继续时才回退,尝试其他分支。 在 DFS 的遍历过程中,我们通常需要暂存当前节点 $u$ 的相邻节点,以便回溯时继续访问未遍历的节点。由于遍历顺序具有「后进先出」的特性,DFS 可以通过递归(系统栈)或显式使用栈结构来实现。 ## 2. 深度优先搜索算法步骤 下面以无向图为例,简要梳理深度优先搜索的基本流程: 1. 选定起始节点 $u$,将其标记为已访问。 2. 判断当前节点 $u$ 是否为目标节点(具体依据题目要求)。 3. 如果 $u$ 为目标节点,直接返回结果。 4. 如果 $u$ 不是目标节点,则遍历 $u$ 的所有未被访问的邻接节点。 5. 对每一个未访问的邻接节点 $v$,递归地从 $v$ 继续进行深度优先搜索。 6. 如果 $u$ 没有未访问的邻接节点,则回溯到上一个节点,继续探索其他分支。 7. 重复步骤 $2 \sim 6$,直到遍历完整个图或找到目标节点为止。 ::: tabs#DFS @tab <1> ![深度优先搜索 1](https://qcdn.itcharge.cn/images/202309042321406.png) @tab <2> ![深度优先搜索 2](https://qcdn.itcharge.cn/images/202309042323911.png) @tab <3> ![深度优先搜索 3](https://qcdn.itcharge.cn/images/202309042324370.png) @tab <4> ![深度优先搜索 4](https://qcdn.itcharge.cn/images/202309042325587.png) @tab <5> ![深度优先搜索 5](https://qcdn.itcharge.cn/images/202309042325689.png) @tab <6> ![深度优先搜索 6](https://qcdn.itcharge.cn/images/202309042325770.png) ::: ## 3. 基于递归的深度优先搜索 ### 3.1 基于递归的深度优先搜索算法步骤 深度优先搜索(DFS)可以通过递归方式实现。其基本递归流程如下: 1. 设 $graph$ 为无向图的邻接表,$visited$ 为已访问节点的集合,$u$ 为当前节点。定义递归函数 `def dfs_recursive(graph, u, visited):`。 2. 将起始节点 $u$ 标记为已访问(`visited.add(u)`)。 3. 判断当前节点 $u$ 是否为目标节点(具体依据题目要求)。 4. 如果 $u$ 为目标节点,直接返回结果。 5. 如果 $u$ 不是目标节点,则遍历 $u$ 的所有未访问的邻接节点 $v$。 6. 对每个未访问的邻接节点 $v$,递归调用 `dfs_recursive(graph, v, visited)` 继续搜索。 7. 如果 $u$ 没有未访问的邻接节点,则自动回溯到上一个节点,继续探索其他分支。 8. 重复步骤 $3 \sim 7$,直到遍历完整个图或找到目标节点为止。 ### 3.2 基于递归的深度优先搜索实现代码 ```python class Solution: def dfs_recursive(self, graph, u, visited): """ 递归实现深度优先搜索(DFS) :param graph: 字典表示的邻接表,key为节点,value为邻接节点列表 :param u: 当前访问的节点 :param visited: 已访问节点的集合 """ print(u) # 访问当前节点 u visited.add(u) # 标记节点 u 已访问 # 遍历所有邻接节点 for v in graph[u]: if v not in visited: # 如果邻接节点 v 未被访问 # 递归访问邻接节点 v self.dfs_recursive(graph, v, visited) # 示例图(邻接表形式,节点为字符串,边为无向边) graph = { "1": ["2", "3"], "2": ["1", "3", "4"], "3": ["1", "2", "4", "5"], "4": ["2", "3", "5", "6"], "5": ["3", "4"], "6": ["4", "7"], "7": [] } # 初始化已访问节点集合 visited = set() # 从节点 "1" 开始进行深度优先搜索 Solution().dfs_recursive(graph, "1", visited) ``` ## 4. 基于堆栈的深度优先搜索 ### 4.1 基于堆栈的深度优先搜索算法步骤 除了递归方式,深度优先搜索(DFS)还可以用显式的栈结构实现。为避免重复访问同一节点,栈中不仅存储当前节点,还记录下一个待访问的邻接节点下标。这样每次出栈时,可以直接定位下一个邻接节点,无需遍历全部邻接点。 基于堆栈的 DFS 步骤如下: 1. 设 $graph$ 为无向图的邻接表,$visited$ 为已访问节点集合,$stack$ 为辅助栈。 2. 选择起始节点 $u$,检查当前节点 $u$ 是否为目标节点(看具体题目要求)。 3. 如果 $u$ 是目标节点,直接返回结果。 4. 如果 $u$ 不是目标节点,将 $[u, 0]$(节点 $u$ 及其下一个邻接节点下标 $0$)压入栈,并将 $u$ 标记为已访问:`stack.append([u, 0])`,`visited.add(u)`。 5. 当栈非空时,弹出栈顶元素 $[u, i]$,其中 $i$ 表示下一个待访问的邻接节点下标。 6. 如果 $i$ 小于 $graph[u]$ 的邻接节点数,则取出 $v = graph[u][i]$。 7. 将 $[u, i + 1]$ 压回栈中,表示下次将访问 $u$ 的下一个邻接节点。 8. 如果 $v$ 未被访问,则对 $v$ 进行相关操作(如打印、判定等),并将 $[v, 0]$ 压入栈,同时标记 $v$ 已访问。 9. 重复步骤 $5 \sim 8$,直到栈为空或找到目标节点为止。 这种实现方式可以模拟递归调用过程,且便于控制递归深度,适合递归层数较深或需手动管理回溯的场景。 ### 4.2 基于堆栈实现的深度优先搜索实现代码 ```python class Solution: def dfs_stack(self, graph, u): """ 基于显式栈的深度优先搜索(DFS),适用于无向图/有向图的邻接表表示。 :param graph: dict,邻接表,key为节点,value为邻接节点列表 :param u: 起始节点 """ visited = set() # 记录已访问节点,防止重复遍历 stack = [] # 显式栈,模拟递归过程 stack.append([u, 0]) # 入栈:节点u及其下一个待访问邻接节点的下标 0 visited.add(u) # 标记起始节点已访问 print(u) # 访问起始节点 while stack: cur, idx = stack.pop() # 取出当前节点及其下一个邻接节点下标 neighbors = graph[cur] # 当前节点的所有邻接节点 # 如果还有未遍历的邻接节点 if idx < len(neighbors): v = neighbors[idx] # 取出下一个邻接节点 stack.append([cur, idx + 1]) # 当前节点下标 + 1,回溯时继续遍历下一个邻接点 if v not in visited: print(v) # 访问新节点 visited.add(v) # 标记为已访问 stack.append([v, 0]) # 新节点入栈,准备遍历其邻接节点 # 也可以返回 visited 集合,便于后续处理 # return visited # 示例图(邻接表形式,节点为字符串,边为无向边) graph = { "1": ["2", "3"], "2": ["1", "3", "4"], "3": ["1", "2", "4", "5"], "4": ["2", "3", "5", "6"], "5": ["3", "4"], "6": ["4", "7"], "7": [] } # 从节点 "1" 开始进行深度优先搜索 Solution().dfs_stack(graph, "1") ``` ## 5. 深度优先搜索应用 ### 5.1 岛屿数量 #### 5.1.1 题目链接 - [200. 岛屿数量 - 力扣(LeetCode)](https://leetcode.cn/problems/number-of-islands/) #### 5.1.2 题目大意 **描述**:给定一个由字符 `'1'`(陆地)和字符 `'0'`(水)组成的的二维网格 `grid`。 **要求**:计算网格中岛屿的数量。 **说明**: - 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 - 此外,你可以假设该网格的四条边均被水包围。 - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 300$。 - $grid[i][j]$ 的值为 `'0'` 或 `'1'`。 **示例**: - 示例 1: ```python 输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1 ``` - 示例 2: ```python 输入:grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] ] 输出:3 ``` #### 5.1.3 解题思路 本题实质是统计二维网格中「上下左右」相连的 `'1'` 组成的连通块数量,即岛屿的个数。 可以采用深度优先搜索(DFS)或广度优先搜索(BFS)来实现。 ##### 思路 1:深度优先搜索 1. 遍历整个 $grid$ 网格。 2. 当遇到字符为 `'1'` 的格子时,说明发现了新的岛屿,执行深度优先搜索(DFS): - 将当前格子标记为 `'0'`,避免重复访问。 - 递归访问其上下左右四个相邻格子(即 $(i - 1, j)$、$(i + 1, j)$、$(i, j - 1)$、$(i, j + 1)$)。 - 如果递归过程中遇到越界或当前格子为 `'0'`,则直接返回。 3. 每当一次完整的 DFS 结束,岛屿计数加一。 4. 最终 DFS 执行的次数即为岛屿的总数。 ##### 思路 1:代码 ```python class Solution: def dfs(self, grid, i, j): n = len(grid) m = len(grid[0]) if i < 0 or i >= n or j < 0 or j >= m or grid[i][j] == '0': return 0 grid[i][j] = '0' self.dfs(grid, i + 1, j) self.dfs(grid, i, j + 1) self.dfs(grid, i - 1, j) self.dfs(grid, i, j - 1) def numIslands(self, grid: List[List[str]]) -> int: count = 0 for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == '1': self.dfs(grid, i, j) count += 1 return count ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。其中 $m$ 和 $n$ 分别为行数和列数。 - **空间复杂度**:$O(m \times n)$。 ### 5.2 克隆图 #### 5.2.1 题目链接 - [133. 克隆图 - 力扣(LeetCode)](https://leetcode.cn/problems/clone-graph/) #### 5.2.2 题目大意 **描述**:以每个节点的邻接列表形式(二维列表)给定一个无向连通图,其中 $adjList[i]$ 表示值为 $i + 1$ 的节点的邻接列表,$adjList[i][j]$ 表示值为 $i + 1$ 的节点与值为 $adjList[i][j]$ 的节点有一条边。 **要求**:返回该图的深拷贝。 **说明**: - 节点数不超过 $100$。 - 每个节点值 $Node.val$ 都是唯一的,$1 \le Node.val \le 100$。 - 无向图是一个简单图,这意味着图中没有重复的边,也没有自环。 - 由于图是无向的,如果节点 $p$ 是节点 $q$ 的邻居,那么节点 $q$ 也必须是节点 $p$ 的邻居。 - 图是连通图,你可以从给定节点访问到所有节点。 **示例**: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/02/01/133_clone_graph_question.png) ```python 输入:adjList = [[2,4],[1,3],[2,4],[1,3]] 输出:[[2,4],[1,3],[2,4],[1,3]] 解释: 图中有 4 个节点。 节点 1 的值是 1,它有两个邻居:节点 2 和 4 。 节点 2 的值是 2,它有两个邻居:节点 1 和 3 。 节点 3 的值是 3,它有两个邻居:节点 2 和 4 。 节点 4 的值是 4,它有两个邻居:节点 1 和 3 。 ``` ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/02/01/graph-1.png) ```python 输入:adjList = [[2],[1]] 输出:[[2],[1]] ``` #### 5.2.3 解题思路 所谓深拷贝,就是构建一张与原图结构、值均一样的图,但是所用的节点不再是原图节点的引用,即每个节点都要新建。 可以用深度优先搜索或者广度优先搜索来做。 ##### 思路 1:深度优先搜索 1. 使用哈希表 $visitedDict$ 记录原图节点与其对应的克隆节点,键为原图节点,值为克隆节点。 2. 从给定节点出发,采用深度优先搜索遍历原图: 1. 如果当前节点已在哈希表中,直接返回对应的克隆节点,避免重复克隆。 2. 如果当前节点未被访问,则新建一个克隆节点,并将其加入哈希表。 3. 遍历当前节点的所有邻居,对每个邻居递归调用克隆函数,并将返回的克隆节点加入当前克隆节点的邻居列表。 3. 递归返回当前节点的克隆节点。 ##### 思路 1:代码 ```python class Solution: def cloneGraph(self, node: 'Node') -> 'Node': if not node: return node visitedDict = dict() def dfs(node: 'Node') -> 'Node': if node in visitedDict: return visitedDict[node] clone_node = Node(node.val, []) visitedDict[node] = clone_node for neighbor in node.neighbors: clone_node.neighbors.append(dfs(neighbor)) return clone_node return dfs(node) ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 为图中节点数量。 - **空间复杂度**:$O(n)$。 ## 练习题目 - [0200. 岛屿数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-islands.md) - [0133. 克隆图](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/clone-graph.md) - [0494. 目标和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/target-sum.md) - [0841. 钥匙和房间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/keys-and-rooms.md) - [0695. 岛屿的最大面积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/max-area-of-island.md) - [0130. 被围绕的区域](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/surrounded-regions.md) - [0417. 太平洋大西洋水流问题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/pacific-atlantic-water-flow.md) - [1020. 飞地的数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/number-of-enclaves.md) - [1254. 统计封闭岛屿的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/number-of-closed-islands.md) - [深度优先搜索题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%B7%B1%E5%BA%A6%E4%BC%98%E5%85%88%E6%90%9C%E7%B4%A2%E9%A2%98%E7%9B%AE) ## 参考资料 - 【文章】[深度优先搜索 - LeetBook - 力扣(LeetCode)](https://leetcode.cn/leetbook/read/dfs/egx6xc/) - 【文章】[算法数据结构:深度优先搜索(DFS) - 掘金](https://juejin.cn/post/6864348493721387021) - 【文章】[Python 图的 BFS 与 DFS - 黄蜜桃的博客 - CSDN 博客](https://blog.csdn.net/qq_37738656/article/details/83027943) - 【文章】[图的深度优先遍历(递归、非递归;邻接表,邻接矩阵)_zjq_smile 的博客 - CSDN博客](https://blog.csdn.net/zscfa/article/details/75947816) - 【题解】[200. 岛屿数量(DFS / BFS) - 岛屿数量 - 力扣(LeetCode)](https://leetcode.cn/problems/number-of-islands/solution/number-of-islands-shen-du-you-xian-bian-li-dfs-or-/) ================================================ FILE: docs/06_graph/06_04_graph_bfs.md ================================================ ## 1. 广度优先搜索简介 > **广度优先搜索算法(Breadth First Search,简称 BFS)**:是一种用于遍历或搜索树、图等结构的经典算法。BFS 从起始节点出发,按照层级逐步向外扩展,优先访问距离起点较近的节点,再访问距离较远的节点,直到遍历完整个结构或找到目标节点。 由于 BFS 的遍历顺序具有「先进先出」的特性,因此通常借助「队列」来实现。 ## 2. 广度优先搜索算法步骤 下面以无向图为例,简要梳理广度优先搜索的基本流程: 1. 将起始节点 $u$ 加入队列,并标记为已访问。 2. 当队列不为空时,重复以下操作: 1. 从队列头部取出一个节点 $x$,访问该节点。 2. 遍历 $x$ 的所有未被访问的邻接节点 $v$,将它们加入队列并标记为已访问,避免重复访问。 3. 如果在遍历过程中找到目标节点,可提前结束;否则直到队列为空为止。 ::: tabs#BFS @tab <1> ![广度优先搜索 1](https://qcdn.itcharge.cn/images/20230905152316.png) @tab <2> ![广度优先搜索 2](https://qcdn.itcharge.cn/images/20230905152327.png) @tab <3> ![广度优先搜索 3](http://qcdn.itcharge.cn/images/20231009141628.png) @tab <4> ![广度优先搜索 4](https://qcdn.itcharge.cn/images/20230905152401.png) @tab <5> ![广度优先搜索 5](https://qcdn.itcharge.cn/images/20230905152420.png) @tab <6> ![广度优先搜索 6](https://qcdn.itcharge.cn/images/20230905152433.png) @tab <7> ![广度优先搜索 7](https://qcdn.itcharge.cn/images/20230905152445.png) ::: ## 3. 基于队列实现的广度优先搜索 ### 3.1 基于队列实现的广度优先搜索算法步骤 1. 定义 $graph$ 为无向图的邻接表,$visited$ 为已访问节点的集合,$queue$ 为队列,$u$ 为起始节点。实现函数 `def bfs(graph, u):`。 2. 将起始节点 $u$ 加入 $visited$ 集合,并入队到 $queue$。 3. 当 $queue$ 非空时,循环执行以下操作: 1. 弹出队首节点 $u$,访问该节点,并进行相应处理(如打印、判定等,视题目要求)。 2. 遍历 $u$ 的所有邻接节点 $v$,如果 $v$ 未被访问,则将 $v$ 加入 $visited$ 集合,并入队到 $queue$。 4. 重复上述过程,直到队列为空,遍历结束。 ### 3.2 基于队列实现的广度优先搜索实现代码 ```python import collections class Solution: def bfs(self, graph, u): visited = set() # 使用 visited 标记访问过的节点 queue = collections.deque([]) # 使用 queue 存放临时节点 visited.add(u) # 将起始节点 u 标记为已访问 queue.append(u) # 将起始节点 u 加入队列中 while queue: # 队列不为空 u = queue.popleft() # 取出队头节点 u print(u) # 访问节点 u for v in graph[u]: # 遍历节点 u 的所有未访问邻接节点 v if v not in visited: # 节点 v 未被访问 visited.add(v) # 将节点 v 标记为已访问 queue.append(v) # 将节点 v 加入队列中 graph = { "1": ["2", "3"], "2": ["1", "3", "4"], "3": ["1", "2", "4", "5"], "4": ["2", "3", "5", "6"], "5": ["3", "4"], "6": ["4", "7"], "7": [] } # 基于队列实现的广度优先搜索 Solution().bfs(graph, "1") ``` ## 4. 广度优先搜索应用 ### 4.1 克隆图 #### 4.1.1 题目链接 - [133. 克隆图 - 力扣(LeetCode)](https://leetcode.cn/problems/clone-graph/) #### 4.1.2 题目大意 **描述**:以每个节点的邻接列表形式(二维列表)给定一个无向连通图,其中 $adjList[i]$ 表示值为 $i + 1$ 的节点的邻接列表,$adjList[i][j]$ 表示值为 $i + 1$ 的节点与值为 $adjList[i][j]$ 的节点有一条边。 **要求**:返回该图的深拷贝。 **说明**: - 节点数不超过 $100$。 - 每个节点值 $Node.val$ 都是唯一的,$1 \le Node.val \le 100$。 - 无向图是一个简单图,这意味着图中没有重复的边,也没有自环。 - 由于图是无向的,如果节点 $p$ 是节点 $q$ 的邻居,那么节点 $q$ 也必须是节点 $p$ 的邻居。 - 图是连通图,你可以从给定节点访问到所有节点。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/02/01/133_clone_graph_question.png) ```python 输入:adjList = [[2,4],[1,3],[2,4],[1,3]] 输出:[[2,4],[1,3],[2,4],[1,3]] 解释: 图中有 4 个节点。 节点 1 的值是 1,它有两个邻居:节点 2 和 4 。 节点 2 的值是 2,它有两个邻居:节点 1 和 3 。 节点 3 的值是 3,它有两个邻居:节点 2 和 4 。 节点 4 的值是 4,它有两个邻居:节点 1 和 3 。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/02/01/graph-1.png) ```python 输入:adjList = [[2],[1]] 输出:[[2],[1]] ``` #### 4.1.3 解题思路 ##### 思路 1:广度优先搜索 1. 使用哈希表 $visited$ 记录原图节点与其克隆节点的映射关系,键为原图节点,值为对应的克隆节点。使用队列 $queue$ 辅助实现广度优先遍历。 2. 首先根据起始节点 $node$ 创建其克隆节点,并加入哈希表:`visited[node] = Node(node.val, [])`,同时将 $node$ 入队:`queue.append(node)`。 3. 当队列不为空时,循环执行以下操作: 1. 弹出队首节点 $node_u$,遍历其所有邻居 $node_v$。 2. 如果 $node_v$ 尚未被访问(即不在 $visited$ 中),则为其创建克隆节点,加入哈希表,并入队:`visited[node_v] = Node(node_v.val, [])`,`queue.append(node_v)`。 3. 将 $node_v$ 的克隆节点加入 $node_u$ 的克隆节点的邻居列表:`visited[node_u].neighbors.append(visited[node_v])`。 4. 队列为空时,广度优先遍历结束,返回起始节点的克隆节点 `visited[node]` 即可。 ##### 思路 1:代码 ```python class Solution: def cloneGraph(self, node: 'Node') -> 'Node': if not node: return node visited = dict() queue = collections.deque() visited[node] = Node(node.val, []) queue.append(node) while queue: node_u = queue.popleft() for node_v in node_u.neighbors: if node_v not in visited: visited[node_v] = Node(node_v.val, []) queue.append(node_v) visited[node_u].neighbors.append(visited[node_v]) return visited[node] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 为图中节点数量。 - **空间复杂度**:$O(n)$。 ### 4.2 岛屿的最大面积 #### 4.2.1 题目链接 - [695. 岛屿的最大面积 - 力扣(LeetCode)](https://leetcode.cn/problems/max-area-of-island/) #### 4.2.2 题目大意 **描述**:给定一个只包含 $0$、$1$ 元素的二维数组,$1$ 代表岛屿,$0$ 代表水。一座岛的面积就是上下左右相邻的 $1$ 所组成的连通块的数目。 **要求**:计算出最大的岛屿面积。 **说明**: - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 50$。 - $grid[i][j]$ 为 $0$ 或 $1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/05/01/maxarea1-grid.jpg) ```python 输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]] 输出:6 解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。 ``` - 示例 2: ```python 输入:grid = [[0,0,0,0,0,0,0,0]] 输出:0 ``` #### 4.2.3 解题思路 ##### 思路 1:广度优先搜索 1. 定义变量 $ans$ 用于记录当前找到的最大岛屿面积。 2. 遍历整个二维数组,对于每个值为 $1$ 的格子: 1. 将该格子置为 $0$,并将其坐标加入队列 $queue$,同时用 $temp\_ans$ 记录当前岛屿的面积(初始为 1)。 2. 当队列 $queue$ 非空时,取出队首元素 $(i, j)$,遍历其上下左右四个方向的相邻格子。如果相邻格子为 $1$,则将其置为 $0$(避免重复访问),并加入队列,同时 $temp\_ans$ 加一。 3. 重复上述过程,直到队列为空,表示当前岛屿已全部遍历完毕。 4. 用 $ans = \max(ans, temp\_ans)$ 更新最大岛屿面积。 3. 遍历结束后,返回 $ans$ 作为最大岛屿面积。 ##### 思路 1:代码 ```python import collections class Solution: def maxAreaOfIsland(self, grid: List[List[int]]) -> int: directs = [(0, 1), (0, -1), (1, 0), (-1, 0)] rows, cols = len(grid), len(grid[0]) ans = 0 for i in range(rows): for j in range(cols): if grid[i][j] == 1: grid[i][j] = 0 temp_ans = 1 queue = collections.deque([(i, j)]) while queue: i, j = queue.popleft() for direct in directs: new_i = i + direct[0] new_j = j + direct[1] if new_i < 0 or new_i >= rows or new_j < 0 or new_j >= cols or grid[new_i][new_j] == 0: continue grid[new_i][new_j] = 0 queue.append((new_i, new_j)) temp_ans += 1 ans = max(ans, temp_ans) return ans ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $m$ 和 $n$ 分别为行数和列数。 - **空间复杂度**:$O(n \times m)$。 ## 练习题目 - [0463. 岛屿的周长](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/island-perimeter.md) - [0752. 打开转盘锁](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/open-the-lock.md) - [0279. 完全平方数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/perfect-squares.md) - [0542. 01 矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/01-matrix.md) - [0322. 零钱兑换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) - [LCR 130. 衣橱整理](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ji-qi-ren-de-yun-dong-fan-wei-lcof.md) - [广度优先搜索题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%B9%BF%E5%BA%A6%E4%BC%98%E5%85%88%E6%90%9C%E7%B4%A2%E9%A2%98%E7%9B%AE) ## 参考资料 - 【文章】[广度优先搜索 - LeetBook - 力扣(LeetCode)](https://leetcode.cn/leetbook/read/bfs/e69rh1/) ================================================ FILE: docs/06_graph/06_05_graph_topological_sorting.md ================================================ ## 1. 拓扑排序简介 > **拓扑排序(Topological Sorting)**:是一种针对有向无环图(DAG)的排序方法。它将图中的所有顶点排成一个线性序列,使得对于任意一条有向边 $$,顶点 $u$ 都排在顶点 $v$ 的前面。这个线性序列就叫做拓扑序列。 需要注意的是,只有有向无环图(DAG)才可以进行拓扑排序。无向图或者有向有环图都无法进行拓扑排序。 ![有向无环图](https://qcdn.itcharge.cn/images/202405092308713.png) 以上图为例,这是一个有向无环图(DAG)。$v_1 \rightarrow v_2 \rightarrow v_3 \rightarrow v_4 \rightarrow v_5 \rightarrow v_6$ 是其中一个拓扑序列。$v_1 \rightarrow v_2 \rightarrow v_3 \rightarrow v_4 \rightarrow v_6 \rightarrow v_5$ 也是一个拓扑序列。也就是说,同一个有向无环图可能有多个不同的拓扑序列。 ## 2. 拓扑排序的实现方法 拓扑排序常用两种实现方式,分别是「Kahn 算法」和「DFS 深度优先搜索算法」。下面我们分别用通俗易懂的方式介绍它们的核心思路。 ### 2.1 Kahn 算法 > **Kahn 算法的核心思想**: > > 1. 不断寻找入度为 $0$ 的节点(即没有依赖的节点),将其加入结果序列。 > 2. 删除该节点及其所有出边(即把它对其他节点的影响去除)。 > 3. 重复上述过程,直到所有节点都被处理,或者没有入度为 $0$ 的节点可选(此时说明图中有环,无法拓扑排序)。 #### 2.1.1 Kahn 算法的详细步骤 1. 用一个数组 $indegrees$ 记录每个节点的入度(有多少条边指向它)。 2. 用一个集合 $S$(可以用队列、栈等)存放所有当前入度为 $0$ 的节点。 3. 每次从 $S$ 中取出一个节点 $u$,将其加入拓扑序列 $order$。 4. 遍历 $u$ 的所有邻接节点 $v$,将 $v$ 的入度减 $1$。如果 $v$ 的入度变为 $0$,就把 $v$ 加入 $S$。 5. 重复上述步骤,直到 $S$ 为空。如果此时还有节点未被加入 $order$,说明图中有环,无法拓扑排序。 6. 如果所有节点都被加入 $order$,那么 $order$ 就是该图的一个拓扑序列。 #### 2.1.2 Kahn 算法的实现代码 ```python import collections class Solution: # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) def topologicalSortingKahn(self, graph: dict): # 初始化所有顶点的入度为 0 indegrees = {u: 0 for u in graph} # 统计每个顶点的入度 for u in graph: for v in graph[u]: indegrees[v] += 1 # 将所有入度为 0 的顶点加入队列 S S = collections.deque([u for u in indegrees if indegrees[u] == 0]) order = [] # 用于存储拓扑序列 while S: u = S.pop() # 取出一个入度为 0 的顶点 order.append(u) # 加入拓扑序列 for v in graph[u]: # 遍历 u 的所有邻接点 indegrees[v] -= 1 # 删除 u 指向 v 的边,v 入度减 1 if indegrees[v] == 0: S.append(v) # 如果 v 入度为 0,加入队列 # 如果 order 长度小于顶点数,说明有环,无法拓扑排序 if len(order) != len(indegrees): return [] return order # 返回拓扑序列 def findOrder(self, n: int, edges): """ n: 顶点个数,编号为 0 ~ n - 1 edges: 边列表,每条边为 (u, v),表示 u 指向 v 返回一个拓扑序列(如果有环则返回空列表) """ # 构建邻接表 graph = {i: [] for i in range(n)} for u, v in edges: graph[u].append(v) # 调用 Kahn 算法进行拓扑排序 return self.topologicalSortingKahn(graph) ``` ### 2.2 基于 DFS 的拓扑排序算法 > **DFS 拓扑排序的核心思想**: > > 1. 对于某个顶点 $u$,用深度优先搜索遍历所有从 $u$ 出发的有向边 $$。只有当 $u$ 的所有相邻顶点 $v$ 都已经被搜索完毕后,才将 $u$ 加入拓扑序列。这样保证 $u$ 一定排在所有 $v$ 的前面。 > 2. 因此,我们可以在每次递归回溯到顶点 $u$ 时,把 $u$ 加入一个栈(或列表),最后将栈中的元素逆序输出,就是一种拓扑排序。 #### 2.2.1 DFS 拓扑排序的具体步骤 1. 用集合 $visited$ 记录哪些顶点已经被访问,避免重复遍历。 2. 用集合 $onStack$ 记录当前递归路径上的顶点。如果在一次深搜过程中遇到已经在 $onStack$ 中的顶点,说明图中有环,无法拓扑排序。 3. 用布尔变量 $hasCycle$ 标记图中是否存在环。 4. 对每个未被访问的顶点 $u$,执行以下操作: 1. 如果 $u$ 已经在 $onStack$,说明遇到了环,直接返回。 2. 如果 $u$ 已经被访问过,或者已经检测到有环,也直接返回。 5. 标记 $u$ 已访问,并加入 $onStack$,然后递归遍历 $u$ 的所有邻接点 $v$。 6. 当 $u$ 的所有邻接点都遍历完毕后,将 $u$ 加入拓扑序列 $order$。 7. 回溯时,将 $u$ 从 $onStack$ 中移除。 8. 对所有顶点重复上述过程,直到全部遍历完或检测到环。 9. 如果没有环,最后将 $order$ 逆序输出,就是拓扑排序结果。 #### 2.2.2 DFS 深度优先搜索算法实现代码 ```python import collections class Solution: # 基于 DFS 的拓扑排序,graph 为邻接表,包含所有顶点(即使无出边也要有键) def topologicalSortingDFS(self, graph: dict): visited = set() # 记录已访问过的顶点,防止重复遍历 onStack = set() # 记录当前递归路径上的顶点,用于检测环 order = [] # 存储拓扑序列(后序遍历结果) hasCycle = False # 标记图中是否存在环 def dfs(u): nonlocal hasCycle if hasCycle: # 已经检测到环,直接返回 return if u in onStack: # 当前节点在递归栈中,说明存在环 hasCycle = True return if u in visited: # 已访问过,无需重复遍历 return visited.add(u) # 标记 u 已访问 onStack.add(u) # 标记 u 在当前递归路径上 for v in graph[u]: # 遍历 u 的所有邻接点 dfs(v) # 递归访问 v order.append(u) # 后序位置加入拓扑序列 onStack.remove(u) # 回溯时移除 u,恢复递归路径标记 # 对所有顶点做 DFS,防止图不连通 for u in graph: if u not in visited: dfs(u) if hasCycle: # 有环,无法拓扑排序 return [] order.reverse() # 后序遍历逆序即为拓扑序 return order def findOrder(self, n: int, edges): """ n: 顶点个数,编号为 0 ~ n-1 edges: 边列表,每条边为 (u, v),表示 u 指向 v 返回一个拓扑序列(有环则返回空列表) """ # 构建邻接表,确保每个顶点都在 graph 中 graph = {i: [] for i in range(n)} for u, v in edges: graph[u].append(v) return self.topologicalSortingDFS(graph) ``` ## 3. 拓扑排序的应用 拓扑排序可以用来解决一些依赖关系的问题,比如项目的执行顺序,课程的选修顺序等。 ### 3.1 课程表 II #### 3.1.1 题目链接 - [210. 课程表 II - 力扣](https://leetcode.cn/problems/course-schedule-ii/) #### 3.1.2 题目大意 **描述**:给定一个整数 $numCourses$,代表这学期必须选修的课程数量,课程编号为 $0 \sim numCourses - 1$。再给定一个数组 $prerequisites$ 表示先修课程关系,其中 $prerequisites[i] = [ai, bi]$ 表示如果要学习课程 $ai$ 则必须要先完成课程 $bi$。 **要求**:返回学完所有课程所安排的学习顺序。如果有多个正确的顺序,只要返回其中一种即可。如果无法完成所有课程,则返回空数组。 **说明**: - $1 \le numCourses \le 2000$。 - $0 \le prerequisites.length \le numCourses \times (numCourses - 1)$。 - $prerequisites[i].length == 2$。 - $0 \le ai, bi < numCourses$。 - $ai \ne bi$。 - 所有$[ai, bi]$ 互不相同。 **示例**: - 示例 1: ```python 输入:numCourses = 2, prerequisites = [[1,0]] 输出:[0,1] 解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1]。 ``` - 示例 2: ```python 输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] 输出:[0,2,1,3] 解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。 因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3]。 ``` #### 3.1.3 解题思路 ##### 思路 1:拓扑排序 本题是「[0207. 课程表](https://leetcode.cn/problems/course-schedule/)」的进阶版,只需在上一题的基础上增加一个用于记录课程顺序的数组 $order$。 具体思路如下: 1. 用哈希表 $graph$ 构建课程之间的依赖关系图,同时统计每门课程的入度,存入 $indegrees$ 数组。 2. 使用队列 $S$,将所有入度为 $0$ 的课程编号加入队列。 3. 每次从队列中取出一个课程 $u$,将其加入结果数组 $order$。 4. 遍历课程 $u$ 指向的所有后继课程 $v$,将 $v$ 的入度减 $1$。如果 $v$ 的入度变为 $0$,则将 $v$ 加入队列 $S$。 5. 重复步骤 $3$ 和 $4$,直到队列为空。 6. 最后判断 $order$ 的长度是否等于课程总数。如果相等,说明可以完成所有课程,返回 $order$;否则说明存在环,无法完成所有课程,返回空数组。 ##### 思路 1:代码 ```python import collections class Solution: # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) def topologicalSortingKahn(self, graph: dict): indegrees = {u: 0 for u in graph} # indegrees 用于记录所有顶点入度 for u in graph: for v in graph[u]: indegrees[v] += 1 # 统计所有顶点入度 # 将入度为 0 的顶点存入集合 S 中 S = collections.deque([u for u in indegrees if indegrees[u] == 0]) order = [] # order 用于存储拓扑序列 while S: u = S.pop() # 从集合中选择一个没有前驱的顶点 0 order.append(u) # 将其输出到拓扑序列 order 中 for v in graph[u]: # 遍历顶点 u 的邻接顶点 v indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 S.append(v) # 将其放入集合 S 中 if len(indegrees) != len(order): # 还有顶点未遍历(存在环),无法构成拓扑序列 return [] return order # 返回拓扑序列 def findOrder(self, numCourses: int, prerequisites): graph = dict() for i in range(numCourses): graph[i] = [] for v, u in prerequisites: graph[u].append(v) return self.topologicalSortingKahn(graph) ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 为课程数,$m$ 为先修课程的要求数。 - **空间复杂度**:$O(n + m)$。 ## 练习题目 - [0207. 课程表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/course-schedule.md) - [0210. 课程表 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/course-schedule-ii.md) - [0802. 找到最终的安全状态](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/find-eventual-safe-states.md) - [拓扑排序题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%9B%BE%E7%9A%84%E6%8B%93%E6%89%91%E6%8E%92%E5%BA%8F%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/06_graph/06_06_graph_minimum_spanning_tree.md ================================================ ## 1. 最小生成树的定义 在介绍「最小生成树」之前,先来理解什么是「生成树」。 > **生成树(Spanning Tree)**:对于一个无向连通图 $G$,如果它的一个子图既包含 $G$ 的所有顶点,又是一棵树(即连通且无环),那么这个子图就叫做 $G$ 的生成树。生成树不是唯一的,从不同的顶点出发遍历,可能得到不同的生成树。 简单来说,生成树就是原图的一个子图,既要包含所有顶点,又要用尽量少的边把这些顶点连起来,并且不能有环。 生成树有以下几个显著特点: 1. **包含所有顶点**:生成树必须覆盖原图的所有顶点。 2. **连通性**:生成树是连通的,任意两个顶点之间都能互相到达。 3. **无环性**:生成树中没有环。 4. **边数最少**:生成树的边数总是等于顶点数减 $1$,即 $n - 1$ 条边。 ![图的生成树](https://qcdn.itcharge.cn/images/20231211100145.png) 如上图,左边是一个有 $6$ 个顶点、$7$ 条边的无向图 $G$。右边展示了 $G$ 的两棵不同的生成树,每棵都包含 $6$ 个顶点和 $5$ 条边。 > **最小生成树(Minimum Spanning Tree, MST)**:在所有可能的生成树中,边的权值之和最小的那一棵,就叫做最小生成树。 最小生成树除了具备生成树的所有性质外,还有一个最重要的特点: 1. **边权和最小**:在所有生成树中,最小生成树的边权之和最小。 ![图的最小生成树](https://qcdn.itcharge.cn/images/20231211101937.png) 如上图,左边是原始带权无向图 $G$,右边是 $G$ 的最小生成树,既包含所有顶点,也只有 $5$ 条边,并且所有边的权值加起来最小。 常用的两种最小生成树算法: - **Prim 算法**:从任意一个顶点出发,每次选择一条连接已选顶点集合和未选顶点集合之间权值最小的边,直到所有顶点都被包含。 - **Kruskal 算法**:把所有边按权值从小到大排序,依次选择不会形成环的最小边,直到选够 $n - 1$ 条边。 这两种算法都能有效地帮助我们找到无向图的最小生成树,实现用最小的代价连接所有顶点。 ## 2. Prim 算法 ### 2.1 Prim 算法的核心思想 > **Prim 算法的核心思想**:每次从当前已选顶点集合出发,选择一条连接到未选顶点、且权值最小的边,把对应的顶点和边加入生成树。这样不断扩展,直到所有顶点都被包含,最终得到边权和最小的生成树。 ### 2.2 Prim 算法的实现步骤 1. 首先,把所有顶点分成两组:一组是已经加入生成树的顶点集合 $V_A$,另一组是还未加入的顶点集合 $V_B$。 2. 随便选一个起点 $start$,把它加入 $V_A$。 3. 每次在 $V_A$ 中找一个顶点 $u$,从 $u$ 出发,挑选一条连接 $V_A$ 和 $V_B$ 的最小权值边。 4. 把这条边和它连接的顶点一起加入生成树(即 $V_A$),并更新集合。 5. 重复第 $3$、$4$ 步,直到所有顶点都被加入生成树为止。 这样,Prim 算法就能一步步用最小的代价把所有顶点连起来,得到最小生成树。 ### 2.3 Prim 算法的代码实现 ```python class Solution: # Prim 算法实现,graph 为邻接表(dict of dict),start 为起始顶点编号 def Prim(self, graph, start): size = len(graph) vis = set() # 已经加入最小生成树的顶点集合 dist = [float('inf')] * size # dist[i] 表示当前未加入集合的点 i 到已选集合的最小边权 ans = 0 # 最小生成树的总权值 dist[start] = 0 # 起点到自身距离为 0 # 初始化 dist 数组:起点到其他点的距离 for i in range(size): if i != start: dist[i] = graph[start][i] vis.add(start) # 起点加入已访问集合 for _ in range(size - 1): # 还需加入 size-1 个顶点 min_dis = float('inf') min_dis_pos = -1 # 在未访问的顶点中,选择距离已选集合最近的顶点 for i in range(size): if i not in vis and dist[i] < min_dis: min_dis = dist[i] min_dis_pos = i if min_dis_pos == -1: # 图不连通,无法生成最小生成树 return -1 ans += min_dis # 累加边权 vis.add(min_dis_pos) # 新顶点加入集合 # 用新加入的顶点更新其他未访问顶点的最小边权 for i in range(size): if i not in vis and dist[i] > graph[min_dis_pos][i]: dist[i] = graph[min_dis_pos][i] return ans # 示例:使用 Prim 算法计算最小生成树的权值和 # 构造一个点集,生成邻接矩阵(曼哈顿距离),并求最小生成树 points = [[0, 0]] graph = dict() size = len(points) for i in range(size): x1, y1 = points[i] for j in range(size): x2, y2 = points[j] dist_ij = abs(x2 - x1) + abs(y2 - y1) # 曼哈顿距离 if i not in graph: graph[i] = dict() if j not in graph: graph[j] = dict() graph[i][j] = dist_ij graph[j][i] = dist_ij # 无向图,双向赋值 # 调用 Prim 算法,输出最小生成树的权值和 print(Solution().Prim(graph, 0)) ``` ### 2.4 Prim 算法复杂度分析 Prim 算法的时间复杂度主要由以下两部分构成: 1. **初始化阶段**: - 初始化距离数组和访问集合,时间复杂度为 $O(V)$,其中 $V$ 表示顶点数。 2. **主循环阶段**: - 外层循环共执行 $V - 1$ 次,每次选择一条边加入生成树。 - 每次循环需: - 在线性扫描中找到未访问顶点中距离最小的顶点,复杂度为 $O(V)$。 - 遍历所有顶点,更新距离数组,复杂度为 $O(V)$。 综上,Prim 算法的总时间复杂度为 $O(V^2)$,空间复杂度为 $O(V)$,主要用于存储距离数组和访问集合。 ## 3. Kruskal 算法 ### 3.1 Kruskal 算法的核心思想 > **Kruskal 算法的核心思想**:每次选择当前权重最小的边,判断这条边连接的两个顶点是否已经在同一个连通块(集合)中。如果不在同一个集合,就把这条边加入最小生成树,并把两个集合合并;如果已经在同一个集合,则跳过这条边,避免成环。如此反复,直到最小生成树包含了 $n - 1$ 条边。 在实际实现时,我们通常用「并查集」这种数据结构来高效管理集合的合并与查询操作,快速判断两个顶点是否属于同一个集合。 ### 3.2 Kruskal 算法的实现步骤 1. 把图中所有的边按照权重从小到大排序。 2. 初始化时,每个顶点自成一个集合。 3. 按照排序后的顺序,依次遍历每一条边。 4. 对于每条边,判断它连接的两个顶点是否属于同一个集合: 1. 如果属于同一个集合,说明这条边会形成环,跳过不选。 2. 如果不属于同一个集合,就把这条边加入最小生成树,并把两个集合合并。 5. 重复第 3、4 步,直到最小生成树中有 $n - 1$ 条边($n$ 为顶点数),算法结束。 这样就能保证生成的树没有环,并且总权值最小。 ### 3.3 Kruskal 算法的代码实现 ```python class UnionFind: def __init__(self, n): # 初始化每个节点的父节点为自己 self.parent = [i for i in range(n)] # 连通分量数量 self.count = n def find(self, x): # 查找根节点 while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): # 合并两个集合 root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return False # 已经在同一个集合,无需合并 self.parent[root_x] = root_y self.count -= 1 return True # 合并成功 def is_connected(self, x, y): # 判断两个节点是否属于同一个集合 return self.find(x) == self.find(y) class Solution: def Kruskal(self, edges, size): """ edges: 边集合,每条边为 [u, v, w],表示 u-v 权重为 w size: 顶点数量 返回最小生成树的权值和 """ union_find = UnionFind(size) # 按权重升序排序所有边 edges.sort(key=lambda x: x[2]) ans = 0 # 最小生成树的总权值 edge_count = 0 # 已加入生成树的边数 for u, v, w in edges: # 如果 u 和 v 不连通,则选这条边 if union_find.union(u, v): ans += w edge_count += 1 # 最小生成树边数为 n - 1 时结束 if edge_count == size - 1: break return ans # 示例:使用 Kruskal 算法计算最小生成树的权值和 # 假设有 4 个顶点,边集如下(每条边为 [u, v, w],u 和 v 为顶点编号,w 为权重): edges = [ [0, 1, 1], [0, 2, 3], [1, 2, 1], [1, 3, 4], [2, 3, 2] ] size = 4 # 顶点数量 # 调用 Kruskal ,输出最小生成树的权值和 mst_weight = Solution().Kruskal(edges, size) print("最小生成树的权值和为:", mst_weight) # 输出:最小生成树的权值和为:4 ``` ### 3.4 Kruskal 算法复杂度分析 Kruskal 算法的时间和空间复杂度分析如下: 1. **边的排序**:对 $E$ 条边按权重排序,时间复杂度为 $O(E \log E)$。 2. **并查集操作**: - 查找(find)和合并(union)操作的均摊时间复杂度均为 $O(\alpha(n))$,其中 $\alpha(n)$ 为阿克曼函数的反函数,增长极慢,实际应用中可视为常数。 3. **遍历边集**:遍历所有边,时间复杂度为 $O(E)$。 综上,Kruskal 算法的总时间复杂度为 $O(E \log E)$,其中 $E$ 为边数。空间复杂度为 $O(V)$,$V$ 为顶点数,主要用于并查集的数据结构。 ## 练习题目 - [1584. 连接所有点的最小费用](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/min-cost-to-connect-all-points.md) - [1631. 最小体力消耗路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/path-with-minimum-effort.md) - [0778. 水位上升的泳池中游泳](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/swim-in-rising-water.md) - [图的最小生成树题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%9B%BE%E7%9A%84%E6%9C%80%E5%B0%8F%E7%94%9F%E6%88%90%E6%A0%91%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/06_graph/06_07_graph_shortest_path_01.md ================================================ ## 1. 单源最短路径简介 > **单源最短路径(Single Source Shortest Path)**:在一个带权图 $G = (V, E)$ 中,给定一个起点(源点)$v$,找到从这个源点出发,到图中其他所有顶点的最短路径长度。这里的「最短路径」指的是路径上所有边的权重之和最小。 简单来说,单源最短路径问题就是:从一个点出发,如何走到其他所有点,并且让每条路径的总权重最小。 这个问题在实际生活中非常常见,比如: - 地图导航(如何从一个城市到其他城市距离最短) - 网络路由(数据包如何选择最快的路径传输) - 通信网络优化等 常用的单源最短路径算法有: 1. **Dijkstra 算法**:一种贪心算法,适用于所有边权都为非负数的图。每次选择当前距离源点最近的未处理节点,并用它来更新其它节点的最短距离。 2. **Bellman-Ford 算法**:可以处理有负权边的图。它通过多次遍历所有边,不断尝试用更短的路径更新节点距离,逐步逼近最短路径。 3. **SPFA 算法**:是 Bellman-Ford 的队列优化版本。每次只处理那些距离被更新过的节点,通常效率更高。 不同算法适用于不同类型的图。根据实际问题的特点,选择合适的算法,才能高效地求解单源最短路径问题。 ## 2. Dijkstra 算法 ### 2.1 Dijkstra 算法的核心思想 > **Dijkstra 算法核心思想**:每次选出距离起点最近、最短路尚未确定的节点,用它去尝试更新其它节点的最短距离,逐步扩展,直到所有节点的最短路径都确定。 Dijkstra 算法是解决单源最短路径的经典方法,适用于所有边权为非负数的图。它的流程很简单:每次从未确定最短路的节点中,选出距离起点最近的那个,把它的最短距离「锁定」,并用它去更新其它节点的距离。重复这个过程,直到所有节点的最短路径都被确定。 本质上,Dijkstra 算法是一种贪心策略:每一步都相信当前能确定的最短距离,认为已经确定的节点最短路不会再被更优路径更新。这样一步步扩展,最终得到从起点到所有节点的最短路径。 需要注意的是,Dijkstra 算法 **不能处理有负权边的图**。如果图中存在负权边,最短路径可能会被后续的负权边更新,导致算法失效。这种情况下应使用 Bellman-Ford 或 SPFA 算法。 ### 2.2 Dijkstra 算法的实现步骤 1. 初始化距离数组 $dist$:将起点 $source$ 的距离设为 $0$,其余所有节点的距离设为无穷大。 2. 准备一个访问集合 $visited$,用于记录哪些节点的最短路径已经确定。 3. 每次从未访问的节点中,选出距离起点最近的节点,将其加入 $visited$。 4. 用这个节点尝试更新所有相邻节点的最短距离。 5. 重复步骤 3 和 4,直到所有节点都被访问。 6. 最终,距离数组中即为起点到所有节点的最短路径长度。如果某些节点无法到达,距离仍为无穷大。 ### 2.3 Dijkstra 算法的实现代码 ```python class Solution: def dijkstra(self, graph, n, source): """ Dijkstra 算法求解单源最短路径 :param graph: 邻接表表示的有向图,graph[u] = {v: w, ...} :param n: 节点总数(节点编号从 1 到 n) :param source: 源点编号 :return: dist 数组,dist[i] 表示源点到 i 的最短距离 """ # 距离数组,初始化为无穷大 dist = [float('inf')] * (n + 1) dist[source] = 0 # 源点到自身距离为 0 visited = set() # 已确定最短路的节点集合 while len(visited) < n: # 在所有未访问的节点中,选择距离源点最近的节点 current_node = -1 min_distance = float('inf') for i in range(1, n + 1): if i not in visited and dist[i] < min_distance: min_distance = dist[i] current_node = i # 如果没有可处理的节点(说明剩下的节点不可达),提前结束 if current_node == -1: break visited.add(current_node) # 标记当前节点为已访问 # 遍历当前节点的所有邻居,尝试更新最短距离 for neighbor, weight in graph.get(current_node, {}).items(): if neighbor not in visited: if dist[current_node] + weight < dist[neighbor]: dist[neighbor] = dist[current_node] + weight return dist # 使用示例 # 构建一个有向图,邻接表表示 graph = { 1: {2: 2, 3: 4}, 2: {3: 1, 4: 7}, 3: {4: 3}, 4: {} } n = 4 # 节点数量 source = 1 # 源点 dist = Solution().dijkstra(graph, n, source) print("从节点", source, "到其他节点的最短距离:") for i in range(1, n + 1): if dist[i] == float('inf'): print(f"到节点 {i} 的距离:不可达") else: print(f"到节点 {i} 的距离:{dist[i]}") ``` ### 2.4 Dijkstra 算法复杂度分析 - **时间复杂度**:$O(V^2)$。 - 外层循环每次选择一个未访问且距离最小的节点,共进行 $O(V)$ 次。 - 每次选择最小距离节点时,需要遍历所有未访问节点,复杂度为 $O(V)$。 - 因此整体时间复杂度为 $O(V^2)$。 - **空间复杂度**:$O(V)$。 - 主要空间消耗在距离数组 $dist$ 和访问集合 $visited$,各占 $O(V)$。 - 总空间复杂度为 $O(V)$。 ## 3. 堆优化 Dijkstra 算法 ### 3.1 堆优化 Dijkstra 算法思想 > **堆优化 Dijkstra 算法**:利用优先队列(小根堆)高效选取当前距离最小的节点,将原本 $O(V^2)$ 的查找过程优化为 $O(\log V)$,显著提升算法效率。 传统 Dijkstra 算法每次都要遍历所有未访问节点以找到距离最小者,时间复杂度为 $O(V)$。堆优化后,借助优先队列动态维护所有待处理节点的最短距离,每次取出最小值仅需 $O(\log V)$。 堆优化 Dijkstra 算法的核心思想如下: 1. 用优先队列实时维护所有待处理节点的最短距离; 2. 每次弹出距离最小的节点进行松弛操作; 3. 如果发现更短路径,则更新距离并将新距离入队; 4. 依靠堆的性质,始终保证每次处理的都是当前距离最小的节点。 ### 3.2 堆优化 Dijkstra 算法实现步骤 1. 初始化距离数组,源点距离设为 $0$,其余节点设为无穷大。 2. 创建优先队列,将源节点及其距离 $(0, source)$ 入队。 3. 当优先队列非空时,重复以下操作: - 弹出队首(距离最小)节点; - 如果该节点的距离已大于当前最短距离,跳过; - 否则,遍历其所有邻居,尝试松弛: - 如果通过当前节点到邻居的距离更短,则更新距离并将新距离入队。 4. 队列为空时结束,返回所有节点的最短距离数组。 ### 3.3 堆优化 Dijkstra 算法实现代码 ```python import heapq class Solution: def dijkstra(self, graph, n, source): """ 堆优化 Dijkstra 算法,计算单源最短路径 :param graph: 邻接表,graph[u] = {v: w, ...} :param n: 节点总数(节点编号从 1 到 n) :param source: 源点编号 :return: dist[i] 表示源点到 i 的最短距离 """ # 距离数组,初始化为无穷大 dist = [float('inf')] * (n + 1) dist[source] = 0 # 源点到自身距离为 0 # 小根堆,存储 (距离, 节点) 元组 priority_queue = [(0, source)] while priority_queue: current_distance, current_node = heapq.heappop(priority_queue) # 如果弹出的节点距离不是最短的,说明已被更新,跳过 if current_distance > dist[current_node]: continue # 遍历当前节点的所有邻居 for neighbor, weight in graph.get(current_node, {}).items(): new_distance = current_distance + weight # 如果找到更短路径,则更新并入堆 if new_distance < dist[neighbor]: dist[neighbor] = new_distance heapq.heappush(priority_queue, (new_distance, neighbor)) return dist # 使用示例 # 构建一个有向图,邻接表表示 graph = { 1: {2: 2, 3: 4}, 2: {3: 1, 4: 7}, 3: {4: 3}, 4: {} } n = 4 # 节点数量 source = 1 # 源点编号 dist = Solution().dijkstra(graph, n, source) print("从节点", source, "到其他节点的最短距离:") for i in range(1, n + 1): if dist[i] == float('inf'): print(f"到节点 {i} 的距离:不可达") else: print(f"到节点 {i} 的距离:{dist[i]}") ``` ### 3.4 堆优化 Dijkstra 算法复杂度分析 - **时间复杂度**:$O((V + E) \log V)$。 - 堆优化 Dijkstra 算法中,每个节点最多会被弹出优先队列一次,每次弹出操作的复杂度为 $O(\log V)$。 - 每条边在松弛操作时最多会导致一次入堆,入堆操作的复杂度同样为 $O(\log V)$。 - 因此,总体时间复杂度为 $O((V + E) \log V)$,其中 $V$ 为节点数,$E$ 为边数。 - **空间复杂度**:$O(V)$。 - 主要空间消耗在距离数组和优先队列,二者最坏情况下均为 $O(V)$ 级别。 ## 练习题目 - [0743. 网络延迟时间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/network-delay-time.md) - [0787. K 站中转内最便宜的航班](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/cheapest-flights-within-k-stops.md) - [1631. 最小体力消耗路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/path-with-minimum-effort.md) - [单源最短路径题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8D%95%E6%BA%90%E6%9C%80%E7%9F%AD%E8%B7%AF%E5%BE%84%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/06_graph/06_08_graph_shortest_path_02.md ================================================ ## 1. Bellman-Ford 算法 ### 1.1 Bellman-Ford 算法的核心思想 > **Bellman-Ford 算法**:一种可以处理带有负权边的单源最短路径算法,还能检测图中是否存在负权环。 Bellman-Ford 算法的本质思路如下: 1. 对图中所有的边,重复进行 $V - 1$ 轮「松弛」操作($V$ 为顶点数)。 2. 每一轮松弛,就是尝试用每条边去更新目标节点的最短距离,看能否变得更短。 3. 如果在 $V - 1$ 轮松弛后,仍然有边可以继续更新距离,说明图中存在负权环。 4. 该算法可以正确处理负权边,但如果有负权环,则无法得到最短路径。 ### 1.2 Bellman-Ford 算法的实现步骤 1. 初始化距离数组:源点距离设为 $0$,其余所有节点距离设为无穷大。 2. 重复 $V - 1$ 轮,每轮: - 遍历所有边 - 对每条边尝试松弛:如果通过这条边能让目标节点距离更短,则更新 3. 第 $V$ 轮再遍历所有边,检查是否还能松弛: - 如果还能松弛,说明有负权环 - 如果不能松弛,说明最短路径已确定 4. 返回最短路径的距离数组 ### 1.3 Bellman-Ford 算法的代码实现 ```python class Solution: def bellmanFord(self, graph, n, source): """ Bellman-Ford 算法求解单源最短路径,可处理负权边,并检测负权环。 :param graph: 邻接表,graph[u] = {v: w, ...} :param n: 节点总数(节点编号从 1 到 n) :param source: 源点编号 :return: dist 数组,dist[i] 表示源点到 i 的最短距离;如果存在负权环返回 None """ # 初始化距离数组,所有点距离为正无穷,源点距离为 0 dist = [float('inf')] * (n + 1) dist[source] = 0 # 进行 n - 1 轮松弛操作 for i in range(n - 1): updated = False # 优化:记录本轮是否有更新 # 遍历所有边,尝试松弛 for u in graph: for v, w in graph[u].items(): # 如果 u 可达,且通过 u 到 v 更短,则更新 if dist[u] != float('inf') and dist[v] > dist[u] + w: dist[v] = dist[u] + w updated = True # 如果本轮没有任何更新,说明已提前收敛,可终止 if not updated: break # 再遍历一遍所有边,检查是否还能松弛,如果能则存在负权环 for u in graph: for v, w in graph[u].items(): if dist[u] != float('inf') and dist[v] > dist[u] + w: return None # 存在负权环 return dist ``` ### 1.4 Bellman-Ford 算法复杂度分析 - **时间复杂度**:$O(VE)$。 - Bellman-Ford 算法的核心在于「松弛」操作。对于 $V$ 个顶点,最短路径最多只需要经过 $V - 1$ 条边,因此算法需要进行 $V - 1$ 轮松弛,每一轮都要遍历所有的边。 - 每一轮松弛操作的时间复杂度为 $O(E)$,共进行 $V - 1$ 轮,总体时间复杂度为 $O((V - 1) \times E)$,通常简写为 $O(VE)$。 - 这种复杂度意味着 Bellman-Ford 算法在稠密图(边很多)时效率较低,但在稀疏图或边数较少时仍然可用。 - 另外,算法还会额外进行一次遍历所有边,用于检测负权环,但这不会改变主导复杂度。 - **空间复杂度**:$O(V)$。 - 主要空间消耗在距离数组 $dist$,用于记录源点到每个节点的最短距离,大小为 $O(V)$。 - 图的存储采用邻接表(dict of dict),如果输入本身就是邻接表,则无需额外空间。 - 不需要像 Dijkstra 算法那样维护优先队列,也不需要额外的辅助数组,因此空间开销较小。 ## 2. SPFA 算法 ### 2.1 SPFA 算法的核心思想 > **SPFA(Shortest Path Faster Algorithm)算法**:是 Bellman-Ford 算法的队列优化版本。它通过只处理那些「距离被更新过」的节点,显著减少了无效的松弛操作,从而提升了效率。 SPFA 的本质可以简单理解为: 1. 用队列维护「待处理」的节点,而不是每轮都遍历所有边。 2. 只有当某个节点的最短距离被更新时,才把它加入队列,等待后续处理。 3. 这样可以跳过很多不需要松弛的节点,避免重复无效计算。 4. SPFA 能处理负权边,也能检测负权环。 ### 2.2 SPFA 算法的实现步骤 1. 初始化距离数组:源点距离设为 $0$,其余节点距离设为无穷大。 2. 创建一个队列,把源点加入队列。 3. 当队列不为空时,重复以下操作: - 取出队首节点 $u$。 - 遍历 $u$ 的所有邻居 $v$。 - 如果通过 $u$ 到 $v$ 的距离更短,则更新 $v$ 的距离。 - 如果 $v$ 不在队列中,则将 $v$ 加入队列,等待后续处理。 4. 重复上述过程,直到队列为空。 5. 最终返回源点到各节点的最短距离数组。 这样,SPFA 只会处理那些「有可能被更新」的节点,效率通常远高于朴素的 Bellman-Ford 算法,尤其在稀疏图中表现更优。 ### 2.3 SPFA 算法的代码实现 ```python from collections import deque def spfa(graph, n, source): """ SPFA(Shortest Path Faster Algorithm)算法,求解单源最短路径,可处理负权边,并检测负权环。 :param graph: 邻接表,graph[u] = {v: w, ...} :param n: 节点总数(节点编号从 1 到 n) :param source: 源点编号 :return: dist 数组,dist[i] 表示源点到 i 的最短距离;如果存在负权环返回 None """ # 距离数组,初始化为无穷大,dist[i] 表示源点到 i 的最短距离 dist = [float('inf')] * (n + 1) dist[source] = 0 # 源点到自身距离为 0 # 队列,存储待处理的节点 queue = deque() queue.append(source) # 标记数组,in_queue[i] 表示节点 i 是否在队列中,避免重复入队 in_queue = [False] * (n + 1) in_queue[source] = True # 记录每个节点的入队次数,用于检测负权环 count = [0] * (n + 1) count[source] = 1 # 源点已入队一次 while queue: u = queue.popleft() in_queue[u] = False # 当前节点出队 # 遍历 u 的所有邻居 v for v, w in graph.get(u, {}).items(): # 如果通过 u 到 v 的距离更短,则更新 if dist[v] > dist[u] + w: dist[v] = dist[u] + w # 只有距离被更新,才需要考虑入队 if not in_queue[v]: queue.append(v) in_queue[v] = True count[v] += 1 # 如果某个节点入队次数超过 n-1,说明存在负权环 if count[v] >= n: return None # 存在负权环,返回 None return dist ``` ### 2.4 SPFA 算法复杂度分析 - **时间复杂度**: - 平均情况下,SPFA 算法的时间复杂度为 $O(kE)$,其中 $E$ 表示边数,$k$ 是每个节点的平均入队次数。由于大多数实际图中 $k$ 较小,SPFA 的实际运行效率通常远高于 Bellman-Ford。 - 最坏情况下,SPFA 的时间复杂度退化为 $O(VE)$,其中 $V$ 为节点数。这种情况一般出现在存在大量负权边或特殊构造的图中,此时每条边都可能被反复松弛,导致每个节点最多入队 $V$ 次,与 Bellman-Ford 算法相同。 - 总结:虽然最坏情况下复杂度与 Bellman-Ford 一致,但在绝大多数实际应用中,SPFA 算法由于只处理被更新的节点,往往能大幅减少无效操作,运行速度更快。 - **空间复杂度**:$O(V)$。 - 主要空间消耗包括: - 距离数组 $dist$,用于记录源点到每个节点的最短距离,空间为 $O(V)$; - 队列 $queue$,用于存储待处理节点,最坏情况下队列长度为 $O(V)$; - 标记数组 $in\_queue$ 和入队次数数组 $count$,均为 $O(V)$。 - 因此,SPFA 算法的总空间复杂度为 $O(V)$,适合大多数实际场景。 ## 练习题目 - [0743. 网络延迟时间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/network-delay-time.md) - [0787. K 站中转内最便宜的航班](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/cheapest-flights-within-k-stops.md) - [1631. 最小体力消耗路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/path-with-minimum-effort.md) - [单源最短路径题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8D%95%E6%BA%90%E6%9C%80%E7%9F%AD%E8%B7%AF%E5%BE%84%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/06_graph/06_09_graph_multi_source_shortest_path.md ================================================ ## 1. 多源最短路径简介 > **多源最短路径(All-Pairs Shortest Paths)**:指的是在一个带权图 $G = (V, E)$ 中,计算任意两个顶点之间的最短路径长度。 多源最短路径问题的本质,就是要找出图中每一对顶点之间的最短路径。这类问题在实际生活和工程中非常常见,例如: 1. 网络通信中,生成路由表以确定任意两点之间的最优传输路径; 2. 地图导航系统中,计算所有地点之间的距离矩阵; 3. 社交网络分析中,寻找两个人之间的最短关系链; 4. 交通网络中,规划任意两地之间的最优行车路线。 常用的多源最短路径算法有: 1. **Floyd-Warshall 算法**:一种基于动态规划的方法,能处理负权边,但无法处理负权环。 2. **Johnson 算法**:结合了 Bellman-Ford 和 Dijkstra 算法,既能处理负权边,也能高效应对稀疏图,但同样不能处理负权环。 3. **多次 Dijkstra 算法**:对每个顶点分别运行一次 Dijkstra 算法,适用于没有负权边的图。 ## 2. Floyd-Warshall 算法 ### 2.1 Floyd-Warshall 算法的核心思想 > **Floyd-Warshall 算法**:这是一种经典的动态规划算法,通过不断尝试引入不同的中间节点,来优化任意两点之间的最短路径。 通俗来说,Floyd-Warshall 算法的核心思想如下: 1. 假设要找从顶点 $i$ 到顶点 $j$ 的最短路径,试着经过某个中间顶点 $k$,看看能不能让路径更短。 2. 如果发现「先从 $i$ 到 $k$,再从 $k$ 到 $j$」的路径比原来「直接从 $i$ 到 $j$」的路径更短,就用这个更短的路径来更新答案。 3. 依次尝试所有顶点作为中间点 $k$,每次都用上述方法去优化所有点对之间的最短路径,最终就能得到全局最优解。 ### 2.2 Floyd-Warshall 算法的实现步骤 1. 先初始化一个距离矩阵 $dist$,$dist[i][j]$ 表示从顶点 $i$ 到顶点 $j$ 的当前最短路径长度。 2. 如果 $i$ 和 $j$ 之间有直接的边,就把 $dist[i][j]$ 设为这条边的权重;如果没有,设为无穷大(表示不可达)。 3. 然后,依次枚举每个顶点 $k$ 作为「中转站」: - 对于所有顶点对 $(i, j)$,如果「从 $i$ 经过 $k$ 到 $j$」的路径更短(即 $dist[i][k] + dist[k][j] < dist[i][j]$),就用更短的路径更新 $dist[i][j]$。 4. 重复第 3 步,直到所有顶点都被作为中间点尝试过。 5. 最终,$dist$ 矩阵中每个 $dist[i][j]$ 就是从 $i$ 到 $j$ 的最短路径长度。 ### 2.3 Floyd-Warshall 算法的实现代码 ```python def floyd_warshall(graph, n): """ Floyd-Warshall 算法,计算所有点对之间的最短路径。 :param graph: 邻接表,graph[i] = {j: weight, ...},节点编号为 0~n-1 :param n: 节点总数 :return: dist 矩阵,dist[i][j] 表示 i 到 j 的最短路径长度 """ # 初始化距离矩阵,所有点对距离设为无穷大 dist = [[float('inf')] * n for _ in range(n)] # 距离矩阵对角线设为 0,表示自己到自己的距离为 0 for i in range(n): dist[i][i] = 0 # 设置直接相连的顶点之间的距离 for j, weight in graph.get(i, {}).items(): dist[i][j] = weight # 三重循环,枚举每个中间点 k for k in range(n): for i in range(n): # 跳过不可达的起点 if dist[i][k] == float('inf'): continue for j in range(n): # 跳过不可达的终点 if dist[k][j] == float('inf'): continue # 如果经过 k 能让 i 到 j 更短,则更新 if dist[i][j] > dist[i][k] + dist[k][j]: dist[i][j] = dist[i][k] + dist[k][j] return dist ``` ### 2.4 Floyd-Warshall 算法分析 - **时间复杂度**:$O(V^3)$ - 算法包含三重嵌套循环,分别枚举所有中间点、起点和终点,因此总时间复杂度为 $O(V^3)$。 - **空间复杂度**:$O(V^2)$ - 主要空间消耗在距离矩阵 $dist$,需要 $O(V^2)$ 的空间。 - 由于采用邻接表存储原图结构,无需额外空间存储图的边。 **Floyd-Warshall 算法优点**: 1. 实现简洁,易于理解和编码。 2. 能处理负权边(但不能有负权环)。 3. 可用于检测负权环(如果某个顶点 $i$ 满足 $dist[i][i] < 0$,则存在负权环)。 4. 特别适合稠密图(边数接近 $V^2$)。 **Floyd-Warshall 算法缺点**: 1. 时间复杂度较高,不适合节点数很大的图。 2. 空间复杂度较高,需要维护完整的 $V \times V$ 距离矩阵。 3. 无法处理存在负权环的情况(如果有负权环,最短路无意义)。 ## 3. Johnson 算法 ### 3.1 Johnson 算法的核心思想 > **Johnson 算法**:是一种结合 Bellman-Ford 和 Dijkstra 算法的多源最短路径算法,能够处理负权边,但无法处理负权环。 Johnson 算法的核心思想如下: 1. 通过对图进行重新赋权,将所有边权变为非负,从而使 Dijkstra 算法适用; 2. 对每个顶点分别运行一次 Dijkstra 算法,计算其到其他所有顶点的最短路径; 3. 最后将结果还原为原图的最短路径权值。 ### 3.2 Johnson 算法的实现步骤 1. 向原图添加一个新顶点 $s$,并从 $s$ 向所有其他顶点连一条权重为 0 的边; 2. 使用 Bellman-Ford 算法以 $s$ 为源点,计算 $s$ 到每个顶点 $v$ 的最短距离 $h(v)$; 3. 对于原图中的每条边 $(u, v)$,将其权重调整为 $w'(u, v) = w(u, v) + h(u) - h(v)$,使所有边权非负; 4. 对每个顶点 $u$,以 $u$ 为源点在重新赋权后的图上运行 Dijkstra 算法,得到 $u$ 到所有顶点的最短距离 $d'(u, v)$; 5. 最终结果还原为原图权重:$d(u, v) = d'(u, v) - h(u) + h(v)$,即为原图中 $u$ 到 $v$ 的最短路径长度。 ### 3.3 Johnson 算法的实现代码 ```python from collections import defaultdict import heapq def johnson(graph, n): """ Johnson 算法:多源最短路径,支持负权边但不支持负权环。 :param graph: 邻接表,graph[u] = {v: w, ...},节点编号 0~n-1 :param n: 节点总数 :return: dist 矩阵,dist[i][j] 表示 i 到 j 的最短路径长度;如果有负权环返回 None """ # 1. 构建新图,添加超级源点 s(编号为 n),从 s 向所有顶点连权重为 0 的边 new_graph = defaultdict(dict) for u in graph: for v, w in graph[u].items(): new_graph[u][v] = w for u in range(n): new_graph[n][u] = 0 # s -> u,权重为 0 # 2. Bellman-Ford 算法,计算超级源点 s 到每个顶点的最短距离 h(v) h = [float('inf')] * (n + 1) h[n] = 0 # s 到自身距离为 0 # 最多 n 轮松弛 for _ in range(n): updated = False for u in new_graph: for v, w in new_graph[u].items(): if h[u] != float('inf') and h[v] > h[u] + w: h[v] = h[u] + w updated = True if not updated: break # 检查负权环:如果还能松弛,说明有负环 for u in new_graph: for v, w in new_graph[u].items(): if h[u] != float('inf') and h[v] > h[u] + w: return None # 存在负权环 # 3. 重新赋权:w'(u,v) = w(u,v) + h[u] - h[v],保证所有边权非负 reweighted_graph = defaultdict(dict) for u in graph: for v, w in graph[u].items(): reweighted_graph[u][v] = w + h[u] - h[v] # 4. 对每个顶点运行 Dijkstra 算法,计算最短路径 dist = [[float('inf')] * n for _ in range(n)] for source in range(n): d = [float('inf')] * n d[source] = 0 heap = [(0, source)] visited = [False] * n while heap: cur_dist, u = heapq.heappop(heap) if visited[u]: continue visited[u] = True for v, w in reweighted_graph[u].items(): if d[v] > cur_dist + w: d[v] = cur_dist + w heapq.heappush(heap, (d[v], v)) # 5. 还原原图权重 for v in range(n): if d[v] != float('inf'): dist[source][v] = d[v] - h[source] + h[v] return dist ``` ### 3.4 Johnson 算法复杂度分析 - **时间复杂度**:$O(VE \log V)$ - 需要运行一次 Bellman-Ford 算法,时间复杂度为 $O(VE)$ - 需要运行 $V$ 次 Dijkstra 算法,每次时间复杂度为 $O(E \log V)$ - 因此总时间复杂度为 $O(VE \log V)$ - **空间复杂度**:$O(V^2)$ - 主要空间消耗在距离矩阵($O(V^2)$)以及重新赋权后的图($O(E)$)。 - 因此总体空间复杂度为 $O(V^2)$。 ## 练习题目 - [0815. 公交路线](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/bus-routes.md) - [1162. 地图分析](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/as-far-from-land-as-possible.md) - [多源最短路径题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%A4%9A%E6%BA%90%E6%9C%80%E7%9F%AD%E8%B7%AF%E5%BE%84%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/06_graph/06_10_graph_the_second_shortest_path.md ================================================ ## 1. 次短路径简介 > **次短路径(Second Shortest Path)**:指从起点到终点的所有简单路径中,路径总权值严格大于最短路径、且在此条件下最小的那条路径。 次短路径的本质,是在所有从起点到终点的路径中,找到一条「长度严格大于最短路径」但又尽可能短的路径。换句话说,最短路径是最优解,次短路径则是在排除所有最优解(即所有与最短路径等长的路径)后,找到的次优解。 这种问题在实际生活和工程中非常常见,主要应用于需要「备用方案」或「容错能力」的场景。例如: - **网络路由**:当主路由失效时,快速切换到次短路径,保证数据传输不中断。 - **交通导航与物流**:为司机或配送员提供绕行路线,规避拥堵或突发状况。 - **通信网络**:设计冗余链路,提高网络的健壮性和可靠性。 - **算法竞赛**:常见「求第 2 优解」类问题,或需要多方案备选时。 > **注意**:本文默认边权非负(与 Dijkstra 条件一致)。如果有负权边,请使用适配的算法并谨慎处理。 ## 2. 次短路径常见解法 在实际问题中,寻找次短路径(即严格大于最短路径的最优路径)通常需要对经典的最短路算法进行适当扩展。最常用且高效的方法是基于 Dijkstra 算法的变体。 ### 2.1 扩展版 Dijkstra 的核心思路 > **扩展版 Dijkstra 的核心思路**: > > 在使用优先队列寻找最短路的过程中,同时为每个节点记录两条路径长度:一条是目前已知的最短路径,另一条是比最短路径严格更长、但次优的路径。每次处理节点时,尝试用新路径更新这两条记录:如果新路径比当前最短路径还短,就把原最短路径作为次短路径,并更新最短路径;如果新路径介于最短和次短之间,就更新次短路径。其余情况直接跳过。最终,终点的次短路径记录就是所求答案。 这种方法思路清晰、实现简单,是解决次短路径问题的主流方案。 ### 2.2 扩展版 Dijkstra 的具体步骤 1. 初始化 $dist1$ 和 $dist2$ 数组,全部赋值为无穷大(表示尚未到达)。 2. 将起点的 $dist1$ 设为 $0$,并将起点以距离 $0$ 加入优先队列。 3. 每次从优先队列中取出距离最小的节点 $u$。 4. 枚举 $u$ 的所有邻接节点 $v$,尝试用 $u$ 的当前路径更新 $v$ 的最短和次短距离: - 如果新路径长度小于 $dist1[v]$,则将 $dist1[v]$ 的原值赋给 $dist2[v]$,并用新路径更新 $dist1[v]$,同时将 $v$ 及其新距离加入队列。 - 如果新路径长度介于 $dist1[v]$ 与 $dist2[v]$ 之间(即 $dist1[v] <$ 新路径 $< dist2[v]$),则用新路径更新 $dist2[v]$,并将 $v$ 及其新距离加入队列。 5. 算法结束后,$dist2[target]$ 即为所求的次短路径长度(如果为无穷大则表示不存在)。 ### 2.3 扩展版 Dijkstra 的代码实现 ```python import heapq from collections import defaultdict def second_shortest_path(n, edges, s, t): """ 求解有向/无向图中从 s 到 t 的次短路径长度(严格大于最短路径的最小路径)。 参数说明: n: 节点数(编号 0 ~ n - 1) edges: List[(u, v, w)],每条边 (u, v, w) 表示 u 到 v 有一条权重为 w 的边 s: 起点编号 t: 终点编号 返回: s 到 t 的次短路径长度,如果不存在返回 float('inf') 注意: - 默认边权非负 - 如果为无向图,请取消 graph[v].append((u, w))的注释 """ # 构建邻接表 graph = defaultdict(list) for u, v, w in edges: graph[u].append((v, w)) # 如果是无向图,取消下行注释 # graph[v].append((u, w)) INF = float('inf') dist1 = [INF] * n # dist1[i]:s 到 i 的最短路径长度 dist2 = [INF] * n # dist2[i]:s 到 i 的严格次短路径长度 dist1[s] = 0 # 优先队列,元素为(当前路径长度, 节点编号) pq = [(0, s)] while pq: d, u = heapq.heappop(pq) # 剪枝:如果当前弹出的距离已大于该点的次短路,则无需处理 if d > dist2[u]: continue # 遍历 u 的所有邻居 for v, w in graph[u]: nd = d + w # 新的路径长度 # 如果找到更短的路径,更新最短和次短 if nd < dist1[v]: dist2[v] = dist1[v] dist1[v] = nd heapq.heappush(pq, (dist1[v], v)) # 如果新路径严格介于最短和次短之间,更新次短 elif dist1[v] < nd < dist2[v]: dist2[v] = nd heapq.heappush(pq, (dist2[v], v)) # 其他情况(如 nd 等于 dist1[v] 或大于等于 dist2[v])无需处理 return dist2[t] # 如果为 INF 表示不存在次短路径 ``` ### 2.4 扩展版 Dijkstra 算法分析 - **时间复杂度**:$O((V + E)\log V)$,其中 $V$ 是节点数,$E$ 是边数。与 Dijkstra 同阶,常数略大,因为每点维护两条距离。 - **空间复杂度**:$O(V)$,用于存储距离数组和优先队列。 ## 3. 进阶与常见问题 ### 3.1 无权图 / 单位权图的次短路径 对于无权图或所有边权均为 $1$ 的单位权图,求次短路径时可以采用 BFS(广度优先搜索)思想,同样维护两个距离数组:$dist1$ 表示最短路径,$dist2$ 表示严格次短路径。使用普通队列按层推进,每当遇到更短路径或介于最短和次短之间的路径时,及时更新对应的距离并将节点入队。整体实现思路与扩展版 Dijkstra 类似,只是优先队列换成了普通队列。 ### 3.2 与 K 短路问题的关系 次短路径实际上是 K 短路问题在 $K = 2$ 时的特例。经典的 K 短路算法有 Yen 算法、Eppstein 算法等,但在 $K = 2$ 的场景下,直接用「扩展版 Dijkstra 维护两条距离」往往更简单高效,代码实现也更直观。 ### 3.3 常见易错点与细节说明 - **严格大于最短路径**:次短路径必须严格大于最短路径。如果不存在严格大于最短路径的方案,应返回 $-1$。如果题目允许等长但不同路径作为次短路径,需根据题意调整实现。 - **松弛顺序问题**:当 `nd < dist1[v]` 时,必须先将原有的 `dist1[v]` 赋值给 `dist2[v]`,再更新 `dist1[v]`,否则会丢失正确的次短路径信息。 - **重复 / 等长路径处理**:当 `nd == dist1[v]` 时,通常不应更新 `dist2[v]`(除非题目特别说明等长但不同路径也算次短路径)。 - **边权要求**:本算法默认所有边权为非负。如果存在负权边,需使用 Bellman-Ford 算法的变体,并仔细验证实现的正确性。 - **有向图与无向图的区别**:注意区分有向图和无向图。对于无向图,构图时每条边需正反各加入一次,避免遗漏路径。 ## 练习题目 - [2045. 到达目的地的第二短时间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/second-minimum-time-to-reach-destination.md) - [次短路径题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%AC%A1%E7%9F%AD%E8%B7%AF%E5%BE%84%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/06_graph/06_11_graph_bipartite_basic.md ================================================ ## 1. 二分图简介 > **二分图(Bipartite Graph)**:又称「二部图」,是一类特殊的无向图。其顶点集可以被划分为两个互不重叠的子集,且所有边仅连接这两个子集之间的顶点,同一子集内的顶点之间没有边相连。 直观地说,就是「左边只连右边,左边不互连;右边只连左边,右边不互连」。 ## 2. 二分图判定 > **二分图判定**:判断一个无向图是否可以将所有顶点划分为两个互不重叠的集合,使得每条边的两个端点都分别属于不同的集合。换句话说,图中不存在奇数长度的环,则该图为二分图。 ### 2.1 二分图判定的具体步骤 判断一个无向图是否为二分图,常用的方法是「染色法」:通过给图的每个顶点染上两种不同的颜色,检查是否能做到每条边的两个端点颜色不同。具体步骤如下: 1. **初始化染色数组**:为每个顶点分配颜色标记,初始均为未染色(如 0 表示未染色,1 和 -1 分别代表两种颜色)。 2. **遍历所有顶点**:对每个未染色的顶点,执行一次 BFS 或 DFS 染色(因图可能不连通,需分别处理每个连通分量)。 3. **染色与冲突检测**: - 从当前顶点开始,赋予一种颜色(如 1)。 - 遍历其所有邻接点: - 如果邻接点未染色,则染为相反颜色(-1),并递归 / 迭代继续处理; - 如果邻接点已染色且与当前顶点颜色相同,则发生冲突,说明不是二分图,立即返回 `False`。 4. **全部顶点染色无冲突**:如果所有顶点均成功染色且未出现冲突,则该图为二分图。 ### 2.2 二分图判定的代码实现 ```python def is_bipartite(graph): """ 判断无向图是否为二分图(染色法) :param graph: List[List[int]],邻接表表示的无向图 :return: bool,是否为二分图 """ n = len(graph) colors = [0] * n # 0 表示未染色,1 和 -1 表示两种颜色 def dfs(node, color): """ 对节点 node 进行染色,并递归染色其所有邻居 :param node: 当前节点编号 :param color: 当前节点应染的颜色(1 或 -1) :return: bool,如果染色无冲突返回 True,否则 False """ colors[node] = color # 给当前节点染色 for neighbor in graph[node]: if colors[neighbor] == color: # 邻居和当前节点颜色相同,冲突,非二分图 return False if colors[neighbor] == 0: # 邻居未染色,递归染成相反颜色 if not dfs(neighbor, -color): return False return True for i in range(n): if colors[i] == 0: # 只对未染色的节点(新连通分量)进行 DFS 染色 if not dfs(i, 1): return False # 染色过程中发现冲突,非二分图 return True # 所有节点染色无冲突,是二分图 ``` ### 2.3 二分图判定的算法分析 - **时间复杂度**:$O(V + E)$,其中 $V$ 为顶点数,$E$ 为边数。每个顶点和每条边最多被访问一次。 - **空间复杂度**:$O(V)$,主要用于存储颜色数组和递归/队列。 ## 练习题目 - [0785. 判断二分图](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/is-graph-bipartite.md) - [二分图基础题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BA%8C%E5%88%86%E5%9B%BE%E5%9F%BA%E7%A1%80%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/06_graph/06_12_graph_bipartite_matching.md ================================================ ## 1. 二分图最大匹配简介 > **二分图最大匹配(Maximum Bipartite Matching)**:图论中的一个基础且重要的问题。其目标是在一个二分图中,找到一组两两不相交的边,使得被匹配的点对数量最大。 - **匹配**:在二分图中,选择若干条边,使得这些边之间没有公共端点(即每个点最多只参与一条匹配边)。 - **最大匹配**:在所有可能的匹配中,选出包含边数最多的那一组,即让尽可能多的点被配对。 二分图最大匹配问题有多种经典算法,常用的有以下三类: **1. 匈牙利算法(Hungarian Algorithm,DFS 增广路)**: 通过不断为未匹配的左侧点寻找「增广路」来扩展匹配的经典方法。它通常采用深度优先搜索(DFS)递归实现,代码简洁,易于理解,适合小中规模的数据场景。其时间复杂度为 $O(VE)$,实现简单,适合竞赛和工程快速上手,但在大规模稠密图下效率有限。 **2. Hopcroft-Karp 算法**: 通过分层 BFS 批量寻找多条增广路,每轮可以一次性扩展多组匹配,从而大幅提升效率。该算法先用 BFS 对图进行分层,再用 DFS 在分层图中寻找增广路,适合处理大规模稠密二分图。其时间复杂度为 $O(\sqrt{V}E)$,效率高,但实现相对匈牙利算法更为复杂。 **3. 网络流算法(最大流建模)**: 将二分图最大匹配问题转化为最大流问题:左侧点连接源点,右侧点连接汇点,边容量为 1,最大流即为最大匹配。常用的最大流算法有 Ford-Fulkerson、Edmonds-Karp、Dinic、ISAP 等。其时间复杂度依赖具体算法,Dinic 算法常见为 $O(\min(V^{2/3}, E^{1/2}) \cdot E)$。该方法可扩展到带权匹配、带容量等复杂约束,适合工程和综合性问题,但实现和调试相对繁琐。 ## 2. 匈牙利算法 ### 2.1 增广路介绍 在介绍匈牙利算法之前,先理解「增广路」的概念: > **增广路(Augmenting Path)**:在当前的匹配状态下,从某个尚未匹配的左侧点出发,沿着图中的边依次前进,最终到达一个同样未被匹配的右侧点。要求这条路径上的边类型要交替出现:先走一条未被匹配的边,再走一条已被匹配的边,如此反复,直到终点。只要存在这样的路径,我们就可以把路径上边的匹配状态「翻转」——原本未匹配的边变为匹配,原本已匹配的边变为未匹配,从而让整体匹配数增加 $1$。 增广路的本质是「为未匹配的点找到一条可以扩展匹配的通路」。匈牙利算法正是不断寻找增广路,并沿着增广路调整匹配关系,从而逐步扩大整体匹配规模。 下面详细介绍匈牙利算法的基本思想。 ### 2.2 匈牙利算法的基本思想 > **匈牙利算法的基本思想**: > > 不断为左侧集合中尚未匹配的点寻找一条「增广路」,即从该点出发,通过深度优先搜索(DFS)探索一条起点和终点均为未匹配点、且匹配边与非匹配边交替出现的路径。如果找到这样的路径,就沿路径翻转匹配关系,使整体匹配数增加 1。重复这一过程,直到所有未匹配点都无法再找到增广路为止,最终得到最大匹配。 ### 2.3 匈牙利算法的具体步骤 匈牙利算法的核心流程可以分为以下几个步骤: 1. **初始化匹配关系**:一开始,右侧所有点都没有被匹配(比如用 `match_right = [-1] * right_size` 表示,-1 代表未匹配)。 2. **依次为每个左侧点找对象**:遍历左侧集合的每个点 $u$,尝试为它找到一条「增广路」来配对。 3. **用 DFS 寻找增广路**: - 对当前左侧点 $u$,依次考察它能连到的每个右侧点 $v$: - 如果 $v$ 这轮还没被访问过,先标记已访问,避免重复。 - 如果 $v$ 还没有配对,或者 $v$ 已配对的左侧点 $u'$ 能继续递归找到新的增广路,那么就让 $u$ 和 $v$ 配对(即 `match_right[v] = u`),并返回成功。 - 如果所有邻接的右侧点都无法配对,则返回失败。 4. **重复上述过程**: - 每当成功为一个左侧点找到增广路并配对,整体匹配数加 1。 - 继续尝试下一个左侧点,直到所有左侧点都无法再增广为止。 5. **输出最大匹配数**:最后统计一共配对了多少组,这个数就是二分图的最大匹配数。 ### 2.4 匈牙利算法的代码实现 ```python def max_bipartite_matching(graph, left_size, right_size): """ 二分图最大匹配(匈牙利算法,DFS 增广路实现) :param graph: 邻接表,graph[u] 存储左侧点 u 能连接到的所有右侧点编号(如 [[], [0,2], ...]) :param left_size: 左侧点个数 :param right_size: 右侧点个数 :return: 最大匹配数 """ match_right = [-1] * right_size # 记录每个右侧点当前匹配到的左侧点编号,-1 表示未匹配 result = 0 # 匹配数 for left in range(left_size): visited = [False] * right_size # 每次为一个左侧点增广时,重置右侧点访问标记 if find_augmenting_path(graph, left, visited, match_right): result += 1 # 成功增广,匹配数加一 return result def find_augmenting_path(graph, left, visited, match_right): """ 尝试为左侧点 left 寻找一条增广路 :param graph: 邻接表 :param left: 当前尝试增广的左侧点编号 :param visited: 右侧点访问标记,防止重复访问 :param match_right: 右侧点的匹配关系 :return: 是否找到增广路 """ for right in graph[left]: # 遍历 left 能连接到的所有右侧点 if not visited[right]: # 只尝试未访问过的右侧点 visited[right] = True # 标记已访问 # 如果右侧点未匹配,或其当前匹配的左侧点还能找到新的增广路 if match_right[right] == -1 or find_augmenting_path(graph, match_right[right], visited, match_right): match_right[right] = left # 配对成功,更新匹配关系 return True return False # 没有找到增广路 ``` ### 2.5 匈牙利算法的算法分析 - **时间复杂度**:O(VE),其中 V 表示顶点数,E 表示边数。每次尝试增广时,最坏情况下需要遍历所有边,整体复杂度为 O(VE)。 - **空间复杂度**: - 如果使用邻接矩阵存储图,空间复杂度为 O(V²)。 - 如果使用邻接表存储图,空间复杂度为 O(V + E)。 ## 3. Hopcroft-Karp 算法 ### 3.1 Hopcroft-Karp 算法的基本思想 > **Hopcroft-Karp 算法的基本思想**: > > 每轮通过 BFS 对图进行分层,快速找到所有最短的不相交增广路,然后用 DFS 同时增广多条路径,从而大幅提升匹配效率。与传统的匈牙利算法每次只增广一条路径不同,Hopcroft-Karp 算法每轮能批量增广多条路径,极大减少了总的增广次数,因此在大规模二分图上表现尤为优越。 ### 3.2 Hopcroft-Karp 算法的具体步骤 #### Hopcroft-Karp 算法的具体步骤 Hopcroft-Karp 算法高效求解二分图最大匹配,其核心思想是 **分层批量增广**,每轮同时增广多条最短增广路,极大提升效率。具体流程如下: 1. **初始化匹配关系**:所有点一开始都没有匹配对象。 2. **分层(BFS)**: - 首先,对所有未匹配的左侧点进行广度优先搜索(BFS),给每个左侧点分配一个「层号」。 - 在搜索过程中,只能沿着「未匹配的边 → 已匹配的边」交替前进,这样就能构建出分层图。 - 如果在 BFS 过程中遇到未匹配的右侧点,说明找到了增广路,并记录下最短的增广路长度。 3. **批量增广(DFS)**: - 接下来,对所有未匹配的左侧点,按照分层图,用深度优先搜索(DFS)去找增广路。 - 只在层号递增的方向递归查找,找到一条增广路就立即进行匹配。 - 这样每一轮可以同时增广多条互不重叠的最短增广路,大大提高效率。 4. **重复迭代**: - 如果本轮找到了增广路,就更新匹配关系,然后回到第 $2$ 步,继续分层和增广。 - 如果本轮没有找到任何增广路,算法结束,此时的匹配就是最大匹配。 ### 3.3 Hopcroft-Karp 算法的代码实现 ```python from collections import deque def hopcroft_karp(graph, left_size, right_size): """ Hopcroft-Karp 算法求二分图最大匹配 :param graph: List[List[int]],graph[i] 存储左侧点 i 能连到的所有右侧点编号 :param left_size: 左侧点数量 :param right_size: 右侧点数量 :return: 最大匹配数 """ # match_left[i] = j 表示左侧点 i 匹配到右侧点 j,未匹配为 -1 match_left = [-1] * left_size # match_right[j] = i 表示右侧点 j 匹配到左侧点 i,未匹配为 -1 match_right = [-1] * right_size result = 0 # 匹配数 while True: # 1. BFS 分层,dist[i] 表示左侧点 i 到未匹配状态的最短距离 dist = [-1] * left_size queue = deque() for i in range(left_size): if match_left[i] == -1: dist[i] = 0 queue.append(i) # 标记本轮是否存在增广路 found_augmenting = False while queue: u = queue.popleft() for v in graph[u]: if match_right[v] == -1: # 右侧点 v 未匹配,说明存在增广路 found_augmenting = True elif dist[match_right[v]] == -1: # 沿着匹配边走,分层 dist[match_right[v]] = dist[u] + 1 queue.append(match_right[v]) if not found_augmenting: # 没有增广路,算法结束 break # 2. DFS 尝试批量增广 def dfs(u): for v in graph[u]: # 如果右侧点未匹配,或者可以沿着分层图递归找到增广路 if match_right[v] == -1 or (dist[match_right[v]] == dist[u] + 1 and dfs(match_right[v])): match_left[u] = v match_right[v] = u return True # 没有找到增广路 return False # 3. 对所有未匹配的左侧点尝试增广 for i in range(left_size): if match_left[i] == -1: if dfs(i): result += 1 return result ``` ### 3.4 Hopcroft-Karp 算法的算法分析 - **时间复杂度**:$O(\sqrt{V}E)$,其中 $V$ 为顶点数,$E$ 为边数。相比传统匈牙利算法,Hopcroft-Karp 算法通过分层批量增广,大幅减少了增广路的查找次数,提升了整体效率。 - **空间复杂度**:$O(V + E)$,主要用于存储邻接表、匹配关系和辅助队列等数据结构。 ### 3.5 Hopcroft-Karp 算法优化 1. **双向 BFS**:从左右两侧同时进行 BFS,进一步缩小搜索范围,加快分层过程。 2. **动态分层策略**:根据当前的匹配情况灵活调整分层方式,提高增广路查找效率。 3. **贪心预处理**:在正式执行算法前,先用贪心策略进行初步匹配,为后续批量增广打下基础。 4. **并行与分布式优化**:利用多线程或分布式计算资源,实现 BFS/DFS 的并行处理,提升整体运行速度。 ## 4. 网络流算法 ### 4.1 网络流算法的基本思想 二分图最大匹配问题可以巧妙地转化为网络流中的最大流问题来求解。其核心思想如下: - **建模方式**:将二分图的左侧点、右侧点分别作为网络中的两类节点,额外引入一个「源点」和一个「汇点」。 - 源点 $S$ 向所有左侧点连一条容量为 $1$ 的有向边。 - 所有右侧点向汇点 $T$ 连一条容量为 $1$ 的有向边。 - 原二分图中每条左侧点 $u$ 到右侧点 $v$ 的边,建一条 $u \to v$ 的有向边,容量为 $1$。 - **流量含义**:每条边的容量为 $1$,表示每个点最多只能参与一次匹配。网络中从 $S$ 到 $T$ 的最大流量,恰好等于二分图的最大匹配数。 - **求解过程**:使用最大流算法(如 Ford-Fulkerson、Edmonds-Karp、Dinic 等)在该网络上求 $S$ 到 $T$ 的最大流,流量的大小即为最大匹配数。 > **网络流算法的直观理解**: > > 每当在网络中找到一条从 $S$ 到 $T$ 的增广路径,就等价于在原二分图中新增一对匹配。由于每个点与源点或汇点之间的边容量均为 $1$,确保每个点最多只参与一次匹配,严格满足二分图匹配的要求。 ### 4.2 网络流算法的具体步骤 ### 4.2 网络流算法的具体步骤 将二分图最大匹配问题转化为最大流问题,通常分为以下几个步骤: 1. **引入源点与汇点**:新增源点 $S$ 和汇点 $T$。 2. **源点连向左侧所有点**:从 $S$ 向每个左侧点各连一条容量为 $1$ 的有向边,表示每个左侧点最多参与一次匹配。 3. **右侧所有点连向汇点**:从每个右侧点向 $T$ 各连一条容量为 $1$ 的有向边,表示每个右侧点最多参与一次匹配。 4. **原二分图边建模**:对于原图中每条左侧点 $u$ 到右侧点 $v$ 的边,添加 $u \to v$ 的有向边,容量为 $1$。 5. **执行最大流算法**:在该网络上运行最大流算法(如 Edmonds-Karp、Dinic 等),计算 $S$ 到 $T$ 的最大流量。 6. **得到最大匹配数**:最大流的数值即为原二分图的最大匹配数。 ### 4.3 网络流算法的代码实现 ```python from collections import defaultdict, deque def max_flow_bipartite_matching(graph, left_size, right_size): """ 使用网络流(Ford-Fulkerson 算法)求解二分图最大匹配 :param graph: List[List[int]],左侧每个点可连的右侧点编号列表 :param left_size: 左侧点个数 :param right_size: 右侧点个数 :return: 最大匹配数 """ # 构建网络流图,节点编号: # 0 ~ left_size-1:左侧点 # left_size ~ left_size+right_size-1:右侧点 # source: left_size+right_size # sink: left_size+right_size+1 flow_graph = defaultdict(dict) source = left_size + right_size sink = source + 1 # 源点到左侧点,容量为 1 for i in range(left_size): flow_graph[source][i] = 1 flow_graph[i][source] = 0 # 反向边,初始为 0 # 右侧点到汇点,容量为 1 for i in range(right_size): right_node = left_size + i flow_graph[right_node][sink] = 1 flow_graph[sink][right_node] = 0 # 反向边 # 左侧点到右侧点,容量为 1 for i in range(left_size): for j in graph[i]: right_node = left_size + j flow_graph[i][right_node] = 1 flow_graph[right_node][i] = 0 # 反向边 def bfs(): """ BFS 寻找一条增广路,返回每个节点的父节点 """ parent = [-1] * (sink + 1) queue = deque([source]) parent[source] = -2 # 源点特殊标记 while queue: u = queue.popleft() for v, capacity in flow_graph[u].items(): # 只走有剩余容量且未访问过的点 if parent[v] == -1 and capacity > 0: parent[v] = u if v == sink: return parent # 找到汇点,返回路径 queue.append(v) return None # 未找到增广路 def ford_fulkerson(): """ 主流程:不断寻找增广路并更新残量网络 """ max_flow = 0 while True: parent = bfs() if not parent: break # 没有增广路,算法结束 # 计算本次增广路的最小残量(本题均为 1,写全以便扩展) v = sink min_capacity = float('inf') while v != source: u = parent[v] min_capacity = min(min_capacity, flow_graph[u][v]) v = u # 沿增广路更新正反向边的容量 v = sink while v != source: u = parent[v] flow_graph[u][v] -= min_capacity flow_graph[v][u] += min_capacity v = u max_flow += min_capacity # 累加总流量 return max_flow return ford_fulkerson() ``` ### 4.4 网络流算法的算法分析 - **时间复杂度分析**: 1. **Ford-Fulkerson 算法**:$O(VE^2)$。该算法每次通过 DFS/BFS 寻找一条增广路,每次最多增加 1 单位流量,最坏情况下需要 $O(E)$ 次增广,每次增广遍历 $O(E)$ 条边,总共 $O(VE^2)$。适合边权为 1 或较小的稀疏图,实际中常用于教学和小规模数据。 2. **Dinic 算法**:$O(V^2E)$。Dinic 算法通过分层网络和多路增广优化了增广过程,理论上在一般网络流问题中表现优秀,尤其适合稠密图。对于二分图最大匹配,Dinic 的实际复杂度可降为 $O(\sqrt{V}E)$,但一般网络流场景下为 $O(V^2E)$。 3. **ISAP 算法**:$O(V^2E)$。ISAP(Improved Shortest Augmenting Path)算法通过维护距离标号和当前弧优化增广过程,适合大规模稠密图,实际表现优于 Dinic,但理论复杂度同为 $O(V^2E)$。 - **空间复杂度**:$O(V + E)$。主要用于存储残量网络(邻接表/矩阵)和辅助数组(如层次、前驱、队列等),与图的规模线性相关。 ## 练习题目 - [LCP 04. 覆盖](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCP/broken-board-dominoes.md) - [1947. 最大兼容性评分和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/maximum-compatibility-score-sum.md) - [1595. 连通两组点的最小成本](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-cost-to-connect-two-groups-of-points.md) - [二分图最大匹配题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BA%8C%E5%88%86%E5%9B%BE%E6%9C%80%E5%A4%A7%E5%8C%B9%E9%85%8D%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/06_graph/index.md ================================================ ![](https://qcdn.itcharge.cn/images/20250923140508.png) ::: tip 引 言 图如同无垠夜空中繁星交织成的瑰丽星网。 点点星光闪烁,彼此相连,交织成一张神秘而浪漫的梦幻之网,铺展出无尽的路径与无限的可能。 ::: ## 本章内容 - [6.1 图的定义和分类](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_01_graph_basic.md) - [6.2 图的存储结构和问题应用](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_02_graph_structure.md) - [6.3 深度优先搜索](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_03_graph_dfs.md) - [6.4 广度优先搜索](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_04_graph_bfs.md) - [6.5 图的拓扑排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_05_graph_topological_sorting.md) - [6.6 图的最小生成树](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_06_graph_minimum_spanning_tree.md) - [6.7 单源最短路径(一)](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_07_graph_shortest_path_01.md) - [6.8 单源最短路径(二)](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_08_graph_shortest_path_02.md) - [6.9 多源最短路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_09_graph_multi_source_shortest_path.md) - [6.10 次短路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_10_graph_the_second_shortest_path.md) - [6.11 二分图基础](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_11_graph_bipartite_basic.md) - [6.12 二分图最大匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/06_graph/06_12_graph_bipartite_matching.md) ================================================ FILE: docs/07_algorithm/07_01_enumeration_algorithm.md ================================================ ## 1. 枚举算法简介 > **枚举算法(Enumeration Algorithm)**,又称穷举算法,是指根据问题的特点,逐一列出所有可能的解,并与目标条件进行比较,找出满足要求的答案。枚举时要确保不遗漏、不重复。 枚举算法的核心思想就是:遍历所有可能的状态,逐个判断是否满足条件,找到符合要求的解。 由于需要遍历所有状态,枚举算法在问题规模较大时效率较低。但它也有明显优点: 1. 实现简单,易于编程和调试。 2. 基于穷举所有情况,正确性容易验证。 因此,枚举算法常用于小规模问题,或作为其他算法的辅助工具,通过枚举部分信息来提升主算法的效率。 ## 2. 枚举算法的解题思路 ### 2.1 枚举算法的解题思路 枚举算法是最简单、最基础的搜索方法,通常是遇到问题时的首选方案。 由于实现简单,我们可以先用枚举算法尝试解决问题,再考虑是否需要优化。 枚举算法的基本步骤如下: 1. 明确需要枚举的对象、枚举范围和约束条件。 2. 逐一枚举所有可能情况,判断是否满足题意。 3. 思考如何提升枚举效率。 提升效率的常用方法有: - 抓住问题本质,尽量缩小状态空间。 - 增加约束条件,减少无效枚举。 - 利用某些问题特有的性质(例如对称性等),避免重复计算。 ### 2.2 枚举算法的简单应用 以经典的「百钱买百鸡问题」为例: > **问题**:公鸡 5 元/只,母鸡 3 元/只,小鸡 1 元/3 只。用 100 元买 100 只鸡,问各买多少只? **解题步骤**: 1. **确定枚举对象和范围** - 枚举对象:公鸡数 $x$,母鸡数 $y$,小鸡数 $z$ - 枚举范围:$0 \le x, y, z \le 100$ - 约束条件:$5x + 3y + \frac{z}{3} = 100$ 且 $x + y + z = 100$ 2. **暴力枚举** ```python class Solution: def buyChicken(self): for x in range(101): for y in range(101): for z in range(101): if z % 3 == 0 and 5 * x + 3 * y + z // 3 == 100 and x + y + z == 100: print("公鸡 %s 只,母鸡 %s 只,小鸡 %s 只" % (x, y, z)) ``` 3. **优化枚举效率** - 利用 $z = 100 - x - y$ 减少一重循环 - 缩小枚举范围:$x \in [0, 20]$,$y \in [0, 33]$ ```python class Solution: def buyChicken(self): for x in range(21): for y in range(34): z = 100 - x - y if z % 3 == 0 and 5 * x + 3 * y + z // 3 == 100: print("公鸡 %s 只,母鸡 %s 只,小鸡 %s 只" % (x, y, z)) ``` ## 3. 枚举算法的应用 ### 3.1 经典例题:两数之和 #### 3.1.1 题目链接 - [1. 两数之和 - 力扣(LeetCode)](https://leetcode.cn/problems/two-sum/) #### 3.1.2 题目大意 **描述**:给定一个整数数组 $nums$ 和一个整数目标值 $target$。 **要求**:在该数组中找出和为 $target$ 的两个整数,并输出这两个整数的下标。可以按任意顺序返回答案。 **说明**: - $2 \le nums.length \le 10^4$。 - $-10^9 \le nums[i] \le 10^9$。 - $-10^9 \le target \le 10^9$。 - 只会存在一个有效答案。 **示例**: - 示例 1: ```python 输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 ``` - 示例 2: ```python 输入:nums = [3,2,4], target = 6 输出:[1,2] ``` #### 3.1.3 解题思路 ##### 思路 1:枚举算法 1. 通过两重循环,依次枚举数组中所有可能的下标对 $(i, j)$(其中 $i < j$),判断 $nums[i] + nums[j]$ 是否等于 $target$。 2. 一旦找到满足条件的下标对,即 $nums[i] + nums[j] == target$,立即返回这两个下标 $[i, j]$ 作为答案。 ##### 思路 1:代码 ```python class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: # 遍历第一个数的下标 for i in range(len(nums)): # 遍历第二个数的下标(只需从i+1开始,避免和自身重复) for j in range(i + 1, len(nums)): # 判断两数之和是否等于目标值 if nums[i] + nums[j] == target: return [i, j] # 返回下标对 return [] # 如果没有找到,返回空列表 ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为数组 $nums$ 的元素数量。 - **空间复杂度**:$O(1)$。 ### 3.2 统计平方和三元组的数目 #### 3.2.1 题目链接 - [1925. 统计平方和三元组的数目 - 力扣(LeetCode)](https://leetcode.cn/problems/count-square-sum-triples/) #### 3.2.2 题目大意 **描述**:给你一个整数 $n$。 **要求**:请你返回满足 $1 \le a, b, c \le n$ 的平方和三元组的数目。 **说明**: - **平方和三元组**:指的是满足 $a^2 + b^2 = c^2$ 的整数三元组 $(a, b, c)$。 - $1 \le n \le 250$。 **示例**: - 示例 1: ```python 输入 n = 5 输出 2 解释 平方和三元组为 (3,4,5) 和 (4,3,5)。 ``` - 示例 2: ```python 输入:n = 10 输出:4 解释:平方和三元组为 (3,4,5),(4,3,5),(6,8,10) 和 (8,6,10)。 ``` #### 3.2.3 解题思路 ##### 思路 1:枚举算法 直接枚举 $a$ 和 $b$,计算 $c^2 = a^2 + b^2$,判断 $c$ 是否为整数且 $1 \leq c \leq n$,如果满足条件则计数加一,最后返回总数。 该方法时间复杂度为 $O(n^2)$。 - 注意:为避免浮点误差,可以用 $\sqrt{a^2 + b^2 + 1}$ 代替 $\sqrt{a^2 + b^2}$,这样判断 $c$ 是否为整数更安全。 ##### 思路 1:代码 ```python class Solution: def countTriples(self, n: int) -> int: cnt = 0 # 统计满足条件的三元组个数 for a in range(1, n + 1): # 枚举 a for b in range(1, n + 1): # 枚举 b # 计算 c,注意加 1 防止浮点误差 c = int(sqrt(a * a + b * b + 1)) # 判断 c 是否在范围内,且 a^2 + b^2 == c^2 if c <= n and a * a + b * b == c * c: cnt += 1 # 满足条件,计数加一 return cnt # 返回最终统计结果 ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ## 4. 总结 枚举算法通过遍历所有可能状态来寻找解,优点是实现简单、思路直接、正确性易于验证;缺点是在问题规模增大时时间开销迅速上升,往往无法满足效率要求。 它适用于规模较小、可快速验证答案的问题,或作为基线方案、结果校验与对拍工具。实战中应尽量结合剪枝(添加约束、提前判定不可能)、缩小搜索空间(利用对称性、边界与不变量)、降维与变量替换、以及避免重复计算等手段,显著提升效率。 实践建议是:先写出「能过的暴力正确解」,再围绕「减分支、减范围、减重算」迭代优化;当复杂度仍难以接受时,考虑切换到更合适的范式,例如哈希加速、双指针与滑动窗口、二分查找、分治、动态规划或图算法等。 ## 练习题目 - [0001. 两数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/two-sum.md) - [0204. 计数质数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/count-primes.md) - [1925. 统计平方和三元组的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/count-square-sum-triples.md) - [2427. 公因子的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2400-2499/number-of-common-factors.md) - [LCR 180. 文件组合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof.md) - [2249. 统计圆内格点数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/count-lattice-points-inside-a-circle.md) - [枚举算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%9E%9A%E4%B8%BE%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/07_algorithm/07_02_recursive_algorithm.md ================================================ ## 1. 递归简介 > **递归(Recursion)**:是一种将复杂问题分解为与原问题结构相同的子问题,并通过重复求解这些子问题来获得最终解答的方法。在大多数编程语言中,递归通常通过函数自身的调用来实现。 以阶乘为例,数学定义如下: $$fact(n) = \begin{cases} 1 & \text{n = 0} \cr n \times fact(n - 1) & \text{n > 0} \end{cases}$$ 我们可以直接用调用函数自身的方式实现阶乘函数 $fact(n)$,代码如下: ```python def fact(n): # 递归终止条件:当 n 等于 0 时,返回 1 if n == 0: return 1 # 递归调用:n 乘以 fact(n - 1),将问题规模缩小 return n * fact(n - 1) ``` 以 $n = 6$ 为例,阶乘函数 $fact(6)$ 的递归计算步骤如下: ```python fact(6) = 6 * fact(5) = 6 * (5 * fact(4)) = 6 * (5 * (4 * fact(3))) = 6 * (5 * (4 * (3 * fact(2)))) = 6 * (5 * (4 * (3 * (2 * fact(1))))) = 6 * (5 * (4 * (3 * (2 * (1 * fact(0)))))) = 6 * (5 * (4 * (3 * (2 * (1 * 1))))) = 6 * (5 * (4 * (3 * (2 * 1)))) = 6 * (5 * (4 * (3 * 2))) = 6 * (5 * (4 * 6)) = 6 * (5 * 24) = 6 * 120 = 720 ``` 上述例子可以用如下方式描述递归的执行过程: 1. 从 $fact(6)$ 开始,函数不断递归调用自身,依次进入 $fact(5)$、$fact(4)$、……,直到到达最底层的 $fact(0)$。 2. 当 $n == 0$ 时,$fact(0)$ 满足终止条件,直接返回 $1$,递归不再继续向下。 3. 返回阶段,从 $fact(0)$ 开始逐层向上,每一层利用下一层的返回值进行计算: - $fact(1)$:通过 $fact(0)$ 的结果 $1$,计算 $fact(1) = 1 \times 1 = 1$,返回 $1$。 - $fact(2)$:通过 $fact(1)$ 的结果 $1$,计算 $fact(2) = 2 \times 1 = 2$,返回 $2$。 - $fact(3)$:通过 $fact(2)$ 的结果 $2$,计算 $fact(3) = 3 \times 2 = 6$,返回 $6$。 - $fact(4)$:通过 $fact(3)$ 的结果 $6$,计算 $fact(4) = 4 \times 6 = 24$,返回 $24$。 - $fact(5)$:通过 $fact(4)$ 的结果 $24$,计算 $fact(5) = 5 \times 24 = 120$,返回 $120$。 - $fact(6)$:通过 $fact(5)$ 的结果 $120$,计算 $fact(6) = 6 \times 120 = 720$,最终返回 $720$。 整个递归过程分为两步: 1. 向下递推:不断分解问题,直到满足终止条件($n == 0$)。 2. 向上回归:逐层返回结果,最终得到原问题的解(即返回 $fact(6) == 720$)。 如下图所示: ![递推过程](https://qcdn.itcharge.cn/images/20220407160648.png) ![回归过程](https://qcdn.itcharge.cn/images/20220407160659.png) 简而言之,递归包含「递推过程」和「回归过程」: - **递推过程**:将大问题逐步分解为更小的同类子问题,直到终止条件。 - **回归过程**:从最小子问题开始,逐层返回结果,最终解决原问题。 递归的核心思想就是:**把大问题拆解为小问题,逐步解决。** 因为每一层的处理方式相同,所以递归函数会调用自身,这正是递归的本质。 ## 2. 递归与数学归纳法 递归的本质与「数学归纳法」高度契合。我们先简要回顾数学归纳法的基本步骤: 1. **基础情形**:证明当 $n = b$($b$ 通常为 $0$ 或 $1$)时,命题成立。 2. **归纳步骤**:假设当 $n = k$ 时命题成立,进一步证明 $n = k + 1$ 时命题也成立。这里的关键是利用 $n = k$ 成立的假设,推导出 $n = k + 1$ 也成立。 完成上述两步后,即可得出:对于所有 $n \ge b$,命题均成立。 将递归与数学归纳法对应起来,可以这样理解: - **递归终止条件**:对应于数学归纳法的基础情形($n = b$),此时直接给出结果。 - **递推过程**:对应于归纳假设部分(假设 $n = k$ 时成立),即假设我们已经知道了规模更小的问题的解。 - **回归过程**:对应于归纳推导部分(由 $n = k$ 推出 $n = k + 1$),即利用子问题的解,推导出当前问题的解。 正因为数学归纳法的推理方式与递归的分解和回归过程一致,所以在解决如阶乘、前 $n$ 项和、斐波那契数列等问题时,递归算法往往是最自然的选择。 ## 3. 递归三步法 递归的核心思想是:**把大问题拆解为小问题,逐步解决**。写递归时,可以遵循以下三步: 1. **写递推公式**:找出原问题与子问题的关系,写出递推公式。 2. **确定终止条件**:明确递归何时结束,以及结束时的返回值。 3. **翻译为代码**: - 定义递归函数(明确参数和返回值含义) - 编写递归主体(递推公式对应的递归调用) - 加入终止条件的判断和处理 ### 3.1 写递推公式 递归的关键在于:将原问题拆解为更小、结构相同的子问题,并用递推公式加以表达。例如,阶乘问题的递推公式为 $fact(n) = n \times fact(n - 1)$。 在思考递归时,无需把每一层的递推和回归过程都在脑海中推演到底,否则容易陷入细节而感到困惑。以阶乘为例,它只需分解为一个子问题,因此递归的每一步都容易理解和实现。 但当一个问题需要分解为多个子问题时,逐层推演每一步的递推和回归过程就会变得复杂且难以理清。 此时,推荐的思考方式是:假设所有子问题(如 $B$、$C$、$D$)都已经解决,我们只需思考如何利用这些子问题的解来解决原问题 $A$。无需再深入每个子问题的内部递归细节,这样可以大大简化思考难度。 实际上,从原问题 $A$ 拆解为子问题 $B$、$C$、$D$ 的过程,就是递归的「递推过程」;而将子问题的解合并为原问题的解,则是「回归过程」。只要明确了如何划分子问题,以及如何通过子问题的解来解决原问题,就能顺利写出递推公式。 因此,编写递归时,重点关注原问题与子问题之间的关系,并据此写出递推公式即可。 ### 3.2 明确终止条件 递归必须有终止条件(递归出口),否则会无限递归导致程序崩溃。终止条件通常是问题的边界值,并在此时直接给出答案。例如,$fact(0) = 1$,$f(1) = 1$。 ### 3.3 翻译为代码 将递推公式和终止条件转化为代码,通常分为三步: 1. **定义递归函数**:明确参数和返回值的含义。 2. **编写递归主体**:根据递推公式递归调用自身。 3. **加入终止条件**:用条件语句判断并处理终止情况。 综合上述步骤,递归伪代码如下: ```python def recursion(大规模问题): if 递归终止条件: 递归终止时的处理方法 return recursion(小规模问题) ``` ## 4. 递归的注意事项 ### 4.1 避免栈溢出 递归在程序执行时依赖于调用栈。每递归调用一次,系统会为该调用分配一个新的栈帧;每当递归返回时,栈帧被销毁。由于系统栈空间有限,如果递归层数过深,极易导致栈溢出(Stack Overflow)。 为降低栈溢出的风险,可以在代码中人为设置递归的最大深度(如 100 层),超过后直接返回错误或采取其他处理措施。但这种方式并不能彻底杜绝栈溢出,因为系统允许的最大递归深度受限于当前可用栈空间,且难以精确预估。 如果递归深度不可控或递归算法难以避免栈溢出,建议将递归改写为非递归(迭代)算法,即用循环和显式栈模拟递归过程,从根本上解决栈空间受限的问题。 ### 4.2 避免重复运算 递归算法常常会遇到重复计算的问题,尤其是在分治结构中多个子问题重叠时。例如,斐波那契数列的递归定义如下: $$ f(n) = \begin{cases} 0 & n = 0 \\ 1 & n = 1 \\ f(n-1) + f(n-2) & n > 1 \end{cases} $$ 如下图所示,计算 $f(5)$ 时,$f(3)$ 会被多次递归计算,$f(2)$、$f(1)$、$f(0)$ 也会被重复计算,导致效率极低。 ![斐波那契数列的递归过程](https://qcdn.itcharge.cn/images/20230307164107.png) 为避免重复运算,可以引入缓存机制(如哈希表、数组或集合)记录已经计算过的子问题结果。这种做法称为「记忆化递归」或「递归 + 备忘录」,也是动态规划的核心思想之一。每次递归调用前,先检查缓存中是否已有结果,如果有则直接返回,无需再次递归,从而显著提升效率。 ## 5. 递归的应用 ### 5.1 经典例题:斐波那契数 #### 5.1.1 题目链接 - [509. 斐波那契数 - 力扣(LeetCode)](https://leetcode.cn/problems/fibonacci-number/) #### 5.1.2 题目大意 **描述**:给定一个整数 $n$。 **要求**:计算第 $n$ 个斐波那契数。 **说明**: - 斐波那契数列的定义如下: - $f(0) = 0, f(1) = 1$。 - $f(n) = f(n - 1) + f(n - 2)$,其中 $n > 1$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:1 解释:F(2) = F(1) + F(0) = 1 + 0 = 1 ``` - 示例 2: ```python 输入:n = 3 输出:2 解释:F(3) = F(2) + F(1) = 1 + 1 = 2 ``` #### 5.1.3 解题思路 ##### 思路 1:递归算法 按照递归解题的「三步走」策略,可以将斐波那契数列问题的递归实现过程梳理如下: 1. 写递推公式:$f(n) = f(n - 1) + f(n - 2)$。 2. 确定终止条件:$f(0) = 0$,$f(1) = 1$。 3. 翻译为代码: 1. 定义递归函数 `fib(self, n)`,其中 $n$ 表示问题规模,返回第 $n$ 个斐波那契数。 2. 递归主体为:`return self.fib(n - 1) + self.fib(n - 2)`。 3. 递归终止条件: - `if n == 0: return 0` - `if n == 1: return 1` ##### 思路 1:代码 ```python class Solution: def fib(self, n: int) -> int: if n == 0: return 0 if n == 1: return 1 return self.fib(n - 1) + self.fib(n - 2) ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O((\frac{1 + \sqrt{5}}{2})^n)$。具体证明方法参考 [递归求斐波那契数列的时间复杂度,不要被网上的答案误导了 - 知乎](https://zhuanlan.zhihu.com/p/256344121)。 - **空间复杂度**:$O(n)$。每次递归的空间复杂度是 $O(1)$, 调用栈的深度为 $n$,所以总的空间复杂度就是 $O(n)$。 ### 5.2 经典例题:二叉树的最大深度 #### 5.2.1 题目链接 - [104. 二叉树的最大深度 - 力扣(LeetCode)](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) #### 5.2.2 题目大意 **描述**:给定一个二叉树的根节点 $root$。 **要求**:找出该二叉树的最大深度。 **说明**: - **二叉树的深度**:根节点到最远叶子节点的最长路径上的节点数。 - **叶子节点**:没有子节点的节点。 **示例**: - 示例 1: ```python 输入:[3,9,20,null,null,15,7] 对应二叉树 3 / \ 9 20 / \ 15 7 输出:3 解释:该二叉树的最大深度为 3 ``` #### 5.2.3 解题思路 ##### 思路 1: 递归算法 按照递归解题的「三步走」策略,整理递归解法如下: 1. 写递推公式:**当前二叉树的最大深度 = max(左子树最大深度, 右子树最大深度) + 1**。 - 即:递归分别计算左右子树的深度,取较大值后加 1,得到当前节点的深度。 2. 确定终止条件:当当前节点为空(即 root 为 None)时,返回 0。 3. 翻译为代码: 1. 定义递归函数:`maxDepth(self, root)`,参数为二叉树根节点 $root$,返回该树的最大深度。 2. 递归主体:`return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1`。 3. 终止条件判断:`if not root: return 0`。 ##### 思路 1:代码 ```python class Solution: def maxDepth(self, root: Optional[TreeNode]) -> int: if not root: return 0 return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1 ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ## 6. 总结 递归的本质是将大问题拆成同结构的小问题,先「递推」到底,再「回归」向上合并结果。写递归时只需站在当前层思考:如果更小规模的子问题都已解决,我如何用它们的结果得到当前答案。 它与数学归纳法一一对应:终止条件对应基础情形,递推与回归对应「假设成立 → 推出更大规模成立」。因此,实践上先写清递推关系,再补充明确的递归出口,最后把思路直接翻译成代码即可。 实战中要特别注意两点:其一,递归层数过深可能导致栈溢出,可限制深度或改写为迭代;其二,子问题重叠会造成重复计算,可使用记忆化(缓存)或转为自底向上的动态规划来优化。 递归尤其适合链式、树形与分治类问题,如二叉树深度、DFS 等。建议先保证正确性,再视场景用缓存或迭代 / DP手段优化时间与空间开销。 ## 练习题目 - [0509. 斐波那契数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/fibonacci-number.md) - [0070. 爬楼梯](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/climbing-stairs.md) - [0226. 翻转二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/invert-binary-tree.md) - [0206. 反转链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/reverse-linked-list.md) - [0092. 反转链表 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-linked-list-ii.md) - [0779. 第K个语法符号](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/k-th-symbol-in-grammar.md) - [递归算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E9%80%92%E5%BD%92%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】算法竞赛入门经典:训练指南 - 刘汝佳,陈锋 著 - 【书籍】算法训练营 陈小玉 著 - 【书籍】挑战程序设计竞赛 第 2 版 - 秋叶拓哉,岩田阳一,北川宜稔 著,巫泽俊,庄俊元,李津羽 译 - 【问答】[对于递归有没有什么好的理解方法? - 知乎 - 方应杭](https://www.zhihu.com/question/31412436/answer/738989709) - 【问答】[对于递归有没有什么好的理解方法? - 知乎 - 老刘](https://www.zhihu.com/question/31412436/answer/724915708) - 【博文】[递归 & 分治 - OI Wiki](https://oi-wiki.org/basic/divide-and-conquer/) - 【博文】[递归详解 - labuladong](https://github.com/labuladong/fucking-algorithm/blob/master/算法思维系列/递归详解.md) - 【博文】[递归 - 数据结构与算法之美 - 极客时间](https://time.geekbang.org/column/article/41440) - 【视频】[清华学长带你从宏观角度看递归](https://mp.weixin.qq.com/s/BHY7ZBxIr3UCpIvY4-IVOQ) ================================================ FILE: docs/07_algorithm/07_03_divide_and_conquer_algorithm.md ================================================ ## 1. 分治算法简介 ### 1.1 分治算法的定义 > **分治算法(Divide and Conquer)**:即「分而治之」,把一个复杂问题拆分成多个相同或相似的子问题,递归分解,直到子问题足够简单可以直接解决,最后将子问题的解合并得到原问题的解。 简而言之,分治算法就是:**把大问题不断拆小,直到可以直接求解,再合并结果**。 ![分治算法的基本思想](https://qcdn.itcharge.cn/images/20220413153059.png) ### 1.2 分治算法与递归算法的关系 分治和递归都强调「拆分问题」。递归是一种实现方式,分治是一种思想。可以理解为:$\text{递归算法} \subset \text{分治算法}$。 分治算法常用递归实现,也可以用迭代实现。例如:快速傅里叶变换、二分查找、非递归归并排序等。 ![分治算法的实现方式](https://qcdn.itcharge.cn/images/20240513162133.png) 下面先介绍分治算法的适用条件,再讲基本步骤。 ### 1.3 分治算法的适用条件 分治算法适用于满足以下 4 个条件的问题: 1. **可分解**:原问题能拆分为若干规模更小、结构相同的子问题。 2. **子问题独立**:各子问题互不影响,无重叠部分。 3. **有终止条件**:子问题足够小时可直接解决。 4. **可合并**:子问题的解能高效合并为原问题的解,且合并过程不能太复杂。 ## 2. 分治算法的基本步骤 分治算法通常包括以下三个核心步骤: 1. **分解**:将原问题拆分为若干个规模更小、结构相同且相互独立的子问题。 2. **求解**:递归地解决每个子问题。 3. **合并**:将各子问题的解按照原问题的要求逐层合并,最终得到整体问题的解。 在第 $1$ 步分解时,建议将问题划分为规模尽量相等的 $k$ 个子问题,这样可以保持递归树的平衡,提升算法效率。实际应用中,$k = 2$ 是最常见的选择。子问题规模均衡,通常比不均衡的划分方式更优。 第 $2$ 步的递归求解,意味着对子问题继续应用相同的分治策略,直到子问题足够简单,可以直接用常数时间解决为止。 完成递归求解后,最小子问题的解可直接获得。随后,按照递归回归的顺序,自底向上逐步合并子问题的解,最终得到原问题的答案。 在实际编写分治算法时,代码结构也应严格遵循上述 $3$ 个步骤,便于理解和维护。伪代码如下: ```python def divide_and_conquer(problem_n): """ 分治算法通用模板 :param problem_n: 问题规模 :return: 原问题的解 """ # 1. 递归终止条件:当问题规模足够小时,直接解决 if problem_n < d: # d 为可直接求解的最小规模 return solve(problem_n) # 直接求解(注意:原代码有拼写错误,应为 solve) # 2. 分解:将原问题分解为 k 个子问题 problems_k = divide(problem_n) # divide 函数返回 k 个子问题的列表 # 3. 递归求解每个子问题 res = [] for sub_problem in problems_k: sub_res = divide_and_conquer(sub_problem) # 递归求解子问题 res.append(sub_res) # 收集每个子问题的解 # 4. 合并:将 k 个子问题的解合并为原问题的解 ans = merge(res) return ans # 返回原问题的解 ``` ## 3. 分治算法分析 分治算法的核心在于:将大问题递归拆分为更小的子问题,直到子问题足够简单(通常可直接用常数时间解决),然后合并子问题的解。实际的时间复杂度主要由「分解」和「合并」两个过程决定。 一般情况下,分治算法会把原问题拆成 $a$ 个规模为 $n/b$ 的子问题,递归式为: $$ T(n) = \begin{cases} \Theta(1) & n = 1 \\ a \times T(n/b) + f(n) & n > 1 \end{cases} $$ 其中,$a$ 表示子问题个数,$n/b$ 是每个子问题的规模,$f(n)$ 是分解和合并的总耗时。 求解分治算法复杂度,常用两种方法:递推法和递归树法。 ### 3.1 递推法 以归并排序为例,其递归式为: $$ T(n) = \begin{cases} O(1) & n = 1 \\ 2T(n/2) + O(n) & n > 1 \end{cases} $$ 递推展开如下: $$ \begin{aligned} T(n) &= 2T(n/2) + O(n) \\ &= 2[2T(n/4) + O(n/2)] + O(n) \\ &= 4T(n/4) + 2O(n/2) + O(n) \\ &= 4T(n/4) + O(n) + O(n) \\ &= 8T(n/8) + 3O(n) \\ &\dots \\ &= 2^x T(n/2^x) + xO(n) \end{aligned} $$ 当 $n = 2^x$,$x = \log_2 n$,最终: $$ T(n) = n \cdot T(1) + \log_2 n \cdot O(n) = O(n \log n) $$ 则归并排序的时间复杂度为 $O(n \log n)$。 ### 3.2 递归树法 递归树法可以直观展示每层的分解和合并成本。以归并排序为例: - 每层分解为 2 个子问题,总共 $\log_2 n$ 层。 - 每层合并的总耗时为 $O(n)$。 总复杂度为: $$ \begin{aligned} \text{总耗时} &= \underbrace{n \cdot O(1)}_{\text{叶子节点}} + \underbrace{O(n) \cdot \log_2 n}_{\text{每层合并}} \\ &= O(n) + O(n \log n) \\ &= O(n \log n) \end{aligned} $$ 下图为归并排序的递归树示意: ![归并排序算法的递归树](https://qcdn.itcharge.cn/images/20220414171458.png) ## 4. 分治算法的应用 ### 4.1 经典例题:归并排序 #### 4.1.1 题目链接 - [912. 排序数组 - 力扣(LeetCode) ](https://leetcode.cn/problems/sort-an-array/) #### 4.1.2 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:对该数组升序排列。 **说明**: - $1 \le nums.length \le 5 * 10^4$。 - $-5 * 10^4 \le nums[i] \le 5 * 10^4$。 **示例**: ```python 输入 nums = [5,2,3,1] 输出 [1,2,3,5] ``` #### 4.1.3 解题思路 本题采用归并排序算法求解,其步骤如下: 1. **分解**:将待排序数组递归地一分为二,分别划分为左右两个子数组,每个子数组大致包含 $\frac{n}{2}$ 个元素。 2. **递归排序**:对左右两个子数组分别递归进行归并排序,直到子数组长度为 $1$,此时视为有序。 3. **合并**:将两个有序子数组合并为一个有序数组,逐层向上合并,最终得到整体有序的结果。 下图展示了归并排序对数组排序的具体过程: ![归并排序算法对数组排序的过程](https://qcdn.itcharge.cn/images/20220414204405.png) #### 4.1.4 代码 ```python class Solution: def merge(self, left_arr, right_arr): # 合并 arr = [] while left_arr and right_arr: # 将两个排序数组中较小元素依次插入到结果数组中 if left_arr[0] <= right_arr[0]: arr.append(left_arr.pop(0)) else: arr.append(right_arr.pop(0)) while left_arr: # 如果左子序列有剩余元素,则将其插入到结果数组中 arr.append(left_arr.pop(0)) while right_arr: # 如果右子序列有剩余元素,则将其插入到结果数组中 arr.append(right_arr.pop(0)) return arr # 返回排好序的结果数组 def mergeSort(self, arr): # 分解 if len(arr) <= 1: # 数组元素个数小于等于 1 时,直接返回原数组 return arr mid = len(arr) // 2 # 将数组从中间位置分为左右两个数组。 left_arr = self.mergeSort(arr[0: mid]) # 递归将左子序列进行分解和排序 right_arr = self.mergeSort(arr[mid:]) # 递归将右子序列进行分解和排序 return self.merge(left_arr, right_arr) # 把当前序列组中有序子序列逐层向上,进行两两合并。 def sortArray(self, nums: List[int]) -> List[int]: return self.mergeSort(nums) ``` ### 4.2 经典例题:二分查找 #### 4.2.1 题目链接 - [704. 二分查找 - 力扣(LeetCode)](https://leetcode.cn/problems/binary-search/) #### 4.2.2 题目大意 **描述**:给定一个含有 $n$ 个元素有序的(升序)整型数组 $nums$ 和一个目标值 $target$。 **要求**:返回 $target$ 在数组 $nums$ 中的位置,如果找不到,则返回 $-1$。 **说明**: - 假设 $nums$ 中的所有元素是不重复的。 - $n$ 将在 $[1, 10000]$ 之间。 - $-9999 \le nums[i] \le 9999$。 **示例**: ```python 输入 nums = [-1,0,3,5,9,12], target = 9 输出 4 解释 9 出现在 nums 中并且下标为 4 ``` #### 4.2.3 解题思路 本题采用分治思想进行求解。与典型的分治问题不同,二分查找无需对子问题的结果进行合并,最小子问题的解即为原问题的解。 具体步骤如下: 1. **分解**:将当前数组划分为左右两个子区间,每个子区间大致包含 $\frac{n}{2}$ 个元素。 2. **处理**:选取中间元素 $nums[mid]$,并与目标值 $target$ 进行比较: 1. 如果 $nums[mid] == target$,则直接返回该元素下标; 2. 如果 $nums[mid] < target$,则在右侧子区间递归查找; 3. 如果 $nums[mid] > target$,则在左侧子区间递归查找。 下图展示了二分查找的分治过程。 ![二分查找的的分治算法过程](https://qcdn.itcharge.cn/images/20211223115032.png) #### 4.2.4 代码 二分查找问题的非递归实现的分治算法代码如下: ```python class Solution: def search(self, nums: List[int], target: int) -> int: left = 0 right = len(nums) - 1 # 在区间 [left, right] 内查找 target while left < right: # 取区间中间节点 mid = left + (right - left) // 2 # nums[mid] 小于目标值,排除掉不可能区间 [left, mid],在 [mid + 1, right] 中继续搜索 if nums[mid] < target: left = mid + 1 # nums[mid] 大于等于目标值,目标元素可能在 [left, mid] 中,在 [left, mid] 中继续搜索 else: right = mid # 判断区间剩余元素是否为目标元素,不是则返回 -1 return left if nums[left] == target else -1 ``` ## 5. 总结 分治是一种「拆分—求解—合并」的通用思维范式:将大问题拆为若干规模更小且相互独立的同构子问题,递归(或迭代)求解到足够小的基例,最后自底向上合并结果。是否适合分治,取决于子问题是否独立、规模能否尽量均衡、以及合并是否足够高效。 实践中要关注三个要点: 1. 明确且正确的递归基与边界,避免无穷递归与越界; 2. 尽量使子问题独立、规模均衡,必要时调整划分策略; 3. 评估合并代价,如果合并过重或子问题高度重叠,应考虑动态规划、记忆化或改用其他范式。 合理运用这些原则,分治能在排序、查找、几何与数值计算等领域提供简洁而高效的解法。 ## 练习题目 - [0050. Pow(x, n)](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/powx-n.md) - [0169. 多数元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/majority-element.md) - [0053. 最大子数组和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximum-subarray.md) - [0932. 漂亮数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/beautiful-array.md) - [0241. 为运算表达式设计优先级](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/different-ways-to-add-parentheses.md) - [0023. 合并 K 个升序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) - [分治算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%88%86%E6%B2%BB%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ## 参考资料 - 【书籍】趣学算法 - 陈小玉 著 - 【书籍】算法之道 - 邹恒铭 著 - 【书籍】算法图解 - 袁国忠 译 - 【书籍】算法训练营 陈小玉 著 - 【博文】[从合并排序算法看“分治法” - 船长&CAP - 博客园](https://www.cnblogs.com/liuning8023/archive/2012/06/25/2562747.html) - 【博文】[递归、迭代、分治、回溯、动态规划、贪心算法 - 力扣](https://leetcode.cn/circle/article/yXFal5/) - 【博文】[递归 & 分治 - OI Wiki](https://oi-wiki.org/basic/divide-and-conquer/) - 【博文】[漫画:5分钟弄懂分治算法!它和递归算法的关系!](https://mp.weixin.qq.com/s/0Z1tiqWTO410jYTJ4K0Ihg) ================================================ FILE: docs/07_algorithm/07_04_backtracking_algorithm.md ================================================ ## 1. 回溯算法简介 > **回溯算法(Backtracking)**:回溯算法是一种系统地搜索所有可能解的算法,通过递归和试错的方式逐步构建解的过程。当发现当前路径无法满足题目要求或无法得到有效解时,算法会撤销上一步的选择(即「回溯」),返回到上一个决策点,尝试其他可能的路径。回溯法的核心思想是「走不通就退回,换条路再试」,而每次需要回退的节点称为「回溯点」。 简而言之,回溯算法就是「遇到死路就回头」。 回溯算法常用递归方式实现,过程通常有两种结果: 1. 找到一个满足条件的解; 2. 尝试所有可能后,确认无解。 ## 2. 从全排列问题直观理解回溯算法 以 $[1, 2, 3]$ 的全排列为例,回溯算法的核心流程如下: 1. 首先选择第一个数字为 $1$: - 接下来可选数字为 $2$ 和 $3$。 - 选择 $2$ 作为第二个数字,剩下只能选 $3$,得到排列 $[1, 2, 3]$。 - 回退一步,撤销 $3$,撤销 $2$,尝试 $3$ 作为第二个数字,剩下只能选 $2$,得到排列 $[1, 3, 2]$。 2. 回退到最初,撤销 $1$,尝试以 $2$ 开头: - 接下来可选数字为 $1$ 和 $3$。 - 选择 $1$ 作为第二个数字,剩下只能选 $3$,得到排列 $[2, 1, 3]$。 - 回退一步,撤销 $3$,撤销 $1$,尝试 $3$ 作为第二个数字,剩下只能选 $1$,得到排列 $[2, 3, 1]$。 3. 再回退到最初,撤销 $2$,尝试以 $3$ 开头: - 接下来可选数字为 $1$ 和 $2$。 - 选择 $1$ 作为第二个数字,剩下只能选 $2$,得到排列 $[3, 1, 2]$。 - 回退一步,撤销 $2$,撤销 $1$,尝试 $2$ 作为第二个数字,剩下只能选 $1$,得到排列 $[3, 2, 1]$。 简而言之,每次选择一个数字作为当前位置的元素,递归地选择下一个位置的数字。当所有数字都被选完时,得到一个完整的排列;如果发现当前选择无法继续,则回退到上一步,尝试其他可能性。这样就能系统地枚举出所有全排列。 全排列的回溯过程可以简要归纳为: - **逐位枚举每个位置可能出现的数字,且每个数字在同一排列中只出现一次。** - 对于每一位,遵循以下步骤: 1. **选择元素**:从当前可选的数字中,挑选一个未被使用的数字。 2. **递归探索**:将该数字加入当前路径,递归进入下一层,继续选择下一个位置的数字,直到满足终止条件(如路径长度等于数组长度)。 3. **撤销选择(回溯)**:递归返回后,移除刚才选择的数字,恢复现场,尝试其他未选过的数字,探索不同的分支,直到所有可能路径都被遍历。 上述决策过程可以用一棵决策树形象表示: ![全排列问题的决策树](https://qcdn.itcharge.cn/images/20220425102048.png) 从全排列的决策树结构可以看出: - 每一层代表当前递归的深度,每个节点及其分支对应一次不同的选择。 - 每个节点表示当前排列的一个「状态」,即已选择的数字序列。 - 向下递归一层,相当于在可选数字中再选一个数字加入当前状态。 - 当某条分支探索结束后,递归会逐层回退(回溯),撤销最近的选择,恢复到上一个状态,继续尝试其他分支。 基于上述思路和决策树结构,下面给出全排列问题的回溯算法代码(假设输入数组 $nums$ 无重复元素): ```python class Solution: def permute(self, nums: List[int]) -> List[List[int]]: """ 回溯法求解全排列问题 :param nums: 输入的数字列表 :return: 所有可能的全排列 """ res = [] # 用于存放所有符合条件的排列结果 path = [] # 用于存放当前递归路径下的排列 def backtracking(): # 递归终止条件:当 path 长度等于 nums 长度时,说明找到一个完整排列 if len(path) == len(nums): res.append(path[:]) # 注意要拷贝一份 path,否则后续 path 变化会影响结果 return # 遍历所有可选的数字 for i in range(len(nums)): if nums[i] in path: # 如果当前数字已经在 path 中,跳过,保证每个数字只出现一次 continue # 做选择:将当前数字加入 path path.append(nums[i]) # 递归进入下一层,继续选择下一个数字 backtracking() # 撤销选择:回退到上一步,移除最后一个数字,尝试其他分支 path.pop() backtracking() return res ``` ## 3. 回溯算法通用模板 结合前文全排列问题的回溯实现,我们可以总结出一套简洁高效的回溯算法通用模板,具体如下: ```python res = [] # 存放所有符合条件结果的集合 path = [] # 存放当前递归路径下的结果 def backtracking(nums): """ 回溯算法通用模板 :param nums: 可选元素列表 """ # 递归终止条件:根据具体问题设定(如 path 满足特定条件) if 满足结束条件: # 例如:len(path) == len(nums) res.append(path[:]) # 注意要拷贝一份 path,避免后续修改影响结果 return # 遍历所有可选的元素 for i in range(len(nums)): # 可选:根据具体问题添加剪枝条件,如元素不能重复选取 # if nums[i] in path: # continue path.append(nums[i]) # 做选择,将当前元素加入 path backtracking(nums) # 递归,继续选择下一个元素 path.pop() # 撤销选择,回退到上一步状态 # 调用回溯函数,开始搜索 backtracking(nums) ``` ## 4. 回溯算法的基本步骤 回溯算法的核心思想是:**通过深度优先搜索,不断尝试所有可能的选择,当发现当前路径不满足条件时就回退(回溯),尝试其他路径,最终找到所有可行解或最优解。** 回溯算法的基本步骤如下: 1. **明确所有选择**:画出决策树,理清每一步有哪些可选项。每个节点的分支代表一次选择。 2. **明确终止条件**:终止条件通常是递归到某一深度、遍历完所有元素或满足题目要求。到达终止条件时,处理当前结果(如加入答案集)。 3. **将决策树和终止条件转化为代码**: - 定义回溯函数(明确函数意义、传入参数、返回结果等)。 - 书写回溯函数主体(给出约束条件、选择元素、递归搜索、撤销选择部分)。 - 明确递归终止条件(给出递归终止条件,以及递归终止时的处理方法)。 ### 4.1 明确所有选择 决策树是帮助我们理清搜索过程的一个很好的工具。我们可以画出搜索过程的决策树,根据决策树来帮助我们确定搜索范围和对应的搜索路径。 ### 4.2 明确终止条件 回溯算法的终止条件,通常对应于决策树的叶子节点,即到达无法继续做选择的位置。 常见的终止条件包括:递归达到指定深度、遍历到叶子节点、遍历完所有元素等。此时需要对当前路径进行处理,例如将符合要求的结果加入答案集合,或输出当前解等。 ### 4.3 将决策树和终止条件转化为代码 #### 4.3.1 定义回溯函数 在定义回溯函数时,首先要清晰地界定递归函数的含义:即该函数的参数、全局变量分别代表什么,以及最终希望通过递归解决什么问题。 - **参数与全局变量设计**:参数和全局变量应能完整表达递归过程中的「当前状态」。通常,参数用于传递当前可选的元素、已做出的选择等信息,全局变量则用于收集所有满足条件的解。 以全排列问题为例,`backtracking(nums)` 的参数 $nums$ 表示当前可选的元素列表,全局变量 $path$ 记录当前递归路径(已选择的元素),$res$ 用于存储所有符合条件的结果。这样,$nums$ 反映可选空间,$path$ 反映当前状态,$res$ 汇总所有解。 - **返回值设计**:回溯函数的返回值通常用于在递归终止时向上一层传递结果。大多数情况下,回溯函数只需返回单个节点或数值,表明当前搜索的结果。 如果采用全局变量(如 $res$)来收集所有解,则回溯函数可以不显式返回结果,直接 return 即可。例如全排列问题中,递归终止时将 $path$ 加入 $res$,无需返回值。 #### 4.3.2 书写回溯函数主体 结合当前可选元素、题目约束(如某元素不可重复选择)、以及用于记录当前路径的变量,我们即可编写回溯函数的核心主体部分。即: ```python for 选择 in 可选列表: if 满足约束: 做选择 backtrack(新参数) 撤销选择 ``` #### 4.3.3 明确递归终止条件 这一环节的本质,是将「4.2 明确终止条件」中分析得到的递归终止条件及其对应的处理逻辑,具体实现为代码中的判断语句和相应的操作。例如,判断是否达到递归深度或满足题目要求,并在满足时将当前结果加入答案集等。 #### 4.3.4 回溯函数通用模板 通过上述三步分析,我们可以归纳出回溯算法的核心流程:**首先枚举所有可选项,然后判断是否满足终止条件,最后递归深入,并在必要时撤销选择进行回溯**。这种结构化的思考方式,使回溯算法能够高效地解决组合、排列、子集等典型问题。接下来,我们将结合具体例题,进一步体会回溯算法在实际问题中的应用与实现。 回溯通用模板如下: ```python def backtrack(参数): if 终止条件: 处理结果 return for 选择 in 可选列表: if 满足约束: 做选择 backtrack(新参数) 撤销选择 ``` ## 5. 回溯算法的应用 ### 5.1 经典例题:子集 #### 5.1.1 题目链接 - [78. 子集 - 力扣(LeetCode)](https://leetcode.cn/problems/subsets/) #### 5.1.2 题目大意 **描述**:给定一个整数数组 $nums$,数组中的元素互不相同。 **要求**:返回该数组所有可能的不重复子集。可以按任意顺序返回解集。 **说明**: - $1 \le nums.length \le 10$。 - $-10 \le nums[i] \le 10$。 - $nums$ 中的所有元素互不相同。 **示例**: - 示例 1: ```python 输入 nums = [1,2,3] 输出 [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]] ``` - 示例 2: ```python 输入:nums = [0] 输出:[[],[0]] ``` #### 5.1.3 解题思路 ##### 思路 1:回溯算法 对于数组中的每个元素,都有「选择」或「不选择」两种可能。 我们可以通过将元素加入当前子集(path)来表示「选择」,递归结束后再将其移除(即回溯),从而实现「撤销选择」,表示「不选择」该元素。 下面结合回溯算法的三大步骤,梳理子集问题的解题思路: ![子集的决策树](https://qcdn.itcharge.cn/images/20220425210640.png) 1. **明确所有选择**:对于数组的每个位置,都可以选择是否将该元素加入当前子集。决策树的每一层对应一个元素的选择与否。 2. **明确终止条件**: - 当递归遍历到数组末尾(即所有元素都被考虑过)时,递归终止。 3. **将思路转化为代码实现**: 1. 定义回溯函数: - `backtracking(nums, index)`,其中 $nums$ 是原始数组,$index$ 表示当前递归到的元素下标。全局变量 $res$ 用于存储所有子集结果,$path$ 用于存储当前子集路径。 - 该函数的含义是:从 $index$ 开始,依次尝试将后续元素加入子集,递归搜索所有可能的组合。 2. 编写回溯主体逻辑(选择、递归、回溯): - 从 $index$ 开始,依次枚举每个可选元素。对于每个元素: - **去重约束**:每次递归都从 $index$ 开始,避免重复选择已考虑过的元素,保证子集不重复(如 {1,2} 和 {2,1} 视为同一子集)。 - **选择**:将当前元素加入 $path$。 - **递归**:递归进入下一层,继续选择下一个元素。 - **回溯**:递归返回后,移除刚刚加入的元素,恢复现场,尝试其他分支。 ```python # 从当前下标开始,依次尝试选择每个元素 for i in range(index, len(nums)): path.append(nums[i]) # 选择当前元素,加入子集 backtracking(i + 1) # 递归,继续选择下一个元素 path.pop() # 撤销选择,回溯到上一步 ``` 3. 明确递归终止条件及结果处理: - 每次进入回溯函数时,都将当前 $path$ 加入结果集 $res$,因为子集问题需要收集所有状态(包括中间状态和叶子节点)。 - 当 $index \ge len(nums)$ 时,递归自然终止,无需额外处理。 简而言之,回溯法通过「选择 - 递归 - 回溯」三步,系统地枚举所有子集,并通过合理的约束避免重复。 ##### 思路 1:代码 ```python class Solution: def subsets(self, nums: List[int]) -> List[List[int]]: """ 回溯法求解子集问题。 :param nums: 输入数组 :return: 所有子集的列表 """ res = [] # 用于存放所有子集结果 path = [] # 用于存放当前递归路径上的子集 def backtracking(index: int): """ 回溯函数,递归枚举所有子集。 :param index: 当前递归到的元素下标 """ # 每次进入回溯函数,都将当前路径(子集)加入结果集 res.append(path[:]) # 递归终止条件:index 超过数组长度时返回 if index >= len(nums): return # 从当前下标开始,依次尝试选择每个元素 for i in range(index, len(nums)): path.append(nums[i]) # 选择当前元素,加入子集 backtracking(i + 1) # 递归,继续选择下一个元素 path.pop() # 撤销选择,回溯到上一步 backtracking(0) # 从下标 0 开始递归 return res ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 2^n)$。其中 $n$ 是数组 $nums$ 的元素个数。回溯过程中,每个元素有选与不选两种状态,共 $2^n$ 种子集,每生成一个子集需要 $O(n)$ 的时间(因为要拷贝 path 到结果集)。 - **空间复杂度**:$O(n)$。递归过程中 path 最深为 $n$,递归栈空间也是 $O(n)$。 ### 5.2 N 皇后 #### 5.2.1 题目链接 - [51. N 皇后 - 力扣(LeetCode)](https://leetcode.cn/problems/n-queens/) #### 5.2.2 题目大意 **描述**:给定一个整数 $n$。 **要求**:返回所有不同的「$n$ 皇后问题」的解决方案。每一种解法包含一个不同的「$n$ 皇后问题」的棋子放置方案,该方案中的 `Q` 和 `.` 分别代表了皇后和空位。 **说明**: - **n 皇后问题**:将 $n$ 个皇后放置在 $n \times n$ 的棋盘上,并且使得皇后彼此之间不能攻击。 - **皇后彼此不能相互攻击**:指的是任何两个皇后都不能处于同一条横线、纵线或者斜线上。 - $1 \le n \le 9$。 **示例**: - 示例 1: ```python 输入:n = 4 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] 解释:如下图所示,4 皇后问题存在 2 个不同的解法。 ``` ![](https://assets.leetcode.com/uploads/2020/11/13/queens.jpg) #### 5.2.3 解题思路 ##### 思路 1:回溯算法 本题是回溯算法的经典应用。我们按照「逐行放置皇后」的顺序进行搜索:即先在第 1 行放皇后,再到第 2 行,依次递归,直到最后一行。 对于 $n \times n$ 的棋盘,每一行有 $n$ 个位置可选。每次尝试将皇后放在当前行的某一列,并判断该位置是否与之前已放置的皇后冲突(即是否在同一列、主对角线、副对角线上)。如果不冲突,则递归进入下一行继续放置;如果冲突,则跳过该位置,尝试下一列。所有皇后都成功放置后,即得到一个有效解。回溯算法会自动探索所有可能的分支,确保所有解都被枚举。 下面结合回溯算法的「三步走」思想,梳理 N 皇后问题的解题流程: ![N 皇后问题的解题流程](https://qcdn.itcharge.cn/images/20220426095225.png) 1. **明确所有选择**:对于当前行,依次尝试将皇后放在每一列的不同位置,每个位置都代表一次选择,整个过程可用决策树表示(如上图)。 2. **明确终止条件**: - 当所有行都已成功放置皇后(即递归到第 $n$ 行),说明找到一个有效解,此时递归终止。 3. **将决策树和终止条件转化为代码实现:** 1. 定义回溯函数: - 使用一个 $n \times n$ 的二维数组 $chessboard$ 表示棋盘,`Q` 表示皇后,`.` 表示空位,初始均为 `.`。 - 定义回溯函数 `backtrack(chessboard, row)`,其中 $chessboard$ 为当前棋盘状态,$row$ 表示当前正在处理的行,全局变量 $res$ 用于收集所有可行解。 - `backtrack(chessboard, row)` 的含义是:在前 $row-1$ 行已放置皇后的前提下,递归尝试为第 $row$ 行放置皇后。 2. 编写回溯函数主体(包括选择、递归、撤销选择): - 遍历当前行的每一列,对于每个位置: - 约束条件:通过辅助函数判断当前位置是否与已放置的皇后冲突,如果无冲突则继续,否则跳过。 - 选择:在 $row, col$ 位置放置皇后(即 $chessboard[row][col] = 'Q'$)。 - 递归:递归处理下一行($row+1$)。 - 撤销选择:回溯时将 $chessboard[row][col]$ 恢复为 `.`,以便尝试其他方案。 ```python # 枚举当前行的每一列,尝试放置皇后 for col in range(n): if self.isValid(n, row, col, chessboard): # 检查当前位置是否合法 chessboard[row][col] = 'Q' # 放置皇后 self.backtrack(n, row + 1, chessboard) # 递归处理下一行 chessboard[row][col] = '.' # 撤销选择,回溯 ``` 3. 明确递归终止条件(即何时递归应当结束,以及结束时如何处理结果)。 - 当递归到第 $n$ 行(即 $row == n$)时,说明所有皇后已成功放置,此时到达决策树的叶子节点,递归终止。 - 终止时,将当前棋盘状态转换为题目要求的格式,并加入结果集 $res$。 ##### 思路 1:代码 ```python class Solution: res = [] # 用于存储所有可行解 def backtrack(self, n: int, row: int, chessboard: List[List[str]]): """ 回溯主函数:在第 row 行尝试放置皇后 :param n: 棋盘大小(n x n) :param row: 当前递归到的行号 :param chessboard: 当前棋盘状态 """ if row == n: # 递归终止条件:所有行都已放置皇后,记录当前棋盘方案 temp_res = [] for temp in chessboard: temp_str = ''.join(temp) # 将每一行转为字符串 temp_res.append(temp_str) self.res.append(temp_res) return # 枚举当前行的每一列,尝试放置皇后 for col in range(n): if self.isValid(n, row, col, chessboard): # 检查当前位置是否合法 chessboard[row][col] = 'Q' # 放置皇后 self.backtrack(n, row + 1, chessboard) # 递归处理下一行 chessboard[row][col] = '.' # 撤销选择,回溯 def isValid(self, n: int, row: int, col: int, chessboard: List[List[str]]): """ 检查在 (row, col) 位置放置皇后是否合法 :param n: 棋盘大小 :param row: 当前行 :param col: 当前列 :param chessboard: 当前棋盘状态 :return: True 表示合法,False 表示冲突 """ # 检查同一列是否有皇后 for i in range(row): if chessboard[i][col] == 'Q': return False # 检查左上对角线是否有皇后 i, j = row - 1, col - 1 while i >= 0 and j >= 0: if chessboard[i][j] == 'Q': return False i -= 1 j -= 1 # 检查右上对角线是否有皇后 i, j = row - 1, col + 1 while i >= 0 and j < n: if chessboard[i][j] == 'Q': return False i -= 1 j += 1 return True # 没有冲突,可以放置 def solveNQueens(self, n: int) -> List[List[str]]: """ 主入口函数,返回所有 N 皇后问题的解 :param n: 棋盘大小 :return: 所有可行解的列表 """ self.res.clear() # 清空历史结果 # 初始化棋盘,全部填充为 '.' chessboard = [['.' for _ in range(n)] for _ in range(n)] self.backtrack(n, 0, chessboard) # 从第 0 行开始回溯 return self.res ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n!)$,其中 $n$ 是皇后数量。 - **空间复杂度**:$O(n^2)$,其中 $n$ 是皇后数量。递归调用层数不会超过 $n$,每个棋盘的空间复杂度为 $O(n^2)$,所以空间复杂度为 $O(n^2)$。 ## 6. 总结 回溯算法是一种通过递归和试错来系统地搜索所有可能解的算法。其核心思想是「走不通就退回,换条路再试」,通过深度优先搜索的方式遍历决策树,当发现当前路径无法满足条件时,会撤销上一步的选择并尝试其他可能性。 回溯算法的关键在于「选择 - 递归 - 回溯」:首先做出选择,然后递归进入下一层继续搜索,最后在递归返回时撤销选择,恢复到之前的状态。这种机制使得算法能够穷尽所有可能的解空间,特别适用于需要枚举所有可能解的问题。 回溯算法在解决组合、排列、子集等经典问题中表现出色,如全排列、N皇后、子集生成等。其通用模板简洁明了,通过明确所有选择、确定终止条件、转化为代码实现三个步骤,可以高效地解决各种回溯类问题。 虽然回溯算法能够保证找到所有解,但其时间复杂度通常较高,特别是在解空间较大时。因此,在实际应用中需要结合剪枝等优化技巧来提高效率,避免不必要的搜索路径。 ## 练习题目 - [0046. 全排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutations.md) - [0047. 全排列 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutations-ii.md) - [0022. 括号生成](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/generate-parentheses.md) - [0017. 电话号码的字母组合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/letter-combinations-of-a-phone-number.md) - [0039. 组合总和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combination-sum.md) - [0040. 组合总和 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combination-sum-ii.md) - [0078. 子集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets.md) - [0090. 子集 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets-ii.md) - [0079. 单词搜索](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/word-search.md) - [回溯算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ## 参考资料 - 【题解】[回溯算法入门级详解 + 练习(持续更新) - 全排列 - 力扣](https://leetcode.cn/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/) - 【题解】[「代码随想录」带你学透回溯算法!51. N-Queens - N 皇后 - 力扣](https://leetcode.cn/problems/n-queens/solution/dai-ma-sui-xiang-lu-51-n-queenshui-su-fa-2k32/) - 【文章】[回溯算法详解](https://mp.weixin.qq.com/s/trILKSiN9EoS58pXmvUtUQ) - 【文章】[回溯算法详解修订版 - labuladong](https://github.com/labuladong/fucking-algorithm/blob/master/算法思维系列/回溯算法详解修订版.md) - 【文章】[【算法】回溯法四步走 - Nemo& - 博客园](https://www.cnblogs.com/blknemo/p/12431911.html) ================================================ FILE: docs/07_algorithm/07_05_greedy_algorithm.md ================================================ ## 1. 贪心算法简介 ### 1.1 贪心算法的定义 > **贪心算法(Greedy Algorithm)**:每一步都选择当前最优(看起来最好的)方案,期望通过一系列局部最优,最终获得全局最优解。 贪心算法的核心思想是:将问题分解为若干步骤,每一步都根据当前情况,按照某种标准选择最优解(即「贪心」选择),不回头、不考虑整体,只关注当前最优。这样可以避免穷举所有可能,大大简化求解过程。 简而言之,贪心算法每次只做出当前看来最优的选择,期望通过一系列这样的选择得到整体最优解。 ### 1.2 贪心算法的特征 贪心算法适用于一类特殊问题:只要每一步都做出当前最优选择,最终就能得到整体最优解或近似最优解。但并非所有问题都适用贪心算法。 通常,能用贪心算法解决的问题需同时满足两个条件: 1. **贪心选择性质** 2. **最优子结构** #### 1.2.1 贪心选择性质 > **贪心选择性质**:全局最优解可以通过一系列局部最优(贪心)选择获得。 也就是说,每次只需关注当前最优选择,无需关心子问题的解。做出选择后,再递归处理剩下的子问题。 ![贪心选择性质](https://qcdn.itcharge.cn/images/20240513163300.png) 贪心算法的每一步可能依赖之前的选择,但不会回溯,也不依赖未来的选择或子问题的解。 #### 1.2.2 最优子结构性质 > **最优子结构性质**:问题的最优解包含其子问题的最优解。 这是贪心算法成立的关键。举例来说,假设原问题 $S = \lbrace a_1, a_2, a_3, a_4 \rbrace$,第一步通过贪心选择得到当前最优解,剩下的子问题 $S_{\text{子问题}} = \lbrace a_2, a_3, a_4 \rbrace$。如果原问题的最优解等于「当前贪心选择」加上「子问题的最优解」,则满足最优子结构。 ![最优子结构性质](https://qcdn.itcharge.cn/images/20240513163310.png) 如果原问题的最优解可以由子问题的最优解推导出来,则说明满足最优子结构;反之,则不满足,不能用贪心算法。 ### 1.3 贪心算法正确性简述 贪心算法的难点在于如何证明其选择策略能得到全局最优解。常见的两种证明方法: > - **数学归纳法**:先验证最小规模(如 $n = 1$)时成立,再证明 $n$ 成立时 $n + 1$ 也成立。 > - **交换论证法**:假设存在更优解,通过交换局部选择,如果不会得到更优结果,则当前贪心解为最优。 实际刷题或面试时,通常不要求严格证明。判断是否可用贪心算法,可以通过: 1. **直觉尝试**:先用贪心思路做一遍,看看局部最优能否推出全局最优。 2. **举反例**:尝试构造反例,如果找不到局部最优导致全局最优失败的例子,基本可以用贪心法。 ## 2. 贪心算法三步走 1. **问题转化**:将原始优化问题转化为可以应用贪心策略的问题,明确每一步都可以做出一个局部最优的选择。 2. **贪心策略制定**:结合题意,选定合适的度量标准,设计出每一步的贪心选择规则,即在当前状态下选择最优(最有利)的方案,获得局部最优解。 3. **最优子结构利用**:保证每次贪心选择后,剩余子问题仍满足同样的结构和贪心选择性质,将每一步的局部最优解累积,最终合成原问题的全局最优解。 ## 3. 贪心算法的应用 ### 3.1 经典例题:分发饼干 #### 3.1.1 题目链接 - [455. 分发饼干 - 力扣](https://leetcode.cn/problems/assign-cookies/) #### 3.1.2 题目大意 **描述**:一位很棒的家长为孩子们分发饼干。对于每个孩子 $i$,都有一个胃口值 $g[i]$,即每个小孩希望得到饼干的最小尺寸值。对于每块饼干 $j$,都有一个尺寸值 $s[j]$。只有当 $s[j] > g[i]$ 时,我们才能将饼干 $j$ 分配给孩子 $i$。每个孩子最多只能给一块饼干。 现在给定代表所有孩子胃口值的数组 $g$ 和代表所有饼干尺寸的数组 $j$。 **要求**:尽可能满足越多数量的孩子,并求出这个最大数值。 **说明**: - $1 \le g.length \le 3 * 10^4$。 - $0 \le s.length \le 3 * 10^4$。 - $1 \le g[i], s[j] \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:g = [1,2,3], s = [1,1] 输出:1 解释:你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1, 2, 3。虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。所以应该输出 1。 ``` - 示例 2: ```python 输入: g = [1,2], s = [1,2,3] 输出: 2 解释: 你有两个孩子和三块小饼干,2个孩子的胃口值分别是1, 2。你拥有的饼干数量和尺寸都足以让所有孩子满足。所以你应该输出 2。 ``` #### 3.1.3 解题思路 ##### 思路 1:贪心算法 为了让尽可能多的孩子得到满足,且每块饼干只能分配给一个孩子,我们应优先用小尺寸的饼干满足胃口较小的孩子,将大尺寸的饼干留给胃口较大的孩子。 基于贪心思想,具体做法如下:先将孩子的胃口数组 $g$ 和饼干尺寸数组 $s$ 分别从小到大排序。然后,依次为每个孩子分配能够满足其胃口的最小尺寸饼干。 结合贪心算法的三步走: 1. **问题转化**:将原问题转化为:每次优先用最小的饼干满足胃口最小的孩子,剩下的孩子和饼干继续按同样方式处理(即递归到子问题)。 2. **贪心选择性质**:对于当前孩子,选择能满足其胃口的最小饼干。 3. **最优子结构**:当前的贪心选择加上剩余子问题的最优解,能够保证全局最优,即最大化被满足的孩子数量。 具体实现步骤如下: 1. 对 $g$ 和 $s$ 升序排序,定义指针 $index\_g$ 和 $index\_s$ 分别指向 $g$ 和 $s$ 的起始位置,结果计数 $res$ 初始化为 $0$。 2. 遍历两个数组,比较 $g[index\_g]$ 和 $s[index\_s]$: 1. 如果 $g[index\_g] \le s[index\_s]$,说明当前饼干可以满足当前孩子,$res$ 加 $1$,$index\_g$ 和 $index\_s$ 同时右移。 2. 如果 $g[index\_g] > s[index\_s]$,说明当前饼干无法满足当前孩子,$index\_s$ 右移,尝试下一块饼干。 3. 遍历结束后,输出 $res$ 即为最多能满足的孩子数量。 ##### 思路 1:代码 ```python class Solution: def findContentChildren(self, g: List[int], s: List[int]) -> int: # 对孩子的胃口值和饼干尺寸进行升序排序 g.sort() s.sort() index_g, index_s = 0, 0 # index_g 指向当前要分配的孩子,index_s 指向当前可用的饼干 res = 0 # 记录能满足的孩子数量 # 遍历两个数组,直到有一个数组遍历完 while index_g < len(g) and index_s < len(s): # 如果当前饼干可以满足当前孩子 if g[index_g] <= s[index_s]: res += 1 # 满足的孩子数加一 index_g += 1 # 指向下一个孩子 index_s += 1 # 指向下一个饼干 else: index_s += 1 # 当前饼干太小,尝试下一块饼干 return res # 返回最多能满足的孩子数量 ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times \log m + n \times \log n)$,其中 $m$ 和 $n$ 分别是数组 $g$ 和 $s$ 的长度。 - **空间复杂度**:$O(\log m + \log n)$。 ### 3.2 经典例题:无重叠区间 #### 3.2.1 题目链接 - [435. 无重叠区间 - 力扣](https://leetcode.cn/problems/non-overlapping-intervals/) #### 3.2.2 题目大意 **描述**:给定一个区间的集合 $intervals$,其中 $intervals[i] = [starti, endi]$。从集合中移除部分区间,使得剩下的区间互不重叠。 **要求**:返回需要移除区间的最小数量。 **说明**: - $1 \le intervals.length \le 10^5$。 - $intervals[i].length == 2$。 - $-5 * 10^4 \le starti < endi \le 5 * 10^4$。 **示例**: - 示例 1: ```python 输入:intervals = [[1,2],[2,3],[3,4],[1,3]] 输出:1 解释:移除 [1,3] 后,剩下的区间没有重叠。 ``` - 示例 2: ```python 输入: intervals = [ [1,2], [1,2], [1,2] ] 输出: 2 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 ``` #### 3.2.3 解题思路 ##### 思路 1:贪心算法 本题可以通过转换思路来简化求解。原题要求移除最少数量的区间,使得剩余区间互不重叠。换句话说,就是要让剩下的互不重叠区间数量最多。因此,答案等价于「总区间数 - 最多不重叠区间数」。问题转化为:在所有区间中,最多能选出多少个互不重叠的区间。 采用贪心算法时,核心策略是将区间按结束时间从小到大排序。每次总是选择结束时间最早且不与已选区间重叠的区间,这样可以在后续留出更多空间,选出更多区间。 具体贪心解题步骤如下: 1. **问题转化**:将原问题转化为「选出最多不重叠区间」。 2. **贪心选择性质**:每次总是选择当前结束时间最早且不与已选区间重叠的区间。 3. **最优子结构性质**:当前选择加上后续子问题的最优解,能够得到全局最优解。 实现流程如下: 1. 先将所有区间按结束坐标升序排序。 2. 维护两个变量:$end\_pos$ 表示当前已选区间的结束位置,$count$ 表示已选的不重叠区间数量。初始时,$end\_pos$ 取第一个区间的结束位置,$count=1$。 3. 遍历后续区间,对于每个区间 $intervals[i]$: - 如果 $end\_pos \le intervals[i][0]$,说明该区间与前面已选区间不重叠,则计数 $count+1$,并更新 $end\_pos$ 为当前区间的结束位置。 4. 最终返回 $len(intervals) - count$,即最少需要移除的区间数。 ##### 思路 1:代码 ```python class Solution: def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: # 如果区间列表为空,直接返回 0 if not intervals: return 0 # 按区间的结束位置从小到大排序 intervals.sort(key=lambda x: x[1]) # 初始化第一个区间的结束位置 end_pos = intervals[0][1] # 记录不重叠区间的数量,初始为 1(第一个区间) count = 1 # 遍历后续区间 for i in range(1, len(intervals)): # 当前区间起点不小于上个已选区间终点,说明不重叠 if end_pos <= intervals[i][0]: count += 1 # 计数加一 end_pos = intervals[i][1] # 更新当前已选区间的结束位置 # 总区间数减去最多不重叠区间数,即为最少需要移除的区间数 return len(intervals) - count ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 是区间的数量。 - **空间复杂度**:$O(\log n)$。 ## 4. 总结 贪心算法是一种简单而有效的算法设计策略,其核心思想是在每一步都做出当前看起来最优的选择,期望通过一系列局部最优选择最终获得全局最优解。这种算法特别适用于具有贪心选择性质和最优子结构性质的问题。 贪心算法的优势在于其简洁性和高效性。相比动态规划需要存储和计算所有子问题的解,贪心算法只需要关注当前步骤的最优选择,大大降低了时间和空间复杂度。同时,贪心算法的实现通常比较简单直观,容易理解和编码。 然而,贪心算法也有其局限性。并非所有问题都适合使用贪心策略,只有同时满足贪心选择性质和最优子结构性质的问题才能保证得到全局最优解。对于不满足这些条件的问题,贪心算法可能只能得到局部最优解或近似解。此外,贪心算法的正确性证明往往比较复杂,需要严格的数学证明。 在实际应用中,贪心算法广泛应用于调度问题、图论问题、优化问题等领域。通过合理设计贪心策略,可以在保证解的质量的同时,显著提高算法的执行效率。掌握贪心算法的关键在于理解其适用条件,学会识别问题特征,并能够设计出合适的贪心选择策略。 ## 练习题目 - [0455. 分发饼干](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/assign-cookies.md) - [0860. 柠檬水找零](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/lemonade-change.md) - [0135. 分发糖果](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/candy.md) - [0055. 跳跃游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/jump-game.md) - [0045. 跳跃游戏 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/jump-game-ii.md) - [0881. 救生艇](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/boats-to-save-people.md) - [0435. 无重叠区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/non-overlapping-intervals.md) - [0452. 用最少数量的箭引爆气球](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-number-of-arrows-to-burst-balloons.md) - [1710. 卡车上的最大单元数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/maximum-units-on-a-truck.md) - [贪心算法题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%E9%A2%98%E7%9B%AE) ## 参考资料 - 【博文】[贪心 - OI Wiki](https://oi-wiki.org/basic/greedy/) - 【博文】[贪心算法 | 算法吧](https://suanfa8.com/greedy/) - 【博文】[贪心算法理论基础 - Carl - 代码随想录](https://github.com/youngyangyang04/leetcode-master/blob/master/problems/贪心算法理论基础.md) - 【博文】[小白带你学 贪心算法(Greedy Algorithm) - 知乎](https://zhuanlan.zhihu.com/p/53334049) - 【书籍】算法导论 第三版(中文版)- 殷建平等 译 - 【书籍】ACM-ICPC 程序设计系列 - 算法设计与实现 - 陈宇 吴昊 主编 ================================================ FILE: docs/07_algorithm/07_06_bit_operation.md ================================================ ## 1. 位运算简介 ### 1.1 位运算与二进制基础 > **位运算(Bit Operation)**:计算机内部所有数据均以「二进制(Binary)」形式存储。位运算是直接对二进制位进行操作的运算方式,能够极大提升程序的执行效率。 在正式学习位运算之前,先简单了解「二进制数」的基本概念。 ![二进制数](https://qcdn.itcharge.cn/images/202405132135165.png) > **二进制数(Binary)**:仅由 $0$ 和 $1$ 两个数字组成。二进制数中的每一位($0$ 或 $1$)称为一个「位(Bit)」。 我们日常使用的十进制数包含 $0 \sim 9$ 共 $10$ 个数字,进位规则为「满十进一」。例如: 1. $7_{(10)} + 2_{(10)} = 9_{(10)}$:$7_{(10)}$ 加 $2_{(10)}$ 得 $9_{(10)}$。 2. $9_{(10)} + 2_{(10)} = 11_{(10)}$:$9_{(10)}$ 加 $2_{(10)}$ 后个位满 $10$ 进一,结果为 $11_{(10)}$。 而二进制数仅有 $0$ 和 $1$,进位规则为「逢二进一」。例如: 1. $1_{(2)} + 0_{(2)} = 1_{(2)}$:$1_{(2)}$ 加 $0_{(2)}$ 得 $1_{(2)}$。 2. $1_{(2)} + 1_{(2)} = 10_{(2)}$:$1_{(2)}$ 加 $1_{(2)}$,满 $2$ 进一,结果为 $10_{(2)}$。 3. $10_{(2)} + 1_{(2)} = 11_{(2)}$:$10_{(2)}$ 加 $1_{(2)}$ 得 $11_{(2)}$。 ### 1.2 二进制与十进制的相互转换 #### 1.2.1 二进制转十进制 将二进制数转为十进制,就是将每一位上的数字乘以对应的 $2$ 的幂次,然后相加。例如:十进制 $2749_{(10)}$ 展开为 $2 \times 10^3 + 7 \times 10^2 + 4 \times 10^1 + 9 \times 10^0 = 2000 + 700 + 40 + 9 = 2749$。 同理,在二进制数中,$01101010_{(2)}$ 展开为 $0 \times 2^7 + 1 \times 2^6 + 1 \times 2^5 + 0 \times 2^4 + 1 \times 2^3 + 0 \times 2^2 + 1 \times 2^1 + 0 \times 2^0 = 0 + 64 + 32 + 0 + 8 + 0 + 2 + 0 = 106_{(10)}$。 ![二进制数转十进制数](https://qcdn.itcharge.cn/images/202405132136456.png) 我们可以通过这样的方式,将一个二进制数转为十进制数。 #### 1.2.2 十进制转二进制 十进制转二进制常用方法是「除2取余,逆序排列」。 以 $106_{(10)}$ 为例: 1. $106 \div 2 = 53$,余 $0$。 2. $53 \div 2 = 26$,余 $1$。 3. $26 \div 2 = 13$,余 $0$。 4. $13 \div 2 = 6$,余 $1$。 5. $6 \div 2 = 3$,余 $0$。 6. $3 \div 2 = 1$,余 $1$。 7. $1 \div 2 = 0$,余 $1$。 8. $0 \div 2 = 0$,余 $0$。 将余数逆序排列,得到 $01101010_{(2)}$。 简而言之:**不断除以 2,记录余数,最后将余数逆序排列即可得到二进制表示。** ## 2. 位运算基础操作 基于二进制表示,我们可以对数字进行多种位运算。常见的位运算包括 $6$ 种:「按位与」、「按位或」、「按位异或」、「取反」、「左移」和「右移」。 其中,「按位与」、「按位或」、「按位异或」、「左移」、「右移」属于双目运算(需要两个操作数): - 「按位与」、「按位或」、「按位异或」:将两个整数转为二进制后,对应位逐一进行运算。 - 「左移」、「右移」:左侧为待移位的整数,右侧为移动的位数,对左侧二进制的所有位整体移动指定次数。 「取反」属于单目运算(只需一个操作数),即对一个整数的每一位进行取反操作。 下面先简要介绍这 $6$ 种位运算的基本规则,后续将逐一详细讲解。 | 运算符 | 描述 | 规则说明 | | --------- | ------------ | --------------------------------------------------------------------------------------------- | | | | 按位或 | 只要对应的两个二进位中有一个为 $1$,结果位即为 $1$,否则为 $0$。 | | `&` | 按位与 | 仅当对应的两个二进位都为 $1$ 时,结果位才为 $1$,否则为 $0$。 | | `^` | 按位异或 | 对应的两个二进位不同则结果位为 $1$,相同则为 $0$。 | | `~` | 按位取反 | 对操作数的每一位取反,$1$ 变为 $0$,$0$ 变为 $1$。 | | `<<` | 左移 | 所有二进位整体向左移动指定的位数,高位溢出丢弃,低位补 $0$。 | | `>>` | 右移 | 所有二进位整体向右移动指定的位数,低位溢出丢弃,高位补 $0$(无符号右移时)。 | ### 2.1 按位与运算 > **按位与运算(AND)**:使用运算符 `&`,对两个二进制数的每一位进行比较,只有当对应位都为 $1$ 时,结果位才为 $1$,否则为 $0$。 - **按位与运算规则**: - `1 & 1 = 1` - `1 & 0 = 0` - `0 & 1 = 0` - `0 & 0 = 0` 例如,将 $01111100_{(2)}$ 与 $00111110_{(2)}$ 进行按位与运算,结果为 $00111100_{(2)}$,如下图所示: ![按位与运算](https://qcdn.itcharge.cn/images/202405132137023.png) ### 2.2 按位或运算 > **按位或运算(OR)**:使用运算符 `|`,对两个二进制数的每一位进行「或」操作。只要对应的两个二进位中有一个为 $1$,结果位就是 $1$,只有两个都是 $0$ 时结果才为 $0$。 - **按位或运算规则**: - `1 | 1 = 1` - `1 | 0 = 1` - `0 | 1 = 1` - `0 | 0 = 0` 例如,将 $01001010_{(2)}$ 与 $01011011_{(2)}$ 进行按位或运算,结果为 $01011011_{(2)}$,如下图所示: ![按位或运算](https://qcdn.itcharge.cn/images/202405132137593.png) ### 2.3 按位异或运算 > **按位异或运算(XOR)**:使用运算符 `^`,对两个二进制数的每一位进行比较。只有当对应的两位不同(即一位为 $1$,一位为 $0$)时,结果位才为 $1$,否则为 $0$。 - **按位异或运算运算规则**: - `0 ^ 0 = 0` - `1 ^ 0 = 1` - `0 ^ 1 = 1` - `1 ^ 1 = 0` 简而言之,异或运算的本质是「相同为 $0$,不同为 $1$」。 例如,将 $01001010_{(2)}$ 与 $01000101_{(2)}$ 进行按位异或运算,结果为 $00001111_{(2)}$,如下图所示: ![按位异或运算](https://qcdn.itcharge.cn/images/202405132137874.png) ### 2.4 取反运算 > **取反运算(NOT)**:取反运算符为 `~`,用于将一个二进制数的每一位进行翻转,即 $1$ 变为 $0$,$0$ 变为 $1$。 - **取反运算规则**: - `~0 = 1` - `~1 = 0` 例如,对二进制数 $01101010_{(2)}$ 进行取反,结果如下图所示: ![取反运算](https://qcdn.itcharge.cn/images/202405132138853.png) ### 2.5 左移运算与右移运算 > **左移运算(SHL)**:使用运算符 `<<`,将一个二进制数的所有位整体向左移动指定的位数。左移时,高位超出部分被舍弃,低位空缺部分补 $0$。 例如,将二进制数 $01101010_{(2)}$ 左移 $1$ 位,得到 $11010100_{(2)}$,如下图所示: ![左移运算](https://qcdn.itcharge.cn/images/202405132138841.png) > **右移运算(SHR)**:使用运算符 `>>`,将一个二进制数的所有位整体向右移动指定的位数。右移时,低位超出部分被舍弃,高位空缺部分补 $0$。 例如,将二进制数 $01101010_{(2)}$ 右移 $1$ 位,得到 $00110101_{(2)}$,如下图所示: ![右移运算](https://qcdn.itcharge.cn/images/202405132138348.png) ## 3. 位运算的应用 ### 3.1 判断整数奇偶 判断一个整数的奇偶性,可以利用其二进制表示的最低位。偶数的二进制最低位为 $0$,奇数的最低位为 $1$。因此,通过将该数与 $1$ 进行按位与运算即可快速判断: - 如果 `(x & 1) == 0`,则 $x$ 为偶数; - 如果 `(x & 1) == 1`,则 $x$ 为奇数。 ### 3.2 二进制数选取指定位 如果需从二进制数 $X$ 中提取指定的若干位(即保留这些位的原值,其余位置为 $0$),可以先构造一个掩码 $Y$,使得需要保留的位置为 $1$,其余为 $0$。随后通过按位与运算(`X & Y`)即可实现目标。 例如,如果要获取 $X = 01101010_{(2)}$ 的最低 $4$ 位,只需将其与 $Y = 00001111_{(2)}$(最低 $4$ 位为 $1$,其余为 $0$)进行按位与运算:`01101010 & 00001111 = 00001010`。结果 $00001010$ 即为 $X$ 的末尾 $4$ 位。 ### 3.3 将指定位设置为 $1$ 如果需将二进制数 $X$ 的某几位强制设置为 $1$(其余位保持原值),可构造一个掩码 $Y$,使得需要设置为 $1$ 的位为 $1$,其余为 $0$。然后通过按位或运算(`X | Y`)即可实现。 例如,如果要将 $X = 01101010_{(2)}$ 的最低 $4$ 位设置为 $1$,其余位不变,只需与 $Y = 00001111_{(2)}$(最低 $4$ 位为 $1$,其余为 $0$)进行按位或运算:`01101010 | 00001111 = 01101111`。结果 $01101111$ 即为所需的新数。 ### 3.4 反转指定位 如果需反转二进制数 $X$ 的某几位,可构造一个掩码 $Y$,使得需要反转的位置为 $1$,其余为 $0$。然后对 $X$ 和 $Y$ 进行按位异或运算(`X ^ Y`),即可实现指定位的反转。 例如,如果要反转 $X = 01101010_{(2)}$ 的最低 $4$ 位,只需将其与 $Y = 00001111_{(2)}$(最低 $4$ 位为 $1$,其余为 $0$)进行异或:`01101010 ^ 00001111 = 01100101`。结果 $01100101$ 即为 $X$ 的最低 $4$ 位被反转后的新值。 ### 3.5 交换两个数 通过按位异或运算,可以无需临时变量实现两个整数的交换(仅适用于整数类型)。示例代码如下: ```python a, b = 10, 20 a ^= b b ^= a a ^= b print(a, b) ``` ### 3.6 将二进制最右侧为 $1$ 的二进位改为 $0$ 要将二进制数 $X$ 最右侧的 $1$ 置为 $0$,只需执行 `X & (X - 1)` 操作即可。 例如,$X = 01101100_{(2)}$,$X - 1 = 01101011_{(2)}$,则 `X & (X - 1) = 01101100 & 01101011 = 01101000`,结果为 $01101000_{(2)}$,即成功将 $X$ 最右侧的 $1$ 变为 $0$。 ### 3.7 计算二进制中二进位为 $1$ 的个数 根据 3.6 节的内容,利用 `X & (X - 1)` 操作可以将二进制数 $X$ 的最右侧一个 $1$ 变为 $0$。因此,如果我们不断对 $X$ 执行该操作,直到 $X$ 变为 $0$,并统计操作次数,就能得到 $X$ 的二进制表示中 $1$ 的个数。 实现代码如下: ```python class Solution: def hammingWeight(self, n: int) -> int: cnt = 0 while n: n = n & (n - 1) cnt += 1 return cnt ``` ### 3.8 判断某数是否为 $2$ 的幂次方 判断一个数 $X$ 是否为 $2$ 的幂,可以利用位运算:只需判断 `X & (X - 1) == 0` 是否成立。 原理如下: - 如果 $X$ 是 $2$ 的幂,则其二进制表示只有一位为 $1$,其余全为 $0$,如 $4_{(10)} = 00000100_{(2)}$,$8_{(10)} = 00001000_{(2)}$。 - 如果 $X$ 不是 $2$ 的幂,则其二进制表示中有多位为 $1$,如 $5_{(10)} = 00000101_{(2)}$,$6_{(10)} = 00000110_{(2)}$。 当 $X > 0$ 时,`X & (X - 1)` 的作用是将 $X$ 最右侧的 $1$ 变为 $0$,其余位保持不变: - 如果 $X$ 是 $2$ 的幂,执行 `X & (X - 1)` 后结果为 $0$。 - 如果 $X$ 不是 $2$ 的幂,执行后结果不为 $0$。 因此,只需判断 `X > 0` 且 `X & (X - 1) == 0`,即可确定 $X$ 是否为 $2$ 的幂。 ### 3.9 位运算的常用操作总结 | 序号 | 操作描述 | 位运算表达式 | 示例 | | :--: | :--------------------------------------- | :------------------------------------- | :-------------------------- | | 1 | 将最低位的 $1$ 置为 $0$ | x & (x - 1) | `100101000 -> 100100000` | | 2 | 保留最右侧的 $1$,其余清零 | x & -xx & (x ^ (x - 1)) | `100101000 -> 1000` | | 3 | 去掉最后一位 | x >> 1 | `101101 -> 10110` | | 4 | 取右数第 $k$ 位 | (x >> (k - 1)) & 1 | `1101101 -> 1, k = 4` | | 5 | 取末尾 $k$ 位 | x & ((1 << k) - 1) | `1101101 -> 101, k = 3`;
`1101101 -> 1101, k = 4` | | 6 | 只保留右边连续的 $1$ | (x ^ (x + 1)) >> 1 | `100101111 -> 1111` | | 7 | 右数第 $k$ 位取反 | x ^ (1 << (k - 1)) | `101001 -> 101101, k = 3` | | 8 | 在最后加一个 $0$ | x << 1 | `101101 -> 1011010` | | 9 | 在最后加一个 $1$ | (x << 1) + 1 | `101101 -> 1011011` | | 10 | 把右数第 $k$ 位变成 $0$ | x & ~(1 << (k - 1)) | `101101 -> 101001, k = 3` | | 11 | 把右数第 $k$ 位变成 $1$ | x | (1 << (k - 1)) | `101001 -> 101101, k = 3` | | 12 | 把右边起第一个 $0$ 变成 $1$ | x | (x + 1) | `100101111 -> 100111111` | | 13 | 把右边连续的 $0$ 变成 $1$ | x | (x - 1) | `11011000 -> 11011111` | | 14 | 把右边连续的 $1$ 变成 $0$ | x & (x + 1) | `100101111 -> 100100000` | | 15 | 把最后一位变成 $0$ | x & ~1 | `101101 -> 101100` | | 16 | 把最后一位变成 $1$ | x | 1 | `101100 -> 101101` | | 17 | 把末尾 $k$ 位变成 $1$ | x | ((1 << k) - 1) | `101001 -> 101111, k = 4` | | 18 | 末尾 $k$ 位取反 | x ^ ((1 << k) - 1) | `101101 -> 101100, k = 1`;
`101001 -> 100110, k = 4` | ### 3.3 二进制枚举子集 在位运算中,常常利用二进制的第 $1 \sim n$ 位上的 $0$ 或 $1$ 来表示由 $1 \sim n$ 组成的集合,从而实现对子集的高效枚举。 #### 3.3.1 二进制枚举子集简介 首先,简要介绍一下「子集」的定义: - **子集**:如果集合 $A$ 的所有元素均属于集合 $S$,则称 $A$ 是 $S$ 的子集,记作 $A \subseteq S$。 实际问题中,常常需要枚举集合 $S$ 的所有子集。枚举子集的方法有多种,这里介绍一种简洁高效的方式:「二进制枚举子集」。 对于一个包含 $n$ 个元素的集合 $S$,每个元素都有「选」或「不选」两种状态。我们可以用二进制数的 $n$ 位来表示每个元素的选取情况:$1$ 表示选取该元素,$0$ 表示不选取。 这样,任意一个 $n$ 位二进制数都唯一对应 $S$ 的一个子集。二进制的每一位对应集合中某个元素,$1$ 代表选取,$0$ 代表不选。 举例说明,设 $S = \lbrace 5, 4, 3, 2, 1 \rbrace$,用 $5$ 位二进制数表示: - $11111_{(2)}$ 表示选取所有元素,即 $S$ 本身: | 元素位置 | 5 | 4 | 3 | 2 | 1 | | :--------------: | :-: | :-: | :-: | :-: | :-: | | 二进制位 | 1 | 1 | 1 | 1 | 1 | | 选取状态 | 选取 | 选取 | 选取 | 选取 | 选取 | - $10101_{(2)}$ 表示选取第 $1$、$3$、$5$ 位元素,即 $\lbrace 5, 3, 1 \rbrace$: | 元素位置 | 5 | 4 | 3 | 2 | 1 | | :--------------: | :-: | :-: | :-: | :-: | :-: | | 二进制位 | 1 | 0 | 1 | 0 | 1 | | 选取状态 | 选取 | 未选取 | 选取 | 未选取 | 选取 | - $01001_{(2)}$ 表示选取第 $1$、$4$ 位元素,即 $\lbrace 4, 1 \rbrace$: | 元素位置 | 5 | 4 | 3 | 2 | 1 | | :--------------: | :-: | :-: | :-: | :-: | :-: | | 二进制位 | 0 | 1 | 0 | 0 | 1 | | 选取状态 | 未选取 | 选取 | 未选取 | 未选取 | 选取 | 综上所述,对于长度为 $n$ 的集合 $S$,只需枚举 $0 \sim 2^n - 1$(共 $2^n$ 种 $n$ 位二进制数),即可高效遍历并生成 $S$ 的所有子集。 #### 3.3.2 二进制枚举子集的实现代码 ```python class Solution: def subsets(self, S): # 返回集合 S 的所有子集 n = len(S) # n 为集合 S 的元素个数 sub_sets = [] # sub_sets 用于保存所有子集 for i in range(1 << n): # 枚举 0 ~ 2^n - 1 的所有可能,每个 i 表示一种选取方案 sub_set = [] # sub_set 用于保存当前子集 for j in range(n): # 枚举集合 S 的每一个元素 # (i >> j) & 1 判断第 j 位是否为 1 # 如果为 1,说明在当前子集方案 i 中选取了 S[j] if (i >> j) & 1: # 如果第 j 位为 1,则选取 S[j] sub_set.append(S[j]) # 将选取的元素 S[j] 加入到当前子集 sub_set 中 sub_sets.append(sub_set) # 将当前子集 sub_set 加入到所有子集数组 sub_sets 中 return sub_sets # 返回所有子集 ``` ## 4. 总结 位运算是一种直接操作二进制位的高效技巧,能够在底层实现中大幅提升算法的时间和空间效率,广泛应用于状态压缩、集合枚举、掩码处理等场景。 位运算基础操作包括按位与、或、异或、取反、左移和右移等,这些操作能够直接高效地处理二进制数据,常用于状态压缩、集合枚举和性能优化等场景。 在实际应用中,常通过二进制状态来表示集合的选取情况,从而高效枚举所有子集。其核心思想是:用 $n$ 位二进制数的每一位对应集合中的一个元素,$1$ 表示选中该元素,$0$ 表示未选中。只需遍历 $0$ 到 $2^n-1$ 的所有二进制数,即可快速生成集合的全部子集。 ## 练习题目 - [0190. 颠倒二进制位](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reverse-bits.md) - [0191. 位1的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/number-of-1-bits.md) - [0201. 数字范围按位与](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/bitwise-and-of-numbers-range.md) - [0136. 只出现一次的数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) - [0137. 只出现一次的数字 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number-ii.md) - [0260. 只出现一次的数字 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/single-number-iii.md) - [位运算题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E4%BD%8D%E8%BF%90%E7%AE%97%E9%A2%98%E7%9B%AE) ## 参考资料 - 【博文】[Python 中的按位运算符 |【生长吧!Python!】- 云社区 - 华为云](https://bbs.huaweicloud.com/blogs/280901) - 【博文】[一文读懂位运算的使用 - 小黑说 Java - 掘金](https://juejin.cn/post/7011407264581943326) - 【博文】[枚举排列和枚举子集 - CUC ACM-Wiki](https://cuccs.github.io/acm-wiki/search/enumeration/) - 【博文】[Swift 运算符 | 菜鸟教程](https://www.runoob.com/swift/swift-operators.html) ================================================ FILE: docs/07_algorithm/index.md ================================================ ![](https://qcdn.itcharge.cn/images/20250923140522.png) ::: tip 引 言 算法如梦中星河,闪烁着智慧的光芒: 枚举如夜空点点,耐心数尽每一颗星辰; 递归如山谷回音,层层叠叠,绵延不绝; 分治如破晓晨曦,将混沌一分为二,终归明朗; 回溯如时光逆旅,步步回首,终觅最美风景; 贪心如追光旅人,逐一缕微光,直奔理想彼岸; 位运算如星尘魔法,指尖轻触,万象变幻。 ::: ## 本章内容 - [7.1 枚举算法](https://github.com/ITCharge/AlgoNote/tree/main/docs/07_algorithm/07_01_enumeration_algorithm.md) - [7.2 递归算法](https://github.com/ITCharge/AlgoNote/tree/main/docs/07_algorithm/07_02_recursive_algorithm.md) - [7.3 分治算法](https://github.com/ITCharge/AlgoNote/tree/main/docs/07_algorithm/07_03_divide_and_conquer_algorithm.md) - [7.4 回溯算法](https://github.com/ITCharge/AlgoNote/tree/main/docs/07_algorithm/07_04_backtracking_algorithm.md) - [7.5 贪心算法](https://github.com/ITCharge/AlgoNote/tree/main/docs/07_algorithm/07_05_greedy_algorithm.md) - [7.6 位运算](https://github.com/ITCharge/AlgoNote/tree/main/docs/07_algorithm/07_06_bit_operation.md) ================================================ FILE: docs/08_dynamic_programming/08_01_dynamic_programming_basic.md ================================================ ## 1. 动态规划简介 ### 1.1 动态规划的定义 > **动态规划(Dynamic Programming,DP)**:是一种求解多阶段决策过程最优化问题的方法。在动态规划中,通过把原问题分解为相对简单的子问题,先求解子问题,再由子问题的解而得到原问题的解。 动态规划最早由理查德 · 贝尔曼于 1957 年在其著作「动态规划(Dynamic Programming)」一书中提出。这里的 Programming 并不是编程的意思,而是指一种「表格处理方法」,即将每一步计算的结果存储在表格中,供随后的计算查询使用。 ### 1.2 动态规划的核心思想 > **动态规划的核心思想**: > > 1. 把「原问题」分解为「若干个重叠的子问题」,每个子问题的求解过程都构成一个「阶段」。在完成一个阶段的计算之后,动态规划方法才会执行下一个阶段的计算。 > 2. 在求解子问题的过程中,按照「自顶向下的记忆化搜索方法」或者「自底向上的递推方法」求解出「子问题的解」,把结果存储在表格中,当需要再次求解此子问题时,直接从表格中查询该子问题的解,从而避免了大量的重复计算。 这看起来很像是分治算法,但动态规划与分治算法的不同点在于: 1. 适用于动态规划求解的问题,在分解之后得到的子问题往往是相互联系的,会出现若干个重叠子问题。 2. 使用动态规划方法会将这些重叠子问题的解保存到表格里,供随后的计算查询使用,从而避免大量的重复计算。 ### 1.3 动态规划的简单例子 下面我们先来通过一个简单的例子来介绍一下什么是动态规划算法,然后再来讲解动态规划中的各种术语。 > **斐波那契数列**:数列由 $f(0) = 0, f(1) = 1$ 开始,后面的每一项数字都是前面两项数字的和。也就是: > > $f(n) = \begin{cases} 0 & n = 0 \cr 1 & n = 1 \cr f(n - 2) + f(n - 1) & n > 1 \end{cases}$ 通过公式 $f(n) = f(n - 2) + f(n - 1)$,我们可以将原问题 $f(n)$ 递归地划分为 $f(n - 2)$ 和 $f(n - 1)$ 这两个子问题。其对应的递归过程如下图所示: ![斐波那契数列的重复计算项](https://qcdn.itcharge.cn/images/20230307164107.png) 从图中可以看出:如果使用传统递归算法计算 $f(5)$,需要先计算 $f(3)$ 和 $f(4)$,而在计算 $f(4)$ 时还需要计算 $f(3)$,这样 $f(3)$ 就进行了多次计算。同理 $f(0)$、$f(1)$、$f(2)$ 都进行了多次计算,从而导致了重复计算问题。 为了避免重复计算,我们可以使用动态规划中的「表格处理方法」来处理。 这里我们使用「自底向上的递推方法」求解出子问题 $f(n - 2)$ 和 $f(n - 1)$ 的解,然后把结果存储在表格中,供随后的计算查询使用。具体过程如下: 1. 定义一个数组 $dp$,用于记录斐波那契数列中的值。 2. 初始化 $dp[0] = 0, dp[1] = 1$。 3. 根据斐波那契数列的递推公式 $f(n) = f(n - 1) + f(n - 2)$,从 $dp(2)$ 开始递推计算斐波那契数列的每个数,直到计算出 $dp(n)$。 4. 最后返回 $dp(n)$ 即可得到第 $n$ 项斐波那契数。 具体代码如下: ```python class Solution: def fib(self, n: int) -> int: """ 使用动态规划(自底向上)方法计算斐波那契数列的第 n 项。 状态定义:dp[i] 表示第 i 项斐波那契数的值。 初始状态:dp[0] = 0, dp[1] = 1 状态转移:dp[i] = dp[i-1] + dp[i-2] """ if n == 0: return 0 # 特判 n = 0 if n == 1: return 1 # 特判 n = 1 # 初始化 dp 数组,长度为 n + 1 dp = [0] * (n + 1) dp[0] = 0 dp[1] = 1 # 递推计算每一项的值 for i in range(2, n + 1): dp[i] = dp[i - 1] + dp[i - 2] # 状态转移方程 return dp[n] # 返回第 n 项的值 ``` 这种使用缓存(哈希表、集合或数组)保存计算结果,从而避免子问题重复计算的方法,就是「动态规划算法」。 ## 2. 动态规划的特征 究竟什么样的问题才可以使用动态规划算法解决呢? 首先,能够使用动态规划方法解决的问题必须满足以下三个特征: 1. **最优子结构性质** 2. **重叠子问题性质** 3. **无后效性** ### 2.1 最优子结构性质 > **最优子结构**:指的是一个问题的最优解包含其子问题的最优解。 举个例子,如下图所示,原问题 $S = \lbrace a_1, a_2, a_3, a_4 \rbrace$,在 $a_1$ 步我们选出一个当前最优解之后,问题就转换为求解子问题 $S_{\text{子问题}} = \lbrace a_2, a_3, a_4 \rbrace$。如果原问题 $S$ 的最优解可以由「第 $a_1$ 步得到的局部最优解」和「 $S_{\text{子问题}}$ 的最优解」构成,则说明该问题满足最优子结构性质。 也就是说,如果原问题的最优解包含子问题的最优解,则说明该问题满足最优子结构性质。 ![最优子结构性质](https://qcdn.itcharge.cn/images/20240513163310.png) ### 2.2 重叠子问题性质 > **重叠子问题性质**:指的是在求解子问题的过程中,有大量的子问题是重复的,一个子问题在下一阶段的决策中可能会被多次用到。如果有大量重复的子问题,那么只需要对其求解一次,然后用表格将结果存储下来,以后使用时可以直接查询,不需要再次求解。 ![重叠子问题性质](https://qcdn.itcharge.cn/images/20230307164107.png) 之前我们提到的「斐波那契数列」例子中,$f(0)$、$f(1)$、$f(2)$、$f(3)$ 都进行了多次重复计算。动态规划算法利用了子问题重叠的性质,在第一次计算 $f(0)$、$f(1)$、$f(2)$、$f(3)$ 时就将其结果存入表格,当再次使用时可以直接查询,无需再次求解,从而提升效率。 ### 2.3 无后效性 > **无后效性**:指的是子问题的解(状态值)只与之前阶段有关,而与后面阶段无关。当前阶段的若干状态值一旦确定,就不再改变,不会再受到后续阶段决策的影响。 也就是说,**一旦某一个子问题的求解结果确定以后,就不会再被修改**。 举个例子,下图是一个有向无环带权图,我们在求解从 $A$ 点到 $F$ 点的最短路径问题时,假设当前已知从 $A$ 点到 $D$ 点的最短路径($2 + 7 = 9$)。那么无论之后的路径如何选择,都不会影响之前从 $A$ 点到 $D$ 点的最短路径长度。这就是「无后效性」。 而如果一个问题具有「后效性」,则可能需要先将其转化或者逆向求解来消除后效性,然后才可以使用动态规划算法。 ![无后效性](https://qcdn.itcharge.cn/images/20240514110127.png) ## 3. 动态规划的基本思路 如下图所示,我们在使用动态规划方法解决某些最优化问题时,可以将解决问题的过程按照一定顺序(时间顺序、空间顺序或其他顺序)分解为若干个相互联系的「阶段」。然后按照顺序对每一个阶段做出「决策」,这个决策既决定了本阶段的效益,也决定了下一阶段的初始状态。依次做完每个阶段的决策之后,就得到了一个整个问题的决策序列。 这样就将一个原问题分解为了一系列的子问题,再通过逐步求解从而获得最终结果。 ![动态规划方法](https://qcdn.itcharge.cn/images/20240514110154.png) 这种前后关联、具有链状结构的多阶段进行决策的问题也叫做「多阶段决策问题」。 通常我们使用动态规划方法来解决问题的基本思路如下: 1. **阶段划分**:将原问题按顺序(时间顺序、空间顺序或其他顺序)分解为若干个相互联系的「阶段」。划分后的阶段⼀定是有序或可排序的,否则问题⽆法求解。 - 这里的「阶段」指的是⼦问题的求解过程。每个⼦问题的求解过程都构成⼀个「阶段」,在完成前⼀阶段的求解后才会进⾏后⼀阶段的求解。 2. **定义状态**:将和子问题相关的某些变量(位置、数量、体积、空间等等)作为一个「状态」表示出来。状态的选择要满⾜⽆后效性。 - 一个「状态」对应一个或多个子问题,所谓某个「状态」下的值,指的就是这个「状态」所对应的子问题的解。 3. **状态转移**:根据「上一阶段的状态」和「该状态下所能做出的决策」,推导出「下一阶段的状态」。或者说根据相邻两个阶段各个状态之间的关系,确定决策,然后推导出状态间的相互转移方式(即「状态转移方程」)。 4. **初始条件和边界条件**:根据问题描述、状态定义和状态转移方程,确定初始条件和边界条件。 5. **最终结果**:确定问题的求解目标,然后按照一定顺序求解每一个阶段的问题。最后根据状态转移方程的递推结果,确定最终结果。 ## 4. 动态规划的应用 动态规划相关的问题往往灵活多变,思维难度大,没有特别明显的套路,并且经常会在各类算法竞赛和面试中出现。 动态规划问题的关键点在于「如何状态设计」和「推导状态转移条件」,还有各种各样的「优化方法」。这类问题一定要多练习、多总结,只有接触的题型多了,才能熟练掌握动态规划思想。 下面来介绍几道关于动态规划的基础题目。 ### 4.1 经典例题:斐波那契数 #### 4.1.1 题目链接 - [509. 斐波那契数 - 力扣](https://leetcode.cn/problems/fibonacci-number/) #### 4.1.2 题目大意 **描述**:给定一个整数 $n$。 **要求**:计算第 $n$ 个斐波那契数。 **说明**: - 斐波那契数列的定义如下: - $f(0) = 0, f(1) = 1$。 - $f(n) = f(n - 1) + f(n - 2)$,其中 $n > 1$。 - $0 \le n \le 30$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:1 解释:F(2) = F(1) + F(0) = 1 + 0 = 1 ``` - 示例 2: ```python 输入:n = 3 输出:2 解释:F(3) = F(2) + F(1) = 1 + 1 = 2 ``` #### 4.1.3 解题思路 ###### 1. 阶段划分 我们可以按照整数顺序进行阶段划分,将其划分为整数 $0 \sim n$。 ###### 2. 定义状态 定义状态 $dp[i]$ 为:第 $i$ 个斐波那契数。 ###### 3. 状态转移方程 根据题目中所给的斐波那契数列的定义 $f(n) = f(n - 1) + f(n - 2)$,则直接得出状态转移方程为 $dp[i] = dp[i - 1] + dp[i - 2]$。 ###### 4. 初始条件 根据题目中所给的初始条件 $f(0) = 0, f(1) = 1$ 确定动态规划的初始条件,即 $dp[0] = 0, dp[1] = 1$。 ###### 5. 最终结果 根据状态定义,最终结果为 $dp[n]$,即第 $n$ 个斐波那契数为 $dp[n]$。 #### 4.1.4 代码 ```python class Solution: def fib(self, n: int) -> int: if n <= 1: return n dp = [0 for _ in range(n + 1)] dp[0] = 0 dp[1] = 1 for i in range(2, n + 1): dp[i] = dp[i - 2] + dp[i - 1] return dp[n] ``` #### 4.1.5 复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 ### 4.2 爬楼梯 #### 4.2.1 经典例题:题目链接 - [70. 爬楼梯 - 力扣](https://leetcode.cn/problems/climbing-stairs/) #### 4.2.2 题目大意 **描述**:假设你正在爬楼梯。需要 $n$ 阶你才能到达楼顶。每次你可以爬 $1$ 或 $2$ 个台阶。现在给定一个整数 $n$。 **要求**:计算出有多少种不同的方法可以爬到楼顶。 **说明**: - $1 \le n \le 45$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:2 解释:有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶 ``` - 示例 2: ```python 输入:n = 3 输出:3 解释:有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶 ``` #### 4.2.3 解题思路 ###### 1. 阶段划分 我们按照台阶的阶层阶段划分,将其划分为 $0 \sim n$ 阶。 ###### 2. 定义状态 定义状态 $dp[i]$ 为:爬到第 $i$ 阶台阶的方案数。 ###### 3. 状态转移方程 根据题目大意,每次只能爬 $1$ 或 $2$ 个台阶。则第 $i$ 阶楼梯只能从第 $i - 1$ 阶向上爬 $1$ 阶上来,或者从第 $i - 2$ 阶向上爬 $2$ 阶上来。所以可以推出状态转移方程为 $dp[i] = dp[i - 1] + dp[i - 2]$。 ###### 4. 初始条件 - 第 $0$ 层台阶方案数:可以看做 $1$ 种方法(从 $0$ 阶向上爬 $0$ 阶),即 $dp[1] = 1$。 - 第 $1$ 层台阶方案数:$1$ 种方法(从 $0$ 阶向上爬 $1$ 阶),即 $dp[1] = 1$。 - 第 $2$ 层台阶方案数:$2$ 中方法(从 $0$ 阶向上爬 $2$ 阶,或者从 $1$ 阶向上爬 $1$ 阶)。 ###### 5. 最终结果 根据状态定义,最终结果为 $dp[n]$,即爬到第 $n$ 阶台阶(即楼顶)的方案数为 $dp[n]$。 虽然这道题跟上一道题的状态转移方程都是 $dp[i] = dp[i - 1] + dp[i - 2]$,但是两道题的考察方式并不相同,一定程度上也可以看出来动态规划相关题目的灵活多变。 #### 4.2.4 代码 ```python class Solution: def climbStairs(self, n: int) -> int: dp = [0 for _ in range(n + 1)] dp[0] = 1 dp[1] = 1 for i in range(2, n + 1): dp[i] = dp[i - 1] + dp[i - 2] return dp[n] ``` #### 4.2.5 复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。因为 $dp[i]$ 的状态只依赖于 $dp[i - 1]$ 和 $dp[i - 2]$,所以可以使用 $3$ 个变量来分别表示 $dp[i]$、$dp[i - 1]$、$dp[i - 2]$,从而将空间复杂度优化到 $O(1)$。 ## 练习题目 - [0509. 斐波那契数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/fibonacci-number.md) - [0070. 爬楼梯](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/climbing-stairs.md) - [0062. 不同路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths.md) - [动态规划基础题目](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%9F%BA%E7%A1%80%E9%A2%98%E7%9B%AE) ## 参考资料 - 【文章】[动态规划基础 - OI Wiki](https://oi-wiki.org/dp/basic/) - 【文章】[动态规划 1 ——基本概念 - 知乎](https://zhuanlan.zhihu.com/p/25441186) - 【文章】[动态规划算法 | 曹世宏的博客](https://cshihong.github.io/2018/03/30/动态规划算法/) - 【文章】[动态规划之初识动规:有了四步解题法模板,再也不害怕动态规划! - 知乎](https://zhuanlan.zhihu.com/p/91680256) - 【文章】[第 6 节 最优子结构、重复子问题、无后效性 | 算法吧](https://suanfa8.com/dynamic-programming/06/) - 【书籍】算法训练营 陈小玉 著 - 【书籍】趣学算法 陈小玉 著 - 【书籍】算法竞赛进阶指南 - 李煜东 著 - 【书籍】ACM-ICPC 程序设计系列 - 算法设计与实现 - 陈宇 吴昊 主编 ================================================ FILE: docs/08_dynamic_programming/08_02_memoization_search.md ================================================ ## 1. 记忆化搜索简介 >**记忆化搜索(Memoization Search)**:是一种通过存储已经遍历过的状态信息,从而避免对同一状态重复遍历的搜索算法。 记忆化搜索是动态规划的一种实现方式。在记忆化搜索中,当算法需要计算某个子问题的结果时,它首先检查是否已经计算过该问题。如果已经计算过,则直接返回已经存储的结果;否则,计算该问题,并将结果存储下来以备将来使用。 举个例子,比如「斐波那契数列」的定义是:$f(0) = 0, f(1) = 1, f(n) = f(n - 1) + f(n - 2)$。如果我们使用递归算法求解第 $n$ 个斐波那契数,则对应的递推过程如下: ![记忆化搜索](https://qcdn.itcharge.cn/images/20240514110503.png) 从图中可以看出:如果使用普通递归算法,想要计算 $f(5)$,需要先计算 $f(3)$ 和 $f(4)$,而在计算 $f(4)$ 时还需要计算 $f(3)$。这样 $f(3)$ 就进行了多次计算,同理 $f(0)$、$f(1)$、$f(2)$ 都进行了多次计算,从而导致了重复计算问题。 为了避免重复计算,在递归的同时,我们可以使用一个缓存(数组或哈希表)来保存已经求解过的 $f(k)$ 的结果。如上图所示,当递归调用用到 $f(k)$ 时,先查看一下之前是否已经计算过结果,如果已经计算过,则直接从缓存中取值返回,而不用再递推下去,这样就避免了重复计算问题。 使用「记忆化搜索」方法解决斐波那契数列的代码如下: ```python class Solution: def fib(self, n: int) -> int: # 使用数组保存已经求解过的 f(k) 的结果 memo = [0 for _ in range(n + 1)] return self.my_fib(n, memo) def my_fib(self, n: int, memo: List[int]) -> int: if n == 0: return 0 if n == 1: return 1 # 已经计算过结果 if memo[n] != 0: return memo[n] # 没有计算过结果 memo[n] = self.my_fib(n - 1, memo) + self.my_fib(n - 2, memo) return memo[n] ``` ## 2. 记忆化搜索与递推区别 「记忆化搜索」与「递推」都是动态规划的实现方式,但是两者之间有一些区别。 > **记忆化搜索**:「自顶向下」的解决问题,采用自然的递归方式编写过程,在过程中会保存每个子问题的解(通常保存在一个数组或哈希表中)来避免重复计算。 > > - 优点:代码清晰易懂,可以有效的处理一些复杂的状态转移方程。有些状态转移方程是非常复杂的,使用记忆化搜索可以将复杂的状态转移方程拆分成多个子问题,通过递归调用来解决。 > - 缺点:可能会因为递归深度过大而导致栈溢出问题。 > > **递推**:「自底向上」的解决问题,采用循环的方式编写过程,在过程中通过保存每个子问题的解(通常保存在一个数组或哈希表中)来避免重复计算。 > > - 优点:避免了深度过大问题,不存在栈溢出问题。计算顺序比较明确,易于实现。 > - 缺点:无法处理一些复杂的状态转移方程。有些状态转移方程非常复杂,如果使用递推方法来计算,就会导致代码实现变得非常困难。 根据记忆化搜索和递推的优缺点,我们可以在不同场景下使用这两种方法。 适合使用「记忆化搜索」的场景: 1. 问题的状态转移方程比较复杂,递推关系不是很明确。 2. 问题适合转换为递归形式,并且递归深度不会太深。 适合使用「递推」的场景: 1. 问题的状态转移方程比较简单,递归关系比较明确。 2. 问题不太适合转换为递归形式,或者递归深度过大容易导致栈溢出。 ## 3. 记忆化搜索解题步骤 我们在使用记忆化搜索解决问题的时候,其基本步骤如下: 1. 写出问题的动态规划「状态」和「状态转移方程」。 2. 定义一个缓存(数组或哈希表),用于保存子问题的解。 3. 定义一个递归函数,用于解决问题。在递归函数中,首先检查缓存中是否已经存在需要计算的结果,如果存在则直接返回结果,否则进行计算,并将结果存储到缓存中,再返回结果。 4. 在主函数中,调用递归函数并返回结果。 ## 4. 记忆化搜索的应用 ### 4.1 目标和 #### 4.1.1 题目链接 - [494. 目标和 - 力扣](https://leetcode.cn/problems/target-sum/) #### 4.1.2 题目大意 **描述**:给定一个整数数组 $nums$ 和一个整数 $target$。数组长度不超过 $20$。向数组中每个整数前加 `+` 或 `-`。然后串联起来构造成一个表达式。 **要求**:返回通过上述方法构造的、运算结果等于 $target$ 的不同表达式数目。 **说明**: - $1 \le nums.length \le 20$。 - $0 \le nums[i] \le 1000$。 - $0 \le sum(nums[i]) \le 1000$。 - $-1000 \le target \le 1000$。 **示例**: - 示例 1: ```python 输入:nums = [1,1,1,1,1], target = 3 输出:5 解释:一共有 5 种方法让最终目标和为 3。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3 ``` - 示例 2: ```python 输入:nums = [1], target = 1 输出:1 ``` #### 4.1.3 解题思路 ##### 思路 1:深度优先搜索(超时) 使用深度优先搜索对每位数字进行 `+` 或者 `-`,具体步骤如下: 1. 定义从位置 $0$、和为 $0$ 开始,到达数组尾部位置为止,和为 $target$ 的方案数为 `dfs(0, 0)`。 2. 下面从位置 $0$、和为 $0$ 开始,以深度优先搜索遍历每个位置。 3. 如果当前位置 $i$ 到达最后一个位置 $size$: 1. 如果和 $cur\_sum$ 等于目标和 $target$,则返回方案数 $1$。 2. 如果和 $cur\_sum$ 不等于目标和 $target$,则返回方案数 $0$。 4. 递归搜索 $i + 1$ 位置,和为 $cur\_sum - nums[i]$ 的方案数。 5. 递归搜索 $i + 1$ 位置,和为 $cur\_sum + nums[i]$ 的方案数。 6. 将 4 ~ 5 两个方案数加起来就是当前位置 $i$、和为 $cur\_sum$ 的方案数,返回该方案数。 7. 最终方案数为 `dfs(0, 0)`,将其作为答案返回即可。 ##### 思路 1:代码 ```python class Solution: def findTargetSumWays(self, nums: List[int], target: int) -> int: size = len(nums) def dfs(i, cur_sum): if i == size: if cur_sum == target: return 1 else: return 0 ans = dfs(i + 1, cur_sum - nums[i]) + dfs(i + 1, cur_sum + nums[i]) return ans return dfs(0, 0) ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n)$。其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(n)$。递归调用的栈空间深度不超过 $n$。 ##### 思路 2:记忆化搜索 在思路 1 中我们单独使用深度优先搜索对每位数字进行 `+` 或者 `-` 的方法超时了。所以我们考虑使用记忆化搜索的方式,避免进行重复搜索。 这里我们使用哈希表 $table$ 记录遍历过的位置 $i$ 及所得到的的当前和$cur\_sum$ 下的方案数,来避免重复搜索。具体步骤如下: 1. 定义从位置 $0$、和为 $0$ 开始,到达数组尾部位置为止,和为 $target$ 的方案数为 `dfs(0, 0)`。 2. 下面从位置 $0$、和为 $0$ 开始,以深度优先搜索遍历每个位置。 3. 如果当前位置 $i$ 遍历完所有位置: 1. 如果和 $cur\_sum$ 等于目标和 $target$,则返回方案数 $1$。 2. 如果和 $cur\_sum$ 不等于目标和 $target$,则返回方案数 $0$。 4. 如果当前位置 $i$、和为 $cur\_sum$ 之前记录过(即使用 $table$ 记录过对应方案数),则返回该方案数。 5. 如果当前位置 $i$、和为 $cur\_sum$ 之前没有记录过,则: 1. 递归搜索 $i + 1$ 位置,和为 $cur\_sum - nums[i]$ 的方案数。 2. 递归搜索 $i + 1$ 位置,和为 $cur\_sum + nums[i]$ 的方案数。 3. 将上述两个方案数加起来就是当前位置 $i$、和为 $cur\_sum$ 的方案数,将其记录到哈希表 $table$ 中,并返回该方案数。 6. 最终方案数为 `dfs(0, 0)`,将其作为答案返回即可。 ##### 思路 2:代码 ```python class Solution: def findTargetSumWays(self, nums: List[int], target: int) -> int: size = len(nums) table = dict() def dfs(i, cur_sum): if i == size: if cur_sum == target: return 1 else: return 0 if (i, cur_sum) in table: return table[(i, cur_sum)] cnt = dfs(i + 1, cur_sum - nums[i]) + dfs(i + 1, cur_sum + nums[i]) table[(i, cur_sum)] = cnt return cnt return dfs(0, 0) ``` ##### 思路 2:复杂度分析 - **时间复杂度**:$O(2^n)$。其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(n)$。递归调用的栈空间深度不超过 $n$。 ### 4.2 第 N 个泰波那契数 #### 4.2.1 题目链接 - [1137. 第 N 个泰波那契数 - 力扣](https://leetcode.cn/problems/n-th-tribonacci-number/) #### 4.2.2 题目大意 **描述**:给定一个整数 $n$。 **要求**:返回第 $n$ 个泰波那契数。 **说明**: - **泰波那契数**:$T_0 = 0, T_1 = 1, T_2 = 1$,且在 $n >= 0$ 的条件下,$T_{n + 3} = T_{n} + T_{n+1} + T_{n+2}$。 - $0 \le n \le 37$。 - 答案保证是一个 32 位整数,即 $answer \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:n = 4 输出:4 解释: T_3 = 0 + 1 + 1 = 2 T_4 = 1 + 1 + 2 = 4 ``` - 示例 2: ```python 输入:n = 25 输出:1389537 ``` #### 4.2.3 解题思路 ##### 思路 1:记忆化搜索 1. 问题的状态定义为:第 $n$ 个泰波那契数。其状态转移方程为:$T_0 = 0, T_1 = 1, T_2 = 1$,且在 $n >= 0$ 的条件下,$T_{n + 3} = T_{n} + T_{n+1} + T_{n+2}$。 2. 定义一个长度为 $n + 1$ 数组 $memo$ 用于保存一斤个计算过的泰波那契数。 3. 定义递归函数 `my_tribonacci(n, memo)`。 1. 当 $n = 0$ 或者 $n = 1$,或者 $n = 2$ 时直接返回结果。 2. 当 $n > 2$ 时,首先检查是否计算过 $T(n)$,即判断 $memo[n]$ 是否等于 $0$。 1. 如果 $memo[n] \ne 0$,说明已经计算过 $T(n)$,直接返回 $memo[n]$。 2. 如果 $memo[n] = 0$,说明没有计算过 $T(n)$,则递归调用 `my_tribonacci(n - 3, memo)`、`my_tribonacci(n - 2, memo)`、`my_tribonacci(n - 1, memo)`,并将计算结果存入 $memo[n]$ 中,并返回 $memo[n]$。 ##### 思路 1:代码 ```python class Solution: def tribonacci(self, n: int) -> int: # 使用数组保存已经求解过的 T(k) 的结果 memo = [0 for _ in range(n + 1)] return self.my_tribonacci(n, memo) def my_tribonacci(self, n: int, memo: List[int]) -> int: if n == 0: return 0 if n == 1 or n == 2: return 1 if memo[n] != 0: return memo[n] memo[n] = self.my_tribonacci(n - 3, memo) + self.my_tribonacci(n - 2, memo) + self.my_tribonacci(n - 1, memo) return memo[n] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ## 练习题目 - [0494. 目标和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/target-sum.md) - [1137. 第 N 个泰波那契数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/n-th-tribonacci-number.md) - [0576. 出界的路径数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/out-of-boundary-paths.md) - [记忆化搜索题目](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E8%AE%B0%E5%BF%86%E5%8C%96%E6%90%9C%E7%B4%A2%E9%A2%98%E7%9B%AE) ## 参考资料 1. 【文章】[记忆化搜索 - OI Wiki](https://oi-wiki.org/dp/memo/) ================================================ FILE: docs/08_dynamic_programming/08_03_linear_dp_01.md ================================================ ## 1. 线性动态规划简介 > **线性动态规划(线性 DP)**:指的是将问题的阶段按线性顺序划分,并基于此进行状态转移的动态规划方法。如下图所示: ![线性 DP](https://qcdn.itcharge.cn/images/20240514110630.png) 即使状态有多个维度,只要每个维度的阶段划分都是线性的,也属于线性 DP。例如,背包问题、区间 DP、数位 DP 等都属于线性 DP 的范畴。 线性 DP 问题的分类方式主要有两种: - 按「状态维度」划分:可分为一维线性 DP、二维线性 DP 和多维线性 DP。 - 按「问题的输入格式」划分:可分为单串线性 DP、双串线性 DP、矩阵线性 DP 以及无串线性 DP。 本文将以「问题的输入格式」的方式进行分类,系统讲解线性 DP 的各类典型问题。 ## 2. 单串线性 DP 问题简介 > **单串线性 DP**:指输入为单个数组或字符串的线性动态规划问题。常见的状态定义为 $dp[i]$,其含义通常有以下三种: > > 1. 以 $nums[i]$ 结尾的子数组($nums[0]$ 到 $nums[i]$,$0 \leq i < n$)的相关解; > 2. 以 $nums[i - 1]$ 结尾的子数组($nums[0]$ 到 $nums[i - 1]$,$1 \leq i \leq n$)的相关解; > 3. 由前 $i$ 个元素组成的子数组($nums[0]$ 到 $nums[i - 1]$,$1 \leq i \leq n$)的相关解。 这三种状态定义的主要区别在于是否包含第 $i$ 个元素 $nums[i]$。 1. 第 1 种状态:子数组长度为 $i + 1$,不可为空; 2. 第 2、3 种状态:本质等价,子数组长度为 $i$,允许为空(当 $i = 0$ 时表示空数组,便于初始化和边界处理)。 ## 3. 单串线性 DP 问题经典题目 ### 3.1 经典例题:最长递增子序列 单串线性 DP 问题中最经典的问题就是「最长递增子序列(Longest Increasing Subsequence,简称 LIS)」。 #### 3.1.1 题目链接 - [300. 最长递增子序列 - 力扣](https://leetcode.cn/problems/longest-increasing-subsequence/) #### 3.1.2 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:找到其中最长严格递增子序列的长度。 **说明**: - **子序列**:由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,$[3,6,2,7]$ 是数组 $[0,3,1,6,2,2,7]$ 的子序列。 - $1 \le nums.length \le 2500$。 - $-10^4 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4。 ``` - 示例 2: ```python 输入:nums = [0,1,0,3,2,3] 输出:4 ``` #### 3.1.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 以子序列的结尾位置 $i$ 作为阶段。 ###### 2. 定义状态 设 $dp[i]$ 表示以 $nums[i]$ 结尾的最长递增子序列的长度。 ###### 3. 状态转移方程 对于每个位置 $i$,我们需要考虑所有在 $i$ 之前的元素 $j$($0 \le j < i$)。 - 如果 $nums[j] < nums[i]$,说明 $nums[i]$ 可以接在以 $nums[j]$ 结尾的递增子序列后面,从而形成更长的递增子序列。此时,更新 $dp[i]$ 为 $dp[i] = \max(dp[i], dp[j] + 1)$。 - 如果 $nums[j] \ge nums[i]$,则 $nums[i]$ 不能接在 $nums[j]$ 后面,无需更新。 因此,状态转移方程为:$dp[i] = \max(dp[i], dp[j] + 1)$,其中 $0 \le j < i$ 且 $nums[j] < nums[i]$。 ###### 4. 初始条件 每个元素自身都可以作为长度为 $1$ 的递增子序列,即 $dp[i] = 1$。 ###### 5. 最终结果 最终答案为 $dp$ 数组中的最大值,即 $\max(dp)$,表示整个数组的最长递增子序列长度。 ##### 思路 1:动态规划代码 ```python class Solution: def lengthOfLIS(self, nums: List[int]) -> int: size = len(nums) dp = [1 for _ in range(size)] for i in range(size): for j in range(i): if nums[i] > nums[j]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。外层和内层循环各遍历一次数组,整体为 $O(n^2)$,最后取最大值为 $O(n)$,因此总时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n)$。仅需一个长度为 $n$ 的一维数组存储状态,故空间复杂度为 $O(n)$。 ##### 思路 2:进阶的线性 DP + 二分查找 ###### 1. 算法思想 使用 **数组作为可实时更新内部的单调栈**,结合 **贪心法** 和 **二分查找** 来优化时间复杂度。 核心思想: - $tails[k]$ 表示长度为 $k+1$ 的 LIS 的最小末尾元素(无闲置索引,动态扩展) - 对于每个元素 $x$,使用二分查找在 $tails$ 中找到第一个 $\ge x$ 的位置 - 如果 $x$ 比所有元素都大,则追加到 $tails$ 末尾,形成新的 LIS 长度 - 否则,用较小的 $x$ 更新 $tails[k]$,优化同长度 LIS 的末尾元素(变得更小) ###### 2. 算法步骤 1. 初始化 $tails$ 为空数组。 2. 遍历数组 $nums$ 中的每个元素 $x$: - 使用二分查找在 $tails$ 中找到第一个 $\ge x$ 的位置 $k$。 - 如果 $k == len(tails)$,说明 $x$ 比所有元素都大,追加到 $tails$ 末尾。 - 否则,用 $x$ 更新 $tails[k]$,优化同长度 LIS 的末尾元素。 3. 返回 $tails$ 的长度,即为最终 LIS 的长度。 ##### 思路 2:进阶的线性 DP + 二分查找代码 ```python import bisect class Solution: def lengthOfLIS(self, nums: List[int]) -> int: if len(nums) == 1: return 1 # tails[k] 表示长度为 k+1 的 LIS 的最小末尾元素(无闲置索引,动态扩展) tails = list() for x in nums: # 二分查找:在tails中找首个≥x的位置 k = bisect.bisect_left(tails, x) # 如果 x 比所有元素都大,k 的结果会是当前数组长度(末尾位置索引 +1) # x 直接新增在 tails 数组末尾,形成新问题(LIS 长度 +1) if k == len(tails): tails.append(x) # 用较小的 x 更新较大的 tails[k],优化同长度 LIS 问题的末尾元素(变得更小) else: tails[k] = x # tails 的长度即为最终 LIS 问题的长度 return len(tails) ``` ##### 思路 2:复杂度分析 - **时间复杂度**:$O(n \log n)$。遍历数组的时间复杂度是 $O(n)$,每个元素进行二分查找的时间复杂度是 $O(\log n)$,所以总体时间复杂度为 $O(n \log n)$。 - **空间复杂度**:$O(n)$。$tails$ 数组最多存储 $n$ 个元素,所以总体空间复杂度为 $O(n)$。 ### 3.2 经典例题:最大子数组和 在线性 DP 问题中,除了关注子序列相关的线性 DP,还常常遇到子数组相关的线性 DP 问题。 > **注意区分**: > > - **子序列**:从原数组中按顺序选取若干元素(可以不连续),只要不改变元素的相对顺序即可。 > - **子数组**:原数组中一段连续的元素组成的序列。 > > 两者都是原数组的部分内容,且都保持元素的原有顺序。区别在于,子数组要求元素连续,而子序列则不要求连续。 #### 3.2.1 题目链接 - [53. 最大子数组和 - 力扣](https://leetcode.cn/problems/maximum-subarray/) #### 3.2.2 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 **说明**: - **子数组**:指的是数组中的一个连续部分。 - $1 \le nums.length \le 10^5$。 - $-10^4 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6。 ``` - 示例 2: ```python 输入:nums = [1] 输出:1 ``` #### 3.2.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 以连续子数组的「结尾位置」为阶段。即每一阶段对应以第 $i$ 个元素结尾的子数组。 ###### 2. 定义状态 设 $dp[i]$ 表示以第 $i$ 个元素结尾的连续子数组的最大和。 ###### 3. 状态转移方程 对于第 $i$ 个元素,考虑两种情况: - 如果 $dp[i - 1] < 0$,说明以 $i - 1$ 结尾的子数组对当前元素有负贡献,此时不如从当前元素重新开始,即 $dp[i] = nums[i]$。 - 如果 $dp[i - 1] \ge 0$,则以 $i - 1$ 结尾的子数组对当前元素有正贡献,可以将其累加,即 $dp[i] = dp[i-1] + nums[i]$。 因此,状态转移方程为: $$ dp[i] = \begin{cases} nums[i], & dp[i - 1] < 0 \\ dp[i-1] + nums[i], & dp[i-1] \ge 0 \end{cases} $$ ###### 4. 初始条件 以第 $0$ 个元素结尾的最大和为 $nums[0]$,即 $dp[0] = nums[0]$。 ###### 5. 最终结果 最终答案为所有 $dp[i]$ 中的最大值,即 $max(dp)$。 ##### 思路 1:代码 ```python class Solution: def maxSubArray(self, nums: List[int]) -> int: size = len(nums) dp = [0 for _ in range(size)] dp[0] = nums[0] for i in range(1, size): if dp[i - 1] < 0: dp[i] = nums[i] else: dp[i] = dp[i - 1] + nums[i] return max(dp) ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的元素个数。 - **空间复杂度**:$O(n)$。 ##### 思路 2:动态规划 + 滚动优化 由于 $dp[i]$ 仅依赖于 $dp[i - 1]$ 和当前元素 $nums[i]$,因此可以用一个变量 $subMax$ 表示以第 $i$ 个元素结尾的连续子数组的最大和,同时用 $ansMax$ 记录全局的最大子数组和,从而实现空间优化。 ##### 思路 2:代码 ```python class Solution: def maxSubArray(self, nums: List[int]) -> int: size = len(nums) subMax = nums[0] ansMax = nums[0] for i in range(1, size): if subMax < 0: subMax = nums[i] else: subMax += nums[i] ansMax = max(ansMax, subMax) return ansMax ``` ##### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的元素个数。 - **空间复杂度**:$O(1)$。 ### 3.3 经典例题:最长的斐波那契子序列的长度 在某些单串线性 DP 问题中,单独用一个结束位置来定义状态无法完整刻画问题,此时需要同时考虑两个结束位置,将状态定义为以这两个位置结尾,从而引入额外的维度。 #### 3.3.1 题目链接 - [873. 最长的斐波那契子序列的长度 - 力扣](https://leetcode.cn/problems/length-of-longest-fibonacci-subsequence/) #### 3.3.2 题目大意 **描述**:给定一个严格递增的正整数数组 $arr$。 **要求**:从数组 $arr$ 中找出最长的斐波那契式的子序列的长度。如果不存斐波那契式的子序列,则返回 0。 **说明**: - **斐波那契式序列**:如果序列 $X_1, X_2, ..., X_n$ 满足: - $n \ge 3$; - 对于所有 $i + 2 \le n$,都有 $X_i + X_{i+1} = X_{i+2}$。 则称该序列为斐波那契式序列。 - **斐波那契式子序列**:从序列 $A$ 中挑选若干元素组成子序列,并且子序列满足斐波那契式序列,则称该序列为斐波那契式子序列。例如:$A = [3, 4, 5, 6, 7, 8]$。则 $[3, 5, 8]$ 是 $A$ 的一个斐波那契式子序列。 - $3 \le arr.length \le 1000$。 - $1 \le arr[i] < arr[i + 1] \le 10^9$。 **示例**: - 示例 1: ```python 输入: arr = [1,2,3,4,5,6,7,8] 输出: 5 解释: 最长的斐波那契式子序列为 [1,2,3,5,8]。 ``` - 示例 2: ```python 输入: arr = [1,3,7,11,12,14,18] 输出: 3 解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18]。 ``` #### 3.3.3 解题思路 ##### 思路 1: 暴力枚举(超时) 假设 $arr[i]$、$arr[j]$、$arr[k]$ 是数组 $arr$ 中的三个元素,且满足 $arr[i] + arr[j] = arr[k]$,那么 $arr[i]$、$arr[j]$、$arr[k]$ 就组成了一个斐波那契式子序列。 已知 $arr[i]$ 和 $arr[j]$ 后,下一个斐波那契式子序列的元素应为 $arr[i] + arr[j]$。 由于 $arr$ 是严格递增的,我们可以在确定 $arr[i]$ 和 $arr[j]$ 后,从 $j + 1$ 开始,依次查找是否存在值为 $arr[i] + arr[j]$ 的元素。如果找到了,就继续向后查找下一个元素(即前两个元素分别为 $arr[j]$ 和 $arr[k]$,下一个应为 $arr[j] + arr[k]$),以此类推,直到无法继续为止。 简而言之,每次固定一对 $arr[i]$ 和 $arr[j]$,就可以从这对出发,尽可能延长斐波那契式子序列,并记录其长度。遍历所有可能的 $arr[i]$ 和 $arr[j]$ 组合,统计所有斐波那契式子序列的长度,最终取最大值作为答案。 ##### 思路 1:代码 ```python class Solution: def lenLongestFibSubseq(self, arr: List[int]) -> int: size = len(arr) ans = 0 for i in range(size): for j in range(i + 1, size): temp_ans = 0 temp_i = i temp_j = j k = j + 1 while k < size: if arr[temp_i] + arr[temp_j] == arr[k]: temp_ans += 1 temp_i = temp_j temp_j = k k += 1 if temp_ans > ans: ans = temp_ans if ans > 0: return ans + 2 else: return ans ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 为数组 $arr$ 的元素个数。 - **空间复杂度**:$O(1)$。 ##### 思路 2:哈希表 对于每一对 $arr[i]$ 和 $arr[j]$,我们需要判断 $arr[i] + arr[j]$ 是否存在于数组 $arr$ 中。为此,可以提前构建一个哈希表,将 $arr$ 中的每个元素值映射到其下标(即 $value : idx$)。这样,在查找 $arr[i] + arr[j]$ 是否存在时,只需 $O(1)$ 的时间即可定位到对应的 $arr[k]$,无需像暴力做法那样进行线性遍历,大大提升了查找效率。 ##### 思路 2:代码 ```python class Solution: def lenLongestFibSubseq(self, arr: List[int]) -> int: size = len(arr) ans = 0 idx_map = dict() for idx, value in enumerate(arr): idx_map[value] = idx for i in range(size): for j in range(i + 1, size): temp_ans = 0 temp_i = i temp_j = j while arr[temp_i] + arr[temp_j] in idx_map: temp_ans += 1 k = idx_map[arr[temp_i] + arr[temp_j]] temp_i = temp_j temp_j = k if temp_ans > ans: ans = temp_ans if ans > 0: return ans + 2 else: return ans ``` ##### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为数组 $arr$ 的元素个数。 - **空间复杂度**:$O(n)$。 ##### 思路 3:动态规划 + 哈希表 ###### 1. 阶段划分 以斐波那契子序列的相邻两项结尾下标 $(i, j)$ 作为阶段。 ###### 2. 定义状态 设 $dp[i][j]$ 表示以 $arr[i]$、$arr[j]$ 结尾的斐波那契子序列的最大长度。 ###### 3. 状态转移方程 如果存在 $k$ 使得 $arr[i] + arr[j] = arr[k]$,则可以在以 $arr[i]$、$arr[j]$ 结尾的基础上接上 $arr[k]$,此时有 $dp[j][k] = dp[i][j] + 1$。因此,状态转移方程为:$dp[j][k] = \max(dp[j][k], dp[i][j] + 1)$,其中 $i < j < k$ 且 $arr[i] + arr[j] = arr[k]$。 ###### 4. 初始条件 任意两项都可以组成长度为 $2$ 的斐波那契子序列,即 $dp[i][j] = 2$。 ###### 5. 最终结果 遍历所有 $dp[i][j]$,取最大值 $ans$ 作为答案。由于题目要求子序列长度至少为 $3$,如果 $ans \ge 3$,返回 $ans$,否则返回 $0$。 > **补充说明**:状态转移时应结合哈希表优化,快速判断 $arr[i] + arr[j]$ 是否存在于数组中,以提升效率。 ##### 思路 3:代码 ```python class Solution: def lenLongestFibSubseq(self, arr: List[int]) -> int: size = len(arr) dp = [[0 for _ in range(size)] for _ in range(size)] ans = 0 # 初始化 dp for i in range(size): for j in range(i + 1, size): dp[i][j] = 2 idx_map = {} # 将 value : idx 映射为哈希表,这样可以快速通过 value 获取到 idx for idx, value in enumerate(arr): idx_map[value] = idx for i in range(size): for j in range(i + 1, size): if arr[i] + arr[j] in idx_map: # 获取 arr[i] + arr[j] 的 idx,即斐波那契式子序列下一项元素 k = idx_map[arr[i] + arr[j]] dp[j][k] = max(dp[j][k], dp[i][j] + 1) ans = max(ans, dp[j][k]) if ans >= 3: return ans return 0 ``` ##### 思路 3:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为数组 $arr$ 的元素个数。 - **空间复杂度**:$O(n)$。 ## 练习题目 - [0300. 最长递增子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-subsequence.md) - [0053. 最大子数组和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximum-subarray.md) - [0198. 打家劫舍](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/house-robber.md) - [0213. 打家劫舍 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/house-robber-ii.md) - [0873. 最长的斐波那契子序列的长度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/length-of-longest-fibonacci-subsequence.md) - [单串线性 DP 问题题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8D%95%E4%B8%B2%E7%BA%BF%E6%80%A7-dp-%E9%97%AE%E9%A2%98) ## 参考资料 - 【书籍】算法竞赛进阶指南 - 【文章】[动态规划概念和基础线性DP | 潮汐朝夕](https://chengzhaoxi.xyz/1a4a2483.html) - 【题解】[进阶的线性DP + 二分查找 | 来自评论区](https://github.com/xiaos2021) ================================================ FILE: docs/08_dynamic_programming/08_04_linear_dp_02.md ================================================ ## 1. 双串线性 DP 问题简介 > **双串线性 DP 问题**:指输入为两个数组或两个字符串的线性动态规划问题。常见的状态定义为 $dp[i][j]$,其含义主要有以下三种方式: > > 1. 以 $nums1[i]$ 结尾的子数组($nums1[0]$ 到 $nums1[i]$,$1 \leq i \leq n$)与以 $nums2[j]$ 结尾的子数组($nums2[0]$ 到 $nums2[j]$,$1 \leq j \leq m$)之间的相关解。 > 2. 以 $nums1[i - 1]$ 结尾的子数组($nums1[0]$ 到 $nums1[i - 1]$,$1 \leq i \leq n$)与以 $nums2[j - 1]$ 结尾的子数组($nums2[0]$ 到 $nums2[j - 1]$,$1 \leq j \leq m$)之间的相关解。 > 3. 由前 $i$ 个元素组成的 $nums1$ 子数组($nums1[0]$ 到 $nums1[i - 1]$,$1 \leq i \leq n$)与前 $j$ 个元素组成的 $nums2$ 子数组($nums2[0]$ 到 $nums2[j - 1]$,$1 \leq j \leq m$)之间的相关解。 这三种状态定义的主要区别在于下标的取值范围,以及是否包含当前元素 $nums1[i]$ 或 $nums2[j]$。 - 第 1 种:子数组长度为 $i + 1$ 或 $j + 1$,不允许为空。 - 第 2、3 种:子数组长度为 $i$ 或 $j$,允许为空(当 $i = 0$ 或 $j = 0$ 时表示空数组),便于初始化和边界处理。 ## 2. 双串线性 DP 问题经典题目 ### 2.1 经典例题:最长公共子序列 双串线性 DP 问题中最经典的问题就是「最长公共子序列(Longest Common Subsequence,简称 LCS)」。 #### 2.1.1 题目链接 - [1143. 最长公共子序列 - 力扣](https://leetcode.cn/problems/longest-common-subsequence/) #### 2.1.2 题目大意 **描述**:给定两个字符串 $text1$ 和 $text2$。 **要求**:返回两个字符串的最长公共子序列的长度。如果不存在公共子序列,则返回 $0$。 **说明**: - **子序列**:原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 - **公共子序列**:两个字符串所共同拥有的子序列。 - $1 \le text1.length, text2.length \le 1000$。 - $text1$ 和 $text2$ 仅由小写英文字符组成。 **示例**: - 示例 1: ```python 输入:text1 = "abcde", text2 = "ace" 输出:3 解释:最长公共子序列是 "ace",它的长度为 3。 ``` - 示例 2: ```python 输入:text1 = "abc", text2 = "abc" 输出:3 解释:最长公共子序列是 "abc",它的长度为 3。 ``` #### 2.1.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 以两个字符串的结尾下标作为阶段。 ###### 2. 状态定义 令 $dp[i][j]$ 表示 $text1$ 的前 $i$ 个字符与 $text2$ 的前 $j$ 个字符的最长公共子序列长度。 ###### 3. 状态转移方程 遍历 $text1$ 和 $text2$ 的所有前缀,状态转移分为两种情况: - 如果 $text1[i - 1] == text2[j - 1]$,说明当前字符相同,则最长公共子序列长度在 $dp[i - 1][j - 1]$ 基础上加 $1$,即 $dp[i][j] = dp[i - 1][j - 1] + 1$。 - 如果 $text1[i - 1] \ne text2[j-1]$,则当前字符不同,$dp[i][j]$ 取 $dp[i - 1][j]$ 和 $dp[i][j - 1]$ 的较大值,即 $dp[i][j] = \max(dp[i - 1][j], dp[i][j - 1])$。 ###### 4. 初始条件 - $dp[0][j] = 0$,即 $text1$ 为空时,与 $text2$ 任意前缀的最长公共子序列长度为 $0$。 - $dp[i][0] = 0$,即 $text2$ 为空时,与 $text1$ 任意前缀的最长公共子序列长度为 $0$。 ###### 5. 结果表示 最终答案为 $dp[size1][size2]$,即 $text1$ 与 $text2$ 的最长公共子序列长度,其中 $size1$、$size2$ 分别为 $text1$、$text2$ 的长度。 ##### 思路 1:代码 ```python class Solution: def longestCommonSubsequence(self, text1: str, text2: str) -> int: size1 = len(text1) size2 = len(text2) dp = [[0 for _ in range(size2 + 1)] for _ in range(size1 + 1)] for i in range(1, size1 + 1): for j in range(1, size2 + 1): if text1[i - 1] == text2[j - 1]: dp[i][j] = dp[i - 1][j - 1] + 1 else: dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) return dp[size1][size2] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$、$m$ 分别是字符串 $text1$、$text2$ 的长度。两重循环遍历的时间复杂度是 $O(n \times m)$,所以总的时间复杂度为 $O(n \times m)$。 - **空间复杂度**:$O(n \times m)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n \times m)$。 ### 2.2 经典例题:最长重复子数组 本节介绍与 LCS(最长公共子序列)类似的「最长重复子数组」问题。两者主要区别在于: - LCS 求最长公共「子序列」,可不连续; - 最长重复子数组要求最长公共「子数组」,必须连续。 两者的状态定义和转移类似,但最长重复子数组只有当前元素相等时才能从左上角转移,否则状态归零。 #### 2.2.1 题目链接 - [718. 最长重复子数组 - 力扣](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) #### 2.2.2 题目大意 **描述**:给定两个整数数组 $nums1$、$nums2$。 **要求**:计算两个数组中公共的、长度最长的子数组长度。 **说明**: - $1 \le nums1.length, nums2.length \le 1000$。 - $0 \le nums1[i], nums2[i] \le 100$。 **示例**: - 示例 1: ```python 输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7] 输出:3 解释:长度最长的公共子数组是 [3,2,1] 。 ``` - 示例 2: ```python 输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0] 输出:5 ``` #### 2.2.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 以两个数组的结尾位置 $(i, j)$ 作为阶段。 ###### 2. 定义状态 设 $dp[i][j]$ 表示以 $nums1$ 的第 $i - 1$ 个元素和 $nums2$ 的第 $j - 1$ 个元素结尾的最长公共子数组长度。 ###### 3. 状态转移方程 - 如果 $nums1[i - 1] == nums2[j - 1]$,则当前元素可以作为公共子数组的延续:$dp[i][j] = dp[i - 1][j - 1] + 1$。 - 如果 $nums1[i - 1] \ne nums2[j - 1]$,则无法延续公共子数组:$dp[i][j] = 0$。 ###### 4. 初始条件 - $dp[0][j] = 0$,表示 $nums1$ 为空时,公共子数组长度为 0; - $dp[i][0] = 0$,表示 $nums2$ 为空时,公共子数组长度为 0。 ###### 5. 最终结果 - 在遍历过程中,用 $res$ 记录所有 $dp[i][j]$ 的最大值,最终 $res$ 即为所求答案。 ##### 思路 1:代码 ```python class Solution: def findLength(self, nums1: List[int], nums2: List[int]) -> int: size1 = len(nums1) size2 = len(nums2) dp = [[0 for _ in range(size2 + 1)] for _ in range(size1 + 1)] res = 0 for i in range(1, size1 + 1): for j in range(1, size2 + 1): if nums1[i - 1] == nums2[j - 1]: dp[i][j] = dp[i - 1][j - 1] + 1 if dp[i][j] > res: res = dp[i][j] return res ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$。其中 $n$ 是数组 $nums1$ 的长度,$m$ 是数组 $nums2$ 的长度。 - **空间复杂度**:$O(n \times m)$。 ### 2.3 经典例题:编辑距离 双串线性 DP 问题中除了经典的最长公共子序列问题之外,还包括字符串的模糊匹配问题。 #### 2.3.1 题目链接 - [72. 编辑距离 - 力扣](https://leetcode.cn/problems/edit-distance/) #### 2.3.2 题目大意 **描述**:给定两个单词 $word1$、$word2$。 对一个单词可以进行以下三种操作: - 插入一个字符 - 删除一个字符 - 替换一个字符 **要求**:计算出将 $word1$ 转换为 $word2$ 所使用的最少操作数。 **说明**: - $0 \le word1.length, word2.length \le 500$。 - $word1$ 和 $word2$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:word1 = "horse", word2 = "ros" 输出:3 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e') ``` - 示例 2: ```python 输入:word1 = "intention", word2 = "execution" 输出:5 解释: intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u') ``` #### 2.3.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 以两个字符串的结尾位置作为阶段进行划分。 ###### 2. 定义状态 设 $dp[i][j]$ 表示将 $word1$ 的前 $i$ 个字符(记为 $str1$)转换为 $word2$ 的前 $j$ 个字符(记为 $str2$)所需的最少操作次数。 ###### 3. 状态转移方程 - 如果当前字符相同($word1[i - 1] == word2[j - 1]$),则无需操作:$dp[i][j] = dp[i - 1][j - 1]$。 - 如果当前字符不同($word1[i - 1] \ne word2[j - 1]$),则有三种操作可选,取最小值: 1. 替换:$dp[i - 1][j - 1] + 1$ 2. 插入:$dp[i][j - 1] + 1$ 3. 删除:$dp[i - 1][j] + 1$ 因此,状态转移方程为: $$ dp[i][j] = \begin{cases} dp[i - 1][j - 1] & \text{如果 } word1[i - 1] == word2[j - 1] \\ \min \left( \begin{array}{l} dp[i - 1][j - 1], \\ dp[i][j - 1], \\ dp[i - 1][j] \\ \end{array} \right) + 1 & \text{否则} \end{cases} $$ ###### 4. 初始条件 - $dp[0][j] = j$:将空串变为 $word2$ 的前 $j$ 个字符,需要插入 $j$ 次。 - $dp[i][0] = i$:将 $word1$ 的前 $i$ 个字符变为空串,需要删除 $i$ 次。 ###### 5. 最终结果 最终答案为 $dp[size1][size2]$,即将 $word1$ 转换为 $word2$ 所需的最少操作次数,其中 $size1$ 和 $size2$ 分别为 $word1$ 和 $word2$ 的长度。 ##### 思路 1:代码 ```python class Solution: def minDistance(self, word1: str, word2: str) -> int: size1 = len(word1) size2 = len(word2) dp = [[0 for _ in range(size2 + 1)] for _ in range(size1 + 1)] for i in range(size1 + 1): dp[i][0] = i for j in range(size2 + 1): dp[0][j] = j for i in range(1, size1 + 1): for j in range(1, size2 + 1): if word1[i - 1] == word2[j - 1]: dp[i][j] = dp[i - 1][j - 1] else: dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1 return dp[size1][size2] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$、$m$ 分别是字符串 $word1$、$word2$ 的长度。两重循环遍历的时间复杂度是 $O(n \times m)$,所以总的时间复杂度为 $O(n \times m)$。 - **空间复杂度**:$O(n \times m)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n \times m)$。 ## 练习题目 - [1143. 最长公共子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/longest-common-subsequence.md) - [双串线性 DP 问题题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8F%8C%E4%B8%B2%E7%BA%BF%E6%80%A7-dp-%E9%97%AE%E9%A2%98) ================================================ FILE: docs/08_dynamic_programming/08_05_linear_dp_03.md ================================================ ## 1. 矩阵线性 DP 问题简介 > **矩阵线性 DP 问题**:是指输入为二维矩阵的动态规划问题。常见的状态定义为 $dp[i][j]$,表示从起点「$(0, 0)$」到达「$(i, j)$」的最优解(如最小路径和、最大路径和等)。 ## 2. 矩阵线性 DP 问题经典题目 ### 2.1 经典例题:最小路径和 #### 2.1.1 题目链接 - [64. 最小路径和 - 力扣](https://leetcode.cn/problems/minimum-path-sum/) #### 2.1.2 题目大意 **描述**:给定一个包含非负整数的 $m \times n$ 大小的网格 $grid$。 **要求**:找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 **说明**: - 每次只能向下或者向右移动一步。 - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 200$。 - $0 \le grid[i][j] \le 100$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/05/minpath.jpg) ```python 输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径 1→3→1→1→1 的总和最小。 ``` - 示例 2: ```python 输入:grid = [[1,2,3],[4,5,6]] 输出:12 ``` #### 2.1.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 以路径终点的位置(即二维坐标 $(i, j)$)作为阶段。 ###### 2. 定义状态 设 $dp[i][j]$ 表示从左上角 $(0, 0)$ 出发,到达 $(i, j)$ 的最小路径和。 ###### 3. 状态转移方程 当前位置 $(i, j)$ 只能从左边 $(i, j - 1)$ 或上方 $(i - 1, j)$ 转移过来。为了保证路径和最小,应选择这两者中较小的路径和,再加上当前格子的值 $grid[i][j]$。 则状态转移方程为:$dp[i][j] = \min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]$。 ###### 4. 初始条件 - 当 $i = 0, j = 0$ 时,$dp[0][0] = grid[0][0]$。 - 当 $i > 0, j = 0$ 时,只能从上方到达,$dp[i][0] = dp[i - 1][0] + grid[i][0]$。 - 当 $i = 0, j > 0$ 时,只能从左侧到达,$dp[0][j] = dp[0][j - 1] + grid[0][j]$。 ###### 5. 最终结果 最终答案为 $dp[rows - 1][cols - 1]$,即从左上角到右下角的最小路径和。其中 $rows$ 和 $cols$ 分别为 $grid$ 的行数和列数。 ##### 思路 1:代码 ```python class Solution: def minPathSum(self, grid: List[List[int]]) -> int: rows, cols = len(grid), len(grid[0]) dp = [[0 for _ in range(cols)] for _ in range(rows)] dp[0][0] = grid[0][0] for i in range(1, rows): dp[i][0] = dp[i - 1][0] + grid[i][0] for j in range(1, cols): dp[0][j] = dp[0][j - 1] + grid[0][j] for i in range(1, rows): for j in range(1, cols): dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j] return dp[rows - 1][cols - 1] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(m * n)$,其中 $m$、$n$ 分别为 $grid$ 的行数和列数。 - **空间复杂度**:$O(m * n)$。 ### 2.2 经典例题:最大正方形 #### 2.2.1 题目链接 - [221. 最大正方形 - 力扣](https://leetcode.cn/problems/maximal-square/) #### 2.2.2 题目大意 **描述**:给定一个由 `'0'` 和 `'1'` 组成的二维矩阵 $matrix$。 **要求**:找到只包含 `'1'` 的最大正方形,并返回其面积。 **说明**: - $m == matrix.length$。 - $n == matrix[i].length$。 - $1 \le m, n \le 300$。 - $matrix[i][j]$ 为 `'0'` 或 `'1'`。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/26/max1grid.jpg) ```python 输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]] 输出:4 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/11/26/max2grid.jpg) ```python 输入:matrix = [["0","1"],["1","0"]] 输出:1 ``` #### 2.2.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 以正方形的右下角坐标 $(i, j)$ 作为阶段。 ###### 2. 定义状态 令 $dp[i][j]$ 表示以 $(i, j)$ 为右下角、且全部为 $1$ 的最大正方形的边长。 ###### 3. 状态转移方程 仅当 $matrix[i][j] == 1$ 时,$(i, j)$ 位置才能作为正方形的右下角: - 如果 $matrix[i][j] == 0$,则 $dp[i][j] = 0$; - 如果 $matrix[i][j] == 1$,则 $dp[i][j] = \min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1$,即取其上方、左方、左上方的最小值加 $1$。 ###### 4. 初始条件 初始时,令所有 $dp[i][j] = 0$,即默认以 $(i, j)$ 为右下角的最大正方形边长为 $0$。 ###### 5. 最终结果 遍历所有 $dp[i][j]$,取最大值即为只包含 $1$ 的最大正方形的边长,面积为其平方。 ##### 思路 1:代码 ```python class Solution: def maximalSquare(self, matrix: List[List[str]]) -> int: rows, cols = len(matrix), len(matrix[0]) max_size = 0 dp = [[0 for _ in range(cols + 1)] for _ in range(rows + 1)] for i in range(rows): for j in range(cols): if matrix[i][j] == '1': if i == 0 or j == 0: dp[i][j] = 1 else: dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1 max_size = max(max_size, dp[i][j]) return max_size * max_size ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$、$n$ 分别为二维矩阵 $matrix$ 的行数和列数。 - **空间复杂度**:$O(m \times n)$。 ## 3. 无串线性 DP 问题简介 > **无串线性 DP 问题**:问题的输入不是显式的数组或字符串,但依然可分解为若干子问题的线性 DP 问题。 ## 4. 无串线性 DP 问题经典题目 ### 4.1 经典例题:整数拆分 #### 4.1.1 题目链接 - [343. 整数拆分 - 力扣](https://leetcode.cn/problems/integer-break/) #### 4.1.2 题目大意 **描述**:给定一个正整数 $n$,将其拆分为 $k (k \ge 2)$ 个正整数的和,并使这些整数的乘积最大化。 **要求**:返回可以获得的最大乘积。 **说明**: - $2 \le n \le 58$。 **示例**: - 示例 1: ```python 输入: n = 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。 ``` - 示例 2: ```python 输入: n = 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。 ``` #### 4.1.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 以正整数 $i$ 为阶段,从小到大依次考虑每个 $i$。 ###### 2. 定义状态 设 $dp[i]$ 表示将正整数 $i$ 至少拆分成两个正整数之和后,所得的最大乘积。 ###### 3. 状态转移方程 对于每个 $i \ge 2$,枚举第一个拆分出的正整数 $j$,其中 $1 \le j < i$。此时有两种选择: 1. 将 $i$ 拆分为 $j$ 和 $i - j$,如果 $i - j$ 不再继续拆分,则乘积为 $j \times (i - j)$; 2. 将 $i$ 拆分为 $j$ 和 $i - j$,如果 $i - j$ 继续拆分,则乘积为 $j \times dp[i - j]$; 最终 $dp[i]$ 取上述两种情况的最大值,即: $$ dp[i] = \max_{1 \le j < i} \left\{ \max\left(j \times (i-j),\ j \times dp[i-j]\right) \right\} $$ ###### 4. 初始条件 - $dp[0] = 0, dp[1] = 0$,因为 $0$ 和 $1$ 无法拆分。 ###### 5. 最终结果 最终答案为 $dp[n]$,即将正整数 $n$ 拆分为至少两个正整数之和后所得的最大乘积。 ##### 思路 1:代码 ```python class Solution: def integerBreak(self, n: int) -> int: dp = [0 for _ in range(n + 1)] for i in range(2, n + 1): for j in range(i): dp[i] = max(dp[i], (i - j) * j, dp[i - j] * j) return dp[n] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(n)$。 ### 4.2 经典例题:只有两个键的键盘 #### 4.2.1 题目链接 - [650. 只有两个键的键盘](https://leetcode.cn/problems/2-keys-keyboard/) #### 4.2.2 题目大意 **描述**:最初记事本上只有一个字符 `'A'`。你每次可以对这个记事本进行两种操作: - **Copy All(复制全部)**:复制这个记事本中的所有字符(不允许仅复制部分字符)。 - **Paste(粘贴)**:粘贴上一次复制的字符。 现在,给定一个数字 $n$,需要使用最少的操作次数,在记事本上输出恰好 $n$ 个 `'A'` 。 **要求**:返回能够打印出 $n$ 个 `'A'` 的最少操作次数。 **说明**: - $1 \le n \le 1000$。 **示例**: - 示例 1: ```python 输入:3 输出:3 解释 最初, 只有一个字符 'A'。 第 1 步, 使用 Copy All 操作。 第 2 步, 使用 Paste 操作来获得 'AA'。 第 3 步, 使用 Paste 操作来获得 'AAA'。 ``` - 示例 2: ```python 输入:n = 1 输出:0 ``` #### 4.2.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 以当前记事本上字符 `'A'` 的个数 $i$ 作为阶段。 ###### 2. 定义状态 设 $dp[i]$ 表示通过若干次「复制全部」和「粘贴」操作,得到恰好 $i$ 个字符 `'A'` 所需的最少操作次数。 ###### 3. 状态转移方程 对于每个 $i$,我们可以枚举 $i$ 的所有因子 $j$($1 \leq j < i$ 且 $i \bmod j = 0$)。如果 $i$ 可以由 $j$ 通过若干次粘贴得到,即 $i = j \times k$,那么可以先通过 $dp[j]$ 步得到 $j$ 个 `'A'`,再执行一次「复制全部」和 $k - 1$ 次「粘贴」,共 $dp[j] + k$ 步。由于 $k = i / j$,因此状态转移为 $dp[i] = \min(dp[i], dp[j] + i / j)$。 同理,$j$ 和 $i/j$ 都是 $i$ 的因子,二者至少有一个不大于 $\sqrt{i}$,因此只需枚举 $j$ 从 $1$ 到 $\sqrt{i}$,并同时考虑 $j$ 和 $i/j$ 两种分解方式: $$ dp[i] = \min\left(dp[i],\ dp[j] + \frac{i}{j},\ dp[\frac{i}{j}] + j\right) $$ ###### 4. 初始条件 - $dp[1] = 0$,即初始只有一个 `'A'` 时不需要任何操作。 ###### 5. 最终结果 最终答案为 $dp[n]$,即得到 $n$ 个字符 `'A'` 所需的最少操作次数。 ##### 思路 1:动态规划代码 ```python import math class Solution: def minSteps(self, n: int) -> int: dp = [0 for _ in range(n + 1)] for i in range(2, n + 1): dp[i] = float('inf') for j in range(1, int(math.sqrt(n)) + 1): if i % j == 0: dp[i] = min(dp[i], dp[j] + i // j, dp[i // j] + j) return dp[n] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n \sqrt{n})$。外层循环遍历的时间复杂度是 $O(n)$,内层循环遍历的时间复杂度是 $O(\sqrt{n})$,所以总体时间复杂度为 $O(n \sqrt{n})$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 ## 练习题目 - [0718. 最长重复子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/maximum-length-of-repeated-subarray.md) - [0072. 编辑距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/edit-distance.md) - [0064. 最小路径和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/minimum-path-sum.md) - [0221. 最大正方形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/maximal-square.md) - [0343. 整数拆分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/integer-break.md) - [0650. 两个键的键盘](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/2-keys-keyboard.md) - [矩阵线性 DP 问题题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E7%9F%A9%E9%98%B5%E7%BA%BF%E6%80%A7-dp-%E9%97%AE%E9%A2%98) - [无串线性 DP 问题题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%97%A0%E4%B8%B2%E7%BA%BF%E6%80%A7-dp-%E9%97%AE%E9%A2%98) ## 参考资料 - 【书籍】算法竞赛进阶指南 - 【文章】[动态规划概念和基础线性DP | 潮汐朝夕](https://chengzhaoxi.xyz/1a4a2483.html) ================================================ FILE: docs/08_dynamic_programming/08_06_knapsack_problem_01.md ================================================ ## 1. 背包问题简介 ### 1.1 背包问题的定义 > **背包问题**:背包问题是线性 DP 问题中一类经典模型。其基本描述为:给定若干物品,每种物品有各自的重量、价值和数量限制,以及一个最大承重为 $W$ 的背包。要求在不超过背包承重上限的前提下,选择若干物品放入背包,使得背包内物品的总价值最大。 ![背包问题](https://qcdn.itcharge.cn/images/20240514111553.png) 根据物品数量和选择方式的不同,背包问题主要分为:0-1 背包问题、完全背包问题、多重背包问题、分组背包问题和混合背包问题等类型。 ### 1.2 背包问题的暴力解法思路 背包问题的暴力解法非常直接。假设有 $n$ 件物品,我们可以枚举所有 $n$ 件物品的选取方案(每件物品选或不选),即遍历所有 $2^n$ 种组合。对于每一种组合,判断其总重量是否不超过背包容量,并计算其总价值,最终取所有可行方案中的最大价值。该方法的时间复杂度为 $O(2^n)$,属于指数级,效率较低。 由于暴力解法效率低下,实际应用中我们通常采用动态规划方法来大幅降低时间复杂度。 接下来将详细介绍如何利用动态规划高效地解决各类背包问题。 ## 2. 0-1 背包问题简介 > **0-1 背包问题**:给定 $n$ 件物品和一个最大承重为 $W$ 的背包。每件物品的重量为 $weight[i]$,价值为 $value[i]$,每种物品只能选择一次。请问在不超过背包承重的前提下,最多能获得多少总价值? ![0-1 背包问题](https://qcdn.itcharge.cn/images/20240514111617.png) ## 3. 0-1 背包问题的基本思路 > **0-1 背包问题的核心**:每种物品只能选一次,可以选择放或不放。 #### 思路 1:动态规划(二维数组) ###### 1. 阶段划分 以物品序号和当前背包剩余容量为阶段。 ###### 2. 定义状态 令 $dp[i][w]$ 表示前 $i$ 件物品,放入容量不超过 $w$ 的背包时可获得的最大价值。 其中 $i$ 表示考虑前 $i$ 件物品,$w$ 表示当前背包容量。 ###### 3. 状态转移方程 对于「将前 $i$ 件物品放入容量为 $w$ 的背包,能获得的最大价值」这个子问题,我们只需关注第 $i - 1$ 件物品的选择情况(即放或不放),即可将问题递归地转化为只与前 $i - 1$ 件物品相关的子问题: 1. **不放第 $i - 1$ 件物品**:此时最大价值为 $dp[i - 1][w]$,即前 $i - 1$ 件物品放入容量为 $w$ 的背包的最大价值。 2. **放第 $i - 1$ 件物品**:前提是背包剩余容量足够($w \ge weight[i - 1]$),此时最大价值为 $dp[i-1][w - weight[i - 1]] + value[i - 1]$,即前 $i - 1$ 件物品放入剩余容量为 $w - weight[i - 1]$ 的背包的最大价值,再加上第 $i - 1$ 件物品的价值。 因此,状态转移分两种情况: - 当 $w < weight[i - 1]$ 时,第 $i - 1$ 件物品无法放入背包,$dp[i][w] = dp[i - 1][w]$。 - 当 $w \ge weight[i - 1]$ 时,第 $i - 1$ 件物品可选可不选,$dp[i][w] = \max\{dp[i - 1][w],\ dp[i - 1][w - weight[i - 1]] + value[i - 1]\}$。 综上,状态转移方程为: $dp[i][w] = \begin{cases} dp[i - 1][w] & w < weight[i - 1] \\ \max\{dp[i - 1][w],\ dp[i-1][w - weight[i - 1]] + value[i - 1]\} & w \ge weight[i-1] \end{cases}$ ###### 4. 初始条件 - 当背包容量为 $0$ 时,无论有多少物品,最大价值为 $0$,即 $dp[i][0] = 0$,$0 \le i \le size$。 - 当没有物品时,无论背包容量多少,最大价值为 $0$,即 $dp[0][w] = 0$,$0 \le w \le W$。 ###### 5. 最终结果 最终答案为 $dp[size][W]$,即用前 $size$ 件物品,背包容量为 $W$ 时能获得的最大价值。 #### 思路 1:代码 ```python class Solution: # 思路 1:动态规划(二维数组) def zeroOnePackMethod1(self, weight: [int], value: [int], W: int) -> int: """ 0-1 背包问题二维动态规划解法 :param weight: List[int],每件物品的重量 :param value: List[int],每件物品的价值 :param W: int,背包最大承重 :return: int,最大可获得价值 """ size = len(weight) # dp[i][w] 表示前 i 件物品,容量不超过 W 时的最大价值 dp = [[0] * (W + 1) for _ in range(size + 1)] # 遍历每一件物品 for i in range(1, size + 1): # 遍历每一种可能的背包容量 for w in range(W + 1): if w < weight[i - 1]: # 当前物品放不下,继承上一个状态 dp[i][w] = dp[i - 1][w] else: # 当前物品可选,取放与不放的最大值 dp[i][w] = max( dp[i - 1][w], # 不放当前物品 dp[i - 1][w - weight[i - 1]] + value[i - 1] # 放当前物品 ) # 返回前 size 件物品、容量为 W 时的最大价值 return dp[size][W] ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times W)$,其中 $n$ 为物品数量,$W$ 为背包的载重上限。 - **空间复杂度**:$O(n \times W)$。 ## 4. 0-1 背包问题的滚动数组优化 通过前面的分析可以发现,在依次处理第 $1 \sim n$ 件物品时,「前 $i$ 件物品的状态」只依赖于「前 $i - 1$ 件物品的状态」,与更早之前的状态无关。 换句话说,状态转移时只涉及当前行(第 $i$ 行)的 $dp[i][w]$ 和上一行(第 $i - 1$ 行)的 $dp[i - 1][w]$、$dp[i - 1][w - weight[i-1]]$。 因此,我们无需保存所有阶段的状态,只需保留当前阶段和上一阶段的状态即可。可以用两个一维数组分别存储相邻两阶段的所有状态:$dp[0][w]$ 存储 $dp[i - 1][w]$,$dp[1][w]$ 存储 $dp[i][w]$。 进一步优化时,其实只需一个一维数组 $dp[w]$,利用「滚动数组」思想,将动态规划的第一维去掉,从而实现空间优化。 #### 思路 2:动态规划 + 滚动数组优化 ###### 1. 阶段划分 以当前背包的剩余容量 $w$ 作为阶段。 ###### 2. 定义状态 令 $dp[w]$ 表示:在背包容量不超过 $w$ 的情况下,能够获得的最大总价值。 ###### 3. 状态转移方程 状态转移如下: $$ dp[w] = \begin{cases} dp[w], & w < weight[i - 1] \\ \max\{dp[w],\ dp[w - weight[i - 1]] + value[i - 1]\}, & w \geq weight[i - 1] \end{cases} $$ 在处理第 $i$ 件物品时,$dp[w]$ 只依赖于上一阶段(即第 $i - 1$ 件物品处理完后)的 $dp[w]$ 和 $dp[w - weight[i - 1]]$。因此,为了避免状态被提前覆盖,必须对 $w$ 采用从大到小(即从 $W$ 到 $0$)的逆序遍历。这样可以确保每次转移用到的 $dp[w - weight[i - 1]]$ 仍然是上一阶段的值。 如果采用从小到大(正序)遍历,则 $dp[w - weight[i - 1]]$ 可能已经被本轮更新,导致状态转移错误。 实际上,当 $w < weight[i-1]$ 时,当前物品无法放入背包,$dp[w]$ 保持不变,无需更新。因此逆序遍历时只需从 $W$ 遍历到 $weight[i - 1]$。 ###### 4. 初始条件 - 对于所有 $0 \leq w \leq W$,$dp[w] = 0$,表示背包容量为 $w$ 时,尚未放入任何物品,最大价值为 $0$。 ###### 5. 最终结果 最终答案为 $dp[W]$,即背包容量为 $W$ 时能够获得的最大总价值。 #### 思路 2:代码 ```python class Solution: # 思路 2:动态规划 + 滚动数组优化 def zeroOnePackMethod2(self, weight: [int], value: [int], W: int) -> int: """ 0-1 背包问题的滚动数组优化解法 :param weight: List[int],每件物品的重量 :param value: List[int],每件物品的价值 :param W: int,背包最大承重 :return: int,背包可获得的最大价值 """ size = len(weight) # dp[w] 表示容量为 w 时背包可获得的最大价值 dp = [0] * (W + 1) # 遍历每一件物品 for i in range(size): # 必须逆序遍历容量,防止状态被提前覆盖 for w in range(W, weight[i] - 1, -1): # 状态转移:不选第 i 件物品 or 选第 i 件物品 # dp[w] = max(不选, 选) dp[w] = max(dp[w], dp[w - weight[i]] + value[i]) # 解释: # dp[w]:不选第 i 件物品,价值不变 # dp[w - weight[i]] + value[i]:选第 i 件物品,容量减少,相应加上价值 return dp[W] ``` #### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times W)$,其中 $n$ 为物品数量,$W$ 为背包的载重上限。 - **空间复杂度**:$O(W)$。 ## 5. 0-1 背包问题的应用 ### 5.1 经典例题:分割等和子集 #### 5.1.1 题目链接 - [416. 分割等和子集 - 力扣](https://leetcode.cn/problems/partition-equal-subset-sum/) #### 5.1.2 题目大意 **描述**:给定一个只包含正整数的非空数组 $nums$。 **要求**:判断是否可以将这个数组分成两个子集,使得两个子集的元素和相等。 **说明**: - $1 \le nums.length \le 200$。 - $1 \le nums[i] \le 100$。 **示例**: - 示例 1: ```python 输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11]。 ``` - 示例 2: ```python 输入:nums = [1,2,3,5] 输出:false 解释:数组不能分割成两个元素和相等的子集。 ``` #### 5.1.3 解题思路 ##### 思路 1:动态规划 本题实质是:能否从数组中选出若干元素,使其和恰好等于整个数组元素和的一半。 这实际上就是典型的「0-1 背包问题」: 1. 设数组元素和为 $sum$,目标为 $target = \frac{sum}{2}$,即背包容量。 2. 数组中的每个元素 $nums[i]$ 视为一件物品,其重量和价值均为 $nums[i]$。 3. 每种物品只能选择一次。 4. 如果能恰好装满容量为 $target$ 的背包,则最大价值也为 $target$。 因此,问题转化为:给定物品数组 $nums$,每件物品重量和价值均为 $nums[i]$,背包容量为 $target$,每件物品最多选一次,问能否恰好装满背包。 ###### 1. 阶段划分 以当前背包容量 $w$ 作为阶段。 ###### 2. 定义状态 令 $dp[w]$ 表示:从 $nums$ 中选取若干元素,恰好装入容量为 $w$ 的背包时,元素和的最大值。 ###### 3. 状态转移方程 $dp[w] = \begin{cases} dp[w] & w < nums[i - 1] \\ \max\{dp[w],\ dp[w - nums[i - 1]] + nums[i - 1]\} & w \geq nums[i - 1] \end{cases}$ ###### 4. 初始条件 - 对所有 $0 \leq w \leq W$,$dp[w] = 0$,即不选任何物品时最大和为 $0$。 ###### 5. 最终结果 只需判断 $dp[target]$ 是否等于 $target$。如果 $dp[target] == target$,说明存在子集和为 $target$,返回 `True`;否则返回 `False`。 ##### 思路 1:代码 ```python class Solution: # 动态规划 + 滚动数组优化的 0-1 背包实现 def zeroOnePackMethod2(self, weight: list[int], value: list[int], W: int) -> int: size = len(weight) dp = [0] * (W + 1) # dp[w] 表示容量为 w 时的最大价值 # 枚举每一件物品 for i in range(size): # 必须逆序遍历容量,防止同一物品被重复选择 for w in range(W, weight[i] - 1, -1): # 状态转移:不选 / 选第 i 件物品 dp[w] = max(dp[w], dp[w - weight[i]] + value[i]) return dp[W] def canPartition(self, nums: list[int]) -> bool: total = sum(nums) # 如果总和为奇数,无法平分 if total % 2 != 0: return False target = total // 2 # 0-1 背包:每个数只能选一次,能否恰好装满 target return self.zeroOnePackMethod2(nums, nums, target) == target ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times target)$,其中 $n$ 为数组 $nums$ 的元素个数,$target$ 是整个数组元素和的一半。 - **空间复杂度**:$O(target)$。 ## 练习题目 - [0416. 分割等和子集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/partition-equal-subset-sum.md) - [0494. 目标和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/target-sum.md) - [1049. 最后一块石头的重量 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/last-stone-weight-ii.md) - [0-1 背包问题题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#0-1-%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E9%A2%98%E7%9B%AE) ## 参考资料 - 【资料】[背包九讲 - 崔添翼](https://github.com/tianyicui/pack) - 【文章】[背包 DP - OI Wiki](https://oi-wiki.org/dp/knapsack/) - 【文章】[背包问题 第四讲 - 宫水三叶的刷题日记](https://juejin.cn/post/7003243733604892685) - 【文章】[Massive Algorithms: 讲透完全背包算法](https://massivealgorithms.blogspot.com/2015/06/unbounded-knapsack-problem.html) ================================================ FILE: docs/08_dynamic_programming/08_07_knapsack_problem_02.md ================================================ ## 1. 完全背包问题简介 > **完全背包问题**:给定 $n$ 种物品和一个最大承重为 $W$ 的背包。每种物品的重量为 $weight[i]$,价值为 $value[i]$,且每种物品的数量不限。请问在不超过背包承重上限的前提下,背包内可获得的最大总价值是多少? ![完全背包问题](https://qcdn.itcharge.cn/images/20240514111640.png) ## 2. 完全背包问题的基本思路 > **完全背包问题的核心特性**:每种物品可以选取任意多次(数量无限)。 完全背包问题与 0-1 背包问题的状态定义和基本思路类似,不同之处在于每种物品可以被多次选择。对于容量为 $w$ 的背包,第 $i - 1$ 种物品最多可以选择 $\left\lfloor \frac{w}{weight[i - 1]} \right\rfloor$ 件。因此,可以在动态规划的基础上增加一层循环,枚举第 $i - 1$ 种物品的选取数量 $k$($0 \leq k \leq \left\lfloor \frac{w}{weight[i - 1]} \right\rfloor$),从而将完全背包问题转化为多重选择的 0-1 背包问题模型。 #### 思路 1:动态规划 + 二维数组基础解法 ###### 1. 阶段划分 以物品种类序号和当前背包剩余容量作为阶段。 ###### 2. 定义状态 设 $dp[i][w]$ 表示前 $i$ 种物品,放入容量不超过 $w$ 的背包时可获得的最大价值。 其中 $i$ 表示考虑前 $i$ 种物品,$w$ 表示当前背包容量。 ###### 3. 状态转移方程 由于每种物品可以选取任意多次,$dp[i][w]$ 可以通过枚举第 $i - 1$ 种物品的选取数量 $k$ 得到: - 选 $0$ 件第 $i - 1$ 种物品:$dp[i - 1][w]$ - 选 $1$ 件第 $i - 1$ 种物品:$dp[i - 1][w - weight[i - 1]] + value[i - 1]$ - 选 $2$ 件第 $i - 1$ 种物品:$dp[i - 1][w - 2 \times weight[i - 1]] + 2 \times value[i - 1]$ - ... - 选 $k$ 件第 $i - 1$ 种物品:$dp[i - 1][w - k \times weight[i - 1]] + k \times value[i - 1]$ 其中 $0 \leq k \leq \left\lfloor \frac{w}{weight[i-1]} \right\rfloor$。 因此,状态转移方程为: $$ dp[i][w] = \max_{0 \leq k \leq \left\lfloor \frac{w}{weight[i-1]} \right\rfloor} \left\{ dp[i-1][w - k \times weight[i-1]] + k \times value[i-1] \right\} $$ ###### 4. 初始条件 - 对所有 $0 \leq i \leq size$,$dp[i][0] = 0$,即背包容量为 $0$ 时最大价值为 $0$。 - 对所有 $0 \leq w \leq W$,$dp[0][w] = 0$,即没有物品时最大价值为 $0$。 ###### 5. 最终结果 最终答案为 $dp[size][W]$,即用前 $size$ 种物品、背包容量为 $W$ 时能获得的最大价值。 #### 思路 1:代码 ```python class Solution: # 思路 1:动态规划 + 二维基本思路 def completePackMethod1(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 枚举第 i - 1 种物品能取个数 for k in range(w // weight[i - 1] + 1): # dp[i][w] 取所有 dp[i - 1][w - k * weight[i - 1] + k * value[i - 1] 中最大值 dp[i][w] = max(dp[i][w], dp[i - 1][w - k * weight[i - 1]] + k * value[i - 1]) return dp[size][W] ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times W \times \sum\frac{W}{weight[i]})$,其中 $n$ 为物品种类数量,$W$ 为背包的载重上限,$weight[i]$ 是第 $i$ 种物品的重量。 - **空间复杂度**:$O(n \times W)$。 ## 3. 完全背包问题的状态转移方程优化 在前面的解法中,对于每种物品,我们都需要枚举所有可能的选取数量 $k$,这导致时间复杂度较高。 实际上,我们可以对状态转移方程进行优化,从而显著降低算法的时间复杂度。 原始的状态转移方程为: $$ dp[i][w] = \max_{0 \leq k \leq \left\lfloor \frac{w}{weight[i-1]} \right\rfloor} \left\{ dp[i-1][w - k \times weight[i-1]] + k \times value[i-1] \right\} $$ 将其展开,可以得到: $$(1)\quad dp[i][w] = \max \begin{cases} dp[i-1][w] \\ dp[i-1][w - weight[i-1]] + value[i-1] \\ dp[i-1][w - 2 \times weight[i-1]] + 2 \times value[i-1] \\ \cdots \\ dp[i-1][w - k \times weight[i-1]] + k \times value[i-1] \end{cases},\quad 0 \leq k \leq \left\lfloor \frac{w}{weight[i-1]} \right\rfloor$$ 再来看 $dp[i][w - weight[i-1]]$ 的展开: $$(2)\quad dp[i][w - weight[i-1]] = \max \begin{cases} dp[i-1][w - weight[i-1]] \\ dp[i-1][w - 2 \times weight[i-1]] + value[i-1] \\ dp[i-1][w - 3 \times weight[i-1]] + 2 \times value[i-1] \\ \cdots \\ dp[i-1][w - k \times weight[i-1]] + (k-1) \times value[i-1] \end{cases}$$ 对比 $(1)$ 和 $(2)$ 可以发现: 1. $(1)$ 有 $k+1$ 项,$(2)$ 有 $k$ 项; 2. $(1)$ 的第 $2$ 到第 $k+1$ 项,与 $(2)$ 的所有项一一对应,且每项多了一个 $value[i-1]$。 因此,将 $(2)$ 式加上 $value[i-1]$,再与 $(1)$ 式合并,可以得到优化后的状态转移方程: $$(3)\quad dp[i][w] = \max \left\{ dp[i-1][w],\ dp[i][w - weight[i-1]] + value[i-1] \right\},\quad w \geq weight[i-1]$$ 这样,原本需要三重循环的解法,优化为只需两重循环即可,大大提升了效率。 > 注意:当 $w < weight[i-1]$ 时,$dp[i][w] = dp[i-1][w]$,即当前物品无法放入背包。 因此,最终的状态转移方程为: $$ dp[i][w] = \begin{cases} dp[i-1][w] & w < weight[i-1] \\ \max\left\{ dp[i-1][w],\ dp[i][w - weight[i-1]] + value[i-1] \right\} & w \geq weight[i-1] \end{cases} $$ 可以看到,这个状态转移方程与 0-1 背包问题的状态转移方程非常相似。 > 唯一的区别在于: > > 1. 0-1 背包问题中,转移用的是 $dp[i-1][w - weight[i-1]] + value[i-1]$,即上一阶段的状态; > 2. 完全背包问题中,转移用的是 $dp[i][w - weight[i-1]] + value[i-1]$,即当前阶段的状态。 #### 思路 2:动态规划 + 状态转移方程优化 ###### 1. 阶段划分 按照物品种类的序号、当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,可以获得的最大价值。 状态 $dp[i][w]$ 是一个二维数组,其中第一维代表「当前正在考虑的物品种类」,第二维表示「当前背包的载重上限」,二维数组值表示「可以获得的最大价值」。 ###### 3. 状态转移方程 $\quad dp[i][w] = \begin{cases} dp[i - 1][w] & w < weight[i - 1] \cr max \lbrace dp[i - 1][w], \quad dp[i][w - weight[i - 1]] + value[i - 1] \rbrace & w \ge weight[i - 1] \end{cases}$ ###### 4. 初始条件 - 如果背包载重上限为 $0$,则无论选取什么物品,可以获得的最大价值一定是 $0$,即 $dp[i][0] = 0, 0 \le i \le size$。 - 无论背包载重上限是多少,前 $0$ 种物品所能获得的最大价值一定为 $0$,即 $dp[0][w] = 0, 0 \le w \le W$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,可以获得的最大价值。则最终结果为 $dp[size][W]$,其中 $size$ 为物品的种类数,$W$ 为背包的载重上限。 #### 思路 2:代码 ```python class Solution: # 思路 2:动态规划 + 状态转移方程优化 def completePackMethod2(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1]: # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 dp[i][w] = dp[i - 1][w] else: # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」与「前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品所得的最大价值」两者中的最大值 dp[i][w] = max(dp[i - 1][w], dp[i][w - weight[i - 1]] + value[i - 1]) return dp[size][W] ``` #### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times W)$,其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 - **空间复杂度**:$O(n \times W)$。 ## 4. 完全背包问题的滚动数组优化 通过观察「思路 2」中的状态转移方程 $dp[i][w] = \begin{cases} dp[i - 1][w] & w < weight[i - 1] \cr max \lbrace dp[i - 1][w], \quad dp[i][w - weight[i - 1]] + value[i - 1] \rbrace & w \ge weight[i - 1] \end{cases}$ 可以看出:我们只用到了当前行(第 $i$ 行)的 $dp[i][w]$、$dp[i][w - weight[i - 1]]$,以及上一行(第 $i - 1$ 行)的 $dp[i - 1][w]$。 所以我们没必要保存所有阶段的状态,只需要使用一个一维数组 $dp[w]$ 保存上一阶段的所有状态,采用使用「滚动数组」的方式对空间进行优化(去掉动态规划状态的第一维)。 #### 思路 3:动态规划 + 滚动数组优化 ###### 1. 阶段划分 按照当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中,可以获得的最大价值。 ###### 3. 状态转移方程 $dp[w] = \begin{cases} dp[w] & w < weight[i - 1] \cr max \lbrace dp[w], \quad dp[w - weight[i - 1]] + value[i - 1] \rbrace & w \ge weight[i - 1] \end{cases}$ > 注意:这里的 $dp[w - weight[i - 1]]$ 是第 $i$ 轮计算之后的「第 $i$ 阶段的状态值」。 因为在计算 $dp[w]$ 时,我们需要用到第 $i$ 轮计算之后的 $dp[w - weight[i - 1]]$,所以我们需要按照「从 $0 \sim W$ 正序递推的方式」递推 $dp[w]$,这样才能得到正确的结果。 因为 $w < weight[i - 1]$ 时,$dp[w]$ 只能取上一阶段的 $dp[w]$,其值相当于没有变化,这部分可以不做处理。所以我们在正序递推 $dp[w]$ 时,只需从 $weight[i - 1]$ 开始遍历即可。 ###### 4. 初始条件 - 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是 $0$,即 $dp[w] = 0, 0 \le w \le W$。 ###### 5. 最终结果 根据我们之前定义的状态, $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中,可以获得的最大价值。则最终结果为 $dp[W]$,其中 $W$ 为背包的载重上限。 #### 思路 3:代码 ```python class Solution: # 思路 3:动态规划 + 滚动数组优化 def completePackMethod3(self, weight: [int], value: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 正序枚举背包装载重量 for w in range(weight[i - 1], W + 1): # dp[w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」与「前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1]) return dp[W] ``` > 通过观察「0-1 背包问题滚动数组优化的代码」和「完全背包问题滚动数组优化的代码」可以看出,两者的唯一区别在于: > > 1. 0-1 背包问题滚动数组优化的代码采用了「从 $W \sim weight[i - 1]$ 逆序递推的方式」。 > 2. 完全背包问题滚动数组优化的代码采用了「从 $weight[i - 1] \sim W$ 正序递推的方式」。 #### 思路 3:复杂度分析 - **时间复杂度**:$O(n \times W)$,其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 - **空间复杂度**:$O(W)$。 ## 练习题目 - [0279. 完全平方数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/perfect-squares.md) - [0322. 零钱兑换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) - [0518. 零钱兑换 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/coin-change-ii.md) - [0377. 组合总和 IV](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/combination-sum-iv.md) - [完全背包问题题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%AE%8C%E5%85%A8%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E9%A2%98%E7%9B%AE) ## 参考资料 - 【资料】[背包九讲 - 崔添翼](https://github.com/tianyicui/pack) - 【文章】[背包 DP - OI Wiki](https://oi-wiki.org/dp/knapsack/) - 【文章】[背包问题 第四讲 - 宫水三叶的刷题日记](https://juejin.cn/post/7003243733604892685) - 【题解】[『 套用完全背包模板 』详解完全背包(含数学推导) - 完全平方数 - 力扣](https://leetcode.cn/problems/perfect-squares/solution/by-flix-sve5/) - 【题解】[『 一文搞懂完全背包问题 』从0-1背包到完全背包,逐层深入+推导 - 零钱兑换 - 力扣](https://leetcode.cn/problems/coin-change/solution/by-flix-su7s/) ================================================ FILE: docs/08_dynamic_programming/08_08_knapsack_problem_03.md ================================================ ## 1. 多重背包问题简介 > **多重背包问题**:有 $n$ 种物品和一个最多能装重量为 $W$ 的背包,第 $i$ 种物品的重量为 $weight[i]$,价值为 $value[i]$,件数为 $count[i]$。请问在总重量不超过背包载重上限的情况下,能装入背包的最大价值是多少? ![多重背包问题](https://qcdn.itcharge.cn/images/20240514111701.png) ## 2. 多重背包问题的基本思路 我们可以参考「0-1 背包问题」的状态定义和基本思路,对于容量为 $w$ 的背包,最多可以装 $min \lbrace count[i - 1], \frac{w}{weight[i - 1]} \rbrace$ 件第 $i - 1$ 件物品。那么我们可以多加一层循环,枚举第 $i - 1$ 件物品可以选择的件数($0 \sim min \lbrace count[i - 1], \frac{w}{weight[i - 1]} \rbrace$),从而将「完全背包问题」转换为「0-1 背包问题」。 #### 思路 1:动态规划 + 二维基本思路 ###### 1. 阶段划分 按照物品种类的序号、当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,可以获得的最大价值。 状态 $dp[i][w]$ 是一个二维数组,其中第一维代表「当前正在考虑的物品种类」,第二维表示「当前背包的载重上限」,二维数组值表示「可以获得的最大价值」。 ###### 3. 状态转移方程 $dp[i][w] = max \lbrace dp[i - 1][w - k \times weight[i - 1]] + k \times value[i - 1] \rbrace, \quad 0 \le k \le min \lbrace count[i - 1], \frac{w}{weight[i - 1]} \rbrace$。 ###### 4. 初始条件 - 如果背包载重上限为 $0$,则无论选取什么物品,可以获得的最大价值一定是 $0$,即 $dp[i][0] = 0, 0 \le i \le size$。 - 无论背包载重上限是多少,前 $0$ 种物品所能获得的最大价值一定为 $0$,即 $dp[0][w] = 0, 0 \le w \le W$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,可以获得的最大价值。则最终结果为 $dp[size][W]$,其中 $size$ 为物品的种类数,$W$ 为背包的载重上限。 ###### 6. 代码实现 ```python class Solution: # 思路 1:动态规划 + 二维基本思路 def multiplePackMethod1(self, weight: [int], value: [int], count: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 枚举第 i - 1 种物品能取个数 for k in range(min(count[i - 1], w // weight[i - 1]) + 1): # dp[i][w] 取所有 dp[i - 1][w - k * weight[i - 1] + k * value[i - 1] 中最大值 dp[i][w] = max(dp[i][w], dp[i - 1][w - k * weight[i - 1]] + k * value[i - 1]) return dp[size][W] ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times W \times C)$,其中 $n$ 为物品种类数量,$W$ 为背包的载重上限,$C$ 是物品的数量数组长度。因为 $n \times C = \sum count[i]$,所以时间复杂度也可以写成 $O(W \times \sum count[i])$。 - **空间复杂度**:$O(n \times W)$。 ## 3. 多重背包问题的滚动数组优化 在「完全背包问题」中,我们通过优化「状态转移方程」的方式,成功去除了对物品件数 $k$ 的依赖,从而将时间复杂度下降了一个维度。 而在「多重背包问题」中,我们在递推 $dp[i][w]$ 时,是无法从 $dp[i][w - weight[i - 1]]$ 状态得知目前究竟已经使用了多个件第 $i - 1$ 种物品,也就无法判断第 $i - 1$ 种物品是否还有剩余数量可选。这就导致了我们无法通过优化「状态转移方程」的方式将「多重背包问题」的时间复杂度降低。 但是我们可以参考「完全背包问题」+「滚动数组优化」的方式,将算法的空间复杂度下降一个维度。 #### 思路 2:动态规划 + 滚动数组优化 ###### 1. 阶段划分 按照当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中,可以获得的最大价值。 ###### 3. 状态转移方程 $dp[w] = max \lbrace dp[w - k \times weight[i - 1]] + k \times value[i - 1] \rbrace, \quad 0 \le k \le min \lbrace count[i - 1], \frac{w}{weight[i - 1]} \rbrace$ ###### 4. 初始条件 - 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是 $0$,即 $dp[w] = 0, 0 \le w \le W$。 ###### 5. 最终结果 根据我们之前定义的状态, $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中,可以获得的最大价值。则最终结果为 $dp[W]$,其中 $W$ 为背包的载重上限。 #### 思路 2:代码 ```python class Solution: # 思路 2:动态规划 + 滚动数组优化 def multiplePackMethod2(self, weight: [int], value: [int], count: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 逆序枚举背包装载重量(避免状态值错误) for w in range(W, weight[i - 1] - 1, -1): # 枚举第 i - 1 种物品能取个数 for k in range(min(count[i - 1], w // weight[i - 1]) + 1): # dp[w] 取所有 dp[w - k * weight[i - 1]] + k * value[i - 1] 中最大值 dp[w] = max(dp[w], dp[w - k * weight[i - 1]] + k * value[i - 1]) return dp[W] ``` #### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times W \times C)$,其中 $n$ 为物品种类数量,$W$ 为背包的载重上限,$C$ 是物品的数量数组长度。因为 $n \times C = \sum count[i]$,所以时间复杂度也可以写成 $O(W \times \sum count[i])$。 - **空间复杂度**:$O(W)$。 ## 4. 多重背包问题的二进制优化 在「思路 2」中,我们通过「滚动数组优化」的方式,降低了算法的空间复杂度。同时也提到了无法通过优化「状态转移方程」的方式将「多重背包问题」的时间复杂度降低。 但我们还是可以从物品数量入手,通过「二进制优化」的方式,将算法的时间复杂度降低。 > **二进制优化**:简单来说,就是把物品的数量 $count[i]$ 拆分成「由 $1, 2, 4, …, 2^m$ 件单个物品组成的大物品」,以及「剩余不足 $2$ 的整数次幂数量的物品,由 $count[i] -2^{\lfloor \log_2(count[i] + 1) \rfloor - 1}$ 件单个物品组成大物品」。 举个例子,第 $i$ 件物品的数量为 $31$,采用「二进制优化」的方式,可以拆分成 $31 = 1 + 2 + 4 + 8 + 16$ 一共 $5$ 件物品。也将是将 $31$ 件物品分成了 $5$ 件大物品: 1. 第 $1$ 件大物品有 $1$ 件第 $i$ 种物品组成; 2. 第 $2$ 件大物品有 $2$ 件第 $i$ 种物品组成; 3. 第 $3$ 件大物品有 $4$ 件第 $i$ 种物品组成; 4. 第 $4$ 件大物品有 $8$ 件第 $i$ 种物品组成; 5. 第 $5$ 件大物品有 $16$ 件第 $i$ 种物品组成。 这 $5$ 件大物品通过不同的组合,可表达出第 $i$ 种物品的数量范围刚好是 $0 \sim 31$。 这样本来第 $i$ 件物品数量需要枚举共计 $32$ 次($0 \sim 31$),而现在只需要枚举 $5$ 次即可。 再举几个例子: 1. 第 $i$ 件物品的数量为 $6$,可以拆分为 $6 = 1 + 2 + 3$ 一共 $3$ 件物品。 2. 第 $i$ 件物品的数量为 $8$,可以拆分为 $8 = 1 + 2 + 4 + 1$ 一共 $4$ 件物品。 3. 第 $i$ 件物品的数量为 $18$,可以拆分为 $18 = 1 + 2 + 4 + 8 + 3$ 一共 $5$ 件物品。 经过「二进制优化」之后,算法的时间复杂度从 $O(W \times \sum count[i])$ 降到了 $O(W \times \sum \log_2{count[i]})$。 #### 思路 3:动态规划 + 二进制优化 ###### 1. 阶段划分 按照当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中,可以获得的最大价值。 ###### 3. 状态转移方程 $dp[w] = max \lbrace dp[w - weight \underline{ } new[i - 1]] + value \underline{ } new[i - 1] \rbrace$ ###### 4. 初始条件 - 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是 $0$,即 $dp[w] = 0, 0 \le w \le W$。 ###### 5. 最终结果 根据我们之前定义的状态, $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中,可以获得的最大价值。则最终结果为 $dp[W]$,其中 $W$ 为背包的载重上限。 #### 思路 3:代码 ```python class Solution: # 思路 3:动态规划 + 二进制优化 def multiplePackMethod3(self, weight: [int], value: [int], count: [int], W: int): weight_new, value_new = [], [] # 二进制优化 for i in range(len(weight)): cnt = count[i] k = 1 while k <= cnt: cnt -= k weight_new.append(weight[i] * k) value_new.append(value[i] * k) k *= 2 if cnt > 0: weight_new.append(weight[i] * cnt) value_new.append(value[i] * cnt) dp = [0 for _ in range(W + 1)] size = len(weight_new) # 枚举前 i 种物品 for i in range(1, size + 1): # 逆序枚举背包装载重量(避免状态值错误) for w in range(W, weight_new[i - 1] - 1, -1): # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight_new[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight_new[i - 1]] + value_new[i - 1]) return dp[W] ``` #### 思路 3:复杂度分析 - **时间复杂度**:$O(W \times \sum \log_2{count[i]})$,其中 $W$ 为背包的载重上限,$count[i]$ 是第 $i$ 种物品的数量。 - **空间复杂度**:$O(W)$。 ## 练习题目 - [多重背包问题题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%A4%9A%E9%87%8D%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E9%A2%98%E7%9B%AE) ## 参考资料 - 【资料】[背包九讲 - 崔添翼](https://github.com/tianyicui/pack) - 【文章】[背包 DP - OI Wiki](https://oi-wiki.org/dp/knapsack/) - 【文章】[【动态规划/背包问题】多重背包の二进制优化](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486796&idx=1&sn=a382b38f8aed295410550bb1767437bd&chksm=fd9ca653caeb2f456262bbf70ffe1eeda8758b426a901a6ac15be184e7017870020e456c6fa2&scene=178&cur_album_id=1869157771795841024#rd) ================================================ FILE: docs/08_dynamic_programming/08_09_knapsack_problem_04.md ================================================ ## 1. 混合背包问题简介 > **混合背包问题**:有 $n$ 种物品和一个最多能装重量为 $W$ 的背包,第 $i$ 种物品的重量为 $weight[i]$,价值为 $value[i]$,件数为 $count[i]$。其中: > > 1. 当 $count[i] = -1$ 时,代表该物品只有 $1$ 件。 > 2. 当 $count[i] = 0$ 时,代表该物品有无限件。 > 3. 当 $count[i] > 0$ 时,代表该物品有 $count[i]$ 件。 > > 请问在总重量不超过背包载重上限的情况下,能装入背包的最大价值是多少? ![混合背包问题](https://qcdn.itcharge.cn/images/20240514111727.png) ## 2. 混合背包问题的基本思路 #### 思路 1:动态规划 混合背包问题其实就是将「0-1 背包问题」、「完全背包问题」和「多重背包问题」这 $3$ 种背包问题综合起来,有的是能取 $1$ 件,有的能取无数件,有的只能取 $count[i]$ 件。 其实只要理解了之前讲解的这 $3$ 种背包问题的核心思想,只要将其合并在一起就可以了。 并且在「多重背包问题」中,我们曾经使用「二进制优化」的方式,将「多重背包问题」转换为「0-1 背包问题」,那么在解决「混合背包问题」时,我们也可以先将「多重背包问题」转换为「0-1 背包问题」,然后直接再区分是「0-1 背包问题」还是「完全背包问题」就可以了。 #### 思路 1:代码 ```python class Solution: def mixedPackMethod1(self, weight: [int], value: [int], count: [int], W: int): weight_new, value_new, count_new = [], [], [] # 二进制优化 for i in range(len(weight)): cnt = count[i] # 多重背包问题,转为 0-1 背包问题 if cnt > 0: k = 1 while k <= cnt: cnt -= k weight_new.append(weight[i] * k) value_new.append(value[i] * k) count_new.append(1) k *= 2 if cnt > 0: weight_new.append(weight[i] * cnt) value_new.append(value[i] * cnt) count_new.append(1) # 0-1 背包问题,直接添加 elif cnt == -1: weight_new.append(weight[i]) value_new.append(value[i]) count_new.append(1) # 完全背包问题,标记并添加 else: weight_new.append(weight[i]) value_new.append(value[i]) count_new.append(0) dp = [0 for _ in range(W + 1)] size = len(weight_new) # 枚举前 i 种物品 for i in range(1, size + 1): # 0-1 背包问题 if count_new[i - 1] == 1: # 逆序枚举背包装载重量(避免状态值错误) for w in range(W, weight_new[i - 1] - 1, -1): # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight_new[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight_new[i - 1]] + value_new[i - 1]) # 完全背包问题 else: # 正序枚举背包装载重量 for w in range(weight_new[i - 1], W + 1): # dp[w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」与「前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight_new[i - 1]] + value_new[i - 1]) return dp[W] ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(W \times \sum \log_2{count[i]})$,其中 $W$ 为背包的载重上限,$count[i]$ 是第 $i$ 种物品的数量。 - **空间复杂度**:$O(W)$。 ## 3. 分组背包问题简介 > **分组背包问题**:有 $n$ 组物品和一个最多能装重量为 $W$ 的背包,第 $i$ 组物品的件数为 $group\_count[i]$,第 $i$ 组的第 $j$ 个物品重量为 $weight[i][j]$,价值为 $value[i][j]$。每组物品中最多只能选择 $1$ 件物品装入背包。请问在总重量不超过背包载重上限的情况下,能装入背包的最大价值是多少? ![分组背包问题](https://qcdn.itcharge.cn/images/20240514111745.png) ## 4. 分组背包问题的基本思路 #### 思路 1:动态规划 + 二维基本思路 ###### 1. 阶段划分 按照物品种类的序号、当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][w]$ 表示为:前 $i$ 组物品放入一个最多能装重量为 $w$ 的背包中,可以获得的最大价值。 状态 $dp[i][w]$ 是一个二维数组,其中第一维代表「当前正在考虑的物品组数」,第二维表示「当前背包的载重上限」,二维数组值表示「可以获得的最大价值」。 ###### 3. 状态转移方程 由于我们可以不选择 $i - 1$ 组物品中的任何物品,也可以从第 $i - 1$ 组物品的第 $0 \sim group\_count[i - 1] - 1$ 件物品中随意选择 $1$ 件物品,所以状态 $dp[i][w]$ 可能从以下方案中选择最大值: 1. 不选择第 $i - 1$ 组中的任何物品:可以获得的最大价值为 $dp[i - 1][w]$。 2. 选择第 $i - 1$ 组物品中第 $0$ 件:可以获得的最大价值为 $dp[i - 1][w - weight[i - 1][0]] + value[i - 1][0]$。 3. 选择第 $i - 1$ 组物品中第 $1$ 件:可以获得的最大价值为 $dp[i - 1][w - weight[i - 1][1]] + value[i - 1][1]$。 4. …… 5. 选择第 $i - 1$ 组物品中最后 $1$ 件:假设 $k = group\_count[i - 1] - 1$,则可以获得的最大价值为 $dp[i - 1][w - weight[i - 1][k]] + value[i - 1][k]$。 则状态转移方程为: $dp[i][w] = max \lbrace dp[i - 1][w], dp[i - 1][w - weight[i - 1][k]] + value[i - 1][k] \rbrace , \quad 0 \le k \le group\_count[i - 1]$ ###### 4. 初始条件 - 如果背包载重上限为 $0$,则无论选取什么物品,可以获得的最大价值一定是 $0$,即 $dp[i][0] = 0, 0 \le i \le size$。 - 无论背包载重上限是多少,前 $0$ 组物品所能获得的最大价值一定为 $0$,即 $dp[0][w] = 0, 0 \le w \le W$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][w]$ 表示为:前 $i$ 组物品放入一个最多能装重量为 $w$ 的背包中,可以获得的最大价值。则最终结果为 $dp[size][W]$,其中 $size$ 为物品的种类数,$W$ 为背包的载重上限。 #### 思路 1:代码 ```python class Solution: # 思路 1:动态规划 + 二维基本思路 def groupPackMethod1(self, group_count: [int], weight: [[int]], value: [[int]], W: int): size = len(group_count) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 组物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 枚举第 i - 1 组物品能取个数 dp[i][w] = dp[i - 1][w] for k in range(group_count[i - 1]): if w >= weight[i - 1][k]: # dp[i][w] 取所有 dp[i - 1][w - weight[i - 1][k]] + value[i - 1][k] 中最大值 dp[i][w] = max(dp[i][w], dp[i - 1][w - weight[i - 1][k]] + value[i - 1][k]) ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times W \times C)$,其中 $n$ 为物品分组数量,$W$ 为背包的载重上限,$C$ 是每组物品的数量。因为 $n \times C = \sum group\_count[i]$,所以时间复杂度也可以写成 $O(W \times \sum group\_count[i])$。 - **空间复杂度**:$O(n \times W)$。 ## 5. 分组背包问题的滚动数组优化 #### 思路 2:动态规划 + 滚动数组优化 ###### 1. 阶段划分 按照当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中,可以获得的最大价值。 ###### 3. 状态转移方程 $dp[w] = max \lbrace dp[w], \quad dp[w - weight[i - 1][k]] + value[i - 1][k] \rbrace , \quad 0 \le k \le group\_count[i - 1]$ ###### 4. 初始条件 - 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是 $0$,即 $dp[w] = 0, 0 \le w \le W$。 ###### 5. 最终结果 根据我们之前定义的状态, $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中,可以获得的最大价值。则最终结果为 $dp[W]$,其中 $W$ 为背包的载重上限。 #### 思路 2:代码 ```python class Solution: # 思路 2:动态规划 + 滚动数组优化 def groupPackMethod2(self, group_count: [int], weight: [[int]], value: [[int]], W: int): size = len(group_count) dp = [0 for _ in range(W + 1)] # 枚举前 i 组物品 for i in range(1, size + 1): # 逆序枚举背包装载重量 for w in range(W, -1, -1): # 枚举第 i - 1 组物品能取个数 for k in range(group_count[i - 1]): if w >= weight[i - 1][k]: # dp[w] 取所有 dp[w - weight[i - 1][k]] + value[i - 1][k] 中最大值 dp[w] = max(dp[w], dp[w - weight[i - 1][k]] + value[i - 1][k]) return dp[W] ``` #### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times W \times C)$,其中 $n$ 为物品分组数量,$W$ 为背包的载重上限,$C$ 是每组物品的数量。因为 $n \times C = \sum group\_count[i]$,所以时间复杂度也可以写成 $O(W \times \sum group\_count[i])$。 - **空间复杂度**:$O(W)$。 ## 6. 二维费用背包问题简介 > **二维费用背包问题**:有 $n$ 件物品和有一个最多能装重量为 $W$、容量为 $V$ 的背包。第 $i$ 件物品的重量为 $weight[i]$,体积为 $volume[i]$,价值为 $value[i]$,每件物品有且只有 $1$ 件。请问在总重量不超过背包载重上限、容量上限的情况下,能装入背包的最大价值是多少? ![二维费用背包问题](https://qcdn.itcharge.cn/images/20240514111802.png) ## 7. 二维费用背包问题的基本思路 我们可以参考「0-1 背包问题」的状态定义和基本思路,在「0-1 背包问题」基本思路的基础上,增加一个维度用于表示物品的容量。 #### 思路 1:动态规划 + 三维基本思路 ###### 1. 阶段划分 按照物品种类的序号、当前背包的载重上限、容量上限进行阶段划分 ###### 2. 定义状态 定义状态 $dp[i][w][v]$ 为:前 $i$ 件物品放入一个最多能装重量为 $w$、容量为 $v$ 的背包中,可以获得的最大价值。 ###### 3. 状态转移方程 $dp[i][w][v] = max(dp[i - 1][w][v], dp[i - 1][w - weight[i - 1]][v - volume[i - 1]] + value[i - 1]), \quad 0 \le weight[i - 1] \le w, 0 \le volume[i - 1] \le v$ > 注意:采用这种「状态定义」和「状态转移方程」,往往会导致内存超出要求限制,所以一般我们会采用「滚动数组」对算法的空间复杂度进行优化。 ###### 4. 初始条件 - 如果背包载重上限为 $0$ 或者容量上限为 $0$,则无论选取什么物品,可以获得的最大价值一定是 $0$,即: - $dp[i][w][0] = 0, 0 \le i \le size, 0 \le w \le W$ - $dp[i][0][v] = 0, 0 \le i \le size, 0 \le v \le V$ - 无论背包载重上限是多少,前 $0$ 种物品所能获得的最大价值一定为 $0$,即: - $dp[0][w][v] = 0, 0 \le w \le W, 0 \le v \le V$ ###### 5. 最终结果 根据我们之前定义的状态, $dp[i][w][v]$ 表示为:前 $i$ 件物品放入一个最多能装重量为 $w$、容量为 $v$ 的背包中,可以获得的最大价值。则最终结果为 $dp[size][W][V]$,其中 $size$ 为物品的种类数,$W$ 为背包的载重上限,$V$ 为背包的容量上限。 #### 思路 1:代码 ```python class Solution: # 思路 1:动态规划 + 三维基本思路 def twoDCostPackMethod1(self, weight: [int], volume: [int], value: [int], W: int, V: int): size = len(weight) dp = [[[0 for _ in range(V + 1)] for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 组物品 for i in range(1, N + 1): # 枚举背包装载重量 for w in range(W + 1): # 枚举背包装载容量 for v in range(V + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1] or v < volume[i - 1]: # dp[i][w][v] 取「前 i - 1 件物品装入装载重量为 w、装载容量为 v 的背包中的最大价值」 dp[i][w][v] = dp[i - 1][w][v] else: # dp[i][w][v] 取所有 dp[w - weight[i - 1]][v - volume[i - 1]] + value[i - 1] 中最大值 dp[i][w][v] = max(dp[i - 1][w][v], dp[i - 1][w - weight[i - 1]][v - volume[i - 1]] + value[i - 1]) return dp[size][W][V] ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times W \times V)$,其中 $n$ 为物品分组数量,$W$ 为背包的载重上限,$V$ 为背包的容量上限。 - **空间复杂度**:$O(n \times W \times V)$。 ## 8. 二维费用背包问题滚动数组优化 #### 思路 2:动态规划 + 滚动数组优化 ###### 1. 阶段划分 按照当前背包的载重上限、容量上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[w][v]$ 表示为:将物品装入最多能装重量为 $w$、容量为 $v$ 的背包中,可以获得的最大价值。 ###### 3. 状态转移方程 $dp[w][v] = max \lbrace dp[w][v], \quad dp[w - weight[i - 1]][v - volume[i - 1]] + value[i - 1] \rbrace , \quad 0 \le weight[i - 1] \le w, 0 \le volume[i - 1] \le v$ ###### 4. 初始条件 - 如果背包载重上限为 $0$ 或者容量上限为 $0$,则无论选取什么物品,可以获得的最大价值一定是 $0$,即: - $dp[w][0] = 0, 0 \le w \le W$ - $dp[0][v] = 0, 0 \le v \le V$ ###### 5. 最终结果 根据我们之前定义的状态, $dp[w][v]$ 表示为:将物品装入最多能装重量为 $w$、容量为 $v$ 的背包中,可以获得的最大价值。则最终结果为 $dp[W][V]$,其中 $W$ 为背包的载重上限,$V$ 为背包的容量上限。 #### 思路 2:代码 ```python class Solution: # 思路 2:动态规划 + 滚动数组优化 def twoDCostPackMethod2(self, weight: [int], volume: [int], value: [int], W: int, V: int): size = len(weight) dp = [[0 for _ in range(V + 1)] for _ in range(W + 1)] # 枚举前 i 组物品 for i in range(1, N + 1): # 逆序枚举背包装载重量 for w in range(W, weight[i - 1] - 1, -1): # 逆序枚举背包装载容量 for v in range(V, volume[i - 1] - 1, -1): # dp[w][v] 取所有 dp[w - weight[i - 1]][v - volume[i - 1]] + value[i - 1] 中最大值 dp[w][v] = max(dp[w][v], dp[w - weight[i - 1]][v - volume[i - 1]] + value[i - 1]) return dp[W][V] ``` #### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times W \times V)$,其中 $n$ 为物品分组数量,$W$ 为背包的载重上限,$V$ 为背包的容量上限。 - **空间复杂度**:$O(W \times V)$。 ## 练习题目 - [1155. 掷骰子等于目标和的方法数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/number-of-dice-rolls-with-target-sum.md) - [0474. 一和零](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/ones-and-zeroes.md) - [分组背包问题题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%88%86%E7%BB%84%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E9%A2%98%E7%9B%AE) - [多维背包问题题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%A4%9A%E7%BB%B4%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E9%A2%98%E7%9B%AE) ## 参考资料 - 【资料】[背包九讲 - 崔添翼](https://github.com/tianyicui/pack) - 【文章】[背包 DP - OI Wiki](https://oi-wiki.org/dp/knapsack/) - 【文章】[【动态规划/背包问题】分组背包问题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487504&idx=1&sn=9ac523ec0ac14c8634a229f8c3f919d7&chksm=fd9cbb0fcaeb32196b80a40e4408f6a7e2651167e0b9e31aa6d7c6109fbc2117340a59db12a1&token=1936267333&lang=zh_CN&scene=21#wechat_redirect) - 【文章】[【动态规划/背包问题】背包问题第一阶段最终章:混合背包问题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487034&idx=1&sn=eaa05b76387d34aa77f7f14f35fa78a4&chksm=fd9ca525caeb2c33095d285222dcee0dd072465bf7288bda0aab39e90a04bb7b1af018b89fd4&token=1872331648&lang=zh_CN&scene=21#wechat_redirect) ================================================ FILE: docs/08_dynamic_programming/08_10_knapsack_problem_05.md ================================================ ## 1. 求恰好装满背包的最大价值 > **背包问题求恰好装满背包的最大价值**:在给定背包重量 $W$,每件物品重量 $weight[i]$,物品间相互关系(分组、依赖等)的背包问题中,请问在恰好装满背包的情况下,能装入背包的最大价值总和是多少? 在背包问题中,有的题目不要求把背包装满,而有的题目要求恰好装满背包。 如果题目要求「恰好装满背包」,则我们可在原有状态定义、状态转移方程的基础上,在初始化时,令 $dp[0] = 0$,以及 $d[w] = -\infty, 1 \le w \le W$。 这样就可以保证最终得到的 $dp[W]$ 为恰好装满背包的最大价值总和。 这是因为:初始化的 $dp$ 数组实际上就是在没有任何物品可以放入背包时的「合法状态」。 如果不要求恰好装满背包,那么: 1. 任何载重上限下的背包,在不放入任何物品时,都有一个合法解,此时背包所含物品的最大价值为 $0$,即 $dp[w] = 0, 0 \le w \le W$。 而如果要求恰好装满背包,那么: 1. 只有载重上限为 $0$ 的背包,在不放入物品时,能够恰好装满背包(有合法解),此时背包所含物品的最大价值为 $0$,即 $dp[0] = 0$。 2. 其他载重上限下的背包,在放入物品的时,都不能恰好装满背包(都没有合法解),此时背包所含物品的最大价值属于未定义状态,值应为 $-\infty$,即 $dp[w] = 0, 0 \le w \le W$。 这样在进行状态转移时,我们可以通过判断 $dp[w]$ 与 $-\infty$ 的关系,来判断是否能恰好装满背包。 下面我们以「0-1 背包问题」求恰好装满背包的最大价值为例。 > **0-1 背包问题求恰好装满背包的最大价值**:有 $n$ 种物品和一个最多能装重量为 $W$ 的背包,第 $i$ 种物品的重量为 $weight[i]$,价值为 $value[i]$,每件物品有且只有 $1$ 件。请问在恰好装满背包的情况下,能装入背包的最大价值总和是多少? #### 思路 1:动态规划 + 一维状态 1. **阶段划分**:按照当前背包的载重上限进行阶段划分。 2. **定义状态**:定义状态 $dp[w]$ 表示为:将物品装入一个最多能装重量为 $w$ 的背包中,恰好装满背包的情况下,能装入背包的最大价值总和。 3. **状态转移方程**:$dp[w] = dp[w] + dp[w - weight[i - 1]]$ 4. **初始条件**: 1. 只有载重上限为 $0$ 的背包,在不放入物品时,能够恰好装满背包(有合法解),此时背包所含物品的最大价值为 $0$,即 $dp[0] = 0$。 2. 其他载重上限下的背包,在放入物品的时,都不能恰好装满背包(都没有合法解),此时背包所含物品的最大价值属于未定义状态,值应为 $-\infty$,即 $dp[w] = 0, 0 \le w \le W$。 5. **最终结果**:根据我们之前定义的状态, $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中的方案总数。则最终结果为 $dp[W]$,其中 $W$ 为背包的载重上限。 #### 思路 1:代码 ```python class Solution: # 0-1 背包问题 求恰好装满背包的最大价值 def zeroOnePackJustFillUp(self, weight: [int], value: [int], W: int): size = len(weight) dp = [float('-inf') for _ in range(W + 1)] dp[0] = 0 # 枚举前 i 种物品 for i in range(1, size + 1): # 逆序枚举背包装载重量(避免状态值错误) for w in range(W, weight[i - 1] - 1, -1): # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1]) if dp[W] == float('-inf'): return -1 return dp[W] ``` #### 思路 1:算法复杂度 - **时间复杂度**:$O(n \times W)$,其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 - **空间复杂度**:$O(W)$。 ## 2. 求方案总数 > **背包问题求方案数**:在给定背包重量 $W$,每件物品重量 $weight[i]$,物品间相互关系(分组、依赖等)的背包问题中,请问在总重量不超过背包载重上限的情况下,或者在总重量不超过某一指定重量的情况下,一共有多少种方案? 这种问题就是将原有状态转移方程中的「求最大值」变为「求和」即可。 下面我们以「0-1 背包问题」求方案总数为例。 > **0-1 背包问题求方案数**:有 $n$ 件物品和有一个最多能装重量为 $W$ 的背包。第 $i$ 件物品的重量为 $weight[i]$,价值为 $value[i]$,每件物品有且只有 $1$ 件。 > > 请问在总重量不超过背包载重上限的情况下,一共有多少种方案? - 如果使用二维状态定义,可定义状态 $dp[i][w]$ 为:前 $i$ 件物品放入一个最多能装重量为 $w$ 的背包中的方案总数。则状态转移方程为:$dp[i][w] = dp[i - 1][w] + dp[i][w - weight[i - 1]]$。 - 如果使用一维状态定义,可定义状态 $dp[w]$ 表示为:将物品装入一个最多能装重量为 $w$ 的背包中的方案总数。则状态转移方程为:$dp[w] = dp[w] + dp[w - weight[i - 1]]$。 下面我们使用一维状态定义方式解决「0-1 背包问题求解方案数」问题。 #### 思路 2:动态规划 + 一维状态 1. **阶段划分**:按照物品种类的序号、当前背包的载重上限进行阶段划分。 2. **定义状态**:定义状态 $dp[w]$ 表示为:将物品装入一个最多能装重量为 $w$ 的背包中的方案总数。 3. **状态转移方程**:$dp[w] = dp[w] + dp[w - weight[i - 1]]$ 4. **初始条件**:如果背包载重上限为 $0$,则一共有 $1$ 种方案(什么也不装),即 $dp[0] = 1$。 5. **最终结果**:根据我们之前定义的状态, $dp[w]$ 表示为:将物品装入最多能装重量为 $w$ 的背包中的方案总数。则最终结果为 $dp[W]$,其中 $W$ 为背包的载重上限。 #### 思路 2:代码 ```python class Solution: # 0-1 背包问题求方案总数 def zeroOnePackNumbers(self, weight: [int], value: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] dp[0] = 1 # 枚举前 i 种物品 for i in range(1, size + 1): # 逆序枚举背包装载重量 for w in range(W, weight[i - 1] - 1, -1): # dp[w] = 前 i - 1 件物品装入载重为 w 的背包中的方案数 + 前 i 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 件物品的方案数 dp[w] = dp[w] + dp[w - weight[i - 1]] return dp[W] ``` #### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times W)$,其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 - **空间复杂度**:$O(W)$。 ## 3. 求最优方案数 > **背包问题求最优方案数**:在给定背包重量 $W$,每件物品重量 $weight[i]$、物品价值 $value[i]$,物品间相互关系(分组、依赖等)的背包问题中,请问在总重量不超过背包载重上限的情况下,使背包总价值最大的方案数是多少? 通过结合「求背包最大可得价值」和「求方案数」两个问题的思路,我们可以分别定义两个状态: 1. 定义 $dp[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,可获得的最大价值。 2. 定义 $op[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,使背包总价值最大的方案数。 下面我们以「0-1 背包问题」求最优方案数为例。 > **0-1 背包问题求最优方案数**:有 $n$ 种物品和一个最多能装重量为 $W$ 的背包,第 $i$ 种物品的重量为 $weight[i]$,价值为 $value[i]$,每件物品有且只有 $1$ 件。请问在总重量不超过背包载重上限的情况下,使背包总价值最大的方案数是多少? #### 思路 3:动态规划 1. **阶段划分**:按照物品种类的序号、当前背包的载重上限进行阶段划分。 2. **定义状态**: 1. 定义 $dp[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,可获得的最大价值。 2. 定义 $op[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,使背包总价值最大的方案数。 3. **状态转移方程**: 1. 如果 $dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]$,则说明选择第 $i - 1$ 件物品获得价值更高,此时方案数 $op[i][w]$ 是在 $op[i - 1][w - weight[i - 1]]$ 基础上添加了第 $i - 1$ 件物品,因此方案数不变,即:$op[i][w] = op[i - 1][w - weight[i - 1]]$。 2. 如果 $dp[i - 1][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1]$,则说明选择与不选择第 $i - 1$ 件物品获得价格相等,此时方案数应为两者之和,即:$op[i][w] = op[i - 1][w] + op[i - 1][w - weight[i - 1]]$。 3. 如果 $dp[i - 1][w] > dp[i - 1][w - weight[i - 1]] + value[i - 1]$,则说明不选择第 $i - 1$ 件物品获得价值更高,此时方案数等于之前方案数,即:$op[i][w] = op[i - 1][w]$。 4. **初始条件**:如果背包载重上限为 $0$,则一共有 $1$ 种方案(什么也不装),即 $dp[0] = 1$。 5. **最终结果**:根据我们之前定义的状态, $op[i][w]$ 表示为:前 $i$ 种物品放入一个最多能装重量为 $w$ 的背包中,使背包总价值最大的方案数。则最终结果为 $op[size][W]$,其中 $size$ 为物品的种类数,$W$ 为背包的载重上限。 #### 思路 3:代码 ```python class Solution: # 0-1 背包问题求最优方案数 思路 1 def zeroOnePackMaxProfitNumbers1(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] op = [[1 for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1]: # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 dp[i][w] = dp[i - 1][w] op[i][w] = op[i - 1][w] else: # 选择第 i - 1 件物品获得价值更高 if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] # 在之前方案基础上添加了第 i - 1 件物品,因此方案数量不变 op[i][w] = op[i - 1][w - weight[i - 1]] # 两种方式获得价格相等 elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w] # 方案数 = 不使用第 i - 1 件物品的方案数 + 使用第 i - 1 件物品的方案数 op[i][w] = op[i - 1][w] + op[i - 1][w - weight[i - 1]] # 不选择第 i - 1 件物品获得价值最高 else: dp[i][w] = dp[i - 1][w] # 不选择第 i - 1 件物品,与之前方案数相等 op[i][w] = op[i - 1][w] return op[size][W] ``` #### 思路 3:复杂度分析 - **时间复杂度**:$O(n \times W)$,其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 - **空间复杂度**:$O(n \times W)$。 ## 4. 求具体方案 > **背包问题求具体方案**:在给定背包重量 $W$,每件物品重量 $weight[i]$、物品价值 $value[i]$,物品间相互关系(分组、依赖等)的背包问题中,请问将哪些物品装入背包,可使这些物品的总重量不超过背包载重上限,且价值总和最大? 一般背包问题都是求解一个最优值,但是如果要输出该最优值的具体方案,除了 $dp[i][w]$,我们可以再定义一个数组 $path[i][w]$ 用于记录状态转移时,所取的状态是状态转移方程中的哪一项,从而确定选择的具体物品。 下面我们以「0-1 背包问题」求具体方案为例。 > **0-1 背包问题求具体方案**:有 $n$ 种物品和一个最多能装重量为 $W$ 的背包,第 $i$ 种物品的重量为 $weight[i]$,价值为 $value[i]$,每件物品有且只有 $1$ 件。请问将哪些物品装入背包,可使这些物品的总重量不超过背包载重上限,且价值总和最大? #### 思路 4:动态规划 + 路径记录 0-1 背包问题的状态转移方程为:$dp[i][w] = max \lbrace dp[i - 1][w], \quad dp[i - 1][w - weight[i - 1]] + value[i - 1] \rbrace$ 则我们可以再定义一个 $path[i][w]$ 用于记录状态转移时,所取的状态是状态转移方程中的哪一项。 1. 如果 $path[i][w] = False$,说明:转移到 $dp[i][w]$ 时,选择了前一项 $dp[i - 1][w]$,并且具体方案中不包括第 $i - 1$ 件物品。 2. 如果 $paht[i][w] = True$,说明:转移到 $dp[i][w]$ 时,选择了后一项 $dp[i - 1][w - weight[i - 1]] + value[i - 1]$,并且具体方案中包括第 $i - 1$ 件物品。 #### 思路 4:代码 ```python class Solution: # 0-1 背包问题求具体方案 def zeroOnePackPrintPath(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] path = [[False for _ in range(W + 1)] for _ in range(size + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1]: # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 dp[i][w] = dp[i - 1][w] path[i][w] = False else: # 选择第 i - 1 件物品获得价值更高 if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] # 取状态转移式第二项:在之前方案基础上添加了第 i - 1 件物品 path[i][w] = True # 两种方式获得价格相等 elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w] # 取状态转移式第二项:尽量使用第 i - 1 件物品 path[i][w] = True # 不选择第 i - 1 件物品获得价值最高 else: dp[i][w] = dp[i - 1][w] # 取状态转移式第一项:不选择第 i - 1 件物品 path[i][w] = False res = [] i, w = size, W while i >= 1 and w >= 0: if path[i][w]: res.append(str(i - 1)) w -= weight[i - 1] i -= 1 return " ".join(res[::-1]) ``` #### 思路 4:复杂度分析 - **时间复杂度**:$O(n \times W)$,其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 - **空间复杂度**:$O(n \times W)$。 ## 5. 求字典序最小的具体方案 > **背包问题求字典序最小的具体方案**:在背包问题中,除了要求最大价值外,有时还要求输出所有满足条件的方案中,按照物品编号排列后字典序最小的那一个。这里的「字典序最小」是指:将选中物品的编号(编号范围为 $0 \sim size - 1$)从小到大排列,得到的序列在所有可行方案中字典序最小。 例如,如果有两种可行方案分别为 [0,2,3] 和 [0,1,4],则 [0,1,4] 的字典序更小。求解此类问题时,通常需要在动态规划的基础上,设计合理的回溯或记录路径方式,确保最终输出的方案满足字典序最小的要求。 #### 思路 5:动态规划 + 路径记录(输出字典序最小的方案) 我们仍以「0-1 背包问题」求字典序最小的具体方案为例。 为了使「字典序最小」。我们可以先将物品的序号进行反转,从 $0 \sim size - 1$ 变为 $size - 1 \sim 0$,然后在返回具体方案时,再根据 $i = size - 1$ 将序号变回来。 这是为了在选择物品时,尽可能的向后选择反转后序号大的物品(即原序号小的物品),从而保证原序号为 $0 \sim size - 1$ 的物品选择方案排列出来之后的字典序最小。 #### 思路 5:代码 ```python class Solution: # 0-1 背包问题求具体方案,要求最小序输出 def zeroOnePackPrintPathMinOrder(self, weight: [int], value: [int], W: int): size = len(weight) dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)] path = [[False for _ in range(W + 1)] for _ in range(size + 1)] weight.reverse() value.reverse() # 枚举前 i 种物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(W + 1): # 第 i - 1 件物品装不下 if w < weight[i - 1]: # dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」 dp[i][w] = dp[i - 1][w] path[i][w] = False else: # 选择第 i - 1 件物品获得价值更高 if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1] # 取状态转移式第二项:在之前方案基础上添加了第 i - 1 件物品 path[i][w] = True # 两种方式获得价格相等 elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]: dp[i][w] = dp[i - 1][w] # 取状态转移式第二项:尽量使用第 i - 1 件物品 path[i][w] = True # 不选择第 i - 1 件物品获得价值最高 else: dp[i][w] = dp[i - 1][w] # 取状态转移式第一项:不选择第 i - 1 件物品 path[i][w] = False res = [] i, w = size, W while i >= 1 and w >= 0: if path[i][w]: res.append(str(size - i)) w -= weight[i - 1] i -= 1 return " ".join(res) ``` #### 思路 5:复杂度分析 - **时间复杂度**:$O(n \times W)$,其中 $n$ 为物品种类数量,$W$ 为背包的载重上限。 - **空间复杂度**:$O(n \times W)$。 ## 参考资料 - 【资料】[背包九讲 - 崔添翼](https://github.com/tianyicui/pack) - 【文章】[背包 DP - OI Wiki](https://oi-wiki.org/dp/knapsack/) - 【文章】[背包问题——“01背包”最优方案总数分析及实现 - wumuzi 的博客](https://blog.csdn.net/wumuzi520/article/details/7019131) - 【文章】[背包问题——“完全背包”最优方案总数分析及实现 - wumuzi的博客](https://blog.csdn.net/wumuzi520/article/details/7019661) - 【文章】[背包问题——“01背包”及“完全背包”装满背包的方案总数分析及实现 - wumuzi的博客](https://blog.csdn.net/wumuzi520/article/details/7021210) ================================================ FILE: docs/08_dynamic_programming/08_11_interval_dp.md ================================================ ## 1. 区间动态规划简介 > **区间动态规划(区间 DP)**:是一类以区间为阶段、以区间的左右端点为状态的动态规划方法。它常用于解决「在一段区间内进行某种操作,使得总代价最小或总价值最大」这类问题。区间 DP 的核心思想是:先解决小区间的最优解,再逐步合并得到大区间的最优解,最终得到整个区间的最优解。 区间 DP 的状态通常用 $dp[i][j]$ 表示区间 $[i, j]$ 的最优解。状态的转移依赖于比 $[i, j]$ 更小的子区间的状态。 常见的区间 DP 问题大致分为两类: 1. **单区间扩展型**:通过在区间 $[i + 1, j - 1]$ 的基础上,向两侧扩展得到 $[i, j]$,例如回文串、石子合并等问题。 2. **多区间合并型**:将区间 $[i, j]$ 拆分为两个或多个更小的区间(如 $[i, k]$ 和 $[k + 1, j]$),通过合并这些小区间的最优解得到大区间的最优解。 接下来,我们将分别介绍这两类区间 DP 问题的基本解题思路。 ## 2. 区间 DP 问题的基本思路 ### 2.1 第 1 类区间 DP 问题的基本思路 这类区间 DP 通常是「从中间向两侧扩展」,其状态转移方程一般为:$dp[i][j] = \max \lbrace dp[i + 1][j - 1],\ dp[i + 1][j],\ dp[i][j - 1] \rbrace + cost[i][j]$,其中 $i \leq j$。 - $dp[i][j]$ 表示区间 $[i, j]$(即下标 $i$ 到 $j$ 的所有元素)上的最优解(如最大价值)。 - $cost[i][j]$ 表示将小区间扩展到 $[i, j]$ 时产生的代价。 - 取 $\max$ 或 $\min$ 取决于题目要求最大值还是最小值。 基本解题流程如下: 1. 枚举区间起点 $i$; 2. 枚举区间终点 $j$; 3. 按照状态转移方程,利用更小区间的最优解递推出更大区间的最优解。 对应代码如下: ```python for i in range(size - 1, -1, -1): # 枚举区间起点 for j in range(i + 1, size): # 枚举区间终点 # 状态转移方程,计算转移到更大区间后的最优值 dp[i][j] = max(dp[i + 1][j - 1], dp[i + 1][j], dp[i][j - 1]) + cost[i][j] ``` ### 2.2 第 2 类区间 DP 问题的基本思路 这类区间 DP 的核心思想是:将一个大区间 $[i, j]$ 拆分成两个更小的子区间 $[i, k]$ 和 $[k + 1, j]$,通过合并子区间的最优解,得到大区间的最优解。常见的状态转移方程为: $$ dp[i][j] = \max/\min \{dp[i][k] + dp[k+1][j] + cost[i][j]\},\quad i \leq k < j $$ 其中: 1. $dp[i][j]$ 表示区间 $[i, j]$(即下标 $i$ 到 $j$ 的所有元素)上的最优解(如最大价值或最小代价)。 2. $cost[i][j]$ 表示将 $[i, k]$ 和 $[k+1, j]$ 合并成 $[i, j]$ 时产生的额外代价。 3. 取 $\max$ 或 $\min$ 取决于题目要求最大值还是最小值。 这类区间 DP 的通用解题步骤如下: 1. 枚举区间长度(从小到大,保证子区间已被计算); 2. 枚举区间起点 $i$,根据区间长度确定终点 $j$; 3. 枚举所有可能的分割点 $k$,用状态转移方程更新 $dp[i][j]$ 的最优值。 对应代码如下: ```python for l in range(1, n): # 枚举区间长度 for i in range(n): # 枚举区间起点 j = i + l - 1 # 根据起点和长度得到终点 if j >= n: break dp[i][j] = float('-inf') # 初始化 dp[i][j] for k in range(i, j + 1): # 枚举区间分割点 # 状态转移方程,计算合并区间后的最优值 dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + cost[i][j]) ``` ## 3. 区间 DP 问题的应用 下面我们根据几个例子来讲解一下区间 DP 问题的具体解题思路。 ### 3.1 经典例题:最长回文子序列 #### 3.1.1 题目链接 - [516. 最长回文子序列 - 力扣](https://leetcode.cn/problems/longest-palindromic-subsequence/) #### 3.1.2 题目大意 **描述**:给定一个字符串 $s$。 **要求**:找出其中最长的回文子序列,并返回该序列的长度。 **说明**: - **子序列**:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。 - $1 \le s.length \le 1000$。 - $s$ 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "bbbab" 输出:4 解释:一个可能的最长回文子序列为 "bbbb"。 ``` - 示例 2: ```python 输入:s = "cbbd" 输出:2 解释:一个可能的最长回文子序列为 "bb"。 ``` #### 3.1.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:字符串 $s$ 在区间 $[i, j]$ 范围内的最长回文子序列长度。 ###### 3. 状态转移方程 我们对区间 $[i, j]$ 边界位置上的字符 $s[i]$ 与 $s[j]$ 进行分类讨论: 1. 如果 $s[i] = s[j]$,则 $dp[i][j]$ 为区间 $[i + 1, j - 1]$ 范围内最长回文子序列长度 + $2$,即 $dp[i][j] = dp[i + 1][j - 1] + 2$。 2. 如果 $s[i] \ne s[j]$,则 $dp[i][j]$ 取决于以下两种情况,取其最大的一种: 1. 加入 $s[i]$ 所能组成的最长回文子序列长度,即:$dp[i][j] = dp[i][j - 1]$。 2. 加入 $s[j]$ 所能组成的最长回文子序列长度,即:$dp[i][j] = dp[i - 1][j]$。 则状态转移方程为: $dp[i][j] = \begin{cases} max \lbrace dp[i + 1][j - 1] + 2 \rbrace & s[i] = s[j] \cr max \lbrace dp[i][j - 1], dp[i - 1][j] \rbrace & s[i] \ne s[j] \end{cases}$ ###### 4. 初始条件 - 单个字符的最长回文序列是 $1$,即 $dp[i][i] = 1$。 ###### 5. 最终结果 由于 $dp[i][j]$ 依赖于 $dp[i + 1][j - 1]$、$dp[i + 1][j]$、$dp[i][j - 1]$,所以我们应该按照从下到上、从左到右的顺序进行遍历。 根据我们之前定义的状态,$dp[i][j]$ 表示为:字符串 $s$ 在区间 $[i, j]$ 范围内的最长回文子序列长度。所以最终结果为 $dp[0][size - 1]$。 ##### 思路 1:代码 ```python class Solution: def longestPalindromeSubseq(self, s: str) -> int: size = len(s) dp = [[0 for _ in range(size)] for _ in range(size)] for i in range(size): dp[i][i] = 1 for i in range(size - 1, -1, -1): for j in range(i + 1, size): if s[i] == s[j]: dp[i][j] = dp[i + 1][j - 1] + 2 else: dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) return dp[0][size - 1] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(n^2)$。 ### 3.2 经典例题:戳气球 #### 3.2.1 题目链接 - [312. 戳气球 - 力扣](https://leetcode.cn/problems/burst-balloons/) #### 3.2.2 题目大意 **描述**:有 $n$ 个气球,编号为 $0 \sim n - 1$,每个气球上都有一个数字,这些数字存在数组 $nums$ 中。现在开始戳破气球。其中戳破第 $i$ 个气球,可以获得 $nums[i - 1] \times nums[i] \times nums[i + 1]$ 枚硬币,这里的 $i - 1$ 和 $i + 1$ 代表和 $i$ 相邻的两个气球的编号。如果 $i - 1$ 或 $i + 1$ 超出了数组的边界,那么就当它是一个数字为 $1$ 的气球。 **要求**:求出能获得硬币的最大数量。 **说明**: - $n == nums.length$。 - $1 \le n \le 300$。 - $0 \le nums[i] \le 100$。 **示例**: - 示例 1: ```python 输入:nums = [3,1,5,8] 输出:167 解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> [] coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167 ``` - 示例 2: ```python 输入:nums = [1,5] 输出:10 解释: nums = [1,5] --> [5] --> [] coins = 1*1*5 + 1*5*1 = 10 ``` #### 3.2.3 解题思路 ##### 思路 1:动态规划 根据题意,如果 $i - 1$ 或 $i + 1$ 超出了数组的边界,那么就当它是一个数字为 $1$ 的气球。我们可以预先在 $nums$ 的首尾位置,添加两个数字为 $1$ 的虚拟气球,这样变成了 $n + 2$ 个气球,气球对应编号也变为了 $0 \sim n + 1$。 对应问题也变成了:给定 $n + 2$ 个气球,每个气球上有 $1$ 个数字,代表气球上的硬币数量,当我们戳破气球 $nums[i]$ 时,就能得到对应 $nums[i - 1] \times nums[i] \times nums[i + 1]$ 枚硬币。现在要戳破 $0 \sim n + 1$ 之间的所有气球(不包括编号 $0$ 和编号 $n + 1$ 的气球),请问最多能获得多少枚硬币? ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:戳破所有气球 $i$ 与气球 $j$ 之间的气球(不包含气球 $i$ 和 气球 $j$),所能获取的最多硬币数。 ###### 3. 状态转移方程 假设气球 $i$ 与气球 $j$ 之间最后一个被戳破的气球编号为 $k$。则 $dp[i][j]$ 取决于由 $k$ 作为分割点分割出的两个区间 $(i, k)$ 与 $(k, j)$ 上所能获取的最多硬币数 + 戳破气球 $k$ 所能获得的硬币数,即状态转移方程为: $dp[i][j] = max \lbrace dp[i][k] + dp[k][j] + nums[i] \times nums[k] \times nums[j] \rbrace, \quad i < k < j$ ###### 4. 初始条件 - $dp[i][j]$ 表示的是开区间,则 $i < j - 1$。而当 $i \ge j - 1$ 时,所能获得的硬币数为 $0$,即 $dp[i][j] = 0, \quad i \ge j - 1$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:戳破所有气球 $i$ 与气球 $j$ 之间的气球(不包含气球 $i$ 和 气球 $j$),所能获取的最多硬币数。所以最终结果为 $dp[0][n + 1]$。 ##### 思路 1:代码 ```python class Solution: def maxCoins(self, nums: List[int]) -> int: size = len(nums) arr = [0 for _ in range(size + 2)] arr[0] = arr[size + 1] = 1 for i in range(1, size + 1): arr[i] = nums[i - 1] dp = [[0 for _ in range(size + 2)] for _ in range(size + 2)] for l in range(3, size + 3): for i in range(0, size + 2): j = i + l - 1 if j >= size + 2: break for k in range(i + 1, j): dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + arr[i] * arr[j] * arr[k]) return dp[0][size + 1] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 为气球数量。 - **空间复杂度**:$O(n^2)$。 ## 练习题目 - [0005. 最长回文子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-palindromic-substring.md) - [0516. 最长回文子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-palindromic-subsequence.md) - [0312. 戳气球](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/burst-balloons.md) - [0486. 预测赢家](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/predict-the-winner.md) - [1547. 切棍子的最小成本](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-cost-to-cut-a-stick.md) - [0664. 奇怪的打印机](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/strange-printer.md) - [区间 DP 题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E5%8C%BA%E9%97%B4-dp-%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/08_dynamic_programming/08_12_tree_dp.md ================================================ ## 1. 树形动态规划简介 > **树形动态规划**:简称为「树形 DP」,是一种在树形结构上进行推导的动态规划方法。如下图所示,树形 DP 的求解过程一般以节点从深到浅(子树从小到大)的顺序作为动态规划的「阶段」。在树形 DP 中,第 $1$ 维通常是节点编号,代表以该节点为根的子树。 ![树形 DP](https://qcdn.itcharge.cn/images/20240514113355.png) 树形 DP 问题的划分方法有多种方式。 如果按照「阶段转移的方向」进行划分,可以划分为以下两种: 1. **自底向上**:通过递归的方式求解每棵子树,然后在回溯时,自底向上地从子节点向上进行状态转移。只有在当前节点的所有子树求解完毕之后,才可以求解当前节点,以及继续向上进行求解。 2. **自顶向下**:从根节点开始向下递归,逐层计算子节点的状态。这种方法常常使用记忆化搜索来避免重复计算,提高效率。 自顶向下的树形 DP 问题比较少见,大部分树形 DP 都是采用「自底向上」的方向进行推导。 如果按照「是否有固定根」进行划分,可以划分为以下两种: 1. **固定根的树形 DP**:事先指定根节点的树形 DP 问题,通常只需要从给定的根节点开始,使用 $1$ 次深度优先搜索。 2. **不定根的树形 DP**:事先没有指定根节点的树形 DP 问题,并且根节点的变化会对一些值,例如子节点深度和、点权和等产生影响。通常需要使用 $2$ 次深度优先搜索,第 $1$ 次预处理诸如深度,点权和之类的信息,第 $2$ 次开始运行换根动态规划。 本文中,我们将按照「是否有固定根」进行分类,对树形 DP 问题中这两种类型问题进行一一讲解。 ## 2. 固定根的树形 DP ### 2.1 固定根的树形 DP 基本思路 固定根的树形 DP 问题,如果是二叉树,树通常是以根节点的形式给出。我们可以直接从指定根节点出发进行深度优先搜索。如果是多叉树,树是以一张 $n$ 个节点、$n - 1$ 条边的无向图形式给出的,并且事先给出指定根节点的编号。这种情况下,我们要先用邻接表存储下这 $n$ 个点和 $n - 1$ 条边,然后从指定根节点出发进行深度优先搜索,并注意标记节点是否已经被访问过,以避免在遍历中沿着反向边回到父节点。 下面以这两道题为例,介绍一下树形 DP 的一般解题思路。 ### 2.2 二叉树中的最大路径和 #### 2.2.1 题目链接 - [124. 二叉树中的最大路径和 - 力扣](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) #### 2.2.2 题目大意 **描述**:给定一个二叉树的根节点 $root$。 **要求**:返回其最大路径和。 **说明**: - **路径**:被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中至多出现一次。该路径至少包含一个节点,且不一定经过根节点。 - **路径和**:路径中各节点值的总和。 - 树中节点数目范围是 $[1, 3 * 10^4]$。 - $-1000 \le Node.val \le 1000$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/13/exx1.jpg) ```python 输入:root = [1,2,3] 输出:6 解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/10/13/exx2.jpg) ```python 输入:root = [-10,9,20,null,null,15,7] 输出:42 解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42 ``` #### 2.2.3 解题思路 ##### 思路 1:树形 DP + 深度优先搜索 根据最大路径和中对应路径是否穿过根节点,我们可以将二叉树分为两种: 1. 最大路径和中对应路径穿过根节点。 2. 最大路径和中对应路径不穿过根节点。 如果最大路径和中对应路径穿过根节点,则:**该二叉树的最大路径和 = 左子树中最大贡献值 + 右子树中最大贡献值 + 当前节点值**。 而如果最大路径和中对应路径不穿过根节点,则:**该二叉树的最大路径和 = 所有子树中最大路径和**。 即:**该二叉树的最大路径和 = max(左子树中最大贡献值 + 右子树中最大贡献值 + 当前节点值,所有子树中最大路径和)**。 对此我们可以使用深度优先搜索递归遍历二叉树,并在递归遍历的同时,维护一个最大路径和变量 $ans$。 然后定义函数 ` def dfs(self, node):` 计算二叉树中以该节点为根节点,并且经过该节点的最大贡献值。 计算的结果可能的情况有 $2$ 种: 1. 经过空节点的最大贡献值等于 $0$。 2. 经过非空节点的最大贡献值等于 **当前节点值 + 左右子节点提供的最大贡献值中较大的一个**。如果该贡献值为负数,可以考虑舍弃,即最大贡献值为 $0$。 在递归时,我们先计算左右子节点的最大贡献值,再更新维护当前最大路径和变量。最终 $ans$ 即为答案。具体步骤如下: 1. 如果根节点 $root$ 为空,则返回 $0$。 2. 递归计算左子树的最大贡献值为 $left\_max$。 3. 递归计算右子树的最大贡献值为 $right\_max$。 4. 更新维护最大路径和变量,即 $self.ans = max \lbrace self.ans, \quad left\_max + right\_max + node.val \rbrace$。 5. 返回以当前节点为根节点,并且经过该节点的最大贡献值。即返回 **当前节点值 + 左右子节点提供的最大贡献值中较大的一个**。 6. 最终 $self.ans$ 即为答案。 ##### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def __init__(self): self.ans = float('-inf') def dfs(self, node): if not node: return 0 left_max = max(self.dfs(node.left), 0) # 左子树提供的最大贡献值 right_max = max(self.dfs(node.right), 0) # 右子树提供的最大贡献值 cur_max = left_max + right_max + node.val # 包含当前节点和左右子树的最大路径和 self.ans = max(self.ans, cur_max) # 更新所有路径中的最大路径和 return max(left_max, right_max) + node.val # 返回包含当前节点的子树的最大贡献值 def maxPathSum(self, root: Optional[TreeNode]) -> int: self.dfs(root) return self.ans ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ### 2.3 相邻字符不同的最长路径 #### 2.3.1 题目链接 - [2246. 相邻字符不同的最长路径 - 力扣](https://leetcode.cn/problems/longest-path-with-different-adjacent-characters/) #### 2.3.2 题目大意 **描述**:给定一个长度为 $n$ 的数组 $parent$ 来表示一棵树(即一个连通、无向、无环图)。该树的节点编号为 $0 \sim n - 1$,共 $n$ 个节点,其中根节点的编号为 $0$。其中 $parent[i]$ 表示节点 $i$ 的父节点,由于节点 $0$ 是根节点,所以 $parent[0] == -1$。再给定一个长度为 $n$ 的字符串,其中 $s[i]$ 表示分配给节点 $i$ 的字符。 **要求**:找出路径上任意一对相邻节点都没有分配到相同字符的最长路径,并返回该路径的长度。 **说明**: - $n == parent.length == s.length$。 - $1 \le n \le 10^5$。 - 对所有 $i \ge 1$ ,$0 \le parent[i] \le n - 1$ 均成立。 - $parent[0] == -1$。 - $parent$ 表示一棵有效的树。 - $s$ 仅由小写英文字母组成。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2022/03/25/testingdrawio.png) ```python 输入:parent = [-1,0,0,1,1,2], s = "abacbe" 输出:3 解释:任意一对相邻节点字符都不同的最长路径是:0 -> 1 -> 3 。该路径的长度是 3 ,所以返回 3。 可以证明不存在满足上述条件且比 3 更长的路径。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2022/03/25/graph2drawio.png) ```python 输入:parent = [-1,0,0,0], s = "aabc" 输出:3 解释:任意一对相邻节点字符都不同的最长路径是:2 -> 0 -> 3 。该路径的长度为 3 ,所以返回 3。 ``` #### 2.3.3 解题思路 ##### 思路 1:树形 DP + 深度优先搜索 因为题目给定的是表示父子节点的 $parent$ 数组,为了方便递归遍历相邻节点,我们可以根据 $partent$ 数组,建立一个由父节点指向子节点的有向图 $graph$。 如果不考虑相邻节点是否为相同字符这一条件,那么这道题就是在求树的直径(树的最长路径长度)中的节点个数。 对于根节点为 $u$ 的树来说: 1. 如果其最长路径经过根节点 $u$,则:**最长路径长度 = 某子树中的最长路径长度 + 另一子树中的最长路径长度 + 1**。 2. 如果其最长路径不经过根节点 $u$,则:**最长路径长度 = 某个子树中的最长路径长度**。 即:**最长路径长度 = max(某子树中的最长路径长度 + 另一子树中的最长路径长度 + 1,某个子树中的最长路径长度)**。 对此,我们可以使用深度优先搜索递归遍历 $u$ 的所有相邻节点 $v$,并在递归遍历的同时,维护一个全局最大路径和变量 $ans$,以及当前节点 $u$ 的最大路径长度变量 $u\_len$。 1. 先计算出从相邻节点 $v$ 出发的最长路径长度 $v\_len$。 2. 更新维护全局最长路径长度为 $self.ans = max(self.ans, \quad u\_len + v\_len + 1)$。 3. 更新维护当前节点 $u$ 的最长路径长度为 $u\_len = max(u\_len, \quad v\_len + 1)$。 因为题目限定了「相邻节点字符不同」,所以在更新全局最长路径长度和当前节点 $u$ 的最长路径长度时,我们需要判断一下节点 $u$ 与相邻节点 $v$ 的字符是否相同,只有在字符不同的条件下,才能够更新维护。 最后,因为题目要求的是树的直径(树的最长路径长度)中的节点个数,而:**路径的节点 = 路径长度 + 1**,所以最后我们返回 $self.ans + 1$ 作为答案。 ##### 思路 1:代码 ```python class Solution: def longestPath(self, parent: List[int], s: str) -> int: size = len(parent) # 根据 parent 数组,建立有向图 graph = [[] for _ in range(size)] for i in range(1, size): graph[parent[i]].append(i) ans = 0 def dfs(u): nonlocal ans u_len = 0 # u 节点的最大路径长度 for v in graph[u]: # 遍历 u 节点的相邻节点 v_len = dfs(v) # 相邻节点的最大路径长度 if s[u] != s[v]: # 相邻节点字符不同 ans = max(ans, u_len + v_len + 1) # 维护最大路径长度 u_len = max(u_len, v_len + 1) # 更新 u 节点的最大路径长度 return u_len # 返回 u 节点的最大路径长度 dfs(0) return ans + 1 ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树的节点数目。 - **空间复杂度**:$O(n)$。 ## 3. 不定根的树形 DP ### 3.1 不定根的树形 DP 基本思路 不定根的树形 DP 问题,如果是二叉树,树通常是以一张 $n$ 个节点、$n - 1$ 条边的无向图形式给出的,并且事先没有指定根节点。通常需要以「每个节点为根节点」进行一系列统计。 这种情况下,我们一般通过「两次扫描与换根法」的方法求解这类题目: 1. 第一次扫描时,任选一个节点为根,在「有固定根的树」上执行一次树形 DP,预处理树的一些相关信息。 2. 第二次扫描时,从刚才的根节点出发,对整棵树再执行一次深度优先搜索,同时携带根节点的一些信息提供给子节点进行推导,计算出「换根」之后的解。 ### 3.2 最小高度树 #### 3.2.1 题目链接 - [310. 最小高度树 - 力扣](https://leetcode.cn/problems/minimum-height-trees/) #### 3.2.2 题目大意 **描述**:有一棵包含 $n$ 个节点的树,节点编号为 $0 \sim n - 1$。给定一个数字 $n$ 和一个有 $n - 1$ 条无向边的 $edges$ 列表来表示这棵树。其中 $edges[i] = [ai, bi]$ 表示树中节点 $ai$ 和 $bi$ 之间存在一条无向边。 可以选择树中的任何一个节点作为根,当选择节点 $x$ 作为根节点时,设结果树的高度为 $h$。在所有可能的树种,具有最小高度的树(即 $min(h)$)被成为最小高度树。 **要求**:找到所有的最小高度树并按照任意顺序返回他们的根节点编号列表。 **说明**: - **树的高度**:指根节点和叶子节点之间最长向下路径上边的数量。 - $1 \le n \le 2 \times 10^4$。 - $edges.length == n - 1$。 - $0 \le ai, bi < n$。 - $ai \ne bi$。 - 所有 $(ai, bi)$ 互不相同。 - 给定的输入保证是一棵树,并且不会有重复的边。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/09/01/e1.jpg) ```python 输入:n = 4, edges = [[1,0],[1,2],[1,3]] 输出:[1] 解释:如图所示,当根是标签为 1 的节点时,树的高度是 1 ,这是唯一的最小高度树。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/09/01/e2.jpg) ```python 输入:n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]] 输出:[3,4] ``` #### 3.2.3 解题思路 ##### 思路 1:树形 DP + 二次遍历换根法 最容易想到的做法是:枚举 $n$ 个节点,以每个节点为根节点,然后进行深度优先搜索,求出每棵树的高度。最后求出所有树中的最小高度即为答案。但这种做法的时间复杂度为 $O(n^2)$,而 $n$ 的范围为 $[1, 2 \times 10^4]$,这样做会导致超时,因此需要进行优化。 在上面的算法中,在一轮深度优先搜索中,除了可以得到整棵树的高度之外,在搜索过程中,其实还能得到以每个子节点为根节点的树的高度。如果我们能够利用这些子树的高度信息,快速得到以其他节点为根节点的树的高度,那么我们就能改进算法,以更小的时间复杂度解决这道题。这就是二次遍历与换根法的思想。 1. 第一次遍历:自底向上的计算出每个节点 $u$ 向下走(即由父节点 $u$ 向子节点 $v$ 走)的最长路径 $down1[u]$、次长路径 $down2[i]$,并记录向下走最长路径所经过的子节点 $p[u]$,方便第二次遍历时计算。 2. 第二次遍历:自顶向下的计算出每个节点 $v$ 向上走(即由子节点 $v$ 向父节点 $u$ 走)的最长路径 $up[v]$。需要注意判断 $u$ 向下走的最长路径是否经过了节点 $v$。 1. 如果经过了节点 $v$,则向上走的最长路径,取决于「父节点 $u$ 向上走的最长路径」与「父节点 $u$ 向下走的次长路径」 的较大值,再加上 $1$。 2. 如果没有经过节点 $v$,则向上走的最长路径,取决于「父节点 $u$ 向上走的最长路径」与「父节点 $u$ 向下走的最长路径」 的较大值,再加上 $1$。 3. 接下来,我们通过枚举 $n$ 个节点向上走的最长路径与向下走的最长路径,从而找出所有树中的最小高度,并将所有最小高度树的根节点放入答案数组中并返回。 整个算法具体步骤如下: 1. 使用邻接表的形式存储树。 3. 定义第一个递归函数 `dfs(u, fa)` 用于计算每个节点向下走的最长路径 $down1[u]$、次长路径 $down2[u]$,并记录向下走的最长路径所经过的子节点 $p[u]$。 1. 对当前节点的相邻节点进行遍历。 2. 如果相邻节点是父节点,则跳过。 3. 递归调用 `dfs(v, u)` 函数计算邻居节点的信息。 4. 根据邻居节点的信息计算当前节点的高度,并更新当前节点向下走的最长路径 $down1[u]$、当前节点向下走的次长路径 $down2$、取得最长路径的子节点 $p[u]$。 4. 定义第二个递归函数 `reroot(u, fa)` 用于计算每个节点作为新的根节点时向上走的最长路径 $up[v]$。 1. 对当前节点的相邻节点进行遍历。 2. 如果相邻节点是父节点,则跳过。 3. 根据当前节点 $u$ 的高度和相邻节点 $v$ 的信息更新 $up[v]$。同时需要判断节点 $u$ 向下走的最长路径是否经过了节点 $v$。 1. 如果经过了节点 $v$,则向上走的最长路径,取决于「父节点 $u$ 向上走的最长路径」与「父节点 $u$ 向下走的次长路径」 的较大值,再加上 $1$,即:$up[v] = max(up[u], down2[u]) + 1$。 2. 如果没有经过节点 $v$,则向上走的最长路径,取决于「父节点 $u$ 向上走的最长路径」与「父节点 $u$ 向下走的最长路径」 的较大值,再加上 $1$,即:$up[v] = max(up[u], down1[u]) + 1$。 4. 递归调用 `reroot(v, u)` 函数计算邻居节点的信息。 5. 调用 `dfs(0, -1)` 函数计算每个节点的最长路径。 6. 调用 `reroot(0, -1)` 函数计算每个节点作为新的根节点时的最长路径。 7. 找到所有树中的最小高度。 8. 将所有最小高度的节点放入答案数组中并返回。 ##### 思路 1:代码 ```python class Solution: def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]: graph = [[] for _ in range(n)] for u, v in edges: graph[u].append(v) graph[v].append(u) # down1 用于记录向下走的最长路径 down1 = [0 for _ in range(n)] # down2 用于记录向下走的最长路径 down2 = [0 for _ in range(n)] p = [0 for _ in range(n)] # 自底向上记录最长路径、次长路径 def dfs(u, fa): for v in graph[u]: if v == fa: continue # 自底向上统计信息 dfs(v, u) height = down1[v] + 1 if height >= down1[u]: down2[u] = down1[u] down1[u] = height p[u] = v elif height > down2[u]: down2[u] = height # 进行换根动态规划,自顶向下统计向上走的最长路径 up = [0 for _ in range(n)] def reroot(u, fa): for v in graph[u]: if v == fa: continue if p[u] == v: up[v] = max(up[u], down2[u]) + 1 else: up[v] = max(up[u], down1[u]) + 1 # 自顶向下统计信息 reroot(v, u) dfs(0, -1) reroot(0, -1) # 找到所有树中的最小高度 min_h = 1e9 for i in range(n): min_h = min(min_h, max(down1[i], up[i])) # 将所有最小高度的节点放入答案数组中并返回 res = [] for i in range(n): if max(down1[i], up[i]) == min_h: res.append(i) return res ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ## 练习题目 - [0687. 最长同值路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/longest-univalue-path.md) - [1617. 统计子树中城市之间最大距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/count-subtrees-with-max-distance-between-cities.md) - [0834. 树中距离之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/sum-of-distances-in-tree.md) - [树形 DP 题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E7%8A%B6%E6%80%81%E5%8E%8B%E7%BC%A9-dp-%E9%A2%98%E7%9B%AE) ## 参考资料 - 【题解】[C++ 容易理解的换根动态规划解法 - 最小高度树](https://leetcode.cn/problems/minimum-height-trees/solution/c-huan-gen-by-vclip-sa84/) - 【题解】[310. 最小高度树 - 最小高度树 - 力扣](https://leetcode.cn/problems/minimum-height-trees/solution/310-zui-xiao-gao-du-shu-by-vincent-40-teg8/) - 【题解】[310. 最小高度树 - 最小高度树 - 力扣](https://leetcode.cn/problems/minimum-height-trees/solution/310-zui-xiao-gao-du-shu-by-vincent-40-teg8/) ================================================ FILE: docs/08_dynamic_programming/08_13_state_compression_dp.md ================================================ ## 1. 状态压缩动态规划简介 > **状态压缩动态规划(状压 DP)**:是一种适用于「小规模数据」的数组或字符串问题的动态规划方法。它利用二进制的特性,将集合的选取状态压缩为一个整数,通过位运算实现高效的状态表示与转移。 在「位运算知识」章节中,我们已经学习过「二进制枚举子集」的方法。这里先简要回顾其核心思想。 ### 1.1 二进制枚举子集 对于一个包含 $n$ 个元素的集合 $S$,每个元素都有「选」或「不选」两种状态。我们可以用一个 $n$ 位的二进制数来表示集合 $S$ 的一个子集:第 $i$ 位为 $1$ 表示选取第 $i$ 个元素,为 $0$ 表示不选。 举例说明,设 $S = \lbrace 5, 4, 3, 2, 1 \rbrace$,用 $5$ 位二进制数表示其子集: - $11111_{(2)}$ 表示选取所有元素,即 $S$ 本身: | 元素位置 | 5 | 4 | 3 | 2 | 1 | | :-------------- | :-: | :-: | :-: | :-: | :-: | | 选取状态 | 选取 | 选取 | 选取 | 选取 | 选取 | | 二进制位 | 1 | 1 | 1 | 1 | 1 | - $10101_{(2)}$ 表示选取第 $1$、$3$、$5$ 位元素,即 $\lbrace 5, 3, 1 \rbrace$: | 元素位置 | 5 | 4 | 3 | 2 | 1 | | :-------------- | :-: | :-: | :-: | :-: | :-: | | 选取状态 | 选取 | 未选取 | 选取 | 未选取 | 选取 | | 二进制位 | 1 | 0 | 1 | 0 | 1 | - $01001_{(2)}$ 表示选取第 $1$、$4$ 位元素,即 $\lbrace 4, 1 \rbrace$: | 元素位置 | 5 | 4 | 3 | 2 | 1 | | :-------------- | :-: | :-: | :-: | :-: | :-: | | 选取状态 | 未选取 | 选取 | 未选取 | 未选取 | 选取 | | 二进制位 | 0 | 1 | 0 | 0 | 1 | 由此可见,对于长度为 $n$ 的集合 $S$,只需枚举 $0$ 到 $2^n-1$ 之间的所有整数(共 $2^n$ 种情况),即可遍历 $S$ 的所有子集。 > 综上所述: > > - 长度为 $n$ 的集合 $S$ 的所有子集,可以通过枚举 $0 \sim 2^n-1$ 的二进制数来表示,每一位代表对应元素的选取状态。 ### 1.2 状态定义与状态转移 #### 1.2.1 状态定义 在状态压缩 DP 中,通常用一个二进制数来表示集合中每个元素的选取状态。对于 $n$ 个元素的集合,可以用一个 $n$ 位的二进制数 $state$,其中第 $i$ 位为 $1$ 表示第 $i$ 个元素被选中,为 $0$ 表示未被选中。 这种表示方法与「二进制枚举子集」类似,每一位都精确对应集合中某个元素的选择情况。通过这种方式,可以高效地描述和操作所有可能的选取状态。 #### 1.2.2 状态转移 状态压缩 DP 的状态转移主要有两种常见方式: 1. **枚举子集**:对于当前状态,枚举其所有子集,或通过枚举每个元素,找到去掉某个元素后的子状态。根据子状态的值和当前状态的关系,更新当前状态的最优解。 2. **枚举超集**:对于当前状态,枚举其所有超集。根据超集的值和当前状态的关系,更新当前状态的最优解。 实际应用中,「枚举子集」是最常用的状态转移方式。 ### 1.3 状压 DP 的适用范围 对于包含 $n$ 个元素的集合,其所有子集的状态总数为 $2^n$,状态数量随 $n$ 呈指数级增长。因此,状态压缩 DP 仅适用于 $n$ 较小的场景(一般 $n \leq 20$)。当 $n$ 较大时,状态数过多,算法效率难以保证,容易超时。 ## 2. 状态压缩 DP 常用位运算技巧 在状态压缩 DP(状压 DP)中,通常用一个整数的二进制位来表示集合的选取状态。对集合的各种操作,本质上就是对二进制数的位运算。下面总结了常用的位运算技巧,设 $n$ 为集合元素个数,$A$、$B$ 为集合对应的二进制状态,$i$ 表示第 $i$ 个元素的位置(从 $0$ 开始): - **总状态数**:`1 << n`(即 $2^n$,所有子集的数量) - **加入第 $i$ 个元素**:`A = A | (1 << i)`(将第 $i$ 位设为 $1$) - **删除第 $i$ 个元素**:`A = A & ~(1 << i)`(将第 $i$ 位设为 $0$) - **判断是否选中第 $i$ 个元素**:`if A & (1 << i):` 或 `if (A >> i) & 1:` - **置空集**:`A = 0` - **置全集**:`A = (1 << n) - 1` - **求补集**:`A = A ^ ((1 << n) - 1)` - **并集**:`A | B` - **交集**:`A & B` - **枚举集合 $A$ 的所有子集(包含 $A$ 本身)**: ```python subA = A while subA: ... subA = (subA - 1) & A # 枚举下一个子集 # 注意:如果需要包含空集,可以在循环后补充 subA = 0 的情况。 ``` - **枚举全集的所有子集**: ```python for state in range(1 << n): # state 表示当前子集 for i in range(n): # 枚举每一位 if (state >> i) & 1: # 第 i 位为 1,表示选中了第 i 个元素 ... ``` ## 3. 状态压缩 DP 的应用 ### 3.1 经典例题:两个数组最小的异或值之和 #### 3.1.1 题目链接 - [1879. 两个数组最小的异或值之和 - 力扣](https://leetcode.cn/problems/minimum-xor-sum-of-two-arrays/) #### 3.1.2 题目大意 **描述**:给定两个整数数组 $nums1$ 和 $nums2$,两个数组长度都为 $n$。 **要求**:将 $nums2$ 中的元素重新排列,使得两个数组的异或值之和最小。并返回重新排列之后的异或值之和。 **说明**: - **两个数组的异或值之和**:$(nums1[0] \oplus nums2[0]) + (nums1[1] \oplus nums2[1]) + ... + (nums1[n - 1] \oplus nums2[n - 1])$(下标从 $0$ 开始)。 - 举个例子,$[1, 2, 3]$ 和 $[3,2,1]$ 的异或值之和 等于 $(1 \oplus 3) + (2 \oplus 2) + (3 \oplus 1) + (3 \oplus 1) = 2 + 0 + 2 = 4$。 - $n == nums1.length$。 - $n == nums2.length$。 - $1 \le n \le 14$。 - $0 \le nums1[i], nums2[i] \le 10^7$。 **示例**: - 示例 1: ```python 输入:nums1 = [1,2], nums2 = [2,3] 输出:2 解释:将 nums2 重新排列得到 [3,2] 。 异或值之和为 (1 XOR 3) + (2 XOR 2) = 2 + 0 = 2。 ``` - 示例 2: ```python 输入:nums1 = [1,0,3], nums2 = [5,3,4] 输出:8 解释:将 nums2 重新排列得到 [5,4,3] 。 异或值之和为 (1 XOR 5) + (0 XOR 4) + (3 XOR 3) = 4 + 4 + 0 = 8。 ``` #### 3.1.3 解题思路 ##### 思路 1:状态压缩 DP 由于 $nums2$ 可以任意重排,我们可以固定 $nums1$ 的顺序,依次为 $nums1$ 的每个元素选择 $nums2$ 中尚未被选过的元素,使得异或值之和最小。 考虑到 $n$ 的范围较小($1 \leq n \leq 14$),我们可以用「状态压缩」来表示 $nums2$ 的选取情况。具体做法是用一个 $n$ 位二进制数 $state$,其中第 $i$ 位为 $1$ 表示 $nums2$ 的第 $i$ 个元素已被选中,为 $0$ 表示未被选中。 例如: 1. $nums2 = \lbrace 1, 2, 3, 4 \rbrace, state = (1001)_2$,表示选择了第 $1$ 个和第 $4$ 个元素,即 $1$ 和 $4$。 2. $nums2 = \lbrace 1, 2, 3, 4, 5, 6 \rbrace, state = (011010)_2$,表示选择了第 $2$、$4$、$5$ 个元素,即 $2$、$4$、$5$。 基于此,我们可以设计动态规划: ###### 1. 阶段划分 以 $nums2$ 的选取状态 $state$ 作为阶段。 ###### 2. 定义状态 设 $dp[state]$ 表示当前 $nums2$ 选取状态为 $state$,且已为 $nums1$ 的前 $count(state)$ 个元素分配了数时,能得到的最小异或值之和。其中 $count(state)$ 表示 $state$ 中 $1$ 的个数。 ###### 3. 状态转移方程 对于每个 $state$,我们可以枚举 $state$ 中每一个为 $1$ 的位置 $i$,表示最后一个被选的 $nums2[i]$。则 $dp[state]$ 可以由 $dp[state \oplus (1 \ll i)]$ 转移而来,转移代价为 $nums1[count(state)-1] \oplus nums2[i]$。即: $$ dp[state] = \min_{i \text{ 满足 } (state \gg i) \& 1} \left\{ dp[state \oplus (1 \ll i)] + (nums1[count(state)-1] \oplus nums2[i]) \right\} $$ ###### 4. 初始条件 - 所有 $dp[state]$ 初始化为无穷大。 - $dp[0] = 0$,即未选任何元素时异或和为 $0$。 ###### 5. 最终结果 最终答案为 $dp[states - 1]$,其中 $states = 1 \ll n$,即所有元素都被选中的状态。 ##### 思路 1:代码 ```python class Solution: def minimumXORSum(self, nums1: List[int], nums2: List[int]) -> int: ans = float('inf') size = len(nums1) states = 1 << size dp = [float('inf') for _ in range(states)] dp[0] = 0 for state in range(states): one_cnt = bin(state).count('1') for i in range(size): if (state >> i) & 1: dp[state] = min(dp[state], dp[state ^ (1 << i)] + (nums1[i] ^ nums2[one_cnt - 1])) return dp[states - 1] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n \times n)$,其中 $n$ 是数组 $nums1$、$nums2$ 的长度。 - **空间复杂度**:$O(2^n)$。 ### 3.2 经典例题:数组的最大与和 #### 3.2.1 题目链接 - [2172. 数组的最大与和 - 力扣](https://leetcode.cn/problems/maximum-and-sum-of-array/) #### 3.2.2 题目大意 **描述**:给定一个长度为 $n$ 的整数数组 $nums$ 和一个整数 $numSlots$ 满足 $2 \times numSlots \ge n$。一共有 $numSlots$ 个篮子,编号为 $1 \sim numSlots$。 现在需要将所有 $n$ 个整数分到这些篮子中,且每个篮子最多有 $2$ 个整数。 **要求**:返回将 $nums$ 中所有数放入 $numSlots$ 个篮子中的最大与和。 **说明**: - **与和**:当前方案中,每个数与它所在篮子编号的按位与运算结果之和。 - 比如,将数字 $[1, 3]$ 放入篮子 $1$ 中,$[4, 6]$ 放入篮子 $2$ 中,这个方案的与和为 $(1 \text{ AND } 1) + (3 \text{ AND } 1) + (4 \text{ AND } 2) + (6 \text{ AND } 2) = 1 + 1 + 0 + 2 = 4$。 - $n == nums.length$。 - $1 \le numSlots \le 9$。 - $1 \le n \le 2 \times numSlots$。 - $1 \le nums[i] \le 15$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,4,5,6], numSlots = 3 输出:9 解释:一个可行的方案是 [1, 4] 放入篮子 1 中,[2, 6] 放入篮子 2 中,[3, 5] 放入篮子 3 中。 最大与和为 (1 AND 1) + (4 AND 1) + (2 AND 2) + (6 AND 2) + (3 AND 3) + (5 AND 3) = 1 + 0 + 2 + 2 + 3 + 1 = 9。 ``` - 示例 2: ```python 输入:nums = [1,3,10,4,7,1], numSlots = 9 输出:24 解释:一个可行的方案是 [1, 1] 放入篮子 1 中,[3] 放入篮子 3 中,[4] 放入篮子 4 中,[7] 放入篮子 7 中,[10] 放入篮子 9 中。 最大与和为 (1 AND 1) + (1 AND 1) + (3 AND 3) + (4 AND 4) + (7 AND 7) + (10 AND 9) = 1 + 1 + 3 + 4 + 7 + 8 = 24 。 注意,篮子 2 ,5 ,6 和 8 是空的,这是允许的。 ``` #### 3.2.3 解题思路 ##### 思路 1:状态压缩 DP 由于每个篮子最多能放 2 个整数,我们可以将每个篮子拆分为 2 个「格子」,这样总共有 $2 \times numSlots$ 个格子,每个格子最多放 $1$ 个整数。 考虑到 $numSlots$ 的范围为 $[1, 9]$,因此总格子数 $2 \times numSlots$ 也仅为 $[2, 18]$,状态空间较小,适合用二进制压缩表示每个格子的放置情况。 具体地,我们用一个 $2 \times numSlots$ 位的二进制数 $state$ 表示所有格子的放置状态:$state$ 的第 $i$ 位为 $1$ 表示第 $i$ 个格子已放入整数,为 $0$ 表示为空。 基于此,可以设计动态规划求解。 ###### 1. 阶段划分 以 $2 \times numSlots$ 个格子的放置状态 $state$ 作为阶段。 ###### 2. 定义状态 设 $dp[state]$ 表示:当前已放入 $count(state)$ 个整数,且格子状态为 $state$ 时,能获得的最大与和。 ###### 3. 状态转移方程 对于每个状态 $state$,它一定是由某个少放一个整数的状态 $prev = state \oplus (1 \ll i)$ 转移而来。我们枚举 $state$ 的每一位 $i$,如果 $state$ 的第 $i$ 位为 $1$,则可以尝试将第 $count(state)-1$ 个整数放入第 $i$ 个格子,格子编号为 $i // 2 + 1$,对应的与和为 $(i // 2 + 1) \& nums[count(state) - 1]$。状态转移为: $$ dp[state] = \max\left(dp[state],\ dp[state \oplus (1 \ll i)] + ((i // 2 + 1) \& nums[count(state) - 1])\right) $$ 其中: 1. $state$ 的第 $i$ 位为 $1$,表示当前格子已放入整数。 2. $state \oplus (1 \ll i)$ 表示去掉第 $i$ 个格子的状态。 3. $i // 2 + 1$ 是该格子对应的篮子编号。 4. $nums[count(state) - 1]$ 是当前要放入的整数。 ###### 4. 初始条件 - $dp[0] = 0$,即所有格子为空时,与和为 0。 ###### 5. 最终结果 最终答案为所有 $dp[state]$ 中 $count(state) = n$(即所有整数都已放入)的最大值,即 $max(dp)$。 > 注意:当 $count(state) > len(nums)$ 时,状态无效,应跳过。 ##### 思路 1:代码 ```python class Solution: def maximumANDSum(self, nums: List[int], numSlots: int) -> int: states = 1 << (numSlots * 2) dp = [0 for _ in range(states)] for state in range(states): one_cnt = bin(state).count('1') if one_cnt > len(nums): continue for i in range(numSlots * 2): if (state >> i) & 1: dp[state] = max(dp[state], dp[state ^ (1 << i)] + ((i // 2 + 1) & nums[one_cnt - 1])) return max(dp) ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(2^m \times m)$,其中 $m = 2 \times numSlots$。 - **空间复杂度**:$O(2^m)$。 ## 练习题目 - [1879. 两个数组最小的异或值之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/minimum-xor-sum-of-two-arrays.md) - [1947. 最大兼容性评分和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/maximum-compatibility-score-sum.md) - [0526. 优美的排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/beautiful-arrangement.md) - [状态压缩 DP 题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E7%8A%B6%E6%80%81%E5%8E%8B%E7%BC%A9-dp-%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/08_dynamic_programming/08_14_counting_dp.md ================================================ ## 1. 计数类 DP 简介 ### 1.1 计数问题简介 > **计数问题**:计算满足特定条件下的可行方案数目的问题。 这里的「可行方案数目」指的是某个问题一共有多少种方法。 「计数问题」本身是组合数学中的重要内容,这类问题通常有两种经典的求解方法: 1. 找到递归关系,然后以动态规划的方式,列出递推式,然后求解出方案数。 2. 转为数学问题,计算出对应的组合数,如:卡特兰数、快速幂、排列数、组合数等。 在解决具体问题时,还需要根据题目的具体情况进行分析。一般情况下,我们使用第 $1$ 种方法来解决。这是因为: 1. 采用动态规划的方式能够高效的处理大规模计数问题,并且使用较少时间和空间复杂度。 2. 即使找到了组合数,还要面临计算组合数的困难(高阶组合数计算困难、计算效率较低)。 所以我们通常使用「动态规划」的方法来解决计数问题。 ### 1.2 计数类 DP 简介 > **计数类 DP**:一类使用动态规划方法来统计可行方案数目的问题。区别于求解最优解,计数类 DP 需要统计所有满足条件的可行解数量,同时需要满足不重复、不遗漏的条件。 计数类 DP 的核心思想就是:通过动态规划的算法思想,去计算出解决这个问题有多少种方法。一般来说,计数类 DP 只关注方案数目,不关注具体方案情况。 比如,从一个矩阵的左上角走到右下角,每次只能向右走或者向下走,一共有多少条不同路径。**注意**:这里求解的是有多少条,而不是具体路线的走法。 ## 2. 计数类 DP 的应用 ### 2.1 不同路径 #### 2.1.1 题目链接 - [62. 不同路径 - 力扣](https://leetcode.cn/problems/unique-paths/) #### 2.1.2 题目大意 **描述**:给定两个整数 $m$ 和 $n$,代表大小为 $m \times n$ 的棋盘, 一个机器人位于棋盘左上角的位置,机器人每次只能向右、或者向下移动一步。 **要求**:计算出机器人从棋盘左上角到达棋盘右下角一共有多少条不同的路径。 **说明**: - $1 \le m, n \le 100$。 - 题目数据保证答案小于等于 $2 \times 10^9$。 **示例**: - 示例 1: ```python 输入:m = 3, n = 7 输出:28 ``` ![](https://assets.leetcode.com/uploads/2018/10/22/robot_maze.png) - 示例 2: ```python 输入:m = 3, n = 2 输出:3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 3. 向下 -> 向右 -> 向下 ``` #### 2.1.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 按照路径的结尾位置(行位置、列位置组成的二维坐标)进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 为:从左上角到达位置 $(i, j)$ 的路径数量。 ###### 3. 状态转移方程 因为我们每次只能向右、或者向下移动一步,因此想要走到 $(i, j)$,只能从 $(i - 1, j)$ 向下走一步走过来;或者从 $(i, j - 1)$ 向右走一步走过来。所以可以写出状态转移方程为:$dp[i][j] = dp[i - 1][j] + dp[i][j - 1]$,此时 $i > 0, j > 0$。 ###### 4. 初始条件 - 从左上角走到 $(0, 0)$ 只有一种方法,即 $dp[0][0] = 1$。 - 第一行元素只有一条路径(即只能通过前一个元素向右走得到),所以 $dp[0][j] = 1$。 - 同理,第一列元素只有一条路径(即只能通过前一个元素向下走得到),所以 $dp[i][0] = 1$。 ###### 5. 最终结果 根据状态定义,最终结果为 $dp[m - 1][n - 1]$,即从左上角到达右下角 $(m - 1, n - 1)$ 位置的路径数量为 $dp[m - 1][n - 1]$。 ##### 思路 1:动态规划代码 ```python class Solution: def uniquePaths(self, m: int, n: int) -> int: dp = [[0 for _ in range(n)] for _ in range(m)] for j in range(n): dp[0][j] = 1 for i in range(m): dp[i][0] = 1 for i in range(1, m): for j in range(1, n): dp[i][j] = dp[i - 1][j] + dp[i][j - 1] return dp[m - 1][n - 1] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。初始条件赋值的时间复杂度为 $O(m + n)$,两重循环遍历的时间复杂度为 $O(m * n)$,所以总体时间复杂度为 $O(m \times n)$。 - **空间复杂度**:$O(m \times n)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(m \times n)$。因为 $dp[i][j]$ 的状态只依赖于上方值 $dp[i - 1][j]$ 和左侧值 $dp[i][j - 1]$,而我们在进行遍历时的顺序刚好是从上至下、从左到右。所以我们可以使用长度为 $m$ 的一维数组来保存状态,从而将空间复杂度优化到 $O(m)$。 ### 2.2 整数拆分 #### 2.2.1 题目链接 - [343. 整数拆分 - 力扣](https://leetcode.cn/problems/integer-break/) #### 2.2.2 题目大意 **描述**:给定一个正整数 $n$,将其拆分为 $k (k \ge 2)$ 个正整数的和,并使这些整数的乘积最大化。 **要求**:返回可以获得的最大乘积。 **说明**: - $2 \le n \le 58$。 **示例**: - 示例 1: ```python 输入: n = 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。 ``` - 示例 2: ```python 输入: n = 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。 ``` #### 2.2.3 解题思路 ##### 思路 1:动态规划 ###### 1. 阶段划分 按照正整数进行划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为:将正整数 $i$ 拆分为至少 $2$ 个正整数的和之后,这些正整数的最大乘积。 ###### 3. 状态转移方程 当 $i \ge 2$ 时,假设正整数 $i$ 拆分出的第 $1$ 个正整数是 $j(1 \le j < i)$,则有两种方法: 1. 将 $i$ 拆分为 $j$ 和 $i - j$ 的和,且 $i - j$ 不再拆分为多个正整数,此时乘积为:$j \times (i - j)$。 2. 将 $i$ 拆分为 $j$ 和 $i - j$ 的和,且 $i - j$ 继续拆分为多个正整数,此时乘积为:$j \times dp[i - j]$。 则 $dp[i]$ 取两者中的最大值。即:$dp[i] = max(j \times (i - j), j \times dp[i - j])$。 由于 $1 \le j < i$,需要遍历 $j$ 得到 $dp[i]$ 的最大值,则状态转移方程如下: $dp[i] = max_{1 \le j < i}\lbrace max(j \times (i - j), j \times dp[i - j]) \rbrace$。 ###### 4. 初始条件 - $0$ 和 $1$ 都不能被拆分,所以 $dp[0] = 0, dp[1] = 0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示为:将正整数 $i$ 拆分为至少 $2$ 个正整数的和之后,这些正整数的最大乘积。则最终结果为 $dp[n]$。 ##### 思路 1:代码 ```python class Solution: def integerBreak(self, n: int) -> int: dp = [0 for _ in range(n + 1)] for i in range(2, n + 1): for j in range(i): dp[i] = max(dp[i], (i - j) * j, dp[i - j] * j) return dp[n] ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(n)$。 ## 练习题目 - [0063. 不同路径 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths-ii.md) - [0343. 整数拆分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/integer-break.md) - [1137. 第 N 个泰波那契数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/n-th-tribonacci-number.md) - [计数 DP 题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%95%B0%E4%BD%8D-dp-%E9%A2%98%E7%9B%AE) ================================================ FILE: docs/08_dynamic_programming/08_15_digit_dp.md ================================================ ## 1. 数位 DP 简介 ### 1.1 数位 DP 简介 > **数位动态规划**:简称为「数位 DP」,是一种与数位相关的一类计数类动态规划问题,即在数位上进行动态规划。这里的数位指的是个位、十位、百位、千位等。 数位 DP 一般用于求解给定区间 $[left, right]$ 中,满足特定条件的数值个数,或者用于求解满足特定条件的第 $k$ 小数。 数位 DP 通常有以下几个特征: 1. 题目会提供一个查询区间(有时也会只提供区间上界)来作为统计限制。 2. 题目中给定区间往往很大(比如 $10^9$),无法采用朴素的方法求解。 3. 题目中给定的给定的限定条件往往与数位有关。 4. 要求统计满足特定条件的数值个数,或者用于求解满足特定条件的第 $k$ 小数。 题目要求一段区间 $[left, right]$ 内满足特定条件的数值个数,如果能找到方法计算出前缀区间 $[0, n]$ 内满足特定条件的数值个数,那么我们就可以利用「前缀和思想」,分别计算出区间 $[0, left - 1]$ 与区间 $[0, right]$ 内满足特定条件的数值个数,然后将两者相减即为所求答案。即:$res[left, right] = res[0, right] - res[0, left - 1]$。 在使用「前缀和思想」思想后,问题转换为计算区间 $[0, n]$ 内满足特定条件的数值个数。 接下来就要用到数位 DP 的基本思想。 > **数位 DP 的基本思想**:将区间数字拆分为数位,然后逐位进行确定。 我们通过将区间上的数字按照数位进行拆分,然后逐位确定每一个数位上的可行方案,从而计算出区间内的可行方案个数。 数位 DP 可以通过「记忆化搜索」的方式实现,也可以通过「迭代递推」的方式实现。因为数位 DP 中需要考虑的参数很多,使用「记忆化搜索」的方式更加方便传入参数,所以这里我们采用「记忆化搜索」的方式来实现。 在使用「记忆化搜索」的时候,需要考虑的参数有: 1. 当前枚举的数位位置($pos$)。 2. 前一位数位(或前几位数位)的情况,比如前几位的总和($total$)、某个数字出现次数($cnt$)、前几位所选数字集合(通常使用「状态压缩」的方式,即用一个二进制整数 $state$ 来表示)等等。 3. 前一位数位(或前几位数位)是否等于上界的前几位数字($isLimit$),用于限制本次搜索的数位范围。 4. 前一位数位是否填了数字($isNum$),如果前一位数位填了数字,则当前位可以从 $0$ 开始填写数字;如果前一位没有填写数字,则当前位可以跳过,或者从 $1$ 开始填写数字。 5. 当前位数位所能选择的最小数字($minX$)和所能选择的最大数字($maxX$)。 对应代码如下: ```python class Solution: def digitDP(self, n: int) -> int: # 将 n 转换为字符串 s s = str(n) @cache # pos: 第 pos 个数位 # state: 之前选过的数字集合。 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 # isNum: 表示 pos 前面的数位是否填了数字。 # 如果为真,则当前位不可跳过;如果为假,则当前位可跳过。 def dfs(pos, state, isLimit, isNum): if pos == len(s): # isNum 为 True,则表示当前方案符合要求 return int(isNum) ans = 0 if not isNum: # 如果 isNumb 为 False,则可以跳过当前数位 ans = dfs(pos + 1, state, False, False) # 如果前一位没有填写数字,则最小可选择数字为 0,否则最少为 1(不能含有前导 0)。 minX = 0 if isNum else 1 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s[pos]) if isLimit else 9 # 枚举可选择的数字 for x in range(minX, maxX + 1): # x 不在选择的数字集合中,即之前没有选择过 x if (state >> x) & 1 == 0: ans += dfs(pos + 1, state | (1 << x), isLimit and x == maxX, True) return ans return dfs(0, 0, True, False) ``` 接下来,我们通过一道简单的例题来具体了解一下数位 DP 以及解题思路。 ### 1.2 统计特殊整数 #### 1.2.1 题目大意 **描述**:给定一个正整数 $n$。 **要求**:求区间 $[1, n]$ 内的所有整数中,特殊整数的数目。 **说明**: - **特殊整数**:如果一个正整数的每一个数位都是互不相同的,则称它是特殊整数。 - $1 \le n \le 2 \times 10^9$。 **示例**: - 示例 1: ```python 输入:n = 20 输出:19 解释:1 到 20 之间所有整数除了 11 以外都是特殊整数。所以总共有 19 个特殊整数。 ``` - 示例 2: ```python 输入:n = 5 输出:5 解释:1 到 5 所有整数都是特殊整数。 ``` #### 1.2.2 解题思路 ##### 思路 1:动态规划 + 数位 DP 将 $n$ 转换为字符串 $s$,定义递归函数 `def dfs(pos, state, isLimit, isNum):` 表示构造第 $pos$ 位及之后所有数位的合法方案数。接下来按照如下步骤进行递归。 1. 从 `dfs(0, 0, True, False)` 开始递归。 `dfs(0, 0, True, False)` 表示: 1. 从位置 $0$ 开始构造。 2. 初始没有使用数字(即前一位所选数字集合为 $0$)。 3. 开始时受到数字 $n$ 对应最高位数位的约束。 4. 开始时没有填写数字。 2. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时: 1. 如果 $isNum == True$,说明当前方案符合要求,则返回方案数 $1$。 2. 如果 $isNum == False$,说明当前方案不符合要求,则返回方案数 $0$。 3. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:$ans = 0$。 4. 如果遇到 $isNum == False$,说明之前位数没有填写数字,当前位可以跳过,这种情况下方案数等于 $pos + 1$ 位置上没有受到 $pos$ 位的约束,并且之前没有填写数字时的方案数,即:`ans = dfs(i + 1, state, False, False)`。 5. 如果 $isNum == True$,则当前位必须填写一个数字。此时: 1. 根据 $isNum$ 和 $isLimit$ 来决定填当前位数位所能选择的最小数字($minX$)和所能选择的最大数字($maxX$), 2. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $x$。 3. 如果之前没有选择 $x$,即 $x$ 不在之前选择的数字集合 $state$ 中,则方案数累加上当前位选择 $x$ 之后的方案数,即:`ans += dfs(pos + 1, state | (1 << x), isLimit and x == maxX, True)`。 1. `state | (1 << x)` 表示之前选择的数字集合 $state$ 加上 $x$。 2. `isLimit and x == maxX` 表示 $pos + 1$ 位受到之前位限制和 $pos$ 位限制。 3. $isNum == True$ 表示 $pos$ 位选择了数字。 ##### 思路 1:代码 ```python class Solution: def countSpecialNumbers(self, n: int) -> int: # 将 n 转换为字符串 s s = str(n) @cache # pos: 第 pos 个数位 # state: 之前选过的数字集合。 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 # isNum: 表示 pos 前面的数位是否填了数字。 # 如果为真,则当前位不可跳过;如果为假,则当前位可跳过。 def dfs(pos, state, isLimit, isNum): if pos == len(s): # isNum 为 True,则表示当前方案符合要求 return int(isNum) ans = 0 if not isNum: # 如果 isNumb 为 False,则可以跳过当前数位 ans = dfs(pos + 1, state, False, False) # 如果前一位没有填写数字,则最小可选择数字为 0,否则最少为 1(不能含有前导 0)。 minX = 0 if isNum else 1 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s[pos]) if isLimit else 9 # 枚举可选择的数字 for x in range(minX, maxX + 1): # x 不在选择的数字集合中,即之前没有选择过 x if (state >> x) & 1 == 0: ans += dfs(pos + 1, state | (1 << x), isLimit and x == maxX, True) return ans return dfs(0, 0, True, False) ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n \times 10 \times 2^{10})$,其中 $n$ 为给定整数。 - **空间复杂度**:$O(\log n \times 2^{10})$。 ## 2. 数位 DP 的应用 ### 2.1 至少有 1 位重复的数字 #### 2.1.1 题目链接 - [1012. 至少有 1 位重复的数字 - 力扣](https://leetcode.cn/problems/numbers-with-repeated-digits/) #### 2.1.2 题目大意 **描述**:给定一个正整数 $n$。 **要求**:返回在 $[1, n]$ 范围内具有至少 $1$ 位重复数字的正整数的个数。 **说明**: - $1 \le n \le 10^9$。 **示例**: - 示例 1: ```python 输入:n = 20 输出:1 解释:具有至少 1 位重复数字的正数(<= 20)只有 11。 ``` - 示例 2: ```python 输入:n = 100 输出:10 解释:具有至少 1 位重复数字的正数(<= 100)有 11,22,33,44,55,66,77,88,99 和 100。 ``` #### 2.1.3 解题思路 ##### 思路 1:动态规划 + 数位 DP 正向求解在 $[1, n]$ 范围内具有至少 $1$ 位重复数字的正整数的个数不太容易,我们可以反向思考,先求解出在 $[1, n]$ 范围内各位数字都不重复的正整数的个数 $ans$,然后 $n - ans$ 就是题目答案。 将 $n$ 转换为字符串 $s$,定义递归函数 `def dfs(pos, state, isLimit, isNum):` 表示构造第 $pos$ 位及之后所有数位的合法方案数。接下来按照如下步骤进行递归。 1. 从 `dfs(0, 0, True, False)` 开始递归。 `dfs(0, 0, True, False)` 表示: 1. 从位置 $0$ 开始构造。 2. 初始没有使用数字(即前一位所选数字集合为 $0$)。 3. 开始时受到数字 $n$ 对应最高位数位的约束。 4. 开始时没有填写数字。 2. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时: 1. 如果 $isNum == True$,说明当前方案符合要求,则返回方案数 $1$。 2. 如果 $isNum == False$,说明当前方案不符合要求,则返回方案数 $0$。 3. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:$ans = 0$。 4. 如果遇到 $isNum == False$,说明之前位数没有填写数字,当前位可以跳过,这种情况下方案数等于 $pos + 1$ 位置上没有受到 $pos$ 位的约束,并且之前没有填写数字时的方案数,即:`ans = dfs(i + 1, state, False, False)`。 5. 如果 $isNum == True$,则当前位必须填写一个数字。此时: 1. 根据 $isNum$ 和 $isLimit$ 来决定填当前位数位所能选择的最小数字($minX$)和所能选择的最大数字($maxX$), 2. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $d$。 3. 如果之前没有选择 $d$,即 $d$ 不在之前选择的数字集合 $state$ 中,则方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, state | (1 << d), isLimit and d == maxX, True)`。 1. `state | (1 << d)` 表示之前选择的数字集合 $state$ 加上 $d$。 2. `isLimit and d == maxX` 表示 $pos + 1$ 位受到之前位限制和 $pos$ 位限制。 3. $isNum == True$ 表示 $pos$ 位选择了数字。 6. 最后的方案数为 `n - dfs(0, 0, True, False)`,将其返回即可。 ##### 思路 1:代码 ```python class Solution: def numDupDigitsAtMostN(self, n: int) -> int: # 将 n 转换为字符串 s s = str(n) @cache # pos: 第 pos 个数位 # state: 之前选过的数字集合。 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 # isNum: 表示 pos 前面的数位是否填了数字。 # 如果为真,则当前位不可跳过;如果为假,则当前位可跳过。 def dfs(pos, state, isLimit, isNum): if pos == len(s): # isNum 为 True,则表示当前方案符合要求 return int(isNum) ans = 0 if not isNum: # 如果 isNumb 为 False,则可以跳过当前数位 ans = dfs(pos + 1, state, False, False) # 如果前一位没有填写数字,则最小可选择数字为 0,否则最少为 1(不能含有前导 0)。 minX = 0 if isNum else 1 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s[pos]) if isLimit else 9 # 枚举可选择的数字 for d in range(minX, maxX + 1): # d 不在选择的数字集合中,即之前没有选择过 d if (state >> d) & 1 == 0: ans += dfs(pos + 1, state | (1 << d), isLimit and d == maxX, True) return ans return n - dfs(0, 0, True, False) ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n \times 10 \times 2^{10})$。 - **空间复杂度**:$O(\log n \times 2^{10})$。 ### 2.2 数字 1 的个数 #### 2.2.1 题目链接 - [233. 数字 1 的个数 - 力扣](https://leetcode.cn/problems/number-of-digit-one/) #### 2.2.2 题目大意 **描述**:给定一个整数 $n$。 **要求**:计算所有小于等于 $n$ 的非负整数中数字 $1$ 出现的个数。 **说明**: - $0 \le n \le 10^9$。 **示例**: - 示例 1: ```python 输入:n = 13 输出:6 ``` - 示例 2: ```python 输入:n = 0 输出:0 ``` #### 2.2.3 解题思路 ##### 思路 1:动态规划 + 数位 DP 将 $n$ 转换为字符串 $s$,定义递归函数 `def dfs(pos, cnt, isLimit):` 表示构造第 $pos$ 位及之后所有数位中数字 $1$ 出现的个数。接下来按照如下步骤进行递归。 1. 从 `dfs(0, 0, True)` 开始递归。 `dfs(0, 0, True)` 表示: 1. 从位置 $0$ 开始构造。 2. 初始数字 $1$ 出现的个数为 $0$。 3. 开始时受到数字 $n$ 对应最高位数位的约束。 2. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时:返回数字 $1$ 出现的个数 $cnt$。 3. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:$ans = 0$。 4. 如果遇到 $isNum == False$,说明之前位数没有填写数字,当前位可以跳过,这种情况下方案数等于 $pos + 1$ 位置上没有受到 $pos$ 位的约束,并且之前没有填写数字时的方案数,即:`ans = dfs(i + 1, state, False, False)`。 5. 如果 $isNum == True$,则当前位必须填写一个数字。此时: 1. 因为不需要考虑前导 $0$ 所以当前位数位所能选择的最小数字($minX$)为 $0$。 2. 根据 $isLimit$ 来决定填当前位数位所能选择的最大数字($maxX$)。 3. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $d$。 4. 方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, cnt + (d == 1), isLimit and d == maxX)`。 1. `cnt + (d == 1)` 表示之前数字 $1$ 出现的个数加上当前位为数字 $1$ 的个数。 2. `isLimit and d == maxX` 表示 $pos + 1$ 位受到之前位 $pos$ 位限制。 6. 最后的方案数为 `dfs(0, 0, True)`,将其返回即可。 ##### 思路 1:代码 ```python class Solution: def countDigitOne(self, n: int) -> int: # 将 n 转换为字符串 s s = str(n) @cache # pos: 第 pos 个数位 # cnt: 之前数字 1 出现的个数。 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 def dfs(pos, cnt, isLimit): if pos == len(s): return cnt ans = 0 # 不需要考虑前导 0,则最小可选择数字为 0 minX = 0 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s[pos]) if isLimit else 9 # 枚举可选择的数字 for d in range(minX, maxX + 1): ans += dfs(pos + 1, cnt + (d == 1), isLimit and d == maxX) return ans return dfs(0, 0, True) ``` ##### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(\log n)$。 ## 练习题目 - [0357. 统计各位数字都不同的数字个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-numbers-with-unique-digits.md) - [0788. 旋转数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotated-digits.md) - [2719. 统计整数数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2700-2799/count-of-integers.md) - [数位 DP 题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%95%B0%E4%BD%8D-dp-%E9%A2%98%E7%9B%AE) ## 参考资料 - 【文章】[AcWing 1081. 度的数量【数位DP基本概念+数位DP记忆化搜索】](https://www.acwing.com/solution/content/66855/) - 【视频】[数位 DP 通用模板【力扣周赛 306】LeetCode - 灵茶山艾府](https://www.bilibili.com/video/BV1rS4y1s721/) ================================================ FILE: docs/08_dynamic_programming/08_16_probability_dp.md ================================================ ## 1. 概率 DP 简介 > **概率 DP**:一类使用动态规划方法来求解概率与期望的问题,也可以分别叫做「概率 DP」、「期望 DP」。由于概率和期望具有线性性质,使得可以在概率和期望之间建立一定的递推关系,从而通过动态规划的方式来解决一些概率问题。 概率 DP 和期望 DP 的难点主要有两点: 1. 状态转移方程的推导。 2. 概率论知识。 其中第 $1$ 点和一般动态规划并无太大差别,而第 $2$ 点涉及到对概率论基础知识的掌握。其中,「概率 PD」对应了概率论知识中的「全概率公式」,「期望 DP」则对应了「全期望公式」。 我们来看看「概率 DP」「期望 DP」中所涉及到的概率论基础知识。 ## 2. 概率论知识 ### 2.1 样本空间、事件和概率 > **基本事件**:在概率论中,我们将一次随机实验中的某个可能结果称为「样本点」或者「基本事件」。 > > **样本空间**:所有可能的结果组成的集合,称为「样本空间」,标记为 $S$。 > > **随机事件**:样本空间 $S$ 的一个子集 $A$($A \subseteq S$),称为「随机事件」。 > **概率**:对于样本空间 $S$ 中的每一个随机事件 $A$,如果都存在一种时间到实数的映射函数 $P(A)$,满足: > > 1. $P(S) = 1$。 >2. $0 \le P(A) \le 1$。 > 3. 对于两个互斥事件,$P(A \cup B) = P(A) + P(B)$。 > > 则称 $P(A)$ 为随机事件 $A$ 的概率。 ### 2.2 随机变量、数学期望 > **随机变量**:对于样本空间 $S$ 中的任意事件 $i$,都有唯一的实数 $X_i$ 与之对应,则称 $X = X_i$ 为样本空间 $S$ 上的随机变量。 常见的随机变量主要有「离散型随机变量」与「连续型随机变量」。「概率 DP」主要涉及到离散型随机变量。 > **数学期望**:如果随机变量 $X = X_i$ 的概率为 $P(X = X_i) = p_i$,则称 $E(x) = \sum p_ix_i$ 为随机变量 $X$ 的数学期望。 数学期望的一些性质: 1. 线性函数性质,满足:$E(aX + bY) = a \times E(X) + b \times E(Y)$。 2. 如果随机变量 $X$、$Y$ 相互独立,那么:$E(XY) = E(X)E(Y)$。 其中数学期望的线性函数性质是我们能够对数学期望进行地推求解的基本依据。 ### 2.3 全概率公式、全期望公式 概率 DP 的理论基础主要是「全概率公式」。 > **全概率公式**:设 $B_1, B_2, B_3, …, B_n$ 是样本空间 $S$ 中互不相交的一系列事件,并且满足 $S = \cup_{j = 1}^n B_j$,那么对于任意事件 $A$,有: $P(A) = \sum_{j = 1}^{n} P(A \text{ | } B_j)P(B_j)$ 「概率 DP」中一般常见的状态转移方程式为:$dp[i] = \sum_{j = 1}^{n} p[i][j] \times dp[j]$。 - 其中 $dp[i]$ 对应全概率公式中的 $P(A)$,$p[i][j]$ 对应了 $P(B_j)$,$dp[j]$ 则对应了 $P(A \text{ | } B_j)$。 与概率 DP 类似,期望 DP 的理论基础主要是「全期望公式」。 > **全期望公式**:设 $X$、$Y$ 为随机变量,$E(Y) = E(E(Y \text{ | } X)) = \sum_{j = 1}^n P(x_j) E(Y \text{ | } x_j)$。 「期望 DP」中一般常见的状态转移方程式为:$dp[i] = \sum_{j = 1}^{n} p[i][j] \times dp[j]$。 - 其中 $dp[i]$ 对应全概率公式中的 $E(Y)$,$p[i][j]$ 对应了 $P(x_j)$,$dp[j]$ 则对应了 $E(Y \text{ | } x_j)$。 ## 练习题目 - [0688. 骑士在棋盘上的概率](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/knight-probability-in-chessboard.md) - [1227. 飞机座位分配概率](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/airplane-seat-assignment-probability.md) - [概率 DP 题目列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/00_preface/00_06_categories_list.md#%E6%A6%82%E7%8E%87-dp-%E9%A2%98%E7%9B%AE) ## 参考资料 - 【文章】[概率动态规划简介 - 动态规划图文学: 树形、图上、概率 & 博弈动态 - LeetBook](https://leetcode.cn/leetbook/read/dynamic-programming-3-plus/nmnp61/) - 【文章】[期望动态规划简介 - 动态规划图文学: 树形、图上、概率 & 博弈动态 - LeetBook](https://leetcode.cn/leetbook/read/dynamic-programming-3-plus/nmwjt6/) - 【文章】浅析竞赛中一类数学期望问题的解决方法 - 汤可因 - 【文章】有关概率和期望问题的研究 - 鬲融 - 【文章】信息学竞赛中概率问题求解初探 - 梅诗珂 ================================================ FILE: docs/08_dynamic_programming/index.md ================================================ ![](https://qcdn.itcharge.cn/images/20250923144601.png) ::: tip 引 言 动态规划如同在迷雾中铺设星光小径。 循着昨日星辰的余晖,步步生辉,终将抵达最璀璨的彼岸。 ::: ## 本章内容 - [8.1 动态规划基础](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_01_dynamic_programming_basic.md) - [8.2 记忆化搜索](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_02_memoization_search.md) - [8.3 线性 DP(一):单串线性 DP](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_03_linear_dp_01.md) - [8.4 线性 DP(二):双串线性 DP](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_04_linear_dp_02.md) - [8.5 线性 DP(三):矩阵线性 DP、无串线性 DP](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_05_linear_dp_02.md) - [8.6 背包问题知识(一):0-1 背包](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_06_knapsack_problem_01.md) - [8.7 背包问题知识(二):完全背包](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_07_knapsack_problem_02.md) - [8.8 背包问题知识(三):多重背包](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_08_knapsack_problem_03.md) - [8.9 背包问题知识(四):混合背包、分组背包](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_09_knapsack_problem_04.md) - [8.10 背包问题知识(五):背包问题变种](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_10_knapsack_problem_05.md) - [8.11 区间 DP](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_11_interval_dp.md) - [8.12 树形 DP](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_12_tree_dp.md) - [8.13 状态压缩 DP](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_13_state_compression_dp.md) - [8.14 计数 DP](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_14_counting_dp.md) - [8.15 数位 DP](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_15_digit_dp.md) - [8.16 概率 DP](https://github.com/ITCharge/AlgoNote/tree/main/docs/08_dynamic_programming/08_16_probability_dp.md) ================================================ FILE: docs/README.md ================================================
alt text

算法通关手册

📚 从零开始的「算法与数据结构」学习教程

一本系统讲解算法与数据结构、涵盖 LeetCode 题解的中文学习手册

代码仓库 PDF 下载
## 1. 本书简介 本书不仅仅只是一本算法题解书,更是一本算法与数据结构基础知识的讲解书。 - 超详细的 **「算法与数据结构」** 基础讲解教程,**「LeetCode 1000+ 道」** 经典题目详细解析。 - 本项目易于理解,没有大跨度的思维跳跃,项目中使用大量图示、例子来帮助理解。 - 本项目先从基础的数据结构和算法开始讲解,再针对不同分类的数据结构和算法,进行具体题目的讲解分析。让读者可以通过「算法基础理论学习」和「编程实战学习」相结合的方式,彻底的掌握算法知识。 - 本项目从各大知名互联网公司面试算法题中整理汇总了 **「LeetCode 200 道高频面试题」**,帮助面试者更有针对性的准备面试。 ### 1.1 目标读者 - 拥有 Python 编程基础或其他编程语言基础的编程爱好者 - 对 LeetCode 刷题感兴趣或准备算法面试的面试人员 - 对算法感兴趣的计算机专业学生或程序员 - 想要提升编程思维和问题解决能力的开发者 ### 1.2 内容结构 本书采用算法与数据结构相结合的方法,把内容分为如下几个主要部分: - **0. 序言**:介绍数据结构与算法的基础知识、算法复杂度、LeetCode 的入门和攻略,为后面的学习打好基础。 - **1. 数组**:讲解数组的基本概念、数组的基本操作。 - **2. 链表**:讲解链表的基本概念、操作和应用,包括单链表、双向链表、循环链表等。 - **3. 栈、队列、哈希表**:详细介绍栈、队列、哈希表这三种数据结构,包括它们的基本概念、实现方式、应用场景以及相关的经典算法题。 - **4. 字符串**:讲解字符串的基本操作、单字符串匹配算法、多字符串匹配算法,以及字符串相关的经典算法题。 - **5. 树结构**:介绍树的基本概念、二叉树、二叉搜索树、线段树、树状数组、并查集等数据结构。 - **6. 图论**:讲解图的基本概念、表示方法、遍历算法和经典应用。 - **7. 基础算法**:介绍基本的算法思想。包括枚举、递归、分治、回溯、贪心以及位运算。 - **8. 动态规划**:介绍动态规划的基础知识、各种动态规划题型的解法。 - **9. 附加内容**:作为全书的扩展模块。 - **10. 题目解析**:讲解 LeetCode 上刷过的所有题目,可按照对应题号进行检索和学习。 ### 1.3 使用说明 - 本电子书左侧提供了完整的章节目录导航,可直接点击跳转至相应内容。 - 本电子书右上角配有搜索栏,便于快速查找所需章节和题解文章。 - 本电子书集成了 giscus 评论系统,欢迎在页面底部评论区留言(需 GitHub 账号登录)。 - 建议按章节顺序系统学习,逐步掌握各知识点;也可根据兴趣自由选择章节阅读。 - 每篇内容末尾设有练习题,建议及时完成以加深理解、巩固所学。 ## 2. 相关说明 ### 2.1 关于作者 我是一名 iOS / macOS 的开发程序员,研究生毕业于北航软件学院。曾在大学期间学习过算法知识,并参加过 3 年的 ACM 比赛, 但水平有限,未能取得理想成绩。但是这 3 年的 ACM 经历,给我最大的收获是锻炼了自己的逻辑思维和解决实际问题的能力,这种能力为我今后的工作、学习打下了坚实的基础。 我从 2021 年 03 月 30 日开始每日在 LeetCode 刷题,到目前为止日已经刷了 1800+ 道题目,并且完成了 1000+ 道题解。努力向着 1500+、2000+ 道题解前进。 ### 2.2 互助与勘误 限于本人的水平和经验,书中一定不乏纰漏和谬误之处。恳切希望读者给予批评指正。这将有利于我改进和提高,以帮助更多的读者。如果您对本书有任何评论和建议,或者遇到问题需要帮助,可在每页评论区留言,或者致信作者邮箱 [i@itcharge.cn](mailto:i@itcharge.cn),我将不胜感激。 ### 2.3 版权说明 - 本书采用 [知识署名—非商业性使用—禁止演绎(BY-NC-ND)4.0 协议国际许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode.zh-Hans) 进行许可。 - 本书题解中的所有题目版权均归 [LeetCode](https://leetcode.com/) 和 [力扣中国](https://leetcode.cn/) 所有。 ### 2.4 致谢 在本书构思与写作阶段,很多朋友给我提出了有益的意见和建议。这些意见和建议令我受益匪浅。感谢在本书著作准备过程中,帮助过我的朋友,以及一起陪我刷题打卡的朋友,还有提供宝贵意见的读者。谢谢诸位。 ================================================ FILE: docs/others/index.md ================================================ ## 本章内容 - [待办清单](https://github.com/ITCharge/AlgoNote/tree/main/docs/others/todo_list.md) - [网站时间线](https://github.com/ITCharge/AlgoNote/tree/main/docs/others/update_time.md) ================================================ FILE: docs/others/todo_list.md ================================================ ## 内容优化 - 借助 DeepSeek 优化全书内容(高优先级) - 单调栈文章内容优化 - 双向队列文章内容优化 - 后缀数组文章内容优化 - 树状数组文章内容优化 - 第 8 章 动态规划章节优化 - 树形 DP 相关内容 - 状态压缩 DP 相关内容 - 计数 DP 相关内容 - 数位 DP 相关内容 - 概率 DP 相关内容 ## 图片和示意图 - 优化全书图片示意图(中优先级) - 补充示意图(高优先级): - 双向队列操作示意图 - 单调栈应用场景示意图 - 后缀数组构建过程示意图 - 树状数组更新和查询示意图 ## 代码和文档完善 - 优化代码注释,丰富多种编程语言(如 Java、C++)的实现示例 - 检查并优化各章节的链接有效性及交叉引用 - 增加更多的题目解析,提升内容的广度与深度 ================================================ FILE: docs/others/update_time.md ================================================ ## 2026-01 - 2026-01-16 **完成「第 1 ~ 1000 题」的题目解析** - 2026-01-15 补充第 600 ~ 699 题的题目解析(增加 3 道题) - 2026-01-12 补充第 600 ~ 699 题的题目解析(增加 11 道题) - 2026-01-11 补充第 001 ~ 599 题的题目解析(增加 35 道题) - 2026-01-09 补充第 900 ~ 999 题的题目解析(增加 69 道题) - 2026-01-08 补充第 800 ~ 899 题的题目解析(增加 22 道题) - 2026-01-07 补充第 800 ~ 899 题的题目解析(增加 37 道题) - 2026-01-06 补充第 700 ~ 799 题的题目解析(增加 8 道题) - 2026-01-05 补充第 700 ~ 799 题的题目解析(增加 9 道题) - 2026-01-03 补充第 700 ~ 799 题的题目解析(增加 27 道题) - 2026-01-02 补充第 600 ~ 699 题的题目解析(增加 34 道题) - 2026-01-01 补充第 500 ~ 599 题的题目解析(增加 34 道题) ## 2025-10 - 2025-10-28 补充第 400 ~ 499 题的题目解析(增加 14 道题) - 2025-10-27 补充第 400 ~ 499 题的题目解析(增加 15 道题) - 2025-10-24 补充第 400 ~ 499 题的题目解析(增加 12 道题) - 2025-10-23 补全第 300 ~ 399 题的题目解析(增加 20 道题) - 2025-10-22 补充第 300 ~ 399 题的题目解析(增加 13 道题) - 2025-10-21 补充第 300 ~ 399 题的题目解析(增加 13 道题) - 2025-10-20 补全第 200 ~ 299 题的题目解析(增加 33 道题) - 2025-10-19 补充第 200 ~ 299 题的题目解析(增加 8 道题) - 2025-10-17 补全第 100 ~ 199 题的题目解析(增加 12 道题) - 2025-10-16 补全第 1 ~ 99 题的题目解析(增加 13 道题) - 2025-10-13 编写生成 PDF 脚本,并生成 AlgoNote-v2.0.pdf - 2025-10-09 修复内容显示问题 - 2025-10-07 更新题目分类 ## 2025-09 - 2025-09-30 修正内容错误 - 2025-09-29 修正表述错误和图片错误 - 2025-09-28 更新题解分类,修复链接错误,优化章节标题和引言 - 2025-09-25 优化章节引言,更新图片 - 2025-09-24 修复和优化语句表述,修正标题描述 - 2025-09-23 章节目录页增加引言和图片 - 2025-09-22 修复和优化语句表述,修正 LaTex 公式 - 2025-09-17 修复和优化语句表述 - 2025-09-09 修复和优化语句表述 - 2025-09-08 修复和优化语句表述 - 2025-09-06 修复连接错误问题 - 2025-09-03 修复和优化语句表述,修正 LaTex 公式 - 2025-09-02 修复和优化语句表述 - 2025-09-01 更新「1032. 字符流」题解 ## 2025-08 - 2025-08-21 修复和优化语句表述 - 2025-08-19 修复和优化语句表述 - 2025-08-18 修复和优化语句表述 - 2025-08-13 修复和优化语句表述 ## 2025-07 - 2025-07-15 更改题目链接 - 2025-07-14 更改题目链接 - 2025-07-04 更新相关内容链接 ## 2025-06 - 2025-06-25 更新每个章节中推荐练习题目相关内容,更新「AC 自动机」和「单模式串匹配」相关内容 - 2025-06-18 更新「后缀数组」相关内容 - 2025-06-17 更新 **「算法通关手册网页 2.0 版本」** - 2025-06-10 更新图论相关内容:「二分图最大匹配」、「二分图基础」、「差分约束系统」、「次短路径」、「多源最短路径」 ## 2025-05 - 2025-05-30 更新「单源最短路径(一、二)」相关内容 - 2025-05-28 补充「图的最小生成树」相关内容 - 2025-05-22 修正拼写错误 - 2025-05-16 修正「二叉树基础」中的拼写错误 - 2025-05-15 修正「插入排序」描述错误 ## 2025-02 - 2025-02-06 修正「二叉树基础」中的拼写错误 ## 2025-01 - 2025-01-27 修正「插入排序」描述错误 - 2025-01-16 更新「0094. 二叉树的中序遍历」题解 ## 2024-12 - 2024-12-31 更新「图的定义和分类」相关内容 ## 2024-09 - 2024-09-21 更新「0169. 多数元素」题解 ## 2024-08 - 2024-08-12 合并修复「KMP 算法」的拉取请求 - 2024-08-10 更新「KMP 算法」相关内容 - 2024-08-06 合并修复数组第K个最大元素的拉取请求 - 2024-08-02 更新「0215. 数组中的第K个最大元素」题解 ## 2024-07 - 2024-07-04 合并修复「线性 DP」的拉取请求 - 2024-07-03 更新「8.3 线性 DP(一): 单串线性 DP」 ## 2024-06 - 2024-06-28 更新「状态压缩 DP」相关内容 - 2024-06-27 更新「0033. 搜索旋转排序数组」题解 - 2024-06-25 更新「并查集」相关内容 - 2024-06-24 更新「KMP 算法」相关内容 - 2024-06-19 更新「Rabin Karp 算法」相关内容 - 2024-06-17 合并修复「哈希表」的拉取请求 - 2024-06-15 更新「哈希表」相关内容 ## 2024-05 - 2024-05-20 更新题解列表,更新「0091. 解码方法」题解 - 2024-05-15 更新「0526. 优美的排列」题解,更新 Latex 公式 - 2024-05-14 更新多个动态规划相关内容 - 2024-05-13 更新多个算法相关内容 - 2024-05-12 更新 LaTex 公式 - 2024-05-11 更新「树」相关图片和标题 - 2024-05-10 更新多个章节内容 - 2024-05-09 更新多个章节的图片和标题 - 2024-05-08 更新「序言」部分相关图片,更新「算法复杂度」相关内容,更新「数组二分查找」相关内容 - 2024-05-06 修正 Latex 公式显示问题 - 2024-05-05 修正「优先队列」中的拼写错误 ## 2024-03 - 2024-03-26 更新项目说明,更新「优先队列」相关内容 - 2024-03-19 合并修复「图的存储结构和问题应用」的拉取请求 - 2024-03-18 更新「图的存储结构和问题应用」相关内容 - 2024-03-12 更新题解列表 ## 2024-01 - 2024-01-22 完成「0892. 三维形体的表面积」题解 - 2024-01-19 更新题解列表 - 2024-01-18 更新题解列表 - 2024-01-17 更新题解列表 - 2024-01-16 更新题解列表 - 2024-01-15 更新题解列表 - 2024-01-12 更新「线性 DP」相关内容 - 2024-01-11 更新「Rabin Karp 算法」相关内容 - 2024-01-10 更新题解列表 - 2024-01-09 更新题解列表 - 2024-01-08 完成「0862. 和至少为 K 的最短子数组」题解 - 2024-01-07 更新题解列表 - 2024-01-05 更新题解列表 - 2024-01-04 更新题解列表 - 2024-01-03 更新题解列表 - 2024-01-02 更新题解列表 - 2023-12-29 更新题解列表 ## 2023-12 - 2023-12-26 完成「全部题目解析」的题目链接优化 ## 2023-09 - 2023-09-29 更新 **「算法通关手册网页 1.0 版本」** - 2023-09-07 完成「滑动窗口」内容优化 - 2023-09-06 完成「数组双指针」内容优化 - 2023-09-05 完成「广度优先搜索」内容优化 - 2023-09-05 完成「深度优先搜索」内容优化 ## 2023-08 - 2023-08-31 完成「堆排序」内容优化 - 2023-08-24 完成「数组基础」内容优化 - 2023-08-22 完成「基数排序」、「桶排序」、「计数排序」内容优化 - 2023-08-18 完成「快速排序」内容优化 - 2023-08-17 完成「归并排序」内容优化 - 2023-08-16 完成「希尔排序」、「插入排序」、「选择排序」、「冒泡排序」内容优化 - 2023-08-15 完成「序言」内容优化 ## 2023-07 - 2023-07-17 完成「树形 DP」相关内容 ## 2023-06 - 2023-06-29 完成「数位 DP」相关内容 - 2023-06-02 完成「状态压缩 DP」相关内容 ## 2023-05 - 2023-05-31 完成「计数类 DP」相关内容 - 2023-05-08 完成「图的拓扑排序」相关内容 ## 2023-04 - 2023-04-07 完成「区间 DP」相关内容 ## 2023-03 - 2023-03-29 完成「背包问题(四、五)」相关内容 - 2023-03-22 完成「背包问题(三)」相关内容 - 2023-03-21 完成「背包问题(一、二)」相关内容 - 2023-03-14 完成「线性 DP(一、二)」相关内容 - 2023-03-08 完成「位运算」相关内容 - 2023-03-08 完成「动态规划基础(重写版)」相关内容 - 2023-03-06 完成「记忆化搜索」相关内容 ## 2022-07 - 2022-07-21 完成「动态规划基础」相关内容 ## 2022-06 - 2022-06-13 完成「LeetCode 面试最常考 200 题」相关内容 - 2022-06-10 完成「LeetCode 面试最常考 100 题」相关内容 ## 2022-05 - 2022-05-11 完成「贪心算法」相关内容 - 2022-05-02 完成「并查集」相关内容 ## 2022-04 - 2022-04-26 完成「回溯算法」相关内容 - 2022-04-14 完成「分治算法」相关内容 - 2022-04-08 完成「递归算法」相关内容 ## 2022-03 - 2022-03-29 完成「枚举算法」相关内容 - 2022-03-18 完成「图的存储结构和问题应用」相关内容 - 2022-03-13 完成「图的定义和分类」相关内容 - 2022-03-03 完成「线段树」相关内容 ## 2022-02 - 2022-02-28 完成「二叉搜索树」相关内容 - 2022-02-23 完成「二叉树的还原」相关内容 - 2022-02-22 完成「二叉树的遍历」相关内容 - 2022-02-21 完成「树与二叉树基础」相关内容 - 2022-02-05 完成「KMP 算法」相关内容 ## 2022-01 - 2022-01-28 完成「Sunday 算法」、「Horspool 算法」相关内容 - 2022-01-27 完成「BM 算法」相关内容 - 2022-01-21 完成「RK 算法」相关内容 - 2022-01-20 完成「BK 算法」相关内容 - 2022-01-19 完成「字符串基础」相关内容 - 2022-01-15 完成「哈希表基础」相关内容 - 2022-01-09 完成「优先队列」相关内容 - 2022-01-06 完成「单调栈」相关内容 ## 2022-12 - 2022-12-21 完成「广度优先搜索」相关内容 - 2022-12-14 完成「深度优先搜索」相关内容 - 2022-12-12 完成「链表双指针」相关内容 - 2022-12-10 完成「链表排序」相关内容 - 2022-12-04 完成「队列基础」相关内容 - 2022-12-04 完成「栈基础」相关内容 ## 2021-11 - 2021-11-08 完成「滑动窗口」相关内容 - 2021-11-08 完成「双指针」相关内容 - 2021-11-04 完成「二分查找」相关内容 ## 2021-10 - 2021-10-20 完成「基数排序」、「桶排序」、「计数排序」相关内容 - 2021-10-19 完成「堆排序」、「快速排序」、「归并排序」、「希尔排序」、「插入排序」、「选择排序」、「冒泡排序」相关内容 ## 2021-09 - 2021-09-17 完成「数组基础」相关内容 - 2021-09-09 完成「算法复杂度」相关内容 ## 2021-07 - 2021-07-05 完成「数据结构与算法」相关内容 ## 2021-01 - 2021-01-26 完成「LeetCode 刷题顺序和技巧」相关内容 - 2021-01-22 开始建立仓库 ================================================ FILE: docs/solutions/0001-0099/3sum-closest.md ================================================ # [0016. 最接近的三数之和](https://leetcode.cn/problems/3sum-closest/) - 标签:数组、双指针、排序 - 难度:中等 ## 题目链接 - [0016. 最接近的三数之和 - 力扣](https://leetcode.cn/problems/3sum-closest/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 和 一个目标值 $target$。 **要求**:从 $nums$ 中选出三个整数,使它们的和与 $target$ 最接近。返回这三个数的和。假定每组输入只存在恰好一个解。 **说明**: - $3 \le nums.length \le 1000$。 - $-1000 \le nums[i] \le 1000$。 - $-10^4 \le target \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [-1,2,1,-4], target = 1 输出:2 解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2)。 ``` - 示例 2: ```python 输入:nums = [0,0,0], target = 1 输出:0 ``` ## 解题思路 ### 思路 1:对撞指针 直接暴力枚举三个数的时间复杂度是 $O(n^3)$。很明显的容易超时。考虑使用双指针减少循环内的时间复杂度。具体做法如下: - 先对数组进行从小到大排序,使用 $ans$ 记录最接近的三数之和。 - 遍历数组,对于数组元素 $nums[i]$,使用两个指针 $left$、$right$。$left$ 指向第 $0$ 个元素位置,$right$ 指向第 $i - 1$ 个元素位置。 - 计算 $nums[i]$、$nums[left]$、$nums[right]$ 的和与 $target$ 的差值,将其与 $ans$ 与 $target$ 的差值作比较。如果差值小,则更新 $ans$。 - 如果 $nums[i] + nums[left] + nums[right] < target$,则说明 $left$ 小了,应该将 $left$ 右移,继续查找。 - 如果 $nums[i] + nums[left] + nums[right] \ge target$,则说明 $right$ 太大了,应该将 $right$ 左移,然后继续判断。 - 当 $left == right$ 时,区间搜索完毕,继续遍历 $nums[i + 1]$。 - 最后输出 $ans$。 这种思路使用了两重循环,其中内层循环当 $left == right$ 时循环结束,时间复杂度为 $O(n)$,外层循环时间复杂度也是 $O(n)$。所以算法的整体时间复杂度为 $O(n^2)$。 ### 思路 1:代码 ```python class Solution: def threeSumClosest(self, nums: List[int], target: int) -> int: nums.sort() res = float('inf') size = len(nums) for i in range(2, size): left = 0 right = i - 1 while left < right: total = nums[left] + nums[right] + nums[i] if abs(total - target) < abs(res - target): res = total if total < target: left += 1 else: right -= 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为数组中元素的个数。 - **空间复杂度**:$O(\log n)$,排序需要 $\log n$ 的栈空间。 ================================================ FILE: docs/solutions/0001-0099/3sum.md ================================================ # [0015. 三数之和](https://leetcode.cn/problems/3sum/) - 标签:数组、双指针、排序 - 难度:中等 ## 题目链接 - [0015. 三数之和 - 力扣](https://leetcode.cn/problems/3sum/) ## 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:判断 $nums$ 中是否存在三个元素 $a$、$b$、$c$,满足 $a + b + c == 0$。要求找出所有满足要求的不重复的三元组。 **说明**: - $3 \le nums.length \le 3000$。 - $-10^5 \le nums[i] \le 10^5$。 **示例**: - 示例 1: ```python 输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] ``` - 示例 2: ```python 输入:nums = [0,1,1] 输出:[] ``` ## 解题思路 ### 思路 1:对撞指针 直接三重遍历查找 $a$、$b$、$c$ 的时间复杂度是:$O(n^3)$。我们可以通过一些操作来降低复杂度。 先将数组进行排序,以保证按顺序查找 $a$、$b$、$c$ 时,元素值为升序,从而保证所找到的三个元素是不重复的。同时也方便下一步使用双指针减少一重遍历。时间复杂度为:$O(n \times \log n)$。 第一重循环遍历 $a$,对于每个 $a$ 元素,从 $a$ 元素的下一个位置开始,使用对撞指针 $left$,$right$。$left$ 指向 $a$ 元素的下一个位置,$right$ 指向末尾位置。先将 $left$ 右移、$right$ 左移去除重复元素,再进行下边的判断。 1. 如果 $nums[a] + nums[left] + nums[right] == 0$,则得到一个解,将其加入答案数组中,并继续将 $left$ 右移,$right$ 左移; 2. 如果 $nums[a] + nums[left] + nums[right] > 0$,说明 $nums[right]$ 值太大,将 $right$ 向左移; 3. 如果 $nums[a] + nums[left] + nums[right] < 0$,说明 $nums[left]$ 值太小,将 $left$ 右移。 ### 思路 1:代码 ```python class Solution: def threeSum(self, nums: List[int]) -> List[List[int]]: n = len(nums) nums.sort() ans = [] for i in range(n): if i > 0 and nums[i] == nums[i - 1]: continue left = i + 1 right = n - 1 while left < right: while left < right and left > i + 1 and nums[left] == nums[left - 1]: left += 1 while left < right and right < n - 1 and nums[right + 1] == nums[right]: right -= 1 if left < right and nums[i] + nums[left] + nums[right] == 0: ans.append([nums[i], nums[left], nums[right]]) left += 1 right -= 1 elif nums[i] + nums[left] + nums[right] > 0: right -= 1 else: left += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0001-0099/4sum.md ================================================ # [0018. 四数之和](https://leetcode.cn/problems/4sum/) - 标签:数组、双指针、排序 - 难度:中等 ## 题目链接 - [0018. 四数之和 - 力扣](https://leetcode.cn/problems/4sum/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 和一个目标值 $target$。 **要求**:找出所有满足以下条件切不重复的四元组。 1. $0 \le a, b, c, d < n$。 2. $a$、$b$、$c$ 和 $d$ 互不相同。 3. $nums[a] + nums[b] + nums[c] + nums[d] == target$。 **说明**: - $1 \le nums.length \le 200$。 - $-10^9 \le nums[i] \le 10^9$。 - $-10^9 \le target \le 10^9$。 **示例**: - 示例 1: ```python 输入:nums = [1,0,-1,0,-2,2], target = 0 输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]] ``` - 示例 2: ```python 输入:nums = [2,2,2,2,2], target = 8 输出:[[2,2,2,2]] ``` ## 解题思路 ### 思路 1:排序 + 双指针 和 [0015. 三数之和](https://leetcode.cn/problems/3sum/) 解法类似。 直接三重遍历查找 $a$、$b$、$c$、$d$ 的时间复杂度是:$O(n^4)$。我们可以通过一些操作来降低复杂度。 1. 先将数组进行排序,以保证按顺序查找 $a$、$b$、$c$、$d$ 时,元素值为升序,从而保证所找到的四个元素是不重复的。同时也方便下一步使用双指针减少一重遍历。这一步的时间复杂度为:$O(n \times \log n)$。 2. 两重循环遍历元素 $a$、$b$,对于每个 $a$ 元素,从 $a$ 元素的下一个位置开始遍历元素 $b$。对于元素 $a$、$b$,使用双指针 $left$,$right$ 来查找 $c$、$d$。$left$ 指向 $b$ 元素的下一个位置,$right$ 指向末尾位置。先将 $left$ 右移、$right$ 左移去除重复元素,再进行下边的判断。 1. 如果 $nums[a] + nums[b] + nums[left] + nums[right] == target$,则得到一个解,将其加入答案数组中,并继续将 $left$ 右移,$right$ 左移; 2. 如果 $nums[a] + nums[b] + nums[left] + nums[right] > target$,说明 $nums[right]$ 值太大,将 $right$ 向左移; 3. 如果 $nums[a] + nums[b] + nums[left] + nums[right] < target$,说明 $nums[left]$ 值太小,将 $left$ 右移。 ### 思路 1:代码 ```python class Solution: def fourSum(self, nums: List[int], target: int) -> List[List[int]]: n = len(nums) nums.sort() ans = [] for i in range(n): if i > 0 and nums[i] == nums[i-1]: continue for j in range(i+1, n): if j > i+1 and nums[j] == nums[j-1]: continue left = j + 1 right = n - 1 while left < right: while left < right and left > j + 1 and nums[left] == nums[left - 1]: left += 1 while left < right and right < n - 1 and nums[right + 1] == nums[right]: right -= 1 if left < right and nums[i] + nums[j] + nums[left] + nums[right] == target: ans.append([nums[i], nums[j], nums[left], nums[right]]) left += 1 right -= 1 elif nums[i] + nums[j] + nums[left] + nums[right] > target: right -= 1 else: left += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 为数组中元素个数。 - **空间复杂度**:$O(\log n)$,排序额外使用空间为 $\log n$。 ================================================ FILE: docs/solutions/0001-0099/add-binary.md ================================================ # [0067. 二进制求和](https://leetcode.cn/problems/add-binary/) - 标签:位运算、数学、字符串、模拟 - 难度:简单 ## 题目链接 - [0067. 二进制求和 - 力扣](https://leetcode.cn/problems/add-binary/) ## 题目大意 给定两个二进制数的字符串 a、b。计算 a 和 b 的和,返回结果也用二进制表示。 ## 解题思路 这道题可以直接将 a、b 转换为十进制数,相加后再转换为二进制数。 也可以利用位运算的一些知识,直接求和。 因为 a、b 为二进制的字符串,先将其转换为二进制数。 本题用到的位运算知识: - 异或运算 x ^ y :可以获得 x + y 无进位的加法结果。 - 与运算 x & y:对应位置为 1,说明 x、y 该位置上原来都为 1,则需要进位。 - 座椅运算 x << 1:将 a 对应二进制数左移 1 位。 这样,通过 x ^ y 运算,我们可以得到相加后无进位结果,再根据 (x & y) << 1,计算进位后结果。 进行 x ^ y 和 (x & y) << 1操作之后判断进位是否为 0,如果不为 0,则继续上一步操作,直到进位为 0。 最后将其结果转为 2 进制返回。 ## 代码 ```python class Solution: def addBinary(self, a: str, b: str) -> str: x = int(a, 2) y = int(b, 2) ans = 0 while y: carry = ((x & y) << 1) x ^= y y = carry return bin(x)[2:] ``` ================================================ FILE: docs/solutions/0001-0099/add-two-numbers.md ================================================ # [0002. 两数相加](https://leetcode.cn/problems/add-two-numbers/) - 标签:递归、链表、数学 - 难度:中等 ## 题目链接 - [0002. 两数相加 - 力扣](https://leetcode.cn/problems/add-two-numbers/) ## 题目大意 **描述**:给定两个非空的链表 `l1` 和 `l2`。分别用来表示两个非负整数,每位数字都是按照逆序的方式存储的,每个节点存储一位数字。 **要求**:计算两个非负整数的和,并逆序返回表示和的链表。 **说明**: - 每个链表中的节点数在范围 $[1, 100]$ 内。 - $0 \le Node.val \le 9$。 - 题目数据保证列表表示的数字不含前导零。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2021/01/02/addtwonumber1.jpg) ```python 输入:l1 = [2,4,3], l2 = [5,6,4] 输出:[7,0,8] 解释:342 + 465 = 807. ``` - 示例 2: ```python 输入:l1 = [0], l2 = [0] 输出:[0] ``` ## 解题思路 ### 思路 1:模拟 模拟大数加法,按位相加,将结果添加到新链表上。需要注意进位和对 $10$ 取余。 ### 思路 1:代码 ```python class Solution: def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode: head = curr = ListNode(0) carry = 0 while l1 or l2 or carry: if l1: num1 = l1.val l1 = l1.next else: num1 = 0 if l2: num2 = l2.val l2 = l2.next else: num2 = 0 sum = num1 + num2 + carry carry = sum // 10 curr.next = ListNode(sum % 10) curr = curr.next return head.next ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(max(m, n))$。其中,$m$ 和 $n$ 分别是链表 `l1` 和 `l2` 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/binary-tree-inorder-traversal.md ================================================ # [0094. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/) - 标签:栈、树、深度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0094. 二叉树的中序遍历 - 力扣](https://leetcode.cn/problems/binary-tree-inorder-traversal/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:返回该二叉树的中序遍历结果。 **说明**: - 树中节点数目在范围 $[0, 100]$ 内。 - $-100 \le Node.val \le 100$。 **示例**: - 示例 1: ![img](https://assets.leetcode.com/uploads/2020/09/15/inorder_1.jpg) ```python 输入:root = [1,null,2,3] 输出:[1,3,2] ``` - 示例 2: ```python 输入:root = [] 输出:[] ``` ## 解题思路 ### 思路 1:递归遍历 二叉树的中序遍历递归实现步骤为: 1. 判断二叉树是否为空,为空则直接返回。 2. 先递归遍历左子树。 3. 然后访问根节点。 4. 最后递归遍历右子树。 ### 思路 1:代码 ```python class Solution: def inorderTraversal(self, root: TreeNode) -> List[int]: res = [] def inorder(root): if not root: return inorder(root.left) res.append(root.val) inorder(root.right) inorder(root) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。 ### 思路 2:模拟栈迭代遍历 我们可以使用一个显式栈 $stack$ 来模拟二叉树的中序遍历递归的过程。 与前序遍历不同,访问根节点要放在左子树遍历完之后。因此我们需要保证:**在左子树访问之前,当前节点不能提前出栈**。 我们应该从根节点开始,循环遍历左子树,不断将当前子树的根节点放入栈中,直到当前节点无左子树时,从栈中弹出该节点并进行处理。 然后再访问该元素的右子树,并进行上述循环遍历左子树的操作。这样可以保证最终遍历顺序为中序遍历顺序。 二叉树的中序遍历显式栈实现步骤如下: 1. 判断二叉树是否为空,为空则直接返回。 2. 初始化维护一个空栈。 3. 当根节点或者栈不为空时: 1. 如果当前节点不为空,则循环遍历左子树,并不断将当前子树的根节点入栈。 1. 如果当前节点为空,说明当前节点无左子树,则弹出栈顶元素 $node$,并访问该元素,然后尝试访问该节点的右子树。 ### 思路 2:代码 ```python class Solution: def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]: if not root: # 二叉树为空直接返回 return [] res = [] stack = [] while root or stack: # 根节点或栈不为空 while root: stack.append(root) # 将当前树的根节点入栈 root = root.left # 找到最左侧节点 node = stack.pop() # 遍历到最左侧,当前节点无左子树时,将最左侧节点弹出 res.append(node.val) # 访问该节点 root = node.right # 尝试访问该节点的右子树 return res ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0001-0099/climbing-stairs.md ================================================ # [0070. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/) - 标签:记忆化搜索、数学、动态规划 - 难度:简单 ## 题目链接 - [0070. 爬楼梯 - 力扣](https://leetcode.cn/problems/climbing-stairs/) ## 题目大意 **描述**:假设你正在爬楼梯。需要 $n$ 阶你才能到达楼顶。每次你可以爬 $1$ 或 $2$ 个台阶。现在给定一个整数 $n$。 **要求**:计算出有多少种不同的方法可以爬到楼顶。 **说明**: - $1 \le n \le 45$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:2 解释:有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶 ``` - 示例 2: ```python 输入:n = 3 输出:3 解释:有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶 ``` ## 解题思路 ### 思路 1:递归(超时) 根据我们的递推三步走策略,写出对应的递归代码。 1. 写出递推公式:$f(n) = f(n - 1) + f(n - 2)$。 2. 明确终止条件:$f(0) = 0, f(1) = 1$。 3. 翻译为递归代码: 1. 定义递归函数:`climbStairs(self, n)` 表示输入参数为问题的规模 $n$,返回结果为爬 $n$ 阶台阶到达楼顶的方案数。 2. 书写递归主体:`return self.climbStairs(n - 1) + self.climbStairs(n - 2)`。 3. 明确递归终止条件: 1. `if n == 0: return 0` 2. `if n == 1: return 1` ### 思路 1:代码 ```python class Solution: def climbStairs(self, n: int) -> int: if n == 1: return 1 if n == 2: return 2 return self.climbStairs(n - 1) + self.climbStairs(n - 2) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O((\frac{1 + \sqrt{5}}{2})^n)$。 - **空间复杂度**:$O(n)$。每次递归的空间复杂度是 $O(1)$, 调用栈的深度为 $n$,所以总的空间复杂度就是 $O(n)$。 ### 思路 2:动态规划 ###### 1. 阶段划分 按照台阶的层数进行划分为 $0 \sim n$。 ###### 2. 定义状态 定义状态 $dp[i]$ 为:爬到第 $i$ 阶台阶的方案数。 ###### 3. 状态转移方程 根据题目大意,每次只能爬 $1$ 或 $2$ 个台阶。则第 $i$ 阶楼梯只能从第 $i - 1$ 阶向上爬 $1$ 阶上来,或者从第 $i - 2$ 阶向上爬 $2$ 阶上来。所以可以推出状态转移方程为 $dp[i] = dp[i - 1] + dp[i - 2]$。 ###### 4. 初始条件 - 第 $0$ 层台阶方案数:可以看做 $1$ 种方法(从 $0$ 阶向上爬 $0$ 阶),即 $dp[0] = 1$。 - 第 $1$ 层台阶方案数:$1$ 种方法(从 $0$ 阶向上爬 $1$ 阶),即 $dp[1] = 1$。 - 第 $2$ 层台阶方案数:$2$ 种方法(从 $0$ 阶向上爬 $2$ 阶,或者从 $1$ 阶向上爬 $1$ 阶)。 ###### 5. 最终结果 根据状态定义,最终结果为 $dp[n]$,即爬到第 $n$ 阶台阶(即楼顶)的方案数为 $dp[n]$。 ### 思路 2:代码 ```python class Solution: def climbStairs(self, n: int) -> int: dp = [0 for _ in range(n + 1)] dp[0] = 1 dp[1] = 1 for i in range(2, n + 1): dp[i] = dp[i - 1] + dp[i - 2] return dp[n] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。因为 $dp[i]$ 的状态只依赖于 $dp[i - 1]$ 和 $dp[i - 2]$,所以可以使用 $3$ 个变量来分别表示 $dp[i]$、$dp[i - 1]$、$dp[i - 2]$,从而将空间复杂度优化到 $O(1)$。 ================================================ FILE: docs/solutions/0001-0099/combination-sum-ii.md ================================================ # [0040. 组合总和 II](https://leetcode.cn/problems/combination-sum-ii/) - 标签:数组、回溯 - 难度:中等 ## 题目链接 - [0040. 组合总和 II - 力扣](https://leetcode.cn/problems/combination-sum-ii/) ## 题目大意 **描述**:给定一个数组 `candidates` 和一个目标数 `target`。 **要求**:找出 `candidates` 中所有可以使数字和为目标数 `target` 的组合。 **说明**: - 数组 `candidates` 中的数字在每个组合中只能使用一次。 - $1 \le candidates.length \le 100$。 - $1 \le candidates[i] \le 50$。 **示例**: - 示例 1: ```python 输入: candidates = [10,1,2,7,6,1,5], target = 8, 输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ] ``` - 示例 2: ```python 输入: candidates = [2,5,2,1,2], target = 5, 输出: [ [1,2,2], [5] ] ``` ## 解题思路 ### 思路 1:回溯算法 跟「[0039. 组合总和](https://leetcode.cn/problems/combination-sum/)」不一样的地方在于本题不能有重复组合,所以关键步骤在于去重。 在回溯遍历的时候,下一层递归的 `start_index` 要从当前节点的后一位开始遍历,即 `i + 1` 位开始。而且统一递归层不能使用相同的元素,即需要增加一句判断 `if i > start_index and candidates[i] == candidates[i - 1]: continue`。 ### 思路 1:代码 ```python class Solution: res = [] path = [] def backtrack(self, candidates: List[int], target: int, sum: int, start_index: int): if sum > target: return if sum == target: self.res.append(self.path[:]) return for i in range(start_index, len(candidates)): if sum + candidates[i] > target: break if i > start_index and candidates[i] == candidates[i - 1]: continue sum += candidates[i] self.path.append(candidates[i]) self.backtrack(candidates, target, sum, i + 1) sum -= candidates[i] self.path.pop() def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]: self.res.clear() self.path.clear() candidates.sort() self.backtrack(candidates, target, 0, 0) return self.res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n \times n)$,其中 $n$ 是数组 `candidates` 的元素个数,$2^n$ 指的是所有状态数。 - **空间复杂度**:$O(target)$,递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $O(target)$,所以空间复杂度为 $O(target)$。 ================================================ FILE: docs/solutions/0001-0099/combination-sum.md ================================================ # [0039. 组合总和](https://leetcode.cn/problems/combination-sum/) - 标签:数组、回溯 - 难度:中等 ## 题目链接 - [0039. 组合总和 - 力扣](https://leetcode.cn/problems/combination-sum/) ## 题目大意 **描述**:给定一个无重复元素的正整数数组 `candidates` 和一个正整数 `target`。 **要求**:找出 `candidates` 中所有可以使数字和为目标数 `target` 的所有不同组合,并以列表形式返回。可以按照任意顺序返回这些组合。 **说明**: - 数组 `candidates` 中的数字可以无限重复选取。 - 如果至少一个数字的被选数量不同,则两种组合是不同的。 - $1 \le candidates.length \le 30$。 - $2 \le candidates[i] \le 40$。 - `candidates` 的所有元素互不相同。 - $1 \le target \le 40$。 **示例**: - 示例 1: ```python 输入:candidates = [2,3,6,7], target = 7 输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。 ``` - 示例 2: ```python 输入: candidates = [2,3,5], target = 8 输出: [[2,2,2,2],[2,3,3],[3,5]] ``` ## 解题思路 ### 思路 1:回溯算法 定义回溯方法,start_index = 1 开始进行回溯。 - 如果 `sum > target`,则直接返回。 - 如果 `sum == target`,则将 path 中的元素加入到 res 数组中。 - 然后对 `[start_index, n]` 范围内的数进行遍历取值。 - 如果 `sum + candidates[i] > target`,可以直接跳出循环。 - 将和累积,即 `sum += candidates[i]`,然后将当前元素 i 加入 path 数组。 - 递归遍历 `[start_index, n]` 上的数。 - 加之前的和回退,即 `sum -= candidates[i]`,然后将遍历的 i 元素进行回退。 - 最终返回 res 数组。 根据回溯算法三步走,写出对应的回溯算法。 1. **明确所有选择**:一个组合每个位置上的元素都可以从剩余可选元素中选出。 2. **明确终止条件**: - 当遍历到决策树的叶子节点时,就终止了。即当前路径搜索到末尾时,递归终止。 3. **将决策树和终止条件翻译成代码:** 1. 定义回溯函数: - `backtrack(total, start_index):` 函数的传入参数是 `total`(当前和)、`start_index`(剩余可选元素开始位置),全局变量是 `res`(存放所有符合条件结果的集合数组)和 `path`(存放当前符合条件的结果)。 - `backtrack(total, start_index):` 函数代表的含义是:当前组合和为 `total`,递归从 `candidates` 的 `start_index` 位置开始,选择剩下的元素。 2. 书写回溯函数主体(给出选择元素、递归搜索、撤销选择部分)。 - 从当前正在考虑元素,到数组结束为止,枚举出所有可选的元素。对于每一个可选元素: - 约束条件:之前已经选择的元素不再重复选用,只能从剩余元素中选择。 - 选择元素:将其添加到当前数组 `path` 中。 - 递归搜索:在选择该元素的情况下,继续递归选择剩下元素。 - 撤销选择:将该元素从当前结果数组 `path` 中移除。 ```python for i in range(start_index, len(candidates)): if total + candidates[i] > target: break total += candidates[i] path.append(candidates[i]) backtrack(total, i) total -= candidates[i] path.pop() ``` 3. 明确递归终止条件(给出递归终止条件,以及递归终止时的处理方法)。 - 当不可能再出现解(`total > target`),或者遍历到决策树的叶子节点时(`total == target`)时,就终止了。 - 当遍历到决策树的叶子节点时(`total == target`)时,将当前结果的数组 `path` 放入答案数组 `res` 中,递归停止。 ### 思路 1:代码 ```python class Solution: def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: res = [] path = [] def backtrack(total, start_index): if total > target: return if total == target: res.append(path[:]) return for i in range(start_index, len(candidates)): if total + candidates[i] > target: break total += candidates[i] path.append(candidates[i]) backtrack(total, i) total -= candidates[i] path.pop() candidates.sort() backtrack(0, 0) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n \times n)$,其中 $n$ 是数组 `candidates` 的元素个数,$2^n$ 指的是所有状态数。 - **空间复杂度**:$O(target)$,递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $O(target)$,所以空间复杂度为 $O(target)$。 ================================================ FILE: docs/solutions/0001-0099/combinations.md ================================================ # [0077. 组合](https://leetcode.cn/problems/combinations/) - 标签:回溯 - 难度:中等 ## 题目链接 - [0077. 组合 - 力扣](https://leetcode.cn/problems/combinations/) ## 题目大意 给定两个整数 `n` 和 `k`,返回范围 `[1, n]` 中所有可能的 `k` 个数的组合。可以按任何顺序返回答案。 ## 解题思路 组合问题通常可以用回溯算法来解决。定义两个数组 res、path。res 用来存放最终答案,path 用来存放当前符合条件的一个结果。再使用一个变量 start_index 来表示从哪一个数开始遍历。 定义回溯方法,start_index = 1 开始进行回溯。 - 如果 path 数组的长度等于 k,则将 path 中的元素加入到 res 数组中。 - 然后对 `[start_index, n]` 范围内的数进行遍历取值。 - 将当前元素 i 加入 path 数组。 - 递归遍历 `[start_index, n]` 上的数。 - 将遍历的 i 元素进行回退。 - 最终返回 res 数组。 ## 代码 ```python class Solution: res = [] path = [] def backtrack(self, n: int, k: int, start_index: int): if len(self.path) == k: self.res.append(self.path[:]) return for i in range(start_index, n - (k - len(self.path)) + 2): self.path.append(i) self.backtrack(n, k, i + 1) self.path.pop() def combine(self, n: int, k: int) -> List[List[int]]: self.res.clear() self.path.clear() self.backtrack(n, k, 1) return self.res ``` ================================================ FILE: docs/solutions/0001-0099/container-with-most-water.md ================================================ # [0011. 盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/) - 标签:贪心、数组、双指针 - 难度:中等 ## 题目链接 - [0011. 盛最多水的容器 - 力扣](https://leetcode.cn/problems/container-with-most-water/) ## 题目大意 **描述**:给定 $n$ 个非负整数 $a_1,a_2, ...,a_n$,每个数代表坐标中的一个点 $(i, a_i)$。在坐标内画 $n$ 条垂直线,垂直线 $i$ 的两个端点分别为 $(i, a_i)$ 和 $(i, 0)$。 **要求**:找出其中的两条线,使得它们与 $x$ 轴共同构成的容器可以容纳最多的水。 **说明**: - $n == height.length$。 - $2 \le n \le 10^5$。 - $0 \le height[i] \le 10^4$。 **示例**: - 示例 1: ![](https://aliyun-lc-upload.oss-cn-hangzhou.aliyuncs.com/aliyun-lc-upload/uploads/2018/07/25/question_11.jpg) ```python 输入:[1,8,6,2,5,4,8,3,7] 输出:49 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。 ``` ## 解题思路 ### 思路 1:对撞指针 从示例中可以看出,如果确定好左右两端的直线,容纳的水量是由 `左右两端直线中较低直线的高度 * 两端直线之间的距离 ` 所决定的。所以我们应该使得 **较低直线的高度尽可能的高**,这样才能使盛水面积尽可能的大。 可以使用对撞指针求解。移动较低直线所在的指针位置,从而得到不同的高度和面积,最终获取其中最大的面积。具体做法如下: 1. 使用两个指针 `left`,`right`。`left` 指向数组开始位置,`right` 指向数组结束位置。 2. 计算 `left` 和 `right` 所构成的面积值,同时维护更新最大面积值。 3. 判断 `left` 和 `right` 的高度值大小。 1. 如果 `left` 指向的直线高度比较低,则将 `left` 指针右移。 2. 如果 `right` 指向的直线高度比较低,则将 `right` 指针左移。 4. 如果遇到 `left == right`,跳出循环,最后返回最大的面积。 ### 思路 1:代码 ```python class Solution: def maxArea(self, height: List[int]) -> int: left = 0 right = len(height) - 1 ans = 0 while left < right: area = min(height[left], height[right]) * (right-left) ans = max(ans, area) if height[left] < height[right]: left += 1 else: right -= 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/count-and-say.md ================================================ # [0038. 外观数列](https://leetcode.cn/problems/count-and-say/) - 标签:字符串 - 难度:中等 ## 题目链接 - [0038. 外观数列 - 力扣](https://leetcode.cn/problems/count-and-say/) ## 题目大意 给定一个正整数 n,$(1 \le n \le 30)$,要求输出外观数列的第 n 项。 外观数列:整数序列,数字由 1 开始,每一项都是对前一项的描述 例如: | 1. | 1 | 由 1 开始 | | --- | ---: | ------------------- | | 2. | 11 | 表示 1 个 1 | | 3. | 21 | 表示 2 个 1 | | 4. | 1211 | 表示 1 个 1,1 个 2 | ## 解题思路 模拟题目遍历求解。 将 ans 设为 "1",每次遍历判断相邻且相同的数字有多少个,再将 ans 拼接上「数字个数 + 数字」。 ## 代码 ```python class Solution: def countAndSay(self, n: int) -> str: ans = "1" for _ in range(1, n): s = "" start = 0 for i in range(len(ans)): if ans[i] != ans[start]: s += str(i-start) + ans[start] start = i s += str(len(ans)-start) + ans[start] ans = s return ans ``` ================================================ FILE: docs/solutions/0001-0099/decode-ways.md ================================================ # [0091. 解码方法](https://leetcode.cn/problems/decode-ways/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [0091. 解码方法 - 力扣](https://leetcode.cn/problems/decode-ways/) ## 题目大意 **描述**:给定一个数字字符串 $s$。该字符串已经按照下面的映射关系进行了编码: - `A` 映射为 $1$。 - `B` 映射为 $2$。 - ... - `Z` 映射为 $26$。 基于上述映射的方法,现在对字符串 $s$ 进行「解码」。即从数字到字母进行反向映射。比如 `"11106"` 可以映射为: - `"AAJF"`,将消息分组为 $(1 1 10 6)$。 - `"KJF"`,将消息分组为 $(11 10 6)$。 **要求**:计算出共有多少种可能的解码方案。 **说明**: - $1 \le s.length \le 100$。 - $s$ 只包含数字,并且可能包含前导零。 - 题目数据保证答案肯定是一个 $32$ 位的整数。 **示例**: - 示例 1: ```python 输入:s = "226" 输出:3 解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照字符串的结尾位置进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为:字符串 $s$ 前 $i$ 个字符构成的字符串可能构成的翻译方案数。 ###### 3. 状态转移方程 $dp[i]$ 的来源有两种情况: 1. 使用了一个字符,对 $s[i]$ 进行翻译。只要 $s[i] != 0$,就可以被翻译为 `A` ~ `I` 的某个字母,此时方案数为 $dp[i] = dp[i - 1]$。 2. 使用了两个字符,对 $s[i - 1]$ 和 $s[i]$ 进行翻译,只有 $s[i - 1] != 0$,且 $s[i - 1]$ 和 $s[i]$ 组成的整数必须小于等于 $26$ 才能翻译,可以翻译为 `J` ~ `Z` 中的某字母,此时方案数为 $dp[i] = dp[i - 2]$。 这两种情况有可能是同时存在的,也有可能都不存在。在进行转移的时候,将符合要求的方案数累加起来即可。 状态转移方程可以写为: $dp[i] += \begin{cases} dp[i-1] & \quad s[i] \ne 0 \cr dp[i-2] & \quad s[i-1] \ne 0, s[i-1:i] \le 26 \end{cases}$ ###### 4. 初始条件 - 字符串为空时,只有一个翻译方案,翻译为空字符串,即 $dp[0] = 1$。 - 字符串只有一个字符时,需要考虑该字符是否为 $0$,不为 $0$ 的话,$dp[1] = 1$,为 $0$ 的话,$dp[0] = 0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示为:字符串 $s$ 前 $i$ 个字符构成的字符串可能构成的翻译方案数。则最终结果为 $dp[size]$,$size$ 为字符串长度。 ### 思路 1:动态规划代码 ```python class Solution: def numDecodings(self, s: str) -> int: size = len(s) dp = [0 for _ in range(size + 1)] dp[0] = 1 for i in range(1, size + 1): if s[i - 1] != '0': dp[i] += dp[i - 1] if i > 1 and s[i - 2] != '0' and int(s[i - 2: i]) <= 26: dp[i] += dp[i - 2] return dp[size] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度是 $O(n)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0001-0099/divide-two-integers.md ================================================ # [0029. 两数相除](https://leetcode.cn/problems/divide-two-integers/) - 标签:位运算、数学 - 难度:中等 ## 题目链接 - [0029. 两数相除 - 力扣](https://leetcode.cn/problems/divide-two-integers/) ## 题目大意 给定两个整数,被除数 dividend 和除数 divisor。要求返回两数相除的商,并且不能使用乘法,除法和取余运算。取值范围在 $[-2^{31}, 2^{31}-1]$。如果结果溢出,则返回 $2^{31} - 1$。 ## 解题思路 题目要求不能使用乘法,除法和取余运算。 可以把被除数和除数当做二进制,这样进行运算的时候,就可以通过移位运算来实现二进制的乘除。 - 先将除数不断左移,移位到位数大于或等于被除数。记录其移位次数 count。 - 然后再将除数右移 count 次,模拟二进制除法运算。 - 如果当前被除数大于等于除数,则将 1 左移 count 位,即为当前位的商,并将其累加答案上。再用除数减去被除数,进行下一次运算。 ## 代码 ```python class Solution: def divide(self, dividend: int, divisor: int) -> int: MIN_INT, MAX_INT = -2147483648, 2147483647 # 标记被除数和除数是否异号 symbol = True if (dividend ^ divisor) < 0 else False # 将被除数和除数转换为正数处理 if dividend < 0: dividend = -dividend if divisor < 0: divisor = -divisor # 除数不断左移,移位到位数大于或等于被除数 count = 0 while dividend >= divisor: count += 1 divisor <<= 1 # 向右移位,不断模拟二进制除法运算 res = 0 while count > 0: count -= 1 divisor >>= 1 if dividend >= divisor: res += (1 << count) dividend -= divisor if symbol: res = -res if MIN_INT <= res <= MAX_INT: return res else: return MAX_INT ``` ================================================ FILE: docs/solutions/0001-0099/edit-distance.md ================================================ # [0072. 编辑距离](https://leetcode.cn/problems/edit-distance/) - 标签:字符串、动态规划 - 难度:困难 ## 题目链接 - [0072. 编辑距离 - 力扣](https://leetcode.cn/problems/edit-distance/) ## 题目大意 **描述**:给定两个单词 $word1$、$word2$。 对一个单词可以进行以下三种操作: - 插入一个字符 - 删除一个字符 - 替换一个字符 **要求**:计算出将 $word1$ 转换为 $word2$ 所使用的最少操作数。 **说明**: - $0 \le word1.length, word2.length \le 500$。 - $word1$ 和 $word2$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:word1 = "horse", word2 = "ros" 输出:3 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e') ``` - 示例 2: ```python 输入:word1 = "intention", word2 = "execution" 输出:5 解释: intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u') ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照两个字符串的结尾位置进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:「以 $word1$ 中前 $i$ 个字符组成的子字符串 $str1$」变为「以 $word2$ 中前 $j$ 个字符组成的子字符串 $str2$」,所需要的最少操作次数。 ###### 3. 状态转移方程 1. 如果当前字符相同($word1[i - 1] = word2[j - 1]$),无需插入、删除、替换。$dp[i][j] = dp[i - 1][j - 1]$。 2. 如果当前字符不同($word1[i - 1] \ne word2[j - 1]$),$dp[i][j]$ 取源于以下三种情况中的最小情况: 1. 替换($word1[i - 1]$ 替换为 $word2[j - 1]$):最少操作次数依赖于「以 $word1$ 中前 $i - 1$ 个字符组成的子字符串 $str1$」变为「以 $word2$ 中前 $j - 1$ 个字符组成的子字符串 $str2$」,再加上替换的操作数 $1$,即:$dp[i][j] = dp[i - 1][j - 1] + 1$。 2. 插入($word1$ 在第 $i - 1$ 位置上插入元素):最少操作次数依赖于「以 $word1$ 中前 $i - 1$ 个字符组成的子字符串 $str1$」 变为「以 $word2$ 中前 $j$ 个字符组成的子字符串 $str2$」,再加上插入需要的操作数 $1$,即:$dp[i][j] = dp[i - 1][j] + 1$。 3. 删除($word1$ 删除第 $i - 1$ 位置元素):最少操作次数依赖于「以 $word1$ 中前 $i$ 个字符组成的子字符串 $str1$」变为「以 $word2$ 中前 $j - 1$ 个字符组成的子字符串 $str2$」,再加上删除需要的操作数 $1$,即:$dp[i][j] = dp[i][j - 1] + 1$。 综合上述情况,状态转移方程为: $dp[i][j] = \begin{cases} dp[i - 1][j - 1] & word1[i - 1] = word2[j - 1] \cr min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1 & word1[i - 1] \ne word2[j - 1] \end{cases}$ ###### 4. 初始条件 - 当 $i = 0$,「以 $word1$ 中前 $i$ 个字符组成的子字符串 $str1$」为空字符串,「$str1$」变为「以 $word2$ 中前 $j$ 个字符组成的子字符串 $str2$」时,至少需要插入 $j$ 次,即:$dp[0][j] = j$。 - 当 $j = 0$,「以 $word2$ 中前 $j$ 个字符组成的子字符串 $str2$」为空字符串,「以 $word1$ 中前 $i$ 个字符组成的子字符串 $str1$」变为「$str2$」时,至少需要删除 $i$ 次,即:$dp[i][0] = i$。 ###### 5. 最终结果 根据状态定义,最后输出 $dp[sise1][size2]$(即 $word1$ 变为 $word2$ 所使用的最少操作数)即可。其中 $size1$、$size2$ 分别为 $word1$、$word2$ 的字符串长度。 ### 思路 1:代码 ```python class Solution: def minDistance(self, word1: str, word2: str) -> int: size1 = len(word1) size2 = len(word2) dp = [[0 for _ in range(size2 + 1)] for _ in range(size1 + 1)] for i in range(size1 + 1): dp[i][0] = i for j in range(size2 + 1): dp[0][j] = j for i in range(1, size1 + 1): for j in range(1, size2 + 1): if word1[i - 1] == word2[j - 1]: dp[i][j] = dp[i - 1][j - 1] else: dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1 return dp[size1][size2] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$、$m$ 分别是字符串 $word1$、$word2$ 的长度。两重循环遍历的时间复杂度是 $O(n \times m)$,所以总的时间复杂度为 $O(n \times m)$。 - **空间复杂度**:$O(n \times m)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n \times m)$。 ================================================ FILE: docs/solutions/0001-0099/find-first-and-last-position-of-element-in-sorted-array.md ================================================ # [0034. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [0034. 在排序数组中查找元素的第一个和最后一个位置 - 力扣](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/) ## 题目大意 **描述**:给你一个按照非递减顺序排列的整数数组 `nums`,和一个目标值 `target`。 **要求**:找出给定目标值在数组中的开始位置和结束位置。 **说明**: - 要求使用时间复杂度为 $O(\log n)$ 的算法解决问题。 **示例**: - 示例 1: ```python 输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4] ``` - 示例 2: ```python 输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1] ``` ## 解题思路 ### 思路 1:二分查找 要求使用时间复杂度为 $O(\log n)$ 的算法解决问题,那么就需要使用「二分查找算法」了。 - 进行两次二分查找,第一次尽量向左搜索。第二次尽量向右搜索。 ### 思路 1:代码 ```python class Solution: def searchRange(self, nums: List[int], target: int) -> List[int]: ans = [-1, -1] n = len(nums) if n == 0: return ans left = 0 right = n - 1 while left < right: mid = left + (right - left) // 2 if nums[mid] < target: left = mid + 1 else: right = mid if nums[left] != target: return ans ans[0] = left left = 0 right = n - 1 while left < right: mid = left + (right - left + 1) // 2 if nums[mid] > target: right = mid - 1 else: left = mid if nums[left] == target: ans[1] = left return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log_2 n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/find-the-index-of-the-first-occurrence-in-a-string.md ================================================ # [0028. 找出字符串中第一个匹配项的下标](https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/) - 标签:双指针、字符串、字符串匹配 - 难度:中等 ## 题目链接 - [0028. 找出字符串中第一个匹配项的下标 - 力扣](https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/) ## 题目大意 **描述**:给定两个字符串 `haystack` 和 `needle`。 **要求**:在 `haystack` 字符串中找出 `needle` 字符串出现的第一个位置(从 `0` 开始)。如果不存在,则返回 `-1`。 **说明**: - 当 `needle` 为空字符串时,返回 `0`。 - $1 \le haystack.length, needle.length \le 10^4$。 - `haystack` 和 `needle` 仅由小写英文字符组成。 **示例**: - 示例 1: ```python 输入:haystack = "hello", needle = "ll" 输出:2 解释:"sad" 在下标 0 和 6 处匹配。第一个匹配项的下标是 0 ,所以返回 0 。 ``` - 示例 2: ```python 输入:haystack = "leetcode", needle = "leeto" 输出:-1 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。 ``` ## 解题思路 字符串匹配的经典题目。常见的字符串匹配算法有:BF(Brute Force)算法、RK(Robin-Karp)算法、KMP(Knuth Morris Pratt)算法、BM(Boyer Moore)算法、Horspool 算法、Sunday 算法等。 ### 思路 1:BF(Brute Force)算法 **BF 算法思想**:对于给定文本串 `T` 与模式串 `p`,从文本串的第一个字符开始与模式串 `p` 的第一个字符进行比较,如果相等,则继续逐个比较后续字符,否则从文本串 `T` 的第二个字符起重新和模式串 `p` 进行比较。依次类推,直到模式串 `p` 中每个字符依次与文本串 `T` 的一个连续子串相等,则模式匹配成功。否则模式匹配失败。 BF 算法具体步骤如下: 1. 对于给定的文本串 `T` 与模式串 `p`,求出文本串 `T` 的长度为 `n`,模式串 `p` 的长度为 `m`。 2. 同时遍历文本串 `T` 和模式串 `p`,先将 `T[0]` 与 `p[0]` 进行比较。 1. 如果相等,则继续比较 `T[1]` 和 `p[1]`。以此类推,一直到模式串 `p` 的末尾 `p[m - 1]` 为止。 2. 如果不相等,则将文本串 `T` 移动到上次匹配开始位置的下一个字符位置,模式串 `p` 则回退到开始位置,再依次进行比较。 3. 当遍历完文本串 `T` 或者模式串 `p` 的时候停止搜索。 ### 思路 1:代码 ```python class Solution: def strStr(self, haystack: str, needle: str) -> int: i = 0 j = 0 len1 = len(haystack) len2 = len(needle) while i < len1 and j < len2: if haystack[i] == needle[j]: i += 1 j += 1 else: i = i - (j - 1) j = 0 if j == len2: return i - j else: return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:平均时间复杂度为 $O(n + m)$,最坏时间复杂度为 $O(m \times n)$。其中文本串 $T$ 的长度为 $n$,模式串 $p$ 的长度为 $m$。 - **空间复杂度**:$O(1)$。 ### 思路 2:RK(Robin Karp)算法 **RK 算法思想**:对于给定文本串 `T` 与模式串 `p`,通过滚动哈希算快速筛选出与模式串 `p` 不匹配的文本位置,然后在其余位置继续检查匹配项。 RK 算法具体步骤如下: 1. 对于给定的文本串 `T` 与模式串 `p`,求出文本串 `T` 的长度为 `n`,模式串 `p` 的长度为 `m`。 2. 通过滚动哈希算法求出模式串 `p` 的哈希值 `hash_p`。 3. 再通过滚动哈希算法对文本串 `T` 中 `n - m + 1` 个子串分别求哈希值 `hash_t`。 4. 然后逐个与模式串的哈希值比较大小。 1. 如果当前子串的哈希值 `hash_t` 与模式串的哈希值 `hash_p` 不同,则说明两者不匹配,则继续向后匹配。 2. 如果当前子串的哈希值 `hash_t` 与模式串的哈希值 `hash_p` 相等,则验证当前子串和模式串的每个字符是否真的相等(避免哈希冲突)。 1. 如果当前子串和模式串的每个字符相等,则说明当前子串和模式串匹配。 2. 如果当前子串和模式串的每个字符不相等,则说明两者不匹配,继续向后匹配。 5. 比较到末尾,如果仍未成功匹配,则说明文本串 `T` 中不包含模式串 `p`,方法返回 `-1`。 ### 思路 2:代码 ```python class Solution: def strStr(self, haystack: str, needle: str) -> int: def rabinKarp(T: str, p: str) -> int: len1, len2 = len(T), len(p) hash_p = hash(p) for i in range(len1 - len2 + 1): hash_T = hash(T[i: i + len2]) if hash_p != hash_T: continue k = 0 for j in range(len2): if T[i + j] != p[j]: break k += 1 if k == len2: return i return -1 return rabinKarp(haystack, needle) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中文本串 $T$ 的长度为 $n$,模式串 $p$ 的长度为 $m$。 - **空间复杂度**:$O(m)$。 ### 思路 3:KMP(Knuth Morris Pratt)算法 **KMP 算法思想**:对于给定文本串 `T` 与模式串 `p`,当发现文本串 `T` 的某个字符与模式串 `p` 不匹配的时候,可以利用匹配失败后的信息,尽量减少模式串与文本串的匹配次数,避免文本串位置的回退,以达到快速匹配的目的。 KMP 算法具体步骤如下: 1. 根据 `next` 数组的构造步骤生成「前缀表」`next`。 2. 使用两个指针 `i`、`j`,其中 `i` 指向文本串中当前匹配的位置,`j` 指向模式串中当前匹配的位置。初始时,`i = 0`,`j = 0`。 3. 循环判断模式串前缀是否匹配成功,如果模式串前缀匹配不成功,将模式串进行回退,即 `j = next[j - 1]`,直到 `j == 0` 时或前缀匹配成功时停止回退。 4. 如果当前模式串前缀匹配成功,则令模式串向右移动 `1` 位,即 `j += 1`。 5. 如果当前模式串 **完全** 匹配成功,则返回模式串 `p` 在文本串 `T` 中的开始位置,即 `i - j + 1`。 6. 如果还未完全匹配成功,则令文本串向右移动 `1` 位,即 `i += 1`,然后继续匹配。 7. 如果直到文本串遍历完也未完全匹配成功,则说明匹配失败,返回 `-1`。 ### 思路 3:代码 ```python class Solution: def strStr(self, haystack: str, needle: str) -> int: # KMP 匹配算法,T 为文本串,p 为模式串 def kmp(T: str, p: str) -> int: n, m = len(T), len(p) next = generateNext(p) # 生成 next 数组 i, j = 0, 0 while i < n and j < m: if j == -1 or T[i] == p[j]: i += 1 j += 1 else: j = next[j] if j == m: return i - j return -1 # 生成 next 数组 # next[i] 表示坏字符在模式串中最后一次出现的位置 def generateNext(p: str): m = len(p) next = [-1 for _ in range(m)] # 初始化数组元素全部为 -1 i, k = 0, -1 while i < m - 1: # 生成下一个 next 元素 if k == -1 or p[i] == p[k]: i += 1 k += 1 if p[i] == p[k]: next[i] = next[k] # 设置 next 元素 else: next[i] = k # 退到更短相同前缀 else: k = next[k] return next return kmp(haystack, needle) ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n + m)$,其中文本串 $T$ 的长度为 $n$,模式串 $p$ 的长度为 $m$。 - **空间复杂度**:$O(m)$。 ### 思路 4:BM(Boyer Moore)算法 **BM 算法思想**:对于给定文本串 `T` 与模式串 `p`,先对模式串 `p` 进行预处理。然后在匹配的过程中,当发现文本串 `T` 的某个字符与模式串 `p` 不匹配的时候,根据启发策略,能够直接尽可能地跳过一些无法匹配的情况,将模式串多向后滑动几位。 BM 算法具体步骤如下: 1. 计算出文本串 `T` 的长度为 `n`,模式串 `p` 的长度为 `m`。 2. 先对模式串 `p` 进行预处理,生成坏字符位置表 `bc_table` 和好后缀规则后移位数表 `gs_talbe`。 3. 将模式串 `p` 的头部与文本串 `T` 对齐,将 `i` 指向文本串开始位置,即 `i = 0`。`j` 指向模式串末尾位置,即 `j = m - 1`,然后从模式串末尾位置开始进行逐位比较。 1. 如果文本串对应位置 `T[i + j]` 上的字符与 `p[j]` 相同,则继续比较前一位字符。 1. 如果模式串全部匹配完毕,则返回模式串 `p` 在文本串中的开始位置 `i`。 2. 如果文本串对应位置 `T[i + j]` 上的字符与 `p[j]` 不相同,则: 1. 根据坏字符位置表计算出在「坏字符规则」下的移动距离 `bad_move`。 2. 根据好后缀规则后移位数表计算出在「好后缀规则」下的移动距离 `good_mode`。 3. 取两种移动距离的最大值,然后对模式串进行移动,即 `i += max(bad_move, good_move)`。 4. 如果移动到末尾也没有找到匹配情况,则返回 `-1`。 ### 思路 4:代码 ```python class Solution: def strStr(self, haystack: str, needle: str) -> int: def boyerMoore(T: str, p: str) -> int: n, m = len(T), len(p) bc_table = generateBadCharTable(p) # 生成坏字符位置表 gs_list = generageGoodSuffixList(p) # 生成好后缀规则后移位数表 i = 0 while i <= n - m: j = m - 1 while j > -1 and T[i + j] == p[j]: j -= 1 if j < 0: return i bad_move = j - bc_table.get(T[i + j], -1) good_move = gs_list[j] i += max(bad_move, good_move) return -1 # 生成坏字符位置表 def generateBadCharTable(p: str): bc_table = dict() for i in range(len(p)): bc_table[p[i]] = i # 坏字符在模式串中最后一次出现的位置 return bc_table # 生成好后缀规则后移位数表 def generageGoodSuffixList(p: str): m = len(p) gs_list = [m for _ in range(m)] suffix = generageSuffixArray(p) j = 0 for i in range(m - 1, -1, -1): if suffix[i] == i + 1: while j < m - 1 - i: if gs_list[j] == m: gs_list[j] = m - 1 - i j += 1 for i in range(m - 1): gs_list[m - 1 - suffix[i]] = m - 1 - i return gs_list def generageSuffixArray(p: str): m = len(p) suffix = [m for _ in range(m)] for i in range(m - 2, -1, -1): start = i while start >= 0 and p[start] == p[m - 1 - i + start]: start -= 1 suffix[i] = i - start return suffix return boyerMoore(haystack, needle) ``` ### 思路 4:复杂度分析 - **时间复杂度**:$O(n + \sigma)$,其中文本串 $T$ 的长度为 $n$,字符集的大小是 $\sigma$。 - **空间复杂度**:$O(m)$。其中模式串 $p$ 的长度为 $m$。 ### 思路 5:Horspool 算法 **Horspool 算法思想**:对于给定文本串 `T` 与模式串 `p`,先对模式串 `p` 进行预处理。然后在匹配的过程中,当发现文本串 `T` 的某个字符与模式串 `p` 不匹配的时候,根据启发策略,能够尽可能的跳过一些无法匹配的情况,将模式串多向后滑动几位。 Horspool 算法具体步骤如下: 1. 计算出文本串 `T` 的长度为 `n`,模式串 `p` 的长度为 `m`。 2. 先对模式串 `p` 进行预处理,生成后移位数表 `bc_table`。 3. 将模式串 `p` 的头部与文本串 `T` 对齐,将 `i` 指向文本串开始位置,即 `i = 0`。`j` 指向模式串末尾位置,即 `j = m - 1`,然后从模式串末尾位置开始比较。 1. 如果文本串对应位置的字符 `T[i + j]` 与模式串对应字符 `p[j]` 相同,则继续比较前一位字符。 1. 如果模式串全部匹配完毕,则返回模式串 `p` 在文本串中的开始位置 `i`。 2. 如果文本串对应位置的字符 `T[i + j]` 与模式串对应字符 `p[j]` 不同,则: 1. 根据后移位数表 `bc_table` 和模式串末尾位置对应的文本串上的字符 `T[i + m - 1]` ,计算出可移动距离 `bc_table[T[i + m - 1]]`,然后将模式串进行后移。 4. 如果移动到末尾也没有找到匹配情况,则返回 `-1`。 ### 思路 5:代码 ```python class Solution: def strStr(self, haystack: str, needle: str) -> int: def horspool(T: str, p: str) -> int: n, m = len(T), len(p) bc_table = generateBadCharTable(p) i = 0 while i <= n - m: j = m - 1 while j > -1 and T[i + j] == p[j]: j -= 1 if j < 0: return i i += bc_table.get(T[i + m - 1], m) return -1 # 生成后移位置表 # bc_table[bad_char] 表示坏字符在模式串中最后一次出现的位置 def generateBadCharTable(p: str): m = len(p) bc_table = dict() for i in range(m - 1): bc_table[p[i]] = m - i - 1 # 更新坏字符在模式串中最后一次出现的位置 return bc_table return horspool(haystack, needle) ``` ### 思路 5:复杂度分析 - **时间复杂度**:$O(n)$。其中文本串 $T$ 的长度为 $n$。 - **空间复杂度**:$O(m)$。其中模式串 $p$ 的长度为 $m$。 ### 思路 6:Sunday 算法 **Sunday 算法思想**:对于给定文本串 `T` 与模式串 `p`,先对模式串 `p` 进行预处理。然后在匹配的过程中,当发现文本串 `T` 的某个字符与模式串 `p` 不匹配的时候,根据启发策略,能够尽可能的跳过一些无法匹配的情况,将模式串多向后滑动几位。 Sunday 算法具体步骤如下: 1. 计算出文本串 `T` 的长度为 `n`,模式串 `p` 的长度为 `m`。 2. 先对模式串 `p` 进行预处理,生成后移位数表 `bc_table`。 3. 将模式串 `p` 的头部与文本串 `T` 对齐,将 `i` 指向文本串开始位置,即 `i = 0`。`j` 指向模式串末尾位置,即 `j = m - 1`,然后从模式串末尾位置开始比较。 1. 如果文本串对应位置的字符 `T[i + j]` 与模式串对应字符 `p[j]` 相同,则继续比较前一位字符。 1. 如果模式串全部匹配完毕,则返回模式串 `p` 在文本串中的开始位置 `i`。 2. 如果文本串对应位置的字符 `T[i + j]` 与模式串对应字符 `p[j]` 不同,则: 1. 根据后移位数表 `bc_table` 和模式串末尾位置对应的文本串上的字符 `T[i + m - 1]` ,计算出可移动距离 `bc_table[T[i + m - 1]]`,然后将模式串进行后移。 4. 如果移动到末尾也没有找到匹配情况,则返回 `-1`。 ### 思路 6:代码 ```python class Solution: def strStr(self, haystack: str, needle: str) -> int: # sunday 算法,T 为文本串,p 为模式串 def sunday(T: str, p: str) -> int: n, m = len(T), len(p) if m == 0: return 0 bc_table = generateBadCharTable(p) # 生成后移位数表 i = 0 while i <= n - m: if T[i: i + m] == p: return i # 匹配完成,返回模式串 p 在文本串 T 中的位置 if i + m >= n: return -1 i += bc_table.get(T[i + m], m + 1) # 通过后移位数表,向右进行进行快速移动 return -1 # 匹配失败 # 生成后移位数表 # bc_table[bad_char] 表示遇到坏字符可以向右移动的距离 def generateBadCharTable(p: str): m = len(p) bc_table = dict() for i in range(m): bc_table[p[i]] = m - i # 更新遇到坏字符可向右移动的距离 return bc_table return sunday(haystack, needle) ``` ### 思路 6:复杂度分析 - **时间复杂度**:$O(n)$。其中文本串 $T$ 的长度为 $n$。 - **空间复杂度**:$O(m)$。其中模式串 $p$ 的长度为 $m$。 ================================================ FILE: docs/solutions/0001-0099/first-missing-positive.md ================================================ # [0041. 缺失的第一个正数](https://leetcode.cn/problems/first-missing-positive/) - 标签:数组、哈希表 - 难度:困难 ## 题目链接 - [0041. 缺失的第一个正数 - 力扣](https://leetcode.cn/problems/first-missing-positive/) ## 题目大意 **描述**:给定一个未排序的整数数组 `nums`。 **要求**:找出其中没有出现的最小的正整数。 **说明**: - $1 \le nums.length \le 5 * 10^5$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 - 要求实现时间复杂度为 `O(n)` 并且只使用常数级别额外空间的解决方案。 **示例**: - 示例 1: ```python 输入:nums = [1,2,0] 输出:3 ``` - 示例 2: ```python 输入:nums = [3,4,-1,1] 输出:2 ``` ## 解题思路 ### 思路 1:哈希表、原地哈希 如果使用普通的哈希表,我们只需要遍历一遍数组,将对应整数存入到哈希表中,再从 `1` 开始,依次判断对应正数是否在哈希表中即可。但是这种做法的空间复杂度为 $O(n)$,不满足常数级别的额外空间要求。 我们可以将当前数组视为哈希表。一个长度为 `n` 的数组,对应存储的元素值应该为 `[1, n + 1]` 之间,其中还包含一个缺失的元素。 1. 我们可以遍历一遍数组,将当前元素放到其对应位置上(比如元素值为 `1` 的元素放到数组第 `0` 个位置上、元素值为 `2` 的元素放到数组第 `1` 个位置上,等等)。 2. 然后再次遍历一遍数组。遇到第一个元素值不等于下标 + 1 的元素,就是答案要求的缺失的第一个正数。 3. 如果遍历完没有在数组中找到缺失的第一个正数,则缺失的第一个正数是 `n + 1`。 4. 最后返回我们找到的缺失的第一个正数。 ### 思路 1:代码 ```python class Solution: def firstMissingPositive(self, nums: List[int]) -> int: size = len(nums) for i in range(size): while 1 <= nums[i] <= size and nums[i] != nums[nums[i] - 1]: index1 = i index2 = nums[i] - 1 nums[index1], nums[index2] = nums[index2], nums[index1] for i in range(size): if nums[i] != i + 1: return i + 1 return size + 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 `nums` 的元素个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/generate-parentheses.md ================================================ # [0022. 括号生成](https://leetcode.cn/problems/generate-parentheses/) - 标签:字符串、回溯算法 - 难度:中等 ## 题目链接 - [0022. 括号生成 - 力扣](https://leetcode.cn/problems/generate-parentheses/) ## 题目大意 **描述**:给定一个整数 $n$,代表生成括号的对数。 **要求**:生成所有有可能且有效的括号组合。 **说明**: - $1 \le n \le 8$。 **示例**: - 示例 1: ```python 输入:n = 3 输出:["((()))","(()())","(())()","()(())","()()()"] ``` - 示例 2: ```python 输入:n = 1 输出:["()"] ``` ## 解题思路 ### 思路 1:回溯算法 为了生成的括号组合是有效的,回溯的时候,使用一个标记变量 `symbol` 来表示是否当前组合是否成对匹配。 如果在当前组合中增加一个 `(`,则令 `symbol` 加 `1`,如果增加一个 `)`,则令 `symbol` 减 `1`。 显然只有在 `symbol < n` 的时候,才能增加 `(`,在 `symbol > 0` 的时候,才能增加 `)`。 如果最终生成 $2 \times n$ 的括号组合,并且 `symbol == 0`,则说明当前组合是有效的,将其加入到最终答案数组中。 下面我们根据回溯算法三步走,写出对应的回溯算法。 1. **明确所有选择**:$2 \times n$ 的括号组合中的每个位置,都可以从 `(` 或者 `)` 中选出。并且,只有在 `symbol < n` 的时候,才能选择 `(`,在 `symbol > 0` 的时候,才能选择 `)`。 2. **明确终止条件**: - 当遍历到决策树的叶子节点时,就终止了。即当前路径搜索到末尾时,递归终止。 3. **将决策树和终止条件翻译成代码:** 1. 定义回溯函数: - `backtracking(symbol, index):` 函数的传入参数是 `symbol`(用于表示是否当前组合是否成对匹配),`index`(当前元素下标),全局变量是 `parentheses`(用于保存所有有效的括号组合),`parenthesis`(当前括号组合),。 - `backtracking(symbol, index)` 函数代表的含义是:递归根据 `symbol`,在 `(` 和 `)` 中选择第 `index` 个元素。 2. 书写回溯函数主体(给出选择元素、递归搜索、撤销选择部分)。 - 从当前正在考虑元素,到第 $2 \times n$ 个元素为止,枚举出所有可选的元素。对于每一个可选元素: - 约束条件:`symbol < n` 或者 `symbol > 0`。 - 选择元素:将其添加到当前括号组合 `parenthesis` 中。 - 递归搜索:在选择该元素的情况下,继续递归选择剩下元素。 - 撤销选择:将该元素从当前括号组合 `parenthesis` 中移除。 ```python if symbol < n: parenthesis.append('(') backtrack(symbol + 1, index + 1) parenthesis.pop() if symbol > 0: parenthesis.append(')') backtrack(symbol - 1, index + 1) parenthesis.pop() ``` 3. 明确递归终止条件(给出递归终止条件,以及递归终止时的处理方法)。 - 当遍历到决策树的叶子节点时,就终止了。也就是当 `index == 2 * n` 时,递归停止。 - 并且在 `symbol == 0` 时,当前组合才是有效的,此时将其加入到最终答案数组中。 ### 思路 1:代码 ```python class Solution: def generateParenthesis(self, n: int) -> List[str]: parentheses = [] # 存放所有括号组合 parenthesis = [] # 存放当前括号组合 def backtrack(symbol, index): if n * 2 == index: if symbol == 0: parentheses.append("".join(parenthesis)) else: if symbol < n: parenthesis.append('(') backtrack(symbol + 1, index + 1) parenthesis.pop() if symbol > 0: parenthesis.append(')') backtrack(symbol - 1, index + 1) parenthesis.pop() backtrack(0, 0) return parentheses ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\frac{2^{2 \times n}}{\sqrt{n}})$,其中 $n$ 为生成括号的对数。 - **空间复杂度**:$O(n)$。 ## 参考资料 - 【题解】[22. 括号生成 - 力扣(Leetcode)](https://leetcode.cn/problems/generate-parentheses/solutions/192912/gua-hao-sheng-cheng-by-leetcode-solution/) ================================================ FILE: docs/solutions/0001-0099/gray-code.md ================================================ # [0089. 格雷编码](https://leetcode.cn/problems/gray-code/) - 标签:位运算、数学、回溯 - 难度:中等 ## 题目链接 - [0089. 格雷编码 - 力扣](https://leetcode.cn/problems/gray-code/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:返回任一有效的 $n$ 位格雷码序列。 **说明**: - **n 位格雷码序列**:是一个由 $2^n$ 个整数组成的序列,其中: - 每个整数都在范围 $[0, 2^n - 1]$ 内(含 $0$ 和 $2^n - 1$)。 - 第一个整数是 $0$。 - 一个整数在序列中出现不超过一次。 - 每对相邻整数的二进制表示恰好一位不同 ,且第一个和最后一个整数的二进制表示恰好一位不同。 - $1 \le n \le 16$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:[0,1,3,2] 解释: [0,1,3,2] 的二进制表示是 [00,01,11,10] 。 - 00 和 01 有一位不同 - 01 和 11 有一位不同 - 11 和 10 有一位不同 - 10 和 00 有一位不同 [0,2,3,1] 也是一个有效的格雷码序列,其二进制表示是 [00,10,11,01] 。 - 00 和 10 有一位不同 - 10 和 11 有一位不同 - 11 和 01 有一位不同 - 01 和 00 有一位不同 ``` - 示例 2: ```python 输入:n = 1 输出:[0,1] ``` ## 解题思路 ### 思路 1:位运算 + 公式法 - 格雷编码生成规则:以二进制值为 $0$ 的格雷编码作为第 $0$ 项,第一次改变最右边的数位,第二次改变从右边数第一个为 $1$ 的数位左边的数位,第三次跟第一次一样,改变最右边的数位,第四次跟第二次一样,改变从右边数第一个为 $1$ 的数位左边的数位。此后,第五、六次,第七、八次 ... 都跟第一二次一样反复进行,直到生成 $2^n$​ 个格雷编码。 - 也可以直接利用二进制转换为格雷编码公式: ![image.png](https://pic.leetcode-cn.com/1013850d7f6c8cf1d99dc0ac3292264b74f6a52d84e0215f540c80952e184f41-image.png) ### 思路 1:代码 ```python class Solution: def grayCode(self, n: int) -> List[int]: gray = [] binary = 0 while binary < (1 << n): gray.append(binary ^ binary >> 1) binary += 1 return gray ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/group-anagrams.md ================================================ # [0049. 字母异位词分组](https://leetcode.cn/problems/group-anagrams/) - 标签:数组、哈希表、字符串、排序 - 难度:中等 ## 题目链接 - [0049. 字母异位词分组 - 力扣](https://leetcode.cn/problems/group-anagrams/) ## 题目大意 给定一个字符串数组,将包含字母相同的字符串组合在一起,不需要考虑输出顺序。 ## 解题思路 使用哈希表记录字母相同的字符串。对每一个字符串进行排序,按照 排序字符串:字母相同的字符串数组 的键值顺序进行存储。 最终将哈希表的值转换为对应数组返回结果。 ## 代码 ```python class Solution: def groupAnagrams(self, strs: List[str]) -> List[List[str]]: str_dict = dict() res = [] for s in strs: sort_s = str(sorted(s)) if sort_s in str_dict: str_dict[sort_s] += [s] else: str_dict[sort_s] = [s] for sort_s in str_dict: res += [str_dict[sort_s]] return res ``` ================================================ FILE: docs/solutions/0001-0099/index.md ================================================ ## 本章内容 - [0001. 两数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/two-sum.md) - [0002. 两数相加](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/add-two-numbers.md) - [0003. 无重复字符的最长子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-substring-without-repeating-characters.md) - [0004. 寻找两个正序数组的中位数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/median-of-two-sorted-arrays.md) - [0005. 最长回文子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-palindromic-substring.md) - [0006. Z 字形变换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/zigzag-conversion.md) - [0007. 整数反转](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-integer.md) - [0008. 字符串转换整数 (atoi)](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/string-to-integer-atoi.md) - [0009. 回文数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/palindrome-number.md) - [0010. 正则表达式匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/regular-expression-matching.md) - [0011. 盛最多水的容器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/container-with-most-water.md) - [0012. 整数转罗马数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/integer-to-roman.md) - [0013. 罗马数字转整数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/roman-to-integer.md) - [0014. 最长公共前缀](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-common-prefix.md) - [0015. 三数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum.md) - [0016. 最接近的三数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/3sum-closest.md) - [0017. 电话号码的字母组合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/letter-combinations-of-a-phone-number.md) - [0018. 四数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/4sum.md) - [0019. 删除链表的倒数第 N 个结点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-nth-node-from-end-of-list.md) - [0020. 有效的括号](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-parentheses.md) - [0021. 合并两个有序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-two-sorted-lists.md) - [0022. 括号生成](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/generate-parentheses.md) - [0023. 合并 K 个升序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-k-sorted-lists.md) - [0024. 两两交换链表中的节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/swap-nodes-in-pairs.md) - [0025. K 个一组翻转链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-nodes-in-k-group.md) - [0026. 删除有序数组中的重复项](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-array.md) - [0027. 移除元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-element.md) - [0028. 找出字符串中第一个匹配项的下标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-the-index-of-the-first-occurrence-in-a-string.md) - [0029. 两数相除](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/divide-two-integers.md) - [0030. 串联所有单词的子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/substring-with-concatenation-of-all-words.md) - [0031. 下一个排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/next-permutation.md) - [0032. 最长有效括号](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/longest-valid-parentheses.md) - [0033. 搜索旋转排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-in-rotated-sorted-array.md) - [0034. 在排序数组中查找元素的第一个和最后一个位置](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/find-first-and-last-position-of-element-in-sorted-array.md) - [0035. 搜索插入位置](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-insert-position.md) - [0036. 有效的数独](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-sudoku.md) - [0037. 解数独](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sudoku-solver.md) - [0038. 外观数列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/count-and-say.md) - [0039. 组合总和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combination-sum.md) - [0040. 组合总和 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combination-sum-ii.md) - [0041. 缺失的第一个正数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/first-missing-positive.md) - [0042. 接雨水](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/trapping-rain-water.md) - [0043. 字符串相乘](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/multiply-strings.md) - [0044. 通配符匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/wildcard-matching.md) - [0045. 跳跃游戏 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/jump-game-ii.md) - [0046. 全排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutations.md) - [0047. 全排列 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutations-ii.md) - [0048. 旋转图像](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/rotate-image.md) - [0049. 字母异位词分组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/group-anagrams.md) - [0050. Pow(x, n)](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/powx-n.md) - [0051. N 皇后](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/n-queens.md) - [0052. N 皇后 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/n-queens-ii.md) - [0053. 最大子数组和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximum-subarray.md) - [0054. 螺旋矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/spiral-matrix.md) - [0055. 跳跃游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/jump-game.md) - [0056. 合并区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-intervals.md) - [0057. 插入区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/insert-interval.md) - [0058. 最后一个单词的长度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/length-of-last-word.md) - [0059. 螺旋矩阵 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/spiral-matrix-ii.md) - [0060. 排列序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/permutation-sequence.md) - [0061. 旋转链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/rotate-list.md) - [0062. 不同路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths.md) - [0063. 不同路径 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-paths-ii.md) - [0064. 最小路径和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/minimum-path-sum.md) - [0065. 有效数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/valid-number.md) - [0066. 加一](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/plus-one.md) - [0067. 二进制求和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/add-binary.md) - [0068. 文本左右对齐](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/text-justification.md) - [0069. x 的平方根](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sqrtx.md) - [0070. 爬楼梯](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/climbing-stairs.md) - [0071. 简化路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/simplify-path.md) - [0072. 编辑距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/edit-distance.md) - [0073. 矩阵置零](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/set-matrix-zeroes.md) - [0074. 搜索二维矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-a-2d-matrix.md) - [0075. 颜色分类](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/sort-colors.md) - [0076. 最小覆盖子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/minimum-window-substring.md) - [0077. 组合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/combinations.md) - [0078. 子集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets.md) - [0079. 单词搜索](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/word-search.md) - [0080. 删除有序数组中的重复项 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-array-ii.md) - [0081. 搜索旋转排序数组 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/search-in-rotated-sorted-array-ii.md) - [0082. 删除排序链表中的重复元素 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-list-ii.md) - [0083. 删除排序链表中的重复元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/remove-duplicates-from-sorted-list.md) - [0084. 柱状图中最大的矩形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/largest-rectangle-in-histogram.md) - [0085. 最大矩形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/maximal-rectangle.md) - [0086. 分隔链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/partition-list.md) - [0087. 扰乱字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/scramble-string.md) - [0088. 合并两个有序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/merge-sorted-array.md) - [0089. 格雷编码](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/gray-code.md) - [0090. 子集 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/subsets-ii.md) - [0091. 解码方法](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/decode-ways.md) - [0092. 反转链表 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/reverse-linked-list-ii.md) - [0093. 复原 IP 地址](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/restore-ip-addresses.md) - [0094. 二叉树的中序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/binary-tree-inorder-traversal.md) - [0095. 不同的二叉搜索树 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-binary-search-trees-ii.md) - [0096. 不同的二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/unique-binary-search-trees.md) - [0097. 交错字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/interleaving-string.md) - [0098. 验证二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/validate-binary-search-tree.md) - [0099. 恢复二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/recover-binary-search-tree.md) ================================================ FILE: docs/solutions/0001-0099/insert-interval.md ================================================ # [0057. 插入区间](https://leetcode.cn/problems/insert-interval/) - 标签:数组 - 难度:中等 ## 题目链接 - [0057. 插入区间 - 力扣](https://leetcode.cn/problems/insert-interval/) ## 题目大意 **描述**: 给定一个无重叠的、按照区间起始端点排序的区间列表 $intervals$,其中 $intervals[i] = [start_i, end_i]$ 表示第 $i$ 个区间的开始和结束,并且 $intervals$ 按照 $start_i$ 升序排列。同样给定一个区间 $newInterval = [start, end]$ 表示另一个区间的开始和结束。 **要求**: 在 $intervals$ 中插入区间 $newInterval$,使得 $intervals$ 依然按照 $start_i$ 升序排列,且区间之间不重叠(如果有必要的话,可以合并区间)。 返回插入之后的 $intervals$。 **说明**: - $0 \le intervals.length \le 10^4$。 - $intervals[i].length == 2$。 - $0 \le start_i \le end_i \le 10^5$。 - $intervals$ 根据 $start_i$ 按升序排列。 - $newInterval.length == 2$。 - $0 \le start \le end \le 10^5$。 **示例**: - 示例 1: ```python 输入:intervals = [[1,3],[6,9]], newInterval = [2,5] 输出:[[1,5],[6,9]] ``` - 示例 2: ```python 输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8] 输出:[[1,2],[3,10],[12,16]] 解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。 ``` ## 解题思路 ### 思路 1:模拟插入 + 区间合并 由于原区间列表已经按照起始端点排序,我们可以采用以下策略: 1. **遍历原区间列表**:从左到右遍历所有区间。 2. **分类处理**: - 如果当前区间完全在新区间之前(当前区间的结束 < 新区间的开始),直接加入结果。 - 如果当前区间与新区间有重叠,则合并区间。 - 如果当前区间完全在新区间之后(当前区间的开始 > 新区间的结束),直接加入结果。 3. **区间合并**:当发现重叠时,将新区间扩展为 `[min(新区间开始, 当前区间开始), max(新区间结束, 当前区间结束)]`。 4. **处理剩余区间**:合并完成后,将剩余的所有区间加入结果。 **关键点**: - 利用已排序的性质,只需要一次遍历 - 合并区间时需要考虑新区间可能跨越多个原区间的情况 - 使用标志位来跟踪是否已经开始合并过程 ### 思路 1:代码 ```python class Solution: def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]: result = [] i = 0 n = len(intervals) # 1. 添加所有在新区间之前的区间 while i < n and intervals[i][1] < newInterval[0]: result.append(intervals[i]) i += 1 # 2. 合并与新区间重叠的区间 while i < n and intervals[i][0] < newInterval[1]: newInterval[0] = min(newInterval[0], intervals[i][0]) newInterval[1] = max(newInterval[1], intervals[i][1]) i += 1 # 3. 添加合并后的新区间 result.append(newInterval) # 4. 添加剩余的区间 while i < n: result.append(intervals[i]) i += 1 return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是原区间列表的长度。我们只需要遍历一次原区间列表。 - **空间复杂度**:$O(1)$,除了返回结果外,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0001-0099/integer-to-roman.md ================================================ # [0012. 整数转罗马数字](https://leetcode.cn/problems/integer-to-roman/) - 标签:哈希表、数学、字符串 - 难度:中等 ## 题目链接 - [0012. 整数转罗马数字 - 力扣](https://leetcode.cn/problems/integer-to-roman/) ## 题目大意 给定一个整数,将其转换为罗马数字。 罗马数字规则: - I 代表数值 1,V 代表数值 5,X 代表数值 10,L 代表数值 50,C 代表数值 100,D 代表数值 500,M 代表数值 1000; - 一般罗马数字较大数字在左边,较小数字在右边,此时值为两者之和,比如 XI = X + I = 10 + 1 = 11。 - 例外情况下,较小数字在左边,较大数字在右边,此时值为后者减前者之差,比如 IX = X - I = 10 - 1 = 9。 ## 解题思路 根据规则,可以得出: - I 代表数值 1,V 代表数值 5,X 代表数值 10,L 代表数值 50,C 代表数值 100,D 代表数值 500,M 代表数值 1000; - CM 代表 900,CD 代表 400,XC 代表 90,XL 代表 40,IX 代表 9,IV 代表 4。 依次排序可得: - 1000 : M、900 : CM、D : 500、400 : CD、100 : C、90 : XC、50 : L、40 : XL、10 : X、9 : IX、5 : V、4 : IV、1 : I。 使用贪心算法。每次尽量用最大的数对应的罗马字符来表示。先选择 1000,再选择 900,然后 500,等等。 ## 代码 ```python class Solution: def intToRoman(self, num: int) -> str: roman_dict = {1000:'M', 900:'CM', 500:'D', 400:'CD', 100:'C', 90:'XC', 50:'L', 40:'XL', 10:'X', 9:'IX', 5:'V', 4:'IV', 1:'I'} res = "" for key in roman_dict: if num // key != 0: res += roman_dict[key] * (num // key) num %= key return res ``` ================================================ FILE: docs/solutions/0001-0099/interleaving-string.md ================================================ # [0097. 交错字符串](https://leetcode.cn/problems/interleaving-string/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [0097. 交错字符串 - 力扣](https://leetcode.cn/problems/interleaving-string/) ## 题目大意 **描述**: 两个字符串 $s$ 和 $t$ 交错的定义与过程如下,其中每个字符串都会被分割成若干非空子字符串: - $s = s1 + s2 + ... + sn$ - $t = t1 + t2 + ... + tm$ - $|n - m| \le 1$ - 「交错」是 $s1 + t1 + s2 + t2 + s3 + t3 + ...$ 或者 $t1 + s1 + t2 + s2 + t3 + s3 + ...$ 注意:$a + b$ 意味着字符串 $a$ 和 $b$ 连接。 现在给定三个字符串 $s1$、$s2$、$s3$。 **要求**: 帮忙验证 $s3$ 是否是由 $s1$ 和 $s2$ 「交错」组成的。 **说明**: - $0 \le s1.length, s2.length \le 100$。 - $0 \le s3.length \le 200$。 - $s1$、$s2$、和 $s3$ 都由小写英文字母组成。 - 进阶:您能否仅使用 $O(s2.length)$ 额外的内存空间来解决它? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/09/02/interleave.jpg) ```python 输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac" 输出:true ``` - 示例 2: ```python 输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc" 输出:false ``` ## 解题思路 ### 思路 1:动态规划 **核心思想**: 使用二维动态规划来解决这个问题。定义 $dp[i][j]$ 表示 $s1$ 的前 $i$ 个字符和 $s2$ 的前 $j$ 个字符能否交错组成 $s3$ 的前 $i + j$ 个字符。 **状态转移方程**: - 如果 $s1$ 的第 $i$ 个字符等于 $s3$ 的第 $i+j$ 个字符,且 $s1$ 的前 $i-1$ 个字符和 $s2$ 的前 $j$ 个字符能组成 $s3$ 的前 $i+j-1$ 个字符,则 $dp[i][j] = True$。 - 如果 $s2$ 的第 $j$ 个字符等于 $s3$ 的第 $i+j$ 个字符,且 $s1$ 的前 $i$ 个字符和 $s2$ 的前 $j-1$ 个字符能组成 $s3$ 的前 $i+j-1$ 个字符,则 $dp[i][j] = True$。 - 否则 $dp[i][j] = False$。 **边界条件**: - $dp[0][0] = True$(空字符串可以组成空字符串)。 - $dp[i][0]$ 取决于 $s1$ 的前 $i$ 个字符是否等于 $s3$ 的前 $i$ 个字符。 - $dp[0][j]$ 取决于 $s2$ 的前 $j$ 个字符是否等于 $s3$ 的前 $j$ 个字符。 ### 思路 1:代码 ```python class Solution: def isInterleave(self, s1: str, s2: str, s3: str) -> bool: len1, len2, len3 = len(s1), len(s2), len(s3) # 长度不匹配直接返回 False if len1 + len2 != len3: return False # 创建 DP 数组 dp = [[False] * (len2 + 1) for _ in range(len1 + 1)] # 初始化边界条件 dp[0][0] = True # 初始化第一行:只使用 s2 for j in range(1, len2 + 1): dp[0][j] = dp[0][j-1] and s2[j-1] == s3[j-1] # 初始化第一列:只使用 s1 for i in range(1, len1 + 1): dp[i][0] = dp[i-1][0] and s1[i-1] == s3[i-1] # 填充 DP 数组 for i in range(1, len1 + 1): for j in range(1, len2 + 1): # 状态转移方程 dp[i][j] = (dp[i-1][j] and s1[i-1] == s3[i+j-1]) or (dp[i][j-1] and s2[j-1] == s3[i+j-1]) return dp[len1][len2] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 是 $s1$ 的长度,$n$ 是 $s2$ 的长度。需要填充 $m \times n$ 的 DP 数组。 - **空间复杂度**:$O(m \times n)$,需要创建 $m \times n$ 的 DP 数组来存储中间结果。 ================================================ FILE: docs/solutions/0001-0099/jump-game-ii.md ================================================ # [0045. 跳跃游戏 II](https://leetcode.cn/problems/jump-game-ii/) - 标签:贪心、数组、动态规划 - 难度:中等 ## 题目链接 - [0045. 跳跃游戏 II - 力扣](https://leetcode.cn/problems/jump-game-ii/) ## 题目大意 **描述**:给定一个非负整数数组 `nums`,数组中每个元素代表在该位置可以跳跃的最大长度。开始位置为数组的第一个下标处。 **要求**:计算出到达最后一个下标处的最少的跳跃次数。假设你总能到达数组的最后一个下标处。 **说明**: - $1 \le nums.length \le 10^4$。 - $0 \le nums[i] \le 1000$。 **示例**: - 示例 1: ```python 输入:nums = [2,3,1,1,4] 输出:2 解释:跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。 ``` ## 解题思路 ### 思路 1:动态规划(超时) ###### 1. 阶段划分 按照位置进行阶段划分。 ###### 2. 定义状态 定义状态 `dp[i]` 表示为:跳到下标 `i` 所需要的最小跳跃次数。 ###### 3. 状态转移方程 对于当前位置 `i`,如果之前的位置 `j`($o \le j < i$) 能够跳到位置 `i` 需要满足:位置 `j`($o \le j < i$)加上位置 `j` 所能跳到的最远长度要大于等于 `i`,即 `j + nums[j] >= i` 。 而跳到下标 `i` 所需要的最小跳跃次数则等于满足上述要求的位置 `j` 中最小跳跃次数加 `1`,即 `dp[i] = min(dp[i], dp[j] + 1)`。 ###### 4. 初始条件 初始状态下,跳到下标 `0` 需要的最小跳跃次数为 `0`,即 `dp[0] = 0`。 ###### 5. 最终结果 根据我们之前定义的状态,`dp[i]` 表示为:跳到下标 `i` 所需要的最小跳跃次数。则最终结果为 `dp[size - 1]`。 ### 思路 1:动态规划(超时)代码 ```python class Solution: def jump(self, nums: List[int]) -> int: size = len(nums) dp = [float("inf") for _ in range(size)] dp[0] = 0 for i in range(1, size): for j in range(i): if j + nums[j] >= i: dp[i] = min(dp[i], dp[j] + 1) return dp[size - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。两重循环遍历的时间复杂度是 $O(n^2)$,所以总体时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 ### 思路 2:动态规划 + 贪心 因为本题的数据规模为 $10^4$,而思路 1 的时间复杂度是 $O(n^2)$,所以就超时了。那么我们有什么方法可以优化一下,减少一下时间复杂度吗? 上文提到,在满足 `j + nums[j] >= i` 的情况下,`dp[i] = min(dp[i], dp[j] + 1)`。 通过观察可以发现,`dp[i]` 是单调递增的,也就是说 `dp[i - 1] <= dp[i] <= dp[i + 1]`。 举个例子,比如跳到下标 `i` 最少需要 `5` 步,即 `dp[i] = 5`,那么必然不可能出现少于 `5` 步就能跳到下标 `i + 1` 的情况,跳到下标 `i + 1` 至少需要 `5` 步或者更多步。 既然 `dp[i]` 是单调递增的,那么在更新 `dp[i]` 时,我们找到最早可以跳到 `i` 的点 `j`,从该点更新 `dp[i]`。即找到满足 `j + nums[j] >= i` 的第一个 `j`,使得 `dp[i] = dp[j] + 1`。 而查找第一个 `j` 的过程可以通过使用一个指针变量 `j` 从前向后迭代查找。 最后,将最终结果 `dp[size - 1]` 返回即可。 ### 思路 2:动态规划 + 贪心代码 ```python class Solution: def jump(self, nums: List[int]) -> int: size = len(nums) dp = [float("inf") for _ in range(size)] dp[0] = 0 j = 0 for i in range(1, size): while j + nums[j] < i: j += 1 dp[i] = dp[j] + 1 return dp[size - 1] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。最外层循环遍历的时间复杂度是 $O(n)$,看似和内层循环结合遍历的时间复杂度是 $O(n^2)$,实际上内层循环只遍历了一遍,与外层循环遍历次数是相加关系,两者的时间复杂度和是 $O(2n)$,$O(2n) = O(n)$,所以总体时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 ### 思路 2:贪心算法 如果第 `i` 个位置所能跳到的位置为 `[i + 1, i + nums[i]]`,则: - 第 `0` 个位置所能跳到的位置就是 `[0 + 1, 0 + nums[0]]`,即 `[1, nums[0]]`。 - 第 `1` 个位置所能跳到的位置就是 `[1 + 1, 1 + nums[1]]`,即 `[2, 1 + nums[1]]`。 - …… 对于每一个位置 `i` 来说,所能跳到的所有位置都可以作为下一个起跳点,为了尽可能使用最少的跳跃次数,所以我们应该使得下一次起跳所能达到的位置尽可能的远。简单来说,就是每次在「可跳范围」内选择可以使下一次跳的更远的位置。这样才能获得最少跳跃次数。具体做法如下: 1. 维护几个变量:当前所能达到的最远位置 `end`,下一步所能跳到的最远位置 `max_pos`,最少跳跃次数 `setps`。 2. 遍历数组 `nums` 的前 `len(nums) - 1` 个元素: 1. 每次更新第 `i` 位置下一步所能跳到的最远位置 `max_pos`。 2. 如果索引 `i` 到达了 `end` 边界,则:更新 `end` 为新的当前位置 `max_pos`,并令步数 `setps` 加 `1`。 3. 最终返回跳跃次数 `steps`。 ### 思路 2:贪心算法代码 ```python class Solution: def jump(self, nums: List[int]) -> int: end, max_pos = 0, 0 steps = 0 for i in range(len(nums) - 1): max_pos = max(max_pos, nums[i] + i) if i == end: end = max_pos steps += 1 return steps ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度是 $O(n)$,所以总体时间复杂度为 $O(n)$。 - **空间复杂度**:$O(1)$。只用到了常数项的变量,所以总体空间复杂度为 $O(1)$。 ## 参考资料 - 【题解】[【宫水三叶の相信科学系列】详解「DP + 贪心 + 双指针」解法,以及该如何猜 DP 的状态定义 - 跳跃游戏 II - 力扣](https://leetcode.cn/problems/jump-game-ii/solution/xiang-jie-dp-tan-xin-shuang-zhi-zhen-jie-roh4/) - 【题解】[动态规划+贪心,易懂。 - 跳跃游戏 II - 力扣](https://leetcode.cn/problems/jump-game-ii/solution/dong-tai-gui-hua-tan-xin-yi-dong-by-optimjie/) ================================================ FILE: docs/solutions/0001-0099/jump-game.md ================================================ # [0055. 跳跃游戏](https://leetcode.cn/problems/jump-game/) - 标签:贪心、数组、动态规划 - 难度:中等 ## 题目链接 - [0055. 跳跃游戏 - 力扣](https://leetcode.cn/problems/jump-game/) ## 题目大意 **描述**:给定一个非负整数数组 `nums`,数组中每个元素代表在该位置可以跳跃的最大长度。开始位置位于数组的第一个下标处。 **要求**:判断是否能够到达最后一个下标。 **说明**: - $1 \le nums.length \le 3 \times 10^4$。 - $0 \le nums[i] \le 10^5$。 **示例**: - 示例 1: ```python 输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。 ``` - 示例 2: ```python 输入:nums = [3,2,1,0,4] 输出:false 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。 ``` ## 解题思路 ### 思路 1:贪心算法 如果我们能通过前面的某个位置 $j$,到达后面的某个位置 $i$,则我们一定能到达区间 $[j, i]$ 中所有的点($j \le i$)。 而前面的位置 $j$ 肯定也是通过 $j$ 前面的点到达的。所以我们可以通过贪心算法来计算出所能到达的最远位置。具体步骤如下: 1. 初始化能到达的最远位置 $max_i$ 为 $0$。 2. 遍历数组 $nums$。 3. 如果能到达当前位置,即 $max_i \le i$,并且当前位置 + 当前位置最大跳跃长度 > 能到达的最远位置,即 $i + nums[i] > max_i$,则更新能到达的最远位置 $max_i$。 4. 遍历完数组,最后比较能到达的最远位置 $max_i$ 和数组最远距离 $size - 1$ 的关系。如果 $max_i >= size - 1$,则返回 `True`,否则返回 `False`。 ### 思路 1:代码 ```python class Solution: def canJump(self, nums: List[int]) -> bool: size = len(nums) max_i = 0 for i in range(size): if max_i >= i: max_i = max(max_i, i + nums[i]) return max_i >= size - 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 `nums` 的长度。 - **空间复杂度**:$O(1)$。 ### 思路 2:动态规划 ###### 1. 阶段划分 按照位置进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为:从位置 $0$ 出发,经过 $j \le i$,可以跳出的最远距离。 ###### 3. 状态转移方程 - 如果能通过 $0 \sim i - 1$ 个位置到达 $i$,即 $dp[i-1] \le i$,则 $dp[i] = max(dp[i-1], i + nums[i])$。 - 如果不能通过 $0 \sim i - 1$ 个位置到达 $i$,即 $dp[i - 1] < i$,则 $dp[i] = dp[i - 1]$。 ###### 4. 初始条件 初始状态下,从 $0$ 出发,经过 $0$,可以跳出的最远距离为 $nums[0]$,即 $dp[0] = nums[0]$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示为:从位置 $0$ 出发,经过 $j \le i$,可以跳出的最远距离。则我们需要判断 $dp[size - 1]$ 与数组最远距离 $size - 1$ 的关系。 ### 思路 2:代码 ```python class Solution: def canJump(self, nums: List[int]) -> bool: size = len(nums) dp = [0 for _ in range(size)] dp[0] = nums[0] for i in range(1, size): if i <= dp[i - 1]: dp[i] = max(dp[i - 1], i + nums[i]) else: dp[i] = dp[i - 1] return dp[size - 1] >= size - 1 ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 `nums` 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0001-0099/largest-rectangle-in-histogram.md ================================================ # [0084. 柱状图中最大的矩形](https://leetcode.cn/problems/largest-rectangle-in-histogram/) - 标签:栈、数组、单调栈 - 难度:困难 ## 题目链接 - [0084. 柱状图中最大的矩形 - 力扣](https://leetcode.cn/problems/largest-rectangle-in-histogram/) ## 题目大意 给定一个非负整数数组 `heights` ,`heights[i]` 用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 要求:计算出在该柱状图中,能够勾勒出来的矩形的最大面积。 ## 解题思路 思路一:枚举「宽度」。一重循环枚举所有柱子,第二重循环遍历柱子右侧的柱子,所得的宽度就是两根柱子形成区间的宽度,高度就是这段区间中的最小高度。然后计算出对应面积,记录并更新最大面积。这样下来,时间复杂度为 $O(n^2)$。 思路二:枚举「高度」。一重循环枚举所有柱子,以柱子高度为当前矩形高度,然后向两侧延伸,遇到小于当前矩形高度的情况就停止。然后计算当前矩形面积,记录并更新最大面积。这样下来,时间复杂度也是 $O(n^2)$。 思路三:利用「单调栈」减少两侧延伸的复杂度。 - 枚举所有柱子。 - 如果当前柱子高度较大,大于等于栈顶柱体的高度,则直接将当前柱体入栈。 - 如果当前柱体高度较小,小于栈顶柱体的高度,则一直出栈,直到当前柱体大于等于栈顶柱体高度。 - 出栈后,说明当前柱体是出栈柱体向右找到的第一个小于当前柱体高度的柱体,那么就可以向右将宽度扩展到当前柱体。 - 出栈后,说明新的栈顶柱体是出栈柱体向左找到的第一个小于新的栈顶柱体高度的柱体,那么就可以向左将宽度扩展到新的栈顶柱体。 - 以新的栈顶柱体为左边界,当前柱体为右边界,以出栈柱体为高度。计算矩形面积,然后记录并更新最大面积。 ## 代码 ```python class Solution: def largestRectangleArea(self, heights: List[int]) -> int: heights.append(0) ans = 0 stack = [] for i in range(len(heights)): while stack and heights[stack[-1]] >= heights[i]: cur = stack.pop(-1) left = stack[-1] + 1 if stack else 0 right = i - 1 ans = max(ans, (right - left + 1) * heights[cur]) stack.append(i) return ans ``` ================================================ FILE: docs/solutions/0001-0099/length-of-last-word.md ================================================ # [0058. 最后一个单词的长度](https://leetcode.cn/problems/length-of-last-word/) - 标签:字符串 - 难度:简单 ## 题目链接 - [0058. 最后一个单词的长度 - 力扣](https://leetcode.cn/problems/length-of-last-word/) ## 题目大意 给定一个字符串 s,返回字符串中最后一个单词长度。 - 「单词」:指仅由字母组成、不包含任何空格字符的最大子字符串。 ## 解题思路 从字符串末尾开始逆序遍历,先过滤掉末尾空白字符,然后统计字符数量,直到遇到空格或到达字符串开始位置。 ## 代码 ```python class Solution: def lengthOfLastWord(self, s: str) -> int: ans = 0 for i in range(len(s)-1, -1, -1): if s[i] == " ": if ans == 0: continue else: return ans else: ans += 1 return ans ``` ================================================ FILE: docs/solutions/0001-0099/letter-combinations-of-a-phone-number.md ================================================ # [0017. 电话号码的字母组合](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/) - 标签:哈希表、字符串、回溯 - 难度:中等 ## 题目链接 - [0017. 电话号码的字母组合 - 力扣](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/) ## 题目大意 **描述**:给定一个只包含数字 2~9 的字符串 `digits`。给出数字到字母的映射如下(与电话按键相同)。注意 $1$ 不对应任何字母。 ![](https://assets.leetcode-cn.com/aliyun-lc-upload/original_images/17_telephone_keypad.png) **要求**:返回字符串 `digits` 在九宫格键盘上所能表示的所有字母组合。答案可以按 「任意顺序」返回。 **说明**: - $0 \le digits.length \le 4$。 - `digits[i]` 是范围 $2 \sim 9$ 的一个数字。 **示例**: - 示例 1: ```python 输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"] ``` - 示例 2: ```python 输入:digits = "2" 输出:["a","b","c"] ``` ## 解题思路 ### 思路 1:回溯算法 + 哈希表 用哈希表保存每个数字键位对应的所有可能的字母,然后进行回溯操作。 回溯过程中,维护一个字符串 combination,表示当前的字母排列组合。初始字符串为空,每次取电话号码的一位数字,从哈希表中取出该数字所对应的所有字母,并将其中一个插入到 combination 后面,然后继续处理下一个数字,知道处理完所有数字,得到一个完整的字母排列。开始进行回退操作,遍历其余的字母排列。 ### 思路 1:代码 ```python class Solution: def letterCombinations(self, digits: str) -> List[str]: if not digits: return [] phone_dict = { "2": "abc", "3": "def", "4": "ghi", "5": "jkl", "6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz" } def backtrack(combination, index): if index == len(digits): combinations.append(combination) else: digit = digits[index] for letter in phone_dict[digit]: backtrack(combination + letter, index + 1) combinations = list() backtrack('', 0) return combinations ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(3^m \times 4^n)$,其中 $m$ 是 `digits` 中对应 $3$ 个字母的数字个数,$m$ 是 `digits` 中对应 $4$ 个字母的数字个数。 - **空间复杂度**:$O(m + n)$。 ================================================ FILE: docs/solutions/0001-0099/longest-common-prefix.md ================================================ # [0014. 最长公共前缀](https://leetcode.cn/problems/longest-common-prefix/) - 标签:字典树、字符串 - 难度:简单 ## 题目链接 - [0014. 最长公共前缀 - 力扣](https://leetcode.cn/problems/longest-common-prefix/) ## 题目大意 **描述**:给定一个字符串数组 `strs`。 **要求**:返回字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 `""`。 **说明**: - $1 \le strs.length \le 200$。 - $0 \le strs[i].length \le 200$。 - `strs[i]` 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:strs = ["flower","flow","flight"] 输出:"fl" ``` - 示例 2: ```python 输入:strs = ["dog","racecar","car"] 输出:"" 解释:输入不存在公共前缀。 ``` ## 解题思路 ### 思路 1:纵向遍历 1. 依次遍历所有字符串的每一列,比较相同位置上的字符是否相同。 1. 如果相同,则继续对下一列进行比较。 2. 如果不相同,则当前列字母不再属于公共前缀,直接返回当前列之前的部分。 2. 如果遍历结束,说明字符串数组中的所有字符串都相等,则可将字符串数组中的第一个字符串作为公共前缀进行返回。 ### 思路 1:代码 ```python class Solution: def longestCommonPrefix(self, strs: List[str]) -> str: if not strs: return "" length = len(strs[0]) count = len(strs) for i in range(length): c = strs[0][i] for j in range(1, count): if len(strs[j]) == i or strs[j][i] != c: return strs[0][:i] return strs[0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 是字符串数组中的字符串的平均长度,$n$ 是字符串的数量。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/longest-palindromic-substring.md ================================================ # [0005. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [0005. 最长回文子串 - 力扣](https://leetcode.cn/problems/longest-palindromic-substring/) ## 题目大意 **描述**:给定一个字符串 $s$。 **要求**:找到 $s$ 中最长的回文子串。 **说明**: - **回文串**:如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。 - $1 \le s.length \le 1000$。 - $s$ 仅由数字和英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。 ``` - 示例 2: ```python 输入:s = "cbbd" 输出:"bb" ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:字符串 $s$ 在区间 $[i, j]$ 范围内是否是一个回文串。 ###### 3. 状态转移方程 - 当子串只有 $1$ 位或 $2$ 位的时候,如果 $s[i] == s[j]$,该子串为回文子串,即:`dp[i][j] = (s[i] == s[j])`。 - 如果子串大于 $2$ 位,则如果 $s[i + 1...j - 1]$ 是回文串,且 $s[i] == s[j]$,则 $s[i...j]$ 也是回文串,即:`dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]`。 ###### 4. 初始条件 - 初始状态下,默认字符串 $s$ 的所有子串都不是回文串。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:字符串 $s$ 在区间 $[i, j]$ 范围内是否是一个回文串。当判断完 $s[i: j]$ 是否为回文串时,同时判断并更新最长回文子串的起始位置 $max\_start$ 和最大长度 $max\_len$。则最终结果为 $s[max\_start, max\_start + max\_len]$。 ### 思路 1:代码 ```python class Solution: def longestPalindrome(self, s: str) -> str: n = len(s) if n <= 1: return s dp = [[False for _ in range(n)] for _ in range(n)] max_start = 0 max_len = 1 for j in range(1, n): for i in range(j): if s[i] == s[j]: if j - i <= 2: dp[i][j] = True else: dp[i][j] = dp[i + 1][j - 1] if dp[i][j] and (j - i + 1) > max_len: max_len = j - i + 1 max_start = i return s[max_start: max_start + max_len] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是字符串的长度。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/0001-0099/longest-substring-without-repeating-characters.md ================================================ # [0003. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) - 标签:哈希表、字符串、滑动窗口 - 难度:中等 ## 题目链接 - [0003. 无重复字符的最长子串 - 力扣](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) ## 题目大意 **描述**:给定一个字符串 $s$。 **要求**:找出其中不含有重复字符的最长子串的长度。 **说明**: - $0 \le s.length \le 5 * 10^4$。 - $s$ 由英文字母、数字、符号和空格组成。 **示例**: - 示例 1: ```python 输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 ``` - 示例 2: ```python 输入: s = "bbbbb" 输出: 1 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 ``` ## 解题思路 ### 思路 1:滑动窗口(不定长度) 用滑动窗口 $window$ 来记录不重复的字符个数,$window$ 为哈希表类型。 1. 设定两个指针:$left$、$right$,分别指向滑动窗口的左右边界,保证窗口中没有重复字符。 2. 一开始,$left$、$right$ 都指向 $0$。 3. 向右移动 $right$,将最右侧字符 $s[right]$ 加入当前窗口 $window$ 中,记录该字符个数。 4. 如果该窗口中该字符的个数多于 $1$ 个,即 $window[s[right]] > 1$,则不断右移 $left$,缩小滑动窗口长度,并更新窗口中对应字符的个数,直到 $window[s[right]] \le 1$。 5. 维护更新无重复字符的最长子串长度。然后继续右移 $right$,直到 $right \ge len(nums)$ 结束。 6. 输出无重复字符的最长子串长度。 ### 思路 1:代码 ```python class Solution: def lengthOfLongestSubstring(self, s: str) -> int: left = 0 right = 0 window = dict() ans = 0 while right < len(s): if s[right] not in window: window[s[right]] = 1 else: window[s[right]] += 1 while window[s[right]] > 1: window[s[left]] -= 1 left += 1 ans = max(ans, right - left + 1) right += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(| \sum |)$。其中 $\sum$ 表示字符集,$| \sum |$ 表示字符集的大小。 ================================================ FILE: docs/solutions/0001-0099/longest-valid-parentheses.md ================================================ # [0032. 最长有效括号](https://leetcode.cn/problems/longest-valid-parentheses/) - 标签:栈、字符串、动态规划 - 难度:困难 ## 题目链接 - [0032. 最长有效括号 - 力扣](https://leetcode.cn/problems/longest-valid-parentheses/) ## 题目大意 **描述**:给定一个只包含 `'('` 和 `')'` 的字符串。 **要求**:找出最长有效(格式正确且连续)括号子串的长度。 **说明**: - $0 \le s.length \le 3 * 10^4$。 - `s[i]` 为 `'('` 或 `')'`。 **示例**: - 示例 1: ```python 输入:s = "(()" 输出:2 解释:最长有效括号子串是 "()" ``` - 示例 2: ```python 输入:s = ")()())" 输出:4 解释:最长有效括号子串是 "()()" ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照最长有效括号子串的结束位置进行阶段划分。 ###### 2. 定义状态 定义状态 `dp[i]` 表示为:以字符 `s[i]` 为结尾的最长有效括号的长度。 ###### 3. 状态转移方程 - 如果 `s[i] == '('`,此时以 `s[i]` 结尾的子串不可能构成有效括号对,则 `dp[i] = 0`。 - 如果 `s[i] == ')'`,我们需要考虑 `s[i - 1]` 来判断是否能够构成有效括号对。 - 如果 `s[i - 1] == '('`,字符串形如 `......()`,此时 `s[i - 1]` 与 `s[i]` 为 `()`,则: - `dp[i]` 取决于「以字符 `s[i - 2]` 为结尾的最长有效括号长度」 + 「`s[i - 1]` 与 `s[i]` 构成的有效括号对长度(`2`)」,即 `dp[i] = dp[i - 2] + 2`。 - 特别地,如果 `s[i - 2]` 不存在,即 `i - 2 < 0`,则 `dp[i]` 直接取决于 「`s[i - 1]` 与 `s[i]` 构成的有效括号对长度(`2`)」,即 `dp[i] = 2`。 - 如果 `s[i - 1] == ')'`,字符串形如 `......))`,此时 `s[i - 1]` 与 `s[i]` 为 `))`。那么以 `s[i - 1]` 为结尾的最长有效长度为 `dp[i - 1]`,则我们需要看 `i - 1 - dp[i - 1]` 位置上的字符 `s[i - 1 - dp[i - 1]]`是否与 `s[i]` 匹配。 - 如果 `s[i - 1 - dp[i - 1]] == '('`,则说明 `s[i - 1 - dp[i - 1]]`与 `s[i]` 相匹配,此时我们需要看以 `s[i - 1 - dp[i - 1]]` 的前一个字符 `s[i - 1 - dp[i - 2]]` 为结尾的最长括号长度是多少,将其加上 ``s[i - 1 - dp[i - 1]]`与 `s[i]`,从而构成更长的有效括号对: - `dp[i]` 取决于「以字符 `s[i - 1]` 为结尾的最长括号长度」 + 「以字符 `s[i - 1 - dp[i - 2]]` 为结尾的最长括号长度」+ 「`s[i - 1 - dp[i - 1]]` 与 `s[i]` 的长度(`2`)」,即 `dp[i] = dp[i - 1] + dp[i - dp[i - 1] - 2] + 2`。 - 特别地,如果 `s[i - dp[i - 1] - 2]` 不存在,即 `i - dp[i - 1] - 2 < 0`,则 `dp[i]` 直接取决于「以字符 `s[i - 1]` 为结尾的最长括号长度」+「`s[i - 1 - dp[i - 1]]` 与 `s[i]` 的长度(`2`)」,即 `dp[i] = dp[i - 1] + 2`。 ###### 4. 初始条件 - 默认所有以字符 `s[i]` 为结尾的最长有效括号的长度为 `0`,即 `dp[i] = 0`。 ###### 5. 最终结果 根据我们之前定义的状态,`dp[i]` 表示为:以字符 `s[i]` 为结尾的最长有效括号的长度。则最终结果为 `max(dp[i])`。 ### 思路 1:代码 ```python class Solution: def longestValidParentheses(self, s: str) -> int: dp = [0 for _ in range(len(s))] ans = 0 for i in range(1, len(s)): if s[i] == '(': continue if s[i - 1] == '(': if i >= 2: dp[i] = dp[i - 2] + 2 else: dp[i] = 2 elif i - dp[i - 1] > 0 and s[i - dp[i - 1] - 1] == '(': if i - dp[i - 1] >= 2: dp[i] = dp[i - 1] + dp[i - dp[i - 1] - 2] + 2 else: dp[i] = dp[i - 1] + 2 ans = max(ans, dp[i]) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串长度。 - **空间复杂度**:$O(n)$。 ### 思路 2:栈 1. 定义一个变量 `ans` 用于维护最长有效括号的长度,初始时,`ans = 0`。 2. 定义一个栈用于判定括号对是否匹配(栈中存储的是括号的下标),栈底元素始终保持「最长有效括号子串的开始元素的前一个元素下标」。 3. 初始时,我们在栈中存储 `-1` 作为哨兵节点,表示「最长有效括号子串的开始元素的前一个元素下标为 `-1`」,即 `stack = [-1]`, 4. 然后从左至右遍历字符串。 1. 如果遇到左括号,即 `s[i] == '('`,则将其下标 `i` 压入栈,用于后续匹配右括号。 2. 如果遇到右括号,即 `s[i] == ')'`,则将其与最近的左括号进行匹配(即栈顶元素),弹出栈顶元素,与当前右括号进行匹配。弹出之后: 1. 如果栈为空,则说明: 1. 之前弹出的栈顶元素实际上是「最长有效括号子串的开始元素的前一个元素下标」,而不是左括号`(`,此时无法完成合法匹配。 2. 将当前右括号的坐标 `i` 压入栈中,充当「下一个有效括号子串的开始元素前一个下标」。 2. 如果栈不为空,则说明: 1. 之前弹出的栈顶元素为左括号 `(`,此时可完成合法匹配。 2. 当前合法匹配的长度为「当前右括号的下标 `i`」 - 「最长有效括号子串的开始元素的前一个元素下标」。即 `i - stack[-1]`。 3. 更新最长匹配长度 `ans` 为 `max(ans, i - stack[-1])`。 5. 遍历完输出答案 `ans`。 ### 思路 2:代码 ```python class Solution: def longestValidParentheses(self, s: str) -> int: stack = [-1] ans = 0 for i in range(len(s)): if s[i] == '(': stack.append(i) else: stack.pop() if stack: ans = max(ans, i - stack[-1]) else: stack.append(i) return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串长度。 - **空间复杂度**:$O(n)$。 ## 参考资料 - 【题解】[动态规划思路详解(C++)——32.最长有效括号](https://leetcode.cn/problems/longest-valid-parentheses/solutions/206995/dong-tai-gui-hua-si-lu-xiang-jie-c-by-zhanganan042/) - 【题解】[32. 最长有效括号 - 力扣(Leetcode)](https://leetcode.cn/problems/longest-valid-parentheses/solutions/314683/zui-chang-you-xiao-gua-hao-by-leetcode-solution/) - 【题解】[【Nick~Hot一百题系列】超简单思路栈!](https://leetcode.cn/problems/longest-valid-parentheses/solutions/1258643/nickhotyi-bai-ti-xi-lie-chao-jian-dan-si-ggi4/) ================================================ FILE: docs/solutions/0001-0099/maximal-rectangle.md ================================================ # [0085. 最大矩形](https://leetcode.cn/problems/maximal-rectangle/) - 标签:栈、数组、动态规划、矩阵、单调栈 - 难度:困难 ## 题目链接 - [0085. 最大矩形 - 力扣](https://leetcode.cn/problems/maximal-rectangle/) ## 题目大意 **描述**: 给定一个仅包含 $0$ 和 $1$ 、大小为 $rows \times cols$ 的二维二进制矩阵。 **要求**: 找出只包含 $1$ 的最大矩形,并返回其面积。 **说明**: - $rows == matrix$.length$。 - $cols == matrix$[0].length$。 - $1 \le row, cols \le 200$。 - $matrix[i][j]$ 为 '0' 或 '1'。 **示例**: - 示例 1: ![](https://pic.leetcode.cn/1722912576-boIxpm-image.png) ```python 输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]] 输出:6 解释:最大矩形如上图所示。 ``` - 示例 2: ```python 输入:matrix = [["0"]] 输出:0 ``` ## 解题思路 ### 思路 1:动态规划 + 单调栈 **核心思想**:将二维矩阵问题转化为一维直方图问题,然后使用单调栈求解最大矩形面积。 **算法步骤**: 1. **构建高度数组**:对于矩阵的每一行,计算以该行为底部的连续1的高度 2. **转化为直方图问题**:每一行的高度数组就相当于一个直方图 3. **使用单调栈求解**:对每个直方图使用单调栈算法求解最大矩形面积 4. **更新全局最大值**:记录所有行中的最大矩形面积 **关键点**: - 对于每一行,如果当前元素是 `'1'`,则高度 $+1$;如果是 `'0'`,则高度重置为 $0$。 - 使用单调栈维护递增的高度序列,当遇到较小高度时,计算之前所有较大高度能形成的最大矩形。 - 在高度数组末尾添加 $0$,确保所有高度都能被处理。 ### 思路 1:代码 ```python class Solution: def largestRectangleArea(self, heights: List[int]) -> int: # 在高度数组末尾添加 0,确保所有高度都能被处理 heights.append(0) ans = 0 stack = [] # 单调栈,存储高度数组的索引 for i in range(len(heights)): # 当栈不为空且当前高度小于等于栈顶高度时 while stack and heights[stack[-1]] >= heights[i]: # 弹出栈顶元素,计算以该高度为矩形高度的最大面积 cur = stack.pop() # 计算矩形的左边界:栈中下一个元素的索引+1,如果栈为空则为 0 left = stack[-1] + 1 if stack else 0 # 计算矩形的右边界:当前索引 -1 right = i - 1 # 计算矩形面积:宽度 × 高度 ans = max(ans, (right - left + 1) * heights[cur]) # 将当前索引压入栈中 stack.append(i) return ans def maximalRectangle(self, matrix: List[List[str]]) -> int: # 处理空矩阵的情况 if not matrix or not matrix[0]: return 0 rows, cols = len(matrix), len(matrix[0]) # 高度数组,记录以当前行为底部的连续 1 的高度 heights = [0] * cols max_area = 0 # 遍历矩阵的每一行 for row in range(rows): # 更新高度数组 for col in range(cols): if matrix[row][col] == '1': # 如果当前元素是 '1',高度 +1 heights[col] += 1 else: # 如果当前元素是 '0',高度重置为 0 heights[col] = 0 # 对当前行的高度数组使用单调栈算法计算最大矩形面积 max_area = max(max_area, self.largestRectangleArea(heights[:])) return max_area ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 是矩阵的行数,$n$ 是矩阵的列数。需要遍历矩阵的每个元素,对每一行使用单调栈的时间复杂度是 $O(n)$。 - **空间复杂度**:$O(n)$,其中 $n$ 是矩阵的列数。需要维护高度数组和单调栈,空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0001-0099/maximum-subarray.md ================================================ # [0053. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/) - 标签:数组、分治、动态规划 - 难度:中等 ## 题目链接 - [0053. 最大子数组和 - 力扣](https://leetcode.cn/problems/maximum-subarray/) ## 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 **说明**: - **子数组**:指的是数组中的一个连续部分。 - $1 \le nums.length \le 10^5$。 - $-10^4 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6。 ``` - 示例 2: ```python 输入:nums = [1] 输出:1 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照连续子数组的结束位置进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 为:以第 $i$ 个数结尾的连续子数组的最大和。 ###### 3. 状态转移方程 状态 $dp[i]$ 为:以第 $i$ 个数结尾的连续子数组的最大和。则我们可以从「第 $i - 1$ 个数结尾的连续子数组的最大和」,以及「第 $i$ 个数的值」来讨论 $dp[i]$。 - 如果 $dp[i - 1] < 0$,则「第 $i - 1$ 个数结尾的连续子数组的最大和」+「第 $i$ 个数的值」<「第 $i$ 个数的值」,即:$dp[i - 1] + nums[i] < nums[i]$。所以,此时 $dp[i]$ 应取「第 $i$ 个数的值」,即 $dp[i] = nums[i]$。 - 如果 $dp[i - 1] \ge 0$,则「第 $i - 1$ 个数结尾的连续子数组的最大和」 +「第 $i$ 个数的值」 >= 第 $i$ 个数的值,即:$dp[i - 1] + nums[i] \ge nums[i]$。所以,此时 $dp[i]$ 应取「第 $i - 1$ 个数结尾的连续子数组的最大和」+「 第 $i$ 个数的值」,即 $dp[i] = dp[i - 1] + nums[i]$。 归纳一下,状态转移方程为: $dp[i] = \begin{cases} nums[i], & dp[i - 1] < 0 \cr dp[i - 1] + nums[i] & dp[i - 1] \ge 0 \end{cases}$ ###### 4. 初始条件 - 第 $0$ 个数结尾的连续子数组的最大和为 $nums[0]$,即 $dp[0] = nums[0]$。 ###### 5. 最终结果 根据状态定义,$dp[i]$ 为:以第 $i$ 个数结尾的连续子数组的最大和。则最终结果应为所有 $dp[i]$ 的最大值,即 $max(dp)$。 ### 思路 1:代码 ```python class Solution: def maxSubArray(self, nums: List[int]) -> int: size = len(nums) dp = [0 for _ in range(size)] dp[0] = nums[0] for i in range(1, size): if dp[i - 1] < 0: dp[i] = nums[i] else: dp[i] = dp[i - 1] + nums[i] return max(dp) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的元素个数。 - **空间复杂度**:$O(n)$。 ### 思路 2:动态规划 + 滚动优化 因为 $dp[i]$ 只和 $dp[i - 1]$ 和当前元素 $nums[i]$ 相关,我们也可以使用一个变量 $subMax$ 来表示以第 $i$ 个数结尾的连续子数组的最大和。然后使用 $ansMax$ 来保存全局中最大值。 ### 思路 2:代码 ```python class Solution: def maxSubArray(self, nums: List[int]) -> int: size = len(nums) subMax = nums[0] ansMax = nums[0] for i in range(1, size): if subMax < 0: subMax = nums[i] else: subMax += nums[i] ansMax = max(ansMax, subMax) return ansMax ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的元素个数。 - **空间复杂度**:$O(1)$。 ### 思路 3:分治算法 我们将数组 $nums$ 根据中心位置分为左右两个子数组。则具有最大和的连续子数组可能存在以下 $3$ 种情况: 1. 具有最大和的连续子数组在左子数组中。 2. 具有最大和的连续子数组在右子数组中。 3. 具有最大和的连续子数组跨过中心位置,一部分在左子数组中,另一部分在右子树组中。 那么我们要求出具有最大和的连续子数组的最大和,则分别对上面 $3$ 种情况求解即可。具体步骤如下: 1. 将数组 $nums$ 根据中心位置递归分为左右两个子数组,直到所有子数组长度为 $1$。 2. 长度为 $1$ 的子数组最大和肯定是数组中唯一的数,将其返回即可。 3. 求出左子数组的最大和 $leftMax$。 4. 求出右子树组的最大和 $rightMax$。 5. 求出跨过中心位置,一部分在左子数组中,另一部分在右子树组的子数组最大和 $leftTotal + rightTotal$。 6. 求出 $3$、$4$、$5$ 中的最大值,即为当前数组的最大和,将其返回即可。 ### 思路 3:代码 ```python class Solution: def maxSubArray(self, nums: List[int]) -> int: def max_sub_array(low, high): if low == high: return nums[low] mid = low + (high - low) // 2 leftMax = max_sub_array(low, mid) rightMax = max_sub_array(mid + 1, high) total = 0 leftTotal = -inf for i in range(mid, low - 1, -1): total += nums[i] leftTotal = max(leftTotal, total) total = 0 rightTotal = -inf for i in range(mid + 1, high + 1): total += nums[i] rightTotal = max(rightTotal, total) return max(leftMax, rightMax, leftTotal + rightTotal) return max_sub_array(0, len(nums) - 1) ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0001-0099/median-of-two-sorted-arrays.md ================================================ # [0004. 寻找两个正序数组的中位数](https://leetcode.cn/problems/median-of-two-sorted-arrays/) - 标签:数组、二分查找、分治 - 难度:困难 ## 题目链接 - [0004. 寻找两个正序数组的中位数 - 力扣](https://leetcode.cn/problems/median-of-two-sorted-arrays/) ## 题目大意 **描述**:给定两个正序(从小到大排序)数组 $nums1$、$nums2$。 **要求**:找出并返回这两个正序数组的中位数。 **说明**: - 算法的时间复杂度应该为 $O(\log (m + n))$ 。 - $nums1.length == m$。 - $nums2.length == n$。 - $0 \le m \le 1000$。 - $0 \le n \le 1000$。 - $1 \le m + n \le 2000$。 - $-10^6 \le nums1[i], nums2[i] \le 10^6$。 **示例**: - 示例 1: ```python 输入:nums1 = [1,2], nums2 = [3,4] 输出:2.50000 解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5 ``` - 示例 2: ```python 输入:nums1 = [1,2], nums2 = [3,4] 输出:2.50000 解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5 ``` ## 解题思路 ### 思路 1:二分查找 单个有序数组的中位数是中间元素位置的元素。如果中间元素位置有两个元素,则为两个元素的平均数。如果是两个有序数组,则可以使用归并排序的方式将两个数组拼接为一个大的有序数组。合并后有序数组中间位置的元素,即为中位数。 当然不合并的话,我们只需找到中位数的位置即可。我们用 $n1$、$n2$ 来表示数组 $nums1$、$nums2$ 的长度,则合并后的大的有序数组长度为 $(n1 + n2)$。 我们可以发现:**中位数把数组分割成了左右两部分,并且左右两部分元素个数相等。** - 如果 $(n1 + n2)$ 是奇数时,中位数是大的有序数组中第 $\lfloor \frac{(n1 + n2)}{2} \rfloor + 1$ 的元素,单侧元素个数为 $\lfloor \frac{(n1 + n2)}{2} \rfloor + 1$ 个(包含中位数)。 - 如果 $(n1 + n2)$ 是偶数时,中位数是第 $\lfloor \frac{(n1 + n2)}{2} \rfloor$ 的元素和第 $\lfloor \frac{(n1 + n2)}{2} \rfloor + 1$ 的元素的平均值,单侧元素个数为 $\lfloor \frac{(n1 + n2)}{2} \rfloor$ 个。 因为是向下取整,上面两种情况综合可以写为:单侧元素个数为:$\lfloor \frac{(n1 + n2 + 1)}{2} \rfloor$ 个。 我们用 $k$ 来表示 $\lfloor \frac{(n1 + n2 + 1)}{2} \rfloor$ 。现在的问题就变为了:**如何在两个有序数组中找到前 k 小的元素位置?** 如果我们从 $nums1$ 数组中取出前 $m1(m1 \le k)$ 个元素,那么从 $nums2$ 就需要取出前 $m2 = k - m1$ 个元素。 并且如果我们在 $nums1$ 数组中找到了合适的 $m1$ 位置,则 $m2$ 的位置也就确定了。 问题就可以进一步转换为:**如何从 $nums1$ 数组中取出前 $m1$ 个元素,使得 $nums1$ 第 $m1$ 个元素或者 $nums2$ 第 $m2 = k - m1$ 个元素为中位线位置**。 我们可以通过「二分查找」的方法,在数组 $nums1$ 中找到合适的 $m1$ 位置,具体做法如下: 1. 让 $left$ 指向 $nums1$ 的头部位置 $0$,$right$ 指向 $nums1$ 的尾部位置 $n1$。 2. 每次取中间位置作为 $m1$,则 $m2 = k - m1$。然后判断 $nums1$ 第 $m1$ 位置上元素和 $nums2$ 第 $m2 - 1$ 位置上元素之间的关系,即 $nums1[m1]$ 和 $nums2[m2 - 1]$ 的关系。 1. 如果 $nums1[m1] < nums2[m2 - 1]$,则 $nums1$ 的前 $m1$ 个元素都不可能是第 $k$ 个元素。说明 $m1$ 取值有点小了,应该将 $m1$ 进行右移操作,即 $left = m1 + 1$。 2. 如果 $nums1[m1] \ge nums2[m2 - 1]$,则说明 $m1$ 取值可能有点大了,应该将 $m1$ 进行左移。根据二分查找排除法的思路(排除一定不存在的区间,在剩下区间中继续查找),这里应取 $right = m1$。 3. 找到 $m1$ 的位置之后,还要根据两个数组长度和 $(n1 + n2)$ 的奇偶性,以及边界条件来计算对应的中位数。 --- 上面之所以要判断 $nums1[m1]$ 和 $nums2[m2 - 1]$ 的关系是因为: > 如果 $nums1[m1] < nums2[m2 - 1]$,则说明: > > - 最多有 $m1 + m2 - 1 = k - 1$ 个元素比 $nums1[m1]$ 小,所以 $nums1[m1]$ 左侧的 $m1$ 个元素都不可能是第 $k$ 个元素。可以将 $m1$ 左侧的元素全部排除,然后将 $m1$ 进行右移。 推理过程: 如果 $nums1[m1] < nums2[m2 - 1]$,则: 1. $nums1[m1]$ 左侧比 $nums1[m1]$ 小的一共有 $m1$ 个元素($nums1[0] ... nums1[m1 - 1]$ 共 $m1$ 个)。 2. $nums2$ 数组最多有 $m2 - 1$ 个元素比 $nums1[m1]$ 小(即便是 $nums2[m2 - 1]$ 左侧所有元素都比 $nums1[m1]$ 小,也只有 $m2 - 1$ 个)。 3. 综上所述,$nums1$、$nums2$ 数组中最多有 $m1 + m2 - 1 = k - 1$ 个元素比 $nums1[m1]$ 小。 4. 所以 $nums1[m1]$ 左侧的 $m1$ 个元素($nums1[0] ... nums1[m1 - 1]$)都不可能是第 $k$ 个元素。可以将 $m1$ 左侧的元素全部排除,然后将 $m1$ 进行右移。 ### 思路 1:代码 ```python class Solution: def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float: n1 = len(nums1) n2 = len(nums2) if n1 > n2: return self.findMedianSortedArrays(nums2, nums1) k = (n1 + n2 + 1) // 2 left = 0 right = n1 while left < right: m1 = left + (right - left) // 2 # 在 nums1 中取前 m1 个元素 m2 = k - m1 # 在 nums2 中取前 m2 个元素 if nums1[m1] < nums2[m2 - 1]: # 说明 nums1 中所元素不够多, left = m1 + 1 else: right = m1 m1 = left m2 = k - m1 c1 = max(float('-inf') if m1 <= 0 else nums1[m1 - 1], float('-inf') if m2 <= 0 else nums2[m2 - 1]) if (n1 + n2) % 2 == 1: return c1 c2 = min(float('inf') if m1 >= n1 else nums1[m1], float('inf') if m2 >= n2 else nums2[m2]) return (c1 + c2) / 2 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log (m + n))$ 。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/merge-intervals.md ================================================ # [0056. 合并区间](https://leetcode.cn/problems/merge-intervals/) - 标签:数组、排序 - 难度:中等 ## 题目链接 - [0056. 合并区间 - 力扣](https://leetcode.cn/problems/merge-intervals/) ## 题目大意 **描述**:给定数组 `intervals` 表示若干个区间的集合,其中单个区间为 `intervals[i] = [starti, endi]` 。 **要求**:合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。 **说明**: - $1 \le intervals.length \le 10^4$。 - $intervals[i].length == 2$。 - $0 \le starti \le endi \le 10^4$。 **示例**: - 示例 1: ```python 输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]] 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. ``` - 示例 2: ```python 输入:intervals = [[1,4],[4,5]] 输出:[[1,5]] 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。 ``` ## 解题思路 ### 思路 1:排序 1. 设定一个数组 `ans` 用于表示最终不重叠的区间数组,然后对原始区间先按照区间左端点大小从小到大进行排序。 2. 遍历所有区间。 3. 先将第一个区间加入 `ans` 数组中。 4. 然后依次考虑后边的区间: 1. 如果第 `i` 个区间左端点在前一个区间右端点右侧,则这两个区间不会重合,直接将该区间加入 `ans` 数组中。 2. 否则的话,这两个区间重合,判断一下两个区间的右区间值,更新前一个区间的右区间值为较大值,然后继续考虑下一个区间,以此类推。 5. 最后返回数组 `ans`。 ### 思路 1:代码 ```python class Solution: def merge(self, intervals: List[List[int]]) -> List[List[int]]: intervals.sort(key=lambda x: x[0]) ans = [] for interval in intervals: if not ans or ans[-1][1] < interval[0]: ans.append(interval) else: ans[-1][1] = max(ans[-1][1], interval[1]) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log_2 n)$。其中 $n$ 为区间数量。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0001-0099/merge-k-sorted-lists.md ================================================ # [0023. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) - 标签:链表、分治、堆(优先队列)、归并排序 - 难度:困难 ## 题目链接 - [0023. 合并 K 个升序链表 - 力扣](https://leetcode.cn/problems/merge-k-sorted-lists/) ## 题目大意 **描述**:给定一个链表数组,每个链表都已经按照升序排列。 **要求**:将所有链表合并到一个升序链表中,返回合并后的链表。 **说明**: - $k == lists.length$。 - $0 \le k \le 10^4$。 - $0 \le lists[i].length \le 500$。 - $-10^4 \le lists[i][j] \le 10^4$。 - $lists[i]$ 按升序排列。 - $lists[i].length$ 的总和不超过 $10^4$。 **示例**: - 示例 1: ```python 输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:[1,1,2,3,4,4,5,6] 解释:链表数组如下: [ 1->4->5, 1->3->4, 2->6 ] 将它们合并到一个有序链表中得到。 1->1->2->3->4->4->5->6 ``` - 示例 2: ```python 输入:lists = [] 输出:[] ``` ## 解题思路 ### 思路 1:分治算法 分而治之的思想。将链表数组不断二分,转为规模为二分之一的子问题,然后再进行归并排序。 ### 思路 1:代码 ```python class Solution: def merge_sort(self, lists: List[ListNode], left: int, right: int) -> ListNode: if left == right: return lists[left] mid = left + (right - left) // 2 node_left = self.merge_sort(lists, left, mid) node_right = self.merge_sort(lists, mid + 1, right) return self.merge(node_left, node_right) def merge(self, a: ListNode, b: ListNode) -> ListNode: root = ListNode(-1) cur = root while a and b: if a.val < b.val: cur.next = a a = a.next else: cur.next = b b = b.next cur = cur.next if a: cur.next = a if b: cur.next = b return root.next def mergeKLists(self, lists: List[ListNode]) -> ListNode: if not lists: return None size = len(lists) return self.merge_sort(lists, 0, size - 1) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k \times n \times \log_2k)$。 - **空间复杂度**:$O(\log_2k)$。 ================================================ FILE: docs/solutions/0001-0099/merge-sorted-array.md ================================================ # [0088. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/) - 标签:数组、双指针、排序 - 难度:简单 ## 题目链接 - [0088. 合并两个有序数组 - 力扣](https://leetcode.cn/problems/merge-sorted-array/) ## 题目大意 **描述**:给定两个有序数组 $nums1$、$nums2$。 **要求**:将 $nums2$ 合并到 $nums1$ 中,使 $nums1$ 成为一个有序数组。 **说明**: - 给定数组 $nums1$ 空间大小为$ m + n$ 个,其中前 $m$ 个为 $nums1$ 的元素。$nums2$ 空间大小为 $n$。这样可以用 $nums1$ 的空间来存储最终的有序数组。 - $nums1.length == m + n$。 - $nums2.length == n$。 - $0 \le m, n \le 200$。 - $1 \le m + n \le 200$。 - $-10^9 \le nums1[i], nums2[j] \le 10^9$。 **示例**: - 示例 1: ```python 输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 输出:[1,2,2,3,5,6] 解释:需要合并 [1,2,3] 和 [2,5,6] 。 合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。 ``` - 示例 2: ```python 输入:nums1 = [1], m = 1, nums2 = [], n = 0 输出:[1] 解释:需要合并 [1] 和 [] 。 合并结果是 [1] 。 ``` ## 解题思路 ### 思路 1:快慢指针 1. 将两个指针 $index1$、$index2$ 分别指向 $nums1$、$nums2$ 数组的尾部,再用一个指针 $index$ 指向数组 $nums1$ 的尾部。 2. 从后向前判断当前指针下 $nums1[index1]$ 和 $nums[index2]$ 的值大小,将较大值存入 $num1[index]$ 中,然后继续向前遍历。 3. 最后再将 $nums2$ 中剩余元素赋值到 $num1$ 前面对应位置上。 ### 思路 1:代码 ```python class Solution: def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None: index1 = m - 1 index2 = n - 1 index = m + n - 1 while index1 >= 0 and index2 >= 0: if nums1[index1] < nums2[index2]: nums1[index] = nums2[index2] index2 -= 1 else: nums1[index] = nums1[index1] index1 -= 1 index -= 1 nums1[:index2+1] = nums2[:index2+1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n)$。 - **空间复杂度**:$O(m + n)$。 ================================================ FILE: docs/solutions/0001-0099/merge-two-sorted-lists.md ================================================ # [0021. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) - 标签:递归、链表 - 难度:简单 ## 题目链接 - [0021. 合并两个有序链表 - 力扣](https://leetcode.cn/problems/merge-two-sorted-lists/) ## 题目大意 **描述**:给定两个升序链表的头节点 `list1` 和 `list2`。 **要求**:将其合并为一个升序链表。 **说明**: - 两个链表的节点数目范围是 $[0, 50]$。 - $-100 \le Node.val \le 100$。 - `list1` 和 `list2` 均按 **非递减顺序** 排列 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/03/merge_ex1.jpg) ```python 输入:list1 = [1,2,4], list2 = [1,3,4] 输出:[1,1,2,3,4,4] ``` - 示例 2: ```python 输入:list1 = [], list2 = [] 输出:[] ``` ## 解题思路 ### 思路 1:归并排序 利用归并排序的思想,具体步骤如下: 1. 使用哑节点 `dummy_head` 构造一个头节点,并使用 `curr` 指向 `dummy_head` 用于遍历。 2. 然后判断 `list1` 和 `list2` 头节点的值,将较小的头节点加入到合并后的链表中。并向后移动该链表的头节点指针。 3. 然后重复上一步操作,直到两个链表中出现链表为空的情况。 4. 将剩余链表链接到合并后的链表中。 5. 将哑节点 `dummy_dead` 的下一个链节点 `dummy_head.next` 作为合并后有序链表的头节点返回。 ### 思路 1:代码 ```python class Solution: def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: dummy_head = ListNode(-1) curr = dummy_head while list1 and list2: if list1.val <= list2.val: curr.next = list1 list1 = list1.next else: curr.next = list2 list2 = list2.next curr = curr.next curr.next = list1 if list1 is not None else list2 return dummy_head.next ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/minimum-path-sum.md ================================================ # [0064. 最小路径和](https://leetcode.cn/problems/minimum-path-sum/) - 标签:数组、动态规划、矩阵 - 难度:中等 ## 题目链接 - [0064. 最小路径和 - 力扣](https://leetcode.cn/problems/minimum-path-sum/) ## 题目大意 **描述**:给定一个包含非负整数的 $m \times n$ 大小的网格 $grid$。 **要求**:找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 **说明**: - 每次只能向下或者向右移动一步。 - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 200$。 - $0 \le grid[i][j] \le 100$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/05/minpath.jpg) ```python 输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径 1→3→1→1→1 的总和最小。 ``` - 示例 2: ```python 输入:grid = [[1,2,3],[4,5,6]] 输出:12 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照路径的结尾位置(行位置、列位置组成的二维坐标)进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 为:从左上角到达 $(i, j)$ 位置的最小路径和。 ###### 3. 状态转移方程 当前位置 $(i, j)$ 只能从左侧位置 $(i, j - 1)$ 或者上方位置 $(i - 1, j)$ 到达。为了使得从左上角到达 $(i, j)$ 位置的最小路径和最小,应从 $(i, j - 1)$ 位置和 $(i - 1, j)$ 位置选择路径和最小的位置达到 $(i, j)$。 即状态转移方程为:$dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]$。 ###### 4. 初始条件 - 当左侧和上方是矩阵边界时(即 $i = 0, j = 0$),$dp[i][j] = grid[i][j]$。 - 当只有左侧是矩阵边界时(即 $i \ne 0, j = 0$),只能从上方到达,$dp[i][j] = dp[i - 1][j] + grid[i][j]$。 - 当只有上方是矩阵边界时(即 $i = 0, j \ne 0$),只能从左侧到达,$dp[i][j] = dp[i][j - 1] + grid[i][j]$。 ###### 5. 最终结果 根据状态定义,最后输出 $dp[rows - 1][cols - 1]$(即从左上角到达 $(rows - 1, cols - 1)$ 位置的最小路径和)即可。其中 $rows$、$cols$ 分别为 $grid$ 的行数、列数。 ### 思路 1:代码 ```python class Solution: def minPathSum(self, grid: List[List[int]]) -> int: rows, cols = len(grid), len(grid[0]) dp = [[0 for _ in range(cols)] for _ in range(rows)] dp[0][0] = grid[0][0] for i in range(1, rows): dp[i][0] = dp[i - 1][0] + grid[i][0] for j in range(1, cols): dp[0][j] = dp[0][j - 1] + grid[0][j] for i in range(1, rows): for j in range(1, cols): dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j] return dp[rows - 1][cols - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m * n)$,其中 $m$、$n$ 分别为 $grid$ 的行数和列数。 - **空间复杂度**:$O(m * n)$。 ================================================ FILE: docs/solutions/0001-0099/minimum-window-substring.md ================================================ # [0076. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/) - 标签:哈希表、字符串、滑动窗口 - 难度:困难 ## 题目链接 - [0076. 最小覆盖子串 - 力扣](https://leetcode.cn/problems/minimum-window-substring/) ## 题目大意 **描述**:给定一个字符串 `s`、一个字符串 `t`。 **要求**:返回 `s` 中涵盖 `t` 所有字符的最小子串。如果 `s` 中不存在涵盖 `t` 所有字符的子串,则返回空字符串 `""`。 **说明**: - $1 \le s.length, t.length \le 10^5$。 - `s` 和 `t` 由英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "ADOBECODEBANC", t = "ABC" 输出:"BANC" ``` - 示例 2: ```python 输入:s = "a", t = "a" 输出:"a" ``` ## 解题思路 ### 思路 1:滑动窗口 1. `left`、`right` 表示窗口的边界,一开始都位于下标 `0` 处。`need` 用于记录短字符串需要的字符数。`window` 记录当前窗口内的字符数。 2. 将 `right` 右移,直到出现了 `t` 中全部字符,开始右移 `left`,减少滑动窗口的大小,并记录下最小覆盖子串的长度和起始位置。 3. 最后输出结果。 ### 思路 1:代码 ```python import collections class Solution: def minWindow(self, s: str, t: str) -> str: need = collections.defaultdict(int) window = collections.defaultdict(int) for ch in t: need[ch] += 1 left, right = 0, 0 valid = 0 start = 0 size = len(s) + 1 while right < len(s): insert_ch = s[right] right += 1 if insert_ch in need: window[insert_ch] += 1 if window[insert_ch] == need[insert_ch]: valid += 1 while valid == len(need): if right - left < size: start = left size = right - left remove_ch = s[left] left += 1 if remove_ch in need: if window[remove_ch] == need[remove_ch]: valid -= 1 window[remove_ch] -= 1 if size == len(s) + 1: return '' return s[start:start+size] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是字符串 $s$ 的长度。 - **空间复杂度**:$O(| \sum |)$。$| \sum |$ 是 $s$ 和 $t$ 的字符集大小。 ================================================ FILE: docs/solutions/0001-0099/multiply-strings.md ================================================ # [0043. 字符串相乘](https://leetcode.cn/problems/multiply-strings/) - 标签:数学、字符串、模拟 - 难度:中等 ## 题目链接 - [0043. 字符串相乘 - 力扣](https://leetcode.cn/problems/multiply-strings/) ## 题目大意 **描述**:给定两个以字符串形式表示的非负整数 `num1` 和 `num2`。 **要求**:返回 `num1` 和 `num2` 的乘积,它们的乘积也表示为字符串形式。 **说明**: - 不能使用任何标准库的大数类型(比如 BigInteger)或直接将输入转换为整数来处理。 - $1 \le num1.length, num2.length \le 200$。 - `num1` 和 `num2` 只能由数字组成。 - `num1` 和 `num2` 都不包含任何前导零,除了数字0本身。 **示例**: - 示例 1: ```python 输入: num1 = "2", num2 = "3" 输出: "6" ``` - 示例 2: ```python 输入: num1 = "123", num2 = "456" 输出: "56088" ``` ## 解题思路 ### 思路 1:模拟 我们可以使用数组来模拟大数乘法。长度为 `len(num1)` 的整数 `num1` 与长度为 `len(num2)` 的整数 `num2` 相乘的结果长度为 `len(num1) + len(num2) - 1` 或 `len(num1) + len(num2)`。所以我们可以使用长度为 `len(num1) + len(num2)` 的整数数组 `nums` 来存储两个整数相乘之后的结果。 整个计算流程的步骤如下: 1. 从个位数字由低位到高位开始遍历 `num1`,取得每一位数字 `digit1`。从个位数字由低位到高位开始遍历 `num2`,取得每一位数字 `digit2`。 2. 将 `digit1 * digit2` 的结果累积存储到 `nums` 对应位置 `i + j + 1` 上。 3. 计算完毕之后从 `len(num1) + len(num2) - 1` 的位置由低位到高位遍历数组 `nums`。将每个数位上大于等于 `10` 的数字进行进位操作,然后对该位置上的数字进行取余操作。 4. 最后判断首位是否有进位。如果首位为 `0`,则从第 `1` 个位置开始将答案数组拼接成字符串。如果首位不为 `0`,则从第 `0` 个位置开始将答案数组拼接成字符串。并返回答案字符串。 ### 思路 1:代码 ```python class Solution: def multiply(self, num1: str, num2: str) -> str: if num1 == "0" or num2 == "0": return "0" len1, len2 = len(num1), len(num2) nums = [0 for _ in range(len1 + len2)] for i in range(len1 - 1, -1, -1): digit1 = int(num1[i]) for j in range(len2 - 1, -1, -1): digit2 = int(num2[j]) nums[i + j + 1] += digit1 * digit2 for i in range(len1 + len2 - 1, 0, -1): nums[i - 1] += nums[i] // 10 nums[i] %= 10 if nums[0] == 0: ans = "".join(str(digit) for digit in nums[1:]) else: ans = "".join(str(digit) for digit in nums[:]) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 和 $n$ 分别为 `nums1` 和 `nums2` 的长度。 - **空间复杂度**:$O(m + n)$。 ================================================ FILE: docs/solutions/0001-0099/n-queens-ii.md ================================================ # [0052. N 皇后 II](https://leetcode.cn/problems/n-queens-ii/) - 标签:回溯 - 难度:困难 ## 题目链接 - [0052. N 皇后 II - 力扣](https://leetcode.cn/problems/n-queens-ii/) ## 题目大意 **描述**:给定一个整数 `n`。 **要求**:返回「`n` 皇后问题」不同解决方案的数量。 **说明**: - **n 皇后问题**:将 `n` 个皇后放置在 `n * n` 的棋盘上,并且使得皇后彼此之间不能攻击。 - **皇后彼此不能相互攻击**:指的是任何两个皇后都不能处于同一条横线、纵线或者斜线上。 - $1 \le n \le 9$。 **示例**: - 示例 1: ```python 输入:n = 4 输出:2 解释:如下图所示,4 皇后问题存在两个不同的解法。 ``` ![](https://assets.leetcode.com/uploads/2020/11/13/queens.jpg) ## 解题思路 ### 思路 1:回溯算法 和「[51. N 皇后 - 力扣](https://leetcode.cn/problems/n-queens/)」做法一致。区别在于「[51. N 皇后 - 力扣](https://leetcode.cn/problems/n-queens/)」需要返回所有解决方案,而这道题只需要得到所有解决方案的数量即可。下面来说一下这道题的解题思路。 我们可以按照行序来放置皇后,也就是先放第一行,再放第二行 …… 一直放到最后一行。 对于 `n * n` 的棋盘来说,每一行有 `n` 列,也就有 `n` 种放法可供选择。我们可以尝试选择其中一列,查看是否与之前放置的皇后有冲突,如果没有冲突,则继续在下一行放置皇后。依次类推,直到放置完所有皇后,并且都不发生冲突时,就得到了一个合理的解。 并且在放置完之后,通过回溯的方式尝试其他可能的分支。 下面我们根据回溯算法三步走,写出对应的回溯算法。 ![N 皇后问题的解题流程](https://qcdn.itcharge.cn/images/20220426095225.png) 1. **明确所有选择**:根据棋盘中当前行的所有列位置上是否选择放置皇后,画出决策树,如上图所示。 2. **明确终止条件**: - 当遍历到决策树的叶子节点时,就终止了。也就是在最后一行放置完皇后时,递归终止。 3. **将决策树和终止条件翻译成代码**: 1. 定义回溯函数: - 首先我们先使用一个 $n \times n$ 大小的二维矩阵 `chessboard` 来表示当前棋盘,`chessboard` 中的字符 `Q` 代表皇后,`.` 代表空位,初始都为 `.`。 - 然后定义回溯函数 `backtrack(chessboard, row): ` 函数的传入参数是 `chessboard`(棋盘数组)和 `row`(代表当前正在考虑放置第 `row` 行皇后),全局变量是 `ans`(所有可行方案的数量)。 - `backtrack(chessboard, row):` 函数代表的含义是:在放置好第 `row` 行皇后的情况下,递归放置剩下行的皇后。 2. 书写回溯函数主体(给出选择元素、递归搜索、撤销选择部分)。 - 枚举出当前行所有的列。对于每一列位置: - 约束条件:定义一个判断方法,先判断一下当前位置是否与之前棋盘上放置的皇后发生冲突,如果不发生冲突则继续放置,否则则继续向后遍历判断。 - 选择元素:选择 `row, col` 位置放置皇后,将其棋盘对应位置设置为 `Q`。 - 递归搜索:在该位置放置皇后的情况下,继续递归考虑下一行。 - 撤销选择:将棋盘上 `row, col` 位置设置为 `.`。 ### 思路 1:代码 ```python class Solution: # 判断当前位置 row, col 是否与之前放置的皇后发生冲突 def isValid(self, n: int, row: int, col: int, chessboard: List[List[str]]): for i in range(row): if chessboard[i][col] == 'Q': return False i, j = row - 1, col - 1 while i >= 0 and j >= 0: if chessboard[i][j] == 'Q': return False i -= 1 j -= 1 i, j = row - 1, col + 1 while i >= 0 and j < n: if chessboard[i][j] == 'Q': return False i -= 1 j += 1 return True def totalNQueens(self, n: int) -> int: chessboard = [['.' for _ in range(n)] for _ in range(n)] # 棋盘初始化 ans = 0 def backtrack(chessboard: List[List[str]], row: int): # 正在考虑放置第 row 行的皇后 if row == n: # 遇到终止条件 nonlocal ans ans += 1 return for col in range(n): # 枚举可放置皇后的列 if self.isValid(n, row, col, chessboard): # 如果该位置与之前放置的皇后不发生冲突 chessboard[row][col] = 'Q' # 选择 row, col 位置放置皇后 backtrack(chessboard, row + 1) # 递归放置 row + 1 行之后的皇后 chessboard[row][col] = '.' # 撤销选择 row, col 位置 backtrack(chessboard, 0) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n!)$,其中 $n$ 是皇后数量。 - **空间复杂度**:$O(n^2)$,其中 $n$ 是皇后数量。递归调用层数不会超过 $n$,每个棋盘的空间复杂度为 $O(n^2)$,所以空间复杂度为 $O(n^2)$。 ================================================ FILE: docs/solutions/0001-0099/n-queens.md ================================================ # [0051. N 皇后](https://leetcode.cn/problems/n-queens/) - 标签:数组、回溯 - 难度:困难 ## 题目链接 - [0051. N 皇后 - 力扣](https://leetcode.cn/problems/n-queens/) ## 题目大意 **描述**:给定一个整数 `n`。 **要求**:返回所有不同的「`n` 皇后问题」的解决方案。每一种解法包含一个不同的「`n` 皇后问题」的棋子放置方案,该方案中的 `Q` 和 `.` 分别代表了皇后和空位。 **说明**: - **n 皇后问题**:将 `n` 个皇后放置在 `n * n` 的棋盘上,并且使得皇后彼此之间不能攻击。 - **皇后彼此不能相互攻击**:指的是任何两个皇后都不能处于同一条横线、纵线或者斜线上。 - $1 \le n \le 9$。 **示例**: - 示例 1: ```python 输入:n = 4 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] 解释:如下图所示,4 皇后问题存在 2 个不同的解法。 ``` ![](https://assets.leetcode.com/uploads/2020/11/13/queens.jpg) ## 解题思路 ### 思路 1:回溯算法 这道题是经典的回溯问题。我们可以按照行序来放置皇后,也就是先放第一行,再放第二行 …… 一直放到最后一行。 对于 `n * n` 的棋盘来说,每一行有 `n` 列,也就有 `n` 种放法可供选择。我们可以尝试选择其中一列,查看是否与之前放置的皇后有冲突,如果没有冲突,则继续在下一行放置皇后。依次类推,直到放置完所有皇后,并且都不发生冲突时,就得到了一个合理的解。 并且在放置完之后,通过回溯的方式尝试其他可能的分支。 下面我们根据回溯算法三步走,写出对应的回溯算法。 ![N 皇后问题的解题流程](https://qcdn.itcharge.cn/images/20220426095225.png) 1. **明确所有选择**:根据棋盘中当前行的所有列位置上是否选择放置皇后,画出决策树,如上图所示。 2. **明确终止条件**: - 当遍历到决策树的叶子节点时,就终止了。也就是在最后一行放置完皇后时,递归终止。 3. **将决策树和终止条件翻译成代码**: 1. 定义回溯函数: - 首先我们先使用一个 $n \times n$ 大小的二维矩阵 `chessboard` 来表示当前棋盘,`chessboard` 中的字符 `Q` 代表皇后,`.` 代表空位,初始都为 `.`。 - 然后定义回溯函数 `backtrack(chessboard, row): ` 函数的传入参数是 `chessboard`(棋盘数组)和 `row`(代表当前正在考虑放置第 `row` 行皇后),全局变量是 `res`(存放所有符合条件结果的集合数组)。 - `backtrack(chessboard, row):` 函数代表的含义是:在放置好第 `row` 行皇后的情况下,递归放置剩下行的皇后。 2. 书写回溯函数主体(给出选择元素、递归搜索、撤销选择部分)。 - 枚举出当前行所有的列。对于每一列位置: - 约束条件:定义一个判断方法,先判断一下当前位置是否与之前棋盘上放置的皇后发生冲突,如果不发生冲突则继续放置,否则则继续向后遍历判断。 - 选择元素:选择 `row, col` 位置放置皇后,将其棋盘对应位置设置为 `Q`。 - 递归搜索:在该位置放置皇后的情况下,继续递归考虑下一行。 - 撤销选择:将棋盘上 `row, col` 位置设置为 `.`。 ```python # 判断当前位置 row, col 是否与之前放置的皇后发生冲突 def isValid(self, n: int, row: int, col: int, chessboard: List[List[str]]): for i in range(row): if chessboard[i][col] == 'Q': return False i, j = row - 1, col - 1 while i >= 0 and j >= 0: if chessboard[i][j] == 'Q': return False i -= 1 j -= 1 i, j = row - 1, col + 1 while i >= 0 and j < n: if chessboard[i][j] == 'Q': return False i -= 1 j += 1 return True ``` ```python for col in range(n): # 枚举可放置皇后的列 if self.isValid(n, row, col, chessboard): # 如果该位置与之前放置的皇后不发生冲突 chessboard[row][col] = 'Q' # 选择 row, col 位置放置皇后 backtrack(row + 1, chessboard) # 递归放置 row + 1 行之后的皇后 chessboard[row][col] = '.' # 撤销选择 row, col 位置 ``` 3. 明确递归终止条件(给出递归终止条件,以及递归终止时的处理方法)。 - 当遍历到决策树的叶子节点时,就终止了。也就是在最后一行放置完皇后(即 `row == n`)时,递归停止。 - 递归停止时,将当前符合条件的棋盘转换为答案需要的形式,然后将其存入答案数组 `res` 中即可。 ### 思路 1:代码 ```python class Solution: res = [] def backtrack(self, n: int, row: int, chessboard: List[List[str]]): if row == n: temp_res = [] for temp in chessboard: temp_str = ''.join(temp) temp_res.append(temp_str) self.res.append(temp_res) return for col in range(n): if self.isValid(n, row, col, chessboard): chessboard[row][col] = 'Q' self.backtrack(n, row + 1, chessboard) chessboard[row][col] = '.' def isValid(self, n: int, row: int, col: int, chessboard: List[List[str]]): for i in range(row): if chessboard[i][col] == 'Q': return False i, j = row - 1, col - 1 while i >= 0 and j >= 0: if chessboard[i][j] == 'Q': return False i -= 1 j -= 1 i, j = row - 1, col + 1 while i >= 0 and j < n: if chessboard[i][j] == 'Q': return False i -= 1 j += 1 return True def solveNQueens(self, n: int) -> List[List[str]]: self.res.clear() chessboard = [['.' for _ in range(n)] for _ in range(n)] self.backtrack(n, 0, chessboard) return self.res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n!)$,其中 $n$ 是皇后数量。 - **空间复杂度**:$O(n^2)$,其中 $n$ 是皇后数量。递归调用层数不会超过 $n$,每个棋盘的空间复杂度为 $O(n^2)$,所以空间复杂度为 $O(n^2)$。 ================================================ FILE: docs/solutions/0001-0099/next-permutation.md ================================================ # [0031. 下一个排列](https://leetcode.cn/problems/next-permutation/) - 标签:数组、双指针 - 难度:中等 ## 题目链接 - [0031. 下一个排列 - 力扣](https://leetcode.cn/problems/next-permutation/) ## 题目大意 **描述**: 整数数组的一个「排列」就是将其所有成员以序列或线性顺序排列。 - 例如,$arr = [1,2,3]$,以下这些都可以视作 $arr$ 的排列:$[1,2,3]$、$[1,3,2]$、$[3,1,2]$、$[2,3,1]$。 整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。 - 例如,$arr = [1,2,3]$ 的下一个排列是 $[1,3,2]$。 - 类似地,$arr = [2,3,1]$ 的下一个排列是 $[3,1,2]$。 - 而 $arr = [3,2,1]$ 的下一个排列是 $[1,2,3]$,因为 $[3,2,1]$ 不存在一个字典序更大的排列。 给定一个整数数组 $nums$。 **要求**: 找出 $nums$ 的下一个排列。 **说明**: - $1 \le nums.length \le 100$。 - $0 \le nums[i] \le 100$。 - 必须原地修改,只允许使用额外常数空间。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3] 输出:[1,3,2] ``` - 示例 2: ```python 输入:nums = [3,2,1] 输出:[1,2,3] ``` ## 解题思路 ### 思路 1:暴力搜索 **核心思想**:从右向左遍历数组,找到第一个可以交换的位置,然后交换并排序剩余部分。 **算法步骤**: 1. 从数组末尾开始向前遍历,找到第一个 $nums[i] < nums[j]$ 的位置(其中 $j > i$)。 2. 找到这个位置后,交换 $nums[i]$ 和 $nums[j]$。 3. 将 $nums[i+1:]$ 部分进行排序,得到字典序最小的排列。 4. 如果找不到这样的位置,说明当前排列已经是最大排列,直接对整个数组排序得到最小排列。 **关键点**: - 从右向左遍历确保找到的是最右边的可交换位置。 - 交换后对剩余部分排序,保证得到的是下一个字典序排列。 ### 思路 1:代码 ```python class Solution: def nextPermutation(self, nums: List[int]) -> None: """ Do not return anything, modify nums in-place instead. """ size = len(nums) for i in range(size - 1, -1, -1): for j in range(size - 1, i, -1): if nums[i] < nums[j]: nums[i], nums[j] = nums[j], nums[i] nums[i + 1: ] = sorted(nums[i + 1:]) return nums.sort() ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是数组长度。最坏情况下需要遍历所有位置,每次遍历需要 $O(n)$ 时间,排序需要 $O(n \log n)$ 时间。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0001-0099/palindrome-number.md ================================================ # [0009. 回文数](https://leetcode.cn/problems/palindrome-number/) - 标签:数学 - 难度:简单 ## 题目链接 - [0009. 回文数 - 力扣](https://leetcode.cn/problems/palindrome-number/) ## 题目大意 给定整数 x,判断 x 是否是回文数。要求不能用整数转为字符串的方式来解决这个问题。 回文数指的是正序(从左向右)和倒序(从右向左)读都是一样的整数。比如 12321。 ## 解题思路 - 首先,负数,10 的倍数都不是回文数,可以直接排除。 - 然后将原数进行按位取余,并按位反转,如果与原数完全相等,则原数为回文数。 - 其实,第二步在反转到一半的时候,就可以进行判断了。因为原数是回文数,那么在反转到中间的时候,留下的前半部分,应该与转换好的后半部分倒转过来相等。比如:1221,转换到一半,原数变为 12,转换好的数变为 12,则说明原数就是回文数。如果原数为奇数,比如:12321,转换到一半,原数变为 12,转换好的数变为 123,则应该将原数与 转换好的数对 10 取余的部分进行比较。 ## 代码 ```python class Solution: def isPalindrome(self, x: int) -> bool: if x < 0 or (x % 10 == 0 and x != 0): return False res = 0 while x > res: res = res * 10 + x % 10 x = x // 10 return x == res or x == res // 10 ``` ================================================ FILE: docs/solutions/0001-0099/partition-list.md ================================================ # [0086. 分隔链表](https://leetcode.cn/problems/partition-list/) - 标签:链表、双指针 - 难度:中等 ## 题目链接 - [0086. 分隔链表 - 力扣](https://leetcode.cn/problems/partition-list/) ## 题目大意 **描述**: 给定一个链表的头节点 $head$ 和一个特定值 $x$。 **要求**: 对链表进行分隔,使得所有「小于 $x$ 的节点」都出现在「大于或等于 $x$ 的节点」之前。 **说明**: - 链表中节点的数目在范围 [0, 200] 内。 - $-100 \le Node.val \le 100$。 - $-200 \le x \le 200$。 - 你应当保留两个分区中每个节点的初始相对位置。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/01/04/partition.jpg) ```python 输入:head = [1,4,3,2,5,2], x = 3 输出:[1,2,2,4,3,5] ``` - 示例 2: ```python 输入:head = [2,1], x = 2 输出:[1,2] ``` ## 解题思路 ### 思路 1:双指针 + 虚拟头节点 **核心思想**: 使用两个虚拟头节点分别构建小于x的链表和大于等于x的链表,然后遍历原链表,根据节点值的大小分别连接到对应的链表中,最后将两个链表合并。 **算法步骤**: 1. **创建虚拟头节点**:创建两个虚拟头节点 $small\_dummy$ 和 $large\_dummy$,分别用于构建小于x和大于等于 $x$ 的链表。 2. **创建指针**:为两个链表分别创建尾指针 $small\_tail$ 和 $large\_tail$ 3. **遍历原链表**:从头节点开始遍历原链表。 4. **分类连接**: - 如果当前节点值 < x,将其连接到 $small\_tail$ 后面,并更新 $small\_tail$。 - 如果当前节点值 >= x,将其连接到 $large\_tail$ 后面,并更新 $large\_tail$。 5. **合并链表**:将 $small\_tail$ 的 $next$ 指向 $large\_dummy.next$,将 $large\_tail$ 的 $next$ 设为 $None$。 6. **返回结果**:返回 $small\_dummy.next$。 **关键点**: - 使用虚拟头节点简化边界处理。 - 保持原链表中节点的相对位置。 - 最后需要将 $large\_tail.next$ 设为 $None$,避免形成环。 ### 思路 1:代码 ```python class Solution: def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]: # 创建两个虚拟头节点 small_dummy = ListNode(0) # 小于x的链表虚拟头 large_dummy = ListNode(0) # 大于等于x的链表虚拟头 # 创建两个尾指针 small_tail = small_dummy large_tail = large_dummy # 遍历原链表 current = head while current: if current.val < x: # 小于x的节点连接到small链表 small_tail.next = current small_tail = small_tail.next else: # 大于等于x的节点连接到large链表 large_tail.next = current large_tail = large_tail.next # 移动到下一个节点 current = current.next # 合并两个链表 small_tail.next = large_dummy.next # 连接两个链表 large_tail.next = None # 避免形成环 # 返回结果 return small_dummy.next ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是链表的长度。需要遍历链表一次,每个节点只访问一次。 - **空间复杂度**:$O(1)$,只使用了常数个额外的指针变量,没有使用额外的数据结构。 ================================================ FILE: docs/solutions/0001-0099/permutation-sequence.md ================================================ # [0060. 排列序列](https://leetcode.cn/problems/permutation-sequence/) - 标签:递归、数学 - 难度:困难 ## 题目链接 - [0060. 排列序列 - 力扣](https://leetcode.cn/problems/permutation-sequence/) ## 题目大意 **描述**: 给出集合 $[1,2,3,...,n]$,其所有元素共有 $n!$ 种排列。 按大小顺序列出所有排列情况,并一一标记,当 $n = 3$ 时, 所有排列如下: 1. `"123"` 2. `"132"` 3. `"213"` 4. `"231"` 5. `"312"` 6. `"321"` 给定 $n$ 和 $k$。 **要求**: 返回第 $k$ 个排列。 **说明**: - $1 \le n \le 9$。 - $1 <= k <= n!$。 **示例**: - 示例 1: ```python 输入:n = 3, k = 3 输出:"213" ``` - 示例 2: ```python 输入:n = 4, k = 9 输出:"2314" ``` ## 解题思路 ### 思路 1:数学计算 **核心思想**: 通过数学计算直接确定第 $k$ 个排列中每个位置上的数字,而不需要生成所有排列。 **算法步骤**: 1. **初始化**:创建候选数字列表 $candidates = [1, 2, 3, ..., n]$ 和结果字符串 $result$。 2. **计算阶乘**:预计算 $factorial[i] = i!$,用于快速计算每个位置可能的排列数。 3. **逐位确定**:对于第 $i$ 位(从 0 开始): - 计算剩余 $n-i$ 个数字的排列数:$factorial[n-i-1]$。 - 确定当前位应该选择第几个数字:$index = (k-1) // factorial[n-i-1]$。 - 将选中的数字添加到结果中,并从候选列表中移除。 - 更新 $k$:$k = k - index \times factorial[n-i-1]$。 4. **返回结果**:当所有位置都确定后,返回结果字符串。 **关键点**: - 使用 $k-1$ 是因为题目中的 $k$ 从 $1$ 开始,而数组索引从 $0$ 开始。 - 每次确定一位后,需要从候选列表中移除已使用的数字。 - 通过数学计算避免了生成所有排列,大大提高了效率。 **数学原理**: 对于 $n$ 个数字的排列,第 $i$ 位确定后,剩余 $n - i$ 个数字有 $(n - i)!$ 种排列方式。因此,第 $i$ 位选择第 $j$ 个候选数字时,会跳过 $j \times (n - i)!$ 个排列。 ### 思路 1:代码 ```python class Solution: def getPermutation(self, n: int, k: int) -> str: # 预计算阶乘数组 factorial = [1] * n for i in range(1, n): factorial[i] = factorial[i - 1] * i # 创建候选数字列表 candidates = [str(i) for i in range(1, n + 1)] result = [] k -= 1 # 转换为从 0 开始的索引 # 逐位确定每个位置的数字 for i in range(n): # 计算当前位应该选择第几个候选数字 index = k // factorial[n - i - 1] # 将选中的数字添加到结果中 result.append(candidates[index]) # 从候选列表中移除已使用的数字 candidates.pop(index) # 更新k k -= index * factorial[n - i - 1] return ''.join(result) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是数字的个数。需要遍历 $n$ 次,每次需要 $O(n)$ 时间从候选列表中移除元素,因此总时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n)$,需要 $O(n)$ 空间存储阶乘数组、候选数字列表和结果列表。 ================================================ FILE: docs/solutions/0001-0099/permutations-ii.md ================================================ # [0047. 全排列 II](https://leetcode.cn/problems/permutations-ii/) - 标签:数组、回溯 - 难度:中等 ## 题目链接 - [0047. 全排列 II - 力扣](https://leetcode.cn/problems/permutations-ii/) ## 题目大意 **描述**:给定一个可包含重复数字的序列 `nums`。 **要求**:按任意顺序返回所有不重复的全排列。 **说明**: - $1 \le nums.length \le 8$。 - $-10 \le nums[i] \le 10$。 **示例**: - 示例 1: ```python 输入:nums = [1,1,2] 输出:[[1,1,2],[1,2,1],[2,1,1]] ``` - 示例 2: ```python 输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] ``` ## 解题思路 ### 思路 1:回溯算法 这道题跟「[0046. 全排列](https://leetcode.cn/problems/permutations/)」不一样的地方在于增加了序列中的元素可重复这一条件。这就涉及到了如何去重。 我们可以先对数组 `nums` 进行排序,然后使用一个数组 `visited` 标记该元素在当前排列中是否被访问过。 如果未被访问过则将其加入排列中,并在访问后将该元素变为未访问状态。 然后再递归遍历下一层元素之前,增加一句语句进行判重:`if i > 0 and nums[i] == nums[i - 1] and not visited[i - 1]: continue`。 然后再进行回溯遍历。 ### 思路 1:代码 ```python class Solution: res = [] path = [] def backtrack(self, nums: List[int], visited: List[bool]): if len(self.path) == len(nums): self.res.append(self.path[:]) return for i in range(len(nums)): if i > 0 and nums[i] == nums[i - 1] and not visited[i - 1]: continue if not visited[i]: visited[i] = True self.path.append(nums[i]) self.backtrack(nums, visited) self.path.pop() visited[i] = False def permuteUnique(self, nums: List[int]) -> List[List[int]]: self.res.clear() self.path.clear() nums.sort() visited = [False for _ in range(len(nums))] self.backtrack(nums, visited) return self.res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times n!)$,其中 $n$ 为数组 `nums` 的元素个数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0001-0099/permutations.md ================================================ # [0046. 全排列](https://leetcode.cn/problems/permutations/) - 标签:数组、回溯 - 难度:中等 ## 题目链接 - [0046. 全排列 - 力扣](https://leetcode.cn/problems/permutations/) ## 题目大意 **描述**:给定一个不含重复数字的数组 `nums`。 **要求**:返回其有可能的全排列。 **说明**: - $1 \le nums.length \le 6$ - $-10 \le nums[i] \le 10$。 - `nums` 中的所有整数互不相同。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] ``` - 示例 2: ```python 输入:nums = [0,1] 输出:[[0,1],[1,0]] ``` ## 解题思路 ### 思路 1:回溯算法 根据回溯算法三步走,写出对应的回溯算法。 ![全排列回溯算法](https://qcdn.itcharge.cn/images/20220425102048.png) 1. **明确所有选择**:全排列中每个位置上的元素都可以从剩余可选元素中选出,对此画出决策树,如上图所示。 2. **明确终止条件**: - 当遍历到决策树的叶子节点时,就终止了。即当前路径搜索到末尾时,递归终止。 3. **将决策树和终止条件翻译成代码:** 1. 定义回溯函数: - `backtracking(nums):` 函数的传入参数是 `nums`(可选数组列表),全局变量是 `res`(存放所有符合条件结果的集合数组)和 `path`(存放当前符合条件的结果)。 - `backtracking(nums):` 函数代表的含义是:递归在 `nums` 中选择剩下的元素。 2. 书写回溯函数主体(给出选择元素、递归搜索、撤销选择部分)。 - 从当前正在考虑元素,到数组结束为止,枚举出所有可选的元素。对于每一个可选元素: - 约束条件:之前已经选择的元素不再重复选用,只能从剩余元素中选择。 - 选择元素:将其添加到当前子集数组 `path` 中。 - 递归搜索:在选择该元素的情况下,继续递归选择剩下元素。 - 撤销选择:将该元素从当前结果数组 `path` 中移除。 ```python for i in range(len(nums)): # 枚举可选元素列表 if nums[i] not in path: # 从当前路径中没有出现的数字中选择 path.append(nums[i]) # 选择元素 backtracking(nums) # 递归搜索 path.pop() # 撤销选择 ``` 3. 明确递归终止条件(给出递归终止条件,以及递归终止时的处理方法)。 - 当遍历到决策树的叶子节点时,就终止了。也就是存放当前结果的数组 `path` 的长度等于给定数组 `nums` 的长度(即 `len(path) == len(nums)`)时,递归停止。 ### 思路 1:代码 ```python class Solution: def permute(self, nums: List[int]) -> List[List[int]]: res = [] # 存放所有符合条件结果的集合 path = [] # 存放当前符合条件的结果 def backtracking(nums): # nums 为选择元素列表 if len(path) == len(nums): # 说明找到了一组符合条件的结果 res.append(path[:]) # 将当前符合条件的结果放入集合中 return for i in range(len(nums)): # 枚举可选元素列表 if nums[i] not in path: # 从当前路径中没有出现的数字中选择 path.append(nums[i]) # 选择元素 backtracking(nums) # 递归搜索 path.pop() # 撤销选择 backtracking(nums) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times n!)$,其中 $n$ 为数组 `nums` 的元素个数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0001-0099/plus-one.md ================================================ # [0066. 加一](https://leetcode.cn/problems/plus-one/) - 标签:数组、数学 - 难度:简单 ## 题目链接 - [0066. 加一 - 力扣](https://leetcode.cn/problems/plus-one/) ## 题目大意 **描述**:给定一个非负整数数组,数组每一位对应整数的一位数字。 **要求**:计算整数加 $1$ 后的结果。 **说明**: - $1 \le digits.length \le 100$。 - $0 \le digits[i] \le 9$。 **示例**: - 示例 1: ```python 输入:digits = [1,2,3] 输出:[1,2,4] 解释:输入数组表示数字 123,加 1 之后为 124。 ``` - 示例 2: ```python 输入:digits = [4,3,2,1] 输出:[4,3,2,2] 解释:输入数组表示数字 4321。 ``` ## 解题思路 ### 思路 1:模拟 这道题把整个数组看成了一个整数,然后个位数加 $1$。问题的实质是利用数组模拟加法运算。 如果个位数不为 $9$ 的话,直接把个位数加 $1$ 就好。如果个位数为 $9$ 的话,还要考虑进位。 具体步骤: 1. 数组前补 $0$ 位。 2. 将个位数字进行加 $1$ 计算。 3. 遍历数组 1. 如果该位数字大于等于 $10$,则向下一位进 $1$,继续下一位判断进位。 2. 如果该位数字小于 $10$,则跳出循环。 ### 思路 1:代码 ```python def plusOne(self, digits: List[int]) -> List[int]: digits = [0] + digits digits[len(digits) - 1] += 1 for i in range(len(digits)-1, 0, -1): if digits[i] != 10: break else: digits[i] = 0 digits[i - 1] += 1 if digits[0] == 0: return digits[1:] else: return digits ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$ 。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/powx-n.md ================================================ # [0050. Pow(x, n)](https://leetcode.cn/problems/powx-n/) - 标签:递归、数学 - 难度:中等 ## 题目链接 - [0050. Pow(x, n) - 力扣](https://leetcode.cn/problems/powx-n/) ## 题目大意 **描述**:给定浮点数 $x$ 和整数 $n$。 **要求**:计算 $x$ 的 $n$ 次方(即 $x^n$)。 **说明**: - $-100.0 < x < 100.0$。 - $-2^{31} \le n \le 2^{31} - 1$。 - $n$ 是一个整数。 - $-10^4 \le x^n \le 10^4$。 **示例**: - 示例 1: ```python 输入:x = 2.00000, n = 10 输出:1024.00000 ``` - 示例 2: ```python 输入:x = 2.00000, n = -2 输出:0.25000 解释:2-2 = 1/22 = 1/4 = 0.25 ``` ## 解题思路 ### 思路 1:分治算法 常规方法是直接将 $x$ 累乘 $n$ 次得出结果,时间复杂度为 $O(n)$。 我们可以利用分治算法来减少时间复杂度。 根据 $n$ 的奇偶性,我们可以得到以下结论: 1. 如果 $n$ 为偶数,$x^n = x^{n / 2} \times x^{n / 2}$。 2. 如果 $n$ 为奇数,$x^n = x \times x^{(n - 1) / 2} \times x^{(n - 1) / 2}$。 $x^{(n / 2)}$ 或 $x^{(n - 1) / 2}$ 又可以继续向下递归划分。 则我们可以利用低纬度的幂计算结果,来得到高纬度的幂计算结果。 这样递归求解,时间复杂度为 $O(\log n)$,并且递归也可以转为递推来做。 需要注意如果 $n$ 为负数,可以转换为 $\frac{1}{x} ^{(-n)}$。 ### 思路 1:代码 ```python class Solution: def myPow(self, x: float, n: int) -> float: if x == 0.0: return 0.0 res = 1 if n < 0: x = 1/x n = -n while n: if n & 1: res *= x x *= x n >>= 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/recover-binary-search-tree.md ================================================ # [0099. 恢复二叉搜索树](https://leetcode.cn/problems/recover-binary-search-tree/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [0099. 恢复二叉搜索树 - 力扣](https://leetcode.cn/problems/recover-binary-search-tree/) ## 题目大意 **描述**: 给你二叉搜索树的根节点 $root$,该树中恰好两个节点的值被错误地交换。 **要求**: 请在不改变其结构的情况下,恢复这棵树。 **说明**: - 树上节点的数目在范围 $[2, 1000]$ 内。 - $-2^{31} \le Node.val \le 2^{31} - 1$。 - 进阶:使用 $O(n)$ 空间复杂度的解法很容易实现。你能想出一个只使用 $O(1)$ 空间的解决方案吗? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/28/recover1.jpg) ```python 输入:root = [1,3,null,null,2] 输出:[3,1,null,null,2] 解释:3 不能是 1 的左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/10/28/recover2.jpg) ```python 输入:root = [3,1,4,null,null,2] 输出:[2,1,4,null,null,3] 解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 和 3 使二叉搜索树有效。 ``` ## 解题思路 ### 思路 1:中序遍历 + 数组存储 **核心思想**: 利用二叉搜索树中序遍历得到有序序列的性质,通过中序遍历找到被错误交换的两个节点,然后交换它们的值。 **算法步骤**: 1. **中序遍历**:对二叉搜索树进行中序遍历,将遍历结果存储在数组 $nodes$ 中。 2. **查找错误节点**:遍历数组 $nodes$,找到第一个 $nodes[i] > nodes[i+1]$ 的位置,此时 $nodes[i]$ 是第一个错误节点。 3. **查找第二个错误节点**:继续遍历,找到最后一个 $nodes[j] < nodes[j-1]$ 的位置,此时 $nodes[j]$ 是第二个错误节点。 4. **交换节点值**:交换这两个节点的值。 **关键点**: - 二叉搜索树的中序遍历结果应该是严格递增的序列。 - 当两个相邻节点被交换时,会出现两个逆序对:$(nodes[i], nodes[i+1])$ 和 $(nodes[j-1], nodes[j])$。 - 第一个错误节点是第一个逆序对中的较大值,第二个错误节点是最后一个逆序对中的较小值。 - 时间复杂度为 $O(n)$,空间复杂度为 $O(n)$。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def recoverTree(self, root: Optional[TreeNode]) -> None: """ Do not return anything, modify root in-place instead. """ # 存储中序遍历的节点值 nodes = [] def inorder_traversal(node): """中序遍历,将节点值存储到数组中""" if not node: return inorder_traversal(node.left) nodes.append(node.val) inorder_traversal(node.right) # 执行中序遍历 inorder_traversal(root) # 找到两个错误的位置 first_error = -1 # 第一个错误节点的索引 second_error = -1 # 第二个错误节点的索引 # 找到第一个错误位置:nodes[i] > nodes[i+1] for i in range(len(nodes) - 1): if nodes[i] > nodes[i + 1]: first_error = i break # 找到第二个错误位置:从第一个错误位置之后开始找 for i in range(first_error + 1, len(nodes) - 1): if nodes[i] > nodes[i + 1]: second_error = i + 1 break # 如果只找到一个错误位置,说明两个错误节点相邻 if second_error == -1: second_error = first_error + 1 # 存储需要交换的两个值 first_val = nodes[first_error] second_val = nodes[second_error] # 再次遍历树,交换这两个值 def swap_values(node): """在树中找到并交换两个错误的值""" if not node: return swap_values(node.left) if node.val == first_val: node.val = second_val elif node.val == second_val: node.val = first_val swap_values(node.right) # 执行值交换 swap_values(root) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树中节点的数量。需要执行两次完整的中序遍历:一次收集节点值,一次交换值。 - **空间复杂度**:$O(n)$,需要存储所有节点的值,以及递归调用栈的空间。 ================================================ FILE: docs/solutions/0001-0099/regular-expression-matching.md ================================================ # [0010. 正则表达式匹配](https://leetcode.cn/problems/regular-expression-matching/) - 标签:递归、字符串、动态规划 - 难度:困难 ## 题目链接 - [0010. 正则表达式匹配 - 力扣](https://leetcode.cn/problems/regular-expression-matching/) ## 题目大意 **描述**:给定一个字符串 `s` 和一个字符模式串 `p`。 **要求**:实现一个支持 `'.'` 和 `'*'` 的正则表达式匹配。两个字符串完全匹配才算匹配成功。如果匹配成功,则返回 `True`,否则返回 `False`。 - `'.'` 匹配任意单个字符。 - `'*'` 匹配零个或多个前面的那一个元素。 **说明**: - $1 \le s.length \le 20$。 - $1 \le p.length \le 30$。 - `s` 只包含从 `a` ~ `z` 的小写字母。 - `p` 只包含从 `a` ~ `z` 的小写字母,以及字符 `.` 和 `*`。 - 保证每次出现字符 `*` 时,前面都匹配到有效的字符。 **示例**: - 示例 1: ```python 输入:s = "aa", p = "a*" 输出:True 解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。 ``` - 示例 2: ```python 输入:s = "aa", p = "a" 输出:False 解释:"a" 无法匹配 "aa" 整个字符串。 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照两个字符串的结尾位置进行阶段划分。 ###### 2. 定义状态 定义状态 `dp[i][j]` 表示为:字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配。 ###### 3. 状态转移方程 - 如果 `s[i - 1] == p[j - 1]`,则字符串 `s` 的第 `i` 个字符与字符串 `p` 的第 `j` 个字符是匹配的。此时「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配」取决于「字符串 `s` 的前 `i - 1` 个字符与字符串 `p` 的前 `j - 1` 个字符是否匹配」。即 `dp[i][j] = dp[i - 1][j - 1] `。 - 如果 `p[j - 1] == '.'`,则字符串 `s` 的第 `i` 个字符与字符串 `p` 的第 `j` 个字符是匹配的(同上)。此时 `dp[i][j] = dp[i - 1][j - 1] `。 - 如果 `p[j - 1] == '*'`,则我们可以对字符 `p[j - 2]` 进行 `0` ~ 若干次数的匹配。 - 如果 `s[i - 1] != p[j - 2]` 并且 `p[j - 2] != '.'`,则说明当前星号匹配不上,只能匹配 `0` 次(即匹配空字符串),则「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配」取决于「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j - 2` 个字符是否匹配」,即 `dp[i][j] = dp[i][j - 2] `。 - 如果 `s[i - 1] == p[j - 2]` 或者 `p[j - 2] == '.'`,则说明当前星号前面的字符 `p[j - 2]` 可以匹配 `s[i - 1]`。 - 如果匹配 `0` 个,则「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配」取决于「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j - 2` 个字符是否匹配」。即 `dp[i][j] = dp[i][j - 2]`。 - 如果匹配 `1` 个,则「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配」取决于「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j - 1` 个字符是否匹配」。即 `dp[i][j] = dp[i][j - 1] `。 - 如果匹配多个,则「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配」取决于「字符串 `s` 的前 `i - 1` 个字符与字符串 `p` 的前 `j` 个字符是否匹配」。即 `dp[i][j] = dp[i - 1][j]`。 ###### 4. 初始条件 - 默认状态下,两个空字符串是匹配的,即 `dp[0][0] = True`。 - 当字符串 `s` 为空,字符串 `p` 右端有 `*` 时,想要匹配,则如果「空字符串」与「去掉字符串 `p` 右端的 `*` 和 `*` 之前的字符之后的字符串」匹配的话,则空字符串与字符串 `p` 匹配。也就是说如果 `p[j - 1] == '*'`,则 `dp[0][j] = dp[0][j - 2]`。 ###### 5. 最终结果 根据我们之前定义的状态, `dp[i][j]` 表示为:字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配。则最终结果为 `dp[size_s][size_p]`,其实 `size_s` 是字符串 `s` 的长度,`size_p` 是字符串 `p` 的长度。 ### 思路 1:动态规划代码 ```python class Solution: def isMatch(self, s: str, p: str) -> bool: size_s, size_p = len(s), len(p) dp = [[False for _ in range(size_p + 1)] for _ in range(size_s + 1)] dp[0][0] = True for j in range(1, size_p + 1): if p[j - 1] == '*': dp[0][j] = dp[0][j - 2] for i in range(1, size_s + 1): for j in range(1, size_p + 1): if s[i - 1] == p[j - 1] or p[j - 1] == '.': dp[i][j] = dp[i - 1][j - 1] elif p[j - 1] == '*': if s[i - 1] != p[j - 2] and p[j - 2] != '.': dp[i][j] = dp[i][j - 2] else: dp[i][j] = dp[i][j - 1] or dp[i][j - 2] or dp[i - 1][j] return dp[size_s][size_p] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m n)$,其中 $m$ 是字符串 `s` 的长度,$n$ 是字符串 `p` 的长度。使用了两重循环,外层循环遍历的时间复杂度是 $O(m)$,内层循环遍历的时间复杂度是 $O(n)$,所以总体的时间复杂度为 $O(m n)$。 - **空间复杂度**:$O(m n)$,其中 $m$ 是字符串 `s` 的长度,$n$ 是字符串 `p` 的长度。使用了二维数组保存状态,且第一维的空间复杂度为 $O(m)$,第二位的空间复杂度为 $O(n)$,所以总体的空间复杂度为 $O(m n)$。 ================================================ FILE: docs/solutions/0001-0099/remove-duplicates-from-sorted-array-ii.md ================================================ # [0080. 删除有序数组中的重复项 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/) - 标签:数组、双指针 - 难度:中等 ## 题目链接 - [0080. 删除有序数组中的重复项 II - 力扣](https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/) ## 题目大意 **描述**:给定一个有序数组 $nums$。 **要求**:在原数组空间基础上删除重复出现 $2$ 次以上的元素,并返回删除后数组的新长度。 **说明**: - $1 \le nums.length \le 3 * 10^4$。 - $-10^4 \le nums[i] \le 10^4$。 - $nums$ 已按升序排列。 **示例**: - 示例 1: ```python 输入:nums = [1,1,1,2,2,3] 输出:5, nums = [1,1,2,2,3] 解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。 不需要考虑数组中超出新长度后面的元素。 ``` - 示例 2: ```python 输入:nums = [0,0,1,1,1,1,2,3,3] 输出:7, nums = [0,0,1,1,2,3,3] 解释:函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3。 不需要考虑数组中超出新长度后面的元素。 ``` ## 解题思路 ### 思路 1:快慢指针 因为数组是有序的,所以重复元素必定是连续的。可以使用快慢指针来解决。具体做法如下: 1. 使用两个指针 $slow$,$fast$。$slow$ 指针指向即将放置元素的位置,$fast$ 指针指向当前待处理元素。 2. 本题要求相同元素最多出现 $2$ 次,并且 $slow - 2$ 是上上次放置了元素的位置。则应该检查 $nums[slow - 2]$ 和当前待处理元素 $nums[fast]$ 是否相同。 1. 如果 $nums[slow - 2] == nums[fast]$ 时,此时必有 $nums[slow - 2] == nums[slow - 1] == nums[fast]$,则当前 $nums[fast]$ 不保留,直接向右移动快指针 $fast$。 2. 如果 $nums[slow - 2] \ne nums[fast]$ 时,则保留 $nums[fast]$。将 $nums[fast]$ 赋值给 $nums[slow]$ ,同时将 $slow$ 右移。然后再向右移动快指针 $fast$。 3. 这样 $slow$ 指针左边均为处理好的数组元素,而从 $slow$ 指针指向的位置开始, $fast$ 指针左边都为舍弃的重复元素。 4. 遍历结束之后,此时 $slow$ 就是新数组的长度。 ### 思路 1:代码 ```python class Solution: def removeDuplicates(self, nums: List[int]) -> int: size = len(nums) if size <= 2: return size slow, fast = 2, 2 while (fast < size): if nums[slow - 2] != nums[fast]: nums[slow] = nums[fast] slow += 1 fast += 1 return slow ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/remove-duplicates-from-sorted-array.md ================================================ # [0026. 删除有序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/) - 标签:数组、双指针 - 难度:简单 ## 题目链接 - [0026. 删除有序数组中的重复项 - 力扣](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/) ## 题目大意 **描述**:给定一个有序数组 `nums`。 **要求**:删除数组 `nums` 中的重复元素,使每个元素只出现一次。并输出去除重复元素之后数组的长度。 **说明**: - 不能使用额外的数组空间,在原地修改数组,并在使用 $O(1)$ 额外空间的条件下完成。 **示例**: - 示例 1: ```python 输入:nums = [1,1,2] 输出:2, nums = [1,2,_] 解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。 ``` - 示例 2: ```python 输入:nums = [0,0,1,1,1,2,2,3,3,4] 输出:5, nums = [0,1,2,3,4] 解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。 ``` ## 解题思路 ### 思路 1:快慢指针 因为数组是有序的,那么重复的元素一定会相邻。 删除重复元素,实际上就是将不重复的元素移到数组左侧。考虑使用双指针。具体算法如下: 1. 定义两个快慢指针 `slow`,`fast`。其中 `slow` 指向去除重复元素后的数组的末尾位置。`fast` 指向当前元素。 2. 令 `slow` 在后, `fast` 在前。令 `slow = 0`,`fast = 1`。 3. 比较 `slow` 位置上元素值和 `fast` 位置上元素值是否相等。 - 如果不相等,则将 `slow` 后移一位,将 `fast` 指向位置的元素复制到 `slow` 位置上。 4. 将 `fast` 右移 `1` 位。 5. 重复上述 3 ~ 4 步,直到 `fast` 等于数组长度。 6. 返回 `slow + 1` 即为新数组长度。 ### 思路 1:代码 ```python class Solution: def removeDuplicates(self, nums: List[int]) -> int: if len(nums) <= 1: return len(nums) slow, fast = 0, 1 while (fast < len(nums)): if nums[slow] != nums[fast]: slow += 1 nums[slow] = nums[fast] fast += 1 return slow + 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/remove-duplicates-from-sorted-list-ii.md ================================================ # [0082. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/) - 标签:链表、双指针 - 难度:中等 ## 题目链接 - [0082. 删除排序链表中的重复元素 II - 力扣](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/) ## 题目大意 **描述**:给定一个已排序的链表的头 $head$。 **要求**:删除原始链表中所有重复数字的节点,只留下不同的数字。返回已排序的链表。 **说明**: - 链表中节点数目在范围 $[0, 300]$ 内。 - $-100 \le Node.val \le 100$。 - 题目数据保证链表已经按升序排列。 **示例**: - 示例 1: ```python 输入:head = [1,2,3,3,4,4,5] 输出:[1,2,5] ``` ## 解题思路 ### 思路 1:遍历 这道题的题意是需要保留所有不同数字,而重复出现的所有数字都要删除。因为给定的链表是升序排列的,所以我们要删除的重复元素在链表中的位置是连续的。所以我们可以对链表进行一次遍历,然后将连续的重复元素从链表中删除即可。具体步骤如下: - 先使用哑节点 $dummy\_head$ 构造一个指向 $head$ 的指针,使得可以防止从 $head$ 开始就是重复元素。 - 然后使用指针 $cur$ 表示链表中当前元素,从 $head$ 开始遍历。 - 当指针 $cur$ 的下一个元素和下下一个元素存在时: - 如果下一个元素值和下下一个元素值相同,则我们使用指针 $temp$ 保存下一个元素,并使用 $temp$ 向后遍历,跳过所有重复元素,然后令 $cur$ 的下一个元素指向 $temp$ 的下一个元素,继续向后遍历。 - 如果下一个元素值和下下一个元素值不同,则令 $cur$ 向右移动一位,继续向后遍历。 - 当指针 $cur$ 的下一个元素或者下下一个元素不存在时,说明已经遍历完,则返回哑节点 $dummy\_head$ 的下一个节点作为头节点。 ### 思路 1:代码 ```python # Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution: def deleteDuplicates(self, head: ListNode) -> ListNode: dummy_head = ListNode(-1) dummy_head.next = head cur = dummy_head while cur.next and cur.next.next: if cur.next.val == cur.next.next.val: temp = cur.next while temp and temp.next and temp.val == temp.next.val: temp = temp.next cur.next = temp.next else: cur = cur.next return dummy_head.next ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 为链表长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/remove-duplicates-from-sorted-list.md ================================================ # [0083. 删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/) - 标签:链表 - 难度:简单 ## 题目链接 - [0083. 删除排序链表中的重复元素 - 力扣](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/) ## 题目大意 **描述**:给定一个已排序的链表的头 $head$。 **要求**:删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。 **说明**: - 链表中节点数目在范围 $[0, 300]$ 内。 - $-100 \le Node.val \le 100$。 - 题目数据保证链表已经按升序排列。 **示例**: - 示例 1: ```python 输入:head = [1,1,2,3,3] 输出:[1,2,3] ``` ## 解题思路 ### 思路 1:遍历 - 使用指针 $curr$ 遍历链表,先将 $head$ 保存到 $curr$ 指针。 - 判断当前元素的值和当前元素下一个节点元素值是否相等。 - 如果相等,则让当前指针指向当前指针下两个节点。 - 否则,让 $curr$ 继续向后遍历。 - 遍历完之后返回头节点 $head$。 ### 思路 1:遍历代码 ```python class Solution: def deleteDuplicates(self, head: ListNode) -> ListNode: if head == None: return head curr = head while curr.next: if curr.val == curr.next.val: curr.next = curr.next.next else: curr = curr.next return head ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 为链表长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/remove-element.md ================================================ # [0027. 移除元素](https://leetcode.cn/problems/remove-element/) - 标签:数组、双指针 - 难度:简单 ## 题目链接 - [0027. 移除元素 - 力扣](https://leetcode.cn/problems/remove-element/) ## 题目大意 **描述**: 给定一个数组 $nums$,和一个值 $val$。 **要求**: 不使用额外数组空间,将数组中所有数值等于 $val$ 值的元素移除掉,并且返回新数组的长度。 **说明**: - $0 \le nums.length \le 100$。 - $0 \le nums[i] \le 50$。 - $0 \le val \le 100$。 **示例**: - 示例 1: ```python 输入:nums = [3,2,2,3], val = 3 输出:2, nums = [2,2] 解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。 ``` - 示例 2: ```python 输入:nums = [0,1,2,2,3,0,4,2], val = 2 输出:5, nums = [0,1,4,0,3] 解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。 ``` ## 解题思路 ### 思路 1:快慢指针 1. 使用两个指针 $slow$,$fast$。$slow$ 指向处理好的非 $val$ 值元素数组的尾部,$fast$ 指针指向当前待处理元素。 2. 不断向右移动 $fast$ 指针,每次移动到非 $val$ 值的元素,则将左右指针对应的数交换,交换同时将 $slow$ 右移。 3. 这样就将非 $val$ 值的元素进行前移,$slow$ 指针左边均为处理好的非 $val$ 值元素,而从 $slow$ 指针指向的位置开始,$fast$ 指针左边都为 $val$ 值。 4. 遍历结束之后,则所有 $val$ 值元素都移动到了右侧,且保持了非零数的相对位置。此时 $slow$ 就是新数组的长度。 ### 思路 1:代码 ```python class Solution: def removeElement(self, nums: List[int], val: int) -> int: slow = 0 fast = 0 while fast < len(nums): if nums[fast] != val: nums[slow], nums[fast] = nums[fast], nums[slow] # 此处也可以直接赋值,因为最终 nums[fast] 会被移除掉 # nums[slow] = nums[fast] slow += 1 fast += 1 return slow ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/remove-nth-node-from-end-of-list.md ================================================ # [0019. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) - 标签:链表、双指针 - 难度:中等 ## 题目链接 - [0019. 删除链表的倒数第 N 个结点 - 力扣](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) ## 题目大意 **描述**:给定一个链表的头节点 `head`。 **要求**:删除链表的倒数第 `n` 个节点,并且返回链表的头节点。 **说明**: - 要求使用一次遍历实现。 - 链表中结点的数目为 `sz`。 - $1 \le sz \le 30$。 - $0 \le Node.val \le 100$。 - $1 \le n \le sz$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/03/remove_ex1.jpg) ```python 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] ``` - 示例 2: ```python 输入:head = [1], n = 1 输出:[] ``` ## 解题思路 ### 思路 1:快慢指针 常规思路是遍历一遍链表,求出链表长度,再遍历一遍到对应位置,删除该位置上的节点。 如果用一次遍历实现的话,可以使用快慢指针。让快指针先走 `n` 步,然后快慢指针、慢指针再同时走,每次一步,这样等快指针遍历到链表尾部的时候,慢指针就刚好遍历到了倒数第 `n` 个节点位置。将该位置上的节点删除即可。 需要注意的是要删除的节点可能包含了头节点。我们可以考虑在遍历之前,新建一个头节点,让其指向原来的头节点。这样,最终如果删除的是头节点,则删除原头节点即可。返回结果的时候,可以直接返回新建头节点的下一位节点。 ### 思路 1:代码 ```python class Solution: def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode: newHead = ListNode(0, head) fast = head slow = newHead while n: fast = fast.next n -= 1 while fast: fast = fast.next slow = slow.next slow.next = slow.next.next return newHead.next ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/restore-ip-addresses.md ================================================ # [0093. 复原 IP 地址](https://leetcode.cn/problems/restore-ip-addresses/) - 标签:字符串、回溯 - 难度:中等 ## 题目链接 - [0093. 复原 IP 地址 - 力扣](https://leetcode.cn/problems/restore-ip-addresses/) ## 题目大意 **描述**:给定一个只包含数字的字符串 `s`,用来表示一个 IP 地址 **要求**:返回所有由 `s` 构成的有效 IP 地址,这些地址可以通过在 `s` 中插入 `'.'` 来形成。不能重新排序或删除 `s` 中的任何数字。可以按任何顺序返回答案。 **说明**: - **有效 IP 地址**:正好由四个整数(每个整数由 $0 \sim 255$ 的数构成,且不能含有前导 0),整数之间用 `.` 分割。 - $1 \le s.length \le 20$。 - `s` 仅由数字组成。 **示例**: - 示例 1: ```python 输入:s = "25525511135" 输出:["255.255.11.135","255.255.111.35"] ``` - 示例 2: ```python 输入:s = "0000" 输出:["0.0.0.0"] ``` ## 解题思路 ### 思路 1:回溯算法 一个有效 IP 地址由四个整数构成,中间用 $3$ 个点隔开。现在给定的是无分隔的整数字符串,我们可以通过在整数字符串中间的不同位置插入 $3$ 个点来生成不同的 IP 地址。这个过程可以通过回溯算法来生成。 根据回溯算法三步走,写出对应的回溯算法。 1. **明确所有选择**:全排列中每个位置上的元素都可以从剩余可选元素中选出。 2. **明确终止条件**: - 当遍历到决策树的叶子节点时,就终止了。即当前路径搜索到末尾时,递归终止。 3. **将决策树和终止条件翻译成代码**: 1. 定义回溯函数: - `backtracking(index):` 函数的传入参数是 `index`(剩余字符开始位置),全局变量是 `res`(存放所有符合条件结果的集合数组)和 `path`(存放当前符合条件的结果)。 - `backtracking(index):` 函数代表的含义是:递归从 `index` 位置开始,从剩下字符中,选择当前子段的值。 2. 书写回溯函数主体(给出选择元素、递归搜索、撤销选择部分)。 - 从当前正在考虑的字符,到字符串结束为止,枚举出所有可作为当前子段值的字符。对于每一个子段值: - 约束条件:只能从 `index` 位置开始选择,并且要符合规则要求。 - 选择元素:将其添加到当前子集数组 `path` 中。 - 递归搜索:在选择该子段值的情况下,继续递归从剩下字符中,选择下一个子段值。 - 撤销选择:将该子段值从当前结果数组 `path` 中移除。 ```python for i in range(index, len(s)): # 枚举可选元素列表 sub = s[index: i + 1] # 如果当前值不在 0 ~ 255 之间,直接跳过 if int(sub) > 255: continue # 如果当前值为 0,但不是单个 0("00..."),直接跳过 if int(sub) == 0 and i != index: continue # 如果当前值大于 0,但是以 0 开头("0XX..."),直接跳过 if int(sub) > 0 and s[index] == '0': continue path.append(sub) # 选择元素 backtracking(i + 1) # 递归搜索 path.pop() # 撤销选择 ``` 3. 明确递归终止条件(给出递归终止条件,以及递归终止时的处理方法)。 - 当遍历到决策树的叶子节点时,就终止了。也就是存放当前结果的数组 `path` 的长度等于 $4$,并且剩余字符开始位置为字符串结束位置(即 `len(path) == 4 and index == len(s)`)时,递归停止。 - 如果回溯过程中,切割次数大于 4(即 `len(path) > 4`),递归停止,直接返回。 ### 思路 1:代码 ```python class Solution: def restoreIpAddresses(self, s: str) -> List[str]: res = [] path = [] def backtracking(index): # 如果切割次数大于 4,直接返回 if len(path) > 4: return # 切割完成,将当前结果加入答案结果数组中 if len(path) == 4 and index == len(s): res.append('.'.join(path)) return for i in range(index, len(s)): sub = s[index: i + 1] # 如果当前值不在 0 ~ 255 之间,直接跳过 if int(sub) > 255: continue # 如果当前值为 0,但不是单个 0("00..."),直接跳过 if int(sub) == 0 and i != index: continue # 如果当前值大于 0,但是以 0 开头("0XX..."),直接跳过 if int(sub) > 0 and s[index] == '0': continue path.append(sub) backtracking(i + 1) path.pop() backtracking(0) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(3^4 \times |s|)$,其中 $|s|$ 是字符串 `s` 的长度。由于 IP 地址的每一子段位数不会超过 $3$,因此在递归时,我们最多只会深入到下一层中的 $3$ 种情况。而 IP 地址由 $4$ 个子段构成,所以递归的最大层数为 $4$ 层,则递归的时间复杂度为 $O(3^4)$。而每次将有效的 IP 地址添加到答案数组的时间复杂度为 $|s|$,所以总的时间复杂度为 $3^4 \times |s|$。 - **空间复杂度**:$O(|s|)$,只记录除了用来存储答案数组之外的空间复杂度。 ================================================ FILE: docs/solutions/0001-0099/reverse-integer.md ================================================ # [0007. 整数反转](https://leetcode.cn/problems/reverse-integer/) - 标签:数学 - 难度:中等 ## 题目链接 - [0007. 整数反转 - 力扣](https://leetcode.cn/problems/reverse-integer/) ## 题目大意 给定一个 32 位有符号整数 x,将 x 进行反转。 ## 解题思路 x 的范围为 $[-2^{31}, 2^{31}-1]$,即 $[-2147483648 ,2147483647]$。 反转的步骤就是让 x 不断对 10 取余,再除以 10,得到每一位的数字,同时累积结果。 注意累积结果的时候需要判断是否溢出。 当 ans * 10 + pop > INT_MAX ,或者 ans * 10 + pop < INT_MIN 时就会溢出。 按题设要求,无法在溢出之后对其判断。那么如何在进行累积操作之前判断溢出呢? ans * 10 + pop > INT_MAX 有两种情况: 1. ans > INT_MAX / 10,这种情况下,无论是否考虑 pop 进位都会溢出; 2. ans == INT_MAX / 10,这种情况下,考虑进位,如果 pop 大于 IN_MAX 的个位数,就会导致溢出。 同理 ans * 10 + pop < INT_MIN 也有两种情况: 1. ans < INT_MIN / 10 2. ans == INT_MIN / 10 且 pop < INT_MIN 的个位数,就会导致溢出 ## 代码 ```python class Solution: def reverse(self, x: int) -> int: INT_MAX_10 = (1<<31)//10 INT_MIN_10 = int((-1<<31)/10) INT_MAX_LAST = (1<<31) % 10 INT_MIN_LAST = (-1<<31) % -10 ans = 0 while x: pop = x % 10 if x > 0 else x % -10 x = x // 10 if x > 0 else int(x / 10) if ans > INT_MAX_10 or (ans == INT_MAX_10 and pop > INT_MAX_LAST): return 0 if ans < INT_MIN_10 or (ans == INT_MIN_10 and pop < INT_MIN_LAST): return 0 ans = ans*10+pop return ans ``` ================================================ FILE: docs/solutions/0001-0099/reverse-linked-list-ii.md ================================================ # [0092. 反转链表 II ](https://leetcode.cn/problems/reverse-linked-list-ii/) - 标签:链表 - 难度:中等 ## 题目链接 - [0092. 反转链表 II - 力扣](https://leetcode.cn/problems/reverse-linked-list-ii/) ## 题目大意 **描述**:给定单链表的头指针 `head` 和两个整数 `left` 和 `right` ,其中 `left <= right`。 **要求**:反转从位置 `left` 到位置 `right` 的链表节点,返回反转后的链表 。 **说明**: - 链表中节点数目为 `n`。 - $1 \le n \le 500$。 - $-500 \le Node.val \le 500$。 - $1 \le left \le right \le n$。 **示例**: - 示例 1: ```python 输入:head = [1,2,3,4,5], left = 2, right = 4 输出:[1,4,3,2,5] ``` ## 解题思路 在「[0206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/)」中我们可以通过迭代、递归两种方法将整个链表反转。这道题而这道题要求对链表的部分区间进行反转。我们同样可以通过迭代、递归两种方法将链表的部分区间进行反转。 ### 思路 1:迭代 我们可以先遍历到需要反转的链表区间的前一个节点,然后对需要反转的链表区间进行迭代反转。最后再返回头节点即可。 但是需要注意一点,如果需要反转的区间包含了链表的第一个节点,那么我们可以事先创建一个哑节点作为链表初始位置开始遍历,这样就能避免找不到需要反转的链表区间的前一个节点。 这道题的具体解题步骤如下: 1. 先使用哑节点 `dummy_head` 构造一个指向 `head` 的指针,使得可以从 `head` 开始遍历。使用 `index` 记录当前元素的序号。 2. 我们使用一个指针 `reverse_start`,初始赋值为 `dummy_head`。然后向右逐步移动到需要反转的区间的前一个节点。 3. 然后再使用两个指针 `cur` 和 `pre` 进行迭代。`pre` 指向 `cur` 前一个节点位置,即 `pre` 指向需要反转节点的前一个节点,`cur` 指向需要反转的节点。初始时,`pre` 指向 `reverse_start`,`cur` 指向 `pre.next`。 4. 当当前节点 `cur` 不为空,且 `index` 在反转区间内时,将 `pre` 和 `cur` 的前后指针进行交换,指针更替顺序为: 1. 使用 `next` 指针保存当前节点 `cur` 的后一个节点,即 `next = cur.next`; 2. 断开当前节点 `cur` 的后一节点链接,将 `cur` 的 `next` 指针指向前一节点 `pre`,即 `cur.next = pre`; 3. `pre` 向前移动一步,移动到 `cur` 位置,即 `pre = cur`; 4. `cur` 向前移动一步,移动到之前 `next` 指针保存的位置,即 `cur = next`。 5. 然后令 `index` 加 `1`。 5. 继续执行第 `4` 步中的 `1`、`2`、`3`、`4`、`5` 步。 6. 最后等到 `cur` 遍历到链表末尾(即 `cur == None`)或者遍历到需要反转区间的末尾时(即 `index > right`) 时,将反转区间的头尾节点分别与之前保存的需要反转的区间的前一个节点 `reverse_start` 相连,即 `reverse_start.next.next = cur`,`reverse_start.next = pre`。 7. 最后返回新的头节点 `dummy_head.next`。 ### 思路 1:代码 ```python # Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution: def reverseBetween(self, head: ListNode, left: int, right: int) -> ListNode: index = 1 dummy_head = ListNode(0) dummy_head.next = head pre = dummy_head reverse_start = dummy_head while reverse_start.next and index < left: reverse_start = reverse_start.next index += 1 pre = reverse_start cur = pre.next while cur and index <= right: next = cur.next cur.next = pre pre = cur cur = next index += 1 reverse_start.next.next = cur reverse_start.next = pre return dummy_head.next ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是链表节点个数。 - **空间复杂度**:$O(1)$。 ### 思路 2:递归算法 #### 1. 翻转链表前 n 个节点 1. 当 `left == 1` 时,无论 `right` 等于多少,实际上都是将当前链表到 `right` 部分进行翻转,也就是将前 `right` 个节点进行翻转。 2. 我们可以先定义一个递归函数 `reverseN(self, head, n)`,含义为:将链表前第 $n$ 个节点位置进行翻转。 1. 然后从 `head.next` 的位置开始调用递归函数,即将 `head.next` 为头节点的链表的的前 $n - 1$ 个位置进行反转,并返回该链表的新头节点 `new_head`。 2. 然后改变 `head`(原先头节点)和 `new_head`(新头节点)之间的指向关系,即将 `head` 指向的节点作为 `head` 下一个节点的下一个节点。 3. 先保存 `head.next` 的 `next` 指针,也就是新链表前 $n$ 个节点的尾指针,即 `last = head.next.next`。 4. 将 `head.next` 的`next` 指针先指向当前节点 `head`,即 `head.next.next = head `。 5. 然后让当前节点 `head` 的 `next` 指针指向 `last`,则完成了前 $n - 1$ 个位置的翻转。 3. 递归终止条件:当 `n == 1` 时,相当于翻转第一个节点,直接返回 `head` 即可。 4. #### 翻转链表 `[left, right]` 上的节点。 接下来我们来翻转区间上的节点。 1. 定义递归函数 `reverseBetween(self, head, left, right)` 为 2. ### 思路 2:代码 ```python class Solution: def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]: if left == 1: return self.reverseN(head, right) head.next = self.reverseBetween(head.next, left - 1, right - 1) return head def reverseN(self, head, n): if n == 1: return head last = self.reverseN(head.next, n - 1) next = head.next.next head.next.next = head head.next = next return last ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。最多需要 $n$ 层栈空间。 ## 参考资料 - 【题解】[动画图解:翻转链表的指定区间 - 反转链表 II - 力扣](https://leetcode.cn/problems/reverse-linked-list-ii/solution/dong-hua-tu-jie-fan-zhuan-lian-biao-de-z-n4px/) - 【题解】[【宫水三叶】一个能应用所有「链表」题里的「哨兵」技巧 - 反转链表 II - 力扣](https://leetcode.cn/problems/reverse-linked-list-ii/solution/yi-ge-neng-ying-yong-suo-you-lian-biao-t-vjx6/) ================================================ FILE: docs/solutions/0001-0099/reverse-nodes-in-k-group.md ================================================ # [0025. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/) - 标签:递归、链表 - 难度:困难 ## 题目链接 - [0025. K 个一组翻转链表 - 力扣](https://leetcode.cn/problems/reverse-nodes-in-k-group/) ## 题目大意 **描述**:给你链表的头节点 `head` ,再给定一个正整数 `k`,`k` 的值小于或等于链表的长度。 **要求**:每 `k` 个节点一组进行翻转,并返回修改后的链表。如果链表节点总数不是 `k` 的整数倍,则将最后剩余的节点保持原有顺序。 **说明**: - 不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。 - 假设链表中的节点数目为 `n`。 - $1 \le k \le n \le 5000$。 - $0 \le Node.val \le 1000$。 - 要求设计一个只用 `O(1)` 额外内存空间的算法解决此问题。 **示例**: - 示例 1: ```python 输入:head = [1,2,3,4,5], k = 2 输出:[2,1,4,3,5] ``` ## 解题思路 ### 思路 1:迭代 在「[0206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/)」中我们可以通过迭代、递归两种方法将整个链表反转。而这道题要求以 `k` 为单位,对链表的区间进行反转。而区间反转其实就是「[0092. 反转链表 II ](https://leetcode.cn/problems/reverse-linked-list-ii/)」这道题的题目要求。 本题中,我们可以以 `k` 为单位对链表进行切分,然后分别对每个区间部分进行反转。最后再返回头节点即可。 但是需要注意一点,如果需要反转的区间包含了链表的第一个节点,那么我们可以事先创建一个哑节点作为链表初始位置开始遍历,这样就能避免找不到需要反转的链表区间的前一个节点。 这道题的具体解题步骤如下: 1. 先使用哑节点 `dummy_head` 构造一个指向 `head` 的指针,避免找不到需要反转的链表区间的前一个节点。使用变量 `index` 记录当前元素的序号。 2. 使用两个指针 `cur`、`tail` 分别表示链表中待反转区间的首尾节点。初始 `cur` 赋值为 `dummy_head`,`tail` 赋值为 `dummy_head.next`,也就是 `head`。 3. 将 `tail` 向右移动,每移动一步,就领 `index` 加 `1`。 1. 当 `index % k != 0` 时,直接将 `tail` 向右移动,直到移动到当前待反转区间的结尾位置。 2. 当 `index % k == 0` 时,说明 `tail` 已经移动到了当前待反转区间的结尾位置,此时调用 `cur = self.reverse(cur, tail.next)` ,将待反转区间进行反转,并返回反转后区间的起始节点赋值给当前反转区间的首节点 `cur`。然后将 `tail` 移动到 `cur` 的下一个节点。 4. 最后返回新的头节点 `dummy_head.next`。 关于 `def reverse(self, head, tail):` 方法这里也说下具体步骤: 1. `head` 代表当前待反转区间的第一个节点的前一个节点,`tail` 代表当前待反转区间的最后一个节点的后一个节点。 2. 先用 `first` 保存一下待反转区间的第一个节点(反转之后为区间的尾节点),方便反转之后进行连接。 3. 我们使用两个指针 `cur` 和 `pre` 进行迭代。`pre` 指向 `cur` 前一个节点位置,即 `pre` 指向需要反转节点的前一个节点,`cur` 指向需要反转的节点。初始时,`pre` 指向待反转区间的第一个节点的前一个节点 `head`,`cur` 指向待反转区间的第一个节点,即 `pre.next`。 4. 当当前节点 `cur` 不等于 `tail` 时,将 `pre` 和 `cur` 的前后指针进行交换,指针更替顺序为: 1. 使用 `next` 指针保存当前节点 `cur` 的后一个节点,即 `next = cur.next`; 2. 断开当前节点 `cur` 的后一节点链接,将 `cur` 的 `next` 指针指向前一节点 `pre`,即 `cur.next = pre`; 3. `pre` 向前移动一步,移动到 `cur` 位置,即 `pre = cur`; 4. `cur` 向前移动一步,移动到之前 `next` 指针保存的位置,即 `cur = next`。 5. 继续执行第 `4` 步中的 `1`、`2`、`3`、`4`步。 6. 最后等到 `cur` 遍历到链表末尾(即 `cur == tail`)时,令「当前待反转区间的第一个节点的前一个节点」指向「反转区间后的头节点」 ,即 `head.next = pre`。令「待反转区间的第一个节点(反转之后为区间的尾节点)」指向「待反转分区间的最后一个节点的后一个节点」,即 `first.next = tail`。 7. 最后返回新的头节点 `dummy_head.next`。 ### 思路 1:代码 ```python # Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution: def reverse(self, head, tail): pre = head cur = pre.next first = cur while cur != tail: next = cur.next cur.next = pre pre = cur cur = next head.next = pre first.next = tail return first def reverseKGroup(self, head: ListNode, k: int) -> ListNode: dummy_head = ListNode(0) dummy_head.next = head cur = dummy_head tail = dummy_head.next index = 0 while tail: index += 1 if index % k == 0: cur = self.reverse(cur, tail.next) tail = cur.next else: tail = tail.next return dummy_head.next ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 为链表的总长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/roman-to-integer.md ================================================ # [0013. 罗马数字转整数](https://leetcode.cn/problems/roman-to-integer/) - 标签:哈希表、数学、字符串 - 难度:简单 ## 题目链接 - [0013. 罗马数字转整数 - 力扣](https://leetcode.cn/problems/roman-to-integer/) ## 题目大意 给定一个罗马数字对应的字符串,将其转换为整数。 罗马数字规则: - I 代表数值 1,V 代表数值 5,X 代表数值 10,L 代表数值 50,C 代表数值 100,D 代表数值 500,M 代表数值 1000; - 一般罗马数字较大数字在左边,较小数字在右边,此时值为两者之和,比如 XI = X + I = 10 + 1 = 11。 - 例外情况下,较小数字在左边,较大数字在右边,此时值为后者减前者之差,比如 IX = X - I = 10 - 1 = 9。 ## 解题思路 用一个哈希表存储罗马数字与对应数值关系。遍历罗马数字对应的字符串,判断相邻两个数大小关系,并计算对应结果。 ## 代码 ```python class Solution: def romanToInt(self, s: str) -> int: nunbers = { "I" : 1, "V" : 5, "X" : 10, "L" : 50, "C" : 100, "D" : 500, "M" : 1000 } sum = 0 pre_num = nunbers[s[0]] for i in range(1, len(s)): cur_num = nunbers[s[i]] if pre_num < cur_num: sum -= pre_num else: sum += pre_num pre_num = cur_num sum += pre_num return sum ``` ================================================ FILE: docs/solutions/0001-0099/rotate-image.md ================================================ # [0048. 旋转图像](https://leetcode.cn/problems/rotate-image/) - 标签:数组、数学、矩阵 - 难度:中等 ## 题目链接 - [0048. 旋转图像 - 力扣](https://leetcode.cn/problems/rotate-image/) ## 题目大意 **描述**:给定一个 $n \times n$ 大小的二维矩阵(代表图像)$matrix$。 **要求**:将二维矩阵 $matrix$ 顺时针旋转 90°。 **说明**: - 不能使用额外的数组空间。 - $n == matrix.length == matrix[i].length$。 - $1 \le n \le 20$。 - $-1000 \le matrix[i][j] \le 1000$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/08/28/mat1.jpg) ```python 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[[7,4,1],[8,5,2],[9,6,3]] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/08/28/mat2.jpg) ```python 输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]] 输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] ``` ## 解题思路 ### 思路 1:原地旋转 如果使用额外数组空间的话,将对应元素存放到对应位置即可。如果不使用额外的数组空间,则需要观察每一个位置上的点最初位置和最终位置有什么规律。 对于矩阵中第 $i$ 行的第 $j$ 个元素,在旋转后,它出现在倒数第 $i$ 列的第 $j$ 个位置。即 $matrixnew[j][n − i − 1] = matrix[i][j]$。 而 $matrixnew[j][n - i - 1]$ 的点经过旋转移动到了 $matrix[n − i − 1][n − j − 1]$ 的位置。 $matrix[n − i − 1][n − j − 1]$ 位置上的点经过旋转移动到了 $matrix[n − j − 1][i]$ 的位置。 $matrix[n− j − 1][i]$ 位置上的点经过旋转移动到了最初的 $matrix[i][j]$ 的位置。 这样就形成了一个循环,我们只需要通过一个临时变量 $temp$ 就可以将循环中的元素逐一进行交换。Python 中则可以直接使用语法直接交换。 ### 思路 1:代码 ```python class Solution: def rotate(self, matrix: List[List[int]]) -> None: n = len(matrix) for i in range(n // 2): for j in range((n + 1) // 2): matrix[i][j], matrix[n - j - 1][i], matrix[n - i - 1][n - j - 1], matrix[j][n - i - 1] = matrix[n - j - 1][i], matrix[n - i - 1][n - j - 1], matrix[j][n - i - 1], matrix[i][j] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:原地翻转 通过观察可以得出:原矩阵可以通过一次「水平翻转」+「主对角线翻转」得到旋转后的二维矩阵。 ### 思路 2:代码 ```python def rotate(self, matrix: List[List[int]]) -> None: n = len(matrix) for i in range(n // 2): for j in range(n): matrix[i][j], matrix[n - i - 1][j] = matrix[n - i - 1][j], matrix[i][j] for i in range(n): for j in range(i): matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/rotate-list.md ================================================ # [0061. 旋转链表](https://leetcode.cn/problems/rotate-list/) - 标签:链表、双指针 - 难度:中等 ## 题目链接 - [0061. 旋转链表 - 力扣](https://leetcode.cn/problems/rotate-list/) ## 题目大意 给定一个链表和整数 k,将链表每个节点向右移动 k 个位置。 ## 解题思路 我们可以将链表先连成环,然后将链表在指定位置断开。 先遍历一遍,求出链表节点个数 n。注意到 k 可能很大,我们只需将链表右移 k % n 个位置即可。 第二次遍历到 n - k % n 的位置,记录下断开后新链表头节点位置,再将其断开并返回新的头节点。 ## 代码 ```python class Solution: def rotateRight(self, head: ListNode, k: int) -> ListNode: if k == 0 or not head or not head.next: return head curr = head count = 1 while curr.next: count += 1 curr = curr.next cut = count - k % count curr.next = head while cut: curr = curr.next cut -= 1 newHead = curr.next curr.next = None return newHead ``` ================================================ FILE: docs/solutions/0001-0099/scramble-string.md ================================================ # [0087. 扰乱字符串](https://leetcode.cn/problems/scramble-string/) - 标签:字符串、动态规划 - 难度:困难 ## 题目链接 - [0087. 扰乱字符串 - 力扣](https://leetcode.cn/problems/scramble-string/) ## 题目大意 **描述**: 使用下面描述的算法可以扰乱字符串 $s$ 得到字符串 $t$: 1. 如果字符串的长度为 $1$,算法停止。 2. 如果字符串的长度 > $1$,执行下述步骤: - 在一个随机下标处将字符串分割成两个非空的子字符串。即,如果已知字符串 $s$ ,则可以将其分成两个子字符串 $x$ 和 $y$,且满足 $s = x + y$。 - 随机决定是要「交换两个子字符串」还是要「保持这两个子字符串的顺序不变」。即,在执行这一步骤之后,$s$ 可能是 $s = x + y$ 或者 $s = y + x$。 - 在 $x$ 和 $y$ 这两个子字符串上继续从步骤 $1$ 开始递归执行此算法。 给定两个长度相等的字符串 $s1$ 和 $s2$。 **要求**: 判断 $s2$ 是否是 $s1$ 的扰乱字符串。如果是,返回 $true$;否则,返回 $false$。 **说明**: - $s1.length == s2.length$。 - $1 \le s1.length \le 30$。 - $s1$ 和 $s2$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s1 = "great", s2 = "rgeat" 输出:true 解释:s1 上可能发生的一种情形是: "great" --> "gr/eat" // 在一个随机下标处分割得到两个子字符串 "gr/eat" --> "gr/eat" // 随机决定:「保持这两个子字符串的顺序不变」 "gr/eat" --> "g/r / e/at" // 在子字符串上递归执行此算法。两个子字符串分别在随机下标处进行一轮分割 "g/r / e/at" --> "r/g / e/at" // 随机决定:第一组「交换两个子字符串」,第二组「保持这两个子字符串的顺序不变」 "r/g / e/at" --> "r/g / e/ a/t" // 继续递归执行此算法,将 "at" 分割得到 "a/t" "r/g / e/ a/t" --> "r/g / e/ a/t" // 随机决定:「保持这两个子字符串的顺序不变」 算法终止,结果字符串和 s2 相同,都是 "rgeat" 这是一种能够扰乱 s1 得到 s2 的情形,可以认为 s2 是 s1 的扰乱字符串,返回 true ``` - 示例 2: ```python 输入:s1 = "abcde", s2 = "caebd" 输出:false ``` ## 解题思路 ### 思路 1:动态规划 **核心思想**: 使用三维动态规划来解决扰乱字符串问题。定义 $dp[i][j][k]$ 表示 $s1$ 从位置 $i$ 开始长度为 $k$ 的子串,是否可以通过扰乱操作得到 $s2$ 从位置 $j$ 开始长度为 $k$ 的子串。 **算法步骤**: 1. **状态定义**:$dp[i][j][k]$ 表示 $s1[i:i+k]$ 是否可以通过扰乱得到 $s2[j:j+k]$。 2. **边界条件**:当 $k = 1$ 时,$dp[i][j][1] = (s1[i] == s2[j])$。 3. **状态转移**:对于长度 $k$ 的子串,尝试所有可能的分割点 $l$($1 \le l < k$): - **不交换情况**:$dp[i][j][l] \land dp[i+l][j+l][k-l]$ - **交换情况**:$dp[i][j+k-l][l] \land dp[i+l][j][k-l]$ 4. **最终结果**:$dp[0][0][n]$ 即为答案。 **关键点**: - 需要枚举所有可能的分割点 $l$,其中 $l$ 表示左子串的长度 - 对于每个分割点,考虑交换和不交换两种情况 - 使用记忆化搜索或自底向上的动态规划来避免重复计算 ### 思路 1:代码 ```python class Solution: def isScramble(self, s1: str, s2: str) -> bool: """ 判断 s2 是否是 s1 的扰乱字符串 使用动态规划方法 """ n = len(s1) if n != len(s2): return False # 三维 DP 数组:dp[i][j][k] 表示 s1[i:i+k] 是否可以通过扰乱得到 s2[j:j+k] dp = [[[False] * (n + 1) for _ in range(n)] for _ in range(n)] # 边界条件:长度为 1 的子串 for i in range(n): for j in range(n): dp[i][j][1] = (s1[i] == s2[j]) # 动态规划:枚举所有可能的长度和分割点 for k in range(2, n + 1): # 子串长度从 2 到 n for i in range(n - k + 1): # s1 的起始位置 for j in range(n - k + 1): # s2 的起始位置 # 尝试所有可能的分割点 l (1 <= l < k) for l in range(1, k): # 情况 1:不交换,左子串对应左子串,右子串对应右子串 if dp[i][j][l] and dp[i + l][j + l][k - l]: dp[i][j][k] = True break # 情况 2:交换,左子串对应右子串,右子串对应左子串 if dp[i][j + k - l][l] and dp[i + l][j][k - l]: dp[i][j][k] = True break return dp[0][0][n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^4)$,其中 $n$ 是字符串长度。需要四重循环:长度 $k$、起始位置 $i$ 和 $j$、分割点 $l$,每重循环最多 $n$ 次。 - **空间复杂度**:$O(n^3)$,需要 $O(n^3)$ 空间存储三维 DP 数组。 ================================================ FILE: docs/solutions/0001-0099/search-a-2d-matrix.md ================================================ # [0074. 搜索二维矩阵](https://leetcode.cn/problems/search-a-2d-matrix/) - 标签:数组、二分查找、矩阵 - 难度:中等 ## 题目链接 - [0074. 搜索二维矩阵 - 力扣](https://leetcode.cn/problems/search-a-2d-matrix/) ## 题目大意 **描述**:给定一个 $m \times n$ 大小的有序二维矩阵 $matrix$。矩阵中每行元素从左到右升序排列,每列元素从上到下升序排列。再给定一个目标值 $target$。 **要求**:判断矩阵中是否存在目标值 $target$。 **说明**: - $m == matrix.length$。 - $n == matrix[i].length$。 - $1 \le m, n \le 100$。 - $-10^4 \le matrix[i][j], target \le 10^4$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/05/mat.jpg) ```python 输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3 输出:True ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/11/25/mat2.jpg) ```python 输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13 输出:False ``` ## 解题思路 ### 思路 1:二分查找 二维矩阵是有序的,可以考虑使用二分搜索来进行查找。 1. 首先二分查找遍历对角线元素,假设对角线元素的坐标为 $(row, col)$。把数组元素按对角线分为右上角部分和左下角部分。 2. 然后对于当前对角线元素右侧第 $row$ 行、对角线元素下侧第 $col$ 列进行二分查找。 1. 如果找到目标,直接返回 `True`。 2. 如果找不到目标,则缩小范围,继续查找。 3. 直到所有对角线元素都遍历完,依旧没找到,则返回 `False`。 ### 思路 1:代码 ```python class Solution: # 二分查找对角线元素 def diagonalBinarySearch(self, matrix, diagonal, target): left = 0 right = diagonal while left < right: mid = left + (right - left) // 2 if matrix[mid][mid] < target: left = mid + 1 else: right = mid return left def rowBinarySearch(self, matrix, begin, cols, target): left = begin right = cols while left < right: mid = left + (right - left) // 2 if matrix[begin][mid] < target: left = mid + 1 elif matrix[begin][mid] > target: right = mid - 1 else: left = mid break return begin <= left <= cols and matrix[begin][left] == target def colBinarySearch(self, matrix, begin, rows, target): left = begin + 1 right = rows while left < right: mid = left + (right - left) // 2 if matrix[mid][begin] < target: left = mid + 1 elif matrix[mid][begin] > target: right = mid - 1 else: left = mid break return begin <= left <= rows and matrix[left][begin] == target def searchMatrix(self, matrix: List[List[int]], target: int) -> bool: rows = len(matrix) if rows == 0: return False cols = len(matrix[0]) if cols == 0: return False min_val = min(rows, cols) index = self.diagonalBinarySearch(matrix, min_val - 1, target) if matrix[index][index] == target: return True for i in range(index + 1): row_search = self.rowBinarySearch(matrix, i, cols - 1, target) col_search = self.colBinarySearch(matrix, i, rows - 1, target) if row_search or col_search: return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log m + \log n)$,其中 $m$、$n$ 分别是矩阵的行数和列数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/search-in-rotated-sorted-array-ii.md ================================================ # [0081. 搜索旋转排序数组 II](https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [0081. 搜索旋转排序数组 II - 力扣](https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/) ## 题目大意 **描述**:一个按照升序排列的整数数组 $nums$,在位置的某个下标 $k$ 处进行了旋转操作。(例如:$[0, 1, 2, 5, 6, 8]$ 可能变为 $[5, 6, 8, 0, 1, 2]$)。 现在给定旋转后的数组 $nums$ 和一个整数 $target$。 **要求**:编写一个函数来判断给定的 $target$ 是否存在与数组中。如果存在则返回 `True`,否则返回 `False`。 **说明**: - $1 \le nums.length \le 5000$。 - $-10^4 \le nums[i] \le 10^4$。 - 题目数据保证 $nums$ 在预先未知的某个下标上进行了旋转。 - $-10^4 \le target \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [2,5,6,0,0,1,2], target = 0 输出:true ``` - 示例 2: ```python 输入:nums = [2,5,6,0,0,1,2], target = 3 输出:false ``` ## 解题思路 ### 思路 1:二分查找 这道题算是「[0033. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/)」的变形,只不过输出变为了判断。 原本为升序排列的数组 nums 经过「旋转」之后,会有两种情况,第一种就是原先的升序序列,另一种是两段升序的序列。 ``` * * * * * * ``` ``` * * * * * * ``` 最直接的办法就是遍历一遍,找到目标值 target。但是还可以有更好的方法。考虑用二分查找来降低算法的时间复杂度。 我们将旋转后的数组看成左右两个升序部分:左半部分和右半部分。 有人会说第一种情况不是只有一个部分吗?其实我们可以把第一种情况中的整个数组看做是左半部分,然后右半部分为空数组。 然后创建两个指针 $left$、$right$,分别指向数组首尾。让后计算出两个指针中间值 $mid$。将 $mid$ 与两个指针做比较,并考虑与 $target$ 的关系。 - 如果 $nums[mid] > nums[left]$,则 $mid$ 在左半部分(因为右半部分值都比 $nums[left]$ 小)。 - 如果 $nums[mid] \ge target$,并且 $target \ge nums[left]$,则 $target$ 在左半部分,并且在 $mid$ 左侧,此时应将 $right$ 左移到 $mid - 1$ 位置。 - 否则如果 $nums[mid] < target$,则 $target$ 在左半部分,并且在 $mid$ 右侧,此时应将 $left$ 右移到 $mid + 1$。 - 否则如果 $nums[left] > target$,则 $target$ 在右半部分,应将 $left$ 移动到 $mid + 1$ 位置。 - 如果 $nums[mid] < nums[left]$,则 $mid$ 在右半部分(因为右半部分值都比 $nums[left]$ 小)。 - 如果 $nums[mid] < target$,并且 $target \le nums[right]$,则 $target$ 在右半部分,并且在 $mid$ 右侧,此时应将 $left$ 右移到 $mid + 1$ 位置。 - 否则如果 $nums[mid] \ge target$,则 $target$ 在右半部分,并且在 $mid$ 左侧,此时应将 $right$ 左移到 $mid - 1$ 位置。 - 否则如果 $nums[right] < target$,则 $target$ 在左半部分,应将 $right$ 左移到 $mid - 1$ 位置。 - 最终判断 $nums[left]$ 是否等于 $target$,如果等于,则返回 `True`,否则返回 `False`。 ### 思路 1:代码 ```python class Solution: def search(self, nums: List[int], target: int) -> bool: n = len(nums) if n == 0: return False left = 0 right = len(nums) - 1 while left < right: mid = left + (right - left) // 2 if nums[mid] > nums[left]: if nums[left] <= target and target <= nums[mid]: right = mid else: left = mid + 1 elif nums[mid] < nums[left]: if nums[mid] < target and target <= nums[right]: left = mid + 1 else: right = mid else: if nums[mid] == target: return True else: left = left + 1 return nums[left] == target ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $nums$ 的长度。最坏情况下数组元素均相等且不为 $target$,我们需要访问所有位置才能得出结果。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/search-in-rotated-sorted-array.md ================================================ # [0033. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [0033. 搜索旋转排序数组 - 力扣](https://leetcode.cn/problems/search-in-rotated-sorted-array/) ## 题目大意 **描述**:给定一个整数数组 $nums$,数组中值互不相同。给定的 $nums$ 是经过升序排列后的又进行了「旋转」操作的。再给定一个整数 $target$。 **要求**:从 $nums$ 中找到 $target$ 所在位置,如果找到,则返回对应下标,找不到则返回 $-1$。 **说明**: - 旋转操作:升序排列的数组 nums 在预先未知的第 k 个位置进行了右移操作,变成了 $[nums[k]], nums[k+1], ... , nums[n-1], ... , nums[0], nums[1], ... , nums[k-1]$。 **示例**: - 示例 1: ```python 输入:nums = [4,5,6,7,0,1,2], target = 0 输出:4 ``` - 示例 2: ```python 输入:nums = [4,5,6,7,0,1,2], target = 3 输出:-1 ``` ## 解题思路 ### 思路 1:二分查找 原本为升序排列的数组 $nums$ 经过「旋转」之后,会有两种情况,第一种就是原先的升序序列,另一种是两段升序的序列。 ```python * * * * * * ``` ```python * * * * * * ``` 最直接的办法就是遍历一遍,找到目标值 $target$。但是还可以有更好的方法。考虑用二分查找来降低算法的时间复杂度。 我们将旋转后的数组看成左右两个升序部分:左半部分和右半部分。 有人会说第一种情况不是只有一个部分吗?其实我们可以把第一种情况中的整个数组看做是左半部分,然后右半部分为空数组。 然后创建两个指针 $left$、$right$,分别指向数组首尾。让后计算出两个指针中间值 $mid$。将 $mid$ 与两个指针做比较,并考虑与 $target$ 的关系。 - 如果 $nums[mid] == target$,说明找到了 $target$,直接返回下标。 - 如果 $nums[mid] \ge nums[left]$,则 $mid$ 在左半部分(因为右半部分值都比 $nums[left]$ 小)。 - 如果 $nums[mid] \ge target$,并且 $target \ge nums[left]$,则 $target$ 在左半部分,并且在 $mid$ 左侧,此时应将 $right$ 左移到 $mid - 1$ 位置。 - 否则如果 $nums[mid] \le target$,则 $target$ 在左半部分,并且在 $mid$ 右侧,此时应将 $left$ 右移到 $mid + 1$ 位置。 - 否则如果 $nums[left] > target$,则 $target$ 在右半部分,应将 $left$ 移动到 $mid + 1$ 位置。 - 如果 $nums[mid] < nums[left]$,则 $mid$ 在右半部分(因为右半部分值都比 $nums[left]$ 小)。 - 如果 $nums[mid] < target$,并且 $target \le nums[right]$,则 $target$ 在右半部分,并且在 $mid$ 右侧,此时应将 $left$ 右移到 $mid + 1$ 位置。 - 否则如果 $nums[mid] \ge target$,则 $target$ 在右半部分,并且在 $mid$ 左侧,此时应将 $right$ 左移到 $mid - 1$ 位置。 - 否则如果 $nums[right] < target$,则 $target$ 在左半部分,应将 $right$ 左移到 $mid - 1$ 位置。 ### 思路 1:代码 ```python class Solution: def search(self, nums: List[int], target: int) -> int: left = 0 right = len(nums) - 1 while left <= right: mid = left + (right - left) // 2 if nums[mid] == target: return mid if nums[mid] >= nums[left]: if nums[mid] > target and target >= nums[left]: right = mid - 1 else: left = mid + 1 else: if nums[mid] < target and target <= nums[right]: left = mid + 1 else: right = mid - 1 return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。二分查找算法的时间复杂度为 $O(\log n)$。 - **空间复杂度**:$O(1)$。只用到了常数空间存放若干变量。 ================================================ FILE: docs/solutions/0001-0099/search-insert-position.md ================================================ # [0035. 搜索插入位置](https://leetcode.cn/problems/search-insert-position/) - 标签:数组、二分查找 - 难度:简单 ## 题目链接 - [0035. 搜索插入位置 - 力扣](https://leetcode.cn/problems/search-insert-position/) ## 题目大意 **描述**:给定一个排好序的数组 $nums$,以及一个目标值 $target$。 **要求**:在数组中找到目标值,并返回下标。如果找不到,则返回目标值按顺序插入数组的位置。 **说明**: - $1 \le nums.length \le 10^4$。 - $-10^4 \le nums[i] \le 10^4$。 - $nums$ 为无重复元素的升序排列数组。 - $-10^4 \le target \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [1,3,5,6], target = 5 输出:2 ``` ## 解题思路 ### 思路 1:二分查找 设定左右节点为数组两端,即 `left = 0`,`right = len(nums) - 1`,代表待查找区间为 $[left, right]$(左闭右闭)。 取两个节点中心位置 $mid$,先比较中心位置值 $nums[mid]$ 与目标值 $target$ 的大小。 - 如果 $target == nums[mid]$,则当前中心位置为待插入数组的位置。 - 如果 $target > nums[mid]$,则将左节点设置为 $mid + 1$,然后继续在右区间 $[mid + 1, right]$ 搜索。 - 如果 $target < nums[mid]$,则将右节点设置为 $mid - 1$,然后继续在左区间 $[left, mid - 1]$ 搜索。 直到查找到目标值返回待插入数组的位置,或者等到 $left > right$ 时停止查找,此时 $left$ 所在位置就是待插入数组的位置。 ### 思路 1:二分查找代码 ```python class Solution: def searchInsert(self, nums: List[int], target: int) -> int: size = len(nums) left, right = 0, size - 1 while left <= right: mid = left + (right - left) // 2 if nums[mid] == target: return mid elif nums[mid] < target: left = mid + 1 else: right = mid - 1 return left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。二分查找算法的时间复杂度为 $O(\log n)$。 - **空间复杂度**:$O(1)$。只用到了常数空间存放若干变量。 ================================================ FILE: docs/solutions/0001-0099/set-matrix-zeroes.md ================================================ # [0073. 矩阵置零](https://leetcode.cn/problems/set-matrix-zeroes/) - 标签:数组、哈希表、矩阵 - 难度:中等 ## 题目链接 - [0073. 矩阵置零 - 力扣](https://leetcode.cn/problems/set-matrix-zeroes/) ## 题目大意 **描述**:给定一个 $m \times n$ 大小的矩阵 $matrix$。 **要求**:如果一个元素为 $0$,则将其所在行和列所有元素都置为 $0$。 **说明**: - 请使用「原地」算法。 - $m == matrix.length$。 - $n == matrix[0].length$。 - $1 \le m, n \le 200$。 - $-2^{31} \le matrix[i][j] \le 2^{31} - 1$。 - **进阶**: - 一个直观的解决方案是使用 $O(m \times n)$ 的额外空间,但这并不是一个好的解决方案。 - 一个简单的改进方案是使用 $O(m + n)$ 的额外空间,但这仍然不是最好的解决方案。 - 你能想出一个仅使用常量空间的解决方案吗? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/08/17/mat1.jpg) ```python 输入:matrix = [[1,1,1],[1,0,1],[1,1,1]] 输出:[[1,0,1],[0,0,0],[1,0,1]] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/08/17/mat2.jpg) ``` 输入:matrix = [[1,1,1],[1,0,1],[1,1,1]] 输出:[[1,0,1],[0,0,0],[1,0,1]] ``` ## 解题思路 ### 思路 1:使用标记变量 直观上可以使用两个数组来标记行和列出现 $0$ 的情况,但这样空间复杂度就是 $O(m+n)$ 了,不符合题意。 考虑使用数组原本的元素进行记录出现 $0$ 的情况。 1. 设定两个变量 $flag\_row0$、$flag\_col0$ 来标记第一行、第一列是否出现了 $0$。 2. 接下来我们使用数组第一行、第一列来标记 $0$ 的情况。 3. 对数组除第一行、第一列之外的每个元素进行遍历,如果某个元素出现 $0$ 了,则使用数组的第一行、第一列对应位置来存储 $0$ 的标记。 4. 再对数组除第一行、第一列之外的每个元素进行遍历,通过对第一行、第一列的标记 $0$ 情况,进行置为 $0$ 的操作。 5. 最后再根据 $flag\_row0$、$flag\_col0$ 的标记情况,对第一行、第一列进行置为 $0$ 的操作。 ### 思路 1:代码 ```python class Solution: def setZeroes(self, matrix: List[List[int]]) -> None: m = len(matrix) n = len(matrix[0]) flag_col0 = False flag_row0 = False for i in range(m): if matrix[i][0] == 0: flag_col0 = True break for j in range(n): if matrix[0][j] == 0: flag_row0 = True break for i in range(1, m): for j in range(1, n): if matrix[i][j] == 0: matrix[i][0] = matrix[0][j] = 0 for i in range(1, m): for j in range(1, n): if matrix[i][0] == 0 or matrix[0][j] == 0: matrix[i][j] = 0 if flag_col0: for i in range(m): matrix[i][0] = 0 if flag_row0: for j in range(n): matrix[0][j] = 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/simplify-path.md ================================================ # [0071. 简化路径](https://leetcode.cn/problems/simplify-path/) - 标签:栈、字符串 - 难度:中等 ## 题目链接 - [0071. 简化路径 - 力扣](https://leetcode.cn/problems/simplify-path/) ## 题目大意 **描述**: 在 Unix 风格的文件系统中规则如下: - 一个点 `'.'` 表示当前目录本身。 - 此外,两个点 `'..'` 表示将目录切换到上一级(指向父目录)。 - 任意多个连续的斜杠(即,`'//'` 或 `'///'`)都被视为单个斜杠 `'/'`。 - 任何其他格式的点(例如,`'...'` 或 `'....'`)均被视为有效的文件 / 目录名称。 给定一个字符串 $path$,表示指向某一文件或目录的 Unix 风格绝对路径 (以 `'/'` 开头), **要求**: 请你将其转化为更加简洁的规范路径。返回简化后得到的规范路径。 **说明**: - $1 \le path.length \le 3000$。 - path 由英文字母,数字,`'.'`,`'/'` 或 `'_'` 组成。 - path 是一个有效的 Unix 风格绝对路径。 - 返回的 简化路径 必须遵循下述格式: - 始终以斜杠 `'/'` 开头。 - 两个目录名之间必须只有一个斜杠 `'/'`。 - 最后一个目录名(如果存在)不能 以 `'/'` 结尾。 - 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 `'.'` 或 `'..'`)。 **示例**: - 示例 1: ```python 输入:path = "/home/" 输出:"/home" 解释:应删除尾随斜杠。 ``` - 示例 2: ```python 输入:path = "/home//foo/" 输出:"/home/foo" 解释:多个连续的斜杠被单个斜杠替换。 ``` ## 解题思路 ### 思路 1:栈 **核心思想**: 使用栈来模拟路径的层级结构,根据不同的路径组件进行相应的栈操作,最后将栈中的元素组合成简化后的路径。 **算法步骤**: 1. **分割路径**:将输入路径 $path$ 按照 `'/'` 分割成路径组件数组 $components$。 2. **遍历组件**:遍历每个路径组件 $component$: - 如果 $component$ 为空字符串或 `'.'`,跳过(表示当前目录)。 - 如果 $component$ 为 `'..'`,弹出栈顶元素(表示返回上一级目录)。 - 否则,将 $component$ 压入栈中(表示进入下一级目录)。 3. **构建结果**:将栈中的元素用 `'/'` 连接,并在前面加上 `'/'` 作为根目录。 **关键点**: - 使用栈来维护当前路径的层级结构。 - 遇到 `'..'` 时弹出栈顶,遇到有效目录名时压入栈中。 - 最终结果以 `'/'` 开头,表示绝对路径。 ### 思路 1:代码 ```python class Solution: def simplifyPath(self, path: str) -> str: """ 简化 Unix 风格的绝对路径 """ # 使用栈来存储路径组件 stack = [] # 按照 '/' 分割路径 components = path.split('/') # 遍历每个路径组件 for component in components: if component == '' or component == '.': # 空字符串或 '.' 表示当前目录,跳过 continue elif component == '..': # '..' 表示返回上一级目录,弹出栈顶元素 if stack: stack.pop() else: # 有效的目录名,压入栈中 stack.append(component) # 构建简化后的路径 # 如果栈为空,返回根目录 '/' if not stack: return '/' # 将栈中的组件用 '/' 连接,并在前面加上 '/' return '/' + '/'.join(stack) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是路径的长度。需要遍历路径一次,每个字符最多被处理一次。 - **空间复杂度**:$O(n)$,其中 $n$ 是路径的长度。栈最多存储 $O(n)$ 个路径组件。 ================================================ FILE: docs/solutions/0001-0099/sort-colors.md ================================================ # [0075. 颜色分类](https://leetcode.cn/problems/sort-colors/) - 标签:数组、双指针、排序 - 难度:中等 ## 题目链接 - [0075. 颜色分类 - 力扣](https://leetcode.cn/problems/sort-colors/) ## 题目大意 **描述**:给定一个数组 $nums$,元素值只有 $0$、$1$、$2$,分别代表红色、白色、蓝色。 **要求**:将数组进行排序,使得红色在前,白色在中间,蓝色在最后。 **说明**: - 要求不使用标准库函数,同时仅用常数空间,一趟扫描解决。 - $n == nums.length$。 - $1 \le n \le 300$。 - $nums[i]$ 为 $0$、$1$ 或 $2$。 **示例**: - 示例 1: ```python 输入:nums = [2,0,2,1,1,0] 输出:[0,0,1,1,2,2] ``` - 示例 2: ```python 输入:nums = [2,0,1] 输出:[0,1,2] ``` ## 解题思路 ### 思路 1:双指针 + 快速排序思想 快速排序算法中的 $partition$ 过程,利用双指针,将序列中比基准数 $pivot$ 大的元素移动到了基准数右侧,将比基准数 $pivot$ 小的元素移动到了基准数左侧。从而将序列分为了三部分:比基准数小的部分、基准数、比基准数大的部分。 这道题我们也可以借鉴快速排序算法中的 $partition$ 过程,将 $1$ 作为基准数 $pivot$,然后将序列分为三部分:$0$(即比 $1$ 小的部分)、等于 $1$ 的部分、$2$(即比 $1$ 大的部分)。具体步骤如下: 1. 使用两个指针 $left$、$right$,分别指向数组的头尾。$left$ 表示当前处理好红色元素的尾部,$right$ 表示当前处理好蓝色的头部。 2. 再使用一个下标 $index$ 遍历数组: - 如果遇到 $nums[index] == 0$,就交换 $nums[index]$ 和 $nums[left]$,同时将 $left$ 和 $index$ 都右移(因为交换后 $nums[index]$ 可能是从 $left$ 位置换过来的,需要继续检查)。 - 如果遇到 $nums[index] == 2$,就交换 $nums[index]$ 和 $nums[right]$,同时将 $right$ 左移(注意 $index$ 不增加,因为交换后 $nums[index]$ 可能是从 $right$ 位置换过来的 0 或 1,需要再次检查)。 - 如果遇到 $nums[index] == 1$,直接将 $index$ 右移。 3. 直到 $index$ 移动到 $right$ 位置之后,停止遍历。遍历结束之后,此时 $left$ 左侧都是红色,$right$ 右侧都是蓝色。 ### 思路 1:代码 ```python class Solution: def sortColors(self, nums: List[int]) -> None: left = 0 right = len(nums) - 1 index = 0 while index <= right: if nums[index] == 0: nums[index], nums[left] = nums[left], nums[index] left += 1 index += 1 elif nums[index] == 2: nums[index], nums[right] = nums[right], nums[index] right -= 1 else: index += 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/spiral-matrix-ii.md ================================================ # [0059. 螺旋矩阵 II](https://leetcode.cn/problems/spiral-matrix-ii/) - 标签:数组、矩阵、模拟 - 难度:中等 ## 题目链接 - [0059. 螺旋矩阵 II - 力扣](https://leetcode.cn/problems/spiral-matrix-ii/) ## 题目大意 给你一个正整数 $n$。 要求:生成一个包含 $1 \sim n^2$ 的所有元素,且元素按顺时针顺序螺旋排列的 $n \times n$ 正方形矩阵 $matrix$。 ## 解题思路 ### 思路 1:模拟 这道题跟「[54. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/)」思路是一样的。 1. 构建一个 $n \times n$ 大小的数组 $matrix$ 存储答案。然后定义一下上、下、左、右的边界。 2. 然后按照顺时针的顺序从边界上依次给数组 $matrix$ 相应位置赋值。 3. 当访问完当前边界之后,要更新一下边界位置,缩小范围,方便下一轮进行访问。 4. 最后返回 $matrix$。 ### 思路 1:代码 ```python class Solution: def generateMatrix(self, n: int) -> List[List[int]]: matrix = [[0 for _ in range(n)] for _ in range(n)] up, down, left, right = 0, len(matrix) - 1, 0, len(matrix[0]) - 1 index = 1 while True: for i in range(left, right + 1): matrix[up][i] = index index += 1 up += 1 if up > down: break for i in range(up, down + 1): matrix[i][right] = index index += 1 right -= 1 if right < left: break for i in range(right, left - 1, -1): matrix[down][i] = index index += 1 down -= 1 if down < up: break for i in range(down, up - 1, -1): matrix[i][left] = index index += 1 left += 1 if left > right: break return matrix ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/0001-0099/spiral-matrix.md ================================================ # [0054. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/) - 标签:数组、矩阵、模拟 - 难度:中等 ## 题目链接 - [0054. 螺旋矩阵 - 力扣](https://leetcode.cn/problems/spiral-matrix/) ## 题目大意 **描述**:给定一个 $m \times n$ 大小的二维矩阵 $matrix$。 **要求**:按照顺时针旋转的顺序,返回矩阵中的所有元素。 **说明**: - $m == matrix.length$。 - $n == matrix[i].length$。 - $1 \le m, n \le 10$。 - $-100 \le matrix[i][j] \le 100$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/13/spiral1.jpg) ```python 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2,3,6,9,8,7,4,5] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/11/13/spiral.jpg) ```python 输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] 输出:[1,2,3,4,8,12,11,10,9,5,6,7] ``` ## 解题思路 ### 思路 1:模拟 1. 使用数组 $ans$ 存储答案。然后定义一下上、下、左、右的边界。 2. 然后按照顺时针的顺序从边界上依次访问元素。 3. 当访问完当前边界之后,要更新一下边界位置,缩小范围,方便下一轮进行访问。 4. 最后返回答案数组 $ans$。 ### 思路 1:代码 ```python class Solution: def spiralOrder(self, matrix: List[List[int]]) -> List[int]: up, down, left, right = 0, len(matrix)-1, 0, len(matrix[0])-1 ans = [] while True: for i in range(left, right + 1): ans.append(matrix[up][i]) up += 1 if up > down: break for i in range(up, down + 1): ans.append(matrix[i][right]) right -= 1 if right < left: break for i in range(right, left - 1, -1): ans.append(matrix[down][i]) down -= 1 if down < up: break for i in range(down, up - 1, -1): ans.append(matrix[i][left]) left += 1 if left > right: break return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。其中 $m$、$n$ 分别为二维矩阵的行数和列数。 - **空间复杂度**:$O(m \times n)$。如果算上答案数组的空间占用,则空间复杂度为 $O(m \times n)$。不算上则空间复杂度为 $O(1)$。 ================================================ FILE: docs/solutions/0001-0099/sqrtx.md ================================================ # [0069. x 的平方根](https://leetcode.cn/problems/sqrtx/) - 标签:数学、二分查找 - 难度:简单 ## 题目链接 - [0069. x 的平方根 - 力扣](https://leetcode.cn/problems/sqrtx/) ## 题目大意 **要求**:实现 `int sqrt(int x)` 函数。计算并返回 $x$ 的平方根(只保留整数部分),其中 $x$ 是非负整数。 **说明**: - $0 \le x \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:x = 4 输出:2 ``` - 示例 2: ```python 输入:x = 8 输出:2 解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。 ``` ## 解题思路 ### 思路 1:二分查找 因为求解的是 $x$ 开方的整数部分。所以我们可以从 $0 \sim x$ 的范围进行遍历,找到 $k^2 \le x$ 的最大结果。 为了减少算法的时间复杂度,我们使用二分查找的方法来搜索答案。 ### 思路 1:代码 ```python class Solution: def mySqrt(self, x: int) -> int: left = 0 right = x ans = -1 while left <= right: mid = (left + right) // 2 if mid * mid <= x: ans = mid left = mid + 1 else: right = mid - 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。二分查找算法的时间复杂度为 $O(\log n)$。 - **空间复杂度**:$O(1)$。只用到了常数空间存放若干变量。 ================================================ FILE: docs/solutions/0001-0099/string-to-integer-atoi.md ================================================ # [0008. 字符串转换整数 (atoi)](https://leetcode.cn/problems/string-to-integer-atoi/) - 标签:字符串 - 难度:中等 ## 题目链接 - [0008. 字符串转换整数 (atoi) - 力扣](https://leetcode.cn/problems/string-to-integer-atoi/) ## 题目大意 **描述**:给定一个字符串 `s`。 **要求**:实现一个 `myAtoi(s)` 函数。使其能换成一个 32 位有符号整数(类似 C / C++ 中的 `atoi` 函数)。需要检测有效性,无法读取返回 $0$。 **说明**: - 函数 `myAtoi(s)` 的算法如下: 1. 读入字符串并丢弃无用的前导空格。 2. 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。 3. 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。 4. 将前面步骤读入的这些数字转换为整数(即,`"123"` -> `123`, `"0032"` -> `32`)。如果没有读入数字,则整数为 `0` 。必要时更改符号(从步骤 2 开始)。 5. 如果整数数超过 32 位有符号整数范围 $[−2^{31}, 2^{31} − 1]$ ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 $−2^{31}$ 的整数应该被固定为 $−2^{31}$ ,大于 $2^{31} − 1$ 的整数应该被固定为 $2^{31} − 1$。 6. 返回整数作为最终结果。 - 本题中的空白字符只包括空格字符 `' '` 。 - 除前导空格或数字后的其余字符串外,请勿忽略任何其他字符。 - $0 \le s.length \le 200$。 - `s` 由英文字母(大写和小写)、数字(`0-9`)、`' '`、`'+'`、`'-'` 和 `'.'` 组成 **示例**: - 示例 1: ```python 输入:s = "42" 输出:42 解释:加粗的字符串为已经读入的字符,插入符号是当前读取的字符。 第 1 步:"42"(当前没有读入字符,因为没有前导空格) ^ 第 2 步:"42"(当前没有读入字符,因为这里不存在 '-' 或者 '+') ^ 第 3 步:"42"(读入 "42") ^ 解析得到整数 42 。 由于 "42" 在范围 [-231, 231 - 1] 内,最终结果为 42 。 ``` - 示例 2: ```python 输入:s = " -42" 输出:-42 解释: 第 1 步:" -42"(读入前导空格,但忽视掉) ^ 第 2 步:" -42"(读入 '-' 字符,所以结果应该是负数) ^ 第 3 步:" -42"(读入 "42") ^ 解析得到整数 -42 。 由于 "-42" 在范围 [-231, 231 - 1] 内,最终结果为 -42 。 ``` ## 解题思路 ### 思路 1:模拟 1. 先去除前后空格。 2. 检测正负号。 3. 读入数字,并用字符串存储数字结果。 4. 将数字字符串转为整数,并根据正负号转换整数结果。 5. 判断整数范围,并返回最终结果。 ### 思路 1:代码 ```python class Solution: def myAtoi(self, s: str) -> int: num_str = "" positive = True start = 0 s = s.lstrip() if not s: return 0 if s[0] == '-': positive = False start = 1 elif s[0] == '+': positive = True start = 1 elif not s[0].isdigit(): return 0 for i in range(start, len(s)): if s[i].isdigit(): num_str += s[i] else: break if not num_str: return 0 num = int(num_str) if not positive: num = -num return max(num, -2 ** 31) else: return min(num, 2 ** 31 - 1) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 `s` 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0001-0099/subsets-ii.md ================================================ # [0090. 子集 II](https://leetcode.cn/problems/subsets-ii/) - 标签:位运算、数组、回溯 - 难度:中等 ## 题目链接 - [0090. 子集 II - 力扣](https://leetcode.cn/problems/subsets-ii/) ## 题目大意 **描述**:给定一个整数数组 `nums`,其中可能包含重复元素。 **要求**:返回该数组所有可能的子集(幂集)。 **说明**: - 解集不能包含重复的子集。返回的解集中,子集可以按任意顺序排列。 - $1 \le nums.length \le 10$。 - $-10 \le nums[i] \le 10$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]] ``` ## 解题思路 ### 思路 1:回溯算法 数组的每个元素都有两个选择:选与不选。 我们可以通过向当前子集数组中添加可选元素来表示选择该元素。也可以在当前递归结束之后,将之前添加的元素从当前子集数组中移除(也就是回溯)来表示不选择该元素。 因为数组中可能包含重复元素,所以我们可以先将数组排序,然后在回溯时,判断当前元素是否和上一个元素相同,如果相同,则直接跳过,从而去除重复元素。 回溯算法解决这道题的步骤如下: - 先对数组 `nums` 进行排序。 - 从第 `0` 个位置开始,调用 `backtrack` 方法进行深度优先搜索。 - 将当前子集数组 `sub_set` 添加到答案数组 `sub_sets` 中。 - 然后从当前位置开始,到数组结束为止,枚举出所有可选的元素。对于每一个可选元素: - 如果当前元素与上一个元素相同,则跳过当前生成的子集。 - 将可选元素添加到当前子集数组 `sub_set` 中。 - 在选择该元素的情况下,继续递归考虑下一个元素。 - 进行回溯,撤销选择该元素。即从当前子集数组 `sub_set` 中移除之前添加的元素。 ### 思路 1:代码 ```python class Solution: def backtrack(self, nums, index, res, path): res.append(path[:]) for i in range(index, len(nums)): if i > index and nums[i] == nums[i - 1]: continue path.append(nums[i]) self.backtrack(nums, i + 1, res, path) path.pop() def subsetsWithDup(self, nums: List[int]) -> List[List[int]]: nums.sort() res, path = [], [] self.backtrack(nums, 0, res, path) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 2^n)$,其中 $n$ 指的是数组 `nums` 的元素个数,$2^n$ 指的是所有状态数。每种状态需要 $O(n)$ 的时间来构造子集。 - **空间复杂度**:$O(n)$,每种状态下构造子集需要使用 $O(n)$ 的空间。 ### 思路 2:二进制枚举 对于一个元素个数为 `n` 的集合 `nums` 来说,每一个位置上的元素都有选取和未选取两种状态。我们可以用数字 `1` 来表示选取该元素,用数字 `0` 来表示不选取该元素。 那么我们就可以用一个长度为 `n` 的二进制数来表示集合 `nums` 或者表示 `nums` 的子集。其中二进制的每一位数都对应了集合中某一个元素的选取状态。对于集合中第 `i` 个元素(`i` 从 `0` 开始编号)来说,二进制对应位置上的 `1` 代表该元素被选取,`0` 代表该元素未被选取。 举个例子来说明一下,比如长度为 `5` 的集合 `nums = {5, 4, 3, 2, 1}`,我们可以用一个长度为 `5` 的二进制数来表示该集合。 比如二进制数 `11111` 就表示选取集合的第 `0` 位、第 `1` 位、第 `2` 位、第 `3` 位、第 `4` 位元素,也就是集合 `{5, 4, 3, 2, 1}` ,即集合 `nums` 本身。如下表所示: | 集合 nums 对应位置(下标) | 4 | 3 | 2 | 1 | 0 | | :------------------------- | :--: | :--: | :--: | :--: | :--: | | 二进制数对应位数 | 1 | 1 | 1 | 1 | 1 | | 对应选取状态 | 选取 | 选取 | 选取 | 选取 | 选取 | 再比如二进制数 `10101` 就表示选取集合的第 `0` 位、第 `2` 位、第 `5` 位元素,也就是集合 `{5, 3, 1}`。如下表所示: | 集合 nums 对应位置(下标) | 4 | 3 | 2 | 1 | 0 | | :------------------------- | :--: | :----: | :--: | :----: | :--: | | 二进制数对应位数 | 1 | 0 | 1 | 0 | 1 | | 对应选取状态 | 选取 | 未选取 | 选取 | 未选取 | 选取 | 再比如二进制数 `01001` 就表示选取集合的第 `0` 位、第 `3` 位元素,也就是集合 `{5, 2}`。如下标所示: | 集合 nums 对应位置(下标) | 4 | 3 | 2 | 1 | 0 | | :------------------------- | :----: | :--: | :----: | :----: | :--: | | 二进制数对应位数 | 0 | 1 | 0 | 0 | 1 | | 对应选取状态 | 未选取 | 选取 | 未选取 | 未选取 | 选取 | 通过上面的例子我们可以得到启发:对于长度为 `5` 的集合 `nums` 来说,我们只需要从 `00000` ~ `11111` 枚举一次(对应十进制为 $0 \sim 2^4 - 1$)即可得到长度为 `5` 的集合 `S` 的所有子集。 我们将上面的例子拓展到长度为 `n` 的集合 `nums`。可以总结为: - 对于长度为 `5` 的集合 `nums` 来说,只需要枚举 $0 \sim 2^n - 1$(共 $2^n$ 种情况),即可得到所有的子集。 因为数组中可能包含重复元素,所以我们可以先对数组进行排序。然后在枚举过程中,如果发现当前元素和上一个元素相同,则直接跳过当前生层的子集,从而去除重复元素。 ### 思路 2:代码 ```python class Solution: def subsetsWithDup(self, nums: List[int]) -> List[List[int]]: nums.sort() n = len(nums) # n 为集合 nums 的元素个数 sub_sets = [] # sub_sets 用于保存所有子集 for i in range(1 << n): # 枚举 0 ~ 2^n - 1 sub_set = [] # sub_set 用于保存当前子集 flag = True # flag 用于判断重复元素 for j in range(n): # 枚举第 i 位元素 if i >> j & 1: # 如果第 i 为元素对应二进制位为 1,则表示选取该元素 if j > 0 and (i >> (j - 1) & 1) == 0 and nums[j] == nums[j - 1]: flag = False # 如果出现重复元素,则跳过当前生成的子集 break sub_set.append(nums[j]) # 将选取的元素加入到子集 sub_set 中 if flag: sub_sets.append(sub_set) # 将子集 sub_set 加入到所有子集数组 sub_sets 中 return sub_sets # 返回所有子集 ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times 2^n)$,其中 $n$ 指的是数组 `nums` 的元素个数,$2^n$ 指的是所有状态数。每种状态需要 $O(n)$ 的时间来构造子集。 - **空间复杂度**:$O(n)$,每种状态下构造子集需要使用 $O(n)$ 的空间。 ================================================ FILE: docs/solutions/0001-0099/subsets.md ================================================ # [0078. 子集](https://leetcode.cn/problems/subsets/) - 标签:位运算、数组、回溯 - 难度:中等 ## 题目链接 - [0078. 子集 - 力扣](https://leetcode.cn/problems/subsets/) ## 题目大意 **描述**:给定一个整数数组 `nums`,数组中的元素互不相同。 **要求**:返回该数组所有可能的不重复子集。可以按任意顺序返回解集。 **说明**: - $1 \le nums.length \le 10$。 - $-10 \le nums[i] \le 10$。 - `nums` 中的所有元素互不相同。 **示例**: - 示例 1: ```python 输入 nums = [1,2,3] 输出 [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]] ``` - 示例 2: ```python 输入:nums = [0] 输出:[[],[0]] ``` ## 解题思路 ### 思路 1:回溯算法 数组的每个元素都有两个选择:选与不选。 我们可以通过向当前子集数组中添加可选元素来表示选择该元素。也可以在当前递归结束之后,将之前添加的元素从当前子集数组中移除(也就是回溯)来表示不选择该元素。 下面我们根据回溯算法三步走,写出对应的回溯算法。 ![回溯算法](https://qcdn.itcharge.cn/images/20220425210640.png) 1. **明确所有选择**:根据数组中每个位置上的元素选与不选两种选择,画出决策树,如上图所示。 2. **明确终止条件**: - 当遍历到决策树的叶子节点时,就终止了。即当前路径搜索到末尾时,递归终止。 3. **将决策树和终止条件翻译成代码**: 1. 定义回溯函数: - `backtracking(nums, index):` 函数的传入参数是 `nums`(可选数组列表)和 `index`(代表当前正在考虑元素是 `nums[i]` ),全局变量是 `res`(存放所有符合条件结果的集合数组)和 `path`(存放当前符合条件的结果)。 - `backtracking(nums, index):` 函数代表的含义是:在选择 `nums[index]` 的情况下,递归选择剩下的元素。 2. 书写回溯函数主体(给出选择元素、递归搜索、撤销选择部分)。 - 从当前正在考虑元素,到数组结束为止,枚举出所有可选的元素。对于每一个可选元素: - 约束条件:之前选过的元素不再重复选用。每次从 `index` 位置开始遍历而不是从 `0` 位置开始遍历就是为了避免重复。集合跟全排列不一样,子集中 `{1, 2}` 和 `{2, 1}` 是等价的。为了避免重复,我们之前考虑过的元素,就不再重复考虑了。 - 选择元素:将其添加到当前子集数组 `path` 中。 - 递归搜索:在选择该元素的情况下,继续递归考虑下一个位置上的元素。 - 撤销选择:将该元素从当前子集数组 `path` 中移除。 ```python for i in range(index, len(nums)): # 枚举可选元素列表 path.append(nums[i]) # 选择元素 backtracking(nums, i + 1) # 递归搜索 path.pop() # 撤销选择 ``` 3. 明确递归终止条件(给出递归终止条件,以及递归终止时的处理方法)。 - 当遍历到决策树的叶子节点时,就终止了。也就是当正在考虑的元素位置到达数组末尾(即 `start >= len(nums)`)时,递归停止。 - 从决策树中也可以看出,子集需要存储的答案集合应该包含决策树上所有的节点,应该需要保存递归搜索的所有状态。所以无论是否达到终止条件,我们都应该将当前符合条件的结果放入到集合中。 ### 思路 1:代码 ```python class Solution: def subsets(self, nums: List[int]) -> List[List[int]]: res = [] # 存放所有符合条件结果的集合 path = [] # 存放当前符合条件的结果 def backtracking(nums, index): # 正在考虑可选元素列表中第 index 个元素 res.append(path[:]) # 将当前符合条件的结果放入集合中 if index >= len(nums): # 遇到终止条件(本题) return for i in range(index, len(nums)): # 枚举可选元素列表 path.append(nums[i]) # 选择元素 backtracking(nums, i + 1) # 递归搜索 path.pop() # 撤销选择 backtracking(nums, 0) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 2^n)$,其中 $n$ 指的是数组 `nums` 的元素个数,$2^n$ 指的是所有状态数。每种状态需要 $O(n)$ 的时间来构造子集。 - **空间复杂度**:$O(n)$,每种状态下构造子集需要使用 $O(n)$ 的空间。 ### 思路 2:二进制枚举 对于一个元素个数为 `n` 的集合 `nums` 来说,每一个位置上的元素都有选取和未选取两种状态。我们可以用数字 `1` 来表示选取该元素,用数字 `0` 来表示不选取该元素。 那么我们就可以用一个长度为 `n` 的二进制数来表示集合 `nums` 或者表示 `nums` 的子集。其中二进制的每一位数都对应了集合中某一个元素的选取状态。对于集合中第 `i` 个元素(`i` 从 `0` 开始编号)来说,二进制对应位置上的 `1` 代表该元素被选取,`0` 代表该元素未被选取。 举个例子来说明一下,比如长度为 `5` 的集合 `nums = {5, 4, 3, 2, 1}`,我们可以用一个长度为 `5` 的二进制数来表示该集合。 比如二进制数 `11111` 就表示选取集合的第 `0` 位、第 `1` 位、第 `2` 位、第 `3` 位、第 `4` 位元素,也就是集合 `{5, 4, 3, 2, 1}` ,即集合 `nums` 本身。如下表所示: | 集合 nums 对应位置(下标) | 4 | 3 | 2 | 1 | 0 | | :------------------------- | :--: | :--: | :--: | :--: | :--: | | 二进制数对应位数 | 1 | 1 | 1 | 1 | 1 | | 对应选取状态 | 选取 | 选取 | 选取 | 选取 | 选取 | 再比如二进制数 `10101` 就表示选取集合的第 `0` 位、第 `2` 位、第 `5` 位元素,也就是集合 `{5, 3, 1}`。如下表所示: | 集合 nums 对应位置(下标) | 4 | 3 | 2 | 1 | 0 | | :------------------------- | :--: | :----: | :--: | :----: | :--: | | 二进制数对应位数 | 1 | 0 | 1 | 0 | 1 | | 对应选取状态 | 选取 | 未选取 | 选取 | 未选取 | 选取 | 再比如二进制数 `01001` 就表示选取集合的第 `0` 位、第 `3` 位元素,也就是集合 `{5, 2}`。如下标所示: | 集合 nums 对应位置(下标) | 4 | 3 | 2 | 1 | 0 | | :------------------------- | :----: | :--: | :----: | :----: | :--: | | 二进制数对应位数 | 0 | 1 | 0 | 0 | 1 | | 对应选取状态 | 未选取 | 选取 | 未选取 | 未选取 | 选取 | 通过上面的例子我们可以得到启发:对于长度为 `5` 的集合 `nums` 来说,我们只需要从 `00000` ~ `11111` 枚举一次(对应十进制为 $0 \sim 2^4 - 1$)即可得到长度为 `5` 的集合 `S` 的所有子集。 我们将上面的例子拓展到长度为 `n` 的集合 `nums`。可以总结为: - 对于长度为 `5` 的集合 `nums` 来说,只需要枚举 $0 \sim 2^n - 1$(共 $2^n$ 种情况),即可得到所有的子集。 ### 思路 2:代码 ```python class Solution: def subsets(self, nums: List[int]) -> List[List[int]]: n = len(nums) # n 为集合 nums 的元素个数 sub_sets = [] # sub_sets 用于保存所有子集 for i in range(1 << n): # 枚举 0 ~ 2^n - 1 sub_set = [] # sub_set 用于保存当前子集 for j in range(n): # 枚举第 i 位元素 if i >> j & 1: # 如果第 i 为元素对应二进制位为 1,则表示选取该元素 sub_set.append(nums[j]) # 将选取的元素加入到子集 sub_set 中 sub_sets.append(sub_set) # 将子集 sub_set 加入到所有子集数组 sub_sets 中 return sub_sets # 返回所有子集 ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times 2^n)$,其中 $n$ 指的是数组 `nums` 的元素个数,$2^n$ 指的是所有状态数。每种状态需要 $O(n)$ 的时间来构造子集。 - **空间复杂度**:$O(n)$,每种状态下构造子集需要使用 $O(n)$ 的空间。 ================================================ FILE: docs/solutions/0001-0099/substring-with-concatenation-of-all-words.md ================================================ # [0030. 串联所有单词的子串](https://leetcode.cn/problems/substring-with-concatenation-of-all-words/) - 标签:哈希表、字符串、滑动窗口 - 难度:困难 ## 题目链接 - [0030. 串联所有单词的子串 - 力扣](https://leetcode.cn/problems/substring-with-concatenation-of-all-words/) ## 题目大意 **描述**: 给定一个字符串 $s$ 和一个字符串数组 $words$。$words$ 中所有字符串长度相同。 $s$ 中的「串联子串」是指一个包含 $words$ 中所有字符串以任意顺序排列连接起来的子串。 - 例如,如果 $words = ["ab","cd","ef"]$, 那么 `"abcdef"`, `"abefcd"`,`"cdabef"`, `"cdefab"`,`"efabcd"` 和 `"efcdab"` 都是串联子串。 `"acdbef"` 不是串联子串,因为他不是任何 $words$ 排列的连接。 **要求**: 返回所有串联子串在 $s$ 中的开始索引。你可以以任意顺序返回答案。 **说明**: - $1 \le s.length \le 10^4$。 - $1 \le words.length \le 5000$。 - $1 <= words[i].length <= 30$。 - $words[i]$ 和 $s$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "barfoothefoobarman", words = ["foo","bar"] 输出:[0,9] 解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。 子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。 子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。 输出顺序无关紧要。返回 [9,0] 也是可以的。 ``` - 示例 2: ```python 输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"] 输出:[] 解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。 s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。 所以我们返回一个空数组。 ``` ## 解题思路 ### 思路 1:滑动窗口 + 哈希表 **核心思想**: 使用滑动窗口技术,结合哈希表来统计单词出现次数,通过固定窗口大小来检查所有可能的串联子串。 **算法步骤**: 1. **计算窗口大小**:串联子串的长度为 $word\_length \times word\_count$,其中 $word\_length$ 是每个单词的长度,$word\_count$ 是单词数组的长度。 2. **构建目标哈希表**:统计 $words$ 数组中每个单词的出现次数,存储在 $target\_count$ 中。 3. **滑动窗口检查**:从字符串 $s$ 的每个可能起始位置开始,使用滑动窗口: - 窗口大小为 $word\_length \times word\_count$。 - 将窗口内的字符串按 $word\_length$ 分割成单词。 - 统计这些单词的出现次数,与 $target\_count$ 比较。 4. **匹配判断**:如果当前窗口内的单词统计与目标统计完全匹配,则记录起始位置。 **关键点**: - 滑动窗口的大小固定为所有单词的总长度。 - 使用哈希表快速比较单词出现次数。 - 需要处理字符串长度不足的情况。 ### 思路 1:代码 ```python class Solution: def findSubstring(self, s: str, words: List[str]) -> List[int]: """ 找到所有串联子串的起始位置 """ if not s or not words or not words[0]: return [] # 获取基本参数 word_length = len(words[0]) # 每个单词的长度 word_count = len(words) # 单词数量 total_length = word_length * word_count # 串联子串的总长度 # 如果字符串长度小于串联子串长度,直接返回空列表 if len(s) < total_length: return [] # 构建目标单词计数哈希表 target_count = {} for word in words: target_count[word] = target_count.get(word, 0) + 1 result = [] # 遍历所有可能的起始位置 for start in range(len(s) - total_length + 1): # 获取当前窗口的子串 window = s[start:start + total_length] # 将窗口按单词长度分割 window_words = [] for i in range(0, total_length, word_length): word = window[i:i + word_length] window_words.append(word) # 统计当前窗口的单词出现次数 current_count = {} for word in window_words: current_count[word] = current_count.get(word, 0) + 1 # 检查是否匹配目标计数 if current_count == target_count: result.append(start) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m \times k)$,其中 $n$ 是字符串 $s$ 的长度,$m$ 是单词数组的长度,$k$ 是每个单词的长度。需要检查 $O(n)$ 个起始位置,每个位置需要处理 $O(m)$ 个单词,每个单词长度为 $O(k)$。 - **空间复杂度**:$O(m \times k)$,用于存储目标单词计数哈希表和当前窗口单词计数哈希表。 ================================================ FILE: docs/solutions/0001-0099/sudoku-solver.md ================================================ # [0037. 解数独](https://leetcode.cn/problems/sudoku-solver/) - 标签:数组、哈希表、回溯、矩阵 - 难度:困难 ## 题目链接 - [0037. 解数独 - 力扣](https://leetcode.cn/problems/sudoku-solver/) ## 题目大意 **描述**:给定一个二维的字符数组 $board$ 用来表示数独,其中数字 $1 \sim 9$ 表示该位置已经填入了数字,`.` 表示该位置还没有填入数字。 **要求**:现在编写一个程序,通过填充空格的方式来解决数独问题,最终不用返回答案,将题目给定 $board$ 修改为可行的方案即可。 **说明**: - 数独解法需遵循如下规则: - 数字 $1 \sim 9$ 在每一行只能出现一次。 - 数字 $1 \sim 9$ 在每一列只能出现一次。 - 数字 $1 \sim 9$ 在每一个以粗直线分隔的 $3 \times 3$ 宫格内只能出现一次。 - $board.length == 9$。 - $board[i].length == 9$。 - $board[i][j]$ 是一位数字或者 `.`。 - 题目数据保证输入数独仅有一个解。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2021/04/12/250px-sudoku-by-l2g-20050714svg.png) ```python 输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]] 输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]] 解释:输入的数独如上图所示,唯一有效的解决方案如下所示: ``` ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2021/04/12/250px-sudoku-by-l2g-20050714_solutionsvg.png) ## 解题思路 ### 思路 1:回溯算法 对于每一行、每一列、每一个数字,都需要一重 `for` 循环来遍历,这样就是三重 `for` 循环。 对于第 $i$ 行、第 $j$ 列的元素来说,如果当前位置为空位,则尝试将第 $k$ 个数字置于此处,并检验数独的有效性。如果有效,则继续遍历下一个空位,直到遍历完所有空位,得到可行方案或者遍历失败时结束。 遍历完下一个空位之后再将此位置进行回退,置为 `.`。 ### 思路 1:代码 ```python class Solution: def backtrack(self, board: List[List[str]]): for i in range(len(board)): for j in range(len(board[0])): if board[i][j] != '.': continue for k in range(1, 10): if self.isValid(i, j, k, board): board[i][j] = str(k) if self.backtrack(board): return True board[i][j] = '.' return False return True def isValid(self, row: int, col: int, val: int, board: List[List[str]]) -> bool: for i in range(0, 9): if board[row][i] == str(val): return False for j in range(0, 9): if board[j][col] == str(val): return False start_row = (row // 3) * 3 start_col = (col // 3) * 3 for i in range(start_row, start_row + 3): for j in range(start_col, start_col + 3): if board[i][j] == str(val): return False return True def solveSudoku(self, board: List[List[str]]) -> None: self.backtrack(board) """ Do not return anything, modify board in-place instead. """ ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(9^m)$,$m$ 为棋盘中 `.` 的数量。 - **空间复杂度**:$O(9^2)$。 ================================================ FILE: docs/solutions/0001-0099/swap-nodes-in-pairs.md ================================================ # [0024. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/) - 标签:递归、链表 - 难度:中等 ## 题目链接 - [0024. 两两交换链表中的节点 - 力扣](https://leetcode.cn/problems/swap-nodes-in-pairs/) ## 题目大意 **描述**:给定一个链表的头节点 `head`。 **要求**:按顺序将链表中每两个节点交换一下,并返回交换后的链表。 **说明**: - 需要实际进行节点交换,而不是纸改变节点内部的值。 - 链表中节点的数目在范围 $[0, 100]$ 内。 - $0 \le Node.val \le 100$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/03/swap_ex1.jpg) ```python 输入:head = [1,2,3,4] 输出:[2,1,4,3] ``` - 示例 2: ```python 输入:head = [] 输出:[] ``` ## 解题思路 ### 思路 1:迭代 1. 创建一个哑节点 `new_head`,令 `new_head.next = head`。 2. 遍历链表,并判断当前链表后两位节点是否为空。如果后两个节点不为空,则使用三个指针:`curr` 指向当前节点,`node1` 指向下一个节点,`node2` 指向下面第二个节点。 3. 将 `curr` 指向 `node2`,`node1` 指向 `node2` 后边的节点,`node2` 指向 `node1`。则节点关系由 `curr → node1 → node2` 变为了 `curr → node2 → node1`。 4. 依次类推,最终返回哑节点连接的后一个节点。 ### 思路 1:代码 ```python class Solution: def swapPairs(self, head: ListNode) -> ListNode: new_head = ListNode(0) new_head.next = head curr = new_head while curr.next and curr.next.next: node1 = curr.next node2 = curr.next.next curr.next = node2 node1.next = node2.next node2.next = node1 curr = node1 return new_head.next ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为链表的节点数量。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0001-0099/text-justification.md ================================================ # [0068. 文本左右对齐](https://leetcode.cn/problems/text-justification/) - 标签:数组、字符串、模拟 - 难度:困难 ## 题目链接 - [0068. 文本左右对齐 - 力扣](https://leetcode.cn/problems/text-justification/) ## 题目大意 **描述**: 给定一个单词数组 $words$ 和一个长度 $maxWidth$, **要求**: 重新排版单词,使其成为每行恰好有 $maxWidth$ 个字符,且左右两端对齐的文本。 你应该使用「贪心算法」来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格 `' '` 填充,使得每行恰好有 $maxWidth$ 个字符。 要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。 文本的最后一行应为左对齐,且单词之间不插入额外的空格。 **说明**: - 单词是指由非空格字符组成的字符序列。 - 每个单词的长度大于 $0$,小于等于 $maxWidth$。 - 输入单词数组 $words$ 至少包含一个单词。 **示例**: - 示例 1: ```python 输入: words = ["This", "is", "an", "example", "of", "text", "justification."], maxWidth = 16 输出: [ "This is an", "example of text", "justification. " ] ``` - 示例 2: ```python 输入:words = ["What","must","be","acknowledgment","shall","be"], maxWidth = 16 输出: [ "What must be", "acknowledgment ", "shall be " ] 解释: 注意最后一行的格式应为 "shall be " 而不是 "shall be", 因为最后一行应为左对齐,而不是左右两端对齐。 第二行同样为左对齐,这是因为这行只包含一个单词。 ``` ## 解题思路 ### 思路 1:贪心算法 + 模拟 **核心思想**: 使用贪心算法尽可能多地往每行放置单词,然后根据当前行的单词数量和总长度来分配空格,确保每行恰好有 $maxWidth$ 个字符。 **算法步骤**: 1. **贪心选择单词**:从当前位置开始,尽可能多地选择单词放入当前行,直到无法再放入更多单词。 2. **计算空格分配**:根据当前行的单词数量 $word\_count$ 和总字符长度 $total\_length$,计算需要分配的空格数 $spaces\_needed = maxWidth - total\_length$。 3. **分配空格**: - 如果只有 $1$ 个单词,所有空格都放在单词后面。 - 如果有多个单词,计算基础空格数 $base\_spaces = \lfloor \frac{spaces\_needed}{word\_count - 1} \rfloor$ 和额外空格数 $extra\_spaces = spaces\_needed \bmod (word\_count - 1)$。 - 前 $extra\_spaces$ 个单词间隔分配 $base\_spaces + 1$ 个空格,其余单词间隔分配 $base\_spaces$ 个空格。 4. **处理最后一行**:最后一行左对齐,单词间只放 $1$ 个空格,剩余空格放在行末。 **关键点**: - 使用贪心策略尽可能多地放置单词。 - 均匀分配空格,左侧空格数不少于右侧。 - 最后一行特殊处理,左对齐即可。 ### 思路 1:代码 ```python class Solution: def fullJustify(self, words: List[str], maxWidth: int) -> List[str]: """ 文本左右对齐 """ result = [] i = 0 while i < len(words): # 贪心选择:尽可能多地选择单词放入当前行 line_words = [] line_length = 0 # 选择当前行的单词 while i < len(words): word = words[i] # 计算加入当前单词后的长度(包括单词间至少一个空格) if line_words: # 已有单词,需要至少一个空格 new_length = line_length + 1 + len(word) else: # 第一个单词,不需要空格 new_length = len(word) # 如果加入当前单词后超过最大宽度,停止选择 if new_length > maxWidth: break line_words.append(word) line_length = new_length i += 1 # 处理当前行 if i == len(words): # 最后一行:左对齐 line = ' '.join(line_words) # 在行末添加剩余空格 line += ' ' * (maxWidth - len(line)) else: # 非最后一行:两端对齐 word_count = len(line_words) if word_count == 1: # 只有一个单词,所有空格放在后面 line = line_words[0] + ' ' * (maxWidth - len(line_words[0])) else: # 多个单词,需要分配空格 total_spaces = maxWidth - line_length + word_count - 1 base_spaces = total_spaces // (word_count - 1) extra_spaces = total_spaces % (word_count - 1) # 构建当前行 line_parts = [] for j in range(word_count - 1): line_parts.append(line_words[j]) # 前 extra_spaces 个间隔多分配一个空格 spaces = base_spaces + (1 if j < extra_spaces else 0) line_parts.append(' ' * spaces) # 添加最后一个单词 line_parts.append(line_words[-1]) line = ''.join(line_parts) result.append(line) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times maxWidth)$,其中 $n$ 是单词的总数。需要遍历所有单词,每行最多处理 $maxWidth$ 个字符。 - **空间复杂度**:$O(n \times maxWidth)$,存储结果字符串,最坏情况下每行都是 $maxWidth$ 个字符。 ================================================ FILE: docs/solutions/0001-0099/trapping-rain-water.md ================================================ # [0042. 接雨水](https://leetcode.cn/problems/trapping-rain-water/) - 标签:栈、数组、双指针、动态规划、单调栈 - 难度:困难 ## 题目链接 - [0042. 接雨水 - 力扣](https://leetcode.cn/problems/trapping-rain-water/) ## 题目大意 **描述**:给定 `n` 个非负整数表示每个宽度为 `1` 的柱子的高度图,用数组 `height` 表示,其中 `height[i]` 表示第 `i` 根柱子的高度。 **要求**:计算按此排列的柱子,下雨之后能接多少雨水。 **说明**: - $n == height.length$。 - $1 \le n \le 2 * 10^4$。 - $0 \le height[i] \le 10^5$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/10/22/rainwatertrap.png) ```python 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 ``` - 示例 2: ```python 输入:height = [4,2,0,3,2,5] 输出:9 ``` ## 解题思路 ### 思路 1:单调栈 1. 遍历高度数组 `height`。 2. 如果当前柱体高度较小,小于等于栈顶柱体的高度,则将当前柱子高度入栈。 3. 如果当前柱体高度较大,大于栈顶柱体的高度,则一直出栈,直到当前柱体小于等于栈顶柱体的高度。 4. 假设当前柱体为 `C`,出栈柱体为 `B`,出栈之后新的栈顶柱体为 `A`。则说明: 1. 当前柱体 `C` 是出栈柱体 `B` 向右找到的第一个大于当前柱体高度的柱体,那么以出栈柱体 `B` 为中心,可以向右将宽度扩展到当前柱体 `C`。 2. 新的栈顶柱体 `A` 是出栈柱体 `B` 向左找到的第一个大于当前柱体高度的柱体,那么以出栈柱体 `B` 为中心,可以向左将宽度扩展到当前柱体 `A`。 5. 出栈后,以新的栈顶柱体 `A` 为左边界,以当前柱体 `C` 为右边界,以左右边界与出栈柱体 `B` 的高度差为深度,计算可以接到雨水的面积。然后记录并更新累积面积。 ### 思路 1:代码 ```python class Solution: def trap(self, height: List[int]) -> int: ans = 0 stack = [] size = len(height) for i in range(size): while stack and height[i] > height[stack[-1]]: cur = stack.pop(-1) if stack: left = stack[-1] + 1 right = i - 1 high = min(height[i], height[stack[-1]]) - height[cur] ans += high * (right - left + 1) else: break stack.append(i) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 `height` 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0001-0099/two-sum.md ================================================ # [0001. 两数之和](https://leetcode.cn/problems/two-sum/) - 标签:数组、哈希表 - 难度:简单 ## 题目链接 - [0001. 两数之和 - 力扣](https://leetcode.cn/problems/two-sum/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 和一个整数目标值 $target$。 **要求**:在该数组中找出和为 $target$ 的两个整数,并输出这两个整数的下标。可以按任意顺序返回答案。 **说明**: - $2 \le nums.length \le 10^4$。 - $-10^9 \le nums[i] \le 10^9$。 - $-10^9 \le target \le 10^9$。 - 只会存在一个有效答案。 **示例**: - 示例 1: ```python 输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 ``` - 示例 2: ```python 输入:nums = [3,2,4], target = 6 输出:[1,2] ``` ## 解题思路 ### 思路 1:枚举算法 1. 使用两重循环枚举数组中每一个数 $nums[i]$、$nums[j]$,判断所有的 $nums[i] + nums[j]$ 是否等于 $target$。 2. 如果出现 $nums[i] + nums[j] == target$,则说明数组中存在和为 $target$ 的两个整数,将两个整数的下标 $i$、$j$ 输出即可。 ### 思路 1:代码 ```python class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: for i in range(len(nums)): for j in range(i + 1, len(nums)): if i != j and nums[i] + nums[j] == target: return [i, j] return [] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是数组 $nums$ 的元素数量。 - **空间复杂度**:$O(1)$。 ### 思路 2:哈希表 哈希表中键值对信息为 $target-nums[i] :i,其中 $i$ 为下标。 1. 遍历数组,对于每一个数 $nums[i]$: 1. 先查找字典中是否存在 $target - nums[i]$,存在则输出 $target - nums[i]$ 对应的下标和当前数组的下标 $i$。 2. 不存在则在字典中存入 $target - nums[i]$ 的下标 $i$。 ### 思路 2:代码 ```python def twoSum(self, nums: List[int], target: int) -> List[int]: numDict = dict() for i in range(len(nums)): if target-nums[i] in numDict: return numDict[target-nums[i]], i numDict[nums[i]] = i return [0] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $nums$ 的元素数量。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0001-0099/unique-binary-search-trees-ii.md ================================================ # [0095. 不同的二叉搜索树 II](https://leetcode.cn/problems/unique-binary-search-trees-ii/) - 标签:树、二叉搜索树、动态规划、回溯、二叉树 - 难度:中等 ## 题目链接 - [0095. 不同的二叉搜索树 II - 力扣](https://leetcode.cn/problems/unique-binary-search-trees-ii/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:请生成返回以 $1$ 到 $n$ 为节点构成的「二叉搜索树」,可以按任意顺序返回答案。 **说明**: - $1 \le n \le 8$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/01/18/uniquebstn3.jpg) ```python 输入:n = 3 输出:[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]] ``` - 示例 2: ```python 输入:n = 1 输出:[[1]] ``` ## 解题思路 ### 思路 1:递归遍历 如果根节点为 $i$,则左子树的节点为 $(1, 2, ..., i - 1)$,右子树的节点为 $(i + 1, i + 2, ..., n)$。可以递归的构建二叉树。 定义递归函数 `generateTrees(start, end)`,表示生成 $[left, ..., right]$ 构成的所有可能的二叉搜索树。 - 如果 $start > end$,返回 `[None]`。 - 初始化存放所有可能二叉搜索树的数组。 - 遍历 $[left, ..., right]$ 的每一个节点 $i$,将其作为根节点。 - 递归构建左右子树。 - 将所有符合要求的左右子树组合起来,将其加入到存放二叉搜索树的数组中。 - 返回存放二叉搜索树的数组。 ### 思路 1:代码 ```python class Solution: def generateTrees(self, n: int) -> List[TreeNode]: if n == 0: return [] def generateTrees(start, end): if start > end: return [None] trees = [] for i in range(start, end+1): left_trees = generateTrees(start, i - 1) right_trees = generateTrees(i + 1, end) for left_tree in left_trees: for right_tree in right_trees: curr_tree = TreeNode(i) curr_tree.left = left_tree curr_tree.right = right_tree trees.append(curr_tree) return trees return generateTrees(1, n) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(C_n)$,其中 $C_n$ 是第 $n$ 个卡特兰数。 - **空间复杂度**:$O(C_n)$,其中 $C_n$ 是第 $n$ 个卡特兰数。 ================================================ FILE: docs/solutions/0001-0099/unique-binary-search-trees.md ================================================ # [0096. 不同的二叉搜索树](https://leetcode.cn/problems/unique-binary-search-trees/) - 标签:树、二叉搜索树、数学、动态规划、二叉树 - 难度:中等 ## 题目链接 - [0096. 不同的二叉搜索树 - 力扣](https://leetcode.cn/problems/unique-binary-search-trees/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:求以 $1$ 到 $n$ 为节点构成的「二叉搜索树」有多少种? **说明**: - $1 \le n \le 19$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/01/18/uniquebstn3.jpg) ```python 输入:n = 3 输出:5 ``` - 示例 2: ```python 输入:n = 1 输出:1 ``` ## 解题思路 ### 思路 1:动态规划 一棵搜索二叉树的左、右子树,要么也是搜索二叉树,要么就是空树。 如果定义 $f[i]$ 表示以 $i$ 为根的二叉搜索树个数,定义 $g(i)$ 表示 $i$ 个节点可以构成的二叉搜索树个数,则有: - $g(i) = f(1) + f(2) + f(3) + … + f(i)$。 其中当 $i$ 为根节点时,则用 $(1, 2, …, i - 1)$ 共 $i - 1$ 个节点去递归构建左子搜索二叉树,用 $(i + 1, i + 2, …, n)$ 共 $n - i$ 个节点去递归构建右子搜索树。则有: - $f(i) = g(i - 1) \times g(n - i)$。 综合上面两个式子 $\begin{cases} g(i) = f(1) + f(2) + f(3) + … + f(i) \cr f(i) = g(i - 1) \times g(n - i) \end{cases}$ 可得出: - $g(n) = g(0) \times g(n - 1) + g(1) \times g(n - 2) + … + g(n - 1) \times g(0)$。 将 $n$ 换为 $i$,可变为: - $g(i) = g(0) \times g(i - 1) + g(1) \times g(i - 2) + … + g(i - 1) \times g(0)$。 再转换一下,可变为: - $g(i) = \sum_{1 \le j \le i} \lbrace g(j - 1) \times g(i - j) \rbrace$。 则我们可以通过动态规划的方法,递推求解 $g(i)$,并求解出 $g(n)$。具体步骤如下: ###### 1. 阶段划分 按照根节点的编号进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为: $i$ 个节点可以构成的二叉搜索树个数。 ###### 3. 状态转移方程 $dp[i] = \sum_{1 \le j \le i} \lbrace dp[j - 1] \times dp[i - j] \rbrace$ ###### 4. 初始条件 - $0$ 个节点可以构成的二叉搜索树个数为 $1$(空树),即 $dp[0] = 1$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示为: $i$ 个节点可以构成的二叉搜索树个数。。 所以最终结果为 $dp[n]$。 ### 思路 1:代码 ```python class Solution: def numTrees(self, n: int) -> int: dp = [0 for _ in range(n + 1)] dp[0] = 1 for i in range(1, n + 1): for j in range(1, i + 1): dp[i] += dp[j - 1] * dp[i - j] return dp[n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(n)$。 ## 参考资料 - 【题解】[画解算法:96. 不同的二叉搜索树 - 不同的二叉搜索树](https://leetcode.cn/problems/unique-binary-search-trees/solution/hua-jie-suan-fa-96-bu-tong-de-er-cha-sou-suo-shu-b/) ================================================ FILE: docs/solutions/0001-0099/unique-paths-ii.md ================================================ # [0063. 不同路径 II](https://leetcode.cn/problems/unique-paths-ii/) - 标签:数组、动态规划、矩阵 - 难度:中等 ## 题目链接 - [0063. 不同路径 II - 力扣](https://leetcode.cn/problems/unique-paths-ii/) ## 题目大意 **描述**:一个机器人位于一个 $m \times n$ 网格的左上角。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。但是网格中有障碍物,不能通过。 现在给定一个二维数组表示网格,$1$ 代表障碍物,$0$ 表示空位。 **要求**:计算出从左上角到右下角会有多少条不同的路径。 **说明**: - $m == obstacleGrid.length$。 - $n == obstacleGrid[i].length$。 - $1 \le m, n \le 100$。 - $obstacleGrid[i][j]$ 为 $0$ 或 $1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/04/robot1.jpg) ```python 输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] 输出:2 解释:3x3 网格的正中间有一个障碍物。 从左上角到右下角一共有 2 条不同的路径: 1. 向右 -> 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 -> 向右 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/11/04/robot2.jpg) ```python 输入:obstacleGrid = [[0,1],[0,0]] 输出:1 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照路径的结尾位置(行位置、列位置组成的二维坐标)进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:从 $(0, 0)$ 到 $(i, j)$ 的不同路径数。 ###### 3. 状态转移方程 因为我们每次只能向右、或者向下移动一步,因此想要走到 $(i, j)$,只能从 $(i - 1, j)$ 向下走一步走过来;或者从 $(i, j - 1)$ 向右走一步走过来。则状态转移方程为:$dp[i][j] = dp[i - 1][j] + dp[i][j - 1]$,其中 $obstacleGrid[i][j] == 0$。 ###### 4. 初始条件 - 对于第一行、第一列,因为只能超一个方向走,所以 $dp[i][0] = 1$,$dp[0][j] = 1$。如果在第一行、第一列遇到障碍,则终止赋值,跳出循环。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:从 $(0, 0)$ 到 $(i, j)$ 的不同路径数。所以最终结果为 $dp[m - 1][n - 1]$。 ### 思路 1:代码 ```python class Solution: def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int: m = len(obstacleGrid) n = len(obstacleGrid[0]) dp = [[0 for _ in range(n)] for _ in range(m)] for i in range(m): if obstacleGrid[i][0] == 1: break dp[i][0] = 1 for j in range(n): if obstacleGrid[0][j] == 1: break dp[0][j] = 1 for i in range(1, m): for j in range(1, n): if obstacleGrid[i][j] == 1: continue dp[i][j] = dp[i - 1][j] + dp[i][j - 1] return dp[m - 1][n - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/0001-0099/unique-paths.md ================================================ # [0062. 不同路径](https://leetcode.cn/problems/unique-paths/) - 标签:数学、动态规划、组合数学 - 难度:中等 ## 题目链接 - [0062. 不同路径 - 力扣](https://leetcode.cn/problems/unique-paths/) ## 题目大意 **描述**:给定两个整数 $m$ 和 $n$,代表大小为 $m \times n$ 的棋盘, 一个机器人位于棋盘左上角的位置,机器人每次只能向右、或者向下移动一步。 **要求**:计算出机器人从棋盘左上角到达棋盘右下角一共有多少条不同的路径。 **说明**: - $1 \le m, n \le 100$。 - 题目数据保证答案小于等于 $2 \times 10^9$。 **示例**: - 示例 1: ```python 输入:m = 3, n = 7 输出:28 ``` ![](https://assets.leetcode.com/uploads/2018/10/22/robot_maze.png) - 示例 2: ```python 输入:m = 3, n = 2 输出:3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 3. 向下 -> 向右 -> 向下 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照路径的结尾位置(行位置、列位置组成的二维坐标)进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 为:从左上角到达位置 $(i, j)$ 的路径数量。 ###### 3. 状态转移方程 因为我们每次只能向右、或者向下移动一步,因此想要走到 $(i, j)$,只能从 $(i - 1, j)$ 向下走一步走过来;或者从 $(i, j - 1)$ 向右走一步走过来。所以可以写出状态转移方程为:$dp[i][j] = dp[i - 1][j] + dp[i][j - 1]$,此时 $i > 0, j > 0$。 ###### 4. 初始条件 - 从左上角走到 $(0, 0)$ 只有一种方法,即 $dp[0][0] = 1$。 - 第一行元素只有一条路径(即只能通过前一个元素向右走得到),所以 $dp[0][j] = 1$。 - 同理,第一列元素只有一条路径(即只能通过前一个元素向下走得到),所以 $dp[i][0] = 1$。 ###### 5. 最终结果 根据状态定义,最终结果为 $dp[m - 1][n - 1]$,即从左上角到达右下角 $(m - 1, n - 1)$ 位置的路径数量为 $dp[m - 1][n - 1]$。 ### 思路 1:动态规划代码 ```python class Solution: def uniquePaths(self, m: int, n: int) -> int: dp = [[0 for _ in range(n)] for _ in range(m)] for j in range(n): dp[0][j] = 1 for i in range(m): dp[i][0] = 1 for i in range(1, m): for j in range(1, n): dp[i][j] = dp[i - 1][j] + dp[i][j - 1] return dp[m - 1][n - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。初始条件赋值的时间复杂度为 $O(m + n)$,两重循环遍历的时间复杂度为 $O(m \times n)$,所以总体时间复杂度为 $O(m \times n)$。 - **空间复杂度**:$O(m \times n)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(m \times n)$。因为 $dp[i][j]$ 的状态只依赖于上方值 $dp[i - 1][j]$ 和左侧值 $dp[i][j - 1]$,而我们在进行遍历时的顺序刚好是从上至下、从左到右。所以我们可以使用长度为 $n$ 的一维数组来保存状态,从而将空间复杂度优化到 $O(n)$。 ================================================ FILE: docs/solutions/0001-0099/valid-number.md ================================================ # [0065. 有效数字](https://leetcode.cn/problems/valid-number/) - 标签:字符串 - 难度:困难 ## 题目链接 - [0065. 有效数字 - 力扣](https://leetcode.cn/problems/valid-number/) ## 题目大意 **描述**: 一般的,一个「有效数字」可以用以下的规则之一定义: 1. 一个「整数」后面跟着一个「可选指数」。 2. 一个「十进制数」后面跟着一个「可选指数」。 关于「整数」、「十进制数」、「指数」、「数字」的定义: - 一个「整数」定义为一个 「可选符号」 `'-'` 或 `'+'` 后面跟着「数字」。 - 一个「十进制数」定义为一个「可选符号」 `'-'` 或 `'+'` 后面跟着下述规则: 1. 「数字」后跟着一个 小数点 `.`。 2. 「数字」后跟着一个 小数点 `.` 再跟着「数位」。 3. 一个「小数点」 `.` 后跟着「数位」。 - 「指数」定义为指数符号 `'e'` 或 `'E'`,后面跟着一个 整数。 - 「数字」定义为一个或多个数位。 例如,下面的都是有效数字: - `"2"`, `"0089"`, `"-0.1"`, `"+3.14"`, `"4."`, `"-.9"`, `"2e10"`, `"-90E3"`, `"3e+7"`, `"+6e-1"`, `"53.5e93"`, `"-123.456e789"`, 下面的都不是有效数字: - `"abc"`, `"1a"`, `"1e"`, `"e3"`, `"99e2.5"`, `"--6"`, `"-+3"`, `"95a54e53"`。 给定一个字符串 $s$。 **要求**: 返回 $s$ 是否是一个有效数字。 **说明**: - $1 \le s.length \le 20$ - s 仅含英文字母(大写和小写),数字(0-9),加号 '+' ,减号 '-' ,或者点 '.' 。 **示例**: - 示例 1: ```python 输入:s = "0" 输出:true ``` - 示例 2: ```python 输入:s = "e" 输出:false ``` ## 解题思路 ### 思路 1:有限状态自动机 **核心思想**: 将有效数字的识别过程建模为一个有限状态自动机(DFA),通过状态转换来判断字符串是否符合有效数字的规则。 **算法步骤**: 1. **定义状态**:定义所有可能的状态,包括开始状态、符号状态、整数部分、小数点、小数部分、指数符号、指数符号、指数整数部分等。 2. **状态转换**:根据当前字符类型(数字、符号、小数点、指数符号等)和当前状态,确定下一个状态。 3. **接受状态**:定义哪些状态是有效的结束状态。 4. **遍历字符串**:逐个字符处理,进行状态转换。 5. **判断结果**:如果最终状态是接受状态,则字符串是有效数字。 **状态定义**: - $S_0$:开始状态 - $S_1$:符号状态(已读取 `+` 或 `-`) - $S_2$:整数部分(已读取数字) - $S_3$:小数点(已读取 `.`) - $S_4$:小数部分(小数点后有数字) - $S_5$:指数符号(已读取 `e` / `E`) - $S_6$:指数符号(指数部分的 `+` 或 `-`) - $S_7$:指数整数部分(指数部分的数字) **状态转换规则**: - 从 $S_0$:数字 → $S_2$,符号 → $S_1$,小数点 → $S_3$ - 从 $S_1$:数字 → $S_2$,小数点 → $S_3$ - 从 $S_2$:数字 → $S_2$,小数点 → $S_4$,指数符号 → $S_5$ - 从 $S_3$:数字 → $S_4$ - 从 $S_4$:数字 → $S_4$,指数符号 → $S_5$ - 从 $S_5$:数字 → $S_7$,符号 → $S_6$ - 从 $S_6$:数字 → $S_7$ - 从 $S_7$:数字 → $S_7$ **接受状态**:$S_2$、$S_4$、$S_7$(分别对应整数、小数、指数形式) ### 思路 1:代码 ```python class Solution: def isNumber(self, s: str) -> bool: """ 使用有限状态自动机判断字符串是否为有效数字 Args: s: 待判断的字符串 Returns: bool: 是否为有效数字 """ # 定义状态转换表 # 状态: 0-开始, 1-符号, 2-整数, 3-小数点, 4-小数, 5-指数符号, 6-指数符号, 7-指数整数 # 字符类型: 0-数字, 1-符号, 2-小数点, 3-指数符号, 4-其他 transitions = { 0: {0: 2, 1: 1, 2: 3, 3: -1, 4: -1}, # 开始状态 1: {0: 2, 1: -1, 2: 3, 3: -1, 4: -1}, # 符号状态 2: {0: 2, 1: -1, 2: 4, 3: 5, 4: -1}, # 整数部分 3: {0: 4, 1: -1, 2: -1, 3: -1, 4: -1}, # 小数点 4: {0: 4, 1: -1, 2: -1, 3: 5, 4: -1}, # 小数部分 5: {0: 7, 1: 6, 2: -1, 3: -1, 4: -1}, # 指数符号 6: {0: 7, 1: -1, 2: -1, 3: -1, 4: -1}, # 指数符号 7: {0: 7, 1: -1, 2: -1, 3: -1, 4: -1} # 指数整数部分 } # 接受状态:整数、小数、指数形式 accept_states = {2, 4, 7} # 当前状态 state = 0 # 遍历字符串 for char in s: # 确定字符类型 if char.isdigit(): char_type = 0 # 数字 elif char in '+-': char_type = 1 # 符号 elif char == '.': char_type = 2 # 小数点 elif char in 'eE': char_type = 3 # 指数符号 else: char_type = 4 # 其他(无效字符) # 状态转换 if state not in transitions or char_type not in transitions[state]: return False state = transitions[state][char_type] # 如果转换到无效状态,直接返回 False if state == -1: return False # 检查最终状态是否为接受状态 return state in accept_states ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串的长度。需要遍历字符串一次,每次状态转换的时间复杂度为 $O(1)$。 - **空间复杂度**:$O(1)$。状态转换表的大小是固定的,不依赖于输入字符串的长度。 ================================================ FILE: docs/solutions/0001-0099/valid-parentheses.md ================================================ # [0020. 有效的括号](https://leetcode.cn/problems/valid-parentheses/) - 标签:栈、字符串 - 难度:简单 ## 题目链接 - [0020. 有效的括号 - 力扣](https://leetcode.cn/problems/valid-parentheses/) ## 题目大意 **描述**:给定一个只包括 `'('`,`')'`,`'{'`,`'}'`,`'['`,`']'` 的字符串 `s` 。 **要求**:判断字符串 `s` 是否有效(即括号是否匹配)。 **说明**: - 有效字符串需满足: 1. 左括号必须用相同类型的右括号闭合。 2. 左括号必须以正确的顺序闭合。 **示例**: - 示例 1: ```python 输入:s = "()" 输出:True ``` - 示例 2: ```python 输入:s = "()[]{}" 输出:True ``` ## 解题思路 ### 思路 1:栈 括号匹配是「栈」的经典应用。我们可以用栈来解决这道题。具体做法如下: 1. 先判断一下字符串的长度是否为偶数。因为括号是成对出现的,所以字符串的长度应为偶数,可以直接判断长度为奇数的字符串不匹配。如果字符串长度为奇数,则说明字符串 `s` 中的括号不匹配,直接返回 `False`。 2. 使用栈 `stack` 来保存未匹配的左括号。然后依次遍历字符串 `s` 中的每一个字符。 1. 如果遍历到左括号时,将其入栈。 2. 如果遍历到右括号时,先看栈顶元素是否是与当前右括号相同类型的左括号。 1. 如果是与当前右括号相同类型的左括号,则令其出栈,继续向前遍历。 2. 如果不是与当前右括号相同类型的左括号,则说明字符串 `s` 中的括号不匹配,直接返回 `False`。 3. 遍历完,还要再判断一下栈是否为空。 1. 如果栈为空,则说明字符串 `s` 中的括号匹配,返回 `True`。 2. 如果栈不为空,则说明字符串 `s` 中的括号不匹配,返回 `False`。 ### 思路 1:代码 ```python class Solution: def isValid(self, s: str) -> bool: if len(s) % 2 == 1: return False stack = list() for ch in s: if ch == '(' or ch == '[' or ch == '{': stack.append(ch) elif ch == ')': if len(stack) !=0 and stack[-1] == '(': stack.pop() else: return False elif ch == ']': if len(stack) !=0 and stack[-1] == '[': stack.pop() else: return False elif ch == '}': if len(stack) !=0 and stack[-1] == '{': stack.pop() else: return False if len(stack) == 0: return True else: return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0001-0099/valid-sudoku.md ================================================ # [0036. 有效的数独](https://leetcode.cn/problems/valid-sudoku/) - 标签:数组、哈希表、矩阵 - 难度:中等 ## 题目链接 - [0036. 有效的数独 - 力扣](https://leetcode.cn/problems/valid-sudoku/) ## 题目大意 **描述**:给定一个数独,用 `9 * 9` 的二维字符数组 `board` 来表示,其中,未填入的空白用 "." 代替。 **要求**:判断该数独是否是一个有效的数独。 **说明**: - 一个有效的数独(部分已被填充)不一定是可解的。 - 只需要根据以上规则,验证已经填入的数字是否有效即可。 - 空白格用 `'.'` 表示。 一个有效的数独需满足: 1. 数字 `1-9` 在每一行只能出现一次。 2. 数字 `1-9` 在每一列只能出现一次。 3. 数字 `1-9` 在每一个以粗实线分隔的 `3 * 3` 宫内只能出现一次。(请参考示例图) **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2021/04/12/250px-sudoku-by-l2g-20050714svg.png) ```python 输入:board = [["5","3",".",".","7",".",".",".","."] ,["6",".",".","1","9","5",".",".","."] ,[".","9","8",".",".",".",".","6","."] ,["8",".",".",".","6",".",".",".","3"] ,["4",".",".","8",".","3",".",".","1"] ,["7",".",".",".","2",".",".",".","6"] ,[".","6",".",".",".",".","2","8","."] ,[".",".",".","4","1","9",".",".","5"] ,[".",".",".",".","8",".",".","7","9"]] 输出:True ``` ## 解题思路 ### 思路 1:哈希表 判断数独有效,需要分别看每一行、每一列、每一个 `3 * 3` 的小方格是否出现了重复数字,如果都没有出现重复数字就是一个有效的数独,如果出现了重复数字则不是有效的数独。 - 用 `3` 个 `9 * 9` 的数组分别来表示该数字是否在所在的行,所在的列,所在的方格出现过。其中方格角标的计算用 `box[(i / 3) * 3 + (j / 3)][n]` 来表示。 - 双重循环遍历数独矩阵。如果对应位置上的数字如果已经在在所在的行 / 列 / 方格出现过,则返回 `False`。 - 遍历完没有重复出现,则返回 `Ture`。 ### 思路 1:代码 ```python class Solution: def isValidSudoku(self, board: List[List[str]]) -> bool: rows_map = [dict() for _ in range(9)] cols_map = [dict() for _ in range(9)] boxes_map = [dict() for _ in range(9)] for i in range(9): for j in range(9): if board[i][j] == '.': continue num = int(board[i][j]) box_index = (i // 3) * 3 + j // 3 row_num = rows_map[i].get(num, 0) col_num = cols_map[j].get(num, 0) box_num = boxes_map[box_index].get(num, 0) if row_num > 0 or col_num > 0 or box_num > 0: return False rows_map[i][num] = 1 cols_map[j][num] = 1 boxes_map[box_index][num] = 1 return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。数独总共 81 个单元格,对每个单元格遍历一次,可以看做是常数级的时间复杂度。 - **空间复杂度**:$O(1)$。使用 81 个单位空间,可以看做是常数级的空间复杂度。 ================================================ FILE: docs/solutions/0001-0099/validate-binary-search-tree.md ================================================ # [0098. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [0098. 验证二叉搜索树 - 力扣](https://leetcode.cn/problems/validate-binary-search-tree/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:判断其是否是一个有效的二叉搜索树。 **说明**: - **二叉搜索树特征**: - 节点的左子树只包含小于当前节点的数。 - 节点的右子树只包含大于当前节点的数。 - 所有左子树和右子树自身必须也是二叉搜索树。 - 树中节点数目范围在$[1, 10^4]$ 内。 - $-2^{31} \le Node.val \le 2^{31} - 1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/12/01/tree1.jpg) ```python 输入:root = [2,1,3] 输出:true ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/12/01/tree2.jpg) ```python 输入:root = [5,1,4,null,null,3,6] 输出:false 解释:根节点的值是 5 ,但是右子节点的值是 4 。 ``` ## 解题思路 ### 思路 1:递归遍历 根据题意进行递归遍历即可。前序、中序、后序遍历都可以。 1. 以前序遍历为例,递归函数为:`preorderTraversal(root, min_v, max_v)`。 2. 前序遍历时,先判断根节点的值是否在 `(min_v, max_v)` 之间。 1. 如果不在则直接返回 `False`。 2. 如果在区间内,则继续递归检测左右子树是否满足,都满足才是一棵二叉搜索树。 3. 当递归遍历左子树的时候,要将上界 `max_v` 改为左子树的根节点值,因为左子树上所有节点的值均小于根节点的值。 4. 当递归遍历右子树的时候,要将下界 `min_v` 改为右子树的根节点值,因为右子树上所有节点的值均大于根节点。 ### 思路 1:代码 ```python class Solution: def isValidBST(self, root: TreeNode) -> bool: def preorderTraversal(root, min_v, max_v): if root == None: return True if root.val >= max_v or root.val <= min_v: return False return preorderTraversal(root.left, min_v, root.val) and preorderTraversal(root.right, root.val, max_v) return preorderTraversal(root, float('-inf'), float('inf')) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0001-0099/wildcard-matching.md ================================================ # [0044. 通配符匹配](https://leetcode.cn/problems/wildcard-matching/) - 标签:贪心、递归、字符串、动态规划 - 难度:困难 ## 题目链接 - [0044. 通配符匹配 - 力扣](https://leetcode.cn/problems/wildcard-matching/) ## 题目大意 **描述**:给定一个字符串 `s` 和一个字符模式串 `p`。 **要求**:实现一个支持 `'?'` 和 `'*'` 的通配符匹配。两个字符串完全匹配才算匹配成功。如果匹配成功,则返回 `True`,否则返回 `False`。 - `'?'` 可以匹配任何单个字符。 - `'*'` 可以匹配任意字符串(包括空字符串)。 **说明**: - `s` 可能为空,且只包含从 `a` ~ `z` 的小写字母。 - `p` 可能为空,且只包含从 `a` ~ `z` 的小写字母,以及字符 `'?'` 和 `'*'`。 **示例**: - 示例 1: ```python 输入:s = "aa" p = "a" 输出:False 解释:"a" 无法匹配 "aa" 整个字符串。 ``` - 示例 2: ```python 输入:s = "aa" p = "*" 输出:True 解释:'*' 可以匹配任意字符串。 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照两个字符串的结尾位置进行阶段划分。 ###### 2. 定义状态 定义状态 `dp[i][j]` 表示为:字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配。 ###### 3. 状态转移方程 - 如果 `s[i - 1] == p[j - 1]`,或者 `p[j - 1] == '?'`,则表示字符串 `s` 的第 `i` 个字符与字符串 `p` 的第 `j` 个字符是匹配的。此时「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配」取决于「字符串 `s` 的前 `i - 1` 个字符与字符串 `p` 的前 `j - 1` 个字符是否匹配」。即 `dp[i][j] = dp[i - 1][j - 1] `。 - 如果 `p[j - 1] == '*'`,则字符串 `p` 的第 `j` 个字符可以对应字符串 `s` 中 `0` ~ 若干个字符。则: - 如果当前星号没有匹配当前第 `i` 个字符,则「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配」取决于「字符串 `s` 的前 `i - 1` 个字符与字符串 `p` 的前 `j` 个字符是否匹配」,即 `dp[i][j] = dp[i - 1][j]`。 - 如果当前星号匹配了当前第 `i` 个字符,则「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配」取决于「字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j - 1` 个字符是否匹配」,即 `dp[i][j] = dp[i][j - 1]`。 - 这两种情况只需匹配一种,就视为匹配,所以 `dp[i][j] = dp[i - 1][j] or dp[i][j - 1] `。 则动态转移方程为: $dp[i][j] = \begin{cases} dp[i - 1][j - 1] & s[i - 1] == p[j - 1] \or p[j - 1] == '?' \cr dp[i - 1][j] or dp[i][j - 1] & p[j - 1] == '*' \end{cases}$ ###### 4. 初始条件 - 默认状态下,两个空字符串是匹配的,即 `dp[0][0] = True`。 - 当字符串 `s` 为空,字符串 `p` 开始字符为若干个 `*` 时,两个字符串是匹配的,即 `p[j - 1] == '*'` 时,`dp[0][j] = True`。 ###### 5. 最终结果 根据我们之前定义的状态, `dp[i][j]` 表示为:字符串 `s` 的前 `i` 个字符与字符串 `p` 的前 `j` 个字符是否匹配。则最终结果为 `dp[size_s][size_p]`,其实 `size_s` 是字符串 `s` 的长度,`size_p` 是字符串 `p` 的长度。 ### 思路 1:动态规划代码 ```python class Solution: def isMatch(self, s: str, p: str) -> bool: size_s, size_p = len(s), len(p) dp = [[False for _ in range(size_p + 1)] for _ in range(size_s + 1)] dp[0][0] = True for j in range(1, size_p + 1): if p[j - 1] != '*': break dp[0][j] = True for i in range(1, size_s + 1): for j in range(1, size_p + 1): if s[i - 1] == p[j - 1] or p[j - 1] == '?': dp[i][j] = dp[i - 1][j - 1] elif p[j - 1] == '*': dp[i][j] = dp[i - 1][j] or dp[i][j - 1] return dp[size_s][size_p] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m n)$,其中 $m$ 是字符串 `s` 的长度,$n$ 是字符串 `p` 的长度。使用了两重循环,外层循环遍历的时间复杂度是 $O(m)$,内层循环遍历的时间复杂度是 $O(n)$,所以总体的时间复杂度为 $O(m n)$。 - **空间复杂度**:$O(m n)$,其中 $m$ 是字符串 `s` 的长度,$n$ 是字符串 `p` 的长度。使用了二维数组保存状态,且第一维的空间复杂度为 $O(m)$,第二位的空间复杂度为 $O(n)$,所以总体的空间复杂度为 $O(m n)$。 ================================================ FILE: docs/solutions/0001-0099/word-search.md ================================================ # [0079. 单词搜索](https://leetcode.cn/problems/word-search/) - 标签:数组、回溯、矩阵 - 难度:中等 ## 题目链接 - [0079. 单词搜索 - 力扣](https://leetcode.cn/problems/word-search/) ## 题目大意 **描述**:给定一个 $m \times n$ 大小的二维字符矩阵 $board$ 和一个字符串单词 $word$。 **要求**:如果 $word$ 存在于网格中,返回 `True`,否则返回 `False`。 **说明**: - 单词必须按照字母顺序通过上下左右相邻的单元格字母构成。且同一个单元格内的字母不允许被重复使用。 - $m == board.length$。 - $n == board[i].length$。 - $1 \le m, n \le 6$。 - $1 \le word.length \le 15$。 - $board$ 和 $word$ 仅由大小写英文字母组成。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/04/word2.jpg) ```python 输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED" 输出:true ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/11/04/word-1.jpg) ```python 输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE" 输出:true ``` ## 解题思路 ### 思路 1:回溯算法 使用回溯算法在二维矩阵 $board$ 中按照上下左右四个方向递归搜索。 设函数 `backtrack(i, j, index)` 表示从 $board[i][j]$ 出发,能否搜索到单词字母 $word[index]$,以及 $index$ 位置之后的后缀子串。如果能搜索到,则返回 `True`,否则返回 `False`。 `backtrack(i, j, index)` 执行步骤如下: 1. 如果 $board[i][j] = word[index]$,而且 index 已经到达 word 字符串末尾,则返回 True。 2. 如果 $board[i][j] = word[index]$,而且 index 未到达 word 字符串末尾,则遍历当前位置的所有相邻位置。如果从某个相邻位置能搜索到后缀子串,则返回 True,否则返回 False。 3. 如果 $board[i][j] \ne word[index]$,则当前字符不匹配,返回 False。 ### 思路 1:代码 ```python class Solution: def exist(self, board: List[List[str]], word: str) -> bool: directs = [(0, 1), (0, -1), (1, 0), (-1, 0)] rows = len(board) if rows == 0: return False cols = len(board[0]) visited = [[False for _ in range(cols)] for _ in range(rows)] def backtrack(i, j, index): if index == len(word) - 1: return board[i][j] == word[index] if board[i][j] == word[index]: visited[i][j] = True for direct in directs: new_i = i + direct[0] new_j = j + direct[1] if 0 <= new_i < rows and 0 <= new_j < cols and visited[new_i][new_j] == False: if backtrack(new_i, new_j, index + 1): return True visited[i][j] = False return False for i in range(rows): for j in range(cols): if backtrack(i, j, 0): return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n \times 2^l)$,其中 $m$、$n$ 为二维矩阵 $board$的行数和列数。$l$ 为字符串 $word$ 的长度。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/0001-0099/zigzag-conversion.md ================================================ # [0006. Z 字形变换](https://leetcode.cn/problems/zigzag-conversion/) - 标签:字符串 - 难度:中等 ## 题目链接 - [0006. Z 字形变换 - 力扣](https://leetcode.cn/problems/zigzag-conversion/) ## 题目大意 **描述**: 给定一个字符串 $s$ 和指定行数 $numRows$。 **要求**: 根据给定的行数 $numRows$,以从上往下、从左到右进行 Z 字形排列。 比如输入字符串为 `"PAYPALISHIRING"` 行数为 $3$ 时,排列如下: ``` P A H N A P L S I I G Y I R ``` 之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:`"PAHNAPLSIIGYIR"`。 请你实现这个将字符串进行指定行数变换的函数: `string convert(string s, int numRows);` **说明**: - $1 \le s.length \le 1000$。 - $s$ 由英文字母(小写和大写)、`','` 和 `'.'` 组成。 - $1 \le numRows \le 1000$。 **示例**: - 示例 1: ```python 输入:s = "PAYPALISHIRING", numRows = 3 输出:"PAHNAPLSIIGYIR" ``` - 示例 2: ```python 输入:s = "PAYPALISHIRING", numRows = 4 输出:"PINALSIGYAHRPI" 解释: P I N A L S I G Y A H R P I ``` ## 解题思路 ### 思路 1:模拟法 **核心思想**:按照 Z 字形排列的规律,模拟字符的放置过程,然后按行读取结果。 **算法步骤**: 1. **创建行数组**:创建 $numRows$ 个空字符串数组,用于存储每一行的字符。 2. **模拟 Z 字形放置**: - 使用变量 $row$ 表示当前行,$direction$ 表示移动方向(向下或向上)。 - 遍历字符串 $s$ 中的每个字符,将其放入对应的行中。 - 当到达第 $0$ 行或第 $numRows-1$ 行时,改变移动方向。 3. **按行读取结果**:将所有行的字符串连接起来。 **关键点**: - 当 $numRows = 1$ 时,直接返回原字符串。 - 使用 $direction$ 变量控制行号的变化:向下时 $row$ 递增,向上时 $row$ 递减。 - 在边界处(第 $0$ 行或第 $numRows-1$ 行)改变移动方向。 ### 思路 1:代码 ```python class Solution: def convert(self, s: str, numRows: int) -> str: # 如果只有一行,直接返回原字符串 if numRows == 1: return s # 创建 numRows 个空字符串,用于存储每一行的字符 rows = [""] * numRows row = 0 # 当前行 direction = 1 # 移动方向:1 表示向下,-1 表示向上 # 遍历字符串中的每个字符 for char in s: # 将当前字符添加到对应行 rows[row] += char # 更新行号 row += direction # 如果到达边界,改变移动方向 if row == 0 or row == numRows - 1: direction = -direction # 将所有行的字符串连接起来 return "".join(rows) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。需要遍历字符串中的每个字符一次 - **空间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。需要存储所有字符到行数组中 ================================================ FILE: docs/solutions/0100-0199/balanced-binary-tree.md ================================================ # [0110. 平衡二叉树](https://leetcode.cn/problems/balanced-binary-tree/) - 标签:树、深度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0110. 平衡二叉树 - 力扣](https://leetcode.cn/problems/balanced-binary-tree/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:判断该二叉树是否是高度平衡的二叉树。 **说明**: - **高度平衡二叉树**:二叉树中每个节点的左右两个子树的高度差的绝对值不超过 $1$。 - 树中的节点数在范围 $[0, 5000]$ 内。 - $-10^4 \le Node.val \le 10^4$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/06/balance_1.jpg) ```python 输入:root = [3,9,20,null,null,15,7] 输出:True ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/10/06/balance_2.jpg) ```python 输入:root = [1,2,2,3,3,null,null,4,4] 输出:False ``` ## 解题思路 ### 思路 1:递归遍历 1. 先递归遍历左右子树,判断左右子树是否平衡,再判断以当前节点为根节点的左右子树是否平衡。 2. 如果遍历的子树是平衡的,则返回它的高度,否则返回 -1。 3. 只要出现不平衡的子树,则该二叉树一定不是平衡二叉树。 ### 思路 1:代码 ```python class Solution: def isBalanced(self, root: TreeNode) -> bool: def height(root: TreeNode) -> int: if root == None: return False leftHeight = height(root.left) rightHeight = height(root.right) if leftHeight == -1 or rightHeight == -1 or abs(leftHeight-rightHeight) > 1: return -1 else: return max(leftHeight, rightHeight)+1 return height(root) >= 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-ii.md ================================================ # [0122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) - 标签:贪心、数组、动态规划 - 难度:中等 ## 题目链接 - [0122. 买卖股票的最佳时机 II - 力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) ## 题目大意 **描述**:给定一个整数数组 `prices` ,其中 `prices[i]` 表示某支股票第 `i` 天的价格。在每一天,你可以决定是否购买 / 出售股票。你在任何时候最多只能持有一股股票。你也可以先购买,然后在同一天出售。 **要求**:计算出能获取的最大利润。 **说明**: - $1 \le prices.length \le 3 * 10^4$。 - $0 \le prices[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:prices = [7,1,5,3,6,4] 输出:7 解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。 总利润为 4 + 3 = 7。 ``` - 示例 2: ```python 输入:prices = [1,2,3,4,5] 输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。 总利润为 4 。 ``` ## 解题思路 ### 思路 1:贪心算法 股票买卖获取利润主要是看差价,必然是低点买入,高点卖出才会赚钱。而要想获取最大利润,就要在跌入谷底的时候买入,在涨到波峰的时候卖出利益才会最大化。所以我们购买股票的策略变为了: 1. 连续跌的时候不买。 2. 跌到最低点买入。 3. 涨到最高点卖出。 在这种策略下,只要计算波峰和谷底的差值即可。而波峰和谷底的差值可以通过两两相减所得的差值来累加计算。 ### 思路 1:代码 ```python class Solution: def maxProfit(self, prices: List[int]) -> int: ans = 0 for i in range(1, len(prices)): ans += max(0, prices[i]-prices[i-1]) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 `prices` 的元素个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-iii.md ================================================ # [0123. 买卖股票的最佳时机 III](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [0123. 买卖股票的最佳时机 III - 力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/) ## 题目大意 给定一个数组 `prices` 代表一只股票,其中 `prices[i]` 代表这只股票第 `i` 天的价格。最多可完成两笔交易,且不同同时参与躲避交易(必须在再次购买前出售掉之前的股票)。 现在要求:计算所能获取的最大利润。 ## 解题思路 动态规划求解。 最多可完成两笔交易意味着总共有三种情况:买卖一次,买卖两次,不买卖。 具体到每一天结束总共有 5 种状态: 0. 未进行买卖状态; 1. 第一次买入状态; 2. 第一次卖出状态; 3. 第二次买入状态; 4. 第二次卖出状态。 所以我们可以定义状态 `dp[i][j]` ,表示为:第 `i` 天第 `j` 种情况(`0 <= j <= 4`)下,所获取的最大利润。 注意:这里第第 `j` 种情况,并不一定是这一天一定要买入或卖出,而是这一天所处于的买入卖出状态。比如说前一天是第一次买入,第二天没有操作,则第二天就沿用前一天的第一次买入状态。 接下来确定状态转移公式: - 第 `0` 种状态下显然利润为 `0`,可以直接赋值为昨天获取的最大利润,即 `dp[i][0] = dp[i - 1][0]`。 - 第 `1` 种状态下可以有两种状态推出,取最大的那一种赋值: - 不做任何操作,直接沿用前一天买入状态所得的最大利润:`dp[i][1] = dp[i - 1][1]`。 - 第一次买入:`dp[i][1] = dp[i - 1][0] - prices[i]`。 - 第 `2` 种状态下可以有两种状态推出,取最大的那一种赋值: - 不做任何操作,直接沿用前一天卖出状态所得的最大利润:`dp[i][2] = dp[i - 1][2]`。 - 第一次卖出:`dp[i][2] = dp[i - 1][1] + prices[i]`。 - 第 `3` 种状态下可以有两种状态推出,取最大的那一种赋值: - 不做任何操作,直接沿用前一天买入状态所得的最大利润:`dp[i][3] = dp[i - 1][3]`。 - 第二次买入:`dp[i][3] = dp[i - 1][2] - prices[i]`。 - 第 `4` 种状态下可以有两种状态推出,取最大的那一种赋值: - 不做任何操作,直接沿用前一天卖出状态所得的最大利润:`dp[i][4] = dp[i - 1][4]`。 - 第二次卖出:`dp[i][4] = dp[i - 1][3] + prices[i]`。 下面确定初始化的边界值: 可以很明显看出第一天不做任何操作就是 `dp[0][0] = 0`,第一次买入就是 `dp[0][1] = -prices[i]`。 第一次卖出的话,可以视作为没有盈利(当天买卖,价格没有变化),即 `dp[0][2] = 0`。第二次买入的话,就是 `dp[0][3] = -prices[i]`。同理第二次卖出就是 `dp[0][4] = 0`。 在递推结束后,最大利润肯定是无操作、第一次卖出、第二次卖出这三种情况里边,且为最大值。我们在维护的时候维护的是最大值,则第一次卖出、第二次卖出所获得的利润肯定大于等于 0。而且,如果最优情况为一笔交易,那么在转移状态时,我们允许在一天内进行两次交易,则一笔交易的状态可以转移至两笔交易。所以最终答案为 `dp[size - 1][4]`。`size` 为股票天数。 ## 代码 ```python class Solution: def maxProfit(self, prices: List[int]) -> int: size = len(prices) if size == 0: return 0 dp = [[0 for _ in range(5)] for _ in range(size)] dp[0][1] = -prices[0] dp[0][3] = -prices[0] for i in range(1, size): dp[i][0] = dp[i - 1][0] dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]) dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]) dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]) dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]) return dp[size - 1][4] ``` ================================================ FILE: docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-iv.md ================================================ # [0188. 买卖股票的最佳时机 IV](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [0188. 买卖股票的最佳时机 IV - 力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/) ## 题目大意 给定一个数组 `prices` 代表一只股票,其中 `prices[i]` 代表这只股票第 `i` 天的价格。再给定一个整数 `k`,表示最多可完成 `k` 笔交易,且不能同时参与多笔交易(必须在再次购买前出售掉之前的股票)。 现在要求:计算所能获取的最大利润。 ## 解题思路 动态规划求解。这道题是「[0123. 买卖股票的最佳时机 III](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/)」的升级版,不过思路一样 最多可完成两笔交易意味着总共有三种情况:买卖一次,买卖两次,不买卖。 具体到每一天结束总共有 `2 * k + 1` 种状态: 0. 未进行买卖状态; 1. 第 `1` 次买入状态; 2. 第 `1` 次卖出状态; 3. 第 `2` 次买入状态; 4. 第 `2` 次卖出状态。 5. ... 6. 第 `m` 次买入状态。 7. 第 `m` 次卖出状态。 因为买入、卖出为两种状态,干脆我们直接让偶数序号表示买入状态,奇数序号表示卖出状态。 所以我们可以定义状态 `dp[i][j]` ,表示为:第 `i` 天第 `j` 种情况(`0 <= j <= 2 * k`)下,所获取的最大利润。 注意:这里第 `j` 种情况,并不一定是这一天一定要买入或卖出,而是这一天所处于的买入卖出状态。比如说前一天是第一次买入,第二天没有操作,则第二天就沿用前一天的第一次买入状态。 接下来确定状态转移公式: - 第 `0` 种状态下显然利润为 `0`,可以直接赋值为昨天获取的最大利润,即 `dp[i][0] = dp[i - 1][0]`。 - 第 `1` 次买入状态下可以有两种状态推出,取最大的那一种赋值: - 不做任何操作,直接沿用前一天买入状态所得的最大利润:`dp[i][1] = dp[i - 1][1]`。 - 第 `1` 次买入:`dp[i][1] = dp[i - 1][0] - prices[i]`。 - 第 `1` 次卖出状态下可以有两种状态推出,取最大的那一种赋值: - 不做任何操作,直接沿用前一天卖出状态所得的最大利润:`dp[i][2] = dp[i - 1][2]`。 - 第 `1` 次卖出:`dp[i][2] = dp[i - 1][1] + prices[i]`。 - 第 `2` 次买入状态下可以有两种状态推出,取最大的那一种赋值: - 不做任何操作,直接沿用前一天买入状态所得的最大利润:`dp[i][3] = dp[i - 1][3]`。 - 第 `2` 次买入:`dp[i][3] = dp[i - 1][2] - prices[i]`。 - 第 `2` 次卖出状态下可以有两种状态推出,取最大的那一种赋值: - 不做任何操作,直接沿用前一天卖出状态所得的最大利润:`dp[i][4] = dp[i - 1][4]`。 - 第 `2` 次卖出:`dp[i][4] = dp[i - 1][3] + prices[i]`。 - ... - 第 `m` 次(`j = 2 * m`)买入状态下可以有两种状态推出,取最大的那一种赋值: - 不做任何操作,直接沿用前一天卖出状态所得的最大利润:`dp[i][j] = dp[i - 1][j]`。 - 第 `m` 次买入:`dp[i][j] = dp[i - 1][j - 1] - prices[i]`。 - 第 `m` 次(`j = 2 * m + 1`)卖出状态下可以有两种状态推出,取最大的那一种赋值: - 不做任何操作,直接沿用前一天卖出状态所得的最大利润:`dp[i][j] = dp[i - 1][j]`。 - 第 `m` 次卖出:`dp[i][j] = dp[i - 1][j - 1] + prices[i]`。 下面确定初始化的边界值: 可以很明显看出第一天不做任何操作就是 `dp[0][0] = 0`,第 `m` 次买入(`j = 2 * m`)就是 `dp[0][j] = -prices[i]`。 第 `m` 次(`j = 2 * m + 1`)卖出的话,可以视作为没有盈利(当天买卖,价格没有变化),即 `dp[0][j] = 0`。 在递推结束后,最大利润肯定是无操作、第 `m` 次卖出这几种种情况里边,且为最大值。我们在维护的时候维护的是最大值,则第 `m` 次卖出所获得的利润肯定大于等于 0。而且,如果最优情况为 `m - 1` 笔交易,那么在转移状态时,我们允许在一天内进行多次交易,则 `m - 1` 笔交易的状态可以转移至 `m` 笔交易,最终都可以转移至 `k` 比交易。 所以最终答案为 `dp[size - 1][2 * k]`。`size` 为股票天数。 ## 代码 ```python class Solution: def maxProfit(self, k: int, prices: List[int]) -> int: size = len(prices) if size == 0: return 0 dp = [[0 for _ in range(2 * k + 1)] for _ in range(size)] for j in range(1, 2 * k, 2): dp[0][j] = -prices[0] for i in range(1, size): for j in range(1, 2 * k + 1): if j % 2 == 1: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]) else: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] + prices[i]) return dp[size - 1][2 * k] ``` ================================================ FILE: docs/solutions/0100-0199/best-time-to-buy-and-sell-stock.md ================================================ # [0121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/) - 标签:数组、动态规划 - 难度:简单 ## 题目链接 - [0121. 买卖股票的最佳时机 - 力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/) ## 题目大意 **描述**:给定一个数组 `prices` ,它的第 `i` 个元素 `prices[i]` 表示一支给定股票第 `i` 天的价格。只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。 **要求**:计算出能获取的最大利润。如果你不能获取任何利润,返回 $0$。 **说明**: - $1 \le prices.length \le 10^5$。 - $0 \le prices[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:[7,1,5,3,6,4] 输出:5 解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。 ``` - 示例 2: ```python 输入:prices = [7,6,4,3,1] 输出:0 解释:在这种情况下, 没有交易完成, 所以最大利润为 0。 ``` ## 解题思路 最简单的思路当然是两重循环暴力枚举,寻找不同天数下的最大利润。但更好的做法是进行一次遍历,递推求解。 ### 思路 1:递推 1. 设置两个变量 `minprice`(用来记录买入的最小值)、`maxprofit`(用来记录可获取的最大利润)。 2. 从左到右进行遍历数组 `prices`。 3. 如果遇到当前价格比 `minprice` 还要小的,就更新 `minprice`。 4. 如果遇到当前价格大于或者等于 `minprice`,则判断一下以当前价格卖出的话能卖多少,如果比 `maxprofit` 还要大,就更新 `maxprofit`。 5. 最后输出 `maxprofit`。 ### 思路 1:代码 ```python class Solution: def maxProfit(self, prices: List[int]) -> int: minprice = 10010 maxprofit = 0 for price in prices: if price < minprice: minprice = price elif price - minprice > maxprofit: maxprofit = price - minprice return maxprofit ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 `prices` 的元素个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/binary-search-tree-iterator.md ================================================ # [0173. 二叉搜索树迭代器](https://leetcode.cn/problems/binary-search-tree-iterator/) - 标签:栈、树、设计、二叉搜索树、二叉树、迭代器 - 难度:中等 ## 题目链接 - [0173. 二叉搜索树迭代器 - 力扣](https://leetcode.cn/problems/binary-search-tree-iterator/) ## 题目大意 **要求**:实现一个二叉搜索树的迭代器 BSTIterator。表示一个按中序遍历二叉搜索树(BST)的迭代器: - `def __init__(self, root: TreeNode):`:初始化 BSTIterator 类的一个对象,会给出二叉搜索树的根节点。 - `def hasNext(self) -> bool:`:如果向右指针遍历存在数字,则返回 True,否则返回 False。 - `def next(self) -> int:`:将指针向右移动,返回指针处的数字。 **说明**: - 指针初始化为一个不存在于 BST 中的数字,所以对 `next()` 的首次调用将返回 BST 中的最小元素。 - 可以假设 `next()` 调用总是有效的,也就是说,当调用 `next()` 时,BST 的中序遍历中至少存在一个下一个数字。 - 树中节点的数目在范围 $[1, 10^5]$ 内。 - $0 \le Node.val \le 10^6$。 - 最多调用 $10^5$ 次 `hasNext` 和 `next` 操作。 - 进阶:设计一个满足下述条件的解决方案,`next()` 和 `hasNext()` 操作均摊时间复杂度为 `O(1)` ,并使用 `O(h)` 内存。其中 `h` 是树的高度。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2018/12/25/bst-tree.png) ```python 输入 ["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"] [[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []] 输出 [null, 3, 7, true, 9, true, 15, true, 20, false] ``` ## 解题思路 ### 思路 1:中序遍历二叉搜索树 中序遍历的顺序是:左、根、右。我们使用一个栈来保存节点,以便于迭代的时候取出对应节点。 - 初始的遍历当前节点的左子树,将其路径上的节点存储到栈中。 - 调用 next 方法的时候,从栈顶取出节点,因为之前已经将路径上的左子树全部存入了栈中,所以此时该节点的左子树为空,这时候取出节点右子树,再将右子树的左子树进行递归遍历,并将其路径上的节点存储到栈中。 - 调用 hasNext 的方法的时候,直接判断栈中是否有值即可。 ### 思路 1:代码 ```python class BSTIterator: def __init__(self, root: TreeNode): self.stack = [] self.in_order(root) def in_order(self, node): while node: self.stack.append(node) node = node.left def next(self) -> int: node = self.stack.pop() if node.right: self.in_order(node.right) return node.val def hasNext(self) -> bool: return len(self.stack) != 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为树中节点数量。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/binary-tree-level-order-traversal-ii.md ================================================ # [0107. 二叉树的层序遍历 II](https://leetcode.cn/problems/binary-tree-level-order-traversal-ii/) - 标签:树、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0107. 二叉树的层序遍历 II - 力扣](https://leetcode.cn/problems/binary-tree-level-order-traversal-ii/) ## 题目大意 **描述**:给定一个二叉树的根节点 $root$。 **要求**:返回其节点值按照「自底向上」的「层序遍历」(即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)。 **说明**: - 树中节点数目在范围 $[0, 2000]$ 内。 - $-1000 \le Node.val \le 1000$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/19/tree1.jpg) ```python 输入:root = [3,9,20,null,null,15,7] 输出:[[15,7],[9,20],[3]] ``` - 示例 2: ```python 输入:root = [1] 输出:[[1]] ``` ## 解题思路 ### 思路 1:二叉树的层次遍历 先得到层次遍历的节点顺序,再将其进行反转返回即可。 其中层次遍历用到了广度优先搜索,不过需要增加一些变化。普通广度优先搜索只取一个元素,变化后的广度优先搜索每次取出第 i 层上所有元素。 具体步骤如下: 1. 根节点入队。 2. 当队列不为空时,求出当前队列长度 $s_i$。 3. 依次从队列中取出这 $s_i$ 个元素,将其左右子节点入队,然后继续迭代。 4. 当队列为空时,结束。 ### 思路 1:代码 ```python class Solution: def levelOrderBottom(self, root: TreeNode) -> List[List[int]]: if not root: return [] queue = [root] order = [] while queue: level = [] size = len(queue) for _ in range(size): curr = queue.pop(0) level.append(curr.val) if curr.left: queue.append(curr.left) if curr.right: queue.append(curr.right) if level: order.append(level) return order[::-1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为树中节点个数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/binary-tree-level-order-traversal.md ================================================ # [0102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/) - 标签:树、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0102. 二叉树的层序遍历 - 力扣](https://leetcode.cn/problems/binary-tree-level-order-traversal/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:返回该二叉树按照「层序遍历」得到的节点值。 **说明**: - 返回结果为二维数组,每一层都要存为数组返回。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/19/tree1.jpg) ```python 输入:root = [3,9,20,null,null,15,7] 输出:[[3],[9,20],[15,7]] ``` - 示例 2: ```python 输入:root = [1] 输出:[[1] ``` ## 解题思路 ### 思路 1:广度优先搜索 广度优先搜索,需要增加一些变化。普通广度优先搜索只取一个元素,变化后的广度优先搜索每次取出第 $i$ 层上所有元素。 具体步骤如下: 1. 判断二叉树是否为空,为空则直接返回。 2. 令根节点入队。 3. 当队列不为空时,求出当前队列长度 $s_i$。 4. 依次从队列中取出这 $s_i$ 个元素,并对这 $s_i$ 个元素依次进行访问。然后将其左右孩子节点入队,然后继续遍历下一层节点。 5. 当队列为空时,结束遍历。 ### 思路 1:代码 ```python class Solution: def levelOrder(self, root: TreeNode) -> List[List[int]]: if not root: return [] queue = [root] order = [] while queue: level = [] size = len(queue) for _ in range(size): curr = queue.pop(0) level.append(curr.val) if curr.left: queue.append(curr.left) if curr.right: queue.append(curr.right) if level: order.append(level) return order ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/binary-tree-maximum-path-sum.md ================================================ # [0124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) - 标签:树、深度优先搜索、动态规划、二叉树 - 难度:困难 ## 题目链接 - [0124. 二叉树中的最大路径和 - 力扣](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) ## 题目大意 **描述**:给定一个二叉树的根节点 $root$。 **要求**:返回其最大路径和。 **说明**: - **路径**:被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中至多出现一次。该路径至少包含一个节点,且不一定经过根节点。 - **路径和**:路径中各节点值的总和。 - 树中节点数目范围是 $[1, 3 * 10^4]$。 - $-1000 \le Node.val \le 1000$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/13/exx1.jpg) ```python 输入:root = [1,2,3] 输出:6 解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/10/13/exx2.jpg) ```python 输入:root = [-10,9,20,null,null,15,7] 输出:42 解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42 ``` ## 解题思路 ### 思路 1:树形 DP + 深度优先搜索 根据最大路径和中对应路径是否穿过根节点,我们可以将二叉树分为两种: 1. 最大路径和中对应路径穿过根节点。 2. 最大路径和中对应路径不穿过根节点。 如果最大路径和中对应路径穿过根节点,则:**该二叉树的最大路径和 = 左子树中最大贡献值 + 右子树中最大贡献值 + 当前节点值**。 而如果最大路径和中对应路径不穿过根节点,则:**该二叉树的最大路径和 = 所有子树中最大路径和**。 即:**该二叉树的最大路径和 = max(左子树中最大贡献值 + 右子树中最大贡献值 + 当前节点值,所有子树中最大路径和)**。 对此我们可以使用深度优先搜索递归遍历二叉树,并在递归遍历的同时,维护一个最大路径和变量 $ans$。 然后定义函数 ` def dfs(self, node):` 计算二叉树中以该节点为根节点,并且经过该节点的最大贡献值。 计算的结果可能的情况有 $2$ 种: 1. 经过空节点的最大贡献值等于 $0$。 2. 经过非空节点的最大贡献值等于 **当前节点值 + 左右子节点提供的最大贡献值中较大的一个**。如果该贡献值为负数,可以考虑舍弃,即最大贡献值为 $0$。 在递归时,我们先计算左右子节点的最大贡献值,再更新维护当前最大路径和变量。最终 $ans$ 即为答案。具体步骤如下: 1. 如果根节点 $root$ 为空,则返回 $0$。 2. 递归计算左子树的最大贡献值为 $left\_max$。 3. 递归计算右子树的最大贡献值为 $right\_max$。 4. 更新维护最大路径和变量,即 $self.ans = max \lbrace self.ans, \quad left\_max + right\_max + node.val \rbrace$。 5. 返回以当前节点为根节点,并且经过该节点的最大贡献值。即返回 **当前节点值 + 左右子节点提供的最大贡献值中较大的一个**。 6. 最终 $self.ans$ 即为答案。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def __init__(self): self.ans = float('-inf') def dfs(self, node): if not node: return 0 left_max = max(self.dfs(node.left), 0) # 左子树提供的最大贡献值 right_max = max(self.dfs(node.right), 0) # 右子树提供的最大贡献值 cur_max = left_max + right_max + node.val # 包含当前节点和左右子树的最大路径和 self.ans = max(self.ans, cur_max) # 更新所有路径中的最大路径和 return max(left_max, right_max) + node.val # 返回包含当前节点的子树的最大贡献值 def maxPathSum(self, root: Optional[TreeNode]) -> int: self.dfs(root) return self.ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/binary-tree-postorder-traversal.md ================================================ # [0145. 二叉树的后序遍历](https://leetcode.cn/problems/binary-tree-postorder-traversal/) - 标签:栈、树、深度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0145. 二叉树的后序遍历 - 力扣](https://leetcode.cn/problems/binary-tree-postorder-traversal/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:返回该二叉树的后序遍历结果。 **说明**: - 树中节点数目在范围 $[0, 100]$ 内。 - $-100 \le Node.val \le 100$。 **示例**: - 示例 1: ![img](https://assets.leetcode.com/uploads/2020/08/28/pre1.jpg) ```python 输入:root = [1,null,2,3] 输出:[3,2,1] ``` - 示例 2: ```python 输入:root = [] 输出:[] ``` ## 解题思路 ### 思路 1:递归遍历 二叉树的后序遍历递归实现步骤为: 1. 判断二叉树是否为空,为空则直接返回。 2. 先递归遍历左子树。 3. 然后递归遍历右子树。 4. 最后访问根节点。 ### 思路 1:代码 ```python class Solution: def postorderTraversal(self, root: TreeNode) -> List[int]: res = [] def postorder(root): if not root: return postorder(root.left) postorder(root.right) res.append(root.val) postorder(root) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。 ### 思路 2:模拟栈迭代遍历 我们可以使用一个显式栈 `stack` 来模拟二叉树的后序遍历递归的过程。 与前序、中序遍历不同,在后序遍历中,根节点的访问要放在左右子树访问之后。因此,我们要保证:**在左右孩子节点访问结束之前,当前节点不能提前出栈**。 我们应该从根节点开始,先将根节点放入栈中,然后依次遍历左子树,不断将当前子树的根节点放入栈中,直到遍历到左子树最左侧的那个节点,从栈中弹出该元素,并判断该元素的右子树是否已经访问完毕,如果访问完毕,则访问该元素。如果未访问完毕,则访问该元素的右子树。 二叉树的后序遍历显式栈实现步骤如下: 1. 判断二叉树是否为空,为空则直接返回。 2. 初始化维护一个空栈,使用 `prev` 保存前一个访问的节点,用于确定当前节点的右子树是否访问完毕。 3. 当根节点或者栈不为空时,从当前节点开始: 1. 如果当前节点有左子树,则不断遍历左子树,并将当前根节点压入栈中。 2. 如果当前节点无左子树,则弹出栈顶元素 `node`。 3. 如果栈顶元素 `node` 无右子树(即 `not node.right`)或者右子树已经访问完毕(即 `node.right == prev`),则访问该元素,然后记录前一节点,并将当前节点标记为空节点。 4. 如果栈顶元素有右子树,则将栈顶元素重新压入栈中,继续访问栈顶元素的右子树。 ### 思路 2:代码 ```python class Solution: def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]: res = [] stack = [] prev = None # 保存前一个访问的节点,用于确定当前节点的右子树是否访问完毕 while root or stack: # 根节点或栈不为空 while root: stack.append(root) # 将当前树的根节点入栈 root = root.left # 继续访问左子树,找到最左侧节点 node = stack.pop() # 遍历到最左侧,当前节点无左子树时,将最左侧节点弹出 # 如果当前节点无右子树或者右子树访问完毕 if not node.right or node.right == prev: res.append(node.val)# 访问该节点 prev = node # 记录前一节点 root = None # 将当前根节点标记为空 else: stack.append(node) # 右子树尚未访问完毕,将当前节点重新压回栈中 root = node.right # 继续访问右子树 return res ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/binary-tree-preorder-traversal.md ================================================ # [0144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) - 标签:栈、树、深度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0144. 二叉树的前序遍历 - 力扣](https://leetcode.cn/problems/binary-tree-preorder-traversal/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:返回该二叉树的前序遍历结果。 **说明**: - 树中节点数目在范围 $[0, 100]$ 内。 - $-100 \le Node.val \le 100$。 **示例**: - 示例 1: ![img](https://assets.leetcode.com/uploads/2020/09/15/inorder_1.jpg) ```python 输入:root = [1,null,2,3] 输出:[1,2,3] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/09/15/inorder_4.jpg) ```python 输入:root = [1,null,2] 输出:[1,2] ``` ## 解题思路 ### 思路 1:递归遍历 二叉树的前序遍历递归实现步骤为: 1. 判断二叉树是否为空,为空则直接返回。 2. 先访问根节点。 3. 然后递归遍历左子树。 4. 最后递归遍历右子树。 ### 思路 1:代码 ```python class Solution: def preorderTraversal(self, root: TreeNode) -> List[int]: res = [] def preorder(root): if not root: return res.append(root.val) preorder(root.left) preorder(root.right) preorder(root) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。 ### 思路 2:模拟栈迭代遍历 二叉树的前序遍历递归实现的过程,实际上就是调用系统栈的过程。我们也可以使用一个显式栈 `stack` 来模拟递归的过程。 前序遍历的顺序为:根节点 - 左子树 - 右子树,而根据栈的「先入后出」特点,所以入栈的顺序应该为:先放入右子树,再放入左子树。这样可以保证最终为前序遍历顺序。 二叉树的前序遍历显式栈实现步骤如下: 1. 判断二叉树是否为空,为空则直接返回。 2. 初始化维护一个栈,将根节点入栈。 3. 当栈不为空时: 1. 弹出栈顶元素 `node`,并访问该元素。 2. 如果 `node` 的右子树不为空,则将 `node` 的右子树入栈。 3. 如果 `node` 的左子树不为空,则将 `node` 的左子树入栈。 ### 思路 2:代码 ```python class Solution: def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]: if not root: # 二叉树为空直接返回 return [] res = [] stack = [root] while stack: # 栈不为空 node = stack.pop() # 弹出根节点 res.append(node.val) # 访问根节点 if node.right: stack.append(node.right) # 右子树入栈 if node.left: stack.append(node.left) # 左子树入栈 return res ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/binary-tree-right-side-view.md ================================================ # [0199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0199. 二叉树的右视图 - 力扣](https://leetcode.cn/problems/binary-tree-right-side-view/) ## 题目大意 **描述**:给定一棵二叉树的根节点 `root`。 **要求**:按照从顶部到底部的顺序,返回从右侧能看到的节点值。 **说明**: - 二叉树的节点个数的范围是 $[0,100]$。 - $-100 \le Node.val \le 100$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/14/tree.jpg) ```python 输入: [1,2,3,null,5,null,4] 输出: [1,3,4] ``` - 示例 2: ```python 输入: [1,null,3] 输出: [1,3] ``` ## 解题思路 ### 思路 1:广度优先搜索 使用广度优先搜索对二叉树进行层次遍历。在遍历每层节点的时候,只需要将最后一个节点加入结果数组即可。 ### 思路 1:代码 ```python class Solution: def rightSideView(self, root: TreeNode) -> List[int]: if not root: return [] queue = [root] order = [] while queue: size = len(queue) for i in range(size): curr = queue.pop(0) if curr.left: queue.append(curr.left) if curr.right: queue.append(curr.right) if i == size - 1: order.append(curr.val) return order ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/binary-tree-upside-down.md ================================================ # [0156. 上下翻转二叉树](https://leetcode.cn/problems/binary-tree-upside-down/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0156. 上下翻转二叉树 - 力扣](https://leetcode.cn/problems/binary-tree-upside-down/) ## 题目大意 **描述**: 给定一个二叉树的根节点 $root$。 **要求**: 将此二叉树上下翻转,并返回新的根节点。 你可以按下面的步骤翻转一棵二叉树: 1. 原来的左子节点变成新的根节点 2. 原来的根节点变成新的右子节点 3. 原来的右子节点变成新的左子节点 ![](https://assets.leetcode.com/uploads/2020/08/29/main.jpg) 上面的步骤逐层进行。题目数据保证每个右节点都有一个同级节点(即共享同一父节点的左节点)且不存在子节点。 **说明**: - 树中节点数目在范围 $[0, 10]$ 内。 - $1 \le Node.val \le 10$。 - 树中的每个右节点都有一个同级节点(即共享同一父节点的左节点)。 - 树中的每个右节点都没有子节点。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/08/29/updown.jpg) ```python 输入:root = [1,2,3,4,5] 输出:[4,5,2,null,null,3,1] ``` - 示例 2: ```python 输入:root = [] 输出:[] ``` ## 解题思路 ### 思路 1:递归 根据题目描述,我们需要将二叉树进行上下翻转。观察翻转规则: 1. 原来的左子节点变成新的根节点 2. 原来的根节点变成新的右子节点 3. 原来的右子节点变成新的左子节点 这是一个递归问题,我们可以采用以下策略: 1. **递归终止条件**:如果当前节点 $root$ 为空或者没有左子节点,直接返回当前节点。 2. **递归处理**:对左子树进行翻转,得到新的根节点 $new\_root$。 3. **重新连接**: - 将原根节点 $root$ 作为新根节点 $new\_root$ 的右子节点 - 将原右子节点 $root.right$ 作为新根节点 $new\_root$ 的左子节点 - 将原根节点 $root$ 的左右子节点设为 $null$,避免循环引用 **关键点**: - 翻转后的新根节点是原树最左边的叶子节点 - 需要保存原根节点和右子节点,用于重新连接 - 翻转是逐层进行的,每层都遵循相同的翻转规则 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def upsideDownBinaryTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]: # 递归终止条件:空节点或没有左子节点 if not root or not root.left: return root # 保存原根节点和右子节点 original_root = root original_right = root.right # 递归处理左子树,得到新的根节点 new_root = self.upsideDownBinaryTree(root.left) # 重新连接节点 # 原左子节点(现在是新根节点)的左子节点指向原右子节点 root.left.left = original_right # 原左子节点(现在是新根节点)的右子节点指向原根节点 root.left.right = original_root # 将原根节点的左右子节点设为 None,避免循环引用 original_root.left = None original_root.right = None return new_root ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(h)$,其中 $h$ 是树的高度。我们需要递归到树的最左边叶子节点,递归深度等于树的高度。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度。递归调用栈的深度等于树的高度。 ================================================ FILE: docs/solutions/0100-0199/binary-tree-zigzag-level-order-traversal.md ================================================ # [0103. 二叉树的锯齿形层序遍历](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/) - 标签:树、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0103. 二叉树的锯齿形层序遍历 - 力扣](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:返回其节点值的锯齿形层序遍历结果。 **说明**: - **锯齿形层序遍历**:从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/19/tree1.jpg) ```python 输入:root = [3,9,20,null,null,15,7] 输出:[[3],[20,9],[15,7]] ``` - 示例 2: ```python 输入:root = [1] 输出:[[1]] ``` ## 解题思路 ### 思路 1:广度优先搜索 在二叉树的层序遍历的基础上需要增加一些变化。 普通广度优先搜索只取一个元素,变化后的广度优先搜索每次取出第 `i` 层上所有元素。 新增一个变量 `odd`,用于判断当前层数是奇数层,还是偶数层。从而判断元素遍历方向。 存储每层元素的 `level` 列表改用双端队列,如果是奇数层,则从末尾添加元素。如果是偶数层,则从头部添加元素。 具体步骤如下: 1. 使用列表 `order` 存放锯齿形层序遍历结果,使用整数 `odd` 变量用于判断奇偶层,使用双端队列 `level` 存放每层元素,使用列表 `queue` 用于进行广度优先搜索。 2. 将根节点放入入队列中,即 `queue = [root]`。 3. 当队列 `queue` 不为空时,求出当前队列长度 $s_i$,并判断当前层数的奇偶性。 4. 依次从队列中取出这 $s_i$ 个元素。 1. 如果当前层为奇数层,如果是奇数层,则从 `level` 末尾添加元素。 2. 如果当前层是偶数层,则从 `level` 头部添加元素。 3. 然后将当前元素的左右子节点加入队列 `queue` 中,然后继续迭代。 5. 将存储当前层元素的 `level` 存入答案列表 `order` 中。 6. 当队列为空时,结束。返回锯齿形层序遍历结果 `order`。 ### 思路 1:代码 ```python import collections class Solution: def zigzagLevelOrder(self, root: TreeNode) -> List[List[int]]: if not root: return [] queue = [root] order = [] odd = True while queue: level = collections.deque() size = len(queue) for _ in range(size): curr = queue.pop(0) if odd: level.append(curr.val) else: level.appendleft(curr.val) if curr.left: queue.append(curr.left) if curr.right: queue.append(curr.right) if level: order.append(list(level)) odd = not odd return order ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/candy.md ================================================ # [0135. 分发糖果](https://leetcode.cn/problems/candy/) - 标签:贪心、数组 - 难度:困难 ## 题目链接 - [0135. 分发糖果 - 力扣](https://leetcode.cn/problems/candy/) ## 题目大意 **描述**:$n$ 个孩子站成一排。老师会根据每个孩子的表现,给每个孩子进行评分。然后根据下面的规则给孩子们分发糖果: - 每个孩子至少得 $1$ 个糖果。 - 评分更高的孩子必须比他两侧相邻位置上的孩子分得更多的糖果。 现在给定 $n$ 个孩子的表现分数数组 `ratings`,其中 `ratings[i]` 表示第 $i$ 个孩子的评分。 **要求**:返回最少需要准备的糖果数目。 **说明**: - $n == ratings.length$。 - $1 \le n \le 2 \times 10^4$。 - $0 \le ratings[i] \le 2 * 10^4$。 **示例**: - 示例 1: ```python 输入:ratings = [1,0,2] 输出:5 解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。 ``` - 示例 2: ```python 输入:ratings = [1,2,2] 输出:4 解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。 第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。 ``` ## 解题思路 ### 思路 1:贪心算法 先来看分发糖果的规则。 「每个孩子至少得 1 个糖果」:说明糖果数目至少为 N 个。 「评分更高的孩子必须比他两侧相邻位置上的孩子分得更多的糖果」:可以看做为以下两种条件: - 当 $ratings[i - 1] < ratings[i]$ 时,第 i 个孩子的糖果数量比第 $i - 1$ 个孩子的糖果数量多; - 当 $ratings[i] > ratings[i + 1]$ 时,第 i 个孩子的糖果数量比第$ i + 1$ 个孩子的糖果数量多。 根据以上信息,我们可以设定一个长度为 N 的数组 sweets 来表示每个孩子分得的最少糖果数,初始每个孩子分得糖果数都为 1。 然后遍历两遍数组,第一遍遍历满足当 $ratings[i - 1] < ratings[i]$ 时,第 $i$ 个孩子的糖果数量比第 $i - 1$ 个孩子的糖果数量多 $1$ 个。第二遍遍历满足当 $ratings[i] > ratings[i + 1]$ 时,第 $i$ 个孩子的糖果数量取「第 $i$ 个孩子目前拥有的糖果数量」和「第 $i + 1$ 个孩子的糖果数量加 $1$」中的最大值。 然后再遍历求所有孩子的糖果数量和即为答案。 ### 思路 1:代码 ```python class Solution: def candy(self, ratings: List[int]) -> int: size = len(ratings) sweets = [1 for _ in range(size)] for i in range(1, size): if ratings[i] > ratings[i - 1]: sweets[i] = sweets[i - 1] + 1 for i in range(size - 2, -1, -1): if ratings[i] > ratings[i + 1]: sweets[i] = max(sweets[i], sweets[i + 1] + 1) res = sum(sweets) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 `ratings` 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/clone-graph.md ================================================ # [0133. 克隆图](https://leetcode.cn/problems/clone-graph/) - 标签:深度优先搜索、广度优先搜索、图、哈希表 - 难度:中等 ## 题目链接 - [0133. 克隆图 - 力扣](https://leetcode.cn/problems/clone-graph/) ## 题目大意 **描述**:以每个节点的邻接列表形式(二维列表)给定一个无向连通图,其中 $adjList[i]$ 表示值为 $i + 1$ 的节点的邻接列表,$adjList[i][j]$ 表示值为 $i + 1$ 的节点与值为 $adjList[i][j]$ 的节点有一条边。 **要求**:返回该图的深拷贝。 **说明**: - 节点数不超过 $100$。 - 每个节点值 $Node.val$ 都是唯一的,$1 \le Node.val \le 100$。 - 无向图是一个简单图,这意味着图中没有重复的边,也没有自环。 - 由于图是无向的,如果节点 $p$ 是节点 $q$ 的邻居,那么节点 $q$ 也必须是节点 $p$ 的邻居。 - 图是连通图,你可以从给定节点访问到所有节点。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/02/01/133_clone_graph_question.png) ```python 输入:adjList = [[2,4],[1,3],[2,4],[1,3]] 输出:[[2,4],[1,3],[2,4],[1,3]] 解释: 图中有 4 个节点。 节点 1 的值是 1,它有两个邻居:节点 2 和 4 。 节点 2 的值是 2,它有两个邻居:节点 1 和 3 。 节点 3 的值是 3,它有两个邻居:节点 2 和 4 。 节点 4 的值是 4,它有两个邻居:节点 1 和 3 。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/02/01/graph-1.png) ```python 输入:adjList = [[2],[1]] 输出:[[2],[1]] ``` ## 解题思路 所谓深拷贝,就是构建一张与原图结构、值均一样的图,但是所用的节点不再是原图节点的引用,即每个节点都要新建。 可以用深度优先搜索或者广度优先搜索来做。 ### 思路 1:深度优先搜索 1. 使用哈希表 $visitedDict$ 来存储原图中被访问过的节点和克隆图中对应节点,键值对为「原图被访问过的节点:克隆图中对应节点」。 2. 从给定节点开始,以深度优先搜索的方式遍历原图。 1. 如果当前节点被访问过,则返回隆图中对应节点。 2. 如果当前节点没有被访问过,则创建一个新的节点,并保存在哈希表中。 3. 遍历当前节点的邻接节点列表,递归调用当前节点的邻接节点,并将其放入克隆图中对应节点。 3. 递归结束,返回克隆节点。 ### 思路 1:代码 ```python class Solution: def cloneGraph(self, node: 'Node') -> 'Node': if not node: return node visited = dict() def dfs(node: 'Node') -> 'Node': if node in visited: return visited[node] clone_node = Node(node.val, []) visited[node] = clone_node for neighbor in node.neighbors: clone_node.neighbors.append(dfs(neighbor)) return clone_node return dfs(node) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 为图中节点数量。 - **空间复杂度**:$O(n)$。 ### 思路 2:广度优先搜索 1. 使用哈希表 $visited$ 来存储原图中被访问过的节点和克隆图中对应节点,键值对为「原图被访问过的节点:克隆图中对应节点」。使用队列 $queue$ 存放节点。 2. 根据起始节点 $node$,创建一个新的节点,并将其添加到哈希表 $visited$ 中,即 `visited[node] = Node(node.val, [])`。然后将起始节点放入队列中,即 `queue.append(node)`。 3. 从队列中取出第一个节点 $node\_u$。访问节点 $node\_u$。 4. 遍历节点 $node\_u$ 的所有未访问邻接节点 $node\_v$(节点 $node\_v$ 不在 $visited$ 中)。 5. 根据节点 $node\_v$ 创建一个新的节点,并将其添加到哈希表 $visited$ 中,即 `visited[node_v] = Node(node_v.val, [])`。 6. 然后将节点 $node\_v$ 放入队列 $queue$ 中,即 `queue.append(node_v)`。 7. 重复步骤 $3 \sim 6$,直到队列 $queue$ 为空。 8. 广度优先搜索结束,返回起始节点的克隆节点(即 $visited[node]$)。 ### 思路 2:代码 ```python class Solution: def cloneGraph(self, node: 'Node') -> 'Node': if not node: return node visited = dict() queue = collections.deque() visited[node] = Node(node.val, []) queue.append(node) while queue: node_u = queue.popleft() for node_v in node_u.neighbors: if node_v not in visited: visited[node_v] = Node(node_v.val, []) queue.append(node_v) visited[node_u].neighbors.append(visited[node_v]) return visited[node] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 为图中节点数量。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/compare-version-numbers.md ================================================ # [0165. 比较版本号](https://leetcode.cn/problems/compare-version-numbers/) - 标签:双指针、字符串 - 难度:中等 ## 题目链接 - [0165. 比较版本号 - 力扣](https://leetcode.cn/problems/compare-version-numbers/) ## 题目大意 **描述**: 给定两个版本号字符串 $version1$ 和 $version2$。 **要求**: 请你比较它们。版本号由被点 `'.'` 分开的修订号组成。修订号的值是它转换为整数并忽略前导零。 比较版本号时,请按从左到右的顺序 依次比较它们的修订号。如果其中一个版本字符串的修订号较少,则将缺失的修订号视为 $0$。 返回规则如下: - 如果 $version1 < version2$ 返回 $-1$。 - 如果 $version1 > version2$ 返回 $1$。 - 除此之外返回 $0$。 **说明**: - $1 \le version1.length, version2.length \le 500$。 - $version1$ 和 $version2$ 仅包含数字和 `'.'`。 - $version1$ 和 $version2$ 都是有效版本号。 - $version1$ 和 $version2$ 的所有修订号都可以存储在 $32$ 位整数中。 **示例**: - 示例 1: ```python 输入:version1 = "1.2", version2 = "1.10" 输出:-1 解释:version1 的第二个修订号为 "2",version2 的第二个修订号为 "10":2 < 10,所以 version1 < version2。 ``` - 示例 2: ```python 输入:version1 = "1.01", version2 = "1.001" 输出:0 解释:忽略前导零,"01" 和 "001" 都代表相同的整数 "1"。 ``` ## 解题思路 ### 思路 1:双指针 我们可以使用双指针的方法来比较版本号。具体思路如下: 1. **分割版本号**:使用双指针 $i$ 和 $j$ 分别遍历 $version1$ 和 $version2$。 2. **提取修订号**:当遇到点号 `'.'` 时,提取当前修订号并转换为整数。 3. **比较修订号**:比较两个修订号的大小: - 如果 $num1 < num2$,返回 $-1$。 - 如果 $num1 > num2$,返回 $1$。 - 如果相等,继续比较下一个修订号。 4. **处理长度差异**:当某个版本号的修订号遍历完毕时,将缺失的修订号视为 $0$。 **关键点**: - 使用 `int()` 函数自动忽略前导零。 - 当指针超出字符串长度时,对应的修订号视为 $0$。 - 需要同时处理两个版本号都遍历完毕的情况。 ### 思路 1:代码 ```python class Solution: def compareVersion(self, version1: str, version2: str) -> int: # 初始化双指针 i, j = 0, 0 len1, len2 = len(version1), len(version2) # 使用双指针遍历两个版本号 while i < len1 or j < len2: # 提取 version1 的当前修订号 num1 = 0 while i < len1 and version1[i] != '.': num1 = num1 * 10 + int(version1[i]) i += 1 i += 1 # 跳过点号 # 提取 version2 的当前修订号 num2 = 0 while j < len2 and version2[j] != '.': num2 = num2 * 10 + int(version2[j]) j += 1 j += 1 # 跳过点号 # 比较修订号 if num1 < num2: return -1 elif num1 > num2: return 1 # 所有修订号都相等 return 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n)$,其中 $m$ 和 $n$ 分别是 $version1$ 和 $version2$ 的长度。我们需要遍历两个字符串的每个字符。 - **空间复杂度**:$O(1)$。只使用了常数额外空间。 ================================================ FILE: docs/solutions/0100-0199/construct-binary-tree-from-inorder-and-postorder-traversal.md ================================================ # [0106. 从中序与后序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/) - 标签:树、数组、哈希表、分治、二叉树 - 难度:中等 ## 题目链接 - [0106. 从中序与后序遍历序列构造二叉树 - 力扣](https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/) ## 题目大意 **描述**:给定一棵二叉树的中序遍历结果 `inorder` 和后序遍历结果 `postorder`。 **要求**:构造出该二叉树并返回其根节点。 **说明**: - $1 \le inorder.length \le 3000$。 - $postorder.length == inorder.length$。 - $-3000 \le inorder[i], postorder[i] \le 3000$。 - `inorder` 和 `postorder` 都由不同的值组成。 - `postorder` 中每一个值都在 `inorder` 中。 - `inorder` 保证是二叉树的中序遍历序列。 - `postorder` 保证是二叉树的后序遍历序列。 - `inorder` 保证为二叉树的中序遍历序列。 **示例**: - 示例 1: ![img](https://assets.leetcode.com/uploads/2021/02/19/tree.jpg) ```python 输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3] 输出:[3,9,20,null,null,15,7] ``` - 示例 2: ```python 输入:inorder = [-1], postorder = [-1] 输出:[-1] ``` ## 解题思路 ### 思路 1:递归 中序遍历的顺序是:左 -> 根 -> 右。后序遍历的顺序是:左 -> 右 -> 根。根据后序遍历的顺序,可以找到根节点位置。然后在中序遍历的结果中可以找到对应的根节点位置,就可以从根节点位置将二叉树分割成左子树、右子树。同时能得到左右子树的节点个数。此时构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历进行上述步骤,直到节点为空,具体操作步骤如下: 1. 从后序遍历顺序中当前根节点的位置在 `postorder[n - 1]`。 2. 通过在中序遍历中查找上一步根节点对应的位置 `inorder[k]`,从而将二叉树的左右子树分隔开,并得到左右子树节点的个数。 3. 从上一步得到的左右子树个数将后序遍历结果中的左右子树分开。 4. 构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。 ### 思路 1:代码 ```python class Solution: def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode: def createTree(inorder, postorder, n): if n == 0: return None k = 0 while postorder[n-1] != inorder[k]: k += 1 node = TreeNode(inorder[k]) node.right = createTree(inorder[k+1: n], postorder[k: n-1], n-k-1) node.left = createTree(inorder[0: k], postorder[0: k], k) return node return createTree(inorder, postorder, len(postorder)) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/construct-binary-tree-from-preorder-and-inorder-traversal.md ================================================ # [0105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) - 标签:树、数组、哈希表、分治、二叉树 - 难度:中等 ## 题目链接 - [0105. 从前序与中序遍历序列构造二叉树 - 力扣](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) ## 题目大意 **描述**:给定一棵二叉树的前序遍历结果 `preorder` 和中序遍历结果 `inorder`。 **要求**:构造出该二叉树并返回其根节点。 **说明**: - $1 \le preorder.length \le 3000$。 - $inorder.length == preorder.length$。 - $-3000 \le preorder[i], inorder[i] \le 3000$。 - $preorder$ 和 $inorder$ 均无重复元素。 - $inorder$ 均出现在 $preorder$。 - $preorder$ 保证为二叉树的前序遍历序列。 - $inorder$ 保证为二叉树的中序遍历序列。 **示例**: - 示例 1: ![img](https://assets.leetcode.com/uploads/2021/02/19/tree.jpg) ```python 输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] 输出: [3,9,20,null,null,15,7] ``` - 示例 2: ```python 输入: preorder = [-1], inorder = [-1] 输出: [-1] ``` ## 解题思路 ### 思路 1:递归遍历 前序遍历的顺序是:根 -> 左 -> 右。中序遍历的顺序是:左 -> 根 -> 右。根据前序遍历的顺序,可以找到根节点位置。然后在中序遍历的结果中可以找到对应的根节点位置,就可以从根节点位置将二叉树分割成左子树、右子树。同时能得到左右子树的节点个数。此时构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历进行上述步骤,直到节点为空,具体操作步骤如下: 1. 从前序遍历顺序中当前根节点的位置在 $postorder[0]$。 2. 通过在中序遍历中查找上一步根节点对应的位置 $inorder[k]$,从而将二叉树的左右子树分隔开,并得到左右子树节点的个数。 3. 从上一步得到的左右子树个数将前序遍历结果中的左右子树分开。 4. 构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。 ### 思路 1:代码 ```python class Solution: def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: def createTree(preorder, inorder, n): if n == 0: return None k = 0 while preorder[0] != inorder[k]: k += 1 node = TreeNode(inorder[k]) node.left = createTree(preorder[1: k+1], inorder[0: k], k) node.right = createTree(preorder[k+1:], inorder[k+1:], n-k-1) return node return createTree(preorder, inorder, len(inorder)) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是二叉树的节点数目。每次递归都需要在中序遍历数组 `inorder` 中查找根节点的位置,最坏情况下需要遍历整个数组,因此每一层递归的查找操作为 $O(n)$,而递归总共 $n$ 层,所以总时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n^2)$。递归过程中每次切片会新建子数组,最坏情况下每层递归都要复制 $O(n)$ 大小的数组,共 $O(n)$ 层,因此总空间复杂度为 $O(n^2)$。如果不考虑切片额外空间,仅考虑递归栈,则为 $O(n)$。 ### 思路 2:递归遍历 + 哈希表 在思路 1 中,每次递归都需要在中序遍历数组 $inorder$ 中查找根节点的位置,这导致了 $O(n)$ 的查找时间。我们可以使用哈希表来优化这个过程,将中序遍历数组中每个元素的值与其索引位置建立映射关系,这样查找根节点位置的时间复杂度就可以优化到 $O(1)$。 具体操作步骤如下: 1. 首先遍历中序遍历数组,将每个元素的值与其索引位置建立映射关系,存储在哈希表中。 2. 从前序遍历顺序中当前根节点的位置在 $preorder[0]$。 3. 通过哈希表查找根节点在中序遍历中的位置 $inorder[k]$,从而将二叉树的左右子树分隔开,并得到左右子树节点的个数。 4. 从上一步得到的左右子树个数将前序遍历结果中的左右子树分开。 5. 构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述步骤,直到节点为空。 ### 思路 2:代码 ```python class Solution: def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: # 构建中序遍历数组中元素值与索引的映射关系 inorder_map = {val: idx for idx, val in enumerate(inorder)} def createTree(preorder_left, preorder_right, inorder_left, inorder_right): if preorder_left > preorder_right: return None # 前序遍历的第一个节点就是根节点 root_val = preorder[preorder_left] root = TreeNode(root_val) # 在中序遍历中找到根节点的位置 root_index = inorder_map[root_val] # 计算左子树的节点个数 left_size = root_index - inorder_left # 递归构建左子树 root.left = createTree(preorder_left + 1, preorder_left + left_size, inorder_left, root_index - 1) # 递归构建右子树 root.right = createTree(preorder_left + left_size + 1, preorder_right, root_index + 1, inorder_right) return root return createTree(0, len(preorder) - 1, 0, len(inorder) - 1) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。每个节点都会被访问一次,且通过哈希表查找根节点位置的时间复杂度为 $O(1)$,因此总时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。哈希表需要 $O(n)$ 的空间存储中序遍历数组中元素值与索引的映射关系,递归栈的深度为 $O(h)$,其中 $h$ 是二叉树的高度。在最坏情况下,二叉树退化为链表,此时 $h = n$,所以总空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/convert-sorted-array-to-binary-search-tree.md ================================================ # [0108. 将有序数组转换为二叉搜索树](https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/) - 标签:树、二叉搜索树、数组、分治、二叉树 - 难度:简单 ## 题目链接 - [0108. 将有序数组转换为二叉搜索树 - 力扣](https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/) ## 题目大意 **描述**:给定一个升序的有序数组 `nums`。 **要求**:将其转换为一棵高度平衡的二叉搜索树。 **说明**: - $1 \le nums.length \le 10^4$。 - $-10^4 \le nums[i] \le 10^4$。 - `nums` 按严格递增顺序排列。 **示例**: - 示例 1: ![img](https://assets.leetcode.com/uploads/2021/02/18/btree1.jpg) ```python 输入:nums = [-10,-3,0,5,9] 输出:[0,-3,9,-10,null,5] 解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案 ``` - 示例 2: ![img](https://assets.leetcode.com/uploads/2021/02/18/btree.jpg) ```python 输入:nums = [1,3] 输出:[3,1] 解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。 ``` ## 解题思路 ### 思路 1:递归遍历 直观上,如果把数组的中间元素当做根,那么数组左侧元素都小于根节点,右侧元素都大于根节点,且左右两侧元素个数相同,或最多相差 $1$ 个。那么构建的树高度差也不会超过 $1$。 所以猜想出:如果左右子树越平均,树就越平衡。这样我们就可以每次取中间元素作为当前的根节点,两侧的元素作为左右子树递归建树,左侧区间 $[L, mid - 1]$ 作为左子树,右侧区间 $[mid + 1, R]$ 作为右子树。 ### 思路 1:代码 ```python class Solution: def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]: def build(left, right): if left > right: return mid = left + (right - left) // 2 root = TreeNode(nums[mid]) root.left = build(left, mid - 1) root.right = build(mid + 1, right) return root return build(0, len(nums) - 1) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是数组的长度。递归过程中每个元素只会被访问一次并构造成树节点,因此总的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(\log n)$。不考虑返回的树结构所占空间,仅计算递归栈空间)。由于每次递归都将区间一分为二,递归深度为二叉树的高度,最平衡情况下高度为 $O(\log n)$,因此递归栈的最大深度为 $O(\log n)$。如果将返回的整棵树节点空间也计入,则空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/convert-sorted-list-to-binary-search-tree.md ================================================ # [0109. 有序链表转换二叉搜索树](https://leetcode.cn/problems/convert-sorted-list-to-binary-search-tree/) - 标签:树、二叉搜索树、链表、分治、二叉树 - 难度:中等 ## 题目链接 - [0109. 有序链表转换二叉搜索树 - 力扣](https://leetcode.cn/problems/convert-sorted-list-to-binary-search-tree/) ## 题目大意 **描述**: 给定一个单链表的头节点 $head$,其中的元素按升序排序。 **要求**: 将其转换为平衡二叉搜索树。 **说明**: - $head$ 中的节点数在 $[0, 2 \times 10^{4}]$ 范围内。 - $-10^{5} \le Node.val \le 10^{5}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/08/17/linked.jpg) ```python 输入: head = [-10,-3,0,5,9] 输出: [0,-3,9,-10,null,5] 解释: 一个可能的答案是 [0,-3,9,-10,null,5],它表示所示的高度平衡的二叉搜索树。 ``` - 示例 2: ```python 输入: head = [] 输出: [] ``` ## 解题思路 ### 思路 1:分治法 + 快慢指针 由于链表是有序的,我们可以使用分治法来构建平衡二叉搜索树。关键是要找到链表的中间节点作为根节点,然后递归构建左右子树。 **算法步骤**: 1. **找到中间节点**:使用快慢指针找到链表的中间节点 $mid$,将链表分为两部分。 2. **构建根节点**:以中间节点的值创建根节点 $root$。 3. **递归构建左子树**:对中间节点左侧的链表递归构建左子树。 4. **递归构建右子树**:对中间节点右侧的链表递归构建右子树。 5. **返回根节点**:返回构建好的二叉搜索树根节点。 **关键点**: - 使用快慢指针找到中间节点,时间复杂度为 $O(n)$。 - 需要断开链表,避免在递归过程中重复处理节点。 - 分治法确保左右子树节点数量平衡,从而构建平衡二叉搜索树。 ### 思路 1:代码 ```python # Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def sortedListToBST(self, head: Optional[ListNode]) -> Optional[TreeNode]: # 递归终止条件:空链表 if not head: return None # 找到中间节点 mid = self.findMiddle(head) # 创建根节点 root = TreeNode(mid.val) # 如果只有一个节点,直接返回 if head == mid: return root # 递归构建左子树 root.left = self.sortedListToBST(head) # 递归构建右子树 root.right = self.sortedListToBST(mid.next) return root def findMiddle(self, head: ListNode) -> ListNode: """ 使用快慢指针找到链表的中间节点 同时断开链表,避免重复处理 """ prev = None # 慢指针的前一个节点 slow = head # 慢指针 fast = head # 快指针 # 快指针每次走两步,慢指针每次走一步 while fast and fast.next: prev = slow slow = slow.next fast = fast.next.next # 断开链表,避免重复处理 if prev: prev.next = None return slow ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是链表的长度。每次递归都需要 $O(n)$ 时间找到中间节点,递归深度为 $O(\log n)$,所以总时间复杂度为 $O(n \log n)$。 - **空间复杂度**:$O(\log n)$,其中 $n$ 是链表的长度。递归调用栈的深度为 $O(\log n)$。 ================================================ FILE: docs/solutions/0100-0199/copy-list-with-random-pointer.md ================================================ # [0138. 随机链表的复制](https://leetcode.cn/problems/copy-list-with-random-pointer/) - 标签:哈希表、链表 - 难度:中等 ## 题目链接 - [0138. 随机链表的复制 - 力扣](https://leetcode.cn/problems/copy-list-with-random-pointer/) ## 题目大意 **描述**:给定一个链表的头节点 `head`,链表中每个节点除了 `next` 指针之外,还包含一个随机指针 `random`,该指针可以指向链表中的任何节点或者空节点。 **要求**:将该链表进行深拷贝。返回复制链表的头节点。 **说明**: - $0 \le n \le 1000$。 - $-10^4 \le Node.val \le 10^4$。 - `Node.random` 为 `null` 或指向链表中的节点。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/01/09/e1.png) ```python 输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]] 输出:[[7,null],[13,0],[11,4],[10,2],[1,0]] ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/01/09/e2.png) ```python 输入:head = [[1,1],[2,1]] 输出:[[1,1],[2,1]] ``` ## 解题思路 ### 思路 1:迭代 1. 遍历链表,利用哈希表,以 `旧节点: 新节点` 为映射关系,将节点关系存储下来。 2. 再次遍历链表,将新链表的 `next` 和 `random` 指针设置好。 ### 思路 1:代码 ```python class Solution: def copyRandomList(self, head: 'Node') -> 'Node': if not head: return None node_dict = dict() curr = head while curr: new_node = Node(curr.val, None, None) node_dict[curr] = new_node curr = curr.next curr = head while curr: if curr.next: node_dict[curr].next = node_dict[curr.next] if curr.random: node_dict[curr].random = node_dict[curr.random] curr = curr.next return node_dict[head] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/distinct-subsequences.md ================================================ # [0115. 不同的子序列](https://leetcode.cn/problems/distinct-subsequences/) - 标签:字符串、动态规划 - 难度:困难 ## 题目链接 - [0115. 不同的子序列 - 力扣](https://leetcode.cn/problems/distinct-subsequences/) ## 题目大意 **描述**:给定两个字符串 `s` 和 `t`。 **要求**:计算在 `s` 的子序列中 `t` 出现的个数。 **说明**: - **字符串的子序列**:通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,`"ACE"` 是 `"ABCDE"` 的一个子序列,而 `"AEC"` 不是)。 - $0 \le s.length, t.length \le 1000$。 - `s` 和 `t` 由英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "rabbbit", t = "rabbit" 输出:3 解释:如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。 ``` $\underline{rabb}b\underline{it}$ $\underline{ra}b\underline{bbit}$ $\underline{rab}b\underline{bit}$ ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照子序列的结尾位置进行阶段划分。 ###### 2. 定义状态 定义状态 `dp[i][j]` 表示为:以第 `i - 1` 个字符为结尾的 `s` 子序列中出现以第 `j - 1` 个字符为结尾的 `t` 的个数。 ###### 3. 状态转移方程 双重循环遍历字符串 `s` 和 `t`,则状态转移方程为: - 如果 `s[i - 1] == t[j - 1]`,则:`dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]`。即 `dp[i][j]` 来源于两部分: - 使用 `s[i - 1]` 匹配 `t[j - 1]`,则 `dp[i][j]` 取源于以 `i - 2` 为结尾的 `s` 子序列中出现以 `j - 2` 为结尾的 `t` 的个数,即 `dp[i - 1][j - 1]`。 - 不使用 `s[i - 1]` 匹配 `t[j - 1]`,则 `dp[i][j]` 取源于以 `i - 2` 为结尾的 `s` 子序列中出现以 `j - 1` 为结尾的 `t` 的个数,即 `dp[i - 1][j]`。 - 如果 `s[i - 1] != t[j - 1]`,那么肯定不能用 `s[i - 1]` 匹配 `t[j - 1]`,则 `dp[i][j]` 取源于 `dp[i - 1][j]`。 ###### 4. 初始条件 - `dp[i][0]` 表示以 `i - 1` 为结尾的 `s` 子序列中出现空字符串的个数。把 `s` 中的元素全删除,出现空字符串的个数就是 `1`,则 `dp[i][0] = 1`。 - `dp[0][j]` 表示空字符串中出现以 `j - 1` 结尾的 `t` 的个数,空字符串无论怎么变都不会变成 `t`,则 `dp[0][j] = 0` - `dp[0][0]` 表示空字符串中出现空字符串的个数,这个应该是 `1`,即 `dp[0][0] = 1`。 ##### 5. 最终结果 根据我们之前定义的状态,`dp[i][j]` 表示为:以第 `i - 1` 个字符为结尾的 `s` 子序列中出现以第 `j - 1` 个字符为结尾的 `t` 的个数。则最终结果为 `dp[size_s][size_t]`,将其返回即可。 ### 思路 1:动态规划代码 ```python class Solution: def numDistinct(self, s: str, t: str) -> int: size_s = len(s) size_t = len(t) dp = [[0 for _ in range(size_t + 1)] for _ in range(size_s + 1)] for i in range(size_s): dp[i][0] = 1 for i in range(1, size_s + 1): for j in range(1, size_t + 1): if s[i - 1] == t[j - 1]: dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] else: dp[i][j] = dp[i - 1][j] return dp[size_s][size_t] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。两重循环遍历的时间复杂度是 $O(n^2)$,所以总的时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n^2)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n^2)$。 ================================================ FILE: docs/solutions/0100-0199/dungeon-game.md ================================================ # [0174. 地下城游戏](https://leetcode.cn/problems/dungeon-game/) - 标签:数组、动态规划、矩阵 - 难度:困难 ## 题目链接 - [0174. 地下城游戏 - 力扣](https://leetcode.cn/problems/dungeon-game/) ## 题目大意 **描述**: 恶魔们抓住了公主并将她关在了地下城 $dungeon$ 的右下角。地下城是由 $m \times n$ 个房间组成的二维网格。我们英勇的骑士最初被安置在「左上角」的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。 骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 $0$ 或以下,他会立即死亡。 有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 $0$),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。 为了尽快解救公主,骑士决定每次只「向右」或「向下」移动一步。 **要求**: 返回确保骑士能够拯救到公主所需的最低初始健康点数。 **说明**: - 任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。 - $m == dungeon.length$。 - $n == dungeon[i].length$。 - $1 \le m, n \le 200$。 - $-10^{3} \le dungeon[i][j] \le 10^{3}$。 **示例**: - 示例 1: ```python ![](https://assets.leetcode.com/uploads/2021/03/13/dungeon-grid-1.jpg) 输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]] 输出:7 解释:如果骑士遵循最佳路径:右 -> 右 -> 下 -> 下 ,则骑士的初始健康点数至少为 7 。 ``` - 示例 2: ```python 输入:dungeon = [[0]] 输出:1 ``` ## 解题思路 ### 思路 1:动态规划(从右下角到左上角) 这道题的关键在于理解:我们需要从右下角开始,逆向思考骑士到达每个位置时所需的最小健康点数。 **状态定义**: - 设 $dp[i][j]$ 表示从位置 $(i, j)$ 到达右下角所需的最小初始健康点数。 **状态转移方程**: - 对于右下角位置 $(m-1, n-1)$:$dp[m-1][n-1] = \max(1, 1 - dungeon[m-1][n-1])$ - 对于其他位置 $(i, j)$: - 如果 $i = m-1$(最后一行):$dp[i][j] = \max(1, dp[i][j+1] - dungeon[i][j])$ - 如果 $j = n-1$(最后一列):$dp[i][j] = \max(1, dp[i+1][j] - dungeon[i][j])$ - 其他情况:$dp[i][j] = \max(1, \min(dp[i+1][j], dp[i][j+1]) - dungeon[i][j])$ **算法步骤**: 1. 初始化 $dp$ 数组,所有值设为无穷大。 2. 从右下角开始,按照状态转移方程填充 $dp$ 数组。 3. 返回 $dp[0][0]$ 作为答案。 ### 思路 1:代码 ```python class Solution: def calculateMinimumHP(self, dungeon: List[List[int]]) -> int: m, n = len(dungeon), len(dungeon[0]) # dp[i][j] 表示从位置 (i,j) 到达右下角所需的最小初始健康点数 dp = [[float('inf')] * n for _ in range(m)] # 从右下角开始逆向填充 for i in range(m - 1, -1, -1): for j in range(n - 1, -1, -1): if i == m - 1 and j == n - 1: # 右下角:至少需要 1 点健康,如果房间有伤害则增加 dp[i][j] = max(1, 1 - dungeon[i][j]) elif i == m - 1: # 最后一行:只能向右走 dp[i][j] = max(1, dp[i][j + 1] - dungeon[i][j]) elif j == n - 1: # 最后一列:只能向下走 dp[i][j] = max(1, dp[i + 1][j] - dungeon[i][j]) else: # 其他位置:选择向右或向下中所需健康点数更少的路径 dp[i][j] = max(1, min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]) return dp[0][0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 和 $n$ 分别是地下城的行数和列数。需要遍历整个二维数组一次。 - **空间复杂度**:$O(m \times n)$,需要创建一个 $m \times n$ 的二维数组来存储状态。 ================================================ FILE: docs/solutions/0100-0199/evaluate-reverse-polish-notation.md ================================================ # [0150. 逆波兰表达式求值](https://leetcode.cn/problems/evaluate-reverse-polish-notation/) - 标签:栈、数组、数学 - 难度:中等 ## 题目链接 - [0150. 逆波兰表达式求值 - 力扣](https://leetcode.cn/problems/evaluate-reverse-polish-notation/) ## 题目大意 **描述**:给定一个字符串数组 `tokens`,表示「逆波兰表达式」。 **要求**:求解表达式的值。 **说明**: - **逆波兰表达式**:也称为后缀表达式。 - 中缀表达式 `( 1 + 2 ) * ( 3 + 4 ) `,对应的逆波兰表达式为 ` ( ( 1 2 + ) ( 3 4 + ) * )` 。 - $1 \le tokens.length \le 10^4$。 - `tokens[i]` 是一个算符(`+`、`-`、`*` 或 `/`),或是在范围 $[-200, 200]$ 内的一个整数。 **示例**: - 示例 1: ```python 输入:tokens = ["4","13","5","/","+"] 输出:6 解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6 ``` - 示例 2: ```python 输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"] 输出:22 解释:该算式转化为常见的中缀算术表达式为: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 = ((10 * (6 / (12 * -11))) + 17) + 5 = ((10 * (6 / -132)) + 17) + 5 = ((10 * 0) + 17) + 5 = (0 + 17) + 5 = 17 + 5 = 22 ``` ## 解题思路 ### 思路 1:栈 这道题是栈的典型应用。我们先来简单介绍一下逆波兰表达式。 逆波兰表达式,也叫做后缀表达式,特点是:没有括号,运算符总是放在和它相关的操作数之后。 我们平常见到的表达式是中缀表达式,可写为:`A 运算符 B`。其中 `A`、`B` 都是操作数。 而后缀表达式可写为:`A B 运算符`。 逆波兰表达式的计算遵循从左到右的规律。我们在计算逆波兰表达式的值时,可以使用一个栈来存放当前的操作数,从左到右依次遍历逆波兰表达式,计算出对应的值。具体操作步骤如下: 1. 使用列表 `stack` 作为栈存放操作数,然后遍历表达式的字符串数组。 2. 如果当前字符为运算符,则取出栈顶两个元素,在进行对应的运算之后,再将运算结果入栈。 3. 如果当前字符为数字,则直接将数字入栈。 4. 遍历结束后弹出栈中最后剩余的元素,这就是最终结果。 ### 思路 1:代码 ```python class Solution: def evalRPN(self, tokens: List[str]) -> int: stack = [] for token in tokens: if token == '+': stack.append(stack.pop() + stack.pop()) elif token == '-': stack.append(-stack.pop() + stack.pop()) elif token == '*': stack.append(stack.pop() * stack.pop()) elif token == '/': stack.append(int(1 / stack.pop() * stack.pop())) else: stack.append(int(token)) return stack.pop() ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/excel-sheet-column-number.md ================================================ # [0171. Excel 表列序号](https://leetcode.cn/problems/excel-sheet-column-number/) - 标签:数学、字符串 - 难度:简单 ## 题目链接 - [0171. Excel 表列序号 - 力扣](https://leetcode.cn/problems/excel-sheet-column-number/) ## 题目大意 给你一个字符串 `columnTitle` ,表示 Excel 表格中的列名称。 要求:返回该列名称对应的列序号。 ## 解题思路 Excel 表的列名称由大写字母组成,共有 26 个,因此列名称的表示实质是 26 进制,需要将 26 进制转换成十进制。转换过程如下: - 将每一位对应列名称转换成整数(注意列序号从 `1` 开始)。 - 将当前结果乘上进制数(`26`),然后累加上当前位上的整数。 最后输出答案。 ## 代码 ```python class Solution: def titleToNumber(self, columnTitle: str) -> int: ans = 0 for ch in columnTitle: num = ord(ch) - ord('A') + 1 ans = ans * 26 + num return ans ``` ================================================ FILE: docs/solutions/0100-0199/excel-sheet-column-title.md ================================================ # [0168. Excel 表列名称](https://leetcode.cn/problems/excel-sheet-column-title/) - 标签:数学、字符串 - 难度:简单 ## 题目链接 - [0168. Excel 表列名称 - 力扣](https://leetcode.cn/problems/excel-sheet-column-title/) ## 题目大意 描述:给定一个正整数 columnNumber。 要求:返回它在 Excel 表中相对应的列名称。 1 -> A,2 -> B,3 -> C,…,26 -> Z,…,28 -> AB ## 解题思路 实质上就是 10 进制转 26 进制。不过映射范围是 1~26,而不是 0~25,如果将 columnNumber 直接对 26 取余,则结果为 0~25,而本题余数为 1~26。可以直接将 columnNumber = columnNumber - 1,这样就可以将范围变为 0~25 就更加容易判断了。 ## 代码 ```python class Solution: def convertToTitle(self, columnNumber: int) -> str: s = "" while columnNumber: columnNumber -= 1 s = chr(65 + columnNumber % 26) + s columnNumber //= 26 return s ``` ================================================ FILE: docs/solutions/0100-0199/factorial-trailing-zeroes.md ================================================ # [0172. 阶乘后的零](https://leetcode.cn/problems/factorial-trailing-zeroes/) - 标签:数学 - 难度:中等 ## 题目链接 - [0172. 阶乘后的零 - 力扣](https://leetcode.cn/problems/factorial-trailing-zeroes/) ## 题目大意 给定一个整数 `n`。 要求:返回 `n!` 结果中尾随零的数量。 注意:$0 <= n <= 10^4$ ## 解题思路 阶乘中,末尾 `0` 的来源只有 `2 * 5`。所以尾随 `0` 的个数为 `2` 的倍数个数和 `5` 的倍数个数的最小值。又因为 `2 < 5`,`2` 的倍数个数肯定小于等于 `5` 的倍数,所以直接统计 `5` 的倍数个数即可。 ## 代码 ```python class Solution: def trailingZeroes(self, n: int) -> int: count = 0 while n > 0: count += n // 5 n = n // 5 return count ``` ================================================ FILE: docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array-ii.md ================================================ # [154. 寻找旋转排序数组中的最小值 II](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/) - 标签:数组、二分查找 - 难度:困难 ## 题目链接 - [154. 寻找旋转排序数组中的最小值 II - 力扣](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/) ## 题目大意 **描述**:给定一个数组 $nums$,$nums$ 是有升序数组经过 $1 \sim n$ 次「旋转」得到的。但是旋转次数未知。数组中可能存在重复元素。 **要求**:找出数组中的最小元素。 **说明**: - 旋转:将数组整体右移 $1$ 位。数组 $[a[0], a[1], a[2], ..., a[n-1]]$ 旋转一次的结果为数组 $[a[n-1], a[0], a[1], a[2], ..., a[n-2]]$。 - $n == nums.length$。 - $1 \le n \le 5000$。 - $-5000 \le nums[i] \le 5000$ - $nums$ 原来是一个升序排序的数组,并进行了 $1 \sim n$ 次旋转。 **示例**: - 示例 1: ```python 输入:nums = [1,3,5] 输出:1 ``` - 示例 2: ```python 输入:nums = [2,2,2,0,1] 输出:0 ``` ## 解题思路 ### 思路 1:二分查找 数组经过「旋转」之后,会有两种情况,第一种就是原先的升序序列,另一种是两段升序的序列。 第一种的最小值在最左边。 ``` * * * * * * ``` 第二种最小值在第二段升序序列的第一个元素。 ``` * * * * * * ``` 最直接的办法就是遍历一遍,找到最小值。但是还可以有更好的方法。考虑用二分查找来降低算法的时间复杂度。 创建两个指针 $left$、$right$,分别指向数组首尾。然后计算出两个指针中间值 $mid$。将 $mid$ 与右边界进行比较。 1. 如果 $nums[mid] > nums[right]$,则最小值不可能在 $mid$ 左侧,一定在 $mid$ 右侧,则将 $left$ 移动到 $mid + 1$ 位置,继续查找右侧区间。 2. 如果 $nums[mid] < nums[right]$,则最小值一定在 $mid$ 左侧,令右边界 $right$ 为 $mid$,继续查找左侧区间。 3. 如果 $nums[mid] == nums[right]$,无法判断在 $mid$ 的哪一侧,可以采用 `right = right - 1` 逐步缩小区域。 ### 思路 1:代码 ```python class Solution: def findMin(self, nums: List[int]) -> int: left = 0 right = len(nums) - 1 while left < right: mid = left + (right - left) // 2 if nums[mid] > nums[right]: left = mid + 1 elif nums[mid] < nums[right]: right = mid else: right = right - 1 return nums[left] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array.md ================================================ # [0153. 寻找旋转排序数组中的最小值](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [0153. 寻找旋转排序数组中的最小值 - 力扣](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/) ## 题目大意 **描述**:给定一个数组 $nums$,$nums$ 是有升序数组经过「旋转」得到的。但是旋转次数未知。数组中不存在重复元素。 **要求**:找出数组中的最小元素。 **说明**: - 旋转操作:将数组整体右移若干位置。 - $n == nums.length$。 - $1 \le n \le 5000$。 - $-5000 \le nums[i] \le 5000$。 - $nums$ 中的所有整数互不相同。 - $nums$ 原来是一个升序排序的数组,并进行了 $1$ 至 $n$ 次旋转。 **示例**: - 示例 1: ```python 输入:nums = [3,4,5,1,2] 输出:1 解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。 ``` - 示例 2: ```python 输入:nums = [4,5,6,7,0,1,2] 输出:0 解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。 ``` ## 解题思路 ### 思路 1:二分查找 数组经过「旋转」之后,会有两种情况,第一种就是原先的升序序列,另一种是两段升序的序列。 第一种的最小值在最左边。第二种最小值在第二段升序序列的第一个元素。 ```python * * * * * * ``` ```python * * * * * * ``` 最直接的办法就是遍历一遍,找到最小值。但是还可以有更好的方法。考虑用二分查找来降低算法的时间复杂度。 创建两个指针 $left$、$right$,分别指向数组首尾。让后计算出两个指针中间值 $mid$。将 $mid$ 与两个指针做比较。 1. 如果 $nums[mid] > nums[right]$,则最小值不可能在 $mid$ 左侧,一定在 $mid$ 右侧,则将 $left$ 移动到 $mid + 1$ 位置,继续查找右侧区间。 2. 如果 $nums[mid] \le nums[right]$,则最小值一定在 $mid$ 左侧,或者 $mid$ 位置,将 $right$ 移动到 $mid$ 位置上,继续查找左侧区间。 ### 思路 1:代码 ```python class Solution: def findMin(self, nums: List[int]) -> int: left = 0 right = len(nums) - 1 while left < right: mid = left + (right - left) // 2 if nums[mid] > nums[right]: left = mid + 1 else: right = mid return nums[left] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。二分查找算法的时间复杂度为 $O(\log n)$。 - **空间复杂度**:$O(1)$。只用到了常数空间存放若干变量。 ================================================ FILE: docs/solutions/0100-0199/find-peak-element.md ================================================ # [0162. 寻找峰值](https://leetcode.cn/problems/find-peak-element/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [0162. 寻找峰值 - 力扣](https://leetcode.cn/problems/find-peak-element/) ## 题目大意 **描述**:给定一个整数数组 `nums`。 **要求**:找到峰值元素并返回其索引。必须实现时间复杂度为 $O(\log n)$ 的算法来解决此问题。 **说明**: - **峰值元素**:指其值严格大于左右相邻值的元素。 - 数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。 - 可以假设 $nums[-1] = nums[n] = -∞$。 - $1 \le nums.length \le 1000$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 - 对于所有有效的 $i$ 都有 $nums[i] != nums[i + 1]$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,1] 输出:2 解释:3 是峰值元素,你的函数应该返回其索引 2。 ``` - 示例 2: ```python 输入:nums = [1,2,1,3,5,6,4] 输出:1 或 5 解释:你的函数可以返回索引 1,其峰值元素为 2;或者返回索引 5, 其峰值元素为 6。 ``` ## 解题思路 ### 思路 1:二分查找 1. 使用两个指针 `left`、`right` 。`left` 指向数组第一个元素,`right` 指向数组最后一个元素。 2. 取区间中间节点 `mid`,并比较 `nums[mid]` 和 `nums[mid + 1]` 的值大小。 1. 如果 `nums[mid]` 小于 `nums[mid + 1]`,则右侧存在峰值,令 `left = mid + 1`。 2. 如果 `nums[mid]` 大于等于 `nums[mid + 1]`,则左侧存在峰值,令 `right = mid`。 3. 最后,当 `left == right` 时,跳出循环,返回 `left`。 ### 思路 1:代码 ```python class Solution: def findPeakElement(self, nums: List[int]) -> int: left = 0 right = len(nums) - 1 while left < right: mid = left + (right - left) // 2 if nums[mid] < nums[mid + 1]: left = mid + 1 else: right = mid return left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log_2 n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/flatten-binary-tree-to-linked-list.md ================================================ # [0114. 二叉树展开为链表](https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/) - 标签:栈、树、深度优先搜索、链表、二叉树 - 难度:中等 ## 题目链接 - [0114. 二叉树展开为链表 - 力扣](https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/) ## 题目大意 **描述**: 给定二叉树的根结点 $root$。 **要求**: 请你将它展开为一个单链表: - 展开后的单链表应该同样使用 $TreeNode$,其中 $right$ 子指针指向链表中下一个结点,而左子指针始终为 $null$。 - 展开后的单链表应该与二叉树先序遍历顺序相同。 **说明**: - 树中结点数在范围 $[0, 2000]$ 内。 - $-10^{3} \le Node.val \le 10^{3}$。 - 进阶:你可以使用原地算法(O(1) 额外空间)展开这棵树吗? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/01/14/flaten.jpg) ```python 输入:root = [1,2,5,3,4,null,6] 输出:[1,null,2,null,3,null,4,null,5,null,6] ``` - 示例 2: ```python 输入:root = [] 输出:[] ``` ## 解题思路 ### 思路 1:递归 + 后序遍历 根据题目要求,我们需要将二叉树按照先序遍历的顺序展开为链表。观察先序遍历的特点: 1. 先访问根节点 $root$ 2. 再访问左子树 $root.left$ 3. 最后访问右子树 $root.right$ 我们可以采用递归 + 后序遍历的方式来解决: 1. **递归终止条件**:如果当前节点 $root$ 为空,直接返回。 2. **递归处理**:先递归处理左子树和右子树,得到展开后的左子树和右子树。 3. **重新连接**: - 将展开后的左子树连接到当前节点的右子节点位置 - 将当前节点的左子节点设为 $null$ - 将展开后的右子树连接到左子树展开链表的末尾 **关键点**: - 使用后序遍历确保在处理当前节点时,左右子树已经被展开 - 需要找到左子树展开链表的末尾节点,用于连接右子树 - 展开后的链表结构:$root \rightarrow left\_flattened \rightarrow right\_flattened$ ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def flatten(self, root: Optional[TreeNode]) -> None: """ Do not return anything, modify root in-place instead. """ # 递归终止条件:空节点直接返回 if not root: return # 递归处理左右子树 self.flatten(root.left) self.flatten(root.right) # 保存当前节点的右子树 right_subtree = root.right # 将左子树移到右子树位置 if root.left: root.right = root.left root.left = None # 找到左子树展开链表的末尾节点 current = root.right while current.right: current = current.right # 将原右子树连接到左子树展开链表的末尾 current.right = right_subtree ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数。我们需要访问每个节点一次,并且对于每个节点,我们可能需要遍历其左子树来找到末尾节点,但总体时间复杂度仍然是 $O(n)$。 - **空间复杂度**:$O(h)$,其中 $h$ 是二叉树的高度。递归调用栈的深度等于树的高度,最坏情况下为 $O(n)$(当树为链式结构时)。 ================================================ FILE: docs/solutions/0100-0199/fraction-to-recurring-decimal.md ================================================ # [0166. 分数到小数](https://leetcode.cn/problems/fraction-to-recurring-decimal/) - 标签:哈希表、数学、字符串 - 难度:中等 ## 题目链接 - [0166. 分数到小数 - 力扣](https://leetcode.cn/problems/fraction-to-recurring-decimal/) ## 题目大意 给定两个整数,分别表示分数的分子 numerator 和分母 denominator,要求以字符串的形式返回该分数对应小数结果。 - 如果小数部分为循环小数,则将循环的小数部分括在括号内。 ## 解题思路 先处理特殊数据,例如 0、负数等。 然后利用整除运算,计算出分数的整数部分。在根据取余运算结果,判断是否含有小数部分。 因为小数部分可能会有循环部分,所以使用哈希表来判断是否出现了循环小数。哈希表所存键值为 数字:数字开始位置。 然后计算小数部分,每次将被除数 * 10 然后对除数进行整除,再对被除数进行取余操作,直到被除数变为 0,或者在字典中出现了循环小数为止。 ## 代码 ```python class Solution: def fractionToDecimal(self, numerator: int, denominator: int) -> str: if numerator == 0: return '0' res = [] if numerator ^ denominator < 0: res.append('-') numerator, denominator = abs(numerator), abs(denominator) res.append(str(numerator // denominator)) numerator %= denominator if numerator == 0: return ''.join(res) res.append('.') record = dict() while numerator: if numerator not in record: record[numerator] = len(res) numerator *= 10 res.append(str(numerator // denominator)) numerator %= denominator else: res.insert(record[numerator], '(') res.append(')') break return ''.join(res) ``` ================================================ FILE: docs/solutions/0100-0199/gas-station.md ================================================ # [0134. 加油站](https://leetcode.cn/problems/gas-station/) - 标签:贪心、数组 - 难度:中等 ## 题目链接 - [0134. 加油站 - 力扣](https://leetcode.cn/problems/gas-station/) ## 题目大意 一条环路上有 N 个加油站,第 i 个加油站有 gas[i] 升汽油。 现在有一辆油箱无限容量的汽车,从第 i 个加油站开往第 i + 1 个加油站需要消耗汽油 cost[i] 升。如果汽车上携带的有两不够 cost[i],则无法从第 i 个加油站开往第 i + 1 个加油站。 现在从其中一个加油站开始出发,且出发时油箱为空。如果能绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。 ## 解题思路 1. 暴力求解 分别考虑从第 0 个点、第 1 个点、…、第 i 个点出发,能否回到第 0 个点、第 1 个点、…、第 i 个点。 2. 贪心算法 - 如果加油站提供的油总和大于等于消耗的汽油量,则必定可以绕环路行驶一周 - 假设先不考虑油量为负的情况,我们从「第 0 个加油站」出发,环行一周。记录下汽油量 gas[i] 和 cost[i] 差值总和 sum_diff,同时记录下油箱剩余油量的最小值 min_sum。 - 如果差值总和 sum_diff < 0,则无论如何都不能环行一周。油不够啊,亲!! - 如果 min_sum ≥ 0,则行驶过程中油箱始终有油,则可以从 0 个加油站出发环行一周。 - 如果 min_sum < 0,则说明行驶过程中油箱油不够了,那么考虑更换开始的起点。 - 从右至左遍历,计算汽油量 gas[i] 和 cost[i] 差值,看哪个加油站能将 min_sum 填平。如果最终达到 min_sum ≥ 0,则说明从该点开始出发,油箱中的油始终不为空,则返回该点下标。 - 如果找不到最返回 -1。 ## 代码 ```python class Solution: def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int: sum_diff, min_sum = 0, float('inf') for i in range(len(gas)): sum_diff += gas[i] - cost[i] min_sum = min(min_sum, sum_diff) if sum_diff < 0: return -1 if min_sum >= 0: return 0 for i in range(len(gas)-1, -1, -1): min_sum += gas[i] - cost[i] if min_sum >= 0: return i return -1 ``` ## 参考链接 - [贪心算法/前缀和 - 加油站 - 力扣(LeetCode)](https://leetcode.cn/problems/gas-station/solution/tan-xin-suan-fa-qian-zhui-he-by-antione/) ================================================ FILE: docs/solutions/0100-0199/house-robber.md ================================================ # [0198. 打家劫舍](https://leetcode.cn/problems/house-robber/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [0198. 打家劫舍 - 力扣](https://leetcode.cn/problems/house-robber/) ## 题目大意 **描述**:给定一个数组 $nums$,$nums[i]$ 代表第 $i$ 间房屋存放的金额。相邻的房屋装有防盗系统,假如相邻的两间房屋同时被偷,系统就会报警。 **要求**:假如你是一名专业的小偷,计算在不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。 **说明**: - $1 \le nums.length \le 100$。 - $0 \le nums[i] \le 400$。 **示例**: - 示例 1: ```python 输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4。 ``` - 示例 2: ```python 输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12。 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照房屋序号进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为:前 $i$ 间房屋所能偷窃到的最高金额。 ###### 3. 状态转移方程 $i$ 间房屋的最后一个房子是 $nums[i - 1]$。 如果房屋数大于等于 $2$ 间,则偷窃第 $i - 1$ 间房屋的时候,就有两种状态: 1. 偷窃第 $i - 1$ 间房屋,那么第 $i - 2$ 间房屋就不能偷窃了,偷窃的最高金额为:前 $i - 2$ 间房屋的最高总金额 + 第 $i - 1$ 间房屋的金额,即 $dp[i] = dp[i - 2] + nums[i - 1]$; 1. 不偷窃第 $i - 1$ 间房屋,那么第 $i - 2$ 间房屋可以偷窃,偷窃的最高金额为:前 $i - 1$ 间房屋的最高总金额,即 $dp[i] = dp[i - 1]$。 然后这两种状态取最大值即可,即状态转移方程为: $dp[i] = \begin{cases} nums[0] & i = 1 \cr max(dp[i - 2] + nums[i - 1], dp[i - 1]) & i \ge 2\end{cases}$ ###### 4. 初始条件 - 前 $0$ 间房屋所能偷窃到的最高金额为 $0$,即 $dp[0] = 0$。 - 前 $1$ 间房屋所能偷窃到的最高金额为 $nums[0]$,即:$dp[1] = nums[0]$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示为:前 $i$ 间房屋所能偷窃到的最高金额。则最终结果为 $dp[size]$,$size$ 为总的房屋数。 ### 思路 1:代码 ```python class Solution: def rob(self, nums: List[int]) -> int: size = len(nums) if size == 0: return 0 dp = [0 for _ in range(size + 1)] dp[0] = 0 dp[1] = nums[0] for i in range(2, size + 1): dp[i] = max(dp[i - 2] + nums[i - 1], dp[i - 1]) return dp[size] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/index.md ================================================ ## 本章内容 - [0100. 相同的树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/same-tree.md) - [0101. 对称二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/symmetric-tree.md) - [0102. 二叉树的层序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-level-order-traversal.md) - [0103. 二叉树的锯齿形层序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-zigzag-level-order-traversal.md) - [0104. 二叉树的最大深度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-depth-of-binary-tree.md) - [0105. 从前序与中序遍历序列构造二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/construct-binary-tree-from-preorder-and-inorder-traversal.md) - [0106. 从中序与后序遍历序列构造二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/construct-binary-tree-from-inorder-and-postorder-traversal.md) - [0107. 二叉树的层序遍历 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-level-order-traversal-ii.md) - [0108. 将有序数组转换为二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/convert-sorted-array-to-binary-search-tree.md) - [0109. 有序链表转换二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/convert-sorted-list-to-binary-search-tree.md) - [0110. 平衡二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/balanced-binary-tree.md) - [0111. 二叉树的最小深度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/minimum-depth-of-binary-tree.md) - [0112. 路径总和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/path-sum.md) - [0113. 路径总和 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/path-sum-ii.md) - [0114. 二叉树展开为链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/flatten-binary-tree-to-linked-list.md) - [0115. 不同的子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/distinct-subsequences.md) - [0116. 填充每个节点的下一个右侧节点指针](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/populating-next-right-pointers-in-each-node.md) - [0117. 填充每个节点的下一个右侧节点指针 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/populating-next-right-pointers-in-each-node-ii.md) - [0118. 杨辉三角](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/pascals-triangle.md) - [0119. 杨辉三角 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/pascals-triangle-ii.md) - [0120. 三角形最小路径和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/triangle.md) - [0121. 买卖股票的最佳时机](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock.md) - [0122. 买卖股票的最佳时机 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-ii.md) - [0123. 买卖股票的最佳时机 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-iii.md) - [0124. 二叉树中的最大路径和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-maximum-path-sum.md) - [0125. 验证回文串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/valid-palindrome.md) - [0126. 单词接龙 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/word-ladder-ii.md) - [0127. 单词接龙](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/word-ladder.md) - [0128. 最长连续序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/longest-consecutive-sequence.md) - [0129. 求根节点到叶节点数字之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sum-root-to-leaf-numbers.md) - [0130. 被围绕的区域](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/surrounded-regions.md) - [0131. 分割回文串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/palindrome-partitioning.md) - [0132. 分割回文串 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/palindrome-partitioning-ii.md) - [0133. 克隆图](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/clone-graph.md) - [0134. 加油站](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/gas-station.md) - [0135. 分发糖果](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/candy.md) - [0136. 只出现一次的数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number.md) - [0137. 只出现一次的数字 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/single-number-ii.md) - [0138. 随机链表的复制](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/copy-list-with-random-pointer.md) - [0139. 单词拆分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/word-break.md) - [0140. 单词拆分 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/word-break-ii.md) - [0141. 环形链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle.md) - [0142. 环形链表 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/linked-list-cycle-ii.md) - [0143. 重排链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reorder-list.md) - [0144. 二叉树的前序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-preorder-traversal.md) - [0145. 二叉树的后序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-postorder-traversal.md) - [0146. LRU 缓存](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/lru-cache.md) - [0147. 对链表进行插入排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/insertion-sort-list.md) - [0148. 排序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/sort-list.md) - [0149. 直线上最多的点数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/max-points-on-a-line.md) - [0150. 逆波兰表达式求值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/evaluate-reverse-polish-notation.md) - [0151. 反转字符串中的单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reverse-words-in-a-string.md) - [0152. 乘积最大子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-product-subarray.md) - [0153. 寻找旋转排序数组中的最小值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array.md) - [0154. 寻找旋转排序数组中的最小值 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-minimum-in-rotated-sorted-array-ii.md) - [0155. 最小栈](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/min-stack.md) - [0156. 上下翻转二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-upside-down.md) - [0157. 用 Read4 读取 N 个字符](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/read-n-characters-given-read4.md) - [0158. 用 Read4 读取 N 个字符 II - 多次调用](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/read-n-characters-given-read4-ii-call-multiple-times.md) - [0159. 至多包含两个不同字符的最长子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/longest-substring-with-at-most-two-distinct-characters.md) - [0160. 相交链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/intersection-of-two-linked-lists.md) - [0161. 相隔为 1 的编辑距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/one-edit-distance.md) - [0162. 寻找峰值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/find-peak-element.md) - [0163. 缺失的区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/missing-ranges.md) - [0164. 最大间距](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/maximum-gap.md) - [0165. 比较版本号](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/compare-version-numbers.md) - [0166. 分数到小数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/fraction-to-recurring-decimal.md) - [0167. 两数之和 II - 输入有序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/two-sum-ii-input-array-is-sorted.md) - [0168. Excel 表列名称](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/excel-sheet-column-title.md) - [0169. 多数元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/majority-element.md) - [0170. 两数之和 III - 数据结构设计](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/two-sum-iii-data-structure-design.md) - [0171. Excel 表列序号](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/excel-sheet-column-number.md) - [0172. 阶乘后的零](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/factorial-trailing-zeroes.md) - [0173. 二叉搜索树迭代器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-search-tree-iterator.md) - [0174. 地下城游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/dungeon-game.md) - [0179. 最大数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/largest-number.md) - [0186. 反转字符串中的单词 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reverse-words-in-a-string-ii.md) - [0187. 重复的DNA序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/repeated-dna-sequences.md) - [0188. 买卖股票的最佳时机 IV](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/best-time-to-buy-and-sell-stock-iv.md) - [0189. 轮转数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/rotate-array.md) - [0190. 颠倒二进制位](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/reverse-bits.md) - [0191. 位1的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/number-of-1-bits.md) - [0198. 打家劫舍](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/house-robber.md) - [0199. 二叉树的右视图](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/binary-tree-right-side-view.md) ================================================ FILE: docs/solutions/0100-0199/insertion-sort-list.md ================================================ # [0147. 对链表进行插入排序](https://leetcode.cn/problems/insertion-sort-list/) - 标签:链表、排序 - 难度:中等 ## 题目链接 - [0147. 对链表进行插入排序 - 力扣](https://leetcode.cn/problems/insertion-sort-list/) ## 题目大意 **描述**:给定链表的头节点 `head`。 **要求**:对链表进行插入排序。 **说明**: - 插入排序算法: - 插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。 - 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。 - 重复直到所有输入数据插入完为止。 - 列表中的节点数在 $[1, 5000]$ 范围内。 - $-5000 \le Node.val \le 5000$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/04/sort1linked-list.jpg) ```python 输入: head = [4,2,1,3] 输出: [1,2,3,4] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/04/sort2linked-list.jpg) ```python 输入: head = [-1,5,3,4,0] 输出: [-1,0,3,4,5] ``` ## 解题思路 ### 思路 1:链表插入排序 1. 先使用哑节点 `dummy_head` 构造一个指向 `head` 的指针,使得可以从 `head` 开始遍历。 2. 维护 `sorted_list` 为链表的已排序部分的最后一个节点,初始时,`sorted_list = head`。 3. 维护 `prev` 为插入元素位置的前一个节点,维护 `cur` 为待插入元素。初始时,`prev = head`,`cur = head.next`。 4. 比较 `sorted_list` 和 `cur` 的节点值。 - 如果 `sorted_list.val <= cur.val`,说明 `cur` 应该插入到 `sorted_list` 之后,则将 `sorted_list` 后移一位。 - 如果 `sorted_list.val > cur.val`,说明 `cur` 应该插入到 `head` 与 `sorted_list` 之间。则使用 `prev` 从 `head` 开始遍历,直到找到插入 `cur` 的位置的前一个节点位置。然后将 `cur` 插入。 5. 令 `cur = sorted_list.next`,此时 `cur` 为下一个待插入元素。 6. 重复 4、5 步骤,直到 `cur` 遍历结束为空。返回 `dummy_head` 的下一个节点。 ### 思路 1:代码 ```python def insertionSortList(self, head: ListNode) -> ListNode: if not head or not head.next: return head dummy_head = ListNode(-1) dummy_head.next = head sorted_list = head cur = head.next while cur: if sorted_list.val <= cur.val: # 将 cur 插入到 sorted_list 之后 sorted_list = sorted_list.next else: prev = dummy_head while prev.next.val <= cur.val: prev = prev.next # 将 cur 到链表中间 sorted_list.next = cur.next cur.next = prev.next prev.next = cur cur = sorted_list.next return dummy_head.next ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/intersection-of-two-linked-lists.md ================================================ # [0160. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/) - 标签:哈希表、链表、双指针 - 难度:简单 ## 题目链接 - [0160. 相交链表 - 力扣](https://leetcode.cn/problems/intersection-of-two-linked-lists/) ## 题目大意 **描述**:给定 `listA`、`listB` 两个链表。 **要求**:判断两个链表是否相交,返回相交的起始点。如果不相交,则返回 `None`。 **说明**: - `listA` 中节点数目为 $m$。 - `listB` 中节点数目为 $n$。 - $1 \le m, n \le 3 * 10^4$。 - $1 \le Node.val \le 10^5$。 - $0 \le skipA \le m$。 - $0 \le skipB \le n$。 - 如果 `listA` 和 `listB` 没有交点,`intersectVal` 为 $0$。 - 如果 `listA` 和 `listB` 有交点,`intersectVal == listA[skipA] == listB[skipB]`。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2018/12/13/160_example_1.png) ```python 输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3 输出:Intersected at '8' 解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。 — 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/05/160_example_2.png) ```python 输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 输出:Intersected at '2' 解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。 在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。 ``` ## 解题思路 ### 思路 1:双指针 如果两个链表相交,那么从相交位置开始,到结束,必有一段等长且相同的节点。假设链表 `listA` 的长度为 $m$、链表 `listB` 的长度为 $n$,他们的相交序列有 $k$ 个,则相交情况可以如下如所示: ![](https://qcdn.itcharge.cn/images/20210401113538.png) 现在问题是如何找到 $m - k$ 或者 $n - k$ 的位置。 考虑将链表 `listA` 的末尾拼接上链表 `listB`,链表 `listB` 的末尾拼接上链表 `listA`。 然后使用两个指针 `pA` 、`pB`,分别从链表 `listA`、链表 `listB` 的头节点开始遍历,如果走到共同的节点,则返回该节点。 否则走到两个链表末尾,返回 `None`。 ![](https://qcdn.itcharge.cn/images/20210401114100.png) ### 思路 1:代码 ```python class Solution: def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: if headA == None or headB == None: return None pA = headA pB = headB while pA != pB: pA = pA.next if pA != None else headB pB = pB.next if pB != None else headA return pA ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/largest-number.md ================================================ # [0179. 最大数](https://leetcode.cn/problems/largest-number/) - 标签:贪心、数组、字符串、排序 - 难度:中等 ## 题目链接 - [0179. 最大数 - 力扣](https://leetcode.cn/problems/largest-number/) ## 题目大意 **描述**:给定一个非负整数数组 `nums`。 **要求**:重新排列数组中每个数的顺序,使之将数组中所有数字按顺序拼接起来所组成的整数最大。 **说明**: - $1 \le nums.length \le 100$。 - $0 \le nums[i] \le 10^9$。 **示例**: - 示例 1: ```python 输入:nums = [10,2] 输出:"210" ``` - 示例 2: ```python 输入:nums = [3,30,34,5,9] 输出:"9534330" ``` ## 解题思路 ### 思路 1:排序 本质上是给数组进行排序。假设 `x`、`y` 是数组 `nums` 中的两个元素。如果拼接字符串 `x + y < y + x`,则 `y > x `。`y` 应该排在 `x` 前面。反之,则 `y < x`。 按照上述规则,对原数组进行排序即可。这里我们使用了 `functools.cmp_to_key` 自定义排序函数。 ### 思路 1:代码 ```python import functools class Solution: def largestNumber(self, nums: List[int]) -> str: def cmp(a, b): if a + b == b + a: return 0 elif a + b > b + a: return 1 else: return -1 nums_s = list(map(str, nums)) nums_s.sort(key=functools.cmp_to_key(cmp), reverse=True) return str(int(''.join(nums_s))) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。其中 $n$ 是给定数组 `nums` 的大小。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/linked-list-cycle-ii.md ================================================ # [0142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/) - 标签:哈希表、链表、双指针 - 难度:中等 ## 题目链接 - [0142. 环形链表 II - 力扣](https://leetcode.cn/problems/linked-list-cycle-ii/) ## 题目大意 **描述**:给定一个链表的头节点 `head`。 **要求**:判断链表中是否有环,如果有环则返回入环的第一个节点,无环则返回 `None`。 **说明**: - 链表中节点的数目范围在范围 $[0, 10^4]$ 内。 - $-10^5 \le Node.val \le 10^5$。 - `pos` 的值为 `-1` 或者链表中的一个有效索引。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2018/12/07/circularlinkedlist.png) ```python 输入:head = [3,2,0,-4], pos = 1 输出:返回索引为 1 的链表节点 解释:链表中有一个环,其尾部连接到第二个节点。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test2.png) ```python 输入:head = [1,2], pos = 0 输出:返回索引为 0 的链表节点 解释:链表中有一个环,其尾部连接到第一个节点。 ``` ## 解题思路 ### 思路 1:快慢指针(Floyd 判圈算法) 1. 利用两个指针,一个慢指针 `slow` 每次前进一步,快指针 `fast` 每次前进两步(两步或多步效果是等价的)。 2. 如果两个指针在链表头节点以外的某一节点相遇(即相等)了,那么说明链表有环。 3. 否则,如果(快指针)到达了某个没有后继指针的节点时,那么说明没环。 4. 如果有环,则再定义一个指针 `ans`,和慢指针一起每次移动一步,两个指针相遇的位置即为入口节点。 这是因为:假设入环位置为 `A`,快慢指针在 `B` 点相遇,则相遇时慢指针走了 $a + b$ 步,快指针走了 $a + n(b+c) + b$ 步。 因为快指针总共走的步数是慢指针走的步数的两倍,即 $2(a + b) = a + n(b + c) + b$,所以可以推出:$a = c + (n-1)(b + c)$。 我们可以发现:从相遇点到入环点的距离 $c$ 加上 $n-1$ 圈的环长 $b + c$ 刚好等于从链表头部到入环点的距离。 ### 思路 1:代码 ```python class Solution: def detectCycle(self, head: ListNode) -> ListNode: fast, slow = head, head while True: if not fast or not fast.next: return None fast = fast.next.next slow = slow.next if fast == slow: break ans = head while ans != slow: ans, slow = ans.next, slow.next return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/linked-list-cycle.md ================================================ # [0141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/) - 标签:哈希表、链表、双指针 - 难度:简单 ## 题目链接 - [0141. 环形链表 - 力扣](https://leetcode.cn/problems/linked-list-cycle/) ## 题目大意 **描述**:给定一个链表的头节点 `head`。 **要求**:判断链表中是否有环。如果有环则返回 `True`,否则返回 `False`。 **说明**: - 链表中节点的数目范围是 $[0, 10^4]$。 - $-10^5 \le Node.val \le 10^5$。 - `pos` 为 `-1` 或者链表中的一个有效索引。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist.png) ```python 输入:head = [3,2,0,-4], pos = 1 输出:True 解释:链表中有一个环,其尾部连接到第二个节点。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/07/circularlinkedlist_test2.png) ```python 输入:head = [1,2], pos = 0 输出:True 解释:链表中有一个环,其尾部连接到第一个节点。 ``` ## 解题思路 ### 思路 1:哈希表 最简单的思路是遍历所有节点,每次遍历节点之前,使用哈希表判断该节点是否被访问过。如果访问过就说明存在环,如果没访问过则将该节点添加到哈希表中,继续遍历判断。 ### 思路 1:代码 ```python class Solution: def hasCycle(self, head: ListNode) -> bool: nodeset = set() while head: if head in nodeset: return True nodeset.add(head) head = head.next return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ### 思路 2:快慢指针(Floyd 判圈算法) 这种方法类似于在操场跑道跑步。两个人从同一位置同时出发,如果跑道有环(环形跑道),那么快的一方总能追上慢的一方。 基于上边的想法,Floyd 用两个指针,一个慢指针(龟)每次前进一步,快指针(兔)指针每次前进两步(两步或多步效果是等价的)。如果两个指针在链表头节点以外的某一节点相遇(即相等)了,那么说明链表有环,否则,如果(快指针)到达了某个没有后继指针的节点时,那么说明没环。 ### 思路 2:代码 ```python class Solution: def hasCycle(self, head: ListNode) -> bool: if head == None or head.next == None: return False slow = head fast = head.next while slow != fast: if fast == None or fast.next == None: return False slow = slow.next fast = fast.next.next return True ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/longest-consecutive-sequence.md ================================================ # [0128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/) - 标签:并查集、数组、哈希表 - 难度:中等 ## 题目链接 - [0128. 最长连续序列 - 力扣](https://leetcode.cn/problems/longest-consecutive-sequence/) ## 题目大意 **描述**:给定一个未排序的整数数组 `nums`。 **要求**:找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。并且要用时间复杂度为 $O(n)$ 的算法解决此问题。 **说明**: - $0 \le nums.length \le 10^5$。 - $-10^9 \le nums[i] \le 10^9$。 **示例**: - 示例 1: ```python 输入:nums = [100,4,200,1,3,2] 输出:4 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。 ``` - 示例 2: ```python 输入:nums = [0,3,7,2,5,8,4,6,0,1] 输出:9 ``` ## 解题思路 暴力做法有两种思路。 - 第 1 种思路是先排序再依次判断,这种做法时间复杂度最少是 $O(n \log_2 n)$。 - 第 2 种思路是枚举数组中的每个数 `num`,考虑以其为起点,不断尝试匹配 `num + 1`、`num + 2`、`...` 是否存在,最长匹配次数为 `len(nums)`。这样下来时间复杂度为 $O(n^2)$。 我们可以使用哈希表优化这个过程。 ### 思路 1:哈希表 1. 先将数组存储到集合中进行去重,然后使用 `curr_streak` 维护当前连续序列长度,使用 `ans` 维护最长连续序列长度。 2. 遍历集合中的元素,对每个元素进行判断,如果该元素不是序列的开始(即 `num - 1` 在集合中),则跳过。 3. 如果 `num - 1` 不在集合中,说明 `num` 是序列的开始,判断 `num + 1` 、`nums + 2`、`...` 是否在哈希表中,并不断更新当前连续序列长度 `curr_streak`。并在遍历结束之后更新最长序列的长度。 4. 最后输出最长序列长度。 ### 思路 1:代码 ```python class Solution: def longestConsecutive(self, nums: List[int]) -> int: ans = 0 nums_set = set(nums) for num in nums_set: if num - 1 not in nums_set: curr_num = num curr_streak = 1 while curr_num + 1 in nums_set: curr_num += 1 curr_streak += 1 ans = max(ans, curr_streak) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。将数组存储到集合中进行去重的操作的时间复杂度是 $O(n)$。查询每个数是否在集合中的时间复杂度是 $O(1)$ ,并且跳过了所有不是起点的元素。更新当前连续序列长度 `curr_streak` 的时间复杂度是 $O(n)$,所以最终的时间复杂度是 $O(n)$。 - **空间复杂度**:$O(n)$。 ## 参考资料 - 【题解】[128. 最长连续序列 - 力扣(Leetcode)](https://leetcode.cn/problems/longest-consecutive-sequence/solutions/1176496/xiao-bai-lang-ha-xi-ji-he-ha-xi-biao-don-j5a2/) ================================================ FILE: docs/solutions/0100-0199/longest-substring-with-at-most-two-distinct-characters.md ================================================ # [0159. 至多包含两个不同字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-most-two-distinct-characters/) - 标签:哈希表、字符串、滑动窗口 - 难度:中等 ## 题目链接 - [0159. 至多包含两个不同字符的最长子串 - 力扣](https://leetcode.cn/problems/longest-substring-with-at-most-two-distinct-characters/) ## 题目大意 给定一个字符串 s,找出之多包含两个不同字符的最长子串 t,并返回该子串的长度。 ## 解题思路 使用滑动窗口来求解。 left,right 指向字符串开始位置。 不断向右移动 right 指针,使用 count 变量来统计滑动窗口中共有多少个字符,以及使用哈希表来统计当前字符的频数。 当滑动窗口的字符多于 2 个时,向右 移动 left 指针,并减少哈希表中对应原 left 指向字符的频数。 最后使用 max_count 来维护最长子串 t 的长度。 ## 代码 ```python import collections class Solution: def lengthOfLongestSubstringTwoDistinct(self, s: str) -> int: max_count = 0 k = 2 counts = collections.defaultdict(int) count = 0 left, right = 0, 0 while right < len(s): if counts[s[right]] == 0: count += 1 counts[s[right]] += 1 right += 1 if count > k: if counts[s[left]] == 1: count -= 1 counts[s[left]] -= 1 left += 1 max_count = max(max_count, right - left) return max_count ``` ================================================ FILE: docs/solutions/0100-0199/lru-cache.md ================================================ # [0146. LRU 缓存](https://leetcode.cn/problems/lru-cache/) - 标签:设计、哈希表、链表、双向链表 - 难度:中等 ## 题目链接 - [0146. LRU 缓存 - 力扣](https://leetcode.cn/problems/lru-cache/) ## 题目大意 **要求**: 设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。 实现 `LRUCache` 类: - `LRUCache(int capacity)` 以 正整数 作为容量 $capacity$ 初始化 LRU 缓存 - `int get(int key)` 如果关键字 $key$ 存在于缓存中,则返回关键字的值,否则返回 $-1$。 - `void put(int key, int value)` - 如果关键字 $key$ 已经存在,则变更其数据值 $value$; - 如果不存在,则向缓存中插入该组 $key-value$。 - 如果插入操作导致关键字数量超过 $capacity$,则应该逐出最久未使用的关键字。 函数 `get` 和 `put` 必须以 $O(1)$ 的平均时间复杂度运行。 **说明**: - $1 \le capacity \le 3000$。 - $0 \le key \le 10000$。 - $0 \le value \le 10^{5}$。 - 最多调用 $2 \times 10^{5}$ 次 `get` 和 `put`。 **示例**: - 示例 1: ```python 输入 ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] 输出 [null, null, null, 1, null, -1, null, -1, 3, 4] 解释 LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // 缓存是 {1=1} lRUCache.put(2, 2); // 缓存是 {1=1, 2=2} lRUCache.get(1); // 返回 1 lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3} lRUCache.get(2); // 返回 -1 (未找到) lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3} lRUCache.get(1); // 返回 -1 (未找到) lRUCache.get(3); // 返回 3 lRUCache.get(4); // 返回 4 ``` ## 解题思路 ### 思路 1:双向链表 + 哈希表 LRU (Least Recently Used) 缓存的核心思想是:当缓存容量达到上限时,删除最久未使用的数据。 为了实现 $O(1)$ 时间复杂度的 `get` 和 `put` 操作,我们需要: 1. **哈希表**:用于快速查找节点,时间复杂度 $O(1)$。 2. **双向链表**:用于维护访问顺序,最近访问的节点在头部,最久未访问的节点在尾部。 **数据结构设计**: - 使用双向链表节点存储 $key$ 和 $value$。 - 使用哈希表 $hash\_map$ 存储 $key$ 到节点的映射。 - 维护虚拟头节点 $head$ 和虚拟尾节点 $tail$,简化边界处理。 **核心操作**: 1. **访问节点**:将节点移动到链表头部。 2. **添加节点**:在链表头部添加新节点。 3. **删除节点**:从链表尾部删除最久未使用的节点。 ### 思路 1:代码 ```python class ListNode: """双向链表节点""" def __init__(self, key=0, value=0): self.key = key self.value = value self.prev = None self.next = None class LRUCache: def __init__(self, capacity: int): self.capacity = capacity self.size = 0 # 哈希表:key -> 节点 self.hash_map = {} # 虚拟头节点和尾节点 self.head = ListNode() self.tail = ListNode() self.head.next = self.tail self.tail.prev = self.head def _add_to_head(self, node): """将节点添加到链表头部""" node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node def _remove_node(self, node): """从链表中删除节点""" node.prev.next = node.next node.next.prev = node.prev def _move_to_head(self, node): """将节点移动到链表头部""" self._remove_node(node) self._add_to_head(node) def _remove_tail(self): """删除链表尾部节点""" last_node = self.tail.prev self._remove_node(last_node) return last_node def get(self, key: int) -> int: if key in self.hash_map: # 节点存在,移动到头部 node = self.hash_map[key] self._move_to_head(node) return node.value return -1 def put(self, key: int, value: int) -> None: if key in self.hash_map: # 节点存在,更新值并移动到头部 node = self.hash_map[key] node.value = value self._move_to_head(node) else: # 节点不存在,创建新节点 new_node = ListNode(key, value) if self.size >= self.capacity: # 缓存已满,删除尾部节点 tail_node = self._remove_tail() del self.hash_map[tail_node.key] self.size -= 1 # 添加新节点到头部 self.hash_map[key] = new_node self._add_to_head(new_node) self.size += 1 # Your LRUCache object will be instantiated and called as such: # obj = LRUCache(capacity) # param_1 = obj.get(key) # obj.put(key,value) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `get(key)`:$O(1)$,哈希表查找 + 链表操作。 - `put(key, value)`:$O(1)$,哈希表操作 + 链表操作。 - **空间复杂度**:$O(capacity)$,其中 $capacity$ 是缓存容量,哈希表和双向链表最多存储 $capacity$ 个节点。 ================================================ FILE: docs/solutions/0100-0199/majority-element.md ================================================ # [0169. 多数元素](https://leetcode.cn/problems/majority-element/) - 标签:数组、哈希表、分治、计数、排序 - 难度:简单 ## 题目链接 - [0169. 多数元素 - 力扣](https://leetcode.cn/problems/majority-element/) ## 题目大意 **描述**:给定一个大小为 $n$ 的数组 $nums$。 **要求**:返回其中的多数元素。 **说明**: - **多数元素**:指在数组中出现次数大于 $\lfloor \frac{n}{2} \rfloor$ 的元素。 - 假设数组是非空的,并且给定的数组总是存在多数元素。 - $n == nums.length$。 - $1 \le n \le 5 \times 10^4$。 - $-10^9 \le nums[i] \le 10^9$。 **示例**: - 示例 1: ```python 输入:nums = [3,2,3] 输出:3 ``` - 示例 2: ```python 输入:nums = [2,2,1,1,1,2,2] 输出:2 ``` ## 解题思路 ### 思路 1:哈希表 1. 遍历数组 $nums$。 2. 对于当前元素 $num$,用哈希表统计每个元素 $num$ 出现的次数。 3. 再遍历一遍哈希表,找出元素个数最多的元素即可。 ### 思路 1:代码 ```python class Solution: def majorityElement(self, nums: List[int]) -> int: numDict = dict() for num in nums: if num in numDict: numDict[num] += 1 else: numDict[num] = 1 max = float('-inf') max_index = -1 for num in numDict: if numDict[num] > max: max = numDict[num] max_index = num return max_index ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ### 思路 2:分治算法 如果 $num$ 是数组 $nums$ 的众数,那么我们将 $nums$ 分为两部分,则 $num$ 至少是其中一部分的众数。 则我们可以用分治法来解决这个问题。具体步骤如下: 1. 将数组 $nums$ 递归地将当前序列平均分成左右两个数组,直到所有子数组长度为 $1$。 2. 长度为 $1$ 的子数组众数肯定是数组中唯一的数,将其返回即可。 3. 将两个子数组依次向上两两合并。 1. 如果两个子数组的众数相同,则说明合并后的数组众数为:两个子数组的众数。 2. 如果两个子数组的众数不同,则需要比较两个众数在整个区间的众数。 4. 最后返回整个数组的众数。 ### 思路 2:代码 ```python class Solution: def majorityElement(self, nums: List[int]) -> int: def get_mode(low, high): if low == high: return nums[low] mid = low + (high - low) // 2 left_mod = get_mode(low, mid) right_mod = get_mode(mid + 1, high) if left_mod == right_mod: return left_mod left_mod_cnt, right_mod_cnt = 0, 0 for i in range(low, high + 1): if nums[i] == left_mod: left_mod_cnt += 1 if nums[i] == right_mod: right_mod_cnt += 1 if left_mod_cnt > right_mod_cnt: return left_mod return right_mod return get_mode(0, len(nums) - 1) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0100-0199/max-points-on-a-line.md ================================================ # [0149. 直线上最多的点数](https://leetcode.cn/problems/max-points-on-a-line/) - 标签:几何、数组、哈希表、数学 - 难度:困难 ## 题目链接 - [0149. 直线上最多的点数 - 力扣](https://leetcode.cn/problems/max-points-on-a-line/) ## 题目大意 给定一个平面上的 n 个点的坐标数组 points,求解最多有多少个点在同一条直线上。 ## 解题思路 两个点可以确定一条直线,固定其中一个点,求其他点与该点的斜率,斜率相同的点则在同一条直线上。可以考虑把斜率当做哈希表的键值,存储经过该点,不同斜率的直线上经过的点数目。 对于点 i,查找经过该点的直线只需要考虑 (i+1,n-1) 位置上的点即可,因为 i-1 之前的点已经在遍历点 i-2 的时候考虑过了。 斜率的计算公式为 $\frac{dy}{dx} = \frac{y_j - y_i}{x_j - x_i}$。 因为斜率是小数会有精度误差,所以我们考虑使用 (dx, dy) 的元组作为哈希表的 key。 > 注意: > > 需要处理倍数关系,dy、dx 异号情况,以及处理垂直直线(两点横坐标差为 0)的水平直线(两点横坐标差为 0)的情况。 ## 代码 ```python class Solution: def maxPoints(self, points: List[List[int]]) -> int: n = len(points) if n < 3: return n ans = 0 for i in range(n): line_dict = dict() line_dict[0] = 0 same = 1 for j in range(i+1, n): dx = points[j][0] - points[i][0] dy = points[j][1] - points[i][1] if dx == 0 and dy == 0: same += 1 continue gcd_dx_dy = math.gcd(abs(dx), abs(dy)) if (dx > 0 and dy > 0) or (dx < 0 and dy < 0): dx = abs(dx) // gcd_dx_dy dy = abs(dy) // gcd_dx_dy elif dx < 0 and dy > 0: dx = -dx // gcd_dx_dy dy = -dy // gcd_dx_dy elif dx > 0 and dy < 0: dx = dx // gcd_dx_dy dy = dy // gcd_dx_dy elif dx == 0 and dy != 0: dy = 1 elif dx != 0 and dy == 0: dx = 1 key = (dx, dy) if key in line_dict: line_dict[key] += 1 else: line_dict[key] = 1 ans = max(ans, same + max(line_dict.values())) return ans ``` ================================================ FILE: docs/solutions/0100-0199/maximum-depth-of-binary-tree.md ================================================ # [0104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0104. 二叉树的最大深度 - 力扣](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:找出该二叉树的最大深度。 **说明**: - **二叉树的深度**:根节点到最远叶子节点的最长路径上的节点数。 - **叶子节点**:没有子节点的节点。 **示例**: - 示例 1: ```python 输入:[3,9,20,null,null,15,7] 对应二叉树 3 / \ 9 20 / \ 15 7 输出:3 解释:该二叉树的最大深度为 3 ``` ## 解题思路 ### 思路 1: 递归算法 根据递归三步走策略,写出对应的递归代码。 1. 写出递推公式:`当前二叉树的最大深度 = max(当前二叉树左子树的最大深度, 当前二叉树右子树的最大深度) + 1`。 - 即:先得到左右子树的高度,在计算当前节点的高度。 2. 明确终止条件:当前二叉树为空。 3. 翻译为递归代码: 1. 定义递归函数:`maxDepth(self, root)` 表示输入参数为二叉树的根节点 `root`,返回结果为该二叉树的最大深度。 2. 书写递归主体:`return max(self.maxDepth(root.left) + self.maxDepth(root.right))`。 3. 明确递归终止条件:`if not root: return 0` ### 思路 1:代码 ```python class Solution: def maxDepth(self, root: Optional[TreeNode]) -> int: if not root: return 0 return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/maximum-gap.md ================================================ # [0164. 最大间距](https://leetcode.cn/problems/maximum-gap/) - 标签:数组、桶排序、基数排序、排序 - 难度:困难 ## 题目链接 - [0164. 最大间距 - 力扣](https://leetcode.cn/problems/maximum-gap/) ## 题目大意 **描述**:给定一个无序数组 $nums$。 **要求**:找出数组在排序之后,相邻元素之间最大的差值。如果数组元素个数小于 $2$,则返回 $0$。 **说明**: - 所有元素都是非负整数,且数值在 $32$ 位有符号整数范围内。 - 请尝试在线性时间复杂度和空间复杂度的条件下解决此问题。 **示例**: - 示例 1: ```python 输入: nums = [3,6,9,1] 输出: 3 解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。 ``` - 示例 2: ```python 输入: nums = [10] 输出: 0 解释: 数组元素个数小于 2,因此返回 0。 ``` ## 解题思路 ### 思路 1:基数排序 这道题的难点在于要求时间复杂度和空间复杂度为 $O(n)$。 这道题分为两步: 1. 数组排序。 2. 计算相邻元素之间的差值。 第 2 步直接遍历数组求解即可,时间复杂度为 $O(n)$。所以关键点在于找到一个时间复杂度和空间复杂度为 $O(n)$ 的排序算法。根据题意可知所有元素都是非负整数,且数值在 32 位有符号整数范围内。所以我们可以选择基数排序。基数排序的步骤如下: - 遍历数组元素,获取数组最大值元素,并取得位数。 - 以个位元素为索引,对数组元素排序。 - 合并数组。 - 之后依次以十位,百位,…,直到最大值元素的最高位处值为索引,进行排序,并合并数组,最终完成排序。 最后,还要注意数组元素个数小于 $2$ 的情况需要特别判断一下。 ### 思路 1:代码 ```python class Solution: def radixSort(self, arr): size = len(str(max(arr))) for i in range(size): buckets = [[] for _ in range(10)] for num in arr: buckets[num // (10 ** i) % 10].append(num) arr.clear() for bucket in buckets: for num in bucket: arr.append(num) return arr def maximumGap(self, nums: List[int]) -> int: if len(nums) < 2: return 0 arr = self.radixSort(nums) return max(arr[i] - arr[i - 1] for i in range(1, len(arr))) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/maximum-product-subarray.md ================================================ # [0152. 乘积最大子数组](https://leetcode.cn/problems/maximum-product-subarray/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [0152. 乘积最大子数组 - 力扣](https://leetcode.cn/problems/maximum-product-subarray/) ## 题目大意 **描述**:给定一个整数数组 `nums`。 **要求**:找出数组中乘积最大的连续子数组(最少包含一个数字),并返回该子数组对应的乘积。 **说明**: - 测试用例的答案是一个 32-位整数。 - **子数组**:数组的连续子序列。 - $1 \le nums.length \le 2 * 10^4$。 - $-10 \le nums[i] \le 10$。 - `nums` 的任何前缀或后缀的乘积都保证是一个 32-位整数。 **示例**: - 示例 1: ```python 输入: nums = [2,3,-2,4] 输出: 6 解释: 子数组 [2,3] 有最大乘积 6。 ``` - 示例 2: ```python 输入: nums = [-2,0,-1] 输出: 0 解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。 ``` ## 解题思路 ### 思路 1:动态规划 这道题跟「[0053. 最大子序和](https://leetcode.cn/problems/maximum-subarray/)」有点相似,不过一个求的是和的最大值,这道题求解的是乘积的最大值。 乘积有个特殊情况,两个正数、两个负数相乘都会得到正数。所以求解的时候需要考虑负数的情况。 如果想要最终的乘积最大,则应该使子数组中的正数元素尽可能的大,负数元素尽可能的小。所以我们可以维护一个最大值变量和最小值变量。 ###### 1. 阶段划分 按照子数组的结尾位置进行阶段划分。 ###### 2. 定义状态 定义状态 `dp_max[i]` 为:以第 $i$ 个元素结尾的乘积最大子数组的乘积。 定义状态 `dp_min[i]` 为:以第 $i$ 个元素结尾的乘积最小子数组的乘积。 ###### 3. 状态转移方程 - `dp_max[i] = max(dp_max[i - 1] * nums[i], nums[i], dp_min[i - 1] * nums[i])` - `dp_min[i] = min(dp_min[i - 1] * nums[i], nums[i], dp_max[i - 1] * nums[i])` ###### 4. 初始条件 - 以第 $0$ 个元素结尾的乘积最大子数组的乘积为 `nums[0]`,即 `dp_max[0] = nums[0]`。 - 以第 $0$ 个元素结尾的乘积最小子数组的乘积为 `nums[0]`,即 `dp_min[0] = nums[0]`。 ###### 5. 最终结果 根据状态定义,最终结果为 $dp_{max}$ 中最大值,即乘积最大子数组的乘积。 ### 思路 1:代码 ```python class Solution: def maxProduct(self, nums: List[int]) -> int: size = len(nums) dp_max = [0 for _ in range(size)] dp_min = [0 for _ in range(size)] dp_max[0] = nums[0] dp_min[0] = nums[0] ans = nums[0] for i in range(1, size): dp_max[i] = max(dp_max[i - 1] * nums[i], nums[i], dp_min[i - 1] * nums[i]) dp_min[i] = min(dp_min[i - 1] * nums[i], nums[i], dp_max[i - 1] * nums[i]) return max(dp_max) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为整数数组 `nums` 的元素个数。 - **空间复杂度**:$O(n)$。 ### 思路 2:动态规划 + 滚动优化 因为状态转移方程中只涉及到当前元素和前一个元素,所以我们也可以不使用数组,只使用两个变量来维护 $dp_{max}[i]$ 和 $dp_{min}[i]$。 ### 思路 2:代码 ```python class Solution: def maxProduct(self, nums: List[int]) -> int: size = len(nums) max_num, min_num = nums[0], nums[0] ans = nums[0] for i in range(1, size): temp_max = max_num temp_min = min_num max_num = max(temp_max * nums[i], nums[i], temp_min * nums[i]) min_num = min(temp_min * nums[i], nums[i], temp_max * nums[i]) ans = max(max_num, ans) return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为整数数组 `nums` 的元素个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/min-stack.md ================================================ # [0155. 最小栈](https://leetcode.cn/problems/min-stack/) - 标签:栈、设计 - 难度:中等 ## 题目链接 - [0155. 最小栈 - 力扣](https://leetcode.cn/problems/min-stack/) ## 题目大意 **要求**:设计一个「栈」。实现 `push` ,`pop` ,`top` ,`getMin` 操作,其中 `getMin` 要求能在常数时间内实现。 **说明**: - $-2^{31} \le val \le 2^{31} - 1$。 - `pop`、`top` 和 `getMin` 操作总是在非空栈上调用 - `push`,`pop`,`top` 和 `getMin` 最多被调用 $3 * 10^4$ 次。 **示例**: - 示例 1: ```python 输入: ["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]] 输出: [null,null,null,null,-3,null,0,-2] 解释: MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); --> 返回 -3. minStack.pop(); minStack.top(); --> 返回 0. minStack.getMin(); --> 返回 -2. ``` ## 解题思路 题目要求在常数时间内获取最小值,所以我们不能在 `getMin` 操作时,再去计算栈中的最小值。而是应该在 `push`、`pop` 操作时就已经计算好了最小值。我们有两种思路来解决这道题。 ### 思路 1:辅助栈 使用辅助栈保存当前栈中的最小值。在元素入栈出栈时,两个栈同步保持插入和删除。具体做法如下: - `push` 操作:当一个元素入栈时,取辅助栈的栈顶存储的最小值,与当前元素进行比较得出最小值,将最小值插入到辅助栈中;该元素也插入到正常栈中。 - `pop` 操作:当一个元素要出栈时,将辅助栈的栈顶元素一起弹出。 - `top` 操作:返回正常栈的栈顶元素值。 - `getMin` 操作:返回辅助栈的栈顶元素值。 ### 思路 1:代码 ```python class MinStack: def __init__(self): self.stack = [] self.minstack = [] def push(self, val: int) -> None: if not self.stack: self.stack.append(val) self.minstack.append(val) else: self.stack.append(val) self.minstack.append(min(val, self.minstack[-1])) def pop(self) -> None: self.stack.pop() self.minstack.pop() def top(self) -> int: return self.stack[-1] def getMin(self) -> int: return self.minstack[-1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。栈的插入、删除、读取操作都是 $O(1)$。 - **空间复杂度**:$O(n)$。其中 $n$ 为总操作数。 ### 思路 2:单个栈 使用单个栈,保存元组:(当前元素值,当前栈内最小值)。具体操作如下: - `push` 操作:如果栈不为空,则判断当前元素值与栈顶元素所保存的最小值,并更新当前最小值,然后将新元素和当前最小值组成的元组保存到栈中。 - `pop`操作:正常出栈,即将栈顶元素弹出。 - `top` 操作:返回栈顶元素保存的值。 - `getMin` 操作:返回栈顶元素保存的最小值。 ### 思路 2:代码 ```python class MinStack: def __init__(self): """ initialize your data structure here. """ self.stack = [] class Node: def __init__(self, x): self.val = x self.min = x def push(self, val: int) -> None: node = self.Node(val) if len(self.stack) == 0: self.stack.append(node) else: topNode = self.stack[-1] if node.min > topNode.min: node.min = topNode.min self.stack.append(node) def pop(self) -> None: self.stack.pop() def top(self) -> int: return self.stack[-1].val def getMin(self) -> int: return self.stack[-1].min ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(1)$。栈的插入、删除、读取操作都是 $O(1)$。 - **空间复杂度**:$O(n)$。其中 $n$ 为总操作数。 ================================================ FILE: docs/solutions/0100-0199/minimum-depth-of-binary-tree.md ================================================ # [0111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索 - 难度:简单 ## 题目链接 - [0111. 二叉树的最小深度 - 力扣](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) ## 题目大意 **描述**:给定一个二叉树的根节点 $root$。 **要求**:找出该二叉树的最小深度。 **说明**: - **最小深度**:从根节点到最近叶子节点的最短路径上的节点数量。 - **叶子节点**:指没有子节点的节点。 - 树中节点数的范围在 $[0, 10^5]$ 内。 - $-1000 \le Node.val \le 1000$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/12/ex_depth.jpg) ```python 输入:root = [3,9,20,null,null,15,7] 输出:2 ``` - 示例 2: ```python 输入:root = [2,null,3,null,4,null,5,null,6] 输出:5 ``` ## 解题思路 ### 思路 1:深度优先搜索 深度优先搜索递归遍历左右子树,记录最小深度。 对于每一个非叶子节点,计算其左右子树的最小叶子节点深度,将较小的深度+1 即为当前节点的最小叶子节点深度。 ### 思路 1:代码 ```python class Solution: def minDepth(self, root: TreeNode) -> int: # 遍历到空节点,直接返回 0 if root == None: return 0 # 左右子树为空,说明为叶子节点 返回 1 if root.left == None and root.right == None: return 1 leftHeight = self.minDepth(root.left) rightHeight = self.minDepth(root.right) # 当前节点的左右子树的最小叶子节点深度 min_depth = 0xffffff if root.left: min_depth = min(leftHeight, min_depth) if root.right: min_depth = min(rightHeight, min_depth) # 当前节点的最小叶子节点深度 return min_depth + 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树中的节点数量。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/missing-ranges.md ================================================ # [0163. 缺失的区间](https://leetcode.cn/problems/missing-ranges/) - 标签:数组 - 难度:简单 ## 题目链接 - [0163. 缺失的区间 - 力扣](https://leetcode.cn/problems/missing-ranges/) ## 题目大意 **描述**: 给定一个闭区间 $[lower, upper]$ 和一个按从小到大排序的整数数组 $nums$ ,其中元素的范围在闭区间 $[lower, upper]$ 当中。 如果一个数字 $x$ 在 $[lower, upper]$ 区间内,并且 $x$ 不在 $nums$ 中,则认为 $x$ 缺失。 **要求**: 返回「准确涵盖所有缺失数字」的「最小排序」区间列表。也就是说,$nums$ 的任何元素都不在任何区间内,并且每个缺失的数字都在其中一个区间内。 **说明**: - $-10^{9} \le lower \le upper \le 10^{9}$。 - $0 \le nums.length \le 10^{3}$。 - $lower \le nums[i] \le upper$。 - nums 中的所有值 互不相同。 **示例**: - 示例 1: ```python 输入: nums = [0, 1, 3, 50, 75], lower = 0 , upper = 99 输出: [[2,2],[4,49],[51,74],[76,99]] 解释:返回的区间是: [2,2] [4,49] [51,74] [76,99] ``` - 示例 2: ```python 输入: nums = [-1], lower = -1, upper = -1 输出: [] 解释: 没有缺失的区间,因为没有缺失的数字。 ``` ## 解题思路 ### 思路 1:线性扫描 我们可以通过线性扫描数组来找到所有缺失的区间。具体思路如下: 1. **初始化边界**:从 $lower$ 开始,到 $upper$ 结束。 2. **遍历数组**:对于数组中的每个元素 $nums[i]$,检查它与前一个边界之间是否有缺失的区间。 3. **添加缺失区间**: - 如果 $prev + 1 < nums[i]$,说明 $[prev + 1, nums[i] - 1]$ 是一个缺失区间。 - 如果 $prev + 1 = nums[i]$,说明没有缺失。 4. **处理最后一个区间**:遍历完数组后,检查 $nums[n-1]$ 到 $upper$ 之间是否有缺失区间。 **关键点**: - 使用变量 $prev$ 记录前一个已处理的边界。 - 对于单个数字的区间,表示为 $[x, x]$。 - 需要处理数组为空的情况。 ### 思路 1:代码 ```python class Solution: def findMissingRanges(self, nums: List[int], lower: int, upper: int) -> List[List[int]]: result = [] prev = lower - 1 # 前一个边界,初始化为 lower - 1 # 遍历数组中的每个元素 for num in nums: # 如果当前数字与前一个边界之间有间隔,添加缺失区间 if prev + 1 < num: result.append([prev + 1, num - 1]) prev = num # 更新前一个边界 # 检查最后一个数字到 upper 之间是否有缺失区间 if prev < upper: result.append([prev + 1, upper]) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $nums$ 的长度。我们需要遍历数组一次。 - **空间复杂度**:$O(1)$,除了返回结果外,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0100-0199/number-of-1-bits.md ================================================ # [0191. 位1的个数](https://leetcode.cn/problems/number-of-1-bits/) - 标签:位运算、分治 - 难度:简单 ## 题目链接 - [0191. 位1的个数 - 力扣](https://leetcode.cn/problems/number-of-1-bits/) ## 题目大意 **描述**:给定一个无符号整数 $n$。 **要求**:统计其对应二进制表达式中 $1$ 的个数。 **说明**: - 输入必须是长度为 $32$ 的二进制串。 **示例**: - 示例 1: ```python 输入:n = 00000000000000000000000000001011 输出:3 解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。 ``` - 示例 2: ```python 输入:n = 00000000000000000000000010000000 输出:1 解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。 ``` ## 解题思路 ### 思路 1:循环按位计算 1. 对整数 $n$ 的每一位进行按位与运算,并统计结果。 ### 思路 1:代码 ```python class Solution: def hammingWeight(self, n: int) -> int: ans = 0 while n: ans += (n & 1) n = n >> 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k)$,其中 $k$ 是二进位的位数,$k = 32$。 - **空间复杂度**:$O(1)$。 ### 思路 2:改进位运算 利用 `n & (n - 1)`。这个运算刚好可以将 $n$ 的二进制中最低位的 $1$ 变为 $0$。 比如 $n = 6$ 时,$6 = 110_{(2)}$,$6 - 1 = 101_{(2)}$,`110 & 101 = 100`。 利用这个位运算,不断的将 $n$ 中最低位的 $1$ 变为 $0$,直到 $n$ 变为 $0$ 即可,其变换次数就是我们要求的结果。 ### 思路 2:代码 ```python class Solution: def hammingWeight(self, n: int) -> int: ans = 0 while n: n = n & (n - 1) ans += 1 return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/one-edit-distance.md ================================================ # [0161. 相隔为 1 的编辑距离](https://leetcode.cn/problems/one-edit-distance/) - 标签:双指针、字符串 - 难度:中等 ## 题目链接 - [0161. 相隔为 1 的编辑距离 - 力扣](https://leetcode.cn/problems/one-edit-distance/) ## 题目大意 **描述**: 给定两个字符串 $s$ 和 $t$。 **要求**: 如果它们的编辑距离为 $1$,则返回 $true$,否则返回 $false$。 字符串 $s$ 和字符串 $t$ 之间满足编辑距离等于 $1$ 有三种可能的情形: - 往 $s$ 中插入「恰好一个」字符得到 $t$。 - 从 $s$ 中删除「恰好一个」字符得到 $t$。 - 在 $s$ 中用「一个不同的字符」替换「恰好一个」字符得到 $t$。 **说明**: - $0 \le s.length, t.length \le 10^{4}$。 - $s$ 和 $t$ 由小写字母,大写字母和数字组成。 **示例**: - 示例 1: ```python 输入: s = "ab", t = "acb" 输出: true 解释: 可以将 'c' 插入字符串 s 来得到 t。 ``` - 示例 2: ```python 输入: s = "cab", t = "ad" 输出: false 解释: 无法通过 1 步操作使 s 变为 t。 ``` ## 解题思路 ### 思路 1:双指针 我们可以使用双指针的方法来判断两个字符串的编辑距离是否为 $1$。具体思路如下: 1. **长度检查**:如果两个字符串的长度差大于 $1$,则编辑距离不可能为 $1$,直接返回 $false$。 2. **双指针遍历**:使用双指针 $i$ 和 $j$ 分别遍历字符串 $s$ 和 $t$。 3. **字符匹配**:当字符匹配时,两个指针同时前进。 4. **处理不匹配**:当字符不匹配时,根据长度关系判断操作类型: - 如果 $len(s) = len(t)$,则进行替换操作,两个指针都前进。 - 如果 $len(s) < len(t)$,则进行插入操作,只有 $j$ 指针前进。 - 如果 $len(s) > len(t)$,则进行删除操作,只有 $i$ 指针前进。 5. **计数操作**:记录不匹配的次数,如果超过 $1$ 次,则返回 $false$。 **关键点**: - 使用双指针 $i$ 和 $j$ 同时遍历两个字符串。 - 根据字符串长度关系判断操作类型。 - 最多允许 $1$ 次不匹配操作。 ### 思路 1:代码 ```python class Solution: def isOneEditDistance(self, s: str, t: str) -> bool: # 获取字符串长度 len_s, len_t = len(s), len(t) # 如果长度差大于1,编辑距离不可能为1 if abs(len_s - len_t) > 1: return False # 如果两个字符串完全相同,编辑距离为0 if s == t: return False # 双指针遍历 i, j = 0, 0 edit_count = 0 # 记录编辑操作次数 while i < len_s and j < len_t: if s[i] == t[j]: # 字符匹配,两个指针都前进 i += 1 j += 1 else: # 字符不匹配,需要编辑操作 edit_count += 1 if edit_count > 1: return False if len_s == len_t: # 长度相等,进行替换操作 i += 1 j += 1 elif len_s < len_t: # s较短,进行插入操作 j += 1 else: # s较长,进行删除操作 i += 1 # 处理剩余字符 remaining_edits = (len_s - i) + (len_t - j) edit_count += remaining_edits return edit_count == 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(min(m, n))$,其中 $m$ 和 $n$ 分别是字符串 $s$ 和 $t$ 的长度。我们需要遍历较短的字符串。 - **空间复杂度**:$O(1)$。只使用了常数额外空间。 ================================================ FILE: docs/solutions/0100-0199/palindrome-partitioning-ii.md ================================================ # [0132. 分割回文串 II](https://leetcode.cn/problems/palindrome-partitioning-ii/) - 标签:字符串、动态规划 - 难度:困难 ## 题目链接 - [0132. 分割回文串 II - 力扣](https://leetcode.cn/problems/palindrome-partitioning-ii/) ## 题目大意 **描述**: 给定一个字符串 $s$。 **要求**: 将 $s$ 分割成一些子串,使每个子串都是回文串。 返回符合要求的「最少分割次数」。 **说明**: - $1 \le s.length \le 2000$。 - $s$ 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "aab" 输出:1 解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。 ``` - 示例 2: ```python 输入:s = "a" 输出:0 ``` ## 解题思路 ### 思路 1:动态规划 这是一个典型的动态规划问题。我们需要找到将字符串分割成回文子串的最少分割次数。 **核心思想**: 1. **预处理回文信息**:使用动态规划预处理所有子串是否为回文,用 $is\_palindrome[i][j]$ 表示字符串 $s[i:j+1]$ 是否为回文。 2. **状态定义**:用 $dp[i]$ 表示字符串 $s[0:i+1]$ 的最少分割次数。 3. **状态转移**:对于位置 $i$,如果 $s[j:i+1]$ 是回文,则 $dp[i] = \min(dp[i], dp[j-1] + 1)$。 **算法步骤**: 1. **预处理回文判断**: - 单个字符都是回文:$is\_palindrome[i][i] = true$ - 两个相邻字符:$is\_palindrome[i][i+1] = (s[i] == s[i+1])$ - 长度大于2的子串:$is\_palindrome[i][j] = (s[i] == s[j]) \land is\_palindrome[i+1][j-1]$ 2. **动态规划求解**: - 初始化:$dp[i] = i$(最坏情况,每个字符都单独分割) - 状态转移:$dp[i] = \min(dp[i], dp[j-1] + 1)$,其中 $j \le i$ 且 $s[j:i+1]$ 是回文 **关键点**: - 使用二维数组预处理回文判断,避免重复计算。 - 状态转移时只需要考虑以当前位置结尾的回文子串。 ### 思路 1:代码 ```python class Solution: def minCut(self, s: str) -> int: n = len(s) # 预处理:判断所有子串是否为回文 is_palindrome = [[False] * n for _ in range(n)] # 单个字符都是回文 for i in range(n): is_palindrome[i][i] = True # 两个相邻字符 for i in range(n - 1): is_palindrome[i][i + 1] = (s[i] == s[i + 1]) # 长度大于2的子串 for length in range(3, n + 1): for i in range(n - length + 1): j = i + length - 1 is_palindrome[i][j] = (s[i] == s[j]) and is_palindrome[i + 1][j - 1] # 动态规划求解最少分割次数 dp = [0] * n for i in range(n): # 最坏情况:每个字符都单独分割 dp[i] = i # 如果整个子串是回文,不需要分割 if is_palindrome[0][i]: dp[i] = 0 else: # 尝试所有可能的分割点 for j in range(1, i + 1): if is_palindrome[j][i]: dp[i] = min(dp[i], dp[j - 1] + 1) return dp[n - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是字符串长度。预处理回文判断需要 $O(n^2)$ 时间,动态规划求解也需要 $O(n^2)$ 时间。 - **空间复杂度**:$O(n^2)$,用于存储回文判断的二维数组和动态规划数组。 ================================================ FILE: docs/solutions/0100-0199/palindrome-partitioning.md ================================================ # [0131. 分割回文串](https://leetcode.cn/problems/palindrome-partitioning/) - 标签:字符串、动态规划、回溯 - 难度:中等 ## 题目链接 - [0131. 分割回文串 - 力扣](https://leetcode.cn/problems/palindrome-partitioning/) ## 题目大意 给定一个字符串 `s`,将 `s` 分割成一些子串,保证每个子串都是「回文串」。返回 `s` 所有可能的分割方案。 ## 解题思路 回溯算法,建立两个数组 res、path。res 用于存放所有满足题意的组合,path 用于存放当前满足题意的一个组合。 在回溯的时候判断当前子串是否为回文串,如果不是则跳过,如果是则继续向下一层遍历。 定义判断是否为回文串的方法和回溯方法,从 `start_index = 0` 的位置开始回溯。 - 如果 `start_index >= len(s)`,则将 path 中的元素加入到 res 数组中。 - 然后对 `[start_index, len(s) - 1]` 范围内的子串进行遍历取值。 - 如果字符串 `s` 在范围 `[start_index, i]` 所代表的子串是回文串,则将其加入 path 数组。 - 递归遍历 `[i + 1, len(s) - 1]` 范围上的子串。 - 然后将遍历的范围 `[start_index, i]` 所代表的子串进行回退。 - 最终返回 res 数组。 ## 代码 ```python class Solution: res = [] path = [] def backtrack(self, s: str, start_index: int): if start_index >= len(s): self.res.append(self.path[:]) return for i in range(start_index, len(s)): if self.ispalindrome(s, start_index, i): self.path.append(s[start_index: i+1]) self.backtrack(s, i + 1) self.path.pop() def ispalindrome(self, s: str, start: int, end: int): i, j = start, end while i < j: if s[i] != s[j]: return False i += 1 j -= 1 return True def partition(self, s: str) -> List[List[str]]: self.res.clear() self.path.clear() self.backtrack(s, 0) return self.res ``` ================================================ FILE: docs/solutions/0100-0199/pascals-triangle-ii.md ================================================ # [0119. 杨辉三角 II](https://leetcode.cn/problems/pascals-triangle-ii/) - 标签:数组、动态规划 - 难度:简单 ## 题目链接 - [0119. 杨辉三角 II - 力扣](https://leetcode.cn/problems/pascals-triangle-ii/) ## 题目大意 **描述**:给定一个非负整数 $rowIndex$。 **要求**:返回杨辉三角的第 $rowIndex$ 行。 **说明**: - $0 \le rowIndex \le 33$。 - 要求使用 $O(k)$ 的空间复杂度。 **示例**: - 示例 1: ```python 输入:rowIndex = 3 输出:[1,3,3,1] ``` ## 解题思路 ### 思路 1:动态规划 因为这道题是从 $0$ 行开始计算,则可以先将 $rowIndex$ 加 $1$,计算出总共的行数,即 $numRows = rowIndex + 1$。 ###### 1. 阶段划分 按照行数进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 为:杨辉三角第 $i$ 行、第 $j$ 列位置上的值。 ###### 3. 状态转移方程 根据观察,很容易得出状态转移方程为:$dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]$,此时 $i > 0$,$j > 0$。 ###### 4. 初始条件 - 每一行第一列都为 $1$,即 $dp[i][0] = 1$。 - 每一行最后一列都为 $1$,即 $dp[i][i] = 1$。 ###### 5. 最终结果 根据题意和状态定义,将 $dp$ 最后一行返回。 ### 思路 1:代码 ```python class Solution: def getRow(self, rowIndex: int) -> List[int]: # 本题从 0 行开始计算 numRows = rowIndex + 1 dp = [[0] * i for i in range(1, numRows + 1)] for i in range(numRows): dp[i][0] = 1 dp[i][i] = 1 for i in range(numRows): for j in range(i): if i != 0 and j != 0: dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] return dp[-1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。初始条件赋值的时间复杂度为 $O(n)$,两重循环遍历的时间复杂度为 $O(n^2)$,所以总的时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n^2)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n^2)$。 ### 思路 2:动态规划 + 滚动数组优化 因为 $dp[i][j]$ 仅依赖于上一行(第 $i - 1$ 行)的 $dp[i - 1][j - 1]$ 和 $dp[i - 1][j]$,所以我们没必要保存所有阶段的状态,只需要保存上一阶段的所有状态和当前阶段的所有状态就可以了,这样使用两个一维数组分别保存相邻两个阶段的所有状态就可以实现了。 其实我们还可以进一步进行优化,即我们只需要使用一个一维数组保存上一阶段的所有状态。 定义 $dp[j]$ 为杨辉三角第 $i$ 行第 $j$ 列位置上的值。则第 $i + 1$ 行、第 $j$ 列的值可以通过 $dp[j]$ + $dp[j - 1]$ 所得到。 这样我们就可以对这个一维数组保存的「上一阶段的所有状态值」进行逐一计算,从而获取「当前阶段的所有状态值」。 需要注意:本题在计算的时候需要从右向左依次遍历每个元素位置,这是因为如果从左向右遍历,如果当前元素 $dp[j]$ 已经更新为当前阶段第 $j$ 列位置的状态值之后,右侧 $dp[j + 1]$ 想要更新的话,需要的是上一阶段的状态值 $dp[j]$,而此时 $dp[j]$ 已经更新了,会破坏当前阶段的状态值。而是用从左向左的顺序,则不会出现该问题。 ### 思路 2:动态规划 + 滚动数组优化代码 ```python class Solution: def getRow(self, rowIndex: int) -> List[int]: # 本题从 0 行开始计算 numRows = rowIndex + 1 dp = [1 for _ in range(numRows)] for i in range(numRows): for j in range(i - 1, -1, -1): if i != 0 and j != 0: dp[j] = dp[j - 1] + dp[j] return dp ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2)$。两重循环遍历的时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n)$。不考虑最终返回值的空间占用,则总的空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/pascals-triangle.md ================================================ # [0118. 杨辉三角](https://leetcode.cn/problems/pascals-triangle/) - 标签:数组、动态规划 - 难度:简单 ## 题目链接 - [0118. 杨辉三角 - 力扣](https://leetcode.cn/problems/pascals-triangle/) ## 题目大意 **描述**:给定一个整数 $numRows$。 **要求**:生成前 $numRows$ 行的杨辉三角。 **说明**: - $1 \le numRows \le 30$。 **示例**: - 示例 1: ```python 输入:numRows = 5 输出:[[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] 即 [ [1], [1,1], [1,2,1], [1,3,3,1], [1,4,6,4,1] ] ``` - 示例 2: ```python 输入: numRows = 1 输出: [[1]] ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照行数进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 为:杨辉三角第 $i$ 行、第 $j$ 列位置上的值。 ###### 3. 状态转移方程 根据观察,很容易得出状态转移方程为:$dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]$,此时 $i > 0$,$j > 0$。 ###### 4. 初始条件 - 每一行第一列都为 $1$,即 $dp[i][0] = 1$。 - 每一行最后一列都为 $1$,即 $dp[i][i] = 1$。 ###### 5. 最终结果 根据题意和状态定义,我们将每行结果存入答案数组中,将其返回。 ### 思路 1:动态规划代码 ```python class Solution: def generate(self, numRows: int) -> List[List[int]]: dp = [[0] * i for i in range(1, numRows + 1)] for i in range(numRows): dp[i][0] = 1 dp[i][i] = 1 res = [] for i in range(numRows): for j in range(i): if i != 0 and j != 0: dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] res.append(dp[i]) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。初始条件赋值的时间复杂度为 $O(n)$,两重循环遍历的时间复杂度为 $O(n^2)$,所以总的时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n^2)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n^2)$。 ### 思路 2:动态规划 + 滚动数组优化 因为 $dp[i][j]$ 仅依赖于上一行(第 $i - 1$ 行)的 $dp[i - 1][j - 1]$ 和 $dp[i - 1][j]$,所以我们没必要保存所有阶段的状态,只需要保存上一阶段的所有状态和当前阶段的所有状态就可以了,这样使用两个一维数组分别保存相邻两个阶段的所有状态就可以实现了。 其实我们还可以进一步进行优化,即我们只需要使用一个一维数组保存上一阶段的所有状态。 定义 $dp[j]$ 为杨辉三角第 $i$ 行第 $j$ 列位置上的值。则第 $i + 1$ 行、第 $j$ 列的值可以通过 $dp[j]$ + $dp[j - 1]$ 所得到。 这样我们就可以对这个一维数组保存的「上一阶段的所有状态值」进行逐一计算,从而获取「当前阶段的所有状态值」。 需要注意:本题在计算的时候需要从右向左依次遍历每个元素位置,这是因为如果从左向右遍历,如果当前元素 $dp[j]$ 已经更新为当前阶段第 $j$ 列位置的状态值之后,右侧 $dp[j + 1]$ 想要更新的话,需要的是上一阶段的状态值 $dp[j]$,而此时 $dp[j]$ 已经更新了,会破坏当前阶段的状态值。而如果用从右向左的顺序,则不会出现该问题。 ### 思路 2:动态规划 + 滚动数组优化代码 ```python class Solution: def generate(self, numRows: int) -> List[List[int]]: dp = [1 for _ in range(numRows + 1)] res = [] for i in range(numRows): for j in range(i - 1, -1, -1): if i != 0 and j != 0: dp[j] = dp[j - 1] + dp[j] res.append(dp[:i + 1]) return res ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2)$。两重循环遍历的时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n)$。不考虑最终返回值的空间占用,则总的空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/path-sum-ii.md ================================================ # [0113. 路径总和 II](https://leetcode.cn/problems/path-sum-ii/) - 标签:树、深度优先搜索、回溯、二叉树 - 难度:中等 ## 题目链接 - [0113. 路径总和 II - 力扣](https://leetcode.cn/problems/path-sum-ii/) ## 题目大意 **描述**:给定一棵二叉树的根节点 `root` 和一个整数目标 `targetSum`。 **要求**:找出「所有从根节点到叶子节点路径总和」等于给定目标和 `targetSum` 的路径。 **说明**: - **叶子节点**:指没有子节点的节点。 - 树中节点总数在范围 $[0, 5000]$ 内。 - $-1000 \le Node.val \le 1000$。 - $-1000 \le targetSum \le 1000$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/01/18/pathsumii1.jpg) ```python 输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22 输出:[[5,4,11,2],[5,8,4,5]] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/01/18/pathsum2.jpg) ```python 输入:root = [1,2,3], targetSum = 5 输出:[] ``` ## 解题思路 ### 思路 1:回溯 在回溯的同时,记录下当前路径。同时维护 `targetSum`,每遍历到一个节点,就减去该节点值。如果遇到叶子节点,并且 `targetSum == 0` 时,将当前路径加入答案数组中。然后递归遍历左右子树,并回退当前节点,继续遍历。 具体步骤如下: 1. 使用列表 `res` 存储所有路径,使用列表 `path` 存储当前路径。 2. 如果根节点为空,则直接返回。 3. 将当前节点值添加到当前路径 `path` 中。 4. `targetSum` 减去当前节点值。 5. 如果遇到叶子节点,并且 `targetSum == 0` 时,将当前路径加入答案数组中。 6. 递归遍历左子树。 7. 递归遍历右子树。 8. 回退当前节点,继续递归遍历。 ### 思路 1:代码 ```python class Solution: def pathSum(self, root: TreeNode, targetSum: int) -> List[List[int]]: res = [] path = [] def dfs(root: TreeNode, targetSum: int): if not root: return path.append(root.val) targetSum -= root.val if not root.left and not root.right and targetSum == 0: res.append(path[:]) dfs(root.left, targetSum) dfs(root.right, targetSum) path.pop() dfs(root, targetSum) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/path-sum.md ================================================ # [0112. 路径总和](https://leetcode.cn/problems/path-sum/) - 标签:树、深度优先搜索 - 难度:简单 ## 题目链接 - [0112. 路径总和 - 力扣](https://leetcode.cn/problems/path-sum/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root` 和一个值 `targetSum`。 **要求**:判断该树中是否存在从根节点到叶子节点的路径,使得这条路径上所有节点值相加等于 `targetSum`。如果存在,返回 `True`;否则,返回 `False`。 **说明**: - 树中节点的数目在范围 $[0, 5000]$ 内。 - $-1000 \le Node.val \le 1000$。 - $-1000 \le targetSum \le 1000$。 **示例**: - 示例 1: ![img](https://assets.leetcode.com/uploads/2021/01/18/pathsum1.jpg) ```python 输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 输出:true 解释:等于目标和的根节点到叶节点路径如上图所示。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/01/18/pathsum2.jpg) ```python 输入:root = [1,2,3], targetSum = 5 输出:false 解释:树中存在两条根节点到叶子节点的路径: (1 --> 2): 和为 3 (1 --> 3): 和为 4 不存在 sum = 5 的根节点到叶子节点的路径。 ``` ## 解题思路 ### 思路 1:递归遍历 1. 定义一个递归函数,递归函数传入当前根节点 `root`,目标节点和 `targetSum`,以及新增变量 `currSum`(表示为从根节点到当前节点的路径上所有节点值之和)。 2. 递归遍历左右子树,同时更新维护 `currSum` 值。 3. 如果当前节点为叶子节点时,判断 `currSum` 是否与 `targetSum` 相等。 1. 如果 `currSum` 与 `targetSum` 相等,则返回 `True`。 2. 如果 `currSum` 不与 `targetSum` 相等,则返回 `False`。 4. 如果当前节点不为叶子节点,则继续递归遍历左右子树。 ### 思路 1:代码 ```python class Solution: def hasPathSum(self, root: TreeNode, targetSum: int) -> bool: return self.sum(root, targetSum, 0) def sum(self, root: TreeNode, targetSum: int, curSum:int) -> bool: if root == None: return False curSum += root.val if root.left == None and root.right == None: return curSum == targetSum else: return self.sum(root.left, targetSum, curSum) or self.sum(root.right, targetSum, curSum) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/populating-next-right-pointers-in-each-node-ii.md ================================================ # [0117. 填充每个节点的下一个右侧节点指针 II](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/) - 标签:树、深度优先搜索、广度优先搜索、链表、二叉树 - 难度:中等 ## 题目链接 - [0117. 填充每个节点的下一个右侧节点指针 II - 力扣](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/) ## 题目大意 **描述**:给定一个二叉树。二叉树结构如下: ```python struct Node { int val; Node *left; Node *right; Node *next; } ``` **要求**:填充每个 `next` 指针,使得这个指针指向下一个右侧节点。如果找不到下一个右侧节点,则将 `next` 置为 `None`。 **说明**: - 初始状态下,所有 next 指针都被设置为 `None`。 - 树中节点的数量在 $[0, 6000]$ 范围内。 - $-100 \le Node.val \le 100$。 - 进阶: - 只能使用常量级额外空间。 - 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2019/02/15/117_sample.png) ```python 输入:root = [1,2,3,4,5,null,7] 输出:[1,#,2,3,#,4,5,7,#] 解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化输出按层序遍历顺序(由 next 指针连接),'#' 表示每层的末尾。 ``` - 示例 2: ```python 输入:root = [] 输出:[] ``` ## 解题思路 ### 思路 1:层次遍历 在层次遍历的过程中,依次取出每一层的节点,并进行连接。然后再扩展下一层节点。 ### 思路 1:代码 ```python import collections class Solution: def connect(self, root: 'Node') -> 'Node': if not root: return root queue = collections.deque() queue.append(root) while queue: size = len(queue) for i in range(size): node = queue.popleft() if i < size - 1: node.next = queue[0] if node.left: queue.append(node.left) if node.right: queue.append(node.right) return root ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为树中的节点数量。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/populating-next-right-pointers-in-each-node.md ================================================ # [0116. 填充每个节点的下一个右侧节点指针](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/) - 标签:树、深度优先搜索、广度优先搜索、链表、二叉树 - 难度:中等 ## 题目链接 - [0116. 填充每个节点的下一个右侧节点指针 - 力扣](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/) ## 题目大意 **描述**:给定一个完美二叉树,所有叶子节点都在同一层,每个父节点都有两个子节点。完美二叉树结构如下: ```python struct Node { int val; Node *left; Node *right; Node *next; } ``` **要求**:填充每个 `next` 指针,使得这个指针指向下一个右侧节点。如果找不到下一个右侧节点,则将 `next` 置为 `None`。 **说明**: - 初始状态下,所有 next 指针都被设置为 `None`。 - 树中节点的数量在 $[0, 2^{12} - 1]$ 范围内。 - $-1000 \le node.val \le 1000$。 - 进阶: - 只能使用常量级额外空间。 - 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2019/02/14/116_sample.png) ```python 输入:root = [1,2,3,4,5,6,7] 输出:[1,#,2,3,#,4,5,6,7,#] 解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化的输出按层序遍历排列,同一层节点由 next 指针连接,'#' 标志着每一层的结束。 ``` - 示例 2: ```python 输入:root = [] 输出:[] ``` ## 解题思路 ### 思路 1:层次遍历 在层次遍历的过程中,依次取出每一层的节点,并进行连接。然后再扩展下一层节点。 ### 思路 1:代码 ```python import collections class Solution: def connect(self, root: 'Node') -> 'Node': if not root: return root queue = collections.deque() queue.append(root) while queue: size = len(queue) for i in range(size): node = queue.popleft() if i < size - 1: node.next = queue[0] if node.left: queue.append(node.left) if node.right: queue.append(node.right) return root ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为树中的节点数量。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/read-n-characters-given-read4-ii-call-multiple-times.md ================================================ # [0158. 用 Read4 读取 N 个字符 II - 多次调用](https://leetcode.cn/problems/read-n-characters-given-read4-ii-call-multiple-times/) - 标签:数组、交互、模拟 - 难度:困难 ## 题目链接 - [0158. 用 Read4 读取 N 个字符 II - 多次调用 - 力扣](https://leetcode.cn/problems/read-n-characters-given-read4-ii-call-multiple-times/) ## 题目大意 **描述**: 给定一个文件 `file`,并且该文件只能通过给定的 `read4` 方法来读取,请实现一个方法使其能够使 `read` 读取 $n$ 个字符。 注意:你的 `read` 方法可能会被调用多次。 `read4` 的定义: - `read4` API 从文件中读取 4 个连续的字符,然后将这些字符写入缓冲区数组 `buf4`。 - 返回值是读取的实际字符数。 - 请注意,`read4()` 有其自己的文件指针,类似于 C 中的 `FILE *fp`。 ```python 参数类型: char[] buf4 返回类型: int ``` 注意: `buf4[]` 是目标缓存区不是源缓存区,`read4` 的返回结果将会复制到 `buf4[]` 当中。 下列是一些使用 `read4` 的例子: ![](https://assets.leetcode.com/uploads/2020/07/01/157_example.png) ```python File file("abcde"); // 文件名为 "abcde",初始文件指针 (fp) 指向 'a' char[] buf4 = new char[4]; // 创建一个缓存区使其能容纳足够的字符 read4(buf4); // read4 返回 4。现在 buf4 = "abcd",fp 指向 'e' read4(buf4); // read4 返回 1。现在 buf4 = "e",fp 指向文件末尾 read4(buf4); // read4 返回 0。现在 buf4 = "",fp 指向文件末尾 ``` `read` 方法: - 通过使用 `read4` 方法,实现 `read` 方法。该方法可以从文件中读取 $n$ 个字符并将其存储到缓存数组 `buf` 中。您 **不能** 直接操作 `file`。 - 返回值为实际读取的字符。 `read` 的定义: ```python 参数类型: char[] buf, int n 返回类型: int ``` 注意: `buf[]` 是目标缓存区不是源缓存区,你需要将结果写入 `buf[]` 中。 **要求**:返回实际读取的字符数。 **说明**: - 你 **不能** 直接操作该文件,文件只能通过 `read4` 获取而 **不能** 通过 `read`。 - `read` 函数可以被调用 **多次**。 - 请记得 **重置** 在 Solution 中声明的类变量(静态变量),因为类变量会在多个测试用例中保持不变,影响判题准确。 - 你可以假定目标缓存数组 `buf` 保证有足够的空间存下 $n$ 个字符。 - 保证在一个给定测试用例中,`read` 函数使用的是同一个 `buf`。 - $1 \le file.length \le 500$。 - `file` 由英语字母和数字组成。 - $1 \le queries.length \le 10$。 - $1 \le queries[i] \le 500$。 **示例**: - 示例 1: ```python 输入:file = "abc", queries = [1,2,1] 输出:[1,2,0] 解释:测试用例表示以下场景: File file("abc"); Solution sol; sol.read(buf, 1); // 调用 read 方法后,buf 应该包含 "a"。我们从文件中总共读取了 1 个字符,所以返回 1。 sol.read(buf, 2); // 现在 buf 应该包含 "bc"。我们从文件中总共读取了 2 个字符,所以返回 2。 sol.read(buf, 1); // 我们已经到达文件的末尾,不能读取更多的字符。所以返回 0。 假设已经分配了 buf,并保证有足够的空间存储文件中的所有字符。 ``` - 示例 2: ```python 输入:file = "abc", queries = [4,1] 输出:[3,0] 解释:测试用例表示以下场景: File file("abc"); Solution sol; sol.read(buf, 4); // 调用 read 方法后,buf 应该包含 "abc"。我们从文件中总共读取了 3 个字符,所以返回 3。 sol.read(buf, 1); // 我们已经到达文件的末尾,不能读取更多的字符。所以返回 0。 ``` ## 解题思路 ### 思路 1:缓存 + 多次调用处理 #### 思路 1:算法描述 这道题与 0157 题的区别在于 `read` 方法会被 **多次调用**,因此需要维护一个内部缓冲区来保存上次调用 `read4` 时多读取的字符。 **核心问题**: - 假设第一次调用 `read(buf, 2)`,`read4` 读取了 4 个字符,但只需要 2 个,剩余 2 个字符需要保存。 - 第二次调用 `read(buf, 3)` 时,应该先使用上次剩余的 2 个字符,再调用 `read4` 读取新字符。 **算法步骤**: 1. 使用实例变量 `self.buffer` 保存上次多读取的字符。 2. 使用实例变量 `self.buffer_ptr` 和 `self.buffer_count` 记录缓冲区的读取位置和有效字符数。 3. 每次调用 `read` 时: - 先从内部缓冲区读取字符。 - 如果缓冲区字符不够,调用 `read4` 读取新字符。 - 将字符复制到目标缓冲区,直到读取 $n$ 个字符或文件结束。 #### 思路 1:代码 ```python # The read4 API is already defined for you. # def read4(buf4: List[str]) -> int: class Solution: def __init__(self): # 内部缓冲区,保存上次多读取的字符 self.buffer = [''] * 4 self.buffer_ptr = 0 # 缓冲区读取指针 self.buffer_count = 0 # 缓冲区有效字符数 def read(self, buf: List[str], n: int) -> int: total = 0 # 已读取的字符总数 while total < n: # 如果缓冲区为空,调用 read4 读取新字符 if self.buffer_ptr == self.buffer_count: self.buffer_count = read4(self.buffer) self.buffer_ptr = 0 # 如果读到文件末尾,结束 if self.buffer_count == 0: break # 从缓冲区复制字符到目标缓冲区 while total < n and self.buffer_ptr < self.buffer_count: buf[total] = self.buffer[self.buffer_ptr] total += 1 self.buffer_ptr += 1 return total ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是需要读取的字符数。每个字符最多被处理一次。 - **空间复杂度**:$O(1)$,只使用了固定大小的内部缓冲区。 ================================================ FILE: docs/solutions/0100-0199/read-n-characters-given-read4.md ================================================ # [0157. 用 Read4 读取 N 个字符](https://leetcode.cn/problems/read-n-characters-given-read4/) - 标签:数组、交互、模拟 - 难度:简单 ## 题目链接 - [0157. 用 Read4 读取 N 个字符 - 力扣](https://leetcode.cn/problems/read-n-characters-given-read4/) ## 题目大意 **描述**: 给定一个文件,并且该文件只能通过给定的 `read4` 方法来读取,请实现一个方法使其能够读取 $n$ 个字符。 `read4` 方法: - API `read4` 可以从文件中读取 4 个连续的字符,并且将它们写入缓存数组 `buf4` 中。 - 返回值为实际读取的字符个数。 - 注意 `read4()` 自身拥有文件指针,很类似于 C 语言中的 `FILE *fp`。 `read4` 的定义: ```python 参数类型: char[] buf4 返回类型: int ``` 注意: `buf4[]` 是目标缓存区不是源缓存区,`read4` 的返回结果将会复制到 `buf4[]` 当中。 下列是一些使用 `read4` 的例子: ```python File file("abcde"); // 文件名为 "abcde",初始文件指针 (fp) 指向 'a' char[] buf4 = new char[4]; // 创建一个缓存区使其能容纳足够的字符 read4(buf4); // read4 返回 4。现在 buf4 = "abcd",fp 指向 'e' read4(buf4); // read4 返回 1。现在 buf4 = "e",fp 指向文件末尾 read4(buf4); // read4 返回 0。现在 buf4 = "",fp 指向文件末尾 ``` `read` 方法: - 通过使用 `read4` 方法,实现 `read` 方法。该方法可以从文件中读取 $n$ 个字符并将其存储到缓存数组 `buf` 中。您 **不能** 直接操作文件。 - 返回值为实际读取的字符。 `read` 的定义: ```python 参数类型: `char[] buf`, `int n` 返回类型: `int` ``` 注意: `buf[]` 是目标缓存区不是源缓存区,你需要将结果写入 `buf[]` 中。 **要求**:返回实际读取的字符数。 **说明**: - 你 **不能** 直接操作该文件,文件只能通过 `read4` 获取而 **不能** 通过 `read`。 - `read` 函数只在每个测试用例调用一次。 - 你可以假定目标缓存数组 `buf` 保证有足够的空间存下 $n$ 个字符。 **示例**: - 示例 1: ```python 输入:file = "abc", n = 4 输出:3 解释:当执行你的 read 方法后,buf 需要包含 "abc"。文件一共 3 个字符,因此返回 3。注意 "abc" 是文件的内容,不是 buf 的内容,buf 是你需要写入结果的目标缓存区。 ``` - 示例 2: ```python 输入:file = "abcde", n = 5 输出:5 解释:当执行你的 read 方法后,buf 需要包含 "abcde"。文件共 5 个字符,因此返回 5。 ``` - 示例 3: ```python 输入:file = "abcdABCD1234", n = 12 输出:12 解释:当执行你的 read 方法后,buf 需要包含 "abcdABCD1234"。文件一共 12 个字符,因此返回 12。 ``` - 示例 4: ```python 输入:file = "leetcode", n = 5 输出:5 解释:当执行你的 read 方法后,buf 需要包含 "leetc"。文件中一共 5 个字符,因此返回 5。 ``` ## 解题思路 ### 思路 1:模拟 + 循环调用 read4 #### 思路 1:算法描述 这道题的核心是使用 `read4` API 来实现读取 $n$ 个字符的功能。 **算法步骤**: 1. 创建一个临时缓冲区 $buf4$,用于存储每次 `read4` 读取的 4 个字符。 2. 循环调用 `read4`,每次最多读取 4 个字符。 3. 将读取的字符复制到目标缓冲区 $buf$ 中,但不能超过 $n$ 个字符。 4. 如果 `read4` 返回的字符数少于 4,说明文件已读完,提前结束。 5. 返回实际读取的字符总数。 **关键点**: - 每次调用 `read4` 最多读取 4 个字符,但实际可能少于 4 个(文件末尾)。 - 需要控制总共读取的字符数不超过 $n$。 - 使用变量 $total$ 记录已读取的字符总数。 #### 思路 1:代码 ```python """ The read4 API is already defined for you. @param buf4, a list of characters @return an integer def read4(buf4): # Below is an example of how the read4 API can be called. file = File("abcdefghijk") # File is "abcdefghijk", initially file pointer (fp) points to 'a' buf4 = [' '] * 4 # Create buffer with enough space to store characters read4(buf4) # read4 returns 4. Now buf = ['a','b','c','d'], fp points to 'e' read4(buf4) # read4 returns 4. Now buf = ['e','f','g','h'], fp points to 'i' read4(buf4) # read4 returns 3. Now buf = ['i','j','k',...], fp points to end of file """ class Solution: def read(self, buf, n): """ :type buf: Destination buffer (List[str]) :type n: Number of characters to read (int) :rtype: The number of actual characters read (int) """ total = 0 # 已读取的字符总数 buf4 = [''] * 4 # 临时缓冲区 while total < n: # 调用 read4 读取最多 4 个字符 count = read4(buf4) # 如果读到文件末尾,提前结束 if count == 0: break # 计算本次应该复制的字符数(不能超过剩余需要读取的字符数) copy_count = min(count, n - total) # 将字符复制到目标缓冲区 for i in range(copy_count): buf[total] = buf4[i] total += 1 return total ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是需要读取的字符数。最多需要调用 $\lceil n / 4 \rceil$ 次 `read4`。 - **空间复杂度**:$O(1)$,只使用了固定大小的临时缓冲区 $buf4$。 ================================================ FILE: docs/solutions/0100-0199/reorder-list.md ================================================ # [0143. 重排链表](https://leetcode.cn/problems/reorder-list/) - 标签:栈、递归、链表、双指针 - 难度:中等 ## 题目链接 - [0143. 重排链表 - 力扣](https://leetcode.cn/problems/reorder-list/) ## 题目大意 **描述**:给定一个单链表 $L$ 的头节点 $head$,单链表 $L$ 表示为:$L_0 \rightarrow L_1 \rightarrow L_2 \rightarrow ... \rightarrow L_{n-1} \rightarrow L_n$。 **要求**:将单链表 $L$ 重新排列为:$L_0 \rightarrow L_n \rightarrow L_1 \rightarrow L_{n-1} \rightarrow L_2 \rightarrow L_{n-2} \rightarrow L_3 \rightarrow L_{n-3} \rightarrow ...$。 **说明**: - 需要将实际节点进行交换。 **示例**: - 示例 1: ![](https://pic.leetcode-cn.com/1626420311-PkUiGI-image.png) ```python 输入:head = [1,2,3,4] 输出:[1,4,2,3] ``` - 示例 2: ![](https://pic.leetcode-cn.com/1626420320-YUiulT-image.png) ```python 输入:head = [1,2,3,4,5] 输出:[1,5,2,4,3] ``` ## 解题思路 ### 思路 1:线性表 因为链表无法像数组那样直接进行随机访问。所以我们可以先将链表转为线性表,然后直接按照提要要求的排列顺序访问对应数据元素,重新建立链表。 ### 思路 1:代码 ```python class Solution: def reorderList(self, head: ListNode) -> None: """ Do not return anything, modify head in-place instead. """ if not head: return vec = [] node = head while node: vec.append(node) node = node.next left, right = 0, len(vec) - 1 while left < right: vec[left].next = vec[right] left += 1 if left == right: break vec[right].next = vec[left] right -= 1 vec[left].next = None ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/repeated-dna-sequences.md ================================================ # [0187. 重复的DNA序列](https://leetcode.cn/problems/repeated-dna-sequences/) - 标签:位运算、哈希表、字符串、滑动窗口、哈希函数、滚动哈希 - 难度:中等 ## 题目链接 - [0187. 重复的DNA序列 - 力扣](https://leetcode.cn/problems/repeated-dna-sequences/) ## 题目大意 **描述**: DNA 序列由一系列核苷酸组成,缩写为 `'A'`, `'C'`, `'G'` 和 `'T'`。 * 例如,`"ACGAATTCCG"` 是一个 DNA 序列。 在研究 DNA 时,识别 DNA 中的重复序列非常有用。 给定一个表示「DNA 序列」的字符串 $s$。 **要求**: 返回所有在 DNA 分子中出现不止一次的「长度为 $10$」的序列(子字符串)。 你可以按「任意顺序」返回答案。 **说明**: - $0 \le s.length \le 10^{5}$。 - `s[i]=='A'`、`'C'`、`'G'` or `'T'`。 **示例**: - 示例 1: ```python 输入:s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT" 输出:["AAAAACCCCC","CCCCCAAAAA"] ``` - 示例 2: ```python 输入:s = "AAAAAAAAAAAAA" 输出:["AAAAAAAAAA"] ``` ## 解题思路 ### 思路 1:哈希表 我们可以使用哈希表来统计所有长度为 $10$ 的子串出现次数,然后找出出现次数大于 $1$ 的子串。 **核心思想**: 1. **滑动窗口**:使用滑动窗口遍历字符串 $s$,每次取长度为 $10$ 的子串。 2. **哈希统计**:使用哈希表 $count\_dict$ 统计每个长度为 $10$ 的子串出现次数。 3. **筛选结果**:遍历哈希表,找出出现次数大于 $1$ 的子串。 **算法步骤**: 1. **边界处理**:如果字符串长度小于 $10$,直接返回空列表。 2. **滑动窗口遍历**:从位置 $0$ 开始,每次取长度为 $10$ 的子串 $s[i:i+10]$。 3. **哈希统计**:将每个子串作为键,出现次数作为值存入哈希表。 4. **结果筛选**:遍历哈希表,将出现次数大于 $1$ 的子串加入结果列表。 **关键点**: - 使用滑动窗口确保每个长度为 $10$ 的子串都被统计。 - 哈希表的时间复杂度为 $O(1)$,可以高效统计子串出现次数。 - 结果去重:由于使用哈希表,重复的子串只会被添加一次。 ### 思路 1:代码 ```python class Solution: def findRepeatedDnaSequences(self, s: str) -> List[str]: # 如果字符串长度小于10,直接返回空列表 if len(s) < 10: return [] # 使用哈希表统计每个长度为10的子串出现次数 count_dict = {} # 滑动窗口遍历字符串 for i in range(len(s) - 9): # 确保有足够的字符组成长度为 10 的子串 # 提取长度为10的子串 substring = s[i:i + 10] # 统计子串出现次数 count_dict[substring] = count_dict.get(substring, 0) + 1 # 筛选出现次数大于1的子串 result = [] for substring, count in count_dict.items(): if count > 1: result.append(substring) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。我们需要遍历字符串一次,每次操作的时间复杂度为 $O(1)$。 - **空间复杂度**:$O(n)$,最坏情况下需要存储 $n-9$ 个不同的长度为 $10$ 的子串。 ================================================ FILE: docs/solutions/0100-0199/reverse-bits.md ================================================ # [0190. 颠倒二进制位](https://leetcode.cn/problems/reverse-bits/) - 标签:位运算、分治 - 难度:简单 ## 题目链接 - [0190. 颠倒二进制位 - 力扣](https://leetcode.cn/problems/reverse-bits/) ## 题目大意 **描述**:给定一个 $32$ 位无符号整数 $n$。 **要求**:将 $n$ 所有二进位进行翻转,并返回翻转后的整数。 **说明**: - 输入是一个长度为 $32$ 的二进制字符串。 **示例**: - 示例 1: ```python 输入:n = 00000010100101000001111010011100 输出:964176192 (00111001011110000010100101000000) 解释:输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596, 因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。 ``` - 示例 2: ```python 输入:n = 11111111111111111111111111111101 输出:3221225471 (10111111111111111111111111111111) 解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293, 因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111。 ``` ## 解题思路 ### 思路 1:逐位翻转 1. 用一个变量 $res$ 存储翻转后的结果。 2. 将 $n$ 不断进行右移(即 `n >> 1`),从低位到高位进行枚举,此时 $n$ 的最低位就是我们枚举的二进位。 3. 同时 $res$ 不断左移(即 `res << 1`),并将当前枚举的二进位翻转后的结果(即 `n & 1`)拼接到 $res$ 的末尾(即 `(res << 1) | (n & 1)`)。 ### 思路 1:代码 ```python class Solution: def reverseBits(self, n: int) -> int: res = 0 for i in range(32): res = (res << 1) | (n & 1) n >>= 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/reverse-words-in-a-string-ii.md ================================================ # [0186. 反转字符串中的单词 II](https://leetcode.cn/problems/reverse-words-in-a-string-ii/) - 标签:双指针、字符串 - 难度:中等 ## 题目链接 - [0186. 反转字符串中的单词 II - 力扣](https://leetcode.cn/problems/reverse-words-in-a-string-ii/) ## 题目大意 **描述**: 给定一个字符数组 $s$。 **要求**: 反转其中「单词」的顺序。 **说明**: - 「单词」的定义为:单词是一个由非空格字符组成的序列。$s$ 中的单词将会由单个空格分隔。 - 必须设计并实现「原地」解法来解决此问题,即不分配额外的空间。 - $1 \le s.length \le 10^{5}$。 - $s[i]$ 可以是一个英文字母(大写或小写)、数字、或是空格 `' '`。 - $s$ 中至少存在一个单词。 - $s$ 不含前导或尾随空格。 - 题目数据保证:$s$ 中的每个单词都由单个空格分隔。 **示例**: - 示例 1: ```python 输入:s = ["t","h","e"," ","s","k","y"," ","i","s"," ","b","l","u","e"] 输出:["b","l","u","e"," ","i","s"," ","s","k","y"," ","t","h","e"] ``` - 示例 2: ```python 输入:s = ["a"] 输出:["a"] ``` ## 解题思路 ### 思路 1:双指针 + 两次反转 这是一个典型的原地反转问题。我们可以使用「两次反转」的策略来解决: 1. **整体反转**:先将整个字符数组 $s$ 反转。 2. **单词反转**:然后逐个反转每个单词。 **核心思想**: - 使用双指针 $left$ 和 $right$ 来标记单词的边界。 - 第一次反转:将整个数组 $s[0:n-1]$ 反转。 - 第二次反转:遍历数组,当遇到空格或到达末尾时,反转当前单词 $s[left:right]$。 **算法步骤**: 1. **整体反转**:将 $s[0:n-1]$ 反转,其中 $n$ 是数组长度。 2. **单词反转**: - 初始化 $left = 0$,遍历数组。 - 当 $s[i]$ 是空格或到达末尾时,反转 $s[left:i]$。 - 更新 $left = i + 1$ 继续下一个单词。 **关键点**: - 使用双指针 $left$ 和 $right$ 标记单词边界。 - 两次反转策略:先整体反转,再逐个单词反转。 - 原地操作,不分配额外空间。 ### 思路 1:代码 ```python class Solution: def reverseWords(self, s: List[str]) -> None: """ Do not return anything, modify s in-place instead. """ n = len(s) # 第一次反转:整体反转整个数组 self.reverse(s, 0, n - 1) # 第二次反转:逐个反转每个单词 left = 0 for i in range(n + 1): # 包含n是为了处理最后一个单词 # 当遇到空格或到达末尾时,反转当前单词 if i == n or s[i] == ' ': self.reverse(s, left, i - 1) left = i + 1 # 更新下一个单词的起始位置 def reverse(self, s: List[str], left: int, right: int) -> None: """ 反转数组s中从left到right的部分 """ while left < right: # 交换左右指针位置的字符 s[left], s[right] = s[right], s[left] left += 1 right -= 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组长度。需要遍历数组两次:一次整体反转,一次逐个单词反转。 - **空间复杂度**:$O(1)$,只使用了常数额外空间,满足原地操作的要求。 ================================================ FILE: docs/solutions/0100-0199/reverse-words-in-a-string.md ================================================ # [0151. 反转字符串中的单词](https://leetcode.cn/problems/reverse-words-in-a-string/) - 标签:双指针、字符串 - 难度:中等 ## 题目链接 - [0151. 反转字符串中的单词 - 力扣](https://leetcode.cn/problems/reverse-words-in-a-string/) ## 题目大意 **描述**:给定一个字符串 `s`。 **要求**:反转字符串中所有单词的顺序。 **说明**: - **单词**:由非空格字符组成的字符串。`s` 中使用至少一个空格将字符串中的单词分隔开。 - 输入字符串 `s`中可能会存在前导空格、尾随空格或者单词间的多个空格。 - 返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。 - $1 \le s.length \le 10^4$。 - `s` 包含英文大小写字母、数字和空格 `' '` - `s` 中至少存在一个单词。 **示例**: - 示例 1: ```python 输入:s = " hello world " 输出:"world hello" 解释:反转后的字符串中不能存在前导空格和尾随空格。 ``` - 示例 2: ```python 输入:s = "a good example" 输出:"example good a" 解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。 ``` ## 解题思路 ### 思路 1:调用库函数 直接调用 Python 的库函数,对字符串进行切片,翻转,然后拼合成字符串。 ### 思路 1:代码 ```python class Solution: def reverseWords(self, s: str) -> str: return " ".join(reversed(s.split())) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 `s` 的长度。 - **空间复杂度**:$O(1)$。 ### 思路 2:模拟 第二种思路根据 API 的思路写出模拟代码,具体步骤如下: - 使用数组 `words` 存放单词,使用字符串变量 `cur` 存放当前单词。 - 遍历字符串,对于当前字符 `ch`。 - 如果遇到空格,则: - 如果当前单词不为空,则将当前单词存入数组 `words` 中,并将当前单词置为空串 - 如果遇到字符,则: - 将其存入当前单词中,即 `cur += ch`。 - 如果遍历完,当前单词不为空,则将当前单词存入数组 `words` 中。 - 然后对数组 `words` 进行翻转操作,令 `words[i]`, `words[len(words) - 1 - i]` 交换元素。 - 最后将 `words` 中的单词连接起来,中间拼接上空格,将其作为答案返回。 ### 思路 2:代码 ```python class Solution: def reverseWords(self, s: str) -> str: words = [] cur = "" for ch in s: if ch == ' ': if cur: words.append(cur) cur = "" else: cur += ch if cur: words.append(cur) for i in range(len(words) // 2): words[i], words[len(words) - 1 - i] = words[len(words) - 1 - i], words[i] return " ".join(words) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 `s` 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/rotate-array.md ================================================ # [0189. 轮转数组](https://leetcode.cn/problems/rotate-array/) - 标签:数组、数学、双指针 - 难度:中等 ## 题目链接 - [0189. 轮转数组 - 力扣](https://leetcode.cn/problems/rotate-array/) ## 题目大意 **描述**:给定一个数组 $nums$,再给定一个数字 $k$。 **要求**:将数组中的元素向右移动 $k$ 个位置。 **说明**: - $1 \le nums.length \le 10^5$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 - $0 \le k \le 10^5$。 - 使用空间复杂度为 $O(1)$ 的原地算法解决这个问题。 **示例**: - 示例 1: ```python 输入: nums = [1,2,3,4,5,6,7], k = 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,4] ``` - 示例 2: ```py 输入:nums = [-1,-100,3,99], k = 2 输出:[3,99,-1,-100] 解释: 向右轮转 1 步: [99,-1,-100,3] 向右轮转 2 步: [3,99,-1,-100] ``` ## 解题思路 ### 思路 1: 数组翻转 可以用一个新数组,先保存原数组的后 $k$ 个元素,再保存原数组的前 $n - k$ 个元素。但题目要求不使用额外的数组空间,那么就需要在原数组上做操作。 我们可以先把整个数组翻转一下,这样后半段元素就到了前边,前半段元素就到了后边,只不过元素顺序是反着的。我们再从 $k$ 位置分隔开,将 $[0...k - 1]$ 区间上的元素和 $[k...n - 1]$ 区间上的元素再翻转一下,就得到了最终结果。 具体步骤: 1. 将数组 $[0, n - 1]$ 位置上的元素全部翻转。 2. 将数组 $[0, k - 1]$ 位置上的元素进行翻转。 3. 将数组 $[k, n - 1]$ 位置上的元素进行翻转。 ### 思路 1:代码 ```python class Solution: def rotate(self, nums: List[int], k: int) -> None: n = len(nums) k = k % n self.reverse(nums, 0, n-1) self.reverse(nums, 0, k-1) self.reverse(nums, k, n-1) def reverse(self, nums: List[int], left: int, right: int) -> None: while left < right : tmp = nums[left] nums[left] = nums[right] nums[right] = tmp left += 1 right -= 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。翻转的时间复杂度为 $O(n)$ 。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/same-tree.md ================================================ # [0100. 相同的树](https://leetcode.cn/problems/same-tree/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0100. 相同的树 - 力扣](https://leetcode.cn/problems/same-tree/) ## 题目大意 **描述**:给定两个二叉树的根节点 $p$ 和 $q$。 **要求**:判断这两棵树是否相同。 **说明**: - **两棵树相同的定义**:结构上相同;节点具有相同的值。 - 两棵树上的节点数目都在范围 $[0, 100]$ 内。 - $-10^4 \le Node.val \le 10^4$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/12/20/ex1.jpg) ```python 输入:p = [1,2,3], q = [1,2,3] 输出:True ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/12/20/ex2.jpg) ```python 输入:p = [1,2], q = [1,null,2] 输出:False ``` ## 解题思路 ### 思路 1:递归 1. 先判断两棵树的根节点是否相同。 2. 然后再递归地判断左右子树是否相同。 ### 思路 1:代码 ```python class Solution: def isSameTree(self, p: TreeNode, q: TreeNode) -> bool: if not p and not q: return True if not p or not q: return False if p.val != q.val: return False return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(min(m, n))$,其中 $m$、$n$ 分别为两棵树中的节点数量。 - **空间复杂度**:$O(min(m, n))$。 ================================================ FILE: docs/solutions/0100-0199/single-number-ii.md ================================================ # [0137. 只出现一次的数字 II](https://leetcode.cn/problems/single-number-ii/) - 标签:位运算、数组 - 难度:中等 ## 题目链接 - [0137. 只出现一次的数字 II - 力扣](https://leetcode.cn/problems/single-number-ii/) ## 题目大意 **描述**:给定一个整数数组 $nums$,除了某个元素仅出现一次外,其余每个元素恰好出现三次。 **要求**:找到并返回那个只出现了一次的元素。 **说明**: - $1 \le nums.length \le 3 * 10^4$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 - $nums$ 中,除某个元素仅出现一次外,其余每个元素都恰出现三次。 **示例**: - 示例 1: ```python 输入:nums = [2,2,3,2] 输出:3 ``` - 示例 2: ```python 输入:nums = [0,1,0,1,0,1,99] 输出:99 ``` ## 解题思路 ### 思路 1:哈希表 1. 利用哈希表统计出每个元素的出现次数。 2. 再遍历一次哈希表,找到仅出现一次的元素。 ### 思路 1:代码 ```python class Solution: def singleNumber(self, nums: List[int]) -> int: nums_dict = dict() for num in nums: if num in nums_dict: nums_dict[num] += 1 else: nums_dict[num] = 1 for key in nums_dict: value = nums_dict[key] if value == 1: return key return 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $nums$ 的元素个数。 - **空间复杂度**:$O(n)$。 ### 思路 2:位运算 将出现三次的元素换成二进制形式放在一起,其二进制对应位置上,出现 $1$ 的个数一定是 $3$ 的倍数(包括 $0$)。此时,如果在放进来只出现一次的元素,则某些二进制位置上出现 $1$ 的个数就不是 $3$ 的倍数了。 将这些二进制位置上出现 $1$ 的个数不是 $3$ 的倍数位置值置为 $1$,是 $3$ 的倍数则置为 $0$。这样对应下来的二进制就是答案所求。 注意:因为 Python 的整数没有位数限制,所以不能通过最高位确定正负。所以 Python 中负整数的补码会被当做正整数。所以在遍历到最后 $31$ 位时进行 $ans -= (1 << 31)$ 操作,目的是将负数的补码转换为「负号 + 原码」的形式。这样就可以正常识别二进制下的负数。参考:[Two's Complement Binary in Python? - Stack Overflow](https://stackoverflow.com/questions/12946116/twos-complement-binary-in-python/12946226) ### 思路 2:代码 ```python class Solution: def singleNumber(self, nums: List[int]) -> int: ans = 0 for i in range(32): count = 0 for j in range(len(nums)): count += (nums[j] >> i) & 1 if count % 3 != 0: if i == 31: ans -= (1 << 31) else: ans = ans | 1 << i return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \log m)$,其中 $n$ 是数组 $nums$ 的长度,$m$ 是数据范围,本题中 $m = 32$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/single-number.md ================================================ # [0136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) - 标签:位运算、数组 - 难度:简单 ## 题目链接 - [0136. 只出现一次的数字 - 力扣](https://leetcode.cn/problems/single-number/) ## 题目大意 **描述**:给定一个非空整数数组 `nums`,`nums` 中除了某个元素只出现一次以外,其余每个元素均出现两次。 **要求**:找出那个只出现了一次的元素。 **说明**: - 要求不能使用额外的存储空间。 **示例**: - 示例 1: ```python 输入: [2,2,1] 输出: 1 ``` - 示例 2: ```python 输入: [4,1,2,1,2] 输出: 4 ``` ## 解题思路 ### 思路 1:位运算 如果没有时间复杂度和空间复杂度的限制,可以使用哈希表 / 集合来存储每个元素出现的次数,如果哈希表中没有该数字,则将该数字加入集合,如果集合中有了该数字,则从集合中删除该数字,最终成对的数字都被删除了,只剩下单次出现的元素。 但是题目要求不使用额外的存储空间,就需要用到位运算中的异或运算。 > 异或运算 $\oplus$ 的三个性质: > > 1. 任何数和 $0$ 做异或运算,结果仍然是原来的数,即 $a \oplus 0 = a$。 > 2. 数和其自身做异或运算,结果是 $0$,即 $a \oplus a = 0$。 > 3. 异或运算满足交换率和结合律:$a \oplus b \oplus a = b \oplus a \oplus a = b \oplus (a \oplus a) = b \oplus 0 = b$。 根据异或运算的性质,对 $n$ 个数不断进行异或操作,最终可得到单次出现的元素。 ### 思路 1:代码 ```python class Solution: def singleNumber(self, nums: List[int]) -> int: if len(nums) == 1: return nums[0] ans = 0 for i in range(len(nums)): ans ^= nums[i] return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0100-0199/sort-list.md ================================================ # [0148. 排序链表](https://leetcode.cn/problems/sort-list/) - 标签:链表、双指针、分治、排序、归并排序 - 难度:中等 ## 题目链接 - [0148. 排序链表 - 力扣](https://leetcode.cn/problems/sort-list/) ## 题目大意 **描述**:给定链表的头节点 `head`。 **要求**:按照升序排列并返回排序后的链表。 **说明**: - 链表中节点的数目在范围 $[0, 5 * 10^4]$ 内。 - $-10^5 \le Node.val \le 10^5$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/09/14/sort_list_1.jpg) ```python 输入:head = [4,2,1,3] 输出:[1,2,3,4] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/09/14/sort_list_2.jpg) ```python 输入:head = [-1,5,3,4,0] 输出:[-1,0,3,4,5] ``` ## 解题思路 ### 思路 1:链表冒泡排序(超时) 1. 使用三个指针 `node_i`、`node_j` 和 `tail`。其中 `node_i` 用于控制外循环次数,循环次数为链节点个数(链表长度)。`node_j` 和 `tail` 用于控制内循环次数和循环结束位置。 2. 排序开始前,将 `node_i` 、`node_j` 置于头节点位置。`tail` 指向链表末尾,即 `None`。 3. 比较链表中相邻两个元素 `node_j.val` 与 `node_j.next.val` 的值大小,如果 `node_j.val > node_j.next.val`,则值相互交换。否则不发生交换。然后向右移动 `node_j` 指针,直到 `node_j.next == tail` 时停止。 4. 一次循环之后,将 `tail` 移动到 `node_j` 所在位置。相当于 `tail` 向左移动了一位。此时 `tail` 节点右侧为链表中最大的链节点。 5. 然后移动 `node_i` 节点,并将 `node_j` 置于头节点位置。然后重复第 3、4 步操作。 6. 直到 `node_i` 节点移动到链表末尾停止,排序结束。 7. 返回链表的头节点 `head`。 ### 思路 1:代码 ```python class Solution: def bubbleSort(self, head: ListNode): node_i = head tail = None # 外层循环次数为 链表节点个数 while node_i: node_j = head while node_j and node_j.next != tail: if node_j.val > node_j.next.val: # 交换两个节点的值 node_j.val, node_j.next.val = node_j.next.val, node_j.val node_j = node_j.next # 尾指针向前移动 1 位,此时尾指针右侧为排好序的链表 tail = node_j node_i = node_i.next return head def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.bubbleSort(head) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:链表选择排序(超时) 1. 使用两个指针 `node_i`、`node_j`。`node_i` 既可以用于控制外循环次数,又可以作为当前未排序链表的第一个链节点位置。 2. 使用 `min_node` 记录当前未排序链表中值最小的链节点。 3. 每一趟排序开始时,先令 `min_node = node_i`(即暂时假设链表中 `node_i` 节点为值最小的节点,经过比较后再确定最小值节点位置)。 4. 然后依次比较未排序链表中 `node_j.val` 与 `min_node.val` 的值大小。如果 `node_j.val < min_node.val`,则更新 `min_node` 为 `node_j`。 5. 这一趟排序结束时,未排序链表中最小值节点为 `min_node`,如果 `node_i != min_node`,则将 `node_i` 与 `min_node` 值进行交换。如果 `node_i == min_node`,则不用交换。 6. 排序结束后,继续向右移动 `node_i`,重复上述步骤,在剩余未排序链表中寻找最小的链节点,并与 `node_i` 进行比较和交换,直到 `node_i == None` 或者 `node_i.next == None` 时,停止排序。 7. 返回链表的头节点 `head`。 ### 思路 2:代码 ```python class Solution: def sectionSort(self, head: ListNode): node_i = head # node_i 为当前未排序链表的第一个链节点 while node_i and node_i.next: # min_node 为未排序链表中的值最小节点 min_node = node_i node_j = node_i.next while node_j: if node_j.val < min_node.val: min_node = node_j node_j = node_j.next # 交换值最小节点与未排序链表中第一个节点的值 if node_i != min_node: node_i.val, min_node.val = min_node.val, node_i.val node_i = node_i.next return head def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.sectionSort(head) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ### 思路 3:链表插入排序(超时) 1. 先使用哑节点 `dummy_head` 构造一个指向 `head` 的指针,使得可以从 `head` 开始遍历。 2. 维护 `sorted_list` 为链表的已排序部分的最后一个节点,初始时,`sorted_list = head`。 3. 维护 `prev` 为插入元素位置的前一个节点,维护 `cur` 为待插入元素。初始时,`prev = head`,`cur = head.next`。 4. 比较 `sorted_list` 和 `cur` 的节点值。 - 如果 `sorted_list.val <= cur.val`,说明 `cur` 应该插入到 `sorted_list` 之后,则将 `sorted_list` 后移一位。 - 如果 `sorted_list.val > cur.val`,说明 `cur` 应该插入到 `head` 与 `sorted_list` 之间。则使用 `prev` 从 `head` 开始遍历,直到找到插入 `cur` 的位置的前一个节点位置。然后将 `cur` 插入。 5. 令 `cur = sorted_list.next`,此时 `cur` 为下一个待插入元素。 6. 重复 4、5 步骤,直到 `cur` 遍历结束为空。返回 `dummy_head` 的下一个节点。 ### 思路 3:代码 ```python class Solution: def insertionSort(self, head: ListNode): if not head or not head.next: return head dummy_head = ListNode(-1) dummy_head.next = head sorted_list = head cur = head.next while cur: if sorted_list.val <= cur.val: # 将 cur 插入到 sorted_list 之后 sorted_list = sorted_list.next else: prev = dummy_head while prev.next.val <= cur.val: prev = prev.next # 将 cur 到链表中间 sorted_list.next = cur.next cur.next = prev.next prev.next = cur cur = sorted_list.next return dummy_head.next def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.insertionSort(head) ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ### 思路 4:链表归并排序(通过) 1. **分割环节**:找到链表中心链节点,从中心节点将链表断开,并递归进行分割。 1. 使用快慢指针 `fast = head.next`、`slow = head`,让 `fast` 每次移动 `2` 步,`slow` 移动 `1` 步,移动到链表末尾,从而找到链表中心链节点,即 `slow`。 2. 从中心位置将链表从中心位置分为左右两个链表 `left_head` 和 `right_head`,并从中心位置将其断开,即 `slow.next = None`。 3. 对左右两个链表分别进行递归分割,直到每个链表中只包含一个链节点。 2. **归并环节**:将递归后的链表进行两两归并,完成一遍后每个子链表长度加倍。重复进行归并操作,直到得到完整的链表。 1. 使用哑节点 `dummy_head` 构造一个头节点,并使用 `cur` 指向 `dummy_head` 用于遍历。 2. 比较两个链表头节点 `left` 和 `right` 的值大小。将较小的头节点加入到合并后的链表中。并向后移动该链表的头节点指针。 3. 然后重复上一步操作,直到两个链表中出现链表为空的情况。 4. 将剩余链表插入到合并中的链表中。 5. 将哑节点 `dummy_dead` 的下一个链节点 `dummy_head.next` 作为合并后的头节点返回。 ### 思路 4:代码 ```python class Solution: def merge(self, left, right): # 归并环节 dummy_head = ListNode(-1) cur = dummy_head while left and right: if left.val <= right.val: cur.next = left left = left.next else: cur.next = right right = right.next cur = cur.next if left: cur.next = left elif right: cur.next = right return dummy_head.next def mergeSort(self, head: ListNode): # 分割环节 if not head or not head.next: return head # 快慢指针找到中心链节点 slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next # 断开左右链节点 left_head, right_head = head, slow.next slow.next = None # 归并操作 return self.merge(self.mergeSort(left_head), self.mergeSort(right_head)) def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.mergeSort(head) ``` ### 思路 4:复杂度分析 - **时间复杂度**:$O(n \times \log_2n)$。 - **空间复杂度**:$O(1)$。 ### 思路 5:链表快速排序(超时) 1. 从链表中找到一个基准值 `pivot`,这里以头节点为基准值。 2. 然后通过快慢指针 `node_i`、`node_j` 在链表中移动,使得 `node_i` 之前的节点值都小于基准值,`node_i` 之后的节点值都大于基准值。从而把数组拆分为左右两个部分。 3. 再对左右两个部分分别重复第二步,直到各个部分只有一个节点,则排序结束。 > 注意: > > 虽然链表快速排序算法的平均时间复杂度为 $O(n \times \log_2n)$。但链表快速排序算法中基准值 `pivot` 的取值做不到数组快速排序算法中的随机选择。一旦给定序列是有序链表,时间复杂度就会退化到 $O(n^2)$。这也是这道题目使用链表快速排序容易超时的原因。 ### 思路 5:代码 ```python class Solution: def partition(self, left: ListNode, right: ListNode): # 左闭右开,区间没有元素或者只有一个元素,直接返回第一个节点 if left == right or left.next == right: return left # 选择头节点为基准节点 pivot = left.val # 使用 node_i, node_j 双指针,保证 node_i 之前的节点值都小于基准节点值,node_i 与 node_j 之间的节点值都大于等于基准节点值 node_i, node_j = left, left.next while node_j != right: # 发现一个小与基准值的元素 if node_j.val < pivot: # 因为 node_i 之前节点都小于基准值,所以先将 node_i 向右移动一位(此时 node_i 节点值大于等于基准节点值) node_i = node_i.next # 将小于基准值的元素 node_j 与当前 node_i 换位,换位后可以保证 node_i 之前的节点都小于基准节点值 node_i.val, node_j.val = node_j.val, node_i.val node_j = node_j.next # 将基准节点放到正确位置上 node_i.val, left.val = left.val, node_i.val return node_i def quickSort(self, left: ListNode, right: ListNode): if left == right or left.next == right: return left pi = self.partition(left, right) self.quickSort(left, pi) self.quickSort(pi.next, right) return left def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: if not head or not head.next: return head return self.quickSort(head, None) ``` ### 思路 5:复杂度分析 - **时间复杂度**:$O(n \times \log_2n)$。 - **空间复杂度**:$O(1)$。 ### 思路 6:链表计数排序(通过) 1. 使用 `cur` 指针遍历一遍链表。找出链表中最大值 `list_max` 和最小值 `list_min`。 2. 使用数组 `counts` 存储节点出现次数。 3. 再次使用 `cur` 指针遍历一遍链表。将链表中每个值为 `cur.val` 的节点出现次数,存入数组对应第 `cur.val - list_min` 项中。 4. 反向填充目标链表: 1. 建立一个哑节点 `dummy_head`,作为链表的头节点。使用 `cur` 指针指向 `dummy_head`。 2. 从小到大遍历一遍数组 `counts`。对于每个 `counts[i] != 0` 的元素建立一个链节点,值为 `i + list_min`,将其插入到 `cur.next` 上。并向右移动 `cur`。同时 `counts[i] -= 1`。直到 `counts[i] == 0` 后继续向后遍历数组 `counts`。 5. 将哑节点 `dummy_dead` 的下一个链节点 `dummy_head.next` 作为新链表的头节点返回。 ### 思路 6:代码 ```python class Solution: def countingSort(self, head: ListNode): if not head: return head # 找出链表中最大值 list_max 和最小值 list_min list_min, list_max = float('inf'), float('-inf') cur = head while cur: if cur.val < list_min: list_min = cur.val if cur.val > list_max: list_max = cur.val cur = cur.next size = list_max - list_min + 1 counts = [0 for _ in range(size)] cur = head while cur: counts[cur.val - list_min] += 1 cur = cur.next dummy_head = ListNode(-1) cur = dummy_head for i in range(size): while counts[i]: cur.next = ListNode(i + list_min) counts[i] -= 1 cur = cur.next return dummy_head.next def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.countingSort(head) ``` ### 思路 6:复杂度分析 - **时间复杂度**:$O(n + k)$,其中 $k$ 代表待排序链表中所有元素的值域。 - **空间复杂度**:$O(k)$。 ### 思路 7:链表桶排序(通过) 1. 使用 `cur` 指针遍历一遍链表。找出链表中最大值 `list_max` 和最小值 `list_min`。 2. 通过 `(最大值 - 最小值) / 每个桶的大小` 计算出桶的个数,即 `bucket_count = (list_max - list_min) // bucket_size + 1` 个桶。 3. 定义数组 `buckets` 为桶,桶的个数为 `bucket_count` 个。 4. 使用 `cur` 指针再次遍历一遍链表,将每个元素装入对应的桶中。 5. 对每个桶内的元素单独排序,可以使用链表插入排序(超时)、链表归并排序(通过)、链表快速排序(超时)等算法。 6. 最后按照顺序将桶内的元素拼成新的链表,并返回。 ### 思路 7:代码 ```python class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next class Solution: # 将链表节点值 val 添加到对应桶 buckets[index] 中 def insertion(self, buckets, index, val): if not buckets[index]: buckets[index] = ListNode(val) return node = ListNode(val) node.next = buckets[index] buckets[index] = node # 归并环节 def merge(self, left, right): dummy_head = ListNode(-1) cur = dummy_head while left and right: if left.val <= right.val: cur.next = left left = left.next else: cur.next = right right = right.next cur = cur.next if left: cur.next = left elif right: cur.next = right return dummy_head.next def mergeSort(self, head: ListNode): # 分割环节 if not head or not head.next: return head # 快慢指针找到中心链节点 slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next # 断开左右链节点 left_head, right_head = head, slow.next slow.next = None # 归并操作 return self.merge(self.mergeSort(left_head), self.mergeSort(right_head)) def bucketSort(self, head: ListNode, bucket_size=5): if not head: return head # 找出链表中最大值 list_max 和最小值 list_min list_min, list_max = float('inf'), float('-inf') cur = head while cur: if cur.val < list_min: list_min = cur.val if cur.val > list_max: list_max = cur.val cur = cur.next # 计算桶的个数,并定义桶 bucket_count = (list_max - list_min) // bucket_size + 1 buckets = [[] for _ in range(bucket_count)] # 将链表节点值依次添加到对应桶中 cur = head while cur: index = (cur.val - list_min) // bucket_size self.insertion(buckets, index, cur.val) cur = cur.next dummy_head = ListNode(-1) cur = dummy_head # 将元素依次出桶,并拼接成有序链表 for bucket_head in buckets: bucket_cur = self.mergeSort(bucket_head) while bucket_cur: cur.next = bucket_cur cur = cur.next bucket_cur = bucket_cur.next return dummy_head.next def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.bucketSort(head) ``` ### 思路 7:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n + m)$。$m$ 为桶的个数。 ### 思路 8:链表基数排序(解答错误,普通链表基数排序只适合非负数) 1. 使用 `cur` 指针遍历链表,获取节点值位数最长的位数 `size`。 2. 从个位到高位遍历位数。因为 `0` ~ `9` 共有 `10` 位数字,所以建立 `10` 个桶。 3. 以每个节点对应位数上的数字为索引,将节点值放入到对应桶中。 4. 建立一个哑节点 `dummy_head`,作为链表的头节点。使用 `cur` 指针指向 `dummy_head`。 5. 将桶中元素依次取出,并根据元素值建立链表节点,并插入到新的链表后面。从而生成新的链表。 6. 之后依次以十位,百位,…,直到最大值元素的最高位处值为索引,放入到对应桶中,并生成新的链表,最终完成排序。 7. 将哑节点 `dummy_dead` 的下一个链节点 `dummy_head.next` 作为新链表的头节点返回。 ### 思路 8:代码 ```python class Solution: def radixSort(self, head: ListNode): # 计算位数最长的位数 size = 0 cur = head while cur: val_len = len(str(cur.val)) if val_len > size: size = val_len cur = cur.next # 从个位到高位遍历位数 for i in range(size): buckets = [[] for _ in range(10)] cur = head while cur: # 以每个节点对应位数上的数字为索引,将节点值放入到对应桶中 buckets[cur.val // (10 ** i) % 10].append(cur.val) cur = cur.next # 生成新的链表 dummy_head = ListNode(-1) cur = dummy_head for bucket in buckets: for num in bucket: cur.next = ListNode(num) cur = cur.next head = dummy_head.next return head def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.radixSort(head) ``` ### 思路 8:复杂度分析 - **时间复杂度**:$O(n \times k)$。其中 $n$ 是待排序元素的个数,$k$ 是数字位数。$k$ 的大小取决于数字位的选择(十进制位、二进制位)和待排序元素所属数据类型全集的大小。 - **空间复杂度**:$O(n + k)$。 ## 参考资料 - 【文章】[单链表的冒泡排序_zhao_miao的博客 - CSDN博客](https://blog.csdn.net/zhao_miao/article/details/81708454) - 【文章】[链表排序总结(全)(C++)- 阿祭儿 - CSDN博客](https://blog.csdn.net/qq_32523711/article/details/107402873) - 【题解】[快排、冒泡、选择排序实现列表排序 - 排序链表 - 力扣](https://leetcode.cn/problems/sort-list/solution/kuai-pai-mou-pao-xuan-ze-pai-xu-shi-xian-ula7/) - 【题解】[归并排序+快速排序 - 排序链表 - 力扣](https://leetcode.cn/problems/sort-list/solution/gui-bing-pai-xu-kuai-su-pai-xu-by-datacruiser/) - 【题解】[排序链表(递归+迭代)详解 - 排序链表 - 力扣](https://leetcode.cn/problems/sort-list/solution/pai-xu-lian-biao-di-gui-die-dai-xiang-jie-by-cherr/) - 【题解】[Sort List (归并排序链表) - 排序链表 - 力扣](https://leetcode.cn/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/) ================================================ FILE: docs/solutions/0100-0199/sum-root-to-leaf-numbers.md ================================================ # [0129. 求根节点到叶节点数字之和](https://leetcode.cn/problems/sum-root-to-leaf-numbers/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0129. 求根节点到叶节点数字之和 - 力扣](https://leetcode.cn/problems/sum-root-to-leaf-numbers/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`,树中每个节点都存放有一个 `0` 到 `9` 之间的数字。每条从根节点到叶节点的路径都代表一个数字。例如,从根节点到叶节点的路径是 `1` -> `2` -> `3`,表示数字 `123`。 **要求**:计算从根节点到叶节点生成的所有数字的和。 **说明**: - **叶节点**:指没有子节点的节点。 - 树中节点的数目在范围 $[1, 1000]$ 内。 - $0 \le Node.val \le 9$。 - 树的深度不超过 $10$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/19/num1tree.jpg) ```python 输入:root = [1,2,3] 输出:25 解释: 从根到叶子节点路径 1->2 代表数字 12 从根到叶子节点路径 1->3 代表数字 13 因此,数字总和 = 12 + 13 = 25 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/02/19/num2tree.jpg) ```python 输入:root = [4,9,0,5,1] 输出:1026 解释: 从根到叶子节点路径 4->9->5 代表数字 495 从根到叶子节点路径 4->9->1 代表数字 491 从根到叶子节点路径 4->0 代表数字 40 因此,数字总和 = 495 + 491 + 40 = 1026 ``` ## 解题思路 ### 思路 1:深度优先搜索 1. 记录下路径上所有节点构成的数字,使用变量 `pre_total` 保存下当前路径上构成的数字。 2. 如果遇到叶节点,则直接返回当前数字。 3. 如果没有遇到叶节点,则递归遍历左右子树,并累加对应结果。 ### 思路 1:代码 ```python class Solution: def dfs(self, root, pre_total): if not root: return 0 total = pre_total * 10 + root.val if not root.left and not root.right: return total return self.dfs(root.left, total) + self.dfs(root.right, total) def sumNumbers(self, root: Optional[TreeNode]) -> int: return self.dfs(root, 0) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/surrounded-regions.md ================================================ # [0130. 被围绕的区域](https://leetcode.cn/problems/surrounded-regions/) - 标签:深度优先搜索、广度优先搜索、并查集、数组、矩阵 - 难度:中等 ## 题目链接 - [0130. 被围绕的区域 - 力扣](https://leetcode.cn/problems/surrounded-regions/) ## 题目大意 **描述**:给定一个 `m * n` 的矩阵 `board`,由若干字符 `X` 和 `O` 构成。 **要求**:找到所有被 `X` 围绕的区域,并将这些区域里所有的 `O` 用 `X` 填充。 **说明**: - $m == board.length$。 - $n == board[i].length$。 - $1 <= m, n <= 200$。 - $board[i][j]$ 为 `'X'` 或 `'O'`。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/19/xogrid.jpg) ```python 输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]] 输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]] 解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。 ``` - 示例 2: ```python 输入:board = [["X"]] 输出:[["X"]] ``` ## 解题思路 ### 思路 1:深度优先搜索 根据题意,任何边界上的 `O` 都不会被填充为`X`。而被填充 `X` 的 `O` 一定在内部不在边界上。 所以我们可以用深度优先搜索先搜索边界上的 `O` 以及与边界相连的 `O`,将其先标记为 `#`。 最后遍历一遍 `board`,将所有 `#` 变换为 `O`,将所有 `O` 变换为 `X`。 ### 思路 1:代码 ```python class Solution: def solve(self, board: List[List[str]]) -> None: """ Do not return anything, modify board in-place instead. """ if not board: return rows, cols = len(board), len(board[0]) def dfs(x, y): if not 0 <= x < rows or not 0 <= y < cols or board[x][y] != 'O': return board[x][y] = '#' dfs(x + 1, y) dfs(x - 1, y) dfs(x, y + 1) dfs(x, y - 1) for i in range(rows): dfs(i, 0) dfs(i, cols - 1) for j in range(cols - 1): dfs(0, j) dfs(rows - 1, j) for i in range(rows): for j in range(cols): if board[i][j] == '#': board[i][j] = 'O' elif board[i][j] == 'O': board[i][j] = 'X' ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $m$ 和 $n$ 分别为行数和列数。 - **空间复杂度**:$O(n \times m)$。 ================================================ FILE: docs/solutions/0100-0199/symmetric-tree.md ================================================ # [0101. 对称二叉树](https://leetcode.cn/problems/symmetric-tree/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0101. 对称二叉树 - 力扣](https://leetcode.cn/problems/symmetric-tree/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:判断该二叉树是否是左右对称的。 **说明**: - 树中节点数目在范围 $[1, 1000]$ 内。 - $-100 \le Node.val \le 100$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/19/symtree1.jpg) ```python 输入:root = [1,2,2,3,4,4,3] 输出:true ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/02/19/symtree2.jpg) ```python 输入:root = [1,2,2,null,3,null,3] 输出:false ``` ## 解题思路 ### 思路 1:递归遍历 如果一棵二叉树是对称的,那么其左子树和右子树的外侧节点的节点值应当是相等的,并且其左子树和右子树的内侧节点的节点值也应当是相等的。 那么我们可以通过递归方式,检查其左子树与右子树外侧节点和内测节点是否相等。即递归检查左子树的左子节点值与右子树的右子节点值是否相等(外侧节点值是否相等),递归检查左子树的右子节点值与右子树的左子节点值是否相等(内测节点值是否相等)。 具体步骤如下: 1. 如果当前根节点为 `None`,则直接返回 `True`。 2. 如果当前根节点不为 `None`,则调用 `check(left, right)` 方法递归检查其左右子树是否对称。 1. 如果左子树节点为 `None`,并且右子树节点也为 `None`,则直接返回 `True`。 2. 如果左子树节点为 `None`,并且右子树节点不为 `None`,则直接返回 `False`。 3. 如果左子树节点不为 `None`,并且右子树节点为 `None`,则直接返回 `False`。 4. 如果左子树节点值不等于右子树节点值,则直接返回 `False`。 5. 如果左子树节点不为 `None`,并且右子树节点不为 `None`,并且左子树节点值等于右子树节点值,则: 1. 递归检测左右子树的外侧节点是否相等。 2. 递归检测左右子树的内测节点是否相等。 3. 如果左右子树的外侧节点、内测节点值相等,则返回 `True`。 ### 思路 1:代码 ```python class Solution: def isSymmetric(self, root: TreeNode) -> bool: if root == None: return True return self.check(root.left, root.right) def check(self, left: TreeNode, right: TreeNode): if left == None and right == None: return True elif left == None and right != None: return False elif left != None and right == None: return False elif left.val != right.val: return False return self.check(left.left, right.right) and self.check(left.right, right.left) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0100-0199/triangle.md ================================================ # [0120. 三角形最小路径和](https://leetcode.cn/problems/triangle/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [0120. 三角形最小路径和 - 力扣](https://leetcode.cn/problems/triangle/) ## 题目大意 **描述**:给定一个代表三角形的二维数组 $triangle$,$triangle$ 共有 $n$ 行,其中第 $i$ 行(从 $0$ 开始编号)包含了 $i + 1$ 个数。 我们每一步只能从当前位置移动到下一行中相邻的节点上。也就是说,如果正位于第 $i$ 行第 $j$ 列的节点,那么下一步可以移动到第 $i + 1$ 行第 $j$ 列的位置上,或者第 $i + 1$ 行,第 $j + 1$ 列的位置上。 **要求**:找出自顶向下的最小路径和。 **说明**: - $1 \le triangle.length \le 200$。 - $triangle[0].length == 1$。 - $triangle[i].length == triangle[i - 1].length + 1$。 - $-10^4 \le triangle[i][j] \le 10^4$。 **示例**: - 示例 1: ```python 输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]] 输出:11 解释:如下面简图所示: 2 3 4 6 5 7 4 1 8 3 自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。 ``` - 示例 2: ```python 输入:triangle = [[-10]] 输出:-10 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照行数进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:从顶部走到第 $i$ 行(从 $0$ 开始编号)、第 $j$ 列的位置时的最小路径和。 ###### 3. 状态转移方程 由于每一步只能从当前位置移动到下一行中相邻的节点上,想要移动到第 $i$ 行、第 $j$ 列的位置,那么上一步只能在第 $i - 1$ 行、第 $j - 1$ 列的位置上,或者在第 $i - 1$ 行、第 $j$ 列的位置上。则状态转移方程为: $dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j]$。其中 $triangle[i][j]$ 表示第 $i$ 行、第 $j$ 列位置上的元素值。 ###### 4. 初始条件 在第 $0$ 行、第 $j$ 列时,最小路径和为 $triangle[0][0]$,即 $dp[0][0] = triangle[0][0]$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:从顶部走到第 $i$ 行(从 $0$ 开始编号)、第 $j$ 列的位置时的最小路径和。为了计算出最小路径和,则需要再遍历一遍 $dp[size - 1]$ 行的每一列,求出最小值即为最终结果。 ### 思路 1:动态规划代码 ```python class Solution: def minimumTotal(self, triangle: List[List[int]]) -> int: size = len(triangle) dp = [[0 for _ in range(size)] for _ in range(size)] dp[0][0] = triangle[0][0] for i in range(1, size): dp[i][0] = dp[i - 1][0] + triangle[i][0] for j in range(1, i): dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j] dp[i][i] = dp[i - 1][i - 1] + triangle[i][i] return min(dp[size - 1]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。两重循环遍历的时间复杂度是 $O(n^2)$,最后求最小值的时间复杂度是 $O(n)$,所以总体时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n^2)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n^2)$。 ================================================ FILE: docs/solutions/0100-0199/two-sum-ii-input-array-is-sorted.md ================================================ # [0167. 两数之和 II - 输入有序数组](https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/) - 标签:数组、双指针、二分查找 - 难度:中等 ## 题目链接 - [0167. 两数之和 II - 输入有序数组 - 力扣](https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/) ## 题目大意 **描述**:给定一个下标从 $1$ 开始计数、升序排列的整数数组:$numbers$ 和一个目标值 $target$。 **要求**:从数组中找出满足相加之和等于 $target$ 的两个数,并返回两个数在数组中下的标值。 **说明**: - $2 \le numbers.length \le 3 \times 10^4$。 - $-1000 \le numbers[i] \le 1000$。 - $numbers$ 按非递减顺序排列。 - $-1000 \le target \le 1000$。 - 仅存在一个有效答案。 **示例**: - 示例 1: ```python 输入:numbers = [2,7,11,15], target = 9 输出:[1,2] 解释:2 与 7 之和等于目标数 9。因此 index1 = 1, index2 = 2。返回 [1, 2]。 ``` - 示例 2: ```python 输入:numbers = [2,3,4], target = 6 输出:[1,3] 解释:2 与 4 之和等于目标数 6。因此 index1 = 1, index2 = 3。返回 [1, 3]。 ``` ## 解题思路 这道题如果暴力遍历数组,从中找到相加之和等于 $target$ 的两个数,时间复杂度为 $O(n^2)$,可以尝试一下。 ```python class Solution: def twoSum(self, numbers: List[int], target: int) -> List[int]: size = len(numbers) for i in range(size): for j in range(i + 1, size): if numbers[i] + numbers[j] == target: return [i + 1, j + 1] return [-1, -1] ``` 结果不出意外的超时了。所以我们要想办法降低时间复杂度。 ### 思路 1:二分查找 因为数组是有序的,可以考虑使用二分查找来减少时间复杂度。具体做法如下: 1. 使用一重循环遍历数组,先固定第一个数,即 $numsbers[i]$。 2. 然后使用二分查找的方法寻找符合要求的第二个数。 3. 使用两个指针 $left$,$right$。$left$ 指向数组第一个数的下一个数,$right$ 指向数组值最大元素位置。 4. 判断第一个数 $numsbers[i]$ 和两个指针中间元素 $numbers[mid]$ 的和与目标值的关系。 1. 如果 $numbers[mid] + numbers[i] < target$,排除掉不可能区间 $[left, mid]$,在 $[mid + 1, right]$ 中继续搜索。 2. 如果 $numbers[mid] + numbers[i] \ge target$,则第二个数可能在 $[left, mid]$ 中,则在 $[left, mid]$ 中继续搜索。 5. 直到 $left$ 和 $right$ 移动到相同位置停止检测。如果 $numbers[left] + numbers[i] == target$,则返回两个元素位置 $[left + 1, i + 1]$(下标从 $1$ 开始计数)。 6. 如果最终仍没找到,则返回 $[-1, -1]$。 ### 思路 1:代码 ```python class Solution: def twoSum(self, numbers: List[int], target: int) -> List[int]: for i in range(len(numbers)): left, right = i + 1, len(numbers) - 1 while left < right: mid = left + (right - left) // 2 if numbers[mid] + numbers[i] < target: left = mid + 1 else: right = mid if numbers[left] + numbers[i] == target: return [i + 1, left + 1] return [-1, -1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:对撞指针 可以考虑使用对撞指针来减少时间复杂度。具体做法如下: 1. 使用两个指针 $left$,$right$。$left$ 指向数组第一个值最小的元素位置,$right$ 指向数组值最大元素位置。 2. 判断两个位置上的元素的和与目标值的关系。 1. 如果元素和等于目标值,则返回两个元素位置。 2. 如果元素和大于目标值,则让 $right$ 左移,继续检测。 3. 如果元素和小于目标值,则让 $left$ 右移,继续检测。 3. 直到 $left$ 和 $right$ 移动到相同位置停止检测。 4. 如果最终仍没找到,则返回 $[-1, -1]$。 ### 思路 2:代码 ```python class Solution: def twoSum(self, numbers: List[int], target: int) -> List[int]: left = 0 right = len(numbers) - 1 while left < right: total = numbers[left] + numbers[right] if total == target: return [left + 1, right + 1] elif total < target: left += 1 else: right -= 1 return [-1, -1] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。只用到了常数空间存放若干变量。 ================================================ FILE: docs/solutions/0100-0199/two-sum-iii-data-structure-design.md ================================================ # [0170. 两数之和 III - 数据结构设计](https://leetcode.cn/problems/two-sum-iii-data-structure-design/) - 标签:设计、数组、哈希表、双指针、数据流 - 难度:简单 ## 题目链接 - [0170. 两数之和 III - 数据结构设计 - 力扣](https://leetcode.cn/problems/two-sum-iii-data-structure-design/) ## 题目大意 设计一个接受整数流的数据结构,使该数据结构支持检查是否存在两数之和等于特定值。 实现 TwoSum 类: - `TwoSum()`:使用空数组初始化 TwoSum 对象 - `def add(self, number: int) -> None:`向数据结构添加一个数 number - `def find(self, value: int) -> bool:`寻找数据结构中是否存在一对整数,使得两数之和与给定的值 value 相等。如果存在,返回 True ;否则,返回 False 。 ## 解题思路 使用哈希表存储数组元素值与元素频数的关系。哈希表中键值对信息为 number: count。count 为 number 在数组中的频数。 - `add(number)` 函数中:在哈希表添加 number 与其频数之间的关系。 - `find(number)` 函数中:遍历哈希表,对于每个 number,检测哈希表中是否存在 value - number,如果存在则终止循环并返回结果。 - 如果 `number == value - number`,则判断哈希表中 number 的数目是否大于等于 2。 ## 代码 ```python class TwoSum: def __init__(self): """ Initialize your data structure here. """ self.num_counts = dict() def add(self, number: int) -> None: """ Add the number to an internal data structure.. """ if number in self.num_counts: self.num_counts[number] += 1 else: self.num_counts[number] = 1 def find(self, value: int) -> bool: """ Find if there exists any pair of numbers which sum is equal to the value. """ for number in self.num_counts.keys(): number2 = value - number if number == number2: if self.num_counts[number] > 1: return True else: if number2 in self.num_counts: return True return False ``` ================================================ FILE: docs/solutions/0100-0199/valid-palindrome.md ================================================ # [0125. 验证回文串](https://leetcode.cn/problems/valid-palindrome/) - 标签:双指针、字符串 - 难度:简单 ## 题目链接 - [0125. 验证回文串 - 力扣](https://leetcode.cn/problems/valid-palindrome/) ## 题目大意 **描述**:给定一个字符串 `s`。 **要求**:判断是否为回文串(只考虑字符串中的字母和数字字符,并且忽略字母的大小写)。 **说明**: - 回文串:正着读和反着读都一样的字符串。 - $1 \le s.length \le 2 * 10^5$。 - `s` 仅由可打印的 ASCII 字符组成。 **示例**: - 示例 1: ```python 输入: "A man, a plan, a canal: Panama" 输出:true 解释:"amanaplanacanalpanama" 是回文串。 ``` - 示例 2: ```python 输入:"race a car" 输出:false 解释:"raceacar" 不是回文串。 ``` ## 解题思路 ### 思路 1:对撞指针 1. 使用两个指针 `left`,`right`。`left` 指向字符串开始位置,`right` 指向字符串结束位置。 2. 判断两个指针对应字符是否是字母或数字。 通过 `left` 右移、`right` 左移的方式过滤掉字母和数字以外的字符。 3. 然后判断 `s[left]` 是否和 `s[right]` 相等(注意大小写)。 1. 如果相等,则将 `left` 右移、`right` 左移,继续进行下一次过滤和判断。 2. 如果不相等,则说明不是回文串,直接返回 `False`。 4. 如果遇到 `left == right`,跳出循环,则说明该字符串是回文串,返回 `True`。 ### 思路 1:代码 ```python class Solution: def isPalindrome(self, s: str) -> bool: left = 0 right = len(s) - 1 while left < right: if not s[left].isalnum(): left += 1 continue if not s[right].isalnum(): right -= 1 continue if s[left].lower() == s[right].lower(): left += 1 right -= 1 else: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(len(s))$。 - **空间复杂度**:$O(len(s))$。 ================================================ FILE: docs/solutions/0100-0199/word-break-ii.md ================================================ # [0140. 单词拆分 II](https://leetcode.cn/problems/word-break-ii/) - 标签:字典树、记忆化搜索、数组、哈希表、字符串、动态规划、回溯 - 难度:困难 ## 题目链接 - [0140. 单词拆分 II - 力扣](https://leetcode.cn/problems/word-break-ii/) ## 题目大意 给定一个非空字符串 `s` 和一个包含非空单词列表的字典 `wordDict`。 要求:在字符串中增加空格来构建一个句子,使得句子中所有的单词都在词典中。返回所有这些可能的句子。 说明: - 分隔时可以重复使用字典中的单词。 - 你可以假设字典中没有重复的单词。 ## 解题思路 回溯 + 记忆化搜索。 对于字符串 `s`,如果某个位置左侧部分是单词列表中的单词,则拆分出该单词,然后对 `s` 右侧剩余部分进行递归拆分。如果可以将整个字符串 `s` 拆分成单词列表中的单词,则得到一个句子。 使用 `memo` 数组进行记忆化存储,这样可以减少重复计算。 ## 代码 ```python class Solution: def wordBreak(self, s: str, wordDict: List[str]) -> List[str]: size = len(s) memo = [None for _ in range(size + 1)] def dfs(start): if start > size - 1: return [[]] if memo[start]: return memo[start] res = [] for i in range(start, size): word = s[start: i + 1] if word in wordDict: rest_res = dfs(i + 1) for item in rest_res: res.append([word] + item) memo[start] = res return res res = dfs(0) ans = [] for item in res: ans.append(" ".join(item)) return ans ``` ================================================ FILE: docs/solutions/0100-0199/word-break.md ================================================ # [0139. 单词拆分](https://leetcode.cn/problems/word-break/) - 标签:字典树、记忆化搜索、数组、哈希表、字符串、动态规划 - 难度:中等 ## 题目链接 - [0139. 单词拆分 - 力扣](https://leetcode.cn/problems/word-break/) ## 题目大意 **描述**:给定一个非空字符串 $s$ 和一个包含非空单词的列表 $wordDict$ 作为字典。 **要求**:判断是否可以利用字典中出现的单词拼接出 $s$ 。 **说明**: - 不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。 - $1 \le s.length \le 300$。 - $1 \le wordDict.length \le 1000$。 - $1 \le wordDict[i].length \le 20$。 - $s$ 和 $wordDict[i]$ 仅有小写英文字母组成。 - $wordDict$ 中的所有字符串互不相同。 **示例**: - 示例 1: ```python 输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true 解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。 ``` - 示例 2: ```python 输入: s = "applepenapple", wordDict = ["apple", "pen"] 输出: true 解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。 注意,你可以重复使用字典中的单词。 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照单词结尾位置进行阶段划分。 ###### 2. 定义状态 $s$ 能否拆分为单词表的单词,可以分解为: - 前 $i$ 个字符构成的字符串,能否分解为单词。 - 剩余字符串,能否分解为单词。 定义状态 $dp[i]$ 表示:长度为 $i$ 的字符串 $s[0: i]$ 能否拆分成单词,如果为 $True$ 则表示可以拆分,如果为 $False$ 则表示不能拆分。 ###### 3. 状态转移方程 - 如果 $s[0: j]$ 可以拆分为单词(即 $dp[j] == True$),并且字符串 $s[j: i]$ 出现在字典中,则 `dp[i] = True`。 - 如果 $s[0: j]$ 不可以拆分为单词(即 $dp[j] == False$),或者字符串 $s[j: i]$ 没有出现在字典中,则 `dp[i] = False`。 ###### 4. 初始条件 - 长度为 $0$ 的字符串 $s[0: i]$ 可以拆分为单词,即 $dp[0] = True$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示:长度为 $i$ 的字符串 $s[0: i]$ 能否拆分成单词。则最终结果为 $dp[size]$,$size$ 为字符串长度。 ### 思路 1:代码 ```python class Solution: def wordBreak(self, s: str, wordDict: List[str]) -> bool: size = len(s) dp = [False for _ in range(size + 1)] dp[0] = True for i in range(size + 1): for j in range(i): if dp[j] and s[j: i] in wordDict: dp[i] = True return dp[size] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0100-0199/word-ladder-ii.md ================================================ # [0126. 单词接龙 II](https://leetcode.cn/problems/word-ladder-ii/) - 标签:广度优先搜索、哈希表、字符串、回溯 - 难度:困难 ## 题目链接 - [0126. 单词接龙 II - 力扣](https://leetcode.cn/problems/word-ladder-ii/) ## 题目大意 **描述**: 给你两个单词 $beginWord$ 和 $endWord$ ,以及一个字典 $wordList$。 **要求**: 按字典 $wordList$ 完成从单词 $beginWord$ 到单词 $endWord$ 转化,一个表示此过程的「转换序列」是形式上像 $beginWord \rightarrow s1 \rightarrow s2 \rightarrow ... \rightarrow sk$ 这样的单词序列,并满足: - 每对相邻的单词之间仅有单个字母不同。 - 转换过程中的每个单词 $si$($1 \le i \le k$)必须是字典 $wordList$ 中的单词。注意,$beginWord$ 不必是字典 $wordList$ 中的单词。 - $sk == endWord$。 找出并返回所有从 $beginWord$ 到 $endWord$ 的「最短转换序列」,如果不存在这样的转换序列,返回一个空列表。每个序列都应该以单词列表 $[beginWord, s1, s2, ..., sk]$ 的形式返回。 **说明**: - $1 \le beginWord.length \le 5$。 - $endWord.length == beginWord.length$。 - $1 \le wordList.length \le 500$。 - $wordList[i].length == beginWord.length$。 - $beginWord$、$endWord$ 和 $wordList[i]$ 由小写英文字母组成。 - $beginWord \ne endWord$。 - $wordList$ 中的所有单词 互不相同。 **示例**: - 示例 1: ```python 输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] 输出:[["hit","hot","dot","dog","cog"],["hit","hot","lot","log","cog"]] 解释:存在 2 种最短的转换序列: "hit" -> "hot" -> "dot" -> "dog" -> "cog" "hit" -> "hot" -> "lot" -> "log" -> "cog" ``` - 示例 2: ```python 输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"] 输出:[] 解释:endWord "cog" 不在字典 wordList 中,所以不存在符合要求的转换序列。 ``` ## 解题思路 ### 思路 1:BFS 构建图 + DFS 回溯 这道题是单词接龙的进阶版本,需要找到所有最短转换序列。我们需要先使用 BFS 找到最短路径长度,然后使用 DFS 回溯找到所有最短路径。 ###### 1. 算法思路 1. **BFS 构建图**:使用 BFS 从 $beginWord$ 开始,逐层扩展,构建转换图 $graph$,同时记录每个单词到 $beginWord$ 的最短距离 $distance$。 2. **DFS 回溯找路径**:从 $endWord$ 开始,使用 DFS 回溯,只访问距离递减的路径,找到所有最短路径。 ###### 2. 具体步骤 1. **预处理**:将 $wordList$ 转换为集合 $wordSet$,便于快速查找。 2. **BFS 构建图**:使用队列进行 BFS,记录每个单词的下一层可达单词和距离。 3. **DFS 回溯**:从 $endWord$ 开始,沿着距离递减的路径回溯到 $beginWord$。 ###### 3. 关键变量 - $wordSet$:字典集合,用于快速查找单词是否存在 - $graph$:转换图,$graph[word]$ 存储 $word$ 可以转换到的所有单词 - $distance$:距离字典,$distance[word]$ 存储 $word$ 到 $beginWord$ 的最短距离 - $result$:存储所有找到的最短路径 ### 思路 1:代码 ```python import collections from typing import List class Solution: def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]: # 将 wordList 转换为集合,便于快速查找 wordSet = set(wordList) if endWord not in wordSet: return [] # 构建转换图和距离字典 graph = collections.defaultdict(list) distance = {} # BFS 构建图 queue = collections.deque([beginWord]) distance[beginWord] = 0 while queue: current = queue.popleft() if current == endWord: break # 尝试改变每个位置的字符 for i in range(len(current)): for c in 'abcdefghijklmnopqrstuvwxyz': if c != current[i]: new_word = current[:i] + c + current[i+1:] if new_word in wordSet: if new_word not in distance: distance[new_word] = distance[current] + 1 queue.append(new_word) if distance[new_word] == distance[current] + 1: graph[new_word].append(current) # 如果 endWord 不在距离字典中,说明无法到达 if endWord not in distance: return [] # DFS 回溯找所有最短路径 result = [] def dfs(current_word, path): if current_word == beginWord: result.append(path[::-1]) # 反转路径 return for next_word in graph[current_word]: if next_word in distance and distance[next_word] == distance[current_word] - 1: dfs(next_word, path + [next_word]) dfs(endWord, [endWord]) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(N \times M \times 26 + K \times L)$,其中 $N$ 是 $wordList$ 的长度,$M$ 是单词的长度,$K$ 是最短路径的数量,$L$ 是最短路径的长度。BFS 构建图的时间复杂度是 $O(N \times M \times 26)$,DFS 回溯的时间复杂度是 $O(K \times L)$。 - **空间复杂度**:$O(N \times M + K \times L)$,用于存储图结构、距离字典和所有最短路径。 ================================================ FILE: docs/solutions/0100-0199/word-ladder.md ================================================ # [0127. 单词接龙](https://leetcode.cn/problems/word-ladder/) - 标签:广度优先搜索、哈希表、字符串 - 难度:困难 ## 题目链接 - [0127. 单词接龙 - 力扣](https://leetcode.cn/problems/word-ladder/) ## 题目大意 给定两个单词 $beginWord$ 和 $endWord$,以及一个字典 $wordList$。找到从 `beginWord` 到 `endWord` 的最短转换序列中的单词数目。如果不存在这样的转换序列,则返回 0。 转换需要遵守的规则如下: - 每次转换只能改变一个字母。 - 转换过程中的中间单词必须为字典中的单词。 ## 解题思路 广度优先搜索。使用队列存储将要遍历的单词和单词数目。 从 `beginWord` 开始变换,把单词的每个字母都用 `a ~ z` 变换一次,变换后的单词是否是 `endWord`,如果是则直接返回。 否则查找变换后的词是否在 `wordList` 中。如果在 `wordList` 中找到就加入队列,找不到就输出 `0`。然后按照广度优先搜索的算法急需要遍历队列中的节点,直到所有单词都出队时结束。 ## 代码 ```python class Solution: def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: if not wordList or endWord not in wordList: return 0 word_set = set(wordList) if beginWord in word_set: word_set.remove(beginWord) queue = collections.deque() queue.append((beginWord, 1)) while queue: word, level = queue.popleft() if word == endWord: return level for i in range(len(word)): for j in range(26): new_word = word[:i] + chr(ord('a') + j) + word[i + 1:] if new_word in word_set: word_set.remove(new_word) queue.append((new_word, level + 1)) return 0 ``` ================================================ FILE: docs/solutions/0200-0299/3sum-smaller.md ================================================ # [0259. 较小的三数之和](https://leetcode.cn/problems/3sum-smaller/) - 标签:数组、双指针、二分查找、排序 - 难度:中等 ## 题目链接 - [0259. 较小的三数之和 - 力扣](https://leetcode.cn/problems/3sum-smaller/) ## 题目大意 **描述**:给定一个长度为 $n$ 的整数数组和一个目标值 $target$。 **要求**:寻找能够使条件 $nums[i] + nums[j] + nums[k] < target$ 成立的三元组 ($i$, $j$, $k$) 的个数($0 <= i < j < k < n$)。 **说明**: - 最好在 $O(n^2)$ 的时间复杂度内解决问题。 - $n == nums.length$。 - $0 \le n \le 3500$。 - $-100 \le nums[i] \le 100$。 - $-100 \le target \le 100$。 **示例**: - 示例 1: ```python 输入: nums = [-2,0,1,3], target = 2 输出: 2 解释: 因为一共有两个三元组满足累加和小于 2: [-2,0,1] [-2,0,3] ``` - 示例 2: ```python 输入: nums = [], target = 0 输出: 0 ``` ## 解题思路 ### 思路 1:排序 + 双指针 三元组直接枚举的时间复杂度是 $O(n^3)$,明显不符合题目要求。那么可以考虑使用双指针减少循环内的时间复杂度。具体做法如下: - 先对数组进行从小到大排序。 - 遍历数组,对于数组元素 $nums[i]$,使用两个指针 $left$、$right$。$left$ 指向第 $i + 1$ 个元素位置,$right$ 指向数组的最后一个元素位置。 - 在区间 $[left, right]$ 中查找满足 $nums[i] + nums[left] + nums[right] < target$的方案数。 - 计算 $nums[i]$、$nums[left]$、$nums[right]$ 的和,将其与 $target$ 比较。 - 如果 $nums[i] + nums[left] + nums[right] < target$,则说明 $i$、$left$、$right$ 作为三元组满足题目要求,同时说明区间 $[left, right]$ 中的元素作为 $right$ 都满足条件,此时将 $left$ 右移,继续判断。 - 如果 $nums[i] + nums[left] + nums[right] \ge target$,则说明 $right$ 太大了,应该缩小 $right$,然后继续判断。 - 当 $left == right$ 时,区间搜索完毕,继续遍历 $nums[i + 1]$。 这种思路使用了两重循环,其中内层循环当 $left == right$ 时循环结束,时间复杂度为 $O(n)$,外层循环时间复杂度也是 $O(n)$。所以算法的整体时间复杂度为 $O(n^2)$,符合题目要求。 ### 思路 1:代码 ```python class Solution: def threeSumSmaller(self, nums: List[int], target: int) -> int: nums.sort() size = len(nums) res = 0 for i in range(size): left, right = i + 1, size - 1 while left < right: total = nums[i] + nums[left] + nums[right] if total < target: res += (right - left) left += 1 else: right -= 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0200-0299/add-digits.md ================================================ # [0258. 各位相加](https://leetcode.cn/problems/add-digits/) - 标签:数学、数论、模拟 - 难度:简单 ## 题目链接 - [0258. 各位相加 - 力扣](https://leetcode.cn/problems/add-digits/) ## 题目大意 给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。 ## 解题思路 根据题意,循环模拟累加即可。 ## 代码 ```python class Solution: def addDigits(self, num: int) -> int: while num >= 10: cur = 0 while num: cur += num % 10 num //= 10 num = cur return num ``` ================================================ FILE: docs/solutions/0200-0299/alien-dictionary.md ================================================ # [0269. 火星词典](https://leetcode.cn/problems/alien-dictionary/) - 标签:深度优先搜索、广度优先搜索、图、拓扑排序、数组、字符串 - 难度:困难 ## 题目链接 - [0269. 火星词典 - 力扣](https://leetcode.cn/problems/alien-dictionary/) ## 题目大意 **描述**: 现有一种使用英语字母的火星语言,这门语言的字母顺序对你来说是未知的。 给定一个来自这种外星语言字典的字符串列表 $words$ ,$words$ 中的字符串已经「按这门新语言的字典序进行了排序」。 **要求**: 如果这种说法是错误的,并且给出的 $words$ 不能对应任何字母的顺序,则返回 `""`。 否则,返回一个按新语言规则的「字典递增顺序」排序的独特字符串。如果有多个解决方案,则返回其中任意一个。 **说明**: - $1 \le words.length \le 10^{3}$。 - $1 \le words[i].length \le 10^{3}$。 - $words[i]$ 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:words = ["wrt","wrf","er","ett","rftt"] 输出:"wertf" ``` - 示例 2: ```python 输入:words = ["z","x"] 输出:"zx" ``` ## 解题思路 ### 思路 1:拓扑排序 + 广度优先搜索 使用拓扑排序来解决这个问题。我们需要构建一个图来表示字母之间的相对顺序关系,然后使用拓扑排序来找到字母的正确顺序。 具体步骤如下: 1. 构建图:遍历相邻的单词对 $(words[i], words[i+1])$,找到第一个不同的字符,建立字符 $c_1$ 到字符 $c_2$ 的边,表示 $c_1$ 在 $c_2$ 之前。 2. 计算入度:统计每个字符的入度 $indegree[c]$。 3. 拓扑排序:使用广度优先搜索,从入度为 $0$ 的字符开始,逐步构建字母顺序。 4. 验证结果:检查是否所有字符都被包含在结果中。 关键点: - 如果存在环,说明无法确定字母顺序,返回空字符串。 - 如果拓扑排序后仍有字符未被访问,说明存在环。 - 需要处理所有出现的字符,包括那些没有相对顺序关系的字符。 ### 思路 1:代码 ```python class Solution: def alienOrder(self, words: List[str]) -> str: # 构建图:字符到其后继字符的映射 graph = {} # 统计每个字符的入度 indegree = {} # 初始化所有出现的字符 for word in words: for char in word: if char not in graph: graph[char] = set() if char not in indegree: indegree[char] = 0 # 构建图:比较相邻单词 for i in range(len(words) - 1): word1, word2 = words[i], words[i + 1] min_len = min(len(word1), len(word2)) # 检查是否存在前缀关系 for j in range(min_len): if word1[j] != word2[j]: # 找到第一个不同的字符,建立边关系 char1, char2 = word1[j], word2[j] if char2 not in graph[char1]: graph[char1].add(char2) indegree[char2] += 1 break else: # 如果所有字符都相同,但第一个单词更长,则无效 if len(word1) > len(word2): return "" # 拓扑排序:使用广度优先搜索 queue = [] # 找到所有入度为 0 的字符 for char in indegree: if indegree[char] == 0: queue.append(char) result = [] while queue: char = queue.pop(0) result.append(char) # 处理当前字符的所有后继字符 for next_char in graph[char]: indegree[next_char] -= 1 if indegree[next_char] == 0: queue.append(next_char) # 检查是否所有字符都被访问(无环) if len(result) != len(indegree): return "" return ''.join(result) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(C)$,其中 $C$ 是所有单词中字符的总数。需要遍历所有字符来构建图和进行拓扑排序。 - **空间复杂度**:$O(1)$,因为字符集大小固定为 $26$ 个字母,所以图的大小和入度数组都是常数级别。 ================================================ FILE: docs/solutions/0200-0299/basic-calculator-ii.md ================================================ # [0227. 基本计算器 II](https://leetcode.cn/problems/basic-calculator-ii/) - 标签:栈、数学、字符串 - 难度:中等 ## 题目链接 - [0227. 基本计算器 II - 力扣](https://leetcode.cn/problems/basic-calculator-ii/) ## 题目大意 **描述**:给定一个字符串表达式 `s`,表达式中所有整数为非负整数,运算符只有 `+`、`-`、`*`、`/`,没有括号。 **要求**:实现一个基本计算器来计算并返回它的值。 **说明**: - $1 \le s.length \le 3 * 10^5$。 - `s` 由整数和算符(`+`、`-`、`*`、`/`)组成,中间由一些空格隔开。 - `s` 表示一个有效表达式。 - 表达式中的所有整数都是非负整数,且在范围 $[0, 2^{31} - 1]$ 内。 - 题目数据保证答案是一个 32-bit 整数。 **示例**: - 示例 1: ```python 输入:s = "3+2*2" 输出:7 ``` - 示例 2: ```python 输入:s = " 3/2 " 输出:1 ``` ## 解题思路 ### 思路 1:栈 计算表达式中,乘除运算优先于加减运算。我们可以先进行乘除运算,再将进行乘除运算后的整数值放入原表达式中相应位置,再依次计算加减。 可以考虑使用一个栈来保存进行乘除运算后的整数值。正整数直接压入栈中,负整数,则将对应整数取负号,再压入栈中。这样最终计算结果就是栈中所有元素的和。 具体做法: 1. 遍历字符串 `s`,使用变量 `op` 来标记数字之前的运算符,默认为 `+`。 2. 如果遇到数字,继续向后遍历,将数字进行累积,得到完整的整数 num。判断当前 op 的符号。 1. 如果 `op` 为 `+`,则将 `num` 压入栈中。 2. 如果 `op` 为 `-`,则将 `-num` 压入栈中。 3. 如果 `op` 为 `*`,则将栈顶元素 `top` 取出,计算 `top * num`,并将计算结果压入栈中。 4. 如果 `op` 为 `/`,则将栈顶元素 `top` 取出,计算 `int(top / num)`,并将计算结果压入栈中。 3. 如果遇到 `+`、`-`、`*`、`/` 操作符,则更新 `op`。 4. 最后将栈中整数进行累加,并返回结果。 ### 思路 1:代码 ```python class Solution: def calculate(self, s: str) -> int: size = len(s) stack = [] op = '+' index = 0 while index < size: if s[index] == ' ': index += 1 continue if s[index].isdigit(): num = ord(s[index]) - ord('0') while index + 1 < size and s[index+1].isdigit(): index += 1 num = 10 * num + ord(s[index]) - ord('0') if op == '+': stack.append(num) elif op == '-': stack.append(-num) elif op == '*': top = stack.pop() stack.append(top * num) elif op == '/': top = stack.pop() stack.append(int(top / num)) elif s[index] in "+-*/": op = s[index] index += 1 return sum(stack) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0200-0299/basic-calculator.md ================================================ # [0224. 基本计算器](https://leetcode.cn/problems/basic-calculator/) - 标签:栈、递归、数学、字符串 - 难度:困难 ## 题目链接 - [0224. 基本计算器 - 力扣](https://leetcode.cn/problems/basic-calculator/) ## 题目大意 **描述**: 给定一个字符串表达式 $s$。 **要求**: 实现一个基本计算器来计算并返回它的值。 **说明**: - 注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 `eval()`。 - $1 \le s.length \le 3 \times 10^{5}$。 - $s$ 由数字、`'+'`、`'-'`、`'('、')'`、和 `' '` 组成。 - $s$ 表示一个有效的表达式。 - `'+'` 不能用作一元运算(例如, `"+1"` 和 `"+(2 + 3)"` 无效)。 - `'-'` 可以用作一元运算(即 `"-1"` 和 `"-(2 + 3)"` 是有效的)。 - 输入中不存在两个连续的操作符。 - 每个数字和运行的计算将适合于一个有符号的 32 位整数。 **示例**: - 示例 1: ```python 输入:s = "1 + 1" 输出:2 ``` - 示例 2: ```python 输入:s = " 2-1 + 2 " 输出:3 ``` ## 解题思路 ### 思路 1:栈 + 递归 使用栈来处理括号和运算符的优先级。由于只有加法和减法,我们可以通过维护一个符号栈来处理括号内的运算。 具体步骤如下: 1. 使用栈 $stack$ 来存储当前的计算结果和符号。 2. 使用变量 $result$ 来存储当前的计算结果。 3. 使用变量 $sign$ 来存储当前的符号($+1$ 或 $-1$)。 4. 遍历字符串 $s$: - 如果遇到数字,则计算完整的数字值。 - 如果遇到 `+`,则设置 $sign = 1$。 - 如果遇到 `-`,则设置 $sign = -1$。 - 如果遇到 `(`,则将当前的 $result$ 和 $sign$ 压入栈中,并重置 $result$ 和 $sign$。 - 如果遇到 `)`,则从栈中弹出之前的结果和符号,与当前结果进行运算 5. 最后返回 $result$。 这种方法可以正确处理括号的嵌套和运算符的优先级。 ### 思路 1:代码 ```python class Solution: def calculate(self, s: str) -> int: # 使用栈来处理括号和运算符 stack = [] result = 0 # 当前计算结果 sign = 1 # 当前符号,1 表示正数,-1 表示负数 i = 0 while i < len(s): char = s[i] if char.isdigit(): # 计算完整的数字 num = 0 while i < len(s) and s[i].isdigit(): num = num * 10 + int(s[i]) i += 1 i -= 1 # 回退一位,因为外层循环会 i += 1 result += sign * num elif char == '+': sign = 1 elif char == '-': sign = -1 elif char == '(': # 遇到左括号,将当前结果和符号压入栈 stack.append(result) stack.append(sign) # 重置结果和符号 result = 0 sign = 1 elif char == ')': # 遇到右括号,从栈中弹出之前的结果和符号 result *= stack.pop() # 弹出符号 result += stack.pop() # 弹出之前的结果 # 跳过空格 i += 1 return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。需要遍历字符串一次。 - **空间复杂度**:$O(n)$,最坏情况下栈的深度为 $O(n)$,例如当所有括号都嵌套时。 ================================================ FILE: docs/solutions/0200-0299/best-meeting-point.md ================================================ # [0296. 最佳的碰头地点](https://leetcode.cn/problems/best-meeting-point/) - 标签:数组、数学、矩阵、排序 - 难度:困难 ## 题目链接 - [0296. 最佳的碰头地点 - 力扣](https://leetcode.cn/problems/best-meeting-point/) ## 题目大意 **描述**: 给定一个 $m \times n$ 的二进制网格 $grid$,其中 $1$ 表示某个朋友的家所处的位置。 **要求**: 返回「最小的」总行走距离。 **说明**: - 总行走距离:指的是朋友们家到碰头地点的距离之和。使用「曼哈顿距离」来计算,其中 $distance(p1, p2) = |p2.x - p1.x| + |p2.y - p1.y|$。 - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 200$。 - $grid[i][j]$ 等于 $0$ 或者 $1$。 - $grid$ 中 至少 有两个朋友。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/14/meetingpoint-grid.jpg) ```python 输入: grid = [[1,0,0,0,1],[0,0,0,0,0],[0,0,1,0,0]] 输出: 6 解释: 给定的三个人分别住在(0,0),(0,4) 和 (2,2): (0,2) 是一个最佳的碰面点,其总行走距离为 2 + 2 + 2 = 6,最小,因此返回 6。 ``` - 示例 2: ```python 输入: grid = [[1,1]] 输出: 1 ``` ## 解题思路 ### 思路 1:中位数优化 这是一个经典的数学优化问题。关键点在于:对于曼哈顿距离,$x$ 坐标和 $y$ 坐标可以独立优化。 具体思路: 1. 收集所有朋友的位置坐标,分别得到 $x$ 坐标数组 $X$ 和 $y$ 坐标数组 $Y$ 2. 对 $x$ 坐标和 $y$ 坐标分别排序 3. 最优的碰头地点的 $x$ 坐标是 $X$ 的中位数,$y$ 坐标是 $Y$ 的中位数 4. 计算所有朋友到最优碰头地点的曼哈顿距离之和 **数学原理**:对于一维情况,中位数是最小化绝对偏差和的最优解。由于曼哈顿距离可以分解为 $x$ 坐标差和 $y$ 坐标差的和,所以可以分别优化。 ### 思路 1:代码 ```python class Solution: def minTotalDistance(self, grid: List[List[int]]) -> int: """ 找到最佳的碰头地点,使所有朋友的总行走距离最小 """ m, n = len(grid), len(grid[0]) # 收集所有朋友的位置坐标 x_coords = [] y_coords = [] for i in range(m): for j in range(n): if grid[i][j] == 1: x_coords.append(i) # 行坐标 y_coords.append(j) # 列坐标 # 对坐标进行排序 x_coords.sort() y_coords.sort() # 计算中位数位置 k = len(x_coords) median_x = x_coords[k // 2] # x 坐标的中位数 median_y = y_coords[k // 2] # y 坐标的中位数 # 计算总距离 total_distance = 0 for i in range(k): total_distance += abs(x_coords[i] - median_x) + abs(y_coords[i] - median_y) return total_distance ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n + k \log k)$,其中 $m \times n$ 是遍历网格的时间,$k$ 是朋友的数量,$k \log k$ 是排序的时间。 - **空间复杂度**:$O(k)$,用于存储所有朋友的坐标。 ================================================ FILE: docs/solutions/0200-0299/binary-tree-longest-consecutive-sequence.md ================================================ # [0298. 二叉树最长连续序列](https://leetcode.cn/problems/binary-tree-longest-consecutive-sequence/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0298. 二叉树最长连续序列 - 力扣](https://leetcode.cn/problems/binary-tree-longest-consecutive-sequence/) ## 题目大意 **描述**: 给你一棵指定的二叉树的根节点 $root$。 **要求**: 计算其中「最长连续序列路径」的长度。 **说明**: - 「最长连续序列路径」是依次递增 $1$ 的路径。该路径,可以是从某个初始节点到树中任意节点,通过「父 - 子」关系连接而产生的任意路径。且必须从父节点到子节点,反过来是不可以的。 - 树中节点的数目在范围 $[1, 3 \times 10^{4}]$ 内。 - $-3 \times 10^{4} \le Node.val \le 3 \times 10^{4}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/14/consec1-1-tree.jpg) ```python 输入:root = [1,null,3,2,4,null,null,null,5] 输出:3 解释:当中,最长连续序列是 3-4-5 ,所以返回结果为 3。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/14/consec1-2-tree.jpg) ```python 输入:root = [2,null,3,2,null,1] 输出:2 解释:当中,最长连续序列是 2-3 。注意,不是 3-2-1,所以返回 2。 ``` ## 解题思路 ### 思路 1:深度优先搜索 这是一个典型的树形动态规划问题。我们需要找到二叉树中最长的连续递增序列路径。 我们可以使用深度优先搜索来解决这个问题: 1. **明确问题**:对于每个节点,我们需要计算以该节点为起点的最长连续序列长度。 2. **状态定义**:定义 $dfs(node)$ 函数,返回以 $node$ 为起点的最长连续序列长度。 3. **状态转移**: - 如果 $node.left$ 存在且 $node.left.val = node.val + 1$,则左子树可以延续当前序列。 - 如果 $node.right$ 存在且 $node.right.val = node.val + 1$,则右子树可以延续当前序列。 - 取左右子树中的最大值,加上当前节点(长度为 $1$)。 4. **全局最优**:在遍历过程中,维护全局最长序列长度 $max\_length$。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def longestConsecutive(self, root: Optional[TreeNode]) -> int: self.max_length = 0 # 全局最长序列长度 def dfs(node): if not node: return 0 # 计算以当前节点为起点的最长连续序列长度 current_length = 1 # 递归计算左右子树 left_length = dfs(node.left) right_length = dfs(node.right) # 如果左子节点可以延续当前序列 if node.left and node.left.val == node.val + 1: current_length = max(current_length, left_length + 1) # 如果右子节点可以延续当前序列 if node.right and node.right.val == node.val + 1: current_length = max(current_length, right_length + 1) # 更新全局最长序列长度 self.max_length = max(self.max_length, current_length) return current_length dfs(root) # 从根节点开始深度优先搜索 return self.max_length ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树中节点的数量。每个节点都会被访问一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度。递归调用栈的深度最多为树的高度。 ================================================ FILE: docs/solutions/0200-0299/binary-tree-paths.md ================================================ # [0257. 二叉树的所有路径](https://leetcode.cn/problems/binary-tree-paths/) - 标签:树、深度优先搜索、字符串、回溯、二叉树 - 难度:简单 ## 题目链接 - [0257. 二叉树的所有路径 - 力扣](https://leetcode.cn/problems/binary-tree-paths/) ## 题目大意 给定一个二叉树,返回所有从根节点到叶子节点的路径。 ## 解题思路 深度优先搜索。在递归遍历时,需考虑当前节点和左右孩子节点。 - 如果当前节点不是叶子节点,则当前拼接路径中加入该点,并继续递归遍历。 - 如果当前节点是叶子节点,则当前拼接路径中加入该点,并将当前路径加入答案数组。 ## 代码 ```python class Solution: def binaryTreePaths(self, root: TreeNode) -> List[str]: res = [] def dfs(root, path): if not root: return path += str(root.val) if not root.left and not root.right: res.append(path) elif not root.right: dfs(root.left, path + "->") elif not root.left: dfs(root.right, path + "->") else: dfs(root.left, path + "->") dfs(root.right, path + "->") dfs(root, "") return res ``` ================================================ FILE: docs/solutions/0200-0299/bitwise-and-of-numbers-range.md ================================================ # [0201. 数字范围按位与](https://leetcode.cn/problems/bitwise-and-of-numbers-range/) - 标签:位运算 - 难度:中等 ## 题目链接 - [0201. 数字范围按位与 - 力扣](https://leetcode.cn/problems/bitwise-and-of-numbers-range/) ## 题目大意 **描述**:给定两个整数 $left$ 和 $right$,表示区间 $[left, right]$。 **要求**:返回此区间内所有数字按位与的结果(包含 $left$、$right$ 端点)。 **说明**: - $0 \le left \le right \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:left = 5, right = 7 输出:4 ``` - 示例 2: ```python 输入:left = 1, right = 2147483647 输出:0 ``` ## 解题思路 ### 思路 1:位运算 很容易想到枚举算法:对于区间 $[left, right]$,如果使用枚举算法,对区间范围内的数依次进行按位与操作,最后输出结果。 但是枚举算法在区间范围很大的时候会超时,所以我们应该换个思路来解决这道题。 我们知道与运算的规则如下: - `0 & 0 == 0` - `0 & 1 == 0` - `1 & 0 == 0` - `1 & 1 == 1`。 只有对应位置上都为 $1$ 的情况下,按位与才能得到 $1$。而对应位置上只要出现 $0$,则该位置上最终的按位与结果一定为 $0$。 那么我们可以先来求一下区间所有数对应二进制的公共前缀,假设这个前缀的长度为 $x$。 公共前缀部分因为每个位置上的二进制值完全一样,所以按位与的结果也相同。 接下来考虑除了公共前缀的剩余的二进制位部分。 这时候剩余部分有两种情况: - $x = 31$。则 $left == right$,其按位与结果就是 $left$ 本身。 - $0 \le x < 31$。这种情况下因为 $left < right$,所以 $left$ 的第 $x + 1$ 位必然为 $0$,$right$ 的第 $x + 1$ 位必然为 $1$。 - 注意:$left$、$right$ 第 $x + 1$ 位上不可能同为 $0$ 或 $1$,这样就是公共前缀了。 - 注意:同样不可能是 $left$ 第 $x + 1$ 位为 $1$,$right$ 第 $x + 1$ 位为 $0$,这样就是 $left > right$ 了。 而从第 $x + 1$ 位起,从 $left$ 到 $right$。肯定会经过 $10000...$ 的位置,从而使得除了公共前缀的剩余部分(后面的 $31 - x$ 位)的按位与结果一定为 $0$。 举个例子,$x = 27$,则除了公共前缀的剩余部分长度为 $4$。则剩余部分从 $0XXX$ 到 $1XXX$ 必然会经过 $1000$,则剩余部分的按位与结果为 $0000$。 那么这道题就转变为了求 $[left, right]$ 区间范围内所有数的二进制公共前缀,然后在后缀位置上补上 $0$。 求解公共前缀,我们借助于 Brian Kernigham 算法中的 `n & (n - 1)` 公式来计算。 - `n & (n - 1)` 公式:对 $n$ 和 $n - 1$ 进行按位与运算后,$n$ 最右边的 $1$ 会变成 $0$,也就是清除了 $n$ 对应二进制的最右侧的 $1$。比如 $n = 10110100_{(2)}$,进行 `n & (n - 1)` 操作之后,就变为了 $n = 10110000_{(2)}$。 具体计算步骤如下: 1. 对于给定的区间范围 $[left, right]$,对 $right$ 进行 `right & (right - 1)` 迭代。 2. 直到 $right$ 小于等于 $left$,此时区间内非公共前缀的 $1$ 均变为了 $0$。 3. 最后输出 $right$ 作为答案。 ### 思路 1:位运算代码 ```python class Solution: def rangeBitwiseAnd(self, left: int, right: int) -> int: while left < right: right = right & (right - 1) return right ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(1)$。 ## 参考资料 - 【题解】[巨好理解的位运算思路 - 数字范围按位与 - 力扣](https://leetcode.cn/problems/bitwise-and-of-numbers-range/solution/ju-hao-li-jie-de-wei-yun-suan-si-lu-by-time-limit/) ================================================ FILE: docs/solutions/0200-0299/bulls-and-cows.md ================================================ # [0299. 猜数字游戏](https://leetcode.cn/problems/bulls-and-cows/) - 标签:哈希表、字符串、计数 - 难度:中等 ## 题目链接 - [0299. 猜数字游戏 - 力扣](https://leetcode.cn/problems/bulls-and-cows/) ## 题目大意 **描述**: 你在和朋友一起玩 猜数字(Bulls and Cows)游戏,该游戏规则如下: 写出一个秘密数字,并请朋友猜这个数字是多少。朋友每猜测一次,你就会给他一个包含下述信息的提示: - 猜测数字中有多少位属于数字和确切位置都猜对了(称为 `"Bulls"`,公牛), - 有多少位属于数字猜对了但是位置不对(称为 `"Cows"`,奶牛)。也就是说,这次猜测中有多少位非公牛数字可以通过重新排列转换成公牛数字。 给定一个秘密数字 $secret$ 和朋友猜测的数字 $guess$。 **要求**: 请你返回对朋友这次猜测的提示。 提示的格式为 `"xAyB"`,$x$ 是公牛个数, $y$ 是奶牛个数,$A$ 表示公牛,$B$ 表示奶牛。 请注意秘密数字和朋友猜测的数字都可能含有重复数字。 **说明**: - $1 \le secret.length, guess.length \le 10^{3}$。 - $secret.length == guess.length$。 - $secret$ 和 $guess$ 仅由数字组成。 **示例**: - 示例 1: ```python 输入:secret = "1807", guess = "7810" 输出:"1A3B" 解释:数字和位置都对(公牛)用 '|' 连接,数字猜对位置不对(奶牛)的采用斜体加粗标识。 "1807" | "7810" ``` - 示例 2: ```python 输入:secret = "1123", guess = "0111" 输出:"1A1B" 解释:数字和位置都对(公牛)用 '|' 连接,数字猜对位置不对(奶牛)的采用斜体加粗标识。 "1123" "1123" | or | "0111" "0111" 注意,两个不匹配的 1 中,只有一个会算作奶牛(数字猜对位置不对)。通过重新排列非公牛数字,其中仅有一个 1 可以成为公牛数字。 ``` ## 解题思路 ### 思路 1:哈希表计数 这是一个典型的计数问题。我们需要分别统计 Bulls 和 Cows 的数量。 **算法步骤**: 1. **统计 Bulls**:遍历 $secret$ 和 $guess$,如果 $secret[i] = guess[i]$,则 $bulls$ 计数加 $1$。 2. **统计 Cows**: - 使用哈希表 $secret\_count$ 统计 $secret$ 中每个数字的出现次数(排除 Bulls 位置)。 - 使用哈希表 $guess\_count$ 统计 $guess$ 中每个数字的出现次数(排除 Bulls 位置)。 - 对于每个数字 $digit$,$cows$ 加上 $min(secret\_count[digit], guess\_count[digit])$。 3. **返回结果**:返回格式为 $f"{bulls}A{cows}B"$。 **关键点**: - Bulls 位置的数字不能重复计算为 Cows。 - 对于重复数字,Cows 的数量是两者中较小值。 ### 思路 1:代码 ```python class Solution: def getHint(self, secret: str, guess: str) -> str: bulls = 0 # Bulls 计数:位置和数字都正确 cows = 0 # Cows 计数:数字正确但位置错误 # 统计 Bulls secret_chars = list(secret) guess_chars = list(guess) for i in range(len(secret)): if secret_chars[i] == guess_chars[i]: bulls += 1 # 将 Bulls 位置的字符标记为已使用,避免重复计算 secret_chars[i] = 'X' guess_chars[i] = 'X' # 统计 Cows secret_count = {} # secret 中每个数字的出现次数(排除 Bulls) guess_count = {} # guess 中每个数字的出现次数(排除 Bulls) # 统计 secret 中非 Bulls 位置的数字 for char in secret_chars: if char != 'X': secret_count[char] = secret_count.get(char, 0) + 1 # 统计 guess 中非 Bulls 位置的数字 for char in guess_chars: if char != 'X': guess_count[char] = guess_count.get(char, 0) + 1 # 计算 Cows:对于每个数字,取两者中的较小值 for digit in secret_count: if digit in guess_count: cows += min(secret_count[digit], guess_count[digit]) return f"{bulls}A{cows}B" ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度。需要遍历字符串两次,哈希表操作的时间复杂度为 $O(1)$。 - **空间复杂度**:$O(1)$,因为数字只有 $0-9$ 共 $10$ 种,哈希表最多存储 $10$ 个键值对,空间复杂度为常数。 ================================================ FILE: docs/solutions/0200-0299/closest-binary-search-tree-value-ii.md ================================================ # [0272. 最接近的二叉搜索树值 II](https://leetcode.cn/problems/closest-binary-search-tree-value-ii/) - 标签:栈、树、深度优先搜索、二叉搜索树、双指针、二叉树、堆(优先队列) - 难度:困难 ## 题目链接 - [0272. 最接近的二叉搜索树值 II - 力扣](https://leetcode.cn/problems/closest-binary-search-tree-value-ii/) ## 题目大意 **描述**: 给定二叉搜索树的根 $root$ 、一个目标值 $target$ 和一个整数 $k$。 **要求**: 返回 BST 中最接近目标的 $k$ 个值。你可以按任意顺序返回答案。 **说明**: - 题目保证该二叉搜索树中只会存在一种 $k$ 个值集合最接近 $target$。 - 二叉树的节点总数为 n。 - $1 \le k \le n \le 10^{4}$。 - $0 \le Node.val \le 10^{9}$。 - $-10^{9} \le target \le 10^{9}$。 - 进阶:假设该二叉搜索树是平衡的,请问您是否能在小于 O(n)( n = total nodes )的时间复杂度内解决该问题呢? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/12/closest1-1-tree.jpg) ```python 输入: root = [4,2,5,1,3],目标值 = 3.714286,且 k = 2 输出: [4,3] ``` - 示例 2: ```python 输入: root = [1], target = 0.000000, k = 1 输出: [1] ``` ## 解题思路 ### 思路 1:中序遍历 + 排序 这是一个典型的二叉搜索树问题。我们需要找到 BST 中最接近目标值 $target$ 的 $k$ 个值。 我们可以使用中序遍历 + 排序的方法来解决这个问题: 1. **中序遍历**:由于 BST 的中序遍历结果是有序的,我们可以先通过中序遍历得到所有节点的值,存储在数组 $values$ 中。 2. **按距离排序**:将所有节点值按照与 $target$ 的距离进行排序,选择距离最小的 $k$ 个值。 3. **算法步骤**: - 对 BST 进行中序遍历,得到有序数组 $values$。 - 创建一个包含节点值和距离的列表 $distances$,其中每个元素为 $(|value - target|, value)$。 - 对 $distances$ 按照距离进行排序。 - 取前 $k$ 个元素的值作为结果。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def closestKValues(self, root: Optional[TreeNode], target: float, k: int) -> List[int]: values = [] # 存储中序遍历的结果 def inorder(node): """中序遍历 BST,得到有序数组""" if not node: return inorder(node.left) # 遍历左子树 values.append(node.val) # 访问根节点 inorder(node.right) # 遍历右子树 inorder(root) # 执行中序遍历 # 创建距离列表,每个元素为 (距离, 值) distances = [] for value in values: distance = abs(value - target) distances.append((distance, value)) # 按距离排序,取前 k 个值 distances.sort() result = [distances[i][1] for i in range(k)] return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是 BST 中节点的数量。需要遍历所有节点进行中序遍历,然后对距离进行排序。 - **空间复杂度**:$O(n)$,需要存储中序遍历的结果数组、距离列表,以及递归调用栈的空间。 ================================================ FILE: docs/solutions/0200-0299/closest-binary-search-tree-value.md ================================================ # [0270. 最接近的二叉搜索树值](https://leetcode.cn/problems/closest-binary-search-tree-value/) - 标签:树、深度优先搜索、二叉搜索树、二分查找、二叉树 - 难度:简单 ## 题目链接 - [0270. 最接近的二叉搜索树值 - 力扣](https://leetcode.cn/problems/closest-binary-search-tree-value/) ## 题目大意 **描述**:给定一个不为空的二叉搜索树的根节点,以及一个目标值 $target$。 **要求**:在二叉搜索树中找到最接近目标值 $target$ 的数值。 **说明**: - 树中节点的数目在范围 $[1, 10^4]$ 内。 - $0 \le Node.val \le 10^9$。 - $-10^9 \le target \le 10^9$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/12/closest1-1-tree.jpg) ```python 输入:root = [4,2,5,1,3], target = 3.714286 输出:4 ``` - 示例 2: ```python 输入:root = [1], target = 4.428571 输出:1 ``` ## 解题思路 ### 思路 1:二分查找算法 题目中最接近目标值 $target$ 的数值指的就是与 $target$ 相减绝对值最小的数值。 而且根据二叉搜索树的性质,我们可以利用二分搜索的方式,查找与 $target$ 相减绝对值最小的数值。具体做法为: - 定义一个变量 $closest$ 表示与 $target$ 最接近的数值,初始赋值为根节点的值 $root.val$。 - 判断当前节点的值域 $closet$ 值哪个更接近 $target$,如果当前值更接近,则更新 $closest$。 - 如果 $target$ < 当前节点值,则从当前节点的左子树继续查找。 - 如果 $target$ ≥ 当前节点值,则从当前节点的右子树继续查找。 ### 思路 1:代码 ```python class Solution: def closestValue(self, root: TreeNode, target: float) -> int: closest = root.val while root: if abs(target - root.val) < abs(target - closest): closest = root.val if target < root.val: root = root.left else: root = root.right return closest ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$,其中 $n$ 为二叉搜索树的节点个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/combination-sum-iii.md ================================================ # [0216. 组合总和 III](https://leetcode.cn/problems/combination-sum-iii/) - 标签:数组、回溯 - 难度:中等 ## 题目链接 - [0216. 组合总和 III - 力扣](https://leetcode.cn/problems/combination-sum-iii/) ## 题目大意 **描述**: 给定两个整数 $n$ 和 $k$。 **要求**: 找出所有相加之和为 $n$ 的 $k$ 个数的组合,且满足下列条件: - 只使用数字 $1$ 到 $9$ - 每个数字最多使用一次。 返回所有可能的有效组合的列表。该列表不能包含相同的组合两次,组合可以以任何顺序返回。 **说明**: - $2 \le k \le 9$。 - $1 \le n \le 60$。 **示例**: - 示例 1: ```python 示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]] 解释: 1 + 2 + 4 = 7 没有其他符合的组合了。 ``` - 示例 2: ```python 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]] 解释: 1 + 2 + 6 = 9 1 + 3 + 5 = 9 2 + 3 + 4 = 9 没有其他符合的组合了。 ``` ## 解题思路 ### 思路 1:回溯算法 这是一个典型的回溯算法问题。我们需要从数字 $1$ 到 $9$ 中选择 $k$ 个数字,使得它们的和等于 $n$,且每个数字最多使用一次。 我们可以使用回溯算法来解决这个问题: 1. **明确所有选择**:对于每个位置,我们可以选择数字 $1$ 到 $9$ 中的任意一个(前提是该数字还没有被使用过)。 2. **明确终止条件**: - 当选择的数字个数达到 $k$ 个时,检查这些数字的和是否等于 $n$。 - 如果和等于 $n$,则找到一个有效组合,将其加入结果集。 - 如果和大于 $n$,则剪枝,因为继续选择只会使和更大。 3. **将决策树和终止条件翻译成代码**: - 定义回溯函数:`backtracking(start, path, current_sum)`,其中 $start$ 表示当前可以选择的数字范围,$path$ 表示当前选择的数字组合,$current\_sum$ 表示当前数字组合的和。 - 对于每个位置,从 $start$ 开始枚举可选数字,避免重复选择。 - 选择数字后递归搜索,搜索完成后撤销选择(回溯)。 ### 思路 1:代码 ```python class Solution: def combinationSum3(self, k: int, n: int) -> List[List[int]]: res = [] # 存放所有符合条件结果的集合 path = [] # 存放当前符合条件的结果 def backtracking(start, path, current_sum): # 终止条件:选择的数字个数达到 k 个 if len(path) == k: if current_sum == n: # 如果和等于 n,找到一个有效组合 res.append(path[:]) # 将当前组合加入结果集 return # 剪枝:如果当前和已经大于 n,或者剩余数字无法凑够 k 个数字 if current_sum > n or len(path) + (9 - start + 1) < k: return # 枚举可选数字 for i in range(start, 10): # 从 start 到 9 path.append(i) # 选择数字 i backtracking(i + 1, path, current_sum + i) # 递归搜索 path.pop() # 撤销选择(回溯) backtracking(1, path, 0) # 从数字 1 开始搜索 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(C(9,k) \times k)$,其中 $C(9,k)$ 表示从 $9$ 个数字中选择 $k$ 个数字的组合数。最坏情况下需要遍历所有可能的组合,每个组合需要 $O(k)$ 的时间来构造。 - **空间复杂度**:$O(k)$,递归调用栈的深度最多为 $k$,每次递归需要 $O(1)$ 的额外空间。 ================================================ FILE: docs/solutions/0200-0299/contains-duplicate-ii.md ================================================ # [0219. 存在重复元素 II](https://leetcode.cn/problems/contains-duplicate-ii/) - 标签:数组、哈希表、滑动窗口 - 难度:简单 ## 题目链接 - [0219. 存在重复元素 II - 力扣](https://leetcode.cn/problems/contains-duplicate-ii/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 和一个整数 $k$。 **要求**:判断是否存在 $nums[i] == nums[j]$($i \ne j$),并且 $i$ 和 $j$ 的差绝对值至多为 $k$。 **说明**: - $1 \le nums.length \le 10^5$。 - $-10^9 <= nums[i] <= 10^9$。 - $0 \le k \le 10^5$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,1], k = 3 输出:True ``` ## 解题思路 ### 思路 1:哈希表 维护一个最多有 $k$ 个元素的哈希表。遍历 $nums$,对于数组中的每个整数 $nums[i]$,判断哈希表中是否存在这个整数。 - 如果存在,则说明出现了两次,且 $i \ne j$,直接返回 $True$。 - 如果不存在,则将 $nums[i]$ 加入哈希表。 - 判断哈希表长度是否超过了 $k$,如果超过了 $k$,则删除哈希表中最旧的元素 $nums[i - k]$。 - 如果遍历完仍旧找不到,则返回 $False$。 ### 思路 1:代码 ```python class Solution: def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool: nums_dict = dict() for i in range(len(nums)): if nums[i] in nums_dict: return True nums_dict[nums[i]] = 1 if len(nums_dict) > k: del nums_dict[nums[i - k]] return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0200-0299/contains-duplicate-iii.md ================================================ # [0220. 存在重复元素 III](https://leetcode.cn/problems/contains-duplicate-iii/) - 标签:数组、桶排序、有序集合、排序、滑动窗口 - 难度:中等 ## 题目链接 - [0220. 存在重复元素 III - 力扣](https://leetcode.cn/problems/contains-duplicate-iii/) ## 题目大意 **描述**:给定一个整数数组 $nums$,以及两个整数 $k$、$t$。 **要求**:判断数组中是否存在两个不同下标的 $i$ 和 $j$,其对应元素满足 $abs(nums[i] - nums[j]) \le t$,同时满足 $abs(i - j) \le k$。如果满足条件则返回 `True`,不满足条件返回 `False`。 **说明**: - $0 \le nums.length \le 2 \times 10^4$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 - $0 \le k \le 10^4$。 - $0 \le t \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,1], k = 3, t = 0 输出:True ``` - 示例 2: ```python 输入:nums = [1,0,1,1], k = 1, t = 2 输出:True ``` ## 解题思路 题目中需要满足两个要求,一个是元素值的要求($abs(nums[i] - nums[j]) \le t$) ,一个是下标范围的要求($abs(i - j) \le k$)。 对于任意一个位置 $i$ 来说,合适的 $j$ 应该在区间 $[i - k, i + k]$ 内,同时 $nums[j]$ 值应该在区间 $[nums[i] - t, nums[i] + t]$ 内。 最简单的做法是两重循环遍历数组,第一重循环遍历位置 $i$,第二重循环遍历 $[i - k, i + k]$ 的元素,判断是否满足 $abs(nums[i] - nums[j]) \le t$。但是这样做的时间复杂度为 $O(n \times k)$,其中 $n$ 是数组 $nums$ 的长度。 我们需要优化一下检测相邻 $2 \times k$ 个元素是否满足 $abs(nums[i] - nums[j]) \le t$ 的方法。有两种思路:「桶排序」和「滑动窗口(固定长度)」。 ### 思路 1:桶排序 1. 利用桶排序的思想,将桶的大小设置为 $t + 1$。只需要使用一重循环遍历位置 $i$,然后根据 $\lfloor \frac{nums[i]}{t + 1} \rfloor$,从而决定将 $nums[i]$ 放入哪个桶中。 2. 这样在同一个桶内各个元素之间的差值绝对值都小于等于 $t$。而相邻桶之间的元素,只需要校验一下两个桶之间的差值是否不超过 $t$。这样就可以以 $O(1)$ 的时间复杂度检测相邻 $2 \times k$ 个元素是否满足 $abs(nums[i] - nums[j]) \le t$。 3. 而 $abs(i - j) \le k$ 条件则可以通过在一重循环遍历时,将超出范围的 $nums[i - k]$ 从对应桶中删除,从而保证桶中元素一定满足 $abs(i - j) \le k$。 具体步骤如下: 1. 将每个桶的大小设置为 $t + 1$。我们将元素按照大小依次放入不同的桶中。 2. 遍历数组 $nums$ 中的元素,对于元素$ nums[i]$ : 1. 如果 $nums[i]$ 放入桶之前桶里已经有元素了,那么这两个元素必然满足 $abs(nums[i] - nums[j]) \le t$, 2. 如果之前桶里没有元素,那么就将 $nums[i]$ 放入对应桶中。 3. 再判断左右桶的左右两侧桶中是否有元素满足 $abs(nums[i] - nums[j]) <= t$。 4. 然后将 $nums[i - k]$ 之前的桶清空,因为这些桶中的元素与 $nums[i]$ 已经不满足 $abs(i - j) \le k$ 了。 3. 最后上述满足条件的情况就返回 `True`,最终遍历完仍不满足条件就返回 `False`。 ### 思路 1:代码 ```python class Solution: def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool: bucket_dict = dict() for i in range(len(nums)): # 将 nums[i] 划分到大小为 t + 1 的不同桶中 num = nums[i] // (t + 1) # 桶中已经有元素了 if num in bucket_dict: return True # 把 nums[i] 放入桶中 bucket_dict[num] = nums[i] # 判断左侧桶是否满足条件 if (num - 1) in bucket_dict and abs(bucket_dict[num - 1] - nums[i]) <= t: return True # 判断右侧桶是否满足条件 if (num + 1) in bucket_dict and abs(bucket_dict[num + 1] - nums[i]) <= t: return True # 将 i - k 之前的旧桶清除,因为之前的桶已经不满足条件了 if i >= k: bucket_dict.pop(nums[i - k] // (t + 1)) return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。$n$ 是给定数组长度。 - **空间复杂度**:$O(min(n, k))$。桶中最多包含 $min(n, k + 1)$ 个元素。 ### 思路 2:滑动窗口(固定长度) 1. 使用一个长度为 $k$ 的滑动窗口,每次遍历到 $nums[right]$ 时,滑动窗口内最多包含 $nums[right]$ 之前最多 $k$ 个元素。只需要检查前 $k$ 个元素是否在 $[nums[right] - t, nums[right] + t]$ 区间内即可。 2. 检查 $k$ 个元素是否在 $[nums[right] - t, nums[right] + t]$ 区间,可以借助保证有序的数据结构(比如 `SortedList`)+ 二分查找来解决,从而减少时间复杂度。 具体步骤如下: 1. 使用有序数组类 $window$ 维护一个长度为 $k$ 的窗口,满足数组内元素有序,且支持增加和删除操作。 2. $left$、$right$ 都指向序列的第一个元素。即:`left = 0`,`right = 0`。 3. 将当前元素填入窗口中,即 `window.add(nums[right])`。 4. 当窗口元素大于 $k$ 个时,即当 $right - left > k$ 时,移除窗口最左侧元素,并向右移动 $left$。 5. 当窗口元素小于等于 $k$ 个时: 1. 使用二分查找算法,查找 $nums[right]$ 在 $window$ 中的位置 $idx$。 2. 判断 $window[idx]$ 与相邻位置上元素差值绝对值,如果满足 $abs(window[idx] - window[idx - 1]) \le t$ 或者 $abs(window[idx + 1] - window[idx]) \le t$ 时返回 `True`。 6. 向右移动 $right$。 7. 重复 $3 \sim 6$ 步,直到 $right$ 到达数组末尾,如果还没找到满足条件的情况,则返回 `False`。 ### 思路 2:代码 ```python from sortedcontainers import SortedList class Solution: def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool: size = len(nums) window = SortedList() left, right = 0, 0 while right < size: window.add(nums[right]) if right - left > k: window.remove(nums[left]) left += 1 idx = bisect.bisect_left(window, nums[right]) if idx > 0 and abs(window[idx] - window[idx - 1]) <= t: return True if idx < len(window) - 1 and abs(window[idx + 1] - window[idx]) <= t: return True right += 1 return False ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times \log (min(n, k)))$。 - **空间复杂度**:$O(min(n, k))$。 ## 参考资料 - 【题解】[利用桶的原理O(n),Python3 - 存在重复元素 III - 力扣](https://leetcode.cn/problems/contains-duplicate-iii/solution/li-yong-tong-de-yuan-li-onpython3-by-zhou-pen-chen/) ================================================ FILE: docs/solutions/0200-0299/contains-duplicate.md ================================================ # [0217. 存在重复元素](https://leetcode.cn/problems/contains-duplicate/) - 标签:数组、哈希表、排序 - 难度:简单 ## 题目链接 - [0217. 存在重复元素 - 力扣](https://leetcode.cn/problems/contains-duplicate/) ## 题目大意 **描述**:给定一个整数数组 `nums`。 **要求**:判断是否存在重复元素。如果有元素在数组中出现至少两次,返回 `True`;否则返回 `False`。 **说明**: - $1 \le nums.length \le 10^5$。 - $-10^9 \le nums[i] \le 10^9$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,1] 输出:True ``` - 示例 2: ```python 输入:nums = [1,2,3,4] 输出:False ``` ## 解题思路 ### 思路 1:哈希表 - 使用一个哈希表存储元素和对应元素数量。 - 遍历元素,如果哈希表中出现了该元素,则直接输出 `True`。如果没有出现,则向哈希表中插入该元素。 - 如果遍历完也没发现重复元素,则输出 `False`。 ### 思路 1:代码 ```python class Solution: def containsDuplicate(self, nums: List[int]) -> bool: numDict = dict() for num in nums: if num in numDict: return True else: numDict[num] = num return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ### 思路 2:集合 - 使用一个 `set` 集合存储数组中所有元素。 - 如果集合中元素个数与数组元素个数不同,则说明出现了重复元素,返回 `True`。 - 如果集合中元素个数与数组元素个数相同,则说明没有出现了重复元素,返回 `False`。 ### 思路 2:集合代码 ```python class Solution: def containsDuplicate(self, nums: List[int]) -> bool: return len(set(nums)) != len(nums) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ### 思路 3:排序 - 对数组进行排序。 - 排序之后,遍历数组,判断相邻元素之间是否出现重复元素。 - 如果相邻元素相同,则说明出现了重复元素,返回 `True`。 - 如果遍历完也没发现重复元素,则输出 `False`。 ### 思路 3:排序代码 ```python class Solution: def containsDuplicate(self, nums: List[int]) -> bool: nums.sort() for i in range(1, len(nums)): if nums[i - 1] == nums[i]: return True return False ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/count-complete-tree-nodes.md ================================================ # [0222. 完全二叉树的节点个数](https://leetcode.cn/problems/count-complete-tree-nodes/) - 标签:树、深度优先搜索、二分查找、二叉树 - 难度:中等 ## 题目链接 - [0222. 完全二叉树的节点个数 - 力扣](https://leetcode.cn/problems/count-complete-tree-nodes/) ## 题目大意 给定一棵完全二叉树的根节点 `root`,返回该树的节点个数。 - 完全二叉树:除了最底层节点可能没有填满外,其余各层节点数都达到了最大值,并且最下面一层的节点都集中在盖层最左边的若干位置。如果最底层在第 `h` 层,则该层包含 $1 \sim 2^h$ 个节点。 ## 解题思路 根据题意可知公式:当前根节点的节点个数 = 左子树节点个数 + 右子树节点个数 + 1。 根据上述公式递归遍历左右子树节点,并返回左右子树节点数 + 1。 ## 代码 ```python class Solution: def countNodes(self, root: TreeNode) -> int: if not root: return 0 return 1 + self.countNodes(root.left) + self.countNodes(root.right) ``` ================================================ FILE: docs/solutions/0200-0299/count-primes.md ================================================ # [0204. 计数质数](https://leetcode.cn/problems/count-primes/) - 标签:数组、数学、枚举、数论 - 难度:中等 ## 题目链接 - [0204. 计数质数 - 力扣](https://leetcode.cn/problems/count-primes/) ## 题目大意 **描述**:给定 一个非负整数 $n$。 **要求**:统计小于 $n$ 的质数数量。 **说明**: - $0 \le n \le 5 * 10^6$。 **示例**: - 示例 1: ```python 输入 n = 10 输出 4 解释 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7。 ``` - 示例 2: ```python 输入:n = 1 输出:0 ``` ## 解题思路 ### 思路 1:枚举算法(超时) 对于小于 $n$ 的每一个数 $x$,我们可以枚举区间 $[2, x - 1]$ 上的数是否是 $x$ 的因数,即是否存在能被 $x$ 整数的数。如果存在,则该数 $x$ 不是质数。如果不存在,则该数 $x$ 是质数。 这样我们就可以通过枚举 $[2, n - 1]$ 上的所有数 $x$,并判断 $x$ 是否为质数。 在遍历枚举的同时,我们维护一个用于统计小于 $n$ 的质数数量的变量 `cnt`。如果符合要求,则将计数 `cnt` 加 $1$。最终返回该数目作为答案。 考虑到如果 $i$ 是 $x$ 的因数,则 $\frac{x}{i}$ 也必然是 $x$ 的因数,则我们只需要检验这两个因数中的较小数即可。而较小数一定会落在 $[2, \sqrt x]$ 上。因此我们在检验 $x$ 是否为质数时,只需要枚举 $[2, \sqrt x]$ 中的所有数即可。 利用枚举算法单次检查单个数的时间复杂度为 $O(\sqrt{n})$,检查 $n$ 个数的整体时间复杂度为 $O(n \sqrt{n})$。 ### 思路 1:代码 ```python class Solution: def isPrime(self, x): for i in range(2, int(pow(x, 0.5)) + 1): if x % i == 0: return False return True def countPrimes(self, n: int) -> int: cnt = 0 for x in range(2, n): if self.isPrime(x): cnt += 1 return cnt ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \sqrt{n})$。 - **空间复杂度**:$O(1)$。 ### 思路 2:埃氏筛法 可以用「埃氏筛」进行求解。这种方法是由古希腊数学家埃拉托斯尼斯提出的,具体步骤如下: - 使用长度为 $n$ 的数组 `is_prime` 来判断一个数是否是质数。如果 `is_prime[i] == True` ,则表示 $i$ 是质数,如果 `is_prime[i] == False`,则表示 $i$ 不是质数。并使用变量 `count` 标记质数个数。 - 然后从 $[2, n - 1]$ 的第一个质数(即数字 $2$) 开始,令 `count` 加 $1$,并将该质数在 $[2, n - 1]$ 范围内所有倍数(即 $4$、$6$、$8$、...)都标记为非质数。 - 然后根据数组 `is_prime` 中的信息,找到下一个没有标记为非质数的质数(即数字 $3$),令 `count` 加 $1$,然后将该质数在 $[2, n - 1]$ 范围内的所有倍数(即 $6$、$9$、$12$、…)都标记为非质数。 - 以此类推,直到所有小于或等于 $n - 1$ 的质数和质数的倍数都标记完毕时,输出 `count`。 优化:对于一个质数 $x$,我们可以直接从 $x \times x$ 开始标记,这是因为 $2 \times x$、$3 \times x$、… 这些数已经在 $x$ 之前就被其他数的倍数标记过了,例如 $2$ 的所有倍数、$3$ 的所有倍数等等。 ### 思路 2:代码 ```python class Solution: def countPrimes(self, n: int) -> int: is_prime = [True] * n count = 0 for i in range(2, n): if is_prime[i]: count += 1 for j in range(i * i, n, i): is_prime[j] = False return count ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times \log_2{log_2n})$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0200-0299/count-univalue-subtrees.md ================================================ # [0250. 统计同值子树](https://leetcode.cn/problems/count-univalue-subtrees/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0250. 统计同值子树 - 力扣](https://leetcode.cn/problems/count-univalue-subtrees/) ## 题目大意 **描述**: 给定一个二叉树。 **要求**: 统计该二叉树数值相同的「子树」个数。 **说明**: - 同值子树:指该子树的所有节点都拥有相同的数值。 - $树中节点的编号在 $[0, 10^{3}]$ 范围内。 - $-10^{3} \le Node.val \le 10^{3}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/08/21/unival_e1.jpg) ```python 输入:root = [5,1,5,5,5,null,5] 输出:4 ``` - 示例 2: ```python 输入:root = [] 输出:0 ``` ## 解题思路 ### 思路 1:深度优先搜索 这是一个典型的树遍历问题。我们需要统计所有同值子树的个数。 **算法步骤**: 1. **定义递归函数**:$is\_unival(node)$ 判断以 $node$ 为根的子树是否为同值子树,同时统计同值子树个数。 2. **递归终止条件**: - 如果 $node$ 为空,返回 $True$(空树视为同值子树)。 - 如果 $node$ 是叶子节点,返回 $True$(单个节点是同值子树)。 3. **递归处理**: - 递归检查左子树 $left\_is\_unival = is\_unival(node.left)$。 - 递归检查右子树 $right\_is\_unival = is\_unival(node.right)$。 - 如果左右子树都是同值子树,且它们的值都等于当前节点值,则当前子树也是同值子树。 4. **统计计数**:如果当前子树是同值子树,则计数器 $count$ 加 $1$。 **关键点**: - 使用后序遍历(左右根)确保先处理子节点再处理父节点。 - 空树和叶子节点都视为同值子树。 - 需要同时检查子树是否为同值以及值是否相等。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def countUnivalSubtrees(self, root: Optional[TreeNode]) -> int: self.count = 0 # 统计同值子树个数 def is_unival(node): """ 判断以 node 为根的子树是否为同值子树 返回: (是否为同值子树, 子树的值) """ # 空节点视为同值子树 if not node: return True, None # 叶子节点是同值子树 if not node.left and not node.right: self.count += 1 return True, node.val # 递归检查左右子树 left_is_unival, left_val = is_unival(node.left) right_is_unival, right_val = is_unival(node.right) # 判断当前子树是否为同值子树 is_current_unival = True # 如果左子树存在且不是同值子树,或者值不相等 if node.left and (not left_is_unival or left_val != node.val): is_current_unival = False # 如果右子树存在且不是同值子树,或者值不相等 if node.right and (not right_is_unival or right_val != node.val): is_current_unival = False # 如果当前子树是同值子树,计数加1 if is_current_unival: self.count += 1 return is_current_unival, node.val is_unival(root) return self.count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树中节点的个数。每个节点都会被访问一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度。递归调用栈的深度等于树的高度,最坏情况下(树退化为链表)为 $O(n)$。 ================================================ FILE: docs/solutions/0200-0299/course-schedule-ii.md ================================================ # [0210. 课程表 II](https://leetcode.cn/problems/course-schedule-ii/) - 标签:深度优先搜索、广度优先搜索、图、拓扑排序 - 难度:中等 ## 题目链接 - [0210. 课程表 II - 力扣](https://leetcode.cn/problems/course-schedule-ii/) ## 题目大意 **描述**:给定一个整数 $numCourses$,代表这学期必须选修的课程数量,课程编号为 $0 \sim numCourses - 1$。再给定一个数组 $prerequisites$ 表示先修课程关系,其中 $prerequisites[i] = [ai, bi]$ 表示如果要学习课程 $ai$ 则必须要先完成课程 $bi$。 **要求**:返回学完所有课程所安排的学习顺序。如果有多个正确的顺序,只要返回其中一种即可。如果无法完成所有课程,则返回空数组。 **说明**: - $1 \le numCourses \le 2000$。 - $0 \le prerequisites.length \le numCourses \times (numCourses - 1)$。 - $prerequisites[i].length == 2$。 - $0 \le ai, bi < numCourses$。 - $ai \ne bi$。 - 所有$[ai, bi]$ 互不相同。 **示例**: - 示例 1: ```python 输入:numCourses = 2, prerequisites = [[1,0]] 输出:[0,1] 解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1]。 ``` - 示例 2: ```python 输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] 输出:[0,2,1,3] 解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。 因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3]。 ``` ## 解题思路 ### 思路 1:拓扑排序 这道题是「[0207. 课程表](https://leetcode.cn/problems/course-schedule/)」的升级版,只需要在上一题的基础上增加一个答案数组 $order$ 即可。 1. 使用哈希表 $graph$ 存放课程关系图,并统计每门课程节点的入度,存入入度列表 $indegrees$。 2. 借助队列 $S$,将所有入度为 $0$ 的节点入队。 3. 从队列中选择一个节点 $u$,并将其加入到答案数组 $order$ 中。 4. 从图中删除该顶点 $u$,并且删除从该顶点出发的有向边 $$(也就是把该顶点可达的顶点入度都减 $1$)。如果删除该边后顶点 $v$ 的入度变为 $0$,则将其加入队列 $S$ 中。 5. 重复上述步骤 $3 \sim 4$,直到队列中没有节点。 6. 最后判断总的顶点数和拓扑序列中的顶点数是否相等,如果相等,则返回答案数组 $order$,否则,返回空数组。 ### 思路 1:代码 ```python import collections class Solution: # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) def topologicalSortingKahn(self, graph: dict): indegrees = {u: 0 for u in graph} # indegrees 用于记录所有顶点入度 for u in graph: for v in graph[u]: indegrees[v] += 1 # 统计所有顶点入度 # 将入度为 0 的顶点存入集合 S 中 S = collections.deque([u for u in indegrees if indegrees[u] == 0]) order = [] # order 用于存储拓扑序列 while S: u = S.pop() # 从集合中选择一个没有前驱的顶点 0 order.append(u) # 将其输出到拓扑序列 order 中 for v in graph[u]: # 遍历顶点 u 的邻接顶点 v indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 S.append(v) # 将其放入集合 S 中 if len(indegrees) != len(order): # 还有顶点未遍历(存在环),无法构成拓扑序列 return [] return order # 返回拓扑序列 def findOrder(self, numCourses: int, prerequisites): graph = dict() for i in range(numCourses): graph[i] = [] for v, u in prerequisites: graph[u].append(v) return self.topologicalSortingKahn(graph) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 为课程数,$m$ 为先修课程的要求数。 - **空间复杂度**:$O(n + m)$。 ================================================ FILE: docs/solutions/0200-0299/course-schedule.md ================================================ # [0207. 课程表](https://leetcode.cn/problems/course-schedule/) - 标签:深度优先搜索、广度优先搜索、图、拓扑排序 - 难度:中等 ## 题目链接 - [0207. 课程表 - 力扣](https://leetcode.cn/problems/course-schedule/) ## 题目大意 **描述**:给定一个整数 $numCourses$,代表这学期必须选修的课程数量,课程编号为 $0 \sim numCourses - 1$。再给定一个数组 $prerequisites$ 表示先修课程关系,其中 $prerequisites[i] = [ai, bi]$ 表示如果要学习课程 $ai$ 则必须要先完成课程 $bi$。 **要求**:判断是否可能完成所有课程的学习。如果可以,返回 `True`,否则,返回 `False`。 **说明**: - $1 \le numCourses \le 10^5$。 - $0 \le prerequisites.length \le 5000$。 - $prerequisites[i].length == 2$。 - $0 \le ai, bi < numCourses$。 - $prerequisites[i]$ 中所有课程对互不相同。 **示例**: - 示例 1: ```python 输入:numCourses = 2, prerequisites = [[1,0]] 输出:true 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。这是可能的。 ``` - 示例 2: ```python 输入:numCourses = 2, prerequisites = [[1,0],[0,1]] 输出:false 解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。 ``` ## 解题思路 ### 思路 1:拓扑排序 1. 使用哈希表 $graph$ 存放课程关系图,并统计每门课程节点的入度,存入入度列表 $indegrees$。 2. 借助队列 $S$,将所有入度为 $0$ 的节点入队。 3. 从队列中选择一个节点 $u$,并令课程数减 $1$。 4. 从图中删除该顶点 $u$,并且删除从该顶点出发的有向边 $$(也就是把该顶点可达的顶点入度都减 $1$)。如果删除该边后顶点 $v$ 的入度变为 $0$,则将其加入队列 $S$ 中。 5. 重复上述步骤 $3 \sim 4$,直到队列中没有节点。 6. 最后判断剩余课程数是否为 $0$,如果为 $0$,则返回 `True`,否则,返回 `False`。 ### 思路 1:代码 ```python import collections class Solution: def topologicalSorting(self, numCourses, graph): indegrees = {u: 0 for u in graph} for u in graph: for v in graph[u]: indegrees[v] += 1 S = collections.deque([u for u in indegrees if indegrees[u] == 0]) while S: u = S.pop() numCourses -= 1 for v in graph[u]: indegrees[v] -= 1 if indegrees[v] == 0: S.append(v) if numCourses == 0: return True return False def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: graph = dict() for i in range(numCourses): graph[i] = [] for v, u in prerequisites: graph[u].append(v) return self.topologicalSorting(numCourses, graph) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 为课程数,$m$ 为先修课程的要求数。 - **空间复杂度**:$O(n + m)$。 ================================================ FILE: docs/solutions/0200-0299/delete-node-in-a-linked-list.md ================================================ # [0237. 删除链表中的节点](https://leetcode.cn/problems/delete-node-in-a-linked-list/) - 标签:链表 - 难度:中等 ## 题目链接 - [0237. 删除链表中的节点 - 力扣](https://leetcode.cn/problems/delete-node-in-a-linked-list/) ## 题目大意 删除链表的给定节点。 ## 解题思路 直接将该节点的后续节点覆盖该节点即可。即让该节点的值等于下一节点值,并让其 next 指针指向下一节点的下一节点。 ## 代码 ```python class Solution: def deleteNode(self, node): node.val = node.next.val node.next = node.next.next ``` ================================================ FILE: docs/solutions/0200-0299/design-add-and-search-words-data-structure.md ================================================ # [0211. 添加与搜索单词 - 数据结构设计](https://leetcode.cn/problems/design-add-and-search-words-data-structure/) - 标签:深度优先搜索、设计、字典树、字符串 - 难度:中等 ## 题目链接 - [0211. 添加与搜索单词 - 数据结构设计 - 力扣](https://leetcode.cn/problems/design-add-and-search-words-data-structure/) ## 题目大意 **要求**:设计一个数据结构,支持「添加新单词」和「查找字符串是否与任何先前添加的字符串匹配」。 实现词典类 WordDictionary: - `WordDictionary()` 初始化词典对象。 - `void addWord(word)` 将 `word` 添加到数据结构中,之后可以对它进行匹配 - `bool search(word)` 如果数据结构中存在字符串与 `word` 匹配,则返回 `True`;否则,返回 `False`。`word` 中可能包含一些 `.`,每个 `.` 都可以表示任何一个字母。 **说明**: - $1 \le word.length \le 25$。 - `addWord` 中的 `word` 由小写英文字母组成。 - `search` 中的 `word` 由 `'.'` 或小写英文字母组成。 - 最多调用 $10^4$ 次 `addWord` 和 `search`。 **示例**: - 示例 1: ```python 输入: ["WordDictionary","addWord","addWord","addWord","search","search","search","search"] [[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]] 输出: [null,null,null,null,false,true,true,true] 解释: WordDictionary wordDictionary = new WordDictionary(); wordDictionary.addWord("bad"); wordDictionary.addWord("dad"); wordDictionary.addWord("mad"); wordDictionary.search("pad"); // 返回 False wordDictionary.search("bad"); // 返回 True wordDictionary.search(".ad"); // 返回 True wordDictionary.search("b.."); // 返回 True ``` ## 解题思路 ### 思路 1:字典树 使用前缀树(字典树)。具体做法如下: - 初始化词典对象时,构造一棵字典树。 - 添加 `word` 时,将 `word` 插入到字典树中。 - 搜索 `word` 时: - 如果遇到 `.`,则递归匹配当前节点所有子节点,并依次向下查找。匹配到了,则返回 `True`,否则返回 `False`。 - 如果遇到其他小写字母,则按 `word` 顺序匹配节点。 - 如果当前节点为 `word` 的结尾,则放回 `True`。 ### 思路 1:代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ def dfs(index, node) -> bool: if index == len(word): return node.isEnd ch = word[index] if ch == '.': for child in node.children.values(): if child is not None and dfs(index + 1, child): return True else: if ch not in node.children: return False child = node.children[ch] if child is not None and dfs(index + 1, child): return True return False return dfs(0, self) class WordDictionary: def __init__(self): self.trie_tree = Trie() def addWord(self, word: str) -> None: self.trie_tree.insert(word) def search(self, word: str) -> bool: return self.trie_tree.search(word) ``` ### 思路 1:复杂度分析 - **时间复杂度**:初始化操作为 $O(1)$。添加单词为 $O(|S|)$,搜索单词的平均时间复杂度为 $O(|S|)$,最坏情况下所有字符都是 `'.'`,所以最坏时间复杂度为 $O(|S|^\sum)$。其中 $|S|$ 为单词长度,$\sum$ 为字符集的大小,此处为 $26$。 - **空间复杂度**:$O(|T| * n)$。其中 $|T|$ 为所有添加单词的最大长度,$n$ 为添加字符串个数。 ================================================ FILE: docs/solutions/0200-0299/different-ways-to-add-parentheses.md ================================================ # [0241. 为运算表达式设计优先级](https://leetcode.cn/problems/different-ways-to-add-parentheses/) - 标签:递归、记忆化搜索、数学、字符串、动态规划 - 难度:中等 ## 题目链接 - [0241. 为运算表达式设计优先级 - 力扣](https://leetcode.cn/problems/different-ways-to-add-parentheses/) ## 题目大意 **描述**:给定一个由数字和运算符组成的字符串 `expression`。 **要求**:按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以按任意顺序返回答案。 **说明**: - 生成的测试用例满足其对应输出值符合 $32$ 位整数范围,不同结果的数量不超过 $10^4$。 - $1 \le expression.length \le 20$。 - `expression` 由数字和算符 `'+'`、`'-'` 和 `'*'` 组成。 - 输入表达式中的所有整数值在范围 $[0, 99]$。 **示例**: - 示例 1: ```python 输入:expression = "2-1-1" 输出:[0,2] 解释: ((2-1)-1) = 0 (2-(1-1)) = 2 ``` - 示例 2: ```python 输入:expression = "2*3-4*5" 输出:[-34,-14,-10,-10,10] 解释: (2*(3-(4*5))) = -34 ((2*3)-(4*5)) = -14 ((2*(3-4))*5) = -10 (2*((3-4)*5)) = -10 (((2*3)-4)*5) = 10 ``` ## 解题思路 ### 思路 1:分治算法 给定的字符串 `expression` 只包含有数字和字符,可以写成类似 `x op y` 的形式,其中 $x$、$y$ 为表达式或数字,$op$ 为字符。 则我们可以根据字符的位置,将其递归分解为 $x$、$y$ 两个部分,接着分别计算 $x$ 部分的结果与 $y$ 部分的结果。然后再将其合并。 ### 思路 1:代码 ```python class Solution: def diffWaysToCompute(self, expression: str) -> List[int]: res = [] if len(expression) <= 2: res.append(int(expression)) return res for i in range(len(expression)): ch = expression[i] if ch == '+' or ch == '-' or ch == '*': left_cnts = self.diffWaysToCompute(expression[ :i]) right_cnts = self.diffWaysToCompute(expression[i + 1:]) for left in left_cnts: for right in right_cnts: if ch == '+': res.append(left + right) elif ch == '-': res.append(left - right) else: res.append(left * right) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(C_n)$,其中 $n$ 为结果数组的大小,$C_n$ 是第 $n$ 个卡特兰数。 - **空间复杂度**:$O(C_n)$。 ================================================ FILE: docs/solutions/0200-0299/encode-and-decode-strings.md ================================================ # [0271. 字符串的编码与解码](https://leetcode.cn/problems/encode-and-decode-strings/) - 标签:设计、数组、字符串 - 难度:中等 ## 题目链接 - [0271. 字符串的编码与解码 - 力扣](https://leetcode.cn/problems/encode-and-decode-strings/) ## 题目大意 **要求**: 设计一个算法,可以将一个「字符串列表」编码成为一个「字符串」。这个编码后的字符串是可以通过网络进行高效传送的,并且可以在接收端被解码回原来的字符串列表。 1 号机(发送方)有如下函数: ```C++ string encode(vector strs) { // ... your code return encoded_string; } ``` 2 号机(接收方)有如下函数: ```C++ vector decode(string s) { //... your code return strs; } ``` 1 号机(发送方)执行: ```C++ string encoded_string = encode(strs); ``` 2 号机(接收方)执行: ```C++ vector strs2 = decode(encoded_string); ``` 此时,2 号机(接收方)的 $strs2$ 需要和 1 号机(发送方)的 $strs$ 相同。 请你来实现这个 $encode$ 和 $decode$ 方法。 不允许使用任何序列化方法解决这个问题(例如 eval)。 **说明**: - $1 \le strs.length \le 200$。 - $0 \le strs[i].length \le 200$。 - $strs[i]$ 包含 $256$ 个有效 ASCII 字符中的任何可能字符。 - 进阶:你能编写一个通用算法来处理任何可能的字符集吗? **示例**: - 示例 1: ```python 输入:dummy_input = ["Hello","World"] 输出:["Hello","World"] 解释: 1 号机: Codec encoder = new Codec(); String msg = encoder.encode(strs); Machine 1 ---msg---> Machine 2 2 号机: Codec decoder = new Codec(); String[] strs = decoder.decode(msg); ``` - 示例 2: ```python 输入:dummy_input = [""] 输出:[""] ``` ## 解题思路 ### 思路 1:长度前缀编码 这是一个典型的设计问题。我们需要将字符串列表编码成单个字符串,然后能够正确解码。 **核心问题**:如何区分不同的字符串?由于字符串可能包含任何 ASCII 字符,包括分隔符,我们需要一种方法来明确标识每个字符串的边界。 **解决方案**:使用长度前缀编码。对于每个字符串 $str_i$,我们将其长度 $len_i$ 和字符串内容连接起来,格式为 $len_i + ":" + str_i$。 **算法步骤**: 1. **编码过程**: - 遍历字符串列表 $strs$。 - 对于每个字符串 $str_i$,计算其长度 $len_i$。 - 将 $len_i + ":" + str_i$ 添加到结果字符串中。 2. **解码过程**: - 使用指针 $i$ 遍历编码后的字符串。 - 找到冒号 `:` 的位置,提取长度信息 $len_i$。 - 根据长度 $len_i$ 提取对应的字符串内容。 - 将提取的字符串加入结果列表,继续处理下一个字符串。 **关键点**: - 冒号 `:` 作为长度和内容的分隔符。 - 长度信息确保我们能够准确提取每个字符串。 - 这种方法可以处理包含任何 ASCII 字符的字符串。 ### 思路 1:代码 ```python class Codec: def encode(self, strs: List[str]) -> str: """Encodes a list of strings to a single string. Args: strs: 字符串列表 Returns: 编码后的字符串 """ encoded = "" for s in strs: # 将每个字符串的长度和内容用冒号分隔 encoded += str(len(s)) + ":" + s return encoded def decode(self, s: str) -> List[str]: """Decodes a single string to a list of strings. Args: s: 编码后的字符串 Returns: 解码后的字符串列表 """ decoded = [] i = 0 while i < len(s): # 找到冒号的位置,提取长度信息 colon_pos = s.find(":", i) length = int(s[i:colon_pos]) # 根据长度提取字符串内容 start = colon_pos + 1 end = start + length decoded.append(s[start:end]) # 更新指针位置 i = end return decoded # Your Codec object will be instantiated and called as such: # codec = Codec() # codec.decode(codec.encode(strs)) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - 编码:$O(n)$,其中 $n$ 是所有字符串的总长度。 - 解码:$O(n)$,需要遍历整个编码字符串一次。 - **空间复杂度**:$O(n)$,存储编码后的字符串。 ================================================ FILE: docs/solutions/0200-0299/expression-add-operators.md ================================================ # [0282. 给表达式添加运算符](https://leetcode.cn/problems/expression-add-operators/) - 标签:数学、字符串、回溯 - 难度:困难 ## 题目链接 - [0282. 给表达式添加运算符 - 力扣](https://leetcode.cn/problems/expression-add-operators/) ## 题目大意 **描述**: 给定一个仅包含数字 $0 \sim 9$ 的字符串 $num$ 和一个目标值整数 $target$ ,在 $num$ 的数字之间添加「二元」运算符(不是一元)`+`、`-` 或 `*`。 **要求**: 返回所有能够得到 $target$ 的表达式。 **说明**: - 注意:返回表达式中的操作数不应该包含前导零。 - 注意:一个数字可以包含多个数位。 - $1 \le num.length \le 10$。 - $num$ 仅含数字。 - $-2^{31} \le target \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入: num = "123", target = 6 输出: ["1+2+3", "1*2*3"] 解释: “1*2*3” 和 “1+2+3” 的值都是6。 ``` - 示例 2: ```python 输入: num = "232", target = 8 输出: ["2*3+2", "2+3*2"] 解释: “2*3+2” 和 “2+3*2” 的值都是8。 ``` ## 解题思路 ### 思路 1:回溯算法 使用回溯算法枚举所有可能的表达式,在每两个数字之间选择插入 `+`、`-`、`*` 或不切分继续拼接数位,并在构造过程中实时计算表达式值以实现剪枝。 **核心思想**: 1. **状态维护**:维护两个关键变量。 - $cur$:当前表达式已计算的值。 - $last$:最近一次加入表达式且尚未「结算」的乘法因子。 2. **运算符处理**: - 加法 `+x`:$cur' = cur + x$,$last' = x$。 - 减法 `-x`:$cur' = cur - x$,$last' = -x$。 - 乘法 `*x`:由于乘法优先级高,需要撤回之前的 $last$,再计算新的值: $cur' = cur - last + last \times x$,$last' = last \times x$。 **算法步骤**: 1. **初始化**:从位置 $i = 0$ 开始,选择第一个数字作为起点。 2. **枚举切分**:从当前位置 $i$ 开始,枚举所有可能的数字段 $num[i..j]$。 3. **前导零处理**:如果 $num[i] = '0'$ 且 $j > i$,则跳过(避免前导零)。 4. **运算符选择**: - 如果是第一个数字段,直接递归处理。 - 否则尝试三种运算符:`+`、`-`、`*`。 5. **递归搜索**:更新状态后递归处理剩余部分。 6. **结果收集**:当处理完所有数字且 $cur = target$ 时,记录当前表达式。 **关键细节**: - 起始位置不能放置运算符,只能选择数字段。 - 前导零约束:以 `'0'` 开头的数字段只能取单个 `'0'`。 - 乘法优先级处理:通过维护 $last$ 变量正确处理乘法的结合性。 ### 思路 1:代码 ```python class Solution: def addOperators(self, num: str, target: int) -> List[str]: # 回溯 + 逐步结算表达式值,使用 last 处理乘法优先级 n = len(num) ans = [] def dfs(i: int, path: str, cur: int, last: int) -> None: # i: 当前处理到 num 的下标 # path: 当前构造的表达式字符串 # cur: 当前表达式已结算的值 # last: 最近一次加入表达式、但在乘法优先级下需要与后续数相乘的因子 if i == n: if cur == target: ans.append(path) return # 从位置 i 开始枚举下一个操作数 num[i:j] # 为避免前导零,如果 num[i] == '0',则只能取单个 '0' val = 0 for j in range(i, n): # 前导零剪枝 if j > i and num[i] == '0': break # 将 num[i..j] 解析为整数 val val = val * 10 + (ord(num[j]) - ord('0')) s = num[i:j + 1] if i == 0: # 表达式起点,不能放运算符 dfs(j + 1, s, val, val) else: # 加号 dfs(j + 1, path + '+' + s, cur + val, val) # 减号 dfs(j + 1, path + '-' + s, cur - val, -val) # 乘号:先撤回 last,再乘 val,加回去 dfs(j + 1, path + '*' + s, cur - last + last * val, last * val) dfs(0, '', 0, 0) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(3^{n-1} \times n)$。共有 $n-1$ 个插入位,每处最多 $3$ 种运算符选择,且每次切分还包含将子串转整数与字符串拼接的 $O(n)$ 开销。 - **空间复杂度**:$O(n)$。递归深度与表达式长度同阶,路径与调用栈消耗线性空间。 ================================================ FILE: docs/solutions/0200-0299/factor-combinations.md ================================================ # [0254. 因子的组合](https://leetcode.cn/problems/factor-combinations/) - 标签:回溯 - 难度:中等 ## 题目链接 - [0254. 因子的组合 - 力扣](https://leetcode.cn/problems/factor-combinations/) ## 题目大意 **描述**: 整数可以被看作是其因子的乘积。 例如:$8 = 2 \times 2 \times 2 = 2 \times 4$. **要求**: 实现一个函数,该函数接收一个整数 $n$ 并返回该整数所有的因子组合。 **说明**: - 可以假定 $n$ 为永远为正数。 - 因子必须大于 $1$ 并且小于 $n$。 - $1 \le n \le 10^{7}$。 **示例**: - 示例 1: ```python 输入: 1 输出: [] ``` - 示例 2: ```python 输入: 12 输出: [ [2, 6], [2, 2, 3], [3, 4] ] ``` ## 解题思路 ### 思路 1: 用回溯(DFS)枚举所有按非递减顺序排列的因子组合。核心思想:当我们已经选择了若干因子组成序列 \( path \),且当前剩余待分解的值为 \( n' \),那么下一步只需要从上一个因子 \( start \) 开始,尝试所有 \( i \in [start, \lfloor\sqrt{n'}\rfloor] \)。一旦 \( i \mid n' \): - 记录组合 \( path + [i, \tfrac{n'}{i}] \)(表示当前选择 \( i \) 后,最后一个因子直接取 \( n'/i \))。 - 继续递归分解 \( n'/i \),并将 \( i \) 加入路径,即递归状态为 \( (n'/i,\; start=i,\; path+[i]) \)。 通过将搜索上界限制为 \( \lfloor\sqrt{n'}\rfloor \) 并使用 \( start \) 保持非递减顺序,可以避免重复组合,且有效剪枝,防止超时。 ### 思路 1:代码 ```python from typing import List class Solution: def getFactors(self, n: int) -> List[List[int]]: # 回溯搜索:在保持非递减顺序的前提下,枚举 n 的因子组合 results: List[List[int]] = [] def dfs(remain: int, start: int, path: List[int]) -> None: # 只需要尝试到 sqrt(remain),其余因子由成对的大因子补齐 i = start # 使用整数平方根作为上界,减少无效枚举 upper = int(remain ** 0.5) while i <= upper: if remain % i == 0: # 形成一组合法解:path + [i, remain // i] results.append(path + [i, remain // i]) # 继续分解 remain // i,保证非递减(起点仍为 i) dfs(remain // i, i, path + [i]) i += 1 # 按题意,因子需严格在 (1, n) 内,n <= 3 时无解 if n <= 3: return results dfs(n, 2, []) return results ``` ### 思路 1:复杂度分析 - **时间复杂度**:输出敏感(output-sensitive)。搜索过程中,每个状态仅尝试到 \( \sqrt{n'} \) 的因子,并通过 \( start \) 约束为非递减顺序,极大减少重复。总体上与因子分解树的规模相关,上界可视为与 \( n \) 的素因子个数指数相关;平均情况下远小于枚举 \( 2^{\log n} \) 的粗略上界。 - **空间复杂度**:\( O(k) \),其中 \( k \) 为结果中最长组合的长度(等于 \( n \) 的素因子重数之和),用于递归栈与路径存储;结果集另算。 ================================================ FILE: docs/solutions/0200-0299/find-median-from-data-stream.md ================================================ # [0295. 数据流的中位数](https://leetcode.cn/problems/find-median-from-data-stream/) - 标签:设计、双指针、数据流、排序、堆(优先队列) - 难度:困难 ## 题目链接 - [0295. 数据流的中位数 - 力扣](https://leetcode.cn/problems/find-median-from-data-stream/) ## 题目大意 要求:设计一个支持一下两种操作的数组结构: - `void addNum(int num)`:从数据流中添加一个整数到数据结构中。 - `double findMedian()`:返回目前所有元素的中位数。 ## 解题思路 使用一个大顶堆 `queMax` 记录大于中位数的数,使用一个小顶堆 `queMin` 小于中位数的数。 - 当添加元素数量为偶数: `queMin` 和 `queMax` 中元素数量相同,则中位数为它们队头的平均值。 - 当添加元素数量为奇数:`queMin` 中的数比 `queMax` 多一个,此时中位数为 `queMin` 的队头。 为了满足上述条件,在进行 `addNum` 操作时,我们应当分情况处理: - `num > max{queMin}`:此时 `num` 大于中位数,将该数添加到大顶堆 `queMax` 中。新的中位数将大于原来的中位数,所以可能需要将 `queMax` 中的最小数移动到 `queMin` 中。 - `num ≤ max{queMin}`:此时 `num` 小于中位数,将该数添加到小顶堆 `queMin` 中。新的中位数将小于等于原来的中位数,所以可能需要将 `queMin` 中最大数移动到 `queMax` 中。 ## 代码 ```python import heapq class MedianFinder: def __init__(self): """ initialize your data structure here. """ self.queMin = list() self.queMax = list() def addNum(self, num: int) -> None: if not self.queMin or num < -self.queMin[0]: heapq.heappush(self.queMin, -num) if len(self.queMax) + 1 < len(self.queMin): heapq.heappush(self.queMax, -heapq.heappop(self.queMin)) else: heapq.heappush(self.queMax, num) if len(self.queMax) > len(self.queMin): heapq.heappush(self.queMin, -heapq.heappop(self.queMax)) def findMedian(self) -> float: if len(self.queMin) > len(self.queMax): return -self.queMin[0] return (-self.queMin[0] + self.queMax[0]) / 2 ``` ================================================ FILE: docs/solutions/0200-0299/find-the-celebrity.md ================================================ # [0277. 搜寻名人](https://leetcode.cn/problems/find-the-celebrity/) - 标签:图、双指针、交互 - 难度:中等 ## 题目链接 - [0277. 搜寻名人 - 力扣](https://leetcode.cn/problems/find-the-celebrity/) ## 题目大意 **描述**: 假设你是一个专业的狗仔,参加了一个 $n$ 人派对,其中每个人被从 $0$ 到 $n - 1$ 标号。在这个派对人群当中可能存在一位「名人」。所谓「名人」的定义是:其他所有 $n - 1$ 个人都认识他 / 她,而他 / 她并不认识其他任何人。 现在你想要确认这个「名人」是谁,或者确定这里没有「名人」。而你唯一能做的就是问诸如「A 你好呀,请问你认不认识 B 呀?」的问题,以确定 A 是否认识 B。你需要在(渐近意义上)尽可能少的问题内来确定这位「名人」是谁(或者确定这里没有 “名人”)。 给定整数 $n$ 和一个辅助函数 `bool knows(a, b)` 用来获取 a 是否认识 b。 **要求**: 实现一个函数 `int findCelebrity(n)`。派对最多只会有一个「名人」参加。 若「名人」存在,请返回他 / 她的编号;若「名人」不存在,请返回 $-1$。 **说明**: - 注意:$n \times n$ 的二维数组 $graph$ 给定的输入并不是直接提供给你的,而是只能通过辅助函数 `knows` 获取。$graph[i][j] == 1$ 表示 $i$ 认识 $j$,而 $graph[i][j] == 0$ 表示 $j$ 不认识 $i$。 - $n == graph.length == graph[i].length$。 - $2 \le n \le 10^{3}$。 - $graph[i][j]$ 是 $0$ 或 $1$。 - $graph[i][i] == 1$。 - 进阶:如果允许调用 API `knows` 的最大次数为 $3 \times n$ ,你可以设计一个不超过最大调用次数的解决方案吗? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2022/01/19/g1.jpg) ```python 输入: graph = [[1,1,0],[0,1,0],[1,1,1]] 输出: 1 解释: 有编号分别为 0、1 和 2 的三个人。graph[i][j] = 1 代表编号为 i 的人认识编号为 j 的人,而 graph[i][j] = 0 则代表编号为 i 的人不认识编号为 j 的人。“名人” 是编号 1 的人,因为 0 和 2 均认识他/她,但 1 不认识任何人。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2022/01/19/g2.jpg) ```python 输入: graph = [[1,0,1],[1,1,0],[0,1,1]] 输出: -1 解释: 没有 “名人” ``` ## 解题思路 ### 思路 1: 利用「候选人淘汰法」在线性次数调用 `knows` 找到唯一可能的名人候选人,再做一次线性校验。 核心观察:若第一个人选为候选人 $cand$,当我们考察另一个人 $i$ 时: - 如果 `knows(cand, i)` 为真,则 $cand$ 不可能是名人(名人不认识任何人),把候选人更新为 $i$。 - 若 `knows(cand, i)` 为假,则 $i$ 不可能是名人(名人需要被所有人认识,但 $cand$ 就不认识 $i$),候选人保持不变。 一次线性扫描后,剩下的 $cand$ 是唯一可能的名人。再用一轮检查验证:对所有 $j \neq cand$,必须满足 `knows(cand, j) == False` 且 `knows(j, cand) == True`,否则不存在名人。 这保证了调用次数为 $O(n)$ 量级,不会超时。 ### 思路 1:代码 ```python # The knows API is already defined for you. # return a bool, whether a knows b # def knows(a: int, b: int) -> bool: class Solution: def findCelebrity(self, n: int) -> int: # 第一阶段:线性淘汰,确定唯一候选人 cand cand = 0 for i in range(1, n): if knows(cand, i): # cand 认识 i,cand 不可能是名人 cand = i else: # cand 不认识 i,i 不可能是名人,cand 保持 pass # 第二阶段:校验 cand 是否为真正名人 for j in range(n): if j == cand: continue # 名人不认识任何人,且所有人都认识名人 if knows(cand, j) or not knows(j, cand): return -1 return cand ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。第一阶段线性淘汰 $n-1$ 次,第二阶段线性校验 $n-1$ 次,总体为线性。 - **空间复杂度**:$O(1)$。仅使用常数个辅助变量。 ================================================ FILE: docs/solutions/0200-0299/find-the-duplicate-number.md ================================================ # [0287. 寻找重复数](https://leetcode.cn/problems/find-the-duplicate-number/) - 标签:位运算、数组、双指针、二分查找 - 难度:中等 ## 题目链接 - [0287. 寻找重复数 - 力扣](https://leetcode.cn/problems/find-the-duplicate-number/) ## 题目大意 **描述**:给定一个包含 $n + 1$ 个整数的数组 $nums$,里边包含的值都在 $1 \sim n$ 之间。可知至少存在一个重复的整数。 **要求**:假设 $nums$ 中只存在一个重复的整数,要求找出这个重复的数。 **说明**: - $1 \le n \le 10^5$。 - $nums.length == n + 1$。 - $1 \le nums[i] \le n$。 - 要求使用空间复杂度为常数级 $O(1)$,时间复杂度小于 $O(n^2)$ 的解决方法。 **示例**: - 示例 1: ```python 输入:nums = [1,3,4,2,2] 输出:2 ``` - 示例 2: ```python 输入:nums = [3,1,3,4,2] 输出:3 ``` ## 解题思路 ### 思路 1:二分查找 利用二分查找的思想。 1. 使用两个指针 $left$,$right$。$left$ 指向 $1$,$right$ 指向 $n$。 2. 将区间 $[1, n]$ 分为 $[left, mid]$ 和 $[mid + 1, right]$。 3. 对于中间数 $mid$,统计 $nums$ 中小于等于 $mid$ 的数个数 $cnt$。 4. 如果 $cnt \le mid$,则重复数一定不会出现在左侧区间,那么从右侧区间开始搜索。 5. 如果 $cut > mid$,则重复数出现在左侧区间,则从左侧区间开始搜索。 ### 思路 1:代码 ```python class Solution: def findDuplicate(self, nums: List[int]) -> int: n = len(nums) left = 1 right = n - 1 while left < right: mid = left + (right - left) // 2 cnt = 0 for num in nums: if num <= mid: cnt += 1 if cnt <= mid: left = mid + 1 else: right = mid return left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/first-bad-version.md ================================================ # [0278. 第一个错误的版本](https://leetcode.cn/problems/first-bad-version/) - 标签:数组、二分查找 - 难度:简单 ## 题目链接 - [0278. 第一个错误的版本 - 力扣](https://leetcode.cn/problems/first-bad-version/) ## 题目大意 **描述**:给你一个整数 $n$,代表已经发布的版本号。还有一个用于检测版本是否出错的接口 `isBadVersion(version):` 。 **要求**:找出第一次出错的版本号 $bad$。 **说明**: - 要求尽可能减少对 `isBadVersion(version):` 接口的调用。 - $1 \le bad \le n \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:n = 5, bad = 4 输出:4 解释: 调用 isBadVersion(3) -> false 调用 isBadVersion(5) -> true 调用 isBadVersion(4) -> true 所以,4 是第一个错误的版本。 ``` - 示例 2: ```python 输入:n = 1, bad = 1 输出:1 ``` ## 解题思路 ### 思路 1:二分查找 题目要求尽可能减少对 `isBadVersion(version):` 接口的调用,所以不能对每个版本都调用接口,而是应该将接口调用的次数降到最低。 可以注意到:如果检测某个版本不是错误版本时,则该版本之前的所有版本都不是错误版本。而当某个版本是错误版本时,则该版本之后的所有版本都是错误版本。我们可以利用这样的性质,在 $[1, n]$ 的区间内使用二分查找方法,从而在 $O(\log n)$ 时间复杂度内找到第一个出错误的版本。 ### 思路 1:代码 ```python class Solution: def firstBadVersion(self, n): left = 1 right = n while left < right: mid = (left + right) // 2 if isBadVersion(mid): right = mid else: left = mid + 1 return left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。二分查找算法的时间复杂度为 $O(\log n)$。 - **空间复杂度**:$O(1)$。只用到了常数空间存放若干变量。 ================================================ FILE: docs/solutions/0200-0299/flatten-2d-vector.md ================================================ # [0251. 展开二维向量](https://leetcode.cn/problems/flatten-2d-vector/) - 标签:设计、数组、双指针、迭代器 - 难度:中等 ## 题目链接 - [0251. 展开二维向量 - 力扣](https://leetcode.cn/problems/flatten-2d-vector/) ## 题目大意 **要求**: 设计并实现一个能够展开二维向量的迭代器。该迭代器需要支持 `next` 和 `hasNext` 两种操作。 实现 $Vector2D$ 类: - `Vector2D(int[][] vec)` 使用二维向量 $vec$ 初始化对象。 - `next()` 从二维向量返回下一个元素并将指针移动到下一个位置。你可以假设对 `next` 的所有调用都是合法的。 * `hasNext()` 当向量中还有元素返回 $true$,否则返回 $false$。 **说明**: - $0 \le vec.length \le 200$。 - $0 \le vec[i].length \le 500$。 - $-500 \le vec[i][j] \le 500$。 - 最多调用 `next` 和 `hasNext` $10^{5}$ 次。 - 进阶:尝试在代码中仅使用 C++ 提供的迭代器 或 Java 提供的迭代器。 **示例**: - 示例 1: ```python 输入: ["Vector2D", "next", "next", "next", "hasNext", "hasNext", "next", "hasNext"] [[[[1, 2], [3], [4]]], [], [], [], [], [], [], []] 输出: [null, 1, 2, 3, true, true, 4, false] 解释: Vector2D vector2D = new Vector2D([[1, 2], [3], [4]]); vector2D.next(); // return 1 vector2D.next(); // return 2 vector2D.next(); // return 3 vector2D.hasNext(); // return True vector2D.hasNext(); // return True vector2D.next(); // return 4 vector2D.hasNext(); // return False ``` ## 解题思路 ### 思路 1:双指针 使用两个指针 $i$ 和 $j$ 来跟踪当前在二维向量中的位置。其中 $i$ 表示当前在第几个一维数组,$j$ 表示在当前一维数组中的位置。 核心思想是维护指针始终指向下一个有效元素,通过预定位机制确保访问安全: - **初始化**:$i = 0$,$j = 0$,然后调用 `_findNext()` 将指针定位到第一个有效元素。 - **`_findNext()` 方法**:跳过所有空的一维数组,将指针移动到下一个有效位置。当 $j$ 超出当前一维数组长度时,移动到下一个一维数组并重置 $j = 0$。 - **`next()` 方法**:返回当前有效元素 $vec[i][j]$,然后 $j$ 自增,再调用 `_findNext()` 预定位到下一个有效元素。 - **`hasNext()` 方法**:简单检查 $i$ 是否小于向量长度,因为指针已经预定位到有效位置。 ### 思路 1:代码 ```python class Vector2D: def __init__(self, vec: List[List[int]]): # 初始化二维向量和指针 self.vec = vec self.i = 0 # 当前一维数组的索引 self.j = 0 # 当前一维数组中的索引 # 初始化时找到第一个有效位置 self._findNext() def _findNext(self): # 跳过空的一维数组,找到下一个有效元素 while self.i < len(self.vec): if self.j < len(self.vec[self.i]): # 当前一维数组还有元素 return else: # 当前一维数组已遍历完,移动到下一个一维数组 self.i += 1 self.j = 0 def next(self) -> int: # 获取当前元素 result = self.vec[self.i][self.j] # 移动指针到下一个位置 self.j += 1 # 找到下一个有效位置 self._findNext() return result def hasNext(self) -> bool: # 检查是否还有下一个元素 return self.i < len(self.vec) # Your Vector2D object will be instantiated and called as such: # obj = Vector2D(vec) # param_1 = obj.next() # param_2 = obj.hasNext() ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$ 对于 `next()` 和 `hasNext()` 操作。虽然 `hasNext()` 可能需要跳过空数组,但每个元素最多被访问一次,总体时间复杂度为 $O(n)$,其中 $n$ 是总元素个数。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量来存储指针位置。 ================================================ FILE: docs/solutions/0200-0299/flip-game-ii.md ================================================ # [0294. 翻转游戏 II](https://leetcode.cn/problems/flip-game-ii/) - 标签:记忆化搜索、数学、动态规划、回溯、博弈 - 难度:中等 ## 题目链接 - [0294. 翻转游戏 II - 力扣](https://leetcode.cn/problems/flip-game-ii/) ## 题目大意 **描述**: 你和朋友玩一个叫做「翻转游戏」的游戏。游戏规则如下: 给定一个字符串 $currentState$,其中只含 `'+'` 和 `'-'`。你和朋友轮流将「连续」的两个 `"++"` 反转成 `"--"`。当一方无法进行有效的翻转时便意味着游戏结束,则另一方获胜。默认每个人都会采取最优策略。 **要求**: 请你写出一个函数来判定起始玩家 是否存在必胜的方案:如果存在,返回 $true$;否则,返回 $false$。 **说明**: - $1 \le currentState.length \le 60$。 - $currentState[i]$ 不是 `'+'` 就是 `'-'`。 - 不能有超过 $20$ 个连续的 `'+'`。 - 进阶:请推导你算法的时间复杂度。 **示例**: - 示例 1: ```python 输入:currentState = "++++" 输出:true 解释:起始玩家可将中间的 "++" 翻转变为 "+--+" 从而得胜。 ``` - 示例 2: ```python 输入:currentState = "+" 输出:false ``` ## 解题思路 ### 思路 1:记忆化搜索 这是一个博弈论问题,可以使用记忆化搜索来解决。核心思想是:当前玩家能否获胜,取决于是否存在一种操作,使得对手在剩余状态下无法获胜。 设 $f(s)$ 表示当前状态为字符串 $s$ 时,当前玩家是否能获胜。则:$f(s) = \bigvee_{i} f(s')$ 其中 $s'$ 是将 $s$ 中第 $i$ 个位置的 `"++"` 翻转为 `"--"` 后得到的新状态,$\bigvee$ 表示逻辑或运算。 具体算法步骤: 1. 遍历字符串,找到所有可以翻转的 `"++"` 位置。 2. 对每个可翻转位置,模拟翻转操作得到新状态。 3. 递归检查新状态下对手是否能获胜。 4. 如果存在任何一个新状态使得对手无法获胜,则当前玩家获胜。 5. 使用记忆化避免重复计算。 ### 思路 1:代码 ```python class Solution: def canWin(self, currentState: str) -> bool: # 使用记忆化搜索,避免重复计算 memo = {} def dfs(state: str) -> bool: # 如果已经计算过,直接返回结果 if state in memo: return memo[state] # 遍历字符串,寻找可以翻转的 "++" for i in range(len(state) - 1): if state[i] == '+' and state[i + 1] == '+': # 模拟翻转操作:将 "++" 变为 "--" new_state = state[:i] + "--" + state[i + 2:] # 递归检查对手是否能获胜 # 如果对手无法获胜,则当前玩家获胜 if not dfs(new_state): memo[state] = True return True # 如果没有可翻转的位置,或者所有翻转都无法获胜 memo[state] = False return False return dfs(currentState) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 2^{n/2})$,其中 $n$ 是字符串长度。最坏情况下,每个状态都需要遍历所有可能的翻转位置,状态空间的大小约为 $2^{n/2}$(因为每次翻转会减少 2 个字符)。 - **空间复杂度**:$O(2^{n/2})$,用于存储记忆化结果,最坏情况下需要存储所有可能的状态。 ================================================ FILE: docs/solutions/0200-0299/flip-game.md ================================================ # [0293. 翻转游戏](https://leetcode.cn/problems/flip-game/) - 标签:字符串 - 难度:简单 ## 题目链接 - [0293. 翻转游戏 - 力扣](https://leetcode.cn/problems/flip-game/) ## 题目大意 **描述**: 你和朋友玩一个叫做「翻转游戏」的游戏。游戏规则如下: 给定一个字符串 $currentState$ ,其中只含 `'+'` 和 `'-'`。你和朋友轮流将「连续」的两个 `"++"` 反转成 `"--"`。当一方无法进行有效的翻转时便意味着游戏结束,则另一方获胜。 **要求**: 计算并返回「一次有效操作」后,字符串 $currentState$ 所有的可能状态,返回结果可以按任意顺序 排列。如果不存在可能的有效操作,请返回一个空列表 $[]$。 **说明**: - $1 \le currentState.length \le 500$。 - $currentState[i]$ 不是 `'+'` 就是 `'-'`。 **示例**: - 示例 1: ```python 输入:currentState = "++++" 输出:["--++","+--+","++--"] ``` - 示例 2: ```python 输入:currentState = "+" 输出:[] ``` ## 解题思路 ### 思路 1:遍历模拟 这是一个简单的字符串模拟问题。我们需要遍历字符串,找到所有连续的 `"++"` 位置,然后将每个位置翻转成 `"--"`,生成所有可能的下一个状态。 核心思想是: - 遍历字符串 $currentState$,寻找所有可以翻转的位置 $i$,满足 `currentState[i] = '+'` 且 `currentState[i+1] = '+'`。 - 对于每个有效位置 $i$,生成新状态:`newState = currentState[:i] + "--" + currentState[i+2:]`。 - 将所有生成的新状态收集到结果列表中。 具体算法步骤: 1. 初始化结果列表 $result = []$。 2. 遍历字符串从位置 $0$ 到 $len(currentState) - 2$。 3. 检查当前位置 $i$ 和 $i+1$ 是否都为 `'+'`。 4. 如果是,则生成新状态并添加到结果中。 5. 返回结果列表。 ### 思路 1:代码 ```python class Solution: def generatePossibleNextMoves(self, currentState: str) -> List[str]: result = [] # 遍历字符串,寻找可以翻转的 "++" 位置 for i in range(len(currentState) - 1): # 检查当前位置和下一个位置是否都是 '+' if currentState[i] == '+' and currentState[i + 1] == '+': # 生成新状态:将 "++" 替换为 "--" new_state = currentState[:i] + "--" + currentState[i + 2:] result.append(new_state) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度。我们需要遍历字符串一次,每次检查两个相邻字符,生成新状态的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(k \times n)$,其中 $k$ 是可能的下一个状态数量。最坏情况下,如果字符串中每两个相邻字符都是 `"++"`,则 $k = O(n)$,每个状态需要 $O(n)$ 空间存储。 ================================================ FILE: docs/solutions/0200-0299/game-of-life.md ================================================ # [0289. 生命游戏](https://leetcode.cn/problems/game-of-life/) - 标签:数组、矩阵、模拟 - 难度:中等 ## 题目链接 - [0289. 生命游戏 - 力扣](https://leetcode.cn/problems/game-of-life/) ## 题目大意 **描述**:给定一个 $m \times n$ 大小的二维数组 $board$,每一个格子都可以看做是一个细胞。每个细胞都有一个初始状态:$1$ 代表活细胞,$0$ 代表死细胞。每个细胞与其相邻的八个位置(水平、垂直、对角线)细胞遵循以下生存规律: - 如果活细胞周围八个位置的活细胞数少于 $2$ 个,则该位置活细胞死亡; - 如果活细胞周围八个位置有 $2$ 个或 $3$ 个活细胞,则该位置活细胞仍然存活; - 如果活细胞周围八个位置有超过 $3$ 个活细胞,则该位置活细胞死亡; - 如果死细胞周围正好有 $3$ 个活细胞,则该位置死细胞复活。 二维数组代表的下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的的。其中细胞的出生和死亡是同时发生的。 现在给定 $m \times n$ 的二维数组 $board$ 的当前状态。 **要求**:返回下一个状态。 **说明**: - $m == board.length$。 - $n == board[i].length$。 - $1 \le m, n \le 25$。 - $board[i][j]$ 为 $0$ 或 $1$。 - **进阶**: - 你可以使用原地算法解决本题吗?请注意,面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。 - 本题中,我们使用二维数组来表示面板。原则上,面板是无限的,但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/12/26/grid1.jpg) ```python 输入:board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]] 输出:[[0,0,0],[1,0,1],[0,1,1],[0,1,0]] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/12/26/grid2.jpg) ```python 输入:board = [[1,1],[1,0]] 输出:[[1,1],[1,1]] ``` ## 解题思路 ### 思路 1:模拟 因为下一个状态隐含了过去细胞的状态,所以不能直接在原二维数组上直接进行修改。细胞的状态总共有四种情况: - 死细胞 -> 死细胞,即 $0 \rightarrow 0$。 - 死细胞 -> 活细胞,即 $0 \rightarrow 1$。 - 活细胞 -> 活细胞,即 $1 \rightarrow 1$。 - 活细胞 -> 死细胞,即 $1 \rightarrow 0$。 死细胞 -> 死细胞,活细胞 -> 活细胞,不会对前后状态造成影响,所以主要考虑另外两种情况。我们把活细胞 -> 死细胞暂时标记为 $-1$,并且统计每个细胞周围活细胞数量时,使用绝对值统计,这样 $abs(-1)$ 也可以暂时标记为活细胞。然后把死细胞 -> 活细胞暂时标记为 $2$,这样判断的时候也不会统计上去。然后开始遍历。 - 遍历二维数组的每一个位置。并对该位置遍历周围八个位置,计算出八个位置上的活细胞数量。 - 如果此位置是活细胞,并且周围活细胞少于 $2$ 个或超过 $3$ 个,则将其暂时标记为 $-1$,意为此细胞死亡。 - 如果此位置是死细胞,并且周围有 $3$ 个活细胞,则将暂时标记为 $2$,意为此细胞复活。 - 遍历完之后,再次遍历一遍二维数组,如果该位置为 $-1$,将其赋值为 $0$,如果该位置为 $2$,将其赋值为 $1$。 ### 思路 1:代码 ```python class Solution: def gameOfLife(self, board: List[List[int]]) -> None: """ Do not return anything, modify board in-place instead. """ directions = {(1, 0), (1, -1), (0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1)} rows = len(board) cols = len(board[0]) for row in range(rows): for col in range(cols): lives = 0 for direction in directions: new_row = row + direction[0] new_col = col + direction[1] if 0 <= new_row < rows and 0 <= new_col < cols and abs(board[new_row][new_col]) == 1: lives += 1 if board[row][col] == 1 and (lives < 2 or lives > 3): board[row][col] = -1 if board[row][col] == 0 and lives == 3: board[row][col] = 2 for row in range(rows): for col in range(cols): if board[row][col] == -1: board[row][col] = 0 elif board[row][col] == 2: board[row][col] = 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$、$n$ 分别为 $board$ 的行数和列数。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/0200-0299/graph-valid-tree.md ================================================ # [0261. 以图判树](https://leetcode.cn/problems/graph-valid-tree/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [0261. 以图判树 - 力扣](https://leetcode.cn/problems/graph-valid-tree/) ## 题目大意 **描述**: 给定编号从 $0$ 到 $n - 1$ 的 $n$ 个结点。给定一个整数 $n$ 和一个 $edges$ 列表,其中 $edges[i] = [ai, bi]$ 表示图中节点 $ai$ 和 $bi$ 之间存在一条无向边。 **要求**: 如果这些边能够形成一个合法有效的树结构,则返回 $true$,否则返回 $false$。 **说明**: - $1 \le n \le 2000$。 - $0 \le edges.length \le 5000$。 - $edges[i].length == 2$。 - $0 \le ai, bi \lt n$。 - $ai \ne bi$。 - 不存在自循环或重复的边。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/12/tree1-graph.jpg) ```python 输入: n = 5, edges = [[0,1],[0,2],[0,3],[1,4]] 输出: true ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/12/tree2-graph.jpg) ```python 输入: n = 5, edges = [[0,1],[1,2],[2,3],[1,3],[1,4]] 输出: false ``` ## 解题思路 ### 思路 1:并查集 这是一个图论问题,需要判断给定的边是否能构成一棵有效的树。根据树的定义,一个有效的树必须满足以下条件: 1. **连通性**:所有 $n$ 个节点都连通。 2. **无环性**:图中不存在环。 3. **边数条件**:恰好有 $n - 1$ 条边。 我们可以使用并查集来解决这个问题。核心思想是: - 如果边的数量不等于 $n-1$,则不可能构成树 - 使用并查集检测环:如果在添加边的过程中发现两个节点已经在同一个连通分量中,则存在环 - 最后检查是否所有节点都在同一个连通分量中 具体算法步骤: 1. 检查边的数量:如果 $|edges| \neq n-1$,返回 $false$。 2. 初始化并查集,每个节点的父节点为自己。 3. 遍历每条边 $[a, b]$: - 找到 $a$ 和 $b$ 的根节点 $root_a$ 和 $root_b$。 - 如果 $root_a = root_b$,说明存在环,返回 $false$。 - 否则将 $a$ 和 $b$ 合并到同一个连通分量。 4. 检查连通性:所有节点是否都在同一个连通分量中。 ### 思路 1:代码 ```python class Solution: def validTree(self, n: int, edges: List[List[int]]) -> bool: # 如果边的数量不等于 n-1,则不可能构成树 if len(edges) != n - 1: return False # 初始化并查集,每个节点的父节点为自己 parent = list(range(n)) def find(x): # 路径压缩优化 if parent[x] != x: parent[x] = find(parent[x]) return parent[x] def union(x, y): # 找到两个节点的根节点 root_x, root_y = find(x), find(y) # 如果已经在同一个连通分量中,说明存在环 if root_x == root_y: return False # 合并两个连通分量 parent[root_x] = root_y return True # 遍历每条边,检查是否存在环 for a, b in edges: if not union(a, b): return False # 检查连通性:所有节点是否都在同一个连通分量中 root = find(0) for i in range(1, n): if find(i) != root: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \alpha(n))$,其中 $\alpha(n)$ 是反阿克曼函数,在实际应用中可以认为是常数。遍历所有边需要 $O(n)$ 时间,每次并查集操作需要 $O(\alpha(n))$ 时间。 - **空间复杂度**:$O(n)$,用于存储并查集的父节点数组。 ================================================ FILE: docs/solutions/0200-0299/group-shifted-strings.md ================================================ # [0249. 移位字符串分组](https://leetcode.cn/problems/group-shifted-strings/) - 标签:数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [0249. 移位字符串分组 - 力扣](https://leetcode.cn/problems/group-shifted-strings/) ## 题目大意 给定一个仅包含小写字母的字符串列表。其中每个字符串都可以进行「移位」操作,也就是将字符串中的每个字母变为其在字母表中后续的字母。比如:`abc` -> `bcd`。 要求:将该列表中满足「移位」操作规律的组合进行分组并返回。 ## 解题思路 我们可以先将满足相同「移位」操作规律的组合翻译为相同的模式,然后利用哈希表进行存储。哈希表对应关系为 翻译后模式:该模式对应的原字符串列表。 ## 代码 ```python import collections class Solution: def groupStrings(self, strings: List[str]) -> List[List[str]]: str_dict = collections.defaultdict(list) for string in strings: if string[0] == 'a': str_dict[string].append(string) else: list_string = list(string) for i in range(len(list_string)): num = (ord(list_string[i]) - ord(string[0]) + 26) % 26 list_string[i] = chr(num + ord('a')) temp_string = ''.join(list_string) str_dict[temp_string].append(string) res = list() for string, sublist in str_dict.items(): res.append(sublist) return res ``` ================================================ FILE: docs/solutions/0200-0299/h-index-ii.md ================================================ # [0275. H 指数 II](https://leetcode.cn/problems/h-index-ii/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [0275. H 指数 II - 力扣](https://leetcode.cn/problems/h-index-ii/) ## 题目大意 **描述**: 给定一个整数数组 $citations$ ,其中 $citations[i]$ 表示研究者的第 $i$ 篇论文被引用的次数,$citations$ 已经按照非降序排列。 **要求**: 计算并返回该研究者的 $h$ 指数。 **说明**: - $h$ 指数的定义:$h$ 代表「高引用次数」(high citations),一名科研人员的 $h$ 指数是指他(她)的($n$ 篇论文中)至少有 $h$ 篇论文分别被引用了至少 $h$ 次。 - 设计并实现对数时间复杂度的算法解决此问题。 - $n == citations.length$。 - $1 \le n \le 10^{5}$。 - $0 \le citations[i] \le 10^{3}$。 - $citations$ 按升序排列。 **示例**: - 示例 1: ```python 输入:citations = [0,1,3,5,6] 输出:3 解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6 次。 由于研究者有3篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3 。 ``` - 示例 2: ```python 输入:citations = [1,2,100] 输出:2 ``` ## 解题思路 ### 思路 1:二分查找 这是一个经典的 H 指数计算问题,由于数组已经按升序排列,我们可以使用二分查找来优化时间复杂度。 核心思想是: - 由于数组 $citations$ 已经按升序排列,我们可以使用二分查找来找到满足条件的最大 $h$ 值。 - 对于位置 $i$,如果 $citations[i] \geq n - i$,说明从位置 $i$ 开始到数组末尾有 $n - i$ 篇论文,且这些论文的引用次数都大于等于 $n - i$。 - 我们需要找到最小的 $i$ 使得 $citations[i] \geq n - i$,此时 $h$ 指数就是 $n - i$。 具体算法步骤: 1. 初始化二分查找的边界:$left = 0$,$right = n - 1$。 2. 在 $[left, right]$ 区间内进行二分查找: - 计算中点 $mid = (left + right) // 2$。 - 如果 $citations[mid] \geq n - mid$,说明从位置 $mid$ 开始有 $n - mid$ 篇论文满足条件,尝试寻找更小的 $mid$,更新 $right = mid - 1$。 - 否则,说明从位置 $mid$ 开始不满足条件,需要增大 $mid$,更新 $left = mid + 1$。 3. 最终 $h$ 指数为 $n - left$。 ### 思路 1:代码 ```python class Solution: def hIndex(self, citations: List[int]) -> int: n = len(citations) left, right = 0, n - 1 # 二分查找满足条件的最小位置 while left <= right: mid = (left + right) // 2 # 如果 citations[mid] >= n - mid,说明从位置 mid 开始 # 有 n - mid 篇论文的引用次数都大于等于 n - mid if citations[mid] >= n - mid: # 尝试寻找更小的 mid right = mid - 1 else: # 需要增大 mid left = mid + 1 # h 指数就是 n - left return n - left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$,其中 $n$ 是数组长度。使用二分查找,每次将搜索范围缩小一半。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0200-0299/h-index.md ================================================ # [0274. H 指数](https://leetcode.cn/problems/h-index/) - 标签:数组、计数排序、排序 - 难度:中等 ## 题目链接 - [0274. H 指数 - 力扣](https://leetcode.cn/problems/h-index/) ## 题目大意 **描述**: 给定一个整数数组 $citations$ ,其中 $citations[i]$ 表示研究者的第 $i$ 篇论文被引用的次数。 **要求**: 计算并返回该研究者的 $h$ 指数。 **说明**: - $h$ 指数的定义:$h$ 代表「高引用次数」,一名科研人员的 $h$ 指数是指他(她)至少发表了 $h$ 篇论文,并且至少有 $h$ 篇论文被引用次数大于等于 $h$。如果 $h$ 有多种可能的值,$h$ 指数是其中最大的那个。 - $n == citations.length$。 - $1 \le n \le 5000$。 - $0 \le citations[i] \le 10^{3}$。 **示例**: - 示例 1: ```python 输入:citations = [3,0,6,1,5] 输出:3 解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。 由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。 ``` - 示例 2: ```python 输入:citations = [1,3,1] 输出:1 ``` ## 解题思路 ### 思路 1:排序 + 遍历 这是一个经典的 H 指数计算问题。我们需要找到一个最大的 $h$ 值,使得至少有 $h$ 篇论文的引用次数大于等于 $h$。 核心思想是: - 将引用次数数组 $citations$ 按照降序排列。 - 从大到小遍历排序后的数组,找到第一个满足 $citations[i] < i + 1$ 的位置。 - 此时 $h$ 指数就是 $i$,表示有 $i$ 篇论文的引用次数大于等于 $i$。 具体算法步骤: 1. 对数组 $citations$ 进行降序排序:$citations.sort(reverse=True)$。 2. 初始化 $h = 0$。 3. 遍历排序后的数组,对于每个位置 $i$: - 如果 $citations[i] \geq i + 1$,说明至少有 $i + 1$ 篇论文的引用次数大于等于 $i + 1$,更新 $h = i + 1$。 - 否则,停止遍历。 4. 返回 $h$ 指数。 ### 思路 1:代码 ```python class Solution: def hIndex(self, citations: List[int]) -> int: # 对引用次数数组进行降序排序 citations.sort(reverse=True) # 初始化 h 指数为 0 h = 0 # 遍历排序后的数组 for i in range(len(citations)): # 如果当前论文的引用次数大于等于 i+1 # 说明至少有 i+1 篇论文的引用次数大于等于 i+1 if citations[i] >= i + 1: h = i + 1 else: # 一旦不满足条件,就可以停止遍历 break return h ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组长度。主要时间消耗在排序操作上。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0200-0299/happy-number.md ================================================ # [0202. 快乐数](https://leetcode.cn/problems/happy-number/) - 标签:哈希表、数学、双指针 - 难度:简单 ## 题目链接 - [0202. 快乐数 - 力扣](https://leetcode.cn/problems/happy-number/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:判断 $n$ 是否为快乐数。 **说明**: - 快乐数定义: - 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。 - 然后重复这个过程直到这个数变为 $1$,也可能是 无限循环 但始终变不到 $1$。 - 如果 可以变为 $1$,那么这个数就是快乐数。 - $1 \le n \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:n = 19 输出:True 解释: 12 + 92 = 82 82 + 22 = 68 62 + 82 = 100 12 + 02 + 02 = 1 ``` - 示例 2: ```python 输入:n = 2 输出:False ``` ## 解题思路 ### 思路 1:哈希表 / 集合 根据题意,不断重复操作,数可能变为 $1$,也可能是无限循环。无限循环其实就相当于链表形成了闭环,可以用哈希表来存储为一位生成的数,每次判断该数是否存在于哈希表中。如果已经出现在哈希表里,则说明进入了无限循环,该数就不是快乐数。如果没有出现则将该数加入到哈希表中,进行下一次计算。不断重复这个过程,直到形成闭环或者变为 $1$。 ### 思路 1:代码 ```python class Solution: def getNext(self, n: int): total_sum = 0 while n > 0: n, digit = divmod(n, 10) total_sum += digit ** 2 return total_sum def isHappy(self, n: int) -> bool: num_set = set() while n != 1 and n not in num_set: num_set.add(n) n = self.getNext(n) return n == 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0200-0299/house-robber-ii.md ================================================ # [0213. 打家劫舍 II](https://leetcode.cn/problems/house-robber-ii/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [0213. 打家劫舍 II - 力扣](https://leetcode.cn/problems/house-robber-ii/) ## 题目大意 **描述**:给定一个数组 $nums$,$num[i]$ 代表第 $i$ 间房屋存放的金额,假设房屋可以围成一圈,最后一间房屋跟第一间房屋可以相连。相邻的房屋装有防盗系统,假如相邻的两间房屋同时被偷,系统就会报警。 **要求**:假如你是一名专业的小偷,计算在不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。 **说明**: - $1 \le nums.length \le 100$。 - $0 \le nums[i] \le 1000$。 **示例**: - 示例 1: ```python 输入:nums = [2,3,2] 输出:3 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。 ``` - 示例 2: ```python 输入:nums = [1,2,3,1] 输出:4 解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4。 ``` ## 解题思路 ### 思路 1:动态规划 这道题可以看做是「[198. 打家劫舍](https://leetcode.cn/problems/house-robber/)」的升级版。 如果房屋数大于等于 $3$ 间,偷窃了第 $1$ 间房屋,则不能偷窃最后一间房屋。同样偷窃了最后一间房屋则不能偷窃第 $1$ 间房屋。 假设总共房屋数量为 $size$,这种情况可以转换为分别求解 $[0, size - 2]$ 和 $[1, size - 1]$ 范围下首尾不相连的房屋所能偷窃的最高金额,然后再取这两种情况下的最大值。而求解 $[0, size - 2]$ 和 $[1, size - 1]$ 范围下首尾不相连的房屋所能偷窃的最高金额问题就跟「[198. 打家劫舍](https://leetcode.cn/problems/house-robber)」所求问题一致了。 这里来复习一下「[198. 打家劫舍](https://leetcode.cn/problems/house-robber)」的解题思路。 ###### 1. 阶段划分 按照房屋序号进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为:前 $i$ 间房屋所能偷窃到的最高金额。 ###### 3. 状态转移方程 $i$ 间房屋的最后一个房子是 $nums[i - 1]$。 如果房屋数大于等于 $2$ 间,则偷窃第 $i - 1$ 间房屋的时候,就有两种状态: 1. 偷窃第 $i - 1$ 间房屋,那么第 $i - 2$ 间房屋就不能偷窃了,偷窃的最高金额为:前 $i - 2$ 间房屋的最高总金额 + 第 $i - 1$ 间房屋的金额,即 $dp[i] = dp[i - 2] + nums[i - 1]$; 1. 不偷窃第 $i - 1$ 间房屋,那么第 $i - 2$ 间房屋可以偷窃,偷窃的最高金额为:前 $i - 1$ 间房屋的最高总金额,即 $dp[i] = dp[i - 1]$。 然后这两种状态取最大值即可,即状态转移方程为: $dp[i] = \begin{cases} nums[0] & i = 1 \cr max(dp[i - 2] + nums[i - 1], dp[i - 1]) & i \ge 2\end{cases}$ ###### 4. 初始条件 - 前 $0$ 间房屋所能偷窃到的最高金额为 $0$,即 $dp[0] = 0$。 - 前 $1$ 间房屋所能偷窃到的最高金额为 $nums[0]$,即:$dp[1] = nums[0]$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示为:前 $i$ 间房屋所能偷窃到的最高金额。假设求解 $[0, size - 2]$ 和 $[1, size - 1]$ 范围下( $size$ 为总的房屋数)首尾不相连的房屋所能偷窃的最高金额问题分别为 $ans1$、$ans2$,则最终结果为 $max(ans1, ans2)$。 ### 思路 1:动态规划代码 ```python class Solution: def helper(self, nums): size = len(nums) if size == 0: return 0 dp = [0 for _ in range(size + 1)] dp[0] = 0 dp[1] = nums[0] for i in range(2, size + 1): dp[i] = max(dp[i - 2] + nums[i - 1], dp[i - 1]) return dp[size] def rob(self, nums: List[int]) -> int: size = len(nums) if size == 1: return nums[0] ans1 = self.helper(nums[:size - 1]) ans2 = self.helper(nums[1:]) return max(ans1, ans2) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0200-0299/implement-queue-using-stacks.md ================================================ # [0232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/) - 标签:栈、设计、队列 - 难度:简单 ## 题目链接 - [0232. 用栈实现队列 - 力扣](https://leetcode.cn/problems/implement-queue-using-stacks/) ## 题目大意 **要求**:仅使用两个栈实现先入先出队列。 要求实现 `MyQueue` 类: - `void push(int x)` 将元素 `x` 推到队列的末尾。 - `int pop()` 从队列的开头移除并返回元素。 - `int peek()` 返回队列开头的元素。 - `boolean empty()` 如果队列为空,返回 `True`;否则,返回 `False`。 **说明**: - 只能使用标准的栈操作 —— 也就是只有 `push to top`, `peek / pop from top`, `size`, 和 `is empty` 操作是合法的。 - 可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。 - $1 <= x <= 9$。 - 最多调用 $100$ 次 `push`、`pop`、`peek` 和 `empty`。 - 假设所有操作都是有效的 (例如,一个空的队列不会调用 `pop` 或者 `peek` 操作)。 - 进阶:实现每个操作均摊时间复杂度为 `O(1)` 的队列。换句话说,执行 `n` 个操作的总时间复杂度为 `O(n)`,即使其中一个操作可能花费较长时间。 **示例**: - 示例 1: ```python 输入: ["MyQueue", "push", "push", "peek", "pop", "empty"] [[], [1], [2], [], [], []] 输出: [null, null, null, 1, 1, false] 解释: MyQueue myQueue = new MyQueue(); myQueue.push(1); // queue is: [1] myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue) myQueue.peek(); // return 1 myQueue.pop(); // return 1, queue is [2] myQueue.empty(); // return false ``` ## 解题思路 ### 思路 1:双栈 使用两个栈,`inStack` 用于输入,`outStack` 用于输出。 - `push` 操作:将元素压入 `inStack` 中。 - `pop` 操作:如果 `outStack` 输出栈为空,将 `inStack` 输入栈元素依次取出,按顺序压入 `outStack` 栈。这样 `outStack` 栈的元素顺序和之前 `inStack` 元素顺序相反,`outStack` 顶层元素就是要取出的队头元素,将其移出,并返回该元素。如果 `outStack` 输出栈不为空,则直接取出顶层元素。 - `peek` 操作:和 `pop` 操作类似,只不过最后一步不需要取出顶层元素,直接将其返回即可。 - `empty` 操作:如果 `inStack` 和 `outStack` 都为空,则队列为空,否则队列不为空。 ### 思路 1:代码 ```python class MyQueue: def __init__(self): self.inStack = [] self.outStack = [] """ Initialize your data structure here. """ def push(self, x: int) -> None: self.inStack.append(x) """ Push element x to the back of queue. """ def pop(self) -> int: if(len(self.outStack) == 0): while(len(self.inStack) != 0): self.outStack.append(self.inStack[-1]) self.inStack.pop() top = self.outStack[-1] self.outStack.pop() return top """ Removes the element from in front of queue and returns that element. """ def peek(self) -> int: if (len(self.outStack) == 0): while (len(self.inStack) != 0): self.outStack.append(self.inStack[-1]) self.inStack.pop() top = self.outStack[-1] return top """ Get the front element. """ def empty(self) -> bool: return len(self.outStack) == 0 and len(self.inStack) == 0 """ Returns whether the queue is empty. """ ``` ### 思路 1:复杂度分析 - **时间复杂度**:`push` 和 `empty` 为 $O(1)$,`pop` 和 `peek` 为均摊 $O(1)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0200-0299/implement-stack-using-queues.md ================================================ # [0225. 用队列实现栈](https://leetcode.cn/problems/implement-stack-using-queues/) - 标签:栈、设计、队列 - 难度:简单 ## 题目链接 - [0225. 用队列实现栈 - 力扣](https://leetcode.cn/problems/implement-stack-using-queues/) ## 题目大意 **要求**:仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的四种操作:`push`、`top`、`pop` 和 `empty`。 要求实现 `MyStack` 类: - `void push(int x)` 将元素 `x` 压入栈顶。 - `int pop()` 移除并返回栈顶元素。 - `int top()` 返回栈顶元素。 - `boolean empty()` 如果栈是空的,返回 `True`;否则,返回 `False`。 **说明**: - 只能使用队列的基本操作 —— 也就是 `push to back`、`peek/pop from front`、`size` 和 `is empty` 这些操作。 - 所使用的语言也许不支持队列。 你可以使用 `list` (列表)或者 `deque`(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。 **示例**: - 示例 1: ```python 输入: ["MyStack", "push", "push", "top", "pop", "empty"] [[], [1], [2], [], [], []] 输出: [null, null, null, 2, 2, false] 解释: MyStack myStack = new MyStack(); myStack.push(1); myStack.push(2); myStack.top(); // 返回 2 myStack.pop(); // 返回 2 myStack.empty(); // 返回 False ``` ## 解题思路 ### 思路 1:双队列 使用两个队列。`pushQueue` 用作入栈,`popQueue` 用作出栈。 - `push` 操作:将新加入的元素压入 `pushQueue` 队列中,并且将之前保存在 `popQueue` 队列中的元素从队头开始依次压入 `pushQueue` 中,此时 `pushQueue` 队列中头节点存放的是新加入的元素,尾部存放的是之前的元素。 而 `popQueue` 则为空。再将 `pushQueue` 和 `popQueue` 相互交换,保持 `pushQueue` 为空,`popQueue` 则用于 `pop`、`top` 等操作。 - `pop` 操作:直接将 `popQueue` 队头元素取出。 - `top` 操作:返回 `popQueue` 队头元素。 - `empty`:判断 `popQueue` 是否为空。 ### 思路 1:代码 ```python class MyStack: def __init__(self): """ Initialize your data structure here. """ self.pushQueue = collections.deque() self.popQueue = collections.deque() def push(self, x: int) -> None: """ Push element x onto stack. """ self.pushQueue.append(x) while self.popQueue: self.pushQueue.append(self.popQueue.popleft()) self.pushQueue, self.popQueue = self.popQueue, self.pushQueue def pop(self) -> int: """ Removes the element on top of the stack and returns that element. """ return self.popQueue.popleft() def top(self) -> int: """ Get the top element. """ return self.popQueue[0] def empty(self) -> bool: """ Returns whether the stack is empty. """ return not self.popQueue # Your MyStack object will be instantiated and called as such: # obj = MyStack() # obj.push(x) # param_2 = obj.pop() # param_3 = obj.top() # param_4 = obj.empty() ``` ### 思路 1:复杂度分析 - **时间复杂度**:入栈操作的时间复杂度为 $O(n)$。出栈、取栈顶元素、判断栈是否为空的时间复杂度为 $O(1)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0200-0299/implement-trie-prefix-tree.md ================================================ # [0208. 实现 Trie (前缀树)](https://leetcode.cn/problems/implement-trie-prefix-tree/) - 标签:设计、字典树、哈希表、字符串 - 难度:中等 ## 题目链接 - [0208. 实现 Trie (前缀树) - 力扣](https://leetcode.cn/problems/implement-trie-prefix-tree/) ## 题目大意 **要求**:实现前缀树数据结构的相关类 `Trie` 类。 `Trie` 类: - `Trie()` 初始化前缀树对象。 - `void insert(String word)` 向前缀树中插入字符串 `word`。 - `boolean search(String word)` 如果字符串 `word` 在前缀树中,返回 `True`(即,在检索之前已经插入);否则,返回 `False`。 - `boolean startsWith(String prefix)` 如果之前已经插入的字符串 `word` 的前缀之一为 `prefix`,返回 `True`;否则,返回 `False`。 **说明**: - $1 \le word.length, prefix.length \le 2000$。 - `word` 和 `prefix` 仅由小写英文字母组成。 - `insert`、`search` 和 `startsWith` 调用次数 **总计** 不超过 $3 * 10^4$ 次。 **示例**: - 示例 1: ```python 输入: ["Trie", "insert", "search", "search", "startsWith", "insert", "search"] [[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]] 输出: [null, null, true, false, true, null, true] 解释: Trie trie = new Trie(); trie.insert("apple"); trie.search("apple"); // 返回 True trie.search("app"); // 返回 False trie.startsWith("app"); // 返回 True trie.insert("app"); trie.search("app"); // 返回 True ``` ## 解题思路 ### 思路 1:前缀树(字典树) 前缀树(字典树)是一棵多叉树,其中每个节点包含指向子节点的指针数组 `children`,以及布尔变量 `isEnd`。`children` 用于存储当前字符节点,一般长度为所含字符种类个数,也可以使用哈希表代替指针数组。`isEnd` 用于判断该节点是否为字符串的结尾。 下面依次讲解插入、查找前缀的具体步骤: **插入字符串**: - 从根节点开始插入字符串。对于待插入的字符,有两种情况: - 如果该字符对应的节点存在,则沿着指针移动到子节点,继续处理下一个字符。 - 如果该字符对应的节点不存在,则创建一个新的节点,保存在 `children` 中对应位置上,然后沿着指针移动到子节点,继续处理下一个字符。 - 重复上述步骤,直到最后一个字符,然后将该节点标记为字符串的结尾。 **查找前缀**: - 从根节点开始查找前缀,对于待查找的字符,有两种情况: - 如果该字符对应的节点存在,则沿着指针移动到子节点,继续查找下一个字符。 - 如果该字符对应的节点不存在,则说明字典树中不包含该前缀,直接返回空指针。 - 重复上述步骤,直到最后一个字符搜索完毕,则说明字典树中存在该前缀。 ### 思路 1:代码 ```python class Node: def __init__(self): self.children = dict() self.isEnd = False class Trie: def __init__(self): self.root = Node() def insert(self, word: str) -> None: cur = self.root for ch in word: if ch not in cur.children: cur.children[ch] = Node() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> bool: cur = self.root for ch in word: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None and cur.isEnd def startsWith(self, prefix: str) -> bool: cur = self.root for ch in prefix: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None ``` ### 思路 1:复杂度分析 - **时间复杂度**:初始化为 $O(1)$。插入操作、查找操作的时间复杂度为 $O(|S|)$。其中 $|S|$ 是每次插入或查找字符串的长度。 - **空间复杂度**:$O(|T| \times \sum)$。其中 $|T|$ 是所有插入字符串的长度之和,$\sum$ 是字符集的大小。 ================================================ FILE: docs/solutions/0200-0299/index.md ================================================ ## 本章内容 - [0200. 岛屿数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-islands.md) - [0201. 数字范围按位与](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/bitwise-and-of-numbers-range.md) - [0202. 快乐数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/happy-number.md) - [0203. 移除链表元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/remove-linked-list-elements.md) - [0204. 计数质数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/count-primes.md) - [0205. 同构字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/isomorphic-strings.md) - [0206. 反转链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/reverse-linked-list.md) - [0207. 课程表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/course-schedule.md) - [0208. 实现 Trie (前缀树)](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-trie-prefix-tree.md) - [0209. 长度最小的子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/minimum-size-subarray-sum.md) - [0210. 课程表 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/course-schedule-ii.md) - [0211. 添加与搜索单词 - 数据结构设计](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/design-add-and-search-words-data-structure.md) - [0212. 单词搜索 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/word-search-ii.md) - [0213. 打家劫舍 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/house-robber-ii.md) - [0214. 最短回文串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/shortest-palindrome.md) - [0215. 数组中的第K个最大元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/kth-largest-element-in-an-array.md) - [0216. 组合总和 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/combination-sum-iii.md) - [0217. 存在重复元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate.md) - [0218. 天际线问题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/the-skyline-problem.md) - [0219. 存在重复元素 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate-ii.md) - [0220. 存在重复元素 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/contains-duplicate-iii.md) - [0221. 最大正方形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/maximal-square.md) - [0222. 完全二叉树的节点个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/count-complete-tree-nodes.md) - [0223. 矩形面积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/rectangle-area.md) - [0224. 基本计算器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/basic-calculator.md) - [0225. 用队列实现栈](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-stack-using-queues.md) - [0226. 翻转二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/invert-binary-tree.md) - [0227. 基本计算器 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/basic-calculator-ii.md) - [0228. 汇总区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/summary-ranges.md) - [0229. 多数元素 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/majority-element-ii.md) - [0230. 二叉搜索树中第 K 小的元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/kth-smallest-element-in-a-bst.md) - [0231. 2 的幂](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/power-of-two.md) - [0232. 用栈实现队列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/implement-queue-using-stacks.md) - [0233. 数字 1 的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/number-of-digit-one.md) - [0234. 回文链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/palindrome-linked-list.md) - [0235. 二叉搜索树的最近公共祖先](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/lowest-common-ancestor-of-a-binary-search-tree.md) - [0236. 二叉树的最近公共祖先](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/lowest-common-ancestor-of-a-binary-tree.md) - [0237. 删除链表中的节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/delete-node-in-a-linked-list.md) - [0238. 除自身以外数组的乘积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/product-of-array-except-self.md) - [0239. 滑动窗口最大值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/sliding-window-maximum.md) - [0240. 搜索二维矩阵 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/search-a-2d-matrix-ii.md) - [0241. 为运算表达式设计优先级](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/different-ways-to-add-parentheses.md) - [0242. 有效的字母异位词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/valid-anagram.md) - [0243. 最短单词距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/shortest-word-distance.md) - [0244. 最短单词距离 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/shortest-word-distance-ii.md) - [0245. 最短单词距离 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/shortest-word-distance-iii.md) - [0246. 中心对称数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/strobogrammatic-number.md) - [0247. 中心对称数 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/strobogrammatic-number-ii.md) - [0248. 中心对称数 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/strobogrammatic-number-iii.md) - [0249. 移位字符串分组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/group-shifted-strings.md) - [0250. 统计同值子树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/count-univalue-subtrees.md) - [0251. 展开二维向量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/flatten-2d-vector.md) - [0252. 会议室](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/meeting-rooms.md) - [0253. 会议室 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/meeting-rooms-ii.md) - [0254. 因子的组合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/factor-combinations.md) - [0255. 验证二叉搜索树的前序遍历序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/verify-preorder-sequence-in-binary-search-tree.md) - [0256. 粉刷房子](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/paint-house.md) - [0257. 二叉树的所有路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/binary-tree-paths.md) - [0258. 各位相加](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/add-digits.md) - [0259. 较小的三数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/3sum-smaller.md) - [0260. 只出现一次的数字 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/single-number-iii.md) - [0261. 以图判树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/graph-valid-tree.md) - [0263. 丑数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/ugly-number.md) - [0264. 丑数 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/ugly-number-ii.md) - [0265. 粉刷房子 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/paint-house-ii.md) - [0266. 回文排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/palindrome-permutation.md) - [0267. 回文排列 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/palindrome-permutation-ii.md) - [0268. 丢失的数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/missing-number.md) - [0269. 火星词典](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/alien-dictionary.md) - [0270. 最接近的二叉搜索树值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/closest-binary-search-tree-value.md) - [0271. 字符串的编码与解码](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/encode-and-decode-strings.md) - [0272. 最接近的二叉搜索树值 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/closest-binary-search-tree-value-ii.md) - [0273. 整数转换英文表示](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/integer-to-english-words.md) - [0274. H 指数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/h-index.md) - [0275. H 指数 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/h-index-ii.md) - [0276. 栅栏涂色](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/paint-fence.md) - [0277. 搜寻名人](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/find-the-celebrity.md) - [0278. 第一个错误的版本](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/first-bad-version.md) - [0279. 完全平方数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/perfect-squares.md) - [0280. 摆动排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/wiggle-sort.md) - [0281. 锯齿迭代器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/zigzag-iterator.md) - [0282. 给表达式添加运算符](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/expression-add-operators.md) - [0283. 移动零](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/move-zeroes.md) - [0284. 窥视迭代器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/peeking-iterator.md) - [0285. 二叉搜索树中的中序后继](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/inorder-successor-in-bst.md) - [0286. 墙与门](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/walls-and-gates.md) - [0287. 寻找重复数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/find-the-duplicate-number.md) - [0288. 单词的唯一缩写](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/unique-word-abbreviation.md) - [0289. 生命游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/game-of-life.md) - [0290. 单词规律](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/word-pattern.md) - [0291. 单词规律 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/word-pattern-ii.md) - [0292. Nim 游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/nim-game.md) - [0293. 翻转游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/flip-game.md) - [0294. 翻转游戏 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/flip-game-ii.md) - [0295. 数据流的中位数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/find-median-from-data-stream.md) - [0296. 最佳的碰头地点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/best-meeting-point.md) - [0297. 二叉树的序列化与反序列化](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/serialize-and-deserialize-binary-tree.md) - [0298. 二叉树最长连续序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/binary-tree-longest-consecutive-sequence.md) - [0299. 猜数字游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/bulls-and-cows.md) ================================================ FILE: docs/solutions/0200-0299/inorder-successor-in-bst.md ================================================ # [0285. 二叉搜索树中的中序后继](https://leetcode.cn/problems/inorder-successor-in-bst/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [0285. 二叉搜索树中的中序后继 - 力扣](https://leetcode.cn/problems/inorder-successor-in-bst/) ## 题目大意 给定一棵二叉搜索树的根节点 `root`。 要求:按中序遍历顺序将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。 ## 解题思路 可以分为两步: 1. 中序遍历二叉搜索树,将节点先存储到列表中。 2. 将列表中的节点构造成一棵递增顺序搜索树。 中序遍历直接按照 `左 -> 根 -> 右` 的顺序递归遍历,然后将遍历的节点存储到 `res` 中。 构造递增顺序搜索树,则用 `head` 保存头节点位置。遍历列表中的每个节点,将其左右指针先置空,再将其连接在上一个节点的右子节点上。 最后返回 `head.right` 即可。 ## 代码 ```python class Solution: def inOrder(self, root, res): if not root: return self.inOrder(root.left, res) res.append(root) self.inOrder(root.right, res) def increasingBST(self, root: TreeNode) -> TreeNode: res = [] self.inOrder(root, res) if not res: return head = TreeNode(-1) cur = head for node in res: node.left = node.right = None cur.right = node cur = cur.right return head.right ``` ================================================ FILE: docs/solutions/0200-0299/integer-to-english-words.md ================================================ # [0273. 整数转换英文表示](https://leetcode.cn/problems/integer-to-english-words/) - 标签:递归、数学、字符串 - 难度:困难 ## 题目链接 - [0273. 整数转换英文表示 - 力扣](https://leetcode.cn/problems/integer-to-english-words/) ## 题目大意 **描述**: 给定一个非负整数 $num$。 **要求**: 将非负整数 $num$ 转换为其对应的英文表示。 **说明**: - $0 \le num \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:num = 123 输出:"One Hundred Twenty Three" ``` - 示例 2: ```python 输入:num = 12345 输出:"Twelve Thousand Three Hundred Forty Five" ``` ## 解题思路 ### 思路 1:递归 + 分治 这是一个字符串处理问题,需要将非负整数转换为对应的英文表示。我们可以采用递归分治的思想来解决: 1. **数字分组**:将数字按照千位分组,每三位为一组(个位、十位、百位)。 2. **单位映射**:定义数字到英文单词的映射关系。 3. **递归处理**:对每个三位数组分别处理,然后加上相应的单位。 具体算法步骤: 1. 定义基础数字映射:$0$ 到 $19$ 的英文单词。 2. 定义十位数映射:$20$ 到 $90$ 的十位数英文单词。 3. 定义单位映射:$Thousand$、$Million$、$Billion$。 4. 递归处理每个三位数组: - 处理百位数:如果 $hundred > 0$,加上对应的百位数字和 `"Hundred"`。 - 处理十位数和个位数:如果 $tens < 20$,直接使用基础映射;否则分别处理十位和个位。 5. 根据数字大小添加相应单位。 ### 思路 1:代码 ```python class Solution: def numberToWords(self, num: int) -> str: # 处理特殊情况:0 if num == 0: return "Zero" # 基础数字映射(0-19) ones = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"] # 十位数映射(20-90) tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"] # 单位映射 thousands = ["", "Thousand", "Million", "Billion"] def convert_three_digits(n): """将三位数转换为英文表示""" result = [] # 处理百位数 if n >= 100: result.append(ones[n // 100]) result.append("Hundred") n %= 100 # 处理十位数和个位数 if n >= 20: result.append(tens[n // 10]) if n % 10: result.append(ones[n % 10]) elif n > 0: result.append(ones[n]) return " ".join(result) # 将数字按千位分组处理 result = [] i = 0 while num > 0: # 处理当前三位数组 three_digits = num % 1000 if three_digits != 0: # 转换当前三位数组 current = convert_three_digits(three_digits) # 添加单位(如果有的话) if i > 0: current += " " + thousands[i] result.append(current) # 移动到下一组 num //= 1000 i += 1 # 从高位到低位拼接结果 return " ".join(reversed(result)) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log_{10} n)$,其中 $n$ 是输入数字。我们需要处理数字的每一位,数字的位数是 $O(\log_{10} n)$。 - **空间复杂度**:$O(\log_{10} n)$,用于存储结果字符串和递归调用栈。 ================================================ FILE: docs/solutions/0200-0299/invert-binary-tree.md ================================================ # [0226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0226. 翻转二叉树 - 力扣](https://leetcode.cn/problems/invert-binary-tree/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:将该二叉树进行左右翻转。 **说明**: - 树中节点数目范围在 $[0, 100]$ 内。 - $-100 \le Node.val \le 100$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/14/invert1-tree.jpg) ```python 输入:root = [4,2,7,1,3,6,9] 输出:[4,7,2,9,6,3,1] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/14/invert2-tree.jpg) ```python 输入:root = [2,1,3] 输出:[2,3,1] ``` ## 解题思路 ### 思路 1:递归遍历 根据我们的递推三步走策略,写出对应的递归代码。 1. 写出递推公式: 1. 递归遍历翻转左子树。 2. 递归遍历翻转右子树。 3. 交换当前根节点 `root` 的左右子树。 2. 明确终止条件:当前节点 `root` 为 `None`。 3. 翻译为递归代码: 1. 定义递归函数:`invertTree(self, root)` 表示输入参数为二叉树的根节点 `root`,返回结果为翻转后二叉树的根节点。 2. 书写递归主体: ```python left = self.invertTree(root.left) right = self.invertTree(root.right) root.left = right root.right = left return root ``` 3. 明确递归终止条件:`if not root: return None` 4. 返回根节点 `root`。 ### 思路 1:代码 ```python class Solution: def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]: if not root: return None left = self.invertTree(root.left) right = self.invertTree(root.right) root.left = right root.right = left return root ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0200-0299/isomorphic-strings.md ================================================ # [0205. 同构字符串](https://leetcode.cn/problems/isomorphic-strings/) - 标签:哈希表、字符串 - 难度:简单 ## 题目链接 - [0205. 同构字符串 - 力扣](https://leetcode.cn/problems/isomorphic-strings/) ## 题目大意 **描述**:给定两个字符串 $s$ 和 $t$。 **要求**:判断字符串 $s$ 和 $t$ 是否是同构字符串。 **说明**: - **同构字符串**:如果 $s$ 中的字符可以按某种映射关系替换得到 $t$ 相同位置上的字符,那么两个字符串是同构的。 - 每个字符都应当映射到另一个字符,且不改变字符顺序。不同字符不能映射到统一字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。 - $1 \le s.length \le 5 \times 10^4$。 - $t.length == s.length$。 - $s$ 和 $t$ 由任意有效的 ASCII 字符组成。 **示例**: - 示例 1: ```python 输入:s = "egg", t = "add" 输出:True ``` - 示例 2: ```python 输入:s = "foo", t = "bar" 输出:False ``` ## 解题思路 ### 思路 1:哈希表 根据题目意思,字符串 $s$ 和 $t$ 每个位置上的字符是一一对应的。$s$ 的每个字符都与 $t$ 对应位置上的字符对应。可以考虑用哈希表来存储 $s[i]: t[i]$ 的对应关系。但是这样不能只能保证对应位置上的字符是对应的,但不能保证是唯一对应的。所以还需要另一个哈希表来存储 $t[i]:s[i]$ 的对应关系来判断是否是唯一对应的。 ### 思路 1:代码 ```python class Solution: def isIsomorphic(self, s: str, t: str) -> bool: s_dict = dict() t_dict = dict() for i in range(len(s)): if s[i] in s_dict and s_dict[s[i]] != t[i]: return False if t[i] in t_dict and t_dict[t[i]] != s[i]: return False s_dict[s[i]] = t[i] t_dict[t[i]] = s[i] return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串长度。 - **空间复杂度**:$O(|S|)$ ,其中 $S$ 是字符串字符集。 ================================================ FILE: docs/solutions/0200-0299/kth-largest-element-in-an-array.md ================================================ # [0215. 数组中的第K个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/) - 标签:数组、分治、快速排序、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [0215. 数组中的第K个最大元素 - 力扣](https://leetcode.cn/problems/kth-largest-element-in-an-array/) ## 题目大意 **描述**:给定一个未排序的整数数组 $nums$ 和一个整数 $k$。 **要求**:返回数组中第 $k$ 个最大的元素。 **说明**: - 要求使用时间复杂度为 $O(n)$ 的算法解决此问题。 - $1 \le k \le nums.length \le 10^5$。 - $-10^4 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入: [3,2,1,5,6,4], k = 2 输出: 5 ``` - 示例 2: ```python 输入: [3,2,3,1,2,4,5,5,6], k = 4 输出: 4 ``` ## 解题思路 很不错的一道题,面试常考。 直接可以想到的思路是:排序后输出数组上对应第 $k$ 位大的数。所以问题关键在于排序方法的复杂度。 冒泡排序、选择排序、插入排序时间复杂度 $O(n^2)$ 太高了,很容易超时。 可考虑堆排序、归并排序、快速排序。 这道题的要求是找到第 $k$ 大的元素,使用归并排序只有到最后排序完毕才能返回第 $k$ 大的数。而堆排序每次排序之后,就会确定一个元素的准确排名,同理快速排序也是如此。 ### 思路 1:堆排序 升序堆排序的思路如下: 1. 将无序序列构造成第 $1$ 个大顶堆(初始堆),使得 $n$ 个元素的最大值处于序列的第 $1$ 个位置。 2. **调整堆**:交换序列的第 $1$ 个元素(最大值元素)与第 $n$ 个元素的位置。将序列前 $n - 1$ 个元素组成的子序列调整成一个新的大顶堆,使得 $n - 1$ 个元素的最大值处于序列第 $1$ 个位置,从而得到第 $2$ 个最大值元素。 3. **调整堆**:交换子序列的第 $1$ 个元素(最大值元素)与第 $n - 1$ 个元素的位置。将序列前 $n - 2$ 个元素组成的子序列调整成一个新的大顶堆,使得 $n - 2$ 个元素的最大值处于序列第 $1$ 个位置,从而得到第 $3$ 个最大值元素。 4. 依次类推,不断交换子序列的第 $1$ 个元素(最大值元素)与当前子序列最后一个元素位置,并将其调整成新的大顶堆。直到获取第 $k$ 个最大值元素为止。 ### 思路 1:代码 ```python class Solution: def findKthLargest(self, nums: List[int], k: int) -> int: # 调整为大顶堆 def heapify(nums, index, end): left = index * 2 + 1 right = left + 1 while left <= end: # 当前节点为非叶子节点 max_index = index if nums[left] > nums[max_index]: max_index = left if right <= end and nums[right] > nums[max_index]: max_index = right if index == max_index: # 如果不用交换,则说明已经交换结束 break nums[index], nums[max_index] = nums[max_index], nums[index] # 继续调整子树 index = max_index left = index * 2 + 1 right = left + 1 # 初始化大顶堆 def buildMaxHeap(nums): size = len(nums) # (size-2) // 2 是最后一个非叶节点,叶节点不用调整 for i in range((size - 2) // 2, -1, -1): heapify(nums, i, size - 1) return nums buildMaxHeap(nums) size = len(nums) for i in range(k-1): nums[0], nums[size-i-1] = nums[size-i-1], nums[0] heapify(nums, 0, size-i-2) return nums[0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:快速排序 使用快速排序在每次调整时,都会确定一个元素的最终位置,且以该元素为界限,将数组分成了左右两个子数组,左子数组中的元素都比该元素小,右子树组中的元素都比该元素大。 这样,只要某次划分的元素恰好是第 $k$ 个下标就找到了答案。并且我们只需关注第 $k$ 个最大元素所在区间的排序情况,与第 $k$ 个最大元素无关的区间排序都可以忽略。这样进一步减少了执行步骤。 ### 思路 2:代码 ```python import random class Solution: # 随机哨兵划分:从 nums[low: high + 1] 中随机挑选一个基准数,并进行移位排序 def randomPartition(self, nums: [int], low: int, high: int) -> int: # 随机挑选一个基准数 i = random.randint(low, high) # 将基准数与最低位互换 nums[i], nums[low] = nums[low], nums[i] # 以最低位为基准数,然后将数组中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上 return self.partition(nums, low, high) # 哨兵划分:以第 1 位元素 nums[low] 为基准数,然后将比基准数小的元素移动到基准数左侧,将比基准数大的元素移动到基准数右侧,最后将基准数放到正确位置上 def partition(self, nums: [int], low: int, high: int) -> int: # 以第 1 位元素为基准数 pivot = nums[low] i, j = low, high while i < j: # 从右向左找到第 1 个小于基准数的元素 while i < j and nums[j] >= pivot: j -= 1 # 从左向右找到第 1 个大于基准数的元素 while i < j and nums[i] <= pivot: i += 1 # 交换元素 nums[i], nums[j] = nums[j], nums[i] # 将基准数放到正确位置上 nums[j], nums[low] = nums[low], nums[j] return j def quickSort(self, nums: [int], low: int, high: int, k: int, size: int) -> [int]: if low < high: # 按照基准数的位置,将数组划分为左右两个子数组 pivot_i = self.randomPartition(nums, low, high) if pivot_i == size - k: return nums[size - k] if pivot_i > size - k: self.quickSort(nums, low, pivot_i - 1, k, size) if pivot_i < size - k: self.quickSort(nums, pivot_i + 1, high, k, size) return nums[size - k] def findKthLargest(self, nums: List[int], k: int) -> int: size = len(nums) return self.quickSort(nums, 0, len(nums) - 1, k, size) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。证明过程可参考「算法导论 9.2:期望为线性的选择算法」。 - **空间复杂度**:$O(\log n)$。递归使用栈空间的空间代价期望为 $O(\log n)$。 ### 思路 3:借用标准库(不建议) 提交代码中的最快代码是调用了 Python 的 `sort` 方法。这种做法适合在打算法竞赛的时候节省时间,日常练习可以尝试一下自己写。 ### 思路 3:代码 ```python class Solution: def findKthLargest(self, nums: List[int], k: int) -> int: nums.sort() return nums[len(nums) - k] ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(1)$。 ### 思路 4:优先队列 1. 遍历数组元素,对于当前元素 $num$: 1. 如果优先队列中的元素个数小于 $k$ 个,则将当前元素 $num$ 放入优先队列中。 2. 如果优先队列中的元素个数大于等于 $k$ 个,并且当前元素 $num$ 大于优先队列的队头元素,则弹出队头元素,并将当前元素 $num$ 插入到优先队列中。 2. 遍历完,此时优先队列的队头元素就是第 $k$ 个最大元素,将其弹出并返回即可。 这里我们借助了 Python 中的 `heapq` 模块实现优先队列算法,这一步也可以通过手写堆的方式实现优先队列。 ### 思路 4:代码 ```python import heapq class Solution: def findKthLargest(self, nums: List[int], k: int) -> int: res = [] for num in nums: if len(res) < k: heapq.heappush(res, num) elif num > res[0]: heapq.heappop(res) heapq.heappush(res, num) return heapq.heappop(res) ``` ### 思路 4:复杂度分析 - **时间复杂度**:$O(n \times \log k)$。 - **空间复杂度**:$O(k)$。 ================================================ FILE: docs/solutions/0200-0299/kth-smallest-element-in-a-bst.md ================================================ # [0230. 二叉搜索树中第 K 小的元素](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [0230. 二叉搜索树中第 K 小的元素 - 力扣](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/) ## 题目大意 **描述**: 给定一个二叉搜索树的根节点 $root$,和一个整数 $k$。 **要求**: 设计一个算法查找其中第 $k$ 小的元素(从 $1$ 开始计数)。 **说明**: - 树中的节点数为 $n$。 - $1 \le k \le n \le 10^{4}$。 - $0 \le Node.val \le 10^{4}$。 - 进阶:如果二叉搜索树经常被修改(插入 / 删除操作)并且你需要频繁地查找第 $k$ 小的值,你将如何优化算法? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/01/28/kthtree1.jpg) ```python 输入:root = [3,1,4,null,2], k = 1 输出:1 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/01/28/kthtree2.jpg) ```python 输入:root = [5,3,6,2,4,null,null,1], k = 3 输出:3 ``` ## 解题思路 ### 思路 1:中序遍历 这是一个经典的二叉搜索树问题。由于二叉搜索树具有左子树所有节点值小于根节点值,右子树所有节点值大于根节点值的性质,我们可以利用中序遍历来获得升序排列的节点值。 核心思想是: - 对二叉搜索树进行中序遍历(左子树 → 根节点 → 右子树)。 - 中序遍历的结果是升序排列的节点值序列。 - 遍历到第 $k$ 个节点时,返回该节点的值。 具体算法步骤: 1. 使用递归或迭代的方式进行中序遍历。 2. 维护一个计数器 $count$,记录当前访问的节点序号。 3. 当 $count = k$ 时,返回当前节点的值。 4. 由于只需要找到第 $k$ 小的元素,可以在找到后立即返回,不需要遍历完整棵树。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def kthSmallest(self, root: Optional[TreeNode], k: int) -> int: # 使用中序遍历找到第k小的元素 self.count = 0 # 计数器,记录当前访问的节点序号 self.result = 0 # 存储结果 def inorder_traversal(node): if not node: return # 遍历左子树 inorder_traversal(node.left) # 访问根节点 self.count += 1 if self.count == k: self.result = node.val return # 遍历右子树 inorder_traversal(node.right) inorder_traversal(root) return self.result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k)$,其中 $k$ 是题目给定的参数。最坏情况下需要遍历 $k$ 个节点才能找到第 $k$ 小的元素。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度。递归调用栈的深度等于树的高度,最坏情况下为 $O(n)$(树退化为链表)。 ================================================ FILE: docs/solutions/0200-0299/lowest-common-ancestor-of-a-binary-search-tree.md ================================================ # [0235. 二叉搜索树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [0235. 二叉搜索树的最近公共祖先 - 力扣](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/) ## 题目大意 **描述**:给定一个二叉搜索树的根节点 `root`,以及两个指定节点 `p` 和 `q`。 **要求**:找到该树中两个指定节点的最近公共祖先。 **说明**: - **祖先**:如果节点 `p` 在节点 `node` 的左子树或右子树中,或者 `p == node`,则称 `node` 是 `p` 的祖先。 - **最近公共祖先**:对于树的两个节点 `p`、`q`,最近公共祖先表示为一个节点 `lca_node`,满足 `lca_node` 是 `p`、`q` 的祖先且 `lca_node` 的深度尽可能大(一个节点也可以是自己的祖先)。 - 所有节点的值都是唯一的。 - `p`、`q` 为不同节点且均存在于给定的二叉搜索树中。 **示例**: - 示例 1: ![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/14/binarysearchtree_improved.png) ```python 输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 输出: 6 解释: 节点 2 和节点 8 的最近公共祖先是 6。 ``` - 示例 2: ```python 输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4 输出: 2 解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。 ``` ## 解题思路 ### 思路 1:递归遍历 对于节点 `p`、节点 `q`,最近公共祖先就是从根节点分别到它们路径上的分岔点,也是路径中最后一个相同的节点,现在我们的问题就是求这个分岔点。 我们可以使用递归遍历查找二叉搜索树的最近公共祖先,具体方法如下。 1. 从根节点 `root` 开始遍历。 2. 如果当前节点的值大于 `p`、`q` 的值,说明 `p` 和 `q` 应该在当前节点的左子树,因此将当前节点移动到它的左子节点,继续遍历; 3. 如果当前节点的值小于 `p`、`q` 的值,说明 `p` 和 `q` 应该在当前节点的右子树,因此将当前节点移动到它的右子节点,继续遍历; 4. 如果当前节点不满足上面两种情况,则说明 `p` 和 `q` 分别在当前节点的左右子树上,则当前节点就是分岔点,直接返回该节点即可。 ### 思路 1:代码 ```python class Solution: def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': ancestor = root while True: if ancestor.val > p.val and ancestor.val > q.val: ancestor = ancestor.left elif ancestor.val < p.val and ancestor.val < q.val: ancestor = ancestor.right else: break return ancestor ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉搜索树的节点个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/lowest-common-ancestor-of-a-binary-tree.md ================================================ # [0236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0236. 二叉树的最近公共祖先 - 力扣](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`,以及二叉树中两个节点 `p` 和 `q`。 **要求**:找到该二叉树中指定节点 `p`、`q` 的最近公共祖先。 **说明**: - **祖先**:如果节点 `p` 在节点 `node` 的左子树或右子树中,或者 `p == node`,则称 `node` 是 `p` 的祖先。 - **最近公共祖先**:对于树的两个节点 `p`、`q`,最近公共祖先表示为一个节点 `lca_node`,满足 `lca_node` 是 `p`、`q` 的祖先且 `lca_node` 的深度尽可能大(一个节点也可以是自己的祖先)。 - 树中节点数目在范围 $[2, 10^5]$ 内。 - $-10^9 \le Node.val \le 10^9$。 - 所有 `Node.val` 互不相同。 - `p != q`。 - `p` 和 `q` 均存在于给定的二叉树中。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2018/12/14/binarytree.png) ```python 输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 输出:3 解释:节点 5 和节点 1 的最近公共祖先是节点 3 。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2018/12/14/binarytree.png) ```python 输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 输出:5 解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。 ``` ## 解题思路 ### 思路 1:递归遍历 设 `lca_node` 为节点 `p`、`q` 的最近公共祖先。则 `lca_node` 只能是下面几种情况: 1. `p`、`q` 在 `lca_node` 的子树中,且分别在 `lca_node` 的两侧子树中。 2. `p == lca_node`,且 `q` 在 `lca_node` 的左子树或右子树中。 3. `q == lca_node`,且 `p` 在 `lca_node` 的左子树或右子树中。 下面递归求解 `lca_node`。递归需要满足以下条件: - 如果 `p`、`q` 都不为空,则返回 `p`、`q` 的公共祖先。 - 如果 `p`、`q` 只有一个存在,则返回存在的一个。 - 如果 `p`、`q` 都不存在,则返回 `None`。 具体思路为: 1. 如果当前节点 `node` 等于 `p` 或者 `q`,那么 `node` 就是 `p`、`q` 的最近公共祖先,直接返回 `node`。 2. 如果当前节点 `node` 不为 `None`,则递归遍历左子树、右子树,并判断左右子树结果。 1. 如果左右子树都不为空,则说明 `p`、`q` 在当前根节点的两侧,当前根节点就是他们的最近公共祖先。 2. 如果左子树为空,则返回右子树。 3. 如果右子树为空,则返回左子树。 4. 如果左右子树都为空,则返回 `None`。 3. 如果当前节点 `node` 为 `None`,则说明 `p`、`q` 不在 `node` 的子树中,不可能为公共祖先,直接返回 `None`。 ### 思路 1:代码 ```python class Solution: def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': if root == p or root == q: return root if root: node_left = self.lowestCommonAncestor(root.left, p, q) node_right = self.lowestCommonAncestor(root.right, p, q) if node_left and node_right: return root elif not node_left: return node_right else: return node_left return None ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0200-0299/majority-element-ii.md ================================================ # [0229. 多数元素 II](https://leetcode.cn/problems/majority-element-ii/) - 标签:数组、哈希表、计数、排序 - 难度:中等 ## 题目链接 - [0229. 多数元素 II - 力扣](https://leetcode.cn/problems/majority-element-ii/) ## 题目大意 **描述**: 给定一个大小为 $n$ 的整数数组。 **要求**: 找出其中所有出现超过 $\lfloor n/3 \rfloor$ 次的元素。 **说明**: - $1 \le nums.length \le 5 \times 10^{4}$。 - $-10^{9} \le nums[i] \le 10^{9}$。 - 进阶:尝试设计时间复杂度为 $O(n)$、空间复杂度为 $O(1)$ 的算法解决此问题。 **示例**: - 示例 1: ```python 示例 1: 输入:nums = [3,2,3] 输出:[3] ``` - 示例 2: ```python 输入:nums = [1] 输出:[1] ``` ## 解题思路 ### 思路 1:哈希表计数 这是一个经典的多数元素问题。我们需要找出所有出现次数超过 $\lfloor n/3 \rfloor$ 次的元素。 核心思想是: - 使用哈希表统计每个元素 $nums[i]$ 的出现次数 $count$。 - 遍历数组,对每个元素进行计数:$count[nums[i]] += 1$。 - 遍历哈希表,找出所有满足 $count[element] > \lfloor n/3 \rfloor$ 的元素。 具体算法步骤: 1. 初始化哈希表 $count = \{\}$ 和结果列表 $result = []$。 2. 遍历数组 $nums$,统计每个元素的出现次数。 3. 计算阈值 $threshold = \lfloor len(nums) / 3 \rfloor$。 4. 遍历哈希表,找出所有出现次数大于阈值的元素。 5. 返回结果列表。 ### 思路 1:代码 ```python class Solution: def majorityElement(self, nums: List[int]) -> List[int]: # 使用哈希表统计每个元素的出现次数 count = {} # 遍历数组,统计每个元素的出现次数 for num in nums: count[num] = count.get(num, 0) + 1 # 计算阈值:出现次数需要超过 ⌊n/3⌋ threshold = len(nums) // 3 # 找出所有出现次数超过阈值的元素 result = [] for num, freq in count.items(): if freq > threshold: result.append(num) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组长度。需要遍历数组一次进行计数,然后遍历哈希表一次找出结果。 - **空间复杂度**:$O(n)$,最坏情况下哈希表需要存储所有不同的元素。 ================================================ FILE: docs/solutions/0200-0299/maximal-square.md ================================================ # [0221. 最大正方形](https://leetcode.cn/problems/maximal-square/) - 标签:数组、动态规划、矩阵 - 难度:中等 ## 题目链接 - [0221. 最大正方形 - 力扣](https://leetcode.cn/problems/maximal-square/) ## 题目大意 **描述**:给定一个由 `'0'` 和 `'1'` 组成的二维矩阵 $matrix$。 **要求**:找到只包含 `'1'` 的最大正方形,并返回其面积。 **说明**: - $m == matrix.length$。 - $n == matrix[i].length$。 - $1 \le m, n \le 300$。 - $matrix[i][j]$ 为 `'0'` 或 `'1'`。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/26/max1grid.jpg) ```python 输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]] 输出:4 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/11/26/max2grid.jpg) ```python 输入:matrix = [["0","1"],["1","0"]] 输出:1 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照正方形的右下角坐标进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:以矩阵位置 $(i, j)$ 为右下角,且值包含 $1$ 的正方形的最大边长。 ###### 3. 状态转移方程 只有当矩阵位置 $(i, j)$ 值为 $1$ 时,才有可能存在正方形。 - 如果矩阵位置 $(i, j)$ 上值为 $0$,则 $dp[i][j] = 0$。 - 如果矩阵位置 $(i, j)$ 上值为 $1$,则 $dp[i][j]$ 的值由该位置上方、左侧、左上方三者共同约束的,为三者中最小值加 $1$。即:$dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1$。 ###### 4. 初始条件 - 默认所有以矩阵位置 $(i, j)$ 为右下角,且值包含 $1$ 的正方形的最大边长都为 $0$,即 $dp[i][j] = 0$。 ###### 5. 最终结果 根据我们之前定义的状态, $dp[i][j]$ 表示为:以矩阵位置 $(i, j)$ 为右下角,且值包含 $1$ 的正方形的最大边长。则最终结果为所有 $dp[i][j]$ 中的最大值。 ### 思路 1:代码 ```python class Solution: def maximalSquare(self, matrix: List[List[str]]) -> int: rows, cols = len(matrix), len(matrix[0]) max_size = 0 dp = [[0 for _ in range(cols + 1)] for _ in range(rows + 1)] for i in range(rows): for j in range(cols): if matrix[i][j] == '1': if i == 0 or j == 0: dp[i][j] = 1 else: dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1 max_size = max(max_size, dp[i][j]) return max_size * max_size ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$、$n$ 分别为二维矩阵 $matrix$ 的行数和列数。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/0200-0299/meeting-rooms-ii.md ================================================ # [0253. 会议室 II](https://leetcode.cn/problems/meeting-rooms-ii/) - 标签:贪心、数组、双指针、前缀和、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [0253. 会议室 II - 力扣](https://leetcode.cn/problems/meeting-rooms-ii/) ## 题目大意 **描述**: 给定一个会议时间安排的数组 $intervals$,每个会议时间都会包括开始和结束的时间 $intervals[i] = [start_i, end_i]$。 **要求**: 返回「所需会议室的最小数量」。 **说明**: - $1 \le intervals.length \le 10^{4}$。 - $0 \le start_i \lt end_i \le 10^{6}$。 **示例**: - 示例 1: ```python 输入:intervals = [[0,30],[5,10],[15,20]] 输出:2 ``` - 示例 2: ```python 输入:intervals = [[7,10],[2,4]] 输出:1 ``` ## 解题思路 ### 思路 1:优先队列(最小堆) 这是一个经典的区间调度问题。我们需要找出同时进行的会议的最大数量,这就是所需会议室的最小数量。 核心思想是: - 使用优先队列(最小堆)来维护当前正在进行的会议的结束时间。 - 按照会议开始时间 $start_i$ 对区间进行排序。 - 对于每个会议,检查是否有会议室可用(堆顶的结束时间 $\le$ 当前会议开始时间)。 - 如果有可用会议室,则复用;否则需要新的会议室。 具体算法步骤: 1. 对会议区间按开始时间排序:$intervals.sort(key=lambda x: x[0])$。 2. 初始化最小堆 $heap = []$ 用于存储当前会议的结束时间。 3. 遍历排序后的会议: - 如果堆不为空且堆顶元素(最早结束时间)$\le$ 当前会议开始时间,则弹出堆顶(复用会议室)。 - 将当前会议的结束时间加入堆中。 4. 堆的大小就是所需会议室的最小数量。 ### 思路 1:代码 ```python import heapq class Solution: def minMeetingRooms(self, intervals: List[List[int]]) -> int: # 处理空数组情况 if not intervals: return 0 # 按开始时间排序 intervals.sort(key=lambda x: x[0]) # 使用最小堆存储当前会议的结束时间 heap = [] # 遍历每个会议 for start, end in intervals: # 如果堆不为空且最早结束的会议已经结束,则复用该会议室 if heap and heap[0] <= start: heapq.heappop(heap) # 将当前会议的结束时间加入堆中 heapq.heappush(heap, end) # 堆的大小就是所需会议室的最小数量 return len(heap) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是会议数量。排序需要 $O(n \log n)$ 时间,每个会议最多进行一次堆操作(插入和删除),堆操作的时间复杂度为 $O(\log n)$。 - **空间复杂度**:$O(n)$,最坏情况下所有会议都需要同时进行,堆的大小为 $n$。 ================================================ FILE: docs/solutions/0200-0299/meeting-rooms.md ================================================ # [0252. 会议室](https://leetcode.cn/problems/meeting-rooms/) - 标签:数组、排序 - 难度:简单 ## 题目链接 - [0252. 会议室 - 力扣](https://leetcode.cn/problems/meeting-rooms/) ## 题目大意 **描述**: 给定一个会议时间安排的数组 $intervals$,每个会议时间都会包括开始和结束的时间 $intervals[i] = [start_i, end_i]$。 **要求**: 判断一个人是否能够参加这里面的全部会议。 **说明**: - $0 \le intervals.length \le 10^{4}$。 - $intervals[i].length == 2$。 - $0 \le start_i \lt end_i \le 10^{6}$。 **示例**: - 示例 1: ```python 输入:intervals = [[0,30],[5,10],[15,20]] 输出:false ``` - 示例 2: ```python 输入:intervals = [[7,10],[2,4]] 输出:true ``` ## 解题思路 ### 思路 1:排序 + 遍历 这是一个经典的区间重叠检测问题。我们需要判断是否存在两个会议时间重叠,如果存在重叠,则无法参加所有会议。 核心思想是: - 对会议区间按开始时间 $start_i$ 进行排序。 - 遍历排序后的会议,检查相邻会议是否存在时间重叠。 - 如果存在重叠(前一个会议的结束时间 $end_{i-1} >$ 后一个会议的开始时间 $start_i$),则无法参加所有会议。 具体算法步骤: 1. 对会议区间按开始时间排序:$intervals.sort(key=lambda x: x[0])$。 2. 遍历排序后的会议,检查相邻会议是否重叠: - 如果 $intervals[i-1][1] > intervals[i][0]$,则存在重叠,返回 $false$。 3. 如果所有会议都不重叠,返回 $true$。 ### 思路 1:代码 ```python class Solution: def canAttendMeetings(self, intervals: List[List[int]]) -> bool: # 处理空数组或只有一个会议的情况 if not intervals or len(intervals) <= 1: return True # 按开始时间排序 intervals.sort(key=lambda x: x[0]) # 检查相邻会议是否重叠 for i in range(1, len(intervals)): # 如果前一个会议的结束时间 > 当前会议的开始时间,则存在重叠 if intervals[i-1][1] > intervals[i][0]: return False # 所有会议都不重叠,可以参加所有会议 return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是会议数量。排序需要 $O(n \log n)$ 时间,遍历需要 $O(n)$ 时间。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0200-0299/minimum-size-subarray-sum.md ================================================ # [0209. 长度最小的子数组](https://leetcode.cn/problems/minimum-size-subarray-sum/) - 标签:数组、二分查找、前缀和、滑动窗口 - 难度:中等 ## 题目链接 - [0209. 长度最小的子数组 - 力扣](https://leetcode.cn/problems/minimum-size-subarray-sum/) ## 题目大意 **描述**:给定一个只包含正整数的数组 $nums$ 和一个正整数 $target$。 **要求**:找出数组中满足和大于等于 $target$ 的长度最小的「连续子数组」,并返回其长度。如果不存在符合条件的子数组,返回 $0$。 **说明**: - $1 \le target \le 10^9$。 - $1 \le nums.length \le 10^5$。 - $1 \le nums[i] \le 10^5$。 **示例**: - 示例 1: ```python 输入:target = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。 ``` - 示例 2: ```python 输入:target = 4, nums = [1,4,4] 输出:1 ``` ## 解题思路 ### 思路 1:滑动窗口(不定长度) 最直接的做法是暴力枚举,时间复杂度为 $O(n^2)$。但是我们可以利用滑动窗口的方法,在时间复杂度为 $O(n)$ 的范围内解决问题。 用滑动窗口来记录连续子数组的和,设定两个指针:$left$、$right$,分别指向滑动窗口的左右边界,保证窗口中的和刚好大于等于 $target$。 1. 一开始,$left$、$right$ 都指向 $0$。 2. 向右移动 $right$,将最右侧元素加入当前窗口和 $window\_sum$ 中。 3. 如果 $window\_sum \ge target$,则不断右移 $left$,缩小滑动窗口长度,并更新窗口和的最小值,直到 $window\_sum < target$。 4. 然后继续右移 $right$,直到 $right \ge len(nums)$ 结束。 5. 输出窗口和的最小值作为答案。 ### 思路 1:代码 ```python class Solution: def minSubArrayLen(self, target: int, nums: List[int]) -> int: size = len(nums) ans = size + 1 left = 0 right = 0 window_sum = 0 while right < size: window_sum += nums[right] while window_sum >= target: ans = min(ans, right - left + 1) window_sum -= nums[left] left += 1 right += 1 return ans if ans != size + 1 else 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/missing-number.md ================================================ # [0268. 丢失的数字](https://leetcode.cn/problems/missing-number/) - 标签:位运算、数组、哈希表、数学、二分查找、排序 - 难度:简单 ## 题目链接 - [0268. 丢失的数字 - 力扣](https://leetcode.cn/problems/missing-number/) ## 题目大意 **描述**:给定一个包含 $[0, n]$ 中 $n$ 个数的数组 $nums$。 **要求**:找出 $[0, n]$ 这个范围内没有出现在数组中的那个数。 **说明**: - $n == nums.length$ - $1 \le n \le 10^4$ - $0 \le nums[i] \le n$。 - $nums$ 中的所有数字都独一无二。 **示例**: - 示例 1: ```python 输入:nums = [3,0,1] 输出:2 解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。 ``` - 示例 2: ```python 输入:nums = [0,1] 输出:2 解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。 ``` ## 解题思路 $[0, n]$ 的范围有 $n + 1$ 个数(包含 $0$)。现在给了我们 $n$ 个数,要求找出其中缺失的那个数。 ### 思路 1:哈希表 将 $nums$ 中所有元素插入到哈希表中,然后遍历 $[0, n]$,找到缺失的数字。 这里的哈希表也可以用长度为 $n + 1$ 的数组代替。 ### 思路 1:代码 ```python class Solution: def missingNumber(self, nums: List[int]) -> int: numSet = set(nums) for num in range(len(nums)+1): if num not in numSet: return num ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ### 思路 2:数学计算 已知 $[0, n]$ 的求和公式为:$\sum_{i=0}^n i = \frac{n*(n+1)}{2}$,则用 $[0, n]$ 的和,减去数组中所有元素的和,就得到了缺失数字。 ### 思路 2:代码 ```python class Solution: def missingNumber(self, nums: List[int]) -> int: sum_nums = sum(nums) n = len(nums) return (n + 1) * n // 2 - sum_nums ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/move-zeroes.md ================================================ # [0283. 移动零](https://leetcode.cn/problems/move-zeroes/) - 标签:数组、双指针 - 难度:简单 ## 题目链接 - [0283. 移动零 - 力扣](https://leetcode.cn/problems/move-zeroes/) ## 题目大意 **描述**:给定一个数组 $nums$。 **要求**:将所有 $0$ 移动到末尾,并保持原有的非 $0$ 数字的相对顺序。 **说明**: - 只能在原数组上进行操作。 - $1 \le nums.length \le 10^4$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0] ``` - 示例 2: ```python 输入: nums = [0] 输出: [0] ``` ## 解题思路 ### 思路 1:冒泡排序(超时) 冒泡排序的思想,就是通过相邻元素的比较与交换,使得较大元素从前面移到后面。 我们可以借用冒泡排序的思想,将值为 $0$ 的元素移动到数组末尾。 因为数据规模为 $10^4$,而冒泡排序的时间复杂度为 $O(n^2)$。所以这种做法会导致超时。 ### 思路 1:代码 ```python class Solution: def moveZeroes(self, nums: List[int]) -> None: for i in range(len(nums)): for j in range(len(nums) - i - 1): if nums[j] == 0 and nums[j + 1] != 0: nums[j], nums[j + 1] = nums[j + 1], nums[j] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:快慢指针 1. 使用两个指针 $slow$,$fast$。$slow$ 指向处理好的非 $0$ 数字数组的尾部,$fast$ 指针指向当前待处理元素。 2. 不断向右移动 $fast$ 指针,每次移动到非零数,则将左右指针对应的数交换,交换同时将 $slow$ 右移。 3. 此时,$slow$ 指针左侧均为处理好的非零数,而从 $slow$ 指针指向的位置开始, $fast$ 指针左边为止都为 $0$。 遍历结束之后,则所有 $0$ 都移动到了右侧,且保持了非零数的相对位置。 ### 思路 2:代码 ```python class Solution: def moveZeroes(self, nums: List[int]) -> None: slow = 0 fast = 0 while fast < len(nums): if nums[fast] != 0: nums[slow], nums[fast] = nums[fast], nums[slow] slow += 1 fast += 1 ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/nim-game.md ================================================ # [0292. Nim 游戏](https://leetcode.cn/problems/nim-game/) - 标签:脑筋急转弯、数学、博弈 - 难度:简单 ## 题目链接 - [0292. Nim 游戏 - 力扣](https://leetcode.cn/problems/nim-game/) ## 题目大意 两个人玩 Nim 游戏。游戏规则是这样的: - 桌上有一堆石子,两个人轮流从石子堆中拿走 1~3 块石头。拿掉最后一块石头的人就是获胜者。 - 假如每个人都尽可能的想赢得比赛,所以每一轮都是最优解。 现在给定一个整数 n 代表石头数目。如果你作为先手,问最终能否赢得比赛。 ## 解题思路 假设石子的数量为 1~3,那么我作为先手,肯定第一次就将所有的石子都拿完了,所以肯定能赢。 假设石子的数量为 4,那么我作为先手,无论第一次拿走 1、2、3 块石头,都不能拿完,而第二个人再拿的时候,会直接将剩下的石头一次性全拿走,所以肯定不会赢。 如果石子数量多于 4,那么我作为先手,为了赢,应该尽可能使得本轮拿走后的石子数为 4,这样对手拿完一次之后,自己肯定会获胜。 所以石子树为 5、6、7 块的时候,我可以通过分别拿走 1、2、3 块石头,使得剩下的石头数为 4,从而在下一轮获得胜利。 如果石子数为 8 块的时候,我无论怎么拿都不能使剩下石子为 4。而对方又会利用这个机会使得他拿走之后的石子数变为 4,从而使我失败。 所以,很显然:当 n 不是 4 的整数倍时,我一定赢得比赛。当 n 为 4 的整数倍时,我一定赢不了比赛。 ## 代码 ```python class Solution: def canWinNim(self, n: int) -> bool: return n % 4 != 0 ``` ================================================ FILE: docs/solutions/0200-0299/number-of-digit-one.md ================================================ # [0233. 数字 1 的个数](https://leetcode.cn/problems/number-of-digit-one/) - 标签:递归、数学、动态规划 - 难度:困难 ## 题目链接 - [0233. 数字 1 的个数 - 力扣](https://leetcode.cn/problems/number-of-digit-one/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:计算所有小于等于 $n$ 的非负整数中数字 $1$ 出现的个数。 **说明**: - $0 \le n \le 10^9$。 **示例**: - 示例 1: ```python 输入:n = 13 输出:6 ``` - 示例 2: ```python 输入:n = 0 输出:0 ``` ## 解题思路 ### 思路 1:动态规划 + 数位 DP 将 $n$ 转换为字符串 $s$,定义递归函数 `def dfs(pos, cnt, isLimit):` 表示构造第 $pos$ 位及之后所有数位中数字 $1$ 出现的个数。接下来按照如下步骤进行递归。 1. 从 `dfs(0, 0, True)` 开始递归。 `dfs(0, 0, True)` 表示: 1. 从位置 $0$ 开始构造。 2. 初始数字 $1$ 出现的个数为 $0$。 3. 开始时受到数字 $n$ 对应最高位数位的约束。 2. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时:返回数字 $1$ 出现的个数 $cnt$。 3. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:`ans = 0`。 4. 如果遇到 $isNum == False$,说明之前位数没有填写数字,当前位可以跳过,这种情况下方案数等于 $pos + 1$ 位置上没有受到 $pos$ 位的约束,并且之前没有填写数字时的方案数,即:`ans = dfs(i + 1, state, False, False)`。 5. 如果 $isNum == True$,则当前位必须填写一个数字。此时: 1. 因为不需要考虑前导 $0$ 所以当前位数位所能选择的最小数字($minX$)为 $0$。 2. 根据 $isLimit$ 来决定填当前位数位所能选择的最大数字($maxX$)。 3. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $d$。 4. 方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, cnt + (d == 1), isLimit and d == maxX)`。 1. `cnt + (d == 1)` 表示之前数字 $1$ 出现的个数加上当前位为数字 $1$ 的个数。 2. `isLimit and d == maxX` 表示 $pos + 1$ 位受到之前位 $pos$ 位限制。 6. 最后的方案数为 `dfs(0, 0, True)`,将其返回即可。 ### 思路 1:代码 ```python class Solution: def countDigitOne(self, n: int) -> int: # 将 n 转换为字符串 s s = str(n) @cache # pos: 第 pos 个数位 # cnt: 之前数字 1 出现的个数。 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 def dfs(pos, cnt, isLimit): if pos == len(s): return cnt ans = 0 # 不需要考虑前导 0,则最小可选择数字为 0 minX = 0 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s[pos]) if isLimit else 9 # 枚举可选择的数字 for d in range(minX, maxX + 1): ans += dfs(pos + 1, cnt + (d == 1), isLimit and d == maxX) return ans return dfs(0, 0, True) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0200-0299/number-of-islands.md ================================================ # [0200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) - 标签:深度优先搜索、广度优先搜索、并查集、数组、矩阵 - 难度:中等 ## 题目链接 - [0200. 岛屿数量 - 力扣](https://leetcode.cn/problems/number-of-islands/) ## 题目大意 **描述**:给定一个由字符 `'1'`(陆地)和字符 `'0'`(水)组成的的二维网格 $grid$。 **要求**:计算网格中岛屿的数量。 **说明**: - 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 - 此外,你可以假设该网格的四条边均被水包围。 - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 300$。 - $grid[i][j]$ 的值为 `'0'` 或 `'1'`。 **示例**: - 示例 1: ```python 输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1 ``` - 示例 2: ```python 输入:grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] ] 输出:3 ``` ## 解题思路 如果把上下左右相邻的字符 `'1'` 看做是 `1` 个连通块,这道题的目的就是求解一共有多少个连通块。 使用深度优先搜索或者广度优先搜索都可以。 ### 思路 1:深度优先搜索 1. 遍历 $grid$。 2. 对于每一个字符为 `'1'` 的元素,遍历其上下左右四个方向,并将该字符置为 `'0'`,保证下次不会被重复遍历。 3. 如果超出边界,则返回 $0$。 4. 对于 $(i, j)$ 位置的元素来说,递归遍历的位置就是 $(i - 1, j)$、$(i, j - 1)$、$(i + 1, j)$、$(i, j + 1)$ 四个方向。每次遍历到底,统计数记录一次。 5. 最终统计出深度优先搜索的次数就是我们要求的岛屿数量。 ### 思路 1:代码 ```python class Solution: def dfs(self, grid, i, j): n = len(grid) m = len(grid[0]) if i < 0 or i >= n or j < 0 or j >= m or grid[i][j] == '0': return 0 grid[i][j] = '0' self.dfs(grid, i + 1, j) self.dfs(grid, i, j + 1) self.dfs(grid, i - 1, j) self.dfs(grid, i, j - 1) def numIslands(self, grid: List[List[str]]) -> int: count = 0 for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == '1': self.dfs(grid, i, j) count += 1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。其中 $m$ 和 $n$ 分别为行数和列数。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/0200-0299/paint-fence.md ================================================ # [0276. 栅栏涂色](https://leetcode.cn/problems/paint-fence/) - 标签:动态规划 - 难度:中等 ## 题目链接 - [0276. 栅栏涂色 - 力扣](https://leetcode.cn/problems/paint-fence/) ## 题目大意 **描述**: 有 $k$ 种颜色的涂料和一个包含 $n$ 个栅栏柱的栅栏,请你按下述规则为栅栏设计涂色方案: - 每个栅栏柱可以用其中「一种」颜色进行上色。 - 相邻的栅栏柱「最多连续两个」颜色相同。 给定两个整数 $k$ 和 $n$。 **要求**: 返回所有有效的涂色「方案数」。 **说明**: - $1 \le n \le 50$。 - $1 \le k \le 10^{5}$。 - 题目数据保证:对于输入的 $n$ 和 $k$,其答案在范围 $[0, 2^{31} - 1]$ 内。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/28/paintfenceex1.png) ```python 输入:n = 3, k = 2 输出:6 解释:所有的可能涂色方案如上图所示。注意,全涂红或者全涂绿的方案属于无效方案,因为相邻的栅栏柱 最多连续两个 颜色相同。 ``` - 示例 2: ```python 输入:n = 1, k = 1 输出:1 ``` ## 解题思路 ### 思路 1:动态规划 这是一个经典的动态规划问题。我们需要计算在相邻栅栏柱最多连续两个颜色相同的约束下,有多少种涂色方案。 核心思想是: - 定义状态:$dp[i]$ 表示前 $i$ 个栅栏柱的涂色方案数。 - 状态转移:考虑第 $i$ 个栅栏柱的涂色情况: - 如果第 $i$ 个栅栏柱与第 $i-1$ 个栅栏柱颜色不同,有 $dp[i-1] \times (k-1)$ 种方案。 - 如果第 $i$ 个栅栏柱与第 $i-1$ 个栅栏柱颜色相同,但第 $i-1$ 个与第 $i-2$ 个颜色不同,有 $dp[i-2] \times (k-1)$ 种方案。 - 状态转移方程:$dp[i] = dp[i-1] \times (k-1) + dp[i-2] \times (k-1) = (dp[i-1] + dp[i-2]) \times (k-1)$ 具体算法步骤: 1. 处理边界情况:如果 $n = 0$,返回 $0$;如果 $n = 1$,返回 $k$;如果 $n = 2$,返回 $k \times k$。 2. 初始化状态:$dp[0] = 0$,$dp[1] = k$,$dp[2] = k \times k$。 3. 状态转移:对于 $i$ 从 $3$ 到 $n$,计算 $dp[i] = (dp[i-1] + dp[i-2]) \times (k-1)$。 4. 返回 $dp[n]$。 ### 思路 1:代码 ```python class Solution: def numWays(self, n: int, k: int) -> int: # 处理边界情况 if n == 0: return 0 if n == 1: return k if n == 2: return k * k # 初始化 dp 数组 # dp[i] 表示前 i 个栅栏柱的涂色方案数 dp = [0] * (n + 1) dp[0] = 0 dp[1] = k dp[2] = k * k # 状态转移 for i in range(3, n + 1): # 第 i 个栅栏柱的涂色方案数 = (前 i-1 个的方案数 + 前 i-2 个的方案数) × (k-1) # 其中 k-1 表示与前面栅栏柱颜色不同的选择数 dp[i] = (dp[i-1] + dp[i-2]) * (k - 1) return dp[n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是栅栏柱数量。需要遍历 $n$ 个状态进行状态转移。 - **空间复杂度**:$O(n)$,需要 $O(n)$ 的空间存储 $dp$ 数组。 ================================================ FILE: docs/solutions/0200-0299/paint-house-ii.md ================================================ # [0265. 粉刷房子 II](https://leetcode.cn/problems/paint-house-ii/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [0265. 粉刷房子 II - 力扣](https://leetcode.cn/problems/paint-house-ii/) ## 题目大意 **描述**: 假如有一排房子共有 $n$ 幢,每个房子可以被粉刷成 $k$ 种颜色中的一种。房子粉刷成不同颜色的花费成本也是不同的。你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。 每个房子粉刷成不同颜色的花费以一个 $n \times k$ 的矩阵表示。 - 例如,$costs[0][0]$ 表示第 $0$ 幢房子粉刷成 $0$ 号颜色的成本;$costs[1][2]$ 表示第 $1$ 幢房子粉刷成 $2$ 号颜色的成本,以此类推。 **要求**: 返回「粉刷完所有房子的最低成本」。 **说明**: - $costs.length == n$。 - $costs[i].length == k$。 - $1 \le n \le 10^{3}$。 - $2 \le k \le 20$。 - $1 \le costs[i][j] \le 20$。 - 进阶:您能否在 $O(n \times k)$ 的时间复杂度下解决此问题? **示例**: - 示例 1: ```python 输入: costs = [[1,5,3],[2,9,4]] 输出: 5 解释: 将房子 0 刷成 0 号颜色,房子 1 刷成 2 号颜色。花费: 1 + 4 = 5; 或者将 房子 0 刷成 2 号颜色,房子 1 刷成 0 号颜色。花费: 3 + 2 = 5. ``` - 示例 2: ```python 输入: costs = [[1,3],[2,4]] 输出: 5 ``` ## 解题思路 ### 思路 1:动态规划 + 优化 这是一个经典的动态规划问题。我们需要找到粉刷所有房子的最低成本,且相邻房子颜色不能相同。 核心思想是: - 定义状态:$dp[i][j]$ 表示第 $i$ 个房子粉刷成第 $j$ 种颜色的最低成本。 - 状态转移:$dp[i][j] = costs[i][j] + \min_{k \neq j} dp[i-1][k]$,即当前房子选择颜色 $j$ 的成本加上前一个房子选择其他颜色的最小成本。 - 为了优化空间复杂度,我们只需要维护前一个房子的最小成本和次小成本,以及对应的颜色。 具体算法步骤: 1. 处理边界情况:如果只有一个房子,返回所有颜色中的最小成本。 2. 初始化:记录前一个房子的最小成本 $min1$、次小成本 $min2$ 和对应的颜色 $color1$、$color2$。 3. 状态转移:对于每个房子,计算选择每种颜色的成本,并更新最小和次小成本。 4. 返回最后一个房子的最小成本。 ### 思路 1:代码 ```python class Solution: def minCostII(self, costs: List[List[int]]) -> int: # 处理空数组情况 if not costs or not costs[0]: return 0 n, k = len(costs), len(costs[0]) # 如果只有一个房子,返回所有颜色中的最小成本 if n == 1: return min(costs[0]) # 初始化前一个房子的最小成本和次小成本 prev_min1 = prev_min2 = 0 # 前一个房子的最小成本和次小成本 prev_color1 = -1 # 前一个房子最小成本对应的颜色 # 遍历每个房子 for i in range(n): # 当前房子的最小成本和次小成本 curr_min1 = curr_min2 = float('inf') curr_color1 = -1 # 遍历每种颜色 for j in range(k): # 计算选择颜色 j 的总成本 if j == prev_color1: # 如果当前颜色与前一个房子的最小成本颜色相同,使用次小成本 cost = costs[i][j] + prev_min2 else: # 否则使用最小成本 cost = costs[i][j] + prev_min1 # 更新当前房子的最小和次小成本 if cost < curr_min1: curr_min2 = curr_min1 curr_min1 = cost curr_color1 = j elif cost < curr_min2: curr_min2 = cost # 更新前一个房子的状态 prev_min1, prev_min2 = curr_min1, curr_min2 prev_color1 = curr_color1 # 返回最后一个房子的最小成本 return prev_min1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times k)$,其中 $n$ 是房子数量,$k$ 是颜色数量。需要遍历每个房子的每种颜色。 - **空间复杂度**:$O(1)$,只使用了常数个额外变量,不依赖于输入规模。 ================================================ FILE: docs/solutions/0200-0299/paint-house.md ================================================ # [0256. 粉刷房子](https://leetcode.cn/problems/paint-house/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [0256. 粉刷房子 - 力扣](https://leetcode.cn/problems/paint-house/) ## 题目大意 **描述**: 假如有一排房子,共 $n$ 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。 当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 $n \times 3$ 的正整数矩阵 $costs$ 来表示的。 例如,$costs[0][0]$ 表示第 0 号房子粉刷成红色的成本花费;$costs[1]$[2] 表示第 1 号房子粉刷成绿色的花费,以此类推。 **要求**: 请计算出粉刷完所有房子最少的花费成本。 **说明**: - $costs.length == n$。 - $costs[i].length == 3$。 - $1 \le n \le 10^{3}$。 - $1 \le costs[i][j] \le 20$。 **示例**: - 示例 1: ```python 输入: costs = [[17,2,17],[16,16,5],[14,3,19]] 输出: 10 解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。 最少花费: 2 + 5 + 3 = 10。 ``` - 示例 2: ```python 输入: costs = [[7,6,2]] 输出: 2 ``` ## 解题思路 ### 思路 1:动态规划 这是一个经典的动态规划问题。我们需要找到粉刷所有房子的最低成本,且相邻房子颜色不能相同。 核心思想是: - 定义状态:$dp[i][j]$ 表示第 $i$ 个房子粉刷成第 $j$ 种颜色的最低成本,其中 $j \in \{0, 1, 2\}$ 分别代表红色、蓝色、绿色。 - 状态转移:$dp[i][j] = costs[i][j] + \min_{k \neq j} dp[i-1][k]$,即当前房子选择颜色 $j$ 的成本加上前一个房子选择其他颜色的最小成本。 - 最终答案:$\min(dp[n-1][0], dp[n-1][1], dp[n-1][2])$,即最后一个房子选择任意颜色的最小成本。 具体算法步骤: 1. 初始化:$dp[0][j] = costs[0][j]$,第一个房子选择任意颜色的成本就是对应的粉刷成本。 2. 状态转移:对于每个房子 $i$ 和每种颜色 $j$,计算 $dp[i][j] = costs[i][j] + \min(dp[i-1][k])$,其中 $k \neq j$。 3. 返回结果:$\min(dp[n-1][0], dp[n-1][1], dp[n-1][2])$。 ### 思路 1:代码 ```python class Solution: def minCost(self, costs: List[List[int]]) -> int: # 处理空数组情况 if not costs: return 0 n = len(costs) # 如果只有一个房子,返回所有颜色中的最小成本 if n == 1: return min(costs[0]) # 初始化 dp 数组,dp[i][j] 表示第 i 个房子选择颜色 j 的最小成本 dp = [[0] * 3 for _ in range(n)] # 初始化第一个房子的成本 for j in range(3): dp[0][j] = costs[0][j] # 动态规划:计算每个房子选择每种颜色的最小成本 for i in range(1, n): for j in range(3): # 当前房子选择颜色 j 的最小成本 = 当前颜色成本 + 前一个房子选择其他颜色的最小成本 dp[i][j] = costs[i][j] + min(dp[i-1][k] for k in range(3) if k != j) # 返回最后一个房子选择任意颜色的最小成本 return min(dp[n-1]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是房子数量。需要遍历每个房子,每个房子计算 $3$ 种颜色的成本。 - **空间复杂度**:$O(n)$,需要 $n \times 3$ 的 dp 数组存储状态。 ================================================ FILE: docs/solutions/0200-0299/palindrome-linked-list.md ================================================ # [0234. 回文链表](https://leetcode.cn/problems/palindrome-linked-list/) - 标签:栈、递归、链表、双指针 - 难度:简单 ## 题目链接 - [0234. 回文链表 - 力扣](https://leetcode.cn/problems/palindrome-linked-list/) ## 题目大意 **描述**:给定一个链表的头节点 `head`。 **要求**:判断该链表是否为回文链表。 **说明**: - 链表中节点数目在范围 $[1, 10^5]$ 内。 - $0 \le Node.val \le 9$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/03/pal1linked-list.jpg) ```python 输入:head = [1,2,2,1] 输出:True ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/03/pal2linked-list.jpg) ```python 输入:head = [1,2] 输出:False ``` ## 解题思路 ### 思路 1:利用数组 + 双指针 1. 利用数组,将链表元素依次存入。 2. 然后再使用两个指针,一个指向数组开始位置,一个指向数组结束位置。 3. 依次判断首尾对应元素是否相等,如果都相等,则为回文链表。如果不相等,则不是回文链表。 ### 思路 1:代码 ```python class Solution: def isPalindrome(self, head: ListNode) -> bool: nodes = [] p1 = head while p1 != None: nodes.append(p1.val) p1 = p1.next return nodes == nodes[::-1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0200-0299/palindrome-permutation-ii.md ================================================ # [0267. 回文排列 II](https://leetcode.cn/problems/palindrome-permutation-ii/) - 标签:哈希表、字符串、回溯 - 难度:中等 ## 题目链接 - [0267. 回文排列 II - 力扣](https://leetcode.cn/problems/palindrome-permutation-ii/) ## 题目大意 **描述**: 给定一个字符串 $s$。 **要求**: 返回「其重新排列组合后可能构成的所有回文字符串,并去除重复的组合」。 你可以按任意顺序返回答案。如果 $s$ 不能形成任何回文排列时,则返回一个空列表。 **说明**: - $1 \le s.length \le 16$。 - $s$ 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入: s = "aabb" 输出: ["abba", "baab"] ``` - 示例 2: ```python 输入: s = "abc" 输出: [] ``` ## 解题思路 ### 思路 1: 先统计每个字符出现次数。记字符串长度为 $n$,各字符计数为 $\{c_i\}$。如果出现次数为奇数的字符超过 $1$ 个,则无法构成回文,直接返回空列表。 如果至多 $1$ 个字符出现奇数次: - 将每个字符取一半次数(即 $\lfloor c_i/2 \rfloor$)构造「左半部分」可用的多重集合,记其总长度为 $m = \sum_i \lfloor c_i/2 \rfloor$。 - 通过回溯在该多重集合上生成所有不重复的半串 $h$(长度为 $m$),回溯过程中对每个字符仅按计数使用,避免因相同字符导致的重复。 - 若存在奇数计数字符,记其中点字符为 $mid$,否则 `mid = ""`。对每个半串 $h$,构造回文 $h + mid + h^R$。 正确性:回文由对称的两半与可选中点构成,穷举半串可唯一确定一个回文。去重依赖「基于计数的回溯」,不会对同一字符的相同选择顺序重复生成。 ### 思路 1:代码 ```python from typing import List from collections import Counter class Solution: def generatePalindromes(self, s: str) -> List[str]: # 统计每个字符的频次 freq = Counter(s) # 检查是否可构成回文:至多 1 个奇数频次 odd_chars = [ch for ch, cnt in freq.items() if cnt % 2 == 1] if len(odd_chars) > 1: return [] # 中间字符(若存在奇数频次) mid = odd_chars[0] if odd_chars else "" # 半串的可用字符次数(每种字符取一半) half_counts = {ch: cnt // 2 for ch, cnt in freq.items()} target_len = sum(half_counts.values()) path: List[str] = [] # 当前构造的半串 res: List[str] = [] # 结果集合 # 回溯基于计数,不会产生重复排列 def dfs() -> None: if len(path) == target_len: half = "".join(path) res.append(half + mid + half[::-1]) return for ch in sorted(half_counts.keys()): # 固定遍历顺序,稳定输出 if half_counts[ch] == 0: continue half_counts[ch] -= 1 path.append(ch) dfs() path.pop() half_counts[ch] += 1 dfs() return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:令 $m = \sum_i \lfloor c_i/2 \rfloor$。半串的唯一排列数为 $\dfrac{m!}{\prod_i (\lfloor c_i/2 \rfloor)!}$。对每个半串构造完整回文需要 $O(n)$ 拼接与拷贝,因此总复杂度为 $O\!\left(\dfrac{m!}{\prod_i (\lfloor c_i/2 \rfloor)!} \times n\right)$。在 $n \le 16$ 的约束下不会超时。 - **空间复杂度**:回溯深度与路径为 $O(m)$,计数表为 $O(\Sigma)$($\Sigma$ 为字符集大小,这里为小写字母)。不计输出的额外空间为 $O(m + \Sigma)$。 ================================================ FILE: docs/solutions/0200-0299/palindrome-permutation.md ================================================ # [0266. 回文排列](https://leetcode.cn/problems/palindrome-permutation/) - 标签:位运算、哈希表、字符串 - 难度:简单 ## 题目链接 - [0266. 回文排列 - 力扣](https://leetcode.cn/problems/palindrome-permutation/) ## 题目大意 **描述**: 给定一个字符串 $s$。 **要求**: 如果该字符串的某个排列是「回文串」,则返回 $true$;否则,返回 $false$。 **说明**: - $1 \le s.length \le 5000$。 - $s$ 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "code" 输出:false ``` - 示例 2: ```python 输入:s = "aab" 输出:true ``` ## 解题思路 ### 思路 1: 利用「字符出现次数的奇偶性」判断。记字符串长度为 $n$,字符集为小写字母(至多 $26$ 种)。一个字符串能被重排成回文,当且仅当至多有 $1$ 个字符的出现次数为奇数。 做法:用一个 $26$ 位的位掩码 `mask` 表示每个字符计数的奇偶性。遍历每个字符 $s[i]$:将对应位取反(异或 $1$),遍历完成后: - 如果 `mask == 0`(所有字符出现偶数次),必然可以构成回文; - 如果 `mask` 只有 $1$ 位为 $1$(即 $\text{popcount}(mask) = 1$),也可构成回文(该字符放在中间); - 否则不行。 ### 思路 1:代码 ```python class Solution: def canPermutePalindrome(self, s: str) -> bool: # 位掩码记录每个字符出现次数的奇偶性(仅小写字母) mask = 0 base = ord('a') for ch in s: bit = 1 << (ord(ch) - base) mask ^= bit # 对应位取反:出现一次翻转一次 # 条件:至多 1 位为 1(允许 0 位) return mask == 0 or (mask & (mask - 1)) == 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。单次遍历字符串,每个字符 $O(1)$ 更新位掩码。 - **空间复杂度**:$O(1)$。仅常数级变量,与 $n$ 和字符集大小无关(字符集固定为 $26$)。 ================================================ FILE: docs/solutions/0200-0299/peeking-iterator.md ================================================ # [0284. 窥视迭代器](https://leetcode.cn/problems/peeking-iterator/) - 标签:设计、数组、迭代器 - 难度:中等 ## 题目链接 - [0284. 窥视迭代器 - 力扣](https://leetcode.cn/problems/peeking-iterator/) ## 题目大意 **要求**: 设计一个迭代器,在集成现有迭代器拥有的 `hasNext` 和 `next` 操作的基础上,还额外支持 `peek` 操作。 实现 `PeekingIterator` 类: * `PeekingIterator(Iterator nums)` 使用指定整数迭代器 $nums$ 初始化迭代器。 * `int next()` 返回数组中的下一个元素,并将指针移动到下个元素处。 * `bool hasNext()` 如果数组中存在下一个元素,返回 $true$;否则,返回 $false$。 * `int peek()` 返回数组中的下一个元素,但不移动指针。 **说明**: - 注意:每种语言可能有不同的构造函数和迭代器 `Iterator`,但均支持 `int next()` 和 `boolean hasNext()` 函数。 - $1 \le nums.length \le 10^{3}$。 - $1 \le nums[i] \le 10^{3}$。 - 对 `next` 和 `peek` 的调用均有效。 - `next`、`hasNext` 和 `peek` 最多调用 $10^{3}$ 次。 - 进阶:你将如何拓展你的设计?使之变得通用化,从而适应所有的类型,而不只是整数型? **示例**: - 示例 1: ```python 输入: ["PeekingIterator", "next", "peek", "next", "next", "hasNext"] [[[1, 2, 3]], [], [], [], [], []] 输出: [null, 1, 2, 2, 3, false] 解释: PeekingIterator peekingIterator = new PeekingIterator([1, 2, 3]); // [1,2,3] peekingIterator.next(); // 返回 1 ,指针移动到下一个元素 [1,2,3] peekingIterator.peek(); // 返回 2 ,指针未发生移动 [1,2,3] peekingIterator.next(); // 返回 2 ,指针移动到下一个元素 [1,2,3] peekingIterator.next(); // 返回 3 ,指针移动到下一个元素 [1,2,3] peekingIterator.hasNext(); // 返回 False ``` - 示例 2: ```python 输入: 输出: ``` ## 解题思路 ### 思路 1:缓存下一个元素 使用缓存机制来实现 `peek` 操作。我们维护一个变量 $next\_val$ 来缓存下一个元素,以及一个布尔变量 $has\_next$ 来标记是否还有下一个元素。 具体步骤如下: 1. 在初始化时,调用底层迭代器的 `next()` 方法获取第一个元素,存储在 $next\_val$ 中 2. 如果底层迭代器还有元素,设置 $has\_next = true$,否则设置 $has\_next = false$ 3. `peek()` 方法直接返回 $next\_val$,不移动指针 4. `next()` 方法返回 $next\_val$,然后调用底层迭代器获取下一个元素更新 $next\_val$ 5. `hasNext()` 方法返回 $has\_next$ 的值 这种方法的时间复杂度为 $O(1)$,空间复杂度为 $O(1)$。 ### 思路 1:代码 ```python # Below is the interface for Iterator, which is already defined for you. # # class Iterator: # def __init__(self, nums): # """ # Initializes an iterator object to the beginning of a list. # :type nums: List[int] # """ # # def hasNext(self): # """ # Returns true if the iteration has more elements. # :rtype: bool # """ # # def next(self): # """ # Returns the next element in the iteration. # :rtype: int # """ class PeekingIterator: def __init__(self, iterator): """ Initialize your data structure here. :type iterator: Iterator """ # 保存底层迭代器 self.iterator = iterator # 缓存下一个元素 self.next_val = None # 标记是否还有下一个元素 self.has_next = False # 初始化时获取第一个元素 if self.iterator.hasNext(): self.next_val = self.iterator.next() self.has_next = True def peek(self): """ Returns the next element in the iteration without advancing the iterator. :rtype: int """ return self.next_val def next(self): """ :rtype: int """ # 保存当前要返回的值 result = self.next_val # 获取下一个元素 if self.iterator.hasNext(): self.next_val = self.iterator.next() self.has_next = True else: self.has_next = False self.next_val = None return result def hasNext(self): """ :rtype: bool """ return self.has_next # Your PeekingIterator object will be instantiated and called as such: # iter = PeekingIterator(Iterator(nums)) # while iter.hasNext(): # val = iter.peek() # Get the next element but not advance the iterator. # iter.next() # Should return the same value as [val]. ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$,所有操作(`peek`、`next`、`hasNext`)都是常数时间。 - **空间复杂度**:$O(1)$,只使用了常数额外空间来存储缓存的下一个元素和状态标志。 ================================================ FILE: docs/solutions/0200-0299/perfect-squares.md ================================================ # [0279. 完全平方数](https://leetcode.cn/problems/perfect-squares/) - 标签:广度优先搜索、数学、动态规划 - 难度:中等 ## 题目链接 - [0279. 完全平方数 - 力扣](https://leetcode.cn/problems/perfect-squares/) ## 题目大意 **描述**:给定一个正整数 $n$。从中找到若干个完全平方数(比如 $1, 4, 9, 16 …$),使得它们的和等于 $n$。 **要求**:返回和为 $n$ 的完全平方数的最小数量。 **说明**: - $1 \le n \le 10^4$。 **示例**: - 示例 1: ```python 输入:n = 12 输出:3 解释:12 = 4 + 4 + 4 ``` - 示例 2: ```python 输入:n = 13 输出:2 解释:13 = 4 + 9 ``` ## 解题思路 暴力枚举思路:对于小于 $n$ 的完全平方数,直接暴力枚举所有可能的组合,并且找到平方数个数最小的一个。 并且对于所有小于 $n$ 的完全平方数($k = 1, 4, 9, 16, ...$),存在公式:$ans(n) = min(ans(n - k) + 1), k = 1, 4, 9, 16 ...$ 即: **n 的完全平方数的最小数量 == n - k 的完全平方数的最小数量 + 1**。 我们可以使用递归解决这个问题。但是因为重复计算了中间解,会产生堆栈溢出。 那怎么解决重复计算问题和避免堆栈溢出? 我们可以转换一下思维。 1. 将 $n$ 作为根节点,构建一棵多叉数。 2. 从 $n$ 节点出发,如果一个小于 $n$ 的数刚好与 $n$ 相差一个平方数,则以该数为值构造一个节点,与 $n$ 相连。 那么求解和为 $n$ 的完全平方数的最小数量就变成了求解这棵树从根节点 $n$ 到节点 $0$ 的最短路径,或者说树的最小深度。 这个过程可以通过广度优先搜索来做。 ### 思路 1:广度优先搜索 1. 定义 $visited$ 为标记访问节点的 set 集合变量,避免重复计算。定义 $queue$ 为存放节点的队列。使用 $count$ 表示为树的最小深度,也就是和为 $n$ 的完全平方数的最小数量。 2. 首先,我们将 $n$ 标记为已访问,即 `visited.add(n)`。并将其加入队列 $queue$ 中,即 `queue.append(n)`。 3. 令 $count$ 加 $1$,表示最小深度加 $1$。然后依次将队列中的节点值取出。 4. 对于取出的节点值 $value$,遍历可能出现的平方数(即遍历 $[1, \sqrt{value} + 1]$ 中的数)。 5. 每次从当前节点值减去一个平方数,并将减完的数加入队列。 1. 如果此时的数等于 $0$,则满足题意,返回当前树的最小深度。 2. 如果此时的数不等于 $0$,则将其加入队列,继续查找。 ### 思路 1:代码 ```python class Solution: def numSquares(self, n: int) -> int: if n == 0: return 0 visited = set() queue = collections.deque([]) visited.add(n) queue.append(n) count = 0 while queue: // 最少步数 count += 1 size = len(queue) for _ in range(size): value = queue.pop() for i in range(1, int(math.sqrt(value)) + 1): x = value - i * i if x == 0: return count if x not in visited: queue.appendleft(x) visited.add(x) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \sqrt{n})$。 - **空间复杂度**:$O(n)$。 ### 思路 2:动态规划 我们可以将这道题转换为「完全背包问题」中恰好装满背包的方案数问题。 1. 将 $k = 1, 4, 9, 16, ...$ 看做是 $k$ 种物品,每种物品都可以无限次使用。 2. 将 $n$ 看做是背包的装载上限。 3. 这道题就变成了,从 $k$ 种物品中选择一些物品,装入装载上限为 $n$ 的背包中,恰好装满背包最少需要多少件物品。 ###### 1. 阶段划分 按照当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[w]$ 表示为:从完全平方数中挑选一些数,使其和恰好凑成 $w$ ,最少需要多少个完全平方数。 ###### 3. 状态转移方程 $dp[w] = min \lbrace dp[w], dp[w - num] + 1$ ###### 4. 初始条件 - 恰好凑成和为 $0$,最少需要 $0$ 个完全平方数。 - 默认情况下,在不使用完全平方数时,都不能恰好凑成和为 $w$ ,此时将状态值设置为一个极大值(比如 $n + 1$),表示无法凑成。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[w]$ 表示为:将物品装入装载上限为 $w$ 的背包中,恰好装满背包,最少需要多少件物品。 所以最终结果为 $dp[n]$。 1. 如果 $dp[n] \ne n + 1$,则说明:$dp[n]$ 为装入装载上限为 $n$ 的背包,恰好装满背包,最少需要的物品数量,则返回 $dp[n]$。 2. 如果 $dp[n] = n + 1$,则说明:无法恰好装满背包,则返回 $-1$。因为 $n$ 肯定能由 $n$ 个 $1$ 组成,所以这种情况并不会出现。 ### 思路 2:代码 ```python class Solution: def numSquares(self, n: int) -> int: dp = [n + 1 for _ in range(n + 1)] dp[0] = 0 for i in range(1, int(sqrt(n)) + 1): num = i * i for w in range(num, n + 1): dp[w] = min(dp[w], dp[w - num] + 1) if dp[n] != n + 1: return dp[n] return -1 ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times \sqrt{n})$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0200-0299/power-of-two.md ================================================ # [0231. 2 的幂](https://leetcode.cn/problems/power-of-two/) - 标签:位运算、递归、数学 - 难度:简单 ## 题目链接 - [0231. 2 的幂 - 力扣](https://leetcode.cn/problems/power-of-two/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:判断该整数 $n$ 是否是 $2$ 的幂次方。如果是,返回 `True`;否则,返回 `False`。 **说明**: - $-2^{31} \le n \le 2^{31} - 1$ **示例**: - 示例 1: ```python 输入:n = 1 输出:True 解释:2^0 = 1 ``` - 示例 2: ```python 输入:n = 16 输出:True 解释:2^4 = 16 ``` ## 解题思路 ### 思路 1:循环判断 1. 不断判断 $n$ 是否能整除 $2$。 1. 如果不能整除,则返回 `False`。 2. 如果能整除,则让 $n$ 整除 $2$,直到 $n < 2$。 2. 如果最后 $n == 1$,则返回 `True`,否则则返回 `False`。 ### 思路 1:代码 ```python class Solution: def isPowerOfTwo(self, n: int) -> bool: if n <= 0: return False while n % 2 == 0: n //= 2 return n == 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log_2 n)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:数论判断 因为 $n$ 能取的最大值为 $2^{31}-1$。我们可以计算出:在 $n$ 的范围内,$2$ 的幂次方最大为 $2^{30} = 1073741824$。 因为 $2$ 为质数,则 $2^{30}$ 的除数只有 $2^0, 2^1, …, 2^{30}$。所以如果 $n$ 为 $2$ 的幂次方,则 $2^{30}$ 肯定能被 $n$ 整除,直接判断即可。 ### 思路 2:代码 ```python class Solution: def isPowerOfTwo(self, n: int) -> bool: return n > 0 and 1073741824 % n == 0 ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(1)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/product-of-array-except-self.md ================================================ # [0238. 除自身以外数组的乘积](https://leetcode.cn/problems/product-of-array-except-self/) - 标签:数组、前缀和 - 难度:中等 ## 题目链接 - [0238. 除自身以外数组的乘积 - 力扣](https://leetcode.cn/problems/product-of-array-except-self/) ## 题目大意 **描述**:给定一个数组 nums。 **要求**:返回数组 $answer$,其中 $answer[i]$ 等于 $nums$ 中除 $nums[i]$ 之外其余各元素的乘积。 **说明**: - 题目数据保证数组 $nums$ 之中任意元素的全部前缀元素和后缀的乘积都在 $32$ 位整数范围内。 - 请不要使用除法,且在 $O(n)$ 时间复杂度内解决问题。 - **进阶**:在 $O(1)$ 的额外空间复杂度内完成这个题目。 - $2 \le nums.length \le 10^5$。 - $-30 \le nums[i] \le 30$。 **示例**: - 示例 1: ```python 输入: nums = [1,2,3,4] 输出: [24,12,8,6] ``` - 示例 2: ```python 输入: nums = [-1,1,0,-3,3] 输出: [0,0,9,0,0] ``` ## 解题思路 ### 思路 1:两次遍历 1. 构造一个答案数组 $res$,长度和数组 $nums$ 长度一致。 2. 先从左到右遍历一遍 $nums$ 数组,将 $nums[i]$ 左侧的元素乘积累积起来,存储到 $res$ 数组中。 3. 再从右到左遍历一遍,将 $nums[i]$ 右侧的元素乘积累积起来,再乘以原本 $res[i]$ 的值,即为 $nums$ 中除了 $nums[i]$ 之外的其他所有元素乘积。 ### 思路 1:代码 ```python class Solution: def productExceptSelf(self, nums: List[int]) -> List[int]: size = len(nums) res = [1 for _ in range(size)] left = 1 for i in range(size): res[i] *= left left *= nums[i] right = 1 for i in range(size-1, -1, -1): res[i] *= right right *= nums[i] return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/rectangle-area.md ================================================ # [0223. 矩形面积](https://leetcode.cn/problems/rectangle-area/) - 标签:几何、数学 - 难度:中等 ## 题目链接 - [0223. 矩形面积 - 力扣](https://leetcode.cn/problems/rectangle-area/) ## 题目大意 给定两个矩形的左下角坐标、右上角坐标 `(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)`。其中 `(ax1, ay1)` 表示第一个矩形左下角坐标,`(ax2, ay2)` 表示第一个矩形右上角坐标,`(bx1, by1)` 表示第二个矩形左下角坐标,`(bx2, by2)` 表示第二个矩形右上角坐标。 要求:计算出两个矩形覆盖的总面积。 ## 解题思路 两个矩形覆盖的总面积 = 第一个矩形面积 + 第二个矩形面积 - 重叠部分面积。 需要分别计算出两个矩形面积,还有求出相交部分的长、宽,并计算出对应重叠部分的面积。 ## 代码 ```python class Solution: def computeArea(self, ax1: int, ay1: int, ax2: int, ay2: int, bx1: int, by1: int, bx2: int, by2: int) -> int: area_a = (ax2 - ax1) * (ay2 - ay1) area_b = (bx2 - bx1) * (by2 - by1) overlap_width = max(0, min(ax2, bx2) - max(ax1, bx1)) overlap_height = max(0, min(ay2, by2) - max(ay1, by1)) area_overlap = overlap_width * overlap_height return area_a + area_b - area_overlap ``` ================================================ FILE: docs/solutions/0200-0299/remove-linked-list-elements.md ================================================ # [0203. 移除链表元素](https://leetcode.cn/problems/remove-linked-list-elements/) - 标签:递归、链表 - 难度:简单 ## 题目链接 - [0203. 移除链表元素 - 力扣](https://leetcode.cn/problems/remove-linked-list-elements/) ## 题目大意 **描述**:给定一个链表的头节点 `head` 和一个值 `val`。 **要求**:删除链表中值为 `val` 的节点,并返回新的链表头节点。 **说明**: - 列表中的节点数目在范围 $[0, 10^4]$ 内。 - $1 \le Node.val \le 50$。 - $0 \le val \le 50$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/06/removelinked-list.jpg) ```python 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5] ``` - 示例 2: ```python 输入:head = [], val = 1 输出:[] ``` ## 解题思路 ### 思路 1:迭代 - 使用两个指针 `prev` 和 `curr`。`prev` 指向前一节点和当前节点,`curr` 指向当前节点。 - 从前向后遍历链表,遇到值为 `val` 的节点时,将 `prev` 的 `next` 指针指向当前节点的下一个节点,继续递归遍历。没有遇到则将 `prev` 指针向后移动一步。 - 向右移动 `curr`,继续遍历。 需要注意的是:因为要删除的节点可能包含了头节点,我们可以考虑在遍历之前,新建一个头节点,让其指向原来的头节点。这样,最终如果删除的是头节点,则直接删除原头节点,然后最后返回新建头节点的下一个节点即可。 ### 思路 1:代码 ```python class Solution: def removeElements(self, head: ListNode, val: int) -> ListNode: newHead = ListNode(0, head) newHead.next = head prev, curr = newHead, head while curr: if curr.val == val: prev.next = curr.next else: prev = curr curr = curr.next return newHead.next ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/reverse-linked-list.md ================================================ # [0206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) - 标签:递归、链表 - 难度:简单 ## 题目链接 - [0206. 反转链表 - 力扣](https://leetcode.cn/problems/reverse-linked-list/) ## 题目大意 **描述**:给定一个单链表的头节点 `head`。 **要求**:将该单链表进行反转。可以迭代或递归地反转链表。 **说明**: - 链表中节点的数目范围是 $[0, 5000]$。 - $-5000 \le Node.val \le 5000$。 **示例**: - 示例 1: ```python 输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1] 解释: 翻转前 1->2->3->4->5->NULL 反转后 5->4->3->2->1->NULL ``` ## 解题思路 ### 思路 1:迭代 1. 使用两个指针 `cur` 和 `pre` 进行迭代。`pre` 指向 `cur` 前一个节点位置。初始时,`pre` 指向 `None`,`cur` 指向 `head`。 2. 将 `pre` 和 `cur` 的前后指针进行交换,指针更替顺序为: 1. 使用 `next` 指针保存当前节点 `cur` 的后一个节点,即 `next = cur.next`; 2. 断开当前节点 `cur` 的后一节点链接,将 `cur` 的 `next` 指针指向前一节点 `pre`,即 `cur.next = pre`; 3. `pre` 向前移动一步,移动到 `cur` 位置,即 `pre = cur`; 4. `cur` 向前移动一步,移动到之前 `next` 指针保存的位置,即 `cur = next`。 3. 继续执行第 2 步中的 1、2、3、4。 4. 最后等到 `cur` 遍历到链表末尾,即 `cur == None`,时,`pre` 所在位置就是反转后链表的头节点,返回新的头节点 `pre`。 使用迭代法反转链表的示意图如下所示: ![迭代法反转链表](https://qcdn.itcharge.cn/images/20220111133639.png) ### 思路 1:代码 ```python class Solution: def reverseList(self, head: ListNode) -> ListNode: pre = None cur = head while cur != None: next = cur.next cur.next = pre pre = cur cur = next return pre ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:递归 具体做法如下: 1. 首先定义递归函数含义为:将链表反转,并返回反转后的头节点。 2. 然后从 `head.next` 的位置开始调用递归函数,即将 `head.next` 为头节点的链表进行反转,并返回该链表的头节点。 3. 递归到链表的最后一个节点,将其作为最终的头节点,即为 `new_head`。 4. 在每次递归函数返回的过程中,改变 `head` 和 `head.next` 的指向关系。也就是将 `head.next` 的`next` 指针先指向当前节点 `head`,即 `head.next.next = head `。 5. 然后让当前节点 `head` 的 `next` 指针指向 `None`,从而实现从链表尾部开始的局部反转。 6. 当递归从末尾开始顺着递归栈的退出,从而将整个链表进行反转。 7. 最后返回反转后的链表头节点 `new_head`。 使用递归法反转链表的示意图如下所示: ![递归法反转链表](https://qcdn.itcharge.cn/images/20220111134246.png) ### 思路 2:代码 ```python class Solution: def reverseList(self, head: ListNode) -> ListNode: if head == None or head.next == None: return head new_head = self.reverseList(head.next) head.next.next = head head.next = None return new_head ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$ - **空间复杂度**:$O(n)$。最多需要 $n$ 层栈空间。 ## 参考资料 - 【题解】[反转链表 - 反转链表 - 力扣](https://leetcode.cn/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-by-leetcode-solution-d1k2/) - 【题解】[【反转链表】:双指针,递归,妖魔化的双指针 - 反转链表 - 力扣(LeetCode)](https://leetcode.cn/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-shuang-zhi-zhen-di-gui-yao-mo-/) ================================================ FILE: docs/solutions/0200-0299/search-a-2d-matrix-ii.md ================================================ # [0240. 搜索二维矩阵 II](https://leetcode.cn/problems/search-a-2d-matrix-ii/) - 标签:二分查找、分治算法 - 难度:中等 ## 题目链接 - [0240. 搜索二维矩阵 II - 力扣](https://leetcode.cn/problems/search-a-2d-matrix-ii/) ## 题目大意 **描述**:给定一个 $m \times n$ 大小的有序整数矩阵 $matrix$。$matrix$ 中的每行元素从左到右升序排列,每列元素从上到下升序排列。再给定一个目标值 $target$。 **要求**:判断矩阵中是否可以找到 $target$,如果可以找到 $target$,返回 `True`,否则返回 `False`。 **说明**: - $m == matrix.length$。 - $n == matrix[i].length$。 - $1 \le n, m \le 300$。 - $-10^9 \le matrix[i][j] \le 10^9$。 - 每行的所有元素从左到右升序排列。 - 每列的所有元素从上到下升序排列。 - $-10^9 \le target \le 10^9$。 **示例**: - 示例 1: ![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/11/25/searchgrid2.jpg) ```python 输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5 输出:True ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/11/25/searchgrid.jpg) ```python 输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20 输出:False ``` ## 解题思路 ### 思路 1:二分查找 矩阵是有序的,可以考虑使用二分查找来做。 1. 迭代对角线元素,假设对角线元素的坐标为 $(row, col)$。把数组元素按对角线分为右上角部分和左下角部分。 2. 对于当前对角线元素右侧第 $row$ 行、对角线元素下侧第 $col$ 列分别进行二分查找。 1. 如果找到目标,直接返回 `True`。 2. 如果找不到目标,则缩小范围,继续查找。 3. 直到所有对角线元素都遍历完,依旧没找到,则返回 `False`。 ### 思路 1:代码 ```python class Solution: def diagonalBinarySearch(self, matrix, diagonal, target): left = 0 right = diagonal while left < right: mid = left + (right - left) // 2 if matrix[mid][mid] < target: left = mid + 1 else: right = mid return left def rowBinarySearch(self, matrix, begin, cols, target): left = begin right = cols while left < right: mid = left + (right - left) // 2 if matrix[begin][mid] < target: left = mid + 1 elif matrix[begin][mid] > target: right = mid - 1 else: left = mid break return begin <= left <= cols and matrix[begin][left] == target def colBinarySearch(self, matrix, begin, rows, target): left = begin + 1 right = rows while left < right: mid = left + (right - left) // 2 if matrix[mid][begin] < target: left = mid + 1 elif matrix[mid][begin] > target: right = mid - 1 else: left = mid break return begin <= left <= rows and matrix[left][begin] == target def searchMatrix(self, matrix, target: int) -> bool: rows = len(matrix) if rows == 0: return False cols = len(matrix[0]) if cols == 0: return False min_val = min(rows, cols) index = self.diagonalBinarySearch(matrix, min_val - 1, target) if matrix[index][index] == target: return True for i in range(index + 1): row_search = self.rowBinarySearch(matrix, i, cols - 1, target) col_search = self.colBinarySearch(matrix, i, rows - 1, target) if row_search or col_search: return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(min(m, n) \times (\log_2 m + \log_2 n))$,其中 $m$ 是矩阵的行数,$n$ 是矩阵的列数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/serialize-and-deserialize-binary-tree.md ================================================ # [0297. 二叉树的序列化与反序列化](https://leetcode.cn/problems/serialize-and-deserialize-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、设计、字符串、二叉树 - 难度:困难 ## 题目链接 - [0297. 二叉树的序列化与反序列化 - 力扣](https://leetcode.cn/problems/serialize-and-deserialize-binary-tree/) ## 题目大意 **要求**:设计一个算法,来实现二叉树的序列化与反序列化。 **说明**: - 不限定序列化 / 反序列化算法执行逻辑,只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。 - 树中结点数在范围 $[0, 10^4]$ 内。 - $-1000 \le Node.val \le 1000$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/09/15/serdeser.jpg) ```python 输入:root = [1,2,3,null,null,4,5] 输出:[1,2,3,null,null,4,5] ``` - 示例 2: ```python 输入:root = [1,2] 输出:[1,2] ``` ## 解题思路 ### 思路 1:深度优先搜索 #### 1. 序列化:将二叉树转为字符串数据表示 1. 按照前序顺序递归遍历二叉树,并将根节点跟左右子树的值链接起来(中间用 `,` 隔开)。 > 注意:如果遇到空节点,则将其标记为 `None`,这样在反序列化时才能唯一确定一棵二叉树。 #### 2. 反序列化:将字符串数据转为二叉树结构 1. 先将字符串按 `,` 分割成数组。然后递归处理每一个元素。 2. 从数组左侧取出一个元素。 1. 如果当前元素为 `None`,则返回 `None`。 2. 如果当前元素不为空,则新建一个二叉树节点作为根节点,保存值为当前元素值。并递归遍历左右子树,不断重复从数组中取出元素,进行判断。 3. 最后返回当前根节点。 ### 思路 1:代码 ```python class Codec: def serialize(self, root): """Encodes a tree to a single string. :type root: TreeNode :rtype: str """ if not root: return 'None' return str(root.val) + ',' + str(self.serialize(root.left)) + ',' + str(self.serialize(root.right)) def deserialize(self, data): """Decodes your encoded data to tree. :type data: str :rtype: TreeNode """ def dfs(datalist): val = datalist.pop(0) if val == 'None': return None root = TreeNode(int(val)) root.left = dfs(datalist) root.right = dfs(datalist) return root datalist = data.split(',') return dfs(datalist) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为二叉树的节点数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0200-0299/shortest-palindrome.md ================================================ # [0214. 最短回文串](https://leetcode.cn/problems/shortest-palindrome/) - 标签:字符串、字符串匹配、哈希函数、滚动哈希 - 难度:困难 ## 题目链接 - [0214. 最短回文串 - 力扣](https://leetcode.cn/problems/shortest-palindrome/) ## 题目大意 **描述**: 给定一个字符串 $s$,你可以通过在字符串前面添加字符将其转换为回文串。 **要求**: 找到并返回可以用这种方式转换的最短回文串。 **说明**: - $0 \le s.length \le 5 \times 10^{4}$。 - s 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "aacecaaa" 输出:"aaacecaaa" ``` - 示例 2: ```python 输入:s = "abcd" 输出:"dcbabcd" ``` ## 解题思路 ### 思路 1:KMP 算法 这道题的关键在于找到字符串 $s$ 的最长回文前缀。如果我们能找到字符串 $s$ 的最长回文前缀,那么剩余部分的反转就是我们需要添加的前缀。 具体思路如下: 1. **构造新字符串**:将字符串 $s$ 和其反转字符串 $s'$ 用特殊字符连接,构造新字符串 `pattern = s + '\#' + s'`。 2. **使用 KMP 算法**:对新字符串 $pattern$ 计算 next 数组,找到最长公共前后缀。 3. **找到最长回文前缀**:next 数组的最后一个值 $next[len-1]$ 就是字符串 $s$ 的最长回文前缀长度。 4. **构造结果**:将 $s$ 中非回文前缀部分反转后添加到 $s$ 前面。 算法步骤: - 设字符串 $s$ 的长度为 $n$,最长回文前缀长度为 $k$。 - 需要添加的字符为 $s[n-1:n-k-1:-1]$(即从第 $n-k-1$ 个字符到第 $n-1$ 个字符的反转)。 - 最终结果为 $s[n-1:n-k-1:-1] + s$。 ### 思路 1:代码 ```python class Solution: def shortestPalindrome(self, s: str) -> str: if not s: return "" # 构造新字符串:s + '#' + s的反转 reversed_s = s[::-1] pattern = s + '#' + reversed_s # 计算 next 数组(KMP 算法) n = len(pattern) next_array = [0] * n # 计算 next 数组 for i in range(1, n): j = next_array[i - 1] # 如果不匹配,回退到前一个匹配位置 while j > 0 and pattern[i] != pattern[j]: j = next_array[j - 1] # 如果匹配,next 值加 1 if pattern[i] == pattern[j]: j += 1 next_array[i] = j # next_array[n-1] 就是 s 的最长回文前缀长度 max_palindrome_length = next_array[n - 1] # 构造最短回文串 # 将非回文前缀部分反转后添加到前面 return s[max_palindrome_length:][::-1] + s ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。KMP 算法计算 $next$ 数组的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$,需要额外的 $O(n)$ 空间存储 $next$ 数组和构造的新字符串。 ================================================ FILE: docs/solutions/0200-0299/shortest-word-distance-ii.md ================================================ # [0244. 最短单词距离 II](https://leetcode.cn/problems/shortest-word-distance-ii/) - 标签:设计、数组、哈希表、双指针、字符串 - 难度:中等 ## 题目链接 - [0244. 最短单词距离 II - 力扣](https://leetcode.cn/problems/shortest-word-distance-ii/) ## 题目大意 **要求**: 请设计一个类,使该类的构造函数能够接收一个字符串数组。然后再实现一个方法,该方法能够分别接收两个单词,并返回列表中这两个单词之间的最短距离。 实现 `WordDistanc` 类: - `WordDistance(String[] wordsDict)` 用字符串数组 $wordsDict$ 初始化对象。 * `int shortest(String word1, String word2)` 返回数组 $worddict$ 中 $word1$ 和 $word2$ 之间的最短距离。 **说明**: - $1 \le wordsDict.length <= 3 \times 10^{4}$ - $1 \le wordsDict[i].length \le 10$。 * $wordsDict[i]$ 由小写英文字母组成。 * $word1$ 和 $word2$ 在数组 $wordsDict$ 中。 * $word1 \ne word2$。 * `shortest` 操作次数不大于 $5000$。 **示例**: - 示例 1: ```python 输入: ["WordDistance", "shortest", "shortest"] [[["practice", "makes", "perfect", "coding", "makes"]], ["coding", "practice"], ["makes", "coding"]] 输出: [null, 3, 1] 解释: WordDistance wordDistance = new WordDistance(["practice", "makes", "perfect", "coding", "makes"]); wordDistance.shortest("coding", "practice"); // 返回 3 wordDistance.shortest("makes", "coding"); // 返回 1 ``` ## 解题思路 ### 思路 1:哈希表 + 双指针 由于 `shortest` 方法会被多次调用,我们需要在构造函数中预处理数据,将每个单词的所有位置索引存储起来,这样在查询时可以快速找到两个单词的所有位置,然后使用双指针方法计算最短距离。 具体步骤如下: 1. 在构造函数中,遍历 $wordsDict$ 数组,使用哈希表 $word\_positions$ 记录每个单词 $word$ 在数组中的所有位置索引。 2. 在 `shortest` 方法中,获取 $word1$ 和 $word2$ 的所有位置列表 $positions1$ 和 $positions2$。 3. 使用双指针 $i$ 和 $j$ 分别指向 $positions1$ 和 $positions2$ 的当前位置。 4. 比较 $positions1[i]$ 和 $positions2[j]$ 的距离,移动较小位置的指针。 5. 重复步骤 4 直到遍历完所有位置,返回最小距离。 这种方法的时间复杂度为 $O(m + n)$,其中 $m$ 和 $n$ 分别是 $word1$ 和 $word2$ 在数组中的出现次数。 ### 思路 1:代码 ```python class WordDistance: def __init__(self, wordsDict: List[str]): # 使用哈希表存储每个单词的所有位置索引 self.word_positions = {} # 遍历数组,记录每个单词的位置 for i, word in enumerate(wordsDict): if word not in self.word_positions: self.word_positions[word] = [] self.word_positions[word].append(i) def shortest(self, word1: str, word2: str) -> int: # 获取两个单词的所有位置列表 positions1 = self.word_positions[word1] positions2 = self.word_positions[word2] # 初始化双指针和最小距离 i, j = 0, 0 min_distance = float('inf') # 使用双指针遍历两个位置列表 while i < len(positions1) and j < len(positions2): # 计算当前位置的距离 distance = abs(positions1[i] - positions2[j]) min_distance = min(min_distance, distance) # 移动较小位置的指针 if positions1[i] < positions2[j]: i += 1 else: j += 1 return min_distance # Your WordDistance object will be instantiated and called as such: # obj = WordDistance(wordsDict) # param_1 = obj.shortest(word1,word2) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - 构造函数:$O(n)$,其中 $n$ 是 $wordsDict$ 的长度。 - `shortest` 方法:$O(m + n)$,其中 $m$ 和 $n$ 分别是 $word1$ 和 $word2$ 在数组中的出现次数。 - **空间复杂度**:$O(n)$,用于存储每个单词的位置索引。 ================================================ FILE: docs/solutions/0200-0299/shortest-word-distance-iii.md ================================================ # [0245. 最短单词距离 III](https://leetcode.cn/problems/shortest-word-distance-iii/) - 标签:数组、字符串 - 难度:中等 ## 题目链接 - [0245. 最短单词距离 III - 力扣](https://leetcode.cn/problems/shortest-word-distance-iii/) ## 题目大意 **描述**: 给定一个字符串数组 $wordsDict$ 和两个字符串 $word1$ 和 $word2$。 **要求**: 返回这两个单词在列表中出现的最短距离。 **说明**: - 注意:$word1$ 和 $word2$ 是有可能相同的,并且它们将分别表示为列表中两个独立的单词。 - $1 \le wordsDict.length \le 10^{5}$。 - $1 \le wordsDict[i].length \le 10$。 - $wordsDict[i]$ 由小写英文字母组成。 - $word1$ 和 $word2$ 都在 $wordsDict$ 中。 **示例**: - 示例 1: ```python 输入:wordsDict = ["practice", "makes", "perfect", "coding", "makes"], word1 = "makes", word2 = "coding" 输出:1 ``` - 示例 2: ```python 输入:wordsDict = ["practice", "makes", "perfect", "coding", "makes"], word1 = "makes", word2 = "makes" 输出:3 ``` ## 解题思路 ### 思路 1:双指针 这道题与前面两题的区别在于 $word1$ 和 $word2$ 可能相同。当两个单词相同时,我们需要找到同一个单词在数组中的两个不同位置之间的最短距离。 具体步骤如下: 1. 遍历数组 $wordsDict$,维护两个指针 $index1$ 和 $index2$ 分别记录 $word1$ 和 $word2$ 的最新位置。 2. 当遇到 $word1$ 时: - 如果 $word1 \neq word2$,直接更新 $index1$ 为当前位置。 - 如果 $word1 = word2$,需要特殊处理:先将 $index2$ 更新为之前的 $index1$,再将 $index1$ 更新为当前位置。 3. 当遇到 $word2$ 且 $word1 \neq word2$ 时,更新 $index2$ 为当前位置。 4. 当 $index1$ 和 $index2$ 都有效且 $index1 \neq index2$ 时,计算距离 $|index1 - index2|$ 并更新最小距离。 5. 最终返回最小距离。 这种方法只需要遍历一次数组,时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。 ### 思路 1:代码 ```python class Solution: def shortestWordDistance(self, wordsDict: List[str], word1: str, word2: str) -> int: # 初始化两个指针,-1 表示还未找到对应的单词 index1, index2 = -1, -1 # 初始化最小距离为数组长度 min_distance = len(wordsDict) # 遍历数组 for i in range(len(wordsDict)): # 如果当前单词是 word1 if wordsDict[i] == word1: # 如果 word1 和 word2 相同,需要特殊处理 if word1 == word2: # 先更新 index2 为之前的 index1,再更新 index1 为当前位置 if index1 != -1: index2 = index1 index1 = i else: # 如果不同,直接更新 index1 index1 = i # 如果当前单词是 word2 且与 word1 不同 elif wordsDict[i] == word2: index2 = i # 如果两个单词都找到了且不是同一个位置,计算距离并更新最小值 if index1 != -1 and index2 != -1 and index1 != index2: min_distance = min(min_distance, abs(index1 - index2)) return min_distance ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $wordsDict$ 的长度。我们只需要遍历一次数组。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0200-0299/shortest-word-distance.md ================================================ # [0243. 最短单词距离](https://leetcode.cn/problems/shortest-word-distance/) - 标签:数组、字符串 - 难度:简单 ## 题目链接 - [0243. 最短单词距离 - 力扣](https://leetcode.cn/problems/shortest-word-distance/) ## 题目大意 **描述**: 给定一个字符串数组 $wordDict$ 和两个已经存在于该数组中的不同的字符串 $word1$ 和 $word2$。 **要求**: 返回列表中这两个单词之间的最短距离。 **说明**: - $1 \le wordsDict.length \le 3 \times 10^{4}$。 - $1 \le wordsDict[i].length \le 10$。 - $wordsDict[i]$ 由小写英文字母组成。 - $word1$ 和 $word2$ 在 $wordsDict$ 中。 - $word1 \ne word2$。 **示例**: - 示例 1: ```python 输入: wordsDict = ["practice", "makes", "perfect", "coding", "makes"], word1 = "coding", word2 = "practice" 输出: 3 ``` - 示例 2: ```python 输入: wordsDict = ["practice", "makes", "perfect", "coding", "makes"], word1 = "makes", word2 = "coding" 输出: 1 ``` ## 解题思路 ### 思路 1:双指针 使用双指针的方法来解决这个问题。我们可以维护两个指针 $i$ 和 $j$,分别指向 $word1$ 和 $word2$ 在数组中的位置。 具体步骤如下: 1. 遍历数组 $wordsDict$,当遇到 $word1$ 时,更新指针 $i$ 为当前位置 2. 当遇到 $word2$ 时,更新指针 $j$ 为当前位置 3. 当 $i$ 和 $j$ 都有效时(即 $i \neq -1$ 且 $j \neq -1$),计算距离 $|i - j|$ 并更新最小距离 4. 最终返回最小距离 这种方法只需要遍历一次数组,时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。 ### 思路 1:代码 ```python class Solution: def shortestDistance(self, wordsDict: List[str], word1: str, word2: str) -> int: # 初始化两个指针,-1 表示还未找到对应的单词 index1, index2 = -1, -1 # 初始化最小距离为数组长度 min_distance = len(wordsDict) # 遍历数组 for i in range(len(wordsDict)): # 如果当前单词是 word1,更新 index1 if wordsDict[i] == word1: index1 = i # 如果当前单词是 word2,更新 index2 elif wordsDict[i] == word2: index2 = i # 如果两个单词都找到了,计算距离并更新最小值 if index1 != -1 and index2 != -1: min_distance = min(min_distance, abs(index1 - index2)) return min_distance ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $wordsDict$ 的长度。我们只需要遍历一次数组。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0200-0299/single-number-iii.md ================================================ # [0260. 只出现一次的数字 III](https://leetcode.cn/problems/single-number-iii/) - 标签:位运算、数组 - 难度:中等 ## 题目链接 - [0260. 只出现一次的数字 III - 力扣](https://leetcode.cn/problems/single-number-iii/) ## 题目大意 **描述**:给定一个整数数组 $nums$。$nums$ 中恰好有两个元素只出现一次,其余所有元素均出现两次。 **要求**:找出只出现一次的那两个元素。可以按任意顺序返回答案。要求时间复杂度是 $O(n)$,空间复杂度是 $O(1)$。 **说明**: - $2 \le nums.length \le 3 \times 10^4$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 - 除两个只出现一次的整数外,$nums$ 中的其他数字都出现两次。 **示例**: - 示例 1: ```python 输入:nums = [1,2,1,3,2,5] 输出:[3,5] 解释:[5, 3] 也是有效的答案。 ``` - 示例 2: ```python 输入:nums = [-1,0] 输出:[-1,0] ``` ## 解题思路 ### 思路 1:位运算 求解这道题之前,我们先来看看如何求解「一个数组中除了某个元素只出现一次以外,其余每个元素均出现两次。」即「[136. 只出现一次的数字](https://leetcode.cn/problems/single-number/)」问题。 我们可以对所有数不断进行异或操作,最终可得到单次出现的元素。 下面我们再来看这道题。 如果数组中有两个数字只出现一次,其余每个元素均出现两次。那么经过全部异或运算。我们可以得到只出现一次的两个数字的异或结果。 根据异或结果的性质,异或运算中如果某一位上为 $1$,则说明异或的两个数在该位上是不同的。根据这个性质,我们将数字分为两组: 1. 一组是和该位为 $0$ 的数字, 2. 一组是该位为 $1$ 的数字。 然后将这两组分别进行异或运算,就可以得到最终要求的两个数字。 ### 思路 1:代码 ```python class Solution: def singleNumbers(self, nums: List[int]) -> List[int]: all_xor = 0 for num in nums: all_xor ^= num # 获取所有异或中最低位的 1 mask = 1 while all_xor & mask == 0: mask <<= 1 a_xor, b_xor = 0, 0 for num in nums: if num & mask == 0: a_xor ^= num else: b_xor ^= num return a_xor, b_xor ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 中的元素个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0200-0299/sliding-window-maximum.md ================================================ # [0239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/) - 标签:队列、数组、滑动窗口、单调队列、堆(优先队列) - 难度:困难 ## 题目链接 - [0239. 滑动窗口最大值 - 力扣](https://leetcode.cn/problems/sliding-window-maximum/) ## 题目大意 **描述**:给定一个整数数组 `nums`,再给定一个整数 `k`,表示为大小为 `k` 的滑动窗口从数组的最左侧移动到数组的最右侧。我们只能看到滑动窗口内的 `k` 个数字,滑动窗口每次只能向右移动一位。 **要求**:返回滑动窗口中的最大值。 **说明**: - $1 \le nums.length \le 10^5$。 - $-10^4 \le nums[i] \le 10^4$。 - $1 \le k \le nums.length$。 **示例**: - 示例 1: ```python 输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值 --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7 ``` - 示例 2: ```python 输入:nums = [1], k = 1 输出:[1] ``` ## 解题思路 暴力求解的话,需要使用二重循环遍历,其时间复杂度为 $O(n * k)$。根据题目给定的数据范围,肯定会超时。 我们可以使用优先队列来做。 ### 思路 1:优先队列 1. 初始的时候将前 `k` 个元素加入优先队列的二叉堆中。存入优先队列的是数组值与索引构成的元组。优先队列将数组值作为优先级。 2. 然后滑动窗口从第 `k` 个元素开始遍历,将当前数组值和索引的元组插入到二叉堆中。 3. 当二叉堆堆顶元素的索引已经不在滑动窗口的范围中时,即 `q[0][1] <= i - k` 时,不断删除堆顶元素,直到最大值元素的索引在滑动窗口的范围中。 4. 将最大值加入到答案数组中,继续向右滑动。 5. 滑动结束时,输出答案数组。 ### 思路 1:代码 ```python class Solution: def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: size = len(nums) q = [(-nums[i], i) for i in range(k)] heapq.heapify(q) res = [-q[0][0]] for i in range(k, size): heapq.heappush(q, (-nums[i], i)) while q[0][1] <= i - k: heapq.heappop(q) res.append(-q[0][0]) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log_2n)$。 - **空间复杂度**:$O(k)$。 ================================================ FILE: docs/solutions/0200-0299/strobogrammatic-number-ii.md ================================================ # [0247. 中心对称数 II](https://leetcode.cn/problems/strobogrammatic-number-ii/) - 标签:递归、数组、字符串 - 难度:中等 ## 题目链接 - [0247. 中心对称数 II - 力扣](https://leetcode.cn/problems/strobogrammatic-number-ii/) ## 题目大意 **描述**: 给定一个整数 $n$。 **要求**: 返回所有长度为 $n$ 的「中心对称数」。你可以以任何顺序返回答案。 **说明**: - 中心对称数:指一个数字在旋转了 180 度之后看起来依旧相同的数字(或者上下颠倒地看)。 - $1 \le n \le 14$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:["11","69","88","96"] ``` - 示例 2: ```python 输入:n = 1 输出:["0","1","8"] ``` ## 解题思路 ### 思路 1:递归构建 中心对称数是指旋转 180 度后看起来相同的数字。只有特定的数字可以构成中心对称数: - $0$ 旋转后还是 $0$。 - $1$ 旋转后还是 $1$。 - $6$ 旋转后变成 $9$。 - $8$ 旋转后还是 $8$。 - $9$ 旋转后变成 $6$。 使用递归方法从外向内构建中心对称数: - 对于长度为 $n$ 的中心对称数,我们从两端开始构建。 - 每次选择一对可以相互旋转的数字 $(a, b)$,其中 $a$ 旋转后变成 $b$。 - 递归处理中间部分,直到构建完成。 - 特殊情况:当 $n$ 为奇数时,中间位置只能放置 $0, 1, 8$;当 $n$ 为偶数时,从空字符串开始构建。 - 注意:不能以 $0$ 开头(除非 $n = 1$)。 ### 思路 1:代码 ```python class Solution: def findStrobogrammatic(self, n: int) -> List[str]: # 定义中心对称数字的映射关系 strobogrammatic_pairs = [ ('0', '0'), ('1', '1'), ('6', '9'), ('8', '8'), ('9', '6') ] def build_strobogrammatic(m): # 递归构建长度为 m 的中心对称数 if m == 0: return [""] # 空字符串 if m == 1: return ["0", "1", "8"] # 单个数字的情况 # 递归构建中间部分 inner = build_strobogrammatic(m - 2) result = [] for pair in strobogrammatic_pairs: left, right = pair for inner_str in inner: # 构建完整的中心对称数 result.append(left + inner_str + right) return result # 获取所有可能的中心对称数 candidates = build_strobogrammatic(n) # 过滤掉以 0 开头的数字(除非 n = 1) if n == 1: return candidates else: return [num for num in candidates if not num.startswith('0')] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(5^{n/2} \times n)$,其中 $n$ 是目标长度。每次递归有 5 种选择,递归深度为 $n/2$,每个结果字符串长度为 $n$。 - **空间复杂度**:$O(5^{n/2} \times n)$,存储所有可能的中心对称数结果。 ================================================ FILE: docs/solutions/0200-0299/strobogrammatic-number-iii.md ================================================ # [0248. 中心对称数 III](https://leetcode.cn/problems/strobogrammatic-number-iii/) - 标签:递归、数组、字符串 - 难度:困难 ## 题目链接 - [0248. 中心对称数 III - 力扣](https://leetcode.cn/problems/strobogrammatic-number-iii/) ## 题目大意 **描述**: 给定两个字符串 $low$ 和 $high$ 表示两个整数 $low$ 和 $high$,其中 $low \le high$。 **要求**: 返回范围 $[low, high]$ 内的「中心对称数」总数。 **说明**: - 中心对称数:指一个数字在旋转了 180 度之后看起来依旧相同的数字(或者上下颠倒地看)。 - $1 \le low.length, high.length \le 15$。 - low 和 high 只包含数字。 - $low \le high$。 - $low$ 和 $high$ 不包含任何前导零,除了零本身。 **示例**: - 示例 1: ```python 输入: low = "50", high = "100" 输出: 3 ``` - 示例 2: ```python 输入: low = "0", high = "0" 输出: 1 ``` ## 解题思路 ### 思路 1:递归生成 + 计数 中心对称数只能由数字 $\{0, 1, 6, 8, 9\}$ 组成,其中: - $0$ 旋转后还是 $0$。 - $1$ 旋转后还是 $1$。 - $6$ 旋转后变成 $9$。 - $8$ 旋转后还是 $8$。 - $9$ 旋转后变成 $6$。 对于长度为 $n$ 的中心对称数,我们可以递归生成: - 当 $n = 0$ 时:返回空字符串 - 当 $n = 1$ 时:只能是 $\{0, 1, 8\}$ - 当 $n > 1$ 时: - 外层可以是 $\{0, 1, 6, 8, 9\}$,但最外层不能是 $0$(避免前导零) - 内层递归生成长度为 $n-2$ 的中心对称数 算法步骤: 1. 生成所有长度在 $[\text{len}(low), \text{len}(high)]$ 范围内的中心对称数 2. 过滤出在 $[low, high]$ 范围内的数字 3. 返回符合条件的数字个数 关键点: - 最外层不能是 $0$,避免生成前导零。 - 内层可以是 $0$,如 `"1001"` 是有效的中心对称数。 - 对于数字字符串的比较,需要先比较长度,再比较字典序。 ### 思路 1:代码 ```python class Solution: def strobogrammaticInRange(self, low: str, high: str) -> int: # 定义中心对称数字的映射关系 strobogrammatic_map = { '0': '0', '1': '1', '6': '9', '8': '8', '9': '6' } def generate_strobogrammatic(n, length): """递归生成长度为 n 的中心对称数,length 表示目标长度""" if n == 0: return [""] if n == 1: return ["0", "1", "8"] # 递归生成长度为 n-2 的中心对称数 shorter = generate_strobogrammatic(n - 2, length) result = [] for s in shorter: # 对于中间的数字,可以是 0, 1, 6, 8, 9 for outer in ['0', '1', '6', '8', '9']: # 如果是最外层且长度 > 1,外层不能是 0(避免前导零) if n != length or outer != '0': result.append(outer + s + strobogrammatic_map[outer]) return result def is_in_range(num_str, low, high): """判断数字是否在指定范围内""" # 先比较长度,再比较字符串内容 if len(num_str) < len(low) or len(num_str) > len(high): return False # 如果长度在中间,肯定在范围内 if len(num_str) > len(low) and len(num_str) < len(high): return True # 长度相等时,直接比较字符串 if len(num_str) == len(low) and len(num_str) == len(high): return low <= num_str <= high elif len(num_str) == len(low): return num_str >= low elif len(num_str) == len(high): return num_str <= high return False count = 0 low_len, high_len = len(low), len(high) # 生成所有长度在 [low_len, high_len] 范围内的中心对称数 for length in range(low_len, high_len + 1): strobogrammatic_nums = generate_strobogrammatic(length, length) for num in strobogrammatic_nums: if is_in_range(num, low, high): count += 1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(5^{n/2} \times n)$,其中 $n$ 是最大长度。递归生成所有可能的中心对称数,每个数字需要 $O(n)$ 时间进行范围检查。 - **空间复杂度**:$O(5^{n/2} \times n)$,存储所有生成的中心对称数。 具体分析: - 对于长度为 $k$ 的中心对称数,有 $5^{k/2}$ 个(外层 4 种选择,内层 5 种选择)。 - 总共有 $\sum_{k=\text{len}(low)}^{\text{len}(high)} 5^{k/2}$ 个中心对称数。 - 每个数字的字符串比较需要 $O(k)$ 时间。 ================================================ FILE: docs/solutions/0200-0299/strobogrammatic-number.md ================================================ # [0246. 中心对称数](https://leetcode.cn/problems/strobogrammatic-number/) - 标签:哈希表、双指针、字符串 - 难度:简单 ## 题目链接 - [0246. 中心对称数 - 力扣](https://leetcode.cn/problems/strobogrammatic-number/) ## 题目大意 **描述**: 中心对称数是指一个数字在旋转了 180 度之后看起来依旧相同的数字(或者上下颠倒地看)。 **要求**: 写一个函数来判断该数字是否是中心对称数,其输入将会以一个字符串的形式来表达数字。 **示例**: - 示例 1: ```python 输入: num = "69" 输出: true ``` - 示例 2: ```python 输入: num = "88" 输出: true ``` ## 解题思路 ### 思路 1:哈希表 + 双指针 中心对称数是指旋转 180 度后看起来相同的数字。只有特定的数字在旋转后有意义: - $0$ 旋转后还是 $0$。 - $1$ 旋转后还是 $1$。 - $6$ 旋转后变成 $9$。 - $8$ 旋转后还是 $8$。 - $9$ 旋转后变成 $6$。 其他数字(如 $2, 3, 4, 5, 7$)旋转后没有意义。 使用哈希表存储每个数字的旋转对应关系,然后用双指针从两端向中间检查: - 如果字符串中包含不能旋转的数字,直接返回 $false$。 - 如果两端字符的旋转对应关系不匹配,返回 $false$。 - 如果所有字符都满足中心对称条件,返回 $true$。 ### 思路 1:代码 ```python class Solution: def isStrobogrammatic(self, num: str) -> bool: # 定义中心对称数字的映射关系 strobogrammatic_map = { '0': '0', '1': '1', '6': '9', '8': '8', '9': '6' } # 双指针从两端向中间检查 left, right = 0, len(num) - 1 while left <= right: # 如果当前字符不能旋转,返回 False if num[left] not in strobogrammatic_map: return False if num[right] not in strobogrammatic_map: return False # 检查旋转后的对应关系 if strobogrammatic_map[num[left]] != num[right]: return False left += 1 right -= 1 return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度。需要遍历字符串一次。 - **空间复杂度**:$O(1)$。哈希表大小固定为 $5$,不随输入规模变化。 ================================================ FILE: docs/solutions/0200-0299/summary-ranges.md ================================================ # [0228. 汇总区间](https://leetcode.cn/problems/summary-ranges/) - 标签:数组 - 难度:简单 ## 题目链接 - [0228. 汇总区间 - 力扣](https://leetcode.cn/problems/summary-ranges/) ## 题目大意 **描述**: 给定一个「无重复元素」的「有序」整数数组 $nums$。 区间 $[a, b]$ 是从 $a$ 到 $b$(包含)的所有整数的集合。 **要求**: 返回「恰好覆盖数组中所有数字」的「最小有序」区间范围列表。也就是说,$nums$ 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个区间但不属于 $nums$ 的数字 $x$。 列表中的每个区间范围 [a,$b$] 应该按如下格式输出: - `"a->b"` ,如果 $a \ne b$。 - `"a"`,如果 $a == b$。 **说明**: - $0 \le nums.length \le 20$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 - $nums$ 中的所有值都 互不相同。 - $nums$ 按升序排列。 **示例**: - 示例 1: ```python 输入:nums = [0,1,2,4,5,7] 输出:["0->2","4->5","7"] 解释:区间范围是: [0,2] --> "0->2" [4,5] --> "4->5" [7,7] --> "7" ``` - 示例 2: ```python 输入:nums = [0,2,3,4,6,8,9] 输出:["0","2->4","6","8->9"] 解释:区间范围是: [0,0] --> "0" [2,4] --> "2->4" [6,6] --> "6" [8,9] --> "8->9" ``` ## 解题思路 ### 思路 1:双指针 使用双指针遍历数组,找到连续区间的起始和结束位置。 具体步骤: 1. 初始化结果列表 $res$ 和双指针 $i = 0$。 2. 遍历数组,对于每个位置 $i$: - 记录当前区间的起始位置 $start = nums[i]$。 - 移动指针 $j$ 直到 $nums[j + 1] \neq nums[j] + 1$ 或到达数组末尾。 - 记录当前区间的结束位置 $end = nums[j]$。 - 根据 $start$ 和 $end$ 的关系生成区间字符串: - 如果 $start = end$,添加 $"start"$。 - 否则,添加 $"start->end"$。 - 更新 $i = j + 1$ 继续处理下一个区间。 ### 思路 1:代码 ```python class Solution: def summaryRanges(self, nums: List[int]) -> List[str]: if not nums: return [] res = [] i = 0 n = len(nums) while i < n: # 记录当前区间的起始位置 start = nums[i] # 找到连续区间的结束位置 j = i while j + 1 < n and nums[j + 1] == nums[j] + 1: j += 1 # 记录当前区间的结束位置 end = nums[j] # 根据起始和结束位置生成区间字符串 if start == end: res.append(str(start)) else: res.append(f"{start}->{end}") # 移动到下一个区间的起始位置 i = j + 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。每个元素最多被访问两次(作为区间起点和终点),总体时间复杂度为 $O(n)$。 - **空间复杂度**:$O(1)$。除了结果数组外,只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0200-0299/the-skyline-problem.md ================================================ # [0218. 天际线问题](https://leetcode.cn/problems/the-skyline-problem/) - 标签:树状数组、线段树、数组、分治、有序集合、扫描线、堆(优先队列) - 难度:困难 ## 题目链接 - [0218. 天际线问题 - 力扣](https://leetcode.cn/problems/the-skyline-problem/) ## 题目大意 城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。 给定所有建筑物的位置和高度所组成的数组 `buildings`。其中三元素 `buildings[i] = [left_i, right_i, height_i]` 表示 `left_i` 是第 `i` 座建筑物左边界的 `x` 坐标。`right_i` 是第 `i` 座建筑物右边界的 `x` 坐标,`height_i` 是第 `i` 做建筑物的高度。 要求:返回由这些建筑物形成的天际线 。 - 天际线:由 “关键点” 组成的列表,格式 `[[x1, y1], [x2, y2], [x3, y3], ...]`,并按 `x` 坐标进行排序。 - 关键点:水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,`y` 坐标始终为 `0`,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。 注意:输出天际线中不得有连续的相同高度的水平线。 - 例如 `[..., [2 3], [4 5], [7 5], [11 5], [12 7], ...]` 是不正确的答案;三条高度为 `5` 的线应该在最终输出中合并为一个:`[..., [2 3], [4 5], [12 7], ...]`。 示例: ![](https://assets.leetcode.com/uploads/2020/12/01/merged.jpg) - 图 A 显示输入的所有建筑物的位置和高度。 - 图 B 显示由这些建筑物形成的天际线。图 B 中的红点表示输出列表中的关键点。 ## 解题思路 可以看出来:关键点的横坐标都在建筑物的左右边界上。 我们可以将左右边界最高处的坐标存入 `points` 数组中,然后按照建筑物左边界、右边界的高度进行排序。 然后用一条条「垂直于 x 轴的扫描线」,从所有建筑物的最左侧依次扫描到最右侧。从而将建筑物分割成规则的矩形。 不难看出:相邻的两个坐标的横坐标与矩形所能达到的最大高度构成了一个矩形。相邻两个坐标的横坐标可以从排序过的 `points` 数组中依次获取,矩形所能达到的最大高度可以用一个优先队列(堆)`max_heap` 来维护。使用数组 `ans` 来作为答案答案。 在依次从左到右扫描坐标时: - 当扫描到建筑物的左边界时,说明必然存在一条向右延伸的边。此时将高度加入到优先队列中。 - 当扫描到建筑物的右边界时,说明从之前的左边界延伸的边结束了,此时将高度从优先队列中移除。 因为三条高度相同的线应该合并为一个,所以我们用 `prev` 来记录之前上一个矩形高度。 - 如果当前矩形高度 `curr` 与之前矩形高度 `prev` 相同,则跳过。 - 如果当前矩形高度 `curr` 与之前矩形高度 `prev `不相同,则将其加入到答案数组中,并更新上一矩形高度 `prev` 的值。 最后,输出答案 `ans`。 ## 代码 ```python from sortedcontainers import SortedList class Solution: def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]: ans = [] points = [] for building in buildings: left, right, hight = building[0], building[1], building[2] points.append([left, -hight]) points.append([right, hight]) points.sort(key=lambda x:(x[0], x[1])) prev = 0 max_heap = SortedList([prev]) for point in points: x, height = point[0], point[1] if height < 0: max_heap.add(-height) else: max_heap.remove(height) curr = max_heap[-1] if curr != prev: ans.append([x, curr]) prev = curr return ans ``` ## 参考资料 - 【题解】[【宫水三叶】扫描线算法基本思路 & 优先队列维护当前最大高度 - 天际线问题 - 力扣](https://leetcode.cn/problems/the-skyline-problem/solution/gong-shui-san-xie-sao-miao-xian-suan-fa-0z6xc/) ================================================ FILE: docs/solutions/0200-0299/ugly-number-ii.md ================================================ # [0264. 丑数 II](https://leetcode.cn/problems/ugly-number-ii/) - 标签:哈希表、数学、动态规划、堆(优先队列) - 难度:中等 ## 题目链接 - [0264. 丑数 II - 力扣](https://leetcode.cn/problems/ugly-number-ii/) ## 题目大意 给定一个整数 `n`。 要求:找出并返回第 `n` 个丑数。 - 丑数:只包含质因数 `2`、`3`、`5` 的正整数。 ## 解题思路 动态规划求解。 定义状态 `dp[i]` 表示第 `i` 个丑数。 状态转移方程为:`dp[i] = min(dp[p2] * 2, dp[p3] * 3, dp[p5] * 5)` ,其中 `p2`、`p3`、`p5` 分别表示当前 `i` 中 `2`、`3`、`5` 的质因子数量。 ## 代码 ```python class Solution: def nthUglyNumber(self, n: int) -> int: dp = [1 for _ in range(n)] p2, p3, p5 = 0, 0, 0 for i in range(1, n): dp[i] = min(dp[p2] * 2, dp[p3] * 3, dp[p5] * 5) if dp[i] == dp[p2] * 2: p2 += 1 if dp[i] == dp[p3] * 3: p3 += 1 if dp[i] == dp[p5] * 5: p5 += 1 return dp[n - 1] ``` ================================================ FILE: docs/solutions/0200-0299/ugly-number.md ================================================ # [0263. 丑数](https://leetcode.cn/problems/ugly-number/) - 标签:数学 - 难度:简单 ## 题目链接 - [0263. 丑数 - 力扣](https://leetcode.cn/problems/ugly-number/) ## 题目大意 给定一个整数 `n`。 要求:判断 `n` 是否为丑数。如果是,则返回 `True`,否则,返回 `False`。 - 丑数:只包含质因数 `2`、`3`、`5` 的正整数。 ## 解题思路 - 如果 `n <= 0`,则 `n` 必然不是丑数,直接返回 `False`。 - 对 `n` 分别进行 `2`、`3`、`5` 的整除操作,直到 `n` 被除完,如果 `n` 最终为 `1`,则 `n` 是丑数,否则不是丑数。 ## 代码 ```python class Solution: def isUgly(self, n: int) -> bool: if n <= 0: return False factors = [2, 3, 5] for factor in factors: while n % factor == 0: n //= factor return n == 1 ``` ================================================ FILE: docs/solutions/0200-0299/unique-word-abbreviation.md ================================================ # [0288. 单词的唯一缩写](https://leetcode.cn/problems/unique-word-abbreviation/) - 标签:设计、数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [0288. 单词的唯一缩写 - 力扣](https://leetcode.cn/problems/unique-word-abbreviation/) ## 题目大意 单词缩写规则:<起始字母><中间字母><结尾字母>。如果单词长度不超过 2,则单词本身就是缩写。 举例: - `dog --> d1g`:第一个字母`d`,最后一个字母 `g`,中间隔着 1 个字母。 - `internationalization --> i18n`:第一个字母 `i` ,最后一个字母 `n`,中间隔着 18 个字母。 - `it --> it`:单词只有两个字符,它就是它自身的缩写。 要求实现 ValidWordAbbr 类: - `ValidWordAbbr(dictionary: List[str]):`使用单词字典初始化对象 - `def isUnique(self, word: str) -> bool:` - 如果字典 dictionary 中没有其他单词的缩写与该单词 word 的缩写相同,返回 True。 - 如果字典 dictionary 中所有与该单词 word 的缩写相同的单词缩写都与 word 相同。 ## 解题思路 将相同缩写的单词进行分类,利用哈希表进行存储。键值对格式为 缩写:该缩写对应的 word 列表。 然后初始化的时候,将 dictionary 里的单词按照缩写进行哈希表存储。 在判断的时候,先判断单词 word 的缩写是否能在哈希表中找到对应的映射关系。 - 如果 word 的缩写 abbr 没有在哈希表中,则返回 True。 - 如果 word 的缩写 abbr 在哈希表中: - 如果缩写 abbr 对应的字符串列表只有一个字符串,并且就是 word,则返回 True。Ï - 否则返回 False。 - 不满足上述要求也返回 False。 ## 代码 ```python def isUnique(self, word: str) -> bool: if len(word) <= 2: abbr = word else: abbr = word[0] + chr(len(word)-2) + word[-1] if abbr not in self.abbr_dict: return True if len(set(self.abbr_dict[abbr])) == 1 and word in set(self.abbr_dict[abbr]): return True return False ``` ================================================ FILE: docs/solutions/0200-0299/valid-anagram.md ================================================ # [0242. 有效的字母异位词](https://leetcode.cn/problems/valid-anagram/) - 标签:哈希表、字符串、排序 - 难度:简单 ## 题目链接 - [0242. 有效的字母异位词 - 力扣](https://leetcode.cn/problems/valid-anagram/) ## 题目大意 **描述**:给定两个字符串 $s$ 和 $t$。 **要求**:判断 $t$ 和 $s$ 是否使用了相同的字符构成(字符出现的种类和数目都相同)。 **说明**: - **字母异位词**:如果 $s$ 和 $t$ 中每个字符出现的次数都相同,则称 $s$ 和 $t$ 互为字母异位词。 - $1 \le s.length, t.length \le 5 \times 10^4$。 - $s$ 和 $t$ 仅包含小写字母。 **示例**: - 示例 1: ```python 输入: s = "anagram", t = "nagaram" 输出: True ``` - 示例 2: ```python 输入: s = "rat", t = "car" 输出: False ``` ## 解题思路 ### 思路 1:哈希表 1. 先判断字符串 $s$ 和 $t$ 的长度,不一样直接返回 `False`; 2. 分别遍历字符串 $s$ 和 $t$。先遍历字符串 $s$,用哈希表存储字符串 $s$ 中字符出现的频次; 3. 再遍历字符串 $t$,哈希表中减去对应字符的频次,出现频次小于 $0$ 则输出 `False`; 4. 如果没出现频次小于 $0$,则输出 `True`。 ### 思路 1:代码 ```python def isAnagram(self, s: str, t: str) -> bool: if len(s) != len(t): return False strDict = dict() for ch in s: if ch in strDict: strDict[ch] += 1 else: strDict[ch] = 1 for ch in t: if ch in strDict: strDict[ch] -= 1 if strDict[ch] < 0: return False else: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$、$m$ 分别为字符串 $s$、$t$ 的长度。 - **空间复杂度**:$O(|S|)$,其中 $S$ 为字符集大小,此处 $S == 26$。 ================================================ FILE: docs/solutions/0200-0299/verify-preorder-sequence-in-binary-search-tree.md ================================================ # [0255. 验证二叉搜索树的前序遍历序列](https://leetcode.cn/problems/verify-preorder-sequence-in-binary-search-tree/) - 标签:栈、树、二叉搜索树、递归、数组、二叉树、单调栈 - 难度:中等 ## 题目链接 - [0255. 验证二叉搜索树的前序遍历序列 - 力扣](https://leetcode.cn/problems/verify-preorder-sequence-in-binary-search-tree/) ## 题目大意 **描述**: 给定一个「无重复元素」的整数数组 $preorder$。 **要求**: 如果它是以二叉搜索树的先序遍历排列,返回 $true$,否则返回 $false$。 **说明**: - $1 \le preorder.length \le 10^{4}$。 - $1 \le preorder[i] \le 10^{4}$。 - $preorder$ 中 无重复元素。 - 进阶:能否使用恒定的空间复杂度来完成此题? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/12/preorder-tree.jpg) ```python 输入: preorder = [5,2,1,3,6] 输出: true ``` - 示例 2: ```python 输入: preorder = [5,2,6,1,3] 输出: false ``` ## 解题思路 ### 思路 1:单调栈 核心思想是利用单调栈来模拟二叉搜索树的前序遍历过程。在二叉搜索树的前序遍历中,对于任意节点,其左子树的所有节点值都小于该节点值,右子树的所有节点值都大于该节点值。 算法步骤: 1. 使用单调栈维护一个递减序列,栈中存储的是当前路径上的节点值。 2. 维护一个变量 $lower\_bound$ 表示当前节点值的最小下界。 3. 遍历前序遍历序列 $preorder$: - 如果当前值 $preorder[i] < lower\_bound$,说明违反了二叉搜索树的性质,返回 $false$。 - 如果当前值 $preorder[i] > preorder[i-1]$,说明进入了右子树,需要更新 $lower\_bound$。 - 将当前值入栈,保持栈的单调递减性质。 具体实现: - 设 $preorder[i]$ 为当前遍历的元素 - 如果 $preorder[i] < lower\_bound$,则返回 $false$ - 如果栈不为空且 $preorder[i] > stack[-1]$,则说明进入了右子树,需要弹出栈中所有小于 $preorder[i]$ 的元素,并更新 $lower\_bound$ 为最后一个被弹出的元素 - 将 $preorder[i]$ 入栈 ### 思路 1:代码 ```python class Solution: def verifyPreorder(self, preorder: List[int]) -> bool: # 单调栈方法:模拟二叉搜索树前序遍历 if not preorder: return True stack = [] # 单调递减栈 lower_bound = float('-inf') # 当前节点值的最小下界 for num in preorder: # 如果当前值小于下界,说明违反了 BST 性质 if num < lower_bound: return False # 如果当前值大于栈顶元素,说明进入了右子树 # 需要弹出所有小于当前值的元素,并更新下界 while stack and num > stack[-1]: lower_bound = stack.pop() # 将当前值入栈 stack.append(num) return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组长度。每个元素最多入栈和出栈一次。 - **空间复杂度**:$O(n)$,最坏情况下栈的大小为 $n$。 ================================================ FILE: docs/solutions/0200-0299/walls-and-gates.md ================================================ # [0286. 墙与门](https://leetcode.cn/problems/walls-and-gates/) - 标签:广度优先搜索、数组、矩阵 - 难度:中等 ## 题目链接 - [0286. 墙与门 - 力扣](https://leetcode.cn/problems/walls-and-gates/) ## 题目大意 给定一个 `m * n` 的二维网络 `rooms`。其中每个元素有三种初始值: - `-1` 表示墙或者障碍物 - `0` 表示一扇门 - `INF` 表示为一个空的房间。这里用 $2^{31} = 2147483647$ 表示 `INF`。通往门的距离总是小于 $2^{31}$。 要求:给每个空房间填上该房间到最近的门的距离,如果无法到达门,则填 `INF`。 ## 解题思路 从每个表示门开始,使用广度优先搜索去照门。因为广度优先搜索保证我们在搜索 `dist + 1` 距离的位置时,距离为 `dist` 的位置都已经搜索过了。所以每到达一个房间的时候一定是最短距离。 ## 代码 ```python class Solution: def wallsAndGates(self, rooms: List[List[int]]) -> None: """ Do not return anything, modify rooms in-place instead. """ INF = 2147483647 rows = len(rooms) if rows == 0: return cols = len(rooms[0]) directions = {(1, 0), (-1, 0), (0, 1), (0, -1)} queue = [] for i in range(rows): for j in range(cols): if rooms[i][j] == 0: queue.append((i, j, 0)) while queue: i, j, dist = queue.pop(0) for direction in directions: new_i = i + direction[0] new_j = j + direction[1] if 0 <= new_i < rows and 0 <= new_j < cols and rooms[new_i][new_j] == INF: rooms[new_i][new_j] = dist + 1 queue.append((new_i, new_j, dist + 1)) ``` ================================================ FILE: docs/solutions/0200-0299/wiggle-sort.md ================================================ # [0280. 摆动排序](https://leetcode.cn/problems/wiggle-sort/) - 标签:贪心、数组、排序 - 难度:中等 ## 题目链接 - [0280. 摆动排序 - 力扣](https://leetcode.cn/problems/wiggle-sort/) ## 题目大意 **描述**: 给定一个的整数数组 $nums$。 **要求**: 将该数组重新排序后使 $nums[0] \le nums[1] \ge nums[2] \le nums[3]...$。 输入数组总是有一个有效的答案。 **说明**: - $1 \le nums.length \le 5 \times 10^{4}$。 - $0 \le nums[i] \le 10^{4}$。 - 输入的 $nums$ 保证至少有一个答案。 - 进阶:你能在 $O(n)$ 时间复杂度下解决这个问题吗? **示例**: - 示例 1: ```python 输入:nums = [3,5,2,1,6,4] 输出:[3,5,1,6,2,4] 解释:[1,6,2,5,3,4]也是有效的答案 ``` - 示例 2: ```python 输入:nums = [6,6,5,6,3,8] 输出:[6,6,5,6,3,8] ``` ## 解题思路 ### 思路 1:贪心算法 这是一个贪心算法问题。我们需要将数组重新排序,使得满足摆动条件:$nums[0] \le nums[1] \ge nums[2] \le nums[3]...$ 核心思想是: - 对于奇数位置 $i$($i$ 为奇数),我们需要 $nums[i] \ge nums[i-1]$ 且 $nums[i] \ge nums[i+1]$(如果存在) - 对于偶数位置 $i$($i$ 为偶数),我们需要 $nums[i] \le nums[i-1]$ 且 $nums[i] \le nums[i+1]$(如果存在) 贪心策略: - 遍历数组,对于每个位置 $i$,检查是否满足摆动条件 - 如果不满足,则与相邻元素交换,使得满足条件 - 具体来说: - 如果 $i$ 是奇数且 $nums[i] < nums[i-1]$,则交换 $nums[i]$ 和 $nums[i-1]$ - 如果 $i$ 是偶数且 $nums[i] > nums[i-1]$,则交换 $nums[i]$ 和 $nums[i-1]$ ### 思路 1:代码 ```python class Solution: def wiggleSort(self, nums: List[int]) -> None: """ Do not return anything, modify nums in-place instead. """ n = len(nums) # 遍历数组,确保每个位置都满足摆动条件 for i in range(1, n): # 如果当前位置是奇数索引,需要 nums[i] >= nums[i-1] if i % 2 == 1: if nums[i] < nums[i-1]: # 交换元素,使得 nums[i] >= nums[i-1] nums[i], nums[i-1] = nums[i-1], nums[i] # 如果当前位置是偶数索引,需要 nums[i] <= nums[i-1] else: if nums[i] > nums[i-1]: # 交换元素,使得 nums[i] <= nums[i-1] nums[i], nums[i-1] = nums[i-1], nums[i] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组长度。只需要遍历一次数组,每个元素最多交换一次。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0200-0299/word-pattern-ii.md ================================================ # [0291. 单词规律 II](https://leetcode.cn/problems/word-pattern-ii/) - 标签:哈希表、字符串、回溯 - 难度:中等 ## 题目链接 - [0291. 单词规律 II - 力扣](https://leetcode.cn/problems/word-pattern-ii/) ## 题目大意 **描述**: 给定一种规律 $pattern$ 和一个字符串 $s$。 **要求**: 判断 $s$ 是否和 $pattern$ 的规律相匹配。 如果存在单个字符到「非空」字符串的「双射映射」,那么字符串 $s$ 匹配 $pattern$,即:如果 $pattern$ 中的每个字符都被它映射到的字符串替换,那么最终的字符串则为 $s$。「双射」意味着映射双方一一对应,不会存在两个字符映射到同一个字符串,也不会存在一个字符分别映射到两个不同的字符串。 **说明**: - $1 \le pattern.length, s.length \le 20$。 - $pattern$ 和 $s$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:pattern = "abab", s = "redblueredblue" 输出:true 解释:一种可能的映射如下: 'a' -> "red" 'b' -> "blue" ``` - 示例 2: ```python 输入:pattern = "aaaa", s = "asdasdasdasd" 输出:true 解释:一种可能的映射如下: 'a' -> "asd" ``` ## 解题思路 ### 思路 1:回溯 + 双射映射 核心在于构造规律字符到字符串子串的双射映射。设规律串为 $pattern$,目标串为 $s$。在回溯过程中维护两张映射表: - $f: \Sigma_{p} \to (\Sigma_{s})^{+}$:从规律字符到非空子串的映射; - $g: (\Sigma_{s})^{+} \to \Sigma_{p}$:从非空子串到规律字符的反向映射; 用深度优先搜索 `dfs(i, j)` 表示当前匹配到 $pattern$ 的下标 $i$ 与 $s$ 的下标 $j$: 1. 终止条件:如果 $i = |pattern|$ 且 $j = |s|$,说明恰好匹配成功,返回 True;如果其中一个到达末尾另一个未到达,返回 False。 2. 设当前规律字符为 $c = pattern[i]$。 - 如果 $c$ 已在 $f$ 中映射到某个子串 $t$,则检查 $s$ 在位置 $j$ 是否以 $t$ 为前缀:即 $s[j: j+|t|] = t$。如果匹配则递归到 $dfs(i+1, j+|t|)$,否则返回 False。 - 如果 $c$ 尚未建立映射,则需要在 $s$ 的后缀中尝试所有可能的非空切分 $s[j:k]$(其中 $j < k \le |s|$)。如果该子串尚未被其他字符使用(即不在 $g$ 中),则建立双射 $f[c] = s[j:k]$、$g[s[j:k]] = c$,并递归 $dfs(i+1, k)$。如果递归失败,撤销映射继续尝试下一个切分。 3. 简单剪枝:剩余字符至少要占用长度 1,如果 $|s| - j < |pattern| - i$,可直接返回 False。 ### 思路 1:代码 ```python class Solution: def wordPatternMatch(self, pattern: str, s: str) -> bool: # 回溯 + 双射映射 # p2w: 规律字符 -> 具体子串;w2p: 子串 -> 规律字符 p2w = {} w2p = {} def dfs(i: int, j: int) -> bool: # 如果同时走到末尾,匹配成功 if i == len(pattern) and j == len(s): return True # 只有一个到末尾则失败 if i == len(pattern) or j == len(s): return False # 剪枝:剩余 pattern 字符至少需要占用剩余 s 的长度 if len(s) - j < len(pattern) - i: return False c = pattern[i] # 如果已有映射,校验前缀 if c in p2w: t = p2w[c] # s 当前前缀不匹配则失败 if not s.startswith(t, j): return False return dfs(i + 1, j + len(t)) # 否则尝试所有可能的非空切分 s[j:k] for k in range(j + 1, len(s) + 1): t = s[j:k] # 子串已被其他字符占用则跳过,保证双射 if t in w2p: continue # 建立双向映射 p2w[c] = t w2p[t] = c if dfs(i + 1, k): return True # 回溯撤销 del p2w[c] del w2p[t] return False return dfs(0, 0) ``` ### 思路 1:复杂度分析 - **时间复杂度**:最坏情况下为指数级。每个未映射的规律字符 $c$ 可能尝试 $O(|s|)$ 个切分,整体可上界为 $O(|s|^{|pattern|})$。由于题目规模较小(均不超过 20),回溯可接受。 - **空间复杂度**:$O(|pattern| + |s|)$。主要为递归栈与映射表存储,映射的子串总长度不超过 $|s|$。 ================================================ FILE: docs/solutions/0200-0299/word-pattern.md ================================================ # [0290. 单词规律](https://leetcode.cn/problems/word-pattern/) - 标签:哈希表、字符串 - 难度:简单 ## 题目链接 - [0290. 单词规律 - 力扣](https://leetcode.cn/problems/word-pattern/) ## 题目大意 给定一种规律 `pattern` 和一个字符串 `str` ,判断 `str` 是否完全匹配相同的规律。 - 完全匹配相同的规律:pattern 的每个字母和字符串 str 中的每个非空单词之间存在这双向连接的对应规律。 - 比如:pattern = "abba", str = "dog cat cat dog",其对应关系为:`a <=> dog,b <=> cat` ## 解题思路 这道题要求判断规律串中的字符与所给字符串中的非空单词,是否是一一对应的。即每个字符都能映射到对应的非空单词,每个非空单词也能映射为字符。 考虑使用两个哈希表,一个用来存储字符到非空单词的映射,另一个用来存储非空单词到字符的映射。 遍历 pattern 中的字符: - 如果字符出现在第一个字典中,且字典中的值不等于对应的非空单词,则返回 False。 - 如果单词出现在第二个字典中,且字典中的值不等于对应的字符,则返回 False。 - 如果遍历完仍没发现不满足要求的情况,则返回 True。 ## 代码 ```python class Solution: def wordPattern(self, pattern: str, s: str) -> bool: pattern_dict = dict() word_dict = dict() words = s.split() if len(pattern) != len(words): return False for i in range(len(words)): p = pattern[i] word = words[i] if p in pattern_dict and pattern_dict[p] != word: return False if word in word_dict and word_dict[word] != p: return False pattern_dict[p] = word word_dict[word] = p return True ``` ================================================ FILE: docs/solutions/0200-0299/word-search-ii.md ================================================ # [0212. 单词搜索 II](https://leetcode.cn/problems/word-search-ii/) - 标签:字典树、数组、字符串、回溯、矩阵 - 难度:困难 ## 题目链接 - [0212. 单词搜索 II - 力扣](https://leetcode.cn/problems/word-search-ii/) ## 题目大意 给定一个 `m * n` 二维字符网格 `board` 和一个单词(字符串)列表 `words`。 要求:找出所有同时在二维网格和字典中出现的单词。 注意:单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中「相邻」单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。 ## 解题思路 - 先将单词列表 `words` 中的所有单词存入字典树中。 - 然后遍历二维字符网络 `board` 的每一个字符 `board[i][j]`。 - 从当前单元格出发,从上下左右四个方向深度优先搜索遍历路径。每经过一个单元格,就将该单元格的字母修改为特殊字符,避免重复遍历,深度优先搜索完毕之后再恢复该单元格。 - 如果当前路径恰好是 `words` 列表中的单词,则将结果添加到答案数组中。 - 如果是 `words` 列表中单词的前缀,则继续搜索。 - 如果不是 `words` 列表中单词的前缀,则停止搜索。 - 最后输出答案数组。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False self.word = "" def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True cur.word = word def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None and cur.isEnd class Solution: def findWords(self, board: List[List[str]], words: List[str]) -> List[str]: trie_tree = Trie() for word in words: trie_tree.insert(word) directs = [(0, 1), (0, -1), (1, 0), (-1, 0)] rows = len(board) cols = len(board[0]) def dfs(cur, row, col): ch = board[row][col] if ch not in cur.children: return cur = cur.children[ch] if cur.isEnd: ans.add(cur.word) board[row][col] = "#" for direct in directs: new_row = row + direct[0] new_col = col + direct[1] if 0 <= new_row < rows and 0 <= new_col < cols: dfs(cur, new_row, new_col) board[row][col] = ch ans = set() for i in range(rows): for j in range(cols): dfs(trie_tree, i, j) return list(ans) ``` ================================================ FILE: docs/solutions/0200-0299/zigzag-iterator.md ================================================ # [0281. 锯齿迭代器](https://leetcode.cn/problems/zigzag-iterator/) - 标签:设计、队列、数组、迭代器 - 难度:中等 ## 题目链接 - [0281. 锯齿迭代器 - 力扣](https://leetcode.cn/problems/zigzag-iterator/) ## 题目大意 **描述**: 给定两个整数向量 $v1$ 和 $v2$。 **要求**: 请你实现一个迭代器,交替返回它们的元素。 实现 `ZigzagIterator` 类: - `ZigzagIterator(List v1, List v2)` 用两个向量 $v1$ 和 $v2$ 初始化对象。 - `boolean hasNext()` 如果迭代器还有元素返回 $true$,否则返回 $false$。 - `int next()` 返回迭代器的当前元素并将迭代器移动到下一个元素。 **说明**: - 拓展:假如给你 $k$ 个向量呢?你的代码在这种情况下的扩展性又会如何呢? - 拓展声明:「锯齿」顺序对于 $k > 2$ 的情况定义可能会有些歧义。所以,假如你觉得「锯齿」这个表述不妥,也可以认为这是一种「循环」。例如: ```python 输入:v1 = [1,2,3], v2 = [4,5,6,7], v3 = [8,9] 输出:[1,4,8,2,5,9,3,6,7] ``` **示例**: - 示例 1: ```python 输入:v1 = [1,2], v2 = [3,4,5,6] 输出:[1,3,2,4,5,6] 解释:通过重复调用 next 直到 hasNext 返回 false,那么 next 返回的元素的顺序应该是:[1,3,2,4,5,6]。 ``` - 示例 2: ```python 输入:v1 = [1], v2 = [] 输出:[1] ``` ## 解题思路 ### 思路 1:队列模拟 使用队列来模拟锯齿迭代的过程。我们可以将两个向量 $v1$ 和 $v2$ 的元素按照锯齿顺序放入队列中,然后依次从队列中取出元素。 具体步骤如下: 1. 初始化时,将两个向量 $v1$ 和 $v2$ 的元素按照锯齿顺序(交替)放入队列 $queue$ 中。 2. 使用指针 $i$ 和 $j$ 分别指向 $v1$ 和 $v2$ 的当前位置。 3. 交替从 $v1$ 和 $v2$ 中取元素,直到其中一个向量遍历完毕。 4. 将剩余向量的所有元素按顺序加入队列。 5. `next()` 方法从队列头部取出元素。 6. `hasNext()` 方法检查队列是否为空。 ### 思路 1:代码 ```python class ZigzagIterator: def __init__(self, v1: List[int], v2: List[int]): # 使用队列存储按锯齿顺序排列的元素 self.queue = [] # 初始化两个指针 i, j = 0, 0 # 交替从 v1 和 v2 中取元素 while i < len(v1) and j < len(v2): self.queue.append(v1[i]) self.queue.append(v2[j]) i += 1 j += 1 # 将剩余元素加入队列 while i < len(v1): self.queue.append(v1[i]) i += 1 while j < len(v2): self.queue.append(v2[j]) j += 1 # 队列索引 self.index = 0 def next(self) -> int: # 从队列中取出下一个元素 result = self.queue[self.index] self.index += 1 return result def hasNext(self) -> bool: # 检查是否还有元素 return self.index < len(self.queue) # Your ZigzagIterator object will be instantiated and called as such: # i, v = ZigzagIterator(v1, v2), [] # while i.hasNext(): v.append(i.next()) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n)$,其中 $m$ 和 $n$ 分别是 $v1$ 和 $v2$ 的长度。初始化时需要遍历两个向量的所有元素。 - **空间复杂度**:$O(m + n)$,需要额外的队列空间存储所有元素。 ================================================ FILE: docs/solutions/0300-0399/additive-number.md ================================================ # [0306. 累加数](https://leetcode.cn/problems/additive-number/) - 标签:字符串、回溯 - 难度:中等 ## 题目链接 - [0306. 累加数 - 力扣](https://leetcode.cn/problems/additive-number/) ## 题目大意 **描述**: 「累加数」是一个字符串,组成它的数字可以形成累加序列。 一个有效的 累加序列 必须「至少」包含 $3$ 个数。除了最开始的两个数以外,序列中的每个后续数字必须是它之前两个数字之和。 给定一个只包含数字 $0 \sim 9$ 的字符串。 **要求**: 编写一个算法来判断给定输入是否是「累加数 」。如果是,返回 $true$;否则,返回 $false$。 **说明**: - 累加序列里的数,除数字 $0$ 之外,不会以 $0$ 开头,所以不会出现 $1, 2, 03$ 或者 $1, 02, 3$ 的情况。 - $1 \le num.length \le 35$。 - $num$ 仅由数字 $0 sim 9$ 组成。 - 进阶:你计划如何处理由过大的整数输入导致的溢出? **示例**: - 示例 1: ```python 输入:"112358" 输出:true 解释:累加序列为: 1, 1, 2, 3, 5, 8 。1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8 ``` - 示例 2: ```python 输入:"199100199" 输出:true 解释:累加序列为: 1, 99, 100, 199。1 + 99 = 100, 99 + 100 = 199 ``` ## 解题思路 ### 思路 1:回溯算法 累加数问题可以通过回溯算法来解决。我们需要找到字符串中所有可能的数字分割方式,使得分割后的数字序列满足累加数的定义。 **问题分析**: 对于字符串 $num$,我们需要: - 确定前两个数字 $a$ 和 $b$。 - 验证后续数字是否满足 $c = a + b$ 的关系。 - 递归验证整个序列。 **算法步骤**: 1. **枚举前两个数字**:使用双重循环枚举所有可能的前两个数字 $a$ 和 $b$ 的起始位置。 2. **验证数字有效性**:确保数字不以 $0$ 开头(除非数字本身就是 $0$)。 3. **递归验证序列**:从第三个数字开始,验证每个数字是否等于前两个数字的和。 4. **回溯剪枝**:如果某个分支不满足条件,立即返回 $false$。 ###### 3. 关键变量 - $i$:第一个数字的结束位置。 - $j$:第二个数字的结束位置。 - $a$:第一个数字的值。 - $b$:第二个数字的值。 - $sum$:前两个数字的和,用于验证第三个数字。 ### 思路 1:代码 ```python class Solution: def isAdditiveNumber(self, num: str) -> bool: n = len(num) # 枚举前两个数字的所有可能位置 for i in range(1, n): for j in range(i + 1, n): # 获取第一个数字 first = num[:i] # 获取第二个数字 second = num[i:j] # 检查数字是否有效(不能以0开头,除非数字本身就是0) if (len(first) > 1 and first[0] == '0') or \ (len(second) > 1 and second[0] == '0'): continue # 转换为整数 a = int(first) b = int(second) # 递归验证剩余部分 if self._isValid(num, j, a, b): return True return False def _isValid(self, num: str, start: int, a: int, b: int) -> bool: # 递归验证从 start 位置开始的字符串是否满足累加数条件 # 如果已经到达字符串末尾,说明验证成功 if start == len(num): return True # 计算下一个数字的期望值 expected_sum = a + b expected_str = str(expected_sum) # 检查剩余字符串是否以期望的数字开头 if num[start:].startswith(expected_str): # 递归验证下一个数字 return self._isValid(num, start + len(expected_str), b, expected_sum) return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 是字符串长度。外层双重循环枚举前两个数字的位置需要 $O(n^2)$ 时间,内层递归验证需要 $O(n)$ 时间,因此总时间复杂度为 $O(n^3)$。 - **空间复杂度**:$O(n)$,递归调用栈的深度最多为 $O(n)$。 ================================================ FILE: docs/solutions/0300-0399/android-unlock-patterns.md ================================================ # [0351. 安卓系统手势解锁](https://leetcode.cn/problems/android-unlock-patterns/) - 标签:动态规划、回溯 - 难度:中等 ## 题目链接 - [0351. 安卓系统手势解锁 - 力扣](https://leetcode.cn/problems/android-unlock-patterns/) ## 题目大意 **描述**:安卓系统手势解锁的界面是一个编号为 $1 \sim 9$、大小为 $3 \times 3$ 的网格。用户可以设定一个「解锁模式」,按照一定顺序经过 $k$ 个点,构成一个「解锁手势」。现在给定两个整数,分别为 $m$ 和 $n$。 **要求**:计算出有多少种不同且有效的解锁模式数量,其中每种解锁模式至少需要经过 $m$ 个点,但是不超过 $n$ 个点。 **说明**: - **有效的解锁模式**: - 解锁模式中所有点不能重复。 - 如果解锁模式中两个点是按顺序经过的,那么这两个点之间的手势轨迹不能跨过其他任何未被经过的点。 - 一些有效和无效解锁模式示例:![](https://assets.leetcode.com/uploads/2018/10/12/android-unlock.png) - 无效手势:$[4,1,3,6]$,连接点 $1$ 和点 $3$ 时经过了未被连接过的 $2$ 号点。 - 无效手势:$[4,1,9,2]$,连接点 $1$ 和点 $9$ 时经过了未被连接过的 $5$ 号点。 - 有效手势:$[2,4,1,3,6]$,连接点 $1$ 和点 $3$ 是有效的,因为虽然它经过了点 $2$,但是点 $2$ 在该手势中之前已经被连过了。 - 有效手势:$[6,5,4,1,9,2]$,连接点 $1$ 和点 $9$ 是有效的,因为虽然它经过了按键 $5$,但是点 $5$ 在该手势中之前已经被连过了。 - $1 \le m, n \le 9$。 - 如果经过的点不同或者经过点的顺序不同,表示为不同的解锁模式。 **示例**: - 示例 1: ```python 输入:m = 1, n = 1 输出:9 ``` - 示例 2: ```python 输入:m = 1, n = 2 输出:65 ``` ## 解题思路 ### 思路 1:状态压缩 + 记忆化搜索 因为手势解锁的界面是一个编号为 $1 \sim 9$、大小为 $3 \times 3$ 的网格,所以我们可以用一个 $9$ 位长度的二进制数 $state$ 来表示当前解锁模式中按键的选取情况。 因为解锁模式中两个点之间的手势轨迹不能跨过其他任何未被经过的点,所以我们可以预先使用一个哈希表 $graph$ 将手势轨迹跨过其他点的情况存储下来,便于判断当前手势轨迹是否有效。 接下来我们使用深度优先搜索方法,将所有有效的解锁模式统计出来,具体做法如下: 1. 定义一个全局变量 $ans$ 用于统计所有有效的解锁模式的方案数。 2. 定义一个深度优先搜索方法为 `def dfs(state, cur, step):`,表示当前键位选择情况为 $state$,从当前键位 $cur$ 出发,已经走了 $step$ 的有效解锁模式。 1. 当 $step$ 在区间 $[m, n]$ 中时,统计有效解锁模式方案数,即:令 $ans$ 加 $1$。 2. 当 $step$ 到达步数上限 $n$ 时,直接返回。 3. 遍历下一步(第 $step + 1$ 步)可选择的键位 $k$,判断键位 $k$ 是否有效。 4. 如果到达 $k$ 没有跨过其他键($k$ 不在 $graph[cur]$ 中),或者到达 $k$ 跨过的键位是已经经过的键 ($state >> graph[cur][k] \text{ \& } 1 == 1$),则继续调用 `dfs(state | (1 << k), k, step + 1)`,其中 `stete | (1 << k)` 表示下一步选择 $k$ 的状态。 3. 遍历开始位置 $1 \sim 9$,从 1 ~ 9 每个数字开始出发,调用 `dfs(1 << i, i, 1)`,进行所有有效的解锁模式的统计。 4. 最后输出 $ans$。 ### 思路 1:代码 ```python class Solution: def numberOfPatterns(self, m: int, n: int) -> int: # 将手势轨迹跨过点的情况存入哈希表中 graph = { 1: {3: 2, 7: 4, 9: 5}, 2: {8: 5}, 3: {1: 2, 7: 5, 9: 6}, 4: {6: 5}, 5: {}, 6: {4: 5}, 7: {1: 4, 3: 5, 9: 8}, 8: {2: 5}, 9: {1: 5, 3: 6, 7: 8}, } ans = 0 def dfs(state, cur, step): nonlocal ans if m <= step <= n: ans += 1 if step == n: return for k in range(1, 10): if state >> k & 1 != 0: continue if k not in graph[cur] or state >> graph[cur][k] & 1: dfs(state | (1 << k), k, step + 1) for i in range(1, 10): dfs(1 << i, i, 1) # 从 1 ~ 9 每个数字开始出发 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n!)$。 - **空间复杂度**:$O(1)$。 ## 参考资料 - 【题解】[LeetCode-351. 安卓系统手势解锁 - mkdocs_blog](https://github.com/zhanguohao/mkdocs_blog/blob/mkdocs_blog/docs/problem/leetcode/LeetCode-351.%20%E5%AE%89%E5%8D%93%E7%B3%BB%E7%BB%9F%E6%89%8B%E5%8A%BF%E8%A7%A3%E9%94%81.md) ================================================ FILE: docs/solutions/0300-0399/best-time-to-buy-and-sell-stock-with-cooldown.md ================================================ # [0309. 买卖股票的最佳时机含冷冻期](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [0309. 买卖股票的最佳时机含冷冻期 - 力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/) ## 题目大意 给定一个整数数组,其中第 `i` 个元素代表了第 `i` 天的股票价格 。 设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票): - 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 - 卖出股票后,你无法在第二天买入股票(即冷冻期为 `1` 天)。 ## 解题思路 这道题是「[0122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/)」的升级版。 冷冻期的意思是:如果昨天卖出了,那么今天不能买。在考虑的时候只要判断一下前一天是不是刚卖出。 对于每一天结束时的状态总共有以下几种: - 买入状态: - 今日买入 - 之前买入,之后一直持有无操作 - 卖出状态: - 今日买出,正处于冷冻期 - 昨天卖出,今天结束后度过了冷冻期 - 之前卖出,度过了冷冻期后无操作 在买入状态中,今日买入和之前买入的状态其实可以看做是股票的持有状态,可以将其合并为一种状态。 在卖出状态中,昨天卖出和之前卖出的状态其实可以看做是无股票并度过了冷冻期状态,可以将其合并为一种状态。 这样总结下来可以划分为三个状态: - 股票的持有状态。 - 无股票,并且处于冷冻期状态。 - 无股票,并且不处于冷冻期状态。 所以我们可以定义状态 `dp[i][j]` ,表示为:第 `i` 天第 `j` 种情况(`0 <= j <= 2`)下,所获取的最大利润。 注意:这里第第 `j` 种情况, 接下来确定状态转移公式: - 第 `0` 种状态(股票的持有状态)下可以有两种状态推出,取最大的那一种赋值: - 昨天就已经持有的:`dp[i][0] = dp[i - 1][0]`: - 今天刚买入的(则昨天不能持有股票也不能处于冷冻期,应来自于前天卖出状态):`dp[i][0] = dp[i - 1][2] - prices[i]` - 第 `1` 种状态(无股票,并且处于冷冻期状态)下可以有一种状态推出: - 今天卖出:`dp[i] = dp[i - 1][0] + prices[i]` - 第 `2` 种状态(无股票,并且不处于冷冻期状态)下可以有两种状态推出,取最大的那一种赋值: - 昨天卖出:`dp[i] = dp[i - 1][1]` - 之前卖出:`dp[i] = dp[i - 1][2]` 下面确定初始化的边界值: 可以很明显看出第一天不做任何操作就是 `dp[0][0] = 0`,第一次买入就是 `dp[0][1] = -prices[i]`。 第一次卖出的话,可以视作为没有盈利(当天买卖,价格没有变化),即 `dp[0][2] = 0`。第二次买入的话,就是 `dp[0][3] = -prices[i]`。同理第二次卖出就是 `dp[0][4] = 0`。 在递推结束后,最大利润肯定是无操作、第一次卖出、第二次卖出这三种情况里边,且为最大值。我们在维护的时候维护的是最大值,则第一次卖出、第二次卖出所获得的利润肯定大于等于 0。而且,如果最优情况为一笔交易,那么在转移状态时,我们允许在一天内进行两次交易,则一笔交易的状态可以转移至两笔交易。所以最终答案为 `dp[size - 1][4]`。`size` 为股票天数。 ## 代码 ```python class Solution: def maxProfit(self, prices: List[int]) -> int: size = len(prices) if size == 0: return 0 dp = [[0 for _ in range(4)] for _ in range(size)] dp[0][0] = -prices[0] for i in range(1, size): dp[i][0] = max(dp[i - 1][0], dp[i - 1][2] - prices[i]) dp[i][1] = dp[i - 1][0] + prices[i] dp[i][2] = max(dp[i - 1][1], dp[i - 1][2]) return max(dp[size - 1][0], dp[size - 1][1], dp[size - 1][2]) ``` ================================================ FILE: docs/solutions/0300-0399/binary-tree-vertical-order-traversal.md ================================================ # [0314. 二叉树的垂直遍历](https://leetcode.cn/problems/binary-tree-vertical-order-traversal/) - 标签:树、深度优先搜索、广度优先搜索、哈希表、二叉树、排序 - 难度:中等 ## 题目链接 - [0314. 二叉树的垂直遍历 - 力扣](https://leetcode.cn/problems/binary-tree-vertical-order-traversal/) ## 题目大意 **描述**: 给定一个二叉树的根结点 $root$。 **要求**: 返回其结点按垂直方向(从上到下,逐列)遍历的结果。 **说明**: - 如果两个结点在同一行和列,那么顺序则为「从左到右」。 - 树中结点的数目在范围 $[0, 10^{3}]$ 内。 - $-10^{3} \le Node.val \le 10^{3}$。 **示例**: - 示例 1: ![](https://pic.leetcode.cn/1727276130-UOKFsu-image.png) ```python 输入:root = [3,9,20,null,null,15,7] 输出:[[9],[3,15],[20],[7]] ``` - 示例 2: ![](https://pic.leetcode.cn/1727276212-bzuKab-image.png) ```python 输入:root = [3,9,8,4,0,1,7] 输出:[[4],[9],[3,0,1],[8],[7]] ``` ## 解题思路 ### 思路 1:BFS + 哈希表 二叉树的垂直遍历问题可以通过 BFS(广度优先搜索)结合哈希表来解决。我们需要为每个节点分配一个列坐标,然后按照列坐标对节点进行分组。 **问题分析**: 对于二叉树中的每个节点,我们需要: - 为根节点分配列坐标 $0$。 - 左子节点的列坐标为父节点列坐标 $-1$。 - 右子节点的列坐标为父节点列坐标 $+1$。 - 使用 BFS 保证同一列中节点的从上到下顺序。 - 使用哈希表按列坐标分组存储节点值。 **算法步骤**: 1. **初始化**:如果根节点为空,返回空列表。创建队列存储 `(节点, 列坐标)` 元组,创建哈希表存储每列的节点值列表。 2. **BFS 遍历**:从根节点开始,将根节点和列坐标 $0$ 加入队列。 3. **处理节点**:对于队列中的每个节点,将其值加入对应列的列表中,然后处理其左右子节点。 4. **更新坐标**:左子节点列坐标 $-1$,右子节点列坐标 $+1$。 5. **排序输出**:按照列坐标从小到大排序,返回每列的节点值列表。 **关键变量**: - $col$:节点的列坐标,根节点为 $0$。 - $queue$:BFS 队列,存储 `(节点, 列坐标)` 元组。 - $column\_table$:哈希表,键为列坐标,值为该列的节点值列表。 - $min\_col$ 和 $max\_col$:记录最小和最大列坐标,用于最终排序。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def verticalOrder(self, root: Optional[TreeNode]) -> List[List[int]]: # 如果根节点为空,返回空列表 if not root: return [] # 使用队列进行 BFS,存储 (节点, 列坐标) 元组 queue = [(root, 0)] # 哈希表存储每列的节点值列表 column_table = {} # 记录最小和最大列坐标 min_col = 0 max_col = 0 # BFS 遍历 while queue: node, col = queue.pop(0) # 将节点值加入对应列的列表 if col not in column_table: column_table[col] = [] column_table[col].append(node.val) # 更新列坐标范围 min_col = min(min_col, col) max_col = max(max_col, col) # 处理左子节点,列坐标 -1 if node.left: queue.append((node.left, col - 1)) # 处理右子节点,列坐标 +1 if node.right: queue.append((node.right, col + 1)) # 按照列坐标从小到大排序,构建结果列表 result = [] for col in range(min_col, max_col + 1): if col in column_table: result.append(column_table[col]) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是树中节点的数量。BFS 遍历需要 $O(n)$ 时间,最后按列坐标排序需要 $O(k \log k)$ 时间,其中 $k$ 是列的数量,最坏情况下 $k = n$,因此总时间复杂度为 $O(n \log n)$。 - **空间复杂度**:$O(n)$,队列和哈希表最多存储 $n$ 个节点,递归调用栈的深度最多为 $O(n)$。 ================================================ FILE: docs/solutions/0300-0399/bomb-enemy.md ================================================ # [0361. 轰炸敌人](https://leetcode.cn/problems/bomb-enemy/) - 标签:数组、动态规划、矩阵 - 难度:中等 ## 题目链接 - [0361. 轰炸敌人 - 力扣](https://leetcode.cn/problems/bomb-enemy/) ## 题目大意 **描述**: 给定一个大小为 $m \times n$ 的矩阵 $grid$,其中每个单元格都放置有一个字符: - `'W'` 表示一堵墙。 - `'E'` 表示一个敌人。 - `'0'`(数字 $0$)表示一个空位。 **要求**: 返回你使用「一颗炸弹」可以击杀的最大敌人数目。 **说明**: - 你只能把炸弹放在一个空位里。 - 由于炸弹的威力不足以穿透墙体,炸弹只能击杀同一行和同一列没被墙体挡住的敌人。 - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 500$。 - $grid[i][j]$ 可以是 `'W'`、`'E'` 或 `'0'`。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/27/bomb1-grid.jpg) ```python 输入:grid = [["0","E","0","0"],["E","0","W","E"],["0","E","0","0"]] 输出:3 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/27/bomb2-grid.jpg) ```python 输入:grid = [["W","W","W"],["0","0","0"],["E","E","E"]] 输出:1 ``` ## 解题思路 ### 思路 1:动态规划 + 预处理 轰炸敌人问题可以通过动态规划预处理来解决。我们需要预先计算每个位置在四个方向上能击杀的敌人数,然后找到最大值。 **问题分析**: 对于矩阵 $grid$ 中的每个空位 $(i, j)$,炸弹可以击杀: - 同一行中左右方向的敌人(被墙阻挡前)。 - 同一列中上下方向的敌人(被墙阻挡前)。 **算法步骤**: 1. **预处理行方向**:对每一行,从左到右和从右到左分别计算每个位置能击杀的敌人数。 2. **预处理列方向**:对每一列,从上到下和从下到上分别计算每个位置能击杀的敌人数。 3. **计算最大值**:遍历所有空位,计算四个方向击杀敌人数之和的最大值。 **关键变量**: - $m$:矩阵的行数。 - $n$:矩阵的列数。 - $row\_kills[i][j]$:位置 $(i, j)$ 在行方向上能击杀的敌人数。 - $col\_kills[i][j]$:位置 $(i, j)$ 在列方向上能击杀的敌人数。 - $max\_kills$:能击杀的最大敌人数。 ### 思路 1:代码 ```python class Solution: def maxKilledEnemies(self, grid: List[List[str]]) -> int: if not grid or not grid[0]: return 0 m, n = len(grid), len(grid[0]) max_kills = 0 # 预处理:计算每个位置在行方向上能击杀的敌人数 row_kills = [[0] * n for _ in range(m)] # 从左到右计算行方向的击杀数 for i in range(m): count = 0 for j in range(n): if grid[i][j] == 'W': count = 0 # 遇到墙,重置计数 elif grid[i][j] == 'E': count += 1 # 遇到敌人,增加计数 else: # 空位 row_kills[i][j] += count # 从右到左计算行方向的击杀数 for i in range(m): count = 0 for j in range(n - 1, -1, -1): if grid[i][j] == 'W': count = 0 # 遇到墙,重置计数 elif grid[i][j] == 'E': count += 1 # 遇到敌人,增加计数 else: # 空位 row_kills[i][j] += count # 预处理:计算每个位置在列方向上能击杀的敌人数 col_kills = [[0] * n for _ in range(m)] # 从上到下计算列方向的击杀数 for j in range(n): count = 0 for i in range(m): if grid[i][j] == 'W': count = 0 # 遇到墙,重置计数 elif grid[i][j] == 'E': count += 1 # 遇到敌人,增加计数 else: # 空位 col_kills[i][j] += count # 从下到上计算列方向的击杀数 for j in range(n): count = 0 for i in range(m - 1, -1, -1): if grid[i][j] == 'W': count = 0 # 遇到墙,重置计数 elif grid[i][j] == 'E': count += 1 # 遇到敌人,增加计数 else: # 空位 col_kills[i][j] += count # 计算每个空位能击杀的敌人数,并更新最大值 for i in range(m): for j in range(n): if grid[i][j] == '0': # 空位 total_kills = row_kills[i][j] + col_kills[i][j] max_kills = max(max_kills, total_kills) return max_kills ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 是矩阵的行数,$n$ 是矩阵的列数。我们需要遍历矩阵四次(行方向两次,列方向两次),每次遍历的时间复杂度都是 $O(m \times n)$。 - **空间复杂度**:$O(m \times n)$,需要两个 $m \times n$ 的二维数组来存储预处理结果。 ================================================ FILE: docs/solutions/0300-0399/bulb-switcher.md ================================================ # [0319. 灯泡开关](https://leetcode.cn/problems/bulb-switcher/) - 标签:脑筋急转弯、数学 - 难度:中等 ## 题目链接 - [0319. 灯泡开关 - 力扣](https://leetcode.cn/problems/bulb-switcher/) ## 题目大意 **描述**: 初始时有 $n$ 个灯泡处于关闭状态。第一轮,你将会打开所有灯泡。接下来的第二轮,你将会每两个灯泡关闭第二个。 第三轮,你每三个灯泡就切换第三个灯泡的开关(即,打开变关闭,关闭变打开)。第 $i$ 轮,你每 $i$ 个灯泡就切换第 $i$ 个灯泡的开关。直到第 $n$ 轮,你只需要切换最后一个灯泡的开关。 **要求**: 找出并返回 $n$ 轮后有多少个亮着的灯泡。 **说明**: - $0 \le n \le 10^{9}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/05/bulb.jpg) ```python 输入:n = 3 输出:1 解释: 初始时, 灯泡状态 [关闭, 关闭, 关闭]. 第一轮后, 灯泡状态 [开启, 开启, 开启]. 第二轮后, 灯泡状态 [开启, 关闭, 开启]. 第三轮后, 灯泡状态 [开启, 关闭, 关闭]. 你应该返回 1,因为只有一个灯泡还亮着。 ``` - 示例 2: ```python 输入:n = 0 输出:0 ``` ## 解题思路 ### 思路 1:数学分析 通过分析灯泡的开关规律,我们可以发现一个重要的数学性质:第 $i$ 个灯泡最终的状态取决于它被切换了多少次。 具体分析: 1. 第 $i$ 个灯泡在第 $j$ 轮会被切换,当且仅当 $j$ 是 $i$ 的因子。 2. 因此,第 $i$ 个灯泡被切换的次数等于 $i$ 的因子个数。 3. 如果一个数有奇数个因子,那么它最终会亮着;如果有偶数个因子,那么它最终会关闭。 4. 只有完全平方数有奇数个因子,因为因子总是成对出现的,除了完全平方数的平方根。 因此,答案就是小于等于 $n$ 的完全平方数的个数,即 $\lfloor \sqrt{n} \rfloor$。 ### 思路 1:代码 ```python class Solution: def bulbSwitch(self, n: int) -> int: return int(n ** 0.5) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$,只需要计算一次平方根。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0300-0399/burst-balloons.md ================================================ # [0312. 戳气球](https://leetcode.cn/problems/burst-balloons/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [0312. 戳气球 - 力扣](https://leetcode.cn/problems/burst-balloons/) ## 题目大意 **描述**:有 $n$ 个气球,编号为 $0 \sim n - 1$,每个气球上都有一个数字,这些数字存在数组 $nums$ 中。现在开始戳破气球。其中戳破第 $i$ 个气球,可以获得 $nums[i - 1] \times nums[i] \times nums[i + 1]$ 枚硬币,这里的 $i - 1$ 和 $i + 1$ 代表和 $i$ 相邻的两个气球的编号。如果 $i - 1$ 或 $i + 1$ 超出了数组的边界,那么就当它是一个数字为 $1$ 的气球。 **要求**:求出能获得硬币的最大数量。 **说明**: - $n == nums.length$。 - $1 \le n \le 300$。 - $0 \le nums[i] \le 100$。 **示例**: - 示例 1: ```python 输入:nums = [3,1,5,8] 输出:167 解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> [] coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167 ``` - 示例 2: ```python 输入:nums = [1,5] 输出:10 解释: nums = [1,5] --> [5] --> [] coins = 1*1*5 + 1*5*1 = 10 ``` ## 解题思路 ### 思路 1:动态规划 根据题意,如果 $i - 1$ 或 $i + 1$ 超出了数组的边界,那么就当它是一个数字为 $1$ 的气球。我们可以预先在 $nums$ 的首尾位置,添加两个数字为 $1$ 的虚拟气球,这样变成了 $n + 2$ 个气球,气球对应编号也变为了 $0 \sim n + 1$。 对应问题也变成了:给定 $n + 2$ 个气球,每个气球上有 $1$ 个数字,代表气球上的硬币数量,当我们戳破气球 $nums[i]$ 时,就能得到对应 $nums[i - 1] \times nums[i] \times nums[i + 1]$ 枚硬币。现在要戳破 $0 \sim n + 1$ 之间的所有气球(不包括编号 $0$ 和编号 $n + 1$ 的气球),请问最多能获得多少枚硬币? ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:戳破所有气球 $i$ 与气球 $j$ 之间的气球(不包含气球 $i$ 和 气球 $j$),所能获取的最多硬币数。 ###### 3. 状态转移方程 假设气球 $i$ 与气球 $j$ 之间最后一个被戳破的气球编号为 $k$。则 $dp[i][j]$ 取决于由 $k$ 作为分割点分割出的两个区间 $(i, k)$ 与 $(k, j)$ 上所能获取的最多硬币数 + 戳破气球 $k$ 所能获得的硬币数,即状态转移方程为: $dp[i][j] = max \lbrace dp[i][k] + dp[k][j] + nums[i] \times nums[k] \times nums[j] \rbrace, \quad i < k < j$ ###### 4. 初始条件 - $dp[i][j]$ 表示的是开区间,则 $i < j - 1$。而当 $i \ge j - 1$ 时,所能获得的硬币数为 $0$,即 $dp[i][j] = 0, \quad i \ge j - 1$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:戳破所有气球 $i$ 与气球 $j$ 之间的气球(不包含气球 $i$ 和 气球 $j$),所能获取的最多硬币数。。所以最终结果为 $dp[0][n + 1]$。 ### 思路 1:代码 ```python class Solution: def maxCoins(self, nums: List[int]) -> int: size = len(nums) arr = [0 for _ in range(size + 2)] arr[0] = arr[size + 1] = 1 for i in range(1, size + 1): arr[i] = nums[i - 1] dp = [[0 for _ in range(size + 2)] for _ in range(size + 2)] for l in range(3, size + 3): for i in range(0, size + 2): j = i + l - 1 if j >= size + 2: break for k in range(i + 1, j): dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + arr[i] * arr[j] * arr[k]) return dp[0][size + 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 为气球数量。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/0300-0399/coin-change.md ================================================ # [0322. 零钱兑换](https://leetcode.cn/problems/coin-change/) - 标签:广度优先搜索、数组、动态规划 - 难度:中等 ## 题目链接 - [0322. 零钱兑换 - 力扣](https://leetcode.cn/problems/coin-change/) ## 题目大意 **描述**:给定代表不同面额的硬币数组 $coins$ 和一个总金额 $amount$。 **要求**:求出凑成总金额所需的最少的硬币个数。如果无法凑出,则返回 $-1$。 **说明**: - $1 \le coins.length \le 12$。 - $1 \le coins[i] \le 2^{31} - 1$。 - $0 \le amount \le 10^4$。 **示例**: - 示例 1: ```python 输入:coins = [1, 2, 5], amount = 11 输出:3 解释:11 = 5 + 5 + 1 ``` - 示例 2: ```python 输入:coins = [2], amount = 3 输出:-1 ``` ## 解题思路 ### 思路 1:广度优先搜索 我们可以从 $amount$ 开始,每次从 $coins$ 的硬币中选中 $1$ 枚硬币,并记录当前挑选硬币的次数。则最快减到 $0$ 的次数就是凑成总金额所需的最少的硬币个数。这道题就变成了从 $amount$ 减到 $0$ 的最短路径问题。我们可以用广度优先搜索的方法来做。 1. 定义 $visited$ 为标记已访问值的集合变量,$queue$ 为存放值的队列。 2. 将 $amount$ 状态标记为访问,并将其加入队列 $queue$。 3. 令当前步数加 $1$,然后将当前队列中的所有值依次出队,并遍历硬币数组: 1. 如果当前值等于当前硬币值,则说明当前硬币刚好能凑成当前值,则直接返回当前次数。 2. 如果当前值大于当前硬币值,并且当前值减去当前硬币值的差值没有出现在已访问集合 $visited$ 中,则将差值添加到队列和访问集合中。 4. 重复执行第 $3$ 步,直到队列为空。 5. 如果队列为空,也未能减到 $0$,则返回 $-1$。 ### 思路 1:代码 ```python class Solution: def coinChange(self, coins: List[int], amount: int) -> int: if amount == 0: return 0 visited = set([amount]) queue = collections.deque([amount]) step = 0 while queue: step += 1 size = len(queue) for _ in range(size): cur = queue.popleft() for coin in coins: if cur == coin: return step elif cur > coin and cur - coin not in visited: queue.append(cur - coin) visited.add(cur - coin) return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(amount \times size)$。其中 $amount$ 表示总金额,$size$ 表示硬币的种类数。 - **空间复杂度**:$O(amount)$。 ### 思路 2:完全背包问题 这道题可以转换为:有 $n$ 种不同的硬币,$coins[i]$ 表示第 $i$ 种硬币的面额,每种硬币可以无限次使用。请问恰好凑成总金额为 $amount$ 的背包,最少需要多少硬币? 与普通完全背包问题不同的是,这里求解的是最少硬币数量。我们可以改变一下「状态定义」和「状态转移方程」。 ###### 1. 阶段划分 按照当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[c]$ 表示为:凑成总金额为 $c$ 的最少硬币数量。 ###### 3. 状态转移方程 $dp[c] = min(dp[c], dp[c - coin] + 1)$ 对于每种硬币 $coin$,当 $c \ge coin$ 时,取下面两种情况中的较小值: 1. 不使用当前硬币,只使用之前硬币凑成金额 $c$ 的最少硬币数量,即 $dp[c]$。 2. 凑成金额 $c - coin$ 的最少硬币数量,再加上当前硬币的数量 $1$,即 $dp[c - coin] + 1$。 ###### 4. 初始条件 - 凑成总金额为 $0$ 的最少硬币数量为 $0$,即 $dp[0] = 0$。 - 默认情况下,在不使用硬币时,都不能恰好凑成总金额为 $c$,此时将状态值设置为一个极大值(比如 $+\infty$),表示无法凑成。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[c]$ 表示为:凑成总金额为 $c$ 的最少硬币数量。则最终结果为 $dp[amount]$。 1. 如果 $dp[amount] \ne +\infty$,则说明: $dp[amount]$ 为凑成金额 $amount$ 的最少硬币数量,则返回 $dp[amount]$。 2. 如果 $dp[amount] = +\infty$,则说明:无法凑成金额 $amount$,则返回 $-1$。 ### 思路 2:代码 ```python class Solution: def coinChange(self, coins: List[int], amount: int) -> int: dp = [float('inf')] * (amount + 1) dp[0] = 0 # 枚举每种硬币 for coin in coins: # 正序枚举背包装载重量 for c in range(coin, amount + 1): dp[c] = min(dp[c], dp[c - coin] + 1) if dp[amount] != float('inf'): return dp[amount] return -1 ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(amount \times size)$。其中 $amount$ 表示总金额,$size$ 表示硬币的种类数。 - **空间复杂度**:$O(amount)$。 ================================================ FILE: docs/solutions/0300-0399/combination-sum-iv.md ================================================ # [0377. 组合总和 Ⅳ](https://leetcode.cn/problems/combination-sum-iv/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [0377. 组合总和 Ⅳ - 力扣](https://leetcode.cn/problems/combination-sum-iv/) ## 题目大意 **描述**:给定一个由不同整数组成的数组 $nums$ 和一个目标整数 $target$。 **要求**:从 $nums$ 中找出并返回总和为 $target$ 的元素组合个数。 **说明**: - 题目数据保证答案符合 32 位整数范围。 - $1 \le nums.length \le 200$。 - $1 \le nums[i] \le 1000$。 - $nums$ 中的所有元素互不相同。 - $1 \le target \le 1000$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3], target = 4 输出:7 解释: 所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 请注意,顺序不同的序列被视作不同的组合。 ``` - 示例 2: ```python 输入:nums = [9], target = 3 输出:0 ``` ## 解题思路 ### 思路 1:动态规划 「完全背包问题求方案数」的变形。本题与「完全背包问题求方案数」不同点在于:方案中不同的物品顺序代表不同方案。 比如「完全背包问题求方案数」中,凑成总和为 $4$ 的方案 $[1, 3]$ 算 $1$ 种方案,但是在本题中 $[1, 3]$、$[3, 1]$ 算 $2$ 种方案数。 我们需要在考虑某一总和 $w$ 时,需要将 $nums$ 中所有元素都考虑到。对应到循环关系时,即将总和 $w$ 的遍历放到外侧循环,将 $nums$ 数组元素的遍历放到内侧循环,即: ```python for w in range(target + 1): for i in range(1, len(nums) + 1): xxxx ``` ###### 1. 阶段划分 按照总和进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[w]$ 表示为:凑成总和 $w$ 的组合数。 ###### 3. 状态转移方程 凑成总和为 $w$ 的组合数 = 「不使用当前 $nums[i - 1]$,只使用之前整数凑成和为 $w$ 的组合数」+「使用当前 $nums[i - 1]$ 凑成和为 $w - nums[i - 1]$ 的方案数」。即状态转移方程为:$dp[w] = dp[w] + dp[w - nums[i - 1]]$。 ###### 4. 初始条件 - 凑成总和 $0$ 的组合数为 $1$,即 $dp[0] = 1$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[w]$ 表示为:凑成总和 $w$ 的组合数。 所以最终结果为 $dp[target]$。 ### 思路 1:代码 ```python class Solution: def combinationSum4(self, nums: List[int], target: int) -> int: size = len(nums) dp = [0 for _ in range(target + 1)] dp[0] = 1 for w in range(target + 1): for i in range(1, size + 1): if w >= nums[i - 1]: dp[w] = dp[w] + dp[w - nums[i - 1]] return dp[target] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times target)$,其中 $n$ 为数组 $nums$ 的元素个数,$target$ 为目标整数。 - **空间复杂度**:$O(target)$。 ================================================ FILE: docs/solutions/0300-0399/count-numbers-with-unique-digits.md ================================================ # [0357. 统计各位数字都不同的数字个数](https://leetcode.cn/problems/count-numbers-with-unique-digits/) - 标签:数学、动态规划、回溯 - 难度:中等 ## 题目链接 - [0357. 统计各位数字都不同的数字个数 - 力扣](https://leetcode.cn/problems/count-numbers-with-unique-digits/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:统计并返回区间 $[0, 10^n)$ 上各位数字都不相同的数字 $x$ 的个数。 **说明**: - $0 \le n \le 8$。 - $0 \le x < 10^n$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:91 解释:答案应为除去 11、22、33、44、55、66、77、88、99 外,在 0 ≤ x < 100 范围内的所有数字。 ``` - 示例 2: ```python 输入:n = 0 输出:1 ``` ## 解题思路 ### 思路 1:动态规划 + 数位 DP 题目求解区间 $[0, 10^n)$ 范围内各位数字都不相同的数字个数。则我们先将 $10^n - 1$ 转换为字符串 $s$,定义递归函数 `def dfs(pos, state, isLimit, isNum):` 表示构造第 $pos$ 位及之后所有数位的合法方案数。接下来按照如下步骤进行递归。 1. 从 `dfs(0, 0, True, False)` 开始递归。 `dfs(0, 0, True, False)` 表示: 1. 从位置 $0$ 开始构造。 2. 初始没有使用数字(即前一位所选数字集合为 $0$)。 3. 开始时受到数字 $n$ 对应最高位数位的约束。 4. 开始时没有填写数字。 2. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时: 1. 如果 $isNum == True$,说明当前方案符合要求,则返回方案数 $1$。 2. 如果 $isNum == False$,说明当前方案不符合要求,则返回方案数 $0$。 3. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:`ans = 0`。 4. 如果遇到 $isNum == False$,说明之前位数没有填写数字,当前位可以跳过,这种情况下方案数等于 $pos + 1$ 位置上没有受到 $pos$ 位的约束,并且之前没有填写数字时的方案数,即:`ans = dfs(i + 1, state, False, False)`。 5. 如果 $isNum == True$,则当前位必须填写一个数字。此时: 1. 根据 $isNum$ 和 $isLimit$ 来决定填当前位数位所能选择的最小数字($minX$)和所能选择的最大数字($maxX$), 2. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $d$。 3. 如果之前没有选择 $d$,即 $d$ 不在之前选择的数字集合 $state$ 中,则方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, state | (1 << d), isLimit and d == maxX, True)`。 1. `state | (1 << d)` 表示之前选择的数字集合 $state$ 加上 $d$。 2. `isLimit and d == maxX` 表示 $pos + 1$ 位受到之前位限制和 $pos$ 位限制。 3. $isNum == True$ 表示 $pos$ 位选择了数字。 6. 最后的方案数为 `dfs(0, 0, True, False) + 1`,因为之前计算时没有考虑 $0$,所以最后统计方案数时要加 $1$。 ### 思路 1:代码 ```python class Solution: def countNumbersWithUniqueDigits(self, n: int) -> int: s = str(10 ** n - 1) @cache # pos: 第 pos 个数位 # state: 之前选过的数字集合。 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 # isNum: 表示 pos 前面的数位是否填了数字。 # 如果为真,则当前位不可跳过;如果为假,则当前位可跳过。 def dfs(pos, state, isLimit, isNum): if pos == len(s): # isNum 为 True,则表示当前方案符合要求 return int(isNum) ans = 0 if not isNum: # 如果 isNumb 为 False,则可以跳过当前数位 ans = dfs(pos + 1, state, False, False) # 如果前一位没有填写数字,则最小可选择数字为 0,否则最少为 1(不能含有前导 0)。 minX = 0 if isNum else 1 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s[pos]) if isLimit else 9 # 枚举可选择的数字 for d in range(minX, maxX + 1): # d 不在选择的数字集合中,即之前没有选择过 d if (state >> d) & 1 == 0: ans += dfs(pos + 1, state | (1 << d), isLimit and d == maxX, True) return ans return dfs(0, 0, True, False) + 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 10 \times 2^{10})$。 - **空间复杂度**:$O(n \times 2^{10})$。 ================================================ FILE: docs/solutions/0300-0399/count-of-range-sum.md ================================================ # [0327. 区间和的个数](https://leetcode.cn/problems/count-of-range-sum/) - 标签:树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 - 难度:困难 ## 题目链接 - [0327. 区间和的个数 - 力扣](https://leetcode.cn/problems/count-of-range-sum/) ## 题目大意 **描述**: 给定一个整数数组 $nums$ 以及两个整数 $lower$ 和 $upper$。 **要求**: 求数组中,值位于范围 $[lower, upper]$(包含 $lower$ 和 $upper$)之内的「区间和的个数」。 **说明**: - 区间和 $S(i, j)$:表示在 $nums$ 中,位置从 $i$ 到 $j$ 的元素之和,包含 $i$ 和 $j$ ($i \le j$)。 - $1 \le nums.length \le 10^{5}$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 - $-10^{5} \le lower \le upper \le 10^{5}$。 - 题目数据保证答案是一个 $32$ 位的整数。 **示例**: - 示例 1: ```python 输入:nums = [-2,5,-1], lower = -2, upper = 2 输出:3 解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。 ``` - 示例 2: ```python 输入:nums = [0], lower = 0, upper = 0 输出:1 ``` ## 解题思路 ### 思路 1:前缀和 + 归并排序 这道题要求统计区间和 $S(i, j) = \sum_{k=i}^{j} nums[k]$ 在 $[lower, upper]$ 范围内的个数。 我们可以使用前缀和的思想:设 $prefix[i] = \sum_{k=0}^{i} nums[k]$,则区间和 $S(i, j) = prefix[j] - prefix[i-1]$。 对于每个位置 $j$,我们需要找到满足条件的 $i$,使得:$lower \leq prefix[j] - prefix[i-1] \leq upper$ 即:$prefix[j] - upper \leq prefix[i-1] \leq prefix[j] - lower$ 这可以转化为:对于每个 $prefix[j]$,统计在 $prefix[0]$ 到 $prefix[j-1]$ 中有多少个值在区间 $[prefix[j] - upper, prefix[j] - lower]$ 内。 使用归并排序的思想: 1. 将数组分为左右两部分 2. 递归处理左右两部分,得到左右两部分的答案 3. 处理跨越中点的区间:对于右半部分的每个 $prefix[j]$,在左半部分中统计满足条件的 $prefix[i]$ 4. 合并左右两部分并排序 ### 思路 1:代码 ```python class Solution: def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int: # 计算前缀和数组 prefix = [0] for num in nums: prefix.append(prefix[-1] + num) def merge_sort_count(left, right): if left >= right: return 0 mid = (left + right) // 2 # 递归处理左右两部分 count = merge_sort_count(left, mid) + merge_sort_count(mid + 1, right) # 处理跨越中点的区间 i = j = mid + 1 for k in range(left, mid + 1): # 找到左边界:prefix[j] >= prefix[k] + lower while i <= right and prefix[i] < prefix[k] + lower: i += 1 # 找到右边界:prefix[j] <= prefix[k] + upper while j <= right and prefix[j] <= prefix[k] + upper: j += 1 # 统计满足条件的区间数量 count += j - i # 合并排序 temp = [] i, j = left, mid + 1 while i <= mid and j <= right: if prefix[i] <= prefix[j]: temp.append(prefix[i]) i += 1 else: temp.append(prefix[j]) j += 1 # 添加剩余元素 while i <= mid: temp.append(prefix[i]) i += 1 while j <= right: temp.append(prefix[j]) j += 1 # 更新原数组 for i in range(len(temp)): prefix[left + i] = temp[i] return count return merge_sort_count(0, len(prefix) - 1) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组长度。归并排序的时间复杂度为 $O(n \log n)$,每次合并时处理跨越中点的区间需要 $O(n)$ 时间。 - **空间复杂度**:$O(n)$,需要额外的空间存储前缀和数组和归并排序的临时数组。 ### 思路 2:树状数组 同样使用前缀和的思想,但这次我们使用树状数组来优化查询。 对于每个前缀和 $prefix[j]$,我们需要统计在 $prefix[0]$ 到 $prefix[j-1]$ 中有多少个值在区间 $[prefix[j] - upper, prefix[j] - lower]$ 内。 使用树状数组的步骤: 1. 计算所有可能的前缀和值,包括 $prefix[j] - upper$ 和 $prefix[j] - lower$ 2. 对这些值进行离散化处理 3. 从左到右遍历前缀和数组,对于每个 $prefix[j]$: - 查询区间 $[prefix[j] - upper, prefix[j] - lower]$ 内的元素个数 - 将 $prefix[j]$ 插入到树状数组中 ### 思路 2:代码 ```python class Solution: def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int: # 计算前缀和数组 prefix = [0] for num in nums: prefix.append(prefix[-1] + num) # 收集所有需要离散化的值 values = set() for p in prefix: values.add(p) values.add(p - lower) values.add(p - upper) # 离散化 sorted_values = sorted(values) value_to_idx = {v: i + 1 for i, v in enumerate(sorted_values)} # 树状数组类 class BIT: def __init__(self, n): self.n = n self.tree = [0] * (n + 1) def update(self, idx, delta): while idx <= self.n: self.tree[idx] += delta idx += idx & (-idx) def query(self, idx): res = 0 while idx > 0: res += self.tree[idx] idx -= idx & (-idx) return res def range_query(self, left, right): return self.query(right) - self.query(left - 1) # 初始化树状数组 bit = BIT(len(sorted_values)) count = 0 # 从左到右处理前缀和 for p in prefix: # 查询区间 [p - upper, p - lower] 内的元素个数 left_idx = value_to_idx[p - upper] right_idx = value_to_idx[p - lower] count += bit.range_query(left_idx, right_idx) # 将当前前缀和插入树状数组 bit.update(value_to_idx[p], 1) return count ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组长度。离散化需要 $O(n \log n)$ 时间,树状数组的每次更新和查询操作都是 $O(\log n)$,总共需要 $O(n \log n)$ 时间。 - **空间复杂度**:$O(n)$,需要存储离散化后的值和树状数组。 ================================================ FILE: docs/solutions/0300-0399/count-of-smaller-numbers-after-self.md ================================================ # [0315. 计算右侧小于当前元素的个数](https://leetcode.cn/problems/count-of-smaller-numbers-after-self/) - 标签:树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 - 难度:困难 ## 题目链接 - [0315. 计算右侧小于当前元素的个数 - 力扣](https://leetcode.cn/problems/count-of-smaller-numbers-after-self/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 。 **要求**:返回一个新数组 $counts$ 。其中 $counts[i]$ 的值是 $nums[i]$ 右侧小于 $nums[i]$ 的元素的数量。 **说明**: - $1 \le nums.length \le 10^5$。 - $-10^4 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [5,2,6,1] 输出:[2,1,1,0] 解释: 5 的右侧有 2 个更小的元素 (2 和 1) 2 的右侧仅有 1 个更小的元素 (1) 6 的右侧有 1 个更小的元素 (1) 1 的右侧有 0 个更小的元素 ``` - 示例 2: ```python 输入:nums = [-1] 输出:[0] ``` ## 解题思路 ### 思路 1:归并排序 在使用归并排序对数组进行排序时,每当遇到 $left\_nums[left\_i] \le right\_nums[right\_i]$ 时,意味着:在合并前,左子数组当前元素 $left\_nums[left\_i]$ 右侧一定有 $left\_i$ 个元素比 $left\_nums[left\_i]$ 小。则我们可以在归并排序的同时,记录 $nums[i]$ 右侧小于 $nums[i]$ 的元素的数量。 1. 将元素值、对应下标、右侧小于 nums[i] 的元素的数量存入数组中。 2. 对其进行归并排序。 3. 当遇到 $left\_nums[left\_i] \le right\_nums[right\_i]$ 时,记录 $left\_nums[left\_i]$ 右侧比 $left\_nums[left\_i]$ 小的元素数量,即:`left_nums[left_i][2] += right_i`。 4. 当合并时 $left\_nums[left\_i]$ 仍有剩余时,说明 $left\_nums[left\_i]$ 右侧有 $right\_i$ 个小于 $left\_nums[left\_i]$ 的元素,记录下来,即:`left_nums[left_i][2] += right_i`。 5. 根据下标及右侧小于 $nums[i]$ 的元素的数量,组合出答案数组,并返回答案数组。 ### 思路 1:代码 ```python class Solution: # 合并过程 def merge(self, left_nums, right_nums): nums = [] left_i, right_i = 0, 0 while left_i < len(left_nums) and right_i < len(right_nums): # 将两个有序子数组中较小元素依次插入到结果数组中 if left_nums[left_i] <= right_nums[right_i]: nums.append(left_nums[left_i]) # left_nums[left_i] 右侧有 right_i 个比 left_nums[left_i] 小的 left_nums[left_i][2] += right_i left_i += 1 else: nums.append(right_nums[right_i]) right_i += 1 # 如果左子数组有剩余元素,则将其插入到结果数组中 while left_i < len(left_nums): nums.append(left_nums[left_i]) # left_nums[left_i] 右侧有 right_i 个比 left_nums[left_i] 小的 left_nums[left_i][2] += right_i left_i += 1 # 如果右子数组有剩余元素,则将其插入到结果数组中 while right_i < len(right_nums): nums.append(right_nums[right_i]) right_i += 1 # 返回合并后的结果数组 return nums # 分解过程 def mergeSort(self, nums) : # 数组元素个数小于等于 1 时,直接返回原数组 if len(nums) <= 1: return nums mid = len(nums) // 2 # 将数组从中间位置分为左右两个数组 left_nums = self.mergeSort(nums[0: mid]) # 递归将左子数组进行分解和排序 right_nums = self.mergeSort(nums[mid:]) # 递归将右子数组进行分解和排序 return self.merge(left_nums, right_nums) # 把当前数组组中有序子数组逐层向上,进行两两合并 def countSmaller(self, nums: List[int]) -> List[int]: size = len(nums) # 将元素值、对应下标、右侧小于 nums[i] 的元素的数量存入数组中 nums = [[num, i, 0] for i, num in enumerate(nums)] nums = self.mergeSort(nums) ans = [0 for _ in range(size)] for num in nums: ans[num[1]] = num[2] return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(n)$。 ### 思路 2:树状数组 1. 首先对数组进行离散化处理。把原始数组中的数据映射到 $[0, len(nums) - 1]$ 这个区间。 2. 然后逆序顺序从数组 $nums$ 中遍历元素 $nums[i]$。 1. 计算其离散化后的排名 $index$,查询比 $index$ 小的数有多少个。将其记录到答案数组的对应位置 $ans[i]$ 上。 2. 然后在树状数组下标为 $index$ 的位置上,更新值为 $1$。 3. 遍历完所有元素,最后输出答案数组 $ans$ 即可。 ### 思路 2:代码 ```python import bisect class BinaryIndexTree: def __init__(self, n): self.size = n self.tree = [0 for _ in range(n + 1)] def lowbit(self, index): return index & (-index) def update(self, index, delta): while index <= self.size: self.tree[index] += delta index += self.lowbit(index) def query(self, index): res = 0 while index > 0: res += self.tree[index] index -= self.lowbit(index) return res class Solution: def countSmaller(self, nums: List[int]) -> List[int]: size = len(nums) if size == 0: return [] if size == 1: return [0] # 离散化 sort_nums = list(set(nums)) sort_nums.sort() size_s = len(sort_nums) bit = BinaryIndexTree(size_s) ans = [0 for _ in range(size)] for i in range(size - 1, -1, -1): index = bisect.bisect_left(sort_nums, nums[i]) + 1 ans[i] = bit.query(index - 1) bit.update(index, 1) return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0300-0399/counting-bits.md ================================================ # [0338. 比特位计数](https://leetcode.cn/problems/counting-bits/) - 标签:位运算、动态规划 - 难度:简单 ## 题目链接 - [0338. 比特位计数 - 力扣](https://leetcode.cn/problems/counting-bits/) ## 题目大意 **描述**:给定一个整数 `n`。 **要求**:对于 `0 ≤ i ≤ n` 的每一个 `i`,计算其二进制表示中 `1` 的个数,返回一个长度为 `n + 1` 的数组 `ans` 作为答案。 **说明**: - $0 \le n \le 10^5$。 - 使用线性时间复杂度 $O(n)$ 解决此问题。 - 不使用任何内置函数解决此问题。 **示例**: - 示例 1: ```python 输入:n = 5 输出:[0,1,1,2,1,2] 解释: 0 --> 0 1 --> 1 2 --> 10 3 --> 11 4 --> 100 5 --> 101 ``` ## 解题思路 ### 思路 1:动态规划 根据整数的二进制特点可以将整数分为两类: - 奇数:其二进制表示中 $1$ 的个数一定比前面相邻的偶数多一个 $1$。 - 偶数:其二进制表示中 $1$ 的个数一定与该数除以 $2$ 之后的数一样多。 另外,边界 $0$ 的二进制表示中 $1$ 的个数为 $0$。 于是可以根据规律,从 $0$ 开始到 $n$ 进行递推求解。 ###### 1. 阶段划分 按照整数 $n$ 进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为:整数 $i$ 对应二进制表示中 $1$ 的个数。 ###### 3. 状态转移方程 - 如果 $i$ 为奇数,则整数 $i$ 对应二进制表示中 $1$ 的个数等于整数 $i - 1$ 对应二进制表示中 $1$ 的个数加 $1$,即 $dp[i] = dp[i - 1] + 1$。 - 如果 $i$ 为偶数,则整数 $i$ 对应二进制表示中 $1$ 的个数等于整数 $i // 2$ 对应二进制表示中 $1$ 的个数,即 $dp[i] = dp[i // 2]$。 ###### 4. 初始条件 整数 $0$ 对应二进制表示中 $1$ 的个数为 $0$。 ###### 5. 最终结果 整个 $dp$ 数组即为最终结果,将其返回即可。 ### 思路 1:动态规划代码 ```python class Solution: def countBits(self, n: int) -> List[int]: dp = [0 for _ in range(n + 1)] for i in range(1, n + 1): if i % 2 == 1: dp[i] = dp[i - 1] + 1 else: dp[i] = dp[i // 2] return dp ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。一重循环的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。用到了一位数组保存状态,所以总的时间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0300-0399/create-maximum-number.md ================================================ # [0321. 拼接最大数](https://leetcode.cn/problems/create-maximum-number/) - 标签:栈、贪心、数组、双指针、单调栈 - 难度:困难 ## 题目链接 - [0321. 拼接最大数 - 力扣](https://leetcode.cn/problems/create-maximum-number/) ## 题目大意 **描述**: 给定两个整数数组 $nums1$ 和 $nums2$,它们的长度分别为 $m$ 和 $n$。数组 $nums1$ 和 $nums2$ 分别代表两个数各位上的数字。同时你也会得到一个整数 $k$。 **要求**: 请你利用这两个数组中的数字创建一个长度为 $k \le m + n$ 的最大数。同一数组中数字的相对顺序必须保持不变。 返回代表答案的长度为 $k$ 的数组。 **说明**: - $m == nums1.length$。 - $n == nums2.length$。 - $1 \le m, n \le 500$。 - $0 \le nums1[i], nums2[i] \le 9$。 - $1 \le k \le m + n$。 - $nums1$ 和 $nums2$ 没有前导 $0$。 **示例**: - 示例 1: ```python 输入:nums1 = [3,4,6,5], nums2 = [9,1,2,5,8,3], k = 5 输出:[9,8,6,5,3] ``` - 示例 2: ```python 输入:nums1 = [6,7], nums2 = [6,0,4], k = 5 输出:[6,7,6,0,4] ``` ## 解题思路 ### 思路 1:贪心 + 单调栈 这道题的核心思想是:对于长度为 $k$ 的最大数,我们需要从两个数组中选择数字,使得最终结果最大。 解题步骤: 1. **枚举所有可能的分割方式**:假设从 $nums1$ 中选择 $i$ 个数字,从 $nums2$ 中选择 $k-i$ 个数字,其中 $0 \le i \le \min(len(nums1), k)$ 且 $0 \le k-i \le len(nums2)$。 2. **从单个数组中选择最大子序列**:使用单调栈的思想,从 $nums1$ 中选择 $i$ 个数字组成最大子序列,从 $nums2$ 中选择 $k-i$ 个数字组成最大子序列。 3. **合并两个子序列**:将两个子序列合并成一个长度为 $k$ 的最大序列。 4. **比较所有可能的结果**:返回所有可能结果中的最大值。 关键算法: - 使用单调栈从数组中选择 $k$ 个数字组成最大子序列。 - 使用双指针合并两个子序列,每次选择较大的数字。 ### 思路 1:代码 ```python class Solution: def maxNumber(self, nums1: List[int], nums2: List[int], k: int) -> List[int]: def getMaxSubsequence(nums, k): """从数组中选择k个数字组成最大子序列""" stack = [] # 需要删除的数字个数 drop = len(nums) - k for num in nums: # 如果栈不为空,且当前数字大于栈顶数字,且还有数字可以删除 while stack and num > stack[-1] and drop > 0: stack.pop() drop -= 1 stack.append(num) # 如果还有数字需要删除,从末尾删除 return stack[:k] def merge(nums1, nums2): """合并两个数组,保持相对顺序,使结果最大""" result = [] i = j = 0 while i < len(nums1) and j < len(nums2): # 比较从当前位置开始的子序列,选择字典序更大的 if nums1[i:] > nums2[j:]: result.append(nums1[i]) i += 1 else: result.append(nums2[j]) j += 1 # 添加剩余元素 result.extend(nums1[i:]) result.extend(nums2[j:]) return result max_result = [] # 枚举所有可能的分割方式 for i in range(max(0, k - len(nums2)), min(len(nums1), k) + 1): j = k - i # 从两个数组中选择最大子序列 sub1 = getMaxSubsequence(nums1, i) sub2 = getMaxSubsequence(nums2, j) # 合并两个子序列 merged = merge(sub1, sub2) # 更新最大结果 if merged > max_result: max_result = merged return max_result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k \times (m + n) \times k)$,其中 $m$ 和 $n$ 分别是两个数组的长度。需要枚举 $O(k)$ 种分割方式,每种方式需要 $O(m + n)$ 时间选择子序列,$O(k)$ 时间合并。 - **空间复杂度**:$O(k)$,用于存储临时结果和栈。 ================================================ FILE: docs/solutions/0300-0399/data-stream-as-disjoint-intervals.md ================================================ # [0352. 将数据流变为多个不相交区间](https://leetcode.cn/problems/data-stream-as-disjoint-intervals/) - 标签:设计、二分查找、有序集合 - 难度:困难 ## 题目链接 - [0352. 将数据流变为多个不相交区间 - 力扣](https://leetcode.cn/problems/data-stream-as-disjoint-intervals/) ## 题目大意 **描述**: 给定一个由非负整数 $a1, a2, ..., an$ 组成的数据流输入。 **要求**: 请你将到目前为止看到的数字总结为不相交的区间列表。 实现 `SummaryRanges` 类: - `SummaryRanges()` 使用一个空数据流初始化对象。 - `void addNum(int val)` 向数据流中加入整数 $val$ 。 - `int[][] getIntervals()` 以不相交区间 $[start_i, end_i]$ 的列表形式返回对数据流中整数的总结。 **说明**: - $0 \le val \le 10^{4}$。 - 最多调用 `addNum` 和 `getIntervals` 方法 $3 \times 10^{4}$ 次。 - 进阶:如果存在大量合并,并且与数据流的大小相比,不相交区间的数量很小,该怎么办? **示例**: - 示例 1: ```python 输入: ["SummaryRanges", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals"] [[], [1], [], [3], [], [7], [], [2], [], [6], []] 输出: [null, null, [[1, 1]], null, [[1, 1], [3, 3]], null, [[1, 1], [3, 3], [7, 7]], null, [[1, 3], [7, 7]], null, [[1, 3], [6, 7]]] 解释: SummaryRanges summaryRanges = new SummaryRanges(); summaryRanges.addNum(1); // arr = [1] summaryRanges.getIntervals(); // 返回 [[1, 1]] summaryRanges.addNum(3); // arr = [1, 3] summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3]] summaryRanges.addNum(7); // arr = [1, 3, 7] summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3], [7, 7]] summaryRanges.addNum(2); // arr = [1, 2, 3, 7] summaryRanges.getIntervals(); // 返回 [[1, 3], [7, 7]] summaryRanges.addNum(6); // arr = [1, 2, 3, 6, 7] summaryRanges.getIntervals(); // 返回 [[1, 3], [6, 7]] ``` ## 解题思路 ### 思路 1:集合 + 动态生成区间 这道题的核心是维护一个数字集合,然后在需要时动态生成不相交的区间列表。 **算法思路**: 1. **数据结构选择**:使用集合(set)存储所有出现过的数字,利用集合的去重特性自动处理重复数字。 2. **添加数字**:当添加数字 $val$ 时,直接将其加入集合中,时间复杂度为 $O(1)$。 3. **获取区间**:当需要获取区间列表时: - 将集合中的所有数字排序。 - 遍历排序后的数字,连续的数字合并为一个区间。 - 遇到不连续的数字时,开始新的区间。 **具体步骤**: 1. 使用集合存储所有添加的数字。 2. `addNum(val)`:将 $val$ 添加到集合中。 3. `getIntervals()`: - 对集合中的数字排序得到 $sorted\_nums$。 - 初始化 $start = end = sorted\_nums[0]$。 - 遍历剩余数字,如果 $sorted\_nums[i] = end + 1$,则扩展当前区间 $end = sorted\_nums[i]$。 - 否则,保存当前区间 $[start, end]$,开始新区间 $start = end = sorted\_nums[i]$。 - 最后添加最后一个区间。 ### 思路 1:代码 ```python class SummaryRanges: def __init__(self): # 使用集合存储所有出现过的数字 self.nums = set() def addNum(self, value: int) -> None: # 将数字添加到集合中,集合自动去重 self.nums.add(value) def getIntervals(self) -> List[List[int]]: # 如果集合为空,返回空列表 if not self.nums: return [] # 将集合中的数字排序 sorted_nums = sorted(self.nums) intervals = [] # 初始化第一个区间 start = end = sorted_nums[0] # 遍历剩余数字,构建区间 for i in range(1, len(sorted_nums)): if sorted_nums[i] == end + 1: # 当前数字与前一个数字连续,扩展当前区间 end = sorted_nums[i] else: # 当前数字与前一个数字不连续,保存当前区间并开始新区间 intervals.append([start, end]) start = end = sorted_nums[i] # 添加最后一个区间 intervals.append([start, end]) return intervals # Your SummaryRanges object will be instantiated and called as such: # obj = SummaryRanges() # obj.addNum(value) # param_2 = obj.getIntervals() ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `addNum(val)`:$O(1)$,集合的插入操作时间复杂度为常数。 - `getIntervals()`:$O(n \log n)$,其中 $n$ 为集合中数字的个数,主要时间消耗在排序上。 - **空间复杂度**:$O(n)$,其中 $n$ 为添加的不同数字的个数。 ================================================ FILE: docs/solutions/0300-0399/decode-string.md ================================================ # [0394. 字符串解码](https://leetcode.cn/problems/decode-string/) - 标签:栈、递归、字符串 - 难度:中等 ## 题目链接 - [0394. 字符串解码 - 力扣](https://leetcode.cn/problems/decode-string/) ## 题目大意 **描述**:给定一个经过编码的字符串 `s`。 **要求**:返回 `s` 经过解码之后的字符串。 **说明**: - 编码规则:`k[encoded_string]`。`encoded_string` 为字符串,`k` 为整数。表示字符串 `encoded_string` 重复 `k` 次。 - $1 \le s.length \le 30$。 - `s` 由小写英文字母、数字和方括号 `[]` 组成。 - `s` 保证是一个有效的输入。 - `s` 中所有整数的取值范围为 $[1, 300]$。 **示例**: - 示例 1: ```python 输入:s = "3[a]2[bc]" 输出:"aaabcbc" ``` - 示例 2: ```python 输入:s = "3[a2[c]]" 输出:"accaccacc" ``` ## 解题思路 ### 思路 1:栈 1. 使用两个栈 `stack1`、`stack2`。`stack1` 用来保存左括号前已经解码的字符串,`stack2` 用来存储左括号前的数字。 2. 用 `res` 存储待解码的字符串、`num` 存储当前数字。 3. 遍历字符串。 1. 如果遇到数字,则累加数字到 `num`。 2. 如果遇到左括号,将当前待解码字符串入栈 `stack1`,当前数字入栈 `stack2`,然后将 `res`、`nums` 清空。 3. 如果遇到右括号,则从 `stack1` 的取出待解码字符串 `res`,从 `stack2` 中取出当前数字 `num`,将其解码拼合成字符串赋值给 `res`。 4. 如果遇到其他情况(遇到字母),则将当前字母加入 `res` 中。 4. 遍历完输出解码之后的字符串 `res`。 ### 思路 1:代码 ```python class Solution: def decodeString(self, s: str) -> str: stack1 = [] stack2 = [] num = 0 res = "" for ch in s: if ch.isdigit(): num = num * 10 + int(ch) elif ch == '[': stack1.append(res) stack2.append(num) res = "" num = 0 elif ch == ']': cur_res = stack1.pop() cur_num = stack2.pop() res = cur_res + res * cur_num else: res += ch return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0300-0399/design-hit-counter.md ================================================ # [0362. 敲击计数器](https://leetcode.cn/problems/design-hit-counter/) - 标签:设计、队列、数组、二分查找、数据流 - 难度:中等 ## 题目链接 - [0362. 敲击计数器 - 力扣](https://leetcode.cn/problems/design-hit-counter/) ## 题目大意 **要求**: 设计一个敲击计数器,使它可以统计在过去 $5$ 分钟内被敲击次数(即过去 $300$ 秒)。 您的系统应该接受一个时间戳参数 $timestamp$ (单位为秒),并且您可以假定对系统的调用是按时间顺序进行的(即 $timestamp$ 是单调递增的)。几次撞击可能同时发生。 实现 `HitCounter` 类: - `HitCounter()` 初始化命中计数器系统。 - `void hit(int timestamp)` 记录在 $timestamp$ (单位为秒)发生的一次命中。在同一个 $timestamp$ 中可能会出现几个点击。 - `int getHits(int timestamp)` 返回 $timestamp$ 在过去 $5$ 分钟内(即过去 $300$ 秒)的命中次数。 **说明**: - $1 \le timestamp \le 2 \times 10^{9}$。 - 所有对系统的调用都是按时间顺序进行的(即 timestamp 是单调递增的)。 - `hit` 和 `getHits` 最多被调用 $300$ 次。 - 进阶: 如果每秒的敲击次数是一个很大的数字,你的计数器可以应对吗? **示例**: - 示例 1: ```python 输入: ["HitCounter", "hit", "hit", "hit", "getHits", "hit", "getHits", "getHits"] [[], [1], [2], [3], [4], [300], [300], [301]] 输出: [null, null, null, null, 3, null, 4, 3] 解释: HitCounter counter = new HitCounter(); counter.hit(1);// 在时刻 1 敲击一次。 counter.hit(2);// 在时刻 2 敲击一次。 counter.hit(3);// 在时刻 3 敲击一次。 counter.getHits(4);// 在时刻 4 统计过去 5 分钟内的敲击次数, 函数返回 3 。 counter.hit(300);// 在时刻 300 敲击一次。 counter.getHits(300); // 在时刻 300 统计过去 5 分钟内的敲击次数,函数返回 4 。 counter.getHits(301); // 在时刻 301 统计过去 5 分钟内的敲击次数,函数返回 3 。 ``` ## 解题思路 ### 思路 1:队列 使用队列来存储所有的敲击时间戳。对于每次 `hit` 操作,我们将时间戳加入队列。对于 `getHits` 操作,我们需要移除所有超过 $300$ 秒的时间戳,然后返回队列中剩余元素的个数。 具体步骤: 1. **初始化**:创建一个空队列 $queue$ 来存储时间戳。 2. **hit 操作**:将当前时间戳 $timestamp$ 加入队列。 3. **getHits 操作**: - 计算时间窗口的起始时间:$start\_time = timestamp - 300$。 - 移除队列中所有小于等于 $start\_time$ 的时间戳。 - 返回队列中剩余元素的个数。 这种方法简单直观,但每次 `getHits` 操作都需要遍历队列来移除过期的时间戳。 ### 思路 1:代码 ```python from collections import deque class HitCounter: def __init__(self): # 使用双端队列存储时间戳 self.queue = deque() def hit(self, timestamp: int) -> None: # 将当前时间戳加入队列 self.queue.append(timestamp) def getHits(self, timestamp: int) -> int: # 计算时间窗口的起始时间(过去 300 秒) start_time = timestamp - 300 # 移除所有过期的时间戳 while self.queue and self.queue[0] <= start_time: self.queue.popleft() # 返回队列中剩余元素的个数 return len(self.queue) # Your HitCounter object will be instantiated and called as such: # obj = HitCounter() # obj.hit(timestamp) # param_2 = obj.getHits(timestamp) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `hit` 操作:$O(1)$,只需要将元素加入队列。 - `getHits` 操作:$O(k)$,其中 $k$ 是需要移除的过期时间戳数量。 - **空间复杂度**:$O(n)$,其中 $n$ 是在过去 $300$ 秒内的敲击次数。 ================================================ FILE: docs/solutions/0300-0399/design-phone-directory.md ================================================ # [0379. 电话目录管理系统](https://leetcode.cn/problems/design-phone-directory/) - 标签:设计、队列、数组、哈希表、链表 - 难度:中等 ## 题目链接 - [0379. 电话目录管理系统 - 力扣](https://leetcode.cn/problems/design-phone-directory/) ## 题目大意 **要求**: 设计一个电话目录管理系统,一开始有 $maxNumbers$ 个位置能够储存号码。系统应该存储号码,检查某个位置是否为空,并清空给定的位置。 实现 `PhoneDirectory` 类: - `PhoneDirectory(int maxNumbers)` 电话目录初始有 $maxNumbers$ 个可用位置。 - `int get()` 提供一个未分配给任何人的号码。如果没有可用号码则返回 $-1$。 - `bool check(int number)` 如果位置 $number$ 可用返回 $true$ 否则返回 $false$。 - `void release(int number)` 回收或释放位置 $number$。 **说明**: - $1 \le maxNumbers \le 10^{4}$。 - $0 \le number \lt maxNumbers$。 - `get`,`check` 和 `release` 最多被调用 $2 \times 10^{4}$ 次。 **示例**: - 示例 1: ```python 输入: ["PhoneDirectory", "get", "get", "check", "get", "check", "release", "check"] [[3], [], [], [2], [], [2], [2], [2]] 输出: [null, 0, 1, true, 2, false, null, true] 解释: PhoneDirectory phoneDirectory = new PhoneDirectory(3); phoneDirectory.get(); // 它可以返回任意可用的数字。这里我们假设它返回 0。 phoneDirectory.get(); // 假设它返回 1。 phoneDirectory.check(2); // 数字 2 可用,所以返回 true。 phoneDirectory.get(); // 返回剩下的唯一一个数字 2。 phoneDirectory.check(2); // 数字 2 不再可用,所以返回 false。 phoneDirectory.release(2); // 将数字 2 释放回号码池。 phoneDirectory.check(2); // 数字 2 重新可用,返回 true。 ``` ## 解题思路 ### 思路 1:集合 + 队列 使用集合来快速检查号码是否可用,使用队列来高效分配号码。这种方法结合了集合的 $O(1)$ 查找性能和队列的先进先出特性。 具体步骤: 1. **初始化**:创建集合 $available$ 存储所有可用号码,创建队列 $queue$ 用于快速分配号码。 2. **get 操作**:从队列头部取出一个号码,并从集合中移除该号码。 3. **check 操作**:检查号码是否在可用集合中。 4. **release 操作**:将号码重新加入集合和队列。 这种方法的关键是维护两个数据结构的一致性,确保号码的状态在两个数据结构中保持同步。 ### 思路 1:代码 ```python from collections import deque class PhoneDirectory: def __init__(self, maxNumbers: int): # 使用集合存储所有可用的号码,支持 O(1) 查找 self.available = set() # 使用队列存储可用号码,支持 O(1) 分配 self.queue = deque() # 初始化所有号码为可用状态 for i in range(maxNumbers): self.available.add(i) self.queue.append(i) def get(self) -> int: # 如果没有可用号码,返回 -1 if not self.available: return -1 # 从队列头部取出一个号码 number = self.queue.popleft() # 从可用集合中移除该号码 self.available.remove(number) return number def check(self, number: int) -> bool: # 检查号码是否在可用集合中 return number in self.available def release(self, number: int) -> None: # 如果号码已经可用,不需要重复释放 if number in self.available: return # 将号码重新加入可用集合和队列 self.available.add(number) self.queue.append(number) # Your PhoneDirectory object will be instantiated and called as such: # obj = PhoneDirectory(maxNumbers) # param_1 = obj.get() # param_2 = obj.check(number) # obj.release(number) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `get` 操作:$O(1)$,从队列头部取出元素,从集合中移除元素。 - `check` 操作:$O(1)$,集合查找操作。 - `release` 操作:$O(1)$,向集合和队列添加元素。 - **空间复杂度**:$O(n)$,其中 $n$ 是 $maxNumbers$,需要存储所有号码的状态。 ================================================ FILE: docs/solutions/0300-0399/design-snake-game.md ================================================ # [0353. 贪吃蛇](https://leetcode.cn/problems/design-snake-game/) - 标签:设计、队列、数组、哈希表、模拟 - 难度:中等 ## 题目链接 - [0353. 贪吃蛇 - 力扣](https://leetcode.cn/problems/design-snake-game/) ## 题目大意 **要求**: 设计一个「贪吃蛇游戏」,该游戏将会在一个「屏幕尺寸 = 宽度 x 高度」的屏幕上运行。如果你不熟悉这个游戏,可以 [点击这里](http://patorjk.com/games/snake/) 在线试玩。 起初时,蛇在左上角的 $(0, 0)$ 位置,身体长度为 $1$ 个单位。 你将会被给出一个数组形式的食物位置序列 $food$ ,其中 $food[i] = (ri, ci)$。当蛇吃到食物时,身子的长度会增加 $1$ 个单位,得分也会 $+1$。 食物不会同时出现,会按列表的顺序逐一显示在屏幕上。比方讲,第一个食物被蛇吃掉后,第二个食物才会出现。 当一个食物在屏幕上出现时,保证不会出现在被蛇身体占据的格子里。 如果蛇越界(与边界相撞)或者头与「移动后」的身体相撞(即,身长为 $4$ 的蛇无法与自己相撞),游戏结束。 实现 `SnakeGame` 类: - `SnakeGame(int width, int height, int[][] food)` 初始化对象,屏幕大小为 $height \times width$,食物位置序列为 $food$。 - `int move(String direction)` 返回蛇在方向 $direction$ 上移动后的得分。如果游戏结束,返回 $-1$。 **说明**: - $1 \le width, height \le 10^{4}$。 - $1 \le food.length \le 50$。 - $food[i].length == 2$。 - $0 \le ri \lt height$。 - $0 \le ci \lt width$。 - $direction.length == 1$。 - $direction$ 为 `'U'`, `'D'`, `'L'`, or `'R'`。 - 最多调用 $10^{4}$ 次 `move` 方法。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/01/13/snake.jpg) ```python 输入: ["SnakeGame", "move", "move", "move", "move", "move", "move"] [[3, 2, [[1, 2], [0, 1]]], ["R"], ["D"], ["R"], ["U"], ["L"], ["U"]] 输出: [null, 0, 0, 1, 1, 2, -1] 解释: SnakeGame snakeGame = new SnakeGame(3, 2, [[1, 2], [0, 1]]); snakeGame.move("R"); // 返回 0 snakeGame.move("D"); // 返回 0 snakeGame.move("R"); // 返回 1 ,蛇吃掉了第一个食物,同时第二个食物出现在 (0, 1) snakeGame.move("U"); // 返回 1 snakeGame.move("L"); // 返回 2 ,蛇吃掉了第二个食物,没有出现更多食物 snakeGame.move("U"); // 返回 -1 ,蛇与边界相撞,游戏结束 ``` ## 解题思路 ### 思路 1:队列 + 哈希表 使用双端队列 `deque` 来存储蛇的身体位置,使用集合 `set` 来快速检查碰撞。蛇的头部在队列的末尾,尾部在队列的开头。 具体步骤: 1. **初始化**: - 使用双端队列 $snake$ 存储蛇的身体位置,初始位置为 $(0, 0)$。 - 使用集合 $occupied$ 存储蛇身体占据的位置,用于快速碰撞检测。 - 记录屏幕尺寸 $width$ 和 $height$。 - 记录食物列表 $food$ 和当前食物索引 $food\_index$。 - 记录当前得分 $score$。 2. **move 操作**: - 根据方向 $direction$ 计算新的头部位置 $new\_head$。 - 检查新位置是否越界:$new\_head[0] < 0$ 或 $new\_head[0] \geq height$ 或 $new\_head[1] < 0$ 或 $new\_head[1] \geq width$。 - 检查新位置是否与蛇身碰撞:$new\_head$ 在 $occupied$ 集合中。 - 如果碰撞,返回 $-1$。 - 将新头部加入队列和集合。 - 检查是否吃到食物:$new\_head$ 等于当前食物位置。 - 如果吃到食物,得分 $+1$,食物索引 $+1$。 - 如果没吃到食物,移除尾部位置。 - 返回当前得分。 ### 思路 1:代码 ```python class SnakeGame: def __init__(self, width: int, height: int, food: List[List[int]]): # 初始化屏幕尺寸 self.width = width self.height = height # 初始化食物列表和索引 self.food = food self.food_index = 0 # 初始化蛇的身体(使用双端队列,头部在末尾) self.snake = deque([(0, 0)]) # 使用集合存储蛇身体占据的位置,用于快速碰撞检测 self.occupied = {(0, 0)} # 初始化得分 self.score = 0 def move(self, direction: str) -> int: # 获取当前头部位置 head_row, head_col = self.snake[-1] # 根据方向计算新的头部位置 if direction == 'U': new_head = (head_row - 1, head_col) elif direction == 'D': new_head = (head_row + 1, head_col) elif direction == 'L': new_head = (head_row, head_col - 1) else: # direction == 'R' new_head = (head_row, head_col + 1) # 检查是否越界 if (new_head[0] < 0 or new_head[0] >= self.height or new_head[1] < 0 or new_head[1] >= self.width): return -1 # 检查是否吃到食物 eating_food = (self.food_index < len(self.food) and new_head == tuple(self.food[self.food_index])) if not eating_food: # 没吃到食物,先移除尾部 tail = self.snake.popleft() self.occupied.remove(tail) # 检查是否与蛇身碰撞(在移除尾部后检查) if new_head in self.occupied: return -1 # 将新头部加入蛇身 self.snake.append(new_head) self.occupied.add(new_head) if eating_food: # 吃到食物,得分 +1,食物索引 +1 self.score += 1 self.food_index += 1 return self.score # Your SnakeGame object will be instantiated and called as such: # obj = SnakeGame(width, height, food) # param_1 = obj.move(direction) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `move` 操作:$O(1)$,所有操作都是常数时间。 - **空间复杂度**:$O(n)$,其中 $n$ 是蛇的最大长度。 ================================================ FILE: docs/solutions/0300-0399/design-tic-tac-toe.md ================================================ # [0348. 设计井字棋](https://leetcode.cn/problems/design-tic-tac-toe/) - 标签:设计、数组、哈希表、矩阵、模拟 - 难度:中等 ## 题目链接 - [0348. 设计井字棋 - 力扣](https://leetcode.cn/problems/design-tic-tac-toe/) ## 题目大意 **要求**: 在 $n \times n$ 的棋盘上,实现一个判定井字棋(Tic-Tac-Toe)胜负的神器,判断每一次玩家落子后,是否有胜出的玩家。 在这个井字棋游戏中,会有 $2$ 名玩家,他们将轮流在棋盘上放置自己的棋子。 在实现这个判定器的过程中,你可以假设以下这些规则一定成立: 1. 每一步棋都是在棋盘内的,并且只能被放置在一个空的格子里; 2. 一旦游戏中有一名玩家胜出的话,游戏将不能再继续; 3. 一个玩家如果在同一行、同一列或者同一斜对角线上都放置了自己的棋子,那么他便获得胜利。 **说明**: - $2 \le n \le 10^{3}$。 - 玩家是 $1$ 或 $2$。 - $0 \le row, col \lt n$。 - 每次调用 `move` 时 $(row, col)$ 都是不同的。 - 最多调用 `move` $n2$ 次。 - 进阶:有没有可能将每一步的 `move()` 操作优化到比 $O(n2)$ 更快吗? **示例**: - 示例 1: ```python 给定棋盘边长 n = 3, 玩家 1 的棋子符号是 "X",玩家 2 的棋子符号是 "O"。 TicTacToe toe = new TicTacToe(3); toe.move(0, 0, 1); -> 函数返回 0 (此时,暂时没有玩家赢得这场对决) |X| | | | | | | // 玩家 1 在 (0, 0) 落子。 | | | | toe.move(0, 2, 2); -> 函数返回 0 (暂时没有玩家赢得本场比赛) |X| |O| | | | | // 玩家 2 在 (0, 2) 落子。 | | | | toe.move(2, 2, 1); -> 函数返回 0 (暂时没有玩家赢得比赛) |X| |O| | | | | // 玩家 1 在 (2, 2) 落子。 | | |X| toe.move(1, 1, 2); -> 函数返回 0 (暂没有玩家赢得比赛) |X| |O| | |O| | // 玩家 2 在 (1, 1) 落子。 | | |X| toe.move(2, 0, 1); -> 函数返回 0 (暂无玩家赢得比赛) |X| |O| | |O| | // 玩家 1 在 (2, 0) 落子。 |X| |X| toe.move(1, 0, 2); -> 函数返回 0 (没有玩家赢得比赛) |X| |O| |O|O| | // 玩家 2 在 (1, 0) 落子. |X| |X| toe.move(2, 1, 1); -> 函数返回 1 (此时,玩家 1 赢得了该场比赛) |X| |O| |O|O| | // 玩家 1 在 (2, 1) 落子。 |X|X|X| ``` ## 解题思路 ### 思路 1:计数法 **核心思想**:使用计数数组来跟踪每个玩家在每行、每列和两条对角线上的棋子数量,避免每次检查整个棋盘。 **算法步骤**: 1. **初始化**:创建 $n \times n$ 的棋盘,以及用于计数的数组: - `rows[i]`:第 $i$ 行上每个玩家的棋子数量。 - `cols[j]`:第 $j$ 列上每个玩家的棋子数量。 - `diagonal`:主对角线上的棋子数量。 - `anti_diagonal`:副对角线上的棋子数量。 2. **落子操作**: - 在位置 $(row, col)$ 放置玩家 $player$ 的棋子。 - 更新对应的行、列计数。 - 如果 $(row, col)$ 在主对角线上($row = col$),更新 `diagonal`。 - 如果 $(row, col)$ 在副对角线上($row + col = n - 1$),更新 `anti_diagonal`。 3. **胜负判断**: - 检查当前行、列或对角线是否被当前玩家完全占据。 - 如果任一计数达到 $n$,则该玩家获胜。 ### 思路 1:代码 ```python class TicTacToe: def __init__(self, n: int): """ 初始化井字棋游戏 :param n: 棋盘大小 n x n """ self.n = n # 初始化棋盘 self.board = [[0 for _ in range(n)] for _ in range(n)] # 计数数组:跟踪每个玩家在每行、每列的棋子数量 # rows[i][player] 表示第 i 行上玩家 player 的棋子数量 self.rows = [[0, 0] for _ in range(n)] # 索引 0 和 1 分别对应玩家 1 和 2 self.cols = [[0, 0] for _ in range(n)] # 索引 0 和 1 分别对应玩家 1 和 2 # 对角线计数 self.diagonal = [0, 0] # 主对角线 (i == j) self.anti_diagonal = [0, 0] # 副对角线 (i + j == n - 1) def move(self, row: int, col: int, player: int) -> int: """ 玩家在指定位置落子 :param row: 行索引 :param col: 列索引 :param player: 玩家编号 (1 或 2) :return: 获胜玩家编号,如果无人获胜返回 0 """ # 将玩家编号转换为数组索引 (1 -> 0, 2 -> 1) player_idx = player - 1 # 在棋盘上放置棋子 self.board[row][col] = player # 更新行计数 self.rows[row][player_idx] += 1 # 更新列计数 self.cols[col][player_idx] += 1 # 更新主对角线计数 (row == col) if row == col: self.diagonal[player_idx] += 1 # 更新副对角线计数 (row + col == n - 1) if row + col == self.n - 1: self.anti_diagonal[player_idx] += 1 # 检查是否获胜 # 如果当前行、列或任一对角线被当前玩家完全占据,则获胜 if (self.rows[row][player_idx] == self.n or self.cols[col][player_idx] == self.n or self.diagonal[player_idx] == self.n or self.anti_diagonal[player_idx] == self.n): return player return 0 # 无人获胜 # Your TicTacToe object will be instantiated and called as such: # obj = TicTacToe(n) # param_1 = obj.move(row,col,player) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。每次 `move` 操作只需要更新计数数组和检查胜负,时间复杂度为常数。 - **空间复杂度**:$O(n)$。需要 $O(n^2)$ 空间存储棋盘,$O(n)$ 空间存储计数数组,总体为 $O(n^2)$。 ================================================ FILE: docs/solutions/0300-0399/design-twitter.md ================================================ # [0355. 设计推特](https://leetcode.cn/problems/design-twitter/) - 标签:设计、哈希表、链表、堆(优先队列) - 难度:中等 ## 题目链接 - [0355. 设计推特 - 力扣](https://leetcode.cn/problems/design-twitter/) ## 题目大意 **要求**: 设计一个简化版的推特(Twitter),可以让用户实现发送推文,关注/取消关注其他用户,能够看见关注人(包括自己)的最近 $10$ 条推文。 实现 `Twitter` 类: - `Twitter() ` 初始化简易版推特对象 - `void postTweet(int userId, int tweetId)` 根据给定的 $tweetId$ 和 $userId$ 创建一条新推文。每次调用此函数都会使用一个不同的 $tweetId$ 。 - `List getNewsFeed(int userId)` 检索当前用户新闻推送中最近 $10$ 条推文的 ID。新闻推送中的每一项都必须是由用户关注的人或者是用户自己发布的推文。推文必须 按照时间顺序由最近到最远排序 。 - `void follow(int followerId, int followeeId)` ID 为 $followerId$ 的用户开始关注 ID 为 $followeeId$ 的用户。 - `void unfollow(int followerId, int followeeId)` ID 为 $followerId$ 的用户不再关注 ID 为 $followeeId$ 的用户。 **说明**: - $1 \le userId, followerId, followeeId \le 500$。 - $0 \le tweetId \le 10^{4}$。 - 所有推特的 ID 都互不相同。 - `postTweet`、`getNewsFeed`、`follow` 和 `unfollow` 方法最多调用 $3 times 10^{4}$ 次。 - 用户不能关注自己。 **示例**: - 示例 1: ```python 输入 ["Twitter", "postTweet", "getNewsFeed", "follow", "postTweet", "getNewsFeed", "unfollow", "getNewsFeed"] [[], [1, 5], [1], [1, 2], [2, 6], [1], [1, 2], [1]] 输出 [null, null, [5], null, null, [6, 5], null, [5]] 解释 Twitter twitter = new Twitter(); twitter.postTweet(1, 5); // 用户 1 发送了一条新推文 (用户 id = 1, 推文 id = 5) twitter.getNewsFeed(1); // 用户 1 的获取推文应当返回一个列表,其中包含一个 id 为 5 的推文 twitter.follow(1, 2); // 用户 1 关注了用户 2 twitter.postTweet(2, 6); // 用户 2 发送了一个新推文 (推文 id = 6) twitter.getNewsFeed(1); // 用户 1 的获取推文应当返回一个列表,其中包含两个推文,id 分别为 -> [6, 5] 。推文 id 6 应当在推文 id 5 之前,因为它是在 5 之后发送的 twitter.unfollow(1, 2); // 用户 1 取消关注了用户 2 twitter.getNewsFeed(1); // 用户 1 获取推文应当返回一个列表,其中包含一个 id 为 5 的推文。因为用户 1 已经不再关注用户 2 ``` ## 解题思路 ### 思路 1:哈希表 + 全局时间戳 使用哈希表来存储用户信息和推文信息,结合全局时间戳来维护推文的时间顺序。 具体步骤: 1. **数据结构设计**: - 使用全局时间戳 $timestamp$ 来记录每条推文的发布时间。 - 使用哈希表 $tweets$ 存储每个用户的推文列表,每个推文包含 $tweetId$ 和 $timestamp$。 - 使用哈希表 $follows$ 存储每个用户关注的用户集合。 - 使用哈希表 $followers$ 存储每个用户的粉丝集合。 2. **postTweet 操作**: - 将新推文 $[tweetId, timestamp]$ 添加到对应用户的推文列表中。 - 递增全局时间戳 $timestamp$。 3. **getNewsFeed 操作**: - 获取用户自己及其关注的所有用户的推文。 - 合并所有推文并按时间戳降序排序。 - 返回最近的 $10$ 条推文。 4. **follow 操作**: - 在 $follows[followerId]$ 中添加 $followeeId$。 - 在 $followers[followeeId]$ 中添加 $followerId$。 5. **unfollow 操作**: - 从 $follows[followerId]$ 中移除 $followeeId$。 - 从 $followers[followeeId]$ 中移除 $followerId$。 ### 思路 1:代码 ```python class Twitter: def __init__(self): # 全局时间戳,用于记录推文发布顺序 self.timestamp = 0 # 存储每个用户的推文列表,每个推文格式为 [tweetId, timestamp] self.tweets = defaultdict(list) # 存储每个用户关注的用户集合 self.follows = defaultdict(set) # 存储每个用户的粉丝集合 self.followers = defaultdict(set) def postTweet(self, userId: int, tweetId: int) -> None: # 将新推文添加到用户的推文列表中 self.tweets[userId].append([tweetId, self.timestamp]) # 递增全局时间戳 self.timestamp += 1 def getNewsFeed(self, userId: int) -> List[int]: # 获取用户自己及其关注的所有用户 all_users = {userId} | self.follows[userId] # 收集所有相关用户的推文 all_tweets = [] for user in all_users: all_tweets.extend(self.tweets[user]) # 按时间戳降序排序(最新的在前) all_tweets.sort(key=lambda x: x[1], reverse=True) # 返回最近的 10 条推文的 tweetId return [tweet[0] for tweet in all_tweets[:10]] def follow(self, followerId: int, followeeId: int) -> None: # 用户不能关注自己 if followerId == followeeId: return # 添加关注关系 self.follows[followerId].add(followeeId) self.followers[followeeId].add(followerId) def unfollow(self, followerId: int, followeeId: int) -> None: # 移除关注关系 self.follows[followerId].discard(followeeId) self.followers[followeeId].discard(followerId) # Your Twitter object will be instantiated and called as such: # obj = Twitter() # obj.postTweet(userId,tweetId) # param_2 = obj.getNewsFeed(userId) # obj.follow(followerId,followeeId) # obj.unfollow(followerId,followeeId) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `postTweet` 操作:$O(1)$,只需要添加推文到列表末尾。 - `getNewsFeed` 操作:$O(n \log n)$,其中 $n$ 是所有相关用户的推文总数,需要排序。 - `follow` 操作:$O(1)$,只需要在集合中添加元素。 - `unfollow` 操作:$O(1)$,只需要从集合中移除元素。 - **空间复杂度**:$O(m + n)$,其中 $m$ 是推文总数,$n$ 是用户总数。 ================================================ FILE: docs/solutions/0300-0399/elimination-game.md ================================================ # [0390. 消除游戏](https://leetcode.cn/problems/elimination-game/) - 标签:递归、数学 - 难度:中等 ## 题目链接 - [0390. 消除游戏 - 力扣](https://leetcode.cn/problems/elimination-game/) ## 题目大意 **描述**: 给定一个整数 $n$。列表 $arr$ 由在范围 $[1, n]$ 中的所有整数组成,并按严格递增排序。 **要求**: 请你对 $arr$ 应用下述算法: - 从左到右,删除第一个数字,然后每隔一个数字删除一个,直到到达列表末尾。 - 重复上面的步骤,但这次是从右到左。也就是,删除最右侧的数字,然后剩下的数字每隔一个删除一个。 - 不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字。 返回 $arr$ 最后剩下的数字。 **说明**: - $1 \le n \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:n = 9 输出:6 解释: arr = [1, 2, 3, 4, 5, 6, 7, 8, 9] arr = [2, 4, 6, 8] arr = [2, 6] arr = [6] ``` - 示例 2: ```python 输入:n = 1 输出:1 ``` ## 解题思路 ### 思路 1:递归 + 数学规律 观察消除过程,我们可以发现以下规律: 1. **从左到右消除**:每次消除后,剩余数字的间隔变为原来的 $2$ 倍,起始位置变为原来的第 $2$ 个位置。 2. **从右到左消除**:每次消除后,剩余数字的间隔变为原来的 $2$ 倍,但起始位置需要重新计算。 设 $f(n)$ 表示从 $1$ 到 $n$ 的数字经过消除游戏后剩余的数字。 **递推关系**: - 当 $n = 1$ 时:$f(1) = 1$。 - 当 $n > 1$ 时: - 从左到右消除后,剩余 $\lfloor \frac{n}{2} \rfloor$ 个数字,间隔为 $2$。 - 然后从右到左消除,相当于对剩余数字进行镜像操作。 - $f(n) = 2 \times (1 + \lfloor \frac{n}{2} \rfloor - f(\lfloor \frac{n}{2} \rfloor))$。 **关键点**: - 从左到右消除后,剩余数字为 $[2, 4, 6, 8, ...]$,共 $\lfloor \frac{n}{2} \rfloor$ 个。 - 这些数字可以重新编号为 $[1, 2, 3, 4, ...]$,共 $\lfloor \frac{n}{2} \rfloor$ 个。 - 对重新编号的数字进行消除游戏,得到 $f(\lfloor \frac{n}{2} \rfloor)$。 - 由于下一步是从右到左开始,需要计算镜像位置:$1 + \lfloor \frac{n}{2} \rfloor - f(\lfloor \frac{n}{2} \rfloor)$。 - 最后乘以 $2$ 得到在原序列中的位置。 **示例验证**($n = 9$): 1. 初始:$[1, 2, 3, 4, 5, 6, 7, 8, 9]$。 2. 从左到右消除:$[2, 4, 6, 8]$(剩余 $4$ 个数字)。 3. 重新编号:$[1, 2, 3, 4]$,对 $4$ 个数字进行消除游戏。 4. 计算 $f(4)$:$f(4) = 2 \times (1 + 2 - f(2)) = 2 \times (1 + 2 - 2) = 2$。 5. 镜像位置:$1 + 4 - 2 = 3$,对应原序列中的 $2 \times 3 = 6$。 ### 思路 1:代码 ```python class Solution: def lastRemaining(self, n: int) -> int: def f(n): """计算从1到n的数字经过消除游戏后剩余的数字""" if n == 1: return 1 # 从左到右消除后,剩余数字为[2,4,6,8,...],共n//2个 # 这些数字可以重新编号为[1,2,3,4,...] # 对重新编号的数字进行消除游戏,得到f(n//2) # 由于下一步是从右到左,需要计算镜像位置 return 2 * (1 + n // 2 - f(n // 2)) return f(n) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$,每次递归调用都将问题规模减半。 - **空间复杂度**:$O(\log n)$,递归调用栈的深度为 $O(\log n)$。 ================================================ FILE: docs/solutions/0300-0399/evaluate-division.md ================================================ # [0399. 除法求值](https://leetcode.cn/problems/evaluate-division/) - 标签:深度优先搜索、广度优先搜索、并查集、图、数组、最短路 - 难度:中等 ## 题目链接 - [0399. 除法求值 - 力扣](https://leetcode.cn/problems/evaluate-division/) ## 题目大意 **描述**:给定一个变量对数组 $equations$ 和一个实数数组 $values$ 作为已知条件,其中 $equations[i] = [Ai, Bi]$ 和 $values[i]$ 共同表示 `Ai / Bi = values[i]`。每个 $Ai$ 或 $Bi$ 是一个表示单个变量的字符串。 再给定一个表示多个问题的数组 $queries$,其中 $queries[j] = [Cj, Dj]$ 表示第 $j$ 个问题,要求:根据已知条件找出 `Cj / Dj = ?` 的结果作为答案。 **要求**:返回所有问题的答案。如果某个答案无法确定,则用 $-1.0$ 代替,如果问题中出现了给定的已知条件中没有出现的表示变量的字符串,则也用 $-1.0$ 代替这个答案。 **说明**: - 未在等式列表中出现的变量是未定义的,因此无法确定它们的答案。 - $1 \le equations.length \le 20$。 - $equations[i].length == 2$。 - $1 \le Ai.length, Bi.length \le 5$。 - $values.length == equations.length$。 - $0.0 < values[i] \le 20.0$。 - $1 \le queries.length \le 20$。 - $queries[i].length == 2$。 - $1 \le Cj.length, Dj.length \le 5$。 - $Ai, Bi, Cj, Dj$ 由小写英文字母与数字组成。 **示例**: - 示例 1: ```python 输入:equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]] 输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000] 解释: 条件:a / b = 2.0, b / c = 3.0 问题:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ? 结果:[6.0, 0.5, -1.0, 1.0, -1.0 ] 注意:x 是未定义的 => -1.0 ``` - 示例 2: ```python 输入:equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]] 输出:[3.75000,0.40000,5.00000,0.20000] ``` ## 解题思路 ### 思路 1:并查集 在「[等式方程的可满足性](https://leetcode.cn/problems/satisfiability-of-equality-equations)」的基础上增加了倍数关系。在「[等式方程的可满足性](https://leetcode.cn/problems/satisfiability-of-equality-equations)」中我们处理传递关系使用了并查集,这道题也是一样,不过在使用并查集的同时还要维护倍数关系。 举例说明: - `a / b = 2.0`:说明 $a == 2b$,$a$ 和 $b$ 在同一个集合。 - `b / c = 3.0`:说明 $b == 3c$,$b$ 和 $c$ 在同一个集合。 根据上述两式可得:$a$、$b$、$c$ 都在一个集合中,且 $a == 2b == 6c$。 我们可以将同一集合中的变量倍数关系都转换为与根节点变量的倍数关系,比如上述例子中都转变为与 $a$ 的倍数关系。 具体操作如下: - 定义并查集结构,并在并查集中定义一个表示倍数关系的 $multiples$ 数组。 - 遍历 $equations$ 数组、$values$ 数组,将每个变量按顺序编号,并使用 `union` 将其并入相同集合。 - 遍历 $queries$ 数组,判断两个变量是否在并查集中,并且是否在同一集合。如果找到对应关系,则将计算后的倍数关系存入答案数组,否则则将 $-1$ 存入答案数组。 - 最终输出答案数组。 并查集中维护倍数相关方法说明: - `find` 方法: - 递推寻找根节点,并将倍数累乘,然后进行路径压缩,并且更新当前节点的倍数关系。 - `union` 方法: - 如果两个节点属于同一集合,则直接返回。 - 如果两个节点不属于同一个集合,合并之前当前节点的倍数关系更新,然后再进行更新。 - `is_connected` 方法: - 如果两个节点不属于同一集合,返回 $-1$。 - 如果两个节点属于同一集合,则返回倍数关系。 ### 思路 1:代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] self.multiples = [1 for _ in range(n)] def find(self, x): multiple = 1.0 origin = x while x != self.parent[x]: multiple *= self.multiples[x] x = self.parent[x] self.parent[origin] = x self.multiples[origin] = multiple return x def union(self, x, y, multiple): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.multiples[root_x] = multiple * self.multiples[y] / self.multiples[x] return def is_connected(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x != root_y: return -1.0 return self.multiples[x] / self.multiples[y] class Solution: def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]: equations_size = len(equations) hash_map = dict() union_find = UnionFind(2 * equations_size) id = 0 for i in range(equations_size): equation = equations[i] var1, var2 = equation[0], equation[1] if var1 not in hash_map: hash_map[var1] = id id += 1 if var2 not in hash_map: hash_map[var2] = id id += 1 union_find.union(hash_map[var1], hash_map[var2], values[i]) queries_size = len(queries) res = [] for i in range(queries_size): query = queries[i] var1, var2 = query[0], query[1] if var1 not in hash_map or var2 not in hash_map: res.append(-1.0) else: id1 = hash_map[var1] id2 = hash_map[var2] res.append(union_find.is_connected(id1, id2)) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O((m + n) \times \alpha(m + n))$,$\alpha$ 是反 `Ackerman` 函数。 - **空间复杂度**:$O(m + n)$。 ================================================ FILE: docs/solutions/0300-0399/find-k-pairs-with-smallest-sums.md ================================================ # [0373. 查找和最小的 K 对数字](https://leetcode.cn/problems/find-k-pairs-with-smallest-sums/) - 标签:数组、堆(优先队列) - 难度:中等 ## 题目链接 - [0373. 查找和最小的 K 对数字 - 力扣](https://leetcode.cn/problems/find-k-pairs-with-smallest-sums/) ## 题目大意 **描述**: 给定两个以「非递减顺序排列」的整数数组 $nums1$ 和 $nums2$, 以及一个整数 $k$。 定义一对值 $(u, v)$,其中第一个元素来自 $nums1$,第二个元素来自 $nums2$。 **要求**: 请找到和最小的 $k$ 个数对 $(u1, v1), (u2, v2) ... (uk, vk)$。 **说明**: - $1 \le nums1.length, nums2.length \le 10^{5}$。 - $-10^{9} \le nums1[i], nums2[i] \le 10^{9}$。 - $nums1$ 和 $nums2$ 均为升序排列。 - $1 \le k \le 10^{4}$。 - $k \le nums1.length \times nums2.length$。 **示例**: - 示例 1: ```python 输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3 输出: [1,2],[1,4],[1,6] 解释: 返回序列中的前 3 对数: [1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6] ``` - 示例 2: ```python 输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2 输出: [1,1],[1,1] 解释: 返回序列中的前 2 对数: [1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3] ``` ## 解题思路 ### 思路 1:优先队列(最小堆) 这道题的核心思想是:**使用优先队列来维护当前和最小的数对候选**。 解题步骤: 1. **初始化优先队列**:将 $nums1$ 中每个元素与 $nums2[0]$ 组成的数对加入优先队列,队列中存储 $(sum, i, j)$ 三元组,其中 $sum = nums1[i] + nums2[j]$。 2. **逐步扩展候选**:每次从队列中取出和最小的数对 $(sum, i, j)$,将其加入结果。 3. **添加新的候选**:如果 $j + 1 < len(nums2)$,则将 $(nums1[i] + nums2[j+1], i, j+1)$ 加入队列。 4. **重复直到找到 $k$ 个数对**。 **关键点**: - 由于 $nums1$ 和 $nums2$ 都是非递减的,对于固定的 $i$,$nums1[i] + nums2[j]$ 随着 $j$ 增大而增大。 - 优先队列确保我们总是处理当前和最小的数对。 - 每次取出一个数对后,只需要考虑该数对的下一个可能候选($j+1$)。 **算法正确性**: 设当前队列中最小和为 $sum_{min} = nums1[i] + nums2[j]$,则所有未处理的数对 $(nums1[x], nums2[y])$ 满足: - 如果 $x < i$,则 $nums1[x] + nums2[y] \ge nums1[x] + nums2[0] \ge nums1[i] + nums2[0] \ge sum_{min}$(因为 $nums1$ 非递减)。 - 如果 $x = i$ 且 $y > j$,则 $nums1[i] + nums2[y] \ge sum_{min}$(因为 $nums2$ 非递减)。 - 如果 $x > i$,则 $nums1[x] + nums2[y] \ge nums1[i+1] + nums2[0] \ge nums1[i] + nums2[0] \ge sum_{min}$。 因此,当前队列中的最小和确实是全局最小和。 ### 思路 1:代码 ```python import heapq from typing import List class Solution: def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]: if not nums1 or not nums2 or k == 0: return [] # 优先队列,存储(sum, i, j)三元组 # sum = nums1[i] + nums2[j] heap = [] # 初始化:将nums1中每个元素与nums2[0]组成的数对加入队列 for i in range(min(len(nums1), k)): heapq.heappush(heap, (nums1[i] + nums2[0], i, 0)) result = [] # 从队列中取出k个和最小的数对 while heap and len(result) < k: # 取出当前和最小的数对 sum_val, i, j = heapq.heappop(heap) result.append([nums1[i], nums2[j]]) # 如果nums2中还有下一个元素,添加新的候选 if j + 1 < len(nums2): heapq.heappush(heap, (nums1[i] + nums2[j + 1], i, j + 1)) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k \log k)$,其中 $k$ 是要求的数对个数。最多需要从优先队列中取出 $k$ 个元素,每次取出和插入操作的时间复杂度为 $O(\log k)$。 - **空间复杂度**:$O(k)$,优先队列最多存储 $k$ 个元素。 ================================================ FILE: docs/solutions/0300-0399/find-leaves-of-binary-tree.md ================================================ # [0366. 寻找二叉树的叶子节点](https://leetcode.cn/problems/find-leaves-of-binary-tree/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0366. 寻找二叉树的叶子节点 - 力扣](https://leetcode.cn/problems/find-leaves-of-binary-tree/) ## 题目大意 **描述**: 给定一棵二叉树的 $root$ 节点。 **要求**: 请按照以下方式收集树的节点: - 收集所有的叶子节点。 - 移除所有的叶子节点。 - 重复以上步骤,直到树为空。 **说明**: - 树中节点的数量在 $[1, 10^{3}]$ 范围内。 - $-10^{3} \le Node.val \le 10^{3}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/16/remleaves-tree.jpg) ```python 输入:root = [1,2,3,4,5] 输出:[[4,5,3],[2],[1]] 解释: [[3,5,4],[2],[1]] 和 [[3,4,5],[2],[1]] 也被视作正确答案,因为每一层返回元素的顺序不影响结果。 ``` - 示例 2: ```python 输入:root = [1] 输出:[[1]] ``` ## 解题思路 ### 思路 1:深度优先搜索 + 高度计算 这道题的核心思想是:**根据节点的高度来分组收集叶子节点**。 解题步骤: 1. **定义节点高度**:设节点 $node$ 的高度为 $height(node)$,表示从该节点到叶子节点的最长路径上的边数。 - 叶子节点的高度为 $0$。 - 非叶子节点的高度为其左右子树高度的最大值加 $1$。 2. **递归计算高度**:使用深度优先搜索遍历二叉树,计算每个节点的高度。 3. **按高度分组**:将具有相同高度的节点值收集到同一个列表中。 4. **返回结果**:按照高度从 $0$ 到最大高度的顺序返回结果列表。 **关键点**: - 第一次收集的叶子节点高度为 $0$。 - 移除叶子节点后,新的叶子节点高度为 $1$。 - 以此类推,第 $i$ 轮收集的节点高度为 $i-1$。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def findLeaves(self, root: Optional[TreeNode]) -> List[List[int]]: def getHeight(node): """计算节点高度并收集节点值""" if not node: return -1 # 空节点高度为-1 # 递归计算左右子树的高度 left_height = getHeight(node.left) right_height = getHeight(node.right) # 当前节点的高度 = max(左子树高度, 右子树高度) + 1 current_height = max(left_height, right_height) + 1 # 确保结果列表有足够的长度 while len(result) <= current_height: result.append([]) # 将当前节点值添加到对应高度的列表中 result[current_height].append(node.val) return current_height result = [] getHeight(root) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树中节点的数量。每个节点被访问一次。 - **空间复杂度**:$O(n)$,其中 $n$ 是树中节点的数量。递归调用栈的深度最多为树的高度,最坏情况下为 $O(n)$;结果列表最多存储 $n$ 个节点值。 ================================================ FILE: docs/solutions/0300-0399/find-the-difference.md ================================================ # [0389. 找不同](https://leetcode.cn/problems/find-the-difference/) - 标签:位运算、哈希表、字符串、排序 - 难度:简单 ## 题目链接 - [0389. 找不同 - 力扣](https://leetcode.cn/problems/find-the-difference/) ## 题目大意 给定两个只包含小写字母的字符串 s、t。字符串 t 是由 s 进行随机重拍之后,再在随机位置添加一个字母得到的。要求:找出字符串 t 中被添加的字母。 ## 解题思路 字符串 t 比字符串 s 多了一个随机字母。可以使用哈希表存储一下字符串 s 中各个字符的数量,再遍历一遍字符串 t 中的字符,从哈希表中减去对应数量的字符,最后剩的那一个字符就是多余的字符。 ## 代码 ```python class Solution: def findTheDifference(self, s: str, t: str) -> str: s_dict = dict() for ch in s: if ch in s_dict: s_dict[ch] += 1 else: s_dict[ch] = 1 for ch in t: if ch in s_dict and s_dict[ch] != 0: s_dict[ch] -= 1 else: return ch ``` ================================================ FILE: docs/solutions/0300-0399/first-unique-character-in-a-string.md ================================================ # [0387. 字符串中的第一个唯一字符](https://leetcode.cn/problems/first-unique-character-in-a-string/) - 标签:队列、哈希表、字符串、计数 - 难度:简单 ## 题目链接 - [0387. 字符串中的第一个唯一字符 - 力扣](https://leetcode.cn/problems/first-unique-character-in-a-string/) ## 题目大意 给定一个只包含小写字母的字符串 `s`。 要求:找到第一个不重复的字符,并返回它的索引。 ## 解题思路 遍历字符串,使用哈希表存储字符串中每个字符的出现次数。然后第二次遍历时,找出只出现一次的字符。 ## 代码 ```python class Solution: def firstUniqChar(self, s: str) -> int: strDict = dict() for i in range(len(s)): if s[i] in strDict: strDict[s[i]] += 1 else: strDict[s[i]] = 1 for i in range(len(s)): if s[i] in strDict and strDict[s[i]] == 1: return i return -1 ``` - 思路 2 代码: ================================================ FILE: docs/solutions/0300-0399/flatten-nested-list-iterator.md ================================================ # [0341. 扁平化嵌套列表迭代器](https://leetcode.cn/problems/flatten-nested-list-iterator/) - 标签:栈、树、深度优先搜索、设计、队列、迭代器 - 难度:中等 ## 题目链接 - [0341. 扁平化嵌套列表迭代器 - 力扣](https://leetcode.cn/problems/flatten-nested-list-iterator/) ## 题目大意 给定一个嵌套的整数列表 `nestedList` 。列表中元素类型为 NestedInteger 类。每个元素(NestedInteger 对象)要么是一个整数,要么是一个列表;该列表的元素也可能是整数或者是其他列表。 NestedInteger 类提供了三个方法: - `isInteger()`,判断当前存储的对象是否为 int; - `getInteger()` ,如果当前存储的元素是 int 型的,那么返回当前的结果 int,否则调用会失败; - `getList()`,如果当前存储的元素是 `List` 型的,那么返回该 List,否则调用会失败。 要求:实现一个迭代器将其扁平化,使之能够遍历这个列表中的所有整数。 实现扁平迭代器类 NestedIterator: - `NestedIterator(List nestedList)` 用嵌套列表 `nestedList` 初始化迭代器。 - `int next()` 返回嵌套列表的下一个整数。 - `boolean hasNext()` 如果仍然存在待迭代的整数,返回 `True`;否则,返回 `False`。 ## 解题思路 初始化时不对元素进行预处理。而是将所有的 `NestedInteger` 逆序放到栈中,当需要展开的时候才进行展开。 ## 代码 ```python class NestedIterator: def __init__(self, nestedList: [NestedInteger]): self.stack = [] size = len(nestedList) for i in range(size - 1, -1, -1): self.stack.append(nestedList[i]) def next(self) -> int: cur = self.stack.pop() return cur.getInteger() def hasNext(self) -> bool: while self.stack: cur = self.stack[-1] if cur.isInteger(): return True self.stack.pop() for i in range(len(cur.getList()) - 1, -1, -1): self.stack.append(cur.getList()[i]) return False ``` ================================================ FILE: docs/solutions/0300-0399/generalized-abbreviation.md ================================================ # [0320. 列举单词的全部缩写](https://leetcode.cn/problems/generalized-abbreviation/) - 标签:位运算、字符串、回溯 - 难度:中等 ## 题目链接 - [0320. 列举单词的全部缩写 - 力扣](https://leetcode.cn/problems/generalized-abbreviation/) ## 题目大意 **描述**: 单词的「广义缩写词」可以通过下述步骤构造:先取任意数量的「不重叠、不相邻」的子字符串,再用它们各自的长度进行替换。 - 例如,`"abcde"` 可以缩写为: - `"a3e"`(`"bcd"` 变为 `"3"`)。 - `"1bcd1"`(`"a"` 和 `"e"` 都变为 `"1"`)。 - `"5"` (`"abcde"` 变为 `"5"`)。 - `"abcde"` (没有子字符串被代替)。 - 然而,这些缩写是 无效的 : - `"23"`(`"ab"` 变为 `"2"`,`"cde"` 变为 `"3"`)是无效的,因为被选择的字符串是相邻的。 - `"22de"` (`"ab"` 变为 `"2"`,`"bc"` 变为 `"2"`) 是无效的,因为被选择的字符串是重叠的。 给定一个字符串 $word$。 **要求**: 返回 一个由 $word$ 的所有可能「广义缩写词」组成的列表。按任意顺序返回答案。 **说明**: - $1 \le word.length \le 15$。 - $word$ 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:word = "word" 输出:["4","3d","2r1","2rd","1o2","1o1d","1or1","1ord","w3","w2d","w1r1","w1rd","wo2","wo1d","wor1","word"] ``` - 示例 2: ```python 输入:word = "a" 输出:["1","a"] ``` ## 解题思路 ### 思路 1:回溯算法 这道题的核心思想是:**使用回溯算法枚举每个字符位置的选择,要么保留原字符,要么作为数字的一部分**。 解题步骤: 1. **定义状态**:设当前处理到位置 $i$,当前缩写字符串为 $current$,上一个位置是否为数字为 $is\_prev\_digit$。 2. **递归决策**:对于位置 $i$ 的字符,有两种选择: - **保留字符**:将字符直接添加到当前缩写中。 - **作为数字**:如果上一个位置不是数字,则开始一个新的数字计数;如果上一个位置是数字,则继续当前数字计数。 3. **回溯处理**:每次选择后递归处理下一个位置,递归返回后撤销选择。 4. **终止条件**:当处理完所有字符时,将当前缩写加入结果列表。 **关键点**: - 数字不能相邻,即不能出现 `"22de"` 这样的情况。 - 数字不能重叠,即不能出现 `"23"` 这样的情况。 - 需要跟踪上一个位置是否为数字,以决定是否可以开始新的数字计数。 **算法正确性**: 设字符串长度为 $n$,每个位置有 2 种选择(保留字符或作为数字),总共有 $2^n$ 种可能的组合。回溯算法会枚举所有有效的组合,确保不遗漏任何可能的缩写。 ### 思路 1:代码 ```python from typing import List class Solution: def generateAbbreviations(self, word: str) -> List[str]: def backtrack(index, current, is_prev_digit): """ 回溯生成所有可能的缩写 Args: index: 当前处理的字符位置 current: 当前构建的缩写字符串 is_prev_digit: 上一个位置是否为数字 """ # 终止条件:处理完所有字符 if index == len(word): result.append(current) return # 选择1:保留当前字符 backtrack(index + 1, current + word[index], False) # 选择2:将当前字符作为数字的一部分 if not is_prev_digit: # 上一个位置不是数字,可以开始新的数字 # 尝试从当前位置开始的所有可能的数字长度 for length in range(1, len(word) - index + 1): backtrack(index + length, current + str(length), True) result = [] backtrack(0, "", False) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n \times n)$,其中 $n$ 是字符串长度。每个位置有 2 种选择,总共有 $2^n$ 种可能的组合,每种组合需要 $O(n)$ 时间构建字符串。 - **空间复杂度**:$O(2^n \times n)$,需要存储所有可能的缩写结果,每个结果长度为 $O(n)$,总共有 $2^n$ 个结果。 ================================================ FILE: docs/solutions/0300-0399/guess-number-higher-or-lower-ii.md ================================================ # [0375. 猜数字大小 II](https://leetcode.cn/problems/guess-number-higher-or-lower-ii/) - 标签:数学、动态规划、博弈 - 难度:中等 ## 题目链接 - [0375. 猜数字大小 II - 力扣](https://leetcode.cn/problems/guess-number-higher-or-lower-ii/) ## 题目大意 **描述**:现在两个人来玩一个猜数游戏,游戏规则如下: 1. 对方从 $1 \sim n$ 中选择一个数字。 2. 我们来猜对方选了哪个数字。 3. 如果我们猜到了正确数字,就会赢得游戏。 4. 如果我们猜错了,那么对方就会告诉我们,所选的数字比我们猜的数字更大或者更小,并且需要我们继续猜数。 5. 每当我们猜了数字 $x$ 并且猜错了的时候,我们需要支付金额为 $x$ 的现金。如果我们花光了钱,就会输掉游戏。 现在给定一个特定数字 $n$。 **要求**:返回能够确保我们获胜的最小现金数(不管对方选择哪个数字)。 **说明**: - $1 \le n \le 200$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/09/10/graph.png) ```python 输入:n = 10 输出:16 解释:制胜策略如下: - 数字范围是 [1,10]。你先猜测数字为 7 。 - 如果这是我选中的数字,你的总费用为 $0。否则,你需要支付 $7。 - 如果我的数字更大,则下一步需要猜测的数字范围是 [8, 10] 。你可以猜测数字为 9。 - 如果这是我选中的数字,你的总费用为 $7。否则,你需要支付 $9。 - 如果我的数字更大,那么这个数字一定是 10。你猜测数字为 10 并赢得游戏,总费用为 $7 + $9 = $16。 - 如果我的数字更小,那么这个数字一定是 8。你猜测数字为 8 并赢得游戏,总费用为 $7 + $9 = $16。 - 如果我的数字更小,则下一步需要猜测的数字范围是 [1, 6]。你可以猜测数字为 3。 - 如果这是我选中的数字,你的总费用为 $7。否则,你需要支付 $3。 - 如果我的数字更大,则下一步需要猜测的数字范围是 [4, 6]。你可以猜测数字为 5。 - 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $5。 - 如果我的数字更大,那么这个数字一定是 6。你猜测数字为 6 并赢得游戏,总费用为 $7 + $3 + $5 = $15。 - 如果我的数字更小,那么这个数字一定是 4。你猜测数字为 4 并赢得游戏,总费用为 $7 + $3 + $5 = $15。 - 如果我的数字更小,则下一步需要猜测的数字范围是 [1, 2]。你可以猜测数字为 1。 - 如果这是我选中的数字,你的总费用为 $7 + $3 = $10。否则,你需要支付 $1。 - 如果我的数字更大,那么这个数字一定是 2。你猜测数字为 2 并赢得游戏,总费用为 $7 + $3 + $1 = $11。 在最糟糕的情况下,你需要支付 $16。因此,你只需要 $16 就可以确保自己赢得游戏。 ``` - 示例 2: ```python 输入:n = 2 输出:1 解释:有两个可能的数字 1 和 2 。 - 你可以先猜 1 。 - 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $1 。 - 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $1 。 最糟糕的情况下,你需要支付 $1。 ``` ## 解题思路 ### 思路 1:动态规划 直觉上这道题应该通过二分查找来求解,但实际上并不能通过二分查找来求解。 因为我们可以通过二分查找方法,能够找到猜中的最小次数,但这个猜中的最小次数所对应的支付金额,并不是最小现金数。 也就是说,通过二分查找的策略,并不能找到确保我们获胜的最小现金数。所以我们需要转换思路。 我们可以用递归的方式来思考。 对于 $1 \sim n$ 中每一个数 $x$: 1. 如果 $x$ 恰好是正确数字,则获胜,付出的现金数为 $0$。 2. 如果 $x$ 不是正确数字,则付出现金数为 $x$,同时我们得知,正确数字比 $x$ 更大还是更小。 1. 如果正确数字比 $x$ 更小,我们只需要求出 $1 \sim x - 1$ 中能够获胜的最小现金数,再加上 $x$ 就是确保我们获胜的最小现金数。 2. 如果正确数字比 $x$ 更大,我们只需要求出 $x + 1 \sim n$ 中能够获胜的最小现金数,再加上 $x$ 就是确保我们获胜的最小现金数。 3. 因为正确数字可能比 $x$ 更小,也可能比 $x$ 更大。在考虑最坏情况下也能获胜,我们需要准备的最小现金应该为两种情况下的最小代价的最大值,再加上 $x$ 本身。 我们可以通过枚举 $x$,并求出所有情况下的最小值,即为确保我们获胜的最小现金数。 我们可以定义一个方法 $f(1)(n)$ 来表示 $1 \sim n$ 中能够获胜的最小现金数,则可以得到递推公式:$f(1)(n) = min_{x = 1}^{x = n} \lbrace max \lbrace f(1)(x - 1), f(x + 1)(n) \rbrace + x \rbrace)$。 将递推公式应用到 $i \sim j$ 中,可得:$f(i)(j) = min_{x = i}^{x = j} \lbrace max \lbrace f(i)(x - 1), f(x + 1)(j) \rbrace + x \rbrace)$ 接下来我们就可以通过动态规划的方式解决这道题了。 ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:数字 $i \sim j$ 中能够确保我们获胜的最小现金数。 ###### 3. 状态转移方程 $dp[i][j] = min_{x = i}^{x = j} \lbrace max \lbrace dp[i][x - 1], dp[x + 1][j] \rbrace + x \rbrace)$ ###### 4. 初始条件 - 默认数字 $i \sim j$ 中能够确保我们获胜的最小现金数为无穷大。 - 当区间长度为 $1$ 时,区间中只有 $1$ 个数,肯定为正确数字,则付出最小现金数为 $0$,即 $dp[i][i] = 0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:数字 $i \sim j$ 中能够确保我们获胜的最小现金数。所以最终结果为 $dp[1][n]$。 ### 思路 1:代码 ```python class Solution: def getMoneyAmount(self, n: int) -> int: dp = [[0 for _ in range(n + 2)] for _ in range(n + 2)] for l in range(2, n + 1): for i in range(1, n + 1): j = i + l - 1 if j > n: break dp[i][j] = float('inf') for k in range(i, j): dp[i][j] = min(dp[i][j], max(dp[i][k - 1] + k, dp[k + 1][j] + k)) return dp[1][n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 为给定整数。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/0300-0399/guess-number-higher-or-lower.md ================================================ # [0374. 猜数字大小](https://leetcode.cn/problems/guess-number-higher-or-lower/) - 标签:二分查找、交互 - 难度:简单 ## 题目链接 - [0374. 猜数字大小 - 力扣](https://leetcode.cn/problems/guess-number-higher-or-lower/) ## 题目大意 **描述**:猜数字游戏。给定一个整数 $n$ 和一个接口 `def guess(num: int) -> int:`,题目会从 $1 \sim n$ 中随机选取一个数 $x$。我们只能通过调用接口来判断自己猜测的数是否正确。 **要求**:要求返回题目选取的数字 $x$。 **说明**: - `def guess(num: int) -> int:` 返回值: - $-1$:我选出的数字比你猜的数字小,即 $pick < num$; - $1$:我选出的数字比你猜的数字大 $pick > num$; - $0$:我选出的数字和你猜的数字一样。恭喜!你猜对了!$pick == num$。 **示例**: - 示例 1: ```python 输入:n = 10, pick = 6 输出:6 ``` - 示例 2: ```python 输入:n = 1, pick = 1 输出:1 ``` ## 解题思路 ### 思路 1:二分查找 利用两个指针 $left$、$right$。$left$ 指向数字 $1$,$right$ 指向数字 $n$。每次从中间开始调用接口猜测是否正确。 - 如果猜测的数比选中的数大,则将 $right$ 向左移,令 `right = mid - 1`,继续从中间调用接口猜测; - 如果猜测的数比选中的数小,则将 $left$ 向右移,令 `left = mid + 1`,继续从中间调用的接口猜测; - 如果猜测正确,则直接返回该数。 ### 思路 1:二分查找代码 ```python class Solution: def guessNumber(self, n: int) -> int: left = 1 right = n while left <= right: mid = left + (right - left) // 2 ans = guess(mid) if ans == 1: left = mid + 1 elif ans == -1: right = mid - 1 else: return mid return 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。二分查找算法的时间复杂度为 $O(\log n)$。 - **空间复杂度**:$O(1)$。只用到了常数空间存放若干变量。 ================================================ FILE: docs/solutions/0300-0399/house-robber-iii.md ================================================ # [0337. 打家劫舍 III](https://leetcode.cn/problems/house-robber-iii/) - 标签:树、深度优先搜索、动态规划、二叉树 - 难度:中等 ## 题目链接 - [0337. 打家劫舍 III - 力扣](https://leetcode.cn/problems/house-robber-iii/) ## 题目大意 小偷发现了一个新的可行窃的地区,这个地区的形状是一棵二叉树。这个地区只有一个入口,称为「根」。除了「根」之外,每栋房子只有一个「父」房子与之相连。如果两个直接相连的房子在同一天被打劫,房屋将自动报警。 现在给定这个代表地区房间的二叉树,每个节点值代表该房间所拥有的金额。要求计算在不触动警报的情况下,小偷一晚上能盗取的最高金额。 ## 解题思路 树形动态规划问题。 对于当前节点 `cur`,不能选择子节点,也不能选择父节点。所以对于一棵子树来说,有两种情况: - 选择了根节点 - 没有选择根节点 ### 1. 选择根节点 如果选择了根节点,则不能再选择左右儿子节点,这种情况下的最大值为:当前节点 + 左子树不选择根节点 + 右子树不选择根节点。 ### 2. 不选择根节点 如果不选择根节点,则可以选择左右儿子节点,共四种可能: - 左子树选择根节点 + 右子树选择根节点 - 左子树选择根节点 + 右子树不选根节点 - 左子树不选根节点 + 右子树选择根节点 - 左子树不选根节点 + 右子树不选根节点 选择其中最大值。 上述描述中,当前节点的选择来自于子节点信息的选择,然后逐层向上,直到根节点。所以我们使用「后序遍历」的方式进行递归遍历。 ## 代码 ```python class Solution: def dfs(self, root: TreeNode): if not root: return [0, 0] left = self.dfs(root.left) right = self.dfs(root.right) val_steal = root.val + left[1] + right[1] val_no_steal = max(left[0], left[1]) + max(right[0], right[1]) return [val_steal, val_no_steal] def rob(self, root: TreeNode) -> int: res = self.dfs(root) return max(res[0], res[1]) ``` ================================================ FILE: docs/solutions/0300-0399/increasing-triplet-subsequence.md ================================================ # [0334. 递增的三元子序列](https://leetcode.cn/problems/increasing-triplet-subsequence/) - 标签:贪心、数组 - 难度:中等 ## 题目链接 - [0334. 递增的三元子序列 - 力扣](https://leetcode.cn/problems/increasing-triplet-subsequence/) ## 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:判断数组中是否存在长度为 3 的递增子序列。 **说明**: - 要求算法时间复杂度为 $O(n)$、空间复杂度为 $O(1)$。 - **长度为 $3$ 的递增子序列**:存在这样的三元组下标 ($i$, $j$, $k$) 且满足 $i < j < k$ ,使得 $nums[i] < nums[j] < nums[k]$。 - $1 \le nums.length \le 5 \times 10^5$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,4,5] 输出:true 解释:任何 i < j < k 的三元组都满足题意 ``` - 示例 2: ```python 输入:nums = [5,4,3,2,1] 输出:false 解释:不存在满足题意的三元组 ``` ## 解题思路 ### 思路 1:快慢指针 常规方法是三重 `for` 循环遍历三个数,但是时间复杂度为 $O(n^3)$,肯定会超时的。 那么如何才能只进行一次遍历,就找到长度为 3 的递增子序列呢? 假设长度为 3 的递增子序列元素为 $a$、$b$、$c$,$a < b < c$。 先来考虑 $a$ 和 $b$。如果我们要使得一个数组 $i < j$,并且 $nums[i] < nums[j]$。那么应该使得 $a$ 尽可能的小,这样子我们下一个数字 $b$ 才可以尽可能地满足条件。 同样对于 $b$ 和 $c$,也应该使得 $b$ 尽可能的小,下一个数字 $c$ 才可以尽可能的满足条件。 所以,我们的目的是:在 $a < b$ 的前提下,保证 a 尽可能小。在 $b < c$ 的条件下,保证 $b$ 尽可能小。 我们可以使用两个数 $a$、$b$ 指向无穷大。遍历数组: - 如果当前数字小于等于 $a$ ,则更新 `a = num`; - 如果当前数字大于等于 $a$,则说明当前数满足 $num > a$,则判断: - 如果 $num \le b$,则更新 `b = num`; - 如果 $num > b$,则说明找到了长度为 3 的递增子序列,直接输出 $True$。 - 如果遍历完仍未找到,则输出 $False$。 ### 思路 1:代码 ```python class Solution: def increasingTriplet(self, nums: List[int]) -> bool: a = float('inf') b = float('inf') for num in nums: if num <= a: a = num elif num <= b: b = num else: return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0300-0399/index.md ================================================ ## 本章内容 - [0300. 最长递增子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-subsequence.md) - [0301. 删除无效的括号](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/remove-invalid-parentheses.md) - [0302. 包含全部黑色像素的最小矩形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/smallest-rectangle-enclosing-black-pixels.md) - [0303. 区域和检索 - 数组不可变](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-immutable.md) - [0304. 二维区域和检索 - 矩阵不可变](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-2d-immutable.md) - [0305. 岛屿数量 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/number-of-islands-ii.md) - [0306. 累加数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/additive-number.md) - [0307. 区域和检索 - 数组可修改](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-mutable.md) - [0308. 二维区域和检索 - 矩阵可修改](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-2d-mutable.md) - [0309. 买卖股票的最佳时机含冷冻期](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/best-time-to-buy-and-sell-stock-with-cooldown.md) - [0310. 最小高度树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/minimum-height-trees.md) - [0311. 稀疏矩阵的乘法](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sparse-matrix-multiplication.md) - [0312. 戳气球](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/burst-balloons.md) - [0313. 超级丑数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/super-ugly-number.md) - [0314. 二叉树的垂直遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/binary-tree-vertical-order-traversal.md) - [0315. 计算右侧小于当前元素的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-of-smaller-numbers-after-self.md) - [0316. 去除重复字母](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/remove-duplicate-letters.md) - [0317. 离建筑物最近的距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/shortest-distance-from-all-buildings.md) - [0318. 最大单词长度乘积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/maximum-product-of-word-lengths.md) - [0319. 灯泡开关](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/bulb-switcher.md) - [0320. 列举单词的全部缩写](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/generalized-abbreviation.md) - [0321. 拼接最大数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/create-maximum-number.md) - [0322. 零钱兑换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) - [0323. 无向图中连通分量的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/number-of-connected-components-in-an-undirected-graph.md) - [0324. 摆动排序 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/wiggle-sort-ii.md) - [0325. 和等于 k 的最长子数组长度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/maximum-size-subarray-sum-equals-k.md) - [0326. 3 的幂](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/power-of-three.md) - [0327. 区间和的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-of-range-sum.md) - [0328. 奇偶链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/odd-even-linked-list.md) - [0329. 矩阵中的最长递增路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-path-in-a-matrix.md) - [0330. 按要求补齐数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/patching-array.md) - [0331. 验证二叉树的前序序列化](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/verify-preorder-serialization-of-a-binary-tree.md) - [0332. 重新安排行程](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reconstruct-itinerary.md) - [0333. 最大二叉搜索子树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/largest-bst-subtree.md) - [0334. 递增的三元子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/increasing-triplet-subsequence.md) - [0335. 路径交叉](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/self-crossing.md) - [0336. 回文对](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/palindrome-pairs.md) - [0337. 打家劫舍 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/house-robber-iii.md) - [0338. 比特位计数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/counting-bits.md) - [0339. 嵌套列表加权和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/nested-list-weight-sum.md) - [0340. 至多包含 K 个不同字符的最长子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-substring-with-at-most-k-distinct-characters.md) - [0341. 扁平化嵌套列表迭代器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/flatten-nested-list-iterator.md) - [0342. 4的幂](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/power-of-four.md) - [0343. 整数拆分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/integer-break.md) - [0344. 反转字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-string.md) - [0345. 反转字符串中的元音字母](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-vowels-of-a-string.md) - [0346. 数据流中的移动平均值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/moving-average-from-data-stream.md) - [0347. 前 K 个高频元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/top-k-frequent-elements.md) - [0348. 设计井字棋](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-tic-tac-toe.md) - [0349. 两个数组的交集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays.md) - [0350. 两个数组的交集 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays-ii.md) - [0351. 安卓系统手势解锁](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/android-unlock-patterns.md) - [0352. 将数据流变为多个不相交区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/data-stream-as-disjoint-intervals.md) - [0353. 贪吃蛇](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-snake-game.md) - [0354. 俄罗斯套娃信封问题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/russian-doll-envelopes.md) - [0355. 设计推特](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-twitter.md) - [0356. 直线镜像](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/line-reflection.md) - [0357. 统计各位数字都不同的数字个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-numbers-with-unique-digits.md) - [0358. K 距离间隔重排字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/rearrange-string-k-distance-apart.md) - [0359. 日志速率限制器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/logger-rate-limiter.md) - [0360. 有序转化数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sort-transformed-array.md) - [0361. 轰炸敌人](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/bomb-enemy.md) - [0362. 敲击计数器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-hit-counter.md) - [0363. 矩形区域不超过 K 的最大数值和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/max-sum-of-rectangle-no-larger-than-k.md) - [0364. 嵌套列表加权和 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/nested-list-weight-sum-ii.md) - [0365. 水壶问题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/water-and-jug-problem.md) - [0366. 寻找二叉树的叶子节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/find-leaves-of-binary-tree.md) - [0367. 有效的完全平方数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/valid-perfect-square.md) - [0368. 最大整除子集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/largest-divisible-subset.md) - [0369. 给单链表加一](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/plus-one-linked-list.md) - [0370. 区间加法](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-addition.md) - [0371. 两整数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sum-of-two-integers.md) - [0372. 超级次方](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/super-pow.md) - [0373. 查找和最小的 K 对数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/find-k-pairs-with-smallest-sums.md) - [0374. 猜数字大小](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/guess-number-higher-or-lower.md) - [0375. 猜数字大小 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/guess-number-higher-or-lower-ii.md) - [0376. 摆动序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/wiggle-subsequence.md) - [0377. 组合总和 Ⅳ](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/combination-sum-iv.md) - [0378. 有序矩阵中第 K 小的元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/kth-smallest-element-in-a-sorted-matrix.md) - [0379. 电话目录管理系统](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-phone-directory.md) - [0380. O(1) 时间插入、删除和获取随机元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/insert-delete-getrandom-o1.md) - [0381. O(1) 时间插入、删除和获取随机元素 - 允许重复](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/insert-delete-getrandom-o1-duplicates-allowed.md) - [0382. 链表随机节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/linked-list-random-node.md) - [0383. 赎金信](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/ransom-note.md) - [0384. 打乱数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/shuffle-an-array.md) - [0385. 迷你语法分析器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/mini-parser.md) - [0386. 字典序排数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/lexicographical-numbers.md) - [0387. 字符串中的第一个唯一字符](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/first-unique-character-in-a-string.md) - [0388. 文件的最长绝对路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-absolute-file-path.md) - [0389. 找不同](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/find-the-difference.md) - [0390. 消除游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/elimination-game.md) - [0391. 完美矩形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/perfect-rectangle.md) - [0392. 判断子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/is-subsequence.md) - [0393. UTF-8 编码验证](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/utf-8-validation.md) - [0394. 字符串解码](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/decode-string.md) - [0395. 至少有 K 个重复字符的最长子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-substring-with-at-least-k-repeating-characters.md) - [0396. 旋转函数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/rotate-function.md) - [0397. 整数替换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/integer-replacement.md) - [0398. 随机数索引](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/random-pick-index.md) - [0399. 除法求值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/evaluate-division.md) ================================================ FILE: docs/solutions/0300-0399/insert-delete-getrandom-o1-duplicates-allowed.md ================================================ # [0381. O(1) 时间插入、删除和获取随机元素 - 允许重复](https://leetcode.cn/problems/insert-delete-getrandom-o1-duplicates-allowed/) - 标签:设计、数组、哈希表、数学、随机化 - 难度:困难 ## 题目链接 - [0381. O(1) 时间插入、删除和获取随机元素 - 允许重复 - 力扣](https://leetcode.cn/problems/insert-delete-getrandom-o1-duplicates-allowed/) ## 题目大意 **描述**: `RandomizedCollection` 是一种包含数字集合(可能是重复的)的数据结构。它应该支持插入和删除特定元素,以及删除随机元素。 **要求**: 实现 `RandomizedCollection` 类: - `RandomizedCollection()` 初始化空的 `RandomizedCollection` 对象。 - `bool insert(int val)` 将一个 $val$ 项插入到集合中,即使该项已经存在。如果该项不存在,则返回 $true$,否则返回 $false$。 - `bool remove(int val)` 如果存在,从集合中移除一个 $val$ 项。如果该项存在,则返回 $true$,否则返回 $false$。注意,如果 $val$ 在集合中出现多次,我们只删除其中一个。 - `int getRandom()` 从当前的多个元素集合中返回一个随机元素。每个元素被返回的概率与集合中包含的相同值的数量线性相关。 **说明**: - 您必须实现类的函数,使每个函数的平均时间复杂度为 $O(1)$。 - 注意:生成测试用例时,只有在 `RandomizedCollection` 中至少有一项时,才会调用 `getRandom`。 - $-2^{31} \le val \le 2^{31} - 1$。 - `insert`, `remove` 和 `getRandom` 最多总共被调用 $2 \times 10^{5}$ 次。 - 当调用 `getRandom` 时,数据结构中至少有一个元素。 **示例**: - 示例 1: ```python 输入 ["RandomizedCollection", "insert", "insert", "insert", "getRandom", "remove", "getRandom"] [[], [1], [1], [2], [], [1], []] 输出 [null, true, false, true, 2, true, 1] 解释 RandomizedCollection collection = new RandomizedCollection();// 初始化一个空的集合。 collection.insert(1); // 返回 true,因为集合不包含 1。 // 将 1 插入到集合中。 collection.insert(1); // 返回 false,因为集合包含 1。 // 将另一个 1 插入到集合中。集合现在包含 [1,1]。 collection.insert(2); // 返回 true,因为集合不包含 2。 // 将 2 插入到集合中。集合现在包含 [1,1,2]。 collection.getRandom(); // getRandom 应当: // 有 2/3 的概率返回 1, // 1/3 的概率返回 2。 collection.remove(1); // 返回 true,因为集合包含 1。 // 从集合中移除 1。集合现在包含 [1,2]。 collection.getRandom(); // getRandom 应该返回 1 或 2,两者的可能性相同。 ``` ## 解题思路 ### 思路 1:数组 + 哈希表 这道题的核心思想是:**使用数组存储所有元素,用哈希表记录每个值在数组中的索引位置,通过交换数组末尾元素来实现 O(1) 删除**。 解题步骤: 1. **数据结构设计**: - 使用数组 $nums$ 存储所有元素,支持 $O(1)$ 随机访问。 - 使用哈希表 $indices$ 记录每个值在数组中的所有索引位置,其中 $indices[val]$ 是一个集合,存储值 $val$ 在数组中的所有索引。 2. **插入操作**: - 将新元素 $val$ 添加到数组末尾,时间复杂度 $O(1)$。 - 在哈希表中记录该元素的索引位置,时间复杂度 $O(1)$。 - 返回该值是否首次出现(即哈希表中该值的集合是否为空)。 3. **删除操作**: - 从哈希表中获取要删除值 $val$ 的任意一个索引位置 $index$。 - 将数组末尾元素 $last\_val$ 移动到位置 $index$,实现 $O(1)$ 删除。 - 更新哈希表中 $last\_val$ 的索引信息。 - 从哈希表中移除 $val$ 的索引记录。 4. **随机获取操作**: - 从数组中随机选择一个索引,返回对应元素,时间复杂度 $O(1)$。 **关键点**: - 删除时通过交换数组末尾元素避免移动其他元素,保证 $O(1)$ 时间复杂度。 - 哈希表使用集合存储索引,支持快速查找和删除任意索引。 - 需要同时维护数组和哈希表的一致性。 **算法正确性**: 设数组长度为 $n$,哈希表 $indices$ 中每个值的索引集合大小之和等于 $n$。删除操作通过交换末尾元素保持数组连续性,哈希表正确维护每个值的索引信息,确保所有操作的正确性。 ### 思路 1:代码 ```python import random from collections import defaultdict class RandomizedCollection: def __init__(self): """ 初始化 RandomizedCollection 对象 - nums: 存储所有元素的数组 - indices: 哈希表,记录每个值在数组中的所有索引位置 """ self.nums = [] # 存储所有元素的数组 self.indices = defaultdict(set) # 值 -> 索引集合的映射 def insert(self, val: int) -> bool: """ 插入元素 val 到集合中 Args: val: 要插入的值 Returns: bool: 如果该值不存在则返回 True,否则返回 False """ # 将元素添加到数组末尾 self.nums.append(val) # 记录该元素在数组中的索引位置 self.indices[val].add(len(self.nums) - 1) # 返回该值是否首次出现 return len(self.indices[val]) == 1 def remove(self, val: int) -> bool: """ 从集合中移除一个 val 元素 Args: val: 要删除的值 Returns: bool: 如果该值存在则返回 True,否则返回 False """ # 如果该值不存在,返回 False if not self.indices[val]: return False # 获取要删除元素的索引位置 index = self.indices[val].pop() # 获取数组末尾元素 last_val = self.nums[-1] # 如果删除的不是最后一个元素,需要交换 if index != len(self.nums) - 1: # 将末尾元素移动到要删除的位置 self.nums[index] = last_val # 更新末尾元素的索引信息 self.indices[last_val].discard(len(self.nums) - 1) self.indices[last_val].add(index) # 删除数组末尾元素 self.nums.pop() # 如果删除后该值的索引集合为空,从哈希表中移除 if not self.indices[val]: del self.indices[val] return True def getRandom(self) -> int: """ 从当前集合中随机返回一个元素 Returns: int: 随机选择的元素 """ # 从数组中随机选择一个索引,返回对应元素 # 注意:根据题目要求,调用此方法时集合中至少有一个元素 return random.choice(self.nums) # Your RandomizedCollection object will be instantiated and called as such: # obj = RandomizedCollection() # param_1 = obj.insert(val) # param_2 = obj.remove(val) # param_3 = obj.getRandom() ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `insert(val)`:$O(1)$,数组末尾插入和哈希表操作都是常数时间。 - `remove(val)`:$O(1)$,通过交换末尾元素实现常数时间删除。 - `getRandom()`:$O(1)$,随机选择数组中的元素。 - **空间复杂度**:$O(n)$,其中 $n$ 是集合中元素的总数。需要 $O(n)$ 空间存储数组和哈希表。 ================================================ FILE: docs/solutions/0300-0399/insert-delete-getrandom-o1.md ================================================ # [0380. 常数时间插入、删除和获取随机元素](https://leetcode.cn/problems/insert-delete-getrandom-o1/) - 标签:设计、数组、哈希表、数学、随机化 - 难度:中等 ## 题目链接 - [0380. 常数时间插入、删除和获取随机元素 - 力扣](https://leetcode.cn/problems/insert-delete-getrandom-o1/) ## 题目大意 设计一个数据结构 ,支持时间复杂度为 O(1) 的以下操作: - insert(val):当元素 val 不存在时,向集合中插入该项。 - remove(val):元素 val 存在时,从集合中移除该项。 - getRandom:随机返回现有集合中的一项。每个元素应该有相同的概率被返回。 ## 解题思路 普通动态数组进行访问操作,需要线性时间查找解决。我们可以利用哈希表记录下每个元素的下标,这样在访问时可以做到常数时间内访问元素了。对应的插入、删除、后去随机元素需要做相应的变化。 - 插入操作:将元素直接插入到数组尾部,并用哈希表记录插入元素的下标位置。 - 删除操作:使用哈希表找到待删除元素所在位置,将其与数组末尾位置元素相互交换,更新哈希表中交换后元素的下标值,并将末尾元素删除。 - 获取随机元素:使用` random.choice` 获取。 ## 代码 ```python import random class RandomizedSet: def __init__(self): """ Initialize your data structure here. """ self.dict = dict() self.list = list() def insert(self, val: int) -> bool: """ Inserts a value to the set. Returns true if the set did not already contain the specified element. """ if val in self.dict: return False self.dict[val] = len(self.list) self.list.append(val) return True def remove(self, val: int) -> bool: """ Removes a value from the set. Returns true if the set contained the specified element. """ if val in self.dict: idx = self.dict[val] last = self.list[-1] self.list[idx] = last self.dict[last] = idx self.list.pop() self.dict.pop(val) return True return False def getRandom(self) -> int: """ Get a random element from the set. """ return random.choice(self.list) ``` ================================================ FILE: docs/solutions/0300-0399/integer-break.md ================================================ # [0343. 整数拆分](https://leetcode.cn/problems/integer-break/) - 标签:数学、动态规划 - 难度:中等 ## 题目链接 - [0343. 整数拆分 - 力扣](https://leetcode.cn/problems/integer-break/) ## 题目大意 **描述**:给定一个正整数 $n$,将其拆分为 $k (k \ge 2)$ 个正整数的和,并使这些整数的乘积最大化。 **要求**:返回可以获得的最大乘积。 **说明**: - $2 \le n \le 58$。 **示例**: - 示例 1: ```python 输入: n = 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。 ``` - 示例 2: ```python 输入: n = 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照正整数进行划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为:将正整数 $i$ 拆分为至少 $2$ 个正整数的和之后,这些正整数的最大乘积。 ###### 3. 状态转移方程 当 $i \ge 2$ 时,假设正整数 $i$ 拆分出的第 $1$ 个正整数是 $j(1 \le j < i)$,则有两种方法: 1. 将 $i$ 拆分为 $j$ 和 $i - j$ 的和,且 $i - j$ 不再拆分为多个正整数,此时乘积为:$j \times (i - j)$。 2. 将 $i$ 拆分为 $j$ 和 $i - j$ 的和,且 $i - j$ 继续拆分为多个正整数,此时乘积为:$j \times dp[i - j]$。 则 $dp[i]$ 取两者中的最大值。即:$dp[i] = max(j \times (i - j), j \times dp[i - j])$。 由于 $1 \le j < i$,需要遍历 $j$ 得到 $dp[i]$ 的最大值,则状态转移方程如下: $dp[i] = max_{1 \le j < i}\lbrace max(j \times (i - j), j \times dp[i - j]) \rbrace$。 ###### 4. 初始条件 - $0$ 和 $1$ 都不能被拆分,所以 $dp[0] = 0, dp[1] = 0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示为:将正整数 $i$ 拆分为至少 $2$ 个正整数的和之后,这些正整数的最大乘积。则最终结果为 $dp[n]$。 ### 思路 1:代码 ```python class Solution: def integerBreak(self, n: int) -> int: dp = [0 for _ in range(n + 1)] for i in range(2, n + 1): for j in range(i): dp[i] = max(dp[i], (i - j) * j, dp[i - j] * j) return dp[n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0300-0399/integer-replacement.md ================================================ # [0397. 整数替换](https://leetcode.cn/problems/integer-replacement/) - 标签:贪心、位运算、记忆化搜索、动态规划 - 难度:中等 ## 题目链接 - [0397. 整数替换 - 力扣](https://leetcode.cn/problems/integer-replacement/) ## 题目大意 **描述**: 给定一个正整数 $n$ ,你可以做如下操作: 1. 如果 $n$ 是偶数,则用 $n / 2$ 替换 $n$。 2. 如果 $n$ 是奇数,则可以用 $n + 1$ 或 $n - 1$ 替换 $n$。 **要求**: 返回 $n$ 变为 $1$ 所需的「最小替换次数」。 **说明**: - $1 \le n \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:n = 8 输出:3 解释:8 -> 4 -> 2 -> 1 ``` - 示例 2: ```python 输入:n = 7 输出:4 解释:7 -> 8 -> 4 -> 2 -> 1 或 7 -> 6 -> 3 -> 2 -> 1 ``` ## 解题思路 ### 思路 1:贪心算法 这道题的核心思想是:**对于奇数 $n$,选择能更快减少二进制表示中 $1$ 的个数的操作**。 解题步骤: 1. **偶数处理**:如果 $n$ 是偶数,直接除以 $2$,即 $n = n / 2$。 2. **奇数处理**:如果 $n$ 是奇数,需要选择 $n + 1$ 或 $n - 1$: - 如果 $n = 1$,直接返回 $0$(已经到达目标)。 - 如果 $n = 3$,选择 $n - 1$(特殊情况)。 - 如果 $n$ 的二进制表示末尾是 $11$(即 $n \bmod 4 = 3$),选择 $n + 1$。 - 否则选择 $n - 1$。 **关键点**: - 对于偶数,除以 $2$ 是最优选择,因为能直接减少一位。 - 对于奇数,我们希望尽可能快地减少二进制表示中 $1$ 的个数。 - 当 $n \bmod 4 = 3$ 时,$n + 1$ 会产生更多的连续 $0$,有利于后续的除法操作。 - 特殊情况:$n = 3$ 时,$3 \to 2 \to 1$ 比 $3 \to 4 \to 2 \to 1$ 更优。 **算法正确性**: 设 $f(n)$ 表示将 $n$ 变为 $1$ 的最小操作次数: - 当 $n$ 为偶数时:$f(n) = 1 + f(n/2)$。 - 当 $n$ 为奇数时: - 如果 $n = 1$:$f(1) = 0$。 - 如果 $n = 3$:$f(3) = 1 + f(2) = 2$。 - 如果 $n \bmod 4 = 3$:选择 $n + 1$ 能更快地减少 $1$ 的个数。 - 否则:选择 $n - 1$ 是更优的。 ### 思路 1:代码 ```python class Solution: def integerReplacement(self, n: int) -> int: count = 0 while n != 1: if n % 2 == 0: # 偶数:直接除以2 n //= 2 else: # 奇数:根据情况选择+1或-1 if n == 3: # 特殊情况:3 -> 2 -> 1 比 3 -> 4 -> 2 -> 1 更优 n -= 1 elif n % 4 == 3: # 如果n mod 4 = 3,选择+1能产生更多连续的0 n += 1 else: # 否则选择-1 n -= 1 count += 1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$,每次操作至少将 $n$ 减半,最多需要 $O(\log n)$ 次操作。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0300-0399/intersection-of-two-arrays-ii.md ================================================ # [0350. 两个数组的交集 II](https://leetcode.cn/problems/intersection-of-two-arrays-ii/) - 标签:数组、哈希表 - 难度:简单 ## 题目链接 - [0350. 两个数组的交集 II - 力扣](https://leetcode.cn/problems/intersection-of-two-arrays-ii/) ## 题目大意 **描述**:给定两个数组 $nums1$ 和 $nums2$。 **要求**:返回两个数组的交集。可以不考虑输出结果的顺序。 **说明**: - 输出结果中,每个元素出现的次数,应该与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。 - $1 \le nums1.length, nums2.length \le 1000$。 - $0 \le nums1[i], nums2[i] \le 1000$。 **示例**: ```python 输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2,2] 输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[4,9] ``` ## 解题思路 ### 思路 1:哈希表 1. 先遍历第一个数组,利用字典来存放第一个数组的元素出现次数。 2. 然后遍历第二个数组,如果字典中存在该元素,则将该元素加入到答案数组中,并减少字典中该元素出现的次数。 3. 遍历完之后,返回答案数组。 ### 思路 1:代码 ```python class Solution: def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]: numDict = dict() nums = [] for num in nums1: if num in numDict: numDict[num] += 1 else: numDict[num] = 1 for num in nums2: if num in numDict and numDict[num] != 0: numDict[num] -= 1 nums.append(num) return nums ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0300-0399/intersection-of-two-arrays.md ================================================ # [0349. 两个数组的交集](https://leetcode.cn/problems/intersection-of-two-arrays/) - 标签:数组、哈希表、双指针、二分查找、排序 - 难度:简单 ## 题目链接 - [0349. 两个数组的交集 - 力扣](https://leetcode.cn/problems/intersection-of-two-arrays/) ## 题目大意 **描述**:给定两个数组 $nums1$ 和 $nums2$。 **要求**:返回两个数组的交集。重复元素只计算一次。 **说明**: - $1 \le nums1.length, nums2.length \le 1000$。 - $0 \le nums1[i], nums2[i] \le 1000$。 **示例**: - 示例 1: ```python 输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2] 示例 2: ``` - 示例 2: ```python 输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[9,4] 解释:[4,9] 也是可通过的 ``` ## 解题思路 ### 思路 1:哈希表 1. 先遍历第一个数组,利用哈希表来存放第一个数组的元素,对应字典值设为 $1$。 2. 然后遍历第二个数组,如果哈希表中存在该元素,则将该元素加入到答案数组中,并且将该键值清空。 ### 思路 1:代码 ```python class Solution: def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]: numDict = dict() nums = [] for num in nums1: if num not in numDict: numDict[num] = 1 for num in nums2: if num in numDict and numDict[num] != 0: numDict[num] -= 1 nums.append(num) return nums ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ### 思路 2:分离双指针 1. 对数组 $nums1$、$nums2$ 先排序。 2. 使用两个指针 $left\_1$、$left\_2$。$left\_1$ 指向第一个数组的第一个元素,即:$left\_1 = 0$,$left\_2$ 指向第二个数组的第一个元素,即:$left\_2 = 0$。 3. 如果 $nums1[left_1]$ 等于 $nums2[left_2]$,则将其加入答案数组(注意去重),并将 $left\_1$ 和 $left\_2$ 右移。 4. 如果 $nums1[left_1]$ 小于 $nums2[left_2]$,则将 $left\_1$ 右移。 5. 如果 $nums1[left_1]$ 大于 $nums2[left_2]$,则将 $left\_2$ 右移。 6. 最后返回答案数组。 ### 思路 2:代码 ```python class Solution: def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]: nums1.sort() nums2.sort() left_1 = 0 left_2 = 0 res = [] while left_1 < len(nums1) and left_2 < len(nums2): if nums1[left_1] == nums2[left_2]: if nums1[left_1] not in res: res.append(nums1[left_1]) left_1 += 1 left_2 += 1 elif nums1[left_1] < nums2[left_2]: left_1 += 1 elif nums1[left_1] > nums2[left_2]: left_2 += 1 return res ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(m \log m + n \log n)$,其中 $m$ 和 $n$ 分别为两个数组的长度。排序用时 $O(m \log m + n \log n)$,双指针遍历用时 $O(m + n)$,因此总的时间复杂度为 $O(m \log m + n \log n)$。 - **空间复杂度**:$O(\min(m, n))$。 ================================================ FILE: docs/solutions/0300-0399/is-subsequence.md ================================================ # [0392. 判断子序列](https://leetcode.cn/problems/is-subsequence/) - 标签:双指针、字符串、动态规划 - 难度:简单 ## 题目链接 - [0392. 判断子序列 - 力扣](https://leetcode.cn/problems/is-subsequence/) ## 题目大意 **描述**:给定字符串 $s$ 和 $t$。 **要求**:判断 $s$ 是否为 $t$ 的子序列。 **说明**: - $0 \le s.length \le 100$。 - $0 \le t.length \le 10^4$。 - 两个字符串都只由小写字符组成。 **示例**: - 示例 1: ```python 输入:s = "abc", t = "ahbgdc" 输出:True ``` - 示例 2: ```python 输入:s = "axc", t = "ahbgdc" 输出:False ``` ## 解题思路 ### 思路 1:双指针 使用两个指针 $i$、$j$ 分别指向字符串 $s$ 和 $t$,然后对两个字符串进行遍历。 - 遇到 $s[i] == t[j]$ 的情况,则 $i$ 向右移。 - 不断右移 $j$。 - 如果超过 $s$ 或 $t$ 的长度则跳出。 - 最后判断指针 $i$ 是否指向了 $s$ 的末尾,即:判断 $i$ 是否等于 $s$ 的长度。如果等于,则说明 $s$ 是 $t$ 的子序列,如果不等于,则不是。 ### 思路 1:代码 ```python class Solution: def isSubsequence(self, s: str, t: str) -> bool: size_s = len(s) size_t = len(t) i, j = 0, 0 while i < size_s and j < size_t: if s[i] == t[j]: i += 1 j += 1 return i == size_s ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$、$m$ 分别为字符串 $s$、$t$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0300-0399/kth-smallest-element-in-a-sorted-matrix.md ================================================ # [0378. 有序矩阵中第 K 小的元素](https://leetcode.cn/problems/kth-smallest-element-in-a-sorted-matrix/) - 标签:数组、二分查找、矩阵、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [0378. 有序矩阵中第 K 小的元素 - 力扣](https://leetcode.cn/problems/kth-smallest-element-in-a-sorted-matrix/) ## 题目大意 给定一个 `n * n` 矩阵 `matrix`,其中每行和每列元素均按升序排序。 要求:找到矩阵中第 `k` 小的元素。 注意:它是排序后的第 `k` 小元素,而不是第 `k` 个 不同的元素。 ## 解题思路 已知二维矩阵 `matrix` 每行每列是按照升序排序的。那么二维矩阵的下界就是左上角元素 `matrix[0][0]`,上界就是右下角元素 `matrix[rows - 1][cols - 1]`。那么我们可以使用二分查找的方法在上界、下界之间搜索所有值,找到第 `k` 小的元素。 我们可以通过判断矩阵中比 `mid` 小的元素个数是否等于 `k` 来确定是否找到第 `k` 小的元素。 - 如果比 `mid` 小的元素个数大于等于 `k`,说明最终答案 `ans` 小于等于 `mid`。 - 如果比 `mid` 小的元素个数小于 `k`,说明最终答案 `ans` 大于 `k`。 ## 代码 ```python class Solution: def kthSmallest(self, matrix: List[List[int]], k: int) -> int: rows, cols = len(matrix), len(matrix[0]) left, right = matrix[0][0], matrix[rows - 1][cols - 1] + 1 while left < right: mid = left + (right - left) // 2 if self.counterKthSmallest(mid, matrix) >= k: right = mid else: left = mid + 1 return left def counterKthSmallest(self, mid, matrix): rows, cols = len(matrix), len(matrix[0]) count = 0 j = cols - 1 for i in range(rows): while j >= 0 and mid < matrix[i][j]: j -= 1 count += j + 1 return count ``` ================================================ FILE: docs/solutions/0300-0399/largest-bst-subtree.md ================================================ # [0333. 最大二叉搜索子树](https://leetcode.cn/problems/largest-bst-subtree/) - 标签:树、深度优先搜索、二叉搜索树、动态规划、二叉树 - 难度:中等 ## 题目链接 - [0333. 最大二叉搜索子树 - 力扣](https://leetcode.cn/problems/largest-bst-subtree/) ## 题目大意 **描述**: 给定一个二叉树。 **要求**: 找到其中最大的二叉搜索树(BST)子树,并返回该子树的大小。其中,最大指的是子树节点数最多的。 **说明**: - 二叉搜索树(BST)中的所有节点都具备以下属性: - 左子树的值小于其父(根)节点的值。 - 右子树的值大于其父(根)节点的值。 - 注意:子树必须包含其所有后代。 - 树上节点数目的范围是 $[0, 10^{4}]$。 - $-10^{4} \le Node.val \le 10^{4}$。 - 进阶: 你能想出 $O(n)$ 时间复杂度的解法吗? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/17/tmp.jpg) ```python 输入:root = [10,5,15,1,8,null,7] 输出:3 解释:本例中最大的 BST 子树是高亮显示的子树。返回值是子树的大小,即 3 。 ``` - 示例 2: ```python 输入:root = [4,2,7,2,3,5,null,2,null,null,null,null,null,1] 输出:2 ``` ## 解题思路 ### 思路 1:深度优先搜索 + 自底向上验证 这道题的核心思想是:**自底向上递归验证每个子树是否为 BST,并记录最大 BST 子树的大小**。 解题步骤: 1. **定义递归函数**:设计一个递归函数 `dfs(node)`,返回一个四元组 $(is\_bst, min\_val, max\_val, size)$: - $is\_bst$:以 $node$ 为根的子树是否为 BST。 - $min\_val$:子树中的最小值。 - $max\_val$:子树中的最大值。 - $size$:子树的大小(节点数)。 2. **递归终止条件**:当 $node$ 为空时,返回 $(True, +\infty, -\infty, 0)$。 3. **递归处理**: - 递归处理左右子树,得到 $(left\_is\_bst, left\_min, left\_max, left\_size)$ 和 $(right\_is\_bst, right\_min, right\_max, right\_size)$。 - 当前子树为 BST 的条件: - 左右子树都是 BST。 - $node.val > left\_max$(当前节点值大于左子树最大值)。 - $node.val < right\_min$(当前节点值小于右子树最小值)。 - 如果当前子树是 BST,更新全局最大 BST 大小。 4. **更新边界值**: - $min\_val = \min(node.val, left\_min)$。 - $max\_val = \max(node.val, right\_max)$。 - $size = left\_size + right\_size + 1$。 **关键点**: - 使用 $+\infty$ 和 $-\infty$ 作为空节点的边界值,确保边界条件正确处理。 - 只有当左右子树都是 BST 且满足 BST 性质时,当前子树才是 BST。 - 在递归过程中实时更新全局最大 BST 大小。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def largestBSTSubtree(self, root: Optional[TreeNode]) -> int: if not root: return 0 self.max_size = 0 # 记录最大BST子树的大小 def dfs(node): """ 递归函数,返回 (is_bst, min_val, max_val, size) is_bst: 当前子树是否为BST min_val: 子树中的最小值 max_val: 子树中的最大值 size: 子树的大小 """ if not node: # 空节点:是BST,边界值为正负无穷,大小为0 return True, float('inf'), float('-inf'), 0 # 递归处理左右子树 left_is_bst, left_min, left_max, left_size = dfs(node.left) right_is_bst, right_min, right_max, right_size = dfs(node.right) # 判断当前子树是否为BST if (left_is_bst and right_is_bst and node.val > left_max and node.val < right_min): # 当前子树是BST,计算大小并更新全局最大值 current_size = left_size + right_size + 1 self.max_size = max(self.max_size, current_size) # 更新边界值 min_val = min(node.val, left_min) max_val = max(node.val, right_max) return True, min_val, max_val, current_size else: # 当前子树不是BST,返回False和任意边界值 return False, 0, 0, 0 dfs(root) return self.max_size ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树中节点的数量。每个节点被访问一次,每次访问的时间复杂度为 $O(1)$。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度。递归调用栈的深度等于树的高度,最坏情况下为 $O(n)$(当树退化为链表时)。 ================================================ FILE: docs/solutions/0300-0399/largest-divisible-subset.md ================================================ # [0368. 最大整除子集](https://leetcode.cn/problems/largest-divisible-subset/) - 标签:数组、数学、动态规划、排序 - 难度:中等 ## 题目链接 - [0368. 最大整除子集 - 力扣](https://leetcode.cn/problems/largest-divisible-subset/) ## 题目大意 **描述**: 给定一个由「无重复」正整数组成的集合 $nums$。 **要求**: 请你找出并返回其中最大的整除子集 $answer$,子集中每一元素对 $(answer[i], answer[j])$ 都应当满足: - $answer[i] \% answer[j] == 0$,或 - $answer[j] \% answer[i] == 0$ 如果存在多个有效解子集,返回其中任何一个均可。 **说明**: - $1 \le nums.length \le 10^{3}$。 - $1 \le nums[i] \le 2 * 10^{9}$。 - nums 中的所有整数 互不相同。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3] 输出:[1,2] 解释:[1,3] 也会被视为正确答案。 ``` - 示例 2: ```python 输入:nums = [1,2,4,8] 输出:[1,2,4,8] ``` ## 解题思路 ### 思路 1:动态规划 + 排序 这道题的核心思想是:**先对数组排序,然后使用动态规划找到最长的整除子集,最后回溯构造结果**。 解题步骤: 1. **排序**:将数组 $nums$ 按升序排序,这样对于任意 $i < j$,如果 $nums[j] \% nums[i] == 0$,则 $nums[i]$ 可以整除 $nums[j]$。 2. **动态规划**: - 定义 $dp[i]$ 表示以 $nums[i]$ 结尾的最长整除子集的长度。 - 定义 $parent[i]$ 表示以 $nums[i]$ 结尾的最长整除子集中,$nums[i]$ 的前一个元素的索引。 - 状态转移方程:$dp[i] = \max(dp[j] + 1)$,其中 $j < i$ 且 $nums[i] \% nums[j] == 0$。 3. **找最大值**:遍历 $dp$ 数组,找到最大值 $max\_len$ 和对应的索引 $max\_index$。 4. **回溯构造结果**:从 $max\_index$ 开始,通过 $parent$ 数组回溯构造最长整除子集。 **关键点**: - 排序后,只需要检查 $nums[i] \% nums[j] == 0$($j < i$),因为如果 $nums[j] > nums[i]$,则 $nums[j] \% nums[i] \neq 0$。 - 使用 $parent$ 数组记录路径,便于后续回溯构造结果。 - 时间复杂度主要由排序决定,为 $O(n^2)$。 ### 思路 1:代码 ```python class Solution: def largestDivisibleSubset(self, nums: List[int]) -> List[int]: if not nums: return [] # 对数组进行排序 nums.sort() n = len(nums) # dp[i] 表示以 nums[i] 结尾的最长整除子集的长度 dp = [1] * n # parent[i] 表示以 nums[i] 结尾的最长整除子集中,nums[i] 的前一个元素的索引 parent = [-1] * n # 动态规划计算最长长度 for i in range(1, n): for j in range(i): # 如果 nums[i] 能被 nums[j] 整除,且当前长度更优 if nums[i] % nums[j] == 0 and dp[j] + 1 > dp[i]: dp[i] = dp[j] + 1 parent[i] = j # 找到最长子集的长度和结束位置 max_len = max(dp) max_index = dp.index(max_len) # 回溯构造结果 result = [] while max_index != -1: result.append(nums[max_index]) max_index = parent[max_index] # 由于是回溯构造的,需要反转结果 return result[::-1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是数组的长度。排序需要 $O(n \log n)$ 时间,动态规划需要 $O(n^2)$ 时间,回溯构造结果需要 $O(n)$ 时间。 - **空间复杂度**:$O(n)$,需要 $O(n)$ 的空间存储 $dp$ 数组和 $parent$ 数组。 ================================================ FILE: docs/solutions/0300-0399/lexicographical-numbers.md ================================================ # [0386. 字典序排数](https://leetcode.cn/problems/lexicographical-numbers/) - 标签:深度优先搜索、字典树 - 难度:中等 ## 题目链接 - [0386. 字典序排数 - 力扣](https://leetcode.cn/problems/lexicographical-numbers/) ## 题目大意 给定一个整数 `n`。 要求:按字典序返回范围 `[1, n]` 的所有整数。并且要求时间复杂度为 `O(n)`,空间复杂度为 `o(1)`。 ## 解题思路 按照字典序进行深度优先搜索。实质上算是构造一棵字典树,然后将 `[1, n]` 中的数插入到字典树中,并将遍历结果存储到列表中。 ## 代码 ```python class Solution: def dfs(self, cur, n, res): if cur > n: return res.append(cur) for i in range(10): num = 10 * cur + i if num > n: return self.dfs(num, n, res) def lexicalOrder(self, n: int) -> List[int]: res = [] for i in range(1, 10): self.dfs(i, n, res) return res ``` ================================================ FILE: docs/solutions/0300-0399/line-reflection.md ================================================ # [0356. 直线镜像](https://leetcode.cn/problems/line-reflection/) - 标签:数组、哈希表、数学 - 难度:中等 ## 题目链接 - [0356. 直线镜像 - 力扣](https://leetcode.cn/problems/line-reflection/) ## 题目大意 **描述**: 在一个二维平面空间中,给定 $n$ 个点的坐标。 **要求**: 问,是否能找出一条平行于 $y$ 轴的直线,让这些点关于这条直线成镜像排布? 如果能,则返回 $true$,否则返回 $false$。 **说明**: - 注意:题目数据中可能有重复的点。 - $n == points.length$。 - $1 \le n \le 10^4$。 - $-10^8 \le points[i][j] \le 10^8$。 - 进阶:你能找到比 O(n2) 更优的解法吗? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/04/23/356_example_1.PNG) ```python 输入:points = [[1,1],[-1,1]] 输出:true 解释:可以找出 x = 0 这条线。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/04/23/356_example_2.PNG) ```python 输入:points = [[1,1],[-1,-1]] 输出:false 解释:无法找出这样一条线。 ``` ## 解题思路 ### 思路 1:哈希表 + 数学 这道题的核心思想是:**利用数学性质,通过哈希表快速判断点集是否关于某条垂直线对称**。 解题步骤: 1. **去重处理**:由于题目数据中可能有重复的点,首先将点集转换为集合去重。 2. **计算对称轴**:如果点集关于垂直线 $x = k$ 对称,那么对于任意点 $(x, y)$,其对称点应该是 $(2k - x, y)$。所有点的 $x$ 坐标的平均值就是对称轴的位置,即 $k = \frac{\min(x) + \max(x)}{2}$。 3. **验证对称性**:遍历所有点,检查每个点 $(x, y)$ 的对称点 $(2k - x, y)$ 是否存在于点集中。 **关键点**: - 对称轴位置:$k = \frac{\min(x) + \max(x)}{2}$。 - 对称点坐标:$(x, y)$ 关于 $x = k$ 的对称点是 $(2k - x, y)$。 - 使用集合存储点坐标,便于 $O(1)$ 时间查找。 - 时间复杂度为 $O(n)$,空间复杂度为 $O(n)$。 ### 思路 1:代码 ```python class Solution: def isReflected(self, points: List[List[int]]) -> bool: if not points: return True # 去重并转换为集合,便于快速查找 point_set = set() x_coords = [] for point in points: x, y = point[0], point[1] point_set.add((x, y)) x_coords.append(x) # 计算对称轴位置:k = (min_x + max_x) / 2 min_x = min(x_coords) max_x = max(x_coords) k = (min_x + max_x) / 2 # 检查每个点是否都有对应的对称点 for x, y in point_set: # 计算对称点的 x 坐标 symmetric_x = 2 * k - x # 检查对称点是否存在于点集中 if (symmetric_x, y) not in point_set: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是点的数量。需要遍历所有点两次:一次去重和收集坐标,一次验证对称性。 - **空间复杂度**:$O(n)$,需要 $O(n)$ 的空间存储去重后的点集和坐标列表。 ================================================ FILE: docs/solutions/0300-0399/linked-list-random-node.md ================================================ # [0382. 链表随机节点](https://leetcode.cn/problems/linked-list-random-node/) - 标签:水塘抽样、链表、数学、随机化 - 难度:中等 ## 题目链接 - [0382. 链表随机节点 - 力扣](https://leetcode.cn/problems/linked-list-random-node/) ## 题目大意 **描述**: 给定一个单链表,随机选择链表的一个节点,并返回相应的节点值。每个节点「被选中的概率一样」。 **要求**: 实现 Solution 类: - `Solution(ListNode head)` 使用整数数组初始化对象。 - `int getRandom()` 从链表中随机选择一个节点并返回该节点的值。链表中所有节点被选中的概率相等。 **说明**: - 链表中的节点数在范围 $[1, 10^{4}]$ 内。 - $-10^{4} \le Node.val \le 10^{4}$。 - 至多调用 `getRandom` 方法 $10^{4}$ 次。 - 进阶: - 如果链表非常大且长度未知,该怎么处理? - 你能否在不使用额外空间的情况下解决此问题? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/16/getrand-linked-list.jpg) ```python 输入 ["Solution", "getRandom", "getRandom", "getRandom", "getRandom", "getRandom"] [[[1, 2, 3]], [], [], [], [], []] 输出 [null, 1, 3, 2, 2, 3] 解释 Solution solution = new Solution([1, 2, 3]); solution.getRandom(); // 返回 1 solution.getRandom(); // 返回 3 solution.getRandom(); // 返回 2 solution.getRandom(); // 返回 2 solution.getRandom(); // 返回 3 // getRandom() 方法应随机返回 1、2、3中的一个,每个元素被返回的概率相等。 ``` ## 解题思路 ### 思路 1:水塘抽样算法 这道题的核心思想是:**使用水塘抽样算法,在不知道链表长度的情况下,保证每个节点被选中的概率相等**。 解题步骤: 1. **初始化**:保存链表头节点 $head$,用于后续遍历。 2. **水塘抽样**: - 遍历链表,对于第 $i$ 个节点(从 $1$ 开始计数),以 $\frac{1}{i}$ 的概率选择该节点。 - 使用随机数生成器 $random.randint(0, i-1)$,如果结果为 $0$,则选择当前节点。 - 这样保证每个节点被选中的概率都是 $\frac{1}{n}$,其中 $n$ 是链表长度。 3. **概率证明**: - 第 $1$ 个节点被选中的概率:$P_1 = 1$。 - 第 $2$ 个节点被选中的概率:$P_2 = \frac{1}{2}$。 - 第 $3$ 个节点被选中的概率:$P_3 = \frac{1}{3}$。 - ... - 第 $n$ 个节点被选中的概率:$P_n = \frac{1}{n}$。 - 最终每个节点被选中的概率:$P_i = \frac{1}{i} \times \frac{i}{i+1} \times \frac{i+1}{i+2} \times ... \times \frac{n-1}{n} = \frac{1}{n}$。 **关键点**: - 水塘抽样算法适用于 **数据流** 或 **未知长度** 的情况。 - 只需要 **一次遍历**,时间复杂度为 $O(n)$。 - 空间复杂度为 $O(1)$,不需要额外存储所有节点。 - 每次调用 `getRandom()` 都需要重新遍历整个链表。 ### 思路 1:代码 ```python # Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next import random class Solution: def __init__(self, head: Optional[ListNode]): # 保存链表头节点 self.head = head def getRandom(self) -> int: # 水塘抽样算法 current = self.head result = 0 count = 0 # 遍历链表 while current: count += 1 # 以 1/count 的概率选择当前节点 if random.randint(0, count - 1) == 0: result = current.val current = current.next return result # Your Solution object will be instantiated and called as such: # obj = Solution(head) # param_1 = obj.getRandom() ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是链表的长度。每次调用 `getRandom()` 都需要遍历整个链表。 - **空间复杂度**:$O(1)$,只使用了常数额外空间,没有使用额外的数据结构存储节点。 ================================================ FILE: docs/solutions/0300-0399/logger-rate-limiter.md ================================================ # [0359. 日志速率限制器](https://leetcode.cn/problems/logger-rate-limiter/) - 标签:设计、哈希表 - 难度:简单 ## 题目链接 - [0359. 日志速率限制器 - 力扣](https://leetcode.cn/problems/logger-rate-limiter/) ## 题目大意 设计一个日志系统,可以流式接受消息和消息的时间戳。每条不重复的信息最多每 10 秒打印一次。即如果在时间 t 打印了 A 信息,则直到 t+10 的时间,才能再次打印这条信息。 要求实现 Logger 类: - `def __init__(self):` 初始化 logger 对象 - `def shouldPrintMessage(self, timestamp: int, message: str) -> bool:` - 如果该条消息 message 在给定时间戳 timestamp 能够打印出来,则返回 True,否则返回 False。 ## 解题思路 初始化一个哈希表,用来存储消息 message 最后一次打印的时间戳。 当新的消息到达是,先判断之前是否出现过相同的消息,如果未出现则可打印,存储时间戳,并返回 True。 如果出现过,且上一次相同的消息在 10 秒之前打印的,则该消息也可打印,更新时间戳,并返回 True。 如果上一次相同的消息是在 10 秒内打印的,则该信息不可打印,直接返回 False。 ## 代码 ```python class Logger: def __init__(self): """ Initialize your data structure here. """ self.msg_dict = dict() def shouldPrintMessage(self, timestamp: int, message: str) -> bool: """ Returns true if the message should be printed in the given timestamp, otherwise returns false. If this method returns false, the message will not be printed. The timestamp is in seconds granularity. """ if message not in self.msg_dict: self.msg_dict[message] = timestamp return True if timestamp - self.msg_dict[message] >= 10: self.msg_dict[message] = timestamp return True else: return False ``` ================================================ FILE: docs/solutions/0300-0399/longest-absolute-file-path.md ================================================ # [0388. 文件的最长绝对路径](https://leetcode.cn/problems/longest-absolute-file-path/) - 标签:栈、深度优先搜索、字符串 - 难度:中等 ## 题目链接 - [0388. 文件的最长绝对路径 - 力扣](https://leetcode.cn/problems/longest-absolute-file-path/) ## 题目大意 **描述**: 假设有一个同时存储文件和目录的文件系统。下图展示了文件系统的一个示例: ![](https://assets.leetcode.com/uploads/2020/08/28/mdir.jpg) 这里将 $dir$ 作为根目录中的唯一目录。$dir$ 包含两个子目录 $subdir1$ 和 $subdir2$。 - $subdir1$ 包含文件 $file1.ext$ 和子目录 $subsubdir1$; - $subdir2$ 包含子目录 $subsubdir2$,该子目录下包含文件 $file2.ext$。 在文本格式中,如下所示(⟶表示制表符): ```python dir ⟶ $subdir1$ ⟶ ⟶ $file1$.ext ⟶ ⟶ $subsubdir1$ ⟶ $subdir2$ ⟶ ⟶ $subsubdir2$ ⟶ ⟶ ⟶ $file2$.ext ``` 如果是代码表示,上面的文件系统可以写为 `"dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext"`。`'\n'` 和 `'\t'` 分别是换行符和制表符。 文件系统中的每个文件和文件夹都有一个唯一的「绝对路径」,即必须打开才能到达文件/目录所在位置的目录顺序,所有路径用 `'/'` 连接。 上面例子中,指向 $file2.ext$ 的「绝对路径」是 `"dir/subdir2/subsubdir2/file2.ext"`。每个目录名由字母、数字和/或空格组成,每个文件名遵循 $name.extension$ 的格式,其中 $name$ 和 $extension$ 由字母、数字和 / 或空格组成。 给定一个以上述格式表示文件系统的字符串 $input$。 **要求**: 返回文件系统中指向「文件」的「最长绝对路径」的长度。如果系统中没有文件,返回 $0$。 **说明**: - $1 \le input.length \le 10^{4}$。 - $input$ 可能包含小写或大写的英文字母,一个换行符 `'\n'`,一个制表符 `'\t'`,一个点 `'.'`,一个空格 `' '`,和数字。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/08/28/dir1.jpg) ```python 输入:input = "dir\n\tsubdir1\n\tsubdir2\n\t\tfile.ext" 输出:20 解释:只有一个文件,绝对路径为 "dir/subdir2/file.ext" ,路径长度 20 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/08/28/dir2.jpg) ```python 输入:input = "dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext" 输出:32 解释:存在两个文件: "dir/subdir1/file1.ext" ,路径长度 21 "dir/subdir2/subsubdir2/file2.ext" ,路径长度 32 返回 32 ,因为这是最长的路径 ``` ## 解题思路 ### 思路 1:栈模拟 这道题的核心思想是:**使用栈来模拟文件系统的层级结构,通过制表符数量判断层级深度,计算每个文件的绝对路径长度**。 解题步骤: 1. **分割输入**:将输入字符串按换行符 `\n` 分割成每一行。 2. **栈维护路径**: - 使用栈 $stack$ 维护当前路径的各个层级。 - 栈中存储每个层级对应的路径长度。 - 栈顶元素表示当前层级的路径长度。 3. **层级判断**: - 通过计算每行开头的制表符 `\t` 数量来确定层级深度 $depth$。 - 制表符数量即为层级深度。 4. **路径计算**: - 如果当前层级 $depth$ 小于栈的长度,说明需要回退到对应层级。 - 弹出栈中深度大于等于 $depth$ 的所有元素。 - 将当前行(去除制表符)的长度加入栈中。 - 如果当前行是文件(包含 `.`),则计算完整路径长度并更新最大值。 5. **路径长度计算**: - 完整路径长度 = 栈中所有元素之和 + 栈的长度 - 1(分隔符 `/` 的数量) - 即:$total\_length = \sum_{i=0}^{stack.size-1} stack[i] + (stack.size - 1)$ **关键点**: - 栈的大小表示当前路径的层级深度。 - 栈中每个元素表示对应层级文件 / 目录名的长度。 - 只有文件(包含 `.`)才参与最长路径的计算。 - 路径分隔符 `/` 的数量等于层级数减 $1$。 ### 思路 1:代码 ```python class Solution: def lengthLongestPath(self, input: str) -> int: # 按换行符分割输入字符串 lines = input.split('\n') # 使用栈维护当前路径的各个层级长度 stack = [] # 记录最长文件路径长度 max_length = 0 for line in lines: # 计算当前行的层级深度(制表符数量) depth = 0 while depth < len(line) and line[depth] == '\t': depth += 1 # 获取当前行去除制表符后的内容 name = line[depth:] # 如果当前层级小于栈的大小,需要回退到对应层级 while len(stack) > depth: stack.pop() # 将当前行长度加入栈中 stack.append(len(name)) # 如果当前行是文件(包含 '.'),计算完整路径长度 if '.' in name: # 完整路径长度 = 所有层级长度之和 + 分隔符数量 total_length = sum(stack) + len(stack) - 1 max_length = max(max_length, total_length) return max_length ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是输入字符串的长度。需要遍历所有行,每行最多入栈和出栈一次。 - **空间复杂度**:$O(d)$,其中 $d$ 是文件系统的最大深度。栈的最大深度不会超过文件系统的层级深度。 ================================================ FILE: docs/solutions/0300-0399/longest-increasing-path-in-a-matrix.md ================================================ # [0329. 矩阵中的最长递增路径](https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/) - 标签:深度优先搜索、广度优先搜索、图、拓扑排序、记忆化搜索、数组、动态规划、矩阵 - 难度:困难 ## 题目链接 - [0329. 矩阵中的最长递增路径 - 力扣](https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/) ## 题目大意 给定一个 `m * n` 大小的整数矩阵 `matrix`。要求:找出其中最长递增路径的长度。 对于每个单元格,可以往上、下、左、右四个方向移动,不能向对角线方向移动或移动到边界外。 ## 解题思路 深度优先搜索。使用二维数组 `record` 存储遍历过的单元格最大路径长度,已经遍历过的单元格就不需要再次遍历了。 ## 代码 ```python class Solution: max_len = 0 directions = {(1, 0), (-1, 0), (0, 1), (0, -1)} def longestIncreasingPath(self, matrix: List[List[int]]) -> int: if not matrix: return 0 rows, cols = len(matrix), len(matrix[0]) record = [[0 for _ in range(cols)] for _ in range(rows)] def dfs(i, j): record[i][j] = 1 for direction in self.directions: new_i, new_j = i + direction[0], j + direction[1] if 0 <= new_i < rows and 0 <= new_j < cols and matrix[new_i][new_j] > matrix[i][j]: if record[new_i][new_j] == 0: dfs(new_i, new_j) record[i][j] = max(record[i][j], record[new_i][new_j] + 1) self.max_len = max(self.max_len, record[i][j]) for i in range(rows): for j in range(cols): if record[i][j] == 0: dfs(i, j) return self.max_len ``` ================================================ FILE: docs/solutions/0300-0399/longest-increasing-subsequence.md ================================================ # [0300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/) - 标签:数组、二分查找、动态规划 - 难度:中等 ## 题目链接 - [0300. 最长递增子序列 - 力扣](https://leetcode.cn/problems/longest-increasing-subsequence/) ## 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:找到其中最长严格递增子序列的长度。 **说明**: - **子序列**:由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,$[3,6,2,7]$ 是数组 $[0,3,1,6,2,2,7]$ 的子序列。 - $1 \le nums.length \le 2500$。 - $-10^4 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4。 ``` - 示例 2: ```python 输入:nums = [0,1,0,3,2,3] 输出:4 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照子序列的结尾位置进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为:以 $nums[i]$ 结尾的最长递增子序列长度。 ###### 3. 状态转移方程 一个较小的数后边如果出现一个较大的数,则会形成一个更长的递增子序列。 对于满足 $0 \le j < i$ 的数组元素 $nums[j]$ 和 $nums[i]$ 来说: - 如果 $nums[j] < nums[i]$,则 $nums[i]$ 可以接在 $nums[j]$ 后面,此时以 $nums[i]$ 结尾的最长递增子序列长度会在「以 $nums[j]$ 结尾的最长递增子序列长度」的基础上加 $1$,即 $dp[i] = dp[j] + 1$。 - 如果 $nums[j] \le nums[i]$,则 $nums[i]$ 不可以接在 $nums[j]$ 后面,可以直接跳过。 综上,我们的状态转移方程为:$dp[i] = max(dp[i], dp[j] + 1), 0 \le j < i, nums[j] < nums[i]$。 ###### 4. 初始条件 默认状态下,把数组中的每个元素都作为长度为 $1$ 的递增子序列。即 $dp[i] = 1$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示为:以 $nums[i]$ 结尾的最长递增子序列长度。那为了计算出最大的最长递增子序列长度,则需要再遍历一遍 $dp$ 数组,求出最大值即为最终结果。 ### 思路 1:动态规划代码 ```python class Solution: def lengthOfLIS(self, nums: List[int]) -> int: size = len(nums) dp = [1 for _ in range(size)] for i in range(size): for j in range(i): if nums[i] > nums[j]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。两重循环遍历的时间复杂度是 $O(n^2)$,最后求最大值的时间复杂度是 $O(n)$,所以总体时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 ### 思路 2:进阶的线性 DP + 二分查找 ###### 1. 算法思想 使用 **数组作为可实时更新内部的单调栈**,结合 **贪心法** 和 **二分查找** 来优化时间复杂度。 核心思想: - $tails[k]$ 表示长度为 $k+1$ 的 LIS 的最小末尾元素(无闲置索引,动态扩展) - 对于每个元素 $x$,使用二分查找在 $tails$ 中找到第一个 $\ge x$ 的位置 - 如果 $x$ 比所有元素都大,则追加到 $tails$ 末尾,形成新的 LIS 长度 - 否则,用较小的 $x$ 更新 $tails[k]$,优化同长度 LIS 的末尾元素(变得更小) ###### 2. 算法步骤 1. 初始化 $tails$ 为空数组。 2. 遍历数组 $nums$ 中的每个元素 $x$: - 使用二分查找在 $tails$ 中找到第一个 $\ge x$ 的位置 $k$。 - 如果 $k == len(tails)$,说明 $x$ 比所有元素都大,追加到 $tails$ 末尾。 - 否则,用 $x$ 更新 $tails[k]$,优化同长度 LIS 的末尾元素。 3. 返回 $tails$ 的长度,即为最终 LIS 的长度。 ### 思路 2:进阶的线性 DP + 二分查找代码 ```python import bisect class Solution: def lengthOfLIS(self, nums: List[int]) -> int: if len(nums) == 1: return 1 # tails[k] 表示长度为 k+1 的 LIS 的最小末尾元素(无闲置索引,动态扩展) tails = list() for x in nums: # 二分查找:在tails中找首个≥x的位置 k = bisect.bisect_left(tails, x) # 如果 x 比所有元素都大,k 的结果会是当前数组长度(末尾位置索引 +1) # x 直接新增在 tails 数组末尾,形成新问题(LIS 长度 +1) if k == len(tails): tails.append(x) # 用较小的 x 更新较大的 tails[k],优化同长度 LIS 问题的末尾元素(变得更小) else: tails[k] = x # tails 的长度即为最终 LIS 问题的长度 return len(tails) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \log n)$。遍历数组的时间复杂度是 $O(n)$,每个元素进行二分查找的时间复杂度是 $O(\log n)$,所以总体时间复杂度为 $O(n \log n)$。 - **空间复杂度**:$O(n)$。$tails$ 数组最多存储 $n$ 个元素,所以总体空间复杂度为 $O(n)$。 ## 参考资料 - 【题解】[进阶的线性DP + 二分查找 | 来自评论区](https://github.com/xiaos2021) ================================================ FILE: docs/solutions/0300-0399/longest-substring-with-at-least-k-repeating-characters.md ================================================ # [0395. 至少有 K 个重复字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/) - 标签:哈希表、字符串、分治、滑动窗口 - 难度:中等 ## 题目链接 - [0395. 至少有 K 个重复字符的最长子串 - 力扣](https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/) ## 题目大意 给定一个字符串 `s` 和一个整数 `k`。 要求:找出 `s` 中的最长子串, 要求该子串中的每一字符出现次数都不少于 `k` 。返回这一子串的长度。 注意:`s` 仅由小写英文字母构成。 ## 解题思路 这道题看起来很像是常规滑动窗口套路的问题,但是用普通滑动窗口思路无法解决问题。 如果窗口需要保证「各种字符出现次数都大于等于 `k`」这一性质。那么当向右移动 `right`,扩大窗口时,如果 `s[right]` 是第一次出现的元素,窗口内的字符种类数量必然会增加,此时缩小 `s[left]` 也不一定满足窗口内「各种字符出现次数都大于等于 `k`」这一性质。那么我们就无法通过这种方式来进行滑动窗口。 但是我们可以通过固定字符种类数的方式进行滑动窗口。因为给定字符串 `s` 仅有小写字母构成,则最长子串中的字符种类数目,最少为 `1` 种,最多为 `26` 种。我们通过枚举最长子串中可能出现的字符种类数目,从而固定窗口中出现的字符种类数目 `i (1 <= i <= 26)`,再进行滑动数组。窗口内需要保证出现的字符种类数目等于 `i`。向右移动 `right`,扩大窗口时,记录窗口内各种类字符数量。当窗口内出现字符数量大于 `i` 时,则不断右移 `right`,保证窗口内出现字符种类等于 `i`。同时,记录窗口内出现次数小于 `k` 的字符数量,当窗口中出现次数小于 `k` 的字符数量为 `0` 时,就可以记录答案,并维护答案最大值了。 整个算法的具体步骤如下: - 使用 `ans` 记录满足要求的最长子串长度。 - 枚举最长子串中的字符种类数目 `i`,最小为 `1` 种,最大为 `26` 种。对于给定字符种类数目 `i`: - 使用两个指针 `left`、`right` 指向滑动窗口的左右边界。 - 使用 `window_count` 变量来统计窗口内字符种类数目,保证窗口中的字符种类数目 `window_count` 不多于 `i`。 - 使用 `letter_map` 哈希表记录窗口中各个字符出现的数目。使用 `less_k_count` 记录窗口内出现次数小于 `k` 次的字符数量。 - 向右移动 `right`,将最右侧字符 `s[right]` 加入当前窗口,用 `letter_map` 记录该字符个数。 - 如果该字符第一次出现,即 `letter_map[s[right]] == 1`,则窗口内字符种类数目 + 1,即 `window_count += 1`。同时窗口内小于 `k` 次的字符数量 + 1(等到 `letter_map[s[right]] >= k` 时再减去),即 `less_k_count += 1`。 - 如果该字符已经出现过 `k` 次,即 `letter_map[s[right]] == k`,则窗口内小于 `k` 次的字符数量 -1,即 `less_k_count -= 1`。 - 当窗口内字符种类数目 `window_count` 大于给定字符种类数目 `i` 时,即 `window_count > i`,则不断右移 `left`,缩小滑动窗口长度,直到 `window_count == i`。 - 如果此时窗口内字符种类数目 `window_count` 等于给定字符种类 `i` 并且小于 `k` 次的字符数量为 `0`,即 `window_count == i and less_k_count == 0` 时,维护更新答案为 `ans = max(right - left + 1, ans)`。 - 最后输出答案 `ans`。 ## 代码 ```python class Solution: def longestSubstring(self, s: str, k: int) -> int: ans = 0 for i in range(1, 27): left, right = 0, 0 window_count = 0 less_k_count = 0 letter_map = dict() while right < len(s): if s[right] in letter_map: letter_map[s[right]] += 1 else: letter_map[s[right]] = 1 if letter_map[s[right]] == 1: window_count += 1 less_k_count += 1 if letter_map[s[right]] == k: less_k_count -= 1 while window_count > i: letter_map[s[left]] -= 1 if letter_map[s[left]] == 0: window_count -= 1 less_k_count -= 1 if letter_map[s[left]] == k - 1: less_k_count += 1 left += 1 if window_count == i and less_k_count == 0: ans = max(right - left + 1, ans) right += 1 return ans ``` ================================================ FILE: docs/solutions/0300-0399/longest-substring-with-at-most-k-distinct-characters.md ================================================ # [0340. 至多包含 K 个不同字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-most-k-distinct-characters/) - 标签:哈希表、字符串、滑动窗口 - 难度:中等 ## 题目链接 - [0340. 至多包含 K 个不同字符的最长子串 - 力扣](https://leetcode.cn/problems/longest-substring-with-at-most-k-distinct-characters/) ## 题目大意 给定一个字符串 `s`, 要求:返回至多包含 `k` 个不同字符的最长子串 `t` 的长度。 ## 解题思路 用滑动窗口 `window_counts` 来记录各个字符个数,`window_counts` 为哈希表类型。用 `ans` 来维护至多包含 `k` 个不同字符的最长子串 `t` 的长度。 设定两个指针:`left`、`right`,分别指向滑动窗口的左右边界,保证窗口中不超过 `k` 种字符。 - 一开始,`left`、`right` 都指向 `0`。 - 将最右侧字符 `s[right]` 加入当前窗口 `window_counts` 中,记录该字符个数,向右移动 `right`。 - 如果该窗口中字符的种数多于 `k` 个,即 `len(window_counts) > k`,则不断右移 `left`,缩小滑动窗口长度,并更新窗口中对应字符的个数,直到 `len(window_counts) <= k`。 - 维护更新至多包含 `k` 个不同字符的最长子串 `t` 的长度。然后继续右移 `right`,直到 `right >= len(nums)` 结束。 - 输出答案 `ans`。 ## 代码 ```python class Solution: def lengthOfLongestSubstringKDistinct(self, s: str, k: int) -> int: ans = 0 window_counts = dict() left, right = 0, 0 while right < len(s): if s[right] in window_counts: window_counts[s[right]] += 1 else: window_counts[s[right]] = 1 while(len(window_counts) > k): window_counts[s[left]] -= 1 if window_counts[s[left]] == 0: del window_counts[s[left]] left += 1 ans = max(ans, right - left + 1) right += 1 return ans ``` ================================================ FILE: docs/solutions/0300-0399/max-sum-of-rectangle-no-larger-than-k.md ================================================ # [0363. 矩形区域不超过 K 的最大数值和](https://leetcode.cn/problems/max-sum-of-rectangle-no-larger-than-k/) - 标签:数组、二分查找、矩阵、有序集合、前缀和 - 难度:困难 ## 题目链接 - [0363. 矩形区域不超过 K 的最大数值和 - 力扣](https://leetcode.cn/problems/max-sum-of-rectangle-no-larger-than-k/) ## 题目大意 **描述**: 给定一个 $m \times n$ 的矩阵 $matrix$ 和一个整数 $k$。 **要求**: 找出并返回矩阵内部矩形区域的不超过 $k$ 的最大数值和。 题目数据保证总会存在一个数值和不超过 $k$ 的矩形区域。 **说明**: - $m == matrix.length$。 - $n == matrix[i].length$。 - $1 \le m, n \le 10^{3}$。 - $-10^{3} \le matrix[i][j] \le 10^{3}$。 - $-10^{5} \le k \le 10^{5}$。 - 进阶:如果行数远大于列数,该如何设计解决方案? **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/18/sum-grid.jpg) ```python 输入:matrix = [[1,0,1],[0,-2,3]], k = 2 输出:2 解释:蓝色边框圈出来的矩形区域 [[0, 1], [-2, 3]] 的数值和是 2,且 2 是不超过 k 的最大数字(k = 2)。 ``` - 示例 2: ```python 输入:matrix = [[2,2,-1]], k = 3 输出:3 ``` ## 解题思路 ### 思路 1:前缀和 + 有序集合 这道题的核心思想是:**将二维问题转化为一维问题,使用前缀和配合有序集合来快速查找不超过 $k$ 的最大子数组和**。 解题步骤: 1. **枚举列边界**:固定矩形的左右边界 $left$ 和 $right$,将二维问题转化为一维问题。 2. **计算行前缀和**:对于每一行,计算从 $left$ 到 $right$ 的列和,得到一维数组 $row\_sums$。 3. **前缀和优化**:使用前缀和数组 $prefix\_sums$,其中 $prefix\_sums[i] = \sum_{j=0}^{i-1} row\_sums[j]$。 4. **有序集合查找**:对于每个位置 $i$,我们需要找到最大的 $j < i$,使得 $prefix\_sums[i] - prefix\_sums[j] \leq k$,即 $prefix\_sums[j] \geq prefix\_sums[i] - k$。 5. **二分查找优化**:使用有序集合(如 `SortedList`)存储已处理的前缀和,通过二分查找快速找到满足条件的最小前缀和。 **关键点**: - 时间复杂度从 $O(m^2n^2)$ 优化到 $O(m^2n \log n)$。 - 使用有序集合维护前缀和的单调性,支持 $O(\log n)$ 的插入和查找操作。 - 对于每个固定的列边界,问题转化为"最大子数组和不超过 $k$"的一维问题。 **算法正确性**: 设当前处理到位置 $i$,前缀和为 $prefix\_sums[i]$,我们需要找到最大的 $j < i$ 使得:$prefix\_sums[i] - prefix\_sums[j] \leq k$。 即:$prefix\_sums[j] \geq prefix\_sums[i] - k$。 由于我们要找最大的子数组和,所以应该找最小的满足条件的 $prefix\_sums[j]$,这样 $prefix\_sums[i] - prefix\_sums[j]$ 最大。 ### 思路 1:代码 ```python from sortedcontainers import SortedList from typing import List class Solution: def maxSumSubmatrix(self, matrix: List[List[int]], k: int) -> int: m, n = len(matrix), len(matrix[0]) result = float('-inf') # 枚举左边界 for left in range(n): # 存储当前列边界下的行和 row_sums = [0] * m # 枚举右边界 for right in range(left, n): # 计算每行从left到right的列和 for i in range(m): row_sums[i] += matrix[i][right] # 使用有序集合维护前缀和 sorted_list = SortedList([0]) # 初始化为0,表示空子数组 prefix_sum = 0 # 遍历每一行,计算前缀和 for row_sum in row_sums: prefix_sum += row_sum # 查找满足条件的最小前缀和 # 需要找到 prefix_sum - x <= k,即 x >= prefix_sum - k target = prefix_sum - k idx = sorted_list.bisect_left(target) # 如果找到了满足条件的前缀和 if idx < len(sorted_list): current_sum = prefix_sum - sorted_list[idx] result = max(result, current_sum) # 将当前前缀和加入有序集合 sorted_list.add(prefix_sum) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m^2n \log n)$,其中 $m$ 是矩阵的行数,$n$ 是矩阵的列数。外层双重循环枚举列边界的时间复杂度是 $O(n^2)$,内层对每行计算前缀和的时间复杂度是 $O(m)$,有序集合的插入和查找操作的时间复杂度是 $O(\log n)$。 - **空间复杂度**:$O(m + n)$,其中 $m$ 是矩阵的行数,$n$ 是矩阵的列数。主要空间消耗来自行和数组 $row\_sums$ 和有序集合 $sorted\_list$。 ================================================ FILE: docs/solutions/0300-0399/maximum-product-of-word-lengths.md ================================================ # [0318. 最大单词长度乘积](https://leetcode.cn/problems/maximum-product-of-word-lengths/) - 标签:位运算、数组、字符串 - 难度:中等 ## 题目链接 - [0318. 最大单词长度乘积 - 力扣](https://leetcode.cn/problems/maximum-product-of-word-lengths/) ## 题目大意 给定一个字符串数组 `words`。字符串中只包含英语的小写字母。 要求:计算当两个字符串 `words[i]` 和 `words[j]` 不包含相同字符时,它们长度的乘积的最大值。如果没有不包含相同字符的一对字符串,返回 0。 ## 解题思路 这道题的核心难点是判断任意两个字符串之间是否包含相同字符。最直接的做法是先遍历第一个字符串的每个字符,再遍历第二个字符串查看是否有相同字符。但是这样做的话,时间复杂度过高。考虑怎么样可以优化一下。 题目中说字符串中只包含英语的小写字母,也就是 `26` 种字符。一个 `32` 位的 `int` 整数每一个二进制位都可以表示一种字符的有无,那么我们就可以通过一个整数来表示一个字符串中所拥有的字符种类。延伸一下,我们可以用一个整数数组来表示一个字符串数组中,每个字符串所拥有的字符种类。 接下来事情就简单了,两重循环遍历整数数组,遇到两个字符串不包含相同字符的情况,就计算一下他们长度的乘积,并维护一个乘积最大值。最后输出最大值即可。 ## 代码 ```python class Solution: def maxProduct(self, words: List[str]) -> int: size = len(words) arr = [0 for _ in range(size)] for i in range(size): word = words[i] len_word = len(word) for j in range(len_word): arr[i] |= 1 << (ord(word[j]) - ord('a')) ans = 0 for i in range(size): for j in range(i + 1, size): if arr[i] & arr[j] == 0: k = len(words[i]) * len(words[j]) ans = k if ans < k else ans return ans ``` ================================================ FILE: docs/solutions/0300-0399/maximum-size-subarray-sum-equals-k.md ================================================ # [0325. 和等于 k 的最长子数组长度](https://leetcode.cn/problems/maximum-size-subarray-sum-equals-k/) - 标签:数组、哈希表、前缀和 - 难度:中等 ## 题目链接 - [0325. 和等于 k 的最长子数组长度 - 力扣](https://leetcode.cn/problems/maximum-size-subarray-sum-equals-k/) ## 题目大意 **描述**: 给定一个数组 $nums$ 和一个目标值 $k$。 **要求**: 找到和等于 $k$ 的最长连续子数组长度。如果不存在任意一个符合要求的子数组,则返回 $0$。 **说明**: - $1 \le nums.length \le 2 \times 10^{5}$。 - $-10^{4} \le nums[i] \le 10^{4}$。 - $-10^{9} \le k \le 10^{9}$。 **示例**: - 示例 1: ```python 输入: nums = [1,-1,5,-2,3], k = 3 输出: 4 解释: 子数组 [1, -1, 5, -2] 和等于 3,且长度最长。 ``` - 示例 2: ```python 输入: nums = [-2,-1,2,1], k = 1 输出: 2 解释: 子数组 [-1, 2] 和等于 1,且长度最长。 ``` ## 解题思路 ### 思路 1:前缀和 + 哈希表 这道题的核心思想是:**使用前缀和配合哈希表来快速查找满足条件的子数组**。 解题步骤: 1. **计算前缀和**:定义 $prefix\_sum[i]$ 表示数组前 $i$ 个元素的和,即 $prefix\_sum[i] = \sum_{j=0}^{i-1} nums[j]$。 2. **利用前缀和性质**:对于子数组 $nums[i:j+1]$,其和为 $prefix\_sum[j+1] - prefix\_sum[i]$。如果这个和等于 $k$,则有 $prefix\_sum[j+1] - prefix\_sum[i] = k$,即 $prefix\_sum[i] = prefix\_sum[j+1] - k$。 3. **哈希表记录**:使用哈希表 $prefix\_map$ 记录每个前缀和第一次出现的位置。对于当前位置 $i$,如果 $prefix\_sum[i] - k$ 在哈希表中存在,说明存在一个子数组的和为 $k$。 4. **更新最长长度**:每次找到满足条件的子数组时,更新最大长度。 **关键点**: - 初始化时,$prefix\_sum[0] = 0$ 对应空数组,位置为 $-1$。 - 哈希表只记录每个前缀和第一次出现的位置,这样可以保证找到的子数组长度最长。 - 对于每个位置,先检查是否存在满足条件的前缀和,再更新当前前缀和的位置。 ### 思路 1:代码 ```python from typing import List class Solution: def maxSubArrayLen(self, nums: List[int], k: int) -> int: if not nums: return 0 # 哈希表记录前缀和第一次出现的位置 prefix_map = {0: -1} # 前缀和为0的位置为-1(空数组) prefix_sum = 0 max_length = 0 for i in range(len(nums)): # 计算当前位置的前缀和 prefix_sum += nums[i] # 检查是否存在前缀和 prefix_sum - k # 如果存在,说明从 prefix_map[prefix_sum - k] + 1 到 i 的子数组和为 k if prefix_sum - k in prefix_map: # 计算当前子数组的长度 current_length = i - prefix_map[prefix_sum - k] max_length = max(max_length, current_length) # 如果当前前缀和还没有记录,则记录其位置 if prefix_sum not in prefix_map: prefix_map[prefix_sum] = i return max_length ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。只需要遍历数组一次,每次哈希表的查找和插入操作都是 $O(1)$。 - **空间复杂度**:$O(n)$,哈希表最多存储 $n$ 个不同的前缀和。 ================================================ FILE: docs/solutions/0300-0399/mini-parser.md ================================================ # [0385. 迷你语法分析器](https://leetcode.cn/problems/mini-parser/) - 标签:栈、深度优先搜索、字符串 - 难度:中等 ## 题目链接 - [0385. 迷你语法分析器 - 力扣](https://leetcode.cn/problems/mini-parser/) ## 题目大意 **描述**: 给定一个字符串 $s$ 表示一个整数嵌套列表。 **要求**: 实现一个解析它的语法分析器并返回解析的结果 `NestedInteger`。 **说明**: - 列表中的每个元素只可能是整数或整数嵌套列表。 - $1 \le s.length \le 5 \times 10^{4}$。 - $s$ 由数字、方括号 `"[]"`、负号 `'-'`、逗号 `','` 组成。 - 用例保证 $s$ 是可解析的 `NestedInteger。` - 输入中的所有值的范围是 $[-10^{6}, 10^{6}]$。 **示例**: - 示例 1: ```python 输入:s = "324", 输出:324 解释:你应该返回一个 NestedInteger 对象,其中只包含整数值 324。 ``` - 示例 2: ```python 输入:s = "[123,[456,[789]]]", 输出:[123,[456,[789]]] 解释:返回一个 NestedInteger 对象包含一个有两个元素的嵌套列表: 1. 一个 integer 包含值 123 2. 一个包含两个元素的嵌套列表: i. 一个 integer 包含值 456 ii. 一个包含一个元素的嵌套列表 a. 一个 integer 包含值 789 ``` ## 解题思路 ### 思路 1:栈 这道题的核心思想是:**使用栈来模拟嵌套结构的解析过程**。 解题步骤: 1. **初始化栈**:创建一个栈 $stack$ 来存储当前正在构建的 `NestedInteger` 对象。 2. **遍历字符串**:逐个字符处理字符串 $s$: - 遇到 `'['`:创建新的 `NestedInteger` 对象并压入栈中。 - 遇到 `']'`:弹出栈顶元素,如果栈不为空,将其添加到新的栈顶元素中。 - 遇到数字或负号:解析完整的数字,创建 `NestedInteger` 对象并添加到当前栈顶元素中。 - 遇到逗号:跳过,继续处理下一个字符。 3. **返回结果**:栈中最终剩余的元素就是解析结果。 **关键点**: - 栈用于维护当前嵌套层级,每遇到 `'['` 就进入新的嵌套层级。 - 每遇到 `']'` 就退出当前嵌套层级,将当前层级的结果添加到上一层级。 - 数字解析需要处理负号和多位数字的情况。 - 如果输入是单个数字(没有方括号),直接返回该数字对应的 `NestedInteger`。 **算法正确性**: 设字符串 $s$ 的长度为 $n$,算法通过栈正确维护了嵌套结构: - 每个 `'['` 对应一个入栈操作,创建新的嵌套层级。 - 每个 `']'` 对应一个出栈操作,完成当前层级的构建。 - 数字解析保证了每个数字都被正确识别和转换。 ### 思路 1:代码 ```python # """ # This is the interface that allows for creating nested lists. # You should not implement it, or speculate about its implementation # """ #class NestedInteger: # def __init__(self, value=None): # """ # If value is not specified, initializes an empty list. # Otherwise initializes a single integer equal to value. # """ # # def isInteger(self): # """ # @return True if this NestedInteger holds a single integer, rather than a nested list. # :rtype bool # """ # # def add(self, elem): # """ # Set this NestedInteger to hold a nested list and adds a nested integer elem to it. # :rtype void # """ # # def setInteger(self, value): # """ # Set this NestedInteger to hold a single integer equal to value. # :rtype void # """ # # def getInteger(self): # """ # @return the single integer that this NestedInteger holds, if it holds a single integer # Return None if this NestedInteger holds a nested list # :rtype int # """ # # def getList(self): # """ # @return the nested list that this NestedInteger holds, if it holds a nested list # Return None if this NestedInteger holds a single integer # :rtype List[NestedInteger] # """ class Solution: def deserialize(self, s: str) -> NestedInteger: # 如果字符串不以'['开头,说明是单个数字 if s[0] != '[': return NestedInteger(int(s)) # 使用栈来维护嵌套结构 stack = [] i = 0 while i < len(s): if s[i] == '[': # 遇到'[',创建新的NestedInteger并压入栈 stack.append(NestedInteger()) i += 1 elif s[i] == ']': # 遇到']',弹出栈顶元素 if len(stack) > 1: # 如果栈中还有元素,将当前元素添加到上一层级 current = stack.pop() stack[-1].add(current) i += 1 elif s[i] == ',': # 遇到逗号,跳过 i += 1 else: # 遇到数字,解析完整的数字 start = i # 处理负号 if s[i] == '-': i += 1 # 解析数字部分 while i < len(s) and s[i].isdigit(): i += 1 # 创建数字对应的NestedInteger并添加到当前栈顶 num = int(s[start:i]) stack[-1].add(NestedInteger(num)) # 返回栈中唯一的元素 return stack[0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。需要遍历字符串中的每个字符一次。 - **空间复杂度**:$O(d)$,其中 $d$ 是嵌套的最大深度。栈的空间复杂度取决于嵌套层级数。 ================================================ FILE: docs/solutions/0300-0399/minimum-height-trees.md ================================================ # [0310. 最小高度树](https://leetcode.cn/problems/minimum-height-trees/) - 标签:深度优先搜索、广度优先搜索、图、拓扑排序 - 难度:中等 ## 题目链接 - [0310. 最小高度树 - 力扣](https://leetcode.cn/problems/minimum-height-trees/) ## 题目大意 **描述**:有一棵包含 $n$ 个节点的树,节点编号为 $0 \sim n - 1$。给定一个数字 $n$ 和一个有 $n - 1$ 条无向边的 $edges$ 列表来表示这棵树。其中 $edges[i] = [ai, bi]$ 表示树中节点 $ai$ 和 $bi$ 之间存在一条无向边。 可以选择树中的任何一个节点作为根,当选择节点 $x$ 作为根节点时,设结果树的高度为 $h$。在所有可能的树种,具有最小高度的树(即 $min(h)$)被成为最小高度树。 **要求**:找到所有的最小高度树并按照任意顺序返回他们的根节点编号列表。 **说明**: - **树的高度**:指根节点和叶子节点之间最长向下路径上边的数量。 - $1 \le n \le 2 * 10^4$。 - $edges.length == n - 1$。 - $0 \le ai, bi < n$。 - $ai \ne bi$。 - 所有 $(ai, bi)$ 互不相同。 - 给定的输入保证是一棵树,并且不会有重复的边。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/09/01/e1.jpg) ```python 输入:n = 4, edges = [[1,0],[1,2],[1,3]] 输出:[1] 解释:如图所示,当根是标签为 1 的节点时,树的高度是 1 ,这是唯一的最小高度树。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/09/01/e2.jpg) ```python 输入:n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]] 输出:[3,4] ``` ## 解题思路 ### 思路 1:树形 DP + 二次遍历换根法 最容易想到的做法是:枚举 $n$ 个节点,以每个节点为根节点,然后进行深度优先搜索,求出每棵树的高度。最后求出所有树中的最小高度即为答案。但这种做法的时间复杂度为 $O(n^2)$,而 $n$ 的范围为 $[1, 2 * 10^4]$,这样做会导致超时,因此需要进行优化。 在上面的算法中,在一轮深度优先搜索中,除了可以得到整棵树的高度之外,在搜索过程中,其实还能得到以每个子节点为根节点的树的高度。如果我们能够利用这些子树的高度信息,快速得到以其他节点为根节点的树的高度,那么我们就能改进算法,以更小的时间复杂度解决这道题。这就是二次遍历与换根法的思想。 1. 第一次遍历:自底向上的计算出每个节点 $u$ 向下走(即由父节点 $u$ 向子节点 $v$ 走)的最长路径 $down1[u]$、次长路径 $down2[i]$,并记录向下走最长路径所经过的子节点 $p[u]$,方便第二次遍历时计算。 2. 第二次遍历:自顶向下的计算出每个节点 $v$ 向上走(即由子节点 $v$ 向父节点 $u$ 走)的最长路径 $up[v]$。需要注意判断 $u$ 向下走的最长路径是否经过了节点 $v$。 1. 如果经过了节点 $v$,则向上走的最长路径,取决于「父节点 $u$ 向上走的最长路径」与「父节点 $u$ 向下走的次长路径」 的较大值,再加上 $1$。 2. 如果没有经过节点 $v$,则向上走的最长路径,取决于「父节点 $u$ 向上走的最长路径」与「父节点 $u$ 向下走的最长路径」 的较大值,再加上 $1$。 3. 接下来,我们通过枚举 $n$​ 个节点向上走的最长路径与向下走的最长路径,从而找出所有树中的最小高度,并将所有最小高度树的根节点放入答案数组中并返回。 整个算法具体步骤如下: 1. 使用邻接表的形式存储树。 3. 定义第一个递归函数 `dfs(u, fa)` 用于计算每个节点向下走的最长路径 $down1[u]$、次长路径 $down2[u]$,并记录向下走的最长路径所经过的子节点 $p[u]$。 1. 对当前节点的相邻节点进行遍历。 2. 如果相邻节点是父节点,则跳过。 3. 递归调用 `dfs(v, u)` 函数计算邻居节点的信息。 4. 根据邻居节点的信息计算当前节点的高度,并更新当前节点向下走的最长路径 $down1[u]$、当前节点向下走的次长路径 $down2$、取得最长路径的子节点 $p[u]$。 4. 定义第二个递归函数 `reroot(u, fa)` 用于计算每个节点作为新的根节点时向上走的最长路径 $up[v]$。 1. 对当前节点的相邻节点进行遍历。 2. 如果相邻节点是父节点,则跳过。 3. 根据当前节点 $u$ 的高度和相邻节点 $v$ 的信息更新 $up[v]$。同时需要判断节点 $u$ 向下走的最长路径是否经过了节点 $v$。 1. 如果经过了节点 $v$,则向上走的最长路径,取决于「父节点 $u$ 向上走的最长路径」与「父节点 $u$ 向下走的次长路径」 的较大值,再加上 $1$,即:$up[v] = max(up[u], down2[u]) + 1$。 2. 如果没有经过节点 $v$,则向上走的最长路径,取决于「父节点 $u$ 向上走的最长路径」与「父节点 $u$ 向下走的最长路径」 的较大值,再加上 $1$,即:$up[v] = max(up[u], down1[u]) + 1$。 4. 递归调用 `reroot(v, u)` 函数计算邻居节点的信息。 5. 调用 `dfs(0, -1)` 函数计算每个节点的最长路径。 6. 调用 `reroot(0, -1)` 函数计算每个节点作为新的根节点时的最长路径。 7. 找到所有树中的最小高度。 8. 将所有最小高度的节点放入答案数组中并返回。 ### 思路 1:代码 ```python class Solution: def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]: graph = [[] for _ in range(n)] for u, v in edges: graph[u].append(v) graph[v].append(u) # down1 用于记录向下走的最长路径 down1 = [0 for _ in range(n)] # down2 用于记录向下走的最长路径 down2 = [0 for _ in range(n)] p = [0 for _ in range(n)] # 自底向上记录最长路径、次长路径 def dfs(u, fa): for v in graph[u]: if v == fa: continue # 自底向上统计信息 dfs(v, u) height = down1[v] + 1 if height >= down1[u]: down2[u] = down1[u] down1[u] = height p[u] = v elif height > down2[u]: down2[u] = height # 进行换根动态规划,自顶向下统计向上走的最长路径 up = [0 for _ in range(n)] def reroot(u, fa): for v in graph[u]: if v == fa: continue if p[u] == v: up[v] = max(up[u], down2[u]) + 1 else: up[v] = max(up[u], down1[u]) + 1 # 自顶向下统计信息 reroot(v, u) dfs(0, -1) reroot(0, -1) # 找到所有树中的最小高度 min_h = 1e9 for i in range(n): min_h = min(min_h, max(down1[i], up[i])) # 将所有最小高度的节点放入答案数组中并返回 res = [] for i in range(n): if max(down1[i], up[i]) == min_h: res.append(i) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ## 参考资料 - 【题解】[C++ 容易理解的换根动态规划解法 - 最小高度树](https://leetcode.cn/problems/minimum-height-trees/solution/c-huan-gen-by-vclip-sa84/) - 【题解】[310. 最小高度树 - 最小高度树 - 力扣](https://leetcode.cn/problems/minimum-height-trees/solution/310-zui-xiao-gao-du-shu-by-vincent-40-teg8/) - 【题解】[310. 最小高度树 - 最小高度树 - 力扣](https://leetcode.cn/problems/minimum-height-trees/solution/310-zui-xiao-gao-du-shu-by-vincent-40-teg8/) ================================================ FILE: docs/solutions/0300-0399/moving-average-from-data-stream.md ================================================ # [0346. 数据流中的移动平均值](https://leetcode.cn/problems/moving-average-from-data-stream/) - 标签:设计、队列、数组、数据流 - 难度:简单 ## 题目链接 - [0346. 数据流中的移动平均值 - 力扣](https://leetcode.cn/problems/moving-average-from-data-stream/) ## 题目大意 给定一个整数 `val` 和一个窗口大小 `size`。 要求:根据滑动窗口的大小,计算滑动窗口里所有数字的平均值。要实现 `MovingAverage` 类: - `MovingAverage(int size)` 用窗口大小 `size` 初始化对象。 - `double next(int val)` 成员函数 `next` 每次调用的时候都会往滑动窗口增加一个整数,请计算并返回数据流中最后 `size` 个值的移动平均值,即滑动窗口里所有数字的平均值。 ## 解题思路 使用队列保存滑动窗口的元素,并记录对应窗口大小和元素和。 在小于窗口大小的时候,直接向队列中添加元素,并记录元素和。 在等于窗口大小的时候,先将队列头部元素弹出,再添加元素,并记录元素和。 然后根据元素和和队列中元素个数计算出平均值。 ## 代码 ```python class MovingAverage: def __init__(self, size: int): """ Initialize your data structure here. """ self.queue = [] self.size = size self.sum = 0 def next(self, val: int) -> float: if len(self.queue) < self.size: self.queue.append(val) else: if self.queue: self.sum -= self.queue[0] self.queue.pop(0) self.queue.append(val) self.sum += val return self.sum / len(self.queue) ``` ================================================ FILE: docs/solutions/0300-0399/nested-list-weight-sum-ii.md ================================================ # [0364. 嵌套列表加权和 II](https://leetcode.cn/problems/nested-list-weight-sum-ii/) - 标签:栈、深度优先搜索、广度优先搜索 - 难度:中等 ## 题目链接 - [0364. 嵌套列表加权和 II - 力扣](https://leetcode.cn/problems/nested-list-weight-sum-ii/) ## 题目大意 **描述**: 给定一个整数嵌套列表 $nestedList$,每一个元素要么是一个整数,要么是一个列表(这个列表中的每个元素也同样是整数或列表)。 - 整数的「深度」取决于它位于多少个列表内部。例如,嵌套列表 $[1,[2,2],[[3],2],1]$ 的每个整数的值都等于它的深度。令 $maxDepth$ 是任意整数的「最大深度」。 - 整数的「权重」为 $maxDepth$ - (整数的深度) $+ 1$。 **要求**: 将 $nestedList$ 列表中每个整数先乘权重再求和,返回该加权和。 **说明**: - $1 \le nestedList.length \le 50$。 - 嵌套列表中整数的值在范围 $[-10^{3}, 10^{3}]$。 - 任意整数的最大「深度」小于等于 $50$。 - 没有空列表。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/27/nestedlistweightsumiiex1.png) ```python 输入:nestedList = [[1,1],2,[1,1]] 输出:8 解释:4 个 1 在深度为 1 的位置, 一个 2 在深度为 2 的位置。 1*1 + 1*1 + 2*2 + 1*1 + 1*1 = 8 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/27/nestedlistweightsumiiex2.png) ```python 输入:nestedList = [1,[4,[6]]] 输出:17 解释:一个 1 在深度为 3 的位置, 一个 4 在深度为 2 的位置,一个 6 在深度为 1 的位置。 1*3 + 4*2 + 6*1 = 17 ``` ## 解题思路 ### 思路 1:两次遍历 1. **第一次遍历**:计算最大深度 $maxDepth$。使用深度优先搜索遍历整个嵌套列表,记录每个整数的深度,并更新最大深度。 2. **第二次遍历**:计算加权和。再次遍历嵌套列表,对于每个整数,其权重为 $maxDepth - depth + 1$,其中 $depth$ 是该整数的深度。将所有整数的值乘以其权重后求和。 具体步骤: - 定义递归函数 `getMaxDepth(nestedList, depth)` 用于计算最大深度。 - 定义递归函数 `calculateSum(nestedList, depth, maxDepth)` 用于计算加权和。 - 对每个 `NestedInteger` 元素: - 如果是整数,则在计算最大深度时进行比较和更新;在计算加权和时累加当前值乘以权重。 - 如果是列表,则递归地处理其内部元素。 ### 思路 1:代码 ```python # """ # This is the interface that allows for creating nested lists. # You should not implement it, or speculate about its implementation # """ #class NestedInteger: # def __init__(self, value=None): # """ # If value is not specified, initializes an empty list. # Otherwise initializes a single integer equal to value. # """ # # def isInteger(self): # """ # @return True if this NestedInteger holds a single integer, rather than a nested list. # :rtype bool # """ # # def add(self, elem): # """ # Set this NestedInteger to hold a nested list and adds a nested integer elem to it. # :rtype void # """ # # def setInteger(self, value): # """ # Set this NestedInteger to hold a single integer equal to value. # :rtype void # """ # # def getInteger(self): # """ # @return the single integer that this NestedInteger holds, if it holds a single integer # Return None if this NestedInteger holds a nested list # :rtype int # """ # # def getList(self): # """ # @return the nested list that this NestedInteger holds, if it holds a nested list # Return None if this NestedInteger holds a single integer # :rtype List[NestedInteger] # """ class Solution: def depthSumInverse(self, nestedList: List[NestedInteger]) -> int: # 第一次遍历:计算最大深度 max_depth = self.getMaxDepth(nestedList, 1) # 第二次遍历:计算加权和 return self.calculateSum(nestedList, 1, max_depth) def getMaxDepth(self, nestedList: List[NestedInteger], depth: int) -> int: """计算嵌套列表的最大深度""" max_depth = depth for item in nestedList: if item.isInteger(): # 如果是整数,更新最大深度 max_depth = max(max_depth, depth) else: # 如果是列表,递归计算子列表的最大深度 max_depth = max(max_depth, self.getMaxDepth(item.getList(), depth + 1)) return max_depth def calculateSum(self, nestedList: List[NestedInteger], depth: int, max_depth: int) -> int: """计算加权和""" total_sum = 0 for item in nestedList: if item.isInteger(): # 如果是整数,计算其加权贡献 # 权重 = max_depth - depth + 1 weight = max_depth - depth + 1 total_sum += item.getInteger() * weight else: # 如果是列表,递归计算子列表的加权和 total_sum += self.calculateSum(item.getList(), depth + 1, max_depth) return total_sum ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是嵌套列表中所有整数的总数。需要遍历两次嵌套列表,每次遍历的时间复杂度都是 $O(n)$。 - **空间复杂度**:$O(d)$,其中 $d$ 是嵌套列表的最大深度。递归调用栈的深度最多为 $d$。 ================================================ FILE: docs/solutions/0300-0399/nested-list-weight-sum.md ================================================ # [0339. 嵌套列表加权和](https://leetcode.cn/problems/nested-list-weight-sum/) - 标签:深度优先搜索、广度优先搜索 - 难度:中等 ## 题目链接 - [0339. 嵌套列表加权和 - 力扣](https://leetcode.cn/problems/nested-list-weight-sum/) ## 题目大意 **描述**: 给定一个嵌套的整数列表 $nestedList$,每个元素要么是整数,要么是列表。同时,列表中元素同样也可以是整数或者是另一个列表。 - 整数的「深度」是其在列表内部的嵌套层数。例如,嵌套列表 $[1,[2,2],[[3],2],1]$ 中每个整数的值就是其深度。 **要求**: 请返回该列表按深度加权后所有整数的总和。 **说明**: - $1 \le nestedList.length \le 50$。 - 嵌套列表中整数的值在范围 $[-10^{3}, 10^{3}]$ 内。 - 任何整数的最大深度都小于或等于 $50$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/01/14/nestedlistweightsumex1.png) ```python 输入:nestedList = [[1,1],2,[1,1]] 输出:10 解释:因为列表中有四个深度为 2 的 1 ,和一个深度为 1 的 2。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/01/14/nestedlistweightsumex2.png) ```python 输入:nestedList = [1,[4,[6]]] 输出:27 解释:一个深度为 1 的 1,一个深度为 2 的 4,一个深度为 3 的 6。所以,1 + 4*2 + 6*3 = 27。 ``` ## 解题思路 ### 思路 1:深度优先搜索 使用深度优先搜索(DFS)递归遍历嵌套列表,在遍历过程中记录当前深度 $depth$,对于每个整数,将其值乘以深度后累加到总和中。 具体步骤: 1. **递归函数设计**:定义递归函数 `dfs(nestedList, depth)`,其中 $nestedList$ 是当前要处理的嵌套列表,$depth$ 是当前深度。 2. **遍历过程**: - 对于每个 `NestedInteger` 元素: - 如果是整数,则将其值乘以当前深度 $depth$ 后累加到总和中。 - 如果是列表,则递归调用 `dfs(item.getList(), depth + 1)`,深度加 $1$。 3. **初始调用**:从深度 $1$ 开始调用 `dfs(nestedList, 1)`。 ### 思路 1:代码 ```python # """ # This is the interface that allows for creating nested lists. # You should not implement it, or speculate about its implementation # """ #class NestedInteger: # def __init__(self, value=None): # """ # If value is not specified, initializes an empty list. # Otherwise initializes a single integer equal to value. # """ # # def isInteger(self): # """ # @return True if this NestedInteger holds a single integer, rather than a nested list. # :rtype bool # """ # # def add(self, elem): # """ # Set this NestedInteger to hold a nested list and adds a nested integer elem to it. # :rtype void # """ # # def setInteger(self, value): # """ # Set this NestedInteger to hold a single integer equal to value. # :rtype void # """ # # def getInteger(self): # """ # @return the single integer that this NestedInteger holds, if it holds a single integer # The result is undefined if this NestedInteger holds a nested list # :rtype int # """ # # def getList(self): # """ # @return the nested list that this NestedInteger holds, if it holds a nested list # The result is undefined if this NestedInteger holds a single integer # :rtype List[NestedInteger] # """ class Solution: def depthSum(self, nestedList: List[NestedInteger]) -> int: def dfs(nested_list, depth): """深度优先搜索计算加权和""" total_sum = 0 for item in nested_list: if item.isInteger(): # 如果是整数,将其值乘以当前深度后累加 total_sum += item.getInteger() * depth else: # 如果是列表,递归处理子列表,深度加 1 total_sum += dfs(item.getList(), depth + 1) return total_sum # 从深度 1 开始递归计算 return dfs(nestedList, 1) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是嵌套列表中所有整数的总数。需要遍历每个元素一次。 - **空间复杂度**:$O(d)$,其中 $d$ 是嵌套列表的最大深度。递归调用栈的深度最多为 $d$。 ================================================ FILE: docs/solutions/0300-0399/number-of-connected-components-in-an-undirected-graph.md ================================================ # [0323. 无向图中连通分量的数目](https://leetcode.cn/problems/number-of-connected-components-in-an-undirected-graph/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [0323. 无向图中连通分量的数目 - 力扣](https://leetcode.cn/problems/number-of-connected-components-in-an-undirected-graph/) ## 题目大意 **描述**:给定 `n` 个节点(编号从 `0` 到 `n - 1`)的图的无向边列表 `edges`,其中 `edges[i] = [u, v]` 表示节点 `u` 和节点 `v` 之间有一条无向边。 **要求**:计算该无向图中连通分量的数量。 **说明**: - $1 \le n \le 2000$。 - $1 \le edges.length \le 5000$。 - $edges[i].length == 2$。 - $0 \le ai \le bi < n$。 - $ai != bi$。 - `edges` 中不会出现重复的边。 **示例**: - 示例 1: ```python 输入: n = 5 和 edges = [[0, 1], [1, 2], [3, 4]] 0 3 | | 1 --- 2 4 输出: 2 ``` - 示例 2: ```python 输入: n = 5 和 edges = [[0, 1], [1, 2], [2, 3], [3, 4]] 0 4 | | 1 --- 2 --- 3 输出: 1 ``` ## 解题思路 先来看一下图论中相关的名次解释。 - **连通图**:在无向图中,如果可以从顶点 $v_i$ 到达 $v_j$,则称 $v_i$ 和 $v_j$ 连通。如果图中任意两个顶点之间都连通,则称该图为连通图。 - **无向图的连通分量**:如果该图为连通图,则连通分量为本身;否则将无向图中的极大连通子图称为连通分量,每个连通分量都是一个连通图。 - **无向图的连通分量个数**:无向图的极大连通子图的个数。 接下来我们来解决这道题。 ### 思路 1:深度优先搜索 1. 使用 `visited` 数组标记遍历过的节点,使用 `count` 记录连通分量数量。 2. 从未遍历过的节点 `u` 出发,连通分量数量加 1。然后遍历与 `u` 节点构成无向边,且为遍历过的的节点 `v`。 3. 再从 `v` 出发继续深度遍历。 4. 直到遍历完与`u` 直接相关、间接相关的节点之后,再遍历另一个未遍历过的节点,继续上述操作。 5. 最后输出连通分量数目。 ### 思路 1:代码 ```python class Solution: def dfs(self, visited, i, graph): visited[i] = True for j in graph[i]: if not visited[j]: self.dfs(visited, j, graph) def countComponents(self, n: int, edges: List[List[int]]) -> int: count = 0 visited = [False for _ in range(n)] graph = [[] for _ in range(n)] for x, y in edges: graph[x].append(y) graph[y].append(x) for i in range(n): if not visited[i]: count += 1 self.dfs(visited, i, graph) return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中$n$ 是顶点个数。 - **空间复杂度**:$O(n)$。 ### 思路 2:广度优先搜索 1. 使用变量 `count` 记录连通分量个数。使用集合变量 `visited` 记录访问过的节点,使用邻接表 `graph` 记录图结构。 2. 从 `0` 开始,依次遍历 `n` 个节点。 3. 如果第 `i` 个节点未访问过: 1. 将其添加到 `visited` 中。 2. 并且连通分量个数累加,即 `count += 1`。 3. 定义一个队列 `queue`,将第 `i` 个节点加入到队列中。 4. 从队列中取出第一个节点,遍历与其链接的节点,并将未遍历过的节点加入到队列 `queue` 和 `visited` 中。 5. 直到队列为空,则继续向后遍历。 4. 最后输出连通分量数目 `count`。 ### 思路 2:代码 ```python import collections class Solution: def countComponents(self, n: int, edges: List[List[int]]) -> int: count = 0 visited = set() graph = [[] for _ in range(n)] for x, y in edges: graph[x].append(y) graph[y].append(x) for i in range(n): if i not in visited: visited.add(i) count += 1 queue = collections.deque([i]) while queue: node_u = queue.popleft() for node_v in graph[node_u]: if node_v not in visited: visited.add(node_v) queue.append(node_v) return count ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。其中$n$ 是顶点个数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0300-0399/number-of-islands-ii.md ================================================ # [0305. 岛屿数量 II](https://leetcode.cn/problems/number-of-islands-ii/) - 标签:并查集、数组、哈希表 - 难度:困难 ## 题目链接 - [0305. 岛屿数量 II - 力扣](https://leetcode.cn/problems/number-of-islands-ii/) ## 题目大意 **描述**: 给定一个大小为 $m \times n$ 的二维二进制网格 $grid$。网格表示一个地图,其中,$0$ 表示水,$1$ 表示陆地。最初,$grid$ 中的所有单元格都是水单元格(即,所有单元格都是 $0$)。 可以通过执行 $addLand$ 操作,将某个位置的水转换成陆地。给你一个数组 $positions$,其中 $positions[i] = [ri, ci]$ 是要执行第 $i$ 次操作的位置 $(ri, ci)$。 **要求**: 返回一个整数数组 $answer$,其中 $answer[i]$ 是将单元格 $(ri, ci)$ 转换为陆地后,地图中岛屿的数量。 **说明**: - 岛屿:指的是被「水」包围的「陆地」,通过水平方向或者垂直方向上相邻的陆地连接而成。你可以假设地图网格的四边均被无边无际的「水」所包围。 - $1 \le m, n, positions.length \le 10^{4}$。 - $1 \le m \times n \le 10^{4}$。 - $positions[i].length == 2$。 - $0 \le ri \lt m$。 - $0 \le ci \lt n$。 - 进阶:你可以设计一个时间复杂度 $O(k \log(mn))$ 的算法解决此问题吗?(其中 $k == positions.length$)。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/10/tmp-grid.jpg) ```python 输入:m = 3, n = 3, positions = [[0,0],[0,1],[1,2],[2,1]] 输出:[1,1,2,3] 解释: 起初,二维网格 grid 被全部注入「水」。(0 代表「水」,1 代表「陆地」) - 操作 #1:addLand(0, 0) 将 grid[0][0] 的水变为陆地。此时存在 1 个岛屿。 - 操作 #2:addLand(0, 1) 将 grid[0][1] 的水变为陆地。此时存在 1 个岛屿。 - 操作 #3:addLand(1, 2) 将 grid[1][2] 的水变为陆地。此时存在 2 个岛屿。 - 操作 #4:addLand(2, 1) 将 grid[2][1] 的水变为陆地。此时存在 3 个岛屿。 ``` - 示例 2: ```python 输入:m = 1, n = 1, positions = [[0,0]] 输出:[1] ``` ## 解题思路 ### 思路 1:并查集 使用并查集(Union Find)数据结构来动态维护岛屿的连通性。每次添加一个新的陆地时,检查其四个方向(上、下、左、右)是否已有陆地,如果有则进行合并操作。 具体步骤: 1. **初始化**:创建并查集,大小为 $m \times n$,初始时所有位置都是水(不属于任何岛屿)。 2. **添加陆地**:对于每个位置 $(r_i, c_i)$: - 将位置 $(r_i, c_i)$ 标记为陆地。 - 检查四个方向 $(r_i-1, c_i)$、$(r_i+1, c_i)$、$(r_i, c_i-1)$、$(r_i, c_i+1)$ 是否已有陆地。 - 如果有陆地,则与当前新添加的陆地合并到同一个连通分量中 - 统计当前连通分量的数量 3. **坐标转换**:将二维坐标 $(r, c)$ 转换为一维索引 $index = r \times n + c$,便于并查集操作。 4. **岛屿计数**:每次添加陆地后,统计并查集中独立连通分量的数量。 ### 思路 1:代码 ```python class UnionFind: def __init__(self, n): """初始化并查集""" self.parent = [i for i in range(n)] # 父节点数组 self.rank = [0] * n # 秩数组,用于路径压缩优化 self.count = 0 # 连通分量数量 def find(self, x): """查找根节点,带路径压缩""" if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) # 路径压缩 return self.parent[x] def union(self, x, y): """合并两个节点""" root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return False # 已经在同一个连通分量中 # 按秩合并 if self.rank[root_x] < self.rank[root_y]: self.parent[root_x] = root_y elif self.rank[root_x] > self.rank[root_y]: self.parent[root_y] = root_x else: self.parent[root_y] = root_x self.rank[root_x] += 1 self.count -= 1 # 合并后连通分量数量减 1 return True def add_island(self, x): """添加一个新的岛屿""" if self.parent[x] != x: # 已经是陆地 return self.parent[x] = x self.count += 1 # 新增一个连通分量 class Solution: def numIslands2(self, m: int, n: int, positions: List[List[int]]) -> List[int]: """使用并查集解决岛屿数量 II 问题""" # 方向数组:上、下、左、右 directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] # 初始化并查集 uf = UnionFind(m * n) # 标记哪些位置是陆地 is_land = [False] * (m * n) result = [] for r, c in positions: # 将二维坐标转换为一维索引 index = r * n + c # 如果该位置已经是陆地,直接返回当前岛屿数量 if is_land[index]: result.append(uf.count) continue # 标记为陆地 is_land[index] = True uf.add_island(index) # 检查四个方向,合并相邻的陆地 for dr, dc in directions: new_r, new_c = r + dr, c + dc # 检查边界 if 0 <= new_r < m and 0 <= new_c < n: new_index = new_r * n + new_c # 如果相邻位置是陆地,进行合并 if is_land[new_index]: uf.union(index, new_index) # 记录当前岛屿数量 result.append(uf.count) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k \times \alpha(mn))$,其中 $k$ 是 $positions$ 的长度,$\alpha$ 是反阿克曼函数,可以认为是常数。每次操作需要检查四个方向并进行并查集操作。 - **空间复杂度**:$O(mn)$,用于存储并查集的父节点数组、秩数组和陆地标记数组。 ================================================ FILE: docs/solutions/0300-0399/odd-even-linked-list.md ================================================ # [0328. 奇偶链表](https://leetcode.cn/problems/odd-even-linked-list/) - 标签:链表 - 难度:中等 ## 题目链接 - [0328. 奇偶链表 - 力扣](https://leetcode.cn/problems/odd-even-linked-list/) ## 题目大意 **描述**:给定一个单链表的头节点 `head`。 **要求**:将链表中的奇数位置上的节点排在前面,偶数位置上的节点排在后面,返回新的链表节点。 **说明**: - 要求空间复杂度为 $O(1)$。 - $n$ 等于链表中的节点数。 - $0 \le n \le 10^4$。 - $-10^6 \le Node.val \le 10^6$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/10/oddeven-linked-list.jpg) ```python 输入: head = [1,2,3,4,5] 输出: [1,3,5,2,4] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/10/oddeven2-linked-list.jpg) ```python 输入: head = [2,1,3,5,6,4,7] 输出: [2,3,6,7,1,5,4] ``` ## 解题思路 ### 思路 1:拆分后合并 1. 使用两个指针 `odd`、`even` 分别表示奇数节点链表和偶数节点链表。 2. 先将奇数位置上的节点和偶数位置上的节点分成两个链表,再将偶数节点的链表接到奇数链表末尾。 3. 过程中需要使用几个必要指针用于保留必要位置(比如原链表初始位置、偶数链表初始位置、当前遍历节点位置)。 ### 思路 1:代码 ```python class Solution: def oddEvenList(self, head: ListNode) -> ListNode: if not head or not head.next or not head.next.next: return head evenHead = head.next odd, even = head, evenHead isOdd = True curr = head.next.next while curr: if isOdd: odd.next = curr odd = curr else: even.next = curr even = curr isOdd = not isOdd curr = curr.next odd.next = evenHead even.next = None return head ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0300-0399/palindrome-pairs.md ================================================ # [0336. 回文对](https://leetcode.cn/problems/palindrome-pairs/) - 标签:字典树、数组、哈希表、字符串 - 难度:困难 ## 题目链接 - [0336. 回文对 - 力扣](https://leetcode.cn/problems/palindrome-pairs/) ## 题目大意 给定一组互不相同的单词列表 `words`。 要求:找出所有不同的索引对 `(i, j)`,使得列表中的两个单词 `words[i] + words[j]` ,可拼接成回文串。 ## 解题思路 如果字符串 `words[i] + words[j]` 能构成一个回文串,把 `words[i]` 分成 `words_left[i]` 和 `words_right[i]` 两部分。即 `words[i] + words[j] = words_left[i] + words_right[i] + words[j]`。则: - `words_right[i]` 本身是回文串,`words_left[i]` 和 `words[j]` 互为逆序。 同理,如果 `words[j] + word[i]` 能构成一个回文串,把 `word[i]` 分成 `words_left[i]` 和 `words_right[i]` 两部分。即 `words[j] + word[i] = words[j] + words_left[i] + words_right[i]`。则: - `words_left[i]` 本身是回文串,`words[j]` 和 `words_right[i]` 互为逆序。 从上面的表述可以得知,`words[j]` 可以通过拆分 `words[i]` 之后逆序得出。 我们使用两重循环遍历。一重循环遍历单词列表 `words` 中的每一个单词 `words[i]`,二重循环遍历每个单词的拆分位置 `j`。然后将每一个单词 `words[i]` 拆分成 `words[i][0:j+1]` 和 `words[i][j+1:]`。然后分别判断 `words[i][0:j+1]` 的逆序和 `words[i][j+1:]` 的逆序是否在单词列表中,如果在单词列表中,则将「`words[i]` 和 `words[i][0:j+1]` 对应的索引」或者 「`words[i]` 和 `words[i][j+1:]` 对应的索引」插入到答案数组中。 至于判断 `words[i][0:j+1]` 的逆序和 `words[i][j+1:]` 的逆序是否在单词列表中,以及获取 `words[i][0:j+1]` 的逆序和 `words[i][j+1:]` 的逆序所对应单词的索引下标可以通过构建字典树的方式获取。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False self.index = -1 def insert(self, word: str, index: int) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True cur.index = index def search(self, word: str) -> int: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return -1 cur = cur.children[ch] if cur is not None and cur.isEnd: return cur.index return -1 class Solution: def isPalindrome(self, word: str) -> bool: left, right = 0, len(word) - 1 while left < right: if word[left] != word[right]: return False left += 1 right -= 1 return True def palindromePairs(self, words: List[str]) -> List[List[int]]: trie_tree = Trie() size = len(words) for i in range(size): word = words[i] trie_tree.insert(word, i) res = [] for i in range(size): word = words[i] for j in range(len(word)): if self.isPalindrome(word[:j+1]): temp = word[j+1:][::-1] index = trie_tree.search(temp) if index != i and index != -1: res.append([index, i]) if temp == "": res.append([i, index]) if self.isPalindrome(word[j+1:]): temp = word[:j+1][::-1] index = trie_tree.search(temp) if index != i and index != -1: res.append([i, index]) return res ``` ================================================ FILE: docs/solutions/0300-0399/patching-array.md ================================================ # [0330. 按要求补齐数组](https://leetcode.cn/problems/patching-array/) - 标签:贪心、数组 - 难度:困难 ## 题目链接 - [0330. 按要求补齐数组 - 力扣](https://leetcode.cn/problems/patching-array/) ## 题目大意 **描述**: 给定一个已排序的正整数数组 $nums$,和一个正整数 $n$。从 $[1, n]$ 区间内选取任意个数字补充到 $nums$ 中,使得 $[1, n]$ 区间内的任何数字都可以用 $nums$ 中某几个数字的和来表示。 **要求**: 请返回「满足上述要求的最少需要补充的数字个数」。 **说明**: - $1 \le nums.length \le 10^{3}$。 - $1 \le nums[i] \le 10^{4}$。 - $nums$ 按升序排列。 - $1 \le n \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入: nums = [1,3], n = 6 输出: 1 解释: 根据 nums 里现有的组合 [1], [3], [1,3],可以得出 1, 3, 4。 现在如果我们将 2 添加到 nums 中, 组合变为: [1], [2], [3], [1,3], [2,3], [1,2,3]。 其和可以表示数字 1, 2, 3, 4, 5, 6,能够覆盖 [1, 6] 区间里所有的数。 所以我们最少需要添加一个数字。 ``` - 示例 2: ```python 输入: nums = [1,5,10], n = 20 输出: 2 解释: 我们需要添加 [2,4]。 ``` ## 解题思路 ### 思路 1:贪心算法 使用贪心算法来解决这个问题。核心思想是维护一个变量 $miss$,表示当前能够表示的最大连续范围是 $[1, miss)$。 具体步骤: 1. **初始化**:设置 $miss = 1$,表示当前能表示的最大连续范围是 $[1, 1)$,即空集。 2. **遍历数组**: - 如果当前数字 $nums[i] \le miss$,说明可以用现有数字表示 $[1, miss + nums[i])$ 范围内的所有数字,更新 $miss = miss + nums[i]$。 - 如果当前数字 $nums[i] > miss$,说明存在缺口,需要补充数字 $miss$,然后更新 $miss = miss + miss = 2 \times miss$。 3. **处理剩余范围**:当数组遍历完后,如果 $miss \le n$,继续补充数字直到能表示 $[1, n]$ 范围内的所有数字。 关键观察:每次补充数字时,选择当前 $miss$ 值是最优的,因为这样可以最大化覆盖范围。 ### 思路 1:代码 ```python class Solution: def minPatches(self, nums: List[int], n: int) -> int: patches = 0 # 记录需要补充的数字个数 miss = 1 # 当前能表示的最大连续范围是 [1, miss) i = 0 # 数组索引 while miss <= n: # 如果当前数字在可表示范围内,扩展可表示范围 if i < len(nums) and nums[i] <= miss: miss += nums[i] i += 1 else: # 需要补充数字 miss,扩展可表示范围 miss += miss patches += 1 return patches ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + \log n)$,其中 $m$ 是数组 $nums$ 的长度。需要遍历数组一次,然后最多需要 $\log n$ 次补充操作。 - **空间复杂度**:$O(1)$。只使用了常数额外空间。 ================================================ FILE: docs/solutions/0300-0399/perfect-rectangle.md ================================================ # [0391. 完美矩形](https://leetcode.cn/problems/perfect-rectangle/) - 标签:数组、扫描线 - 难度:困难 ## 题目链接 - [0391. 完美矩形 - 力扣](https://leetcode.cn/problems/perfect-rectangle/) ## 题目大意 **描述**:给定一个数组 `rectangles`,其中 `rectangles[i] = [xi, yi, ai, bi]` 表示一个坐标轴平行的矩形。这个矩形的左下顶点是 `(xi, yi)`,右上顶点是 `(ai, bi)`。 **要求**:如果所有矩形一起精确覆盖了某个矩形区域,则返回 `True`;否则,返回 `False`。 **说明**: - $1 \le rectangles.length \le 2 * 10^4$。 - $rectangles[i].length == 4$。 - $-10^5 \le xi, yi, ai, bi \le 10^5$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/27/perectrec1-plane.jpg) ```python 输入:rectangles = [[1,1,3,3],[3,1,4,2],[3,2,4,4],[1,3,2,4],[2,3,3,4]] 输出:True 解释:5 个矩形一起可以精确地覆盖一个矩形区域。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/27/perfectrec2-plane.jpg) ```python 输入:rectangles = [[1,1,2,3],[1,3,2,4],[3,1,4,2],[3,2,4,4]] 输出:false 解释:两个矩形之间有间隔,无法覆盖成一个矩形。 ``` - 示例 3: ![](https://assets.leetcode.com/uploads/2021/03/27/perfecrrec4-plane.jpg) ```python 输入:rectangles = [[1,1,3,3],[3,1,4,2],[1,3,2,4],[2,2,4,4]] 输出:False 解释:因为中间有相交区域,虽然形成了矩形,但不是精确覆盖。 ``` ## 解题思路 ### 思路 1:线段树 首先我们要先判断所有小矩形的面积和是否等于外接矩形区域的面积。如果不相等,则说明出现了重叠或者空缺,明显不符合题意。 在两者面积相等的情况下,还可能会发生重叠的情况。接下来我们要思考如何判断重叠。 - 第一种思路:暴力枚举所有矩形对,两两进行比较,判断是否出现了重叠。这样的时间复杂度是 $O(n^2)$,容易超时。 - 第二种思路: - 如果所有小矩形可以精确覆盖某个矩形区域,那这些小矩形一定是相互挨着的,也就是说相邻两个矩形的边会重合在一起。比如说 矩形 `A` 下边刚好是 `B` 的上边,或者是 `B` 的上边的一部分。 - 我们可以固定一个坐标轴,比如说固定 `y` 轴,然后只看水平方向上所有矩形的边。然后我们就会发现,满足题意要求的矩形区域中,纵坐标为 `y` 的平行线上,「所有上边纵坐标为 `y` 的矩形上边区间」与「所有下边纵坐标为 `y` 的矩形下边区间」是完全一样,或者说重合在一起的(除了矩形矩形最上边和最下边只有一条,不会重合之外)。 - 这样我们就可以用扫描线的思路,建立一个线段树。然后先固定纵坐标 `y`,将「所有上边纵坐标为 `y` 的矩形上边区间」对应的区间值减 `1`,再将「所有下边纵坐标为 `y` 的矩形下边区间」对应的区间值加 `1`。然后查询整个线代树区间值,如果区间值超过 `1`,则说明发生了重叠,不符合题目要求。如果扫描完所有的纵坐标,没有发生重叠,则说明符合题意要求。 - 因为横坐标的范围为 $[-10^5,10^5]$,但是最多只有 $2 * 10^4$ 个横坐标,所以我们可以先对所有坐标做一下离散化处理,再根据离散化之后的横坐标建立线段树。 具体步骤如下: 1. 通过遍历所有小矩形,计算出所有小矩形的面积和为 `area`。同时计算出矩形区域四个顶点位置,并根据四个顶点计算出矩形区域的面积为 `total_area`。如果所有小矩形面积不等于矩形区域的面积,则直接返回 `False`。 2. 再次遍历所有小矩形,将所有坐标点进行离散化处理,将其编号存入两个哈希表 `x_dict`、`y_dict`。 3. 使用哈希表 `top_dict`、`bottom_dict` 分别存储每个矩阵的上下两条边。将上下两条边的横坐标 `x1`、`x2`。分别存入到 `top_dict[y_dict[y2]]`、`top_dict[y_dict[y2]]` 中。 4. 建立区间长度为横坐标个数的线段树 `STree`。 5. 遍历所有的纵坐标,对于纵坐标 `i`: 1. 先遍历当前纵坐标下矩阵的上边数组,即 `top_dict[i]`,取出边的横坐标 `x1`、`x2`。令区间 `[x1, x2 - 1]` 上的值减 `1`。 2. 再遍历当前纵坐标下矩阵的下边数组,即 `bottom_dict[i]`,取出边的横坐标 `x1`、`x2`。令区间 `[x1, x2 - 1]` 上的值加 `1`。 3. 如果上下边覆盖完之后,被覆盖次数超过了 `1`,则说明出现了重叠,直接返回 `Fasle`。 6. 如果遍历完所有的纵坐标,没有发现重叠,则返回 `True`。 ### 思路 1:线段树代码 ```python # 线段树的节点类 class SegTreeNode: def __init__(self, val=0): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值) self.lazy_tag = None # 区间和问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, nums, function): self.size = len(nums) self.tree = [SegTreeNode() for _ in range(4 * self.size)] # 维护 SegTreeNode 数组 self.nums = nums # 原始数据 self.function = function # function 是一个函数,左右区间的聚合方法 if self.size > 0: self.__build(0, 0, self.size - 1) # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.nums[i] = val self.__update_point(i, val, 0) # 区间更新接口:将区间为 [q_left, q_right] 上的所有元素值加上 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, 0) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, 0) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self): for i in range(self.size): self.nums[i] = self.query_interval(i, i) return self.nums # 以下为内部实现方法 # 构建线段树实现方法:节点的存储下标为 index,节点的区间为 [left, right] def __build(self, index, left, right): self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,节点值为对应位置的元素值 self.tree[index].val = self.nums[left] return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.__build(left_index, left, mid) # 递归创建左子树 self.__build(right_index, mid + 1, right) # 递归创建右子树 self.__pushup(index) # 向上更新节点的区间值 # 区间更新实现方法 def __update_interval(self, q_left, q_right, val, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 if self.tree[index].lazy_tag is not None: self.tree[index].lazy_tag += val # 将当前节点的延迟标记增加 val else: self.tree[index].lazy_tag = val # 将当前节点的延迟标记增加 val self.tree[index].val += val # 当前节点所在区间每个元素值增加 val return if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return self.__pushdown(index) # 向下更新节点的区间值 mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if q_left <= mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, left_index) if q_right > mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间查询实现方法:在线段树中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return self.tree[index].val # 直接返回节点值 if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(index) mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, left_index) if q_right > mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, right_index) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:更新下标为 index 的节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, index): left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.tree[index].val = self.function(self.tree[left_index].val, self.tree[right_index].val) # 向下更新实现方法:更新下标为 index 的节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, index): lazy_tag = self.tree[index].lazy_tag if lazy_tag is None: return left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if self.tree[left_index].lazy_tag is not None: self.tree[left_index].lazy_tag += lazy_tag # 更新左子节点懒惰标记 else: self.tree[left_index].lazy_tag = lazy_tag self.tree[left_index].val += lazy_tag if self.tree[right_index].lazy_tag is not None: self.tree[right_index].lazy_tag += lazy_tag # 更新右子节点懒惰标记 else: self.tree[right_index].lazy_tag = lazy_tag self.tree[right_index].val += lazy_tag self.tree[index].lazy_tag = None # 更新当前节点的懒惰标记 class Solution: def isRectangleCover(self, rectangles) -> bool: left, right, bottom, top = math.inf, -math.inf, math.inf, -math.inf area = 0 x_set, y_set = set(), set() for rectangle in rectangles: x1, y1, x2, y2 = rectangle left, right = min(left, x1), max(right, x2) bottom, top = min(bottom, y1), max(top, y2) area += (y2 - y1) * (x2 - x1) x_set.add(x1) x_set.add(x2) y_set.add(y1) y_set.add(y2) total_area = (top - bottom) * (right - left) # 判断所有小矩形面积是否等于所有矩形顶点构成最大矩形面积,不等于则直接返回 False if area != total_area: return False # 离散化处理所有点的横坐标、纵坐标 x_dict, y_dict = dict(), dict() idx = 0 for x in sorted(list(x_set)): x_dict[x] = idx idx += 1 idy = 0 for y in sorted(list(y_set)): y_dict[y] = idy idy += 1 # 使用哈希表 top_dict、bottom_dict 分别存储每个矩阵的上下两条边。 bottom_dict, top_dict = collections.defaultdict(list), collections.defaultdict(list) for i in range(len(rectangles)): x1, y1, x2, y2 = rectangles[i] bottom_dict[y_dict[y1]].append([x_dict[x1], x_dict[x2]]) top_dict[y_dict[y2]].append([x_dict[x1], x_dict[x2]]) # 建立线段树 self.STree = SegmentTree([0 for _ in range(len(x_set))], lambda x, y: max(x, y)) for i in range(idy): for x1, x2 in top_dict[i]: self.STree.update_interval(x1, x2 - 1, -1) for x1, x2 in bottom_dict[i]: self.STree.update_interval(x1, x2 - 1, 1) cnt = self.STree.query_interval(0, len(x_set) - 1) if cnt > 1: return False return True ``` ## 参考资料 - 【题解】[线段树+扫描线 - 完美矩形 - 力扣](https://leetcode.cn/problems/perfect-rectangle/solution/xian-duan-shu-sao-miao-xian-by-lucifer10-raw5/) ================================================ FILE: docs/solutions/0300-0399/plus-one-linked-list.md ================================================ # [0369. 给单链表加一](https://leetcode.cn/problems/plus-one-linked-list/) - 标签:链表、数学 - 难度:中等 ## 题目链接 - [0369. 给单链表加一 - 力扣](https://leetcode.cn/problems/plus-one-linked-list/) ## 题目大意 **描述**: 给定一个用链表表示的非负整数。 **要求**: 将这个整数再加上 $1$。 **说明**: - 这些数字的存储是这样的:最高位有效的数字位于链表的首位 $head$。 - 链表中的节点数在 $[1, 10^{3}]$ 的范围内。 - $0 \le Node.val \le 9$。 - 由链表表示的数字不包含前导零,除了零本身。 **示例**: - 示例 1: ```python 输入: head = [1,2,3] 输出: [1,2,4] ``` - 示例 2: ```python 输入: head = [0] 输出: [1] ``` ## 解题思路 ### 思路 1:递归 + 进位处理 使用递归的方式从链表末尾开始处理,模拟加 $1$ 的进位过程。从最后一个节点开始,如果当前节点值为 $9$,则需要进位,将当前节点设为 $0$ 并继续向前处理;否则直接加 $1$ 并返回。 具体步骤: 1. **递归终止条件**:当到达链表末尾($head$ 为 $None$)时,返回进位标志 $1$。 2. **递归处理**: - 递归处理下一个节点,得到进位标志 $carry$。 - 如果 $carry = 1$: - 如果当前节点值为 $9$,则设为 $0$,继续向前进位。 - 否则当前节点值加 $1$,进位结束。 - 如果 $carry = 0$,则不需要处理,直接返回。 3. **处理最高位进位**:如果最高位也需要进位(即原数字全为 $9$),则创建新节点作为新的头节点。 ### 思路 1:代码 ```python # Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution: def plusOne(self, head: Optional[ListNode]) -> Optional[ListNode]: def dfs(node): """递归处理链表加 1,返回是否需要进位""" if not node: # 到达链表末尾,返回进位 1 return 1 # 递归处理下一个节点 carry = dfs(node.next) if carry == 1: if node.val == 9: # 当前节点为 9,需要进位 node.val = 0 return 1 else: # 当前节点不为 9,直接加 1 node.val += 1 return 0 # 不需要进位 return 0 # 递归处理整个链表 carry = dfs(head) # 如果最高位也需要进位,创建新的头节点 if carry == 1: new_head = ListNode(1) new_head.next = head return new_head return head ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是链表的长度。需要遍历链表一次。 - **空间复杂度**:$O(n)$,其中 $n$ 是链表的长度。递归调用栈的深度为链表长度。 ================================================ FILE: docs/solutions/0300-0399/power-of-four.md ================================================ # [0342. 4的幂](https://leetcode.cn/problems/power-of-four/) - 标签:位运算、递归、数学 - 难度:简单 ## 题目链接 - [0342. 4的幂 - 力扣](https://leetcode.cn/problems/power-of-four/) ## 题目大意 给定一个整数 $n$,判断 $n$ 是否是 $4$ 的幂次方,如果是的话,返回 True。不是的话,返回 False。 ## 解题思路 通过循环可以直接做。但有更好的方法。 $n$ 如果是 $4$ 的幂次方,那么 $n$ 肯定是 $2$ 的幂次方,$2$ 的幂次方二进制表示只含有一个 $1$,可以通过 $n \text{ \& } (n - 1)$ 将 $n$ 的最后位置上 的 $1$ 置为 $0$,通过判断 $n$ 是否满足 $n \text { \& } (n - 1) == 0$ 来判断 $n$ 是否是 $2$ 的幂次方。 如果根据上述判断,得出 $n$ 是 $2$ 的幂次方,则可以写为:$n = x^{2k}$ 或者 $n = x^{2k+1}$。如果 $n$ 是 $4$ 的幂次方,则 $n = 2^{k}$。 下面来看一下 $2^{2x}$、$2^{2x}+1$ 的情况: - $(2^{2x} \mod 3) = (4^x \mod 3) = ((3+1)^x \mod 3) == 1$ - $(2^{2x+1} \mod 3) = ((2 \times 4^x) \mod 3) = ((2 \times (3+1)^x) \mod 3) == 2$ 则如果 $n \mod 3 == 1$,则 $n$ 为 $4$ 的幂次方。 ## 代码 ```python class Solution: def isPowerOfFour(self, n: int) -> bool: return n > 0 and (n & (n-1)) == 0 and (n-1) % 3 == 0 ``` ================================================ FILE: docs/solutions/0300-0399/power-of-three.md ================================================ # [0326. 3 的幂](https://leetcode.cn/problems/power-of-three/) - 标签:递归、数学 - 难度:简单 ## 题目链接 - [0326. 3 的幂 - 力扣](https://leetcode.cn/problems/power-of-three/) ## 题目大意 给定一个整数 n,判断 n 是否是 3 的幂次方。$-2^{31} \le n \le 2^{31}-1$ ## 解题思路 首先排除负数,因为 3 的幂次方不可能为负数。 因为 n 的最大值为 $2^{31}-1$。计算出在 n 的范围内,3 的幂次方最大为 $3^{19} = 1162261467$。 3 为质数,则 $3^{19}$ 的除数只有 $3^0, 3^1, …, 3^{19}$。所以如果 n 为 3 的幂次方,则 n 肯定能被 $3^{19}$ 整除,直接判断即可。 ## 代码 ```python class Solution: def isPowerOfThree(self, n: int) -> bool: if n <= 0: return False if (3 ** 19) % n == 0: return True return False ``` ================================================ FILE: docs/solutions/0300-0399/random-pick-index.md ================================================ # [0398. 随机数索引](https://leetcode.cn/problems/random-pick-index/) - 标签:水塘抽样、哈希表、数学、随机化 - 难度:中等 ## 题目链接 - [0398. 随机数索引 - 力扣](https://leetcode.cn/problems/random-pick-index/) ## 题目大意 **描述**: 给定一个可能含有「重复元素」的整数数组 $nums$。 **要求**: 请你随机输出给定的目标数字 $target$ 的索引。你可以假设给定的数字一定存在于数组中。 实现 `Solution` 类: - `Solution(int[] nums)` 用数组 $nums$ 初始化对象。 - `int pick(int target)` 从 $nums$ 中选出一个满足 $nums[i] == target$ 的随机索引 $i$。如果存在多个有效的索引,则每个索引的返回概率应当相等。 **说明**: - $1 \le nums.length \le 2 \times 10^{4}$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 - $target$ 是 $nums$ 中的一个整数。 - 最多调用 $pick$ 函数 $10^{4}$ 次。 **示例**: - 示例 1: ```python 输入 ["Solution", "pick", "pick", "pick"] [[[1, 2, 3, 3, 3]], [3], [1], [3]] 输出 [null, 4, 0, 2] 解释 Solution solution = new Solution([1, 2, 3, 3, 3]); solution.pick(3); // 随机返回索引 2, 3 或者 4 之一。每个索引的返回概率应该相等。 solution.pick(1); // 返回 0 。因为只有 nums[0] 等于 1 。 solution.pick(3); // 随机返回索引 2, 3 或者 4 之一。每个索引的返回概率应该相等。 ``` ## 解题思路 ### 思路 1:哈希表预处理 **算法思路**: 由于题目中 `pick` 函数会被频繁调用,如果每次调用都遍历整个数组,时间复杂度为 $O(n)$,在大量调用时会导致超时。我们可以使用哈希表预处理的方法来优化: 1. **预处理阶段**:在构造函数中,遍历数组 $nums$,将每个值对应的所有索引存储在哈希表中。 2. **查询阶段**:在 `pick` 函数中,直接从哈希表中获取目标值的所有索引,然后随机选择一个。 **具体实现**: 1. **初始化**: - 创建哈希表 $index\_map$,键为数组中的值,值为该值对应的所有索引列表。 - 遍历数组 $nums$,将每个 $nums[i]$ 的索引 $i$ 添加到对应的列表中。 2. **随机选择**: - 从哈希表中获取 $target$ 对应的索引列表。 - 使用 `random.choice()` 从列表中随机选择一个索引返回。 ### 思路 1:代码 ```python import random from typing import List from collections import defaultdict class Solution: def __init__(self, nums: List[int]): """预处理:构建值到索引列表的映射""" self.index_map = defaultdict(list) for i, num in enumerate(nums): self.index_map[num].append(i) def pick(self, target: int) -> int: """从预处理的索引列表中随机选择一个""" return random.choice(self.index_map[target]) # Your Solution object will be instantiated and called as such: # obj = Solution(nums) # param_1 = obj.pick(target) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - 构造函数:$O(n)$,其中 $n$ 是数组的长度。需要遍历数组一次构建哈希表。 - `pick` 函数:$O(1)$,直接从哈希表中获取索引列表并随机选择。 - **空间复杂度**:$O(n)$,其中 $n$ 是数组的长度。哈希表最多存储 $n$ 个索引。 ================================================ FILE: docs/solutions/0300-0399/range-addition.md ================================================ # [0370. 区间加法](https://leetcode.cn/problems/range-addition/) - 标签:数组、前缀和 - 难度:中等 ## 题目链接 - [0370. 区间加法 - 力扣](https://leetcode.cn/problems/range-addition/) ## 题目大意 **描述**:给定一个数组的长度 `length` ,初始情况下数组中所有数字均为 `0`。再给定 `k` 个更新操作。其中每个操作是一个三元组 `[startIndex, endIndex, inc]`,表示将子数组 `nums[startIndex ... endIndex]` (包括 `startIndex`、`endIndex`)上所有元素增加 `inc`。 **要求**:返回 `k` 次操作后的数组。 **示例**: - 示例 1: ``` 给定 length = 5,即 nums = [0, 0, 0, 0, 0] 操作 [1, 3, 2] -> [0, 2, 2, 2, 0] 操作 [2, 4, 3] -> [0, 2, 5, 5, 3] 操作 [0, 2, -2] -> [-2, 0, 3, 5, 3] ``` ## 解题思路 ### 思路 1:线段树 - 初始化一个长度为 `length`,值全为 `0` 的 `nums` 数组。 - 然后根据 `nums` 数组构建一棵线段树。每个线段树的节点类存储当前区间的左右边界和该区间的和。并且线段树使用延迟标记。 - 然后遍历三元组操作,进行区间累加运算。 - 最后从线段树中查询数组所有元素,返回该数组即可。 这样构建线段树的时间复杂度为 $O(\log n)$,单次区间更新的时间复杂度为 $O(\log n)$,单次区间查询的时间复杂度为 $O(\log n)$。总体时间复杂度为 $O(\log n)$。 ### 思路 1:线段树代码 ```python # 线段树的节点类 class SegTreeNode: def __init__(self, val=0): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值) self.lazy_tag = None # 区间和问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, nums, function): self.size = len(nums) self.tree = [SegTreeNode() for _ in range(4 * self.size)] # 维护 SegTreeNode 数组 self.nums = nums # 原始数据 self.function = function # function 是一个函数,左右区间的聚合方法 if self.size > 0: self.__build(0, 0, self.size - 1) # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.nums[i] = val self.__update_point(i, val, 0) # 区间更新接口:将区间为 [q_left, q_right] 上的所有元素值加上 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, 0) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, 0) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self): for i in range(self.size): self.nums[i] = self.query_interval(i, i) return self.nums # 以下为内部实现方法 # 构建线段树实现方法:节点的存储下标为 index,节点的区间为 [left, right] def __build(self, index, left, right): self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,节点值为对应位置的元素值 self.tree[index].val = self.nums[left] return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.__build(left_index, left, mid) # 递归创建左子树 self.__build(right_index, mid + 1, right) # 递归创建右子树 self.__pushup(index) # 向上更新节点的区间值 # 单点更新实现方法:将 nums[i] 更改为 val,节点的存储下标为 index def __update_point(self, i, val, index): left = self.tree[index].left right = self.tree[index].right if left == right: self.tree[index].val = val # 叶子节点,节点值修改为 val return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if i <= mid: # 在左子树中更新节点值 self.__update_point(i, val, left_index) else: # 在右子树中更新节点值 self.__update_point(i, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间更新实现方法 def __update_interval(self, q_left, q_right, val, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 if self.tree[index].lazy_tag is not None: self.tree[index].lazy_tag += val # 将当前节点的延迟标记增加 val else: self.tree[index].lazy_tag = val # 将当前节点的延迟标记增加 val interval_size = (right - left + 1) # 当前节点所在区间大小 self.tree[index].val += val * interval_size # 当前节点所在区间每个元素值增加 val return if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return self.__pushdown(index) # 向下更新节点的区间值 mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if q_left <= mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, left_index) if q_right > mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间查询实现方法:在线段树中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return self.tree[index].val # 直接返回节点值 if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(index) mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, left_index) if q_right > mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, right_index) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:更新下标为 index 的节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, index): left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.tree[index].val = self.function(self.tree[left_index].val, self.tree[right_index].val) # 向下更新实现方法:更新下标为 index 的节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, index): lazy_tag = self.tree[index].lazy_tag if lazy_tag is None: return left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if self.tree[left_index].lazy_tag is not None: self.tree[left_index].lazy_tag += lazy_tag # 更新左子节点懒惰标记 else: self.tree[left_index].lazy_tag = lazy_tag left_size = (self.tree[left_index].right - self.tree[left_index].left + 1) self.tree[left_index].val += lazy_tag * left_size # 左子节点每个元素值增加 lazy_tag if self.tree[right_index].lazy_tag is not None: self.tree[right_index].lazy_tag += lazy_tag # 更新右子节点懒惰标记 else: self.tree[right_index].lazy_tag = lazy_tag right_size = (self.tree[right_index].right - self.tree[right_index].left + 1) self.tree[right_index].val += lazy_tag * right_size # 右子节点每个元素值增加 lazy_tag self.tree[index].lazy_tag = None # 更新当前节点的懒惰标记 class Solution: def getModifiedArray(self, length: int, updates: List[List[int]]) -> List[int]: nums = [0 for _ in range(length)] self.ST = SegmentTree(nums, lambda x, y: x + y) for update in updates: self.ST.update_interval(update[0], update[1], update[2]) return self.ST.get_nums() ``` ================================================ FILE: docs/solutions/0300-0399/range-sum-query-2d-immutable.md ================================================ # [0304. 二维区域和检索 - 矩阵不可变](https://leetcode.cn/problems/range-sum-query-2d-immutable/) - 标签:设计、数组、矩阵、前缀和 - 难度:中等 ## 题目链接 - [0304. 二维区域和检索 - 矩阵不可变 - 力扣](https://leetcode.cn/problems/range-sum-query-2d-immutable/) ## 题目大意 给定一个二维矩阵 `matrix`。 要求:满足以下多个请求: - ` def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:`计算以 `(row1, col1)` 为左上角、`(row2, col2)` 为右下角的子矩阵中各个元素的和。 - `def __init__(self, matrix: List[List[int]]):` 对二维矩阵 `matrix` 进行初始化操作。 ## 解题思路 在进行初始化的时候做预处理,这样在多次查询时可以减少重复计算,也可以减少时间复杂度。 在进行初始化的时候,使用一个二维数组 `pre_sum` 记录下以 `(0, 0)` 为左上角,以当前 `(row, col)` 为右下角的子数组各个元素和,即 `pre_sum[row + 1][col + 1]`。 则在查询时,以 `(row1, col1)` 为左上角、`(row2, col2)` 为右下角的子矩阵中各个元素的和就等于以 `(0, 0)` 到 `(row2, col2)` 的大子矩阵减去左边 `(0, 0)` 到 `(row2, col1 - 1)`的子矩阵,再减去上边 `(0, 0)` 到 `(row1 - 1, col2)` 的子矩阵,再加上左上角 `(0, 0)` 到 `(row1 - 1, col1 - 1)` 的子矩阵(因为之前重复减了)。即 `pre_sum[row2 + 1][col2 + 1] - self.pre_sum[row2 + 1][col1] - self.pre_sum[row1][col2 + 1] + self.pre_sum[row1][col1]`。 ## 代码 ```python class NumMatrix: def __init__(self, matrix: List[List[int]]): rows = len(matrix) cols = len(matrix[0]) self.pre_sum = [[0 for _ in range(cols + 1)] for _ in range(rows + 1)] for row in range(rows): for col in range(cols): self.pre_sum[row + 1][col + 1] = self.pre_sum[row + 1][col] + self.pre_sum[row][col + 1] - self.pre_sum[row][col] + matrix[row][col] def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int: return self.pre_sum[row2 + 1][col2 + 1] - self.pre_sum[row2 + 1][col1] - self.pre_sum[row1][col2 + 1] + self.pre_sum[row1][col1] ``` ================================================ FILE: docs/solutions/0300-0399/range-sum-query-2d-mutable.md ================================================ # [0308. 二维区域和检索 - 矩阵可修改](https://leetcode.cn/problems/range-sum-query-2d-mutable/) - 标签:设计、树状数组、线段树、数组、矩阵 - 难度:中等 ## 题目链接 - [0308. 二维区域和检索 - 矩阵可修改 - 力扣](https://leetcode.cn/problems/range-sum-query-2d-mutable/) ## 题目大意 **描述**: 给定一个二维矩阵 $matrix$。 **要求**: 处理以下类型的多个查询: 1. 更新 $matrix$ 中单元格的值。 2. 计算由 左上角 $(row1, col1)$ 和 右下角 $(row2, col2)$ 定义的 $matrix$ 内矩阵元素的和。 实现 `NumMatrix` 类: - `NumMatrix(int[][] matrix)` 用整数矩阵 $matrix$ 初始化对象。 - `void update(int row, int col, int val)` 更新 $matrix[row][col]$ 的值到 $val$ 。 - `int sumRegion(int row1, int col1, int row2, int col2)` 返回矩阵 $matrix$ 中指定矩形区域元素的和,该区域由左上角 $(row1, col1)$ 和 右下角 $(row2, col2)$ 界定。 **说明**: - $m == matrix.length$。 - $n == matrix[i].length$。 - $1 \le m, n \le 200$。 - $-10^{3} \le matrix[i][j] \le 10^{3}$。 - $0 \le row \lt m$。 - $0 \le col \lt n$。 - $-10^{3} \le val \le 10^{3}$。 - $0 \le row1 \le row2 \lt m$。 - $0 \le col1 \le col2 \lt n$。 - 最多调用 $5000$ 次 `sumRegion` 和 `update` 方法。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/14/summut-grid.jpg) ```python 输入 ["NumMatrix", "sumRegion", "update", "sumRegion"] [[[[3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5]]], [2, 1, 4, 3], [3, 2, 2], [2, 1, 4, 3]] 输出 [null, 8, null, 10] 解释 NumMatrix numMatrix = new NumMatrix([[3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5]]); numMatrix.sumRegion(2, 1, 4, 3); // 返回 8 (即, 左侧红色矩形的和) numMatrix.update(3, 2, 2); // 矩阵从左图变为右图 numMatrix.sumRegion(2, 1, 4, 3); // 返回 10 (即,右侧红色矩形的和) ``` ## 解题思路 ### 思路 1:二维前缀和数组 **算法思路**: 使用二维前缀和数组来快速计算矩形区域的和。二维前缀和数组 $prefix[i][j]$ 表示从 $(0, 0)$ 到 $(i-1, j-1)$ 的矩形区域内所有元素的和。 对于查询矩形区域 $(row1, col1)$ 到 $(row2, col2)$ 的和,可以使用容斥原理: $$sum = prefix[row2+1][col2+1] - prefix[row1][col2+1] - prefix[row2+1][col1] + prefix[row1][col1]$$ **具体实现**: 1. **初始化**: - 创建二维前缀和数组 $prefix$,大小为 $(m+1) \times (n+1)$,其中 $m$ 和 $n$ 分别是矩阵的行数和列数。 - 计算前缀和:$prefix[i][j] = prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1] + matrix[i-1][j-1]$ 2. **更新操作**: - 计算差值 $diff = val - matrix[row][col]$ - 更新原矩阵:$matrix[row][col] = val$ - 更新前缀和数组:从 $(row+1, col+1)$ 开始,所有受影响的区域都需要加上 $diff$ 3. **查询操作**: - 使用容斥原理计算指定矩形区域的和 ### 思路 1:代码 ```python from typing import List class NumMatrix: def __init__(self, matrix: List[List[int]]): """初始化二维前缀和数组""" self.matrix = matrix self.m, self.n = len(matrix), len(matrix[0]) # 创建前缀和数组,大小为 (m+1) x (n+1) self.prefix = [[0] * (self.n + 1) for _ in range(self.m + 1)] # 计算前缀和 for i in range(1, self.m + 1): for j in range(1, self.n + 1): self.prefix[i][j] = (self.prefix[i-1][j] + self.prefix[i][j-1] - self.prefix[i-1][j-1] + matrix[i-1][j-1]) def update(self, row: int, col: int, val: int) -> None: """更新矩阵元素并维护前缀和数组""" # 计算差值 diff = val - self.matrix[row][col] self.matrix[row][col] = val # 更新前缀和数组 for i in range(row + 1, self.m + 1): for j in range(col + 1, self.n + 1): self.prefix[i][j] += diff def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int: """使用容斥原理计算矩形区域和""" return (self.prefix[row2+1][col2+1] - self.prefix[row1][col2+1] - self.prefix[row2+1][col1] + self.prefix[row1][col1]) # Your NumMatrix object will be instantiated and called as such: # obj = NumMatrix(matrix) # obj.update(row,col,val) # param_2 = obj.sumRegion(row1,col1,row2,col2) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - 构造函数:$O(m \times n)$,其中 $m$ 和 $n$ 分别是矩阵的行数和列数。需要计算整个前缀和数组。 - `update` 函数:$O(m \times n)$,最坏情况下需要更新整个前缀和数组。 - `sumRegion` 函数:$O(1)$,直接通过前缀和数组计算。 - **空间复杂度**:$O(m \times n)$,需要存储前缀和数组。 ================================================ FILE: docs/solutions/0300-0399/range-sum-query-immutable.md ================================================ # [0303. 区域和检索 - 数组不可变](https://leetcode.cn/problems/range-sum-query-immutable/) - 标签:设计、数组、前缀和 - 难度:简单 ## 题目链接 - [0303. 区域和检索 - 数组不可变 - 力扣](https://leetcode.cn/problems/range-sum-query-immutable/) ## 题目大意 **描述**:给定一个整数数组 `nums`。 **要求**:实现 `NumArray` 类,该类能处理区间为 `[left, right]` 之间的区间求和的多次查询。 `NumArray` 类: - `NumArray(int[] nums)` 使用数组 `nums` 初始化对象。 - `int sumRange(int i, int j)` 返回数组 `nums` 中索引 `left` 和 `right` 之间的元素的 总和 ,包含 `left` 和 `right` 两点(也就是 `nums[left] + nums[left + 1] + ... + nums[right]`)。 **说明**: - $1 \le nums.length \le 10^4$。 - $-10^5 \le nums[i] \le 10^5$。 - $0 \le left \le right < nums.length$。 - `sumRange` 方法调用次数不超过 $10^4$ 次。 **示例**: - 示例 1: ```python 给定 nums = [-2, 0, 3, -5, 2, -1] 求和 sumRange(0, 2) -> 1 求和 sumRange(2, 5) -> -1 求和 sumRange(0, 5) -> -3 ``` ## 解题思路 ### 思路 1:线段树 - 根据 `nums` 数组,构建一棵线段树。每个线段树的节点类存储当前区间的左右边界和该区间的和。 这样构建线段树的时间复杂度为 $O(\log n)$,单次区间查询的时间复杂度为 $O(\log n)$。总体时间复杂度为 $O(\log n)$。 ### 思路 1 线段树代码: ```python # 线段树的节点类 class SegTreeNode: def __init__(self, val=0): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值) # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, nums, function): self.size = len(nums) self.tree = [SegTreeNode() for _ in range(4 * self.size)] # 维护 SegTreeNode 数组 self.nums = nums # 原始数据 self.function = function # function 是一个函数,左右区间的聚合方法 if self.size > 0: self.__build(0, 0, self.size - 1) # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.nums[i] = val self.__update_point(i, val, 0) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, 0) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self): for i in range(self.size): self.nums[i] = self.query_interval(i, i) return self.nums # 以下为内部实现方法 # 构建线段树实现方法:节点的存储下标为 index,节点的区间为 [left, right] def __build(self, index, left, right): self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,节点值为对应位置的元素值 self.tree[index].val = self.nums[left] return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.__build(left_index, left, mid) # 递归创建左子树 self.__build(right_index, mid + 1, right) # 递归创建右子树 self.__pushup(index) # 向上更新节点的区间值 # 单点更新实现方法:将 nums[i] 更改为 val。节点的存储下标为 index,节点的区间为 [left, right] def __update_point(self, i, val, index): left = self.tree[index].left right = self.tree[index].right if left == right: self.tree[index].val = val # 叶子节点,节点值修改为 val return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if i <= mid: # 在左子树中更新节点值 self.__update_point(i, val, left_index) else: # 在右子树中更新节点值 self.__update_point(i, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间查询实现方法:在线段树的 [left, right] 区间范围中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return self.tree[index].val # 直接返回节点值 if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, left_index) if q_right > mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, right_index) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:下标为 index 的节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, index): left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.tree[index].val = self.function(self.tree[left_index].val, self.tree[right_index].val) class NumArray: def __init__(self, nums: List[int]): self.STree = SegmentTree(nums, lambda x, y: x + y) def sumRange(self, left: int, right: int) -> int: return self.STree.query_interval(left, right) ``` ================================================ FILE: docs/solutions/0300-0399/range-sum-query-mutable.md ================================================ # [0307. 区域和检索 - 数组可修改](https://leetcode.cn/problems/range-sum-query-mutable/) - 标签:设计、树状数组、线段树、数组 - 难度:中等 ## 题目链接 - [0307. 区域和检索 - 数组可修改 - 力扣](https://leetcode.cn/problems/range-sum-query-mutable/) ## 题目大意 **描述**:给定一个数组 `nums`。 **要求**: 1. 完成两类查询: 1. 要求将数组元素 `nums[index]` 的值更新为 `val`。 2. 要求返回数组 `nums` 中区间 `[left, right]` 之间(包含 `left`、`right`)的 `nums` 元素的和。其中 $left \le right$。 2. 实现 `NumArray` 类: 1. `NumArray(int[] nums)` 用整数数组 `nums` 初始化对象。 2. `void update(int index, int val)` 将 `nums[index]` 的值更新为 `val`。 3. `int sumRange(int left, int right)` 返回数组 `nums` 中索引 `left` 和索引 `right` 之间( 包含 )的 `nums` 元素的和(即 `nums[left] + nums[left + 1], ..., nums[right]`)。 **说明**: - $1 \le nums.length \le 3 * 10^4$。 - $-100 \le nums[i] \le 100$。 - $0 <= index < num.length$。 - $0 \le left \le right < nums.length$。 - 调用 `update` 和 `sumRange` 的方法次数不大于 $3 * 10^4$ 次。 **示例**: - 示例 1: ``` 给定 nums = [1, 3, 5] 求和 sumRange(0, 2) -> 9 更新 update(1, 2) 求和 sumRange(0, 2) -> 8 ``` ## 解题思路 ### 思路 1:线段树 根据 `nums` 数组,构建一棵线段树。每个线段树的节点类存储当前区间的左右边界和该区间的和。 这样构建线段树的时间复杂度为 $O(\log n)$,每次单点更新的时间复杂度为 $O(\log n)$,每次区间查询的时间复杂度为 $O(\log n)$。总体时间复杂度为 $O(\log n)$。 ### 思路 1 线段树代码: ```python # 线段树的节点类 class SegTreeNode: def __init__(self, val=0): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值) # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, nums, function): self.size = len(nums) self.tree = [SegTreeNode() for _ in range(4 * self.size)] # 维护 SegTreeNode 数组 self.nums = nums # 原始数据 self.function = function # function 是一个函数,左右区间的聚合方法 if self.size > 0: self.__build(0, 0, self.size - 1) # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.nums[i] = val self.__update_point(i, val, 0) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, 0) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self): for i in range(self.size): self.nums[i] = self.query_interval(i, i) return self.nums # 以下为内部实现方法 # 构建线段树实现方法:节点的存储下标为 index,节点的区间为 [left, right] def __build(self, index, left, right): self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,节点值为对应位置的元素值 self.tree[index].val = self.nums[left] return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.__build(left_index, left, mid) # 递归创建左子树 self.__build(right_index, mid + 1, right) # 递归创建右子树 self.__pushup(index) # 向上更新节点的区间值 # 单点更新实现方法:将 nums[i] 更改为 val。节点的存储下标为 index,节点的区间为 [left, right] def __update_point(self, i, val, index): left = self.tree[index].left right = self.tree[index].right if left == right: self.tree[index].val = val # 叶子节点,节点值修改为 val return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if i <= mid: # 在左子树中更新节点值 self.__update_point(i, val, left_index) else: # 在右子树中更新节点值 self.__update_point(i, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间查询实现方法:在线段树的 [left, right] 区间范围中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return self.tree[index].val # 直接返回节点值 if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, left_index) if q_right > mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, right_index) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:下标为 index 的节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, index): left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.tree[index].val = self.function(self.tree[left_index].val, self.tree[right_index].val) class NumArray: def __init__(self, nums: List[int]): self.STree = SegmentTree(nums, lambda x, y: x + y) def update(self, index: int, val: int) -> None: self.STree.update_point(index, val) def sumRange(self, left: int, right: int) -> int: return self.STree.query_interval(left, right) ``` ================================================ FILE: docs/solutions/0300-0399/ransom-note.md ================================================ # [0383. 赎金信](https://leetcode.cn/problems/ransom-note/) - 标签:哈希表、字符串、计数 - 难度:简单 ## 题目链接 - [0383. 赎金信 - 力扣](https://leetcode.cn/problems/ransom-note/) ## 题目大意 **描述**:为了不在赎金信中暴露字迹,从杂志上搜索各个需要的字母,组成单词来表达意思。 给定一个赎金信字符串 $ransomNote$ 和一个杂志字符串 $magazine$。 **要求**:判断 $ransomNote$ 能不能由 $magazines$ 里面的字符构成。如果可以构成,返回 `True`;否则返回 `False`。 **说明**: - $magazine$ 中的每个字符只能在 $ransomNote$ 中使用一次。 - $1 \le ransomNote.length, magazine.length \le 10^5$。 - $ransomNote$ 和 $magazine$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:ransomNote = "a", magazine = "b" 输出:False ``` - 示例 2: ```python 输入:ransomNote = "aa", magazine = "ab" 输出:False ``` ## 解题思路 ### 思路 1:哈希表 暴力做法是双重循环遍历字符串 $ransomNote$ 和 $magazines$。我们可以用哈希表来减少算法的时间复杂度。具体做法如下: - 先用哈希表存储 $magazines$ 中各个字符的个数(哈希表可用字典或数组实现)。 - 再遍历字符串 $ransomNote$ 中每个字符,对于每个字符: - 如果在哈希表中个数为 $0$,直接返回 `False`。 - 如果在哈希表中个数不为 $0$,将其个数减 $1$。 - 遍历到最后,则说明 $ransomNote$ 能由 $magazines$ 里面的字符构成。返回 `True`。 ### 思路 1:代码 ```python class Solution: def canConstruct(self, ransomNote: str, magazine: str) -> bool: magazine_counts = [0 for _ in range(26)] for ch in magazine: num = ord(ch) - ord('a') magazine_counts[num] += 1 for ch in ransomNote: num = ord(ch) - ord('a') if magazine_counts[num] == 0: return False else: magazine_counts[num] -= 1 return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n)$,其中 $m$ 是字符串 $ransomNote$ 的长度,$n$ 是字符串 $magazines$ 的长度。 - **空间复杂度**:$O(|S|)$,其中 $S$ 是字符集,本题中 $|S| = 26$。 ================================================ FILE: docs/solutions/0300-0399/rearrange-string-k-distance-apart.md ================================================ # [0358. K 距离间隔重排字符串](https://leetcode.cn/problems/rearrange-string-k-distance-apart/) - 标签:贪心、哈希表、字符串、计数、排序、堆(优先队列) - 难度:困难 ## 题目链接 - [0358. K 距离间隔重排字符串 - 力扣](https://leetcode.cn/problems/rearrange-string-k-distance-apart/) ## 题目大意 **描述**: 给定一个非空的字符串 $s$ 和一个整数 $k$。 **要求**: 你要将这个字符串 $s$ 中的字母进行重新排列,使得重排后的字符串中相同字母的位置间隔距离「至少」为 $k$。如果无法做到,请返回一个空字符串 `""`。 **说明**: - $1 \le s.length \le 3 \times 10^{5}$。 - $s$ 仅由小写英文字母组成。 - $0 \le k \le s.length$。 **示例**: - 示例 1: ```python 输入: s = "aabbcc", k = 3 输出: "abcabc" 解释: 相同的字母在新的字符串中间隔至少 3 个单位距离。 ``` - 示例 2: ```python 输入: s = "aaabc", k = 3 输出: "" 解释: 没有办法找到可能的重排结果。 ``` ## 解题思路 ### 思路 1:贪心算法 + 优先队列 这道题的核心思想是贪心策略:每次选择剩余字符中频率最高的字符,但要确保它与之前放置的字符保持至少 $k$ 的距离。 ###### 1. 问题分析 - 统计每个字符的出现频率 $freq[c]$。 - 使用优先队列(最大堆)维护字符频率,优先选择频率高的字符。 - 维护一个队列记录最近使用的字符,确保距离约束。 ###### 2. 算法步骤 1. **统计频率**:遍历字符串 $s$,统计每个字符 $c$ 的出现次数 $freq[c]$。 2. **构建优先队列**:将所有字符按频率降序放入优先队列。 3. **贪心放置**: - 每次从优先队列中取出频率最高的字符。 - 如果该字符与最近 $k-1$ 个位置上的字符不同,则放置。 - 将使用过的字符放入等待队列,当距离满足条件时重新加入优先队列。 4. **检查可行性**:如果无法放置所有字符,返回空字符串。 ###### 3. 关键变量 - $freq[c]$:字符 $c$ 的出现频率。 - $wait\_queue$:等待队列,存储最近使用的字符。 - $result$:结果字符串。 - $used\_chars$:最近使用的字符集合。 ### 思路 1:代码 ```python import heapq from collections import Counter, deque class Solution: def rearrangeString(self, s: str, k: int) -> str: # 特殊情况:k <= 1 时不需要间隔 if k <= 1: return s # 统计字符频率 freq = Counter(s) # 构建最大堆(使用负数实现) max_heap = [] for char, count in freq.items(): heapq.heappush(max_heap, (-count, char)) # 等待队列,存储最近使用的字符 wait_queue = deque() result = [] while max_heap: # 取出频率最高的字符 neg_count, char = heapq.heappop(max_heap) count = -neg_count # 将字符添加到结果中 result.append(char) # 将使用过的字符加入等待队列 wait_queue.append((count - 1, char)) # 如果等待队列长度达到 k-1,说明可以重新使用最早使用的字符 if len(wait_queue) >= k: old_count, old_char = wait_queue.popleft() # 如果该字符还有剩余,重新加入优先队列 if old_count > 0: heapq.heappush(max_heap, (-old_count, old_char)) # 检查是否所有字符都被使用 if len(result) == len(s): return ''.join(result) else: return "" ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log m)$,其中 $n$ 为字符串长度,$m$ 为不同字符的数量。每个字符最多被处理 $O(n)$ 次,每次堆操作的时间复杂度为 $O(\log m)$。 - **空间复杂度**:$O(m)$,其中 $m$ 为不同字符的数量。用于存储字符频率、优先队列和等待队列。 ================================================ FILE: docs/solutions/0300-0399/reconstruct-itinerary.md ================================================ # [0332. 重新安排行程](https://leetcode.cn/problems/reconstruct-itinerary/) - 标签:深度优先搜索、图、欧拉回路 - 难度:困难 ## 题目链接 - [0332. 重新安排行程 - 力扣](https://leetcode.cn/problems/reconstruct-itinerary/) ## 题目大意 **描述**: 给定一份航线列表 $tickets$ ,其中 $tickets[i] = [from_i, to_i]$ 表示飞机出发和降落的机场地点。 **要求**: 请你对该行程进行重新规划排序。 所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。 - 例如,行程 `["JFK", "LGA"]` 与 `["JFK", "LGB"]` 相比就更小,排序更靠前。 假定所有机票至少存在一种合理的行程。且所有的机票「必须都用一次」且「只能用一次」。 **说明**: - $1 \le tickets.length \le 300$。 - $tickets[i].length == 2$。 - $from_i.length == 3$。 - $to_i.length == 3$。 - $from_i$ 和 $to_i$ 由大写英文字母组成。 - $from_i \ne to_i$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/14/itinerary1-graph.jpg) ```python 输入:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]] 输出:["JFK","MUC","LHR","SFO","SJC"] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/14/itinerary2-graph.jpg) ```python 输入:tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]] 输出:["JFK","ATL","JFK","SFO","ATL","SFO"] 解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"] ,但是它字典排序更大更靠后。 ``` ## 解题思路 ### 思路 1:深度优先搜索 + 欧拉回路 这道题本质上是寻找欧拉回路的问题。给定一个有向图,要求找到一条从 JFK 开始的路径,使得每条边都被恰好访问一次。 **1. 问题分析** - 将每个机场看作图中的节点,每张机票看作图中的有向边。 - 需要找到一条欧拉路径(从 JFK 开始,经过所有边恰好一次)。 - 当存在多条路径时,选择字典序最小的路径。 **2. 算法思路** 使用深度优先搜索(DFS)来构建欧拉路径: - 构建邻接表 $graph$,其中 $graph[from]$ 存储从 $from$ 出发可以到达的所有机场。 - 对每个机场的邻接列表进行排序,确保优先选择字典序较小的机场。 - 使用 DFS 遍历图,每次选择字典序最小的未访问边。 - 使用栈来记录访问路径,当无法继续前进时,将当前节点加入结果路径。 **3. 关键步骤** 1. 构建邻接表:$graph[from_i] = [to_1, to_2, ..., to_k]$,并按字典序排序。 2. 从 JFK 开始 DFS 遍历。 3. 对于当前节点 $current$,依次访问其邻接节点中字典序最小的未访问节点。 4. 当无法继续前进时,将 $current$ 加入结果路径。 5. 最终将结果路径反转得到正确的行程。 ### 思路 1:代码 ```python class Solution: def findItinerary(self, tickets: List[List[str]]) -> List[str]: # 构建邻接表 graph = {} for from_airport, to_airport in tickets: if from_airport not in graph: graph[from_airport] = [] graph[from_airport].append(to_airport) # 对每个机场的邻接列表进行排序,确保字典序最小 for airport in graph: graph[airport].sort() # 使用栈进行 DFS 遍历 stack = ["JFK"] # 从 JFK 开始 result = [] # 存储最终结果 while stack: current = stack[-1] # 如果当前机场还有未访问的邻接机场 if current in graph and graph[current]: # 选择字典序最小的邻接机场 next_airport = graph[current].pop(0) stack.append(next_airport) else: # 当前机场没有未访问的邻接机场,将其加入结果 result.append(stack.pop()) # 反转结果得到正确的行程顺序 return result[::-1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(E \log E)$,其中 $E$ 为机票数量。构建邻接表需要 $O(E)$ 时间,对每个机场的邻接列表排序需要 $O(E \log E)$ 时间,DFS 遍历需要 $O(E)$ 时间。 - **空间复杂度**:$O(E)$,用于存储邻接表和递归栈空间。 ================================================ FILE: docs/solutions/0300-0399/remove-duplicate-letters.md ================================================ # [0316. 去除重复字母](https://leetcode.cn/problems/remove-duplicate-letters/) - 标签:栈、贪心、字符串、单调栈 - 难度:中等 ## 题目链接 - [0316. 去除重复字母 - 力扣](https://leetcode.cn/problems/remove-duplicate-letters/) ## 题目大意 **描述**:给定一个字符串 `s`。 **要求**:去除字符串中重复的字母,使得每个字母只出现一次。需要保证 **「返回结果的字典序最小(要求不能打乱其他字符的相对位置)」**。 **说明**: - $1 \le s.length \le 10^4$。 - `s` 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "bcabc" 输出:"abc" ``` - 示例 2: ```python 输入:s = "cbacdcbc" 输出:"acdb" ``` ## 解题思路 ### 思路 1:哈希表 + 单调栈 针对题目的三个要求:去重、不能打乱其他字符顺序、字典序最小。我们来一一分析。 1. **去重**:可以通过 **「使用哈希表存储字母出现次数」** 的方式,将每个字母出现的次数统计起来,再遍历一遍,去除重复的字母。 2. **不能打乱其他字符顺序**:按顺序遍历,将非重复的字母存储到答案数组或者栈中,最后再拼接起来,就能保证不打乱其他字符顺序。 3. **字典序最小**:意味着字典序小的字母应该尽可能放在前面。 1. 对于第 `i` 个字符 `s[i]` 而言,如果第 `0` ~ `i - 1` 之间的某个字符 `s[j]` 在 `s[i]` 之后不再出现了,那么 `s[j]` 必须放到 `s[i]` 之前。 2. 而如果 `s[j]` 在之后还会出现,并且 `s[j]` 的字典序大于 `s[i]`,我们则可以先舍弃 `s[j]`,把 `s[i]` 尽可能的放到前面。后边再考虑使用 `s[j]` 所对应的字符。 要满足第 3 条需求,我们可以使用 **「单调栈」** 来解决。我们使用单调栈存储 `s[i]` 之前出现的非重复、并且字典序最小的字符序列。整个算法步骤如下: 1. 先遍历一遍字符串,用哈希表 `letter_counts` 统计出每个字母出现的次数。 2. 然后使用单调递减栈保存当前字符之前出现的非重复、并且字典序最小的字符序列。 3. 当遍历到 `s[i]` 时,如果 `s[i]` 没有在栈中出现过: 1. 比较 `s[i]` 和栈顶元素 `stack[-1]` 的字典序。如果 `s[i]` 的字典序小于栈顶元素 `stack[-1]`,并且栈顶元素之后的出现次数大于 `0`,则将栈顶元素弹出。 2. 然后继续判断 `s[i]` 和栈顶元素 `stack[-1]`,并且知道栈顶元素出现次数为 `0` 时停止弹出。此时将 `s[i]` 添加到单调栈中。 4. 从哈希表 `letter_counts` 中减去 `s[i]` 出现的次数,继续遍历。 5. 最后将单调栈中的字符依次拼接为答案字符串,并返回。 ### 思路 1:代码 ```python class Solution: def removeDuplicateLetters(self, s: str) -> str: stack = [] letter_counts = dict() for ch in s: if ch in letter_counts: letter_counts[ch] += 1 else: letter_counts[ch] = 1 for ch in s: if ch not in stack: while stack and ch < stack[-1] and letter_counts[stack[-1]] > 0: stack.pop() stack.append(ch) letter_counts[ch] -= 1 return ''.join(stack) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(|\sum|)$,其中 $\sum$ 为字符集合,$|\sum|$ 为字符种类个数。由于栈中字符不能重复,因此栈中最多有 $|\sum|$ 个字符。 ## 参考资料 - 【题解】[去除重复数组 - 去除重复字母 - 力扣(LeetCode)](https://leetcode.cn/problems/remove-duplicate-letters/solution/qu-chu-zhong-fu-shu-zu-by-lu-shi-zhe-sokp/) ================================================ FILE: docs/solutions/0300-0399/remove-invalid-parentheses.md ================================================ # [0301. 删除无效的括号](https://leetcode.cn/problems/remove-invalid-parentheses/) - 标签:广度优先搜索、字符串、回溯 - 难度:困难 ## 题目链接 - [0301. 删除无效的括号 - 力扣](https://leetcode.cn/problems/remove-invalid-parentheses/) ## 题目大意 **描述**: 给定一个由若干括号和字母组成的字符串 $s$,删除最小数量的无效括号,使得输入的字符串有效。 **要求**: 返回所有可能的结果。答案可以按任意顺序返回。 **说明**: - $1 \le s.length \le 25$。 - $s$ 由小写英文字母以及括号 `'('` 和 `')'` 组成。 - $s$ 中至多含 $20$ 个括号。 **示例**: - 示例 1: ```python 输入:s = "()())()" 输出:["(())()","()()()"] ``` - 示例 2: ```python 输入:s = "(a)())()" 输出:["(a())()","(a)()()"] ``` ## 解题思路 ### 思路 1:回溯算法 这道题要求删除最小数量的无效括号,使得字符串有效。我们可以使用回溯算法来解决这个问题。 **1. 问题分析** - 首先需要计算需要删除的最少左括号数量 $left\_remove$ 和右括号数量 $right\_remove$。 - 然后使用回溯算法尝试删除这些括号,生成所有可能的有效字符串。 - 为了避免重复结果,需要去重处理。 **2. 算法思路** 使用回溯算法来生成所有可能的有效字符串: - 首先遍历字符串,计算需要删除的最少左括号和右括号数量。 - 使用回溯函数 $backtrack$,参数包括当前字符串 $current$、当前位置 $index$、剩余需要删除的左括号数量 $left\_count$、右括号数量 $right\_count$、当前左括号数量 $left\_open$、右括号数量 $right\_open$。 - 在回溯过程中,对于每个字符: - 如果是左括号,可以选择删除或保留。 - 如果是右括号,可以选择删除或保留(但要保证左括号数量大于右括号数量)。 - 如果是其他字符,直接保留。 - 当遍历完整个字符串且括号数量平衡时,将结果加入答案集合。 **3. 关键步骤** 1. 计算需要删除的最少括号数量:$left\_remove$ 和 $right\_remove$。 2. 使用回溯函数生成所有可能的有效字符串。 3. 对于每个位置,尝试删除或保留当前括号。 4. 使用集合去重,返回所有有效结果。 ### 思路 1:代码 ```python class Solution: def removeInvalidParentheses(self, s: str) -> List[str]: # 计算需要删除的最少左括号和右括号数量 left_remove = 0 # 需要删除的左括号数量 right_remove = 0 # 需要删除的右括号数量 for char in s: if char == '(': left_remove += 1 elif char == ')': if left_remove > 0: left_remove -= 1 else: right_remove += 1 result = set() # 使用集合去重 def backtrack(current, index, left_count, right_count, left_open, right_open): # 如果遍历完整个字符串 if index == len(s): # 如果括号数量平衡,加入结果 if left_open == right_open: result.add(current) return char = s[index] # 如果是左括号 if char == '(': # 选择删除这个左括号 if left_count > 0: backtrack(current, index + 1, left_count - 1, right_count, left_open, right_open) # 选择保留这个左括号 backtrack(current + char, index + 1, left_count, right_count, left_open + 1, right_open) # 如果是右括号 elif char == ')': # 选择删除这个右括号 if right_count > 0: backtrack(current, index + 1, left_count, right_count - 1, left_open, right_open) # 选择保留这个右括号(但要保证左括号数量大于右括号数量) if left_open > right_open: backtrack(current + char, index + 1, left_count, right_count, left_open, right_open + 1) # 如果是其他字符,直接保留 else: backtrack(current + char, index + 1, left_count, right_count, left_open, right_open) # 开始回溯 backtrack("", 0, left_remove, right_remove, 0, 0) return list(result) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n)$,其中 $n$ 是字符串长度。在最坏情况下,每个括号都有两种选择(删除或保留),所以时间复杂度为 $O(2^n)$。 - **空间复杂度**:$O(n)$,递归栈的深度最多为 $n$,结果集合的空间复杂度也为 $O(n)$。 ================================================ FILE: docs/solutions/0300-0399/reverse-string.md ================================================ # [0344. 反转字符串](https://leetcode.cn/problems/reverse-string/) - 标签:双指针、字符串 - 难度:简单 ## 题目链接 - [0344. 反转字符串 - 力扣](https://leetcode.cn/problems/reverse-string/) ## 题目大意 **描述**:给定一个字符数组 $s$。 **要求**:将其反转。 **说明**: - 不能使用额外的数组空间,必须原地修改输入数组、使用 $O(1)$ 的额外空间解决问题。 - $1 \le s.length \le 10^5$。 - $s[i]$ 都是 ASCII 码表中的可打印字符。 **示例**: - 示例 1: ```python 输入:s = ["h","e","l","l","o"] 输出:["o","l","l","e","h"] ``` - 示例 2: ```python 输入:s = ["H","a","n","n","a","h"] 输出:["h","a","n","n","a","H"] ``` ## 解题思路 ### 思路 1:对撞指针 1. 使用两个指针 $left$,$right$。$left$ 指向字符数组开始位置,$right$ 指向字符数组结束位置。 2. 交换 $s[left]$ 和 $s[right]$,将 $left$ 右移、$right$ 左移。 3. 如果遇到 $left == right$,跳出循环。 ### 思路 1:代码 ```python class Solution: def reverseString(self, s: List[str]) -> None: left, right = 0, len(s) - 1 while left < right: s[left], s[right] = s[right], s[left] left += 1 right -= 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0300-0399/reverse-vowels-of-a-string.md ================================================ # [0345. 反转字符串中的元音字母](https://leetcode.cn/problems/reverse-vowels-of-a-string/) - 标签:双指针、字符串 - 难度:简单 ## 题目链接 - [0345. 反转字符串中的元音字母 - 力扣](https://leetcode.cn/problems/reverse-vowels-of-a-string/) ## 题目大意 **描述**:给定一个字符串 $s$。 **要求**:将字符串中的元音字母进行反转。 **说明**: - 元音字母包括 `'a'`、`'e'`、`'i'`、`'o'`、`'u'`,且可能以大小写两种形式出现不止一次。 - $1 \le s.length \le 3 \times 10^5$。 - $s$ 由可打印的 ASCII 字符组成。 **示例**: - 示例 1: ```python 输入:s = "hello" 输出:"holle" ``` - 示例 2: ```python 输入:s = "leetcode" 输出:"leotcede" ``` ## 解题思路 ### 思路 1:对撞指针 1. 因为 Python 的字符串是不可变的,所以我们先将字符串转为数组。 2. 使用两个指针 $left$,$right$。$left$ 指向字符串开始位置,$right$ 指向字符串结束位置。 3. 然后 $left$ 依次从左到右移动查找元音字母,$right$ 依次从右到左查找元音字母。 4. 如果都找到了元音字母,则交换字符,然后继续进行查找。 5. 如果遇到 $left == right$ 时停止。 6. 最后返回对应的字符串即可。 ### 思路 1:代码 ```python class Solution: def reverseVowels(self, s: str) -> str: vowels = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'] left = 0 right = len(s)-1 s_list = list(s) while left < right: if s_list[left] not in vowels: left += 1 continue if s_list[right] not in vowels: right -= 1 continue s_list[left], s_list[right] = s_list[right], s_list[left] left += 1 right -= 1 return "".join(s_list) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0300-0399/rotate-function.md ================================================ # [0396. 旋转函数](https://leetcode.cn/problems/rotate-function/) - 标签:数组、数学、动态规划 - 难度:中等 ## 题目链接 - [0396. 旋转函数 - 力扣](https://leetcode.cn/problems/rotate-function/) ## 题目大意 **描述**: 给定一个长度为 $n$ 的整数数组 $nums$。 假设 $arrk$ 是数组 $nums$ 顺时针旋转 $k$ 个位置后的数组,我们定义 $nums$ 的 旋转函数 $F$ 为: - $F(k) = 0 \times arrk[0] + 1 \times arrk[1] + ... + (n - 1) \times arrk[n - 1]$ **要求**: 返回 $F(0), F(1), ..., F(n-1)$ 中的最大值。 **说明**: - 生成的测试用例让答案符合 $32$ 位整数。 - $n == nums.length$。 - $1 \le n \le 10^{5}$。 - $-10^{3} \le nums[i] \le 10^{3}$。 **示例**: - 示例 1: ```python 输入: nums = [4,3,2,6] 输出: 26 解释: F(0) = (0 * 4) + (1 * 3) + (2 * 2) + (3 * 6) = 0 + 3 + 4 + 18 = 25 F(1) = (0 * 6) + (1 * 4) + (2 * 3) + (3 * 2) = 0 + 4 + 6 + 6 = 16 F(2) = (0 * 2) + (1 * 6) + (2 * 4) + (3 * 3) = 0 + 6 + 8 + 9 = 23 F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4) = 0 + 2 + 12 + 12 = 26 所以 F(0), F(1), F(2), F(3) 中的最大值是 F(3) = 26 。 ``` - 示例 2: ```python 输入: nums = [100] 输出: 0 ``` ## 解题思路 ### 思路 1:数学推导 通过观察旋转函数的计算规律,我们可以发现相邻旋转函数值之间存在递推关系。 设数组长度为 $n$,数组元素为 $nums[0], nums[1], ..., nums[n-1]$。 对于旋转函数 $F(k)$: - $F(0) = 0 \times nums[0] + 1 \times nums[1] + 2 \times nums[2] + ... + (n-1) \times nums[n-1]$ - $F(1) = 0 \times nums[n-1] + 1 \times nums[0] + 2 \times nums[1] + ... + (n-1) \times nums[n-2]$ 通过数学推导,我们可以得到递推公式: $$F(k) = F(k-1) + sum - n \times nums[n-k]$$ 其中 $sum$ 是数组所有元素的和。 **算法步骤**: 1. 计算 $F(0)$ 的值和数组元素总和 $sum$。 2. 使用递推公式依次计算 $F(1), F(2), ..., F(n-1)$。 3. 返回所有 $F(k)$ 值中的最大值。 ### 思路 1:代码 ```python class Solution: def maxRotateFunction(self, nums: List[int]) -> int: n = len(nums) # 计算 F(0) 和数组元素总和 f0 = 0 total_sum = 0 for i in range(n): f0 += i * nums[i] total_sum += nums[i] # 初始化最大值为 F(0) max_val = f0 current_f = f0 # 使用递推公式计算 F(1) 到 F(n-1) for k in range(1, n): # F(k) = F(k-1) + sum - n * nums[n-k] current_f = current_f + total_sum - n * nums[n - k] max_val = max(max_val, current_f) return max_val ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组长度。需要遍历数组两次:一次计算 $F(0)$ 和总和,一次使用递推公式计算所有旋转函数值。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0300-0399/russian-doll-envelopes.md ================================================ # [0354. 俄罗斯套娃信封问题](https://leetcode.cn/problems/russian-doll-envelopes/) - 标签:数组、二分查找、动态规划、排序 - 难度:困难 ## 题目链接 - [0354. 俄罗斯套娃信封问题 - 力扣](https://leetcode.cn/problems/russian-doll-envelopes/) ## 题目大意 给定一个二维整数数组 envelopes 表示信封,其中 $envelopes[i] = [wi, hi]$,表示第 $i$ 个信封的宽度 $w_i$ 和高度 $h_i$。 当一个信封的宽度和高度比另一个信封大时,则小的信封可以放进大信封里,就像俄罗斯套娃一样。 现在要求:计算最多能有多少个信封组成一组「俄罗斯套娃」信封。 注意:不允许旋转信封(也就是说宽高不能互换)。 ## 解题思路 如果最多有 k 个信封可以组成「俄罗斯套娃」信封。那么这 k 个信封按照宽高关系排序一定满足: - $w_0 < w_1 < ... < w_{k-1}$ - $h_0 < h_1 < ... < h_{k-1}$ 因为原二维数组是无序的,直接暴力搜素宽高升序序列并不容易。所以我们可以先固定一个维度,将其变为升序状态。再在另一个维度上进行选择。比如固定宽度为升序,则我们的问题就变为了:在高度这一维度下,求解数组的最长递增序列的长度。就变为了经典的「最长递增序列的长度问题」。即 [0300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/)。 「最长递增序列的长度问题」的思路如下: 动态规划的状态 `dp[i]` 表示为:以第 i 个数字结尾的前 i 个元素中最长严格递增子序列的长度。 遍历前 i 个数字,`0 ≤ j ≤ i`: - 当 `nums[j] < nums[i]` 时,`nums[i]` 可以接在 `nums[j]` 后面,此时以第 i 个数字结尾的最长严格递增子序列长度 + 1,即 `dp[i] = dp[j] + 1`。 - 当 `nums[j] ≥ nums[i]` 时,可以直接跳过。 则状态转移方程为:`dp[i] = max(dp[i], dp[j] + 1)`,`0 ≤ j ≤ i`,`nums[j] < nums[i]`。 最后再遍历一遍 dp 数组,求出最大值即可。 ## 代码 ```python class Solution: def maxEnvelopes(self, envelopes: List[List[int]]) -> int: if not envelopes: return 0 size = len(envelopes) envelopes.sort(key=lambda x: (x[0], -x[1])) dp = [1 for _ in range(size)] for i in range(size): for j in range(i): if envelopes[j][1] < envelopes[i][1]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) ``` ================================================ FILE: docs/solutions/0300-0399/self-crossing.md ================================================ # [0335. 路径交叉](https://leetcode.cn/problems/self-crossing/) - 标签:几何、数组、数学 - 难度:困难 ## 题目链接 - [0335. 路径交叉 - 力扣](https://leetcode.cn/problems/self-crossing/) ## 题目大意 **描述**: 给定一个整数数组 $distance$。 从 X-Y 平面上的点 $(0,0)$ 开始,先向北移动 $distance[0]$ 米,然后向西移动 $distance[1]$ 米,向南移动 $distance[2]$ 米,向东移动 $distance[3]$ 米,持续移动。也就是说,每次移动后你的方位会发生逆时针变化。 **要求**: 判断你所经过的路径是否相交。如果相交,返回 $true$;否则,返回 $false$。 **说明**: - $1 \le distance.length \le 10^{5}$。 - $1 \le distance[i] \le 10^{5}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/14/selfcross1-plane.jpg) ```python 输入:distance = [2,1,1,2] 输出:true ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/14/selfcross2-plane.jpg) ```python 输入:distance = [1,2,3,4] 输出:false ``` ## 解题思路 ### 思路 1:分类讨论 路径交叉问题可以通过分析路径的几何特征来解决。关键在于:第 $i$ 条线段只可能与第 $i-3$、$i-4$、$i-5$ 条线段相交。 设路径移动序列为 $distance = [d_0, d_1, d_2, d_3, ...]$,从点 $(0, 0)$ 开始,按照北、西、南、东的方向依次循环移动。 路径交叉主要有以下三种情况: 1. **第 $i$ 条线段与第 $i-3$ 条线段相交**:当 $i \ge 3$ 时,如果 $d_i \ge d_{i-2}$ 且 $d_{i-1} \le d_{i-3}$,则发生交叉。 2. **第 $i$ 条线段与第 $i-4$ 条线段相交**:当 $i \ge 4$ 时,如果 $d_{i-1} = d_{i-3}$ 且 $d_i + d_{i-4} \ge d_{i-2}$,则发生交叉。 3. **第 $i$ 条线段与第 $i-5$ 条线段相交**:当 $i \ge 5$ 时,需要满足多个条件才会交叉。 **算法步骤**: 1. 遍历路径数组,从第 4 条线段开始检查。 2. 对于每条线段,检查是否与前面的第 3、4、5 条线段相交。 3. 如果发现交叉,返回 $true$;否则继续检查。 4. 如果遍历完所有线段都没有交叉,返回 $false$。 ### 思路 1:代码 ```python class Solution: def isSelfCrossing(self, distance: List[int]) -> bool: n = len(distance) # 路径长度小于 4 时不可能交叉 if n < 4: return False # 从第 4 条线段开始检查 for i in range(3, n): # 第 i 条线段与第 i-3 条线段相交 if i >= 3: if distance[i] >= distance[i - 2] and distance[i - 1] <= distance[i - 3]: return True # 第 i 条线段与第 i-4 条线段相交 if i >= 4: if distance[i - 1] == distance[i - 3] and distance[i] + distance[i - 4] >= distance[i - 2]: return True # 第 i 条线段与第 i-5 条线段相交 if i >= 5: if (distance[i - 2] >= distance[i - 4] and distance[i - 3] >= distance[i - 1] and distance[i - 1] + distance[i - 5] >= distance[i - 3] and distance[i] + distance[i - 4] >= distance[i - 2]): return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是路径数组的长度。需要遍历数组一次,每个位置的处理时间是常数时间。 - **空间复杂度**:$O(1)$,只使用了常数额外空间,没有使用额外的数据结构。 ================================================ FILE: docs/solutions/0300-0399/shortest-distance-from-all-buildings.md ================================================ # [0317. 离建筑物最近的距离](https://leetcode.cn/problems/shortest-distance-from-all-buildings/) - 标签:广度优先搜索、数组、矩阵 - 难度:困难 ## 题目链接 - [0317. 离建筑物最近的距离 - 力扣](https://leetcode.cn/problems/shortest-distance-from-all-buildings/) ## 题目大意 **描述**: 给定一个 $m \times n$ 的网格,值为 $0$、$1$ 或 $2$,其中: - 每一个 $0$ 代表一块你可以自由通过的「空地」。 - 每一个 $1$ 代表一个你不能通过的「建筑」。 - 每一个 $2$ 标记一个你不能通过的「障碍」。 你想要在一块空地上建造一所房子,在「最短的总旅行距离」内到达所有的建筑。你只能上下左右移动。 **要求**: 返回到该房子的「最短旅行距离」。如果根据上述规则无法建造这样的房子,则返回 $-1$。 **说明**: - 总旅行距离:是朋友们家到聚会地点的距离之和。 - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 50$。 - $grid[i][j]$ 是 $0$, $1$ 或 $2$。 - $grid$ 中至少有一幢建筑。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/14/buildings-grid.jpg) ```python 输入:grid = [[1,0,2,0,1],[0,0,0,0,0],[0,0,1,0,0]] 输出:7 解析:给定三个建筑物 (0,0)、(0,4) 和 (2,2) 以及一个位于 (0,2) 的障碍物。 由于总距离之和 3+3+1=7 最优,所以位置 (1,2) 是符合要求的最优地点。 故返回 7。 ``` - 示例 2: ```python 输入: grid = [[1,0]] 输出: 1 ``` ## 解题思路 ### 思路 1:多源 BFS **核心思想**:从每个建筑物出发,使用 BFS 计算到所有空地的距离,然后找到距离所有建筑物总距离最小的空地。 **算法步骤**: 1. **统计建筑物数量**:遍历网格,统计建筑物(值为 $1$)的总数 $buildingCount$。 2. **多源 BFS**:对每个建筑物执行 BFS,计算到所有空地的距离: - 使用队列 $queue$ 存储待访问的位置。 - 使用 $dist$ 数组记录从当前建筑物到各位置的距离。 - 使用 $reachCount$ 数组记录每个位置能到达的建筑物数量。 3. **距离累加**:使用 $totalDist$ 数组累加所有建筑物到各位置的距离。 4. **寻找最优解**:遍历所有空地,找到能到达所有建筑物且总距离最小的位置。 ### 思路 1:代码 ```python class Solution: def shortestDistance(self, grid: List[List[int]]) -> int: if not grid or not grid[0]: return -1 m, n = len(grid), len(grid[0]) # 统计建筑物数量 building_count = 0 for i in range(m): for j in range(n): if grid[i][j] == 1: building_count += 1 # 记录每个位置能到达的建筑物数量和总距离 reach_count = [[0] * n for _ in range(m)] total_dist = [[0] * n for _ in range(m)] # 方向数组:上下左右 directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] # 从每个建筑物开始 BFS for i in range(m): for j in range(n): if grid[i][j] == 1: # 找到建筑物 # BFS 计算从当前建筑物到所有空地的距离 queue = [(i, j)] dist = [[-1] * n for _ in range(m)] dist[i][j] = 0 while queue: x, y = queue.pop(0) # 遍历四个方向 for dx, dy in directions: nx, ny = x + dx, y + dy # 检查边界和是否为空地 if (0 <= nx < m and 0 <= ny < n and grid[nx][ny] == 0 and dist[nx][ny] == -1): dist[nx][ny] = dist[x][y] + 1 queue.append((nx, ny)) # 累加距离和到达的建筑物数量 total_dist[nx][ny] += dist[nx][ny] reach_count[nx][ny] += 1 # 寻找能到达所有建筑物且总距离最小的空地 min_dist = float('inf') for i in range(m): for j in range(n): if (grid[i][j] == 0 and reach_count[i][j] == building_count and total_dist[i][j] < min_dist): min_dist = total_dist[i][j] return min_dist if min_dist != float('inf') else -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n \times k)$,其中 $m$ 和 $n$ 是网格的行数和列数,$k$ 是建筑物的数量。每个建筑物都需要执行一次 BFS,每次 BFS 的时间复杂度为 $O(m \times n)$。 - **空间复杂度**:$O(m \times n)$,需要额外的数组来存储距离信息和访问状态。 ================================================ FILE: docs/solutions/0300-0399/shuffle-an-array.md ================================================ # [0384. 打乱数组](https://leetcode.cn/problems/shuffle-an-array/) - 标签:数组、数学、随机化 - 难度:中等 ## 题目链接 - [0384. 打乱数组 - 力扣](https://leetcode.cn/problems/shuffle-an-array/) ## 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:设计算法来打乱一个没有重复元素的数组。打乱后,数组的所有排列应该是等可能的。 实现 `Solution class`: - `Solution(int[] nums)` 使用整数数组 $nums$ 初始化对象。 - `int[] reset()` 重设数组到它的初始状态并返回。 - `int[] shuffle()` 返回数组随机打乱后的结果。 **说明**: - $1 \le nums.length \le 50$。 - $-10^6 \le nums[i] \le 10^6$。 - $nums$ 中的所有元素都是 唯一的。 - 最多可以调用 $10^4$ 次 `reset` 和 `shuffle`。 **示例**: - 示例 1: ```python 输入: ["Solution", "shuffle", "reset", "shuffle"] [[[1, 2, 3]], [], [], []] 输出: [null, [3, 1, 2], [1, 2, 3], [1, 3, 2]] 解释: Solution solution = new Solution([1, 2, 3]); solution.shuffle(); // 打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。例如,返回 [3, 1, 2] solution.reset(); // 重设数组到它的初始状态 [1, 2, 3] 。返回 [1, 2, 3] solution.shuffle(); // 随机返回数组 [1, 2, 3] 打乱后的结果。例如,返回 [1, 3, 2] ``` ## 解题思路 ### 思路 1:洗牌算法 题目要求在打乱顺序后,数组的所有排列应该是等可能的。对于长度为 $n$ 的数组,我们可以把问题转换为:分别在 $n$ 个位置上,选择填入某个数的概率是相同。具体选择方法如下: - 对于第 $0$ 个位置,我们从 $0 \sim n - 1$ 总共 $n$ 个数中随机选择一个数,将该数与第 $0$ 个位置上的数进行交换。则每个数被选到的概率为 $\frac{1}{n}$。 - 对于第 $1$ 个位置,我们从剩下 $n - 1$ 个数中随机选择一个数,将该数与第 $1$ 个位置上的数进行交换。则每个数被选到的概率为 $\frac{n - 1}{n} \times \frac{1}{n - 1} = \frac{1}{n}$ (第一次没选到并且第二次被选中)。 - 对于第 $2$ 个位置,我们从剩下 $n - 2$ 个数中随机选择一个数,将该数与第 $2$ 个位置上的数进行交换。则每个数被选到的概率为 $\frac{n - 1}{n} \times \frac{n - 2}{n - 1} \times \frac{1}{n - 2} = \frac{1}{n}$ (第一次没选到、第二次没选到,并且第三次被选中)。 - 依次类推,对于每个位置上,每个数被选中的概率都是 $\frac{1}{n}$。 ### 思路 1:洗牌算法代码 ```python class Solution: def __init__(self, nums: List[int]): self.nums = nums def reset(self) -> List[int]: return self.nums def shuffle(self) -> List[int]: self.shuffle_nums = self.nums.copy() for i in range(len(self.shuffle_nums)): swap_index = random.randrange(i, len(self.shuffle_nums)) self.shuffle_nums[i], self.shuffle_nums[swap_index] = self.shuffle_nums[swap_index], self.shuffle_nums[i] return self.shuffle_nums ``` ## 参考资料 - 【题解】[「Python/Java/JavaScript/Go」 洗牌算法 - 打乱数组 - 力扣](https://leetcode.cn/problems/shuffle-an-array/solution/pythonjavajavascriptgo-xi-pai-suan-fa-by-k7i2/) ================================================ FILE: docs/solutions/0300-0399/smallest-rectangle-enclosing-black-pixels.md ================================================ # [0302. 包含全部黑色像素的最小矩形](https://leetcode.cn/problems/smallest-rectangle-enclosing-black-pixels/) - 标签:深度优先搜索、广度优先搜索、数组、二分查找、矩阵 - 难度:困难 ## 题目链接 - [0302. 包含全部黑色像素的最小矩形 - 力扣](https://leetcode.cn/problems/smallest-rectangle-enclosing-black-pixels/) ## 题目大意 **描述**: 图片在计算机处理中往往是使用二维矩阵来表示的。 给定一个大小为 $m \times n$ 的二进制矩阵 $image$ 表示一张黑白图片,$0$ 代表白色像素,$1$ 代表黑色像素。 黑色像素相互连接,也就是说,图片中只会有一片连在一块儿的黑色像素。像素点是水平或竖直方向连接的。 给定两个整数 $x$ 和 $y$ 表示某一个黑色像素的位置。 **要求**: 请你找出包含全部黑色像素的最小矩形(与坐标轴对齐),并返回该矩形的面积。 你必须设计并实现一个时间复杂度低于 $O(m \times n)$ 的算法来解决此问题。 **说明**: - $m == image.length$。 - $n == image[i].length$。 - $1 \le m, n \le 10^{3}$。 - $image[i][j]$ 为 `'0'` 或 `'1'`。 - $1 \le x \lt m$。 - $1 \le y \lt n$。 - $image[x][y] == '1'$。 - $image$ 中的黑色像素仅形成一个组件。 **示例**: - 示例 1: ```python ![](https://assets.leetcode.com/uploads/2021/03/14/pixel-grid.jpg) 输入:image = [["0","0","1","0"],["0","1","1","0"],["0","1","0","0"]], x = 0, y = 2 输出:6 ``` - 示例 2: ```python 输入:image = [["1"]], x = 0, y = 0 输出:1 ``` ## 解题思路 ### 思路 1:二分查找 **核心思想**:由于黑色像素是连通的,我们可以使用二分查找来找到包含所有黑色像素的最小矩形的边界。通过二分查找分别确定矩形的上、下、左、右边界。 **算法步骤**: 1. **确定边界范围**:由于黑色像素是连通的,矩形的边界就是所有黑色像素的极值位置。 2. **二分查找上边界**:在 $[0, x]$ 范围内二分查找最小的 $top$,使得第 $top$ 行包含黑色像素。 3. **二分查找下边界**:在 $[x, m-1]$ 范围内二分查找最大的 $bottom$,使得第 $bottom$ 行包含黑色像素。 4. **二分查找左边界**:在 $[0, y]$ 范围内二分查找最小的 $left$,使得第 $left$ 列包含黑色像素。 5. **二分查找右边界**:在 $[y, n-1]$ 范围内二分查找最大的 $right$,使得第 $right$ 列包含黑色像素。 6. **计算面积**:返回 $(bottom - top + 1) \times (right - left + 1)$。 ### 思路 1:代码 ```python class Solution: def minArea(self, image: List[List[str]], x: int, y: int) -> int: if not image or not image[0]: return 0 m, n = len(image), len(image[0]) # 二分查找上边界:找到最小的 top,使得第 top 行包含黑色像素 def find_top(): left, right = 0, x while left < right: mid = (left + right) // 2 if any(image[mid][j] == '1' for j in range(n)): right = mid else: left = mid + 1 return left # 二分查找下边界:找到最大的 bottom,使得第 bottom 行包含黑色像素 def find_bottom(): left, right = x, m - 1 while left < right: mid = (left + right + 1) // 2 # 向上取整 if any(image[mid][j] == '1' for j in range(n)): left = mid else: right = mid - 1 return left # 二分查找左边界:找到最小的 left,使得第 left 列包含黑色像素 def find_left(): left, right = 0, y while left < right: mid = (left + right) // 2 if any(image[i][mid] == '1' for i in range(m)): right = mid else: left = mid + 1 return left # 二分查找右边界:找到最大的 right,使得第 right 列包含黑色像素 def find_right(): left, right = y, n - 1 while left < right: mid = (left + right + 1) // 2 # 向上取整 if any(image[i][mid] == '1' for i in range(m)): left = mid else: right = mid - 1 return left # 找到矩形的四个边界 top = find_top() bottom = find_bottom() left = find_left() right = find_right() # 计算并返回面积 return (bottom - top + 1) * (right - left + 1) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \log n + n \log m)$,其中 $m$ 和 $n$ 分别是矩阵的行数和列数。每次二分查找的时间复杂度为 $O(\log k)$,其中 $k$ 是查找范围的大小。查找行边界需要 $O(m \log n)$ 时间,查找列边界需要 $O(n \log m)$ 时间。 - **空间复杂度**:$O(1)$,只使用了常数额外空间,没有使用额外的数据结构。 ================================================ FILE: docs/solutions/0300-0399/sort-transformed-array.md ================================================ # [0360. 有序转化数组](https://leetcode.cn/problems/sort-transformed-array/) - 标签:数组、数学、双指针、排序 - 难度:中等 ## 题目链接 - [0360. 有序转化数组 - 力扣](https://leetcode.cn/problems/sort-transformed-array/) ## 题目大意 **描述**:给定一个已经排好的整数数组 $nums$ 和整数 $a$、$b$、$c$。 **要求**:对于数组中的每一个数 $x$,计算函数值 $f(x) = ax^2 + bx + c$,请将函数值产生的数组返回。 **说明**: - 返回的这个数组必须按照升序排列,并且我们所期望的解法时间复杂度为 $O(n)$。 - $1 \le nums.length \le 200$。 - $-100 \le nums[i], a, b, c \le 100$。 - $nums$ 按照升序排列。 **示例**: - 示例 1: ```python 输入: nums = [-4,-2,2,4], a = 1, b = 3, c = 5 输出: [3,9,15,33] ``` - 示例 2: ```python 输入: nums = [-4,-2,2,4], a = -1, b = 3, c = 5 输出: [-23,-5,1,7] ``` ## 解题思路 ### 思路 1: 数学 + 对撞指针 这是一道数学题。需要根据一元二次函数的性质来解决问题。因为返回的数组必须按照升序排列,并且期望的解法时间复杂度为 $O(n)$。这就不能先计算再排序了,而是要在线性时间复杂度内考虑问题。 我们先定义一个函数用来计算 $f(x)$。然后进行分情况讨论。 - 如果 $a == 0$,说明函数是一条直线。则根据 $b$ 值的正负来确定数组遍历顺序。 - 如果 $b \ge 0$,说明这条直线是一条递增直线。则按照从头到尾的顺序依次计算函数值,并依次存入答案数组。 - 如果 $b < 0$,说明这条直线是一条递减直线。则按照从尾到头的顺序依次计算函数值,并依次存入答案数组。 - 如果 $a > 0$,说明函数是一条开口向上的抛物线,最小值横坐标为 $diad = \frac{-b}{2.0 * a}$,离 diad 越远,函数值越大。则可以使用双指针从远到近,由大到小依次填入数组。具体步骤如下: - 使用双指针 $left$、$right$,令 $left$ 指向数组第一个元素位置,$right$ 指向数组最后一个元素位置。再定义 $index = len(nums) - 1$ 作为答案数组填入顺序的索引值。 - 比较 $left - diad$ 与 $right - diad$ 的绝对值大小。大的就是目前距离 $diad$ 最远的那个。 - 如果 $abs(nums[left] - diad)$ 更大,则将其填入答案数组对应位置,并令 $left += 1$。 - 如果 $abs(nums[right] - diad)$ 更大,则将其填入答案数组对应位置,并令 $right -= 1$。 - 令 $index -= 1$。 - 直到 $left == right$,最后将 $nums[left]$ 填入答案数组对应位置。 - 如果 $a < 0$,说明函数是一条开口向下的抛物线,最大值横坐标为 $diad = \frac{-b}{2.0 * a}$,离 diad 越远,函数值越小。则可以使用双指针从远到近,由小到大一次填入数组。具体步骤如下: - 使用双指针 $left$、$right$,令 $left$ 指向数组第一个元素位置,$right$ 指向数组最后一个元素位置。再定义 $index = 0$ 作为答案数组填入顺序的索引值。 - 比较 $left - diad$ 与 $right - diad$ 的绝对值大小。大的就是目前距离 $diad$ 最远的那个。 - 如果 $abs(nums[left] - diad)$ 更大,则将其填入答案数组对应位置,并令 $left += 1$。 - 如果 $abs(nums[right] - diad)$ 更大,则将其填入答案数组对应位置,并令 $right -= 1$。 - 令 $index += 1$。 - 直到 $left == right$,最后将 $nums[left]$ 填入答案数组对应位置。 ### 思路 1:代码 ```python class Solution: def calFormula(self, x, a, b, c): return a * x * x + b * x + c def sortTransformedArray(self, nums: List[int], a: int, b: int, c: int) -> List[int]: size = len(nums) res = [0 for _ in range(size)] # 直线 if a == 0: if b >= 0: index = 0 for i in range(size): res[index] = self.calFormula(nums[i], a, b, c) index += 1 else: index = 0 for i in range(size - 1, -1, -1): res[index] = self.calFormula(nums[i], a, b, c) index += 1 else: diad = -(b / (2.0 * a)) left, right = 0, size - 1 if a > 0: index = size - 1 while left < right: if abs(diad - nums[left]) > abs(diad - nums[right]): res[index] = self.calFormula(nums[left], a, b, c) left += 1 else: res[index] = self.calFormula(nums[right], a, b, c) right -= 1 index -= 1 res[index] = self.calFormula(nums[left], a, b, c) else: diad = -(b / (2.0 * a)) left, right = 0, size - 1 index = 0 while left < right: if abs(diad - nums[left]) > abs(diad - nums[right]): res[index] = self.calFormula(nums[left], a, b, c) left += 1 else: res[index] = self.calFormula(nums[right], a, b, c) right -= 1 index += 1 res[index] = self.calFormula(nums[left], a, b, c) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$,不考虑最终返回值的空间占用。 ================================================ FILE: docs/solutions/0300-0399/sparse-matrix-multiplication.md ================================================ # [0311. 稀疏矩阵的乘法](https://leetcode.cn/problems/sparse-matrix-multiplication/) - 标签:数组、哈希表、矩阵 - 难度:中等 ## 题目链接 - [0311. 稀疏矩阵的乘法 - 力扣](https://leetcode.cn/problems/sparse-matrix-multiplication/) ## 题目大意 **描述**: 给定两个「稀疏矩阵」:大小为 $m \times k$ 的稀疏矩阵 $mat1$ 和大小为 $k \times n$ 的稀疏矩阵 $mat2$。 **要求**: 返回 $mat1 \times mat2$ 的结果。你可以假设乘法总是可能的。 **说明**: - $m == mat1.length$。 - $k == mat1[i].length == mat2.length$。 - $n == mat2[i].length$。 - $1 \le m, n, k \le 10^{3}$。 - $-10^{3} \le mat1[i][j], mat2[i][j] \le 10^{3}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/12/mult-grid.jpg) ```python 输入:mat1 = [[1,0,0],[-1,0,3]], mat2 = [[7,0,0],[0,0,0],[0,0,1]] 输出:[[7,0,0],[-7,0,3]] ``` - 示例 2: ```python 输入:mat1 = [[0]], mat2 = [[0]] 输出:[[0]] ``` ## 解题思路 ### 思路 1:直接矩阵乘法 **核心思想**:按照矩阵乘法的定义,直接计算两个矩阵的乘积。对于结果矩阵 $result[i][j]$,它等于 $mat1$ 的第 $i$ 行与 $mat2$ 的第 $j$ 列对应元素乘积的和。 **算法步骤**: 1. **初始化结果矩阵**:创建一个大小为 $m \times n$ 的结果矩阵 $result$,初始化为全零。 2. **三重循环计算**: - 外层循环遍历 $mat1$ 的每一行 $i$($0 \le i < m$)。 - 中层循环遍历 $mat2$ 的每一列 $j$($0 \le j < n$)。 - 内层循环遍历公共维度 $k$($0 \le k < k$)。 3. **计算乘积**:对于每个位置 $(i, j)$,计算 $result[i][j] = \sum_{k=0}^{k-1} mat1[i][k] \times mat2[k][j]$。 4. **返回结果**:返回计算完成的结果矩阵。 ### 思路 1:代码 ```python class Solution: def multiply(self, mat1: List[List[int]], mat2: List[List[int]]) -> List[List[int]]: # 获取矩阵维度 m = len(mat1) # mat1 的行数 k = len(mat1[0]) # mat1 的列数,也是 mat2 的行数 n = len(mat2[0]) # mat2 的列数 # 初始化结果矩阵,大小为 m × n result = [[0] * n for _ in range(m)] # 三重循环计算矩阵乘法 for i in range(m): # 遍历 mat1 的每一行 for j in range(n): # 遍历 mat2 的每一列 for l in range(k): # 遍历公共维度 # 累加 mat1[i][l] * mat2[l][j] 到 result[i][j] result[i][j] += mat1[i][l] * mat2[l][j] return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n \times k)$,其中 $m$ 是 $mat1$ 的行数,$n$ 是 $mat2$ 的列数,$k$ 是公共维度。需要三重循环遍历所有元素进行计算。 - **空间复杂度**:$O(m \times n)$,用于存储结果矩阵,不包含输入矩阵的空间。 ================================================ FILE: docs/solutions/0300-0399/sum-of-two-integers.md ================================================ # [0371. 两整数之和](https://leetcode.cn/problems/sum-of-two-integers/) - 标签:位运算、数学 - 难度:中等 ## 题目链接 - [0371. 两整数之和 - 力扣](https://leetcode.cn/problems/sum-of-two-integers/) ## 题目大意 **描述**:给定两个整数 $a$ 和 $b$。 **要求**:不使用运算符 `+` 和 `-` ,计算两整数 $a$ 和 $b$ 的和。 **说明**: - $-1000 \le a, b \le 1000$。 **示例**: - 示例 1: ```python 输入:a = 1, b = 2 输出:3 ``` - 示例 2: ```python 输入:a = 2, b = 3 输出:5 ``` ## 解题思路 ### 思路 1:位运算 需要用到位运算的一些知识。 - 异或运算 `a ^ b`:可以获得 $a + b$ 无进位的加法结果。 - 与运算 `a & b`:对应位置为 $1$,说明 $a$、$b$ 该位置上原来都为 $1$,则需要进位。 - 左移运算 `a << 1`:将 $a$ 对应二进制数左移 $1$ 位。 这样,通过 `a ^ b` 运算,我们可以得到相加后无进位结果,再根据 `(a & b) << 1`,计算进位后结果。 进行 `a ^ b` 和 `(a & b) << 1` 操作之后判断进位是否为 $0$,如果不为 $0$,则继续上一步操作,直到进位为 $0$。 > 注意: > > Python 的整数类型是无限长整数类型,负数不确定符号位是第几位。所以我们可以将输入的数字手动转为 $32$ 位无符号整数。 > > 通过 `a &= 0xFFFFFFFF` 即可将 $a$ 转为 $32$ 位无符号整数。最后通过对 $a$ 的范围判断,将其结果映射为有符号整数。 ### 思路 1:代码 ```python class Solution: def getSum(self, a: int, b: int) -> int: MAX_INT = 0x7FFFFFFF MASK = 0xFFFFFFFF a &= MASK b &= MASK while b: carry = ((a & b) << 1) & MASK a ^= b b = carry if a <= MAX_INT: return a else: return ~(a ^ MASK) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log k)$,其中 $k$ 为 $int$ 所能表达的最大整数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0300-0399/super-pow.md ================================================ # [0372. 超级次方](https://leetcode.cn/problems/super-pow/) - 标签:数学、分治 - 难度:中等 ## 题目链接 - [0372. 超级次方 - 力扣](https://leetcode.cn/problems/super-pow/) ## 题目大意 **描述**: 给定两个正整数 $a$ 和 $b$。其中 $b$ 是一个非常大的正整数且会以数组形式给出。 **要求**: 计算 $a^b$ 对 $1337$ 取模。 **说明**: - $1 \le a \le 2^{31} - 1$。 - $1 \le b.length \le 2000$。 - $0 \le b[i] \le 9$。 - $b$ 不含前导 $0$。 **示例**: - 示例 1: ```python 输入:a = 2, b = [3] 输出:8 ``` - 示例 2: ```python 输入:a = 2, b = [1,0] 输出:1024 ``` ## 解题思路 ### 思路 1:快速幂 + 模运算 **核心思想**:由于 $b$ 是一个非常大的数(以数组形式给出),直接计算 $a^b$ 会导致溢出。我们可以使用快速幂算法结合模运算的性质来高效计算 $a^b \bmod 1337$。 **数学原理**: - 模运算性质:$(a \times b) \bmod m = ((a \bmod m) \times (b \bmod m)) \bmod m$ - 幂运算性质:$a^{b_1b_2...b_n} = a^{b_1 \times 10^{n-1}} \times a^{b_2 \times 10^{n-2}} \times ... \times a^{b_n}$ - 快速幂:$a^n = \begin{cases} (a^{n/2})^2 & \text{if } n \text{ is even} \\ a \times a^{n-1} & \text{if } n \text{ is odd} \end{cases}$ **算法步骤**: 1. **处理数组形式的指数**:将数组 $b$ 转换为实际的指数值,但由于 $b$ 可能非常大,我们需要逐位处理。 2. **快速幂计算**:对于每一位数字 $b[i]$,计算 $a^{b[i] \times 10^{n-i-1}} \bmod 1337$。 3. **模运算优化**:在每次乘法运算后都进行模运算,避免数值溢出。 4. **逐位累乘**:将每一位的结果相乘并对 $1337$ 取模。 ### 思路 1:代码 ```python class Solution: def superPow(self, a: int, b: List[int]) -> int: # 模数 MOD = 1337 # 快速幂函数:计算 base^exp % MOD def quick_pow(base: int, exp: int) -> int: result = 1 base %= MOD # 先对底数取模 while exp > 0: if exp & 1: # 如果指数是奇数 result = (result * base) % MOD base = (base * base) % MOD # 底数平方 exp >>= 1 # 指数右移一位(相当于除以2) return result # 处理数组形式的指数 result = 1 for digit in b: # 对于每一位数字,先计算 result^10,再乘以 a^digit result = quick_pow(result, 10) # result^10 % MOD result = (result * quick_pow(a, digit)) % MOD # 乘以 a^digit % MOD return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log k)$,其中 $n$ 是数组 $b$ 的长度,$k$ 是最大的指数值(最大为 $9$)。对于数组中的每一位数字,我们需要调用快速幂函数,快速幂的时间复杂度为 $O(\log k)$,总共有 $n$ 位数字。 - **空间复杂度**:$O(1)$,只使用了常数额外空间,没有使用额外的数据结构。 ================================================ FILE: docs/solutions/0300-0399/super-ugly-number.md ================================================ # [0313. 超级丑数](https://leetcode.cn/problems/super-ugly-number/) - 标签:数组、数学、动态规划 - 难度:中等 ## 题目链接 - [0313. 超级丑数 - 力扣](https://leetcode.cn/problems/super-ugly-number/) ## 题目大意 **描述**: 「超级丑数」是一个正整数,并满足其所有质因数都出现在质数数组 $primes$ 中。 给定一个整数 $n$ 和一个整数数组 $primes$。 **要求**: 返回第 $n$ 个 超级丑数。 **说明**: - 题目数据保证第 $n$ 个「超级丑数」在 $32-bit$ 带符号整数范围内。 - $1 \le n \le 10^{5}$。 - $1 \le primes.length \le 10^{3}$。 - $2 \le primes[i] \le 10^{3}$。 - 题目数据 保证 $primes[i]$ 是一个质数。 - $primes$ 中的所有值都「互不相同」,且按「递增顺序」排列。 **示例**: - 示例 1: ```python 输入:n = 12, primes = [2,7,13,19] 输出:32 解释:给定长度为 4 的质数数组 primes = [2,7,13,19],前 12 个超级丑数序列为:[1,2,4,7,8,13,14,16,19,26,28,32] 。 ``` - 示例 2: ```python 输入:n = 1, primes = [2,3,5] 输出:1 解释:1 不含质因数,因此它的所有质因数都在质数数组 primes = [2,3,5] 中。 ``` ## 解题思路 ### 思路 1:动态规划 + 多指针 **核心思想**:超级丑数是只包含给定质数数组中质因数的正整数。我们可以使用动态规划的思想,维护一个数组 $dp$ 来存储前 $n$ 个超级丑数,并使用多个指针来跟踪每个质数应该与哪个超级丑数相乘。 **数学原理**: - 超级丑数序列:$1, p_1, p_2, p_1^2, p_1 \times p_2, p_2^2, p_1^3, ...$ - 每个超级丑数都可以表示为:$dp[i] = \min(dp[pointer[j]] \times primes[j])$,其中 $j$ 遍历所有质数 - 当 $dp[i] = dp[pointer[j]] \times primes[j]$ 时,将 $pointer[j]$ 加 1 **算法步骤**: 1. **初始化**:创建数组 $dp$ 存储超级丑数,$dp[0] = 1$。创建指针数组 $pointers$,初始化为全 0。 2. **动态规划计算**:对于 $i$ 从 1 到 $n-1$: - 计算所有可能的候选值:$candidates[j] = dp[pointers[j]] \times primes[j]$ - 找到最小值:$dp[i] = \min(candidates)$ - 更新指针:对于所有 $j$,如果 $dp[i] = candidates[j]$,则 $pointers[j]++$ 3. **返回结果**:返回 $dp[n-1]$。 ### 思路 1:代码 ```python class Solution: def nthSuperUglyNumber(self, n: int, primes: List[int]) -> int: # dp[i] 表示第 i+1 个超级丑数 dp = [0] * n dp[0] = 1 # 第一个超级丑数是 1 # pointers[j] 表示 primes[j] 应该与 dp[pointers[j]] 相乘 pointers = [0] * len(primes) # 计算第 2 到第 n 个超级丑数 for i in range(1, n): # 计算所有可能的候选值 candidates = [dp[pointers[j]] * primes[j] for j in range(len(primes))] # 找到最小值作为下一个超级丑数 dp[i] = min(candidates) # 更新指针:如果当前超级丑数等于某个候选值,则对应指针加 1 for j in range(len(primes)): if dp[i] == dp[pointers[j]] * primes[j]: pointers[j] += 1 return dp[n - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times k)$,其中 $n$ 是要求的超级丑数个数,$k$ 是质数数组的长度。对于每个超级丑数,我们需要遍历所有质数来计算候选值并更新指针。 - **空间复杂度**:$O(n + k)$,其中 $O(n)$ 用于存储 $dp$ 数组,$O(k)$ 用于存储 $pointers$ 数组。 ================================================ FILE: docs/solutions/0300-0399/top-k-frequent-elements.md ================================================ # [0347. 前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/) - 标签:数组、哈希表、分治、桶排序、计数、快速选择、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [0347. 前 K 个高频元素 - 力扣](https://leetcode.cn/problems/top-k-frequent-elements/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 和一个整数 $k$。 **要求**:返回出现频率前 $k$ 高的元素。可以按任意顺序返回答案。 **说明**: - $1 \le nums.length \le 10^5$。 - $k$ 的取值范围是 $[1, \text{ 数组中不相同的元素的个数}]$。 - 题目数据保证答案唯一,换句话说,数组中前 $k$ 个高频元素的集合是唯一的。 **示例**: - 示例 1: ```python 输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2] ``` - 示例 2: ```python 输入: nums = [1], k = 1 输出: [1] ``` ## 解题思路 ### 思路 1:哈希表 + 优先队列 1. 使用哈希表记录下数组中各个元素的频数。 2. 然后将哈希表中的元素去重,转换为新数组。时间复杂度 $O(n)$,空间复杂度 $O(n)$。 3. 使用二叉堆构建优先队列,优先级为元素频数。此时堆顶元素即为频数最高的元素。时间复杂度 $O(n)$,空间复杂度 $O(n)$。 4. 将堆顶元素加入到答案数组中,进行出队操作。时间复杂度 $O(log{n})$。 - 出队操作:交换堆顶元素与末尾元素,将末尾元素已移出堆。继续调整大顶堆。 5. 不断重复第 4 步,直到 $k$ 次结束。调整 $k$ 次的时间复杂度 $O(n \times \log n)$。 ### 思路 1:代码 ```python class Heapq: # 堆调整方法:调整为大顶堆 def heapAdjust(self, nums: [int], nums_dict, index: int, end: int): left = index * 2 + 1 right = left + 1 while left <= end: # 当前节点为非叶子结点 max_index = index if nums_dict[nums[left]] > nums_dict[nums[max_index]]: max_index = left if right <= end and nums_dict[nums[right]] > nums_dict[nums[max_index]]: max_index = right if index == max_index: # 如果不用交换,则说明已经交换结束 break nums[index], nums[max_index] = nums[max_index], nums[index] # 继续调整子树 index = max_index left = index * 2 + 1 right = left + 1 # 将数组构建为二叉堆 def heapify(self, nums: [int], nums_dict): size = len(nums) # (size - 2) // 2 是最后一个非叶节点,叶节点不用调整 for i in range((size - 2) // 2, -1, -1): # 调用调整堆函数 self.heapAdjust(nums, nums_dict, i, size - 1) # 入队操作 def heappush(self, nums: list, nums_dict, value): nums.append(value) size = len(nums) i = size - 1 # 寻找插入位置 while (i - 1) // 2 >= 0: cur_root = (i - 1) // 2 # value 小于当前根节点,则插入到当前位置 if nums_dict[nums[cur_root]] > nums_dict[value]: break # 继续向上查找 nums[i] = nums[cur_root] i = cur_root # 找到插入位置或者到达根位置,将其插入 nums[i] = value # 出队操作 def heappop(self, nums: list, nums_dict) -> int: size = len(nums) nums[0], nums[-1] = nums[-1], nums[0] # 得到最大值(堆顶元素)然后调整堆 top = nums.pop() if size > 0: self.heapAdjust(nums, nums_dict, 0, size - 2) return top class Solution: def topKFrequent(self, nums: List[int], k: int) -> List[int]: # 统计元素频数 nums_dict = dict() for num in nums: if num in nums_dict: nums_dict[num] += 1 else: nums_dict[num] = 1 # 使用 set 方法去重,得到新数组 new_nums = list(set(nums)) size = len(new_nums) heap = Heapq() queue = [] for num in new_nums: heap.heappush(queue, nums_dict, num) res = [] for i in range(k): res.append(heap.heappop(queue, nums_dict)) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0300-0399/utf-8-validation.md ================================================ # [0393. UTF-8 编码验证](https://leetcode.cn/problems/utf-8-validation/) - 标签:位运算、数组 - 难度:中等 ## 题目链接 - [0393. UTF-8 编码验证 - 力扣](https://leetcode.cn/problems/utf-8-validation/) ## 题目大意 **描述**: 给定一个表示数据的整数数组 $data$。 **要求**: 返回它是否为有效的 $UTF-8$ 编码。 $UTF-8$ 中的一个字符可能的长度为 $1$ 到 $4$ 字节,遵循以下的规则: 1. 对于 $1$ 字节的字符,字节的第一位设为 $0$,后面 $7$ 位为这个符号的 $unicode$ 码。 2. 对于 $n$ 字节的字符 ($n > 1$),第一个字节的前 $n$ 位都设为 $1$,第 $n+1$ 位设为 $0$,后面字节的前两位一律设为 $10$。剩下的没有提及的二进制位,全部为这个符号的 $unicode$ 码。 这是 $UTF-8$ 编码的工作方式: ```python Number of Bytes | UTF-8 octet sequence | (binary) ---------------------+-------------------------------------------- 1 | 0xxxxxxx 2 | 110xxxxx 10xxxxxx 3 | 1110xxxx 10xxxxxx 10xxxxxx 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ``` $x$ 表示二进制形式的一位,可以是 $0$ 或 $1$。 **说明**: - 注意:输入是整数数组。只有每个整数的 最低 $8$ 个有效位用来存储数据。这意味着每个整数只表示 $1$ 字节的数据。 - $1 \le data.length \le 2 \times 10^{4}$。 - $0 \le data[i] \le 255$。 **示例**: - 示例 1: ```python 输入:data = [197,130,1] 输出:true 解释:数据表示字节序列: 11000101 10000010 00000001。 这是有效的 utf-8 编码,为一个 2 字节字符,跟着一个 1 字节字符。 ``` - 示例 2: ```python 输入:data = [235,140,4] 输出:false 解释:数据表示 8 位的序列: 11101011 10001100 00000100. 前 3 位都是 1 ,第 4 位为 0 表示它是一个 3 字节字符。 下一个字节是开头为 10 的延续字节,这是正确的。 但第二个延续字节不以 10 开头,所以是不符合规则的。 ``` ## 解题思路 ### 思路 1:逐字节验证 **核心思想**:根据 UTF-8 编码规则,逐字节验证数据是否符合 UTF-8 编码格式。 **算法步骤**: 1. **初始化**:设置索引 $i = 0$,遍历整个数组 $data$。 2. **判断首字节类型**: - 如果 $data[i] \& 128 = 0$,说明是 1 字节字符,$i$ 自增 1。 - 如果 $data[i] \& 224 = 192$,说明是 2 字节字符,需要验证后面 1 个字节。 - 如果 $data[i] \& 240 = 224$,说明是 3 字节字符,需要验证后面 2 个字节。 - 如果 $data[i] \& 248 = 240$,说明是 4 字节字符,需要验证后面 3 个字节。 - 其他情况都是无效的首字节。 3. **验证后续字节**:对于 $n$ 字节字符,验证后面 $n-1$ 个字节是否都以 $10$ 开头(即 $data[j] \& 192 = 128$)。 4. **边界检查**:确保数组长度足够,不会越界访问。 ### 思路 1:代码 ```python class Solution: def validUtf8(self, data: List[int]) -> bool: i = 0 n = len(data) while i < n: # 获取当前字节 byte = data[i] # 判断是几字节字符 if (byte & 128) == 0: # 1 字节字符:0xxxxxxx i += 1 elif (byte & 224) == 192: # 2 字节字符:110xxxxx 10xxxxxx if i + 1 >= n or (data[i + 1] & 192) != 128: return False i += 2 elif (byte & 240) == 224: # 3 字节字符:1110xxxx 10xxxxxx 10xxxxxx if i + 2 >= n or (data[i + 1] & 192) != 128 or (data[i + 2] & 192) != 128: return False i += 3 elif (byte & 248) == 240: # 4 字节字符:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx if i + 3 >= n or (data[i + 1] & 192) != 128 or (data[i + 2] & 192) != 128 or (data[i + 3] & 192) != 128: return False i += 4 else: # 无效的首字节 return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $data$ 的长度。每个字节最多被访问一次。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0300-0399/valid-perfect-square.md ================================================ # [0367. 有效的完全平方数](https://leetcode.cn/problems/valid-perfect-square/) - 标签:数学、二分查找 - 难度:简单 ## 题目链接 - [0367. 有效的完全平方数 - 力扣](https://leetcode.cn/problems/valid-perfect-square/) ## 题目大意 **描述**:给定一个正整数 $num$。 **要求**:判断 num 是不是完全平方数。 **说明**: - 要求不能使用内置的库函数,如 `sqrt`。 - $1 \le num \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:num = 16 输出:True 解释:返回 true,因为 4 * 4 = 16 且 4 是一个整数。 ``` - 示例 2: ```python 输入:num = 14 输出:False 解释:返回 false,因为 3.742 * 3.742 = 14 但 3.742 不是一个整数。 ``` ## 解题思路 ### 思路 1:二分查找 如果 $num$ 是完全平方数,则 $num = x \times x$,$x$ 为整数。问题就变为了对于正整数 $num$,是否能找到一个整数 $x$,使得 $x \times x = num$。 而对于 $x$,我们可以通过二分查找算法快速找到。 ### 思路 1:代码 ```python class Solution: def isPerfectSquare(self, num: int) -> bool: left = 0 right = num while left < right: mid = left + (right - left) // 2 if mid * mid > num: right = mid - 1 elif mid * mid < num: left = mid + 1 else: left = mid break return left * left == num ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$,其中 $n$ 为正整数 $num$ 的最大值。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0300-0399/verify-preorder-serialization-of-a-binary-tree.md ================================================ # [0331. 验证二叉树的前序序列化](https://leetcode.cn/problems/verify-preorder-serialization-of-a-binary-tree/) - 标签:栈、树、字符串、二叉树 - 难度:中等 ## 题目链接 - [0331. 验证二叉树的前序序列化 - 力扣](https://leetcode.cn/problems/verify-preorder-serialization-of-a-binary-tree/) ## 题目大意 **描述**: 序列化二叉树的一种方法是使用「前序遍历」。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 `#`。 ![](https://assets.leetcode.com/uploads/2021/03/12/pre-tree.jpg) 例如,上面的二叉树可以被序列化为字符串 `"9,3,4,#,#,1,#,#,2,#,6,#,#"`,其中 `#` 代表一个空节点。 给定一串以逗号分隔的序列。 **要求**: 验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。 **说明**: - 保证:每个以逗号分隔的字符或为一个整数或为一个表示 $null$ 指针的 `'#'`。 你可以认为输入格式总是有效的 - 例如它永远不会包含两个连续的逗号,比如 `"1,,3"`。 - 注意:不允许重建树。 - $1 \le preorder.length \le 10^{4}$。 - $preorder$ 由以逗号 `','` 分隔的 $[0,10^{3}]$ 范围内的整数和 `'#'` 组成。 **示例**: - 示例 1: ```python 输入: preorder = "9,3,4,#,#,1,#,#,2,#,6,#,#" 输出: true ``` - 示例 2: ```python 输入: preorder = "1,#" 输出: false ``` ## 解题思路 ### 思路 1:度数计算 **核心思想**:在二叉树的前序序列化中,每个非空节点都会提供 $2$ 个出度(左子树和右子树),每个空节点 `#` 会消耗 $1$ 个入度。如果序列化是有效的,那么最终度数应该为 $0$。 **数学原理**: - 设 $degree$ 为当前可用的度数。 - 对于非空节点:$degree = degree + 2$(提供 2 个出度)。 - 对于空节点 `#`:$degree = degree - 1$(消耗 1 个入度)。 - 有效序列化的条件:遍历过程中 $degree \geq 0$,且最终 $degree = 0$。 **算法步骤**: 1. **初始化**:将输入字符串按逗号分割,$degree = 1$(根节点提供 1 个初始度数)。 2. **遍历序列**:对于每个节点 $node$: - 如果 `node == "#"`:$degree = degree - 1$(空节点消耗 1 个度数)。 - 否则:$degree = degree + 2 - 1 = degree + 1$(非空节点提供 2 个度数,消耗 1 个度数)。 - 如果 $degree < 0$,说明序列无效,返回 $false$。 3. **验证结果**:遍历结束后,如果 $degree = 0$,则序列有效,否则无效。 ### 思路 1:代码 ```python class Solution: def isValidSerialization(self, preorder: str) -> bool: # 将字符串按逗号分割成节点列表 nodes = preorder.split(',') # 初始度数为 1(根节点提供 1 个初始度数) degree = 1 # 遍历每个节点 for node in nodes: # 先消耗 1 个度数(每个节点都需要消耗 1 个度数) degree -= 1 # 如果度数变为负数,说明序列无效 if degree < 0: return False # 如果不是空节点,则提供 2 个度数(左子树和右子树) if node != '#': degree += 2 # 最终度数应该为 0 return degree == 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是序列化字符串的长度。需要遍历一次字符串并分割节点。 - **空间复杂度**:$O(n)$,用于存储分割后的节点列表。 ================================================ FILE: docs/solutions/0300-0399/water-and-jug-problem.md ================================================ # [0365. 水壶问题](https://leetcode.cn/problems/water-and-jug-problem/) - 标签:深度优先搜索、广度优先搜索、数学 - 难度:中等 ## 题目链接 - [0365. 水壶问题 - 力扣](https://leetcode.cn/problems/water-and-jug-problem/) ## 题目大意 **描述**: 有两个水壶,容量分别为 $x$ 和 $y$ 升。水的供应是无限的。 **要求**: 确定是否有可能使用这两个壶准确得到 $target$ 升。 你可以: - 装满任意一个水壶 - 清空任意一个水壶 - 将水从一个水壶倒入另一个水壶,直到接水壶已满,或倒水壶已空。 **说明**: - $1 \le x, y, target \le 10^{3}$。 **示例**: - 示例 1: ```python 输入: x = 3,y = 5,target = 4 输出: true 解释: 按照以下步骤操作,以达到总共 4 升水: 1. 装满 5 升的水壶(0, 5)。 2. 把 5 升的水壶倒进 3 升的水壶,留下 2 升(3, 2)。 3. 倒空 3 升的水壶(0, 2)。 4. 把 2 升水从 5 升的水壶转移到 3 升的水壶(2, 0)。 5. 再次加满 5 升的水壶(2, 5)。 6. 从 5 升的水壶向 3 升的水壶倒水直到 3 升的水壶倒满。5 升的水壶里留下了 4 升水(3, 4)。 7. 倒空 3 升的水壶。现在,5 升的水壶里正好有 4 升水(0, 4)。 ``` - 示例 2: ```python 输入: x = 2, y = 6, target = 5 输出: false ``` ## 解题思路 ### 思路 1:数学方法(贝祖定理) **核心思想**:这是一个经典的数学问题,可以使用贝祖定理(Bézout's identity)来解决。根据贝祖定理,对于两个整数 $x$ 和 $y$,存在整数 $a$ 和 $b$ 使得 $ax + by = \gcd(x, y)$。因此,我们能够测量的水量必须是 $x$ 和 $y$ 的最大公约数的倍数。 **数学原理**: - 贝祖定理:对于整数 $x$ 和 $y$,存在整数 $a$ 和 $b$ 使得 $ax + by = \gcd(x, y)$。 - 在操作过程中,我们只能通过装满、倒空、倒水等操作来改变水壶中的水量。 - 这些操作本质上是在做 $x$ 和 $y$ 的线性组合。 - 因此,能够测量的水量必须是 $\gcd(x, y)$ 的倍数。 **算法步骤**: 1. **特殊情况处理**:如果 $target = 0$,直接返回 $true$(两个水壶都为空)。 2. **容量检查**:如果 $target > x + y$,返回 $false$(总容量不足)。 3. **贝祖定理判断**:检查 $target$ 是否能被 $\gcd(x, y)$ 整除。 4. **返回结果**:如果 $target \bmod \gcd(x, y) = 0$,则返回 $true$,否则返回 $false$。 ### 思路 1:代码 ```python class Solution: def canMeasureWater(self, x: int, y: int, target: int) -> bool: # 特殊情况:目标为 0,两个水壶都为空即可 if target == 0: return True # 特殊情况:目标大于两个水壶的总容量 if target > x + y: return False # 使用贝祖定理:能够测量的水量必须是 gcd(x, y) 的倍数 def gcd(a: int, b: int) -> int: """计算最大公约数""" while b: a, b = b, a % b return a # 计算 x 和 y 的最大公约数 g = gcd(x, y) # 检查 target 是否能被 gcd(x, y) 整除 return target % g == 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log(\min(x, y)))$,计算最大公约数的时间复杂度为 $O(\log(\min(x, y)))$,其中 $x$ 和 $y$ 是水壶的容量。 - **空间复杂度**:$O(1)$,只使用了常数额外空间,没有使用额外的数据结构。 ================================================ FILE: docs/solutions/0300-0399/wiggle-sort-ii.md ================================================ # [0324. 摆动排序 II](https://leetcode.cn/problems/wiggle-sort-ii/) - 标签:数组、分治、快速选择、排序 - 难度:中等 ## 题目链接 - [0324. 摆动排序 II - 力扣](https://leetcode.cn/problems/wiggle-sort-ii/) ## 题目大意 给你一个整数数组 `nums`。 要求:将它重新排列成 `nums[0] < nums[1] > nums[2] < nums[3] ...` 的顺序。可以假设所有输入数组都可以得到满足题目要求的结果。 注意: - $1 \le nums.length \le 5 * 10^4$。 - $0 \le nums[i] \le 5000$。 ## 解题思路 `num[i]` 的取值在 `[0, 5000]`。所以我们可以用桶排序算法将排序算法的时间复杂度降到 $O(n)$。然后按照下标的奇偶性遍历两次数组,第一次遍历将桶中的元素从末尾到头部依次放到对应奇数位置上。第二次遍历将桶中剩余元素从末尾到头部依次放到对应偶数位置上。 ## 代码 ```python class Solution: def wiggleSort(self, nums: List[int]) -> None: """ Do not return anything, modify nums in-place instead. """ buckets = [0 for _ in range(5010)] for num in nums: buckets[num] += 1 size = len(nums) big = size - 2 if (size & 1) == 1 else size - 1 small = size - 1 if (size & 1) == 1 else size - 2 index = 5000 for i in range(1, big + 1, 2): while buckets[index] == 0: index -= 1 nums[i] = index buckets[index] -= 1 for i in range(0, small + 1, 2): while buckets[index] == 0: index -= 1 nums[i] = index buckets[index] -= 1 ``` ================================================ FILE: docs/solutions/0300-0399/wiggle-subsequence.md ================================================ # [0376. 摆动序列](https://leetcode.cn/problems/wiggle-subsequence/) - 标签:贪心、数组、动态规划 - 难度:中等 ## 题目链接 - [0376. 摆动序列 - 力扣](https://leetcode.cn/problems/wiggle-subsequence/) ## 题目大意 如果一个数组序列中,连续项之间的差值是严格的在正数、负数之间交替,则称该数组序列为「摆动序列」。第一个差值可能为正数,也可能为负数。只有一个元素或者还有两个不等元素的数组序列也可以看做是摆动序列。 - 例如:`[1, 7, 4, 9, 2, 5]` 是摆动序列 ,因为差值 `(6, -3, 5, -7, 3)` 是正负交替出现的。 - 相反,`[1, 4, 7, 2, 5]` 和 `[1, 7, 4, 5, 5]` 不是摆动序列。第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。 现在给定一个整数数组 nums,返回 nums 中作为「摆动序列」的「最长子序列长度」。 ## 解题思路 我们先通过一个例子来说明如何求摆动数组最长子序列的长度。 下图是 `nums = [1,17,5,10,13,15,10,5,16,8]` 的图示。 ![](http://qcdn.itcharge.cn/images/20210805131834.png) 根据题意可知,摆动数组中连续项的差值是正负交替的,直观表现就像是一条高低起伏的山脉,或者像一把锯齿。 观察图像可知,貌似当我们不断交错的选择山脉的「波峰」和「波谷」作为子序列的元素,就会使摆动数组的子序列尽可能的长。例如下图选择 `[1, 17, 5, 15, 5, 16, 8]`。 ![](http://qcdn.itcharge.cn/images/20210805131848.png) 可是为什么选择「峰」「谷」就能使摆动数组的子序列尽可能的长?为什么我们不选择「中间元素」呢? 其实也可以选择「中间元素」,**因为一路从波谷爬坡到波峰再到波谷,和从波谷爬坡到半山腰再回到波谷所形成的摆动数组最长子序列的长度是一样的。** 只不过如果选择中间元素,这个中间元素两侧必有波峰和波谷,我们假设选择的序列出现顺序为:「谷 -> 中间元素 -> 谷」,则「谷」和「谷」中间的「峰」必定没有出现在选择的序列中,我们必然可以将选择的「中间元素」替换为「峰」。 同理,「峰 -> 中间元素 -> 峰」中选择的「中间元素」必然也可以替换为「谷」。 所以既然中可以替换,所以我们干脆直接选择「峰」「谷」就可以满足最长子序列的长度。 所以题目就变为了:统计序列中「峰」「谷」的数量。 记录下前一对连续项的差值、当前对连续项的差值,并判断是否是互为正负的。 - 如果互为正负,则为「峰」或「谷」,记录下个数,并更新前一对连续项的差值。 - 如果符号相同,则继续向后判断。 ## 代码 ```python class Solution: def wiggleMaxLength(self, nums: List[int]) -> int: size = len(nums) cur_diff = 0 pre_diff = 0 res = 1 for i in range(size - 1): cur_diff = nums[i + 1] - nums[i] if (cur_diff > 0 and pre_diff <= 0) or (pre_diff >= 0 and cur_diff < 0): res += 1 pre_diff = cur_diff return res ``` ================================================ FILE: docs/solutions/0400-0499/132-pattern.md ================================================ # [0456. 132 模式](https://leetcode.cn/problems/132-pattern/) - 标签:栈、数组、二分查找、有序集合、单调栈 - 难度:中等 ## 题目链接 - [0456. 132 模式 - 力扣](https://leetcode.cn/problems/132-pattern/) ## 题目大意 **描述**: 给定一个整数数组 $nums$ ,数组中共有 $n$ 个整数。「132 模式的子序列」由三个整数 $nums[i]$、$nums[j]$ 和 $nums[k]$ 组成,并同时满足:$i < j < k$ 和 $nums[i] < nums[k] < nums[j]$。 **要求**: 如果 $nums$ 中存在「132 模式的子序列」,返回 $true$;否则,返回 $false$。 **说明**: - $n == nums.length$。 - $1 \le n \le 2 \times 10^{5}$。 - $-10^{9} \le nums[i] \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,4] 输出:false 解释:序列中不存在 132 模式的子序列。 ``` - 示例 2: ```python 输入:nums = [3,1,4,2] 输出:true 解释:序列中有 1 个 132 模式的子序列: [1, 4, 2]。 ``` ## 解题思路 ### 思路 1:单调栈 1. 我们需要找到三个位置 $i < j < k$,使得 $nums[i] < nums[k] < nums[j]$。 2. 从右往左遍历数组,维护一个单调递减的栈 $stack$,栈中存储的是可能的 $nums[j]$ 值。 3. 同时维护一个变量 $max\_k$,表示当前找到的最大的 $nums[k]$ 值。 4. 对于当前位置 $i$: - 如果 $nums[i] < max\_k$,说明找到了 132 模式,返回 $true$。 - 否则,将栈中所有小于 $nums[i]$ 的元素弹出,更新 $max\_k$ 为这些弹出元素的最大值。 - 将 $nums[i]$ 压入栈中。 5. 遍历结束后,如果没有找到 132 模式,返回 $false$。 ### 思路 1:代码 ```python class Solution: def find132pattern(self, nums: List[int]) -> bool: n = len(nums) if n < 3: return False # 单调栈,存储可能的 nums[j] 值 stack = [] # max_k 表示当前找到的最大的 nums[k] 值 max_k = float('-inf') # 从右往左遍历 for i in range(n - 1, -1, -1): # 如果当前元素小于 max_k,说明找到了 132 模式 if nums[i] < max_k: return True # 维护单调递减栈 # 弹出所有小于当前元素的栈顶元素 while stack and stack[-1] < nums[i]: # 更新 max_k 为弹出元素的最大值 max_k = max(max_k, stack.pop()) # 将当前元素压入栈中 stack.append(nums[i]) return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。每个元素最多入栈和出栈一次。 - **空间复杂度**:$O(n)$,其中 $n$ 是数组的长度。最坏情况下栈的大小为 $n$。 ================================================ FILE: docs/solutions/0400-0499/4sum-ii.md ================================================ # [0454. 四数相加 II](https://leetcode.cn/problems/4sum-ii/) - 标签:数组、哈希表 - 难度:中等 ## 题目链接 - [0454. 四数相加 II - 力扣](https://leetcode.cn/problems/4sum-ii/) ## 题目大意 **描述**:给定四个整数数组 $nums1$、$nums2$、$nums3$、$nums4$。 **要求**:计算有多少不同的 $(i, j, k, l)$ 满足以下条件。 1. $0 \le i, j, k, l < n$。 2. $nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0$。 **说明**: - $n == nums1.length$。 - $n == nums2.length$。 - $n == nums3.length$。 - $n == nums4.length$。 - $1 \le n \le 200$。 - $-2^{28} \le nums1[i], nums2[i], nums3[i], nums4[i] \le 2^{28}$。 **示例**: - 示例 1: ```python 输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2] 输出:2 解释: 两个元组如下: 1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0 2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0 ``` - 示例 2: ```python 输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0] 输出:1 ``` ## 解题思路 ### 思路 1:哈希表 直接暴力搜索的时间复杂度是 $O(n^4)$。我们可以降低一下复杂度。 将四个数组分为两组。$nums1$ 和 $nums2$ 分为一组,$nums3$ 和 $nums4$ 分为一组。 已知 $nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0$,可以得到 $nums1[i] + nums2[j] = -(nums3[k] + nums4[l])$ 建立一个哈希表。两重循环遍历数组 $nums1$、$nums2$,先将 $nums[i] + nums[j]$ 的和个数记录到哈希表中,然后再用两重循环遍历数组 $nums3$、$nums4$。如果 $-(nums3[k] + nums4[l])$ 的结果出现在哈希表中,则将结果数累加到答案中。最终输出累加之后的答案。 ### 思路 1:代码 ```python class Solution: def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int: nums_dict = dict() for num1 in nums1: for num2 in nums2: sum = num1 + num2 if sum in nums_dict: nums_dict[sum] += 1 else: nums_dict[sum] = 1 count = 0 for num3 in nums3: for num4 in nums4: sum = num3 + num4 if -sum in nums_dict: count += nums_dict[-sum] return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为数组的元素个数。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/0400-0499/add-strings.md ================================================ # [0415. 字符串相加](https://leetcode.cn/problems/add-strings/) - 标签:数学、字符串、模拟 - 难度:简单 ## 题目链接 - [0415. 字符串相加 - 力扣](https://leetcode.cn/problems/add-strings/) ## 题目大意 **描述**:给定两个字符串形式的非负整数 `num1` 和`num2`。 **要求**:计算它们的和,并同样以字符串形式返回。 **说明**: - $1 \le num1.length, num2.length \le 10^4$。 - $num1$ 和 $num2$ 都只包含数字 $0 \sim 9$。 - $num1$ 和 $num2$ 都不包含任何前导零。 - 你不能使用任何內建 BigInteger 库, 也不能直接将输入的字符串转换为整数形式。 **示例**: - 示例 1: ```python 输入:num1 = "11", num2 = "123" 输出:"134" ``` - 示例 2: ```python 输入:num1 = "456", num2 = "77" 输出:"533" ``` ## 解题思路 ### 思路 1:双指针 需要用字符串的形式来模拟大数加法。 加法的计算方式是:从个位数开始,由低位到高位,按位相加,如果相加之后超过 `10`,就需要向前进位。 模拟加法的做法是: 1. 用一个数组存储按位相加后的结果,每一位对应一位数。 2. 然后分别使用一个指针变量,对两个数 `num1`、`num2` 字符串进行反向遍历,将相加后的各个位置上的结果保存在数组中,这样计算完成之后就得到了一个按位反向的结果。 3. 最后返回结果的时候将数组反向转为字符串即可。 注意需要考虑 `num1`、`num2` 不等长的情况,让短的那个字符串对应位置按 $0$ 计算即可。 ### 思路 1:代码 ```python class Solution: def addStrings(self, num1: str, num2: str) -> str: # num1 位数 digit1 = len(num1) - 1 # num2 位数 digit2 = len(num2) - 1 # 进位 carry = 0 # sum 存储反向结果 sum = [] # 逆序相加 while carry > 0 or digit1 >= 0 or digit2 >= 0: # 获取对应位数上的数字 num1_d = int(num1[digit1]) if digit1 >= 0 else 0 num2_d = int(num2[digit2]) if digit2 >= 0 else 0 digit1 -= 1 digit2 -= 1 # 计算结果,存储,进位 num = num1_d+num2_d+carry sum.append('%d'%(num%10)) carry = num // 10 # 返回计算结果 return "".join(sum[::-1]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(max(m + n))$。其中 $m$ 是字符串 $num1$ 的长度,$n$ 是字符串 $num2$ 的长度。 - **空间复杂度**:$O(max(m + n))$。 ================================================ FILE: docs/solutions/0400-0499/add-two-numbers-ii.md ================================================ # [0445. 两数相加 II](https://leetcode.cn/problems/add-two-numbers-ii/) - 标签:栈、链表、数学 - 难度:中等 ## 题目链接 - [0445. 两数相加 II - 力扣](https://leetcode.cn/problems/add-two-numbers-ii/) ## 题目大意 给定两个非空链表的头节点 `l1` 和 `l2` 来代表两个非负整数。数字最高位位于链表开始位置。每个节点只储存一位数字。除了数字 `0` 之外,这两个链表代表的数字都不会以 `0` 开头。 要求:将这两个数相加会返回一个新的链表。 ## 解题思路 链表中最高位位于链表开始位置,最低位位于链表结束位置。这与我们做加法的数位顺序是相反的。为了将链表逆序,从而从低位开始处理数位,我们可以借用两个栈:将链表中所有数字分别压入两个栈中,再依次取出相加。 同时,在相加的时候,还要考虑进位问题。具体步骤如下: - 将链表 `l1` 中所有节点值压入 `stack1` 栈中,再将链表 `l2` 中所有节点值压入 `stack2` 栈中。 - 使用 `res` 存储新的结果链表,一开始指向 `None`,`carry` 记录进位。 - 如果 `stack1` 或 `stack2` 不为空,或着进位 `carry` 不为 `0`,则: - 从 `stack1` 中取出栈顶元素 `num1`,如果 `stack1` 为空,则 `num1 = 0`。 - 从 `stack2` 中取出栈顶元素 `num2`,如果 `stack2` 为空,则 `num2 = 0`。 - 计算相加结果,并计算进位。 - 建立新节点,存储进位后余下的值,并令其指向 `res`。 - `res` 指向新节点,继续判断。 - 如果 `stack1`、`stack2` 都为空,并且进位 `carry` 为 `0`,则输出 `res`。 ## 代码 ```python class Solution: def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode: stack1, stack2 = [], [] while l1: stack1.append(l1.val) l1 = l1.next while l2: stack2.append(l2.val) l2 = l2.next res = None carry = 0 while stack1 or stack2 or carry != 0: num1 = stack1.pop() if stack1 else 0 num2 = stack2.pop() if stack2 else 0 cur_sum = num1 + num2 + carry carry = cur_sum // 10 cur_sum %= 10 cur_node = ListNode(cur_sum) cur_node.next = res res = cur_node return res ``` ================================================ FILE: docs/solutions/0400-0499/all-oone-data-structure.md ================================================ # [0432. 全 O(1) 的数据结构](https://leetcode.cn/problems/all-oone-data-structure/) - 标签:设计、哈希表、链表、双向链表 - 难度:困难 ## 题目链接 - [0432. 全 O(1) 的数据结构 - 力扣](https://leetcode.cn/problems/all-oone-data-structure/) ## 题目大意 **要求**: 设计一个用于存储字符串计数的数据结构,并能够返回计数最小和最大的字符串。 实现 `AllOne` 类: - `AllOne()` 初始化数据结构的对象。 - `inc(String key)` 字符串 $key$ 的计数增加 $1$。如果数据结构中尚不存在 $key$,那么插入计数为 $1$ 的 $key$ 。 - `dec(String key)` 字符串 $key$ 的计数减少 $1$。如果 $key$ 的计数在减少后为 $0$,那么需要将这个 $key$ 从数据结构中删除。测试用例保证:在减少计数前,$key$ 存在于数据结构中。 - `getMaxKey()` 返回任意一个计数最大的字符串。如果没有元素存在,返回一个空字符串 `""`。 - `getMinKey()` 返回任意一个计数最小的字符串。如果没有元素存在,返回一个空字符串 `""`。 **说明**: - 注意:每个函数都应当满足 $O(1)$ 平均时间复杂度。 - $1 \le key.length \le 10$。 - $key$ 由小写英文字母组成。 - 测试用例保证:在每次调用 `dec` 时,数据结构中总存在 $key$。 - 最多调用 `inc`、`dec`、`getMaxKey` 和 `getMinKey` 方法 $5 \times 10^{4}$ 次。 **示例**: - 示例 1: ```python 输入 ["AllOne", "inc", "inc", "getMaxKey", "getMinKey", "inc", "getMaxKey", "getMinKey"] [[], ["hello"], ["hello"], [], [], ["leet"], [], []] 输出 [null, null, null, "hello", "hello", null, "hello", "leet"] 解释 AllOne allOne = new AllOne(); allOne.inc("hello"); allOne.inc("hello"); allOne.getMaxKey(); // 返回 "hello" allOne.getMinKey(); // 返回 "hello" allOne.inc("leet"); allOne.getMaxKey(); // 返回 "hello" allOne.getMinKey(); // 返回 "leet" ``` ## 解题思路 ### 思路 1:哈希表 + 双向链表 1. 我们需要设计一个数据结构,支持 $O(1)$ 时间复杂度的 `inc`、`dec`、`getMaxKey` 和 `getMinKey` 操作。 2. 使用哈希表 $key\_to\_node$ 存储每个字符串 $key$ 对应的节点,实现 $O(1)$ 的查找。 3. 使用双向链表维护计数有序的节点,每个节点存储相同计数的所有字符串。 4. 维护双向链表的头尾指针,头节点存储最小计数,尾节点存储最大计数。 5. 对于 `inc(key)` 操作: - 如果 $key$ 不存在,创建计数为 $1$ 的节点。 - 如果 $key$ 存在,将其从当前计数节点移除,添加到计数 $+1$ 的节点中。 - 如果当前计数节点为空,删除该节点。 6. 对于 `dec(key)` 操作: - 将 $key$ 从当前计数节点移除,添加到计数 $-1$ 的节点中。 - 如果计数为 $0$,从哈希表中删除 $key$。 - 如果当前计数节点为空,删除该节点。 7. `getMaxKey()` 和 `getMinKey()` 分别返回尾节点和头节点中的任意一个字符串。 ### 思路 1:代码 ```python class Node: """双向链表节点""" def __init__(self, count=0): self.count = count # 计数 self.keys = set() # 存储相同计数的所有字符串 self.prev = None # 前驱节点 self.next = None # 后继节点 class AllOne: def __init__(self): # 哈希表:key -> node self.key_to_node = {} # 双向链表头尾哨兵节点 self.head = Node() # 头节点(最小计数) self.tail = Node() # 尾节点(最大计数) self.head.next = self.tail self.tail.prev = self.head def inc(self, key: str) -> None: """增加 key 的计数""" if key in self.key_to_node: # key 已存在,移动到下一个计数节点 node = self.key_to_node[key] self._move_key(key, node, node.count + 1) else: # key 不存在,创建计数为 1 的节点 self._add_key(key, 1) def dec(self, key: str) -> None: """减少 key 的计数""" node = self.key_to_node[key] if node.count == 1: # 计数为 1,直接删除 self._remove_key(key) else: # 移动到前一个计数节点 self._move_key(key, node, node.count - 1) def getMaxKey(self) -> str: """返回计数最大的任意一个字符串""" if self.tail.prev == self.head: return "" # 返回尾节点的前一个节点中的任意一个字符串 return next(iter(self.tail.prev.keys)) def getMinKey(self) -> str: """返回计数最小的任意一个字符串""" if self.head.next == self.tail: return "" # 返回头节点的下一个节点中的任意一个字符串 return next(iter(self.head.next.keys)) def _add_key(self, key: str, count: int) -> None: """添加新的 key""" # 查找或创建计数为 count 的节点 if self.head.next != self.tail and self.head.next.count == count: # 头节点的下一个节点计数正好是 count node = self.head.next else: # 需要创建新节点 node = Node(count) self._insert_after(self.head, node) # 将 key 添加到节点中 node.keys.add(key) self.key_to_node[key] = node def _remove_key(self, key: str) -> None: """删除 key""" node = self.key_to_node[key] node.keys.remove(key) del self.key_to_node[key] # 如果节点为空,删除节点 if not node.keys: self._remove_node(node) def _move_key(self, key: str, from_node: Node, to_count: int) -> None: """将 key 从一个计数节点移动到另一个计数节点""" # 从原节点移除 from_node.keys.remove(key) # 查找或创建目标计数节点 to_node = None if to_count == from_node.count + 1: # 移动到下一个节点 if from_node.next != self.tail and from_node.next.count == to_count: to_node = from_node.next else: to_node = Node(to_count) self._insert_after(from_node, to_node) else: # 移动到前一个节点 if from_node.prev != self.head and from_node.prev.count == to_count: to_node = from_node.prev else: to_node = Node(to_count) self._insert_after(from_node.prev, to_node) # 添加到目标节点 to_node.keys.add(key) self.key_to_node[key] = to_node # 如果原节点为空,删除原节点 if not from_node.keys: self._remove_node(from_node) def _insert_after(self, node: Node, new_node: Node) -> None: """在 node 后面插入 new_node""" new_node.prev = node new_node.next = node.next node.next.prev = new_node node.next = new_node def _remove_node(self, node: Node) -> None: """删除节点""" node.prev.next = node.next node.next.prev = node.prev # Your AllOne object will be instantiated and called as such: # obj = AllOne() # obj.inc(key) # obj.dec(key) # param_3 = obj.getMaxKey() # param_4 = obj.getMinKey() ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `inc(key)`:$O(1)$,哈希表查找和双向链表操作都是 $O(1)$。 - `dec(key)`:$O(1)$,哈希表查找和双向链表操作都是 $O(1)$。 - `getMaxKey()`:$O(1)$,直接访问尾节点的前一个节点。 - `getMinKey()`:$O(1)$,直接访问头节点的下一个节点。 - **空间复杂度**:$O(n)$,其中 $n$ 是不同字符串的数量。哈希表和双向链表都需要 $O(n)$ 的空间。 ================================================ FILE: docs/solutions/0400-0499/arithmetic-slices-ii-subsequence.md ================================================ # [0446. 等差数列划分 II - 子序列](https://leetcode.cn/problems/arithmetic-slices-ii-subsequence/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [0446. 等差数列划分 II - 子序列 - 力扣](https://leetcode.cn/problems/arithmetic-slices-ii-subsequence/) ## 题目大意 **描述**: 给定一个整数数组 $nums$。 **要求**: 返回 $nums$ 中所有「等差子序列」的数目。 **说明**: - 如果一个序列中「至少有三个元素」,并且任意两个相邻元素之差相同,则称该序列为等差序列。 - 例如,$[1, 3, 5, 7, 9]$、$[7, 7, 7, 7]$ 和 $[3, -1, -5, -9]$ 都是等差序列。 - 再例如,$[1, 1, 2, 5, 7]$ 不是等差序列。 - 数组中的子序列是从数组中删除一些元素(也可能不删除)得到的一个序列。 - 例如,$[2,5,10]$ 是 $[1,2,1,2,4,1,5,10]$ 的一个子序列。 - 题目数据保证答案是一个 32-bit 整数。 - $1 \le nums.length \le 10^{3}$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:nums = [2,4,6,8,10] 输出:7 解释:所有的等差子序列为: [2,4,6] [4,6,8] [6,8,10] [2,4,6,8] [4,6,8,10] [2,4,6,8,10] [2,6,10] ``` - 示例 2: ```python 输入:nums = [7,7,7,7,7] 输出:16 解释:数组中的任意子序列都是等差子序列。 ``` ## 解题思路 ### 思路 1:动态规划 1. 我们需要统计所有长度至少为 $3$ 的等差子序列的数目。 2. 使用动态规划,定义 $dp[i][d]$ 表示以位置 $i$ 结尾,公差为 $d$ 的等差子序列的数目。 3. 对于每个位置 $i$,我们遍历所有前面的位置 $j$($j < i$),计算公差 $d = nums[i] - nums[j]$。 4. 如果 $dp[j][d]$ 存在,说明以 $j$ 结尾、公差为 $d$ 的等差子序列可以扩展到 $i$,所以 $dp[i][d] += dp[j][d] + 1$。 5. 这里 $+1$ 是因为 $[nums[j], nums[i]]$ 本身就是一个长度为 $2$ 的序列,可以形成新的等差子序列。 6. 最终答案是所有 $dp[i][d]$ 中长度至少为 $3$ 的等差子序列数目的总和。 ### 思路 1:代码 ```python class Solution: def numberOfArithmeticSlices(self, nums: List[int]) -> int: n = len(nums) if n < 3: return 0 # dp[i][d] 表示以位置 i 结尾,公差为 d 的等差子序列的数目 dp = [{} for _ in range(n)] result = 0 # 遍历所有位置 for i in range(n): # 遍历所有前面的位置 for j in range(i): # 计算公差 diff = nums[i] - nums[j] # 如果 j 位置存在以 diff 为公差的等差子序列 if diff in dp[j]: # 可以扩展到 i 位置,形成新的等差子序列 dp[i][diff] = dp[i].get(diff, 0) + dp[j][diff] + 1 # 累加到结果中(dp[j][diff] 表示长度至少为 3 的等差子序列) result += dp[j][diff] else: # 如果 j 位置不存在以 diff 为公差的等差子序列 # 那么 [nums[j], nums[i]] 是一个长度为 2 的序列 dp[i][diff] = dp[i].get(diff, 0) + 1 return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是数组的长度。需要遍历所有位置对 $(i, j)$,其中 $j < i$。 - **空间复杂度**:$O(n^2)$,其中 $n$ 是数组的长度。最坏情况下,每个位置可能对应 $O(n)$ 个不同的公差,所以总空间复杂度为 $O(n^2)$。 ================================================ FILE: docs/solutions/0400-0499/arithmetic-slices.md ================================================ # [0413. 等差数列划分](https://leetcode.cn/problems/arithmetic-slices/) - 标签:数组、动态规划、滑动窗口 - 难度:中等 ## 题目链接 - [0413. 等差数列划分 - 力扣](https://leetcode.cn/problems/arithmetic-slices/) ## 题目大意 **描述**: 给定一个整数数组 $nums$。 **要求**: 返回数组 $nums$ 中所有为等差数组的「子数组」个数。 **说明**: - 如果一个数列「至少有三个元素」,并且任意两个相邻元素之差相同,则称该数列为等差数列。 - 例如,$[1,3,5,7,9]$、$[7,7,7,7] $和 $[3,-1,-5,-9]$ 都是等差数列。 - 「子数组」是数组中的一个连续序列。 - $1 \le nums.length \le 5000$。 - $-10^{3} \le nums[i] \le 10^{3}$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,4] 输出:3 解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。 ``` - 示例 2: ```python 输入:nums = [1] 输出:0 ``` ## 解题思路 ### 思路 1:动态规划 1. 我们需要统计所有长度至少为 $3$ 的等差子数组的个数。 2. 使用动态规划,定义 $dp[i]$ 表示以位置 $i$ 结尾的等差子数组的个数。 3. 对于每个位置 $i$,如果 $nums[i] - nums[i-1] = nums[i-1] - nums[i-2]$,说明当前位置可以延续前面的等差子数组。 4. 如果满足等差条件,则 $dp[i] = dp[i-1] + 1$,其中 $+1$ 表示新增的长度为 $3$ 的等差子数组 $[nums[i-2], nums[i-1], nums[i]]$。 5. 如果不满足等差条件,则 $dp[i] = 0$。 6. 最终答案是所有 $dp[i]$ 的总和。 ### 思路 1:代码 ```python class Solution: def numberOfArithmeticSlices(self, nums: List[int]) -> int: n = len(nums) if n < 3: return 0 # dp[i] 表示以位置 i 结尾的等差子数组的个数 dp = [0] * n result = 0 # 从位置 2 开始遍历(因为至少需要 3 个元素) for i in range(2, n): # 检查是否满足等差条件 if nums[i] - nums[i-1] == nums[i-1] - nums[i-2]: # 可以延续前面的等差子数组 dp[i] = dp[i-1] + 1 # 累加到结果中 result += dp[i] else: # 不满足等差条件,重置为 0 dp[i] = 0 return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。只需要遍历一次数组。 - **空间复杂度**:$O(n)$,其中 $n$ 是数组的长度。使用了长度为 $n$ 的 $dp$ 数组。 ================================================ FILE: docs/solutions/0400-0499/arranging-coins.md ================================================ # [0441. 排列硬币](https://leetcode.cn/problems/arranging-coins/) - 标签:数学、二分查找 - 难度:简单 ## 题目链接 - [0441. 排列硬币 - 力扣](https://leetcode.cn/problems/arranging-coins/) ## 题目大意 **描述**: 你总共有 $n$ 枚硬币,并计划将它们按阶梯状排列。对于一个由 $k$ 行组成的阶梯,其第 $i$ 行必须正好有 $i$ 枚硬币。阶梯的最后一行 可能 是不完整的。 给定一个数字 $n$。 **要求**: 计算并返回可形成「完整阶梯行」的总行数。 **说明**: - $1 \le n \le 2^{31} - 1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/09/arrangecoins1-grid.jpg) ```python 输入:n = 5 输出:2 解释:因为第三行不完整,所以返回 2 。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/04/09/arrangecoins2-grid.jpg) ```python 输入:n = 8 输出:3 解释:因为第四行不完整,所以返回 3 。 ``` ## 解题思路 ### 思路 1:数学公式 1. 我们需要找到最大的完整行数 $k$,使得前 $k$ 行使用的硬币总数不超过 $n$。 2. 前 $k$ 行使用的硬币总数为:$1 + 2 + 3 + \cdots + k = \frac{k(k+1)}{2}$。 3. 我们需要找到最大的 $k$,使得 $\frac{k(k+1)}{2} \leq n$。 4. 解这个不等式:$k^2 + k - 2n \leq 0$。 5. 使用求根公式:$k = \frac{-1 + \sqrt{1 + 8n}}{2}$。 6. 由于 $k$ 必须是正整数,我们取 $\lfloor \frac{-1 + \sqrt{1 + 8n}}{2} \rfloor$。 ### 思路 1:代码 ```python class Solution: def arrangeCoins(self, n: int) -> int: # 使用数学公式直接计算 # k = floor((-1 + sqrt(1 + 8n)) / 2) return int((-1 + math.sqrt(1 + 8 * n)) // 2) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$,只需要一次数学计算。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/assign-cookies.md ================================================ # [0455. 分发饼干](https://leetcode.cn/problems/assign-cookies/) - 标签:贪心、数组、双指针、排序 - 难度:简单 ## 题目链接 - [0455. 分发饼干 - 力扣](https://leetcode.cn/problems/assign-cookies/) ## 题目大意 **描述**:一位很棒的家长为孩子们分发饼干。对于每个孩子 `i`,都有一个胃口值 `g[i]`,即每个小孩希望得到饼干的最小尺寸值。对于每块饼干 `j`,都有一个尺寸值 `s[j]`。只有当 `s[j] > g[i]` 时,我们才能将饼干 `j` 分配给孩子 `i`。每个孩子最多只能给一块饼干。 现在给定代表所有孩子胃口值的数组 `g` 和代表所有饼干尺寸的数组 `j`。 **要求**:尽可能满足越多数量的孩子,并求出这个最大数值。 **说明**: - $1 \le g.length \le 3 * 10^4$。 - $0 \le s.length \le 3 * 10^4$。 - $1 \le g[i], s[j] \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:g = [1,2,3], s = [1,1] 输出:1 解释:你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1, 2, 3。虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。所以应该输出 1。 ``` - 示例 2: ```python 输入: g = [1,2], s = [1,2,3] 输出: 2 解释: 你有两个孩子和三块小饼干,2个孩子的胃口值分别是1, 2。你拥有的饼干数量和尺寸都足以让所有孩子满足。所以你应该输出 2。 ``` ## 解题思路 ### 思路 1:贪心算法 为了尽可能的满⾜更多的⼩孩,而且一块饼干不能掰成两半,所以我们应该尽量让胃口小的孩子吃小块饼干,这样胃口大的孩子才有大块饼干吃。 所以,从贪心算法的角度来考虑,我们应该按照孩子的胃口从小到大对数组 `g` 进行排序,然后按照饼干的尺寸大小从小到大对数组 `s` 进行排序,并且对于每个孩子,应该选择满足这个孩子的胃口且尺寸最小的饼干。 下面我们使用贪心算法三步走的方法解决这道题。 1. **转换问题**:将原问题转变为,当胃口最小的孩子选择完满足这个孩子的胃口且尺寸最小的饼干之后,再解决剩下孩子的选择问题(子问题)。 2. **贪心选择性质**:对于当前孩子,用尺寸尽可能小的饼干满足这个孩子的胃口。 3. **最优子结构性质**:在上面的贪心策略下,当前孩子的贪心选择 + 剩下孩子的子问题最优解,就是全局最优解。也就是说在贪心选择的方案下,能够使得满足胃口的孩子数量达到最大。 使用贪心算法的代码解决步骤描述如下: 1. 对数组 `g`、`s` 进行从小到大排序,使用变量 `index_g` 和 `index_s` 分别指向 `g`、`s` 初始位置,使用变量 `res` 保存结果,初始化为 `0`。 2. 对比每个元素 `g[index_g]` 和 `s[index_s]`: 1. 如果 `g[index_g] <= s[index_s]`,说明当前饼干满足当前孩子胃口,则答案数量加 `1`,并且向右移动 `index_g` 和 `index_s`。 2. 如果 `g[index_g] > s[index_s]`,说明当前饼干无法满足当前孩子胃口,则向右移动 `index_s`,判断下一块饼干是否可以满足当前孩子胃口。 3. 遍历完输出答案 `res`。 ### 思路 1:代码 ```python class Solution: def findContentChildren(self, g: List[int], s: List[int]) -> int: g.sort() s.sort() index_g, index_s = 0, 0 res = 0 while index_g < len(g) and index_s < len(s): if g[index_g] <= s[index_s]: res += 1 index_g += 1 index_s += 1 else: index_s += 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times \log m + n \times \log n)$,其中 $m$ 和 $n$ 分别是数组 $g$ 和 $s$ 的长度。 - **空间复杂度**:$O(\log m + \log n)$。 ================================================ FILE: docs/solutions/0400-0499/battleships-in-a-board.md ================================================ # [0419. 棋盘上的战舰](https://leetcode.cn/problems/battleships-in-a-board/) - 标签:深度优先搜索、数组、矩阵 - 难度:中等 ## 题目链接 - [0419. 棋盘上的战舰 - 力扣](https://leetcode.cn/problems/battleships-in-a-board/) ## 题目大意 **描述**: 给定一个大小为 $m \times n$ 的矩阵 $board$ 表示棋盘,其中,每个单元格可以是一艘战舰 `'X'` 或者是一个空位 `'.'`。 「舰队」只能水平或者垂直放置在 $board$ 上。换句话说,舰队只能按 $1 \times k$($1$ 行,$k$ 列)或 $k \times 1$($k$ 行,$1$ 列)的形状放置,其中 $k$ 可以是任意大小。两个舰队之间至少有一个水平或垂直的空格分隔 (即没有相邻的舰队)。 **要求**: 返回在棋盘 $board$ 上放置的「舰队」的数量。 **说明**: - $m == board.length$。 - $n == board[i].length$。 - $1 \le m, n \le 200$。 - $board[i][j]$ 是 `'.'` 或 `'X'`。 - 进阶:你可以实现一次扫描算法,并只使用 $O(1)$ 额外空间,并且不修改 $board$ 的值来解决这个问题吗? **示例**: - 示例 1: ![](https://pic.leetcode.cn/1719200420-KKnzye-image.png) ```python 输入:board = [["X",".",".","X"],[".",".",".","X"],[".",".",".","X"]] 输出:2 ``` - 示例 2: ```python 输入:board = [["."]] 输出:0 ``` ## 解题思路 ### 思路 1:一次扫描 + 左上角检测 1. 由于战舰只能水平或垂直放置,且战舰之间不相邻,我们可以通过检测战舰的"头部"来统计战舰数量。 2. 战舰的"头部"是指战舰最左上角的 `'X'` 位置。对于水平战舰,头部是同一行中最左边的 `'X'`;对于垂直战舰,头部是同一列中最上面的 `'X'`。 3. 我们可以通过一次扫描整个棋盘,对于每个 `'X'` 位置 $(i, j)$,检查其左边 $(i, j-1)$ 和上边 $(i-1, j)$ 是否也是 `'X'`。 4. 如果左边或上边是 `'X'`,说明当前位置不是战舰头部,跳过;否则,当前位置是战舰头部,计数器加 $1$。 5. 这样我们只需要一次扫描就能统计出所有战舰的数量,时间复杂度为 $O(m \times n)$,空间复杂度为 $O(1)$。 ### 思路 1:代码 ```python class Solution: def countBattleships(self, board: List[List[str]]) -> int: m, n = len(board), len(board[0]) count = 0 # 遍历整个棋盘 for i in range(m): for j in range(n): # 如果当前位置是 'X' if board[i][j] == 'X': # 检查是否为战舰头部 # 战舰头部:左边和上边都不是 'X'(或者超出边界) is_head = True # 检查左边 if j > 0 and board[i][j-1] == 'X': is_head = False # 检查上边 if i > 0 and board[i-1][j] == 'X': is_head = False # 如果是战舰头部,计数器加 1 if is_head: count += 1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 是棋盘的行数,$n$ 是棋盘的列数。需要遍历整个棋盘一次。 - **空间复杂度**:$O(1)$,只使用了常数额外空间,没有使用额外的数据结构。 ================================================ FILE: docs/solutions/0400-0499/binary-watch.md ================================================ # [0401. 二进制手表](https://leetcode.cn/problems/binary-watch/) - 标签:位运算、回溯 - 难度:简单 ## 题目链接 - [0401. 二进制手表 - 力扣](https://leetcode.cn/problems/binary-watch/) ## 题目大意 **描述**: 二进制手表顶部有 $4$ 个 LED 代表「小时($0 \sim 11$)」,底部的 $6$ 个 LED 代表「分钟($0 \sim 59$)」。每个 LED 代表一个 $0$ 或 $1$,最低位在右侧。 - 例如,下面的二进制手表读取 `"4:51"`。 ![](https://assets.leetcode.com/uploads/2021/04/08/binarywatch.jpg) **要求**: 返回二进制手表可以表示的所有可能时间。你可以按任意顺序返回答案。 **说明**: - 小时不会以零开头: - 例如,`"01:00"` 是无效的时间,正确的写法应该是 `"1:00"`。 - 分钟必须由两位数组成,可能会以零开头: - 例如,`"10:2"` 是无效的时间,正确的写法应该是 `"10:02"`。 - $0 \le turnedOn \le 10$。 **示例**: - 示例 1: ```python 输入:turnedOn = 1 输出:["0:01","0:02","0:04","0:08","0:16","0:32","1:00","2:00","4:00","8:00"] ``` - 示例 2: ```python 输入:turnedOn = 9 输出:[] ``` ## 解题思路 ### 思路 1:枚举所有可能的时间 1. 二进制手表有 $4$ 个 LED 表示小时($0 \sim 11$),$6$ 个 LED 表示分钟($0 \sim 59$)。 2. 我们需要找到所有可能的 LED 组合,使得总共亮起的 LED 数量等于 $turnedOn$。 3. 对于每个可能的小时 $h$($0 \leq h \leq 11$),计算其二进制表示中 $1$ 的个数 $count\_hour$。 4. 对于每个可能的分钟 $m$($0 \leq m \leq 59$),计算其二进制表示中 $1$ 的个数 $count\_minute$。 5. 如果 $count\_hour + count\_minute = turnedOn$,则时间 $h:m$ 是一个有效解。 6. 将所有有效解格式化为字符串形式(小时不补零,分钟补零到两位数)。 ### 思路 1:代码 ```python class Solution: def readBinaryWatch(self, turnedOn: int) -> List[str]: result = [] # 枚举所有可能的小时 (0-11) for hour in range(12): # 计算小时对应的二进制中 1 的个数 hour_ones = bin(hour).count('1') # 枚举所有可能的分钟 (0-59) for minute in range(60): # 计算分钟对应的二进制中 1 的个数 minute_ones = bin(minute).count('1') # 如果总 LED 数量等于 turnedOn,则是一个有效时间 if hour_ones + minute_ones == turnedOn: # 格式化时间:小时不补零,分钟补零到两位数 time_str = f"{hour}:{minute:02d}" result.append(time_str) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(12 \times 60) = O(720)$,需要枚举所有可能的小时和分钟组合。 - **空间复杂度**:$O(k)$,其中 $k$ 是结果的数量,最多为 $720$ 个时间字符串。 ================================================ FILE: docs/solutions/0400-0499/can-i-win.md ================================================ # [0464. 我能赢吗](https://leetcode.cn/problems/can-i-win/) - 标签:位运算、记忆化搜索、数学、动态规划、状态压缩、博弈 - 难度:中等 ## 题目链接 - [0464. 我能赢吗 - 力扣](https://leetcode.cn/problems/can-i-win/) ## 题目大意 **描述**:给定两个整数,$maxChoosableInteger$ 表示可以选择的最大整数,$desiredTotal$ 表示累计和。现在开始玩一个游戏,两个玩家轮流从 $1 \sim maxChoosableInteger$ 中不重复的抽取一个整数,直到累积整数和大于等于 $desiredTotal$ 时,这个人就赢得比赛。假设两位玩家玩游戏时都表现最佳。 **要求**:判断先出手的玩家是否能够稳赢,如果能稳赢,则返回 `True`,否则返回 `False`。 **说明**: - $1 \le maxChoosableInteger \le 20$。 - $0 \le desiredTotal \le 300$。 **示例**: - 示例 1: ```python 输入:maxChoosableInteger = 10, desiredTotal = 11 输出:False 解释: 无论第一个玩家选择哪个整数,他都会失败。 第一个玩家可以选择从 1 到 10 的整数。 如果第一个玩家选择 1,那么第二个玩家只能选择从 2 到 10 的整数。 第二个玩家可以通过选择整数 10(那么累积和为 11 >= desiredTotal),从而取得胜利. 同样地,第一个玩家选择任意其他整数,第二个玩家都会赢。 ``` - 示例 2: ```python 输入:maxChoosableInteger = 10, desiredTotal = 0 输出:True ``` ## 解题思路 ### 思路 1:状态压缩 + 记忆化搜索 $maxChoosableInteger$ 的区间范围是 $[1, 20]$,数据量不是很大,我们可以使用状态压缩来判断当前轮次中数字的选取情况。 题目假设两位玩家玩游戏时都表现最佳,则每个人都会尽力去赢,在每轮次中,每个人都会分析此次选择后,对后续轮次的影响,判断自己是必赢还是必输。 1. 如果当前轮次选择某个数之后,自己一定会赢时,才会选择这个数。 2. 如果当前轮次无论选择哪个数,自己一定会输时,那无论选择哪个数其实都已经无所谓了。 这样我们可以定义一个递归函数 `dfs(state, curTotal)`,用于判断处于状态 $state$,并且当前累计和为 $curTotal$ 时,自己是否一定会赢。如果自己一定会赢,返回 `True`,否则返回 `False`。递归函数内容如下: 1. 从 $1 \sim maxChoosableInteger$ 中选择一个之前没有选过的数 $k$。 2. 如果选择的数 $k$ 加上当前的整数和 $curTotal$ 之后大于等于 $desiredTotal$,则自己一定会赢。 3. 如果选择的数 $k$ 之后,对方必输(即递归调用 `dfs(state | (1 << (k - 1)), curTotal + k)` 为 `Flase` 时),则自己一定会赢。 4. 如果无论选择哪个数,自己都赢不了,则自己必输,返回 `False`。 这样,我们从 $state = 0, curTotal = 0$ 开始调用递归方法 `dfs(state, curTotal)`,即可判断先出手的玩家是否能够稳赢。 接下来,我们还需要考虑一些边界条件。 1. 当 $maxChoosableInteger$ 直接大于等于 $desiredTotal$,则先手玩家无论选什么,直接就赢了,这种情况下,我们直接返回 `True`。 2. 当 $1 \sim maxChoosableInteger$ 中所有数加起来都小于 $desiredTotal$,则先手玩家无论怎么选,都无法稳赢,题目要求我们判断先出手的玩家是否能够稳赢,既然先手无法稳赢,我们直接返回 `False`。 ### 思路 1:代码 ```python class Solution: def canIWin(self, maxChoosableInteger: int, desiredTotal: int) -> bool: @cache def dfs(state, curTotal): for k in range(1, maxChoosableInteger + 1): # 从 1 ~ maxChoosableInteger 中选择一个数 if state >> (k - 1) & 1 != 0: # 如果之前选过该数则跳过 continue if curTotal + k >= desiredTotal: # 如果选择了 k,累积整数和大于等于 desiredTotal,则该玩家一定赢 return True if not dfs(state | (1 << (k - 1)), curTotal + k): # 如果当前选择了 k 之后,对手一定输,则当前玩家一定赢 return True return False # 以上都赢不了的话,当前玩家一定输 # maxChoosableInteger 直接大于等于 desiredTotal,则先手玩家一定赢 if maxChoosableInteger >= desiredTotal: return True # 1 ~ maxChoosableInteger 所有数加起来都不够 desiredTotal,则先手玩家一定输 if (1 + maxChoosableInteger) * maxChoosableInteger // 2 < desiredTotal: return False return dfs(0, 0) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 2^n)$,其中 $n$ 为 $maxChoosableInteger$。 - **空间复杂度**:$O(2^n)$。 ================================================ FILE: docs/solutions/0400-0499/circular-array-loop.md ================================================ # [0457. 环形数组是否存在循环](https://leetcode.cn/problems/circular-array-loop/) - 标签:数组、哈希表、双指针 - 难度:中等 ## 题目链接 - [0457. 环形数组是否存在循环 - 力扣](https://leetcode.cn/problems/circular-array-loop/) ## 题目大意 **描述**: 存在一个不含 $0$ 的「环形」数组 $nums$,每个 $nums[i]$ 都表示位于下标 $i$ 的角色应该向前或向后移动的下标个数: - 如果 $nums[i]$ 是正数,向前(下标递增方向)移动 $|nums[i]|$ 步。 - 如果 $nums[i]$ 是负数,向后(下标递减方向)移动 $|nums[i]|$ 步。 因为数组是「环形」的,所以可以假设从最后一个元素向前移动一步会到达第一个元素,而第一个元素向后移动一步会到达最后一个元素。 数组中的「循环」由长度为 $k$ 的下标序列 $seq$ 标识: - 遵循上述移动规则将导致一组重复下标序列 $seq[0] \rightarrow seq[1] \rightarrow ... \rightarrow seq[k - 1] \rightarrow seq[0] \rightarrow ...$。 - 所有 $nums[seq[j]]$ 应当不是「全正」就是「全负」。 - $k > 1$。 **要求**: 如果 $nums$ 中存在循环,返回 $true$;否则,返回 $false$。 **说明**: - $1 \le nums.length \le 5000$。 - $-10^{3} \le nums[i] \le 10^{3}$。 - $nums[i] \ne 0$。 - 进阶:你能设计一个时间复杂度为 $O(n)$ 且额外空间复杂度为 $O(1)$ 的算法吗? **示例**: - 示例 1: ![](https://pic.leetcode.cn/1723688159-qYjpWT-image.png) ```python 输入:nums = [2,-1,1,2,2] 输出:true 解释:图片展示了节点间如何连接。白色节点向前跳跃,而红色节点向后跳跃。 我们可以看到存在循环,按下标 0 -> 2 -> 3 -> 0 --> ...,并且其中的所有节点都是白色(以相同方向跳跃)。 ``` - 示例 2: ![](https://pic.leetcode.cn/1723688183-lRSkjp-image.png) ```python 输入:nums = [-1,-2,-3,-4,-5,6] 输出:false 解释:图片展示了节点间如何连接。白色节点向前跳跃,而红色节点向后跳跃。 唯一的循环长度为 1,所以返回 false。 ``` ## 解题思路 ### 思路 1:快慢指针检测 1. 使用快慢指针(Floyd 判圈算法)来检测循环。设置两个指针 $slow$ 和 $fast$,初始都指向同一个位置。 2. 慢指针每次移动 $1$ 步,快指针每次移动 $2$ 步。 3. 根据数组元素的值计算下一个位置:$next = (current + nums[current]) \bmod n$,如果结果为负数,需要加上 $n$ 来保证在有效范围内。 4. 在每次移动前需要检查: - 下一个位置的元素符号必须与起始方向相同(全正或全负)。 - 不能出现自环(当前位置的下一个位置就是自己)。 5. 如果快慢指针相遇,说明存在有效循环,返回 $true$。 6. 对于每个起始位置都进行检测,如果找到一个有效循环就返回 $true$,否则继续检测下一个位置。 ### 思路 1:代码 ```python class Solution: def circularArrayLoop(self, nums: List[int]) -> bool: n = len(nums) def getNext(cur): """计算下一个位置""" return (cur + nums[cur]) % n # 遍历每个位置作为起始点 for i in range(n): # 如果当前位置已经被访问过,跳过 if nums[i] == 0: continue # 记录当前路径的方向(true 为正,false 为负) direction = nums[i] > 0 # 使用快慢指针检测循环 slow = i fast = i # 移动快慢指针直到相遇或违反条件 while True: # 慢指针移动一步前,检查下一个位置的符号和自环 slow_next = getNext(slow) if (nums[slow_next] > 0) != direction or slow == slow_next: break slow = slow_next # 快指针移动两步,每步都需要检查 fast_next = getNext(fast) if (nums[fast_next] > 0) != direction or fast == fast_next: break fast = fast_next fast_next = getNext(fast) if (nums[fast_next] > 0) != direction or fast == fast_next: break fast = fast_next # 如果快慢指针相遇,说明存在有效循环 if slow == fast: return True # 将当前路径上的所有元素标记为已访问 # 这样可以避免重复检测 temp = i while nums[temp] != 0 and (nums[temp] > 0) == direction: next_pos = getNext(temp) nums[temp] = 0 # 标记为已访问 temp = next_pos return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。每个位置最多被访问一次,总的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(1)$,只使用了常数额外空间,没有使用额外的数据结构。 ================================================ FILE: docs/solutions/0400-0499/concatenated-words.md ================================================ # [0472. 连接词](https://leetcode.cn/problems/concatenated-words/) - 标签:深度优先搜索、字典树、数组、字符串、动态规划、排序 - 难度:困难 ## 题目链接 - [0472. 连接词 - 力扣](https://leetcode.cn/problems/concatenated-words/) ## 题目大意 **描述**: 给定一个「不含重复」单词的字符串数组 $words$。 **要求**: 请你找出并返回 $words$ 中的所有 连接词。 **说明**: - 连接词:一个完全由给定数组中的至少两个较短单词(不一定是不同的两个单词)组成的字符串。 - $1 \le words.length \le 10^{4}$。 - $1 \le words[i].length \le 30$。 - $words[i]$ 仅由小写英文字母组成。 - $words$ 中的所有字符串都是唯一的。 - $1 \le sum(words[i].length) \le 10^{5}$。 **示例**: - 示例 1: ```python 输入:words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"] 输出:["catsdogcats","dogcatsdog","ratcatdogcat"] 解释:"catsdogcats" 由 "cats", "dog" 和 "cats" 组成; "dogcatsdog" 由 "dog", "cats" 和 "dog" 组成; "ratcatdogcat" 由 "rat", "cat", "dog" 和 "cat" 组成。 ``` - 示例 2: ```python 输入:words = ["cat","dog","catdog"] 输出:["catdog"] ``` ## 解题思路 ### 思路 1:深度优先搜索 + 记忆化 1. 首先将 $words$ 数组按长度排序,这样可以确保在处理较长的单词时,所有较短的单词都已经被处理过。 2. 对于每个单词 $word$,使用深度优先搜索来判断它是否可以由其他单词组成。 3. 定义函数 `canForm(word, wordSet)` 来判断单词 $word$ 是否可以由集合 $wordSet$ 中的单词组成。 4. 在 `canForm` 函数中,使用记忆化来避免重复计算。定义 $memo[i]$ 表示从位置 $i$ 开始的后缀是否可以由其他单词组成。 5. 对于每个位置 $i$,尝试所有可能的前缀 $word[i:j+1]$,如果前缀在 $wordSet$ 中,则递归检查后缀 $word[j+1:]$。 6. 如果找到任何一个有效的前缀和后缀组合,则返回 $True$。 7. 将可以组成的单词添加到结果列表中。 ### 思路 1:代码 ```python class Solution: def findAllConcatenatedWordsInADict(self, words: List[str]) -> List[str]: # 按长度排序,确保处理长单词时短单词已经处理过 words.sort(key=len) result = [] word_set = set() def canForm(word, wordSet): """判断单词是否可以由集合中的其他单词组成""" if not word: return True # 记忆化数组,memo[i] 表示从位置 i 开始的后缀是否可以组成 memo = {} def dfs(start): if start in memo: return memo[start] if start == len(word): return True # 尝试所有可能的前缀 for end in range(start + 1, len(word) + 1): prefix = word[start:end] # 如果前缀在集合中,且不是当前单词本身 if prefix in wordSet and prefix != word: if dfs(end): memo[start] = True return True memo[start] = False return False return dfs(0) # 逐个处理每个单词 for word in words: if canForm(word, word_set): result.append(word) # 将当前单词添加到集合中,供后续单词使用 word_set.add(word) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m^3)$,其中 $n$ 是单词的数量,$m$ 是单词的平均长度。对于每个单词,最坏情况下需要检查所有可能的前缀和后缀组合。 - **空间复杂度**:$O(n \times m)$,其中 $n$ 是单词的数量,$m$ 是单词的平均长度。主要用于存储单词集合和记忆化数组。 ================================================ FILE: docs/solutions/0400-0499/construct-quad-tree.md ================================================ # [0427. 建立四叉树](https://leetcode.cn/problems/construct-quad-tree/) - 标签:树、数组、分治、矩阵 - 难度:中等 ## 题目链接 - [0427. 建立四叉树 - 力扣](https://leetcode.cn/problems/construct-quad-tree/) ## 题目大意 **描述**: 给定一个 $n \times n$ 矩阵 $grid$ ,矩阵由若干 $0$ 和 $1$ 组成。 **要求**: 请你用四叉树表示该矩阵 $grid$。 你需要返回能表示矩阵 $grid$ 的四叉树的根结点。 四叉树数据结构中,每个内部节点只有四个子节点。此外,每个节点都有两个属性: - $val$:储存叶子结点所代表的区域的值。$1$ 对应 $True$,$0$ 对应 $False$。注意,当 $isLeaf$ 为 $False$ 时,你可以把 $True$ 或者 $False$ 赋值给节点,两种值都会被判题机制 接受 。 - $isLeaf$: 当这个节点是一个叶子结点时为 $True$,如果它有 $4$ 个子节点则为 $False$。 ```Java class Node { public boolean val; public boolean isLeaf; public Node topLeft; public Node topRight; public Node bottomLeft; public Node bottomRight; } ``` 我们可以按以下步骤为二维区域构建四叉树: 1. 如果当前网格的值相同(即,全为 0 或者全为 1),将 $isLeaf$ 设为 True ,将 $val$ 设为网格相应的值,并将四个子节点都设为 $Null$ 然后停止。 2. 如果当前网格的值不同,将 $isLeaf$ 设为 False, 将 $val$ 设为任意值,然后如下图所示,将当前网格划分为四个子网格。 3. 使用适当的子网格递归每个子节点。 ![](https://assets.leetcode.com/uploads/2020/02/11/new_top.png) 如果你想了解更多关于四叉树的内容,可以参考 [百科](https://baike.baidu.com/item/%E5%9B%9B%E5%8F%89%E6%A0%91)。 四叉树格式: 你不需要阅读本节来解决这个问题。只有当你想了解输出格式时才会这样做。输出为使用层序遍历后四叉树的序列化形式,其中 $null$ 表示路径终止符,其下面不存在节点。 它与二叉树的序列化非常相似。唯一的区别是节点以列表形式表示 $[isLeaf, val]$。 如果 $isLeaf$ 或者 $val$ 的值为 $True$,则表示它在列表 $[isLeaf, val]$ 中的值为 $1$;如果 $isLeaf$ 或者 $val$ 的值为 $False$ ,则表示值为 $0$。 **说明**: - $n == grid.length == grid[i].length$。 - $n == 2^x$ 其中 $0 \le x \le 6$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/02/11/grid1.png) ```python 输入:grid = [[0,1],[1,0]] 输出:[[0,1],[1,0],[1,1],[1,1],[1,0]] 解释:此示例的解释如下: 请注意,在下面四叉树的图示中,0 表示 false,1 表示 True 。 ``` ![](https://assets.leetcode.com/uploads/2020/02/12/e1tree.png) - 示例 2: ![](https://assets.leetcode.com/uploads/2020/02/12/e2mat.png) ```python 输入:grid = [[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0]] 输出:[[0,1],[1,1],[0,1],[1,1],[1,0],null,null,null,null,[1,0],[1,0],[1,1],[1,1]] 解释:网格中的所有值都不相同。我们将网格划分为四个子网格。 topLeft,bottomLeft 和 bottomRight 均具有相同的值。 topRight 具有不同的值,因此我们将其再分为 4 个子网格,这样每个子网格都具有相同的值。 解释如下图所示: ``` ![](https://assets.leetcode.com/uploads/2020/02/12/e2tree.png) ## 解题思路 ### 思路 1:递归分治 1. 四叉树的构建是一个典型的递归分治问题。对于给定的 $n \times n$ 矩阵,我们需要递归地将其划分为四个子区域。 2. 定义递归函数 $constructQuadTree(grid, row, col, size)$,其中 $row$ 和 $col$ 表示当前子矩阵的左上角坐标,$size$ 表示子矩阵的边长。 3. 对于当前子矩阵,首先检查所有元素是否相同: - 如果所有元素都相同(全为 $0$ 或全为 $1$),则创建一个叶子节点,$isLeaf = True$,$val$ 为对应的值。 - 如果元素不相同,则创建一个内部节点,$isLeaf = False$,$val$ 可以设为任意值,然后递归构建四个子节点。 4. 四个子节点的区域划分: - 左上角:$(row, col)$ 到 $(row + size/2 - 1, col + size/2 - 1)$ - 右上角:$(row, col + size/2)$ 到 $(row + size/2 - 1, col + size - 1)$ - 左下角:$(row + size/2, col)$ 到 $(row + size - 1, col + size/2 - 1)$ - 右下角:$(row + size/2, col + size/2)$ 到 $(row + size - 1, col + size - 1)$ 5. 递归终止条件:当 $size = 1$ 时,直接创建叶子节点。 ### 思路 1:代码 ```python """ # Definition for a QuadTree node. class Node: def __init__(self, val, isLeaf, topLeft, topRight, bottomLeft, bottomRight): self.val = val self.isLeaf = isLeaf self.topLeft = topLeft self.topRight = topRight self.bottomLeft = bottomLeft self.bottomRight = bottomRight """ class Solution: def construct(self, grid: List[List[int]]) -> 'Node': def constructQuadTree(row, col, size): # 检查当前子矩阵的所有元素是否相同 all_same = True first_val = grid[row][col] # 遍历当前子矩阵检查是否所有元素相同 for i in range(row, row + size): for j in range(col, col + size): if grid[i][j] != first_val: all_same = False break if not all_same: break # 如果所有元素相同,创建叶子节点 if all_same: return Node(first_val == 1, True, None, None, None, None) # 如果元素不相同,创建内部节点并递归构建子节点 half_size = size // 2 # 递归构建四个子节点 topLeft = constructQuadTree(row, col, half_size) topRight = constructQuadTree(row, col + half_size, half_size) bottomLeft = constructQuadTree(row + half_size, col, half_size) bottomRight = constructQuadTree(row + half_size, col + half_size, half_size) # 创建内部节点,val 可以设为任意值 return Node(True, False, topLeft, topRight, bottomLeft, bottomRight) n = len(grid) return constructQuadTree(0, 0, n) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \log n)$,其中 $n$ 是矩阵的边长。在最坏情况下,每个节点都需要检查其对应子矩阵的所有元素,总共有 $O(\log n)$ 层,每层需要检查 $O(n^2)$ 个元素。 - **空间复杂度**:$O(\log n)$,其中 $n$ 是矩阵的边长。递归调用栈的深度为 $O(\log n)$,因为每次递归都将矩阵大小减半。 ================================================ FILE: docs/solutions/0400-0499/construct-the-rectangle.md ================================================ # [0492. 构造矩形](https://leetcode.cn/problems/construct-the-rectangle/) - 标签:数学 - 难度:简单 ## 题目链接 - [0492. 构造矩形 - 力扣](https://leetcode.cn/problems/construct-the-rectangle/) ## 题目大意 **描述**: 作为一位 web 开发者, 懂得怎样去规划一个页面的尺寸是很重要的。所以,现给定一个具体的矩形页面面积,你的任务是设计一个长度为 $L$ 和宽度为 $W$ 且满足以下要求的矩形的页面。 **要求**: 1. 你设计的矩形页面必须等于给定的目标面积。 2. 宽度 $W$ 不应大于长度 $L$,换言之,要求 $L \ge W$。 3. 长度 $L$ 和宽度 $W$ 之间的差距应当尽可能小。 返回一个 数组 $[L, W]$,其中 $L$ 和 $W$ 是你按照顺序设计的网页的长度和宽度。 **说明**: - $1 \le area \le 10^{7}$。 **示例**: - 示例 1: ```python 输入: 4 输出: [2, 2] 解释: 目标面积是 4, 所有可能的构造方案有 [1,4], [2,2], [4,1]。 但是根据要求2,[1,4] 不符合要求; 根据要求3,[2,2] 比 [4,1] 更能符合要求. 所以输出长度 L 为 2, 宽度 W 为 2。 ``` - 示例 2: ```python 输入: area = 37 输出: [37,1] ``` ## 解题思路 ### 思路 1:数学方法 1. 我们需要找到两个整数 $L$ 和 $W$,使得 $L \times W = area$ 且 $L \geq W$,并且 $L - W$ 尽可能小。 2. 由于 $L \geq W$,我们可以从 $W = \lfloor \sqrt{area} \rfloor$ 开始向下遍历,找到第一个能整除 $area$ 的 $W$。 3. 这样找到的 $W$ 是最大的可能宽度,对应的 $L = \frac{area}{W}$ 是最小的可能长度,从而使得 $L - W$ 最小。 4. 如果 $area$ 是完全平方数,则 $L = W = \sqrt{area}$ 是最优解。 ### 思路 1:代码 ```python class Solution: def constructRectangle(self, area: int) -> List[int]: # 从 sqrt(area) 开始向下遍历,找到最大的能整除 area 的宽度 width = int(area ** 0.5) # 向下遍历,找到第一个能整除 area 的宽度 while width > 0: if area % width == 0: # 找到能整除的宽度,计算对应的长度 length = area // width return [length, width] width -= 1 # 理论上不会到达这里,因为 width=1 时一定能整除 return [area, 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\sqrt{area})$,最坏情况下需要遍历从 $\sqrt{area}$ 到 $1$ 的所有整数。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/convert-a-number-to-hexadecimal.md ================================================ # [0405. 数字转换为十六进制数](https://leetcode.cn/problems/convert-a-number-to-hexadecimal/) - 标签:位运算、数学 - 难度:简单 ## 题目链接 - [0405. 数字转换为十六进制数 - 力扣](https://leetcode.cn/problems/convert-a-number-to-hexadecimal/) ## 题目大意 **描述**:给定一个整数 $num$。 **要求**:编写一个算法将这个数转换为十六进制数。对于负整数,我们通常使用「补码运算」方法。 **说明**: - 十六进制中所有字母($a \sim f$)都必须是小写。 - 十六进制字符串中不能包含多余的前导零。如果要转化的数为 $0$,那么以单个字符 $0$ 来表示。 - 对于其他情况,十六进制字符串中的第一个字符将不会是 $0$ 字符。 - 给定的数确保在 $32$ 位有符号整数范围内。 - 不能使用任何由库提供的将数字直接转换或格式化为十六进制的方法。 **示例**: - 示例 1: ```python 输入: 26 输出: "1a" ``` - 示例 2: ```python 输入: -1 输出: "ffffffff" ``` ## 解题思路 ### 思路 1:模拟 主要是对不同情况的处理。 - 当 $num$ 为 0 时,直接返回 $0$。 - 当 $num$ 为负数时,对负数进行「补码运算」,转换为对应的十进制正数(将其绝对值与 $2^{32} - 1$ 异或再加 1),然后执行和 $nums$ 为正数一样的操作。 - 当 $num$ 为正数时,将其对 $16$ 取余,并转为对应的十六进制字符,并按位拼接到字符串中,再将 $num$ 除以 $16$,继续对 $16$ 取余,直到 $num$ 变为为 0。 - 最后将拼接好的字符串逆序返回就是答案。 ### 思路 1:代码 ```python class Solution: def toHex(self, num: int) -> str: res = '' if num == 0: return '0' if num < 0: num = (abs(num) ^ (2 ** 32 - 1)) + 1 while num: digit = num % 16 if digit >= 10: digit = chr(ord('a') + digit - 10) else: digit = str(digit) res += digit num >>= 4 return res[::-1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(C)$,其中 $C$ 为构造的十六进制数的长度。 - **空间复杂度**:$O(C)$。 ================================================ FILE: docs/solutions/0400-0499/convert-binary-search-tree-to-sorted-doubly-linked-list.md ================================================ # [0426. 将二叉搜索树转化为排序的双向链表](https://leetcode.cn/problems/convert-binary-search-tree-to-sorted-doubly-linked-list/) - 标签:栈、树、深度优先搜索、二叉搜索树、链表、二叉树、双向链表 - 难度:中等 ## 题目链接 - [0426. 将二叉搜索树转化为排序的双向链表 - 力扣](https://leetcode.cn/problems/convert-binary-search-tree-to-sorted-doubly-linked-list/) ## 题目大意 给定一棵二叉树的根节点 `root`。 要求:将这棵二叉树转换为一个已排序的双向循环链表。要求不能创建新的节点,只能调整树中节点指针的指向。 ## 解题思路 通过中序递归遍历可以将二叉树升序排列输出。这道题需要在中序遍历的同时,将节点的左右指向进行改变。使用 `head`、`tail` 存放双向链表的头尾节点,然后从根节点开始,进行中序递归遍历。 具体做法如下: - 如果当前节点为空,直接返回。 - 如果当前节点不为空: - 递归遍历左子树。 - 如果尾节点不为空,则将尾节点与当前节点进行连接。 - 如果尾节点为空,则初始化头节点。 - 将当前节点标记为尾节点。 - 递归遍历右子树。 - 最后将头节点和尾节点进行连接。 ## 代码 ```python class Solution: def treeToDoublyList(self, root: 'Node') -> 'Node': def dfs(node: 'Node'): if not node: return dfs(node.left) if self.tail: self.tail.right = node node.left = self.tail else: self.head = node self.tail = node dfs(node.right) if not root: return None self.head, self.tail = None, None dfs(root) self.head.left = self.tail self.tail.right = self.head return self.head ``` ================================================ FILE: docs/solutions/0400-0499/convex-polygon.md ================================================ # [0469. 凸多边形](https://leetcode.cn/problems/convex-polygon/) - 标签:几何、数组、数学 - 难度:中等 ## 题目链接 - [0469. 凸多边形 - 力扣](https://leetcode.cn/problems/convex-polygon/) ## 题目大意 **描述**: 给定 X-Y 平面上的一顶点坐标 $points$,其中 $points[i] = [x_i, y_i]$。这些点按顺序连成一个多边形。 **要求**: 判断该多边形是否为凸多边形。如果是凸多边形,则返回 true,否则返回 false。 **说明**: - 假设由给定点构成的多边形总是一个 简单的多边形(简单多边形的定义)。换句话说,我们要保证每个顶点处恰好是两条边的汇合点,并且这些边「互不相交」。 - $3 \le points.length \le 10^4$。 - $points[i].length = 2$。 - $-10^4 \le points[i][0], points[i][1] \le 10^4$。 - 所有点都是唯一的。 **示例**: - 示例 1: ```python 输入:points = [[0,0],[0,5],[5,5],[5,0]] 输出:true ``` - 示例 2: ```python 输入:points = [[0,0],[0,10],[10,10],[10,0],[5,5]] 输出:false 解释:这是一个凹多边形。 ``` ## 解题思路 ### 思路 1:叉积判断凸多边形 判断一个多边形是否为凸多边形。凸多边形的特点是:所有内角都小于 180 度,或者说所有顶点的转向方向一致。 **核心思路**: - 使用向量叉积判断转向方向。 - 对于三个连续的点 $A$、$B$、$C$,计算向量 $\vec{AB}$ 和 $\vec{BC}$ 的叉积。 - 叉积的符号表示转向方向:正数表示左转,负数表示右转。 - 如果所有转向方向一致(都是左转或都是右转),则为凸多边形。 **解题步骤**: 1. 遍历所有连续的三个点(包括首尾相连)。 2. 计算叉积:$cross = (x_2 - x_1) \times (y_3 - y_2) - (y_2 - y_1) \times (x_3 - x_2)$。 3. 记录叉积的符号,如果出现不同符号的叉积,说明不是凸多边形。 4. 注意:叉积为 0 表示三点共线,可以忽略。 ### 思路 1:代码 ```python class Solution: def isConvex(self, points: List[List[int]]) -> bool: n = len(points) prev_cross = 0 # 记录上一个叉积的符号 for i in range(n): # 三个连续的点 x1, y1 = points[i] x2, y2 = points[(i + 1) % n] x3, y3 = points[(i + 2) % n] # 计算叉积 cross = (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2) # 如果叉积不为 0,检查符号是否一致 if cross != 0: if cross * prev_cross < 0: return False prev_cross = cross return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是多边形的顶点数。需要遍历所有顶点。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/count-the-repetitions.md ================================================ # [0466. 统计重复个数](https://leetcode.cn/problems/count-the-repetitions/) - 标签:字符串、动态规划 - 难度:困难 ## 题目链接 - [0466. 统计重复个数 - 力扣](https://leetcode.cn/problems/count-the-repetitions/) ## 题目大意 **描述**: 定义 $str = [s, n]$ 表示 $str$ 由 $n$ 个字符串 $s$ 连接构成。 - 例如,`str == ["abc", 3] == "abcabcabc"`。 如果可以从 $s2$ 中删除某些字符使其变为 $s1$,则称字符串 $s1$ 可以从字符串 $s2$ 获得。 - 例如,根据定义,`s1 = "abc"` 可以从 `s2 = "abdbec"` 获得,仅需要删除加粗且用斜体标识的字符。 现在给定两个字符串 $s1$ 和 $s2$ 和两个整数 $n1$ 和 $n2$。由此构造得到两个字符串,其中 $str1 = [s1, n1]$、$str2 = [s2, n2]$。 **要求**: 请你找出一个最大整数 $m$,以满足 $str = [str2, m]$ 可以从 $str1$ 获得。 **说明**: - $1 \le s1.length, s2.length \le 10^{3}$。 - $s1$ 和 $s2$ 由小写英文字母组成。 - $1 \le n1, n2 \le 10^{6}$。 **示例**: - 示例 1: ```python 输入:s1 = "acb", n1 = 4, s2 = "ab", n2 = 2 输出:2 ``` - 示例 2: ```python 输入:s1 = "acb", n1 = 1, s2 = "acb", n2 = 1 输出:1 ``` ## 解题思路 ### 思路 1:模拟匹配 1. 我们需要找到最大的整数 $m$,使得 $str2$ 重复 $m$ 次后得到的字符串可以从 $str1$ 重复 $n1$ 次后得到的字符串中获得。 2. 由于 $n1$ 和 $n2$ 可能很大(最大 $10^6$),直接构造完整字符串会超出内存限制。 3. 我们可以通过模拟匹配过程来解决:遍历 $str1$ 的每个字符,尝试匹配 $str2$ 的字符。 4. 使用两个指针 $i$ 和 $j$,分别指向 $str1$ 和 $str2$ 的当前位置。 5. 当 $str1[i] = str2[j]$ 时,$j$ 指针前进;否则只前进 $i$ 指针。 6. 当 $j$ 指针到达 $str2$ 末尾时,说明完成了一次 $str2$ 的匹配,计数器加 $1$,$j$ 重置为 $0$。 7. 重复这个过程直到遍历完 $n1$ 个 $str1$,统计总共能匹配多少个完整的 $str2$。 8. 最终答案是 $\lfloor \frac{\text{匹配的 str2 个数}}{n2} \rfloor$。 ### 思路 1:代码 ```python class Solution: def getMaxRepetitions(self, s1: str, n1: int, s2: str, n2: int) -> int: # 统计在 n1 个 s1 中能匹配多少个完整的 s2 s2_count = 0 # 匹配的 s2 个数 s2_index = 0 # s2 的当前匹配位置 # 遍历 n1 个 s1 for i in range(n1): # 遍历当前 s1 的每个字符 for char in s1: # 如果当前字符匹配 s2 的当前字符 if char == s2[s2_index]: s2_index += 1 # 如果匹配完一个完整的 s2 if s2_index == len(s2): s2_count += 1 s2_index = 0 # 重置 s2 的匹配位置 # 返回能构造多少个 [s2, n2] return s2_count // n2 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n1 \times |s1|)$,其中 $|s1|$ 是字符串 $s1$ 的长度。需要遍历 $n1$ 个 $s1$,每个 $s1$ 需要遍历其所有字符。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/delete-node-in-a-bst.md ================================================ # [0450. 删除二叉搜索树中的节点](https://leetcode.cn/problems/delete-node-in-a-bst/) - 标签:树、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [0450. 删除二叉搜索树中的节点 - 力扣](https://leetcode.cn/problems/delete-node-in-a-bst/) ## 题目大意 **描述**:给定一个二叉搜索树的根节点 `root`,以及一个值 `key`。 **要求**:从二叉搜索树中删除 key 对应的节点。并保证删除后的树仍是二叉搜索树。要求算法时间复杂度为 $0(h)$,$h$ 为树的高度。最后返回二叉搜索树的根节点。 **说明**: - 节点数的范围 $[0, 10^4]$。 - $-10^5 \le Node.val \le 10^5$。 - 节点值唯一。 - `root` 是合法的二叉搜索树。 - $-10^5 \le key \le 10^5$。 **示例**: - 示例 1: ![img](https://assets.leetcode.com/uploads/2020/09/04/del_node_1.jpg) ```python 输入:root = [5,3,6,2,4,null,7], key = 3 输出:[5,4,6,2,null,null,7] 解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。 一个正确的答案是 [5,4,6,2,null,null,7], 如上图所示。 另一个正确答案是 [5,2,6,null,4,null,7]。 ``` - 示例 2: ```python 输入: root = [5,3,6,2,4,null,7], key = 0 输出: [5,3,6,2,4,null,7] 解释: 二叉树不包含值为 0 的节点 ``` ## 解题思路 ### 思路 1:递归 删除分两个步骤:查找和删除。查找通过递归查找,删除的话需要考虑情况。 1. 从根节点 `root` 开始,递归遍历搜索二叉树。 1. 如果当前节点节点为空,返回当前节点。 2. 如果当前节点值大于 `key`,则去左子树中搜索并删除,此时 `root.left` 也要跟着递归更新,递归完成后返回当前节点。 3. 如果当前节点值小于 `key`,则去右子树中搜索并删除,此时 `root.right` 也要跟着递归更新,递归完成后返回当前节点。 4. 如果当前节点值等于 `key`,则该节点就是待删除节点。 1. 如果当前节点的左子树为空,则删除该节点之后,则右子树代替当前节点位置,返回右子树。 2. 如果当前节点的右子树为空,则删除该节点之后,则左子树代替当前节点位置,返回左子树。 3. 如果当前节点的左右子树都有,则将左子树转移到右子树最左侧的叶子节点位置上,然后右子树代替当前节点位置。返回右子树。 ### 思路 1:代码 ```python class Solution: def deleteNode(self, root: TreeNode, key: int) -> TreeNode: if not root: return root if root.val > key: root.left = self.deleteNode(root.left, key) return root elif root.val < key: root.right = self.deleteNode(root.right, key) return root else: if not root.left: return root.right elif not root.right: return root.left else: curr = root.right while curr.left: curr = curr.left curr.left = root.left return root.right ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉搜索树的节点数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0400-0499/diagonal-traverse.md ================================================ # [0498. 对角线遍历](https://leetcode.cn/problems/diagonal-traverse/) - 标签:数组、矩阵、模拟 - 难度:中等 ## 题目链接 - [0498. 对角线遍历 - 力扣](https://leetcode.cn/problems/diagonal-traverse/) ## 题目大意 **描述**:给定一个大小为 $m \times n$ 的矩阵 $mat$ 。 **要求**:以对角线遍历的顺序,用一个数组返回这个矩阵中的所有元素。 **说明**: - $m == mat.length$。 - $n == mat[i].length$。 - $1 \le m, n \le 10^4$。 - $1 \le m \times n \le 10^4$。 - $-10^5 \le mat[i][j] \le 10^5$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/10/diag1-grid.jpg) ```python 输入:mat = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2,4,7,5,3,6,8,9] ``` - 示例 2: ```python 输入:mat = [[1,2],[3,4]] 输出:[1,2,3,4] ``` ## 解题思路 ### 思路 1:找规律 + 考虑边界问题 这道题的关键是「找规律」和「考虑边界问题」。 找规律: 1. 当「行号 + 列号」为偶数时,遍历方向为从左下到右上。可以记为右上方向 $(-1, +1)$,即行号减 $1$,列号加 $1$。 2. 当「行号 + 列号」为奇数时,遍历方向为从右上到左下。可以记为左下方向 $(+1, -1)$,即行号加 $1$,列号减 $1$。 边界情况: 1. 向右上方向移动时: 1. 如果在最后一列,则向下方移动,即 `x += 1`。 2. 如果在第一行,则向右方移动,即 `y += 1`。 3. 其余情况向右上方向移动,即 `x -= 1`、`y += 1`。 2. 向左下方向移动时: 1. 如果在最后一行,则向右方移动,即 `y += 1`。 2. 如果在第一列,则向下方移动,即 `x += 1`。 3. 其余情况向左下方向移动,即 `x += 1`、`y -= 1`。 ### 思路 1:代码 ```python class Solution: def findDiagonalOrder(self, mat: List[List[int]]) -> List[int]: rows = len(mat) cols = len(mat[0]) count = rows * cols x, y = 0, 0 ans = [] for i in range(count): ans.append(mat[x][y]) if (x + y) % 2 == 0: # 最后一列 if y == cols - 1: x += 1 # 第一行 elif x == 0: y += 1 # 右上方向 else: x -= 1 y += 1 else: # 最后一行 if x == rows - 1: y += 1 # 第一列 elif y == 0: x += 1 # 左下方向 else: x += 1 y -= 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。其中 $m$、$n$ 分别为二维矩阵的行数、列数。 - **空间复杂度**:$O(m \times n)$。如果算上答案数组的空间占用,则空间复杂度为 $O(m \times n)$。不算上则空间复杂度为 $O(1)$。 ## 参考资料 - 【题解】[「498. 对角线遍历」最简单易懂! - 对角线遍历 - 力扣(LeetCode)](https://leetcode.cn/problems/diagonal-traverse/solution/498-dui-jiao-xian-bian-li-zui-jian-dan-y-ibu3/) ================================================ FILE: docs/solutions/0400-0499/encode-n-ary-tree-to-binary-tree.md ================================================ # [0431. 将 N 叉树编码为二叉树](https://leetcode.cn/problems/encode-n-ary-tree-to-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、设计、二叉树 - 难度:困难 ## 题目链接 - [0431. 将 N 叉树编码为二叉树 - 力扣](https://leetcode.cn/problems/encode-n-ary-tree-to-binary-tree/) ## 题目大意 **描述**: 给定一个 N 叉树的根节点 $root$。 **要求**: 设计一个算法将 N 叉树编码为二叉树,并能够将二叉树解码回 N 叉树。编码和解码算法的实现没有限制,只需要保证 N 叉树可以编码为二叉树并且能够解码回原来的 N 叉树。 **说明**: - 一个 N 叉树是指每个节点都有不超过 N 个孩子节点的有根树。 - 一个二叉树是指每个节点都有不超过 2 个孩子节点的有根树。 - N 叉树的高度小于等于 $1000$。 - 节点总数在范围 $[0, 10^4]$ 内。 **示例**: - 示例 1: ```python 输入:root = [1,null,3,2,4,null,5,6] 输出:[1,null,3,2,4,null,5,6] 解释:编码后再解码,得到原来的 N 叉树。 ``` ## 解题思路 ### 思路 1:左孩子右兄弟表示法 将 N 叉树编码为二叉树,需要设计一种编码方式,使得可以唯一地还原。 **核心思路**: - 使用"左孩子右兄弟"表示法: - 二叉树的左子节点:存储 N 叉树节点的第一个子节点。 - 二叉树的右子节点:存储 N 叉树节点的下一个兄弟节点。 **编码步骤**: 1. 对于 N 叉树的每个节点: - 如果有子节点,将第一个子节点作为二叉树的左子节点。 - 将所有兄弟节点用右子节点连接起来。 2. 递归处理所有节点。 **解码步骤**: 1. 二叉树的左子节点对应 N 叉树的第一个子节点。 2. 二叉树的右子节点对应 N 叉树的兄弟节点。 3. 递归还原所有节点。 ### 思路 1:代码 ```python """ # Definition for a Node. class Node: def __init__(self, val=None, children=None): self.val = val self.children = children if children is not None else [] """ """ # Definition for a binary tree node. class TreeNode: def __init__(self, x): self.val = x self.left = None self.right = None """ class Codec: # 将 N 叉树编码为二叉树 def encode(self, root: 'Optional[Node]') -> Optional[TreeNode]: if not root: return None # 创建二叉树根节点 binary_root = TreeNode(root.val) # 如果有子节点 if root.children: # 第一个子节点作为左子节点 binary_root.left = self.encode(root.children[0]) # 将兄弟节点用右子节点连接 current = binary_root.left for i in range(1, len(root.children)): current.right = self.encode(root.children[i]) current = current.right return binary_root # 将二叉树解码为 N 叉树 def decode(self, data: Optional[TreeNode]) -> 'Optional[Node]': if not data: return None # 创建 N 叉树根节点 root = Node(data.val, []) # 左子节点是第一个子节点 current = data.left while current: # 递归解码子节点 root.children.append(self.decode(current)) # 右子节点是兄弟节点 current = current.right return root # Your Codec object will be instantiated and called as such: # codec = Codec() # codec.decode(codec.encode(root)) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是节点数。每个节点访问一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度,递归栈的深度。 ================================================ FILE: docs/solutions/0400-0499/encode-string-with-shortest-length.md ================================================ # [0471. 编码最短长度的字符串](https://leetcode.cn/problems/encode-string-with-shortest-length/) - 标签:字符串、动态规划 - 难度:困难 ## 题目链接 - [0471. 编码最短长度的字符串 - 力扣](https://leetcode.cn/problems/encode-string-with-shortest-length/) ## 题目大意 **描述**: 给定一个字符串 $s$,需要将其编码为最短的字符串。 编码规则:如果子串重复出现 $k$ 次,可以用 `k[substring]` 表示。例如,`"aaaa"` 可以编码为 `"4[a]"`,`"abcabcabc"` 可以编码为 `"3[abc]"`。 编码可以嵌套,例如 `"abababab"` 可以编码为 `"2[2[ab]]"`。 **要求**: 返回编码后长度最短的字符串。 **说明**: - $1 \le s.length \le 150$。 - $s$ 只包含小写英文字母。 **示例**: - 示例 1: ```python 输入:s = "aaa" 输出:"aaa" 解释:无法编码得更短。 ``` - 示例 2: ```python 输入:s = "aaaaa" 输出:"5[a]" ``` - 示例 3: ```python 输入:s = "aaaaaaaaaa" 输出:"10[a]" ``` ## 解题思路 ### 思路 1:区间动态规划 给定一个字符串,需要找到编码后长度最短的字符串。编码规则是:如果子串重复出现,可以用 `k[substring]` 表示。 **核心思路**: - 使用区间 DP,$dp[i][j]$ 表示子串 $s[i:j+1]$ 的最短编码。 - 对于每个子串,尝试: 1. 不编码,保持原样。 2. 检查是否由重复子串组成,如果是则编码为 `k[pattern]`。 3. 分割成两部分,分别编码。 **解题步骤**: 1. 初始化 $dp[i][j] = s[i:j+1]$(不编码)。 2. 枚举子串长度 $length$ 从 1 到 $n$。 3. 对于每个子串 $s[i:j+1]$: - 检查是否由重复模式组成:使用 $(s[i:j+1] + s[i:j+1]).find(s[i:j+1], 1)$ 判断。 - 如果是重复模式,计算编码后的长度。 - 尝试分割点 $k$,比较 $dp[i][k] + dp[k+1][j]$ 的长度。 4. 返回 $dp[0][n-1]$。 ### 思路 1:代码 ```python class Solution: def encode(self, s: str) -> str: n = len(s) # dp[i][j] 表示 s[i:j+1] 的最短编码 dp = [[""] * n for _ in range(n)] # 枚举子串长度 for length in range(1, n + 1): for i in range(n - length + 1): j = i + length - 1 substr = s[i:j+1] # 初始化为不编码 dp[i][j] = substr # 如果长度小于 5,编码后不会更短 if length < 5: continue # 检查是否由重复模式组成 pos = (substr + substr).find(substr, 1) if pos < len(substr): # 重复模式的长度 pattern_len = pos repeat_count = len(substr) // pattern_len encoded = str(repeat_count) + "[" + dp[i][i + pattern_len - 1] + "]" if len(encoded) < len(dp[i][j]): dp[i][j] = encoded # 尝试分割 for k in range(i, j): if len(dp[i][k]) + len(dp[k+1][j]) < len(dp[i][j]): dp[i][j] = dp[i][k] + dp[k+1][j] return dp[0][n-1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 是字符串长度。需要枚举所有子串和分割点。 - **空间复杂度**:$O(n^2)$,存储 DP 数组。 ================================================ FILE: docs/solutions/0400-0499/find-all-anagrams-in-a-string.md ================================================ # [0438. 找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/) - 标签:哈希表、字符串、滑动窗口 - 难度:中等 ## 题目链接 - [0438. 找到字符串中所有字母异位词 - 力扣](https://leetcode.cn/problems/find-all-anagrams-in-a-string/) ## 题目大意 **描述**:给定两个字符串 $s$ 和 $p$。 **要求**:找到 $s$ 中所有 $p$ 的异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 **说明**: - **异位词**:指由相同字母重排列形成的字符串(包括相同的字符串)。 - $1 <= s.length, p.length <= 3 * 10^4$。 - $s$ 和 $p$ 仅包含小写字母。 **示例**: - 示例 1: ```python 输入: s = "cbaebabacd", p = "abc" 输出: [0,6] 解释: 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。 ``` - 示例 2: ```python 输入: s = "abab", p = "ab" 输出: [0,1,2] 解释: 起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。 起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。 起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。 ``` ## 解题思路 ### 思路 1:滑动窗口 维护一个固定长度为 $len(p)$ 的滑动窗口。于是问题的难点变为了如何判断 $s$ 的子串和 $p$ 是异位词。可以使用两个字典来分别存储 $s$ 的子串中各个字符个数和 $p$ 中各个字符个数。如果两个字典对应的键值全相等,则说明 $s$ 的子串和 $p$ 是异位词。但是这样每一次比较的操作时间复杂度是 $O(n)$,我们可以通过在滑动数组中逐字符比较的方式来减少两个字典之间相互比较的复杂度,并用 $valid$ 记录经过验证的字符个数。整个算法步骤如下: - 使用哈希表 $need$ 记录 $p$ 中各个字符出现次数。使用字典 $window$ 记录 $s$ 的子串中各个字符出现的次数。使用数组 $res$ 记录答案。使用 $valid$ 记录 $s$ 的子串中经过验证的字符个数。使用 $window\_size$ 表示窗口大小,值为 $len(p)$。使用两个指针 $left$、$right$。分别指向滑动窗口的左右边界。 - 一开始,$left$、$right$ 都指向 $0$。 - 如果 $s[right]$ 出现在 $need$ 中,将最右侧字符 $s[right]$ 加入当前窗口 $window$ 中,记录该字符个数。并验证该字符是否和 $need$ 中个对应字符个数相等。如果相等则验证的字符个数加 $1$,即 `valid += 1`。 - 如果该窗口字符长度大于等于 $window\_size$ 个,即 $right - left + 1 \ge window\_size$。则不断右移 $left$,缩小滑动窗口长度。 - 如果验证字符个数 $valid$ 等于窗口长度 $window\_size$,则 $s[left, right + 1]$ 为 $p$ 的异位词,所以将 $left$ 加入到答案数组中。 - 如果$s[left]$ 在 $need$ 中,则更新窗口中对应字符的个数,同时维护 $valid$ 值。 - 右移 $right$,直到 $right \ge len(nums)$ 结束。 - 输出答案数组 $res$。 ### 思路 1:代码 ```python class Solution: def findAnagrams(self, s: str, p: str) -> List[int]: need = collections.defaultdict(int) for ch in p: need[ch] += 1 window = collections.defaultdict(int) window_size = len(p) res = [] left, right = 0, 0 valid = 0 while right < len(s): if s[right] in need: window[s[right]] += 1 if window[s[right]] == need[s[right]]: valid += 1 if right - left + 1 >= window_size: if valid == len(need): res.append(left) if s[left] in need: if window[s[left]] == need[s[left]]: valid -= 1 window[s[left]] -= 1 left += 1 right += 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m + |\sum|)$,其中 $n$、$m$ 分别为字符串 $s$、$p$ 的长度,$\sum$ 为字符集,本题中 $|\sum| = 26$。 - **空间复杂度**:$|\sum|$。 ================================================ FILE: docs/solutions/0400-0499/find-all-duplicates-in-an-array.md ================================================ # [0442. 数组中重复的数据](https://leetcode.cn/problems/find-all-duplicates-in-an-array/) - 标签:数组、哈希表 - 难度:中等 ## 题目链接 - [0442. 数组中重复的数据 - 力扣](https://leetcode.cn/problems/find-all-duplicates-in-an-array/) ## 题目大意 **描述**: 给定一个长度为 $n$ 的整数数组 $nums$,其中 $nums$ 的所有整数都在范围 $[1, n]$ 内,且每个整数出现「最多两次」。 **要求**: 请你找出所有出现「两次」的整数,并以数组形式返回。 你必须设计并实现一个时间复杂度为 $O(n)$ 且仅使用常量额外空间(不包括存储输出所需的空间)的算法解决此问题。 **说明**: - $n == nums.length$。 - $1 \le n \le 10^{5}$。 - $1 \le nums[i] \le n$。 - $nums$ 中的每个元素出现「一次」或「两次」。 **示例**: - 示例 1: ```python 输入:nums = [4,3,2,7,8,2,3,1] 输出:[2,3] ``` - 示例 2: ```python 输入:nums = [1,1,2] 输出:[1] ``` ## 解题思路 ### 思路 1:正负号标记法 由于数组中的元素都在范围 $[1, n]$ 内,且每个整数最多出现两次。可以利用数组中的位置索引本身来表示数字是否存在。 我们可以遍历数组 $nums$,对于每一个元素 $nums[i]$: - 如果 $nums[abs(nums[i]) - 1] > 0$,说明 $abs(nums[i])$ 第一次出现,将其对应位置的数变为负数作为标记。 - 如果 $nums[abs(nums[i]) - 1] < 0$,说明 $abs(nums[i])$ 已经出现过一次了,此时 $abs(nums[i])$ 就是重复的数字,将其加入到结果数组中。 算法步骤如下: - 初始化结果数组 $res$。 - 遍历数组 $nums$,对于每个元素 $nums[i]$: - 计算 $abs(nums[i]) - 1$ 作为索引 $index$。 - 如果 $nums[index] < 0$,说明 $abs(nums[i])$ 是重复数字,将其加入 $res$。 - 否则将 $nums[index]$ 标记为负数,即 $nums[index] = -nums[index]$。 - 遍历结束后,返回结果数组 $res$。 ### 思路 1:代码 ```python class Solution: def findDuplicates(self, nums: List[int]) -> List[int]: res = [] # 遍历数组,使用正负号标记法 for i in range(len(nums)): # 计算索引位置 index = abs(nums[i]) - 1 # 如果该位置的数已经是负数,说明已经出现过一次 if nums[index] < 0: # 将当前元素(取绝对值)加入结果数组 res.append(abs(nums[i])) else: # 将该位置的数标记为负数 nums[index] = -nums[index] return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组长度。我们只需要遍历一次数组。 - **空间复杂度**:$O(1)$。除了返回结果数组,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/find-all-numbers-disappeared-in-an-array.md ================================================ # [0448. 找到所有数组中消失的数字](https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array/) - 标签:数组、哈希表 - 难度:简单 ## 题目链接 - [0448. 找到所有数组中消失的数字 - 力扣](https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array/) ## 题目大意 **描述**: 给定一个含 $n$ 个整数的数组 $nums$,其中 $nums[i]$ 在区间 $[1, n]$ 内。 **要求**: 请你找出所有在 $[1, n]$ 范围内但没有出现在 $nums$ 中的数字,并以数组的形式返回结果。 **说明**: - $n == nums.length$。 - $1 \le n \le 10^{5}$。 - $1 \le nums[i] \le n$。 - 进阶:你能在不使用额外空间且时间复杂度为 $O(n)$ 的情况下解决这个问题吗? 你可以假定返回的数组不算在额外空间内。 **示例**: - 示例 1: ```python 输入:nums = [4,3,2,7,8,2,3,1] 输出:[5,6] ``` - 示例 2: ```python 输入:nums = [1,1] 输出:[2] ``` ## 解题思路 ### 思路 1:正负号标记法 由于数组中的所有数字都在范围 $[1, n]$ 内,且数组长度为 $n$,我们可以利用数组本身作为哈希表来标记数字是否出现。 具体思路: 1. 遍历数组 $nums$,对于每个元素 $nums[i]$,计算 $abs(nums[i]) - 1$ 作为索引。 2. 将对应位置 $nums[abs(nums[i]) - 1]$ 的数变为负数,作为标记,表示该数字已经出现过。 3. 再次遍历数组,如果某个位置 $i$ 的数 $nums[i]$ 还是正数,说明 $i + 1$ 这个数字没有出现过,将其加入结果数组。 算法步骤: - 遍历数组 $nums$,对于每个元素 $nums[i]$: - 计算 $abs(nums[i]) - 1$ 作为索引 $index$。 - 如果 $nums[index] > 0$,将 $nums[index]$ 标记为负数,即 $nums[index] = -nums[index]$。 - 遍历结束后的数组,对于每个位置 $i$: - 如果 $nums[i] > 0$,说明 $i + 1$ 没有出现过,将 $i + 1$ 加入结果数组 $res$。 - 返回结果数组 $res$。 ### 思路 1:代码 ```python class Solution: def findDisappearedNumbers(self, nums: List[int]) -> List[int]: # 使用正负号标记法,标记出现过的数字 for i in range(len(nums)): # 计算索引位置(因为数组中的数字在 [1, n] 范围内) index = abs(nums[i]) - 1 # 如果该位置的数大于 0,将其标记为负数 if nums[index] > 0: nums[index] = -nums[index] # 收集结果:所有仍然是正数的位置对应的数字就是消失的数字 res = [] for i in range(len(nums)): # 如果该位置的数仍然是正数,说明 i + 1 没有出现过 if nums[i] > 0: res.append(i + 1) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组长度。需要遍历两次数组,每次遍历的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(1)$。除了返回结果数组,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/find-permutation.md ================================================ # [0484. 寻找排列](https://leetcode.cn/problems/find-permutation/) - 标签:栈、贪心、数组、字符串 - 难度:中等 ## 题目链接 - [0484. 寻找排列 - 力扣](https://leetcode.cn/problems/find-permutation/) ## 题目大意 **描述**: 给定一个由 `'I'`(递增)和 `'D'`(递减)组成的字符串 $s$,长度为 $n$。 $s$ 描述了一个排列 $perm$(由 $1$ 到 $n$ 的整数组成)的相邻比较关系: - 如果 $perm[i] < perm[i+1]$,则 $s[i] == 'I'$(递增) - 如果 $perm[i] > perm[i+1]$,则 $s[i] == 'D'$(递减) 需要找到一个由 $1$ 到 $n+1$ 组成的排列,使得: - 如果 $s[i] = 'I'$,则 $perm[i] < perm[i+1]$。 - 如果 $s[i] = 'D'$,则 $perm[i] > perm[i+1]$。 **要求**: 满足该 $s$ 条件的「字典序最小」的排列 $perm$。 **说明**: - $1 \le s.length \le 10^5$。 - $s$ 只包含字符 `'I'` 和 `'D'`。 **示例**: - 示例 1: ```python 输入:s = "I" 输出:[1,2] 解释:[1,2] 是唯一满足条件的排列。 ``` - 示例 2: ```python 输入:s = "DI" 输出:[2,1,3] 解释:[2,1,3] 和 [3,1,2] 都满足条件,但 [2,1,3] 字典序更小。 ``` ## 解题思路 ### 思路 1:栈 + 贪心 给定一个由 `'I'`(递增)和 `'D'`(递减)组成的字符串 $s$,需要找到字典序最小的排列,使得相邻元素满足递增或递减关系。 **核心思路**: - 使用贪心策略:尽可能让前面的数字小。 - 遇到 `'I'` 时,应该让当前数字尽可能小。 - 遇到连续的 `'D'` 时,需要将这段数字按降序排列。 **解题步骤**: 1. 初始化结果数组,长度为 $n + 1$($n$ 是字符串长度)。 2. 使用栈来处理连续的 `'D'`。 3. 遍历字符串,将数字 $1$ 到 $n + 1$ 依次处理: - 将当前数字压入栈。 - 如果遇到 `'I'` 或到达末尾,将栈中所有数字弹出并加入结果。 - 这样可以保证连续的 `'D'` 对应的数字是降序的。 ### 思路 1:代码 ```python class Solution: def findPermutation(self, s: str) -> List[int]: n = len(s) result = [] stack = [] # 遍历 1 到 n+1 for i in range(1, n + 2): stack.append(i) # 如果遇到 'I' 或到达末尾,弹出栈中所有元素 if i == n + 1 or s[i - 1] == 'I': while stack: result.append(stack.pop()) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度。每个数字最多入栈和出栈各一次。 - **空间复杂度**:$O(n)$,栈的空间开销。 ================================================ FILE: docs/solutions/0400-0499/find-right-interval.md ================================================ # [0436. 寻找右区间](https://leetcode.cn/problems/find-right-interval/) - 标签:数组、二分查找、排序 - 难度:中等 ## 题目链接 - [0436. 寻找右区间 - 力扣](https://leetcode.cn/problems/find-right-interval/) ## 题目大意 **描述**: 给定一个区间数组 $intervals$ ,其中 $intervals[i] = [start_i, end_i]$,且每个 $start_i$ 都不同。 区间 $i$ 的「右侧区间」是满足 $start_j \ge end_i$,且 $start_j$ 「最小」的区间 $j$。注意 $i$ 可能等于 $j$。 **要求**: 返回一个由每个区间 $i$ 对应的「右侧区间」下标组成的数组。如果某个区间 $i$ 不存在对应的「右侧区间」,则下标 $i$ 处的值设为 $-1$。 **说明**: - $1 \le intervals.length \le 2 \times 10^{4}$。 - $intervals[i].length == 2$。 - $-10^{6} \le start_i \le end_i \le 10^{6}$。 - 每个间隔的起点都不相同。 **示例**: - 示例 1: ```python 输入:intervals = [[1,2]] 输出:[-1] 解释:集合中只有一个区间,所以输出-1。 ``` - 示例 2: ```python 输入:intervals = [[3,4],[2,3],[1,2]] 输出:[-1,0,1] 解释:对于 [3,4] ,没有满足条件的“右侧”区间。 对于 [2,3] ,区间[3,4]具有最小的“右”起点; 对于 [1,2] ,区间[2,3]具有最小的“右”起点。 ``` ## 解题思路 ### 思路 1:排序 + 二分查找 1. 为每个区间保存原索引,按照区间起点 $start_i$ 进行排序。 2. 对于每个区间 $intervals[i] = [start_i, end_i]$,用二分查找在排序后的数组中找第一个 $start_j \ge end_i$ 的区间。 3. 若找到则返回对应原索引,否则为 $-1$。 ### 思路 1:代码 ```python class Solution: def findRightInterval(self, intervals: List[List[int]]) -> List[int]: n = len(intervals) # 保存每个区间的原索引和起点 start_pos = [(intervals[i][0], i) for i in range(n)] # 按照起点排序 start_pos.sort() ans = [] # 对于每个区间,使用二分查找找右区间 for start, end in intervals: # 二分查找第一个 start_j >= end 的区间 left, right = 0, n - 1 res_index = -1 while left <= right: mid = (left + right) // 2 if start_pos[mid][0] >= end: res_index = start_pos[mid][1] right = mid - 1 # 继续往左找更小的 else: left = mid + 1 ans.append(res_index) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是区间数量。排序 $O(n \log n)$,二分查找 $n$ 次共 $O(n \log n)$。 - **空间复杂度**:$O(n)$,排序需要额外空间。 ================================================ FILE: docs/solutions/0400-0499/fizz-buzz.md ================================================ # [0412. Fizz Buzz](https://leetcode.cn/problems/fizz-buzz/) - 标签:数学、字符串、模拟 - 难度:简单 ## 题目链接 - [0412. Fizz Buzz - 力扣](https://leetcode.cn/problems/fizz-buzz/) ## 题目大意 给定一个整数 n,按照规则,输出 1~n 的字符串表示。 规则: - 如果 i 是 3 的倍数,输出 "Fizz"; - 如果 i 是 5 的倍数,输出 "Buzz"; - 如果 i 是 3 和 5 的倍数,则输出 "FizzBuzz"。 ## 解题思路 简单题,按照题目规则输出即可。 ## 代码 ```python class Solution: def fizzBuzz(self, n: int) -> List[str]: ans = [] for i in range(1,n+1): if i % 15 == 0: ans.append("FizzBuzz") elif i % 3 == 0: ans.append("Fizz") elif i % 5 == 0: ans.append("Buzz") else: ans.append(str(i)) return ans ``` ================================================ FILE: docs/solutions/0400-0499/flatten-a-multilevel-doubly-linked-list.md ================================================ # [0430. 扁平化多级双向链表](https://leetcode.cn/problems/flatten-a-multilevel-doubly-linked-list/) - 标签:深度优先搜索、链表、双向链表 - 难度:中等 ## 题目链接 - [0430. 扁平化多级双向链表 - 力扣](https://leetcode.cn/problems/flatten-a-multilevel-doubly-linked-list/) ## 题目大意 给定一个带子链表指针 child 的双向链表,将 child 的子链表进行扁平化处理,使所有节点出现在单级双向链表中。 扁平化处理如下: ``` 原链表: 1---2---3---4---5---6--NULL | 7---8---9---10--NULL | 11--12--NULL 扁平化之后: 1---2---3---7---8---11---12---9---10---4---5---6--NULL ``` ## 解题思路 递归处理多层链表的扁平化。遍历链表,找到 child 非空的节点, 将其子链表链接到当前节点的 next 位置(自身扁平化处理)。然后继续向后遍历,不断找到 child 节点,并进行链接。直到处理到尾部位置。 ## 代码 ```python class Solution: def dfs(self, node: 'Node'): # 找到链表的尾节点或 child 链表不为空的节点 while node.next and not node.child: node = node.next tail = None if node.child: # 如果 child 链表不为空,将 child 链表扁平化 tail = self.dfs(node.child) # 将扁平化的 child 链表链接在该节点之后 temp = node.next node.next = node.child node.next.prev = node node.child = None tail.next = temp if temp: temp.prev = tail # 链接之后,从 child 链表的尾节点继续向后处理链表 return self.dfs(tail) # child 链表为空,则该节点是尾节点,直接返回 return node def flatten(self, head: 'Node') -> 'Node': if not head: return head self.dfs(head) return head ``` ================================================ FILE: docs/solutions/0400-0499/frog-jump.md ================================================ # [0403. 青蛙过河](https://leetcode.cn/problems/frog-jump/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [0403. 青蛙过河 - 力扣](https://leetcode.cn/problems/frog-jump/) ## 题目大意 **描述**:一只青蛙要过河,这条河被等分为若干个单元格,每一个单元格内可能放油一块石子(也可能没有)。青蛙只能跳到有石子的单元格内,不能跳到没有石子的单元格内。 现在给定一个严格按照升序排序的数组 $stones$,其中 $stones[i]$ 代表第 $i$ 块石子所在的单元格序号。默认第 $0$ 块石子序号为 $0$(即 $stones[0] == 0$)。 开始时,青蛙默认站在序号为 $0$ 石子上(即 $stones[0]$),并且假定它第 $1$ 步只能跳跃 $1$ 个单位(即只能从序号为 $0$ 的单元格跳到序号为 $1$ 的单元格)。 如果青蛙在上一步向前跳跃了 $k$ 个单位,则下一步只能向前跳跃 $k - 1$、$k$ 或者 $k + 1$ 个单位。 **要求**:判断青蛙能否成功过河(即能否在最后一步跳到最后一块石子上)。如果能,则返回 `True`;否则,则返回 `False`。 **说明**: - $2 \le stones.length \le 2000$。 - $0 \le stones[i] \le 2^{31} - 1$。 - $stones[0] == 0$。 - $stones$ 按严格升序排列。 **示例**: - 示例 1: ```python 输入:stones = [0,1,3,5,6,8,12,17] 输出:true 解释:青蛙可以成功过河,按照如下方案跳跃:跳 1 个单位到第 2 块石子, 然后跳 2 个单位到第 3 块石子, 接着 跳 2 个单位到第 4 块石子, 然后跳 3 个单位到第 6 块石子, 跳 4 个单位到第 7 块石子, 最后,跳 5 个单位到第 8 个石子(即最后一块石子)。 ``` ## 解题思路 ### 思路 1:动态规划 题目中说:如果青蛙在上一步向前跳跃了 $k$ 个单位,则下一步只能向前跳跃 $k - 1$、$k$ 或者 $k + 1$ 个单位。则下一步的状态可以由 $3$ 种状态转移而来。 - 上一步所在石子到下一步所在石头的距离为 $k - 1$。 - 上一步所在石子到下一步所在石头的距离为 $k$。 - 上一步所在石子到下一步所在石头的距离为 $k + 1$。 则我们可以通过石子块数,跳跃距离来进行阶段划分和定义状态,以及推导状态转移方程。 ###### 1. 阶段划分 按照石子块数进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][k]$ 表示为:青蛙能否以长度为 $k$ 的距离,到达第 $i$ 块石子。 ###### 3. 状态转移方程 1. 外层循环遍历每一块石子 $i$,对于每一块石子 $i$,使用内层循环遍历石子 $i$ 之前所有的石子 $j$。 2. 并计算出上一步所在石子 $j$ 到当前所在石子 $i$ 之间的距离为 $k$。 3. 如果上一步所在石子 $j$ 通过上上一步以长度为 $k - 1$、$k$ 或者 $k + 1$ 的距离到达石子 $j$,那么当前步所在石子也可以通过 $k$ 的距离到达石子 $i$。即通过检查 $dp[j][k - 1]$、$dp[j][k]$、$dp[j][k + 1]$ 中是否至少有一个为真,即可判断 $dp[i][k]$ 是否为真。 - 即:$dp[i][k] = dp[j][k - 1] \lor dp[j][k] \lor dp[j][k + 1]$。 ###### 4. 初始条件 刚开始青蛙站在序号为 $0$ 石子上(即 $stones[0]$),肯定能以长度为 $0$ 的距离,到达第 $0$ 块石子,即 $dp[0][0] = True$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][k]$ 表示为:青蛙能否以长度为 $k$ 的距离,到达第 $i$ 块石子。则如果 $dp[size - 1][k]$ 为真,则说明青蛙能成功过河(即能在最后一步跳到最后一块石子上);否则则说明青蛙不能成功过河。 ### 思路 1:动态规划代码 ```python class Solution: def canCross(self, stones: List[int]) -> bool: size = len(stones) stone_dict = dict() for i in range(size): stone_dict[stones[i]] = i dp = [[False for _ in range(size + 1)] for _ in range(size)] dp[0][0] = True for i in range(1, size): for j in range(i): k = stones[i] - stones[j] if k <= 0 or k > j + 1: continue dp[i][k] = dp[j][k - 1] or dp[j][k] or dp[j][k + 1] if dp[size - 1][k]: return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。两重循环遍历的时间复杂度是 $O(n^2)$,所以总的时间复杂度为 $O(n^2)$。 - **空间复杂度**:$O(n^2)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n^2)$。 ## 参考资料 - 【题解】[【403. 青蛙过河】理解理解动态规划与dfs - 青蛙过河 - 力扣](https://leetcode.cn/problems/frog-jump/solution/403-qing-wa-guo-he-li-jie-li-jie-dong-ta-oyt9/) ================================================ FILE: docs/solutions/0400-0499/generate-random-point-in-a-circle.md ================================================ # [0478. 在圆内随机生成点](https://leetcode.cn/problems/generate-random-point-in-a-circle/) - 标签:几何、数学、拒绝采样、随机化 - 难度:中等 ## 题目链接 - [0478. 在圆内随机生成点 - 力扣](https://leetcode.cn/problems/generate-random-point-in-a-circle/) ## 题目大意 **描述**: 给定圆的半径和圆心的位置。 **要求**: 实现函数 `randPoint`,在圆中产生均匀随机点。 实现 `Solution` 类: - `Solution(double radius, double x_center, double y_center)` 用圆的半径 $radius$ 和圆心的位置 $(x_center, y_center)$ 初始化对象 - `randPoint()` 返回圆内的一个随机点。圆周上的一点被认为在圆内。答案作为数组返回 $[x, y]$。 **说明**: - $0 \lt radius \le 10^{8}$。 - $-10^{7} \le x\_center, y\_center \le 10^{7}$。 - `randPoint` 最多被调用 $3 \times 10^{4}$ 次。 **示例**: - 示例 1: ```python 输入: ["Solution","randPoint","randPoint","randPoint"] [[1.0, 0.0, 0.0], [], [], []] 输出: [null, [-0.02493, -0.38077], [0.82314, 0.38945], [0.36572, 0.17248]] 解释: Solution solution = new Solution(1.0, 0.0, 0.0); solution.randPoint ();//返回[-0.02493,-0.38077] solution.randPoint ();//返回[0.82314,0.38945] solution.randPoint ();//返回[0.36572,0.17248] ``` ## 解题思路 ### 思路 1:拒绝采样 这道题要求在圆内生成均匀随机点。最直观的方法是 **拒绝采样(Rejection Sampling)**。 **算法思路**: 1. 在圆的外接正方形区域内随机生成点 $[x, y]$,其中 $x \in [x\_center - radius, x\_center + radius]$,$y \in [y\_center - radius, y\_center + radius]$。 2. 判断点是否在圆内:计算点与圆心的距离 $dist = \sqrt{(x - x\_center)^2 + (y - y\_center)^2}$。 3. 如果 $dist \le radius$,则接受该点;否则拒绝,重新生成。 4. 重复生成直到得到一个在圆内的点。 ### 思路 1:代码 ```python import random class Solution: def __init__(self, radius: float, x_center: float, y_center: float): self.radius = radius # 圆的半径 self.x_center = x_center # 圆心 x 坐标 self.y_center = y_center # 圆心 y 坐标 def randPoint(self) -> List[float]: # 在外接正方形内生成随机点 while True: # 生成 [x_center - radius, x_center + radius] 范围内的随机 x 坐标 x = random.uniform(self.x_center - self.radius, self.x_center + self.radius) # 生成 [y_center - radius, y_center + radius] 范围内的随机 y 坐标 y = random.uniform(self.y_center - self.radius, self.y_center + self.radius) # 计算点到圆心的距离 dist = ((x - self.x_center) ** 2 + (y - self.y_center) ** 2) ** 0.5 # 如果点在圆内(包括圆周上),则接受该点 if dist <= self.radius: return [x, y] # Your Solution object will be instantiated and called as such: # obj = Solution(radius, x_center, y_center) # param_1 = obj.randPoint() ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$ 期望时间复杂度。由于每次生成点的成功率为 $\frac{\pi r^2}{(2r)^2} = \frac{\pi}{4} \approx 0.785$,期望生成次数为常数级。 - **空间复杂度**:$O(1)$。只使用了常数个变量。 ================================================ FILE: docs/solutions/0400-0499/hamming-distance.md ================================================ # [0461. 汉明距离](https://leetcode.cn/problems/hamming-distance/) - 标签:位运算 - 难度:简单 ## 题目链接 - [0461. 汉明距离 - 力扣](https://leetcode.cn/problems/hamming-distance/) ## 题目大意 给定两个整数 x 和 y,计算他们之间的汉明距离。 - 汉明距离:两个数字对应二进制位上不同的位置的数目 ## 解题思路 先对两个数进行异或运算(相同位置上,值相同,结果为 0,值不同,结果为 1),用于记录 x 和 y 不同位置上的异同情况。 然后再按位统计异或结果中 1 的位数。 这里统计 1 的位数可以逐位移动,检查每一位是否为 1。 也可以借助 $n \text{ \& } (n - 1)$ 运算。这个运算刚好可以将 n 的二进制中最低位的 1 变为 0。 ## 代码 1. 逐位移动 ```python class Solution: def hammingDistance(self, x: int, y: int) -> int: xor = x ^ y distance = 0 while xor: if xor & 1: distance += 1 xor >>= 1 return distance ``` 2. $n \text{ \& } (n - 1)$ 运算 ```python class Solution: def hammingDistance(self, x: int, y: int) -> int: xor = x ^ y distance = 0 while xor: distance += 1 xor = xor & (xor - 1) return distance ``` ================================================ FILE: docs/solutions/0400-0499/heaters.md ================================================ # [0475. 供暖器](https://leetcode.cn/problems/heaters/) - 标签:数组、双指针、二分查找、排序 - 难度:中等 ## 题目链接 - [0475. 供暖器 - 力扣](https://leetcode.cn/problems/heaters/) ## 题目大意 **描述**: 给定位于一条水平线上的房屋 $houses$ 和供暖器 $heaters$ 的位置。 **要求**: 冬季已经来临。 你的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。 在加热器的加热半径范围内的每个房屋都可以获得供暖。 请你找出并返回可以覆盖所有房屋的最小加热半径。 **说明**: - 注意:所有供暖器 $heaters$ 都遵循你的半径标准,加热的半径也一样。 - $1 \le houses.length, heaters.length \le 3 \times 10^{4}$。 - $1 \le houses[i], heaters[i] \le 10^{9}$。 **示例**: - 示例 1: ```python 输入: houses = [1,2,3], heaters = [2] 输出: 1 解释: 仅在位置 2 上有一个供暖器。如果我们将加热半径设为 1,那么所有房屋就都能得到供暖。 ``` - 示例 2: ```python 输入:houses = [1,5], heaters = [2] 输出:3 ``` ## 解题思路 ### 思路 1:排序 + 二分查找 为了能够覆盖所有房屋,我们需要找到每个房屋最近的供暖器距离,然后在这些距离中取最大值作为最小加热半径。 具体思路如下: 1. **排序**:先对房屋数组 $houses$ 和供暖器数组 $heaters$ 进行排序。 2. **二分查找**:对于每个房屋 $houses[i]$,使用二分查找找到距离它最近的供暖器位置。 3. **计算距离**:对于每个房屋,计算到最近供暖器的距离 $distance$。 4. **取最大值**:在所有距离中取最大值,即为所需的最小加热半径。 实现细节: - 对房屋 $houses[i]$,在排序后的 $heaters$ 中二分查找第一个大于等于 $houses[i]$ 的供暖器位置 $right$。 - 计算 $houses[i]$ 到 $heaters[right]$ 的距离 $dist1$。 - 计算 $houses[i]$ 到 $heaters[right-1]$ 的距离 $dist2$(如果存在)。 - 取 $min(dist1, dist2)$ 作为当前房屋最近的供暖器距离。 ### 思路 1:代码 ```python class Solution: def findRadius(self, houses: List[int], heaters: List[int]) -> int: # 对房屋和供暖器进行排序 houses.sort() heaters.sort() # 初始化答案为最小整数 ans = 0 # 遍历每个房屋 for house in houses: # 使用二分查找找到第一个大于等于 house 的供暖器位置 left, right = 0, len(heaters) - 1 right_pos = len(heaters) # 初始化右边界位置 while left <= right: mid = (left + right) // 2 if heaters[mid] >= house: right_pos = mid right = mid - 1 else: left = mid + 1 # 计算到右侧供暖器的距离(如果存在) dist1 = abs(house - heaters[right_pos]) if right_pos < len(heaters) else float('inf') # 计算到左侧供暖器的距离(如果存在) dist2 = abs(house - heaters[right_pos - 1]) if right_pos > 0 else float('inf') # 取较小的距离作为当前房屋最近的供暖器距离 min_dist = min(dist1, dist2) # 更新全局答案,取所有房屋最近供暖器距离的最大值 ans = max(ans, min_dist) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n + m \log m)$,其中 $n$ 是房屋数量,$m$ 是供暖器数量。排序的时间复杂度为 $O(n \log n + m \log m)$,对每个房屋进行二分查找的时间复杂度为 $O(n \log m)$,总体时间复杂度为 $O(n \log n + m \log m)$。 - **空间复杂度**:$O(\log n + \log m)$,排序所需的额外空间。 ================================================ FILE: docs/solutions/0400-0499/implement-rand10-using-rand7.md ================================================ # [0470. 用 Rand7() 实现 Rand10()](https://leetcode.cn/problems/implement-rand10-using-rand7/) - 标签:数学、拒绝采样、概率与统计、随机化 - 难度:中等 ## 题目链接 - [0470. 用 Rand7() 实现 Rand10() - 力扣](https://leetcode.cn/problems/implement-rand10-using-rand7/) ## 题目大意 **描述**: 给定方法 `rand7` 可生成 $[1,7]$ 范围内的均匀随机整数。 **要求**: 试写一个方法 `rand10` 生成 `[1,10]` 范围内的均匀随机整数。 你只能调用 `rand7()` 且不能调用其他方法。请不要使用系统的 `Math.random()` 方法。 每个测试用例将有一个内部参数 $n$,即你实现的函数 `rand10()` 在测试时将被调用的次数。请注意,这不是传递给 `rand10()` 的参数。 **说明**: - $1 \le n \le 10^{5}$。 - 进阶: - `rand7()` 调用次数的期望值是多少 ?。 - 你能否尽量少调用 `rand7()`?。 **示例**: - 示例 1: ```python 输入: 1 输出: [2] ``` - 示例 2: ```python 输入: 2 输出: [2,8] ``` ## 解题思路 ### 思路 1:拒绝采样 我们可以通过拒绝采样(Rejection Sampling)的方法,用 `rand7()` 生成 `rand10()`。 具体思路: 1. 调用两次 `rand7()`,生成两个随机数 $a$ 和 $b$。 2. 通过 $7 \times (a-1) + b$ 生成 $[1, 49]$ 范围内的均匀随机整数(因为 $a \in [1, 7]$,$b \in [1, 7]$,所以 $a-1 \in [0, 6]$,$7 \times (a-1) + b \in [1, 49]$)。 3. 只接受 $[1, 40]$ 范围内的数,拒绝 $[41, 49]$ 的数,如果被拒绝则重新生成。这样我们可以得到 $[1, 40]$ 范围内均匀分布的随机数。 4. 将接受的数字对 $10$ 取模并加 $1$,即 $(num \% 10) + 1$,得到 $[1, 10]$ 范围内的均匀随机整数。 为什么只接受 $[1, 40]$ 而不接受 $[1, 49]$? - 因为我们需要生成 $[1, 10]$ 的均匀随机数。 - 如果接受 $[1, 49]$,那么 $1-9$ 会出现 $5$ 次,$10$ 会出现 $4$ 次,这样就不均匀了。 - 如果只接受 $[1, 40]$,那么每个数字 $[1, 10]$ 都会出现恰好 $4$ 次,保证了均匀性。 接受率的计算: - 接受 $40$ 个数字,拒绝 $9$ 个数字。 - 接受率为 $p = \frac{40}{49} = \frac{40}{49}$。 - 期望调用 `rand7()` 的次数为 $E = 2 \times \frac{49}{40} = 2.45$。 ### 思路 1:代码 ```python # The rand7() API is already defined for you. # def rand7(): # @return a random integer in the range 1 to 7 class Solution: def rand10(self): """ :rtype: int """ while True: # 第一次调用 rand7() 生成 [1, 7] 的随机数 a a = rand7() # 第二次调用 rand7() 生成 [1, 7] 的随机数 b b = rand7() # 生成 [1, 49] 范围内的均匀随机数 num = 7 * (a - 1) + b # 只接受 [1, 40] 范围内的数 if num <= 40: # 对 10 取模并加 1,得到 [1, 10] 的均匀随机数 return (num % 10) + 1 # 如果 num > 40,拒绝这个数字,继续循环重新生成 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$ 期望时间复杂度。期望调用 `rand7()` 的次数为 $\frac{2 \times 49}{40} = 2.45$ 次。 - **空间复杂度**:$O(1)$。只使用了几个额外的变量。 ================================================ FILE: docs/solutions/0400-0499/index.md ================================================ ## 本章内容 - [0400. 第 N 位数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/nth-digit.md) - [0401. 二进制手表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/binary-watch.md) - [0402. 移掉 K 位数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/remove-k-digits.md) - [0403. 青蛙过河](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/frog-jump.md) - [0404. 左叶子之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sum-of-left-leaves.md) - [0405. 数字转换为十六进制数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/convert-a-number-to-hexadecimal.md) - [0406. 根据身高重建队列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/queue-reconstruction-by-height.md) - [0407. 接雨水 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/trapping-rain-water-ii.md) - [0408. 有效单词缩写](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/valid-word-abbreviation.md) - [0409. 最长回文串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/longest-palindrome.md) - [0410. 分割数组的最大值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/split-array-largest-sum.md) - [0411. 最短独占单词缩写](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-unique-word-abbreviation.md) - [0412. Fizz Buzz](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/fizz-buzz.md) - [0413. 等差数列划分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/arithmetic-slices.md) - [0414. 第三大的数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/third-maximum-number.md) - [0415. 字符串相加](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-strings.md) - [0416. 分割等和子集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/partition-equal-subset-sum.md) - [0417. 太平洋大西洋水流问题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/pacific-atlantic-water-flow.md) - [0418. 屏幕可显示句子的数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sentence-screen-fitting.md) - [0419. 棋盘上的战舰](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/battleships-in-a-board.md) - [0420. 强密码检验器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/strong-password-checker.md) - [0421. 数组中两个数的最大异或值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/maximum-xor-of-two-numbers-in-an-array.md) - [0422. 有效的单词方块](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/valid-word-square.md) - [0423. 从英文中重建数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/reconstruct-original-digits-from-english.md) - [0424. 替换后的最长重复字符](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/longest-repeating-character-replacement.md) - [0425. 单词方块](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/word-squares.md) - [0426. 将二叉搜索树转化为排序的双向链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/convert-binary-search-tree-to-sorted-doubly-linked-list.md) - [0427. 建立四叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/construct-quad-tree.md) - [0428. 序列化和反序列化 N 叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/serialize-and-deserialize-n-ary-tree.md) - [0429. N 叉树的层序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/n-ary-tree-level-order-traversal.md) - [0430. 扁平化多级双向链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/flatten-a-multilevel-doubly-linked-list.md) - [0431. 将 N 叉树编码为二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/encode-n-ary-tree-to-binary-tree.md) - [0432. 全 O(1) 的数据结构](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/all-oone-data-structure.md) - [0433. 最小基因变化](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-genetic-mutation.md) - [0434. 字符串中的单词数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/number-of-segments-in-a-string.md) - [0435. 无重叠区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/non-overlapping-intervals.md) - [0436. 寻找右区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-right-interval.md) - [0437. 路径总和 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/path-sum-iii.md) - [0438. 找到字符串中所有字母异位词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-all-anagrams-in-a-string.md) - [0439. 三元表达式解析器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/ternary-expression-parser.md) - [0440. 字典序的第K小数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/k-th-smallest-in-lexicographical-order.md) - [0441. 排列硬币](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/arranging-coins.md) - [0442. 数组中重复的数据](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-all-duplicates-in-an-array.md) - [0443. 压缩字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/string-compression.md) - [0444. 序列重建](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sequence-reconstruction.md) - [0445. 两数相加 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/add-two-numbers-ii.md) - [0446. 等差数列划分 II - 子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/arithmetic-slices-ii-subsequence.md) - [0447. 回旋镖的数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/number-of-boomerangs.md) - [0448. 找到所有数组中消失的数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-all-numbers-disappeared-in-an-array.md) - [0449. 序列化和反序列化二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/serialize-and-deserialize-bst.md) - [0450. 删除二叉搜索树中的节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/delete-node-in-a-bst.md) - [0451. 根据字符出现频率排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sort-characters-by-frequency.md) - [0452. 用最少数量的箭引爆气球](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-number-of-arrows-to-burst-balloons.md) - [0453. 最小操作次数使数组元素相等](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-moves-to-equal-array-elements.md) - [0454. 四数相加 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/4sum-ii.md) - [0455. 分发饼干](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/assign-cookies.md) - [0456. 132 模式](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/132-pattern.md) - [0457. 环形数组是否存在循环](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/circular-array-loop.md) - [0458. 可怜的小猪](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/poor-pigs.md) - [0459. 重复的子字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/repeated-substring-pattern.md) - [0460. LFU 缓存](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/lfu-cache.md) - [0461. 汉明距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/hamming-distance.md) - [0462. 最小操作次数使数组元素相等 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/minimum-moves-to-equal-array-elements-ii.md) - [0463. 岛屿的周长](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/island-perimeter.md) - [0464. 我能赢吗](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/can-i-win.md) - [0465. 最优账单平衡](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/optimal-account-balancing.md) - [0466. 统计重复个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/count-the-repetitions.md) - [0467. 环绕字符串中唯一的子字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/unique-substrings-in-wraparound-string.md) - [0468. 验证IP地址](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/validate-ip-address.md) - [0469. 凸多边形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/convex-polygon.md) - [0470. 用 Rand7() 实现 Rand10()](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/implement-rand10-using-rand7.md) - [0471. 编码最短长度的字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/encode-string-with-shortest-length.md) - [0472. 连接词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/concatenated-words.md) - [0473. 火柴拼正方形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/matchsticks-to-square.md) - [0474. 一和零](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/ones-and-zeroes.md) - [0475. 供暖器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/heaters.md) - [0476. 数字的补数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/number-complement.md) - [0477. 汉明距离总和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/total-hamming-distance.md) - [0478. 在圆内随机生成点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/generate-random-point-in-a-circle.md) - [0479. 最大回文数乘积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/largest-palindrome-product.md) - [0480. 滑动窗口中位数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/sliding-window-median.md) - [0481. 神奇字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/magical-string.md) - [0482. 密钥格式化](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/license-key-formatting.md) - [0483. 最小好进制](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/smallest-good-base.md) - [0484. 寻找排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/find-permutation.md) - [0485. 最大连续 1 的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/max-consecutive-ones.md) - [0486. 预测赢家](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/predict-the-winner.md) - [0487. 最大连续1的个数 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/max-consecutive-ones-ii.md) - [0488. 祖玛游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/zuma-game.md) - [0489. 扫地机器人](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/robot-room-cleaner.md) - [0490. 迷宫](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/the-maze.md) - [0491. 非递减子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/non-decreasing-subsequences.md) - [0492. 构造矩形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/construct-the-rectangle.md) - [0493. 翻转对](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/reverse-pairs.md) - [0494. 目标和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/target-sum.md) - [0495. 提莫攻击](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/teemo-attacking.md) - [0496. 下一个更大元素 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/next-greater-element-i.md) - [0497. 非重叠矩形中的随机点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/random-point-in-non-overlapping-rectangles.md) - [0498. 对角线遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/diagonal-traverse.md) - [0499. 迷宫 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/the-maze-iii.md) ================================================ FILE: docs/solutions/0400-0499/island-perimeter.md ================================================ # [0463. 岛屿的周长](https://leetcode.cn/problems/island-perimeter/) - 标签:深度优先搜索、广度优先搜索、数组、矩阵 - 难度:简单 ## 题目链接 - [0463. 岛屿的周长 - 力扣](https://leetcode.cn/problems/island-perimeter/) ## 题目大意 **描述**:给定一个 `row * col` 大小的二维网格地图 `grid` ,其中:`grid[i][j] = 1` 表示陆地,`grid[i][j] = 0` 表示水域。 网格中的格子水平和垂直方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(多个表示陆地的格子相连组成)。 岛屿内部中没有「湖」(指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。 **要求**:计算这个岛屿的周长。 **说明**: - $row == grid.length$。 - $col == grid[i].length$。 - $1 <= row, col <= 100$。 - $grid[i][j]$ 为 $0$ 或 $1$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/10/12/island.png) ```python 输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]] 输出:16 解释:它的周长是上面图片中的 16 个黄色的边 ``` - 示例 2: ```python 输入:grid = [[1]] 输出:4 ``` ## 解题思路 ### 思路 1:广度优先搜索 1. 使用整形变量 `count` 存储周长,使用队列 `queue` 用于进行广度优先搜索。 2. 遍历一遍二维数组 `grid`,对 `grid[row][col] == 1` 的区域进行广度优先搜索。 3. 先将起始点 `(row, col)` 加入队列。 4. 如果队列不为空,则取出队头坐标 `(row, col)`。先将 `(row, col)` 标记为 `2`,避免重复统计。 5. 然后遍历上、下、左、右四个方向的相邻区域,如果遇到边界或者水域,则周长加 1。 6. 如果相邻区域 `grid[new_row][new_col] == 1`,则将其赋值为 `2`,并将坐标加入队列。 7. 继续执行 4 ~ 6 步,直到队列为空时返回 `count`。 ### 思路 1:代码 ```python class Solution: def bfs(self, grid, rows, cols, row, col): directs = [(0, 1), (0, -1), (1, 0), (-1, 0)] queue = collections.deque([(row, col)]) count = 0 while queue: row, col = queue.popleft() # 避免重复统计 grid[row][col] = 2 for direct in directs: new_row = row + direct[0] new_col = col + direct[1] # 遇到边界或者水域,则周长加 1 if new_row < 0 or new_row >= rows or new_col < 0 or new_col >= cols or grid[new_row][new_col] == 0: count += 1 # 相邻区域为陆地,则将其标记为 2,加入队列 elif grid[new_row][new_col] == 1: grid[new_row][new_col] = 2 queue.append((new_row, new_col)) # 相邻区域为 2 的情况不做处理 return count def islandPerimeter(self, grid: List[List[int]]) -> int: rows, cols = len(grid), len(grid[0]) for row in range(rows): for col in range(cols): if grid[row][col] == 1: return self.bfs(grid, rows, cols, row, col) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $m$ 和 $n$ 分别为行数和列数。 - **空间复杂度**:$O(n \times m)$。 ## 参考资料 - 【题解】[Golang BFS 实现,性能比dfs要高 - 岛屿的周长 - 力扣](https://leetcode.cn/problems/island-perimeter/solution/golang-bfs-shi-xian-xing-neng-bi-dfsyao-nln2g/) ================================================ FILE: docs/solutions/0400-0499/k-th-smallest-in-lexicographical-order.md ================================================ # [0440. 字典序的第K小数字](https://leetcode.cn/problems/k-th-smallest-in-lexicographical-order/) - 标签:字典树 - 难度:困难 ## 题目链接 - [0440. 字典序的第K小数字 - 力扣](https://leetcode.cn/problems/k-th-smallest-in-lexicographical-order/) ## 题目大意 **描述**: 给定整数 $n$ 和 $k$。 **要求**: 返回 $[1, n]$ 中字典序第 $k$ 小的数字。 **说明**: - $1 \le k \le n \le 10^{9}$。 **示例**: - 示例 1: ```python 输入: n = 13, k = 2 输出: 10 解释: 字典序的排列是 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9],所以第二小的数字是 10。 ``` - 示例 2: ```python 输入: n = 1, k = 1 输出: 1 ``` ## 解题思路 ### 思路 1:前缀计数法 这道题可以利用字典树(十叉树)的思想来解决。我们可以将 $[1, n]$ 中的数字看作一棵字典树的节点,每个节点最多有 $10$ 个子节点($0-9$)。 核心思路是:**通过前缀计数的方式,逐步确定第 $k$ 小数字的每一位**。 算法步骤: 1. **初始化**:从 $prefix = 1$ 开始,$k = k$(还需要找到的数字数量)。 2. **前缀搜索**:对于当前前缀 $prefix$,计算以它为前缀且不超过 $n$ 的数字个数 $count$。 3. **判断**: - 如果 $count < k$,说明第 $k$ 小的数字不在以 $prefix$ 为前缀的子树中,跳过这些数字,更新 $k = k - count$,并尝试下一个前缀 $prefix + 1$。 - 如果 $count \ge k$,说明第 $k$ 小的数字在以 $prefix$ 为前缀的子树中,将 $prefix$ 作为当前位的结果,继续深入下一层,即 $prefix = prefix \times 10$,$k = k - 1$(因为 $prefix$ 本身也是一个数字)。 4. **重复**直到 $k = 0$,此时 $prefix$ 就是所求的第 $k$ 小数字。 对于**计算前缀数量**的辅助函数 $countPrefix(prefix, n)$: - 从 $prefix$ 开始,统计所有以 $prefix$ 为前缀且在 $[1, n]$ 范围内的数字。 - 使用层次遍历的方式:下一层的数字为 $prefix \times 10$ 到 $prefix \times 10 + 9$。 - 例如 $countPrefix(1, 13) = 7$(包含 $1, 10, 11, 12, 13$),$countPrefix(2, 13) = 1$(只包含 $2$)。 ### 思路 1:代码 ```python class Solution: def countPrefix(self, prefix: int, n: int) -> int: """计算以 prefix 为前缀且在 [1, n] 范围内的数字个数""" count = 0 # 当前层的数字范围 first = prefix # 当前层第一个数字 next_prefix = prefix + 1 # 下一层前缀 while first <= n: # 计算当前层有多少个数字(不超过 n) count += min(next_prefix, n + 1) - first # 移动到下一层(扩大10倍) first *= 10 next_prefix *= 10 return count def findKthNumber(self, n: int, k: int) -> int: # 初始化:从前缀 1 开始 prefix = 1 k = k # 还需要找到的数字数量 while k > 1: # 计算以当前 prefix 为前缀且不超过 n 的数字个数 count = self.countPrefix(prefix, n) if count < k: # 如果数量少于 k,说明第 k 小的数字不在这个前缀下 # 跳过这些数字,尝试下一个前缀 k -= count prefix += 1 else: # 如果数量大于等于 k,说明第 k 小的数字在这个前缀下 # 继续深入这一层,prefix 作为当前位的结果 k -= 1 prefix *= 10 return prefix ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log^2 n)$,其中 $n$ 是给定的数字范围。外层循环最多执行 $O(\log n)$ 次(每层确定一位数字),内层 $countPrefix$ 函数的时间复杂度也为 $O(\log n)$(因为需要逐层计算,层数不超过 $\log_{10} n$)。总体时间复杂度为 $O(\log^2 n)$。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/largest-palindrome-product.md ================================================ # [0479. 最大回文数乘积](https://leetcode.cn/problems/largest-palindrome-product/) - 标签:数学、枚举 - 难度:困难 ## 题目链接 - [0479. 最大回文数乘积 - 力扣](https://leetcode.cn/problems/largest-palindrome-product/) ## 题目大意 **描述**: 给定一个整数 $n$。 **要求**: 返回可表示为两个 $n$ 位整数乘积的「最大回文整数」。因为答案可能非常大,所以返回它对 $1337$ 取余。 **说明**: - $1 \le n \le 8$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:987 解释:99 x 91 = 9009, 9009 % 1337 = 987 ``` - 示例 2: ```python 输入:n = 1 输出:9 ``` ## 解题思路 ### 思路 1:数学 + 枚举 这道题的核心思想是**构造回文数并检查是否能分解为两个 $n$ 位整数的乘积**。 具体思路如下: 1. **确定范围**: - 两个 $n$ 位整数的最小值为 $10^{n-1}$,最大值为 $10^n - 1$。 - 最大乘积为 $(10^n - 1)^2$。 2. **构造回文数**: - 从大到小枚举所有可能的回文数(因为要找最大的)。 - 对于 $2n$ 位的回文数,可以表示为:$p = \text{left} \times 10^n + \text{reverse(left)}$,其中 $\text{left}$ 为 $n$ 位数字。 - 例如:$n = 2$ 时,$99$ 对应回文数 $9999$,$98$ 对应回文数 $9889$。 3. **检查回文数是否能分解**: - 对于每个回文数 $p$,检查是否存在两个 $n$ 位整数 $a$ 和 $b$,使得 $a \times b = p$。 - 由于 $p = a \times b$,则 $a$ 和 $b$ 中至少有一个不小于 $\sqrt{p}$。 - 因此可以从 $a = \lfloor \sqrt{p} \rfloor$ 开始枚举到 $10^n - 1$(取较小值的上界)。 4. **返回结果**: - 找到第一个可以分解的回文数 $p$,返回 $p \% 1337$。 **关键点**: - 注意 $n = 1$ 的特殊情况,直接返回 $9$。 - 构造回文数时,需要处理奇数和偶数位数的情况。 - 通过反向构造回文数可以避免生成和检查所有数字。 ### 思路 1:代码 ```python class Solution: def largestPalindrome(self, n: int) -> int: # n = 1 的特殊情况 if n == 1: return 9 # 计算上下界 upper = 10 ** n - 1 # n 位数的最大值 lower = 10 ** (n - 1) # n 位数的最小值 # 从大到小枚举所有可能的回文数 # 通过 left 构造回文数:left + reverse(left) for left in range(upper, lower - 1, -1): # 构造回文数 p p = int(str(left) + str(left)[::-1]) # 检查是否能分解为两个 n 位数的乘积 # 从 sqrt(p) 开始向下枚举,避免重复计算 for divisor in range(upper, int(p ** 0.5) - 1, -1): if p % divisor == 0: # 检查另一个因子是否在 [lower, upper] 范围内 if lower <= p // divisor <= upper: return p % 1337 # 理论上不会执行到这里 return 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(10^{2n})$。外层循环枚举所有 $n$ 位数(共 $10^n - 10^{n-1}$ 个),内层循环在最坏情况下需要检查 $O(10^n)$ 个因子,但实际上由于从 $\sqrt{p}$ 开始枚举,实际时间复杂度会更低。 - **空间复杂度**:$O(1)$。只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0400-0499/lfu-cache.md ================================================ # [0460. LFU 缓存](https://leetcode.cn/problems/lfu-cache/) - 标签:设计、哈希表、链表、双向链表 - 难度:困难 ## 题目链接 - [0460. LFU 缓存 - 力扣](https://leetcode.cn/problems/lfu-cache/) ## 题目大意 **要求**: 请你为「[最不经常使用(LFU)](https://baike.baidu.com/item/%E7%BC%93%E5%AD%98%E7%AE%97%E6%B3%95)」缓存算法设计并实现数据结构。 实现 `LFUCache` 类: - `LFUCache(int capacity)` - 用数据结构的容量 $capacity$ 初始化对象。 - `int get(int key)` - 如果键 $key$ 存在于缓存中,则获取键的值,否则返回 $-1$。 - `void put(int key, int value)` - 如果键 $key$ 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 $capacity$ 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除「最久未使用」的键。 为了确定最不常使用的键,可以为缓存中的每个键维护一个「使用计数器」。使用计数最小的键是最久未使用的键。 当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 `put` 操作)。对缓存中的键执行 `get` 或 `put` 操作,使用计数器的值将会递增。 函数 `get` 和 `put` 必须以 $O(1)$ 的平均时间复杂度运行。 **说明**: - $1 \le capacity \le 10^{4}$。 - $0 \le key \le 10^{5}$。 - $0 \le value \le 10^{9}$。 - 最多调用 $2 \times 10^{5}$ 次 `get` 和 `put` 方法。 **示例**: - 示例 1: ```python 输入: ["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]] 输出: [null, null, null, 1, null, -1, 3, null, -1, 3, 4] 解释: // cnt(x) = 键 x 的使用计数 // cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的) LFUCache lfu = new LFUCache(2); lfu.put(1, 1); // cache=[1,_], cnt(1)=1 lfu.put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1 lfu.get(1); // 返回 1 // cache=[1,2], cnt(2)=1, cnt(1)=2 lfu.put(3, 3); // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小 // cache=[3,1], cnt(3)=1, cnt(1)=2 lfu.get(2); // 返回 -1(未找到) lfu.get(3); // 返回 3 // cache=[3,1], cnt(3)=2, cnt(1)=2 lfu.put(4, 4); // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用 // cache=[4,3], cnt(4)=1, cnt(3)=2 lfu.get(1); // 返回 -1(未找到) lfu.get(3); // 返回 3 // cache=[3,4], cnt(4)=1, cnt(3)=3 lfu.get(4); // 返回 4 // cache=[3,4], cnt(4)=2, cnt(3)=3 ``` ## 解题思路 ### 思路 1:双层双向链表 + 哈希表 LFU Cache 的核心在于既要维护频率信息,又要维护相同频率下的 LRU 顺序。 我们可以使用以下数据结构: - 使用一个哈希表 $node\_dict$ 存储键值对 $(key, node)$,用于 $O(1)$ 时间访问节点 - 使用一个哈希表 $freq\_dict$ 存储频率到双向链表的映射 $freq \rightarrow DoublyLinkedList$ - 每个节点包含 $key$、$value$、$freq$(频率)、$prev$、$next$ 指针 - 每个频率对应一个双向链表,链表内部按照 LRU 规则排序(头部是最新访问,尾部是最久未访问) 算法步骤: 1. `get(key)` 操作: - 如果 $key$ 不在 $node\_dict$ 中,返回 $-1$ - 如果 $key$ 存在,从原来的频率链表中移除,频率加 $1$,插入到新频率链表中,更新 $node\_dict$ - 更新最小频率 $min\_freq$(如果原来的最小频率链表为空) 2. `put(key, value)` 操作: - 如果 $key$ 已存在,更新其 $value$,并执行一次 $get$ 操作来更新频率 - 如果 $key$ 不存在: - 如果容量已满,删除 $freq\_dict[min\_freq]$ 链表尾部的节点 - 创建新节点,频率为 $1$,插入到频率为 $1$ 的链表中 - 更新 $min\_freq = 1$ ### 思路 1:代码 ```python class Node: def __init__(self, key=0, value=0, freq=0): self.key = key self.value = value self.freq = freq # 使用频率 self.prev = None # 前驱节点 self.next = None # 后继节点 class DoublyLinkedList: """双向链表,用于维护相同频率的节点""" def __init__(self): # 创建虚拟头尾节点 self.head = Node() self.tail = Node() self.head.next = self.tail self.tail.prev = self.head self.size = 0 # 当前链表大小 def add_node_to_head(self, node): """在链表头部添加节点""" node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node self.size += 1 def remove_node(self, node): """删除指定节点""" node.prev.next = node.next node.next.prev = node.prev self.size -= 1 def remove_tail(self): """删除链表尾部节点(最久未使用)""" if self.size == 0: return None node = self.tail.prev self.remove_node(node) return node class LFUCache: def __init__(self, capacity: int): self.capacity = capacity self.min_freq = 0 # 最小频率 self.node_dict = {} # 存储键值对:key -> node self.freq_dict = {} # 存储频率到双向链表的映射:freq -> DoublyLinkedList def get(self, key: int) -> int: # 如果 key 不存在,返回 -1 if key not in self.node_dict: return -1 node = self.node_dict[key] # 从原来频率的链表中移除 self.freq_dict[node.freq].remove_node(node) # 如果移除后,该频率链表为空且是最小频率,更新 min_freq if self.freq_dict[node.freq].size == 0 and self.min_freq == node.freq: self.min_freq += 1 # 频率加 1 node.freq += 1 # 将节点插入到新频率链表的头部 if node.freq not in self.freq_dict: self.freq_dict[node.freq] = DoublyLinkedList() self.freq_dict[node.freq].add_node_to_head(node) return node.value def put(self, key: int, value: int) -> None: if self.capacity == 0: return # 如果 key 已存在,更新 value 并执行 get 操作来更新频率 if key in self.node_dict: node = self.node_dict[key] node.value = value self.get(key) # 触发频率更新 return # 如果 key 不存在,需要插入新节点 # 如果容量已满,需要删除最少使用的节点 if len(self.node_dict) >= self.capacity: # 删除 min_freq 链表尾部的节点 tail_node = self.freq_dict[self.min_freq].remove_tail() del self.node_dict[tail_node.key] # 创建新节点,频率为 1 node = Node(key, value, freq=1) self.node_dict[key] = node # 插入到频率为 1 的链表头部 if 1 not in self.freq_dict: self.freq_dict[1] = DoublyLinkedList() self.freq_dict[1].add_node_to_head(node) # 更新最小频率为 1 self.min_freq = 1 # Your LFUCache object will be instantiated and called as such: # obj = LFUCache(capacity) # param_1 = obj.get(key) # obj.put(key,value) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。所有操作(`get` 和 `put`)的平均时间复杂度都是 $O(1)$,因为都是通过哈希表访问节点,通过双向链表进行插入和删除。 - **空间复杂度**:$O(capacity)$。其中 $capacity$ 是缓存的容量。哈希表的空间复杂度为 $O(capacity)$,双向链表节点数为 $O(capacity)$。 ================================================ FILE: docs/solutions/0400-0499/license-key-formatting.md ================================================ # [0482. 密钥格式化](https://leetcode.cn/problems/license-key-formatting/) - 标签:字符串 - 难度:简单 ## 题目链接 - [0482. 密钥格式化 - 力扣](https://leetcode.cn/problems/license-key-formatting/) ## 题目大意 **描述**: 给定一个许可密钥字符串 $s$,仅由字母、数字字符和破折号组成。字符串由 $n$ 个破折号分成 $n + 1$ 组。你也会得到一个整数 $k$。 我们想要重新格式化字符串 $s$,使每一组包含 $k$ 个字符,除了第一组,它可以比 $k$ 短,但仍然必须包含至少一个字符。此外,两组之间必须插入破折号,并且应该将所有小写字母转换为大写字母。 **要求**: 返回「重新格式化的许可密钥」。 **说明**: - $1 \le s.length \le 10^{5}$。 - $s$ 只包含字母、数字和破折号 `'-'`。 - $1 \le k \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:S = "5F3Z-2e-9-w", k = 4 输出:"5F3Z-2E9W" 解释:字符串 S 被分成了两个部分,每部分 4 个字符; 注意,两个额外的破折号需要删掉。 ``` - 示例 2: ```python 输入:S = "2-5g-3-J", k = 2 输出:"2-5G-3J" 解释:字符串 S 被分成了 3 个部分,按照前面的规则描述,第一部分的字符可以少于给定的数量,其余部分皆为 2 个字符。 ``` ## 解题思路 ### 思路 1:模拟 这道题的思路比较直观,我们需要重新格式化密钥字符串。 具体思路如下: 1. **预处理**:遍历字符串 $s$,去除所有破折号,并将小写字母转换为大写字母,得到一个新的字符串 $res$。 2. **分组插入破折号**:从后往前遍历处理后的字符串 $res$,每 $k$ 个字符插入一个破折号。 3. **返回结果**:返回格式化后的字符串。 注意:最后一组(即字符串开头的部分)的长度可能小于 $k$,这是允许的,因为题目要求第一组可以比 $k$ 短。 **算法步骤**: - 遍历原始字符串 $s$,将所有有效的字符(字母和数字)收集起来,并转为大写,存储在列表 $chars$ 中。 - 使用变量 $group\_size$ 记录当前组的字符数,初始化为 $0$。 - 从后往前构建结果字符串:将 $chars$ 中的字符从后往前依次添加到结果中,每 $k$ 个字符添加一个破折号。 ### 思路 1:代码 ```python class Solution: def licenseKeyFormatting(self, s: str, k: int) -> str: # 收集所有有效字符并转为大写 chars = [] for char in s: if char != '-': chars.append(char.upper()) # 如果所有字符都是破折号,返回空字符串 if not chars: return "" # 从后往前构建结果 res = [] group_size = 0 # 当前组的字符数 for i in range(len(chars) - 1, -1, -1): # 添加字符 res.append(chars[i]) group_size += 1 # 每 k 个字符添加一个破折号(最后不添加) if group_size == k and i > 0: res.append('-') group_size = 0 # 反转结果字符串 return ''.join(res[::-1]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。需要遍历字符串两次(收集字符和构建结果)。 - **空间复杂度**:$O(n)$,需要额外的空间存储处理后的字符和结果字符串。 ================================================ FILE: docs/solutions/0400-0499/longest-palindrome.md ================================================ # [0409. 最长回文串](https://leetcode.cn/problems/longest-palindrome/) - 标签:贪心、哈希表、字符串 - 难度:简单 ## 题目链接 - [0409. 最长回文串 - 力扣](https://leetcode.cn/problems/longest-palindrome/) ## 题目大意 给定一个包含大写字母和小写字母的字符串 `s`。 要求:找到通过这些字母构造成的最长的回文串。 注意: - 在构造过程中,请注意区分大小写。比如 `Aa` 不能当做一个回文字符串。 - 假设字符串的长度不会超过 `1010`。 ## 解题思路 这道题目是通过给定字母构造回文串,并找到最长的回文串长度。那就要先看看回文串的特点。在回文串中,最多只有一个字母出现过奇数次,其余字符都出现过偶数次。且相同字母是中心对称的。 则我们可以用哈希表统计字符出现次数。对于每个字符,使用尽可能多的偶数次字符作为回文串的两侧,并记录下使用的字符个数,记录到答案中。再使用一个 `flag` 标记下是否有奇数次的字符,如果有的话,最终答案再加 1。最后输出答案。 ## 代码 ```python class Solution: def longestPalindrome(self, s: str) -> int: word_dict = dict() for ch in s: if ch in word_dict: word_dict[ch] += 1 else: word_dict[ch] = 1 ans = 0 flag = False for value in word_dict.values(): ans += value // 2 * 2 if value % 2 == 1: flag = True if flag: ans += 1 return ans ``` ================================================ FILE: docs/solutions/0400-0499/longest-repeating-character-replacement.md ================================================ # [0424. 替换后的最长重复字符](https://leetcode.cn/problems/longest-repeating-character-replacement/) - 标签:哈希表、字符串、滑动窗口 - 难度:中等 ## 题目链接 - [0424. 替换后的最长重复字符 - 力扣](https://leetcode.cn/problems/longest-repeating-character-replacement/) ## 题目大意 **描述**:给定一个仅由大写英文字母组成的字符串 $s$,以及一个整数 $k$。可以将任意位置上的字符替换成另外的大写字母,最多可替换 $k$ 次。 **要求**:在进行上述操作后,找到包含重复字母的最长子串长度。 **说明**: - $1 \le s.length \le 10^5$。 - $s$ 仅由大写英文字母组成。 - $0 \le k \le s.length$。 **示例**: - 示例 1: ```python 输入:s = "ABAB", k = 2 输出:4 解释:用两个'A'替换为两个'B',反之亦然。 ``` - 示例 2: ```python 输入:s = "AABABBA", k = 1 输出:4 解释: 将中间的一个'A'替换为'B',字符串变为 "AABBBBA"。 子串 "BBBB" 有最长重复字母, 答案为 4。 可能存在其他的方法来得到同样的结果。 ``` ## 解题思路 先来考虑暴力求法。枚举字符串 s 的所有子串,对于每一个子串: - 统计子串中出现次数最多的字符,替换除它以外的字符 k 次。 - 维护最长子串的长度。 但是这种暴力求法中,枚举子串的时间复杂度为 $O(n^2)$,统计出现次数最多的字符和替换字符时间复杂度为 $0(n)$,且两者属于平行处理,总体下来的时间复杂度为 $O(n^3)$。这样做会超时。 ### 思路 1:滑动窗口 1. 使用 counts 数组来统计字母频数。使用 left、right 双指针分别指向滑动窗口的首尾位置,使用 max_count 来维护最长子串的长度。 2. 不断右移 right 指针,增加滑动窗口的长度。 3. 对于当前滑动窗口的子串,如果当前窗口的间距 > 当前出现最大次数的字符的次数 + k 时,意味着替换 k 次仍不能使当前窗口中的字符全变为相同字符,则此时应该将左边界右移,同时将原先左边界的字符频次减少。 ### 思路 1:代码 ```python class Solution: def characterReplacement(self, s: str, k: int) -> int: max_count = 0 left, right = 0, 0 counts = [0 for _ in range(26)] while right < len(s): num_right = ord(s[right]) - ord('A') counts[num_right] += 1 max_count = max(max_count, counts[num_right]) right += 1 if right - left > max_count + k: num_left = ord(s[left]) - ord('A') counts[num_left] -= 1 left += 1 return right - left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串的长度。 - **空间复杂度**:$O(|\sum|)$,其中 $\sum$ 是字符集,本题中 $| \sum | = 26$。 ================================================ FILE: docs/solutions/0400-0499/magical-string.md ================================================ # [0481. 神奇字符串](https://leetcode.cn/problems/magical-string/) - 标签:双指针、字符串 - 难度:中等 ## 题目链接 - [0481. 神奇字符串 - 力扣](https://leetcode.cn/problems/magical-string/) ## 题目大意 **描述**: 神奇字符串 $s$ 仅由 `'1'` 和 `'2'` 组成,并需要遵守下面的规则: - 神奇字符串 $s$ 的神奇之处在于,串联字符串中 `'1'` 和 `'2'` 的连续出现次数可以生成该字符串。 $s$ 的前几个元素是 `s = "1221121221221121122……"`。如果将 $s$ 中连续的若干 $1$ 和 $2$ 进行分组,可以得到 `"1 22 11 2 1 22 1 22 11 2 11 22 ......"`。每组中 $1$ 或者 $2$ 的出现次数分别是 `"1 2 2 1 1 2 1 2 2 1 2 2 ......"`。上面的出现次数正是 $s$ 自身。 给定一个整数 $n$。 **要求**: 返回在神奇字符串 $s$ 的前 $n$ 个数字中 $1$ 的数目。 **说明**: - $1 \le n \le 10^{5}$。 **示例**: - 示例 1: ```python 输入:n = 6 输出:3 解释:神奇字符串 s 的前 6 个元素是 “122112”,它包含三个 1,因此返回 3 。 ``` - 示例 2: ```python 输入:n = 1 输出:1 ``` ## 解题思路 ### 思路 1:双指针模拟 这道题的关键在于理解神奇字符串的构造规律。 由于神奇字符串 $s$ 的特殊性质:将 $s$ 连续的出现次数进行分组,结果正好是 $s$ 自身。我们可以使用双指针来模拟这个过程: - 使用指针 $i$ 表示当前需要重复的字符在 $s$ 中的位置,$s[i]$ 表示需要重复的次数。 - 使用指针 $j$ 表示当前要写入新字符的位置。 - 我们从 $s = "122"$ 开始,使用双指针逐步扩展字符串 $s$。 - 在每个位置,我们读取 $s[i]$ 的值(重复次数),然后在位置 $j$ 写入相应次数的字符。 - 交替写入 $'1'$ 和 $'2'$:如果当前写入的字符是 $'1'$,下一次写入 $'2'$;如果当前是 $'2'$,下一次写入 $'1'$。 **算法步骤**: 1. 初始化字符串 $s$ 为 `"122"`。 2. 初始化指针 $i = 2$(从第 $3$ 个位置开始,因为前 $3$ 个字符已经固定),$j = 3$(从第 $4$ 个位置开始写入)。 3. 初始化计数器 $count\_one$ 用于统计 $1$ 的个数。 4. 当 $j < n$ 时,重复以下步骤: - 读取 $s[i]$ 的值 $repeat$,表示需要重复的次数。 - 根据当前字符交替写入 $repeat$ 次,更新 $count\_one$。 - 将 $i$ 和 $j$ 指针向右移动。 5. 统计前 $n$ 个字符中 $1$ 的个数。 ### 思路 1:代码 ```python class Solution: def magicalString(self, n: int) -> int: # 初始化字符串 s = "122" i = 2 # 读取位置 j = 3 # 写入位置 count_one = 1 # 统计 1 的个数,"122" 中有 1 个 1 # 当前字符标志,True 表示当前应该是 '1',False 表示当前应该是 '2' current_char = True # 扩展字符串直到长度达到 n while j < n: repeat = int(s[i]) # 读取需要重复的次数 # 写入 repeat 次字符 for _ in range(repeat): if j >= n: break if current_char: s += '1' count_one += 1 else: s += '2' j += 1 # 切换字符标志(交替写入 1 和 2) current_char = not current_char i += 1 return count_one ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是给定的整数。我们需要生成长度为 $n$ 的神奇字符串。 - **空间复杂度**:$O(n)$,需要存储长度为 $n$ 的字符串。 ================================================ FILE: docs/solutions/0400-0499/matchsticks-to-square.md ================================================ # [0473. 火柴拼正方形](https://leetcode.cn/problems/matchsticks-to-square/) - 标签:位运算、数组、动态规划、回溯、状态压缩 - 难度:中等 ## 题目链接 - [0473. 火柴拼正方形 - 力扣](https://leetcode.cn/problems/matchsticks-to-square/) ## 题目大意 **描述**:给定一个表示火柴长度的数组 $matchsticks$,其中 $matchsticks[i]$ 表示第 $i$ 根火柴的长度。 **要求**:找出一种能使用所有火柴拼成一个正方形的方法。不能折断火柴,可以将火柴连接起来,并且每根火柴都要用到。如果能拼成正方形,则返回 `True`,否则返回 `False`。 **说明**: - $1 \le matchsticks.length \le 15$。 - $1 \le matchsticks[i] \le 10^8$。 **示例**: - 示例 1: ```python 输入: matchsticks = [1,1,2,2,2] 输出: True 解释: 能拼成一个边长为 2 的正方形,每边两根火柴。 ``` - 示例 2: ```python 输入: matchsticks = [3,3,3,3,4] 输出: False 解释: 不能用所有火柴拼成一个正方形。 ``` ## 解题思路 ### 思路 1:回溯算法 1. 先排除数组为空和火柴总长度不是 $4$ 的倍数的情况,直接返回 `False`。 2. 然后将火柴按照从大到小排序。用数组 $sums$ 记录四个边长分组情况。 3. 将火柴分为 $4$ 组,把每一根火柴依次向 $4$ 条边上放。 4. 直到放置最后一根,判断能否构成正方形,如果能构成正方形,则返回 `True`,否则返回 `False`。 ### 思路 1:代码 ```python class Solution: def dfs(self, index, sums, matchsticks, size, side_len): if index == size: return True for i in range(4): # 如果两条边的情况相等,只需要计算一次,没必要多次重复计算 if i > 0 and sums[i] == sums[i - 1]: continue sums[i] += matchsticks[index] if sums[i] <= side_len and self.dfs(index + 1, sums, matchsticks, size, side_len): return True sums[i] -= matchsticks[index] return False def makesquare(self, matchsticks: List[int]) -> bool: if not matchsticks: return False size = len(matchsticks) sum_len = sum(matchsticks) if sum_len % 4 != 0: return False side_len = sum_len // 4 matchsticks.sort(reverse=True) sums = [0 for _ in range(4)] return self.dfs(0, sums, matchsticks, size, side_len) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(4^n)$。$n$ 是火柴的数目。 - **空间复杂度**:$O(n)$。递归栈的空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0400-0499/max-consecutive-ones-ii.md ================================================ # [0487. 最大连续1的个数 II](https://leetcode.cn/problems/max-consecutive-ones-ii/) - 标签:数组、动态规划、滑动窗口 - 难度:中等 ## 题目链接 - [0487. 最大连续1的个数 II - 力扣](https://leetcode.cn/problems/max-consecutive-ones-ii/) ## 题目大意 **描述**:给定一个二进制数组 $nums$,可以最多将 $1$ 个 $0$ 翻转为 $1$。 **要求**:如果最多可以翻转一个 $0$,则返回数组中连续 $1$ 的最大个数。 **说明**: - 1 <= nums.length <= 105 nums[i] 不是 0 就是 1. **示例**: - 示例 1: ```python 输入:nums = [1,0,1,1,0] 输出:4 解释:翻转第一个 0 可以得到最长的连续 1。当翻转以后,最大连续 1 的个数为 4。 ``` - 示例 2: ```python 输入:nums = [1,0,1,1,0,1] 输出:4 ``` ## 解题思路 ### 思路 1:滑动窗口 暴力解法是遍历数组,将每一个 $0$ 依次翻转为 $1$,并统计此时连续 $1$ 的最大个数,最终取最大值。但这种做法的时间复杂度较高,不够高效。 我们可以采用滑动窗口的方法来优化。核心思想是维护一个窗口,使得窗口内最多只包含 $1$ 个 $0$。具体步骤如下: 设定两个指针 $left$ 和 $right$,分别表示滑动窗口的左右边界。用 $zero\_count$ 统计当前窗口内 $0$ 的数量,用 $ans$ 记录最大连续 $1$ 的个数。 - 初始时,$left$ 和 $right$ 都指向数组起始位置 $0$。 - 每次将 $right$ 向右移动一位,如果 $nums[right] == 0$,则 $zero\_count$ 加 $1$。 - 当窗口内 $0$ 的数量超过 $1$(即 $zero\_count > 1$)时,不断右移 $left$,并在遇到 $0$ 时将 $zero\_count$ 减 $1$,直到窗口内至多只有 $1$ 个 $0$。 - 每次更新 $ans$,取当前窗口长度 $right - left + 1$ 的最大值。 - 重复上述过程,直到 $right$ 遍历完整个数组。 - 最终返回 $ans$ 即为所求最大连续 $1$ 的个数。 ### 思路 1:代码 ```python class Solution: def findMaxConsecutiveOnes(self, nums: List[int]) -> int: left, right = 0, 0 ans = 0 zero_count = 0 while right < len(nums): if nums[right] == 0: zero_count += 1 while zero_count > 1: if nums[left] == 0: zero_count -= 1 left += 1 ans = max(ans, right - left + 1) right += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0400-0499/max-consecutive-ones.md ================================================ # [0485. 最大连续 1 的个数](https://leetcode.cn/problems/max-consecutive-ones/) - 标签:数组 - 难度:简单 ## 题目链接 - [0485. 最大连续 1 的个数 - 力扣](https://leetcode.cn/problems/max-consecutive-ones/) ## 题目大意 **描述**:给定一个二进制数组 $nums$, 数组中只包含 $0$ 和 $1$。 **要求**:计算其中最大连续 $1$ 的个数。 **说明**: - $1 \le nums.length \le 10^5$。 - $nums[i]$ 不是 $0$ 就是 $1$。 **示例**: - 示例 1: ```python 输入:nums = [1,1,0,1,1,1] 输出:3 解释:开头的两位和最后的三位都是连续 1 ,所以最大连续 1 的个数是 3. ``` - 示例 2: ```python 输入:nums = [1,0,1,1,0,1] 输出:2 ``` ## 解题思路 ### 思路 1:一次遍历 1. 使用两个变量 $cnt$ 和 $ans$。$cnt$ 用于存储当前连续 $1$ 的个数,$ans$ 用于存储最大连续 $1$ 的个数。 2. 然后进行一次遍历,统计当前连续 $1$ 的个数,并更新最大的连续 $1$ 个数。 3. 最后返回 $ans$ 作为答案。 ### 思路 1:代码 ```python class Solution: def findMaxConsecutiveOnes(self, nums: List[int]) -> int: ans = 0 cnt = 0 for num in nums: if num == 1: cnt += 1 ans = max(ans, cnt) else: cnt = 0 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0400-0499/maximum-xor-of-two-numbers-in-an-array.md ================================================ # [0421. 数组中两个数的最大异或值](https://leetcode.cn/problems/maximum-xor-of-two-numbers-in-an-array/) - 标签:位运算、字典树、数组、哈希表 - 难度:中等 ## 题目链接 - [0421. 数组中两个数的最大异或值 - 力扣](https://leetcode.cn/problems/maximum-xor-of-two-numbers-in-an-array/) ## 题目大意 给定一个整数数组 `nums`。 要求:返回 `num[i] XOR nums[j]` 的最大运算结果。其中 `0 ≤ i ≤ j < n`。 ## 解题思路 最直接的想法暴力求解。两层循环计算两两之间的异或结果,记录并更新最大异或结果。 更好的做法可以减少一重循环。首先,要取得异或结果的最大值,那么从二进制的高位到低位,尽可能的让每一位异或结果都为 `1`。 将数组中所有数字的二进制形式从高位到低位依次存入字典树中。然后是利用异或运算交换律:如果 `a ^ b = max` 成立,那么 `a ^ max = b` 与 `b ^ max = a` 均成立。这样当我们知道 `a` 和 `max` 时,可以通过交换律求出 `b`。`a` 是我们遍历的每一个数,`max` 是我们想要尝试的最大值,从 `111111...` 开始,从高位到低位依次填 `1`。 对于 `a` 和 `max`,如果我们所求的 `b` 也在字典树中,则表示 `max` 是可以通过 `a` 和 `b` 得到的,那么 `max` 就是所求最大的异或。如果 `b` 不在字典树中,则减小 `max` 值继续判断,或者继续查询下一个 `a`。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, num: int, max_bit: int) -> None: """ Inserts a word into the trie. """ cur = self for i in range(max_bit, -1, -1): bit = num >> i & 1 if bit not in cur.children: cur.children[bit] = Trie() cur = cur.children[bit] cur.isEnd = True def search(self, num: int, max_bit: int) -> int: """ Returns if the word is in the trie. """ cur = self res = 0 for i in range(max_bit, -1, -1): bit = num >> i & 1 if 1 - bit not in cur.children: res = res * 2 cur = cur.children[bit] else: res = res * 2 + 1 cur = cur.children[1 - bit] return res class Solution: def findMaximumXOR(self, nums: List[int]) -> int: trie_tree = Trie() max_bit = len(format(max(nums), 'b')) - 1 ans = 0 for num in nums: trie_tree.insert(num, max_bit) ans = max(ans, trie_tree.search(num, max_bit)) return ans ``` ================================================ FILE: docs/solutions/0400-0499/minimum-genetic-mutation.md ================================================ # [0433. 最小基因变化](https://leetcode.cn/problems/minimum-genetic-mutation/) - 标签:广度优先搜索、哈希表、字符串 - 难度:中等 ## 题目链接 - [0433. 最小基因变化 - 力扣](https://leetcode.cn/problems/minimum-genetic-mutation/) ## 题目大意 **描述**: 基因序列可以表示为一条由 $8$ 个字符组成的字符串,其中每个字符都是 `'A'`、`'C'`、`'G'` 和 `'T'` 之一。 假设我们需要调查从基因序列 $start$ 变为 $end$ 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。 - 例如,`"AACCGGTT"` --> `"AACCGGTA"` 就是一次基因变化。 另有一个基因库 $bank$ 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。(变化后的基因必须位于基因库 $bank$ 中) 给定两个基因序列 $start$ 和 $end$,以及一个基因库 $bank$。 **要求**: 请你找出并返回能够使 $start$ 变化为 $end$ 所需的最少变化次数。如果无法完成此基因变化,返回 $-1$。 **说明**: - 注意:起始基因序列 $start$ 默认是有效的,但是它并不一定会出现在基因库中。 - $start.length == 8$。 - $end.length == 8$。 - $0 \le bank.length \le 10$。 - $bank[i].length == 8$。 - $start$、$end$ 和 $bank[i]$ 仅由字符 `['A', 'C', 'G', 'T']` 组成。 **示例**: - 示例 1: ```python 输入:start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"] 输出:1 ``` - 示例 2: ```python 输入:start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"] 输出:2 ``` ## 解题思路 ### 思路 1:广度优先搜索 这是一个最短路径问题。我们需要找到从 $start$ 到 $end$ 的最少基因变化次数。 **核心思想**: - 每次基因变化只能修改一个字符,且变化后的基因必须在基因库 $bank$ 中。 - 使用广度优先搜索(BFS)逐层遍历所有可能的基因变化。 - 使用哈希集合记录已访问过的基因,避免重复访问。 - 当找到目标基因 $end$ 时,返回变化次数。 **算法步骤**: 1. 如果 $start$ 等于 $end$,直接返回 $0$。 2. 将 $bank$ 转换为哈希集合 $bank\_set$,方便查找。 3. 如果 $end$ 不在 $bank\_set$ 中,返回 $-1$。 4. 初始化队列 $queue$,将 $(start, 0)$ 加入队列,表示从 $start$ 开始,变化次数为 $0$。 5. 初始化集合 $visited$,记录已访问的基因。 6. 定义字符集 $chars = ['A', 'C', 'G', 'T']$。 7. 从队列中取出当前基因 $current$ 和变化次数 $level$。 8. 遍历当前基因的每个位置 $i$,遍历每个字符 $c$: - 如果 $current[i] \neq c$,生成新基因 $new\_gene$。 - 如果 $new\_gene$ 在 $bank\_set$ 中且未被访问过: - 如果 $new\_gene == end$,返回 $level + 1$。 - 否则将 $new\_gene$ 加入队列和 $visited$ 集合。 9. 如果队列为空仍未找到目标,返回 $-1$。 ### 思路 1:代码 ```python from collections import deque class Solution: def minMutation(self, startGene: str, endGene: str, bank: List[str]) -> int: # 如果起始基因和目标基因相同,直接返回 0 if startGene == endGene: return 0 # 将基因库转换为集合,方便查找 bank_set = set(bank) # 如果目标基因不在基因库中,无法完成变化,返回 -1 if endGene not in bank_set: return -1 # 定义可能的基因字符 chars = ['A', 'C', 'G', 'T'] # 初始化队列,使用 BFS 遍历 queue = deque([(startGene, 0)]) visited = {startGene} # BFS 遍历 while queue: current, level = queue.popleft() # 尝试改变基因的每一个位置 for i in range(len(current)): # 尝试将当前位置的字符替换为其他字符 for c in chars: if current[i] != c: # 生成新的基因序列 new_gene = current[:i] + c + current[i+1:] # 如果新基因在基因库中且未被访问过 if new_gene in bank_set and new_gene not in visited: # 如果找到了目标基因,返回变化次数 if new_gene == endGene: return level + 1 # 将新基因加入队列和已访问集合 queue.append((new_gene, level + 1)) visited.add(new_gene) # 无法完成基因变化,返回 -1 return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(N \times 8 \times 4) = O(N)$,其中 $N$ 为基因库中有效基因序列的数量。对于每个基因序列,我们需要遍历其 $8$ 个位置,每个位置有 $4$ 种可能的字符替换。在最坏情况下,需要遍历所有有效基因序列。 - **空间复杂度**:$O(N)$。主要开销包括:$O(N)$ 的哈希集合 $bank\_set$,$O(N)$ 的已访问集合 $visited$,以及 $O(N)$ 的队列空间。 ================================================ FILE: docs/solutions/0400-0499/minimum-moves-to-equal-array-elements-ii.md ================================================ # [0462. 最小操作次数使数组元素相等 II](https://leetcode.cn/problems/minimum-moves-to-equal-array-elements-ii/) - 标签:数组、数学、排序 - 难度:中等 ## 题目链接 - [0462. 最小操作次数使数组元素相等 II - 力扣](https://leetcode.cn/problems/minimum-moves-to-equal-array-elements-ii/) ## 题目大意 **描述**: 给定一个长度为 $n$ 的整数数组 $nums$。 **要求**: 返回使所有数组元素相等需要的最小操作数。 在一次操作中,你可以使数组中的一个元素加 $1$ 或者减 $1$。 **说明**: - 测试用例经过设计以使答案在 32 位整数范围内。 - $n == nums.length$。 - $1 \le nums.length \le 10^{5}$。 - $-10^{9} \le nums[i] \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3] 输出:2 解释: 只需要两次操作(每次操作指南使一个元素加 1 或减 1): [1,2,3] => [2,2,3] => [2,2,2] ``` - 示例 2: ```python 输入:nums = [1,10,2,9] 输出:16 ``` ## 解题思路 ### 思路 1:排序 + 中位数 这道题要求使所有数组元素相等的最少操作数,每次操作可以将一个元素加 $1$ 或减 $1$。 假设最终所有元素都变成 $x$,那么操作次数为:$\sum_{i=0}^{n-1} |nums[i] - x|$。 根据数学性质,使 $\sum_{i=0}^{n-1} |nums[i] - x|$ 最小的 $x$ 值就是数组的中位数。 **算法思路**: 1. 对数组进行排序。 2. 找到数组的中位数 $median$。如果数组长度为奇数,中位数为中间元素;如果数组长度为偶数,中位数为中间两个元素中的任意一个。 3. 遍历数组,计算所有元素到中位数的绝对距离之和。 ### 思路 1:代码 ```python class Solution: def minMoves2(self, nums: List[int]) -> int: # 对数组进行排序 nums.sort() # 计算中位数 n = len(nums) median = nums[n // 2] # 计算所有元素到中位数的绝对距离之和 moves = 0 for num in nums: moves += abs(num - median) return moves ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$。排序的时间复杂度为 $O(n \log n)$,遍历数组的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0400-0499/minimum-moves-to-equal-array-elements.md ================================================ # [0453. 最小操作次数使数组元素相等](https://leetcode.cn/problems/minimum-moves-to-equal-array-elements/) - 标签:数组、数学 - 难度:中等 ## 题目链接 - [0453. 最小操作次数使数组元素相等 - 力扣](https://leetcode.cn/problems/minimum-moves-to-equal-array-elements/) ## 题目大意 **描述**: 给定一个长度为 $n$ 的整数数组,每次操作将会使 $n - 1$ 个元素增加 $1$。 **要求**: 返回让数组所有元素相等的最小操作次数。 **说明**: - $n == nums.length$。 - $1 \le nums.length \le 10^{5}$。 - $-10^{9} \le nums[i] \le 10^{9}$。 - 答案保证符合 32-bit 整数。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3] 输出:3 解释: 只需要3次操作(注意每次操作会增加两个元素的值): [1,2,3] => [2,3,3] => [3,4,3] => [4,4,4] ``` - 示例 2: ```python 输入:nums = [1,1,1] 输出:0 ``` ## 解题思路 ### 思路 1:数学转化 这道题要求每次操作使 $n - 1$ 个元素增加 $1$,最终使所有元素相等。 我们可以从另一个角度思考:每次操作使 $n - 1$ 个元素增加 $1$,等价于每次操作使 $1$ 个元素减少 $1$。 假设最终所有元素都变成 $x$,且最小值为 $\min(nums)$,那么操作次数为:$\sum_{i=0}^{n-1} (nums[i] - x)$。 为了使操作次数最小,我们应该让所有元素都变成数组的最小值 $\min(nums)$,这样操作次数为: $$moves = \sum_{i=0}^{n-1} (nums[i] - \min(nums)) = \sum_{i=0}^{n-1} nums[i] - n \times \min(nums)$$ **算法思路**: 1. 找到数组的最小值 $min\_val$。 2. 遍历数组,计算所有元素与最小值的差值之和,即为最小操作次数。 ### 思路 1:代码 ```python class Solution: def minMoves(self, nums: List[int]) -> int: # 找到数组中的最小值 min_val = min(nums) # 计算所有元素与最小值的差值之和 moves = 0 for num in nums: moves += num - min_val return moves ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组长度。需要遍历两次数组,一次找最小值,一次计算差值之和。 - **空间复杂度**:$O(1)$。只使用了常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/minimum-number-of-arrows-to-burst-balloons.md ================================================ # [0452. 用最少数量的箭引爆气球](https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/) - 标签:贪心、数组、排序 - 难度:中等 ## 题目链接 - [0452. 用最少数量的箭引爆气球 - 力扣](https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/) ## 题目大意 **描述**:在一个坐标系中有许多球形的气球。对于每个气球,给定气球在 x 轴上的开始坐标和结束坐标 $(x_{start}, x_{end})$。 同时,在 $x$ 轴的任意位置都能垂直发出弓箭,假设弓箭发出的坐标就是 x。那么如果有气球满足 $x_{start} \le x \le x_{end}$,则该气球就会被引爆,且弓箭可以无限前进,可以将满足上述要求的气球全部引爆。 现在给定一个数组 `points`,其中 $points[i] = [x_{start}, x_{end}]$ 代表每个气球的开始坐标和结束坐标。 **要求**:返回能引爆所有气球的最小弓箭数。 **说明**: - $1 \le points.length \le 10^5$。 - $points[i].length == 2$。 - $-2^{31} \le x_{start} < x_{end} \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:points = [[10,16],[2,8],[1,6],[7,12]] 输出:2 解释:气球可以用 2 支箭来爆破: - 在x = 6 处射出箭,击破气球 [2,8] 和 [1,6]。 - 在x = 11 处发射箭,击破气球 [10,16] 和 [7,12]。 ``` - 示例 2: ```python 输入:points = [[1,2],[3,4],[5,6],[7,8]] 输出:4 解释:每个气球需要射出一支箭,总共需要 4 支箭。 ``` ## 解题思路 ### 思路 1:贪心算法 弓箭的起始位置和结束位置可以看做是一段区间,直观上来看,为了使用最少的弓箭数,可以尽量射中区间重叠最多的地方。 所以问题变为了:**如何寻找区间重叠最多的地方,也就是区间交集最多的地方。** 我们将 `points` 按结束坐标升序排序(为什么按照结束坐标排序后边说)。 然后维护两个变量:一个是当前弓箭的坐标 `arrow_pos`、另一个是弓箭的数目 `count`。 为了尽可能的穿过更多的区间,所以每一支弓箭都应该尽可能的从区间的结束位置穿过,这样才能覆盖更多的区间。 初始情况下,第一支弓箭的坐标为第一个区间的结束位置,然后弓箭数为 $1$。然后依次遍历每段区间。 如果遇到弓箭坐标小于区间起始位置的情况,说明该弓箭不能引爆该区间对应的气球,需要用新的弓箭来射,所以弓箭数加 $1$,弓箭坐标也需要更新为新区间的结束位置。 最终返回弓箭数目。 再来看为什么将 `points` 按结束坐标升序排序而不是按照开始坐标升序排序? 其实也可以,但是按开始坐标排序不如按结束坐标排序简单。 按开始坐标升序排序需要考虑一种情况:有交集关系的区间中,有的区间结束位置比较早。比如 `[0, 6]、[1, 2] [4, 5]`,按照开始坐标升序排序的话,就像下图一样: ``` [0..................6] [1..2] [4..5] ``` 第一箭的位置需要进行迭代判断,取区间 `[0, 6]、[1, 2]` 中结束位置最小的位置,即 `arrow_pos = min(points[i][1], arrow_pos)`,然后再判断接下来的区间是否能够引爆。 而按照结束坐标排序的话,箭的位置一开始就确定了,不需要再改变和判断箭的位置,直接判断区间即可。 ### 思路 1:代码 1. 按照结束位置升序排序 ```python class Solution: def findMinArrowShots(self, points: List[List[int]]) -> int: if not points: return 0 points.sort(key=lambda x: x[1]) arrow_pos = points[0][1] count = 1 for i in range(1, len(points)): if arrow_pos < points[i][0]: count += 1 arrow_pos = points[i][1] return count ``` 2. 按照开始位置升序排序 ```python class Solution: def findMinArrowShots(self, points: List[List[int]]) -> int: if not points: return 0 points.sort(key=lambda x: x[0]) arrow_pos = points[0][1] count = 1 for i in range(1, len(points)): if arrow_pos < points[i][0]: count += 1 arrow_pos = points[i][1] else: arrow_pos = min(points[i][1], arrow_pos) return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$, 其中 $n$ 是数组 `points` 的长度。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0400-0499/minimum-unique-word-abbreviation.md ================================================ # [0411. 最短独占单词缩写](https://leetcode.cn/problems/minimum-unique-word-abbreviation/) - 标签:位运算、数组、字符串、回溯 - 难度:困难 ## 题目链接 - [0411. 最短独占单词缩写 - 力扣](https://leetcode.cn/problems/minimum-unique-word-abbreviation/) ## 题目大意 **描述**: 给定目标单词 $target$ 和字典 $dictionary$。 缩写规则: - 你可以选择 $target$ 中任意数量的「不相邻」的子字符串(即这些子字符串在原串中不重叠且不相邻),将它们替换成它们的长度(十进制表示)。 - 其余字符保留原样。 例如 `"substitution"` 可以缩写为: - `"s10n"`(替换 `"ubstitutio"` 长度为 10) - `"sub4u4"`(替换 `"stit"` 和 `"tion"`,长度分别为 4 和 4,它们不相邻) - `"12"`(替换整个字符串) - `"su3i1u2on"`(替换 `"bst"`、`"t"`、`"ti"`,长度分别为 3、1、2,它们不相邻) - `"substitution"`(不替换任何子串) 不允许替换两个相邻的子串,因为那样它们应该合并成一个更大的子串来替换。 - 例如 `"s55n"` 试图替换 `"ubsti"`(长度 5)和 `"tutio"`(长度 5),但这两个子串在原串中是相邻的(`"ubstitutio"` 是连续的),所以不允许,应该直接替换成 `"s10n"`。 **要求**: 在 $target$ 的所有合法缩写中,找出一个「长度最短」的缩写,并且这个缩写「不能」是 $dictionary$ 中任何其他字符串的缩写形式。 如果有多个最短长度的缩写,任意返回一个即可。 **说明**: - $1 \le target.length \le 21$。 - $0 \le dictionary.length \le 1000$。 - $1 \le dictionary[i].length \le 100$。 - $target$ 和 $dictionary[i]$ 只包含小写英文字母。 **示例**: - 示例 1: ```python 输入:target = "apple", dictionary = ["blade"] 输出:"a4" 解释:"a4" 是最短的独占缩写,"blade" 无法匹配。 ``` - 示例 2: ```python 输入:target = "apple", dictionary = ["plain","amber","blade"] 输出:"1p3" 解释:"1p3" 是最短的独占缩写。 ``` ## 解题思路 ### 思路 1:回溯 + 剪枝 给定目标单词 $target$ 和字典 $dictionary$,需要找到最短的独占缩写,使得字典中的单词都不能匹配这个缩写。 **核心思路**: - 枚举所有可能的缩写(保留不同位置的字符)。 - 对于每个缩写,检查是否与字典中的单词冲突。 - 使用位运算表示保留哪些字符:第 $i$ 位为 1 表示保留第 $i$ 个字符。 - 找到长度最短的不冲突缩写。 **解题步骤**: 1. 枚举所有可能的位掩码($0$ 到 $2^n - 1$)。 2. 对于每个位掩码,生成对应的缩写。 3. 检查该缩写是否与字典中的单词冲突。 4. 记录最短的不冲突缩写。 **优化**: - 按照保留字符数从少到多枚举(缩写长度从短到长)。 - 一旦找到不冲突的缩写,立即返回。 ### 思路 1:代码 ```python class Solution: def minAbbreviation(self, target: str, dictionary: List[str]) -> str: n = len(target) # 过滤掉长度不同的单词 dictionary = [word for word in dictionary if len(word) == n] # 如果字典为空,返回最短缩写 if not dictionary: return str(n) # 生成缩写 def get_abbr(word, mask): abbr = [] count = 0 for i in range(len(word)): if mask & (1 << i): if count > 0: abbr.append(str(count)) count = 0 abbr.append(word[i]) else: count += 1 if count > 0: abbr.append(str(count)) return ''.join(abbr) # 检查缩写是否匹配单词 def matches(abbr, word): i = j = 0 while i < len(abbr) and j < len(word): if abbr[i].isdigit(): # 解析数字 if abbr[i] == '0': return False num = 0 while i < len(abbr) and abbr[i].isdigit(): num = num * 10 + int(abbr[i]) i += 1 j += num else: if abbr[i] != word[j]: return False i += 1 j += 1 return i == len(abbr) and j == len(word) # 按照保留字符数从少到多枚举 min_len = float('inf') result = "" for mask in range(1 << n): abbr = get_abbr(target, mask) # 检查是否与字典中的单词冲突 valid = True for word in dictionary: if matches(abbr, word): valid = False break if valid and len(abbr) < min_len: min_len = len(abbr) result = abbr return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n \times m \times n)$,其中 $n$ 是目标单词长度,$m$ 是字典大小。需要枚举 $2^n$ 个缩写,每个缩写需要与 $m$ 个单词匹配。 - **空间复杂度**:$O(n)$,存储缩写字符串。 ================================================ FILE: docs/solutions/0400-0499/n-ary-tree-level-order-traversal.md ================================================ # [0429. N 叉树的层序遍历](https://leetcode.cn/problems/n-ary-tree-level-order-traversal/) - 标签:树、广度优先搜索 - 难度:中等 ## 题目链接 - [0429. N 叉树的层序遍历 - 力扣](https://leetcode.cn/problems/n-ary-tree-level-order-traversal/) ## 题目大意 给定一个 N 叉树的根节点 `root`。 要求:返回其节点值的层序遍历(即从左到右,逐层遍历)。 树的序列化输入是用层序遍历,每组子节点都由 null 值分隔。 ## 解题思路 和二叉树的层序遍历类似。广度优先搜索每次取出第 `i` 层上所有元素。具体步骤如下: - 根节点入队。 - 当队列不为空时,求出当前队列长度 $size$。 - 依次从队列中取出这 $size$ 个元素,并将元素值存入当前层级列表 `level` 中。 - 将该层所有节点的所有孩子节点入队,遍历完之后将这层节点数组加入答案数组中,然后继续迭代。 - 当队列为空时,结束。 ## 代码 ```python class Solution: def levelOrder(self, root: 'Node') -> List[List[int]]: ans = [] if not root: return ans queue = [root] while queue: level = [] size = len(queue) for _ in range(size): cur = queue.pop(0) level.append(cur.val) for child in cur.children: queue.append(child) ans.append(level) return ans ``` ================================================ FILE: docs/solutions/0400-0499/next-greater-element-i.md ================================================ # [0496. 下一个更大元素 I](https://leetcode.cn/problems/next-greater-element-i/) - 标签:栈、数组、哈希表、单调栈 - 难度:简单 ## 题目链接 - [0496. 下一个更大元素 I - 力扣](https://leetcode.cn/problems/next-greater-element-i/) ## 题目大意 **描述**:给定两个没有重复元素的数组 `nums1` 和 `nums2` ,其中 `nums1` 是 `nums2` 的子集。 **要求**:找出 `nums1` 中每个元素在 `nums2` 中的下一个比其大的值。 **说明**: - `nums1` 中数字 `x` 的下一个更大元素是指: `x` 在 `nums2` 中对应位置的右边的第一个比 `x` 大的元素。如果不存在,对应位置输出 `-1`。 - $1 \le nums1.length \le nums2.length \le 1000$。 - $0 \le nums1[i], nums2[i] \le 10^4$。 - $nums1$ 和 $nums2$ 中所有整数互不相同。 - $nums1$ 中的所有整数同样出现在 $nums2$ 中。 **示例**: - 示例 1: ```python 输入:nums1 = [4,1,2], nums2 = [1,3,4,2]. 输出:[-1,3,-1] 解释:nums1 中每个值的下一个更大元素如下所述: - 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。 - 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。 - 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。 ``` - 示例 2: ```python 输入:nums1 = [2,4], nums2 = [1,2,3,4]. 输出:[3,-1] 解释:nums1 中每个值的下一个更大元素如下所述: - 2 ,用加粗斜体标识,nums2 = [1,2,3,4]。下一个更大元素是 3 。 - 4 ,用加粗斜体标识,nums2 = [1,2,3,4]。不存在下一个更大元素,所以答案是 -1 。 ``` ## 解题思路 最直接的思路是根据题意直接暴力求解。遍历 `nums1` 中的每一个元素。对于 `nums1` 的每一个元素 `nums1[i]`,再遍历一遍 `nums2`,查找 `nums2` 中对应位置右边第一个比 `nums1[i]` 大的元素。这种解法的时间复杂度是 $O(n^2)$。 另一种思路是单调栈。 ### 思路 1:单调栈 因为 `nums1` 是 `nums2` 的子集,所以我们可以先遍历一遍 `nums2`,并构造单调递增栈,求出 `nums2` 中每个元素右侧下一个更大的元素。然后将其存储到哈希表中。然后再遍历一遍 `nums1`,从哈希表中取出对应结果,存放到答案数组中。这种解法的时间复杂度是 $O(n)$。具体做法如下: 1. 使用数组 `res` 存放答案。使用 `stack` 表示单调递增栈。使用哈希表 `num_map` 用于存储 `nums2` 中下一个比当前元素大的数值,映射关系为 `当前元素值:下一个比当前元素大的数值`。 2. 遍历数组 `nums2`,对于当前元素: 1. 如果当前元素值较小,则直接让当前元素值入栈。 2. 如果当前元素值较大,则一直出栈,直到当前元素值小于栈顶元素。 1. 出栈时,第一个大于栈顶元素值的元素,就是当前元素。则将其映射到 `num_map` 中。 3. 遍历完数组 `nums2`,建立好所有元素下一个更大元素的映射关系之后,再遍历数组 `nums1`。 4. 从 `num_map` 中取出对应的值,将其加入到答案数组中。 5. 最终输出答案数组 `res`。 ### 思路 1:代码 ```python class Solution: def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]: res = [] stack = [] num_map = dict() for num in nums2: while stack and num > stack[-1]: num_map[stack[-1]] = num stack.pop() stack.append(num) for num in nums1: res.append(num_map.get(num, -1)) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0400-0499/non-decreasing-subsequences.md ================================================ # [0491. 非递减子序列](https://leetcode.cn/problems/non-decreasing-subsequences/) - 标签:位运算、数组、哈希表、回溯 - 难度:中等 ## 题目链接 - [0491. 非递减子序列 - 力扣](https://leetcode.cn/problems/non-decreasing-subsequences/) ## 题目大意 给定一个整数数组 `nums`,找出并返回该数组的所有递增子序列,递增子序列的长度至少为 2。 ## 解题思路 可以利用回溯算法求解。 建立两个数组 res、path。res 用于存放所有递增子序列,path 用于存放当前的递增子序列。 定义回溯方法,从 `start_index = 0` 的位置开始遍历。 - 如果当前子序列的长度大于等于 2,则将当前递增子序列添加到 res 数组中(注意:不用返回,因为还要继续向下查找) - 对数组 `[start_index, len(nums) - 1]` 范围内的元素进行取值,判断当前元素是否在本层出现过。如果出现过则跳出循环。 - 将 `nums[i]` 标记为使用过。 - 将 `nums[i]` 加入到当前 path 中。 - 继续从 `i + 1` 开发遍历下一节点。 - 进行回退操作。 - 最终返回 res 数组。 ## 代码 ```python class Solution: res = [] path = [] def backtrack(self, nums: List[int], start_index): if len(self.path) > 1: self.res.append(self.path[:]) num_set = set() for i in range(start_index, len(nums)): if self.path and nums[i] < self.path[-1] or nums[i] in num_set: continue num_set.add(nums[i]) self.path.append(nums[i]) self.backtrack(nums, i + 1) self.path.pop() def findSubsequences(self, nums: List[int]) -> List[List[int]]: self.res.clear() self.path.clear() self.backtrack(nums, 0) return self.res ``` ================================================ FILE: docs/solutions/0400-0499/non-overlapping-intervals.md ================================================ # [0435. 无重叠区间](https://leetcode.cn/problems/non-overlapping-intervals/) - 标签:贪心、数组、动态规划、排序 - 难度:中等 ## 题目链接 - [0435. 无重叠区间 - 力扣](https://leetcode.cn/problems/non-overlapping-intervals/) ## 题目大意 **描述**:给定一个区间的集合 `intervals`,其中 `intervals[i] = [starti, endi]`。从集合中移除部分区间,使得剩下的区间互不重叠。 **要求**:返回需要移除区间的最小数量。 **说明**: - $1 \le intervals.length \le 10^5$。 - $intervals[i].length == 2$。 - $-5 * 10^4 \le starti < endi \le 5 * 10^4$。 **示例**: - 示例 1: ```python 输入:intervals = [[1,2],[2,3],[3,4],[1,3]] 输出:1 解释:移除 [1,3] 后,剩下的区间没有重叠。 ``` - 示例 2: ```python 输入: intervals = [ [1,2], [1,2], [1,2] ] 输出: 2 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 ``` ## 解题思路 ### 思路 1:贪心算法 这道题我们可以转换一下思路。原题要求保证移除区间最少,使得剩下的区间互不重叠。换个角度就是:「如何使得剩下互不重叠区间的数目最多」。那么答案就变为了:「总区间个数 - 不重叠区间的最多个数」。我们的问题也变成了求所有区间中不重叠区间的最多个数。 从贪心算法的角度来考虑,我们应该将区间按照结束时间排序。每次选择结束时间最早的区间,然后再在剩下的时间内选出最多的区间。 我们用贪心三部曲来解决这道题。 1. **转换问题**:将原问题转变为,当选择结束时间最早的区间之后,再在剩下的时间内选出最多的区间(子问题)。 2. **贪心选择性质**:每次选择时,选择结束时间最早的区间。这样选出来的区间一定是原问题最优解的区间之一。 3. **最优子结构性质**:在上面的贪心策略下,贪心选择当前时间最早的区间 + 剩下的时间内选出最多区间的子问题最优解,就是全局最优解。也就是说在贪心选择的方案下,能够使所有区间中不重叠区间的个数最多。 使用贪心算法的代码解决步骤描述如下: 1. 将区间集合按照结束坐标升序排列,然后维护两个变量,一个是当前不重叠区间的结束时间 `end_pos`,另一个是不重叠区间的个数 `count`。初始情况下,结束坐标 `end_pos` 为第一个区间的结束坐标,`count` 为 `1`。 2. 依次遍历每段区间。对于每段区间:`intervals[i]`: 1. 如果 `end_pos <= intervals[i][0]`,即 `end_pos` 小于等于区间起始位置,则说明出现了不重叠区间,令不重叠区间数 `count` 加 `1`,`end_pos` 更新为新区间的结束位置。 3. 最终返回「总区间个数 - 不重叠区间的最多个数」即 `len(intervals) - count` 作为答案。 ### 思路 1:代码 ```python class Solution: def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: if not intervals: return 0 intervals.sort(key=lambda x: x[1]) end_pos = intervals[0][1] count = 1 for i in range(1, len(intervals)): if end_pos <= intervals[i][0]: count += 1 end_pos = intervals[i][1] return len(intervals) - count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 是区间的数量。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0400-0499/nth-digit.md ================================================ # [0400. 第 N 位数字](https://leetcode.cn/problems/nth-digit/) - 标签:数学、二分查找 - 难度:中等 ## 题目链接 - [0400. 第 N 位数字 - 力扣](https://leetcode.cn/problems/nth-digit/) ## 题目大意 **描述**:给你一个整数 $n$。 **要求**:在无限的整数序列 $[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...]$ 中找出并返回第 $n$ 位上的数字。 **说明**: - $1 \le n \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:n = 3 输出:3 ``` - 示例 2: ```python 输入:n = 11 输出:0 解释:第 11 位数字在序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... 里是 0 ,它是 10 的一部分。 ``` ## 解题思路 ### 思路 1:找规律 数字以 $0123456789101112131415…$ 的格式序列化到一个字符序列中。在这个序列中,第 $5$ 位(从下标 $0$ 开始计数)是 $5$,第 $13$ 位是 $1$,第 $19$ 位是 $4$,等等。 根据题意中的字符串,找数学规律: - $1$ 位数字有 $9$ 个,共 $9$ 位:$123456789$。 - $2$ 位数字有 $90$ 个,共 $2 \times 90$ 位:$10111213...9899$。 - $3$ 位数字有 $900$ 个,共 $3 \times 900$ 位:$100...999$。 - $4$ 位数字有 $9000$ 个,共 $4 \times 9000$ 位: $1000...9999$。 - $……$ 则我们可以按照以下步骤解决这道题: 1. 我们可以先找到第 $n$ 位所在整数 $number$ 所对应的位数 $digit$。 2. 同时找到该位数 $digit$ 的起始整数 $start$。 3. 再计算出 $n$ 所在整数 $number$。$number$ 等于从起始数字 $start$ 开始的第 $\lfloor \frac{n - 1}{digit} \rfloor$ 个数字。即 `number = start + (n - 1) // digit`。 4. 然后确定 $n$ 对应的是数字 $number$ 中的哪一位。即 $digit\_idx = (n - 1) \mod digit$。 5. 最后返回结果。 ### 思路 1:代码 ```python class Solution: def findNthDigit(self, n: int) -> int: digit = 1 start = 1 base = 9 while n > base: n -= base digit += 1 start *= 10 base = start * digit * 9 number = start + (n - 1) // digit digit_idx = (n - 1) % digit return int(str(number)[digit_idx]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:二分查找 假设第 $n$ 位数字所在的整数是 $digit$ 位数,我们可以定义一个方法 $totalDigits(x)$ 用于计算所有位数不超过 $x$ 的整数的所有位数和。 根据题意我们可知,所有位数不超过 $digit - 1$ 的整数的所有位数和一定小于 $n$,并且所有不超过 $digit$ 的整数的所有位数和一定大于等于 $n$。 因为所有位数不超过 $x$ 的整数的所有位数和 $totalDigits(x)$ 是关于 $x$ 单调递增的,所以我们可以使用二分查找的方式,确定第 $n$ 位数字所在的整数的位数 $digit$。 $n$ 的最大值为 $2^{31} - 1$,约为 $2 \times 10^9$。而 $9$ 位数字有 $9 \times 10^8$ 个,共 $9 \times 9 \times 10^8 = 8.1 \times 10^9 > 2 \times 10 ^ 9$,所以第 $n$ 位所在整数的位数 $digit$ 最多为 $9$ 位,最小为 $1$ 位。即 $digit$ 的取值范围为 $[1, 9]$。 我们使用二分查找算法得到 $digit$ 之后,还可以计算出不超过 $digit - 1$ 的整数的所有位数和 $pre\_digits = totalDigits(digit - 1)$,则第 $n$ 位数字所在整数在所有 $digit$ 位数中的下标是 $idx = n - pre\_digits - 1$。 得到下标 $idx$ 后,可以计算出 $n$ 所在整数 $number$。$number$ 等于从起始数字 $10^{digit - 1}$ 开始的第 $\lfloor \frac{idx}{digit} \rfloor$ 个数字。即 `number = 10 ** (digit - 1) + idx // digit`。 该整数 $number$ 中第 $idx \mod digit$ 即为第 $n$ 位上的数字,将其作为答案返回即可。 ### 思路 2:代码 ```python class Solution: def totalDigits(self, x): digits = 0 digit, cnt = 1, 9 while digit <= x: digits += digit * cnt digit += 1 cnt *= 10 return digits def findNthDigit(self, n: int) -> int: left, right = 1, 9 while left < right: mid = left + (right - left) // 2 if self.totalDigits(mid) < n: left = mid + 1 else: right = mid digit = left pre_digits = self.totalDigits(digit - 1) idx = n - pre_digits - 1 number = 10 ** (digit - 1) + idx // digit digit_idx = idx % digit return int(str(number)[digit_idx]) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$\log n \times \log \log n$,位数上限 $D$ 为 $\log n$,二分查找的时间复杂度为 $\log D$,每次执行的时间复杂度为 $D$,总的时间复杂度为 $D \times \log D = O(\log n \times \log \log n)$。 - **空间复杂度**:$O(1)$。 ## 参考资料 - 【题解】[400. 第 N 位数字 - 清晰易懂的找规律解法(击败100%, 几乎双百)](https://leetcode.cn/problems/nth-digit/solutions/1129463/geekplayers-leetcode-ac-qing-xi-yi-dong-uasjy/) - 【题解】[400. 第 N 位数字 - 方法一:二分查找](https://leetcode.cn/problems/nth-digit/solutions/1128000/di-n-wei-shu-zi-by-leetcode-solution-mdl2/) ================================================ FILE: docs/solutions/0400-0499/number-complement.md ================================================ # [0476. 数字的补数](https://leetcode.cn/problems/number-complement/) - 标签:位运算 - 难度:简单 ## 题目链接 - [0476. 数字的补数 - 力扣](https://leetcode.cn/problems/number-complement/) ## 题目大意 **描述**: 对整数的二进制表示取反($0$ 变 $1$,$1$ 变 $0$)后,再转换为十进制表示,可以得到这个整数的补数。 - 例如,整数 $5$ 的二进制表示是 `"101"`,取反后得到 `"010"`,再转回十进制表示得到补数 $2$。 给定一个整数 $num$。 **要求**: 输出它的补数。 **说明**: - $1 \le num \lt 2^{31}$。 - 注意:本题与 [1009. 十进制整数的反码](https://leetcode-cn.com/problems/complement-of-base-10-integer/) 相同。 **示例**: - 示例 1: ```python 输入:num = 5 输出:2 解释:5 的二进制表示为 101(没有前导零位),其补数为 010。所以你需要输出 2 。 ``` - 示例 2: ```python 输入:num = 1 输出:0 解释:1 的二进制表示为 1(没有前导零位),其补数为 0。所以你需要输出 0 。 ``` ## 解题思路 ### 思路 1: 1. 将十进制数 $num$ 转为二进制 $binary$。 2. 遍历二进制 $binary$ 的每一个数位 $digit$。 1. 如果 $digit$ 为 $0$,则将其转为 $1$,存入答案 $res$ 中。 2. 如果 $digit$ 为 $1$,则将其转为 $0$,存入答案 $res$ 中。 3. 返回答案 $res$。 ### 思路 1:代码 ```python class Solution: def findComplement(self, num: int) -> int: # 将 num 转为二进制字符串 binary = "" while num: binary += str(num % 2) # 不断取余,转换为二进制 num //= 2 # 如果 binary 为空,说明 num 为 0,补数为 1 if binary == "": binary = "0" else: binary = binary[::-1] # 翻转二进制字符串,得到正确的二进制表示 # 将二进制字符串进行补数转换(0 变 1,1 变 0) res = 0 for digit in binary: if digit == '0': res = res * 2 + 1 # 0 变为 1 else: res = res * 2 # 1 变为 0 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log num)$,其中 $num$ 为给定的正整数。我们需要将 $num$ 转换为二进制表示,需要 $O(\log num)$ 时间,然后对二进制的每一位进行处理。 - **空间复杂度**:$O(\log num)$,其中 $num$ 为给定的正整数。我们需要 $O(\log num)$ 的空间来存储二进制字符串。 ================================================ FILE: docs/solutions/0400-0499/number-of-boomerangs.md ================================================ # [0447. 回旋镖的数量](https://leetcode.cn/problems/number-of-boomerangs/) - 标签:数组、哈希表、数学 - 难度:中等 ## 题目链接 - [0447. 回旋镖的数量 - 力扣](https://leetcode.cn/problems/number-of-boomerangs/) ## 题目大意 给定平面上点坐标的数组 points,其中 $points[i] = [x_i, y_i]$。判断 points 中是否存在三个点 i,j,k,满足 i 和 j 之间的距离等于 i 和 k 之间的距离,即 $dist[i, j] = dist[i, k]$。找出满足上述关系的答案数量。 ## 解题思路 使用哈希表记录每两个点之间的距离。然后使用两重循环遍历坐标数组,对于每两个点 i、点 j,计算两个点之间的距离,并将距离存进哈希表中。再从哈希表中选取距离相同的关系中依次选出两个,作为三个点之间的距离关系 $dist[i, j] =dist[i, k]$,因为还需考虑顺序,所以共有 $value * (value-1)$ 种情况。累加到答案中。 ## 代码 ```python class Solution: def numberOfBoomerangs(self, points: List[List[int]]) -> int: ans = 0 for point_i in points: dis_dict = dict() for point_j in points: if point_i != point_j: dx = point_i[0] - point_j[0] dy = point_i[1] - point_j[1] dis = dx * dx + dy * dy if dis in dis_dict: dis_dict[dis] += 1 else: dis_dict[dis] = 1 for value in dis_dict.values(): ans += value*(value-1) return ans ``` ================================================ FILE: docs/solutions/0400-0499/number-of-segments-in-a-string.md ================================================ # [0434. 字符串中的单词数](https://leetcode.cn/problems/number-of-segments-in-a-string/) - 标签:字符串 - 难度:简单 ## 题目链接 - [0434. 字符串中的单词数 - 力扣](https://leetcode.cn/problems/number-of-segments-in-a-string/) ## 题目大意 **描述**: 给定字符串 $s$。 **要求**: 统计字符串中的单词个数,这里的单词指的是连续的不是空格的字符。 **说明**: - 可以假定字符串里不包括任何不可打印的字符。 **示例**: - 示例 1: ```python 输入: "Hello, my name is John" 输出: 5 解释: 这里的单词是指连续的不是空格的字符,所以 "Hello," 算作 1 个单词。 ``` ## 解题思路 ### 思路 1:遍历计数 思路非常简单,直接遍历统计单词数即可。 对于字符串 $s$ 来说,我们使用两个变量: - $ans$:用来统计单词个数 - $prev$:用来记录上一个字符是否为空格 然后遍历字符串: - 如果当前字符不是空格,且上一个字符是空格(或者是第一个字符),则说明遇到了一个新的单词,单词数 $ans$ 加 $1$ - 最后返回 $ans$ 即可。 ### 思路 1:代码 ```python class Solution: def countSegments(self, s: str) -> int: # ans 统计单词数,prev 记录上一个字符是否为空格 ans = 0 prev = True # 遍历字符串 for ch in s: # 如果当前字符不是空格,且上一个字符是空格,则遇到新单词 if ch != ' ' and prev: ans += 1 # 更新 prev:当前字符是否为空格 prev = (ch == ' ') return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是字符串 $s$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0400-0499/ones-and-zeroes.md ================================================ # [0474. 一和零](https://leetcode.cn/problems/ones-and-zeroes/) - 标签:数组、字符串、动态规划 - 难度:中等 ## 题目链接 - [0474. 一和零 - 力扣](https://leetcode.cn/problems/ones-and-zeroes/) ## 题目大意 **描述**:给定一个二进制字符串数组 $strs$,以及两个整数 $m$ 和 $n$。 **要求**:找出并返回 $strs$ 的最大子集的大小,该子集中最多有 $m$ 个 $0$ 和 $n$ 个 $1$。 **说明**: - 如果 $x$ 的所有元素也是 $y$ 的元素,集合 $x$ 是集合 $y$ 的子集。 - $1 \le strs.length \le 600$。 - $1 \le strs[i].length \le 100$。 - $strs[i]$ 仅由 `'0'` 和 `'1'` 组成。 - $1 \le m, n \le 100$。 **示例**: - 示例 1: ```python 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 输出:4 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3。 ``` - 示例 2: ```python 输入:strs = ["10", "0", "1"], m = 1, n = 1 输出:2 解释:最大的子集是 {"0", "1"} ,所以答案是 2。 ``` ## 解题思路 ### 思路 1:动态规划 这道题可以转换为「二维 0-1 背包问题」来做。 把 $0$ 的个数和 $1$ 的个数视作一个二维背包的容量。每一个字符串都当做是一件物品,其成本为字符串中 $1$ 的数量和 $0$ 的数量,每个字符串的价值为 $1$。 ###### 1. 阶段划分 按照物品的序号、当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:最多有 $i$ 个 $0$ 和 $j$ 个 $1$ 的字符串 $strs$ 的最大子集的大小。 ###### 3. 状态转移方程 填满最多由 $i$ 个 $0$ 和 $j$ 个 $1$ 构成的二维背包的最多物品数为下面两种情况中的最大值: - 使用之前字符串填满容量为 $i - zero\_num$、$j - one\_num$ 的背包的物品数 + 当前字符串价值 - 选择之前字符串填满容量为 $i$、$j$ 的物品数。 则状态转移方程为:$dp[i][j] = max(dp[i][j], dp[i - zero\_num][j - one\_num] + 1)$。 ###### 4. 初始条件 - 无论有多少个 $0$,多少个 $1$,只要不选 $0$,也不选 $1$,则最大子集的大小为 $0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:最多有 $i$ 个 $0$ 和 $j$ 个 $1$ 的字符串 $strs$ 的最大子集的大小。所以最终结果为 $dp[m][n]$。 ### 思路 1:代码 ```python class Solution: def findMaxForm(self, strs: List[str], m: int, n: int) -> int: dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)] for str in strs: one_num = 0 zero_num = 0 for ch in str: if ch == '0': zero_num += 1 else: one_num += 1 for i in range(m, zero_num - 1, -1): for j in range(n, one_num - 1, -1): dp[i][j] = max(dp[i][j], dp[i - zero_num][j - one_num] + 1) return dp[m][n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(l \times m \times n)$,其中 $l$ 为字符串 $strs$ 的长度。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/0400-0499/optimal-account-balancing.md ================================================ # [0465. 最优账单平衡](https://leetcode.cn/problems/optimal-account-balancing/) - 标签:位运算、数组、动态规划、回溯、状态压缩 - 难度:困难 ## 题目链接 - [0465. 最优账单平衡 - 力扣](https://leetcode.cn/problems/optimal-account-balancing/) ## 题目大意 **描述**: 给定一组交易记录 $transactions$,其中 $transactions[i] = [from_i, to_i, amount_i]$ 表示 ID 为 $from_i$ 的人给 ID 为 $to_i$ 的人转账 $amount_i$ 元。 **要求**: 返回清偿所有债务所需的最少交易次数。 **说明**: - $1 \le transactions.length \le 8$。 - $transactions[i].length = 3$。 - $0 \le from_i, to_i < 12$。 - $from_i \ne to_i$。 - $1 \le amount_i \le 100$。 **示例**: - 示例 1: ```python 输入:transactions = [[0,1,10],[2,0,5]] 输出:2 解释: 人 0 给人 1 转账 10 元。 人 2 给人 0 转账 5 元。 需要两次交易: 1. 人 0 给人 2 转账 5 元。 2. 人 0 给人 1 转账 5 元。 ``` - 示例 2: ```python 输入:transactions = [[0,1,10],[1,0,1],[1,2,5],[2,0,5]] 输出:1 解释: 人 0 净收支:-10 + 1 + 5 = -4 人 1 净收支:10 - 1 - 5 = 4 人 2 净收支:5 - 5 = 0 只需一次交易:人 0 给人 1 转账 4 元。 ``` ## 解题思路 ### 思路 1:回溯 + 剪枝 给定一组交易记录,需要找到最少的交易次数使所有账户平衡。 **核心思路**: - 首先计算每个人的净收支(收入 - 支出)。 - 净收支为 0 的人不需要参与后续交易。 - 问题转化为:将所有正数(债权人)和负数(债务人)通过最少的交易次数归零。 - 使用回溯尝试所有可能的交易组合。 **解题步骤**: 1. 计算每个人的净收支,过滤掉为 0 的账户。 2. 使用回溯: - 找到第一个非零账户。 - 尝试与其他相反符号的账户进行交易。 - 递归处理剩余账户,记录最少交易次数。 3. 剪枝:如果两个账户的净收支相加为 0,可以直接抵消。 ### 思路 1:代码 ```python from collections import defaultdict class Solution: def minTransfers(self, transactions: List[List[int]]) -> int: # 计算每个人的净收支 balance = defaultdict(int) for u, v, amount in transactions: balance[u] -= amount balance[v] += amount # 过滤掉净收支为 0 的账户 debts = [amount for amount in balance.values() if amount != 0] n = len(debts) def dfs(start): # 跳过已经平衡的账户 while start < n and debts[start] == 0: start += 1 # 所有账户都已平衡 if start == n: return 0 min_transactions = float('inf') # 尝试与其他账户交易 for i in range(start + 1, n): # 只与相反符号的账户交易 if debts[start] * debts[i] < 0: # 进行交易 debts[i] += debts[start] # 递归处理剩余账户 min_transactions = min(min_transactions, 1 + dfs(start + 1)) # 回溯 debts[i] -= debts[start] return min_transactions return dfs(0) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n!)$,其中 $n$ 是非零账户的数量。最坏情况下需要尝试所有交易组合。 - **空间复杂度**:$O(n)$,递归栈的深度。 ================================================ FILE: docs/solutions/0400-0499/pacific-atlantic-water-flow.md ================================================ # [0417. 太平洋大西洋水流问题](https://leetcode.cn/problems/pacific-atlantic-water-flow/) - 标签:深度优先搜索、广度优先搜索、数组、矩阵 - 难度:中等 ## 题目链接 - [0417. 太平洋大西洋水流问题 - 力扣](https://leetcode.cn/problems/pacific-atlantic-water-flow/) ## 题目大意 **描述**:给定一个 `m * n` 大小的二维非负整数矩阵 `heights` 来表示一片大陆上各个单元格的高度。`heights[i][j]` 表示第 `i` 行第 `j` 列所代表的陆地高度。这个二维矩阵所代表的陆地被太平洋和大西洋所包围着。左上角是「太平洋」,右下角是「大西洋」。规定水流只能按照上、下、左、右四个方向流动,且只能从高处流到低处,或者在同等高度上流动。 **要求**:找出代表陆地的二维矩阵中,水流既可以从该处流动到太平洋,又可以流动到大西洋的所有坐标。以二维数组 `res` 的形式返回,其中 `res[i] = [ri, ci]` 表示雨水从单元格 `(ri, ci)` 既可流向太平洋也可流向大西洋。 **说明**: - $m == heights.length$。 - $n == heights[r].length$。 - $1 \le m, n \le 200$。 - $0 \le heights[r][c] \le 10^5$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/06/08/waterflow-grid.jpg) ```python 输入: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]] 输出: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]] ``` - 示例 2: ```python 输入: heights = [[2,1],[1,2]] 输出: [[0,0],[0,1],[1,0],[1,1]] ``` ## 解题思路 ### 思路 1:深度优先搜索 雨水由高处流向低处,如果我们根据雨水的流向搜索,来判断是否能从某一位置流向太平洋和大西洋不太容易。我们可以换个思路。 1. 分别从太平洋和大西洋(就是矩形边缘)出发,逆流而上,找出水流逆流能达到的地方,可以用两个二维数组 `pacific`、`atlantic` 分别记录太平洋和大西洋能到达的位置。 2. 然后再对二维数组进行一次遍历,找出两者交集的位置,就是雨水既可流向太平洋也可流向大西洋的位置,将其加入答案数组 `res` 中。 3. 最后返回答案数组 `res`。 ### 思路 1:代码 ```python class Solution: def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]: rows, cols = len(heights), len(heights[0]) pacific = [[False for _ in range(cols)] for _ in range(rows)] atlantic = [[False for _ in range(cols)] for _ in range(rows)] directs = [(0, 1), (0, -1), (1, 0), (-1, 0)] def dfs(i, j, visited): visited[i][j] = True for direct in directs: new_i = i + direct[0] new_j = j + direct[1] if new_i < 0 or new_i >= rows or new_j < 0 or new_j >= cols: continue if heights[new_i][new_j] >= heights[i][j] and not visited[new_i][new_j]: dfs(new_i, new_j, visited) for j in range(cols): dfs(0, j, pacific) dfs(rows - 1, j, atlantic) for i in range(rows): dfs(i, 0, pacific) dfs(i, cols - 1, atlantic) res = [] for i in range(rows): for j in range(cols): if pacific[i][j] and atlantic[i][j]: res.append([i, j]) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。其中 $m$ 和 $n$ 分别为行数和列数。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/0400-0499/partition-equal-subset-sum.md ================================================ # [0416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [0416. 分割等和子集 - 力扣](https://leetcode.cn/problems/partition-equal-subset-sum/) ## 题目大意 **描述**:给定一个只包含正整数的非空数组 $nums$。 **要求**:判断是否可以将这个数组分成两个子集,使得两个子集的元素和相等。 **说明**: - $1 \le nums.length \le 200$。 - $1 \le nums[i] \le 100$。 **示例**: - 示例 1: ```python 输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11]。 ``` - 示例 2: ```python 输入:nums = [1,2,3,5] 输出:false 解释:数组不能分割成两个元素和相等的子集。 ``` ## 解题思路 ### 思路 1:动态规划 这道题换一种说法就是:从数组中选择一些元素组成一个子集,使子集的元素和恰好等于整个数组元素和的一半。 这样的话,这道题就可以转变为「0-1 背包问题」。 1. 把整个数组中的元素和记为 $sum$,把元素和的一半 $target = \frac{sum}{2}$ 看做是「0-1 背包问题」中的背包容量。 2. 把数组中的元素 $nums[i]$ 看做是「0-1 背包问题」中的物品。 3. 第 $i$ 件物品的重量为 $nums[i]$,价值也为 $nums[i]$。 4. 因为物品的重量和价值相等,如果能装满载重上限为 $target$ 的背包,那么得到的最大价值也应该是 $target$。 这样问题就转变为:给定一个数组 $nums$ 代表物品,数组元素和的一半 $target = \frac{sum}{2}$ 代表背包的载重上限。其中第 $i$ 件物品的重量为 $nums[i]$,价值为 $nums[i]$,每件物品有且只有 $1$ 件。请问在总重量不超过背包装载重量上限的情况下,能否将背包装满从而得到最大价值? ###### 1. 阶段划分 当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[w]$ 表示为:从数组 $nums$ 中选择一些元素,放入最多能装元素和为 $w$ 的背包中,得到的元素和最大为多少。 ###### 3. 状态转移方程 $dp[w] = \begin{cases} dp[w] & w < nums[i - 1] \cr max \lbrace dp[w], \quad dp[w - nums[i - 1]] + nums[i - 1] \rbrace & w \ge nums[i - 1] \end{cases}$ ###### 4. 初始条件 - 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是 $0$,即 $dp[w] = 0, 0 \le w \le W$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[target]$ 表示为:从数组 $nums$ 中选择一些元素,放入最多能装元素和为 $target = \frac{sum}{2}$ 的背包中,得到的元素和最大值。 所以最后判断一下 $dp[target]$ 是否等于 $target$。如果 $dp[target] == target$,则说明集合中的子集刚好能够凑成总和 $target$,此时返回 `True`;否则返回 `False`。 ### 思路 1:代码 ```python class Solution: # 思路 2:动态规划 + 滚动数组优化 def zeroOnePackMethod2(self, weight: [int], value: [int], W: int): size = len(weight) dp = [0 for _ in range(W + 1)] # 枚举前 i 种物品 for i in range(1, size + 1): # 逆序枚举背包装载重量(避免状态值错误) for w in range(W, weight[i - 1] - 1, -1): # dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值 dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1]) return dp[W] def canPartition(self, nums: List[int]) -> bool: sum_nums = sum(nums) if sum_nums & 1: return False target = sum_nums // 2 return self.zeroOnePackMethod2(nums, nums, target) == target ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times target)$,其中 $n$ 为数组 $nums$ 的元素个数,$target$ 是整个数组元素和的一半。 - **空间复杂度**:$O(target)$。 ================================================ FILE: docs/solutions/0400-0499/path-sum-iii.md ================================================ # [0437. 路径总和 III](https://leetcode.cn/problems/path-sum-iii/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0437. 路径总和 III - 力扣](https://leetcode.cn/problems/path-sum-iii/) ## 题目大意 给定一个二叉树的根节点 `root`,和一个整数 `sum`。 要求:求出该二叉树里节点值之和等于 `sum` 的路径的数目。 - 路径:不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。 ## 解题思路 直观想法是: 以每一个节点 `node` 为起始节点,向下检测延伸的路径。递归遍历每一个节点所有可能的路径,然后将这些路径数目加起来即为答案。 但是这样会存在许多重复计算。我们可以定义节点的前缀和来减少重复计算。 - 节点的前缀和:从根节点到当前节点路径上所有节点的和。 有了节点的前缀和,我们就可以通过前缀和来计算两节点之间的路劲和。即:`则两节点之间的路径和 = 两节点之间的前缀和之差`。 为了计算符合要求的路径数量,我们用哈希表存储「前缀和的节点数量」。哈希表以「当前节点的前缀和」为键,以「该前缀和的节点数量」为值。这样就能通过哈希表直接计算出符合要求的路径数量,从而累加到答案上。 整个算法的具体步骤如下: - 通过先序遍历方式递归遍历二叉树,计算每一个节点的前缀和 `cur_sum`。 - 从哈希表中取出 `cur_sum - sum` 的路径数量(也就是表示存在从前缀和为 `cur_sum - sum` 所对应的节点到前缀和为 `cur_sum` 所对应的节点的路径个数)累加到答案 `res` 中。 - 然后以「当前节点的前缀和」为键,以「该前缀和的节点数量」为值,存入哈希表中。 - 递归遍历二叉树,并累加答案值。 - 恢复哈希表「当前前缀和的节点数量」,返回答案。 ## 代码 ```python class Solution: prefixsum_count = dict() def dfs(self, root, prefixsum_count, target_sum, cur_sum): if not root: return 0 res = 0 cur_sum += root.val res += prefixsum_count.get(cur_sum - target_sum, 0) prefixsum_count[cur_sum] = prefixsum_count.get(cur_sum, 0) + 1 res += self.dfs(root.left, prefixsum_count, target_sum, cur_sum) res += self.dfs(root.right, prefixsum_count, target_sum, cur_sum) prefixsum_count[cur_sum] -= 1 return res def pathSum(self, root: TreeNode, sum: int) -> int: if not root: return 0 prefixsum_count = dict() prefixsum_count[0] = 1 return self.dfs(root, prefixsum_count, sum, 0) ``` ================================================ FILE: docs/solutions/0400-0499/poor-pigs.md ================================================ # [0458. 可怜的小猪](https://leetcode.cn/problems/poor-pigs/) - 标签:数学、动态规划、组合数学 - 难度:困难 ## 题目链接 - [0458. 可怜的小猪 - 力扣](https://leetcode.cn/problems/poor-pigs/) ## 题目大意 **描述**: 有 $buckets$ 桶液体,其中「正好有一桶」含有毒药,其余装的都是水。它们从外观看起来都一样。为了弄清楚哪只水桶含有毒药,你可以喂一些猪喝,通过观察猪是否会死进行判断。不幸的是,你只有 $minutesToTest$ 分钟时间来确定哪桶液体是有毒的。 喂猪的规则如下: 1. 选择若干活猪进行喂养。 2. 可以允许小猪同时饮用任意数量的桶中的水,并且该过程不需要时间。 3. 小猪喝完水后,必须有 $minutesToDie$ 分钟的冷却时间。在这段时间里,你只能观察,而不允许继续喂猪。 4. 过了 $minutesToDie$ 分钟后,所有喝到毒药的猪都会死去,其他所有猪都会活下来。 5. 重复这一过程,直到时间用完。 给定桶的数目 $buckets$,$minutesToDie$ 和 $minutesToTest$。 **要求**: 返回在规定时间内判断哪个桶有毒所需的「最小」猪数。 **说明**: - $1 \le buckets \le 10^{3}$。 - $1 \le minutesToDie \le minutesToTest \le 10^{3}$。 **示例**: - 示例 1: ```python 输入:buckets = 1000, minutesToDie = 15, minutesToTest = 60 输出:5 ``` - 示例 2: ```python 输入:buckets = 4, minutesToDie = 15, minutesToTest = 15 输出:2 ``` ## 解题思路 ### 思路 1:数学 这是一道经典的数学和信息论问题。 假设有 $x$ 只小猪,每只小猪可以进行 $rounds = \lfloor \frac{minutesToTest}{minutesToDie} \rfloor + 1$ 轮测试(第一轮 + 冷却后继续测试)。 每只小猪有 $rounds + 1$ 种状态: - 第 0 轮死亡(喝了第 0 次被毒死)。 - 第 1 轮死亡(喝了第 1 次被毒死)。 - ... - 第 $rounds - 1$ 轮死亡。 - 存活到最后(没有中毒)。 $x$ 只小猪的状态组合总数为 $rounds^x$,每一种状态组合可以对应一个桶。 因此问题转化为:需要找到最小的 $x$,使得 $rounds^x \ge buckets$。 对不等式两边取对数:$x \times \log(rounds) \ge \log(buckets)$。 解得:$x \ge \frac{\log(buckets)}{\log(rounds)}$。 因此最小的 $x$ 为 $\lceil \frac{\log(buckets)}{\log(rounds)} \rceil$。 具体步骤: - 计算 $rounds = \lfloor \frac{minutesToTest}{minutesToDie} \rfloor + 1$(即每只小猪可能的死亡状态数)。 - 如果 $rounds = 1$,说明无法进行有效测试,需要小猪数等于桶数。 - 否则计算 $\frac{\log(buckets)}{\log(rounds)}$。 - 由于浮点数精度问题,如果结果接近整数,直接返回该整数,否则向上取整。 ### 思路 1:代码 ```python import math class Solution: def poorPigs(self, buckets: int, minutesToDie: int, minutesToTest: int) -> int: # rounds 表示每只小猪可能的死亡状态数 rounds = minutesToTest // minutesToDie + 1 # 特殊情况:如果只有 1 个状态(即无法测试),需要小猪数等于桶数 if rounds == 1: return buckets # 使用换底公式:log_rounds(buckets) = log(buckets) / log(rounds) # 添加小的 epsilon 避免浮点数精度问题 result = math.log(buckets) / math.log(rounds) # 如果 result 是整数(考虑浮点误差),直接返回,否则向上取整 if abs(result - round(result)) < 1e-10: return round(result) else: return math.ceil(result) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。只进行常数次计算。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0400-0499/predict-the-winner.md ================================================ # [0486. 预测赢家](https://leetcode.cn/problems/predict-the-winner/) - 标签:递归、数组、数学、动态规划、博弈 - 难度:中等 ## 题目链接 - [0486. 预测赢家 - 力扣](https://leetcode.cn/problems/predict-the-winner/) ## 题目大意 **描述**:给定搞一个整数数组 $nums$。玩家 $1$ 和玩家 $2$ 基于这个数组设计了一个游戏。 玩家 $1$ 和玩家 $2$ 轮流进行自己的回合,玩家 $1$ 先手。 开始时,两个玩家的初始分值都是 $0$。每一回合,玩家从数组的任意一端取一个数字(即 $nums[0]$ 或 $nums[nums.length - 1]$),取到的数字将会从数组中移除(数组长度减 $1$)。玩家选中的数字将会加到他的得分上。当数组中没有剩余数字可取时,游戏结束。 **要求**:如果玩家 $1$ 能成为赢家,则返回 `True`。否则返回 `False`。如果两个玩家得分相等,同样认为玩家 $1$ 是游戏的赢家,也返回 `True`。假设每个玩家的玩法都会使他的分数最大化。 **说明**: - $1 \le nums.length \le 20$。 - $0 \le nums[i] \le 10^7$。 **示例**: - 示例 1: ```python 输入:nums = [1,5,2] 输出:False 解释:一开始,玩家 1 可以从 1 和 2 中进行选择。 如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。 所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。 因此,玩家 1 永远不会成为赢家,返回 False。 ``` - 示例 2: ```python 输入:nums = [1,5,233,7] 输出:True 解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。 最终,玩家 1(234 分)比玩家 2(12 分)获得更多的分数,所以返回 True,表示玩家 1 可以成为赢家。 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:玩家 $1$ 与玩家 $2$ 在 $nums[i]...nums[j]$ 之间互相选取,玩家 $1$ 比玩家 $2$ 多的最大分数。 ###### 3. 状态转移方程 根据状态的定义,只有在 $i \le j$ 时才有意义,所以当 $i > j$ 时,$dp[i][j] = 0$。 1. 当 $i == j$ 时,当前玩家只能拿取 $nums[i]$,因此对于所有 $0 \le i < nums.length$,都有:$dp[i][i] = nums[i]$。 2. 当 $i < j$ 时,当前玩家可以选择 $nums[i]$ 或 $nums[j]$,并是自己的分数最大化,然后换另一位玩家从剩下部分选取数字。则转移方程为:$dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1])$。 ###### 4. 初始条件 - 当 $i > j$ 时,$dp[i][j] = 0$。 - 当 $i == j$ 时,$dp[i][j] = nums[i]$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:玩家 $1$ 与玩家 $2$ 在 $nums[i]...nums[j]$ 之间互相选取,玩家 $1$ 比玩家 $2$ 多的最大分数。则如果玩家 $1$ 想要赢,则 $dp[0][size - 1]$ 必须大于等于 $0$。所以最终结果为 $dp[0][size - 1] >= 0$。 ### 思路 1:代码 ```python class Solution: def PredictTheWinner(self, nums: List[int]) -> bool: size = len(nums) dp = [[0 for _ in range(size)] for _ in range(size)] for l in range(1, size + 1): for i in range(size): j = i + l - 1 if j >= size: break if l == 1: dp[i][j] = nums[i] else: dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]) return dp[0][size - 1] >= 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/0400-0499/queue-reconstruction-by-height.md ================================================ # [0406. 根据身高重建队列](https://leetcode.cn/problems/queue-reconstruction-by-height/) - 标签:贪心、树状数组、线段树、数组、排序 - 难度:中等 ## 题目链接 - [0406. 根据身高重建队列 - 力扣](https://leetcode.cn/problems/queue-reconstruction-by-height/) ## 题目大意 n 个人打乱顺序排成一排,给定一个数组 people 表示队列中人的属性(顺序是打乱的)。其中 $people[i] = [h_i, k_i]$ 表示第 i 个人的身高为 $h_i$,前面正好有 $k_i$ 个身高大于或等于 $h_i$ 的人。 现在重新构造并返回输入数组 people 所表示的队列 queue。其中 $queue[j] = [h_j, k_j]$ 是队列中第 j 个人的信息,表示为身高为 $h_j$,前面正好有 $k_j$ 个身高大于或等于 $h_j$​ 的人。 ## 解题思路 这道题目有两个维度,身高 $h_j$ 和满足条件的数量 $k_j$。进行排序的时候如果同时考虑两个维度条件,就有点复杂了。我们可以考虑固定一个维度,先排好序,再考虑另一个维度的要求。 我们可以先确定身高维度。将数组按身高从高到低进行排序,身高相同的则按照 k 值升序排列。这样排序之后可以确定目前对于第 j 个人来说,前面的 j - 1 个人肯定比他都高。 然后建立一个包含 n 个位置的空队列 queue,按照上边排好的顺序遍历,依次将其插入到第 $k_j$​ 位置上。最后返回新的队列。 ## 代码 ```python class Solution: def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]: queue = [] people.sort(key = lambda x: (-x[0], x[1])) for p in people: queue.insert(p[1], p) return queue ``` ================================================ FILE: docs/solutions/0400-0499/random-point-in-non-overlapping-rectangles.md ================================================ # [0497. 非重叠矩形中的随机点](https://leetcode.cn/problems/random-point-in-non-overlapping-rectangles/) - 标签:水塘抽样、数组、数学、二分查找、有序集合、前缀和、随机化 - 难度:中等 ## 题目链接 - [0497. 非重叠矩形中的随机点 - 力扣](https://leetcode.cn/problems/random-point-in-non-overlapping-rectangles/) ## 题目大意 **描述**: 给定一个由非重叠的轴对齐矩形的数组 $rects$ ,其中 $rects[i] = [ai, bi, xi, yi]$ 表示 $(ai, bi)$ 是第 $i$ 个矩形的左下角点,$(xi, yi)$ 是第 $i$ 个矩形的右上角点。 **要求**: 设计一个算法来随机挑选一个被某一矩形覆盖的整数点。矩形周长上的点也算做是被矩形覆盖。所有满足要求的点必须等概率被返回。 在给定的矩形覆盖的空间内的任何整数点都有可能被返回。 实现 `Solution` 类: - `Solution(int[][] rects)` 用给定的矩形数组 $rects$ 初始化对象。 - `int[] pick()` 返回一个随机的整数点 $[u, v]$ 在给定的矩形所覆盖的空间内。 **说明**: - 整数点是具有整数坐标的点。 - $1 \le rects.length \le 10^{3}$。 - $rects[i].length == 4$。 - $-10^{9} \le ai \lt xi \le 10^{9}$。 - $-10^{9} \le bi \lt yi \le 10^{9}$。 - $xi - ai \le 2000$。 - $yi - bi \le 2000$。 - 所有的矩形不重叠。 - $pick$ 最多被调用 $10^{4}$ 次。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/07/24/lc-pickrandomrec.jpg) ```python 输入: ["Solution", "pick", "pick", "pick", "pick", "pick"] [[[[-2, -2, 1, 1], [2, 2, 4, 6]]], [], [], [], [], []] 输出: [null, [1, -2], [1, -1], [-1, -2], [-2, -2], [0, 0]] 解释: Solution solution = new Solution([[-2, -2, 1, 1], [2, 2, 4, 6]]); solution.pick(); // 返回 [1, -2] solution.pick(); // 返回 [1, -1] solution.pick(); // 返回 [-1, -2] solution.pick(); // 返回 [-2, -2] solution.pick(); // 返回 [0, 0] ``` ## 解题思路 ### 思路 1:前缀和 + 二分查找 这道题的关键是要让所有满足要求的点等概率被返回。 我们可以将问题分成两步: 1. **随机选择一个矩形**:由于不同矩形包含的点数不同,我们需要根据点数加权随机选择一个矩形。 2. **在选中的矩形内随机选一个点**:在矩形内均匀随机选择整数点坐标。 具体实现: 对于每个矩形 $rects[i] = [a_i, b_i, x_i, y_i]$,它包含的点数为: $count_i = (x_i - a_i + 1) \times (y_i - b_i + 1)$ 我们在初始化时: - 计算每个矩形的点数 $count_i$。 - 建立前缀和数组 $prefix$,其中 $prefix[i] = \sum_{j=0}^{i} count_j$。 - 记录所有矩形信息。 在调用 `pick()` 时: - 生成一个随机整数 $target$,范围为 $[1, prefix[n-1]]$。 - 使用二分查找找到第一个 $prefix[i] \ge target$ 的矩形索引 $i$。 - 在矩形 $i$ 内随机选择一个点:横坐标在 $[a_i, x_i]$ 范围内随机,纵坐标在 $[b_i, y_i]$ 范围内随机。 - 返回该点的坐标 $[rand_x, rand_y]$。 这样就能保证每个点被选中的概率相等。 ### 思路 1:代码 ```python import random import bisect class Solution: def __init__(self, rects: List[List[int]]): self.rects = rects # prefix[i] 表示前 i 个矩形(包括第 i 个)包含的总点数 self.prefix = [] # 计算每个矩形的点数并建立前缀和数组 for a, b, x, y in rects: # 计算矩形包含的点数:(x - a + 1) * (y - b + 1) count = (x - a + 1) * (y - b + 1) # 前缀和为前一个值加上当前矩形的点数 if self.prefix: self.prefix.append(self.prefix[-1] + count) else: self.prefix.append(count) def pick(self) -> List[int]: # 在 [1, prefix[-1]] 范围内随机选择一个目标值 target = random.randint(1, self.prefix[-1]) # 使用二分查找找到目标值所在的矩形索引 # bisect_left 返回第一个 >= target 的位置 rect_idx = bisect.bisect_left(self.prefix, target) # 获取对应矩形 a, b, x, y = self.rects[rect_idx] # 在矩形内随机选择点的横坐标和纵坐标 rand_x = random.randint(a, x) rand_y = random.randint(b, y) return [rand_x, rand_y] # Your Solution object will be instantiated and called as such: # obj = Solution(rects) # param_1 = obj.pick() ``` ### 思路 1:复杂度分析 - **时间复杂度**: - 初始化:$O(n)$,其中 $n$ 是矩形数量。需要遍历所有矩形计算前缀和。 - `pick()` 方法:$O(\log n)$。二分查找时间为 $O(\log n)$。 - **空间复杂度**:$O(n)$。需要存储前缀和数组。 ================================================ FILE: docs/solutions/0400-0499/reconstruct-original-digits-from-english.md ================================================ # [0423. 从英文中重建数字](https://leetcode.cn/problems/reconstruct-original-digits-from-english/) - 标签:哈希表、数学、字符串 - 难度:中等 ## 题目链接 - [0423. 从英文中重建数字 - 力扣](https://leetcode.cn/problems/reconstruct-original-digits-from-english/) ## 题目大意 **描述**: 给定一个字符串 $s$,其中包含字母顺序打乱的用英文单词表示的若干数字($0 \sim 9$)。 **要求**: 按升序返回原始的数字。 **说明**: - $1 \le s.length \le 10^{5}$。 - $s[i]$ 为 `["e","g","f","i","h","o","n","s","r","u","t","w","v","x","z"]` 这些字符之一。 - $s$ 保证是一个符合题目要求的字符串。 **示例**: - 示例 1: ```python 输入:s = "owoztneoer" 输出:"012" ``` - 示例 2: ```python 输入:s = "fviefuro" 输出:"45" ``` ## 解题思路 ### 思路 1:哈希表统计 + 特征字符识别 这道题目的关键在于利用英文字母的特征来识别数字。 每个英文单词中都有一些独特字符可以帮助我们识别: - `"zero"` 中的 `'z'` 是独一无二的 - `"two"` 中的 `'w'` 是独一无二的 - `"four"` 中的 `'u'` 是独一无二的 - `"six"` 中的 `'x'` 是独一无二的 - `"eight"` 中的 `'g'` 是独一无二的 利用这些特征字符,我们可以: 1. 先统计字符串 $s$ 中每个字符 $ch$ 的出现次数 $count[ch]$。 2. 识别出具有特征字符的数字: - $cnt[0]$ 的数量 = $count['z']$ - $cnt[2]$ 的数量 = $count['w']$ - $cnt[4]$ 的数量 = $count['u']$ - $cnt[6]$ 的数量 = $count['x']$ - $cnt[8]$ 的数量 = $count['g']$ 3. 根据已有数字的出现次数,推断其他数字: - $cnt[1]$ 的数量 = $count['o'] - cnt[0] - cnt[2] - cnt[4]$(`'o'` 出现在 `"zero"`, `"one"`, `"two"`, `"four"` 中) - $cnt[3]$ 的数量 = $count['h'] - cnt[8]$(`'h'` 出现在 `"three"`, `"eight"` 中) - $cnt[5]$ 的数量 = $count['f'] - cnt[4]$(`'f'` 出现在 `"five"`, `"four"` 中) - $cnt[7]$ 的数量 = $count['s'] - cnt[6]$(`'s'` 出现在 `"seven"`, `"six"` 中) - $cnt[9]$ 的数量 = $count['i'] - cnt[5] - cnt[6] - cnt[8]$(`'i'` 出现在 `"nine"`, `"five"`, `"six"`, `å` 中) 4. 最后按照数字大小 $0 \sim 9$ 的顺序构造结果字符串。 ### 思路 1:代码 ```python class Solution: def originalDigits(self, s: str) -> str: # 统计每个字符的出现次数 count = {} for ch in s: count[ch] = count.get(ch, 0) + 1 # cnt[i] 表示数字 i 的出现次数 cnt = [0] * 10 # 通过特征字符识别数字 cnt[0] = count.get('z', 0) # zero cnt[2] = count.get('w', 0) # two cnt[4] = count.get('u', 0) # four cnt[6] = count.get('x', 0) # six cnt[8] = count.get('g', 0) # eight # 根据已有数字推断其他数字 cnt[1] = count.get('o', 0) - cnt[0] - cnt[2] - cnt[4] # one (o 在 zero, one, two, four 中) cnt[3] = count.get('h', 0) - cnt[8] # three (h 在 three, eight 中) cnt[5] = count.get('f', 0) - cnt[4] # five (f 在 four, five 中) cnt[7] = count.get('s', 0) - cnt[6] # seven (s 在 six, seven 中) cnt[9] = count.get('i', 0) - cnt[5] - cnt[6] - cnt[8] # nine (i 在 five, six, eight, nine 中) # 构造结果字符串 res = "" for i in range(10): res += str(i) * cnt[i] return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + 10)$。其中 $n$ 是字符串 $s$ 的长度。需要遍历字符串统计字符出现次数($O(n)$),然后进行常数次计算($O(10)$)。 - **空间复杂度**:$O(|\Sigma| + 10)$。其中 $|\Sigma|$ 是字符集大小,这里最多为 26 个字母。$O(10)$ 是用于存储每个数字出现次数的数组。 ================================================ FILE: docs/solutions/0400-0499/remove-k-digits.md ================================================ # [0402. 移掉 K 位数字](https://leetcode.cn/problems/remove-k-digits/) - 标签:栈、贪心、字符串、单调栈 - 难度:中等 ## 题目链接 - [0402. 移掉 K 位数字 - 力扣](https://leetcode.cn/problems/remove-k-digits/) ## 题目大意 **描述**: 给定一个以字符串表示的非负整数 $num$ 和一个整数 $k$。 **要求**: 移除这个数中的 $k$ 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。 **说明**: - $1 \le k \le num.length \le 10^{5}$。 - num 仅由若干位数字($0 \sim 9$)组成。 - 除了 $0$ 本身之外,$num$ 不含任何前导零。 **示例**: - 示例 1: ```python 输入:num = "1432219", k = 3 输出:"1219" 解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。 ``` - 示例 2: ```python 输入:num = "10200", k = 1 输出:"200" 解释:移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。 ``` ## 解题思路 ### 思路 1:贪心算法 + 单调栈 **核心思想**:要想让数字尽可能小,应该让高位尽可能小。从左到右遍历数字,如果当前数字比前面的数字小,则删除前面的数字可以让整体数字更小。 **算法步骤**: 1. 初始化单调栈 $stack$,用于维护一个从底到顶单调递增的数字序列。 2. 从左到右遍历字符串 $num$ 的每一位数字 $num[i]$: - 当栈不为空、$k > 0$ 且当前数字 $num[i] < stack[-1]$ 时,弹出栈顶元素并减少 $k$(贪心策略:删除高位较大的数字)。 - 否则将当前数字压入栈中。 3. 如果 $k > 0$(还有需要删除的数字),则从栈底或栈顶继续删除 $k$ 位数字。 4. 去除前导零,将栈中剩余数字转换为字符串返回。 **注意**:需要处理以下边界情况: - 如果所有数字都被删除完毕,返回 `"0"`。 - 返回结果需要去除前导零。 ### 思路 1:代码 ```python class Solution: def removeKdigits(self, num: str, k: int) -> str: stack = [] # 遍历字符串 for digit in num: # 当前数字比栈顶小,删除栈顶(贪心) while stack and k > 0 and digit < stack[-1]: stack.pop() k -= 1 stack.append(digit) # 如果还有剩余删除次数,从末尾删除 if k > 0: stack = stack[:-k] # 去除前导零,转换为字符串 result = ''.join(stack).lstrip('0') return result if result else '0' ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是字符串 $num$ 的长度。每个数字最多进栈和出栈一次。 - **空间复杂度**:$O(n)$。使用了一个栈存储数字,最坏情况下栈大小为 $n$。 ================================================ FILE: docs/solutions/0400-0499/repeated-substring-pattern.md ================================================ # [0459. 重复的子字符串](https://leetcode.cn/problems/repeated-substring-pattern/) - 标签:字符串、字符串匹配 - 难度:简单 ## 题目链接 - [0459. 重复的子字符串 - 力扣](https://leetcode.cn/problems/repeated-substring-pattern/) ## 题目大意 **描述**:给定一个非空的字符串 `s`。 **要求**:检查该字符串 `s` 是否可以通过由它的一个子串重复多次构成。 **说明**: - $1 \le s.length \le 10^4$。 - `s` 由小写英文字母组成 **示例**: - 示例 1: ```python 输入: s = "abab" 输出: true 解释: 可由子串 "ab" 重复两次构成。 ``` - 示例 2: ```python 输入: s = "aba" 输出: false ``` ## 解题思路 ### 思路 1:KMP 算法 这道题我们可以使用 KMP 算法的 `next` 数组来解决。我们知道 `next[j]` 表示的含义是:**记录下标 `j` 之前(包括 `j`)的模式串 `p` 中,最长相等前后缀的长度。** 而如果整个模式串 `p` 的最长相等前后缀长度不为 `0`,即 `next[len(p) - 1] != 0` ,则说明整个模式串 `p` 中有最长相同的前后缀,假设 `next[len(p) - 1] == k`,则说明 `p[0: k] == p[m - k: m]`。比如字符串 `"abcabcabc"`,最长相同前后缀为 `"abcabc" = "abcabc"`。 - 如果最长相等的前后缀是重叠的,比如之前的例子 `"abcabcabc"`。 - 如果我们去除字符串中相同的前后缀的重叠部分,剩下两头前后缀部分(这两部分是相同的)。然后再去除剩余的后缀部分,只保留剩余的前缀部分。比如字符串 `"abcabcabc"` 去除重叠部分和剩余的后缀部分之后就是 `"abc"`。实际上这个部分就是字符串去除整个后缀部分的剩余部分。 - 如果整个字符串可以通过子串重复构成的话,那么这部分就是最小周期的子串。 - 我们只需要判断整个子串的长度是否是剩余部分长度的整数倍即可。也就是判断 `len(p) % (len(p) - next[size - 1]) == 0` 是否成立,如果成立,则字符串 `s` 可由 `s[0: len(p) - next[size - 1]]` 构成的子串重复构成,返回 `True`。否则返回 `False`。 - 如果最长相等的前后缀是不重叠的,那我们可将重叠部分视为长度为 `0` 的空串,则剩余的部分其实就是去除后缀部分的剩余部分,上述结论依旧成立。 ### 思路 1:代码 ```python class Solution: def generateNext(self, p: str): m = len(p) next = [0 for _ in range(m)] left = 0 for right in range(1, m): while left > 0 and p[left] != p[right]: left = next[left - 1] if p[left] == p[right]: left += 1 next[right] = left return next def repeatedSubstringPattern(self, s: str) -> bool: size = len(s) if size == 0: return False next = self.generateNext(s) if next[size - 1] != 0 and size % (size - next[size - 1]) == 0: return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m)$,其中模式串 $p$ 的长度为 $m$。 - **空间复杂度**:$O(m)$。 ================================================ FILE: docs/solutions/0400-0499/reverse-pairs.md ================================================ # [0493. 翻转对](https://leetcode.cn/problems/reverse-pairs/) - 标签:树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 - 难度:困难 ## 题目链接 - [0493. 翻转对 - 力扣](https://leetcode.cn/problems/reverse-pairs/) ## 题目大意 **描述**: 给定一个数组 $nums$,如果 $i < j$ 且 $nums[i] > 2 \times nums[j]$ 我们就将 $(i, j)$ 称作一个重要翻转对。 **要求**: 返回给定数组中的重要翻转对的数量。 **说明**: - 给定数组的长度不会超过 $50000$。 - 输入数组中的所有数字都在 32 位整数的表示范围内。 **示例**: - 示例 1: ```python 输入: [1,3,2,3,1] 输出: 2 ``` - 示例 2: ```python 输入: [2,4,3,5,1] 输出: 3 ``` ## 解题思路 ### 思路 1:归并排序 + 分治思想 **核心思想**:翻转对问题可以转化为在归并排序过程中统计满足 $nums[i] > 2 \times nums[j]$ 且 $i < j$ 的 $(i, j)$ 对的数量。 **算法步骤**: 1. **分解**:将数组分成左右两部分,分别统计左右两部分内部的翻转对数量。 2. **合并前统计**:在合并左右两部分之前,统计跨越中点的翻转对数量(即 $i$ 在左半部分,$j$ 在右半部分)。 3. **合并**:按照归并排序的方式合并左右两部分,并返回总的翻转对数量。 **统计跨越中点的翻转对**: - 对于左半部分的每个元素 $nums[i]$,在右半部分中找到所有满足 $nums[i] > 2 \times nums[j]$ 的元素 $nums[j]$。 - 由于两部分都是有序的,可以使用双指针技术高效统计。 **注意**:在统计翻转对和合并的过程中,左右两部分的相对顺序会影响统计结果,需要分别处理。 ### 思路 1:代码 ```python class Solution: def reversePairs(self, nums: List[int]) -> int: # 用于统计翻转对数量 self.count = 0 # 归并排序函数 def merge_sort(nums, left, right): if left >= right: return # 计算中点 mid = (left + right) // 2 # 递归处理左右两部分 merge_sort(nums, left, mid) merge_sort(nums, mid + 1, right) # 统计跨越中点的翻转对 i = left j = mid + 1 # 对于左半部分的每个元素,统计右半部分中满足条件的元素 while i <= mid: while j <= right and nums[i] > 2 * nums[j]: j += 1 # 右半部分中满足 nums[i] > 2 * nums[j] 的元素个数 self.count += (j - mid - 1) i += 1 # 合并左右两部分(使用临时数组) temp = [] i = left j = mid + 1 while i <= mid and j <= right: if nums[i] <= nums[j]: temp.append(nums[i]) i += 1 else: temp.append(nums[j]) j += 1 # 添加剩余元素 while i <= mid: temp.append(nums[i]) i += 1 while j <= right: temp.append(nums[j]) j += 1 # 将排序后的结果写回原数组 for k in range(len(temp)): nums[left + k] = temp[k] # 执行归并排序 merge_sort(nums, 0, len(nums) - 1) return self.count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$。其中 $n$ 是数组 $nums$ 的长度。归并排序的时间复杂度为 $O(n \log n)$,在每次合并过程中统计翻转对需要 $O(n)$ 时间,因此总的时间复杂度为 $O(n \log n)$。 - **空间复杂度**:$O(n)$。归并排序需要使用额外的临时数组来存储中间结果,递归调用栈的深度为 $O(\log n)$,因此总的空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0400-0499/robot-room-cleaner.md ================================================ # [0489. 扫地机器人](https://leetcode.cn/problems/robot-room-cleaner/) - 标签:回溯、交互 - 难度:困难 ## 题目链接 - [0489. 扫地机器人 - 力扣](https://leetcode.cn/problems/robot-room-cleaner/) ## 题目大意 **描述**: 给定一个房间(二维网格),其中 $0$ 表示空地,$1$ 表示障碍物。有一个扫地机器人,初始位置和朝向未知。 机器人可以通过以下 API 与环境交互: - `move()`:向前移动一格,如果前方是障碍物或边界则返回 $false$,否则返回 $true$。 - `turnLeft()`:原地左转 90 度。 - `turnRight()`:原地右转 90 度。 - `clean()`:清扫当前格子。 **要求**: 使用机器人清扫所有可达的空地。 **说明**: - 房间的大小未知。 - 机器人的初始位置和朝向未知。 - 机器人不能穿过障碍物。 - 所有空单元格都可以从起始位置出发访问到。 - $m == room.length$。 - $n == room[i].length$。 - $1 \le m \le 100$。 - $1 \le n \le 200$。 - $room[i][j]$ 为 0 或 1。 - $0 \le row < m$。 - $0 \le col < n$。 - $room[row][col] == 1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/07/17/lc-grid.jpg) ```python 输入:room = [[1,1,1,1,1,0,1,1],[1,1,1,1,1,0,1,1],[1,0,1,1,1,1,1,1],[0,0,0,1,0,0,0,0],[1,1,1,1,1,1,1,1]], row = 1, col = 3 输出:Robot cleaned all rooms. 解释: 房间内的所有单元格用 0 或 1 填充。 0 表示障碍物,1 表示可以通过。 机器人从 row=1, col=3 的初始位置出发。 在左上角的一行以下,三列以右。 ``` - 示例 2: ```python 输入:room = [[1]], row = 0, col = 0 输出:Robot cleaned all rooms. ``` ## 解题思路 ### 思路 1:DFS + 回溯 扫地机器人需要清扫所有可达的格子。使用 DFS 遍历所有可达位置。 **核心思路**: - 机器人只能通过 API 与环境交互,不知道房间的布局。 - 使用 DFS 遍历,记录已访问的位置。 - 关键是在回溯时恢复机器人的方向和位置。 **解题步骤**: 1. 定义四个方向:上、右、下、左(顺时针)。 2. 使用 DFS 遍历: - 清扫当前格子。 - 尝试四个方向,如果可以移动且未访问,递归访问。 - 回溯:转向相反方向,移动回原位置。 3. 使用集合记录已访问的位置,避免重复访问。 **关键点**: - 机器人的方向需要维护,使用 `turnRight()` 和 `turnLeft()` 调整方向。 - 回溯时需要让机器人回到原位置和原方向。 ### 思路 1:代码 ```python # """ # This is the robot's control interface. # You should not implement it, or speculate about its implementation # """ # class Robot: # def move(self): # """ # Returns true if the cell in front is open and robot moves into the cell. # Returns false if the cell in front is blocked and robot stays in the current cell. # :rtype bool # """ # # def turnLeft(self): # """ # Robot will stay in the same cell after calling turnLeft/turnRight. # Each turn will be 90 degrees. # :rtype void # """ # # def turnRight(self): # """ # Robot will stay in the same cell after calling turnLeft/turnRight. # Each turn will be 90 degrees. # :rtype void # """ # # def clean(self): # """ # Clean the current cell. # :rtype void # """ class Solution: def cleanRoom(self, robot): """ :type robot: Robot :rtype: None """ # 四个方向:上、右、下、左(顺时针) directions = [(-1, 0), (0, 1), (1, 0), (0, -1)] visited = set() def go_back(): # 回到上一个位置 robot.turnRight() robot.turnRight() robot.move() robot.turnRight() robot.turnRight() def dfs(x, y, direction): # 清扫当前格子 robot.clean() visited.add((x, y)) # 尝试四个方向 for i in range(4): new_direction = (direction + i) % 4 dx, dy = directions[new_direction] nx, ny = x + dx, y + dy # 如果未访问且可以移动 if (nx, ny) not in visited and robot.move(): dfs(nx, ny, new_direction) go_back() # 转向下一个方向 robot.turnRight() dfs(0, 0, 0) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n - m)$,其中 $n$ 是房间中的格子总数,$m$ 是障碍物数量。每个可达格子访问一次。 - **空间复杂度**:$O(n - m)$,递归栈和 $visited$ 集合的空间开销。 ================================================ FILE: docs/solutions/0400-0499/sentence-screen-fitting.md ================================================ # [0418. 屏幕可显示句子的数量](https://leetcode.cn/problems/sentence-screen-fitting/) - 标签:数组、字符串、动态规划 - 难度:中等 ## 题目链接 - [0418. 屏幕可显示句子的数量 - 力扣](https://leetcode.cn/problems/sentence-screen-fitting/) ## 题目大意 **描述**: 给定一个句子数组 $sentence$ 和屏幕的行数 $rows$ 和列数 $cols$。 句子中的单词用空格分隔,需要按顺序在屏幕上显示。如果一行放不下当前单词,则将单词移到下一行。句子循环显示。 **要求**: 返回屏幕上可以显示多少次完整的句子。 **说明**: - $1 \le sentence.length \le 100$。 - $1 \le sentence[i].length \le 10$。 - $sentence[i]$ 只包含小写英文字母。 - $1 \le rows, cols \le 2 \times 10^4$。 **示例**: - 示例 1: ```python 输入:sentence = ["hello","world"], rows = 2, cols = 8 输出:1 解释: hello--- world--- 可以显示 1 次完整的句子。 ``` - 示例 2: ```python 输入:sentence = ["a", "bcd", "e"], rows = 3, cols = 6 输出:2 解释: a-bcd- e-a--- bcd-e- 可以显示 2 次完整的句子。 ``` ## 解题思路 ### 思路 1:模拟 + 优化 给定一个句子数组 $sentence$ 和屏幕的行数 $rows$ 和列数 $cols$,需要计算屏幕上可以显示多少次完整的句子。 **核心思路**: - 将句子拼接成一个循环字符串(单词之间用空格分隔)。 - 模拟在屏幕上逐行填充字符。 - 使用优化:预计算从每个单词开始,一行可以放下多少个字符。 **解题步骤**: 1. 将句子拼接成字符串,单词之间用空格分隔,形成 `"word1 word2 ... wordn "`。 2. 使用变量 $start$ 记录当前在句子字符串中的位置。 3. 对于每一行: - 计算这一行可以放下多少个字符:$start += cols$。 - 如果 $start$ 位置是空格,继续向后跳过空格。 - 如果 $start$ 位置不是空格,需要回退到上一个空格(保证单词完整)。 4. 最后计算 $start$ 除以句子长度,得到完整句子的次数。 ### 思路 1:代码 ```python class Solution: def wordsTyping(self, sentence: List[str], rows: int, cols: int) -> int: # 拼接句子,单词之间用空格分隔 s = ' '.join(sentence) + ' ' n = len(s) start = 0 # 当前在句子字符串中的位置 for _ in range(rows): # 这一行可以放下 cols 个字符 start += cols # 如果当前位置是空格,可以继续向后 if s[start % n] == ' ': start += 1 else: # 回退到上一个空格(保证单词完整) while start > 0 and s[(start - 1) % n] != ' ': start -= 1 # 计算完整句子的次数 return start // n ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(rows \times cols)$,最坏情况下每行需要回退 $cols$ 次。实际上,由于单词长度有限,回退次数通常很少。 - **空间复杂度**:$O(m)$,其中 $m$ 是句子的总长度。 ================================================ FILE: docs/solutions/0400-0499/sequence-reconstruction.md ================================================ # [0444. 序列重建](https://leetcode.cn/problems/sequence-reconstruction/) - 标签:图、拓扑排序、数组 - 难度:中等 ## 题目链接 - [0444. 序列重建 - 力扣](https://leetcode.cn/problems/sequence-reconstruction/) ## 题目大意 **描述**: 给定一个整数数组 $nums$ 和一个整数数组序列 $sequences$,其中,其中 $nums$ 是范围为 $[1, n]$ 的整数的排列,$sequences[i]$ 是 $nums$ 的一个子序列。 **要求**: 判断 $nums$ 是否是唯一的最短超序列。即判断 $nums$ 是否是唯一可以从 $sequences$ 重建出来的序列。 **说明**: - 最短「超序列」:是「长度最短」的序列,并且所有序列 $sequences[i]$ 都是它的子序列。对于给定的数组 $sequences$,可能存在多个有效的「超序列」。 - 例如,对于 $sequences = [[1,2],[1,3]]$,有两个最短的「超序列」,$[1,2,3]$ 和 $[1,3,2]$。 - 而对于 $sequences = [[1,2],[1,3],[1,2,3]]$,唯一可能的最短「超序列」是 $[1,2,3]$。$[1,2,3,4]$ 是可能的超序列,但不是最短的。 - $1 \le nums.length \le 10^4$。 - $1 \le sequences.length \le 10^4$。 - $1 \le sequences[i].length \le 10^4$。 - $1 \le sum(sequences[i].length) \le 10^5$。 - $1 \le nums[i], sequences[i][j] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3], sequences = [[1,2],[1,3]] 输出:false 解释:有两种可能的超序列:[1,2,3]和[1,3,2]。 序列 [1,2] 是[1,2,3]和[1,3,2]的子序列。 序列 [1,3] 是[1,2,3]和[1,3,2]的子序列。 因为 nums 不是唯一最短的超序列,所以返回false。 ``` - 示例 2: ```python 输入:nums = [1,2,3], sequences = [[1,2]] 输出:false 解释:最短可能的超序列为 [1,2]。 序列 [1,2] 是它的子序列:[1,2]。 因为 nums 不是最短的超序列,所以返回false。 ``` ## 解题思路 ### 思路 1:拓扑排序 判断是否可以从 $sequences$ 中唯一重建出原始序列 $nums$。 **核心思想**: - 如果 $nums$ 是唯一的拓扑排序结果,那么在拓扑排序的每一步,入度为 0 的节点必须唯一。 - 同时,$sequences$ 中的所有边必须能构建出 $nums$ 的相邻关系。 **解题步骤**: 1. 根据 $sequences$ 构建图的邻接表和入度数组。 2. 检查 $sequences$ 中的所有数字是否都在 $nums$ 中。 3. 使用拓扑排序(BFS): - 每次只能有一个入度为 0 的节点(保证唯一性)。 - 拓扑排序的结果必须与 $nums$ 完全一致。 4. 检查 $nums$ 中相邻的元素在图中是否有边连接。 ### 思路 1:代码 ```python from collections import defaultdict, deque class Solution: def sequenceReconstruction(self, nums: List[int], sequences: List[List[int]]) -> bool: n = len(nums) graph = defaultdict(set) indegree = {i: 0 for i in nums} # 检查 sequences 中的数字是否都在 nums 中 all_nums = set() for seq in sequences: for num in seq: all_nums.add(num) if num not in indegree: return False # 如果 sequences 中的数字集合与 nums 不一致 if all_nums != set(nums): return False # 构建图 for seq in sequences: for i in range(len(seq) - 1): u, v = seq[i], seq[i + 1] if v not in graph[u]: graph[u].add(v) indegree[v] += 1 # 拓扑排序 queue = deque([num for num in nums if indegree[num] == 0]) result = [] while queue: # 每次必须只有一个入度为 0 的节点(保证唯一性) if len(queue) != 1: return False node = queue.popleft() result.append(node) # 更新邻居的入度 for neighbor in graph[node]: indegree[neighbor] -= 1 if indegree[neighbor] == 0: queue.append(neighbor) # 检查拓扑排序结果是否与 nums 一致 return result == nums ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 是 $nums$ 的长度,$m$ 是 $sequences$ 中所有元素的总数。 - **空间复杂度**:$O(n + m)$,存储图和入度数组。 ================================================ FILE: docs/solutions/0400-0499/serialize-and-deserialize-bst.md ================================================ # [0449. 序列化和反序列化二叉搜索树](https://leetcode.cn/problems/serialize-and-deserialize-bst/) - 标签:树、深度优先搜索、广度优先搜索、设计、二叉搜索树、字符串、二叉树 - 难度:中等 ## 题目链接 - [0449. 序列化和反序列化二叉搜索树 - 力扣](https://leetcode.cn/problems/serialize-and-deserialize-bst/) ## 题目大意 **描述**: 序列化是将数据结构或对象转换为一系列位的过程,以便它可以存储在文件或内存缓冲区中,或通过网络连接链路传输,以便稍后在同一个或另一个计算机环境中重建。 **要求**: 设计一个算法来序列化和反序列化「二叉搜索树」。对序列化 / 反序列化算法的工作方式没有限制。 您只需确保二叉搜索树可以序列化为字符串,并且可以将该字符串反序列化为最初的二叉搜索树。 编码的字符串应尽可能紧凑。 **说明**: - 树中节点数范围是 $[0, 10^{4}]$。 - $0 \le Node.val \le 10^{4}$。 - 题目数据保证输入的树是一棵二叉搜索树。 **示例**: - 示例 1: ```python 输入:root = [2,1,3] 输出:[2,1,3] ``` - 示例 2: ```python 输入:root = [] 输出:[] ``` ## 解题思路 ### 思路 1:前序遍历 + BST 特性 **核心思想**:利用二叉搜索树(BST)的特性,BST 中左子树所有节点值都小于根节点值,右子树所有节点值都大于根节点值。因此,可以通过前序遍历序列来唯一确定一棵 BST。 **算法步骤**: 1. **序列化**: - 使用前序遍历(根 -> 左 -> 右)访问 BST 的所有节点。 - 将每个节点的值转换为字符串,用逗号 `','` 分隔。 - 返回序列化后的字符串 $data$。 2. **反序列化**: - 将字符串 $data$ 按逗号分割,得到前序遍历数组 $values$。 - 对于前序遍历的结果,第一个值是根节点 $root$。 - 利用 BST 特性,找到第一个大于 $root$ 的值,该位置之前的元素属于左子树,之后的元素属于右子树。 - 递归构建左子树和右子树。 **关键点**:BST 的前序遍历序列具有唯一性,第一个值确定根节点后,根据 BST 特性可以唯一确定左右子树的分界点。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val = x # self.left = None # self.right = None class Codec: def serialize(self, root: Optional[TreeNode]) -> str: """Encodes a tree to a single string. """ # 存储序列化结果 res = [] def preorder(node): if not node: return # 前序遍历:先访问根节点 res.append(str(node.val)) preorder(node.left) preorder(node.right) preorder(root) # 用逗号连接所有节点值 return ','.join(res) def deserialize(self, data: str) -> Optional[TreeNode]: """Decodes your encoded data to tree. """ if not data: return None # 将字符串按逗号分割,转换为整数列表 values = [int(val) for val in data.split(',')] def build_tree(preorder_values): if not preorder_values: return None # 第一个值是根节点 root_val = preorder_values[0] root = TreeNode(root_val) # 找到第一个大于根节点的值,即右子树的起点 i = 1 while i < len(preorder_values) and preorder_values[i] < root_val: i += 1 # 左子树:从位置 1 到 i-1 root.left = build_tree(preorder_values[1:i]) # 右子树:从位置 i 到末尾 root.right = build_tree(preorder_values[i:]) return root return build_tree(values) # Your Codec object will be instantiated and called as such: # ser = Codec() # deser = Codec() # tree = ser.serialize(root) # ans = deser.deserialize(tree) # return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是 BST 的节点数。序列化需要遍历所有节点,时间复杂度为 $O(n)$;反序列化时,每个节点需要处理一次,时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。递归调用栈的最大深度为 $O(n)$(最坏情况下是链状 BST),序列化字符串的空间为 $O(n)$。 ================================================ FILE: docs/solutions/0400-0499/serialize-and-deserialize-n-ary-tree.md ================================================ # [0428. 序列化和反序列化 N 叉树](https://leetcode.cn/problems/serialize-and-deserialize-n-ary-tree/) - 标签:树、深度优先搜索、广度优先搜索、字符串 - 难度:困难 ## 题目链接 - [0428. 序列化和反序列化 N 叉树 - 力扣](https://leetcode.cn/problems/serialize-and-deserialize-n-ary-tree/) ## 题目大意 要求:设计一个序列化和反序列化 N 叉树的算法。序列化 / 反序列化算法的算法实现没有限制。你只需要保证 N 叉树可以被序列化为一个字符串并且该字符串可以被反序列化成原树结构即可。 - 序列化是指将一个数据结构转化为位序列的过程,因此可以将其存储在文件中或内存缓冲区中,以便稍后在相同或不同的计算机环境中恢复结构。 - N 叉树是指每个节点都有不超过 N 个孩子节点的有根树。 ## 解题思路 - 序列化:通过深度优先搜索的方式,递归遍历节点,以 `root.val`、`len(root.children)`、`root.children` 的顺序生成序列化结果,并用 `-` 链接,返回结果字符串。 - 反序列化:先将字符串按 `-` 分割成数组。然后按照 `root.val`、`len(root.children)`、`root.children` 的顺序解码,并建立对应节点。最后返回根节点。 ## 代码 ```python class Codec: def serialize(self, root: 'Node') -> str: """Encodes a tree to a single string. :type root: Node :rtype: str """ if not root: return 'None' data = str(root.val) + '-' + str(len(root.children)) for child in root.children: data += '-' + self.serialize(child) return data def deserialize(self, data: str) -> 'Node': """Decodes your encoded data to tree. :type data: str :rtype: Node """ datalist = data.split('-') return self.dfs(datalist) def dfs(self, datalist): val = datalist.pop(0) if val == 'None': return None root = Node(int(val)) root.children = [] size = int(datalist.pop(0)) for _ in range(size): root.children.append(self.dfs(datalist)) return root ``` ================================================ FILE: docs/solutions/0400-0499/sliding-window-median.md ================================================ # [0480. 滑动窗口中位数](https://leetcode.cn/problems/sliding-window-median/) - 标签:数组、哈希表、滑动窗口、堆(优先队列) - 难度:困难 ## 题目链接 - [0480. 滑动窗口中位数 - 力扣](https://leetcode.cn/problems/sliding-window-median/) ## 题目大意 **描述**:给定一个数组 $nums$,有一个长度为 $k$ 的窗口从最左端滑动到最右端。窗口中有 $k$ 个数,每次窗口向右移动 $1$ 位。 **要求**:找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。 **说明**: - **中位数**:有序序列最中间的那个数。如果序列的长度是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。 - 例如: - $[2,3,4]$,中位数是 $3$ - $[2,3]$,中位数是 $(2 + 3) / 2 = 2.5$。 - 你可以假设 $k$ 始终有效,即:$k$ 始终小于等于输入的非空数组的元素个数。 - 与真实值误差在 $10 ^ {-5}$ 以内的答案将被视作正确答案。 **示例**: - 示例 1: ```python 给出 nums = [1,3,-1,-3,5,3,6,7],以及 k = 3。 窗口位置 中位数 --------------- ----- [1 3 -1] -3 5 3 6 7 1 1 [3 -1 -3] 5 3 6 7 -1 1 3 [-1 -3 5] 3 6 7 -1 1 3 -1 [-3 5 3] 6 7 3 1 3 -1 -3 [5 3 6] 7 5 1 3 -1 -3 5 [3 6 7] 6 因此,返回该滑动窗口的中位数数组 [1,-1,-1,3,5,6]。 ``` ## 解题思路 ### 思路 1:小顶堆 + 大顶堆 题目要求动态维护长度为 $k$ 的窗口中元素的中位数。如果对窗口元素进行排序,时间复杂度一般是 $O(k \times \log k)$。如果对每个区间都进行排序,那时间复杂度就更大了,肯定会超时。 我们需要借助一个内部有序的数据结构,来降低取窗口中位数的时间复杂度。Python 可以借助 `heapq` 构建大顶堆和小顶堆。通过 $k$ 的奇偶性和堆顶元素来获取中位数。 接下来还要考虑几个问题:初始化问题、取中位数问题、窗口滑动中元素的添加删除操作。接下来一一解决。 初始化问题: 我们将所有大于中位数的元素放到 $heap\_max$(小顶堆)中,并且元素个数向上取整。然后再将所有小于等于中位数的元素放到 $heap\_min$(大顶堆)中,并且元素个数向下取整。这样当 $k$ 为奇数时,$heap\_max$ 比 $heap\_min$ 多一个元素,中位数就是 $heap\_max$ 堆顶元素。当 $k$ 为偶数时,$heap\_max$ 和 $heap\_min$ 中的元素个数相同,中位数就是 $heap\_min$ 堆顶元素和 $heap\_max$ 堆顶元素的平均数。这个过程操作如下: - 先将数组中前 $k$ 个元素放到 $heap\_max$ 中。 - 再从 $heap\_max$ 中取出 $k // 2$ 个堆顶元素放到 $heap\_min$ 中。 取中位数问题(上边提到过): - 当 $k$ 为奇数时,中位数就是 $heap\_max$ 堆顶元素。当 $k$ 为偶数时,中位数就是 $heap\_max$ 堆顶元素和 $heap\_min$ 堆顶元素的平均数。 窗口滑动过程中元素的添加和删除问题: - 删除:每次滑动将窗口左侧元素删除。由于 `heapq` 没有提供删除中间特定元素相对应的方法。所以我们使用「延迟删除」的方式先把待删除的元素标记上,等到待删除的元素出现在堆顶时,再将其移除。我们使用 $removes$ (哈希表)来记录待删除元素个数。 - 将窗口左侧元素删除的操作为:`removes[nums[left]] += 1`。 - 添加:每次滑动在窗口右侧添加元素。需要根据上一步删除的结果来判断需要添加到哪一个堆上。我们用 $banlance$ 记录 $heap\_max$ 和 $heap\_min$ 元素个数的差值。 - 如果窗口左边界 $nums[left]$小于等于 $heap\_max$ 堆顶元素 ,则说明上一步删除的元素在 $heap\_min$ 上,则让 `banlance -= 1`。 - 如果窗口左边界 $nums[left]$ 大于 $heap\_max$ 堆顶元素,则说明上一步删除的元素在 $heap\_max$ 上,则上 `banlance += 1`。 - 如果窗口右边界 $nums[right]$ 小于等于 $heap\_max$ 堆顶元素,则说明待添加元素需要添加到 $heap\_min$ 上,则让 `banlance += 1`。 - 如果窗口右边界 $nums[right]$ 大于 $heap\_max$ 堆顶元素,则说明待添加元素需要添加到 $heap\_max$ 上,则让 `banlance -= 1`。 - 经过上述操作,$banlance$ 的取值为 $0$、$-2$、$2$ 中的一种。需要经过调整使得 $banlance == 0$。 - 如果 $banlance == 0$,已经平衡,不需要再做操作。 - 如果 $banlance == -2$,则说明 $heap\_min$ 比 $heap\_max$ 的元素多了两个。则从 $heap\_min$ 中取出堆顶元素添加到 $heap\_max$ 中。 - 如果 $banlance == 2$,则说明 $heap\_max$ 比 $heap\_min$ 的元素多了两个。则从 $heap\_max$ 中取出堆顶元素添加到 $heap\_min$ 中。 - 调整完之后,分别检查 $heap\_max$ 和 $heap\_min$ 的堆顶元素。 - 如果 $heap\_max$ 堆顶元素恰好为待删除元素,即 $removes[-heap\_max[0]] > 0$,则弹出 $heap\_max$ 堆顶元素。 - 如果 $heap\_min$ 堆顶元素恰好为待删除元素,即 $removes[heap\_min[0]] > 0$,则弹出 $heap\_min$ 堆顶元素。 - 最后取中位数放入答案数组中,然后继续滑动窗口。 ### 思路 1:代码 ```python import collections import heapq class Solution: def median(self, heap_max, heap_min, k): if k % 2 == 1: return -heap_max[0] else: return (-heap_max[0] + heap_min[0]) / 2 def medianSlidingWindow(self, nums: List[int], k: int) -> List[float]: heap_max, heap_min = [], [] removes = collections.Counter() for i in range(k): heapq.heappush(heap_max, -nums[i]) for i in range(k // 2): heapq.heappush(heap_min, -heapq.heappop(heap_max)) res = [self.median(heap_max, heap_min, k)] for i in range(k, len(nums)): banlance = 0 left, right = i - k, i removes[nums[left]] += 1 if heap_max and nums[left] <= -heap_max[0]: banlance -= 1 else: banlance += 1 if heap_max and nums[right] <= -heap_max[0]: heapq.heappush(heap_max, -nums[i]) banlance += 1 else: banlance -= 1 heapq.heappush(heap_min, nums[i]) if banlance == -2: heapq.heappush(heap_max, -heapq.heappop(heap_min)) if banlance == 2: heapq.heappush(heap_min, -heapq.heappop(heap_max)) while heap_max and removes[-heap_max[0]] > 0: removes[-heapq.heappop(heap_max)] -= 1 while heap_min and removes[heap_min[0]] > 0: removes[heapq.heappop(heap_min)] -= 1 res.append(self.median(heap_max, heap_min, k)) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(n)$。 ## 参考资料 - 【题解】[《风 险 对 冲》:双堆对顶,大堆小堆同时维护,44ms - 滑动窗口中位数 - 力扣](https://leetcode.cn/problems/sliding-window-median/solution/feng-xian-dui-chong-shuang-dui-dui-ding-hq1dt/) ================================================ FILE: docs/solutions/0400-0499/smallest-good-base.md ================================================ # [0483. 最小好进制](https://leetcode.cn/problems/smallest-good-base/) - 标签:数学、二分查找 - 难度:困难 ## 题目链接 - [0483. 最小好进制 - 力扣](https://leetcode.cn/problems/smallest-good-base/) ## 题目大意 **描述**: 以字符串的形式给出 $n$。 **要求**: 以字符串的形式返回 $n$ 的最小「好进制」。 **说明**: - 好进制:如果 $n$ 的 $k(k \ge 2)$ 进制数的所有数位全为 $1$,则称 $k(k \ge 2)$ 是 $n$ 的一个「好进制」。 - $n$ 的取值范围是 $[3, 10^{18}]$。 - $n$ 没有前导 $0$。 **示例**: - 示例 1: ```python 输入:n = "13" 输出:"3" 解释:13 的 3 进制是 111。 ``` - 示例 2: ```python 输入:n = "4681" 输出:"8" 解释:4681 的 8 进制是 11111。 ``` ## 解题思路 ### 思路 1:数学 + 二分查找 **核心思想**:如果 $n$ 的 $k$ 进制表示为全 $1$,那么 $n = k^0 + k^1 + k^2 + ... + k^m$,即 $n = \frac{k^{m+1} - 1}{k - 1}$。我们需要找到最小的 $k$ 使得该等式成立。 **算法步骤**: 1. **枚举进制表示的长度** $m$:取值范围为 $[1, \lfloor \log_2 n \rfloor]$(因为 $k \ge 2$,最多有 $\log_2 n$ 位)。 2. **二分查找进制** $k$:对于每个固定的长度 $m$,二分查找满足条件的进制 $k$。 - 左边界 $left = 2$(进制至少为 $2$)。 - 右边界 $right = n - 1$(最大进制值)。 - 在区间 $[left, right]$ 中二分查找 $k$。 3. **验证等式**:判断 $k$ 是否满足 $n = \frac{k^{m+1} - 1}{k - 1}$。 4. **返回最小 $k$**:找到满足条件的 $k$ 后返回。 **注意**:需要考虑边界情况,如 $n = 1$ 的特殊情况。 ### 思路 1:代码 ```python class Solution: def smallestGoodBase(self, n: str) -> str: n_num = int(n) # 枚举进制表示的长度 m(从大到小,找到的第一个即为最小的 k) max_len = len(bin(n_num)) - 2 # n 的二进制表示长度 for m in range(max_len, 0, -1): # 二分查找进制 k left, right = 2, n_num - 1 while left <= right: k = (left + right) // 2 # 计算 k^0 + k^1 + ... + k^m 的和 s = 0 current = 1 # k^0 for i in range(m + 1): s += current # 检查是否溢出 if s > n_num: break if i < m: current *= k # 提前检查是否会溢出 if current > n_num: s = n_num + 1 # 标记溢出 break if s == n_num: return str(k) elif s < n_num: left = k + 1 else: right = k - 1 # 如果没找到,返回 n - 1(此时 k = n - 1 一定满足条件) return str(n_num - 1) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log^3 n)$。其中 $n$ 是给定的数字。外层枚举长度 $m$ 的复杂度为 $O(\log n)$,内层二分查找的复杂度为 $O(\log n)$,每次验证计算等比数列和的复杂度为 $O(m) = O(\log n)$,因此总的时间复杂度为 $O(\log^3 n)$。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0400-0499/sort-characters-by-frequency.md ================================================ # [0451. 根据字符出现频率排序](https://leetcode.cn/problems/sort-characters-by-frequency/) - 标签:哈希表、字符串、桶排序、计数、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [0451. 根据字符出现频率排序 - 力扣](https://leetcode.cn/problems/sort-characters-by-frequency/) ## 题目大意 **描述**:给定一个字符串 `s`。 **要求**:将字符串 `s` 里的字符按照出现的频率降序排列。如果有多个答案,返回其中任何一个。 **说明**: - $1 \le s.length \le 5 * 10^5$。 - `s` 由大小写英文字母和数字组成。 **示例**: - 示例 1: ```python 输入: s = "tree" 输出: "eert" 解释: 'e'出现两次,'r'和't'都只出现一次。 因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。 ``` - 示例 2: ```python 输入: s = "cccaaa" 输出: "cccaaa" 解释: 'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。 注意"cacaca"是不正确的,因为相同的字母必须放在一起。 ``` ## 解题思路 ### 思路 1:优先队列 1. 使用哈希表 `s_dict` 统计字符频率。 2. 然后遍历哈希表 `s_dict`,将字符以及字符频数存入优先队列中。 3. 将优先队列中频数最高的元素依次加入答案数组中。 4. 最后拼接答案数组为字符串,将其返回。 ### 思路 1:代码 ```python import heapq class Solution: def frequencySort(self, s: str) -> str: # 统计元素频数 s_dict = dict() for ch in s: if ch in s_dict: s_dict[ch] += 1 else: s_dict[ch] = 1 priority_queue = [] for ch in s_dict: heapq.heappush(priority_queue, (-s_dict[ch], ch)) res = [] while priority_queue: ch = heapq.heappop(priority_queue)[-1] times = s_dict[ch] while times: res.append(ch) times -= 1 return ''.join(res) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + k \times log_2k)$。其中 $n$ 为字符串 $s$ 的长度,$k$ 是字符串中不同字符的个数。 - **空间复杂度**:$O(n + k)$。 ================================================ FILE: docs/solutions/0400-0499/split-array-largest-sum.md ================================================ # [0410. 分割数组的最大值](https://leetcode.cn/problems/split-array-largest-sum/) - 标签:贪心、数组、二分查找、动态规划、前缀和 - 难度:困难 ## 题目链接 - [0410. 分割数组的最大值 - 力扣](https://leetcode.cn/problems/split-array-largest-sum/) ## 题目大意 **描述**:给定一个非负整数数组 $nums$ 和一个整数 $k$,将数组分成 $k$ 个非空的连续子数组。 **要求**:使 $k$ 个子数组各自和的最大值最小,并求出子数组各自和的最大值。 **说明**: - $1 \le nums.length \le 1000$。 - $0 \le nums[i] \le 10^6$。 - $1 \le k \le min(50, nums.length)$。 **示例**: - 示例 1: ```python 输入:nums = [7,2,5,10,8], k = 2 输出:18 解释: 一共有四种方法将 nums 分割为 2 个子数组。 其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。 因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。 ``` - 示例 2: ```python 输入:nums = [1,2,3,4,5], k = 2 输出:9 ``` ## 解题思路 ### 思路 1:二分查找算法 先来理解清楚题意。题目的目的是使得 $m$ 个连续子数组各自和的最大值最小。意思是将数组按顺序分成 $m$ 个子数组,然后计算每个子数组的和,然后找出 $m$ 个和中的最大值,要求使这个最大值尽可能小。最后输出这个尽可能小的和最大值。 可以用二分查找来找这个子数组和的最大值,我们用 $ans$ 来表示这个值。$ans$ 最小为数组 $nums$ 所有元素的最大值,最大为数组 $nums$ 所有元素的和。即 $ans$ 范围是 $[max(nums), sum(nums)]$。 所以就确定了二分查找的两个指针位置。$left$ 指向 $max(nums)$,$right$ 指向 $sum(nums)$。然后取中间值 $mid$,计算当子数组和的最大值为 mid 时,所需要分割的子数组最少个数。 - 如果需要分割的子数组最少个数大于 $m$ 个,则说明子数组和的最大值取小了,不满足条件,应该继续调大,将 $left$ 右移,从右区间继续查找。 - 如果需要分割的子数组最少个数小于或等于 $m$ 个,则说明子数组和的最大值满足条件,并且还可以继续调小,将 $right$ 左移,从左区间继续查找,看是否有更小的数组和满足条件。 - 最终,返回符合条件的最小值即可。 ### 思路 1:代码 ```python class Solution: def splitArray(self, nums: List[int], m: int) -> int: def get_count(x): total = 0 count = 1 for num in nums: if total + num > x: count += 1 total = num else: total += num return count left = max(nums) right = sum(nums) while left < right: mid = left + (right - left) // 2 if get_count(mid) > m: left = mid + 1 else: right = mid return left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log (\sum nums))$,其中 $n$ 为数组中的元素个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0400-0499/string-compression.md ================================================ # [0443. 压缩字符串](https://leetcode.cn/problems/string-compression/) - 标签:双指针、字符串 - 难度:中等 ## 题目链接 - [0443. 压缩字符串 - 力扣](https://leetcode.cn/problems/string-compression/) ## 题目大意 **描述**:给定一个字符数组 $chars$。请使用下述算法压缩: 从一个空字符串 $s$ 开始。对于 $chars$ 中的每组连续重复字符: - 如果这一组长度为 $1$,则将字符追加到 $s$ 中。 - 如果这一组长度超过 $1$,则需要向 $s$ 追加字符,后跟这一组的长度。 压缩后得到的字符串 $s$ 不应该直接返回 ,需要转储到字符数组 $chars$ 中。需要注意的是,如果组长度为 $10$ 或 $10$ 以上,则在 $chars$ 数组中会被拆分为多个字符。 **要求**:在修改完输入数组后,返回该数组的新长度。 **说明**: - $1 \le chars.length \le 2000$。 - $chars[i]$ 可以是小写英文字母、大写英文字母、数字或符号。 - 必须设计并实现一个只使用常量额外空间的算法来解决此问题。 **示例**: - 示例 1: ```python 输入:chars = ["a","a","b","b","c","c","c"] 输出:返回 6 ,输入数组的前 6 个字符应该是:["a","2","b","2","c","3"] 解释:"aa" 被 "a2" 替代。"bb" 被 "b2" 替代。"ccc" 被 "c3" 替代。 ``` - 示例 2: ```python 输入:chars = ["a"] 输出:返回 1 ,输入数组的前 1 个字符应该是:["a"] 解释:唯一的组是“a”,它保持未压缩,因为它是一个字符。 ``` ## 解题思路 ### 思路 1:快慢指针 题目要求原地修改字符串数组。我们可以使用快慢指针来解决原地修改问题,具体解决方法如下: - 定义两个快慢指针 $slow$,$fast$。其中 $slow$ 指向压缩后的当前字符位置,$fast$ 指向压缩前的当前字符位置。 - 记录下当前待压缩字符的起始位置 $fast\_start = start$,然后过滤掉连续相同的字符。 - 将待压缩字符的起始位置的字符存入压缩后的当前字符位置,即 $chars[slow] = chars[fast\_start]$,并向右移动压缩后的当前字符位置,即 $slow += 1$。 - 判断一下待压缩字符的数目是否大于 $1$: - 如果数量为 $1$,则不用记录该数量。 - 如果数量大于 $1$(即 $fast - fast\_start > 0$),则我们需要将对应数量存入压缩后的当前字符位置。这时候还需要判断一下数量是否大于等于 $10$。 - 如果数量大于等于 $10$,则需要先将数字从个位到高位转为字符,存入压缩后的当前字符位置(此时数字为反,比如原数字是 $321$,则此时存入后为 $123$)。因为数字为反,所以我们需要将对应位置上的子字符串进行反转。 - 如果数量小于 $10$,则直接将数字存入压缩后的当前字符位置,无需取反。 - 判断完之后向右移动压缩前的当前字符位置 $fast$,然后继续压缩字符串,直到全部压缩完,则返回压缩后的当前字符位置 $slow$ 即为答案。 ### 思路 1:代码 ```python class Solution: def compress(self, chars: List[str]) -> int: def reverse(left, right): while left < right: chars[left], chars[right] = chars[right], chars[left] left += 1 right -= 1 slow, fast = 0, 0 while fast < len(chars): fast_start = fast while fast + 1 < len(chars) and chars[fast + 1] == chars[fast]: fast += 1 chars[slow] = chars[fast_start] slow += 1 if fast - fast_start > 0: cnt = fast - fast_start + 1 slow_start = slow while cnt != 0: chars[slow] = str(cnt % 10) slow += 1 cnt = cnt // 10 reverse(slow_start, slow - 1) fast += 1 return slow ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0400-0499/strong-password-checker.md ================================================ # [0420. 强密码检验器](https://leetcode.cn/problems/strong-password-checker/) - 标签:贪心、字符串、堆(优先队列) - 难度:困难 ## 题目链接 - [0420. 强密码检验器 - 力扣](https://leetcode.cn/problems/strong-password-checker/) ## 题目大意 **描述**: 满足以下条件的密码被认为是强密码: - 由至少 $6$ 个,至多 $20$ 个字符组成。 - 包含至少「一个小写字母」,至少「一个大写字母」,和至少「一个数字」。 - 不包含连续三个重复字符 (比如 `"Baaabb0"` 是弱密码, 但是 `"Baaba0"` 是强密码)。 给定一个字符串 $password$。 **要求**: 返回 将 $password$ 修改到满足强密码条件需要的最少修改步数。如果 $password$ 已经是强密码,则返回 $0$。 在一步修改操作中,你可以: - 插入一个字符到 $password$ , - 从 $password$ 中删除一个字符,或 - 用另一个字符来替换 $password$ 中的某个字符。 **说明**: - $1 \le password.length \le 50$。 - $password$ 由字母、数字、点 `'.'` 或者感叹号 `'!'` 组成。 **示例**: - 示例 1: ```python 输入:password = "a" 输出:5 ``` - 示例 2: ```python 输入:password = "aA1" 输出:3 ``` ## 解题思路 ### 思路 1:分类讨论 + 贪心策略 **核心思想**:根据密码长度 $n$ 的不同情况,采用不同的修改策略。 **算法步骤**: 1. **统计缺少的字符类型**: - 使用布尔变量 $has\_lower$、$has\_upper$、$has\_digit$ 记录是否存在小写字母、大写字母和数字 - 计算缺少的类型数量 $missing\_types = 3 - (has\_lower + has\_upper + has\_digit)$ 2. **统计连续字符问题**: - 使用 $replace\_count$ 记录需要替换的总次数(每 $len$ 长度的连续段需要 $len // 3$ 次替换) - 使用 $one\_mod$ 记录长度模 3 余数为 0 的段的个数(删除 1 个字符可以减少 1 次替换) - 使用 $two\_mod$ 记录长度模 3 余数为 1 的段的个数(删除 2 个字符可以减少 1 次替换) 3. **根据长度分类处理**: - **情况 1**:$n < 6$(长度过短)。 - 需要插入 $6 - n$ 个字符。 - 需要替换的连续字符数为 $replace\_count$。 - 需要添加的字符类型数为 $missing\_types$。 - 返回 $\max(6 - n, replace\_count, missing\_types)$(插入操作可以同时解决多个问题)。 - **情况 2**:$6 \le n \le 20$(长度合理)。 - 只需要替换字符即可满足条件。 - 返回 $\max(replace\_count, missing\_types)$。 - **情况 3**:$n > 20$(长度过长)。 - 需要删除 $deletions = n - 20$ 个字符。 - 贪心地利用删除操作减少替换次数: * 优先删除 $one\_mod$ 段的 1 个字符(每次可以减少 1 次替换)。 * 其次删除 $two\_mod$ 段的 2 个字符(每次可以减少 1 次替换)。 * 最后删除其他段每 3 个字符(每次可以减少 1 次替换)。 - 返回 $deletions + \max(replace\_count, missing\_types)$。 **注意**:删除字符可以同时减少替换次数,贪心策略能最大程度减少总操作次数。 ### 思路 1:代码 ```python class Solution: def strongPasswordChecker(self, password: str) -> int: n = len(password) # 统计缺少的字符类型 has_lower = False has_upper = False has_digit = False for char in password: if char.islower(): has_lower = True elif char.isupper(): has_upper = True elif char.isdigit(): has_digit = True # 缺少的字符类型数 missing_types = 3 - (has_lower + has_upper + has_digit) # 统计连续相同字符段的长度及需要替换的次数 replace_count = 0 # 需要替换的总次数 one_mod = 0 # length % 3 == 0 的段的个数(删除 1 个可以减少 1 次替换) two_mod = 0 # length % 3 == 1 的段的个数(删除 2 个可以减少 1 次替换) i = 0 while i < n: # 找出连续相同字符的结束位置 j = i while j < n and password[j] == password[i]: j += 1 length = j - i # 只处理长度 >= 3 的连续段 if length >= 3: replace_count += length // 3 if length % 3 == 0: one_mod += 1 elif length % 3 == 1: two_mod += 1 i = j # 根据长度分类处理 if n < 6: # 长度过短:需要插入字符 # 插入操作可以同时解决多个问题 return max(6 - n, missing_types, replace_count) elif n <= 20: # 长度合理:只需要替换 return max(replace_count, missing_types) else: # 长度过长:需要删除字符 delete_count = n - 20 # 贪心地用删除操作减少替换次数 # 优先删除 length % 3 == 0 的段的 1 个字符(删除 1 个可以减少 1 次替换) replace_count -= min(delete_count, one_mod) delete_count -= min(delete_count, one_mod) # 其次删除 length % 3 == 1 的段的 2 个字符(删除 2 个可以减少 1 次替换) replace_count -= min(delete_count // 2, two_mod) delete_count -= min(delete_count, 2 * two_mod) # 最后删除其他段的 3 个字符(删除 3 个可以减少 1 次替换) replace_count -= delete_count // 3 return (n - 20) + max(replace_count, missing_types) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是密码的长度。只需要遍历一次字符串进行统计,后续处理是常数时间。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0400-0499/sum-of-left-leaves.md ================================================ # [0404. 左叶子之和](https://leetcode.cn/problems/sum-of-left-leaves/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0404. 左叶子之和 - 力扣](https://leetcode.cn/problems/sum-of-left-leaves/) ## 题目大意 给定一个二叉树,计算所有左叶子之和。 ## 解题思路 深度优先搜索递归遍历二叉树,如果当前节点不为空,且左孩子节点不为空,且左孩子节点的左右孩子节点都为空,则该节点的左孩子节点为左叶子节点。将其值累加起来,即为答案。 ## 代码 ```python class Solution: def sumOfLeftLeaves(self, root: TreeNode) -> int: self.ans = 0 def dfs(node): if not node: return None if node.left and not node.left.left and not node.left.right: self.ans += node.left.val dfs(node.left) dfs(node.right) dfs(root) return self.ans ``` ================================================ FILE: docs/solutions/0400-0499/target-sum.md ================================================ # [0494. 目标和](https://leetcode.cn/problems/target-sum/) - 标签:数组、动态规划、回溯 - 难度:中等 ## 题目链接 - [0494. 目标和 - 力扣](https://leetcode.cn/problems/target-sum/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 和一个整数 $target$。数组长度不超过 $20$。向数组中每个整数前加 `+` 或 `-`。然后串联起来构造成一个表达式。 **要求**:返回通过上述方法构造的、运算结果等于 $target$ 的不同表达式数目。 **说明**: - $1 \le nums.length \le 20$。 - $0 \le nums[i] \le 1000$。 - $0 \le sum(nums[i]) \le 1000$。 - $-1000 \le target \le 1000$。 **示例**: - 示例 1: ```python 输入:nums = [1,1,1,1,1], target = 3 输出:5 解释:一共有 5 种方法让最终目标和为 3。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3 ``` - 示例 2: ```python 输入:nums = [1], target = 1 输出:1 ``` ## 解题思路 ### 思路 1:深度优先搜索(超时) 使用深度优先搜索对每位数字进行 `+` 或者 `-`,具体步骤如下: 1. 定义从位置 $0$、和为 $0$ 开始,到达数组尾部位置为止,和为 $target$ 的方案数为 `dfs(0, 0)`。 2. 下面从位置 $0$、和为 $0$ 开始,以深度优先搜索遍历每个位置。 3. 如果当前位置 $i$ 到达最后一个位置 $size$: 1. 如果和 $cur\_sum$ 等于目标和 $target$,则返回方案数 $1$。 2. 如果和 $cur\_sum$ 不等于目标和 $target$,则返回方案数 $0$。 4. 递归搜索 $i + 1$ 位置,和为 $cur\_sum - nums[i]$ 的方案数。 5. 递归搜索 $i + 1$ 位置,和为 $cur\_sum + nums[i]$ 的方案数。 6. 将 4 ~ 5 两个方案数加起来就是当前位置 $i$、和为 $cur\_sum$ 的方案数,返回该方案数。 7. 最终方案数为 `dfs(0, 0)`,将其作为答案返回即可。 ### 思路 1:代码 ```python class Solution: def findTargetSumWays(self, nums: List[int], target: int) -> int: size = len(nums) def dfs(i, cur_sum): if i == size: if cur_sum == target: return 1 else: return 0 ans = dfs(i + 1, cur_sum - nums[i]) + dfs(i + 1, cur_sum + nums[i]) return ans return dfs(0, 0) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n)$。其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(n)$。递归调用的栈空间深度不超过 $n$。 ### 思路 2:记忆化搜索 在思路 1 中我们单独使用深度优先搜索对每位数字进行 `+` 或者 `-` 的方法超时了。所以我们考虑使用记忆化搜索的方式,避免进行重复搜索。 这里我们使用哈希表 $$table$$ 记录遍历过的位置 $i$ 及所得到的的当前和 $cur\_sum$ 下的方案数,来避免重复搜索。具体步骤如下: 1. 定义从位置 $0$、和为 $0$ 开始,到达数组尾部位置为止,和为 $target$ 的方案数为 `dfs(0, 0)`。 2. 下面从位置 $0$、和为 $0$ 开始,以深度优先搜索遍历每个位置。 3. 如果当前位置 $i$ 遍历完所有位置: 1. 如果和 $cur\_sum$ 等于目标和 $target$,则返回方案数 $1$。 2. 如果和 $cur\_sum$ 不等于目标和 $target$,则返回方案数 $0$。 4. 如果当前位置 $i$、和为 $cur\_sum$ 之前记录过(即使用 $table$ 记录过对应方案数),则返回该方案数。 5. 如果当前位置 $i$、和为 $cur\_sum$ 之前没有记录过,则: 1. 递归搜索 $i + 1$ 位置,和为 $cur\_sum - nums[i]$ 的方案数。 2. 递归搜索 $i + 1$ 位置,和为 $cur\_sum + nums[i]$ 的方案数。 3. 将上述两个方案数加起来就是当前位置 $i$、和为 $cur\_sum$ 的方案数,将其记录到哈希表 $table$ 中,并返回该方案数。 6. 最终方案数为 `dfs(0, 0)`,将其作为答案返回即可。 ### 思路 2:代码 ```python class Solution: def findTargetSumWays(self, nums: List[int], target: int) -> int: size = len(nums) table = dict() def dfs(i, cur_sum): if i == size: if cur_sum == target: return 1 else: return 0 if (i, cur_sum) in table: return table[(i, cur_sum)] cnt = dfs(i + 1, cur_sum - nums[i]) + dfs(i + 1, cur_sum + nums[i]) table[(i, cur_sum)] = cnt return cnt return dfs(0, 0) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(2^n)$。其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(n)$。递归调用的栈空间深度不超过 $n$。 ### 思路 3:动态规划 假设数组中所有元素和为 $sum$,数组中所有符号为 `+` 的元素为 $sum\_x$,符号为 `-` 的元素和为 $sum\_y$。则 $target = sum\_x - sum\_y$。 而 $sum\_x + sum\_y = sum$。根据两个式子可以求出 $2 \times sum\_x = target + sum$,即 $sum\_x = (target + sum) / 2$。 那么这道题就变成了,如何在数组中找到一个集合,使集合中元素和为 $(target + sum) / 2$。这就变为了「0-1 背包问题」中求装满背包的方案数问题。 ###### 1. 定义状态 定义状态 $dp[i]$ 表示为:填满容量为 $i$ 的背包,有 $dp[i]$ 种方法。 ###### 2. 状态转移方程 填满容量为 $i$ 的背包的方法数来源于: 1. 不使用当前 $num$:只使用之前元素填满容量为 $i$ 的背包的方法数。 2. 使用当前 $num$:填满容量 $i - num$ 的包的方法数,再填入 $num$ 的方法数。 则动态规划的状态转移方程为:$dp[i] = dp[i] + dp[i - num]$。 ###### 3. 初始化 初始状态下,默认填满容量为 $0$ 的背包有 $1$ 种办法(什么也不装)。即 $dp[i] = 1$。 ###### 4. 最终结果 根据状态定义,最后输出 $dp[sise]$(即填满容量为 $size$ 的背包,有 $dp[size]$ 种方法)即可,其中 $size$ 为数组 $nums$ 的长度。 ### 思路 3:代码 ```python class Solution: def findTargetSumWays(self, nums: List[int], target: int) -> int: sum_nums = sum(nums) if abs(target) > abs(sum_nums) or (target + sum_nums) % 2 == 1: return 0 size = (target + sum_nums) // 2 dp = [0 for _ in range(size + 1)] dp[0] = 1 for num in nums: for i in range(size, num - 1, -1): dp[i] = dp[i] + dp[i - num] return dp[size] ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0400-0499/teemo-attacking.md ================================================ # [0495. 提莫攻击](https://leetcode.cn/problems/teemo-attacking/) - 标签:数组、模拟 - 难度:简单 ## 题目链接 - [0495. 提莫攻击 - 力扣](https://leetcode.cn/problems/teemo-attacking/) ## 题目大意 **描述**: 在《英雄联盟》的世界中,有一个叫「提莫」的英雄。他的攻击可以让敌方英雄艾希(编者注:寒冰射手)进入中毒状态。 当提莫攻击艾希,艾希的中毒状态正好持续 $duration$ 秒。 正式地讲,提莫在 $t$ 发起攻击意味着艾希在时间区间 $[t, t + duration - 1]$(含 $t$ 和 $t + duration - 1$)处于中毒状态。如果提莫在中毒影响结束「前」再次攻击,中毒状态计时器将会「重置」,在新的攻击之后,中毒影响将会在 $duration$ 秒后结束。 给定一个「非递减」的整数数组 $timeSeries$ ,其中 $timeSeries[i]$ 表示提莫在 $timeSeries[i]$ 秒时对艾希发起攻击,以及一个表示中毒持续时间的整数 $duration$。 **要求**: 返回艾希处于中毒状态的总秒数。 **说明**: - $1 \le timeSeries.length \le 10^{4}$。 - $0 \le timeSeries[i], duration \le 10^{7}$。 - $timeSeries$ 按非递减顺序排列。 **示例**: - 示例 1: ```python 输入:timeSeries = [1,4], duration = 2 输出:4 解释:提莫攻击对艾希的影响如下: - 第 1 秒,提莫攻击艾希并使其立即中毒。中毒状态会维持 2 秒,即第 1 秒和第 2 秒。 - 第 4 秒,提莫再次攻击艾希,艾希中毒状态又持续 2 秒,即第 4 秒和第 5 秒。 艾希在第 1、2、4、5 秒处于中毒状态,所以总中毒秒数是 4。 ``` - 示例 2: ```python 输入:timeSeries = [1,2], duration = 2 输出:3 解释:提莫攻击对艾希的影响如下: - 第 1 秒,提莫攻击艾希并使其立即中毒。中毒状态会维持 2 秒,即第 1 秒和第 2 秒。 - 第 2 秒,提莫再次攻击艾希,并重置中毒计时器,艾希中毒状态需要持续 2 秒,即第 2 秒和第 3 秒。 艾希在第 1、2、3 秒处于中毒状态,所以总中毒秒数是 3。 ``` ## 解题思路 ### 思路 1:贪心算法 **核心思想**:使用贪心算法,按顺序处理每次攻击,计算相邻两次攻击之间前一次攻击贡献的中毒秒数。 **算法步骤**: 1. 初始化总中毒秒数 $res = 0$。 2. 遍历攻击时刻数组,设相邻两次攻击的时间差为 $gap = timeSeries[i+1] - timeSeries[i]$。 3. 判断时间差 $gap$ 与持续时间 $duration$ 的关系: - 如果 $gap \ge duration$:说明前一次中毒已经完全结束,前一次攻击贡献 $duration$ 秒,将 $duration$ 累加到 $res$。 - 如果 $gap < duration$:说明前一次中毒还没结束就被重置了,只贡献了 $gap$ 秒,将 $gap$ 累加到 $res$。 4. 最后一次攻击总是完整贡献 $duration$ 秒,将其累加到 $res$。 5. 返回总中毒秒数 $res$。 **关键点**:通过比较相邻攻击的时间间隔和中毒持续时间,确定每次攻击实际贡献的中毒秒数。 ### 思路 1:代码 ```python class Solution: def findPoisonedDuration(self, timeSeries: List[int], duration: int) -> int: # 初始化总中毒秒数 res = 0 # 遍历攻击时刻数组 for i in range(len(timeSeries) - 1): # 计算相邻两次攻击的时间差 gap = timeSeries[i + 1] - timeSeries[i] if gap >= duration: # 前一次中毒完全结束,贡献 duration 秒 res += duration else: # 前一次中毒被重置,只贡献 gap 秒 res += gap # 最后一次攻击总是完整贡献 duration 秒 res += duration return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是数组 $timeSeries$ 的长度。需要遍历数组一次。 - **空间复杂度**:$O(1)$。只使用常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/ternary-expression-parser.md ================================================ # [0439. 三元表达式解析器](https://leetcode.cn/problems/ternary-expression-parser/) - 标签:栈、递归、字符串 - 难度:中等 ## 题目链接 - [0439. 三元表达式解析器 - 力扣](https://leetcode.cn/problems/ternary-expression-parser/) ## 题目大意 **描述**: 给定一个由数字、`T`、`F`、`'?'` 和 `':'` 组成的字符串 $expression$,表示一个三元表达式,其中 `T` 为真,`F` 为假。表达式中的所有数字都是一位 数(即在 $[0,9]$ 范围内)。 三元表达式的格式为:`条件 ? 值1 : 值2`,如果条件为真,返回值 1,否则返回值 2。表达式可以嵌套。 **要求**: 返回表达式的计算结果。 **说明**: - $5 \le expression.length \le 10^4$。 - $expression$ 由数字、`'T'`、`'F'`、`'?'` 和 `':'` 组成。 - 保证 $expression$ 是一个有效的三元表达式,且每个数字都是一位数。 **示例**: - 示例 1: ```python 输入:expression = "T?2:3" 输出:"2" 解释:条件为 T(真),返回 2。 ``` - 示例 2: ```python 输入:expression = "F?1:T?4:5" 输出:"4" 解释:条件为 F(假),计算 T?4:5,条件为 T(真),返回 4。 ``` ## 解题思路 ### 思路 1:栈 + 递归 三元表达式的格式为:`条件 ? 值1 : 值2`,可以嵌套。需要从右向左解析,因为右边的表达式可能是嵌套的三元表达式。 **解题步骤**: 1. 使用栈来处理嵌套的三元表达式。 2. 从右向左遍历字符串: - 遇到数字或字母,压入栈。 - 遇到 `'?'`,说明找到一个三元表达式的开始,此时栈顶应该是两个值(`值1` 和 `值2`)。 - 继续向左找到条件(`'T'` 或 `'F'`),根据条件选择对应的值压入栈。 3. 最后栈中只剩一个元素,即为结果。 **关键点**: - 从右向左遍历,遇到 `'?'` 时,栈顶两个元素分别是 `值1` 和 `值2`。 - 遇到 `':'` 时跳过。 - 条件字符在 `'?'` 的左边。 ### 思路 1:代码 ```python class Solution: def parseTernary(self, expression: str) -> str: stack = [] n = len(expression) # 从右向左遍历 i = n - 1 while i >= 0: char = expression[i] if char == '?': # 栈顶两个元素是 值1 和 值2 val1 = stack.pop() val2 = stack.pop() # 向左找条件 i -= 1 condition = expression[i] # 根据条件选择值 if condition == 'T': stack.append(val1) else: stack.append(val2) elif char != ':': # 数字或字母,压入栈 stack.append(char) i -= 1 return stack[0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是表达式的长度。每个字符最多被处理一次。 - **空间复杂度**:$O(n)$,栈的空间开销。 ================================================ FILE: docs/solutions/0400-0499/the-maze-iii.md ================================================ # [0499. 迷宫 III](https://leetcode.cn/problems/the-maze-iii/) - 标签:深度优先搜索、广度优先搜索、图、数组、字符串、矩阵、最短路、堆(优先队列) - 难度:困难 ## 题目链接 - [0499. 迷宫 III - 力扣](https://leetcode.cn/problems/the-maze-iii/) ## 题目大意 **描述**: 给定一个迷宫(二维数组)$maze$,其中 $0$ 表示空地,$1$ 表示墙壁。球可以向上、下、左、右四个方向滚动,但在碰到墙壁或洞前不会停止滚动。当球停下时,可以选择下一个方向。 迷宫中有一个洞 $hole$,球一旦滚到洞的位置就会掉进洞里。 给定球的起始位置 $ball$ 和洞的位置 $hole$。 **要求**: 返回球到达洞的最短路径的「字典序最小」的指令序列。指令用 `'u'`(上)、`'d'`(下)、`'l'`(左)、`'r'`(右)表示。如果球无法到达洞,返回 `"impossible"`。 **说明**: - $m == maze.length$。 - $n == maze[i].length$。 - $1 \le m, n \le 100$。 - $maze[i][j]$ 是 $0$ 或 $1$。 - $ball.length = 2$。 - $hole.length = 2$。 **示例**: - 示例 1: ```python 输入 1: 迷宫由以下二维数组表示 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 1 0 0 1 0 1 0 0 0 输入 2: 球的初始位置 (rowBall, colBall) = (4, 3) 输入 3: 洞的位置 (rowHole, colHole) = (0, 1) 输出: "lul" 解析: 有两条让球进洞的最短路径。 第一条路径是 左 -> 上 -> 左, 记为 "lul". 第二条路径是 上 -> 左, 记为 'ul'. 两条路径都具有最短距离6, 但'l' < 'u',故第一条路径字典序更小。因此输出"lul"。 ``` ![](https://assets.leetcode.com/uploads/2018/10/13/maze_2_example_1.png) - 示例 2: ```python 输入 1: 迷宫由以下二维数组表示 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 1 0 0 1 0 1 0 0 0 输入 2: 球的初始位置 (rowBall, colBall) = (4, 3) 输入 3: 洞的位置 (rowHole, colHole) = (3, 0) 输出: "impossible" 示例: 球无法到达洞。 ``` ![](https://assets.leetcode.com/uploads/2018/10/13/maze_2_example_2.png) ## 解题思路 ### 思路 1:Dijkstra + 优先队列 这是迷宫问题的升级版,不仅要找到终点,还要找到字典序最小的路径。 **核心思路**: - 球会沿着一个方向一直滚动,直到遇到墙壁、边界或洞。 - 使用 Dijkstra 算法找最短路径,同时记录路径字符串。 - 优先队列按照 (距离, 路径字符串) 排序,保证找到字典序最小的最短路径。 **解题步骤**: 1. 使用优先队列,存储 (距离, 路径, x, y)。 2. 四个方向分别对应字符 `'d'`(下)、`'l'`(左)、`'r'`(右)、`'u'`(上)。 3. 对于每个位置,尝试四个方向滚动: - 如果遇到洞,记录距离和路径。 - 否则滚动到停止位置,更新距离和路径。 4. 使用字典记录每个位置的最短距离和路径,避免重复访问。 5. 返回到达洞的最短路径,如果无法到达返回 `"impossible"`。 ### 思路 1:代码 ```python import heapq class Solution: def findShortestWay(self, maze: List[List[int]], ball: List[int], hole: List[int]) -> str: m, n = len(maze), len(maze[0]) # 方向:下、左、右、上(字典序) directions = [(1, 0, 'd'), (0, -1, 'l'), (0, 1, 'r'), (-1, 0, 'u')] # 优先队列:(距离, 路径, x, y) heap = [(0, '', ball[0], ball[1])] # 记录每个位置的最短距离和路径 visited = {(ball[0], ball[1]): (0, '')} while heap: dist, path, x, y = heapq.heappop(heap) # 如果到达洞 if [x, y] == hole: return path # 如果当前状态不是最优,跳过 if (x, y) in visited and (dist, path) > visited[(x, y)]: continue # 尝试四个方向 for dx, dy, direction in directions: nx, ny = x, y steps = 0 # 沿着当前方向滚动 while (0 <= nx + dx < m and 0 <= ny + dy < n and maze[nx + dx][ny + dy] == 0): nx += dx ny += dy steps += 1 # 如果遇到洞,停止 if [nx, ny] == hole: break new_dist = dist + steps new_path = path + direction # 如果找到更短的路径或字典序更小的路径 if ((nx, ny) not in visited or (new_dist, new_path) < visited[(nx, ny)]): visited[(nx, ny)] = (new_dist, new_path) heapq.heappush(heap, (new_dist, new_path, nx, ny)) return "impossible" ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n \times \max(m, n) \times \log(m \times n))$,每个位置最多访问一次,每次滚动需要 $O(\max(m, n))$,堆操作需要 $O(\log(m \times n))$。 - **空间复杂度**:$O(m \times n)$,存储访问状态和优先队列。 ================================================ FILE: docs/solutions/0400-0499/the-maze.md ================================================ # [0490. 迷宫](https://leetcode.cn/problems/the-maze/) - 标签:深度优先搜索、广度优先搜索、数组、矩阵 - 难度:中等 ## 题目链接 - [0490. 迷宫 - 力扣](https://leetcode.cn/problems/the-maze/) ## 题目大意 **描述**: 给定一个迷宫(二维数组)$maze$,其中 $0$ 表示空地,$1$ 表示墙壁。球可以向上、下、左、右四个方向滚动,但在碰到墙壁前不会停止滚动。当球停下时,可以选择下一个方向。 给定球的起始位置 $start$ 和目的地 $destination$。 **要求**: 判断球能否在目的地停下。 **说明**: - $m == maze.length$。 - $n == maze[i].length$。 - $1 \le m, n \le 100$。 - $maze[i][j]$ 是 $0$ 或 $1$。 - $start.length = 2$。 - $destination.length = 2$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/31/maze1-1-grid.jpg) ```python 输入:maze = [[0,0,1,0,0],[0,0,0,0,0],[0,0,0,1,0],[1,1,0,1,1],[0,0,0,0,0]], start = [0,4], destination = [4,4] 输出:true 解释:一种可能的路径是 : 左 -> 下 -> 左 -> 下 -> 右 -> 下 -> 右。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/31/maze1-2-grid.jpg) ```python 输入:maze = [[0,0,1,0,0],[0,0,0,0,0],[0,0,0,1,0],[1,1,0,1,1],[0,0,0,0,0]], start = [0,4], destination = [3,2] 输出:false 解释:不存在能够使球停在目的地的路径。注意,球可以经过目的地,但无法在那里停驻。 ``` ## 解题思路 ### 思路 1:BFS + 模拟 球在迷宫中滚动,遇到墙壁或边界才会停下。需要判断球能否从起点滚到终点。 **核心思路**: - 球会沿着一个方向一直滚动,直到遇到墙壁或边界。 - 使用 BFS 搜索所有可能的停止位置。 - 每次从一个位置出发,尝试四个方向,滚动到停止位置。 **解题步骤**: 1. 使用 BFS,初始位置为 $start$。 2. 对于每个位置,尝试四个方向(上、下、左、右)。 3. 沿着每个方向一直滚动,直到遇到墙壁或边界,记录停止位置。 4. 如果停止位置是终点,返回 `True`。 5. 如果停止位置未访问过,加入队列继续搜索。 6. 使用 $visited$ 集合避免重复访问。 ### 思路 1:代码 ```python from collections import deque class Solution: def hasPath(self, maze: List[List[int]], start: List[int], destination: List[int]) -> bool: m, n = len(maze), len(maze[0]) directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] # 右、左、下、上 queue = deque([tuple(start)]) visited = {tuple(start)} while queue: x, y = queue.popleft() # 如果到达终点 if [x, y] == destination: return True # 尝试四个方向 for dx, dy in directions: nx, ny = x, y # 沿着当前方向一直滚动 while 0 <= nx + dx < m and 0 <= ny + dy < n and maze[nx + dx][ny + dy] == 0: nx += dx ny += dy # 如果停止位置未访问过 if (nx, ny) not in visited: visited.add((nx, ny)) queue.append((nx, ny)) return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n \times \max(m, n))$,其中 $m$ 和 $n$ 是迷宫的行数和列数。每个位置最多访问一次,每次滚动最多需要 $O(\max(m, n))$ 时间。 - **空间复杂度**:$O(m \times n)$,$visited$ 集合和队列的空间开销。 ================================================ FILE: docs/solutions/0400-0499/third-maximum-number.md ================================================ # [0414. 第三大的数](https://leetcode.cn/problems/third-maximum-number/) - 标签:数组、排序 - 难度:简单 ## 题目链接 - [0414. 第三大的数 - 力扣](https://leetcode.cn/problems/third-maximum-number/) ## 题目大意 **描述**: 给定一个非空数组,返回此数组中「第三大的数」。 **要求**: 如果不存在,则返回数组中最大的数。 **说明**: - $1 \le nums.length \le 10^{4}$。 - $-2^{31} \le nums[i] \le 2^{31} - 1$。 - 进阶:你能设计一个时间复杂度 $O(n)$ 的解决方案吗? **示例**: - 示例 1: ```python 输入:[3, 2, 1] 输出:1 解释:第三大的数是 1 。 ``` - 示例 2: ```python 输入:[1, 2] 输出:2 解释:第三大的数不存在, 所以返回最大的数 2 。 ``` ## 解题思路 ### 思路 1:一次遍历维护前三大的数 **核心思想**:遍历一次数组,使用三个变量 $first$、$second$、$third$ 分别维护第一大、第二大和第三大的数。 **算法步骤**: 1. 初始化 $first = second = third = -\infty$(表示三个变量都未设置)。 2. 遍历数组中的每个数 $num$: - 如果 $num > first$:更新 $third = second$、$second = first$、$first = num$。 - 如果 $num < first$ 且 $num > second$:更新 $third = second$、$second = num$。 - 如果 $num < second$ 且 $num > third$:更新 $third = num$。 - 其他情况忽略(重复数字)。 3. 遍历结束后,如果 $third$ 仍然为 $-\infty$,说明不存在第三大的数,返回 $first$;否则返回 $third$。 **关键点**:通过一次遍历实时维护前三大元素,避免排序带来的 $O(n \log n)$ 时间复杂度。 ### 思路 1:代码 ```python class Solution: def thirdMax(self, nums: List[int]) -> int: # 初始化三个变量,使用 None 表示未设置 first = second = third = None # 遍历数组 for num in nums: # 更新 first if first is None or num > first: third = second second = first first = num # 更新 second(避免重复) elif num != first and (second is None or num > second): third = second second = num # 更新 third(避免重复) elif num != first and num != second and (third is None or num > third): third = num # 如果第三大的数不存在,返回最大的数 if third is None: return first else: return third ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是数组 $nums$ 的长度。只需要遍历一次数组。 - **空间复杂度**:$O(1)$。只使用了三个变量存储状态。 ================================================ FILE: docs/solutions/0400-0499/total-hamming-distance.md ================================================ # [0477. 汉明距离总和](https://leetcode.cn/problems/total-hamming-distance/) - 标签:位运算、数组、数学 - 难度:中等 ## 题目链接 - [0477. 汉明距离总和 - 力扣](https://leetcode.cn/problems/total-hamming-distance/) ## 题目大意 **描述**: 两个整数的「汉明距离」指的是这两个数字的二进制数对应位不同的数量。 给你一个整数数组 $nums$。 **要求**: 请你计算并返回 $nums$ 中任意两个数之间「汉明距离的总和」。 **说明**: - $1 \le nums.length \le 10^{4}$。 - $0 \le nums[i] \le 10^{9}$。 - 给定输入的对应答案符合 32-bit 整数范围。 **示例**: - 示例 1: ```python 输入:nums = [4,14,2] 输出:6 解释:在二进制表示中,4 表示为 0100 ,14 表示为 1110 ,2表示为 0010 。(这样表示是为了体现后四位之间关系) 所以答案为: HammingDistance(4, 14) + HammingDistance(4, 2) + HammingDistance(14, 2) = 2 + 2 + 2 = 6 ``` - 示例 2: ```python 输入:nums = [4,14,4] 输出:4 ``` ## 解题思路 ### 思路 1:按位统计 **核心思想**:对于每一位(bit),统计该位上 $1$ 的个数 $count_1$ 和 $0$ 的个数 $count_0$,则该位对总和的贡献为 $count_1 \times count_0$(即配对 $1$ 和 $0$ 的数量)。 **算法步骤**: 1. 初始化总和 $res = 0$。 2. 遍历 32 位整数中的每一位(从第 $0$ 位到第 $31$ 位): - 统计在当前位 $i$ 上,数组中 $1$ 的个数 $count_1$。 - 计算 $0$ 的个数 $count_0 = len(nums) - count_1$。 - 当前位的贡献为 $count_1 \times count_0$,累加到 $res$。 3. 返回总汉明距离 $res$。 **关键点**:将问题转换为按位统计,每一对数字的汉明距离等于它们在每一位上不同的位数的总和。对于每一位,不同位的数量等于该位上 $1$ 的个数乘以 $0$ 的个数。 ### 思路 1:代码 ```python class Solution: def totalHammingDistance(self, nums: List[int]) -> int: # 初始化总汉明距离 res = 0 # 遍历 32 位整数的每一位 for i in range(32): # 统计当前位上 1 的个数 count_1 = 0 for num in nums: # 获取当前位的值(0 或 1) if (num >> i) & 1: count_1 += 1 # 计算当前位上 0 的个数 count_0 = len(nums) - count_1 # 当前位的贡献:配对 1 和 0 的数量 res += count_1 * count_0 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 32)$。其中 $n$ 是数组 $nums$ 的长度。需要遍历 32 位,每一位都需要遍历整个数组统计。 - **空间复杂度**:$O(1)$。只使用常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/trapping-rain-water-ii.md ================================================ # [0407. 接雨水 II](https://leetcode.cn/problems/trapping-rain-water-ii/) - 标签:广度优先搜索、数组、矩阵、堆(优先队列) - 难度:困难 ## 题目链接 - [0407. 接雨水 II - 力扣](https://leetcode.cn/problems/trapping-rain-water-ii/) ## 题目大意 **描述**: 给定一个 $m \times n$ 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度。 **要求**: 请计算图中形状最多能接多少体积的雨水。 **说明**: - $m == heightMap.length$。 - $n == heightMap[i].length$。 - $1 \le m, n \le 200$。 - $0 \le heightMap[i][j] \le 2 \times 10^{4}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/08/trap1-3d.jpg) ```python 输入: heightMap = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]] 输出: 4 解释: 下雨后,雨水将会被上图蓝色的方块中。总的接雨水量为 1+2+1=4。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/04/08/trap2-3d.jpg) ```python 输入: heightMap = [[3,3,3,3,3],[3,2,2,2,3],[3,2,1,2,3],[3,2,2,2,3],[3,3,3,3,3]] 输出: 10 ``` ## 解题思路 ### 思路 1:优先队列(最小堆)+ BFS **核心思想**:使用优先队列维护当前所有边界单元格的高度,从最矮的边界开始向内扩展,确保每个单元格都能接尽可能多的雨水。 **算法步骤**: 1. 初始化优先队列 $min\_heap$,将所有边界单元格 $(i, j, heightMap[i][j])$ 加入队列,并用 $visited$ 数组标记。 2. 初始化答案 $res = 0$。 3. 当队列不为空时: - 取出最小高度的单元格 $(i, j, height)$。 - 检查四个方向的邻居单元格 $(x, y)$: - 如果 $(x, y)$ 未被访问且合法,设其高度为 $h = heightMap[x][y]$。 - 如果 $h < height$,则可以接雨水 $height - h$,累加到 $res$。 - 更新 $heightMap[x][y] = \max(height, h)$,并将 $(x, y, heightMap[x][y])$ 加入队列。 - 标记 $(x, y)$ 为已访问。 4. 返回总接雨水量 $res$。 **关键点**:从最矮的边界开始处理,确保每个单元格的高度不低于其已处理邻居的最高边界,从而确定该单元格能接多少雨水。 ### 思路 1:代码 ```python import heapq from typing import List class Solution: def trapRainWater(self, heightMap: List[List[int]]) -> int: # 获取矩阵的行数和列数 m, n = len(heightMap), len(heightMap[0]) # 初始化结果 res = 0 # 初始化访问标记数组 visited = [[False] * n for _ in range(m)] # 初始化优先队列(最小堆) min_heap = [] # 将边界单元格加入队列 for i in range(m): for j in range(n): # 如果是边界单元格 if i == 0 or i == m - 1 or j == 0 or j == n - 1: # 将 (高度, 行坐标, 列坐标) 加入队列 heapq.heappush(min_heap, (heightMap[i][j], i, j)) visited[i][j] = True # 定义四个方向 directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] # BFS 遍历 while min_heap: # 取出最小高度的单元格 height, i, j = heapq.heappop(min_heap) # 遍历四个方向 for dx, dy in directions: x, y = i + dx, j + dy # 检查邻居是否合法且未访问 if 0 <= x < m and 0 <= y < n and not visited[x][y]: # 如果邻居高度小于当前边界高度,可以接雨水 if heightMap[x][y] < height: res += height - heightMap[x][y] # 更新邻居高度为当前边界高度 heightMap[x][y] = height # 将邻居加入队列 heapq.heappush(min_heap, (heightMap[x][y], x, y)) visited[x][y] = True return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n \times \log(m + n))$。其中 $m$ 和 $n$ 分别是矩阵的行数和列数。需要遍历所有单元格,每次堆操作的时间复杂度为 $O(\log(m + n))$。 - **空间复杂度**:$O(m \times n)$。需要 $visited$ 数组和优先队列的空间,最坏情况下队列大小为 $O(m + n)$。 ================================================ FILE: docs/solutions/0400-0499/unique-substrings-in-wraparound-string.md ================================================ # [0467. 环绕字符串中唯一的子字符串](https://leetcode.cn/problems/unique-substrings-in-wraparound-string/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [0467. 环绕字符串中唯一的子字符串 - 力扣](https://leetcode.cn/problems/unique-substrings-in-wraparound-string/) ## 题目大意 把字符串 `s` 看作是 `abcdefghijklmnopqrstuvwxyz` 的无限环绕字符串,所以 `s` 看起来是这样的:`...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd....`。 给定一个字符串 `p`。 要求:你需要的是找出 `s` 中有多少个唯一的 `p` 的非空子串,尤其是当你的输入是字符串 `p` ,你需要输出字符串 `s` 中 `p` 的不同的非空子串的数目。 注意: `p` 仅由小写的英文字母组成,`p` 的大小可能超过 `10000`。 ## 解题思路 字符串 `s` 是个 `a` ~ `z` 无限循环的字符串,题目要求计算字符串 `s` 和字符串 `p` 中有多少个相等的非空子串。发现以该字符结尾的连续子串的长度,就等于以该字符结尾的相等子串的个数。所以我们可以按以下步骤求解: - 记录以每个字符结尾的字符串最长长度。 - 将其累加起来就是最终答案。 ## 代码 ```python class Solution: def findSubstringInWraproundString(self, p: str) -> int: dp = collections.defaultdict(int) dp[p[0]] = 1 max_len = 1 for i in range(1, len(p)): if (ord(p[i]) - ord(p[i - 1])) % 26 == 1: max_len += 1 else: max_len = 1 dp[p[i]] = max(dp[p[i]], max_len) ans = 0 for key, value in dp.items(): ans += value return ans ``` ================================================ FILE: docs/solutions/0400-0499/valid-word-abbreviation.md ================================================ # [0408. 有效单词缩写](https://leetcode.cn/problems/valid-word-abbreviation/) - 标签:双指针、字符串 - 难度:简单 ## 题目链接 - [0408. 有效单词缩写 - 力扣](https://leetcode.cn/problems/valid-word-abbreviation/) ## 题目大意 **描述**: 给定一个非空字符串 $word$ 和一个缩写 $abbr$,判断缩写是否是该单词的有效缩写。 字符串的缩写规则:用数字代替连续的字符。 例如,单词 `"substitution"` 可以缩写为: - `"s10n"` (`"s ubstitutio n"`) - `"sub4u4"` (`"sub stit u tion"`) - `"12"` (`"substitution"`) - `"su3i1u2on"` (`"su bst i t u ti on"`) - `"substitution"` (没有替换子字符串) 下列是不合法的缩写: - `"s55n"` (`"s ubsti tutio n"`,两处缩写相邻) - `"s010n"` (缩写存在前导零) - `"s0ubstitution"` (缩写是一个空字符串) **要求**: 判断 $abbr$ 是否是 $word$ 的有效缩写。 **说明**: - $1 \le word.length \le 20$。 - $1 \le abbr.length \le 10$。 - $word$ 只包含小写英文字母。 - $abbr$ 只包含小写英文字母和数字。 - 数字不能有前导零。 **示例**: - 示例 1: ```python 输入:word = "internationalization", abbr = "i12iz4n" 输出:true 解释:单词 "internationalization" 可以缩写为 "i12iz4n" ("i nternational iz atio n")。 ``` - 示例 2: ```python 输入:word = "apple", abbr = "a2e" 输出:false 解释:单词 "apple" 无法缩写为 "a2e"。 ``` ## 解题思路 ### 思路 1:双指针 判断缩写 $abbr$ 是否是单词 $word$ 的有效缩写。缩写规则是用数字代替连续的字符。 **解题步骤**: 1. 使用两个指针 $i$ 和 $j$ 分别指向 $word$ 和 $abbr$。 2. 遍历 $abbr$: - 如果当前字符是数字,解析完整的数字(可能是多位数),然后将 $i$ 向后移动相应位数。 - 注意:数字不能有前导零(如 "01" 是非法的)。 - 如果当前字符是字母,检查是否与 $word[i]$ 相同,相同则 $i$ 和 $j$ 都向后移动。 3. 最后检查两个指针是否都到达末尾。 **边界情况**: - 数字有前导零:返回 `False`。 - 数字导致 $i$ 超出 $word$ 长度:返回 `False`。 ### 思路 1:代码 ```python class Solution: def validWordAbbreviation(self, word: str, abbr: str) -> bool: i, j = 0, 0 # i 指向 word,j 指向 abbr while i < len(word) and j < len(abbr): if abbr[j].isdigit(): # 检查前导零 if abbr[j] == '0': return False # 解析完整的数字 num = 0 while j < len(abbr) and abbr[j].isdigit(): num = num * 10 + int(abbr[j]) j += 1 # 移动 i 指针 i += num else: # 字母必须匹配 if word[i] != abbr[j]: return False i += 1 j += 1 # 两个指针都应该到达末尾 return i == len(word) and j == len(abbr) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n)$,其中 $m$ 和 $n$ 分别是 $word$ 和 $abbr$ 的长度。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/valid-word-square.md ================================================ # [0422. 有效的单词方块](https://leetcode.cn/problems/valid-word-square/) - 标签:数组、矩阵 - 难度:简单 ## 题目链接 - [0422. 有效的单词方块 - 力扣](https://leetcode.cn/problems/valid-word-square/) ## 题目大意 **描述**: 给定一个单词序列 $words$,判断它是否构成一个有效的单词方块。 有效的单词方块需要满足:第 $i$ 行第 $j$ 列的字符等于第 $j$ 行第 $i$ 列的字符,即 $words[i][j] = words[j][i]$。 **要求**: 如果是有效的单词方块,返回 $true$;否则返回 $false$。 **说明**: - $1 \le words.length \le 500$。 - $1 \le words[i].length \le 500$。 - $words[i]$ 仅由小写英文字母组成。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/09/validsq1-grid.jpg) ```python 输入: words = ["abcd","bnrt","crmy","dtye"] 输出: true 解释: 第 1 行和第 1 列都读作 "abcd"。 第 2 行和第 2 列都读作 "bnrt"。 第 3 行和第 3 列都读作 "crmy"。 第 4 行和第 4 列都读作 "dtye"。 因此,它构成了一个有效的单词方块。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/04/09/validsq2-grid.jpg) ```python 输入: words = ["abcd","bnrt","crm","dt"] 输出: true 解释: 第 1 行和第 1 列都读作 "abcd"。 第 2 行和第 2 列都读作 "bnrt"。 第 3 行和第 3 列都读作 "crm"。 第 4 行和第 4 列都读作 "dt"。 因此,它构成了一个有效的单词方块。 ``` ## 解题思路 ### 思路 1:矩阵转置验证 有效的单词方块需要满足:第 $i$ 行的第 $j$ 个字符等于第 $j$ 行的第 $i$ 个字符,即 $words[i][j] = words[j][i]$。 **解题步骤**: 1. 遍历每一行 $i$ 和每一列 $j$。 2. 检查 $words[i][j]$ 是否等于 $words[j][i]$。 3. 需要注意边界情况: - 行的长度可能不同。 - 访问 $words[j][i]$ 时,需要确保 $j < len(words)$ 且 $i < len(words[j])$。 - 访问 $words[i][j]$ 时,需要确保 $i < len(words)$ 且 $j < len(words[i])$。 **关键点**: - 如果 $words[i]$ 的长度大于 $words$ 的行数,说明存在列索引超出行数,不是有效的单词方块。 - 对称性检查:$words[i][j]$ 必须等于 $words[j][i]$。 ### 思路 1:代码 ```python class Solution: def validWordSquare(self, words: List[str]) -> bool: n = len(words) for i in range(n): for j in range(len(words[i])): # 检查对称位置是否存在且相等 # words[i][j] 应该等于 words[j][i] # 如果 j >= n,说明列索引超出行数 if j >= n: return False # 如果 words[j] 的长度不够,或者字符不匹配 if i >= len(words[j]) or words[i][j] != words[j][i]: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是行数,$m$ 是平均每行的字符数。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0400-0499/validate-ip-address.md ================================================ # [0468. 验证IP地址](https://leetcode.cn/problems/validate-ip-address/) - 标签:字符串 - 难度:中等 ## 题目链接 - [0468. 验证IP地址 - 力扣](https://leetcode.cn/problems/validate-ip-address/) ## 题目大意 **描述**:给定一个字符串 `queryIP`。 **要求**:如果是有效的 IPv4 地址,返回 `"IPv4"`;如果是有效的 IPv6 地址,返回 `"IPv6"`;如果不是上述类型的 IP 地址,返回 `"Neither"`。 **说明**: - **有效的 IPv4 地址**:格式为 `"x1.x2.x3.x4"` 形式的 IP 地址。 其中: - $0 \le xi \le 255$。 - $xi$ 不能包含前导零。 - 例如: `"192.168.1.1"` 、 `"192.168.1.0"` 为有效 IPv4 地址,`"192.168.01.1"` 为无效 IPv4 地址,`"192.168.1.00"` 、 `"192.168@1.1"` 为无效 IPv4 地址。 - **有效的 IPv6 地址**: 格式为`"x1:x2:x3:x4:x5:x6:x7:x8"` 的 IP 地址,其中: - $1 \le xi.length \le 4$。 - $xi$ 是一个十六进制字符串,可以包含数字、小写英文字母(`'a'` 到 `'f'`)和大写英文字母(`'A'` 到 `'F'`)。 - 在 $xi$ 中允许前导零。 - 例如:`"2001:0db8:85a3:0000:0000:8a2e:0370:7334"` 和 `"2001:db8:85a3:0:0:8A2E:0370:7334"` 是有效的 IPv6 地址,而 `"2001:0db8:85a3::8A2E:037j:7334"` 和 `"02001:0db8:85a3:0000:0000:8a2e:0370:7334"` 是无效的 IPv6 地址。 - `queryIP` 仅由英文字母,数字,字符 `'.'` 和 `':'` 组成。 **示例**: - 示例 1: ```python 输入:queryIP = "172.16.254.1" 输出:"IPv4" 解释:有效的 IPv4 地址,返回 "IPv4" ``` - 示例 2: ```python 输入:queryIP = "2001:0db8:85a3:0:0:8A2E:0370:7334" 输出:"IPv6" 解释:有效的 IPv6 地址,返回 "IPv6" ``` ## 解题思路 ### 思路 1:模拟 根据题意以及有效的 IPV4 地址规则、有效的 IPv6 地址规则,我们可以分两步来做:第一步,验证是否为有效的 IPV4 地址。第二步,验证是否为有效的 IPv6 地址。 #### 1. 验证是否为有效的 IPv4 地址 1. 将字符串按照 `'.'` 进行分割,将不同分段存入数组 `path` 中。 2. 如果分段数组 `path` 长度等于 $4$,则说明该字符串为 IPv4 地址,接下里验证是否为有效的 IPv4 地址。 3. 遍历分段数组 `path`,去验证每个分段 `sub`。 1. 如果当前分段 `sub` 为空,或者不是纯数字,则返回 `"Neither"`。 2. 如果当前分段 `sub` 有前导 $0$,并且长度不为 $1$,则返回 `"Neither"`。 3. 如果当前分段 `sub` 对应的值不在 $0 \sim 255$ 范围内,则返回 `"Neither"`。 4. 遍历完分段数组 `path`,扔未发现问题,则该字符串为有效的 IPv4 地址,返回 `IPv4`。 #### 2. 验证是否为有效的 IPv6 地址 1. 将字符串按照 `':'` 进行分割,将不同分段存入数组 `path` 中。 2. 如果分段数组 `path` 长度等于 $8$,则说明该字符串为 IPv6 地址,接下里验证是否为有效的 IPv6 地址。 3. 定义一个代表十六进制不同字符的字符串 `valid = "0123456789abcdefABCDEF"`,用于验证分段的每一位是否为 $16$ 进制数。 4. 遍历分段数组 `path`,去验证每个分段 `sub`。 1. 如果当前分段 `sub` 为空,则返回 `"Neither"`。 2. 如果当前分段 `sub` 长度超过 $4$,则返回 `"Neither"`。 3. 如果当前分段 `sub` 对应的每一位的值不在 `valid` 内,则返回 `"Neither"`。 5. 遍历完分段数组 `path`,扔未发现问题,则该字符串为有效的 IPv6 地址,返回 `IPv6`。 如果通过上面两步验证,该字符串既不是有效的 IPv4 地址,也不是有效的 IPv6 地址,则返回 `"Neither"`。 ### 思路 1:代码 ```python class Solution: def validIPAddress(self, queryIP: str) -> str: path = queryIP.split('.') if len(path) == 4: for sub in path: if not sub or not sub.isdecimal(): return "Neither" if sub[0] == '0' and len(sub) != 1: return "Neither" if int(sub) > 255: return "Neither" return "IPv4" path = queryIP.split(':') if len(path) == 8: valid = "0123456789abcdefABCDEF" for sub in path: if not sub: return "Neither" if len(sub) > 4: return "Neither" for digit in sub: if digit not in valid: return "Neither" return "IPv6" return "Neither" ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串 `queryIP` 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0400-0499/word-squares.md ================================================ # [0425. 单词方块](https://leetcode.cn/problems/word-squares/) - 标签:字典树、数组、字符串、回溯 - 难度:困难 ## 题目链接 - [0425. 单词方块 - 力扣](https://leetcode.cn/problems/word-squares/) ## 题目大意 给定一个单词集合 `words`(没有重复)。 要求:找出其中所有的单词方块 。 - 单词方块:指从第 `k` 行和第 `k` 列 `(0 ≤ k < max(行数, 列数))` 来看都是相同的字符串。 例如,单词序列 ["ball","area","lead","lady"] 形成了一个单词方块,因为每个单词从水平方向看和从竖直方向看都是相同的。 ``` b a l l a r e a l e a d l a d y ``` ## 解题思路 根据单词方块的第一个单词,可以推出下一个单词的前缀。 比如第一个单词是 `ball`,那么单词方块的长度是 `4 * 4`,则下一个单词(第二个单词)的前缀为 `a`。这样我们就又找到了一个以 `a` 为前缀且长度为 `4` 的单词,即 `area`,此时就变成了 `[ball, area]`。 那么下一个单词(第三个单词)的前缀为 `le`。这样我们就又找到了一个以 `le` 为前缀且长度为 `4` 的单词,即 `lead`。此时就变成了 `[ball, area, lead]`。 以此类推,就可以得到整个单词方块。 并且我们可以使用字典树(前缀树)来存储单词,并且通过回溯得到所有的解。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str): """ Returns if the word is in the trie. """ cur = self res = [] for ch in word: if ch not in cur.children: return res cur = cur.children[ch] cur.dfs(word, res) return res def dfs(self, word, res): cur = self if cur and cur.isEnd: res.append(word) return for ch in cur.children: node = cur.children[ch] node.dfs(word + ch, res) class Solution: def backtrace(self, index, size, path, res, trie_tree): if index == size: res.append(path[:]) return next_prefix = "" # 下一行的前缀 for i in range(index): next_prefix += path[i][index] next_words_with_prefix = trie_tree.search(next_prefix) for word in next_words_with_prefix: path.append(word) self.backtrace(index + 1, size, path, res, trie_tree) path.pop(-1) def wordSquares(self, words: List[str]) -> List[List[str]]: trie_tree = Trie() for word in words: trie_tree.insert(word) size = len(words[0]) res = [] path = [] for word in words: path.append(word) self.backtrace(1, size, path, res, trie_tree) path.pop(-1) return res ``` ================================================ FILE: docs/solutions/0400-0499/zuma-game.md ================================================ # [0488. 祖玛游戏](https://leetcode.cn/problems/zuma-game/) - 标签:栈、广度优先搜索、记忆化搜索、字符串、动态规划 - 难度:困难 ## 题目链接 - [0488. 祖玛游戏 - 力扣](https://leetcode.cn/problems/zuma-game/) ## 题目大意 **描述**: 你正在参与祖玛游戏的一个变种。 在这个祖玛游戏变体中,桌面上有一排彩球,每个球的颜色可能是:红色 `'R'`、黄色 `'Y'`、蓝色 `'B'`、绿色 `'G'` 或白色 `'W'`。你的手中也有一些彩球。 你的目标是「清空」桌面上所有的球。每一回合: - 从你手上的彩球中选出「任意一颗」,然后将其插入桌面上那一排球中:两球之间或这一排球的任一端。 - 接着,如果有出现「三个或者三个以上」且 颜色相同 的球相连的话,就把它们移除掉。 - 如果这种移除操作同样导致出现三个或者三个以上且颜色相同的球相连,则可以继续移除这些球,直到不再满足移除条件。 - 如果桌面上所有球都被移除,则认为你赢得本场游戏。 - 重复这个过程,直到你赢了游戏或者手中没有更多的球。 给定一个字符串 $board$,表示桌面上最开始的那排球。另给你一个字符串 $hand$,表示手里的彩球。 **要求**: 请你按上述操作步骤移除掉桌上所有球,计算并返回所需的「最少」球数。如果不能移除桌上所有的球,返回 $-1$。 **说明**: - $1 \le board.length \le 16$。 - $1 \le hand.length \le 5$。 - $board$ 和 $hand$ 由字符 `'R'`、`'Y'`、`'B'`、`'G'` 和 `'W'` 组成。 - 桌面上一开始的球中,不会有三个及三个以上颜色相同且连着的球。 **示例**: - 示例 1: ```python 输入:board = "WRRBBW", hand = "RB" 输出:-1 解释:无法移除桌面上的所有球。可以得到的最好局面是: - 插入一个 'R' ,使桌面变为 WRRRBBW 。WRRRBBW -> WBBW - 插入一个 'B' ,使桌面变为 WBBBW 。WBBBW -> WW 桌面上还剩着球,没有其他球可以插入。 ``` - 示例 2: ```python 输入:board = "WWRRBBWW", hand = "WRBRW" 输出:2 解释:要想清空桌面上的球,可以按下述步骤: - 插入一个 'R' ,使桌面变为 WWRRRBBWW 。WWRRRBBWW -> WWBBWW - 插入一个 'B' ,使桌面变为 WWBBBWW 。WWBBBWW -> WWWW -> empty 只需从手中出 2 个球就可以清空桌面。 ``` ## 解题思路 ### 思路 1:广度优先搜索 **核心思想**:使用广度优先搜索(BFS)逐层探索所有可能的状态。每次从队列中取出一个状态,使用「智能剪枝」在关键位置插入手中的球,然后递归删除连续的同色球。 **算法步骤**: 1. 初始化:将 $hand$ 进行排序,如 $hand = "WRBYG"$ 排序为 $"BGRWY"$,避免相同颜色不同排列的重复计算。 2. 初始化队列:将初始状态 $(board, sorted\_hand, 0)$ 加入队列。 3. 使用哈希表 $visited$ 记录已访问的状态 $(board, hand)$。 4. 逐层处理队列: - 取出当前状态 $(curr\_board, curr\_hand, step)$。 - 使用 $product$ 尝试在每个位置 $i \in [0, len(curr\_board) + 1]$ 插入手中的每个球 $j \in [0, len(curr\_hand)]$。 - **剪枝 1**:如果当前球和上一个球颜色相同,跳过(避免重复)。 - **剪枝 2**:跳过在连续相同颜色球开头之后的位置插入相同颜色(因为开头之前插入效果相同)。 - **剪枝 3**:只在两种情况下插入球: - 插入位置的球颜色与插入颜色相同(形成连续); - 前后颜色相同且与插入颜色不同(形成消除)。 - 插入后执行递归删除操作,如果桌面为空则返回当前步数。 - 将新状态加入队列(如果未访问过)。 **关键点**: - **三重剪枝策略**:通过三个剪枝条件大幅减少无效的插入尝试,提升效率。 - **状态去重**:对 $hand$ 排序并使用 $visited$ 避免重复状态。 - **高效删除**:使用正则表达式 `re.subn` 递归删除连续 $3$ 个及以上同色球。 ### 思路 1:代码 ```python from collections import deque import re class Solution: def findMinStep(self, board: str, hand: str) -> int: # 递归删除连续 3 个及以上同色球 def remove_balls(s): """删除字符串中连续的 3 个及以上相同字符""" while True: # 查找是否有连续的 3 个及以上相同字符 n = len(s) if n < 3: break # 记录当前连续相同字符的开始位置 start = 0 found = False for i in range(1, n + 1): # 如果到达末尾或字符不同,检查是否需要删除 if i == n or s[i] != s[start]: # 如果连续长度 >= 3,进行删除 if i - start >= 3: s = s[:start] + s[i:] found = True break start = i # 如果没有找到需要删除的连续字符,退出循环 if not found: break return s # 对手中的球进行排序,将相同颜色的球归类在一起 sorted_hand = ''.join(sorted(hand)) # 初始化 BFS 队列和访问记录 # 每个状态包含:(当前桌面状态, 手中剩余球, 已使用步数) queue = deque() queue.append((board, sorted_hand, 0)) # 记录已访问过的状态,避免重复搜索 seen = {(board, sorted_hand)} while queue: curr_board, curr_hand, steps = queue.popleft() # 尝试在每一个可能的位置插入每一个球 for pos in range(len(curr_board) + 1): for idx in range(len(curr_hand)): # 获取要插入的球的颜色 ball = curr_hand[idx] # 剪枝 1:如果这个球和上一个球颜色相同,跳过 # (避免对同一个颜色重复尝试) if idx > 0 and curr_hand[idx] == curr_hand[idx - 1]: continue # 剪枝 2:跳过在连续相同颜色球的开头之后插入相同颜色 # 原理:在连续相同颜色块中间插入相同颜色的球, # 效果和在块开头之前插入相同,会产生重复状态 if pos > 0 and curr_board[pos - 1] == ball: continue # 剪枝 3:只在有效的情况下插入球 # 情况 a:插入位置的颜色与插入球颜色相同(形成连续) # 情况 b:前后颜色相同且与插入球颜色不同(可形成消除) should_insert = False # 检查情况 a if pos < len(curr_board) and curr_board[pos] == ball: should_insert = True # 检查情况 b elif pos > 0 and pos < len(curr_board): if curr_board[pos - 1] == curr_board[pos] and curr_board[pos - 1] != ball: should_insert = True if should_insert: # 在指定位置插入球 new_board = curr_board[:pos] + ball + curr_board[pos:] # 删除插入后形成的连续球 new_board = remove_balls(new_board) # 更新手中的球(移除已使用的球) new_hand = curr_hand[:idx] + curr_hand[idx + 1:] # 如果桌面已清空,返回步数 if not new_board: return steps + 1 # 构建新状态 new_state = (new_board, new_hand) # 如果这个新状态没有被访问过,加入队列 if new_state not in seen: queue.append((new_board, new_hand, steps + 1)) seen.add(new_state) # 无法清空桌面 return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m \times 5^m \times 2^n)$。其中 $n$ 是 $board$ 的长度(最多 $16$),$m$ 是 $hand$ 的长度(最多 $5$)。最坏情况下需要遍历所有可能的状态组合,但由于三重剪枝策略和状态去重,搜索空间被大幅缩减,实际运行时间会被控制在合理范围内。 - **空间复杂度**:$O(5^m \times 2^n)$。用于存储 BFS 队列和已访问状态的集合。虽然理论状态数可能达到指数级别,但由于剪枝策略,实际状态数会大大减少,且 $n \le 16$、$m \le 5$ 保证了实际运行的可行性。 ## 参考资料 - [0488. 祖玛游戏-官方题解](https://leetcode.cn/problems/zuma-game/solutions/1092466/zu-ma-you-xi-by-leetcode-solution-lrp4/) ================================================ FILE: docs/solutions/0500-0599/01-matrix.md ================================================ # [0542. 01 矩阵](https://leetcode.cn/problems/01-matrix/) - 标签:广度优先搜索、数组、动态规划、矩阵 - 难度:中等 ## 题目链接 - [0542. 01 矩阵 - 力扣](https://leetcode.cn/problems/01-matrix/) ## 题目大意 **描述**:给定一个 $m * n$ 大小的、由 `0` 和 `1` 组成的矩阵 $mat$。 **要求**:输出一个大小相同的矩阵 $res$,其中 $res[i][j]$ 表示对应位置元素(即 $mat[i][j]$)到最近的 $0$ 的距离。 **说明**: - 两个相邻元素间的距离为 $1$。 - $m == mat.length$。 - $n == mat[i].length$。 - $1 \le m, n \le 10^4$。 - $1 \le m * n \le 10^4$。 - $mat[i][j] === 0$ 或者 $mat[i][j] == 1$。 - $mat$ 中至少有一个 $0$。 **示例**: - 示例 1: ![](https://pic.leetcode-cn.com/1626667201-NCWmuP-image.png) ```python 输入:mat = [[0,0,0],[0,1,0],[0,0,0]] 输出:[[0,0,0],[0,1,0],[0,0,0]] ``` - 示例 2: ![](https://pic.leetcode-cn.com/1626667205-xFxIeK-image.png) ```python 输入:mat = [[0,0,0],[0,1,0],[1,1,1]] 输出:[[0,0,0],[0,1,0],[1,2,1]] ``` ## 解题思路 ### 思路 1:广度优先搜索 题目要求的是每个 `1` 到 `0`的最短曼哈顿距离。 比较暴力的做法是,从每个 `1` 开始进行广度优先搜索,每一步累积距离,当搜索到第一个 `0`,就是离这个 `1` 最近的 `0`,我们更新对应 `1` 位置上的答案距离。然后从下一个 `1` 开始进行广度优先搜索。 这样做每次进行广度优先搜索的时间复杂度为 $O(m \times n)$。对于 $m \times n$ 个节点来说,每个节点可能都要进行一次广度优先搜索,总的时间复杂度为 $O(m^2 \times n^2)$。时间复杂度太高了。 我们可以换个角度:求每个 `0` 到 `1` 的最短曼哈顿距离(和求每个 `1` 到 `0` 是等价的)。 我们将所有值为 `0` 的元素位置保存到队列中,然后对所有值为 `0` 的元素开始进行广度优先搜索,每搜一步距离加 `1`,当每次搜索到 `1` 时,就可以得到 `0` 到这个 `1` 的最短距离,也就是当前离这个 `1` 最近的 `0` 的距离。 这样对于所有节点来说,总共需要进行一次广度优先搜索就可以了,时间复杂度为 $O(m \times n)$。 具体步骤如下: 1. 使用一个集合变量 `visited` 存储所有值为 `0` 的元素坐标。使用队列变量 `queue` 存储所有值为 `0` 的元素坐标。使用二维数组 `res` 存储对应位置元素(即 $mat[i][j]$)到最近的 $0$ 的距离。 2. 我们从所有为如果队列 `queue` 不为空,则从队列中依次取出值为 `0` 的元素坐标,遍历其上、下、左、右位置。 3. 如果相邻区域未被访问过(说明遇到了值为 `1` 的元素),则更新相邻位置的距离值,并把相邻位置坐标加入队列 `queue` 和访问集合 `visited` 中。 4. 继续执行 2 ~ 3 步,直到队列为空时,返回 `res`。 ### 思路 1:代码 ```python import collections class Solution: def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]: rows, cols = len(mat), len(mat[0]) res = [[0 for _ in range(cols)] for _ in range(rows)] visited = set() for i in range(rows): for j in range(cols): if mat[i][j] == 0: visited.add((i, j)) directions = {(1, 0), (-1, 0), (0, 1), (0, -1)} queue = collections.deque(visited) while queue: i, j = queue.popleft() for direction in directions: new_i = i + direction[0] new_j = j + direction[1] if 0 <= new_i < rows and 0 <= new_j < cols and (new_i, new_j) not in visited: res[new_i][new_j] = res[i][j] + 1 queue.append((new_i, new_j)) visited.add((new_i, new_j)) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/0500-0599/array-nesting.md ================================================ # [0565. 数组嵌套](https://leetcode.cn/problems/array-nesting/) - 标签:深度优先搜索、数组 - 难度:中等 ## 题目链接 - [0565. 数组嵌套 - 力扣](https://leetcode.cn/problems/array-nesting/) ## 题目大意 **描述**: 给定索引从 $0$ 开始长度为 $N$ 的数组 $A$,包含 $0 \sim N - 1$ 的所有整数。 **要求**: 找到最大的集合 $S$ 并返回其大小,其中 $S[i] = \{A[i], A[A[i]], A[A[A[i]]], ... \}$ 且遵守以下的规则。 假设选择索引为 $i$ 的元素 $A[i]$ 为 $S$ 的第一个元素,$S$ 的下一个元素应该是 $A[A[i]]$,之后是 $A[A[A[i]]]...$ 以此类推,不断添加直到 $S$ 出现重复的元素。 **说明**: - $1 \le nums.length \le 10^{5}$。 - $0 \le nums[i] \lt nums.length$。 - $A$ 中不含有重复的元素。 **示例**: - 示例 1: ```python 输入: A = [5,4,0,3,1,6,2] 输出: 4 解释: A[0] = 5, A[1] = 4, A[2] = 0, A[3] = 3, A[4] = 1, A[5] = 6, A[6] = 2. 其中一种最长的 S[K]: S[0] = {A[0], A[5], A[6], A[2]} = {5, 6, 2, 0} ``` ## 解题思路 ### 思路 1: 将数组 $A$ 看成一个有向图:每个下标 $i$ 指向唯一的下标 $A[i]$。由于 $A$ 是 $0\sim N-1$ 的一个排列(元素不重复,且都在范围内),图中每个节点出度均为 $1$,因此整张图由若干个「不相交的简单环」组成。题目中的集合 $S[i] = \{A[i], A[A[i]], A[A[A[i]]], \ldots\}$ 实际上就是从起点 $i$ 出发顺着边前进直到首次重复时形成的环,答案即为所有环中最大的长度。 做法:从每个未访问的起点 $i$ 出发,沿着 $i \to A[i] \to A[A[i]] \to \cdots$ 走,并统计本次路径中第一次遇到已访问节点前的步数,即该环的长度。用布尔数组(或集合) $visited$ 标记已经访问过的下标,保证每个下标只被遍历一次。 **变量含义**: - $N$:数组长度。 - $A$:输入数组。 - $visited$:访问标记数组,`visited[x] = True` 表示下标 $x$ 已被计入某个环或已遍历过。 - $ans$:当前找到的最大环长。 - $curr$:从某个起点出发的游标位置。 - $cnt$:从当前起点出发走到重复前累计的长度。 **算法步骤**: 1. 初始化 $visited$ 全为 `False`,$ans = 0$。 2. 枚举起点 $i \in [0, N-1]$。若 `visited[i]` 为 `True`,跳过;否则从 $i$ 出发: - 置 $cnt = 0$,$curr = i$。 - 当 `visited[curr]` 为 `False` 时:标记 `visited[curr] = True`,令 $curr = A[curr]$,$cnt++$。 - 本轮结束时,更新 $ans = \max(ans, cnt)$。 3. 返回 $ans$。 ### 思路 1:代码 ```python class Solution: def arrayNesting(self, nums: List[int]) -> int: n = len(nums) visited = [False] * n # 访问标记:True 表示该下标已被遍历过 ans = 0 for i in range(n): if visited[i]: continue # 已处理过,跳过 cnt = 0 curr = i # 顺着 i -> nums[i] -> nums[nums[i]] ... 直到遇到已访问位置 while not visited[curr]: visited[curr] = True curr = nums[curr] cnt += 1 # 本次从起点 i 出发形成的环长度为 cnt if cnt > ans: ans = cnt return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(N)$。每个下标至多被访问一次。 - **空间复杂度**:$O(N)$。使用了大小为 $N$ 的 $visited$ 辅助数组。 ================================================ FILE: docs/solutions/0500-0599/array-partition.md ================================================ # [0561. 数组拆分](https://leetcode.cn/problems/array-partition/) - 标签:贪心、数组、计数排序、排序 - 难度:简单 ## 题目链接 - [0561. 数组拆分 - 力扣](https://leetcode.cn/problems/array-partition/) ## 题目大意 **描述**:给定一个长度为 $2 \times n$ 的整数数组 $nums$。 **要求**:将数组中的数拆分成 $n$ 对,每对数求最小值,求 $n$ 对数最小值的最大总和是多少。 **说明**: - $1 \le n \le 10^4$。 - $nums.length == 2 * n$。 - $-10^4 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [1,4,3,2] 输出:4 解释:所有可能的分法(忽略元素顺序)为: 1. (1, 4), (2, 3) -> min(1, 4) + min(2, 3) = 1 + 2 = 3 2. (1, 3), (2, 4) -> min(1, 3) + min(2, 4) = 1 + 2 = 3 3. (1, 2), (3, 4) -> min(1, 2) + min(3, 4) = 1 + 3 = 4 所以最大总和为 4 ``` - 示例 2: ```python 输入:nums = [6,2,6,5,1,2] 输出:9 解释:最优的分法为 (2, 1), (2, 5), (6, 6). min(2, 1) + min(2, 5) + min(6, 6) = 1 + 2 + 6 = 9 ``` ## 解题思路 ### 思路 1:计数排序 因为 $nums[i]$ 的范围为 $[-10^4, 10^4]$,范围不是很大,所以我们可以使用计数排序算法先将数组 $nums$ 进行排序。 要想每对数最小值的总和最大,就得使每对数的最小值尽可能大。只有让较大的数与较大的数一起组合,较小的数与较小的数一起结合,才能才能使总和最大。所以,排序完之后将相邻两个元素的最小值进行相加,即得到结果。 ### 思路 1:代码 ```python class Solution: def countingSort(self, nums: [int]) -> [int]: # 计算待排序数组中最大值元素 nums_max 和最小值元素 nums_min nums_min, nums_max = min(nums), max(nums) # 定义计数数组 counts,大小为 最大值元素 - 最小值元素 + 1 size = nums_max - nums_min + 1 counts = [0 for _ in range(size)] # 统计值为 num 的元素出现的次数 for num in nums: counts[num - nums_min] += 1 # 生成累积计数数组 for i in range(1, size): counts[i] += counts[i - 1] # 反向填充目标数组 res = [0 for _ in range(len(nums))] for i in range(len(nums) - 1, -1, -1): num = nums[i] # 根据累积计数数组,将 num 放在数组对应位置 res[counts[num - nums_min] - 1] = num # 将 num 的对应放置位置减 1,从而得到下个元素 num 的放置位置 counts[nums[i] - nums_min] -= 1 return res def arrayPairSum(self, nums: List[int]) -> int: nums = self.countingSort(nums) return sum(nums[::2]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + k)$,其中 $k$ 代表数组 $nums$ 的值域。 - **空间复杂度**:$O(k)$。 ### 思路 2:排序 要想每对数最小值的总和最大,就得使每对数的最小值尽可能大。只有让较大的数与较大的数一起组合,较小的数与较小的数一起结合,才能才能使总和最大。 1. 对 $nums$ 进行排序。 2. 将相邻两个元素的最小值进行相加,即得到结果。 ### 思路 1:代码 ```python class Solution: def arrayPairSum(self, nums: List[int]) -> int: nums.sort() return sum(nums[::2]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0500-0599/base-7.md ================================================ # [0504. 七进制数](https://leetcode.cn/problems/base-7/) - 标签:数学 - 难度:简单 ## 题目链接 - [0504. 七进制数 - 力扣](https://leetcode.cn/problems/base-7/) ## 题目大意 **描述**:给定一个整数 $num$。 **要求**:将其转换为 $7$ 进制数,并以字符串形式输出。 **说明**: - $-10^7 \le num \le 10^7$。 **示例**: - 示例 1: ```python 输入: num = 100 输出: "202" ``` - 示例 2: ```python 输入: num = -7 输出: "-10" ``` ## 解题思路 ### 思路 1:模拟 1. $num$ 不断对 $7$ 取余整除。 2. 然后将取到的余数进行拼接成字符串即可。 ### 思路 1:代码 ```python class Solution: def convertToBase7(self, num: int) -> str: if num == 0: return "0" if num < 0: return "-" + self.convertToBase7(-num) ans = "" while num: ans = str(num % 7) + ans num //= 7 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log |n|)$。 - **空间复杂度**:$O(\log |n|)$。 ================================================ FILE: docs/solutions/0500-0599/beautiful-arrangement.md ================================================ # [0526. 优美的排列](https://leetcode.cn/problems/beautiful-arrangement/) - 标签:位运算、数组、动态规划、回溯、状态压缩 - 难度:中等 ## 题目链接 - [0526. 优美的排列 - 力扣](https://leetcode.cn/problems/beautiful-arrangement/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:返回可以构造的「优美的排列」的数量。 **说明**: - **优美的排列**:假设有 $1 \sim n$ 的 $n$ 个整数。如果用这些整数构造一个数组 $perm$(下标从 $1$ 开始),使得数组第 $i$ 位元素 $perm[i]$ 满足下面两个条件之一,则该数组就是一个「优美的排列」: - $perm[i]$ 能够被 $i$ 整除; - $i$ 能够被 $perm[i]$ 整除。 - $1 \le n \le 15$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:2 解释: 第 1 个优美的排列是 [1,2]: - perm[1] = 1 能被 i = 1 整除 - perm[2] = 2 能被 i = 2 整除 第 2 个优美的排列是 [2,1]: - perm[1] = 2 能被 i = 1 整除 - i = 2 能被 perm[2] = 1 整除 ``` - 示例 2: ```python 输入:n = 1 输出:1 ``` ## 解题思路 ### 思路 1:回溯算法 这道题可以看做是「[0046. 全排列](https://leetcode.cn/problems/permutations/)」的升级版。 1. 通过回溯算法我们可以将数组的所有排列情况列举出来。 2. 因为只有满足第 $i$ 位元素能被 $i$ 整除,或者满足 $i$ 能整除第 $i$ 位元素的条件下才符合要求,所以我们可以进行剪枝操作,不再考虑不满足要求的情况。 3. 最后回溯完输出方案数。 ### 思路 1:代码 ```python class Solution: def countArrangement(self, n: int) -> int: ans = 0 visited = set() def backtracking(index): nonlocal ans if index == n + 1: ans += 1 return for i in range(1, n + 1): if i in visited: continue if i % index == 0 or index % i == 0: visited.add(i) backtracking(index + 1) visited.remove(i) backtracking(1) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n!)$,其中 $n$ 为给定整数。 - **空间复杂度**:$O(n)$,递归栈空间大小为 $O(n)$。 ### 思路 2:状态压缩 DP 因为 $n$ 最大只有 $15$,所以我们可以考虑使用「状态压缩」。 「状态压缩」指的是使用一个 $n$ 位的二进制数来表示排列中数的选取情况。 举个例子: 1. $n = 4, state = (1001)_2$,表示选择了数字 $1, 4$,剩余数字 $2$ 和 $3$ 未被选择。 2. $n = 6, state = (011010)_2$,表示选择了数字 $2, 4, 5$,剩余数字 $1, 3, 6$ 未被选择。 这样我们就可以使用 $n$ 位的二进制数 $state$ 来表示当前排列中数的选取情况。 如果我们需要检查值为 $k$ 的数字是否被选择时,可以通过判断 $(state \text{ >} \text{> } (k - 1)) \text{ \& } 1$ 是否为 $1$ 来确定。 如果为 $1$,则表示值为 $k$ 的数字被选择了,如果为 $0$,则表示值为 $k$ 的数字没有被选择。 ###### 1. 阶段划分 按照排列的数字个数、数字集合的选择情况进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][state]$ 表示为:考虑前 $i$ 个数,且当数字集合的选择情况为 $state$ 时的方案数。 ###### 3. 状态转移方程 假设 $dp[i][state]$ 中第 $i$ 个位置所选数字为 $k$,则:$state$ 中第 $k$ 位为 $1$,且 $k \mod i == 0$ 或者 $i \mod k == 0$。 那么 $dp[i][state]$ 肯定是由考虑前 $i - 1$ 个位置,且 $state$ 第 $k$ 位为 $0$ 的状态而来,即:$dp[i - 1][state \& (\neg(1 \text{ <}\text{< } (k - 1)))]$。 所以状态转移方程为:$dp[i][state] = \sum_{k = 1}^n dp[i - 1][state \text{ \& } (\neg(1 \text{ <} \text{< } (k - 1)))]$。 ###### 4. 初始条件 - 不考虑任何数($i = 0, state = 0$)的情况下,方案数为 $1$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][state]$ 表示为:考虑前 $i$ 个数,且当数字集合的选择情况为 $state$ 时的方案数。所以最终结果为 $dp[i][states - 1]$,其中 $states = 1 \text{ <} \text{< } n$。 ### 思路 2:代码 ```python class Solution: def countArrangement(self, n: int) -> int: states = 1 << n dp = [[0 for _ in range(states)] for _ in range(n + 1)] dp[0][0] = 1 for i in range(1, n + 1): # 枚举第 i 个位置 for state in range(states): # 枚举所有状态 one_num = bin(state).count("1") # 计算当前状态中选择了多少个数字(即统计 1 的个数) if one_num != i: # 只有 i 与选择数字个数相同时才能计算 continue for k in range(1, n + 1): # 枚举第 i 个位置(最后 1 位)上所选的数字 if state >> (k - 1) & 1 == 0: # 只有 state 第 k 个位置上为 1 才表示选了该数字 continue if k % i == 0 or i % k == 0: # 只有满足整除关系才符合要求 # dp[i][state] 由前 i - 1 个位置,且 state 第 k 位为 0 的状态而来 dp[i][state] += dp[i - 1][state & (~(1 << (k - 1)))] return dp[i][states - 1] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2 \times 2^n)$,其中 $n$ 为给定整数。 - **空间复杂度**:$O(n \times 2^n)$。 ### 思路 3:状态压缩 DP + 优化 通过二维的「状态压缩 DP」可以看出,当我们在考虑第 $i$ 个位置时,其选择数字个数也应该为 $i$。 而我们可以根据 $state$ 中 $1$ 的个数来判断当前选择的数字个数,这样我们就可以减少用于枚举第 $i$ 个位置的循环,改用统计 $state$ 中 $1$ 的个数来判断前选择的数字个数或者说当前正在考虑的元素位置。 而这样,我们还可以进一步优化状态的定义,将二维的状态优化为一维的状态。具体做法如下: ###### 1. 阶段划分 按照数字集合的选择情况进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[state]$ 表示为:当数字集合的选择情况为 $state$ 时的方案数。 ###### 3. 状态转移方程 对于状态 $state$,先统计出 $state$ 中选择的数字个数(即统计二进制中 $1$ 的个数)$one\_num$。 则 $dp[state]$ 表示选择了前 $one\_num$ 个数字,且选择情况为 $state$ 时的方案数。 $dp[state]$ 的状态肯定是由前 $one\_num - 1$ 个数字,且 $state$ 第 $k$ 位为 $0$ 的状态而来对应状态转移而来,即:$dp[state \oplus (1 << (k - 1))]$。 所以状态转移方程为:$dp[state] = \sum_{k = 1}^n dp[state \oplus (1 << (k - 1))]$ ###### 4. 初始条件 - 不考虑任何数的情况下,方案数为 $1$,即:$dp[0] = 1$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[state]$ 表示为:当数字集合选择状态为 $state$ 时的方案数。所以最终结果为 $dp[states - 1]$,其中 $states = 1 << n$。 ### 思路 3:代码 ```python class Solution: def countArrangement(self, n: int) -> int: states = 1 << n dp = [0 for _ in range(states)] dp[0] = 1 for state in range(states): # 枚举所有状态 one_num = bin(state).count("1") # 计算当前状态中选择了多少个数字(即统计 1 的个数) for k in range(1, n + 1): # 枚举最后 1 位上所选的数字 if state >> (k - 1) & 1 == 0: # 只有 state 第 k 个位置上为 1 才表示选了该数字 continue if one_num % k == 0 or k % one_num == 0: # 只有满足整除关系才符合要求 # dp[state] 由前 one_num - 1 个位置,且 state 第 k 位为 0 的状态而来 dp[state] += dp[state ^ (1 << (k - 1))] return dp[states - 1] ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n \times 2^n)$,其中 $n$ 为给定整数。 - **空间复杂度**:$O(2^n)$。 ## 参考资料 - 【题解】[【宫水三叶】详解两种状态压缩 DP 思路 - 优美的排列](https://leetcode.cn/problems/beautiful-arrangement/solution/gong-shui-san-xie-xiang-jie-liang-chong-vgsia/) ================================================ FILE: docs/solutions/0500-0599/binary-tree-longest-consecutive-sequence-ii.md ================================================ # [0549. 二叉树最长连续序列 II](https://leetcode.cn/problems/binary-tree-longest-consecutive-sequence-ii/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0549. 二叉树最长连续序列 II - 力扣](https://leetcode.cn/problems/binary-tree-longest-consecutive-sequence-ii/) ## 题目大意 **描述**: 给定一棵二叉树的根节点 $root$,返回树中最长连续序列路径的长度。 连续序列路径是指路径中相邻节点的值相差 1。路径可以是递增的或递减的,路径可以是从父节点到子节点,也可以是从子节点到父节点(即路径可以"拐弯")。 - 例如,$[1,2,3,4]$ 和 $[4,3,2,1]$ 都被认为有效,但路径 $[1,2,4,3]$ 无效。 **要求**: 返回最长连续序列路径的长度。 **说明**: - 树中节点数在范围 $[1, 3 \times 10^4]$ 内。 - $-3 \times 10^4 \le Node.val \le 3 \times 10^4$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/14/consec2-1-tree.jpg) ```python 输入: root = [1,2,3] 输出: 2 解释: 最长的连续路径是 [1, 2] 或者 [2, 1]。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/14/consec2-2-tree.jpg) ```python 输入: root = [2,1,3] 输出: 3 解释: 最长的连续路径是 [1, 2, 3] 或者 [3, 2, 1]。 ``` ## 解题思路 ### 思路 1:后序遍历 + 动态规划 本题扩展了连续序列既可递增也可递减,且可以"拐弯"(以父节点为顶点的左右递增或递减链)。 **核心思路**: - 对于每个节点,返回两个值:以该节点为根向下的最大递增长度和最大递减长度。 - 递归处理左右子树,根据子节点与当前节点的值关系更新递增/递减长度。 - 全局维护最大值:可以是左递增 + 右递减 + 1,或左递减 + 右递增 + 1。 假设以 $node$ 节点为根节点,则返回 $(inc, dec)$,分别为递增和递减的最大长度。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def longestConsecutive(self, root: Optional[TreeNode]) -> int: self.ans = 0 def dfs(node): if not node: return (0, 0) # (inc, dec) inc = dec = 1 if node.left: l_inc, l_dec = dfs(node.left) if node.val == node.left.val + 1: dec = max(dec, l_dec + 1) elif node.val == node.left.val - 1: inc = max(inc, l_inc + 1) if node.right: r_inc, r_dec = dfs(node.right) if node.val == node.right.val + 1: dec = max(dec, r_dec + 1) elif node.val == node.right.val - 1: inc = max(inc, r_inc + 1) # 以 node 为顶点合并(不重叠) self.ans = max(self.ans, inc + dec - 1) return inc, dec dfs(root) return self.ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是节点数。每个节点只遍历一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度,递归栈空间。 ================================================ FILE: docs/solutions/0500-0599/binary-tree-tilt.md ================================================ # [0563. 二叉树的坡度](https://leetcode.cn/problems/binary-tree-tilt/) - 标签:树、深度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0563. 二叉树的坡度 - 力扣](https://leetcode.cn/problems/binary-tree-tilt/) ## 题目大意 **描述**: 给定一个二叉树的根节点 $root$。 **要求**: 计算并返回「整个树」的坡度。 **说明**: - 一个树的「节点的坡度」定义为:该节点左子树的节点之和和右子树节点之和的「差的绝对值」。如果没有左子树的话,左子树的节点之和为 $0$;没有右子树的话也是一样。空结点的坡度是 $0$。 - 「整个树」的坡度就是其所有节点的坡度之和。 - 树中节点数目的范围在 $[0, 10^{4}]$ 内。 - $-10^{3} \le Node.val \le 10^{3}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/20/tilt1.jpg) ```python 输入:root = [1,2,3] 输出:1 解释: 节点 2 的坡度:|0-0| = 0(没有子节点) 节点 3 的坡度:|0-0| = 0(没有子节点) 节点 1 的坡度:|2-3| = 1(左子树就是左子节点,所以和是 2 ;右子树就是右子节点,所以和是 3 ) 坡度总和:0 + 0 + 1 = 1 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/10/20/tilt2.jpg) ```python 输入:root = [4,2,9,3,5,null,7] 输出:15 解释: 节点 3 的坡度:|0-0| = 0(没有子节点) 节点 5 的坡度:|0-0| = 0(没有子节点) 节点 7 的坡度:|0-0| = 0(没有子节点) 节点 2 的坡度:|3-5| = 2(左子树就是左子节点,所以和是 3 ;右子树就是右子节点,所以和是 5 ) 节点 9 的坡度:|0-7| = 7(没有左子树,所以和是 0 ;右子树正好是右子节点,所以和是 7 ) 节点 4 的坡度:|(3+5+2)-(9+7)| = |10-16| = 6(左子树值为 3、5 和 2 ,和是 10 ;右子树值为 9 和 7 ,和是 16 ) 坡度总和:0 + 0 + 0 + 2 + 7 + 6 = 15 ``` ## 解题思路 ### 思路 1:递归计算 对于每个节点,我们需要: 1. 计算左子树所有节点的和 $left\_sum$。 2. 计算右子树所有节点的和 $right\_sum$。 3. 计算该节点的坡度 $|left\_sum - right\_sum|$。 4. 返回该节点及其子树的总和 $node.val + left\_sum + right\_sum$。 使用后序遍历(DFS),在遍历过程中: - 递归计算左右子树的和 - 计算当前节点的坡度并累加到总坡度中 - 返回当前子树的和 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def findTilt(self, root: Optional[TreeNode]) -> int: total_tilt = 0 def dfs(node: Optional[TreeNode]) -> int: nonlocal total_tilt if not node: return 0 # 递归计算左右子树的和 left_sum = dfs(node.left) right_sum = dfs(node.right) # 计算当前节点的坡度并累加 tilt = abs(left_sum - right_sum) total_tilt += tilt # 返回当前子树的和 return node.val + left_sum + right_sum dfs(root) return total_tilt ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树的节点数,需要遍历每个节点一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度,递归栈的深度最多为 $h$。 ================================================ FILE: docs/solutions/0500-0599/boundary-of-binary-tree.md ================================================ # [0545. 二叉树的边界](https://leetcode.cn/problems/boundary-of-binary-tree/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0545. 二叉树的边界 - 力扣](https://leetcode.cn/problems/boundary-of-binary-tree/) ## 题目大意 **描述**: 给定一棵二叉树的根节点 $root$。 **要求**: 按逆时针顺序返回边界节点的值。 **说明**: - 二叉树的边界包含: 1. 根节点 2. 左边界:从根节点到最左侧叶子节点的路径(不包括叶子节点) 3. 所有叶子节点(从左到右) 4. 右边界:从最右侧叶子节点到根节点的路径(不包括根节点和叶子节点) - 「左边界」是满足下述定义的节点集合: - 根节点的左子节点在左边界中。如果根节点不含左子节点,那么左边界就为 空 。 - 如果一个节点在左边界中,并且该节点有左子节点,那么它的左子节点也在左边界中。 - 如果一个节点在左边界中,并且该节点 不含 左子节点,那么它的右子节点就在左边界中。 - 最左侧的叶节点「不在」左边界中。 - 「右边界」定义方式与 左边界 相同,只是将左替换成右。 - 「叶节点」是没有任何子节点的节点。对于此问题,根节点「不是」叶节点。 - 树中节点的数目在范围 $[1, 10^4]$ 内。 - $-1000 \le Node.val \le 1000$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/11/boundary1.jpg) ```python 输入:root = [1,null,2,3,4] 输出:[1,3,4,2] 解释: - 左边界为空,因为二叉树不含左子节点。 - 右边界是 [2] 。从根节点的右子节点开始的路径为 2 -> 4 ,但 4 是叶节点,所以右边界只有 2 。 - 叶节点从左到右是 [3,4] 。 按题目要求依序连接得到结果 [1] + [] + [3,4] + [2] = [1,3,4,2]。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/11/11/boundary2.jpg) ```python 输入:root = [1,2,3,4,5,6,null,null,null,7,8,9,10] 输出:[1,2,4,7,8,9,10,6,3] 解释: - 左边界为 [2] 。从根节点的左子节点开始的路径为 2 -> 4 ,但 4 是叶节点,所以左边界只有 2 。 - 右边界是 [3,6] ,逆序为 [6,3] 。从根节点的右子节点开始的路径为 3 -> 6 -> 10 ,但 10 是叶节点。 - 叶节点从左到右是 [4,7,8,9,10] 按题目要求依序连接得到结果 [1] + [2] + [4,7,8,9,10] + [6,3] = [1,2,4,7,8,9,10,6,3]。 ``` ## 解题思路 ### 思路 1:分治 + DFS 将问题分解为三个部分: 1. 找左边界:从根节点开始,优先走左子树,如果没有左子树则走右子树,直到叶子节点(不包括叶子)。 2. 找所有叶子节点:使用 DFS 遍历,找到所有叶子节点。 3. 找右边界:从根节点开始,优先走右子树,如果没有右子树则走左子树,直到叶子节点(不包括叶子),最后需要反转。 注意:根节点单独处理,避免重复。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def boundaryOfBinaryTree(self, root: Optional[TreeNode]) -> List[int]: if not root: return [] # 判断是否为叶子节点 def is_leaf(node): return node and not node.left and not node.right # 获取左边界(不包括叶子节点) def get_left_boundary(node): boundary = [] while node: if not is_leaf(node): boundary.append(node.val) # 优先走左子树,没有左子树则走右子树 if node.left: node = node.left else: node = node.right return boundary # 获取所有叶子节点 def get_leaves(node): if not node: return [] if is_leaf(node): return [node.val] return get_leaves(node.left) + get_leaves(node.right) # 获取右边界(不包括叶子节点) def get_right_boundary(node): boundary = [] while node: if not is_leaf(node): boundary.append(node.val) # 优先走右子树,没有右子树则走左子树 if node.right: node = node.right else: node = node.left return boundary[::-1] # 反转 # 如果根节点是叶子节点,直接返回 if is_leaf(root): return [root.val] # 组合结果 result = [root.val] result.extend(get_left_boundary(root.left)) result.extend(get_leaves(root)) result.extend(get_right_boundary(root.right)) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树的节点数。每个节点最多被访问一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度,递归栈的深度。 ================================================ FILE: docs/solutions/0500-0599/brick-wall.md ================================================ # [0554. 砖墙](https://leetcode.cn/problems/brick-wall/) - 标签:数组、哈希表 - 难度:中等 ## 题目链接 - [0554. 砖墙 - 力扣](https://leetcode.cn/problems/brick-wall/) ## 题目大意 **描述**: 你的面前有一堵矩形的、由 $n$ 行砖块组成的砖墙。这些砖块高度相同(也就是一个单位高)但是宽度不同。每一行砖块的宽度之和相等。 你现在要画一条 **自顶向下** 的、穿过 **最少** 砖块的垂线。如果你画的线只是从砖块的边缘经过,就不算穿过这块砖。你不能沿着墙的两个垂直边缘之一画线,这样显然是没有穿过一块砖的。 给你一个二维数组 $wall$,该数组包含这堵墙的相关信息。其中,$wall[i]$ 是一个代表从左至右每块砖的宽度的数组。你需要找出怎样画才能使这条线 **穿过的砖块数量最少**,并且返回 **穿过的砖块数量**。 **要求**: 返回穿过的砖块数量的最小值。 **说明**: - $n == wall.length$。 - $1 \le n \le 10^4$。 - $1 \le wall[i].length \le 10^4$。 - $1 \le sum(wall[i].length) \le 2 \times 10^4$。 - 对于每一行 $i$,$sum(wall[i])$ 是相同的。 - $1 \le wall[i][j] \le 2^{31} - 1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2025/01/17/a.png) ```python 输入:wall = [[1,2,2,1],[3,1,2],[1,3,2],[2,4],[3,1,2],[1,3,1,1]] 输出:2 ``` - 示例 2: ```python 输入:wall = [[1],[1],[1]] 输出:3 ``` ## 解题思路 ### 思路 1:哈希表统计边缘位置 #### 思路 1:算法描述 要使得穿过的砖块数量最少,我们需要让垂线尽可能多地穿过砖块之间的缝隙。 **核心思路**: - 对于每一行,计算每个砖块右边缘的位置(前缀和),这些位置就是可以穿过缝隙的位置。 - 使用哈希表统计每个位置有多少行在这个位置有缝隙。 - 最终答案 = 总行数 - 最多行数在同一个位置有缝隙。 注意:不能沿着墙的两端画线,所以不考虑总宽度位置。 **算法步骤**: 1. 遍历每一行砖块。 2. 对于每一行,计算前缀和(每个砖块右边缘的位置)。 3. 使用哈希表 $edge\_count$ 统计每个边缘位置出现的次数(不包括最后一块砖的右边缘)。 4. 找到出现次数最多的边缘位置 $max\_edges$。 5. 返回 $rows - max\_edges$。 #### 思路 1:代码 ```python from collections import defaultdict class Solution: def leastBricks(self, wall: List[List[int]]) -> int: # 统计每个位置有多少行有缝隙 edge_count = defaultdict(int) # 遍历每一行 for row in wall: prefix_sum = 0 # 计算每个砖块右边缘的位置(不包括最后一块砖的右边缘) for i in range(len(row) - 1): prefix_sum += row[i] edge_count[prefix_sum] += 1 # 如果没有缝隙,返回总行数 if not edge_count: return len(wall) # 找到最多行数在同一个位置有缝隙 max_edges = max(edge_count.values()) # 最少穿过的砖块数 = 总行数 - 最多行数在同一个位置有缝隙 return len(wall) - max_edges ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是行数,$m$ 是平均每行的砖块数。需要遍历所有砖块。 - **空间复杂度**:$O(w)$,其中 $w$ 是墙的总宽度。哈希表最多存储 $w$ 个不同的边缘位置。 ================================================ FILE: docs/solutions/0500-0599/coin-change-ii.md ================================================ # [0518. 零钱兑换 II](https://leetcode.cn/problems/coin-change-ii/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [0518. 零钱兑换 II - 力扣](https://leetcode.cn/problems/coin-change-ii/) ## 题目大意 **描述**:给定一个整数数组 $coins$ 表示不同面额的硬币,另给一个整数 $amount$ 表示总金额。 **要求**:计算并返回可以凑成总金额的硬币方案数。如果无法凑出总金额,则返回 $0$。 **说明**: - 每一种面额的硬币枚数为无限个。 - $1 \le coins.length \le 300$。 - $1 \le coins[i] \le 5000$。 - $coins$ 中的所有值互不相同。 - $0 \le amount \le 5000$。 **示例**: - 示例 1: ```python 输入:amount = 5, coins = [1, 2, 5] 输出:4 解释:有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1 ``` - 示例 2: ```python 输入:amount = 3, coins = [2] 输出:0 解释:只用面额 2 的硬币不能凑成总金额 3。 ``` ## 解题思路 ### 思路 1:动态规划 这道题可以转换为:有 $n$ 种不同的硬币,$coins[i]$ 表示第 $i$ 种硬币的面额,每种硬币可以无限次使用。请问凑成总金额为 $amount$ 的背包,一共有多少种方案? 这就变成了完全背包问题。「[322. 零钱兑换](https://leetcode.cn/problems/coin-change/)」中计算的是凑成总金额的最少硬币个数,而这道题计算的是凑成总金额的方案数。 ###### 1. 阶段划分 按照当前背包的载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为:凑成总金额为 $i$ 的方案总数。 ###### 3. 状态转移方程 凑成总金额为 $i$ 的方案数 = 「不使用当前 $coin$,只使用之前硬币凑成金额 $i$ 的方案数」+「使用当前 $coin$ 凑成金额 $i - coin$ 的方案数」。即状态转移方程为:$dp[i] = dp[i] + dp[i - coin]$。 ###### 4. 初始条件 - 凑成总金额为 $0$ 的方案数为 $1$,即 $dp[0] = 1$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示为:凑成总金额为 $i$ 的方案总数。 所以最终结果为 $dp[amount]$。 ### 思路 1:代码 ```python class Solution: def change(self, amount: int, coins: List[int]) -> int: dp = [0 for _ in range(amount + 1)] dp[0] = 1 for coin in coins: for i in range(coin, amount + 1): dp[i] += dp[i - coin] return dp[amount] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times amount)$,其中 $n$ 为数组 $coins$ 的元素个数,$amount$ 为总金额。 - **空间复杂度**:$O(amount)$。 ================================================ FILE: docs/solutions/0500-0599/complex-number-multiplication.md ================================================ # [0537. 复数乘法](https://leetcode.cn/problems/complex-number-multiplication/) - 标签:数学、字符串、模拟 - 难度:中等 ## 题目链接 - [0537. 复数乘法 - 力扣](https://leetcode.cn/problems/complex-number-multiplication/) ## 题目大意 **描述**: 复数可以用字符串表示,遵循 "实部 + 虚部 i" 的形式,并满足下述条件: - 实部 是一个整数,取值范围是 $[-100, 100]$。 - 虚部 也是一个整数,取值范围是 $[-100, 100]$。 - $i^2 == -1$。 给定两个字符串表示的复数 $num1$ 和 $num2$。 **要求**: 遵循复数表示形式,返回表示它们乘积的字符串。 **说明**: - $num1$ 和 $num2$ 都是有效的复数表示。 **示例**: - 示例 1: ```python 输入:num1 = "1+1i", num2 = "1+1i" 输出:"0+2i" 解释:(1 + i) * (1 + i) = 1 + i2 + 2 * i = 2i ,你需要将它转换为 0+2i 的形式。 ``` - 示例 2: ```python 输入:num1 = "1+-1i", num2 = "1+-1i" 输出:"0+-2i" 解释:(1 - i) * (1 - i) = 1 + i2 - 2 * i = -2i ,你需要将它转换为 0+-2i 的形式。 ``` ## 解题思路 ### 思路 1:解析并计算 复数乘法的公式:$(a + bi) \times (c + di) = (ac - bd) + (ad + bc)i$ 步骤: 1. 解析两个复数字符串,提取实部 $a, c$ 和虚部 $b, d$ 2. 计算结果的实部:$ac - bd$ 3. 计算结果的虚部:$ad + bc$ 4. 格式化输出结果 ### 思路 1:代码 ```python class Solution: def complexNumberMultiply(self, num1: str, num2: str) -> str: # 解析 num1: 格式为 "a+bi" 或 "a+-bi" def parse_complex(num_str: str) -> tuple: # 找到 '+' 的位置(可能是最后一个 '+') plus_idx = num_str.rfind('+') real = int(num_str[:plus_idx]) # 虚部去掉 'i' imag = int(num_str[plus_idx + 1:-1]) return real, imag # 解析两个复数 a, b = parse_complex(num1) c, d = parse_complex(num2) # 计算复数乘法:(a + bi) * (c + di) = (ac - bd) + (ad + bc)i real_part = a * c - b * d imag_part = a * d + b * c # 格式化输出 return f"{real_part}+{imag_part}i" ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$,字符串解析和计算都是常数时间。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/construct-binary-tree-from-string.md ================================================ # [0536. 从字符串生成二叉树](https://leetcode.cn/problems/construct-binary-tree-from-string/) - 标签:栈、树、深度优先搜索、字符串、二叉树 - 难度:中等 ## 题目链接 - [0536. 从字符串生成二叉树 - 力扣](https://leetcode.cn/problems/construct-binary-tree-from-string/) ## 题目大意 **描述**: 给定一个包括括号和整数字符串 $s$,你需要根据字符串构造一棵二叉树。 输入的字符串代表一棵二叉树,字符串的格式如下: - 二叉树的节点值用数字表示(可能是负数) - 开头的整数:代表根的值。 - 整数后跟着 0、1 或 2 对括号。 - 括号内表示子树。 - 左子树用一对括号 `()` 包裹。 - 右子树也用一对括号 `()` 包裹。 - 如果只有右子树没有左子树,左子树位置用空括号 `()` 表示。 **要求**: 返回构造的二叉树的根节点。 **说明**: - $0 \le s.length \le 3 \times 10^4$。 - $s$ 只包含数字、`'('`、`')'` 和 `'-'`。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/09/02/butree.jpg) ```python 输入: s = "4(2(3)(1))(6(5))" 输出: [4,2,6,3,1,5] 解释: 4 / \ 2 6 / \ / 3 1 5 ``` - 示例 2: ```python 输入: s = "4(2(3)(1))(6(5)(7))" 输出: [4,2,6,3,1,5,7] ``` ## 解题思路 ### 思路 1:递归解析 使用递归的方式解析字符串: **解题步骤**: 1. 首先解析当前节点的值(可能是多位数或负数)。 2. 如果遇到左括号 `'('`,说明有子树,递归解析。 3. 第一个括号内的是左子树,第二个括号内的是右子树。 4. 使用索引 $index$ 追踪当前解析位置。 关键是正确匹配括号,找到每个子树的范围。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def str2tree(self, s: str) -> Optional[TreeNode]: if not s: return None self.index = 0 def parse(): if self.index >= len(s): return None # 解析节点值(可能是负数或多位数) start = self.index if s[self.index] == '-': self.index += 1 while self.index < len(s) and s[self.index].isdigit(): self.index += 1 val = int(s[start:self.index]) node = TreeNode(val) # 解析左子树 if self.index < len(s) and s[self.index] == '(': self.index += 1 # 跳过 '(' node.left = parse() self.index += 1 # 跳过 ')' # 解析右子树 if self.index < len(s) and s[self.index] == '(': self.index += 1 # 跳过 '(' node.right = parse() self.index += 1 # 跳过 ')' return node return parse() ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度。每个字符最多被访问一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度,递归栈的深度。 ================================================ FILE: docs/solutions/0500-0599/contiguous-array.md ================================================ # [0525. 连续数组](https://leetcode.cn/problems/contiguous-array/) - 标签:数组、哈希表、前缀和 - 难度:中等 ## 题目链接 - [0525. 连续数组 - 力扣](https://leetcode.cn/problems/contiguous-array/) ## 题目大意 给定一个二进制数组 `nums`。 要求:找到含有相同数量 `0` 和 `1` 的最长连续子数组,并返回该子数组的长度。 ## 解题思路 「`0` 和 `1` 数量相同」等价于「`1` 的数量减去 `0` 的数量等于 `0`」。 我们可以使用一个变量 `pre_diff` 来记录下前 `i` 个数中,`1` 的数量比 `0` 的数量多多少个。我们把这个 `pre_diff`叫做「`1` 和 `0` 数量差」,也可以理解为变种的前缀和。 然后我们再用一个哈希表 `pre_dic` 来记录「`1` 和 `0` 数量差」第一次出现的下标。 那么,如果我们在遍历的时候,发现 `pre_diff` 相同的数量差已经在之前出现过了,则说明:这两段之间相减的 `1` 和 `0` 数量差为 `0`。 什么意思呢? 比如说:`j < i`,前 `j` 个数中第一次出现 `pre_diff == 2` ,然后前 `i` 个数中个第二次又出现了 `pre_diff == 2`。那么这两段形成的子数组 `nums[j + 1: i]` 中 `1` 比 `0` 多 `0` 个,则 `0` 和 `1` 数量相同的子数组长度为 `i - j`。 而第二次之所以又出现 `pre_diff == 2` ,是因为前半段子数组 `nums[0: j]` 贡献了相同的差值。 接下来还有一个小问题,如何计算「`1` 和 `0` 数量差」? 我们可以把数组中的 `1` 记为贡献 `+1`,`0` 记为贡献 `-1`。然后使用一个变量 `count`,只要出现 `1` 就让 `count` 加上 `1`,意思是又多出了 `1` 个 `1`。只要出现 `0`,将让 `count` 减去 `1`,意思是 `0` 和之前累积的 `1` 个 `1` 相互抵消掉了。这样遍历完数组,也就计算出了对应的「`1` 和 `0` 数量差」。 整个思路的具体做法如下: - 创建一个哈希表,键值对关系为「`1` 和 `0` 的数量差:最早出现的下标 `i`」。 - 使用变量 `pre_diff` 来计算「`1` 和 `0` 数量差」,使用变量 `count` 来记录 `0` 和 `1` 数量相同的连续子数组的最长长度,然后遍历整个数组。 - 如果 `nums[i] == 1`,则让 `pre_diff += 1`;如果 `nums[i] == 0`,则让 `pre_diff -= 1`。 - 如果在哈希表中发现了相同的 `pre_diff`,则计算相应的子数组长度,与 `count` 进行比较并更新 `count` 值。 - 如果在哈希表中没有发现相同的 `pre_diff`,则在哈希表中记录下第一次出现 `pre_diff` 的下标 `i`。 - 最后遍历完输出 `count`。 > 注意:初始化哈希表为:`pre_dic = {0: -1}`,意思为空数组时,默认「`1` 和 `0` 数量差」为 `0`,且第一次出现的下标为 `-1`。 > > 之所以这样做,是因为在遍历过程中可能会直接出现 `pre_diff == 0` 的情况,这种情况下说明 `nums[0: i]` 中 `0` 和 `1` 数量相同,如果像上边这样初始化后,就可以直接计算出此时子数组长度为 `i - (-1) = i + 1`。 ## 代码 ```python class Solution: def findMaxLength(self, nums: List[int]) -> int: pre_dic = {0: -1} count = 0 pre_sum = 0 for i in range(len(nums)): if nums[i]: pre_sum += 1 else: pre_sum -= 1 if pre_sum in pre_dic: count = max(count, i - pre_dic[pre_sum]) else: pre_dic[pre_sum] = i return count ``` ================================================ FILE: docs/solutions/0500-0599/continuous-subarray-sum.md ================================================ # [0523. 连续的子数组和](https://leetcode.cn/problems/continuous-subarray-sum/) - 标签:数组、哈希表、数学、前缀和 - 难度:中等 ## 题目链接 - [0523. 连续的子数组和 - 力扣](https://leetcode.cn/problems/continuous-subarray-sum/) ## 题目大意 **描述**: 给定一个整数数组 $nums$ 和一个整数 $k$。 **要求**: 如果 $nums$ 有一个「好的子数组」返回 true,否则返回 false: **说明**: - 一个「好的子数组」是: - 长度至少为 $2$ ,且 - 子数组元素总和为 $k$ 的倍数。 - 注意: - 子数组是数组中连续的部分。 - 如果存在一个整数 $n$,令整数 $x$ 符合 $x = n \times k$,则称 $x$ 是 $k$ 的一个倍数。$0$ 始终视为 $k$ 的一个倍数。 - $1 \le nums.length \le 10^{5}$。 - $0 \le nums[i] \le 10^{9}$。 - $0 \le sum(nums[i]) \le 2^{31} - 1$。 - $1 \le k \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:nums = [23,2,4,6,7], k = 6 输出:true 解释:[2,4] 是一个大小为 2 的子数组,并且和为 6 。 ``` - 示例 2: ```python 输入:nums = [23,2,6,4,7], k = 6 输出:true 解释:[23, 2, 6, 4, 7] 是大小为 5 的子数组,并且和为 42 。 42 是 6 的倍数,因为 42 = 7 * 6 且 7 是一个整数。 ``` ## 解题思路 ### 思路 1:前缀和 + 哈希表 这道题要求找到长度至少为 $2$ 的连续子数组,其元素和为 $k$ 的倍数。 核心思路:利用同余定理。如果两个前缀和 $preSum[i]$ 和 $preSum[j]$ 对 $k$ 取模的结果相同,那么区间 $[i+1, j]$ 的元素和就是 $k$ 的倍数。 具体步骤: 1. 使用哈希表 $remainder\_map$ 存储前缀和对 $k$ 取模的余数及其第一次出现的位置。 2. 初始化 $remainder\_map[0] = -1$,表示前缀和为 $0$ 的位置在索引 $-1$(方便处理从索引 $0$ 开始的子数组)。 3. 遍历数组,计算当前前缀和 $preSum$ 对 $k$ 的余数 $remainder$。 4. 如果 $remainder$ 已经在哈希表中,且当前位置与该余数第一次出现位置的距离至少为 $2$,返回 $True$。 5. 如果 $remainder$ 不在哈希表中,将其加入哈希表。 ### 思路 1:代码 ```python class Solution: def checkSubarraySum(self, nums: List[int], k: int) -> bool: # 哈希表存储余数及其第一次出现的位置 remainder_map = {0: -1} pre_sum = 0 for i in range(len(nums)): # 计算前缀和 pre_sum += nums[i] # 计算余数 remainder = pre_sum % k # 如果余数已存在 if remainder in remainder_map: # 检查子数组长度是否至少为 2 if i - remainder_map[remainder] >= 2: return True else: # 记录余数第一次出现的位置 remainder_map[remainder] = i return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组长度,只需遍历一次数组。 - **空间复杂度**:$O(min(n, k))$,哈希表最多存储 $k$ 个不同的余数。 ================================================ FILE: docs/solutions/0500-0599/convert-bst-to-greater-tree.md ================================================ # [0538. 把二叉搜索树转换为累加树](https://leetcode.cn/problems/convert-bst-to-greater-tree/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [0538. 把二叉搜索树转换为累加树 - 力扣](https://leetcode.cn/problems/convert-bst-to-greater-tree/) ## 题目大意 给定一棵二叉搜索树(BST)的根节点,且二叉搜索树的节点值各不相同。要求将其转化为「累加树」,使其每个节点 `node` 的新值等于原树中大于或等于 `node.val` 的值之和。 二叉搜索树的定义: - 如果左子树不为空,则左子树上所有节点值均小于它的根节点值; - 如果右子树不为空,则右子树上所有节点值均大于它的根节点值; - 任意节点的左、右子树也分别为二叉搜索树。 ## 解题思路 题目要求将每个节点的值修改为原来的节点值加上大于它的节点值之和。已知二叉搜索树的中序遍历可以得到一个升序数组。 题目就可以变为:修改升序数组中每个节点值为末尾元素累加和。由于末尾元素累加和的求和过程和遍历顺序相反,所以我们可以考虑换种思路。 二叉搜索树的中序遍历顺序为:左 -> 根 -> 右,从而可以得到一个升序数组,那么我们将左右反着遍历,即顺序为:右 -> 根 -> 左,就可以得到一个降序数组,这样就可以在遍历的同时求前缀和。 当然我们在计算前缀和的时候,需要用到前一个节点的值,所以需要用变量 `pre` 存储前一节点的值。 ## 代码 ```python class Solution: pre = 0 def createBinaryTree(self, root: TreeNode): if not root: return self.createBinaryTree(root.right) root.val += self.pre self.pre = root.val self.createBinaryTree(root.left) def convertBST(self, root: TreeNode) -> TreeNode: self.pre = 0 self.createBinaryTree(root) return root ``` ================================================ FILE: docs/solutions/0500-0599/delete-operation-for-two-strings.md ================================================ # [0583. 两个字符串的删除操作](https://leetcode.cn/problems/delete-operation-for-two-strings/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [0583. 两个字符串的删除操作 - 力扣](https://leetcode.cn/problems/delete-operation-for-two-strings/) ## 题目大意 给定两个单词 `word1` 和 `word2`,找到使得 `word1` 和 `word2` 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。 ## 解题思路 动态规划求解。 先定义状态 `dp[i][j]` 为以 `i - 1` 为结尾的字符串 `word1` 和以 `j - 1` 字结尾的字符串 `word2` 想要达到相等,所需要删除元素的最少次数。 然后确定状态转移方程。 - 如果 `word1[i - 1] == word2[j - 1]`,`dp[i][j]` 取源于以 `i - 2` 结尾结尾的字符串 `word1` 和以 `j - 1` 结尾的字符串 `word2`,即 `dp[i][j] = dp[i - 1][j - 1]`。 - 如果 `word1[i - 1] != word2[j - 1]`,`dp[i][j]` 取源于以下三种情况中的最小情况: - 删除 `word1[i - 1]`,最少操作次数为:`dp[i - 1][j] + 1`。 - 删除 `word2[j - 1]`,最少操作次数为:`dp[i][j - 1] + 1`。 - 同时删除 `word1[i - 1]`、`word2[j - 1]`,最少操作次数为 `dp[i - 1][j - 1] + 2`。 然后确定一下边界条件。 - 当 `word1` 为空字符串,以 `j - 1` 结尾的字符串 `word2` 要删除 `j` 个字符才能和 `word1` 相同,即 `dp[0][j] = j`。 - 当 `word2` 为空字符串,以 `i - 1` 结尾的字符串 `word1` 要删除 `i` 个字符才能和 `word2` 相同,即 `dp[i][0] = i`。 最后递推求解,最终输出 `dp[size1][size2]` 为答案。 ## 代码 ```python class Solution: def minDistance(self, word1: str, word2: str) -> int: size1 = len(word1) size2 = len(word2) dp = [[0 for _ in range(size2 + 1)] for _ in range(size1 + 1)] for i in range(size1 + 1): dp[i][0] = i for j in range(size2 + 1): dp[0][j] = j for i in range(1, size1 + 1): for j in range(1, size2 + 1): if word1[i - 1] == word2[j - 1]: dp[i][j] = dp[i - 1][j - 1] else: dp[i][j] = min(dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1) return dp[size1][size2] ``` ================================================ FILE: docs/solutions/0500-0599/design-in-memory-file-system.md ================================================ # [0588. 设计内存文件系统](https://leetcode.cn/problems/design-in-memory-file-system/) - 标签:设计、字典树、哈希表、字符串、排序 - 难度:困难 ## 题目链接 - [0588. 设计内存文件系统 - 力扣](https://leetcode.cn/problems/design-in-memory-file-system/) ## 题目大意 **描述**: 设计一个内存文件系统,模拟以下功能: **要求**: 实现 FileSystem 类: - `FileSystem()` 初始化系统对象。 - `List ls(String path)` 如果 $path$ 是文件路径,返回一个列表,包含这个文件的名字;如果 $path$ 是目录路径,返回该目录下所有文件和目录的名字,结果按字典序排列。 - `void mkdir(String path)` 根据给定的 $path$ 创建一个新目录。给定的目录路径不存在,如果路径中的某些目录不存在,则需要创建这些目录。 - `void addContentToFile(String filePath, String content)` 如果 $filePath$ 不存在,创建包含给定内容 $content$ 的文件;如果文件已存在,将给定的内容 $content$ 附加到原始内容之后。 - `String readContentFromFile(String filePath)` 返回 $filePath$ 下文件的内容。 **说明**: - $1 \le path.length, filePath.length \le 100$。 - $1 \le content.length \le 50$。 - $path$ 和 $filePath$ 都是绝对路径,除非是根目录 `'/'` 自身,其他路径都是以 `'/'` 开头且不以 `'/'` 结束。 - 可以假定所有操作的参数都是有效的,即用户不会获取不存在文件的内容,或者获取不存在文件夹和文件的列表。 - 可以假定所有文件夹名字和文件名字都只包含小写字母,且同一文件夹下不会有相同名字的文件夹或文件。 - 可以假定 `addContentToFile` 中的文件的父目录都存在。 - `ls`、`mkdir`、`addContentToFile` 和 `readContentFromFile` 最多被调用 $300$ 次。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/28/filesystem.png) ```python 输入: ["FileSystem","ls","mkdir","addContentToFile","ls","readContentFromFile"] [[],["/"],["/a/b/c"],["/a/b/c/d","hello"],["/"],["/a/b/c/d"]] 输出: [null,[],null,null,["a"],"hello"] 解释: FileSystem fileSystem = new FileSystem(); fileSystem.ls("/"); // 返回 [] fileSystem.mkdir("/a/b/c"); fileSystem.addContentToFile("/a/b/c/d", "hello"); fileSystem.ls("/"); // 返回 ["a"] fileSystem.readContentFromFile("/a/b/c/d"); // 返回 "hello" ``` ## 解题思路 ### 思路 1:字典树(Trie) 使用字典树结构来模拟文件系统。每个节点代表一个目录或文件。 核心设计: 1. 定义节点类 `TrieNode`,包含: - $children$:字典,存储子目录和文件。 - $content$:字符串,存储文件内容(目录为空字符串)。 - $is\_file$:布尔值,标识是否为文件。 2. 路径解析:将路径按 `'/'` 分割,过滤空字符串。 3. 各操作实现: - `ls`:遍历到目标节点,如果是文件返回文件名,如果是目录返回排序后的子节点列表。 - `mkdir`:沿路径创建不存在的目录节点。 - `addContentToFile`:沿路径创建节点,最后节点标记为文件并追加内容。 - `readContentFromFile`:遍历到文件节点,返回内容。 ### 思路 1:代码 ```python class TrieNode: def __init__(self): self.children = {} # 子目录/文件 self.content = "" # 文件内容 self.is_file = False # 是否为文件 class FileSystem: def __init__(self): self.root = TrieNode() def ls(self, path: str) -> List[str]: node = self.root # 解析路径 if path != "/": parts = path.split("/") for part in parts: if part: node = node.children[part] # 如果是文件,返回文件名 if node.is_file: return [path.split("/")[-1]] # 如果是目录,返回排序后的子节点列表 return sorted(node.children.keys()) def mkdir(self, path: str) -> None: node = self.root parts = path.split("/") # 沿路径创建目录 for part in parts: if part: if part not in node.children: node.children[part] = TrieNode() node = node.children[part] def addContentToFile(self, filePath: str, content: str) -> None: node = self.root parts = filePath.split("/") # 沿路径创建节点 for part in parts: if part: if part not in node.children: node.children[part] = TrieNode() node = node.children[part] # 标记为文件并追加内容 node.is_file = True node.content += content def readContentFromFile(self, filePath: str) -> str: node = self.root parts = filePath.split("/") # 遍历到文件节点 for part in parts: if part: node = node.children[part] return node.content # Your FileSystem object will be instantiated and called as such: # obj = FileSystem() # param_1 = obj.ls(path) # obj.mkdir(path) # obj.addContentToFile(filePath,content) # param_4 = obj.readContentFromFile(filePath) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `ls`:$O(m + k \log k)$,其中 $m$ 为路径深度,$k$ 为子节点数量(排序)。 - `mkdir`:$O(m)$,其中 $m$ 为路径深度。 - `addContentToFile`:$O(m + |content|)$,其中 $m$ 为路径深度。 - `readContentFromFile`:$O(m)$,其中 $m$ 为路径深度。 - **空间复杂度**:$O(N)$,其中 $N$ 为所有路径和文件内容的总长度。 ================================================ FILE: docs/solutions/0500-0599/detect-capital.md ================================================ # [0520. 检测大写字母](https://leetcode.cn/problems/detect-capital/) - 标签:字符串 - 难度:简单 ## 题目链接 - [0520. 检测大写字母 - 力扣](https://leetcode.cn/problems/detect-capital/) ## 题目大意 **描述**: 我们定义,在以下情况时,单词的大写用法是正确的: - 全部字母都是大写,比如 `"USA"`。 - 所有字母都不是大写,比如 `"leetcode"`。 - 只有首字母大写, 比如 `"Google"`。 给定一个字符串 $word$。 **要求**: 如果大写用法正确,返回 true;否则,返回 false。 **说明**: - $1 \le word.length \le 10^{3}$。 - word 由小写和大写英文字母组成。 **示例**: - 示例 1: ```python 输入:word = "USA" 输出:true ``` - 示例 2: ```python 输入:word = "FlaG" 输出:false ``` ## 解题思路 ### 思路 1:分类判断 根据题目要求,大写用法正确的情况有三种: 1. 全部字母都是大写 2. 所有字母都不是大写 3. 只有首字母大写 我们可以统计字符串中大写字母的数量 $upper\_count$ 和总长度 $n$: - 如果 $upper\_count == 0$:所有字母都不是大写,正确 - 如果 $upper\_count == n$:全部字母都是大写,正确 - 如果 $upper\_count == 1$ 且首字母是大写:只有首字母大写,正确 - 其他情况:不正确 ### 思路 1:代码 ```python class Solution: def detectCapitalUse(self, word: str) -> bool: n = len(word) upper_count = sum(1 for c in word if c.isupper()) # 全部小写 if upper_count == 0: return True # 全部大写 if upper_count == n: return True # 只有首字母大写 if upper_count == 1 and word[0].isupper(): return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度,需要遍历字符串统计大写字母数量。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/diameter-of-binary-tree.md ================================================ # [0543. 二叉树的直径](https://leetcode.cn/problems/diameter-of-binary-tree/) - 标签:树、深度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0543. 二叉树的直径 - 力扣](https://leetcode.cn/problems/diameter-of-binary-tree/) ## 题目大意 **描述**:给一个二叉树的根节点 $root$。 **要求**:计算该二叉树的直径长度。 **说明**: - **二叉树的直径长度**:二叉树中任意两个节点路径长度中的最大值。 - 两节点之间的路径长度是以它们之间边的数目表示。 - 这条路径可能穿过也可能不穿过根节点。 **示例**: - 示例 1: ```python 给定二叉树: 1 / \ 2 3 / \ 4 5 输出:3 解释:该二叉树的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。 ``` ## 解题思路 ### 思路 1:树形 DP + 深度优先搜索 这道题重点是理解直径长度的定义。「二叉树的直径长度」的定义为:二叉树中任意两个节点路径长度中的最大值。并且这条路径可能穿过也可能不穿过根节点。 对于根为 $root$ 的二叉树来说,其直径长度并不简单等于「左子树高度」加上「右子树高度」。 根据路径是否穿过根节点,我们可以将二叉树分为两种: 1. 直径长度所对应的路径穿过根节点。 2. 直径长度所对应的路径不穿过根节点。 我们来看下图中的两个例子。 ![二叉树的直径](https://qcdn.itcharge.cn/images/20230427111005.png) 如图所示,左侧这棵二叉树就是一棵常见的平衡二叉树,其直径长度所对应的路径是穿过根节点的($D\rightarrow B \rightarrow A \rightarrow C$)。这种情况下:$\text{二叉树的直径} = \text{左子树高度} + \text{右子树高度}$。 而右侧这棵特殊的二叉树,其直径长度所对应的路径是没有穿过根节点的($F \rightarrow D \rightarrow B \rightarrow E \rightarrow G$)。这种情况下:$\text{二叉树的直径} = \text{所有子树中最大直径长度}$。 也就是说根为 $root$ 的二叉树的直径长度可能来自于 $\text{左子树高度} + \text{右子树高度}$,也可能来自于 $\text{子树中的最大直径}$,即 $\text{二叉树的直径} = max(\text{左子树高度} + \text{右子树高度}, \quad \text{所有子树中最大直径长度})$。 那么现在问题就变成为如何求「子树的高度」和「子树中的最大直径」。 1. 子树的高度:我们可以利用深度优先搜索方法,递归遍历左右子树,并分别返回左右子树的高度。 2. 子树中的最大直径:我们可以在递归求解子树高度的时候维护一个 $ans$ 变量,用于记录所有 $\text{左子树高度} + \text{右子树高度}$ 中的最大值。 最终 $ans$ 就是我们所求的该二叉树的最大直径,将其返回即可。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def __init__(self): self.ans = 0 def dfs(self, node): if not node: return 0 left_height = self.dfs(node.left) # 左子树高度 right_height = self.dfs(node.right) # 右子树高度 self.ans = max(self.ans, left_height + right_height) # 维护所有路径中的最大直径 return max(left_height, right_height) + 1 # 返回该节点的高度 = 左右子树最大高度 + 1 def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int: self.dfs(root) return self.ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。递归函数需要用到栈空间,栈空间取决于递归深度,最坏情况下递归深度为 $n$,所以空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0500-0599/distribute-candies.md ================================================ # [0575. 分糖果](https://leetcode.cn/problems/distribute-candies/) - 标签:数组、哈希表 - 难度:简单 ## 题目链接 - [0575. 分糖果 - 力扣](https://leetcode.cn/problems/distribute-candies/) ## 题目大意 给定一个偶数长度为 `n` 的数组,其中不同的数字代表不同种类的糖果,每一个数字代表一个糖果。 要求:将这些糖果按种类平均分为一个弟弟和一个妹妹。返回妹妹可以获得的最大糖果的种类数。 ## 解题思路 `n` 个糖果分为两个人,每个人最多只能得到 `n // 2` 个糖果。假设糖果种数为 `m`。则如果糖果种类数大于糖果总数的一半,即 `m > n // 2`,则返回糖果数量的一半就好,也就说糖果总数一半的糖果都可以是不同种类的糖果。妹妹能获得最多 `n // 2` 种糖果。而如果让给种类数小于等于糖果总数的一半,即 `m <= n // 2`,则返回种类数,也就是说妹妹可以最多获得 `m` 种糖果。 综合这两种情况,其最终结果就是 `ans = min(m, n // 2)`。 计算糖果种类可以用 set 集合来做。 ## 代码 ```python class Solution: def distributeCandies(self, candyType: List[int]) -> int: candy_set = set(candyType) return min(len(candyType) // 2, len(candy_set)) ``` ================================================ FILE: docs/solutions/0500-0599/encode-and-decode-tinyurl.md ================================================ # [0535. TinyURL 的加密与解密](https://leetcode.cn/problems/encode-and-decode-tinyurl/) - 标签:设计、哈希表、字符串、哈希函数 - 难度:中等 ## 题目链接 - [0535. TinyURL 的加密与解密 - 力扣](https://leetcode.cn/problems/encode-and-decode-tinyurl/) ## 题目大意 **描述**: TinyURL 是一种 URL 简化服务, 比如:当你输入一个 URL https://leetcode.com/problems/design-tinyurl 时,它将返回一个简化的 URL http://tinyurl.com/4e9iAk。 **要求**: 请你设计一个类来加密与解密 TinyURL。 加密和解密算法如何设计和运作是没有限制的,你只需要保证一个 URL 可以被加密成一个 TinyURL,并且这个 TinyURL 可以用解密方法恢复成原本的 URL。 实现 Solution 类: - `Solution()` 初始化 TinyURL 系统对象。 - `String encode(String longUrl)` 返回 $longUrl$ 对应的 TinyURL。 - `String decode(String shortUrl)` 返回 $shortUrl$ 原本的 URL。题目数据保证给定的 $shortUrl$ 是由同一个系统对象加密的。 **说明**: - $1 \le url.length \le 10^{4}$。 - 题目数据保证 url 是一个有效的 URL。 **示例**: - 示例 1: ```python 输入:url = "https://leetcode.com/problems/design-tinyurl" 输出:"https://leetcode.com/problems/design-tinyurl" 解释: Solution obj = new Solution(); string tiny = obj.encode(url); // 返回加密后得到的 TinyURL 。 string ans = obj.decode(tiny); // 返回解密后得到的原本的 URL 。 ``` ## 解题思路 ### 思路 1:哈希表 + 自增 ID 使用哈希表存储长 URL 和短 URL 的映射关系,使用自增 ID 生成短 URL。 核心思路: 1. 使用两个哈希表: - $long\_to\_short$:存储长 URL 到短 URL 的映射。 - $short\_to\_long$:存储短 URL 到长 URL 的映射。 2. 编码(encode): - 检查长 URL 是否已编码过,如果是则直接返回对应的短 URL。 - 否则,使用自增 ID 生成新的短 URL,格式为 `http://tinyurl.com/{id}`。 - 将映射关系存入两个哈希表。 3. 解码(decode): - 从短 URL 中提取 ID,通过哈希表查找对应的长 URL。 ### 思路 1:代码 ```python class Codec: def __init__(self): self.long_to_short = {} # 长 URL -> 短 URL self.short_to_long = {} # 短 URL -> 长 URL self.id = 0 # 自增 ID self.base_url = "http://tinyurl.com/" def encode(self, longUrl: str) -> str: """Encodes a URL to a shortened URL. """ # 如果已经编码过,直接返回 if longUrl in self.long_to_short: return self.long_to_short[longUrl] # 生成短 URL self.id += 1 short_url = self.base_url + str(self.id) # 存储映射关系 self.long_to_short[longUrl] = short_url self.short_to_long[short_url] = longUrl return short_url def decode(self, shortUrl: str) -> str: """Decodes a shortened URL to its original URL. """ # 从哈希表中查找长 URL return self.short_to_long[shortUrl] # Your Codec object will be instantiated and called as such: # codec = Codec() # codec.decode(codec.encode(url)) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `encode`:$O(1)$,哈希表插入操作。 - `decode`:$O(1)$,哈希表查询操作。 - **空间复杂度**:$O(n)$,其中 $n$ 为编码的 URL 数量。 ================================================ FILE: docs/solutions/0500-0599/erect-the-fence.md ================================================ # [0587. 安装栅栏](https://leetcode.cn/problems/erect-the-fence/) - 标签:几何、数组、数学 - 难度:困难 ## 题目链接 - [0587. 安装栅栏 - 力扣](https://leetcode.cn/problems/erect-the-fence/) ## 题目大意 **描述**: 给定一个数组 $trees$,其中 $trees[i] = [xi, yi]$ 表示树在花园中的位置。 **要求**: 用最短长度的绳子把整个花园围起来,因为绳子很贵。只有把所有的树都围起来,花园才围得很好。 返回恰好位于围栏周边的树木的坐标。 **说明**: - $1 \le points.length \le 3000$。 - $points[i].length == 2$。 - $0 \le xi, yi \le 100$。 - 所有给定的点都是唯一的。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/24/erect2-plane.jpg) ```python 输入: points = [[1,1],[2,2],[2,0],[2,4],[3,3],[4,2]] 输出: [[1,1],[2,0],[3,3],[2,4],[4,2]] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/04/24/erect1-plane.jpg) ```python 输入: points = [[1,2],[2,2],[4,2]] 输出: [[4,2],[2,2],[1,2]] ``` ## 解题思路 ### 思路 1:Graham 扫描算法(凸包) 这是一个经典的凸包问题。凸包是包含所有点的最小凸多边形。 Graham 扫描算法步骤: 1. 找到最左下角的点作为起点。 2. 按照极角排序其他点(相对于起点)。 3. 使用栈维护凸包的顶点。 4. 对于每个点,判断是否需要弹出栈顶(使用叉积判断转向)。 5. 如果是左转或共线,保留;如果是右转,弹出栈顶。 注意:题目要求返回所有在凸包边界上的点,包括共线的点。 ### 思路 1:代码 ```python class Solution: def outerTrees(self, trees: List[List[int]]) -> List[List[int]]: def cross(o, a, b): # 计算向量 OA 和 OB 的叉积 return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) n = len(trees) if n < 4: return trees # 按照 x 坐标排序,x 相同则按 y 排序 trees.sort() # 构建下凸包 lower = [] for p in trees: while len(lower) >= 2 and cross(lower[-2], lower[-1], p) < 0: lower.pop() lower.append(p) # 构建上凸包 upper = [] for p in reversed(trees): while len(upper) >= 2 and cross(upper[-2], upper[-1], p) < 0: upper.pop() upper.append(p) # 合并,去除重复点(首尾点会重复) return list(map(list, set(map(tuple, lower[:-1] + upper[:-1])))) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,主要是排序的时间复杂度,构建凸包是 $O(n)$。 - **空间复杂度**:$O(n)$,需要存储凸包的顶点。 ================================================ FILE: docs/solutions/0500-0599/fibonacci-number.md ================================================ # [0509. 斐波那契数](https://leetcode.cn/problems/fibonacci-number/) - 标签:递归、记忆化搜索、数学、动态规划 - 难度:简单 ## 题目链接 - [0509. 斐波那契数 - 力扣](https://leetcode.cn/problems/fibonacci-number/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:计算第 $n$ 个斐波那契数。 **说明**: - 斐波那契数列的定义如下: - $f(0) = 0, f(1) = 1$。 - $f(n) = f(n - 1) + f(n - 2)$,其中 $n > 1$。 - $0 \le n \le 30$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:1 解释:F(2) = F(1) + F(0) = 1 + 0 = 1 ``` - 示例 2: ```python 输入:n = 3 输出:2 解释:F(3) = F(2) + F(1) = 1 + 1 = 2 ``` ## 解题思路 ### 思路 1:递归算法 根据我们的递推三步走策略,写出对应的递归代码。 1. 写出递推公式:$f(n) = f(n - 1) + f(n - 2)$。 2. 明确终止条件:$f(0) = 0, f(1) = 1$。 3. 翻译为递归代码: 1. 定义递归函数:`fib(self, n)` 表示输入参数为问题的规模 $n$,返回结果为第 $n$ 个斐波那契数。 2. 书写递归主体:`return self.fib(n - 1) + self.fib(n - 2)`。 3. 明确递归终止条件: 1. `if n == 0: return 0` 2. `if n == 1: return 1` ### 思路 1:代码 ```python class Solution: def fib(self, n: int) -> int: if n == 0: return 0 if n == 1: return 1 return self.fib(n - 1) + self.fib(n - 2) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O((\frac{1 + \sqrt{5}}{2})^n)$。具体证明方法参考 [递归求斐波那契数列的时间复杂度,不要被网上的答案误导了 - 知乎](https://zhuanlan.zhihu.com/p/256344121)。 - **空间复杂度**:$O(n)$。每次递归的空间复杂度是 $O(1)$, 调用栈的深度为 $n$,所以总的空间复杂度就是 $O(n)$。 ### 思路 2:动态规划算法 ###### 1. 阶段划分 我们可以按照整数顺序进行阶段划分,将其划分为整数 $0 \sim n$。 ###### 2. 定义状态 定义状态 $dp[i]$ 为:第 $i$ 个斐波那契数。 ###### 3. 状态转移方程 根据题目中所给的斐波那契数列的定义 $f(n) = f(n - 1) + f(n - 2)$,则直接得出状态转移方程为 $dp[i] = dp[i - 1] + dp[i - 2]$。 ###### 4. 初始条件 根据题目中所给的初始条件 $f(0) = 0, f(1) = 1$ 确定动态规划的初始条件,即 $dp[0] = 0, dp[1] = 1$。 ###### 5. 最终结果 根据状态定义,最终结果为 $dp[n]$,即第 $n$ 个斐波那契数为 $dp[n]$。 ### 思路 2:代码 ```python class Solution: def fib(self, n: int) -> int: if n <= 1: return n dp = [0 for _ in range(n + 1)] dp[0] = 0 dp[1] = 1 for i in range(2, n + 1): dp[i] = dp[i - 2] + dp[i - 1] return dp[n] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。因为 $dp[i]$ 的状态只依赖于 $dp[i - 1]$ 和 $dp[i - 2]$,所以可以使用 $3$ 个变量来分别表示 $dp[i]$、$dp[i - 1]$、$dp[i - 2]$,从而将空间复杂度优化到 $O(1)$。 ================================================ FILE: docs/solutions/0500-0599/find-bottom-left-tree-value.md ================================================ # [0513. 找树左下角的值](https://leetcode.cn/problems/find-bottom-left-tree-value/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0513. 找树左下角的值 - 力扣](https://leetcode.cn/problems/find-bottom-left-tree-value/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:找出该二叉树 「最底层」的「最左边」节点的值。 **说明**: - 假设二叉树中至少有一个节点。 - 二叉树的节点个数的范围是 $[1,10^4]$。 - $-2^{31} \le Node.val \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:[1,2,3,4,null,5,6,null,null,7] 输出:7 ``` ![](https://assets.leetcode.com/uploads/2020/12/14/tree2.jpg) ## 解题思路 ### 思路 1:层序遍历 这个问题可以拆分为两个问题: 1. 如何找到「最底层」。 2. 在「最底层」如何找到最左边的节点。 第一个问题,我们可以通过层序遍历直接确定最底层节点。而第二个问题可以通过改变层序遍历的左右节点访问顺序从而找到「最底层」的「最左边节点」。具体方法如下: 1. 对二叉树进行层序遍历。每层元素先访问右节点,再访问左节点。 2. 当遍历到最后一个元素时,此时最后一个元素就是「最底层」的「最左边」节点,即左下角的节点,将该节点的值返回即可。 ### 思路 1:层序遍历代码 ```python import collections class Solution: def findBottomLeftValue(self, root: TreeNode) -> int: if not root: return -1 queue = collections.deque() queue.append(root) while queue: cur = queue.popleft() if cur.right: queue.append(cur.right) if cur.left: queue.append(cur.left) return cur.val ``` ================================================ FILE: docs/solutions/0500-0599/find-largest-value-in-each-tree-row.md ================================================ # [0515. 在每个树行中找最大值](https://leetcode.cn/problems/find-largest-value-in-each-tree-row/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0515. 在每个树行中找最大值 - 力扣](https://leetcode.cn/problems/find-largest-value-in-each-tree-row/) ## 题目大意 给定一棵二叉树的根节点 `root`。 要求:找出二叉树中每一层的最大值。 ## 解题思路 利用队列进行层序遍历,并记录下每一层的最大值,将其存入答案数组中。 ## 代码 ```python ``` ================================================ FILE: docs/solutions/0500-0599/find-mode-in-binary-search-tree.md ================================================ # [0501. 二叉搜索树中的众数](https://leetcode.cn/problems/find-mode-in-binary-search-tree/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:简单 ## 题目链接 - [0501. 二叉搜索树中的众数 - 力扣](https://leetcode.cn/problems/find-mode-in-binary-search-tree/) ## 题目大意 给定一个有相同值的二叉搜索树(BST),要求找出 BST 中所有众数(出现频率最高的元素)。 二叉搜索树定义: - 如果左子树不为空,则左子树上所有节点值均小于它的根节点值; - 如果右子树不为空,则右子树上所有节点值均大于它的根节点值; - 任意节点的左、右子树也分别为二叉搜索树。 ## 解题思路 中序递归遍历二叉搜索树所得到的结果是一个有序数组,所以问题就变为了如何统计有序数组的众数。 定义几个变量。`count` 用来统计当前元素值对应的节点个数,`max_count` 用来元素出现次数最多的次数。数组 `res` 用来存储所有众数结果(因为众数可能不止一个)。 因为中序递归遍历二叉树,比较的元素肯定是相邻节点,所以需要再使用一个变量 `pre` 来指向前一节点。下面就开始愉快的递归了。 - 如果当前节点为空,直接返回。 - 递归遍历左子树。 - 比较当前节点和前一节点: - 如果前一节点为空,则当前元素频率赋值为 1。 - 如果前一节点值与当前节点值相同,则当前元素频率 + 1。 - 如果前一节点值与当前节点值不同,则重新计算当前元素频率,将当前元素频率赋值为 1。 - 判断当前元素频率和最高频率关系: - 如果当前元素频率和最高频率值相等,则将对应元素值加入 res 数组。 - 如果当前元素频率大于最高频率值,则更新最高频率值,并清空原 res 数组,将当前元素加入 res 数组。 - 递归遍历右子树。 最终得到的 res 数组即为所求的众数。 ## 代码 ```python class Solution: res = [] count = 0 max_count = 0 pre = None def search(self, cur: TreeNode): if not cur: return self.search(cur.left) if not self.pre: self.count = 1 elif self.pre.val == cur.val: self.count += 1 else: self.count = 1 self.pre = cur if self.count == self.max_count: self.res.append(cur.val) elif self.count > self.max_count: self.max_count = self.count self.res.clear() self.res.append(cur.val) self.search(cur.right) return def findMode(self, root: TreeNode) -> List[int]: self.count = 0 self.max_count = 0 self.res.clear() self.pre = None self.search(root) return self.res ``` ================================================ FILE: docs/solutions/0500-0599/find-the-closest-palindrome.md ================================================ # [0564. 寻找最近的回文数](https://leetcode.cn/problems/find-the-closest-palindrome/) - 标签:数学、字符串 - 难度:困难 ## 题目链接 - [0564. 寻找最近的回文数 - 力扣](https://leetcode.cn/problems/find-the-closest-palindrome/) ## 题目大意 **描述**: 给定一个表示整数的字符串 $n$。 **要求**: 返回与它最近的回文整数(不包括自身)。如果不止一个,返回较小的那个。 **说明**: - 「最近的」定义为两个整数差的绝对值最小。 - $1 \le n.length \le 18$。 - $n$ 只由数字组成。 - $n$ 不含前导 $0$。 - $n$ 代表在 $[1, 10^{18} - 1]$ 范围内的整数。 **示例**: - 示例 1: ```python 输入: n = "123" 输出: "121" ``` - 示例 2: ```python 输入: n = "1" 输出: "0" 解释: 0 和 2 是最近的回文,但我们返回最小的,也就是 0。 ``` ## 解题思路 ### 思路 1:枚举候选回文数 对于给定的数字 $n$,最近的回文数只可能是以下几种情况之一: 1. $999...999$($n$ 的位数减 $1$)。 2. $100...001$($n$ 的位数加 $1$)。 3. 将 $n$ 的前半部分镜像到后半部分。 4. 将 $n$ 的前半部分加 $1$ 后镜像到后半部分。 5. 将 $n$ 的前半部分减 $1$ 后镜像到后半部分。 生成这些候选数,然后选择与 $n$ 差值最小的(排除 $n$ 本身),如果差值相同则选择较小的。 ### 思路 1:代码 ```python class Solution: def nearestPalindromic(self, n: str) -> str: length = len(n) candidates = set() # 情况 1: 999...999 (length - 1 位) candidates.add(str(10**(length - 1) - 1)) # 情况 2: 100...001 (length + 1 位) candidates.add(str(10**length + 1)) # 情况 3, 4, 5: 基于前半部分的镜像 prefix_length = (length + 1) // 2 prefix = int(n[:prefix_length]) for delta in [-1, 0, 1]: new_prefix = str(prefix + delta) if length % 2 == 0: # 偶数长度 palin = new_prefix + new_prefix[::-1] else: # 奇数长度 palin = new_prefix + new_prefix[-2::-1] candidates.add(palin) # 移除 n 本身 candidates.discard(n) # 找到最近的回文数 num = int(n) result = min(candidates, key=lambda x: (abs(int(x) - num), int(x))) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(L)$,其中 $L$ 是字符串 $n$ 的长度,需要生成和比较常数个候选数。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/fraction-addition-and-subtraction.md ================================================ # [0592. 分数加减运算](https://leetcode.cn/problems/fraction-addition-and-subtraction/) - 标签:数学、字符串、模拟 - 难度:中等 ## 题目链接 - [0592. 分数加减运算 - 力扣](https://leetcode.cn/problems/fraction-addition-and-subtraction/) ## 题目大意 **描述**: 给定一个表示分数加减运算的字符串 $expression$。 **要求**: 返回一个字符串形式的计算结果。 这个结果应该是不可约分的分数,即「最简分数」。 如果最终结果是一个整数,例如 $2$,你需要将它转换成分数形式,其分母为 $1$。所以在上述例子中, $2$ 应该被转换为 $2/1$。 **说明**: - 输入和输出字符串只包含 `0` 到 `9` 的数字,以及 `/`, `+` 和 `-`。 - 输入和输出分数格式均为 ±分子/分母。如果输入的第一个分数或者输出的分数是正数,则 `+` 会被省略掉。 - 输入只包含合法的 最简分数,每个分数的分子与分母的范围是 $[1,10]$。 如果分母是 $1$,意味着这个分数实际上是一个整数。 - 输入的分数个数范围是 $[1,10]$。 - 最终结果的分子与分母保证是 32 位整数范围内的有效整数。 **示例**: - 示例 1: ```python 输入: expression = "-1/2+1/2" 输出: "0/1" ``` - 示例 2: ```python 输入: expression = "-1/2+1/2+1/3" 输出: "1/3" ``` ## 解题思路 ### 思路 1:解析字符串并计算 我们需要解析字符串,提取所有分数,然后进行加减运算。 步骤: 1. 解析字符串,提取所有分数。每个分数格式为 $\pm numerator/denominator$,第一个分数可能没有符号(默认为正)。 2. 将所有分数通分后相加。通分需要找到所有分母的最小公倍数 $lcm$,然后将每个分数的分子乘以 $lcm / denominator$。 3. 计算所有分子的和 $sum\_numerator$。 4. 将结果约分到最简形式。需要计算 $gcd(sum\_numerator, lcm)$,然后分子分母同时除以最大公约数。 ### 思路 1:代码 ```python import re from math import gcd class Solution: def fractionAddition(self, expression: str) -> str: # 解析所有分数:提取分子和分母 fractions = re.findall(r'([+-]?\d+)/(\d+)', expression) # 转换为整数列表 numerators = [] denominators = [] for num_str, den_str in fractions: numerators.append(int(num_str)) denominators.append(int(den_str)) # 计算所有分母的最小公倍数 def lcm(a, b): return a * b // gcd(a, b) common_denominator = 1 for den in denominators: common_denominator = lcm(common_denominator, den) # 将所有分数通分并求和 total_numerator = 0 for i in range(len(numerators)): total_numerator += numerators[i] * (common_denominator // denominators[i]) # 约分到最简形式 g = gcd(abs(total_numerator), common_denominator) total_numerator //= g common_denominator //= g return f"{total_numerator}/{common_denominator}" ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log M)$,其中 $n$ 是分数个数,$M$ 是分母的最大值。需要计算最小公倍数和最大公约数。 - **空间复杂度**:$O(n)$,存储所有分数的分子和分母。 ================================================ FILE: docs/solutions/0500-0599/freedom-trail.md ================================================ # [0514. 自由之路](https://leetcode.cn/problems/freedom-trail/) - 标签:深度优先搜索、广度优先搜索、字符串、动态规划 - 难度:困难 ## 题目链接 - [0514. 自由之路 - 力扣](https://leetcode.cn/problems/freedom-trail/) ## 题目大意 **描述**: 电子游戏「辐射4」中,任务「通向自由」要求玩家到达名为 `Freedom Trail Ring` 的金属表盘,并使用表盘拼写特定关键词才能开门。 给定一个字符串 $ring$ ,表示刻在外环上的编码;给定另一个字符串 $key$,表示需要拼写的关键词。 **要求**: 算出能够拼写关键词中所有字符的最少步数。 最初,$ring$ 的第一个字符与 12:00 方向对齐。您需要顺时针或逆时针旋转 $ring$ 以使 $key$ 的一个字符在 12:00 方向对齐,然后按下中心按钮,以此逐个拼写完 $key$ 中的所有字符。 旋转 $ring$ 拼出 $key$ 字符 $key[i]$ 的阶段中: 1. 您可以将 $ring$ 顺时针或逆时针旋转 一个位置 ,计为 1 步。旋转的最终目的是将字符串 $ring$ 的一个字符与 12:00 方向对齐,并且这个字符必须等于字符 $key[i]$。 2. 如果字符 $key[i]$ 已经对齐到 12:00 方向,您需要按下中心按钮进行拼写,这也将算作 1 步。按完之后,您可以开始拼写 $key$ 的下一个字符(下一阶段),直至完成所有拼写。 **说明**: - $1 \le ring.length, key.length \le 10^{3}$。 - $ring$ 和 $key$ 只包含小写英文字母。 - 保证字符串 $key$ 一定可以由字符串 $ring$ 旋转拼出。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2018/10/22/ring.jpg) ```python 输入: ring = "godding", key = "gd" 输出: 4 解释: 对于 key 的第一个字符 'g',已经在正确的位置, 我们只需要1步来拼写这个字符。 对于 key 的第二个字符 'd',我们需要逆时针旋转 ring "godding" 2步使它变成 "ddinggo"。 当然, 我们还需要1步进行拼写。 因此最终的输出是 4。 ``` - 示例 2: ```python 输入: ring = "godding", key = "godding" 输出: 13 ``` ## 解题思路 ### 思路 1:动态规划 定义 $dp[i][j]$ 表示拼写完 $key$ 的前 $i$ 个字符,且 $ring$ 的第 $j$ 个字符对齐到 12:00 方向时的最少步数。 状态转移: - 对于 $key[i]$,需要找到 $ring$ 中所有等于 $key[i]$ 的位置 - 对于每个这样的位置 $k$,可以从上一个状态 $dp[i-1][j]$ 转移过来 - 转移代价 = 旋转步数 + 按下按钮的 1 步 - 旋转步数 = $\min(|k - j|, len(ring) - |k - j|)$(顺时针或逆时针的最小值) 初始状态:$dp[0][j] = \min(j, len(ring) - j) + 1$(从位置 0 旋转到位置 $j$ 并按下按钮) ### 思路 1:代码 ```python class Solution: def findRotateSteps(self, ring: str, key: str) -> int: from collections import defaultdict n, m = len(ring), len(key) # 记录每个字符在 ring 中的所有位置 pos = defaultdict(list) for i, ch in enumerate(ring): pos[ch].append(i) # dp[i][j] 表示拼写完 key 的前 i 个字符,ring 的第 j 个字符对齐时的最少步数 dp = [[float('inf')] * n for _ in range(m + 1)] dp[0][0] = 0 for i in range(m): # 对于 key[i],找到 ring 中所有等于 key[i] 的位置 for k in pos[key[i]]: # 从上一个状态转移 for j in range(n): if dp[i][j] != float('inf'): # 计算从位置 j 旋转到位置 k 的最少步数 dist = min(abs(k - j), n - abs(k - j)) dp[i + 1][k] = min(dp[i + 1][k], dp[i][j] + dist + 1) # 返回拼写完所有字符的最少步数 return min(dp[m]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n^2)$,其中 $m$ 是 $key$ 的长度,$n$ 是 $ring$ 的长度。 - **空间复杂度**:$O(m \times n)$,需要存储 DP 数组。 ================================================ FILE: docs/solutions/0500-0599/index.md ================================================ ## 本章内容 - [0500. 键盘行](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/keyboard-row.md) - [0501. 二叉搜索树中的众数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/find-mode-in-binary-search-tree.md) - [0502. IPO](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/ipo.md) - [0503. 下一个更大元素 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/next-greater-element-ii.md) - [0504. 七进制数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/base-7.md) - [0505. 迷宫 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/the-maze-ii.md) - [0506. 相对名次](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/relative-ranks.md) - [0507. 完美数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/perfect-number.md) - [0508. 出现次数最多的子树元素和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/most-frequent-subtree-sum.md) - [0509. 斐波那契数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/fibonacci-number.md) - [0510. 二叉搜索树中的中序后继 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/inorder-successor-in-bst-ii.md) - [0513. 找树左下角的值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/find-bottom-left-tree-value.md) - [0514. 自由之路](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/freedom-trail.md) - [0515. 在每个树行中找最大值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/find-largest-value-in-each-tree-row.md) - [0516. 最长回文子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-palindromic-subsequence.md) - [0517. 超级洗衣机](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/super-washing-machines.md) - [0518. 零钱兑换 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/coin-change-ii.md) - [0519. 随机翻转矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/random-flip-matrix.md) - [0520. 检测大写字母](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/detect-capital.md) - [0521. 最长特殊序列 Ⅰ](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-uncommon-subsequence-i.md) - [0522. 最长特殊序列 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-uncommon-subsequence-ii.md) - [0523. 连续的子数组和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/continuous-subarray-sum.md) - [0524. 通过删除字母匹配到字典里最长单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-word-in-dictionary-through-deleting.md) - [0525. 连续数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/contiguous-array.md) - [0526. 优美的排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/beautiful-arrangement.md) - [0527. 单词缩写](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/word-abbreviation.md) - [0528. 按权重随机选择](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/random-pick-with-weight.md) - [0529. 扫雷游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/minesweeper.md) - [0530. 二叉搜索树的最小绝对差](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/minimum-absolute-difference-in-bst.md) - [0531. 孤独像素 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/lonely-pixel-i.md) - [0532. 数组中的 k-diff 数对](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/k-diff-pairs-in-an-array.md) - [0533. 孤独像素 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/lonely-pixel-ii.md) - [0535. TinyURL 的加密与解密](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/encode-and-decode-tinyurl.md) - [0536. 从字符串生成二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/construct-binary-tree-from-string.md) - [0537. 复数乘法](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/complex-number-multiplication.md) - [0538. 把二叉搜索树转换为累加树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/convert-bst-to-greater-tree.md) - [0539. 最小时间差](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/minimum-time-difference.md) - [0540. 有序数组中的单一元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/single-element-in-a-sorted-array.md) - [0541. 反转字符串 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/reverse-string-ii.md) - [0542. 01 矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/01-matrix.md) - [0543. 二叉树的直径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/diameter-of-binary-tree.md) - [0544. 输出比赛匹配对](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/output-contest-matches.md) - [0545. 二叉树的边界](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/boundary-of-binary-tree.md) - [0546. 移除盒子](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/remove-boxes.md) - [0547. 省份数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/number-of-provinces.md) - [0548. 将数组分割成和相等的子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/split-array-with-equal-sum.md) - [0549. 二叉树最长连续序列 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/binary-tree-longest-consecutive-sequence-ii.md) - [0551. 学生出勤记录 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/student-attendance-record-i.md) - [0552. 学生出勤记录 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/student-attendance-record-ii.md) - [0553. 最优除法](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/optimal-division.md) - [0554. 砖墙](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/brick-wall.md) - [0555. 分割连接字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/split-concatenated-strings.md) - [0556. 下一个更大元素 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/next-greater-element-iii.md) - [0557. 反转字符串中的单词 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/reverse-words-in-a-string-iii.md) - [0558. 四叉树交集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/logical-or-of-two-binary-grids-represented-as-quad-trees.md) - [0559. N 叉树的最大深度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/maximum-depth-of-n-ary-tree.md) - [0560. 和为 K 的子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subarray-sum-equals-k.md) - [0561. 数组拆分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/array-partition.md) - [0562. 矩阵中最长的连续1线段](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-line-of-consecutive-one-in-matrix.md) - [0563. 二叉树的坡度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/binary-tree-tilt.md) - [0564. 寻找最近的回文数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/find-the-closest-palindrome.md) - [0565. 数组嵌套](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/array-nesting.md) - [0566. 重塑矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/reshape-the-matrix.md) - [0567. 字符串的排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/permutation-in-string.md) - [0568. 最大休假天数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/maximum-vacation-days.md) - [0572. 另一棵树的子树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/subtree-of-another-tree.md) - [0573. 松鼠模拟](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/squirrel-simulation.md) - [0575. 分糖果](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/distribute-candies.md) - [0576. 出界的路径数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/out-of-boundary-paths.md) - [0581. 最短无序连续子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/shortest-unsorted-continuous-subarray.md) - [0582. 杀掉进程](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/kill-process.md) - [0583. 两个字符串的删除操作](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/delete-operation-for-two-strings.md) - [0587. 安装栅栏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/erect-the-fence.md) - [0588. 设计内存文件系统](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/design-in-memory-file-system.md) - [0589. N 叉树的前序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/n-ary-tree-preorder-traversal.md) - [0590. N 叉树的后序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/n-ary-tree-postorder-traversal.md) - [0591. 标签验证器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/tag-validator.md) - [0592. 分数加减运算](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/fraction-addition-and-subtraction.md) - [0593. 有效的正方形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/valid-square.md) - [0594. 最长和谐子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/longest-harmonious-subsequence.md) - [0598. 区间加法 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/range-addition-ii.md) - [0599. 两个列表的最小索引总和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/minimum-index-sum-of-two-lists.md) ================================================ FILE: docs/solutions/0500-0599/inorder-successor-in-bst-ii.md ================================================ # [0510. 二叉搜索树中的中序后继 II](https://leetcode.cn/problems/inorder-successor-in-bst-ii/) - 标签:树、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [0510. 二叉搜索树中的中序后继 II - 力扣](https://leetcode.cn/problems/inorder-successor-in-bst-ii/) ## 题目大意 **描述**: 给定一个二叉搜索树的节点 $node$,节点包含指向父节点的指针 $parent$。找到该节点在中序遍历中的后继节点。 节点的中序后继是中序遍历序列中该节点的下一个节点。如果不存在后继节点,返回 $null$。 **要求**: 返回给定节点的中序后继节点。 **说明**: - 树中节点数在范围 $[1, 10^4]$ 内。 - $-10^5 \le Node.val \le 10^5$。 - 所有节点的值都是唯一的。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2019/01/23/285_example_1.PNG) ```python 输入:tree = [2,1,3], node = 1 输出:2 解析:1 的中序后继结点是 2 。注意节点和返回值都是 Node 类型的。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2019/01/23/285_example_2.PNG) ```python 输入:tree = [5,3,6,2,4,null,null,1], node = 6 输出:null 解析:该结点没有中序后继,因此返回 null 。 ``` ## 解题思路 ### 思路 1:分情况讨论 在二叉搜索树中,节点的中序后继有两种情况: 1. **如果节点有右子树**:后继节点是右子树中最左边的节点。 2. **如果节点没有右子树**:需要向上找父节点,直到找到一个节点是其父节点的左子节点,该父节点就是后继节点。 利用父节点指针,可以在不使用递归栈的情况下找到后继节点。 ### 思路 1:代码 ```python """ # Definition for a Node. class Node: def __init__(self, val): self.val = val self.left = None self.right = None self.parent = None """ class Solution: def inorderSuccessor(self, node: 'Node') -> 'Optional[Node]': # 情况 1:如果有右子树,找右子树中最左边的节点 if node.right: node = node.right while node.left: node = node.left return node # 情况 2:没有右子树,向上找父节点 # 找到第一个是其父节点左子节点的节点 while node.parent and node == node.parent.right: node = node.parent return node.parent ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(h)$,其中 $h$ 是树的高度。最坏情况下需要从叶子节点遍历到根节点。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/ipo.md ================================================ # [0502. IPO](https://leetcode.cn/problems/ipo/) - 标签:贪心、数组、排序、堆(优先队列) - 难度:困难 ## 题目链接 - [0502. IPO - 力扣](https://leetcode.cn/problems/ipo/) ## 题目大意 **描述**: 假设 力扣(LeetCode)即将开始 IPO。为了以更高的价格将股票卖给风险投资公司,力扣希望在 IPO 之前开展一些项目以增加其资本。由于资源有限,它只能在 IPO 之前完成最多 $k$ 个不同的项目。帮助力扣设计完成最多 $k$ 个不同项目后得到最大总资本的方式。 给定 $n$ 个项目。对于每个项目 $i$,它都有一个纯利润 $profits[i]$,和启动该项目需要的最小资本 $capital[i]$。 最初,你的资本为 $w$。当你完成一个项目时,你将获得纯利润,且利润将被添加到你的总资本中。 **要求**: 总而言之,从给定项目中选择 最多 $k$ 个不同项目的列表,以「最大化最终资本」,并输出最终可获得的最多资本。 答案保证在 32 位有符号整数范围内。 **说明**: - $1 \le k \le 10^{5}$。 - $0 \le w \le 10^{9}$。 - $n == profits.length$。 - $n == capital.length$。 - $1 \le n \le 10^{5}$。 - $0 \le profits[i] \le 10^{4}$。 - $0 \le capital[i] \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:k = 2, w = 0, profits = [1,2,3], capital = [0,1,1] 输出:4 解释: 由于你的初始资本为 0,你仅可以从 0 号项目开始。 在完成后,你将获得 1 的利润,你的总资本将变为 1。 此时你可以选择开始 1 号或 2 号项目。 由于你最多可以选择两个项目,所以你需要完成 2 号项目以获得最大的资本。 因此,输出最后最大化的资本,为 0 + 1 + 3 = 4。 ``` - 示例 2: ```python 输入:k = 3, w = 0, profits = [1,2,3], capital = [0,1,2] 输出:6 ``` ## 解题思路 ### 思路 1:贪心 + 堆(优先队列) 这道题的核心是在有限资本下,选择利润最大的项目。使用贪心策略:每次选择当前资本能启动的项目中利润最大的。 核心思路: 1. 将所有项目按启动资本从小到大排序。 2. 使用最大堆存储当前资本能启动的所有项目的利润。 3. 重复 $k$ 次: - 将所有启动资本不超过当前资本 $w$ 的项目的利润加入最大堆。 - 从最大堆中取出利润最大的项目,将利润加到当前资本 $w$ 上。 - 如果堆为空,说明没有可启动的项目,提前结束。 ### 思路 1:代码 ```python import heapq class Solution: def findMaximizedCapital(self, k: int, w: int, profits: List[int], capital: List[int]) -> int: n = len(profits) # 将项目按启动资本排序 projects = sorted(zip(capital, profits)) # 最大堆(Python 的 heapq 是最小堆,所以存负值) max_heap = [] idx = 0 # 最多完成 k 个项目 for _ in range(k): # 将所有当前资本能启动的项目加入堆 while idx < n and projects[idx][0] <= w: # 存入负利润(实现最大堆) heapq.heappush(max_heap, -projects[idx][1]) idx += 1 # 如果没有可启动的项目,提前结束 if not max_heap: break # 选择利润最大的项目 w += -heapq.heappop(max_heap) return w ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n + k \log n)$,其中 $n$ 为项目数量。排序需要 $O(n \log n)$,每次堆操作需要 $O(\log n)$,最多进行 $k$ 次。 - **空间复杂度**:$O(n)$,排序和堆的空间开销。 ================================================ FILE: docs/solutions/0500-0599/k-diff-pairs-in-an-array.md ================================================ # [0532. 数组中的 k-diff 数对](https://leetcode.cn/problems/k-diff-pairs-in-an-array/) - 标签:数组、哈希表、双指针、二分查找、排序 - 难度:中等 ## 题目链接 - [0532. 数组中的 k-diff 数对 - 力扣](https://leetcode.cn/problems/k-diff-pairs-in-an-array/) ## 题目大意 **描述**: 给定一个整数数组 $nums$ 和一个整数 $k$。 **要求**: 请你在数组中找出 不同的 $k - diff$ 数对,并返回不同的 $k - diff$ 数对的数目。 **说明**: - $k - diff$ 数对定义为一个整数对 ($nums[i], nums[j]$) ,并满足下述全部条件: - $0 \le i, j < nums.length$ - $i \ne j$ - $|nums[i] - nums[j]| == k$ - 注意,$|val|$ 表示 $val$ 的绝对值。 - $1 \le nums.length \le 10^{4}$。 - $-10^{7} \le nums[i] \le 10^{7}$。 - $0 \le k \le 10^{7}$。 **示例**: - 示例 1: ```python 输入:nums = [3, 1, 4, 1, 5], k = 2 输出:2 解释:数组中有两个 2-diff 数对, (1, 3) 和 (3, 5)。 尽管数组中有两个 1 ,但我们只应返回不同的数对的数量。 ``` - 示例 2: ```python 输入:nums = [1, 2, 3, 4, 5], k = 1 输出:4 解释:数组中有四个 1-diff 数对, (1, 2), (2, 3), (3, 4) 和 (4, 5) 。 ``` ## 解题思路 ### 思路 1:哈希表 对于 $k-diff$ 数对,需要满足 $|nums[i] - nums[j]| = k$,即 $nums[i] = nums[j] + k$ 或 $nums[i] = nums[j] - k$。 使用哈希表统计每个数字出现的次数。对于每个数字 $num$: - 如果 $k = 0$,需要 $num$ 出现至少 2 次才能形成数对 - 如果 $k > 0$,检查 $num + k$ 是否存在(避免重复,只检查 $num + k$) 使用集合记录已经处理过的数对,避免重复计数。 ### 思路 1:代码 ```python from collections import Counter class Solution: def findPairs(self, nums: List[int], k: int) -> int: count = Counter(nums) result = 0 if k == 0: # k = 0 时,需要数字出现至少 2 次 for num, cnt in count.items(): if cnt >= 2: result += 1 else: # k > 0 时,检查 num + k 是否存在 for num in count: if num + k in count: result += 1 return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组长度,需要遍历数组统计频率,然后遍历哈希表。 - **空间复杂度**:$O(n)$,哈希表最多存储 $n$ 个不同的数字。 ================================================ FILE: docs/solutions/0500-0599/keyboard-row.md ================================================ # [0500. 键盘行](https://leetcode.cn/problems/keyboard-row/) - 标签:数组、哈希表、字符串 - 难度:简单 ## 题目链接 - [0500. 键盘行 - 力扣](https://leetcode.cn/problems/keyboard-row/) ## 题目大意 **描述**: 给定一个字符串数组 $words$。 **要求**: 只返回可以使用在「美式键盘」同一行的字母打印出来的单词。键盘如下图所示。 ![](https://assets.leetcode.cn/aliyun-lc-upload/uploads/2018/10/12/keyboard.png) **说明**: - 字符串「不区分大小写」,相同字母的大小写形式都被视为在同一行。 - 美式键盘中: - 第一行由字符 `"qwertyuiop"` 组成。 - 第二行由字符 `"asdfghjkl"` 组成。 - 第三行由字符 `"zxcvbnm"` 组成。 - $1 \le words.length \le 20$。 - $1 \le words[i].length \le 10^{3}$。 - $words[i]$ 由英文字母(小写和大写字母)组成。 **示例**: - 示例 1: ```python 输入:words = ["Hello","Alaska","Dad","Peace"] 输出:["Alaska","Dad"] 解释: 由于不区分大小写,"a" 和 "A" 都在美式键盘的第二行。 ``` - 示例 2: ```python 输入:words = ["omk"] 输出:[] ``` ## 解题思路 ### 思路 1:哈希表映射 将键盘的三行字符分别存储,使用哈希表记录每个字符属于哪一行。对于每个单词,检查其所有字符(转换为小写)是否都在同一行。 具体步骤: 1. 定义三行字符:`row1 = "qwertyuiop", row2 = "asdfghjkl", row3 = "zxcvbnm"`。 2. 创建哈希表,将每个字符映射到其所在行号。 3. 遍历每个单词,检查所有字符是否在同一行。 4. 如果都在同一行,加入结果列表。 ### 思路 1:代码 ```python class Solution: def findWords(self, words: List[str]) -> List[str]: # 定义键盘三行 row1 = set("qwertyuiop") row2 = set("asdfghjkl") row3 = set("zxcvbnm") result = [] for word in words: # 转换为小写 word_lower = word.lower() # 判断单词的所有字符是否在同一行 if all(c in row1 for c in word_lower) or \ all(c in row2 for c in word_lower) or \ all(c in row3 for c in word_lower): result.append(word) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是单词数量,$m$ 是平均单词长度,需要检查每个单词的每个字符。 - **空间复杂度**:$O(1)$,只使用了常数额外空间存储三行字符集合。 ================================================ FILE: docs/solutions/0500-0599/kill-process.md ================================================ # [0582. 杀掉进程](https://leetcode.cn/problems/kill-process/) - 标签:树、深度优先搜索、广度优先搜索、数组、哈希表 - 难度:中等 ## 题目链接 - [0582. 杀掉进程 - 力扣](https://leetcode.cn/problems/kill-process/) ## 题目大意 **描述**: 给定一个进程 ID 列表 $pid$ 和对应的父进程 ID 列表 $ppid$,其中 $pid[i]$ 是第 $i$ 个进程的 ID,$ppid[i]$ 是第 $i$ 个进程的父进程 ID。 在给定一个要杀掉的进程 ID $kill$。 当一个进程被杀掉时,它的所有子进程和后代进程也会被杀掉。 **要求**: 返回被杀掉的所有进程的 ID 列表(包括 $kill$ 本身)。 **说明**: - $n == pid.length$。 - $n == ppid.length$。 - $1 \le n \le 5 \times 10^4$。 - $1 \le pid[i] \le 5 \times 10^4$。 - $0 \le ppid[i] \le 5 \times 10^4$。 - 只有一个进程的父进程 ID 为 0,表示根进程。 - 所有 $pid$ 都是唯一的。 - 题目数据保证 $kill$ 在 $pid$ 中。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/24/ptree.jpg) ```python 输入:pid = [1,3,10,5], ppid = [3,0,5,3], kill = 5 输出:[5,10] 解释: 进程树: 3 / \ 1 5 / 10 杀掉进程 5,同时杀掉其子进程 10。 ``` - 示例 2: ```python 输入:pid = [1], ppid = [0], kill = 1 输出:[1] ``` ## 解题思路 ### 思路 1:构建进程树 + DFS 给定进程 ID 列表和父进程 ID 列表,以及要杀掉的进程 ID,需要返回该进程及其所有子进程。 **解题步骤**: 1. 使用哈希表构建进程树:$parent\_to\_children[parent\_id] = [child\_id1, child\_id2, ...]$。 2. 从要杀掉的进程 $kill$ 开始,使用 DFS 或 BFS 遍历所有子进程。 3. 将遍历到的所有进程 ID 加入结果列表。 ### 思路 1:代码 ```python from collections import defaultdict class Solution: def killProcess(self, pid: List[int], ppid: List[int], kill: int) -> List[int]: # 构建进程树:parent -> children tree = defaultdict(list) for i in range(len(pid)): tree[ppid[i]].append(pid[i]) result = [] # DFS 遍历所有子进程 def dfs(process_id: int): result.append(process_id) # 递归处理所有子进程 for child in tree[process_id]: dfs(child) dfs(kill) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是进程数量。需要遍历所有进程构建树,然后遍历要杀掉的进程的所有子进程。 - **空间复杂度**:$O(n)$,哈希表存储进程树,递归栈的深度最多为 $n$。 ================================================ FILE: docs/solutions/0500-0599/logical-or-of-two-binary-grids-represented-as-quad-trees.md ================================================ # [0558. 四叉树交集](https://leetcode.cn/problems/logical-or-of-two-binary-grids-represented-as-quad-trees/) - 标签:树、分治 - 难度:中等 ## 题目链接 - [0558. 四叉树交集 - 力扣](https://leetcode.cn/problems/logical-or-of-two-binary-grids-represented-as-quad-trees/) ## 题目大意 **描述**: 二进制矩阵中的所有元素不是 $0$ 就是 $1$。 给定两个四叉树,$quadTree1$ 和 $quadTree2$。其中 $quadTree1$ 表示一个 $n \times n$ 二进制矩阵,而 $quadTree2$ 表示另一个 $n \times n$ 二进制矩阵。 **要求**: 返回一个表示 $n \times n$ 二进制矩阵的四叉树,它是 $quadTree1$ 和 $quadTree2$ 所表示的两个二进制矩阵进行「按位逻辑或运算」的结果。 注意,当 $isLeaf$ 为 False 时,你可以把 True 或者 False 赋值给节点,两种值都会被判题机制接受。 四叉树数据结构中,每个内部节点只有四个子节点。此外,每个节点都有两个属性: - $val$:储存叶子结点所代表的区域的值。1 对应 True,0 对应 False; - $isLeaf$: 当这个节点是一个叶子结点时为 True,如果它有 4 个子节点则为 False。 ```Java class Node { public boolean val; public boolean isLeaf; public Node topLeft; public Node topRight; public Node bottomLeft; public Node bottomRight; } ``` 我们可以按以下步骤为二维区域构建四叉树: 1. 如果当前网格的值相同(即,全为 $0$ 或者全为 $1$),将 $isLeaf$ 设为 True,将 $val$ 设为网格相应的值,并将四个子节点都设为 $Null$ 然后停止。 2. 如果当前网格的值不同,将 $isLeaf$ 设为 False,将 $val$ 设为任意值,然后如下图所示,将当前网格划分为四个子网格。 3. 使用适当的子网格递归每个子节点。 ![](https://assets.leetcode.com/uploads/2020/02/11/new_top.png) **说明**: - 四叉树格式: - 输出为使用层序遍历后四叉树的序列化形式,其中 $null$ 表示路径终止符,其下面不存在节点。 - 它与二叉树的序列化非常相似。唯一的区别是节点以列表形式表示 $[isLeaf, val]$。 - 如果 $isLeaf$ 或者 $val$ 的值为 True,则表示它在列表 $[isLeaf, val]$ 中的值为 1;如果 $isLeaf$ 或者 $val$ 的值为 False,则表示值为 0。 - quadTree1 和 quadTree2 都是符合题目要求的四叉树,每个都代表一个 $n \times n$ 的矩阵。 - $n == 2x$,其中 $0 \le x \le 9$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/02/11/qt1.png) ![](https://assets.leetcode.com/uploads/2020/02/11/qt2.png) ```python 输入:quadTree1 = [[0,1],[1,1],[1,1],[1,0],[1,0]] , quadTree2 = [[0,1],[1,1],[0,1],[1,1],[1,0],null,null,null,null,[1,0],[1,0],[1,1],[1,1]] 输出:[[0,0],[1,1],[1,1],[1,1],[1,0]] 解释:quadTree1 和 quadTree2 如上所示。由四叉树所表示的二进制矩阵也已经给出。 如果我们对这两个矩阵进行按位逻辑或运算,则可以得到下面的二进制矩阵,由一个作为结果的四叉树表示。 注意,我们展示的二进制矩阵仅仅是为了更好地说明题意,你无需构造二进制矩阵来获得结果四叉树。 ``` ![](https://assets.leetcode.com/uploads/2020/02/11/qtr.png) - 示例 2: ```python 输入:quadTree1 = [[1,0]] , quadTree2 = [[1,0]] 输出:[[1,0]] 解释:两个数所表示的矩阵大小都为 1*1,值全为 0 结果矩阵大小为 1*1,值全为 0。 ``` ## 解题思路 ### 思路 1:递归 + 分治 四叉树的逻辑或运算可以通过递归实现。对于两个四叉树节点,按照以下规则合并: 核心规则: 1. 如果其中一个节点是叶子节点且值为 $True$(表示全为 $1$),结果就是该节点。 2. 如果其中一个节点是叶子节点且值为 $False$(表示全为 $0$),结果就是另一个节点。 3. 如果两个节点都是叶子节点: - 如果值相同,返回任意一个。 - 如果值不同,返回值为 $True$ 的节点。 4. 如果两个节点都不是叶子节点,递归处理四个子节点。 5. 递归后检查四个子节点是否都是叶子节点且值相同,如果是则合并为一个叶子节点。 ### 思路 1:代码 ```python """ # Definition for a QuadTree node. class Node: def __init__(self, val, isLeaf, topLeft, topRight, bottomLeft, bottomRight): self.val = val self.isLeaf = isLeaf self.topLeft = topLeft self.topRight = topRight self.bottomLeft = bottomLeft self.bottomRight = bottomRight """ class Solution: def intersect(self, quadTree1: 'Node', quadTree2: 'Node') -> 'Node': # 如果 quadTree1 是叶子节点 if quadTree1.isLeaf: # 如果值为 True,整个区域都是 1,返回 quadTree1 if quadTree1.val: return quadTree1 # 如果值为 False,结果取决于 quadTree2 else: return quadTree2 # 如果 quadTree2 是叶子节点 if quadTree2.isLeaf: # 如果值为 True,整个区域都是 1,返回 quadTree2 if quadTree2.val: return quadTree2 # 如果值为 False,结果取决于 quadTree1 else: return quadTree1 # 两个都不是叶子节点,递归处理四个子节点 top_left = self.intersect(quadTree1.topLeft, quadTree2.topLeft) top_right = self.intersect(quadTree1.topRight, quadTree2.topRight) bottom_left = self.intersect(quadTree1.bottomLeft, quadTree2.bottomLeft) bottom_right = self.intersect(quadTree1.bottomRight, quadTree2.bottomRight) # 检查四个子节点是否都是叶子节点且值相同 if (top_left.isLeaf and top_right.isLeaf and bottom_left.isLeaf and bottom_right.isLeaf and top_left.val == top_right.val == bottom_left.val == bottom_right.val): # 合并为一个叶子节点 return Node(top_left.val, True, None, None, None, None) # 否则返回非叶子节点 return Node(False, False, top_left, top_right, bottom_left, bottom_right) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为两棵四叉树的节点总数,最坏情况下需要遍历所有节点。 - **空间复杂度**:$O(\log n)$,递归调用栈的深度,最坏情况下为树的高度。 ================================================ FILE: docs/solutions/0500-0599/lonely-pixel-i.md ================================================ # [0531. 孤独像素 I](https://leetcode.cn/problems/lonely-pixel-i/) - 标签:数组、哈希表、矩阵 - 难度:中等 ## 题目链接 - [0531. 孤独像素 I - 力扣](https://leetcode.cn/problems/lonely-pixel-i/) ## 题目大意 **描述**: 给定一个由 `'B'` 和 `'W'` 组成的二维矩阵 $picture$,其中 `'B'` 表示黑色像素,`'W'` 表示白色像素。 如果一个黑色像素 `'B'` 满足以下条件,则称其为孤独像素: - 它所在的行和列中,只有这一个黑色像素 **要求**: 返回图片中孤独像素的数量。 **说明**: - $m == picture.length$。 - $n == picture[i].length$。 - $1 \le m, n \le 500$。 - $picture[i][j]$ 为 `'W'` 或 `'B'`。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/11/pixel1.jpg) ```python 输入:picture = [["W","W","B"],["W","B","W"],["B","W","W"]] 输出:3 解释:全部三个 'B' 都是黑色的孤独像素 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/11/11/pixel2.jpg) ```python 输入:picture = [["B","B","B"],["B","B","W"],["B","B","B"]] 输出:0 ``` ## 解题思路 ### 思路 1:统计行列黑色像素数量 先统计每一行和每一列中黑色像素的数量,然后遍历矩阵,对于每个黑色像素,检查其所在行和列的黑色像素数量是否都为 $1$。 **解题步骤**: 1. 统计每一行的黑色像素数量 $row\_count[i]$。 2. 统计每一列的黑色像素数量 $col\_count[j]$。 3. 遍历矩阵,对于 `picture[i][j] == 'B'`,如果 $row\_count[i] == 1$ 且 $col\_count[j] == 1$,则计数加 $1$。 ### 思路 1:代码 ```python class Solution: def findLonelyPixel(self, picture: List[List[str]]) -> int: if not picture or not picture[0]: return 0 m, n = len(picture), len(picture[0]) # 统计每行和每列的黑色像素数量 row_count = [0] * m col_count = [0] * n for i in range(m): for j in range(n): if picture[i][j] == 'B': row_count[i] += 1 col_count[j] += 1 # 统计孤独像素 lonely_count = 0 for i in range(m): for j in range(n): if picture[i][j] == 'B' and row_count[i] == 1 and col_count[j] == 1: lonely_count += 1 return lonely_count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 和 $n$ 分别是矩阵的行数和列数。需要遍历矩阵两次。 - **空间复杂度**:$O(m + n)$,需要存储每行和每列的黑色像素数量。 ================================================ FILE: docs/solutions/0500-0599/lonely-pixel-ii.md ================================================ # [0533. 孤独像素 II](https://leetcode.cn/problems/lonely-pixel-ii/) - 标签:数组、哈希表、矩阵 - 难度:中等 ## 题目链接 - [0533. 孤独像素 II - 力扣](https://leetcode.cn/problems/lonely-pixel-ii/) ## 题目大意 **描述**: 给定一个由 `'B'` 和 `'W'` 组成的二维矩阵 $picture$ 和一个整数 $target$,其中 `'B'` 表示黑色像素,`'W'` 表示白色像素。 如果一个黑色像素 `'B'` 满足以下条件,则称其为孤独像素: - 它所在的行恰好有 $target$ 个黑色像素。 - 它所在的列恰好有 $target$ 个黑色像素。 - 所有在同一行和同一列的黑色像素所在的行必须完全相同。 **要求**: 返回图片中孤独像素的数量。 **说明**: - $m == picture.length$。 - $n == picture[i].length$。 - $1 \le m, n \le 200$。 - $picture[i][j]$ 为 `'W'` 或 `'B'`。 - $1 \le target \le \min(m, n)$。 **示例**: - 示例 1: ![](https://pic.leetcode.cn/1694957797-UWXAxl-image.png) ```python 输入:picture = [["W","B","W","B","B","W"],["W","B","W","B","B","W"],["W","B","W","B","B","W"],["W","W","B","W","B","W"]], target = 3 输出:6 解释:所有绿色的 'B' 都是我们所求的像素(第 1 列和第 3 列的所有 'B' ) 以行 r = 0 和列 c = 1 的 'B' 为例: - 规则 1 ,行 r = 0 和列 c = 1 都恰好有 target = 3 个黑色像素 - 规则 2 ,列 c = 1 的黑色像素分别位于行 0,行 1 和行 2。和行 r = 0 完全相同。 ``` - 示例 2: ![](https://pic.leetcode.cn/1694957806-FyCCMF-image.png) ```python 输入:picture = [["W","W","B"],["W","W","B"],["W","W","B"]], target = 1 输出:0 ``` ## 解题思路 ### 思路 1:哈希表 + 行模式匹配 需要满足三个条件: 1. 行中恰好有 $target$ 个黑色像素。 2. 列中恰好有 $target$ 个黑色像素。 3. 同一列的所有黑色像素所在的行必须完全相同。 **解题步骤**: 1. 统计每行和每列的黑色像素数量。 2. 使用哈希表记录每种行模式出现的次数。 3. 对于每个黑色像素,检查是否满足所有条件。 ### 思路 1:代码 ```python class Solution: def findBlackPixel(self, picture: List[List[str]], target: int) -> int: if not picture or not picture[0]: return 0 m, n = len(picture), len(picture[0]) # 统计每行和每列的黑色像素数量 row_count = [0] * m col_count = [0] * n for i in range(m): for j in range(n): if picture[i][j] == 'B': row_count[i] += 1 col_count[j] += 1 # 使用哈希表记录每种行模式及其出现次数 from collections import defaultdict row_pattern = defaultdict(list) for i in range(m): if row_count[i] == target: pattern = ''.join(picture[i]) row_pattern[pattern].append(i) # 统计孤独像素 lonely_count = 0 for pattern, rows in row_pattern.items(): if len(rows) != target: continue # 检查每一列 for j in range(n): if pattern[j] == 'B' and col_count[j] == target: # 检查该列的所有黑色像素是否都在这些行中 valid = True for i in range(m): if picture[i][j] == 'B' and i not in rows: valid = False break if valid: lonely_count += target return lonely_count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 和 $n$ 分别是矩阵的行数和列数。 - **空间复杂度**:$O(m \times n)$,需要存储行模式和相关信息。 ================================================ FILE: docs/solutions/0500-0599/longest-harmonious-subsequence.md ================================================ # [0594. 最长和谐子序列](https://leetcode.cn/problems/longest-harmonious-subsequence/) - 标签:数组、哈希表、计数、排序、滑动窗口 - 难度:简单 ## 题目链接 - [0594. 最长和谐子序列 - 力扣](https://leetcode.cn/problems/longest-harmonious-subsequence/) ## 题目大意 **描述**: 和谐数组是指一个数组里元素的最大值和最小值之间的差别正好是 1。 给定一个整数数组 $nums$。 **要求**: 在所有可能的「子序列」中找到最长的和谐子序列的长度。 **说明**: - 数组的「子序列」是一个由数组派生出来的序列,它可以通过删除一些元素或不删除元素、且不改变其余元素的顺序而得到。 - $1 \le nums.length \le 2 * 10^{4}$。 - $-10^{9} \le nums[i] \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:nums = [1,3,2,2,5,2,3,7] 输出:5 解释:最长和谐子序列是 [3,2,2,2,3]。 ``` - 示例 2: ```python 输入:nums = [1,2,3,4] 输出:2 解释:最长和谐子序列是 [1,2],[2,3] 和 [3,4],长度都为 2。 ``` ## 解题思路 ### 思路 1:哈希表统计 和谐子序列要求最大值和最小值的差正好为 1,这意味着和谐子序列中只能包含两种不同的数字,且这两个数字相差 1。 我们可以使用哈希表统计每个数字出现的次数 $count[x]$。对于每个数字 $x$,如果存在 $x+1$,那么由 $x$ 和 $x+1$ 组成的和谐子序列长度为 $count[x] + count[x+1]$。 遍历所有数字,对于每个数字 $x$,计算 $count[x] + count[x+1]$(如果 $x+1$ 存在),取最大值即可。 ### 思路 1:代码 ```python from collections import Counter class Solution: def findLHS(self, nums: List[int]) -> int: # 统计每个数字出现的次数 count = Counter(nums) max_length = 0 # 遍历所有数字 for num in count: # 如果存在 num + 1,计算和谐子序列长度 if num + 1 in count: max_length = max(max_length, count[num] + count[num + 1]) return max_length ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组长度。需要遍历数组统计频率,然后遍历哈希表。 - **空间复杂度**:$O(n)$,哈希表最多存储 $n$ 个不同的数字。 ================================================ FILE: docs/solutions/0500-0599/longest-line-of-consecutive-one-in-matrix.md ================================================ # [0562. 矩阵中最长的连续1线段](https://leetcode.cn/problems/longest-line-of-consecutive-one-in-matrix/) - 标签:数组、动态规划、矩阵 - 难度:中等 ## 题目链接 - [0562. 矩阵中最长的连续1线段 - 力扣](https://leetcode.cn/problems/longest-line-of-consecutive-one-in-matrix/) ## 题目大意 **描述**: 给定一个二维二进制矩阵 $mat$。 **要求**: 找出矩阵中最长的连续 $1$ 线段的长度。 这条线段可以是水平的、垂直的、对角线的或者反对角线的。 **说明**: - $m == mat.length$。 - $n == mat[i].length$。 - $1 \le m, n \le 10^4$。 - $1 \le m \times n \le 10^4$。 - $mat[i][j]$ 不是 $0$ 就是 $1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/24/long1-grid.jpg) ```python 输入:mat = [[0,1,1,0],[0,1,1,0],[0,0,0,1]] 输出:3 解释:最长的连续 1 线段是对角线方向的,长度为 3。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/04/24/long2-grid.jpg) ```python 输入: mat = [[1,1,1,1],[0,1,1,0],[0,0,0,1]] 输出: 4 解释:最长的连续 1 线段是水平方向的,长度为 4。 ``` ## 解题思路 ### 思路 1:动态规划 使用三维 DP 数组 $dp[i][j][d]$ 表示以位置 $(i, j)$ 结尾、方向为 $d$ 的最长连续 $1$ 的长度。 方向 $d$ 有 4 种: - $0$:水平方向(从左到右) - $1$:垂直方向(从上到下) - $2$:对角线方向(从左上到右下) - $3$:反对角线方向(从右上到左下) 状态转移: - 如果 $mat[i][j] == 1$: - $dp[i][j][0] = dp[i][j-1][0] + 1$(水平) - $dp[i][j][1] = dp[i-1][j][1] + 1$(垂直) - $dp[i][j][2] = dp[i-1][j-1][2] + 1$(对角线) - $dp[i][j][3] = dp[i-1][j+1][3] + 1$(反对角线) - 如果 $mat[i][j] == 0$:所有方向的 $dp$ 值都为 $0$ ### 思路 1:代码 ```python class Solution: def longestLine(self, mat: List[List[int]]) -> int: if not mat or not mat[0]: return 0 m, n = len(mat), len(mat[0]) # dp[i][j][d] 表示以 (i,j) 结尾、方向 d 的最长连续 1 长度 # d: 0-水平, 1-垂直, 2-对角线, 3-反对角线 dp = [[[0] * 4 for _ in range(n)] for _ in range(m)] max_len = 0 for i in range(m): for j in range(n): if mat[i][j] == 1: # 水平方向 dp[i][j][0] = dp[i][j-1][0] + 1 if j > 0 else 1 # 垂直方向 dp[i][j][1] = dp[i-1][j][1] + 1 if i > 0 else 1 # 对角线方向 dp[i][j][2] = dp[i-1][j-1][2] + 1 if i > 0 and j > 0 else 1 # 反对角线方向 dp[i][j][3] = dp[i-1][j+1][3] + 1 if i > 0 and j < n - 1 else 1 # 更新最大值 max_len = max(max_len, max(dp[i][j])) return max_len ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 和 $n$ 分别是矩阵的行数和列数。需要遍历整个矩阵。 - **空间复杂度**:$O(m \times n)$,需要存储 DP 数组。可以优化到 $O(n)$ 使用滚动数组。 ================================================ FILE: docs/solutions/0500-0599/longest-palindromic-subsequence.md ================================================ # [0516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [0516. 最长回文子序列 - 力扣](https://leetcode.cn/problems/longest-palindromic-subsequence/) ## 题目大意 **描述**:给定一个字符串 $s$。 **要求**:找出其中最长的回文子序列,并返回该序列的长度。 **说明**: - **子序列**:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。 - $1 \le s.length \le 1000$。 - $s$ 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "bbbab" 输出:4 解释:一个可能的最长回文子序列为 "bbbb"。 ``` - 示例 2: ```python 输入:s = "cbbd" 输出:2 解释:一个可能的最长回文子序列为 "bb"。 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:字符串 $s$ 在区间 $[i, j]$ 范围内的最长回文子序列长度。 ###### 3. 状态转移方程 我们对区间 $[i, j]$ 边界位置上的字符 $s[i]$ 与 $s[j]$ 进行分类讨论: 1. 如果 $s[i] = s[j]$,则 $dp[i][j]$ 为区间 $[i + 1, j - 1]$ 范围内最长回文子序列长度 + $2$,即 $dp[i][j] = dp[i + 1][j - 1] + 2$。 2. 如果 $s[i] \ne s[j]$,则 $dp[i][j]$ 取决于以下两种情况,取其最大的一种: 1. 加入 $s[i]$ 所能组成的最长回文子序列长度,即:$dp[i][j] = dp[i][j - 1]$。 2. 加入 $s[j]$ 所能组成的最长回文子序列长度,即:$dp[i][j] = dp[i - 1][j]$。 则状态转移方程为: $dp[i][j] = \begin{cases} max \lbrace dp[i + 1][j - 1] + 2 \rbrace & s[i] = s[j] \cr max \lbrace dp[i][j - 1], dp[i - 1][j] \rbrace & s[i] \ne s[j] \end{cases}$ ###### 4. 初始条件 - 单个字符的最长回文序列是 $1$,即 $dp[i][i] = 1$。 ###### 5. 最终结果 由于 $dp[i][j]$ 依赖于 $dp[i + 1][j - 1]$、$dp[i + 1][j]$、$dp[i][j - 1]$,所以我们应该按照从下到上、从左到右的顺序进行遍历。 根据我们之前定义的状态,$dp[i][j]$ 表示为:字符串 $s$ 在区间 $[i, j]$ 范围内的最长回文子序列长度。所以最终结果为 $dp[0][size - 1]$。 ### 思路 1:代码 ```python class Solution: def longestPalindromeSubseq(self, s: str) -> int: size = len(s) dp = [[0 for _ in range(size)] for _ in range(size)] for i in range(size): dp[i][i] = 1 for i in range(size - 1, -1, -1): for j in range(i + 1, size): if s[i] == s[j]: dp[i][j] = dp[i + 1][j - 1] + 2 else: dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) return dp[0][size - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/0500-0599/longest-uncommon-subsequence-i.md ================================================ # [0521. 最长特殊序列 Ⅰ](https://leetcode.cn/problems/longest-uncommon-subsequence-i/) - 标签:字符串 - 难度:简单 ## 题目链接 - [0521. 最长特殊序列 Ⅰ - 力扣](https://leetcode.cn/problems/longest-uncommon-subsequence-i/) ## 题目大意 **描述**: 给定两个字符串 $a$ 和 $b$。 **要求**: 返回这两个字符串中「最长的特殊序列」的长度。如果不存在,则返回 $-1$。 **说明**: - 「最长特殊序列」 定义如下:该序列为某字符串独有的最长子序列(即不能是其他字符串的子序列)。 - 字符串 $s$ 的子序列是在从 $s$ 中删除任意数量的字符后可以获得的字符串。 - 例如,`"abc"` 是 `"aebdc"` 的子序列,因为删除 `"aebdc"` 中斜体加粗的字符可以得到 `"abc"`。`"aebdc"` 的子序列还包括 `"aebdc"`、 `"aeb"` 和 `""` (空字符串)。 - $1 \le a.length, b.length \le 10^{3}$。 - $a$ 和 $b$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入: a = "aba", b = "cdc" 输出: 3 解释: 最长特殊序列可为 "aba" (或 "cdc"),两者均为自身的子序列且不是对方的子序列。 ``` - 示例 2: ```python 输入:a = "aaa", b = "bbb" 输出:3 解释: 最长特殊序列是 "aaa" 和 "bbb" 。 ``` ## 解题思路 ### 思路 1:字符串比较 最长特殊序列的定义:该序列为某字符串独有的最长子序列(即不能是其他字符串的子序列)。 关键观察: - 如果 $a == b$,那么 $a$ 的所有子序列都是 $b$ 的子序列,不存在特殊序列,返回 -1 - 如果 $a \neq b$,那么较长的字符串本身就是特殊序列(因为它不能是较短字符串的子序列,因为长度更长) - 如果长度相同但内容不同,那么任意一个字符串都是特殊序列,返回长度。 因此,如果 $a == b$,返回 -1;否则返回 $\max(len(a), len(b))$。 ### 思路 1:代码 ```python class Solution: def findLUSlength(self, a: str, b: str) -> int: # 如果两个字符串相同,不存在特殊序列 if a == b: return -1 # 如果不同,较长的字符串就是最长特殊序列 return max(len(a), len(b)) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度,需要比较两个字符串是否相等。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/longest-uncommon-subsequence-ii.md ================================================ # [0522. 最长特殊序列 II](https://leetcode.cn/problems/longest-uncommon-subsequence-ii/) - 标签:数组、哈希表、双指针、字符串、排序 - 难度:中等 ## 题目链接 - [0522. 最长特殊序列 II - 力扣](https://leetcode.cn/problems/longest-uncommon-subsequence-ii/) ## 题目大意 **描述**: 给定字符串列表 $strs$。 **要求**: 返回其中「最长的特殊序列」的长度。如果最长特殊序列不存在,返回 $-1$。 **说明**: - 「特殊序列」定义如下:该序列为某字符串独有的子序列(即不能是其他字符串的子序列)。 - $s$ 的子序列可以通过删去字符串 $s$ 中的某些字符实现。 - 例如,`"abc"` 是 `"aebdc"` 的子序列,因为您可以删除 `"aebdc"` 中的下划线字符来得到 `"abc"`。`"aebdc"` 的子序列还包括 `"aebdc"`、`"aeb"` 和 `""` (空字符串)。 - $2 \le strs.length \le 50$。 - $1 \le strs[i].length \le 10$。 - $strs[i]$ 只包含小写英文字母。 **示例**: - 示例 1: ```python 输入: strs = ["aba","cdc","eae"] 输出: 3 ``` - 示例 2: ```python 输入: strs = ["aaa","aaa","aa"] 输出: -1 ``` ## 解题思路 ### 思路 1:暴力枚举 & 判子序列 对所有字符串按长度从大到小枚举,把每个字符串 $s$ 作为候选,检查它是否不是其它任一字符串的子序列。 - 判断子序列用双指针时间 $O(L)$,其中$L$为字符串最长长度。 - 遍历未与自己相等的所有字符串,只要$s$不是其他的子序列,即可返回 $|s|$。 ### 思路 1:代码 ```python class Solution: def findLUSlength(self, strs: List[str]) -> int: # 检查s是不是t的子序列 def is_subseq(s, t): i = 0 for c in t: if i < len(s) and s[i] == c: i += 1 return i == len(s) res = -1 for i, s in enumerate(strs): if all(i == j or not is_subseq(s, t) for j, t in enumerate(strs)): res = max(res, len(s)) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 L)$,$n$为字符串数量,$L$为最大长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0500-0599/longest-word-in-dictionary-through-deleting.md ================================================ # [0524. 通过删除字母匹配到字典里最长单词](https://leetcode.cn/problems/longest-word-in-dictionary-through-deleting/) - 标签:数组、双指针、字符串、排序 - 难度:中等 ## 题目链接 - [0524. 通过删除字母匹配到字典里最长单词 - 力扣](https://leetcode.cn/problems/longest-word-in-dictionary-through-deleting/) ## 题目大意 **描述**: 给定一个字符串 $s$ 和一个字符串数组 $dictionary$。 **要求**: 找出并返回 $dictionary$ 中最长的字符串,该字符串可以通过删除 $s$ 中的某些字符得到。 如果答案不止一个,返回长度最长且字母序最小的字符串。如果答案不存在,则返回空字符串。 **说明**: - $1 \le s.length \le 10^{3}$。 - $1 \le dictionary.length \le 10^{3}$。 - $1 \le dictionary[i].length \le 10^{3}$。 - $s$ 和 $dictionary[i]$ 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "abpcplea", dictionary = ["ale","apple","monkey","plea"] 输出:"apple" ``` - 示例 2: ```python 输入:s = "abpcplea", dictionary = ["a","b","c"] 输出:"a" ``` ## 解题思路 ### 思路 1:双指针匹配 & 按要求排序 核心在于判断每个字典中的单词 $w$ 是否 $w$ 是 $s$ 子序列。 1. 对 $dictionary$ 按长度降序、字典序升序排序。 2. 对每个字符串用双指针扫是否为子序列,遇到第一个返回即可。 ### 思路 1:代码 ```python class Solution: def findLongestWord(self, s: str, dictionary: List[str]) -> str: def is_subseq(word, s): it = iter(s) return all(c in it for c in word) # 长度优先、字典序次之 dictionary.sort(key=lambda x: (-len(x), x)) for word in dictionary: if is_subseq(word, s): return word return "" ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(nm)$,$n$为字典总单词数,$m$为$s$长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0500-0599/maximum-depth-of-n-ary-tree.md ================================================ # [0559. N 叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-n-ary-tree/) - 标签:树、深度优先搜索、广度优先搜索 - 难度:简单 ## 题目链接 - [0559. N 叉树的最大深度 - 力扣](https://leetcode.cn/problems/maximum-depth-of-n-ary-tree/) ## 题目大意 **描述**: 给定一个 N 叉树。 **要求**: 找到其最大深度。 **说明**: - 最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。 - N 叉树输入按层序遍历序列化表示,每组子节点由空值分隔(请参见示例)。 - 树的深度不会超过 $10^{3}$。 - 树的节点数目位于 $[0, 10^{4}]$ 之间。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2018/10/12/narytreeexample.png) ```python 输入:root = [1,null,3,2,4,null,5,6] 输出:3 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2019/11/08/sample_4_964.png) ```python 输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14] 输出:5 ``` ## 解题思路 ### 思路 1:递归(深度优先搜索) 对于 N 叉树,最大深度等于根节点到最远叶子节点的路径长度。 使用递归方法: - 如果节点为空,返回深度 $0$。 - 否则,递归计算所有子节点的最大深度 $max\_child\_depth$。 - 当前节点的深度为 $1 + max\_child\_depth$。 ### 思路 1:代码 ```python """ # Definition for a Node. class Node: def __init__(self, val: Optional[int] = None, children: Optional[List['Node']] = None): self.val = val self.children = children """ class Solution: def maxDepth(self, root: 'Node') -> int: if not root: return 0 # 如果没有子节点,深度为 1 if not root.children: return 1 # 递归计算所有子节点的最大深度 max_child_depth = 0 for child in root.children: max_child_depth = max(max_child_depth, self.maxDepth(child)) # 当前节点深度 = 1 + 子节点最大深度 return 1 + max_child_depth ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树的节点数,需要遍历每个节点一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度,递归栈的深度最多为 $h$。 ================================================ FILE: docs/solutions/0500-0599/maximum-vacation-days.md ================================================ # [0568. 最大休假天数](https://leetcode.cn/problems/maximum-vacation-days/) - 标签:数组、动态规划、矩阵 - 难度:困难 ## 题目链接 - [0568. 最大休假天数 - 力扣](https://leetcode.cn/problems/maximum-vacation-days/) ## 题目大意 **描述**: LeetCode 想让一个最优秀的员工在 $N$ 个城市中旅行 $K$ 周。员工的工作就是安排旅行使得最大化可以休假的天数,但是需要遵守一些规则和限制。 规则和限制: - 只能在 $N$ 个城市之间旅行,城市用 $0 \sim n - 1$ 的索引表示。一开始,员工位于索引 0 的城市,并且那天是星期一。 - 城市之间通过航班相连,这些航班用一个 $n \times n$ 的矩阵 $flights$ 表示,$flights[i][j] = 1$ 表示城市 $i$ 和城市 $j$ 之间有航班($i = j$ 表示可以留在当前城市)。 - 员工总共有 $K$ 周(每周 7 天)的时间旅行,每天最多只能乘坐一次航班,并且只能在每周一上午乘坐航班(不需要考虑飞行时间的影响)。 - 每个城市的休假天数是不一样的,给定 $N \times K$ 的矩阵 $days$,$days[i][j]$ 表示在第 $j$ 周在城市 $i$ 可以休假的最长天数。 - 如果您从 A 市飞往 B 市,并在当天休假,扣除的假期天数将计入 B 市当周的休假天数。 - 我们不考虑飞行时数对休假天数计算的影响。 员工从城市 $0$ 开始,每周必须选择一个可以到达的城市(包括当前城市)。 **要求**: 返回员工在 $K$ 周内最多可以休假的天数。 **说明**: - $N$ 和 $K$ 都是正整数,范围在 $[1, 100]$ 内。 - $flights$ 矩阵的大小为 $N \times N$。 - $days$ 矩阵的大小为 $N \times K$。 - $0 \le days[i][j] \le 7$。 **示例**: - 示例 1: ```python 输入:flights = [[0,1,1],[1,0,1],[1,1,0]], days = [[1,3,1],[6,0,3],[3,3,3]] 输出: 12 解释: 最好的策略之一: 第一个星期 : 星期一从城市 0 飞到城市 1,玩 6 天,工作 1 天。 (虽然你是从城市 0 开始,但因为是星期一,我们也可以飞到其他城市。) 第二个星期 : 星期一从城市 1 飞到城市 2,玩 3 天,工作 4 天。 第三个星期 : 呆在城市 2,玩 3 天,工作 4 天。 Ans = 6 + 3 + 3 = 12. ``` - 示例 2: ```python 输入:flights = [[0,0,0],[0,0,0],[0,0,0]], days = [[1,1,1],[7,7,7],[7,7,7]] 输出: 3 解释: 由于没有航班可以让您飞到其他城市,你必须在城市 0 呆整整 3 个星期。 对于每一个星期,你只有一天时间玩,剩下六天都要工作。 所以最大休假天数为 3. Ans = 1 + 1 + 1 = 3. ``` ## 解题思路 ### 思路 1:动态规划 定义 $dp[week][city]$ 表示在第 $week$ 周结束时位于城市 $city$ 能获得的最大休假天数。 状态转移: - 对于第 $week$ 周,如果要在城市 $j$ 度过,需要从某个城市 $i$ 飞过来(或留在原地) - $dp[week][j] = \max(dp[week-1][i] + days[j][week])$,其中 $flights[i][j] = 1$ 初始状态:$dp[0][0] = days[0][0]$,以及所有从城市 $0$ 可以直达的城市。 ### 思路 1:代码 ```python class Solution: def maxVacationDays(self, flights: List[List[int]], days: List[List[int]]) -> int: n = len(flights) # 城市数量 k = len(days[0]) # 周数 # dp[week][city] 表示第 week 周在 city 城市的最大休假天数 dp = [[-1] * n for _ in range(k)] # 初始化第 0 周 dp[0][0] = days[0][0] for j in range(n): if flights[0][j] == 1: dp[0][j] = days[j][0] # 动态规划 for week in range(1, k): for j in range(n): # 尝试从每个城市 i 到达城市 j for i in range(n): if dp[week-1][i] != -1 and (i == j or flights[i][j] == 1): dp[week][j] = max(dp[week][j], dp[week-1][i] + days[j][week]) # 返回最后一周的最大值 return max(dp[k-1]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(K \times N^2)$,其中 $K$ 是周数,$N$ 是城市数量。需要遍历每周每个城市的每个来源城市。 - **空间复杂度**:$O(K \times N)$,需要存储 $dp$ 数组。可以优化到 $O(N)$ 使用滚动数组。 ================================================ FILE: docs/solutions/0500-0599/minesweeper.md ================================================ # [0529. 扫雷游戏](https://leetcode.cn/problems/minesweeper/) - 标签:深度优先搜索、广度优先搜索、数组、矩阵 - 难度:中等 ## 题目链接 - [0529. 扫雷游戏 - 力扣](https://leetcode.cn/problems/minesweeper/) ## 题目大意 **描述**: 让我们一起来玩扫雷游戏! 给定一个大小为 $m$ $x$ $n$ 二维字符矩阵 $board$ ,表示扫雷游戏的盘面,其中: - `'M'` 代表一个「未挖出的」地雷, - `'E'` 代表一个「未挖出的」空方块, - `'B'` 代表没有相邻(上,下,左,右,和所有4个对角线)地雷的「已挖出的」空白方块, - 数字(`'1'` 到 `'8'`)表示有多少地雷与这块「已挖出的」方块相邻, - `'X'` 则表示一个「已挖出的」地雷。 给定一个整数数组 $click$,其中 $click = [clickr, clickc]$ 表示在所有「未挖出的」方块(`'M'` 或者 `'E'`)中的下一个点击位置($clickr$ 是行下标,$clickc$ 是列下标)。 **要求**: 根据以下规则,返回相应位置被点击后对应的盘面: 1. 如果一个地雷(`'M'`)被挖出,游戏就结束了- 把它改为 `'X'`。 2. 如果一个 没有相邻地雷 的空方块(`'E'`)被挖出,修改它为(`'B'`),并且所有和其相邻的「未挖出的」方块都应该被递归地揭露。 3. 如果一个「至少与一个地雷相邻」的空方块(`'E'`)被挖出,修改它为数字(`'1'` 到 `'8'`),表示相邻地雷的数量。 4. 如果在此次点击中,若无更多方块可被揭露,则返回盘面。 **说明**: - $m == board.length$。 - $n == board[i].length$。 - $1 \le m, n \le 50$。 - $board[i][j]$ 为 `'M'`、`'E'`、`'B'` 或数字 `'1'` 到 `'8'` 中的一个。 - $click.length == 2$。 - $0 \le clickr \lt m$。 - $0 \le clickc \lt n$。 - $board[clickr][clickc]$ 为 `'M'` 或 `'E'`。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2023/08/09/untitled.jpeg) ```python 输入:board = [["E","E","E","E","E"],["E","E","M","E","E"],["E","E","E","E","E"],["E","E","E","E","E"]], click = [3,0] 输出:[["B","1","E","1","B"],["B","1","M","1","B"],["B","1","1","1","B"],["B","B","B","B","B"]] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2023/08/09/untitled-2.jpeg) ```python 输入:board = [["B","1","E","1","B"],["B","1","M","1","B"],["B","1","1","1","B"],["B","B","B","B","B"]], click = [1,2] 输出:[["B","1","E","1","B"],["B","1","X","1","B"],["B","1","1","1","B"],["B","B","B","B","B"]] ``` ## 解题思路 ### 思路 1:深度优先搜索(DFS) 这道题是经典的扫雷游戏模拟,需要根据点击位置进行不同的处理。 核心思路: 1. 如果点击位置是地雷 `'M'`,直接标记为 `'X'` 并返回。 2. 如果点击位置是空方块 `'E'`: - 统计周围 $8$ 个方向的地雷数量。 - 如果周围有地雷,将当前位置标记为地雷数量(`'1'` 到 `'8'`)。 - 如果周围没有地雷,标记为 `'B'`,并递归处理周围 $8$ 个方向的未挖出方块。 3. 使用 DFS 递归处理相邻的空方块。 ### 思路 1:代码 ```python class Solution: def updateBoard(self, board: List[List[str]], click: List[int]) -> List[List[str]]: m, n = len(board), len(board[0]) x, y = click[0], click[1] # 8 个方向 directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] # 如果点击的是地雷,游戏结束 if board[x][y] == 'M': board[x][y] = 'X' return board def dfs(i, j): # 统计周围地雷数量 mine_count = 0 for di, dj in directions: ni, nj = i + di, j + dj if 0 <= ni < m and 0 <= nj < n and board[ni][nj] == 'M': mine_count += 1 # 如果周围有地雷,标记数量 if mine_count > 0: board[i][j] = str(mine_count) else: # 周围没有地雷,标记为 'B' 并递归处理相邻方块 board[i][j] = 'B' for di, dj in directions: ni, nj = i + di, j + dj # 只处理未挖出的空方块 if 0 <= ni < m and 0 <= nj < n and board[ni][nj] == 'E': dfs(ni, nj) # 从点击位置开始 DFS dfs(x, y) return board ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 和 $n$ 分别为矩阵的行数和列数,最坏情况下需要遍历所有方块。 - **空间复杂度**:$O(m \times n)$,递归调用栈的深度,最坏情况下为矩阵大小。 ================================================ FILE: docs/solutions/0500-0599/minimum-absolute-difference-in-bst.md ================================================ # [0530. 二叉搜索树的最小绝对差](https://leetcode.cn/problems/minimum-absolute-difference-in-bst/) - 标签:树、深度优先搜索、广度优先搜索、二叉搜索树、二叉树 - 难度: ## 题目链接 - [0530. 二叉搜索树的最小绝对差 - 力扣](https://leetcode.cn/problems/minimum-absolute-difference-in-bst/) ## 题目大意 **描述**:给定一个二叉搜索树的根节点 $root$。 **要求**:返回树中任意两不同节点值之间的最小差值。 **说明**: - **差值**:是一个正数,其数值等于两值之差的绝对值。 - 树中节点的数目范围是 $[2, 10^4]$。 - $0 \le Node.val \le 10^5$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/05/bst1.jpg) ```python 输入:root = [4,2,6,1,3] 输出:1 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/02/05/bst2.jpg) ```python 输入:root = [1,0,48,null,null,12,49] 输出:1 ``` ## 解题思路 ### 思路 1:中序遍历 先来看二叉搜索树的定义: - 如果左子树不为空,则左子树上所有节点值均小于它的根节点值; - 如果右子树不为空,则右子树上所有节点值均大于它的根节点值; - 任意节点的左、右子树也分别为二叉搜索树。 题目要求二叉搜索树上任意两节点的差的绝对值的最小值。 二叉树的中序遍历顺序是:左 -> 根 -> 右,二叉搜索树的中序遍历最终得到就是一个升序数组。而升序数组中绝对值差的最小值就是比较相邻两节点差值的绝对值,找出其中最小值。 那么我们就可以先对二叉搜索树进行中序遍历,并保存中序遍历的结果。然后再比较相邻节点差值的最小值,从而找出最小值。 ### 思路 1:代码 ```Python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def inorderTraversal(self, root: TreeNode) -> List[int]: res = [] def inorder(root): if not root: return inorder(root.left) res.append(root.val) inorder(root.right) inorder(root) return res def getMinimumDifference(self, root: Optional[TreeNode]) -> int: inorder = self.inorderTraversal(root) ans = float('inf') for i in range(1, len(inorder)): ans = min(ans, abs(inorder[i - 1] - inorder[i])) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为二叉搜索树中的节点数量。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0500-0599/minimum-index-sum-of-two-lists.md ================================================ # [0599. 两个列表的最小索引总和](https://leetcode.cn/problems/minimum-index-sum-of-two-lists/) - 标签:数组、哈希表、字符串 - 难度:简单 ## 题目链接 - [0599. 两个列表的最小索引总和 - 力扣](https://leetcode.cn/problems/minimum-index-sum-of-two-lists/) ## 题目大意 Andy 和 Doris 都有一个表示最喜欢餐厅的列表 list1、list2,每个餐厅的名字用字符串表示。 找出他们共同喜爱的餐厅,要求两个餐厅在列表中的索引和最小,如果答案不唯一,则输出所有答案。 ## 解题思路 遍历 list1,建立一个哈希表 list1_dict,以 list1[i] : i 键值对的方式,将 list1 的下标存储起来。 然后遍历 list2,判断 list2[i] 是否在哈希表中,如果在,则根据 i + list1_dict[i] 和 min_sum 的比较,判断是否需要更新最小索引和。如果 i + list1_dict[i] < min_sum,则更新最小索引和,并清空答案数据,添加新的答案。如果 i + list1_dict[i] == min_sum,则更新最小索引和,并添加答案。 ## 代码 ```python class Solution: def findRestaurant(self, list1: List[str], list2: List[str]) -> List[str]: list1_dict = dict() len1 = len(list1) len2 = len(list2) for i in range(len1): list1_dict[list1[i]] = i min_sum = len1 + len2 res = [] for i in range(len2): if list2[i] in list1_dict: sum = i + list1_dict[list2[i]] if sum < min_sum: res = [list2[i]] min_sum = sum elif sum == min_sum: res.append(list2[i]) return res ``` ================================================ FILE: docs/solutions/0500-0599/minimum-time-difference.md ================================================ # [0539. 最小时间差](https://leetcode.cn/problems/minimum-time-difference/) - 标签:数组、数学、字符串、排序 - 难度:中等 ## 题目链接 - [0539. 最小时间差 - 力扣](https://leetcode.cn/problems/minimum-time-difference/) ## 题目大意 给定一个 24 小时制形式(小时:分钟 "HH:MM")的时间列表 `timePoints`。 要求:找出列表中任意两个时间的最小时间差并以分钟数表示。 ## 解题思路 - 遍历时间列表 `timePoints`,将每个时间转换为以分钟计算的整数形式,比如时间 `14:20`,将其转换为 `14 * 60 + 20 = 860`,存放到新的时间列表 `times` 中。 - 为了处理最早时间、最晚时间之间的时间间隔,我们将 `times` 中最小时间添加到列表末尾一起进行排序。 - 然后将新的时间列表 `times` 按照升序排列。 - 遍历排好序的事件列表 `times` ,找出相邻两个时间的最小间隔值即可。 ## 代码 ```python class Solution: def changeTime(self, timePoint: str): hours, minutes = timePoint.split(':') return int(hours) * 60 + int(minutes) def findMinDifference(self, timePoints: List[str]) -> int: if not timePoints or len(timePoints) > 24 * 60: return 0 times = sorted(self.changeTime(time) for time in timePoints) times.append(times[0] + 24 * 60) res = times[-1] for i in range(1, len(times)): res = min(res, times[i] - times[i - 1]) return res ``` ================================================ FILE: docs/solutions/0500-0599/most-frequent-subtree-sum.md ================================================ # [0508. 出现次数最多的子树元素和](https://leetcode.cn/problems/most-frequent-subtree-sum/) - 标签:树、深度优先搜索、哈希表、二叉树 - 难度:中等 ## 题目链接 - [0508. 出现次数最多的子树元素和 - 力扣](https://leetcode.cn/problems/most-frequent-subtree-sum/) ## 题目大意 **描述**: 给定一个二叉树的根结点 $root$。 **要求**: 返回出现次数最多的子树元素和。如果有多个元素出现的次数相同,返回所有出现次数最多的子树元素和(不限顺序)。 **说明**: - 一个结点的「子树元素和」定义为以该结点为根的二叉树上所有结点的元素之和(包括结点本身)。 - 节点数在 $[1, 10^{4}]$ 范围内。 - $-10^{5} \le Node.val \le 10^{5}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/24/freq1-tree.jpg) ```python 输入: root = [5,2,-3] 输出: [2,-3,4] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/04/24/freq2-tree.jpg) ```python 输入: root = [5,2,-5] 输出: [2] ``` ## 解题思路 ### 思路 1:递归 + 哈希表 对于每个节点,我们需要计算以该节点为根的子树元素和。使用后序遍历(DFS): 1. 递归计算左右子树的元素和 2. 当前节点的子树元素和 = $node.val + left\_sum + right\_sum$ 3. 使用哈希表统计每个子树元素和出现的次数 4. 遍历完成后,找到出现次数最多的子树元素和 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right from collections import defaultdict class Solution: def findFrequentTreeSum(self, root: Optional[TreeNode]) -> List[int]: sum_count = defaultdict(int) def dfs(node: Optional[TreeNode]) -> int: if not node: return 0 # 递归计算左右子树的元素和 left_sum = dfs(node.left) right_sum = dfs(node.right) # 当前节点的子树元素和 subtree_sum = node.val + left_sum + right_sum # 统计子树元素和的出现次数 sum_count[subtree_sum] += 1 return subtree_sum dfs(root) # 找到出现次数最多的子树元素和 if not sum_count: return [] max_count = max(sum_count.values()) return [sum_val for sum_val, count in sum_count.items() if count == max_count] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树的节点数,需要遍历每个节点一次。 - **空间复杂度**:$O(n)$,递归栈的深度最多为 $n$,哈希表最多存储 $n$ 个不同的子树元素和。 ================================================ FILE: docs/solutions/0500-0599/n-ary-tree-postorder-traversal.md ================================================ # [0590. N 叉树的后序遍历](https://leetcode.cn/problems/n-ary-tree-postorder-traversal/) - 标签:栈、树、深度优先搜索 - 难度:简单 ## 题目链接 - [0590. N 叉树的后序遍历 - 力扣](https://leetcode.cn/problems/n-ary-tree-postorder-traversal/) ## 题目大意 给定一个 N 叉树的根节点 `root`。 要求:返回其节点值的后序遍历。 ## 解题思路 N 叉树的后序遍历顺序为:子节点顺序递归遍历 -> 根节点。 一个取巧的方法是先按照:根节点 -> 子节点逆序递归遍历 的顺序将遍历顺序存储到答案数组。 然后再将其进行翻转就变为了后序遍历顺序。具体操作如下: - 用栈保存根节点 `root`。然后遍历栈。 - 循环判断栈是否为空。 - 如果栈不为空,取出栈顶节点,将节点值加入答案数组。 - 顺序遍历栈顶节点的子节点,将其依次放入栈中(顺序遍历保证取出顺序为逆序)。 - 然后继续第 2 ~ 4 步,直到栈为空。 最后将答案数组逆序返回。 ## 代码 ```python class Solution: def postorder(self, root: 'Node') -> List[int]: res = [] stack = [] if not root: return res stack.append(root) while stack: node = stack.pop() res.append(node.val) for child in node.children: stack.append(child) return res[::-1] ``` ================================================ FILE: docs/solutions/0500-0599/n-ary-tree-preorder-traversal.md ================================================ # [0589. N 叉树的前序遍历](https://leetcode.cn/problems/n-ary-tree-preorder-traversal/) - 标签:栈、树、深度优先搜索 - 难度:简单 ## 题目链接 - [0589. N 叉树的前序遍历 - 力扣](https://leetcode.cn/problems/n-ary-tree-preorder-traversal/) ## 题目大意 给定一棵 N 叉树的根节点 `root`。 要求:返回其节点值的前序遍历。 进阶:使用迭代法完成。 ## 解题思路 递归法很好写。迭代法需要借助于栈。 - 用栈保存根节点 `root`。然后遍历栈。 - 循环判断栈是否为空。 - 如果栈不为空,取出栈顶节点,将节点值加入答案数组。 - 逆序遍历栈顶节点的子节点,将其依次放入栈中(逆序保证取出顺序为正)。 - 然后继续第 2 ~ 4 步,直到栈为空。 最后输出答案数组。 ## 代码 ```python class Solution: def preorder(self, root: 'Node') -> List[int]: res = [] stack = [] if not root: return res stack.append(root) while stack: node = stack.pop() res.append(node.val) for i in range(len(node.children) - 1, -1, -1): if node.children[i]: stack.append(node.children[i]) return res ``` ================================================ FILE: docs/solutions/0500-0599/next-greater-element-ii.md ================================================ # [0503. 下一个更大元素 II](https://leetcode.cn/problems/next-greater-element-ii/) - 标签:栈、数组、单调栈 - 难度:中等 ## 题目链接 - [0503. 下一个更大元素 II - 力扣](https://leetcode.cn/problems/next-greater-element-ii/) ## 题目大意 给定一个循环数组 `nums`(最后一个元素的下一个元素是数组的第一个元素)。 要求:输出每个元素的下一个更大元素。如果不存在,则输出 `-1`。 - 数字 `x` 的下一个更大的元素:按数组遍历顺序,这个数字之后的第一个比它更大的数。这意味着你应该循环地搜索它的下一个更大的数。 ## 解题思路 第一种思路是根据题意直接暴力求解。遍历 `nums` 中的每一个元素。对于 `nums` 的每一个元素 `nums[i]`,查找 `nums[i]` 右边第一个比 `nums1[i]` 大的元素。这种解法的时间复杂度是 $O(n^2)$。 第二种思路是使用单调递增栈。遍历数组 `nums`,构造单调递增栈,求出 `nums` 中每个元素右侧下一个更大的元素。然后将其存储到答案数组中。这种解法的时间复杂度是 $O(n)$。 而循环数组的求解方法可以将 `nums` 复制一份到末尾,生成长度为 `len(nums) * 2` 的数组,或者通过取模运算将下标映射到 `0` ~ `len(nums) * 2 - 1` 之间。 具体做法如下: - 使用数组 `res` 存放答案,初始值都赋值为 `-1`。使用变量 `stack` 表示单调递增栈。 - 遍历数组 `nums`,对于当前元素: - 如果当前元素值小于栈顶元素,则说明当前元素「下一个更大元素」与栈顶元素的「下一个更大元素」相同。应该直接让当前元素的下标入栈。 - 如果当前元素值大于栈顶元素,则说明当前元素是之前元素的「下一个更大元素」,则不断将栈顶元素出栈。直到当前元素值小于栈顶元素值。 - 出栈时,出栈元素的「下一个更大元素」是当前元素。则将当前元素值存入到答案数组 `res` 中出栈元素所对应的位置中。 - 最终输出答案数组 `res`。 ## 代码 ```python size = len(nums) res = [-1 for _ in range(size)] stack = [] for i in range(size * 2): while stack and nums[i % size] > nums[stack[-1]]: index = stack.pop() res[index] = nums[i % size] stack.append(i % size) return res ``` ================================================ FILE: docs/solutions/0500-0599/next-greater-element-iii.md ================================================ # [0556. 下一个更大元素 III](https://leetcode.cn/problems/next-greater-element-iii/) - 标签:数学、双指针、字符串 - 难度:中等 ## 题目链接 - [0556. 下一个更大元素 III - 力扣](https://leetcode.cn/problems/next-greater-element-iii/) ## 题目大意 **描述**: 给定一个正整数 $n$。 **要求**: 找出符合条件的最小整数,其由重新排列 $n$ 中存在的每位数字组成,并且其值大于 $n$。如果不存在这样的正整数,则返回 $-1$。 **说明**: - 注意:返回的整数应当是一个 32 位整数,如果存在满足题意的答案,但不是 32 位整数,同样返回 $-1$。 - $1 \le n \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:n = 12 输出:21 ``` - 示例 2: ```python 输入:n = 21 输出:-1 ``` ## 解题思路 ### 思路 1:下一个排列 这个问题本质上是求数字的下一个排列。算法步骤: 1. 将数字转换为字符数组 2. 从右往左找到第一个满足 $nums[i] < nums[i+1]$ 的位置 $i$ 3. 如果找不到,说明已经是最大排列,返回 $-1$ 4. 从右往左找到第一个大于 $nums[i]$ 的位置 $j$ 5. 交换 $nums[i]$ 和 $nums[j]$ 6. 反转 $nums[i+1:]$ 使其升序 7. 将结果转换回整数,检查是否在 32 位整数范围内 ### 思路 1:代码 ```python class Solution: def nextGreaterElement(self, n: int) -> int: nums = list(str(n)) n_len = len(nums) # 从右往左找第一个 nums[i] < nums[i+1] 的位置 i = n_len - 2 while i >= 0 and nums[i] >= nums[i + 1]: i -= 1 # 如果找不到,说明已经是最大排列 if i < 0: return -1 # 从右往左找第一个大于 nums[i] 的位置 j = n_len - 1 while j > i and nums[j] <= nums[i]: j -= 1 # 交换 nums[i] 和 nums[j] nums[i], nums[j] = nums[j], nums[i] # 反转 nums[i+1:] 使其升序 nums[i + 1:] = reversed(nums[i + 1:]) # 转换回整数 result = int(''.join(nums)) # 检查是否在 32 位整数范围内 if result > 2**31 - 1: return -1 return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(d)$,其中 $d$ 是数字的位数,需要遍历数字的每一位。 - **空间复杂度**:$O(d)$,需要存储字符数组。 ================================================ FILE: docs/solutions/0500-0599/number-of-provinces.md ================================================ # [0547. 省份数量](https://leetcode.cn/problems/number-of-provinces/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [0547. 省份数量 - 力扣](https://leetcode.cn/problems/number-of-provinces/) ## 题目大意 **描述**: 有 `n` 个城市,其中一些彼此相连,另一些没有相连。如果城市 `a` 与城市 `b` 直接相连,且城市 `b` 与城市 `c` 直接相连,那么城市 `a` 与城市 `c` 间接相连。 「省份」是由一组直接或间接链接的城市组成,组内不含有其他没有相连的城市。 现在给定一个 `n * n` 的矩阵 `isConnected` 表示城市的链接关系。其中 `isConnected[i][j] = 1` 表示第 `i` 个城市和第 `j` 个城市直接相连,`isConnected[i][j] = 0` 表示第 `i` 个城市和第 `j` 个城市没有相连。 **要求**: 根据给定的城市关系,返回「省份」的数量。 **说明**: - $1 \le n \le 200$。 - $n == isConnected.length$。 - $n == isConnected[i].length$。 - $isConnected[i][j]$ 为 $1$ 或 $0$。 - $isConnected[i][i] == 1$。 - $isConnected[i][j] == isConnected[j][i]$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/12/24/graph1.jpg) ```python 输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]] 输出:2 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/12/24/graph2.jpg) ```python 输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]] 输出:3 ``` ## 解题思路 ### 思路 1:并查集 1. 遍历矩阵 `isConnected`。如果 `isConnected[i][j] == 1`,将 `i` 节点和 `j` 节点相连。 2. 然后判断每个城市节点的根节点,然后统计不重复的根节点有多少个,即为「省份」的数量。 ### 思路 1:代码 ```python class UnionFind: def __init__(self, n): # 初始化 self.fa = [i for i in range(n)] # 每个元素的集合编号初始化为数组 fa 的下标索引 def find(self, x): # 查找元素根节点的集合编号内部实现方法 while self.fa[x] != x: # 递归查找元素的父节点,直到根节点 self.fa[x] = self.fa[self.fa[x]] # 隔代压缩优化 x = self.fa[x] return x # 返回元素根节点的集合编号 def union(self, x, y): # 合并操作:令其中一个集合的树根节点指向另一个集合的树根节点 root_x = self.find(x) root_y = self.find(y) if root_x == root_y: # x 和 y 的根节点集合编号相同,说明 x 和 y 已经同属于一个集合 return False self.fa[root_x] = root_y # x 的根节点连接到 y 的根节点上,成为 y 的根节点的子节点 return True def is_connected(self, x, y): # 查询操作:判断 x 和 y 是否同属于一个集合 return self.find(x) == self.find(y) class Solution: def findCircleNum(self, isConnected: List[List[int]]) -> int: size = len(isConnected) union_find = UnionFind(size) cnt = size for i in range(size): for j in range(i + 1, size): if isConnected[i][j] == 1: if union_find.union(i, j): cnt -= 1 return cnt ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \times \alpha(n))$。其中 $n$ 是城市的数量,$\alpha$ 是反 `Ackerman` 函数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0500-0599/optimal-division.md ================================================ # [0553. 最优除法](https://leetcode.cn/problems/optimal-division/) - 标签:数组、数学、动态规划 - 难度:中等 ## 题目链接 - [0553. 最优除法 - 力扣](https://leetcode.cn/problems/optimal-division/) ## 题目大意 **描述**: 给定一正整数数组 $nums$,$nums$ 中的相邻整数将进行浮点除法。 - 例如,$nums = [2,3,4]$,我们将求表达式的值 `"2/3/4"`。 但是,你可以在任意位置添加任意数目的括号,来改变算数的优先级。 **要求**: 找出怎么添加括号,以便计算后的表达式的值为最大值。 以字符串格式返回具有最大值的对应表达式。 **说明**: - 注意:你的表达式不应该包含多余的括号。 - $1 \le nums.length \le 10$。 - $2 \le nums[i] \le 1000$。 - 对于给定的输入只有一种最优除法。 **示例**: - 示例 1: ```python 输入: [1000,100,10,2] 输出: "1000/(100/10/2)" 解释: 1000/(100/10/2) = 1000/((100/10)/2) = 200 但是,以下加粗的括号 "1000/((100/10)/2)" 是冗余的, 因为他们并不影响操作的优先级,所以你需要返回 "1000/(100/10/2)"。 其他用例: 1000/(100/10)/2 = 50 1000/(100/(10/2)) = 50 1000/100/10/2 = 0.5 1000/100/(10/2) = 2 ``` - 示例 2: ```python 输入: nums = [2,3,4] 输出: "2/(3/4)" 解释: (2/(3/4)) = 8/3 = 2.667 可以看出,在尝试了所有的可能性之后,我们无法得到一个结果大于 2.667 的表达式。 ``` ## 解题思路 ### 思路 1:数学分析 要使表达式 $nums[0] / nums[1] / nums[2] / ... / nums[n-1]$ 的值最大,我们需要让分子尽可能大,分母尽可能小。 观察:$nums[0]$ 必须作为分子,$nums[1]$ 必须作为分母。要使结果最大,应该让 $nums[1]$ 之后的所有数都作为分母的一部分,即让它们连除。 最优方案:$nums[0] / (nums[1] / nums[2] / ... / nums[n-1])$ 这样 $nums[1]$ 之后的所有数都会作为分母,使得分母最小,结果最大。 特殊情况: - 如果数组长度为 $1$,直接返回 $nums[0]$。 - 如果数组长度为 $2$,返回 $nums[0] / nums[1]$。 ### 思路 1:代码 ```python class Solution: def optimalDivision(self, nums: List[int]) -> str: n = len(nums) # 特殊情况 if n == 1: return str(nums[0]) if n == 2: return f"{nums[0]}/{nums[1]}" # 一般情况:nums[0] / (nums[1] / nums[2] / ... / nums[n-1]) result = f"{nums[0]}/({nums[1]}" for i in range(2, n): result += f"/{nums[i]}" result += ")" return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,需要遍历数组一次构建字符串。 - **空间复杂度**:$O(n)$,字符串的长度为 $O(n)$。 ================================================ FILE: docs/solutions/0500-0599/out-of-boundary-paths.md ================================================ # [0576. 出界的路径数](https://leetcode.cn/problems/out-of-boundary-paths/) - 标签:动态规划 - 难度:中等 ## 题目链接 - [0576. 出界的路径数 - 力扣](https://leetcode.cn/problems/out-of-boundary-paths/) ## 题目大意 **描述**:有一个大小为 $m \times n$ 的网络和一个球。球的起始位置为 $(startRow, startColumn)$。你可以将球移到在四个方向上相邻的单元格内(可以穿过网格边界到达网格之外)。最多可以移动 $maxMove$ 次球。 现在给定五个整数 $m$、$n$、$maxMove$、$startRow$ 以及 $startColumn$。 **要求**:找出并返回可以将球移出边界的路径数量。因为答案可能非常大,返回对 $10^9 + 7$ 取余后的结果。 **说明**: - $1 \le m, n \le 50$。 - $0 \le maxMove \le 50$。 - $0 \le startRow < m$。 - $0 \le startColumn < n$。 **示例**: - 示例 1: ```python 输入:m = 2, n = 2, maxMove = 2, startRow = 0, startColumn = 0 输出:6 ``` ![](https://assets.leetcode.com/uploads/2021/04/28/out_of_boundary_paths_1.png) ## 解题思路 ### 思路 1:记忆化搜索 1. 问题的状态定义为:从位置 $(i, j)$ 出发,最多使用 $moveCount$ 步,可以将球移出边界的路径数量。 2. 定义一个 $m \times n \times (maxMove + 1)$ 的三维数组 $memo$ 用于记录已经计算过的路径数量。 3. 定义递归函数 $dfs(i, j, moveCount)$ 用于计算路径数量。 1. 如果 $(i, j)$ 已经出界,则说明找到了一条路径,返回方案数为 $1$。 2. 如果没有移动次数了,则返回方案数为 $0$。 3. 定义方案数 $ans$,遍历四个方向,递归计算四个方向的方案数,累积到 $ans$ 中,并进行取余。 4. 返回方案数 $ans$。 4. 调用递归函数 $dfs(startRow, startColumn, maxMove)$,并将其返回值作为答案进行返回。 ### 思路 1:代码 ```python class Solution: def findPaths(self, m: int, n: int, maxMove: int, startRow: int, startColumn: int) -> int: directions = {(1, 0), (-1, 0), (0, 1), (0, -1)} mod = 10 ** 9 + 7 memo = [[[-1 for _ in range(maxMove + 1)] for _ in range(n)] for _ in range(m)] def dfs(i, j, moveCount): if i < 0 or i >= m or j < 0 or j >= n: return 1 if moveCount == 0: return 0 if memo[i][j][moveCount] != -1: return memo[i][j][moveCount] ans = 0 for direction in directions: new_i = i + direction[0] new_j = j + direction[1] ans += dfs(new_i, new_j, moveCount - 1) ans %= mod memo[i][j][moveCount] = ans return ans return dfs(startRow, startColumn, maxMove) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n \times maxMove)$。 - **空间复杂度**:$O(m \times n \times maxMove)$。 ### 思路 2:动态规划 我们需要统计从 $(startRow, startColumn)$ 位置出发,最多移动 $maxMove$ 次能够穿过边界的所有路径数量。则我们可以根据位置和移动步数来阶段划分和定义状态。 ###### 1. 阶段划分 按照位置进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j][k]$ 表示为:从位置 $(i, j)$ 最多移动 $k$ 次最终穿过边界的所有路径数量。 ###### 3. 状态转移方程 因为球可以在上下左右四个方向上进行移动,所以对于位置 $(i, j)$,最多移动 $k$ 次最终穿过边界的所有路径数量取决于周围四个方向上最多经过 $k - 1$ 次穿过对应位置上的所有路径数量和。 即:$dp[i][j][k] = dp[i - 1][j][k - 1] + dp[i + 1][j][k - 1] + dp[i][j - 1][k - 1] + dp[i][j + 1][k - 1]$。 ###### 4. 初始条件 如果位置 $[i, j]$ 已经处于边缘,只差一步就穿过边界。则此时位置 $(i, j)$ 最多移动 $k$ 次最终穿过边界的所有路径数量取决于有相邻多少个方向是边界。也可以通过对上面 $(i - 1, j)$、$(i + 1, j)$、$(i, j - 1)$、$(i, j + 1)$ 是否已经穿过边界进行判断(每一个方向穿过一次,就累积一次),来计算路径数目。然后将其作为初始条件。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j][k]$ 表示为:从位置 $(i, j)$ 最多移动 $k$ 次最终穿过边界的所有路径数量。则最终答案为 $dp[startRow][startColumn][maxMove]$。 ### 思路 2:动态规划代码 ```python class Solution: def findPaths(self, m: int, n: int, maxMove: int, startRow: int, startColumn: int) -> int: directions = {(1, 0), (-1, 0), (0, 1), (0, -1)} mod = 10 ** 9 + 7 dp = [[[0 for _ in range(maxMove + 1)] for _ in range(n)] for _ in range(m)] for k in range(1, maxMove + 1): for i in range(m): for j in range(n): for direction in directions: new_i = i + direction[0] new_j = j + direction[1] if 0 <= new_i < m and 0 <= new_j < n: dp[i][j][k] = (dp[i][j][k] + dp[new_i][new_j][k - 1]) % mod else: dp[i][j][k] = (dp[i][j][k] + 1) % mod return dp[startRow][startColumn][maxMove] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(m \times n \times maxMove)$。三重循环遍历的时间复杂度为 $O(m \times n \times maxMove)$。 - **空间复杂度**:$O(m \times n \times maxMove)$。使用了三维数组保存状态,所以总体空间复杂度为 $O(m \times n \times maxMove)$。 ================================================ FILE: docs/solutions/0500-0599/output-contest-matches.md ================================================ # [0544. 输出比赛匹配对](https://leetcode.cn/problems/output-contest-matches/) - 标签:递归、字符串、模拟 - 难度:中等 ## 题目链接 - [0544. 输出比赛匹配对 - 力扣](https://leetcode.cn/problems/output-contest-matches/) ## 题目大意 **描述**: 给定一个整数 $n$,表示有 $n$ 支队伍参加 NBA 季后赛比赛,编号从 $1$ 到 $n$。 比赛规则: - 第一轮:编号最小的队伍与编号最大的队伍配对,第二小的与第二大的配对,以此类推。 - 每轮比赛后,获胜队伍进入下一轮。 - 重复上述过程,直到决出冠军。 **要求**: 输出表示比赛配对的字符串。 使用括号 `'('` 和 `')'` 以及逗号 `','` 来表示比赛的匹配情况。其中括号用来表示匹配,逗号用来表示分组。 **说明**: - $n == 2^x$,并且 $x$ 在范围 $[1, 12]$ 内。 **示例**: - 示例 1: ```python 输入:n = 4 输出:"((1,4),(2,3))" 解释: 第一轮:队伍 1 和 4 配对,队伍 2 和 3 配对。 第二轮:(1,4) 的获胜者和 (2,3) 的获胜者配对。 ``` - 示例 2: ```python 输入:n = 8 输出:"(((1,8),(4,5)),((2,7),(3,6)))" 解释: 第一轮:(1,8),(2,7),(3,6),(4,5) 第二轮:((1,8),(4,5)),((2,7),(3,6)) 第三轮:(((1,8),(4,5)),((2,7),(3,6))) 由于第三轮会决出最终胜者,故输出答案为(((1,8),(4,5)),((2,7),(3,6))) ``` ## 解题思路 ### 思路 1:模拟 + 递归 使用列表存储每轮的配对结果,初始时每个队伍是一个字符串。 每轮比赛: 1. 将队伍列表首尾配对:$teams[i]$ 和 $teams[n-1-i]$ 配对 2. 配对结果为 $(teams[i], teams[n-1-i])$ 3. 将配对结果作为新的队伍列表 4. 重复直到只剩一个元素 ### 思路 1:代码 ```python class Solution: def findContestMatch(self, n: int) -> str: # 初始化队伍列表 teams = [str(i) for i in range(1, n + 1)] # 模拟每轮比赛 while len(teams) > 1: next_round = [] # 首尾配对 for i in range(len(teams) // 2): match = f"({teams[i]},{teams[len(teams) - 1 - i]})" next_round.append(match) teams = next_round return teams[0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,共有 $\log n$ 轮比赛,每轮需要处理 $O(n)$ 个队伍(字符串拼接)。 - **空间复杂度**:$O(n)$,需要存储每轮的配对结果。 ================================================ FILE: docs/solutions/0500-0599/perfect-number.md ================================================ # [0507. 完美数](https://leetcode.cn/problems/perfect-number/) - 标签:数学 - 难度:简单 ## 题目链接 - [0507. 完美数 - 力扣](https://leetcode.cn/problems/perfect-number/) ## 题目大意 **描述**: 对于一个「正整数」,如果它和除了它自身以外的所有「正因子」之和相等,我们称它为「完美数」。 给定一个整数 $n$。 **要求**: 如果是完美数,返回 true;否则返回 false。 **说明**: - $1 \le num \le 10^{8}$。 **示例**: - 示例 1: ```python 输入:num = 28 输出:true 解释:28 = 1 + 2 + 4 + 7 + 14 1, 2, 4, 7, 和 14 是 28 的所有正因子。 ``` - 示例 2: ```python 输入:num = 7 输出:false ``` ## 解题思路 ### 思路 1:枚举因子 完美数的定义:一个正整数等于它所有正因子(不包括自身)之和。 我们可以枚举从 $1$ 到 $\sqrt{num}$ 的所有因子。对于每个因子 $i$: - 如果 $num \% i == 0$,那么 $i$ 和 $num / i$ 都是因子(注意 $i \neq num / i$ 时,两个都要加上)。 - 累加所有因子,最后检查是否等于 $num$。 注意:$1$ 不是完美数(因为 $1$ 没有除了自身以外的正因子)。 ### 思路 1:代码 ```python class Solution: def checkPerfectNumber(self, num: int) -> bool: if num == 1: return False factor_sum = 1 # 1 是因子 # 枚举从 2 到 sqrt(num) 的因子 i = 2 while i * i <= num: if num % i == 0: factor_sum += i # 如果 i != num / i,也要加上 num / i if i != num // i: factor_sum += num // i i += 1 return factor_sum == num ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\sqrt{num})$,需要枚举到 $\sqrt{num}$ 的所有因子。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/permutation-in-string.md ================================================ # [0567. 字符串的排列](https://leetcode.cn/problems/permutation-in-string/) - 标签:哈希表、双指针、字符串、滑动窗口 - 难度:中等 ## 题目链接 - [0567. 字符串的排列 - 力扣](https://leetcode.cn/problems/permutation-in-string/) ## 题目大意 **描述**:给定两个字符串 $s1$ 和 $s2$ 。 **要求**:判断 $s2$ 是否包含 $s1$ 的排列。如果包含,返回 $True$;否则,返回 $False$。 **说明**: - $1 \le s1.length, s2.length \le 10^4$。 - $s1$ 和 $s2$ 仅包含小写字母。 **示例**: - 示例 1: ```python 输入:s1 = "ab" s2 = "eidbaooo" 输出:true 解释:s2 包含 s1 的排列之一 ("ba"). ``` - 示例 2: ```python 输入:s1= "ab" s2 = "eidboaoo" 输出:False ``` ## 解题思路 ### 思路 1:滑动窗口 题目要求判断 $s2$ 是否包含 $s1$ 的排列,则 $s2$ 的子串长度等于 $s1$ 的长度。我们可以维护一个长度为字符串 $s1$ 长度的固定长度的滑动窗口。 先统计出字符串 $s1$ 中各个字符的数量,我们用 $s1\_count$ 来表示。这个过程可以用字典、数组来实现,也可以直接用 `collections.Counter()` 实现。再统计 $s2$ 对应窗口内的字符数量 $window\_count$,然后不断向右滑动,然后进行比较。如果对应字符数量相同,则返回 $True$,否则继续滑动。直到末尾时,返回 $False$。整个解题步骤具体如下: 1. $s1\_count$ 用来统计 $s1$ 中各个字符数量。$window\_count$ 用来维护窗口中 $s2$ 对应子串的各个字符数量。$window\_size$ 表示固定窗口的长度,值为 $len(s1)$。 2. 先统计出 $s1$ 中各个字符数量。 3. $left$ 、$right$ 都指向序列的第一个元素,即:`left = 0`,`right = 0`。 4. 向右移动 $right$,先将 $len(s1)$ 个元素填入窗口中。 5. 当窗口元素个数为 $window\_size$ 时,即:$right - left + 1 \ge window\_size$ 时,判断窗口内各个字符数量 $window\_count$ 是否等于 $s1 $ 中各个字符数量 $s1\_count$。 1. 如果等于,直接返回 $True$。 2. 如果不等于,则向右移动 $left$,从而缩小窗口长度,即 `left += 1`,使得窗口大小始终保持为 $window\_size$。 6. 重复 $4 \sim 5$ 步,直到 $right$ 到达数组末尾。返回 $False$。 ### 思路 1:代码 ```python import collections class Solution: def checkInclusion(self, s1: str, s2: str) -> bool: left, right = 0, 0 s1_count = collections.Counter(s1) window_count = collections.Counter() window_size = len(s1) while right < len(s2): window_count[s2[right]] += 1 if right - left + 1 >= window_size: if window_count == s1_count: return True window_count[s2[left]] -= 1 if window_count[s2[left]] == 0: del window_count[s2[left]] left += 1 right += 1 return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m + |\sum|)$,其中 $n$、$m$ 分别是字符串 $s1$、$s2$ 的长度,$\sum$ 是字符集,本题中 $|\sum| = 26$。 - **空间复杂度**:$O(|\sum|)$。 ================================================ FILE: docs/solutions/0500-0599/random-flip-matrix.md ================================================ # [0519. 随机翻转矩阵](https://leetcode.cn/problems/random-flip-matrix/) - 标签:水塘抽样、哈希表、数学、随机化 - 难度:中等 ## 题目链接 - [0519. 随机翻转矩阵 - 力扣](https://leetcode.cn/problems/random-flip-matrix/) ## 题目大意 **描述**: 给定一个 $m \times n$ 的二元矩阵 $matrix$,且所有值被初始化为 $0$。 **要求**: 设计一个算法,随机选取一个满足 $matrix[i][j] == 0$ 的下标 $(i, j)$,并将它的值变为 $1$。所有满足 $matrix[i][j] == 0$ 的下标 $(i, j)$ 被选取的概率应当均等。 尽量最少调用内置的随机函数,并且优化时间和空间复杂度。 实现 Solution 类: - `Solution(int m, int n)` 使用二元矩阵的大小 $m$ 和 $n$ 初始化该对象。 - `int[] flip()` 返回一个满足 $matrix[i][j] == 0$ 的随机下标 $[i, j]$,并将其对应格子中的值变为 $1$。 - `void reset()` 将矩阵中所有的值重置为 $0$。 **说明**: - $1 \le m, n \le 10^{4}$。 - 每次调用 `flip` 时,矩阵中至少存在一个值为 $0$ 的格子。 - 最多调用 $10^{3}$ 次 `flip` 和 `reset` 方法。 **示例**: - 示例 1: ```python 输入 ["Solution", "flip", "flip", "flip", "reset", "flip"] [[3, 1], [], [], [], [], []] 输出 [null, [1, 0], [2, 0], [0, 0], null, [2, 0]] 解释 Solution solution = new Solution(3, 1); solution.flip(); // 返回 [1, 0],此时返回 [0,0]、[1,0] 和 [2,0] 的概率应当相同 solution.flip(); // 返回 [2, 0],因为 [1,0] 已经返回过了,此时返回 [2,0] 和 [0,0] 的概率应当相同 solution.flip(); // 返回 [0, 0],根据前面已经返回过的下标,此时只能返回 [0,0] solution.reset(); // 所有值都重置为 0 ,并可以再次选择下标返回 solution.flip(); // 返回 [2, 0],此时返回 [0,0]、[1,0] 和 [2,0] 的概率应当相同 ``` ## 解题思路 ### 思路 1:哈希表 + 映射交换 这道题要求随机翻转矩阵中的 $0$,且每个 $0$ 被选中的概率相等。关键是避免存储整个矩阵。 核心思路: 1. 将二维矩阵映射为一维数组,位置 $(i, j)$ 对应索引 $i \times n + j$。 2. 维护变量 $total$,表示剩余可翻转的位置数量,初始为 $m \times n$。 3. 使用哈希表 $pos\_map$ 存储被翻转位置的映射关系(类似数组交换)。 4. `flip` 操作: - 随机生成 $[0, total-1]$ 范围内的索引 $idx$。 - 如果 $idx$ 在哈希表中,取映射值;否则取 $idx$ 本身。 - 将 $idx$ 位置与 $total-1$ 位置交换(通过哈希表记录)。 - $total$ 减 $1$。 - 将一维索引转换为二维坐标返回。 5. `reset` 操作:清空哈希表,重置 $total$。 ### 思路 1:代码 ```python import random class Solution: def __init__(self, m: int, n: int): self.m = m self.n = n self.total = m * n # 剩余可翻转位置数量 self.pos_map = {} # 位置映射 def flip(self) -> List[int]: # 随机选择一个位置 idx = random.randint(0, self.total - 1) # 获取实际位置(如果被映射过则取映射值) actual_idx = self.pos_map.get(idx, idx) # 将当前位置与最后一个位置交换 # 记录映射关系:idx 位置现在对应 total-1 位置的值 self.pos_map[idx] = self.pos_map.get(self.total - 1, self.total - 1) # 减少可用位置数量 self.total -= 1 # 将一维索引转换为二维坐标 return [actual_idx // self.n, actual_idx % self.n] def reset(self) -> None: self.total = self.m * self.n self.pos_map.clear() # Your Solution object will be instantiated and called as such: # obj = Solution(m, n) # param_1 = obj.flip() # obj.reset() ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `flip`:$O(1)$,哈希表操作和随机数生成都是常数时间。 - `reset`:$O(1)$,清空哈希表。 - **空间复杂度**:$O(k)$,其中 $k$ 为调用 `flip` 的次数,哈希表最多存储 $k$ 个映射关系。 ================================================ FILE: docs/solutions/0500-0599/random-pick-with-weight.md ================================================ # [0528. 按权重随机选择](https://leetcode.cn/problems/random-pick-with-weight/) - 标签:数组、数学、二分查找、前缀和、随机化 - 难度:中等 ## 题目链接 - [0528. 按权重随机选择 - 力扣](https://leetcode.cn/problems/random-pick-with-weight/) ## 题目大意 **描述**: 给定一个下标从 $0$ 开始的正整数数组 $w$,其中 $w[i]$ 代表第 $i$ 个下标的权重。 **要求**: 实现一个函数 `pickIndex`,它可以「随机地」从范围 $[0, w.length - 1]$ 内(含 $0$ 和 $w.length - 1$)选出并返回一个下标。选取下标 $i$ 的 概率 为 $w[i] / sum(w)$ 。 - 例如,对于 $w = [1, 3]$,挑选下标 $0$ 的概率为 $1 / (1 + 3) = 0.25$ (即,25%),而选取下标 $1$ 的概率为 $3 / (1 + 3) = 0.75$(即,75%)。 **说明**: - $1 \le w.length \le 10^{4}$。 - $1 \le w[i] \le 10^{5}$。 - $pickIndex$ 将被调用不超过 $10^{4}$ 次。 **示例**: - 示例 1: ```python 输入: ["Solution","pickIndex"] [[[1]],[]] 输出: [null,0] 解释: Solution solution = new Solution([1]); solution.pickIndex(); // 返回 0,因为数组中只有一个元素,所以唯一的选择是返回下标 0。 ``` - 示例 2: ```python 输入: ["Solution","pickIndex","pickIndex","pickIndex","pickIndex","pickIndex"] [[[1,3]],[],[],[],[],[]] 输出: [null,1,1,1,1,0] 解释: Solution solution = new Solution([1, 3]); solution.pickIndex(); // 返回 1,返回下标 1,返回该下标概率为 3/4 。 solution.pickIndex(); // 返回 1 solution.pickIndex(); // 返回 1 solution.pickIndex(); // 返回 1 solution.pickIndex(); // 返回 0,返回下标 0,返回该下标概率为 1/4 。 由于这是一个随机问题,允许多个答案,因此下列输出都可以被认为是正确的: [null,1,1,1,1,0] [null,1,1,1,1,1] [null,1,1,1,0,0] [null,1,1,1,0,1] [null,1,0,1,0,0] ...... 诸若此类。 ``` ## 解题思路 ### 思路 1:前缀和 + 二分查找 这道题要求按权重随机选择下标,权重越大被选中的概率越大。 核心思路: 1. 计算前缀和数组 $prefix\_sum$,其中 $prefix\_sum[i]$ 表示前 $i+1$ 个权重的总和。 2. 总权重为 $prefix\_sum[-1]$。 3. `pickIndex` 操作: - 在 $[1, prefix\_sum[-1]]$ 范围内随机生成一个数 $target$。 - 使用二分查找在前缀和数组中找到第一个大于等于 $target$ 的位置,该位置即为选中的下标。 4. 原理:前缀和将权重映射到连续区间,权重越大对应的区间越长,被随机数命中的概率越大。 例如:$w = [1, 3]$,前缀和为 $[1, 4]$ - 下标 $0$ 对应区间 $[1, 1]$,长度为 $1$,概率为 $1/4$ - 下标 $1$ 对应区间 $[2, 4]$,长度为 $3$,概率为 $3/4$ ### 思路 1:代码 ```python import random import bisect class Solution: def __init__(self, w: List[int]): # 计算前缀和 self.prefix_sum = [] total = 0 for weight in w: total += weight self.prefix_sum.append(total) def pickIndex(self) -> int: # 在 [1, total] 范围内随机生成一个数 target = random.randint(1, self.prefix_sum[-1]) # 二分查找第一个大于等于 target 的位置 # bisect_left 返回插入位置,即第一个 >= target 的位置 return bisect.bisect_left(self.prefix_sum, target) # Your Solution object will be instantiated and called as such: # obj = Solution(w) # param_1 = obj.pickIndex() ``` ### 思路 1:复杂度分析 - **时间复杂度**: - 初始化:$O(n)$,其中 $n$ 为权重数组长度,需要计算前缀和。 - `pickIndex`:$O(\log n)$,二分查找的时间复杂度。 - **空间复杂度**:$O(n)$,存储前缀和数组。 ================================================ FILE: docs/solutions/0500-0599/range-addition-ii.md ================================================ # [0598. 区间加法 II](https://leetcode.cn/problems/range-addition-ii/) - 标签:数组、数学 - 难度:简单 ## 题目链接 - [0598. 区间加法 II - 力扣](https://leetcode.cn/problems/range-addition-ii/) ## 题目大意 **描述**: 给定一个 $m \times n$ 的矩阵 $M$ 和一个操作数组 $op$ 。矩阵初始化时所有的单元格都为 $0$。$ops[i] = [ai, bi]$ 意味着当所有的 $0 \le x < ai$ 和 $0 \le y < bi$ 时, $M[x][y]$ 应该加 $1$。 **要求**: 在执行完所有操作后,计算并返回「矩阵中最大整数的个数」。 **说明**: - $1 \le m, n \le 4 \times 10^{4}$。 - $0 \le ops.length \le 10^{4}$。 - $ops[i].length == 2$。 - $1 \le ai \le m$。 - $1 \le bi \le n$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/02/ex1.jpg) ```python 输入: m = 3, n = 3,ops = [[2,2],[3,3]] 输出: 4 解释: M 中最大的整数是 2, 而且 M 中有4个值为2的元素。因此返回 4。 ``` - 示例 2: ```python 输入: m = 3, n = 3, ops = [[2,2],[3,3],[3,3],[3,3],[2,2],[3,3],[3,3],[3,3],[2,2],[3,3],[3,3],[3,3]] 输出: 4 ``` ## 解题思路 ### 思路 1:找最小重叠区域 观察题目,每次操作 $ops[i] = [a_i, b_i]$ 会将矩阵 $M$ 中所有满足 $0 \le x < a_i$ 且 $0 \le y < b_i$ 的位置 $M[x][y]$ 加 1。这意味着每次操作都会影响从 $(0, 0)$ 到 $(a_i-1, b_i-1)$ 的矩形区域。 执行所有操作后,被操作次数最多的位置就是所有操作都覆盖到的区域,即所有矩形区域的交集。这个交集区域的大小就是所有 $a_i$ 的最小值和所有 $b_i$ 的最小值组成的矩形。 因此,我们只需要找到所有操作中 $a_i$ 的最小值 $min_a$ 和 $b_i$ 的最小值 $min_b$,那么最大整数的个数就是 $min_a \times min_b$。如果没有操作,则整个矩阵都是最大值,返回 $m \times n$。 ### 思路 1:代码 ```python class Solution: def maxCount(self, m: int, n: int, ops: List[List[int]]) -> int: # 如果没有操作,整个矩阵都是最大值 if not ops: return m * n # 找到所有操作中 a_i 和 b_i 的最小值 min_a = min(op[0] for op in ops) min_b = min(op[1] for op in ops) # 返回最小重叠区域的面积 return min_a * min_b ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k)$,其中 $k$ 是 $ops$ 的长度,需要遍历所有操作找到最小值。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/relative-ranks.md ================================================ # [0506. 相对名次](https://leetcode.cn/problems/relative-ranks/) - 标签:数组、排序、堆(优先队列) - 难度:简单 ## 题目链接 - [0506. 相对名次 - 力扣](https://leetcode.cn/problems/relative-ranks/) ## 题目大意 **描述**:给定一个长度为 $n$ 的数组 $score$。其中 $score[i]$ 表示第 $i$ 名运动员在比赛中的成绩。所有成绩互不相同。 **要求**:找出他们的相对名次,并授予前三名对应的奖牌。前三名运动员将会被分别授予「金牌(`"Gold Medal"`)」,「银牌(`"Silver Medal"`)」和「铜牌(`"Bronze Medal"`)」。 **说明**: - $n == score.length$。 - $1 \le n \le 10^4$。 - $0 \le score[i] \le 10^6$。 - $score$ 中的所有值互不相同。 **示例**: - 示例 1: ```python 输入:score = [5,4,3,2,1] 输出:["Gold Medal","Silver Medal","Bronze Medal","4","5"] 解释:名次为 [1st, 2nd, 3rd, 4th, 5th] 。 ``` - 示例 2: ```python 输入:score = [10,3,8,9,4] 输出:["Gold Medal","5","Bronze Medal","Silver Medal","4"] 解释:名次为 [1st, 5th, 3rd, 2nd, 4th] 。 ``` ## 解题思路 ### 思路 1:排序 1. 先对数组 $score$ 进行排序。 2. 再将对应前三个位置上的元素替换成对应的字符串:`"Gold Medal"`, `"Silver Medal"`, `"Bronze Medal"`。 ### 思路 1:代码 ```python class Solution: def shellSort(self, arr): size = len(arr) gap = size // 2 while gap > 0: for i in range(gap, size): temp = arr[i] j = i while j >= gap and arr[j - gap] < temp: arr[j] = arr[j - gap] j -= gap arr[j] = temp gap = gap // 2 return arr def findRelativeRanks(self, score: List[int]) -> List[str]: nums = score.copy() nums = self.shellSort(nums) score_map = dict() for i in range(len(nums)): score_map[nums[i]] = i + 1 res = [] for i in range(len(score)): if score[i] == nums[0]: res.append("Gold Medal") elif score[i] == nums[1]: res.append("Silver Medal") elif score[i] == nums[2]: res.append("Bronze Medal") else: res.append(str(score_map[score[i]])) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。因为采用了时间复杂度为 $O(n \times \log n)$ 的希尔排序。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0500-0599/remove-boxes.md ================================================ # [0546. 移除盒子](https://leetcode.cn/problems/remove-boxes/) - 标签:记忆化搜索、数组、动态规划 - 难度:困难 ## 题目链接 - [0546. 移除盒子 - 力扣](https://leetcode.cn/problems/remove-boxes/) ## 题目大意 **描述**:给定一个代表不同颜色盒子的正数数组 $boxes$,盒子的颜色由不同正数组成,其中 $boxes[i]$ 表示第 $i$ 个盒子的颜色。 我们将经过若干轮操作去去掉盒子,直到所有盒子都去掉为止。每一轮我们可以移除具有相同颜色的连续 $k$ 个盒子($k \ge 1$),这样一轮之后,我们将获得 $k \times k$ 个积分。 **要求**:返回我们能获得的最大积分和。 **说明**: - $1 \le boxes.length \le 100$。 - $1 \le boxes[i] \le 100$。 **示例**: - 示例 1: ```python 输入:boxes = [1,3,2,2,2,3,4,3,1] 输出:23 解释: [1, 3, 2, 2, 2, 3, 4, 3, 1] ----> [1, 3, 3, 4, 3, 1] (3*3=9 分) ----> [1, 3, 3, 3, 1] (1*1=1 分) ----> [1, 1] (3*3=9 分) ----> [] (2*2=4 分) ``` - 示例 2: ```python 输入:boxes = [1,1,1] 输出:9 ``` ## 解题思路 ### 思路 1:动态规划 对于每个盒子, 如果使用二维状态 $dp[i][j]$ 表示为:移除区间 $[i, j]$ 之间的盒子,所能够得到的最大积分和。但实际上,移除区间 $[i, j]$ 之间盒子,所能得到的最大积分和,并不只依赖于子区间,也依赖于之前移除其他区间对当前区间的影响。比如当前区间的某个值和其他区间的相同值连起来可以获得更高的额分数。 因此,我们需要再二维状态的基础上,增加更多维数的状态。 对于当前区间 $[i, j]$,我们需要凑一些尽可能长的同色盒子一起消除,从而获得更高的分数。我们不妨每次都选择消除区间 $[i, j]$ 中最后一个盒子 $boxes[j]$,并且记录 $boxes[j]$ 之后与 $boxes[j]$ 颜色相同的盒子数量。 ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j][k]$ 表示为:移除区间 $[i, j]$ 之间的盒子,并且区间右侧有 $k$ 个与 $boxes[j]$ 颜色相同的盒子,所能够得到的最大积分和。 ###### 3. 状态转移方程 - 当区间长度为 $1$ 时,当前区间只有一个盒子,区间末尾有 $k$ 个与 $boxes[j]$ 颜色相同的盒子,所能够得到的最大积分为 $(k + 1) \times (k + 1)$。 - 当区间长度大于 $1$ 时,对于区间末尾的 $k$ 个与 $boxes[j]$ 颜色相同的盒子,有两种处理方式: - 将末尾的盒子移除,所能够得到的最大积分为:移除末尾盒子之前能够获得的最大积分和,再加上本轮移除末尾盒子能够获得的积分和,即:$dp[i][j - 1][0] + (k + 1) \times (k + 1)$。 - 在区间中找到一个位置 $t$,使得第 $t$ 个盒子与第 $j$ 个盒子颜色相同,先将区间 $[t + 1, j - 1]$ 的盒子消除,然后继续凑同色盒子,即:$dp[t + 1][j - 1][0] + dp[i][t][k + 1]$。 ###### 4. 初始条件 - 区间长度为 $1$ 时,当前区间只有一个盒子,区间末尾有 $k$ 个与 $boxes[j]$ 颜色相同的盒子,所能够得到的最大积分为 $(k + 1) \times (k + 1)$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j][k]$ 表示为:移除区间 $[i, j]$ 之间的盒子,并且区间右侧有 $k$ 个与 $boxes[j]$ 颜色相同的盒子,所能够得到的最大积分和。所以最终结果为 $dp[0][size - 1][0]$。 ### 思路 1:代码 ```python class Solution: def removeBoxes(self, boxes: List[int]) -> int: size = len(boxes) dp = [[[0 for _ in range(size)] for _ in range(size)] for _ in range(size)] for l in range(1, size + 1): for i in range(size): j = i + l - 1 if j >= size: break for k in range(size - j): if l == 1: dp[i][j][k] = max(dp[i][j][k], (k + 1) * (k + 1)) else: dp[i][j][k] = max(dp[i][j][k], dp[i][j - 1][0] + (k + 1) * (k + 1)) for t in range(i, j): if boxes[t] == boxes[j]: dp[i][j][k] = max(dp[i][j][k], dp[t + 1][j - 1][0] + dp[i][t][k + 1]) return dp[0][size - 1][0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^4)$,其中 $n$ 为数组 $boxes$ 的元素个数。 - **空间复杂度**:$O(n^3)$。 ================================================ FILE: docs/solutions/0500-0599/reshape-the-matrix.md ================================================ # [0566. 重塑矩阵](https://leetcode.cn/problems/reshape-the-matrix/) - 标签:数组、矩阵、模拟 - 难度:简单 ## 题目链接 - [0566. 重塑矩阵 - 力扣](https://leetcode.cn/problems/reshape-the-matrix/) ## 题目大意 **描述**: 在 MATLAB 中,有一个非常有用的函数 $reshape$ ,它可以将一个 $m \times n$ 矩阵重塑为另一个大小不同($r \times c$)的新矩阵,但保留其原始数据。 给定一个由二维数组 $mat$ 表示的 $m \times n$ 矩阵,以及两个正整数 $r$ 和 $c$ ,分别表示想要的重构的矩阵的行数和列数。 重构后的矩阵需要将原始矩阵的所有元素以相同的「行遍历顺序」填充。 **要求**: 如果具有给定参数的 $reshape$ 操作是可行且合理的,则输出新的重塑矩阵;否则,输出原始矩阵。 **说明**: - $m == mat.length$。 - $n == mat[i].length$。 - $1 \le m, n \le 10^{3}$。 - $-10^{3} \le mat[i][j] \le 10^{3}$。 - $1 \le r, c \le 300$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/24/reshape1-grid.jpg) ```python 输入:mat = [[1,2],[3,4]], r = 1, c = 4 输出:[[1,2,3,4]] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/04/24/reshape2-grid.jpg) ```python 输入:mat = [[1,2],[3,4]], r = 2, c = 4 输出:[[1,2],[3,4]] ``` ## 解题思路 ### 思路 1:一维展开再重塑 将 $m \times n$ 的矩阵重塑为 $r \times c$ 的矩阵,需要满足 $m \times n = r \times c$。如果不满足,返回原矩阵。 我们可以将原矩阵按行遍历顺序展开成一维数组,然后按照新的行列数重新组织。对于原矩阵中的元素 $mat[i][j]$,在一维数组中的索引为 $i \times n + j$。对于新矩阵中的位置 $(i', j')$,在一维数组中的索引为 $i' \times c + j'$。 因此,我们可以直接通过索引映射来填充新矩阵:$new\_mat[i'][j'] = mat[(i' \times c + j') // n][(i' \times c + j') \% n]$。 ### 思路 1:代码 ```python class Solution: def matrixReshape(self, mat: List[List[int]], r: int, c: int) -> List[List[int]]: m, n = len(mat), len(mat[0]) # 如果元素总数不匹配,返回原矩阵 if m * n != r * c: return mat # 创建新矩阵 new_mat = [[0] * c for _ in range(r)] # 按行遍历顺序填充新矩阵 for i in range(r): for j in range(c): # 计算在一维数组中的索引 idx = i * c + j # 映射回原矩阵的坐标 new_mat[i][j] = mat[idx // n][idx % n] return new_mat ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(r \times c)$,需要遍历新矩阵的所有位置进行填充。 - **空间复杂度**:$O(r \times c)$,需要创建新矩阵存储结果(不包括输入空间)。 ================================================ FILE: docs/solutions/0500-0599/reverse-string-ii.md ================================================ # [0541. 反转字符串 II](https://leetcode.cn/problems/reverse-string-ii/) - 标签:双指针、字符串 - 难度:简单 ## 题目链接 - [0541. 反转字符串 II - 力扣](https://leetcode.cn/problems/reverse-string-ii/) ## 题目大意 **描述**: 给定一个字符串 $s$ 和一个整数 $k$。 **要求**: 从字符串开头算起,每计数至 $2 \times k$ 个字符,就反转这 $2 \times k$ 字符中的前 $k$ 个字符。 - 如果剩余字符少于 $k$ 个,则将剩余字符全部反转。 - 如果剩余字符小于 $2 \times k$ 但大于或等于 $k$ 个,则反转前 $k$ 个字符,其余字符保持原样。 **说明**: - $1 \le s.length \le 10^{4}$。 - $s$ 仅由小写英文组成。 - $1 \le k \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:s = "abcdefg", k = 2 输出:"bacdfeg" ``` - 示例 2: ```python 输入:s = "abcd", k = 2 输出:"bacd" ``` ## 解题思路 ### 思路 1:分段反转 按照题目要求,每 $2 \times k$ 个字符为一组进行处理: - 对于每组的前 $k$ 个字符进行反转 - 后 $k$ 个字符保持原样 具体实现: 1. 遍历字符串,每次步进 $2 \times k$ 个字符 2. 对于每个区间 $[i, i + 2 \times k)$: - 如果剩余字符 $\ge k$,反转前 $k$ 个字符(即 $[i, i + k)$) - 如果剩余字符 $< k$,反转所有剩余字符 ### 思路 1:代码 ```python class Solution: def reverseStr(self, s: str, k: int) -> str: s_list = list(s) n = len(s_list) # 每次处理 2k 个字符 for i in range(0, n, 2 * k): # 确定反转的右边界:取 min(i + k, n) left = i right = min(i + k - 1, n - 1) # 反转 [left, right] 区间 while left < right: s_list[left], s_list[right] = s_list[right], s_list[left] left += 1 right -= 1 return ''.join(s_list) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度,需要遍历字符串一次。 - **空间复杂度**:$O(n)$,需要将字符串转换为列表进行操作。 ================================================ FILE: docs/solutions/0500-0599/reverse-words-in-a-string-iii.md ================================================ # [0557. 反转字符串中的单词 III](https://leetcode.cn/problems/reverse-words-in-a-string-iii/) - 标签:双指针、字符串 - 难度:简单 ## 题目链接 - [0557. 反转字符串中的单词 III - 力扣](https://leetcode.cn/problems/reverse-words-in-a-string-iii/) ## 题目大意 **描述**:给定一个字符串 `s`。 **要求**:将字符串中每个单词的字符顺序进行反装,同时仍保留空格和单词的初始顺序。 **说明**: - $1 \le s.length \le 5 * 10^4$。 - `s` 包含可打印的 ASCII 字符。 - `s` 不包含任何开头或结尾空格。 - `s` 里至少有一个词。 - `s` 中的所有单词都用一个空格隔开。 **示例**: - 示例 1: ```python 输入:s = "Let's take LeetCode contest" 输出:"s'teL ekat edoCteeL tsetnoc" ``` - 示例 2: ```python 输入: s = "God Ding" 输出:"doG gniD" ``` ## 解题思路 ### 思路 1:使用额外空间 因为 Python 的字符串是不可变的,所以在原字符串空间上进行切换顺序操作肯定是不可行的了。但我们可以利用切片方法。 1. 将字符串按空格进行分割,分割成一个个的单词。 2. 再将每个单词进行反转。 3. 最后将每个单词连接起来。 ### 思路 1:代码 ```python class Solution: def reverseWords(self, s: str) -> str: return " ".join(word[::-1] for word in s.split(" ")) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0500-0599/shortest-unsorted-continuous-subarray.md ================================================ # [0581. 最短无序连续子数组](https://leetcode.cn/problems/shortest-unsorted-continuous-subarray/) - 标签:栈、贪心、数组、双指针、排序、单调栈 - 难度:中等 ## 题目链接 - [0581. 最短无序连续子数组 - 力扣](https://leetcode.cn/problems/shortest-unsorted-continuous-subarray/) ## 题目大意 **描述**: 给定一个整数数组 $nums$。 **要求**: 找出一个「连续子数组」,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。 请找出符合题意的「最短」子数组,并输出它的长度。 **说明**: - $1 \le nums.length \le 10^{4}$。 - $-10^{5} \le nums[i] \le 10^{5}$。 - 进阶:你可以设计一个时间复杂度为 $O(n)$ 的解决方案吗? **示例**: - 示例 1: ```python 输入:nums = [2,6,4,8,10,9,15] 输出:5 解释:你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。 ``` - 示例 2: ```python 输入:nums = [1,2,3,4] 输出:0 ``` ## 解题思路 ### 思路 1:双指针找边界 要找到最短的未排序连续子数组,我们需要找到: 1. 左边界:从右往左遍历,找到第一个不满足"右边所有元素都大于等于它"的位置。 2. 右边界:从左往右遍历,找到第一个不满足"左边所有元素都小于等于它"的位置。 具体算法: - 从右往左遍历,维护右边的最小值 $min\_right$。如果 $nums[i] > min\_right$,说明 $i$ 位置需要排序,更新左边界 $left = i$。 - 从左往右遍历,维护左边的最大值 $max\_left$。如果 $nums[i] < max\_left$,说明 $i$ 位置需要排序,更新右边界 $right = i$。 - 返回 $right - left + 1$(如果 $left < right$),否则返回 0。 ### 思路 1:代码 ```python class Solution: def findUnsortedSubarray(self, nums: List[int]) -> int: n = len(nums) # 从右往左找左边界 min_right = float('inf') left = -1 for i in range(n - 1, -1, -1): if nums[i] > min_right: left = i min_right = min(min_right, nums[i]) # 从左往右找右边界 max_left = float('-inf') right = -1 for i in range(n): if nums[i] < max_left: right = i max_left = max(max_left, nums[i]) # 如果找到了需要排序的子数组 if left != -1 and right != -1: return right - left + 1 return 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,需要遍历数组两次。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/single-element-in-a-sorted-array.md ================================================ # [0540. 有序数组中的单一元素](https://leetcode.cn/problems/single-element-in-a-sorted-array/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [0540. 有序数组中的单一元素 - 力扣](https://leetcode.cn/problems/single-element-in-a-sorted-array/) ## 题目大意 **描述**: 给定一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。 **要求**: 找出并返回只出现一次的那个数。 **说明**: - 设计的解决方案必须满足 $O(\log n)$ 时间复杂度和 $O(1)$ 空间复杂度。 - $1 \le nums.length \le 10^{5}$。 - $0 \le nums[i] \le 10^{5}$。 **示例**: - 示例 1: ```python 输入: nums = [1,1,2,3,3,4,4,8,8] 输出: 2 ``` - 示例 2: ```python 输入: nums = [3,3,7,7,10,11,11] 输出: 10 ``` ## 解题思路 ### 思路 1:二分查找 由于数组是有序的,且除了一个元素外,其他元素都出现两次,我们可以使用二分查找。 关键观察:在单一元素之前,所有成对出现的元素中,第一个元素出现在偶数索引,第二个元素出现在奇数索引;在单一元素之后,这个规律会反转。 具体算法: - 使用二分查找,维护区间 $[left, right]$ - 计算中点 $mid = (left + right) // 2$ - 如果 $mid$ 是偶数,检查 $nums[mid]$ 是否等于 $nums[mid+1]$: - 如果相等,说明单一元素在右半部分,$left = mid + 2$ - 否则,单一元素在左半部分,$right = mid$ - 如果 $mid$ 是奇数,检查 $nums[mid]$ 是否等于 $nums[mid-1]$: - 如果相等,说明单一元素在右半部分,$left = mid + 1$ - 否则,单一元素在左半部分,$right = mid - 1$ ### 思路 1:代码 ```python class Solution: def singleNonDuplicate(self, nums: List[int]) -> int: left, right = 0, len(nums) - 1 while left < right: mid = (left + right) // 2 # 如果 mid 是偶数,应该和 mid+1 配对 # 如果 mid 是奇数,应该和 mid-1 配对 if mid % 2 == 0: # 偶数索引,检查是否和下一个元素相等 if mid + 1 < len(nums) and nums[mid] == nums[mid + 1]: # 单一元素在右半部分 left = mid + 2 else: # 单一元素在左半部分(包括 mid) right = mid else: # 奇数索引,检查是否和前一个元素相等 if nums[mid] == nums[mid - 1]: # 单一元素在右半部分 left = mid + 1 else: # 单一元素在左半部分(包括 mid) right = mid - 1 return nums[left] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$,其中 $n$ 是数组长度,二分查找的时间复杂度。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/split-array-with-equal-sum.md ================================================ # [0548. 将数组分割成和相等的子数组](https://leetcode.cn/problems/split-array-with-equal-sum/) - 标签:数组、哈希表、前缀和 - 难度:困难 ## 题目链接 - [0548. 将数组分割成和相等的子数组 - 力扣](https://leetcode.cn/problems/split-array-with-equal-sum/) ## 题目大意 **描述**: 给定一个整数数组 $nums$。 **要求**: 判断是否能找到三个索引 $i$、$j$、$k$,将数组分成四个非空子数组,使得这四个子数组的和相等。 具体来说,需要找到三个分割点 $i$、$j$、$k$($1 \le i < j < k < n-1$),使得: - 子数组 $(0, i - 1)$,$(i + 1, j - 1)$,$(j + 1, k - 1)$,$(k + 1, n - 1)$ 的和应该相等。 如果可以找到这样的分割,返回 $true$,否则返回 $false$。 **说明**: - $n == nums.length。 - $1 \le nums.length \le 2000$。 - $-10^6 \le nums[i] \le 10^6$。 **示例**: - 示例 1: ```python 输入: nums = [1,2,1,2,1,2,1] 输出: True 解释: i = 1, j = 3, k = 5. sum(0, i - 1) = sum(0, 0) = 1 sum(i + 1, j - 1) = sum(2, 2) = 1 sum(j + 1, k - 1) = sum(4, 4) = 1 sum(k + 1, n - 1) = sum(6, 6) = 1 ``` - 示例 2: ```python 输入: nums = [1,2,1,2,1,2,1,2] 输出: false ``` ## 解题思路 ### 思路 1:前缀和 + 哈希表 关键观察:固定中间的分割点 $j$,然后在左边找满足条件的 $i$,在右边找满足条件的 $k$。 **算法步骤**: 1. 计算前缀和数组 $prefix$,其中 $prefix[i]$ 表示 $nums[0:i]$ 的和。 2. 枚举中间分割点 $j$(范围:$[3, n-4]$,保证每部分至少有一个元素)。 3. 对于每个 $j$: - 在左边找所有可能的和:遍历 $i \in [1, j-2]$,如果 $sum(nums[0:i]) = sum(nums[i+1:j])$,将这个和加入集合 $left\_sums$。 - 在右边找所有可能的和:遍历 $k \in [j+2, n-2]$,如果 $sum(nums[j+1:k]) = sum(nums[k+1:n])$,检查这个和是否在 $left\_sums$ 中。 4. 如果找到匹配,返回 $true$。 ### 思路 1:代码 ```python class Solution: def splitArray(self, nums: List[int]) -> bool: n = len(nums) if n < 7: # 至少需要 7 个元素 return False # 计算前缀和 prefix = [0] * (n + 1) for i in range(n): prefix[i + 1] = prefix[i] + nums[i] # 枚举中间分割点 j for j in range(3, n - 3): # 左边可能的和 left_sums = set() for i in range(1, j - 1): # sum[0:i] == sum[i+1:j] left_sum = prefix[i] middle_sum = prefix[j] - prefix[i + 1] if left_sum == middle_sum: left_sums.add(left_sum) # 右边可能的和 for k in range(j + 2, n - 1): # sum[j+1:k] == sum[k+1:n] middle_sum = prefix[k] - prefix[j + 1] right_sum = prefix[n] - prefix[k + 1] if middle_sum == right_sum and middle_sum in left_sums: return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是数组长度。需要枚举 $j$,对于每个 $j$ 枚举 $i$ 和 $k$。 - **空间复杂度**:$O(n)$,需要存储前缀和数组和哈希集合。 ================================================ FILE: docs/solutions/0500-0599/split-concatenated-strings.md ================================================ # [0555. 分割连接字符串](https://leetcode.cn/problems/split-concatenated-strings/) - 标签:贪心、数组、字符串 - 难度:中等 ## 题目链接 - [0555. 分割连接字符串 - 力扣](https://leetcode.cn/problems/split-concatenated-strings/) ## 题目大意 **描述**: 给定一个字符串数组 $strs$,你可以将这些字符串连接成一个循环字符串。你可以对每个字符串进行以下操作: 1. 选择反转或不反转该字符串 2. 将所有字符串按顺序连接成一个字符串 3. 在连接后的字符串中选择一个分割点,将字符串分成两部分,然后交换这两部分的位置 **要求**: 返回通过上述操作能得到的字典序最大的字符串。 **说明**: - $1 \le strs.length \le 1000$。 - $1 \le strs[i].length \le 1000$。 - $1 \le sum(strs[i].length) \le 1000$。 - $strs[i]$ 只包含小写英文字母。 **示例**: - 示例 1: ```python 输入:strs = ["abc","xyz"] 输出:"zyxcba" 解释: 反转两个字符串得到 "cba" 和 "zyx" 连接得到 "cbazyx" 在位置 3 分割得到 "cba" 和 "zyx" 交换得到 "zyxcba" ``` - 示例 2: ```python 输入:strs = ["abc"] 输出:"cba" ``` ## 解题思路 ### 思路 1:贪心 + 枚举 **核心策略**: 1. 对于每个字符串,预先选择它本身和它的反转中字典序较大的版本。 2. 枚举每个字符串作为分割点的起始位置。 3. 对于每个分割点,枚举该字符串的每个位置作为分割位置。 4. 尝试该字符串使用原串或反转串。 5. 计算分割后的结果,保留字典序最大的。 **算法步骤**: 1. 预处理:对于数组中的每个字符串 $s$,选择 $\max(s, reverse(s))$。 2. 枚举每个字符串 $strs[i]$ 作为分割的起始位置。 3. 对于当前字符串,尝试使用原串和反转串。 4. 枚举该字符串的每个位置 $j$ 作为分割点。 5. 构造候选字符串:$s[j:] + strs[i+1:] + strs[:i] + s[:j]$。 6. 更新字典序最大的结果。 ### 思路 1:代码 ```python class Solution: def splitLoopedString(self, strs: List[str]) -> str: # 预处理:对每个字符串,选择它和反转后较大的版本 strs = [max(s, s[::-1]) for s in strs] result = "" # 枚举每个字符串作为分割的起始位置 for i in range(len(strs)): # 尝试原串和反转串 for s in [strs[i], strs[i][::-1]]: # 枚举该字符串的每个位置作为分割点 for j in range(len(s)): # 构造分割后的字符串 # 从位置 j 开始的部分 + 后面的字符串 + 前面的字符串 + 位置 j 之前的部分 candidate = s[j:] + "".join(strs[i+1:]) + "".join(strs[:i]) + s[:j] result = max(result, candidate) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times L^2)$,其中 $n$ 是字符串数量,$L$ 是字符串的平均长度。需要枚举每个字符串和每个分割位置,字符串拼接需要 $O(L)$ 时间。 - **空间复杂度**:$O(n \times L)$,需要存储处理后的字符串数组。 ================================================ FILE: docs/solutions/0500-0599/squirrel-simulation.md ================================================ # [0573. 松鼠模拟](https://leetcode.cn/problems/squirrel-simulation/) - 标签:数组、数学 - 难度:中等 ## 题目链接 - [0573. 松鼠模拟 - 力扣](https://leetcode.cn/problems/squirrel-simulation/) ## 题目大意 **描述**: 在一个平面上,有一棵树、一只松鼠和若干坚果。给定: - $height$ 和 $width$:平面的高度和宽度。 - $tree$:树的位置 $[tree\_x, tree\_y]$。 - $squirrel$:松鼠的初始位置 $[squirrel\_x, squirrel\_y]$。 - $nuts$:坚果的位置列表 $[[nut1\_x, nut1\_y], [nut2\_x, nut2\_y], ...]$。 松鼠需要将所有坚果收集到树的位置。每次只能携带一个坚果,并且能够向上、下、左、右四个方向移动到相邻的单元格。 **要求**: 返回松鼠收集所有坚果饼主义放在树下的最小移动距离。 **说明**: - **距离** 是指移动的次数。 - $1 \le height, width \le 100$。 - $tree.length == 2$。 - $squirrel.length == 2$。 - $1 \le nuts.length \le 5000$。 - $nuts[i].length == 2$。 - $0 \le tree_r, squirrel_r, nuti_r \le height$。 - $0 \le tree_c, squirrel_c, nuti_c \le width$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/24/squirrel1-grid.jpg) ```python 输入:height = 5, width = 7, tree = [2,2], squirrel = [4,4], nuts = [[3,0], [2,5]] 输出:12 解释:为实现最小的距离,松鼠应该先摘 [2, 5] 位置的坚果。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/04/24/squirrel2-grid.jpg) ```python 输入:height = 1, width = 3, tree = [0,1], squirrel = [0,0], nuts = [[0,2]] 输出:3 ``` ## 解题思路 ### 思路 1:贪心算法 关键观察: - 除了第一个坚果,其他所有坚果都需要从树出发,拿到坚果再回到树,距离为 $2 \times dist(tree, nut)$ - 第一个坚果是从松鼠位置出发,距离为 $dist(squirrel, nut) + dist(nut, tree)$ 总距离 = $\sum_{i=0}^{n-1} 2 \times dist(tree, nuts[i]) - dist(tree, first\_nut) + dist(squirrel, first\_nut)$ 为了最小化总距离,应该选择使 $dist(squirrel, nut) - dist(tree, nut)$ 最小的坚果作为第一个收集的坚果。 ### 思路 1:代码 ```python class Solution: def minDistance(self, height: int, width: int, tree: List[int], squirrel: List[int], nuts: List[List[int]]) -> int: # 计算曼哈顿距离 def manhattan_distance(p1, p2): return abs(p1[0] - p2[0]) + abs(p1[1] - p2[1]) # 计算所有坚果到树的距离之和的两倍(假设都从树出发) total_distance = 0 for nut in nuts: total_distance += 2 * manhattan_distance(tree, nut) # 找到最优的第一个坚果 # 第一个坚果的额外距离是:dist(squirrel, nut) - dist(tree, nut) min_diff = float('inf') for nut in nuts: diff = manhattan_distance(squirrel, nut) - manhattan_distance(tree, nut) min_diff = min(min_diff, diff) return total_distance + min_diff ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是坚果的数量。需要遍历所有坚果两次。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/student-attendance-record-i.md ================================================ # [0551. 学生出勤记录 I](https://leetcode.cn/problems/student-attendance-record-i/) - 标签:字符串 - 难度:简单 ## 题目链接 - [0551. 学生出勤记录 I - 力扣](https://leetcode.cn/problems/student-attendance-record-i/) ## 题目大意 **描述**: 给定一个字符串 $s$ 表示一个学生的出勤记录,其中的每个字符用来标记当天的出勤情况(缺勤、迟到、到场)。记录中只含下面三种字符: - `A`:Absent,缺勤 - `L`:Late,迟到 - `P`:Present,到场 如果学生能够 同时 满足下面两个条件,则可以获得出勤奖励: - 按「总出勤」计,学生缺勤(`A`)严格 少于两天。 - 学生 不会 存在 连续 3 天或 连续 3 天以上的迟到(`L`)记录。 **要求**: 如果学生可以获得出勤奖励,返回 true ;否则,返回 false 。 **说明**: - $1 \le s.length \le 10^{3}$。 - $s[i]$ 为 `A`、`L` 或 `P`。 **示例**: - 示例 1: ```python 输入:s = "PPALLP" 输出:true 解释:学生缺勤次数少于 2 次,且不存在 3 天或以上的连续迟到记录。 ``` - 示例 2: ```python 输入:s = "PPALLL" 输出:false 解释:学生最后三天连续迟到,所以不满足出勤奖励的条件。 ``` ## 解题思路 ### 思路 1:遍历检查 根据题目要求,需要同时满足两个条件: 1. 缺勤(`A`)次数严格少于 2 天,即 $count_A < 2$ 2. 不存在连续 3 天或以上的迟到(`L`)记录 遍历字符串,统计: - `A` 的出现次数 - 连续 `L` 的最大长度 如果 `A` 次数 $\ge 2$ 或存在连续 3 个或以上的 `L`,返回 `False`;否则返回 `True`。 ### 思路 1:代码 ```python class Solution: def checkRecord(self, s: str) -> bool: absent_count = 0 late_streak = 0 max_late_streak = 0 for char in s: if char == 'A': absent_count += 1 late_streak = 0 elif char == 'L': late_streak += 1 max_late_streak = max(max_late_streak, late_streak) else: # 'P' late_streak = 0 # 检查条件:缺勤少于 2 次,且没有连续 3 天或以上的迟到 return absent_count < 2 and max_late_streak < 3 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度,需要遍历字符串一次。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/student-attendance-record-ii.md ================================================ # [0552. 学生出勤记录 II](https://leetcode.cn/problems/student-attendance-record-ii/) - 标签:动态规划 - 难度:困难 ## 题目链接 - [0552. 学生出勤记录 II - 力扣](https://leetcode.cn/problems/student-attendance-record-ii/) ## 题目大意 **描述**: 可以用字符串表示一个学生的出勤记录,其中的每个字符用来标记当天的出勤情况(缺勤、迟到、到场)。记录中只含下面三种字符: - `A`:Absent,缺勤 - `L`:Late,迟到 - `P`:Present,到场 如果学生能够 同时 满足下面两个条件,则可以获得出勤奖励: - 按「总出勤」计,学生缺勤(`A`)严格 少于两天。 - 学生 不会 存在 连续 3 天或 连续 3 天以上的迟到(`L`)记录。 给定一个整数 $n$,表示出勤记录的长度(次数)。 **要求**: 返回记录长度为 $n$ 时,可能获得出勤奖励的记录情况数量。答案可能很大,所以返回对 $10^9 + 7$ 取余的结果。 **说明**: - $1 \le n \le 10^{5}$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:8 解释: 有 8 种长度为 2 的记录将被视为可奖励: "PP" , "AP", "PA", "LP", "PL", "AL", "LA", "LL" 只有"AA"不会被视为可奖励,因为缺勤次数为 2 次(需要少于 2 次)。 ``` - 示例 2: ```python 输入:n = 1 输出:3 ``` ## 解题思路 ### 思路 1:动态规划(状态机) 定义状态 $dp[i][j][k]$ 表示前 $i$ 天,有 $j$ 次缺勤($j \in \{0, 1\}$),连续迟到 $k$ 次($k \in \{0, 1, 2\}$)的可奖励记录数量。 状态转移: - 如果第 $i$ 天是 $P$(到场):$dp[i][j][0] = dp[i-1][j][0] + dp[i-1][j][1] + dp[i-1][j][2]$ - 如果第 $i$ 天是 $A$(缺勤):$dp[i][1][0] = dp[i-1][0][0] + dp[i-1][0][1] + dp[i-1][0][2]$ - 如果第 $i$ 天是 $L$(迟到): - $dp[i][j][1] = dp[i-1][j][0]$ - $dp[i][j][2] = dp[i-1][j][1]$ 初始状态:$dp[1][0][0] = 1$(P),$dp[1][1][0] = 1$(A),$dp[1][0][1] = 1$(L) ### 思路 1:代码 ```python class Solution: def checkRecord(self, n: int) -> int: MOD = 10**9 + 7 # dp[i][j][k] 表示第 i 天,j 次缺勤,连续 k 次迟到的记录数 # j: 0 或 1(缺勤次数) # k: 0, 1, 2(连续迟到次数) dp = [[[0] * 3 for _ in range(2)] for _ in range(n + 1)] # 初始状态 dp[0][0][0] = 1 for i in range(1, n + 1): # 第 i 天是 P(到场) for j in range(2): for k in range(3): dp[i][j][0] = (dp[i][j][0] + dp[i-1][j][k]) % MOD # 第 i 天是 A(缺勤) for k in range(3): dp[i][1][0] = (dp[i][1][0] + dp[i-1][0][k]) % MOD # 第 i 天是 L(迟到) for j in range(2): for k in range(1, 3): dp[i][j][k] = (dp[i][j][k] + dp[i-1][j][k-1]) % MOD # 统计所有可奖励的记录数 result = 0 for j in range(2): for k in range(3): result = (result + dp[n][j][k]) % MOD return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,需要遍历 $n$ 天,每天的状态转移是常数时间。 - **空间复杂度**:$O(n)$,需要存储 DP 数组。可以优化到 $O(1)$ 使用滚动数组。 ================================================ FILE: docs/solutions/0500-0599/subarray-sum-equals-k.md ================================================ # [0560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/) - 标签:数组、哈希表、前缀和 - 难度:中等 ## 题目链接 - [0560. 和为 K 的子数组 - 力扣](https://leetcode.cn/problems/subarray-sum-equals-k/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 和一个整数 $k$。 **要求**:找到该数组中和为 $k$ 的连续子数组的个数。 **说明**: - $1 \le nums.length \le 2 \times 10^4$。 - $-1000 \le nums[i] \le 1000$。 $-10^7 \le k \le 10^7$。 **示例**: - 示例 1: ```python 输入:nums = [1,1,1], k = 2 输出:2 ``` - 示例 2: ```python 输入:nums = [1,2,3], k = 3 输出:2 ``` ## 解题思路 ### 思路 1:枚举算法(超时) 先考虑暴力做法,外层两重循环,遍历所有连续子数组,然后最内层再计算一下子数组的和。部分代码如下: ```python for i in range(len(nums)): for j in range(i + 1): sum = countSum(i, j) ``` 这样下来时间复杂度就是 $O(n^3)$ 了。下一步是想办法降低时间复杂度。 对于以 $i$ 开头,以 $j$ 结尾($i \le j$)的子数组 $nums[i]…nums[j]$ 来说,我们可以通过顺序遍历 $j$,逆序遍历 $i$ 的方式(或者前缀和的方式),从而在 $O(n^2)$ 的时间复杂度内计算出子数组的和,同时使用变量 $cnt$ 统计出和为 $k$ 的子数组个数。 但这样提交上去超时了。 ### 思路 1:代码 ```python class Solution: def subarraySum(self, nums: List[int], k: int) -> int: cnt = 0 for j in range(len(nums)): sum = 0 for i in range(j, -1, -1): sum += nums[i] if sum == k: cnt += 1 return cnt ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:前缀和 + 哈希表 先用一重循环遍历数组,计算出数组 $nums$ 中前 $j$ 个元素的和(前缀和),保存到一维数组 $pre\_sum$ 中,那么对于任意 $nums[i]…nums[j]$ 的子数组的和为 $pre\_sum[j] - pre\_sum[i - 1]$。这样计算子数组和的时间复杂度降为了 $O(1)$。总体时间复杂度为 $O(n^2)$。 但是还是超时了。。 由于我们只关心和为 $k$ 出现的次数,不关心具体的解,可以使用哈希表来加速运算。 $pre\_sum[i]$ 的定义是前 $i$ 个元素和,则 $pre\_sum[i]$ 可以由 $pre\_sum[i - 1]$ 递推而来,即:$pre\_sum[i] = pre\_sum[i - 1] + num[i]$。 $[i..j]$ 子数组和为 $k$ 可以转换为:$pre\_sum[j] - pre\_sum[i - 1] == k$。 综合一下,可得:$pre\_sum[i - 1] == pre\_sum[j] - k $。 所以,当我们考虑以 $j$ 结尾和为 $k$ 的连续子数组个数时,只需要统计有多少个前缀和为 $pre\_sum[j] - k$ (即 $pre\_sum[i - 1]$)的个数即可。具体做法如下: - 使用 $pre\_sum$ 变量记录前缀和(代表 $pre\_sum[j]$)。 - 使用哈希表 $pre\_dic$ 记录 $pre\_sum[j]$ 出现的次数。键值对为 $pre\_sum[j] : pre\_sum\_count$。 - 从左到右遍历数组,计算当前前缀和 $pre\_sum$。 - 如果 $pre\_sum - k$ 在哈希表中,则答案个数累加上 $pre\_dic[pre\_sum - k]$。 - 如果 $pre\_sum$ 在哈希表中,则前缀和个数累加 $1$,即 $pre\_dic[pre\_sum] += 1$。 - 最后输出答案个数。 ### 思路 2:代码 ```python class Solution: def subarraySum(self, nums: List[int], k: int) -> int: pre_dic = {0: 1} pre_sum = 0 count = 0 for num in nums: pre_sum += num if pre_sum - k in pre_dic: count += pre_dic[pre_sum - k] if pre_sum in pre_dic: pre_dic[pre_sum] += 1 else: pre_dic[pre_sum] = 1 return count ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0500-0599/subtree-of-another-tree.md ================================================ # [0572. 另一棵树的子树](https://leetcode.cn/problems/subtree-of-another-tree/) - 标签:树、深度优先搜索、二叉树、字符串匹配、哈希函数 - 难度:简单 ## 题目链接 - [0572. 另一棵树的子树 - 力扣](https://leetcode.cn/problems/subtree-of-another-tree/) ## 题目大意 **描述**: 给定两棵二叉树 $root$ 和 $subRoot$ 。 **要求**: 检验 $root$ 中是否包含和 $subRoot$ 具有相同结构和节点值的子树。如果存在,返回 true;否则,返回 false。 **说明**: - 二叉树 $tree$ 的一棵子树包括 $tree$ 的某个节点和这个节点的所有后代节点。$tree$ 也可以看做它自身的一棵子树。 - $root$ 树上的节点数量范围是 $[1, 2000]$。 - $subRoot$ 树上的节点数量范围是 $[1, 10^{3}]$。 - $-10^{4} \le root.val \le 10^{4}$。 - $-10^{4} \le subRoot.val \le 10^{4}$。 **示例**: - 示例 1: ![](https://pic.leetcode.cn/1724998676-cATjhe-image.png) ```python 输入:root = [3,4,5,1,2], subRoot = [4,1,2] 输出:true ``` - 示例 2: ![](https://pic.leetcode.cn/1724998698-sEJWnq-image.png) ```python 输入:root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2] 输出:false ``` ## 解题思路 ### 思路 1:递归匹配 对于树 $root$ 中的每个节点,我们需要检查以该节点为根的子树是否与 $subRoot$ 相同。 定义两个辅助函数: 1. `isSameTree(p, q)`:判断两棵树是否完全相同。 2. `isSubtree(root, subRoot)`:判断 $subRoot$ 是否是 $root$ 的子树。 对于 `isSubtree`,如果当前 $root$ 为空,返回 `False`。否则: - 检查以 $root$ 为根的树是否与 $subRoot$ 相同。 - 递归检查 $root$ 的左子树和右子树是否包含 $subRoot$。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool: # 判断两棵树是否完全相同 def isSameTree(p: Optional[TreeNode], q: Optional[TreeNode]) -> bool: if not p and not q: return True if not p or not q: return False return p.val == q.val and isSameTree(p.left, q.left) and isSameTree(p.right, q.right) # 如果 root 为空,返回 False if not root: return False # 检查当前节点为根的树是否与 subRoot 相同 # 或者递归检查左右子树 return isSameTree(root, subRoot) or \ self.isSubtree(root.left, subRoot) or \ self.isSubtree(root.right, subRoot) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 是 $root$ 的节点数,$n$ 是 $subRoot$ 的节点数。最坏情况下需要检查 $root$ 中的每个节点,每次检查需要 $O(n)$ 时间。 - **空间复杂度**:$O(m)$,递归栈的深度最多为 $m$。 ================================================ FILE: docs/solutions/0500-0599/super-washing-machines.md ================================================ # [0517. 超级洗衣机](https://leetcode.cn/problems/super-washing-machines/) - 标签:贪心、数组 - 难度:困难 ## 题目链接 - [0517. 超级洗衣机 - 力扣](https://leetcode.cn/problems/super-washing-machines/) ## 题目大意 **描述**: 假设有 $n$ 台超级洗衣机放在同一排上。开始的时候,每台洗衣机内可能有一定量的衣服,也可能是空的。 在每一步操作中,你可以选择任意 $m$ ($1 \le m \le n$) 台洗衣机,与此同时将每台洗衣机的一件衣服送到相邻的一台洗衣机。 给定一个整数数组 $machines$ 代表从左至右每台洗衣机中的衣物数量。 **要求**: 给出能让所有洗衣机中剩下的衣物的数量相等的「最少的操作步数」。如果不能使每台洗衣机中衣物的数量相等,则返回 $-1$。 **说明**: - $n == machines.length$。 - $1 \le n \le 10^{4}$。 - $0 \le machines[i] \le 10^{5}$。 **示例**: - 示例 1: ```python 输入:machines = [1,0,5] 输出:3 解释: 第一步: 1 0 <-- 5 => 1 1 4 第二步: 1 <-- 1 <-- 4 => 2 1 3 第三步: 2 1 <-- 3 => 2 2 2 ``` - 示例 2: ```python 输入:machines = [0,3,0] 输出:2 解释: 第一步: 0 <-- 3 0 => 1 2 0 第二步: 1 2 --> 0 => 1 1 1 ``` ## 解题思路 ### 思路 1:贪心算法 首先判断是否可能达到平衡:总衣物数必须能被洗衣机数量整除。 设目标值为 $target = sum(machines) / n$。 关键观察: 1. 对于每台洗衣机 $i$,计算它需要转移的衣物数量:$diff[i] = machines[i] - target$ 2. 计算前缀和 $prefix[i]$,表示前 $i$ 台洗衣机需要向右转移的衣物总数 3. 最少操作步数取决于两个因素: - 某台洗衣机需要转出的最大衣物数:$\max(diff[i])$(如果为正) - 经过某台洗衣机的最大流量:$\max(|prefix[i]|)$ 答案为 $\max(\max(diff[i]), \max(|prefix[i]|))$。 ### 思路 1:代码 ```python class Solution: def findMinMoves(self, machines: List[int]) -> int: total = sum(machines) n = len(machines) # 如果无法平均分配,返回 -1 if total % n != 0: return -1 target = total // n max_moves = 0 prefix_sum = 0 for i in range(n): # 当前洗衣机需要转移的衣物数(正数表示需要转出,负数表示需要转入) diff = machines[i] - target prefix_sum += diff # 更新最大操作步数 # 1. 当前洗衣机需要转出的衣物数 # 2. 经过当前位置的最大流量 max_moves = max(max_moves, abs(prefix_sum), diff) return max_moves ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是洗衣机的数量,只需要遍历一次数组。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/tag-validator.md ================================================ # [0591. 标签验证器](https://leetcode.cn/problems/tag-validator/) - 标签:栈、字符串 - 难度:困难 ## 题目链接 - [0591. 标签验证器 - 力扣](https://leetcode.cn/problems/tag-validator/) ## 题目大意 **描述**: 给定一个表示代码片段的字符串。 **要求**: 实现一个验证器来解析这段代码,并返回它是否合法。 **说明**: - 合法的代码片段需要遵守以下的所有规则: 1. 代码必须被合法的闭合标签包围。否则,代码是无效的。 2. 闭合标签(不一定合法)要严格符合格式:`TAG_CONTENT`。其中,`` 是起始标签,`` 是结束标签。起始和结束标签中的 `TAG_NAME` 应当相同。当且仅当 `TAG_NAME` 和 `TAG_CONTENT` 都是合法的,闭合标签才是合法的。 3. 合法的 `TAG_NAME` 仅含有大写字母,长度在范围 $[1,9]$ 之间。否则,该 `TAG_NAME` 是不合法的。 4. 合法的 `TAG_CONTENT` 可以包含其他合法的闭合标签,$cdata$ (请参考规则7)和任意字符(注意参考规则 1)除了不匹配的 `<`、不匹配的起始和结束标签、不匹配的或带有不合法 `TAG_NAME` 的闭合标签。否则,`TAG_CONTENT` 是不合法的。 5. 一个起始标签,如果没有具有相同 `TAG_NAME` 的结束标签与之匹配,是不合法的。反之亦然。不过,你也需要考虑标签嵌套的问题。 6. 一个 `<`,如果你找不到一个后续的 `>` 与之匹配,是不合法的。并且当你找到一个 `<` 或 `` 的前的字符,都应当被解析为 `TAG_NAME`(不一定合法)。 7. $cdata$ 有如下格式:``。`CDATA_CONTENT` 的范围被定义成 `` 之间的字符。 8. `CDATA_CONTENT` 可以包含任意字符。$cdata$ 的功能是阻止验证器解析 `CDATA_CONTENT`,所以即使其中有一些字符可以被解析为标签(无论合法还是不合法),也应该将它们视为常规字符。 - 假设输入的代码(包括提到的任意字符)只包含数字, 字母, `<`, `>`, `/`, `!`, `[`, `]` 和 ` `。 **示例**: - 示例 1: ```python 合法代码的例子: 输入: "
This is the first line ]]>
" 输出: True 解释: 代码被包含在了闭合的标签内:
。 TAG_NAME 是合法的,TAG_CONTENT 包含了一些字符和 cdata 。 即使 CDATA_CONTENT 含有不匹配的起始标签和不合法的 TAG_NAME,它应该被视为普通的文本,而不是标签。 所以 TAG_CONTENT 是合法的,因此代码是合法的。最终返回True。 输入: "
>> ![cdata[]] ]>]]>]]>>]
" 输出: True 解释: 我们首先将代码分割为: start_tag|tag_content|end_tag 。 start_tag -> "
" end_tag -> "
" tag_content 也可被分割为: text1|cdata|text2 。 text1 -> ">> ![cdata[]] " cdata -> "]>]]>" ,其中 CDATA_CONTENT 为 "
]>" text2 -> "]]>>]" start_tag 不是 "
>>" 的原因参照规则 6 。 cdata 不是 "]>]]>]]>" 的原因参照规则 7 。 ``` - 示例 2: ```python 不合法代码的例子: 输入: " " 输出: False 解释: 不合法。如果 "" 是闭合的,那么 "" 一定是不匹配的,反之亦然。 输入: "
div tag is not closed
" 输出: False 输入: "
unmatched <
" 输出: False 输入: "
closed tags with invalid tag name 123
" 输出: False 输入: "
unmatched tags with invalid tag name and
" 输出: False 输入: "
unmatched start tag and unmatched end tag
" 输出: False ``` ## 解题思路 ### 思路 1:栈 + 状态机 这是一个复杂的字符串解析问题,需要使用栈来匹配标签,并处理多种特殊情况。 关键点: 1. 使用栈存储标签名,匹配开始和结束标签 2. 处理 CDATA 部分,CDATA 内的内容不需要验证 3. 验证标签名的合法性(1-9 个大写字母) 4. 确保代码被合法的闭合标签包围 5. 处理各种边界情况 步骤: 1. 从左到右扫描字符串 2. 遇到 `<` 时,判断是开始标签、结束标签还是 CDATA 3. 使用栈维护标签的嵌套关系 4. 验证每个标签的合法性 ### 思路 1:代码 ```python class Solution: def isValid(self, code: str) -> bool: stack = [] i = 0 n = len(code) while i < n: if i > 0 and not stack: # 如果不在标签内且不是开头,无效 return False if code[i:i+9] == '', i) if j == -1: return False i = j + 3 elif code[i:i+2] == '', i) if j == -1: return False tag_name = code[i+2:j] if not self.is_valid_tag_name(tag_name): return False if not stack or stack[-1] != tag_name: return False stack.pop() i = j + 1 elif code[i] == '<': # 开始标签 j = code.find('>', i) if j == -1: return False tag_name = code[i+1:j] if not self.is_valid_tag_name(tag_name): return False stack.append(tag_name) i = j + 1 else: # 普通字符 if not stack: return False i += 1 return len(stack) == 0 def is_valid_tag_name(self, tag_name: str) -> bool: # 标签名必须是 1-9 个大写字母 if not tag_name or len(tag_name) > 9: return False return all(c.isupper() for c in tag_name) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度,需要遍历整个字符串。 - **空间复杂度**:$O(n)$,栈的深度最多为 $O(n)$。 ================================================ FILE: docs/solutions/0500-0599/the-maze-ii.md ================================================ # [0505. 迷宫 II](https://leetcode.cn/problems/the-maze-ii/) - 标签:深度优先搜索、广度优先搜索、图、数组、矩阵、最短路、堆(优先队列) - 难度:中等 ## 题目链接 - [0505. 迷宫 II - 力扣](https://leetcode.cn/problems/the-maze-ii/) ## 题目大意 **描述**: 给定一个迷宫(二维数组)$maze$,其中 $0$ 表示空地,$1$ 表示墙壁。球可以向上、下、左、右四个方向滚动,但在碰到墙壁前不会停止滚动。当球停下时,可以选择下一个方向。 给定球的起始位置 $start$ 和目的地 $destination$。 **要求**: 返回球到达目的地的最短距离。如果球无法到达目的地,返回 $-1$。 **说明**: - 距离 是指球从起始位置(不包括)到终点(包括)经过的空地数量。 - 可以假设迷宫的边界都是墙。 - $m == maze.length$。 - $n == maze[i].length$。 - $1 \le m, n \le 100$。 - $maze[i][j]$ 是 $0$ 或 $1$。 - $start.length == 2$。 - $destination.length == 2$。 - $0 \le start\_row, destination\_row < m$。 - $0 \le start\_col, destination\_col < n$。 - 球和目的地都存在于一个空地中,它们最初不会处于相同的位置。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/31/maze1-1-grid.jpg) ```python 输入: maze = [[0,0,1,0,0],[0,0,0,0,0],[0,0,0,1,0],[1,1,0,1,1],[0,0,0,0,0]], start = [0,4], destination = [4,4] 输出: 12 解析: 一条最短路径 : left -> down -> left -> down -> right -> down -> right。 总距离为 1 + 1 + 3 + 1 + 2 + 2 + 2 = 12。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/31/maze1-2-grid.jpg) ```python 输入: maze = [[0,0,1,0,0],[0,0,0,0,0],[0,0,0,1,0],[1,1,0,1,1],[0,0,0,0,0]], start = [0,4], destination = [3,2] 输出: -1 解析: 球不可能在目的地停下来。注意,你可以经过目的地,但不能在那里停下来。 ``` ## 解题思路 ### 思路 1:Dijkstra 算法(优先队列 + BFS) 这是一个最短路径问题,可以使用 Dijkstra 算法求解。 关键点: 1. 球会一直滚动直到碰到墙壁才停下 2. 需要记录到达每个停止位置的最短距离 3. 使用优先队列(最小堆)保证每次取出距离最小的位置 步骤: 1. 使用优先队列存储 $(distance, row, col)$ 2. 使用 $dist$ 数组记录到达每个位置的最短距离 3. 对于每个位置,尝试四个方向滚动,直到碰到墙壁 4. 如果新的距离更短,更新并加入队列 ### 思路 1:代码 ```python import heapq class Solution: def shortestDistance(self, maze: List[List[int]], start: List[int], destination: List[int]) -> int: m, n = len(maze), len(maze[0]) # 距离数组,初始化为无穷大 dist = [[float('inf')] * n for _ in range(m)] dist[start[0]][start[1]] = 0 # 优先队列:(距离, 行, 列) pq = [(0, start[0], start[1])] directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] while pq: d, x, y = heapq.heappop(pq) # 如果到达目的地 if x == destination[0] and y == destination[1]: return d # 如果当前距离大于已记录的距离,跳过 if d > dist[x][y]: continue # 尝试四个方向 for dx, dy in directions: nx, ny = x, y steps = 0 # 一直滚动直到碰到墙壁 while 0 <= nx + dx < m and 0 <= ny + dy < n and maze[nx + dx][ny + dy] == 0: nx += dx ny += dy steps += 1 # 计算新的距离 new_dist = d + steps # 如果找到更短的路径 if new_dist < dist[nx][ny]: dist[nx][ny] = new_dist heapq.heappush(pq, (new_dist, nx, ny)) # 无法到达目的地 return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n \times \log(m \times n))$,其中 $m$ 和 $n$ 是迷宫的行数和列数。每个位置最多入队一次,堆操作的时间复杂度为 $O(\log(m \times n))$。 - **空间复杂度**:$O(m \times n)$,需要存储距离数组和优先队列。 ================================================ FILE: docs/solutions/0500-0599/valid-square.md ================================================ # [0593. 有效的正方形](https://leetcode.cn/problems/valid-square/) - 标签:几何、数学 - 难度:中等 ## 题目链接 - [0593. 有效的正方形 - 力扣](https://leetcode.cn/problems/valid-square/) ## 题目大意 **描述**: 给定 2D 空间中四个点的坐标 $p1$, $p2$, $p3$ 和 $p4$。 **要求**: 如果这四个点构成一个正方形,则返回 true。 **说明**: - 点的坐标 $pi$ 表示为 $[xi, yi]$。 输入没有任何顺序。 - 一个有效的正方形有四条等边和四个等角(90 度角)。 - $p1.length == p2.length == p3.length == p4.length == 2$。 - $-10^{4} \le xi, yi \le 10^{4}$。 **示例**: - 示例 1: ```python 输入: p1 = [0,0], p2 = [1,1], p3 = [1,0], p4 = [0,1] 输出: true ``` - 示例 2: ```python 输入:p1 = [0,0], p2 = [1,1], p3 = [1,0], p4 = [0,12] 输出:false ``` ## 解题思路 ### 思路 1:距离判断 一个有效的正方形需要满足: 1. 四条边长度相等。 2. 两条对角线长度相等。 3. 四个角都是 90 度(可以通过边长的平方和等于对角线平方的一半来判断,即 $a^2 + b^2 = c^2$)。 我们可以计算所有点对之间的距离,对于一个正方形,应该有: - 4 条边长度相等(设为 $side$)。 - 2 条对角线长度相等(设为 $diagonal$)。 - 满足 $2 \times side^2 = diagonal^2$(勾股定理)。 计算所有 $6$ 个点对之间的距离,排序后应该得到:$4$ 个相等的边长度和 $2$ 个相等的对角线长度,且满足上述关系。同时需要排除所有点重合的情况。 ### 思路 1:代码 ```python class Solution: def validSquare(self, p1: List[int], p2: List[int], p3: List[int], p4: List[int]) -> bool: def distance(p1: List[int], p2: List[int]) -> int: # 计算两点间距离的平方(避免浮点数误差) return (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2 # 计算所有点对之间的距离平方 points = [p1, p2, p3, p4] distances = [] for i in range(4): for j in range(i + 1, 4): dist = distance(points[i], points[j]) distances.append(dist) # 排序距离 distances.sort() # 检查:应该有 4 条相等的边和 2 条相等的对角线 # 且满足 2 * side^2 = diagonal^2 # 同时排除所有点重合的情况(最小距离不能为 0) if distances[0] == 0: return False # 前 4 个应该是边,后 2 个应该是对角线 return distances[0] == distances[1] == distances[2] == distances[3] and distances[4] == distances[5] and 2 * distances[0] == distances[4] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$,固定计算 $6$ 个点对的距离并排序。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0500-0599/word-abbreviation.md ================================================ # [0527. 单词缩写](https://leetcode.cn/problems/word-abbreviation/) - 标签:贪心、字典树、数组、字符串、排序 - 难度:困难 ## 题目链接 - [0527. 单词缩写 - 力扣](https://leetcode.cn/problems/word-abbreviation/) ## 题目大意 **描述**: 给定一个字符串数组 $words$,该数组由 **互不相同** 的若干字符串组成,需要为每个单词生成最短的唯一缩写。 缩写规则如下: - 初始缩写由起始字母 + 省略字母的数量 + 结尾字母组成。 - 如果多个单词的缩写相同,则使用更长的前缀代替首字母,直到从单词到缩写唯一。换而言之,最终的缩写必须只能映射到一个单词。 - 如果缩写不比原词短,则保持原词。 **要求**: 返回每个单词的最短唯一缩写列表。 **说明**: - $1 \le words.length \le 400$。 - $2 \le words[i].length \le 400$。 - $words[i]$ 由小写英文字母组成。 - 所有 $words[i]$ 都是唯一的。 **示例**: - 示例 1: ```python 输入: words = ["like", "god", "internal", "me", "internet", "interval", "intension", "face", "intrusion"] 输出: ["l2e","god","internal","me","i6t","interval","inte4n","f2e","intr4n"] ``` - 示例 2: ```python 输入:words = ["aa","aaa"] 输出:["aa","aaa"] ``` ## 解题思路 ### 思路 1:分组 + 字典树 每个单词的缩写规则:首字母 + 中间字符数量 + 尾字母。例如 `"like"` 缩写为 `"l2e"`。要求每个缩写唯一,不唯一时需增加前缀长度直到唯一。 **算法步骤**: 1. 将所有单词按 (长度, 首字母, 尾字母) 分组,只有这三个属性相同的单词才可能产生冲突。 2. 对每组单词构建字典树,记录每个前缀路径经过的单词数量。 3. 对于每个单词,在字典树中查找最短的唯一前缀(即路径上 $count = 1$ 的位置)。 4. 生成缩写:如果中间部分长度大于 1,使用 $word[:k] + str(len(word)-k-1) + word[-1]$;否则保持原词。 ### 思路 1:代码 ```python class TrieNode: def __init__(self): self.children = {} self.count = 0 # 路径经过多少单词 class Solution: def wordsAbbreviation(self, words: List[str]) -> List[str]: from collections import defaultdict n = len(words) res = [''] * n # 按 (长度, 首字母, 尾字母) 分组 groups = defaultdict(list) for i, word in enumerate(words): key = (len(word), word[0], word[-1]) groups[key].append((word, i)) # 处理每组 for group in groups.values(): # 构造字典树 root = TrieNode() for word, _ in group: node = root for c in word: node = node.children.setdefault(c, TrieNode()) node.count += 1 # 查询每个单词的唯一前缀 for word, idx in group: node = root prefix_len = 0 for c in word: node = node.children[c] prefix_len += 1 if node.count == 1: break # 生成缩写 if len(word) - prefix_len - 1 > 1: res[idx] = word[:prefix_len] + str(len(word) - prefix_len - 1) + word[-1] else: res[idx] = word return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times L)$,其中 $n$ 是单词数量,$L$ 是单词的平均长度。需要构建字典树和查询前缀。 - **空间复杂度**:$O(n \times L)$,字典树节点和分组的空间开销。 ================================================ FILE: docs/solutions/0600-0699/2-keys-keyboard.md ================================================ # [0650. 两个键的键盘](https://leetcode.cn/problems/2-keys-keyboard/) - 标签:数学、动态规划 - 难度:中等 ## 题目链接 - [0650. 两个键的键盘 - 力扣](https://leetcode.cn/problems/2-keys-keyboard/) ## 题目大意 **描述**:最初记事本上只有一个字符 `'A'`。你每次可以对这个记事本进行两种操作: - **Copy All(复制全部)**:复制这个记事本中的所有字符(不允许仅复制部分字符)。 - **Paste(粘贴)**:粘贴上一次复制的字符。 现在,给定一个数字 $n$,需要使用最少的操作次数,在记事本上输出恰好 $n$ 个 `'A'` 。 **要求**:返回能够打印出 $n$ 个 `'A'` 的最少操作次数。 **说明**: - $1 \le n \le 1000$。 **示例**: - 示例 1: ```python 输入:3 输出:3 解释 最初, 只有一个字符 'A'。 第 1 步, 使用 Copy All 操作。 第 2 步, 使用 Paste 操作来获得 'AA'。 第 3 步, 使用 Paste 操作来获得 'AAA'。 ``` - 示例 2: ```python 输入:n = 1 输出:0 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照字符 `'A'` 的个数进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为:通过「复制」和「粘贴」操作,得到 $i$ 个字符 `'A'`,最少需要的操作数。 ###### 3. 状态转移方程 1. 对于 $i$ 个字符 `'A'`,如果 $i$ 可以被一个小于 $i$ 的整数 $j$ 除尽($j$ 是 $i$ 的因子),则说明 $j$ 个字符 `'A'` 可以通过「复制」+「粘贴」总共 $\frac{i}{j}$ 次得到 $i$ 个字符 `'A'`。 2. 而得到 $j$ 个字符 `'A'`,最少需要的操作数可以通过 $dp[j]$ 获取。 则我们可以枚举 $i$ 的因子,从中找到在满足 $j$ 能够整除 $i$ 的条件下,最小的 $dp[j] + \frac{i}{j}$,即为 $dp[i]$,即 $dp[i] = min_{j | i}(dp[i], dp[j] + \frac{i}{j})$。 由于 $j$ 能够整除 $i$,则 $j$ 与 $\frac{i}{j}$ 都是 $i$ 的因子,两者中必有一个因子是小于等于 $\sqrt{i}$ 的,所以在枚举 $i$ 的因子时,我们只需要枚举区间 $[1, \sqrt{i}]$ 即可。 综上所述,状态转移方程为:$dp[i] = min_{j | i}(dp[i], dp[j] + \frac{i}{j}, dp[\frac{i}{j}] + j)$。 ###### 4. 初始条件 - 当 $i$ 为 $1$ 时,最少需要的操作数为 $0$。所以 $dp[1] = 0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示为:通过「复制」和「粘贴」操作,得到 $i$ 个字符 `'A'`,最少需要的操作数。 所以最终结果为 $dp[n]$。 ### 思路 1:动态规划代码 ```python import math class Solution: def minSteps(self, n: int) -> int: dp = [0 for _ in range(n + 1)] for i in range(2, n + 1): dp[i] = float('inf') for j in range(1, int(math.sqrt(n)) + 1): if i % j == 0: dp[i] = min(dp[i], dp[j] + i // j, dp[i // j] + j) return dp[n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \sqrt{n})$。外层循环遍历的时间复杂度是 $O(n)$,内层循环遍历的时间复杂度是 $O(\sqrt{n})$,所以总体时间复杂度为 $O(n \sqrt{n})$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0600-0699/24-game.md ================================================ # [0679. 24 点游戏](https://leetcode.cn/problems/24-game/) - 标签:数组、数学、回溯 - 难度:困难 ## 题目链接 - [0679. 24 点游戏 - 力扣](https://leetcode.cn/problems/24-game/) ## 题目大意 **描述**: 给定一个长度为4的整数数组 $cards$。你有 $4$ 张卡片,每张卡片上都包含一个范围在 $[1,9]$ 的数字。您应该使用运算符 `['+', '-', '*', '/']` 和括号 `'('` 和 `')'` 将这些卡片上的数字排列成数学表达式,以获得值 $24$。 你须遵守以下规则: - 除法运算符 `/` 表示实数除法,而不是整数除法。 - 例如, `"4 / (1 - 2 / 3)= 4 / (1 / 3) = 12"`。 - 每个运算都在两个数字之间。特别是,不能使用 `-` 作为一元运算符。 - 例如,如果 $cards =[1,1,1,1]$,则表达式 `"-1 -1 -1 -1"` 是 不允许 的。 - 你不能把数字串在一起 - 例如,如果 $cards =[1,2,1,2]$,则表达式 `"12 + 12"` 无效。 **要求**: 如果可以得到这样的表达式,其计算结果为 24,则返回 true ,否则返回 false。 **说明**: - $cards.length == 4$。 - $1 \le cards[i] \le 9$。 **示例**: - 示例 1: ```python 输入: cards = [4, 1, 8, 7] 输出: true 解释: (8-4) * (7-1) = 24 ``` - 示例 2: ```python 输入: cards = [1, 2, 1, 2] 输出: false ``` ## 解题思路 ### 思路 1:回溯算法 #### 思路 1:算法描述 这道题目要求判断是否可以通过四则运算和括号将四个数字组合成 $24$。 我们可以使用回溯算法来枚举所有可能的运算顺序和运算符组合。每次从数组中选择两个数字进行运算,将运算结果放回数组,然后递归处理剩余的数字,直到数组中只剩下一个数字,判断是否等于 $24$。 具体步骤如下: 1. 如果数组中只剩下一个数字,判断是否等于 $24$(考虑浮点数误差,判断是否在 $24$ 的附近)。 2. 枚举数组中的任意两个数字 $a$ 和 $b$,以及四种运算符 $+$、$-$、$\times$、$\div$。 3. 计算 $a$ 和 $b$ 的运算结果,将结果放回数组,递归处理剩余的数字。 4. 如果找到一种方案使得最终结果为 $24$,返回 $True$。 5. 回溯,尝试其他的数字组合和运算符。 注意:除法运算需要判断除数是否为 $0$。 #### 思路 1:代码 ```python class Solution: def judgePoint24(self, cards: List[int]) -> bool: TARGET = 24 EPSILON = 1e-6 # 浮点数误差范围 def backtrack(nums): # 如果只剩下一个数字,判断是否等于 24 if len(nums) == 1: return abs(nums[0] - TARGET) < EPSILON # 枚举任意两个数字进行运算 n = len(nums) for i in range(n): for j in range(n): if i == j: continue a, b = nums[i], nums[j] # 剩余的数字 remaining = [nums[k] for k in range(n) if k != i and k != j] # 枚举四种运算符 # 加法 if backtrack(remaining + [a + b]): return True # 减法 if backtrack(remaining + [a - b]): return True # 乘法 if backtrack(remaining + [a * b]): return True # 除法(需要判断除数是否为 0) if abs(b) > EPSILON and backtrack(remaining + [a / b]): return True return False # 将整数转换为浮点数 return backtrack([float(card) for card in cards]) ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。虽然看起来是指数级别的复杂度,但由于数组长度固定为 $4$,所以时间复杂度是常数级别的。 - **空间复杂度**:$O(1)$。递归调用栈的深度最多为 $4$,空间复杂度是常数级别的。 ================================================ FILE: docs/solutions/0600-0699/4-keys-keyboard.md ================================================ # [0651. 四个键的键盘](https://leetcode.cn/problems/4-keys-keyboard/) - 标签:数学、动态规划 - 难度:中等 ## 题目链接 - [0651. 四个键的键盘 - 力扣](https://leetcode.cn/problems/4-keys-keyboard/) ## 题目大意 **描述**: 假设你有一个特殊的键盘包含下面的按键: - `A`:在屏幕上打印一个 `'A'`。 - `Ctrl-A`:选中整个屏幕。 - `Ctrl-C`:复制选中区域到缓冲区。 - `Ctrl-V`:将缓冲区内容输出到上次输入的结束位置,并显示在屏幕上。 现在,你可以 **最多** 按键 $n$ 次(使用上述四种按键)。 **要求**: 返回屏幕上最多可以显示 `'A'` 的个数。 **说明**: - $1 \le n \le 50$。 **示例**: - 示例 1: ```python 输入: n = 3 输出: 3 解释: 我们最多可以在屏幕上显示三个 'A' 通过如下顺序按键: A, A, A ``` - 示例 2: ```python 输入: n = 7 输出: 9 解释: 我们最多可以在屏幕上显示九个 'A' 通过如下顺序按键: A, A, A, Ctrl-A, Ctrl-C, Ctrl-V, Ctrl-V ``` ## 解题思路 ### 思路 1:动态规划 这道题目要求在最多按键 $n$ 次的情况下,屏幕上最多可以显示多少个 `'A'`。 我们可以使用动态规划来解决这个问题。定义 $dp[i]$ 表示按键 $i$ 次后屏幕上最多可以显示的 `'A'` 的个数。 对于每次按键,有两种选择: 1. **按 `A` 键**:屏幕上增加一个 `'A'`,即 $dp[i] = dp[i - 1] + 1$。 2. **使用 `Ctrl-A`、`Ctrl-C`、`Ctrl-V` 组合键进行复制粘贴操作**:假设在第 $j$ 次按键后进行全选复制,然后连续粘贴 $i - j - 2$ 次(需要 $2$ 次按键进行全选和复制),则 $dp[i] = dp[j] \times (i - j - 1)$。 我们需要枚举所有可能的 $j$,取最大值。 **算法步骤**: 1. 初始化 $dp$ 数组,$dp[0] = 0$。 2. 对于 $i$ 从 $1$ 到 $n$: - 选择 1:按 `A` 键,$dp[i] = dp[i - 1] + 1$。 - 选择 2:枚举在第 $j$ 次按键后进行全选复制($1 \le j \le i - 2$),$dp[i] = \max(dp[i], dp[j] \times (i - j - 1))$。 3. 返回 $dp[n]$。 ### 思路 1:代码 ```python class Solution: def maxA(self, n: int) -> int: # dp[i] 表示按键 i 次后屏幕上最多可以显示的 'A' 的个数 dp = [0] * (n + 1) for i in range(1, n + 1): # 选择 1:按 A 键 dp[i] = dp[i - 1] + 1 # 选择 2:使用复制粘贴操作 # 枚举在第 j 次按键后进行全选复制 for j in range(1, i - 2): # 需要 2 次按键进行全选和复制,剩余 i - j - 2 次按键进行粘贴 # 粘贴 i - j - 2 次,相当于复制了 i - j - 1 份 dp[i] = max(dp[i], dp[j] * (i - j - 1)) return dp[n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。需要两层循环,外层循环 $n$ 次,内层循环最多 $n$ 次。 - **空间复杂度**:$O(n)$。需要使用长度为 $n + 1$ 的数组存储动态规划的状态。 ================================================ FILE: docs/solutions/0600-0699/add-bold-tag-in-string.md ================================================ # [0616. 给字符串添加加粗标签](https://leetcode.cn/problems/add-bold-tag-in-string/) - 标签:字典树、数组、哈希表、字符串、字符串匹配 - 难度:中等 ## 题目链接 - [0616. 给字符串添加加粗标签 - 力扣](https://leetcode.cn/problems/add-bold-tag-in-string/) ## 题目大意 给定一个字符串 `s` 和一个字符串列表 `words`。 要求:如果 `s` 的子串在字符串列表 `words` 中出现过,则在该子串前后添加加粗闭合标签 `` 和 ``。如果两个子串有重叠部分,则将它们一起用一对闭合标签包围起来。同理,如果两个子字符串连续被加粗,那么你也需要把它们合起来用一对加粗标签包围。最后返回添加加粗标签后的字符串 `s`。 ## 解题思路 构建字典树,将字符串列表 `words` 中所有字符串添加到字典树中。 然后遍历字符串 `s`,从每一个位置开始查询字典树。在第一个符合要求的单词前面添加 ``。在连续符合要求的单词中的最后一个单词后面添加 ``。 最后返回添加加粗标签后的字符串 `s`。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None and cur.isEnd class Solution: def addBoldTag(self, s: str, words: List[str]) -> str: trie_tree = Trie() for word in words: trie_tree.insert(word) size = len(s) bold_left, bold_right = -1, -1 ans = "" for i in range(size): cur = trie_tree if s[i] in cur.children: bold_left = i while bold_left < size and s[bold_left] in cur.children: cur = cur.children[s[bold_left]] bold_left += 1 if cur.isEnd: if bold_right == -1: ans += "" bold_right = max(bold_left, bold_right) if i == bold_right: ans += "" bold_right = -1 ans += s[i] if bold_right >= 0: ans += "" return ans ``` ================================================ FILE: docs/solutions/0600-0699/add-one-row-to-tree.md ================================================ # [0623. 在二叉树中增加一行](https://leetcode.cn/problems/add-one-row-to-tree/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0623. 在二叉树中增加一行 - 力扣](https://leetcode.cn/problems/add-one-row-to-tree/) ## 题目大意 **描述**: 给定一个二叉树的根 $root$ 和两个整数 $val$ 和 $depth$。 **要求**: 在给定的深度 $depth$ 处添加一个值为 $val$ 的节点行。 注意,根节点 $root$ 位于深度 1。 **说明**: - 加法规则如下: - 给定整数 $depth$,对于深度为 $depth - 1$ 的每个非空树节点 $cur$ ,创建两个值为 $val$ 的树节点作为 $cur$ 的左子树根和右子树根。 - $cur$ 原来的左子树应该是新的左子树根的左子树。 - $cur$ 原来的右子树应该是新的右子树根的右子树。 - 如果 $depth == 1$ 意味着 $depth - 1$ 根本没有深度,那么创建一个树节点,值 $val$ 作为整个原始树的新根,而原始树就是新根的左子树。 - 节点数在 $[1, 10^{4}]$ 范围内。 - 树的深度在 $[1, 10^{4}]$ 范围内。 - $-10^{3} \le Node.val \le 10^{3}$。 - $-10^{5} \le val \le 10^{5}$。 - $1 \le depth \le the depth of tree + 1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/15/addrow-tree.jpg) ```python 输入: root = [4,2,6,3,1,5], val = 1, depth = 2 输出: [4,1,1,2,null,null,6,3,1,5] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/11/add2-tree.jpg) ```python 输入: root = [4,2,null,3,1], val = 1, depth = 3 输出: [4,2,null,1,1,3,null,null,1] ``` ## 解题思路 ### 思路 1:广度优先搜索 #### 思路 1:算法描述 这道题目要求在二叉树的指定深度 $depth$ 处添加一行值为 $val$ 的节点。 我们可以使用广度优先搜索(BFS)来找到深度为 $depth - 1$ 的所有节点,然后为这些节点添加新的左右子节点。 特殊情况:如果 $depth = 1$,则需要创建一个新的根节点,原来的树作为新根节点的左子树。 具体步骤如下: 1. 如果 $depth = 1$,创建一个新的根节点,值为 $val$,原来的根节点作为新根节点的左子节点,返回新根节点。 2. 使用 BFS 遍历二叉树,找到深度为 $depth - 1$ 的所有节点。 3. 对于深度为 $depth - 1$ 的每个节点 $node$: - 创建两个新节点 $left\_node$ 和 $right\_node$,值都为 $val$。 - 将 $node$ 的原左子节点作为 $left\_node$ 的左子节点。 - 将 $node$ 的原右子节点作为 $right\_node$ 的右子节点。 - 将 $left\_node$ 和 $right\_node$ 分别设置为 $node$ 的左右子节点。 4. 返回根节点。 #### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def addOneRow(self, root: Optional[TreeNode], val: int, depth: int) -> Optional[TreeNode]: # 特殊情况:在根节点前添加一行 if depth == 1: new_root = TreeNode(val) new_root.left = root return new_root # 使用 BFS 找到深度为 depth - 1 的所有节点 queue = [root] current_depth = 1 while queue and current_depth < depth - 1: size = len(queue) for _ in range(size): node = queue.pop(0) if node.left: queue.append(node.left) if node.right: queue.append(node.right) current_depth += 1 # 为深度为 depth - 1 的所有节点添加新的左右子节点 for node in queue: # 创建新的左子节点 left_node = TreeNode(val) left_node.left = node.left node.left = left_node # 创建新的右子节点 right_node = TreeNode(val) right_node.right = node.right node.right = right_node return root ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数。最坏情况下需要遍历所有节点。 - **空间复杂度**:$O(n)$。队列中最多存储 $n$ 个节点。 ================================================ FILE: docs/solutions/0600-0699/average-of-levels-in-binary-tree.md ================================================ # [0637. 二叉树的层平均值](https://leetcode.cn/problems/average-of-levels-in-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0637. 二叉树的层平均值 - 力扣](https://leetcode.cn/problems/average-of-levels-in-binary-tree/) ## 题目大意 **描述**: 给定一个非空二叉树的根节点 $root$。 **要求**: 以数组的形式返回每一层节点的平均值。与实际答案相差 $10^{-5}$ 以内的答案可以被接受。 **说明**: - 树中节点数量在 $[1, 10^{4}]$ 范围内。 - $-2^{31} \le Node.val \le 2^{31} - 1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/03/09/avg1-tree.jpg) ```python 输入:root = [3,9,20,null,null,15,7] 输出:[3.00000,14.50000,11.00000] 解释:第 0 层的平均值为 3,第 1 层的平均值为 14.5,第 2 层的平均值为 11 。 因此返回 [3, 14.5, 11] 。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/03/09/avg2-tree.jpg) ```python 输入:root = [3,9,20,15,7] 输出:[3.00000,14.50000,11.00000] ``` ## 解题思路 ### 思路 1:广度优先搜索 #### 思路 1:算法描述 这道题目要求返回二叉树每一层节点的平均值。我们可以使用广度优先搜索(BFS)来层序遍历二叉树。 具体步骤如下: 1. 初始化结果数组 $ans$ 和队列 $queue$,将根节点加入队列。 2. 当队列不为空时,执行以下操作: - 记录当前层的节点数量 $size$。 - 初始化当前层的节点值之和 $level\_sum = 0$。 - 遍历当前层的所有节点: - 从队列中取出节点,将其值加到 $level\_sum$ 中。 - 如果节点有左子节点,将左子节点加入队列。 - 如果节点有右子节点,将右子节点加入队列。 - 计算当前层的平均值 $level\_sum / size$,加入结果数组 $ans$。 3. 返回结果数组 $ans$。 #### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def averageOfLevels(self, root: Optional[TreeNode]) -> List[float]: if not root: return [] ans = [] queue = [root] while queue: size = len(queue) # 当前层的节点数量 level_sum = 0 # 当前层的节点值之和 # 遍历当前层的所有节点 for _ in range(size): node = queue.pop(0) level_sum += node.val # 将下一层的节点加入队列 if node.left: queue.append(node.left) if node.right: queue.append(node.right) # 计算当前层的平均值 ans.append(level_sum / size) return ans ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数。需要遍历所有节点。 - **空间复杂度**:$O(n)$。队列中最多存储 $n$ 个节点。 ================================================ FILE: docs/solutions/0600-0699/baseball-game.md ================================================ # [0682. 棒球比赛](https://leetcode.cn/problems/baseball-game/) - 标签:栈、数组、模拟 - 难度:简单 ## 题目链接 - [0682. 棒球比赛 - 力扣](https://leetcode.cn/problems/baseball-game/) ## 题目大意 **描述**: 你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。 比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 $ops$,其中 $ops[i]$ 是你需要记录的第 $i$ 项操作,$ops$ 遵循下述规则: 1. 整数 $x$:表示本回合新获得分数 $x$ 2. `"+"`:表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。 3. `"D"`:表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。 4. `"C"`:表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。 **要求**: 返回记录中所有得分的总和。 **说明**: - $1 \le ops.length \le 10^{3}$。 - $ops[i]$ 为 `"C"`、`"D"`、`"+"`,或者一个表示整数的字符串。整数范围是 $[-3 \times 10^{4}, 3 \times 10^{4}]$。 - 对于 `"+"` 操作,题目数据保证记录此操作时前面总是存在两个有效的分数。 - 对于 `"C"` 和 `"D"` 操作,题目数据保证记录此操作时前面总是存在一个有效的分数。 **示例**: - 示例 1: ```python 输入:ops = ["5","2","C","D","+"] 输出:30 解释: "5" - 记录加 5 ,记录现在是 [5] "2" - 记录加 2 ,记录现在是 [5, 2] "C" - 使前一次得分的记录无效并将其移除,记录现在是 [5]. "D" - 记录加 2 * 5 = 10 ,记录现在是 [5, 10]. "+" - 记录加 5 + 10 = 15 ,记录现在是 [5, 10, 15]. 所有得分的总和 5 + 10 + 15 = 30 ``` - 示例 2: ```python 输入:ops = ["5","-2","4","C","D","9","+","+"] 输出:27 解释: "5" - 记录加 5 ,记录现在是 [5] "-2" - 记录加 -2 ,记录现在是 [5, -2] "4" - 记录加 4 ,记录现在是 [5, -2, 4] "C" - 使前一次得分的记录无效并将其移除,记录现在是 [5, -2] "D" - 记录加 2 * -2 = -4 ,记录现在是 [5, -2, -4] "9" - 记录加 9 ,记录现在是 [5, -2, -4, 9] "+" - 记录加 -4 + 9 = 5 ,记录现在是 [5, -2, -4, 9, 5] "+" - 记录加 9 + 5 = 14 ,记录现在是 [5, -2, -4, 9, 5, 14] 所有得分的总和 5 + -2 + -4 + 9 + 5 + 14 = 27 ``` ## 解题思路 ### 思路 1:栈 #### 思路 1:算法描述 这道题目需要根据操作记录计算得分总和。我们可以使用栈来模拟这个过程。 具体步骤如下: 1. 初始化一个空栈 $stack$,用于存储有效的得分记录。 2. 遍历操作列表 $ops$,对于每个操作 $op$: - 如果 $op$ 是 `"+"`,则将栈顶两个元素的和加入栈中。 - 如果 $op$ 是 `"D"`,则将栈顶元素的两倍加入栈中。 - 如果 $op$ 是 `"C"`,则将栈顶元素弹出。 - 否则,$op$ 是一个整数,将其转换为整数后加入栈中。 3. 遍历结束后,返回栈中所有元素的和。 #### 思路 1:代码 ```python class Solution: def calPoints(self, operations: List[str]) -> int: stack = [] # 用栈存储有效得分 for op in operations: if op == "+": # 前两次得分的总和 stack.append(stack[-1] + stack[-2]) elif op == "D": # 前一次得分的两倍 stack.append(stack[-1] * 2) elif op == "C": # 移除前一次得分 stack.pop() else: # 新得分 stack.append(int(op)) # 返回所有得分的总和 return sum(stack) ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是操作列表的长度。需要遍历一次操作列表。 - **空间复杂度**:$O(n)$。栈中最多存储 $n$ 个元素。 ================================================ FILE: docs/solutions/0600-0699/beautiful-arrangement-ii.md ================================================ # [0667. 优美的排列 II](https://leetcode.cn/problems/beautiful-arrangement-ii/) - 标签:数组、数学 - 难度:中等 ## 题目链接 - [0667. 优美的排列 II - 力扣](https://leetcode.cn/problems/beautiful-arrangement-ii/) ## 题目大意 **描述**: 给定两个整数 $n$ 和 $k$。 **要求**: 构造一个答案列表 $answer$,该列表应当包含从 1 到 $n$ 的 $n$ 个不同正整数,并同时满足下述条件: - 假设该列表是 $answer = [a_1, a_2, a_3, ..., a_n]$,那么列表 $[|a_1 - a_2|, |a_2 - a_3|, |a_3 - a_4|, ..., |a_{n-1} - a_n|]$ 中应该有且仅有 $k$ 个不同整数。 返回列表 $answer$ 。如果存在多种答案,只需返回其中任意一种。 **说明**: - $1 \le k \lt n \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:n = 3, k = 1 输出:[1, 2, 3] 解释:[1, 2, 3] 包含 3 个范围在 1-3 的不同整数,并且 [1, 1] 中有且仅有 1 个不同整数:1 ``` - 示例 2: ```python 输入:n = 3, k = 2 输出:[1, 3, 2] 解释:[1, 3, 2] 包含 3 个范围在 1-3 的不同整数,并且 [2, 1] 中有且仅有 2 个不同整数:1 和 2 ``` ## 解题思路 ### 思路 1:构造法 #### 思路 1:算法描述 这道题目要求构造一个包含 $1$ 到 $n$ 的排列,使得相邻元素的差值恰好有 $k$ 个不同的整数。 我们可以使用构造法来解决这个问题。观察发现: - 如果数组是 $[1, 2, 3, ..., n]$,那么相邻元素的差值都是 $1$,只有 $1$ 个不同的整数。 - 如果数组是 $[1, n, 2, n-1, 3, n-2, ...]$,那么相邻元素的差值是 $n-1, n-2, n-3, ...$,有 $n-1$ 个不同的整数。 因此,我们可以先构造前 $k$ 个元素,使得它们的差值恰好有 $k$ 个不同的整数,然后剩余的元素按顺序排列。 具体步骤如下: 1. 初始化结果数组 $ans$。 2. 使用两个指针 $left = 1$ 和 $right = k + 1$,交替取值,构造前 $k + 1$ 个元素: - 先取 $left$,然后取 $right$,再取 $left + 1$,再取 $right - 1$,以此类推。 - 这样可以保证前 $k + 1$ 个元素的差值恰好有 $k$ 个不同的整数。 3. 将剩余的元素 $[k + 2, k + 3, ..., n]$ 按顺序加入结果数组。 4. 返回结果数组 $ans$。 #### 思路 1:代码 ```python class Solution: def constructArray(self, n: int, k: int) -> List[int]: ans = [] left, right = 1, k + 1 # 构造前 k + 1 个元素,使得差值恰好有 k 个不同的整数 while left <= right: ans.append(left) left += 1 if left <= right: ans.append(right) right -= 1 # 将剩余的元素按顺序加入结果数组 for i in range(k + 2, n + 1): ans.append(i) return ans ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。需要构造长度为 $n$ 的数组。 - **空间复杂度**:$O(1)$。不考虑结果数组的空间,只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0600-0699/binary-number-with-alternating-bits.md ================================================ # [0693. 交替位二进制数](https://leetcode.cn/problems/binary-number-with-alternating-bits/) - 标签:位运算 - 难度:简单 ## 题目链接 - [0693. 交替位二进制数 - 力扣](https://leetcode.cn/problems/binary-number-with-alternating-bits/) ## 题目大意 **描述**: 给定一个正整数 $n$。 **要求**: 检查它的二进制表示是否总是 $0$、$1$ 交替出现:换句话说,就是二进制表示中相邻两位的数字永不相同。 **说明**: - $1 \le n \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:n = 5 输出:true 解释:5 的二进制表示是:101 ``` - 示例 2: ```python 输入:n = 7 输出:false 解释:7 的二进制表示是:111. ``` ## 解题思路 ### 思路 1:位运算 #### 思路 1:算法描述 这道题目要求判断一个正整数的二进制表示是否总是 $0$、$1$ 交替出现。 我们可以使用位运算来解决这个问题。如果二进制表示中相邻两位的数字永不相同,那么将 $n$ 右移一位后与 $n$ 进行异或运算,得到的结果应该是所有位都为 $1$ 的数。 具体步骤如下: 1. 计算 $a = n \oplus (n >> 1)$,其中 $\oplus$ 表示异或运算。 2. 如果 $n$ 的二进制表示是交替的,那么 $a$ 的二进制表示应该是所有位都为 $1$。 3. 判断 $a$ 是否满足 $a \& (a + 1) = 0$,如果满足则返回 $True$,否则返回 $False$。 **解释**:如果 $a$ 的所有位都为 $1$,那么 $a + 1$ 会产生进位,使得 $a \& (a + 1) = 0$。 #### 思路 1:代码 ```python class Solution: def hasAlternatingBits(self, n: int) -> bool: # 将 n 右移一位后与 n 异或 a = n ^ (n >> 1) # 判断 a 是否所有位都为 1 return (a & (a + 1)) == 0 ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。只需要进行常数次位运算。 - **空间复杂度**:$O(1)$。只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0600-0699/bulb-switcher-ii.md ================================================ # [0672. 灯泡开关 Ⅱ](https://leetcode.cn/problems/bulb-switcher-ii/) - 标签:位运算、深度优先搜索、广度优先搜索、数学 - 难度:中等 ## 题目链接 - [0672. 灯泡开关 Ⅱ - 力扣](https://leetcode.cn/problems/bulb-switcher-ii/) ## 题目大意 **描述**: 房间中有 $n$ 只已经打开的灯泡,编号从 $1$ 到 $n$。墙上挂着 $4$ 个开关。 这 $4$ 个开关各自都具有不同的功能,其中: - 开关 1 :反转当前所有灯的状态(即开变为关,关变为开) - 开关 2 :反转编号为偶数的灯的状态(即 $0, 2, 4, ...$) - 开关 3 :反转编号为奇数的灯的状态(即 $1, 3, ...$) - 开关 4 :反转编号为 $j = 3 \times k + 1$ 的灯的状态,其中 $k = 0, 1, 2, ...$(即 $1, 4, 7, 10, ...$) 你必须「恰好」按压开关 $presses$ 次。每次按压,你都需要从 $4$ 个开关中选出一个来执行按压操作。 给定两个整数 $n$ 和 $presses$。 **要求**: 执行完所有按压之后,返回「不同可能状态」的数量。 **说明**: - $1 \le n \le 10^{3}$。 - $0 \le presses \le 10^{3}$。 **示例**: - 示例 1: ```python 输入:n = 1, presses = 1 输出:2 解释:状态可以是: - 按压开关 1 ,[关] - 按压开关 2 ,[开] ``` - 示例 2: ```python 输入:n = 2, presses = 1 输出:3 解释:状态可以是: - 按压开关 1 ,[关, 关] - 按压开关 2 ,[开, 关] - 按压开关 3 ,[关, 开] ``` ## 解题思路 ### 思路 1:数学 + 枚举 #### 思路 1:算法描述 这道题目要求在恰好按压开关 $presses$ 次后,返回不同可能状态的数量。 我们需要分析四个开关的作用: - 开关 1:反转所有灯的状态。 - 开关 2:反转编号为偶数的灯的状态。 - 开关 3:反转编号为奇数的灯的状态。 - 开关 4:反转编号为 $3k + 1$ 的灯的状态($k = 0, 1, 2, ...$)。 关键观察: 1. 按压同一个开关两次等于没有按压,所以每个开关最多只需要按压一次。 2. 按压开关的顺序不影响最终结果。 3. 对于前 $3$ 个灯,可以完全确定所有灯的状态(因为灯的状态是周期性的)。 我们可以枚举所有可能的开关组合(最多 $2^4 = 16$ 种),然后判断哪些组合是有效的(按压次数的奇偶性与 $presses$ 相同)。 具体步骤如下: 1. 如果 $presses = 0$,返回 $1$(所有灯都亮着)。 2. 枚举所有可能的开关组合(用二进制表示,$0$ 表示不按压,$1$ 表示按压)。 3. 对于每个组合,计算按压次数,判断是否与 $presses$ 的奇偶性相同,且按压次数不超过 $presses$。 4. 如果有效,计算前 $\min(n, 3)$ 个灯的状态,加入集合中。 5. 返回集合的大小。 #### 思路 1:代码 ```python class Solution: def flipLights(self, n: int, presses: int) -> int: if presses == 0: return 1 # 只需要考虑前 3 个灯的状态 n = min(n, 3) states = set() # 枚举所有可能的开关组合(4 个开关,2^4 = 16 种组合) for mask in range(16): # 计算按压次数 press_count = bin(mask).count('1') # 判断按压次数是否有效 if press_count % 2 != presses % 2 or press_count > presses: continue # 计算前 n 个灯的状态 lights = [1] * n # 初始状态:所有灯都亮着 # 开关 1:反转所有灯 if mask & 1: for i in range(n): lights[i] ^= 1 # 开关 2:反转编号为偶数的灯(索引为 1, 3, 5, ...) if mask & 2: for i in range(1, n, 2): lights[i] ^= 1 # 开关 3:反转编号为奇数的灯(索引为 0, 2, 4, ...) if mask & 4: for i in range(0, n, 2): lights[i] ^= 1 # 开关 4:反转编号为 3k + 1 的灯(索引为 0, 3, 6, ...) if mask & 8: for i in range(0, n, 3): lights[i] ^= 1 # 将状态加入集合 states.add(tuple(lights)) return len(states) ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。枚举的组合数是常数($16$ 种),每种组合的计算时间也是常数。 - **空间复杂度**:$O(1)$。集合中最多存储常数个状态。 ================================================ FILE: docs/solutions/0600-0699/can-place-flowers.md ================================================ # [0605. 种花问题](https://leetcode.cn/problems/can-place-flowers/) - 标签:贪心、数组 - 难度:简单 ## 题目链接 - [0605. 种花问题 - 力扣](https://leetcode.cn/problems/can-place-flowers/) ## 题目大意 **描述**: 假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。 给定一个整数数组 $flowerbed$ 表示花坛,由若干 $0$ 和 $1$ 组成,其中 $0$ 表示没种植花,$1$ 表示种植了花。另给定一个数 $n$。 **要求**: 能否在不打破种植规则的情况下种入 $n$ 朵花?能则返回 true,不能则返回 false 。 **说明**: - $1 \le flowerbed.length \le 2 \times 10^{4}$。 - $flowerbed[i]$ 为 $0$ 或 $1$。 - $flowerbed$ 中不存在相邻的两朵花。 - $0 \le n \le flowerbed.length$。 **示例**: - 示例 1: ```python 输入:flowerbed = [1,0,0,0,1], n = 1 输出:true ``` - 示例 2: ```python 输入:flowerbed = [1,0,0,0,1], n = 2 输出:false ``` ## 解题思路 ### 思路 1:贪心算法 #### 思路 1:算法描述 这道题目要求在不违反种植规则的情况下,判断能否种入 $n$ 朵花。种植规则是:花不能种植在相邻的地块上。 我们可以使用贪心算法,从左到右遍历花坛,只要当前位置和相邻位置都没有花,就尽可能地种花。 具体步骤如下: 1. 遍历花坛数组 $flowerbed$,对于每个位置 $i$: - 如果 $flowerbed[i] = 0$(当前位置没有花)。 - 并且 $i = 0$ 或 $flowerbed[i - 1] = 0$(左边没有花或者是边界)。 - 并且 $i = len(flowerbed) - 1$ 或 $flowerbed[i + 1] = 0$(右边没有花或者是边界)。 - 则在当前位置种花,将 $flowerbed[i]$ 设置为 $1$,并将计数器 $n$ 减 $1$。 2. 如果 $n \le 0$,说明已经种够了 $n$ 朵花,返回 $True$。 3. 遍历结束后,如果 $n > 0$,说明无法种够 $n$ 朵花,返回 $False$。 #### 思路 1:代码 ```python class Solution: def canPlaceFlowers(self, flowerbed: List[int], n: int) -> bool: # 遍历花坛 for i in range(len(flowerbed)): # 当前位置没有花,且左右两边都没有花(或者是边界) if flowerbed[i] == 0 and (i == 0 or flowerbed[i - 1] == 0) and (i == len(flowerbed) - 1 or flowerbed[i + 1] == 0): # 在当前位置种花 flowerbed[i] = 1 n -= 1 # 如果已经种够了,直接返回 True if n <= 0: return True # 遍历结束后,判断是否种够了 return n <= 0 ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(m)$,其中 $m$ 是花坛的长度。只需要遍历一次花坛数组。 - **空间复杂度**:$O(1)$。只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0600-0699/coin-path.md ================================================ # [0656. 成本最小路径](https://leetcode.cn/problems/coin-path/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [0656. 成本最小路径 - 力扣](https://leetcode.cn/problems/coin-path/) ## 题目大意 **描述**: 给定一个整数数组 $coins$(下标从 $1$ 开始)长度为 $n$,以及一个整数 $maxJump$。你可以跳到数组 $coins$ 的任意下标 $i$(满足 $coins[i] \ne -1$),访问下标 $i$ 时需要支付 $coins[i]$。此外,如果你当前位于下标 $i$,你只能跳到下标 $i + k$(满足 $i + k \le n$),其中 $k$ 是范围 $[1, maxJump]$ 内的一个值。 初始时你位于下标 $1$($coins[1]$ 不是 $-1$)。 **要求**: 找到一条到达下标 $n$ 的成本最小路径。 返回一个整数数组,包含你访问的下标顺序,以便你以最小成本达到下标 $n$。如果存在多条成本相同的路径,返回 **字典序最小** 的路径。如果无法达到下标 $n$,返回一个空数组。 路径 $p_1 = [Pa_1, Pa_2, ..., Pa_x]$ 的长度为 $x$,路径 $p_2 = [Pb_1, Pb_2, ..., Pb_x]$ 的长度为 $y$,如果在两条路径的第一个不同的下标 $j$ 处,$Pa_j$ 小于 $Pb_j$,则 $p_1$ 在字典序上小于 $p_2$;如果不存在这样的 $j$,则较短的路径字典序较小。 **说明**: - $1 \le coins.length \le 10^3$。 - $-1 \le coins[i] \le 10^3$。 - $coins[1] \ne -1$。 - $1 \le maxJump \le 10^3$。 **示例**: - 示例 1: ```python 输入:coins = [1,2,4,-1,2], maxJump = 2 输出:[1,3,5] ``` - 示例 2: ```python 输入:coins = [1,2,4,-1,2], maxJump = 1 输出:[] ``` ## 解题思路 ### 思路 1:动态规划(反向) 这道题目要求找到一条到达终点的成本最小路径,如果存在多条成本相同的路径,返回字典序最小的路径。 我们可以使用**反向动态规划**来解决这个问题。从终点往前推,定义 $dp[i]$ 表示从位置 $i$ 到达终点的最小成本。 **为什么使用反向DP?** - 当成本相同时,我们需要选择字典序最小的路径 - 从后往前DP时,如果成本相同,我们选择索引较小的下一个节点,这样可以保证字典序最小 - 如果从前往后DP,即使选择索引较小的前驱节点,也无法保证整个路径的字典序最小 **算法步骤**: 1. 初始化 $dp$ 数组,$dp[n-1] = coins[n-1]$(终点位置)。 2. 从后往前遍历每个位置 $i$,枚举所有可能的下一个位置 $j$(满足 $i < j \le i + maxJump$ 且 $j < n$),更新 $dp[i]$。 3. 使用 $next[i]$ 数组记录从位置 $i$ 出发的下一个节点。如果成本相同,选择索引较小的下一个节点(保证字典序最小)。 4. 从起点开始,沿着 $next$ 数组构造路径。 **注意**:如果某个位置的 $coins[i] = -1$,则该位置不可达。 ### 思路 1:代码 ```python class Solution: def cheapestJump(self, coins: List[int], maxJump: int) -> List[int]: n = len(coins) # 如果起点或终点不可达,返回空数组 if coins[0] == -1 or coins[n - 1] == -1: return [] # dp[i] 表示从位置 i 到达终点的最小成本 dp = [float('inf')] * n dp[n - 1] = coins[n - 1] # next[i] 表示从位置 i 出发的下一个节点(用于构造字典序最小的路径) next_node = [-1] * n # 从后往前进行动态规划 for i in range(n - 2, -1, -1): if coins[i] == -1: continue # 枚举所有可能的下一个位置 for j in range(i + 1, min(i + maxJump + 1, n)): if coins[j] == -1: continue # 如果从 j 无法到达终点,跳过 if dp[j] == float('inf'): continue cost = coins[i] + dp[j] # 更新最小成本和下一个节点 if cost < dp[i]: dp[i] = cost next_node[i] = j elif cost == dp[i] and (next_node[i] == -1 or j < next_node[i]): # 成本相同,选择索引较小的下一个节点(保证字典序最小) next_node[i] = j # 如果无法从起点到达终点 if dp[0] == float('inf'): return [] # 从起点开始,沿着 next_node 数组构造路径 path = [] i = 0 # 沿着 next_node 数组遍历,直到到达终点 while i < n and next_node[i] >= 0: path.append(i + 1) # 题目中位置从 1 开始 i = next_node[i] # 检查是否成功到达终点 if i == n - 1 and coins[i] >= 0: path.append(n) else: return [] return path ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times maxJump)$,其中 $n$ 是数组的长度。需要两层循环,外层循环 $n$ 次,内层循环最多 $maxJump$ 次。 - **空间复杂度**:$O(n)$。需要使用两个长度为 $n$ 的数组存储动态规划的状态和下一个节点信息。 ================================================ FILE: docs/solutions/0600-0699/construct-string-from-binary-tree.md ================================================ # [0606. 根据二叉树创建字符串](https://leetcode.cn/problems/construct-string-from-binary-tree/) - 标签:树、深度优先搜索、字符串、二叉树 - 难度:中等 ## 题目链接 - [0606. 根据二叉树创建字符串 - 力扣](https://leetcode.cn/problems/construct-string-from-binary-tree/) ## 题目大意 **描述**: 给定二叉树的根节点 $root$。 **要求**: 采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。 空节点使用一对空括号对 `"()"` 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。 **说明**: - 树中节点的数目范围是 $[1, 10^{4}]$。 - $-10^{3} \le Node.val \le 10^{3}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/05/03/cons1-tree.jpg) ```python 输入:root = [1,2,3,4] 输出:"1(2(4))(3)" 解释:初步转化后得到 "1(2(4)())(3()())" ,但省略所有不必要的空括号对后,字符串应该是"1(2(4))(3)" 。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/05/03/cons2-tree.jpg) ```python 输入:root = [1,2,3,null,4] 输出:"1(2()(4))(3)" 解释:和第一个示例类似,但是无法省略第一个空括号对,否则会破坏输入与输出一一映射的关系。 ``` ## 解题思路 ### 思路 1:深度优先搜索 #### 思路 1:算法描述 这道题目要求将二叉树转化为一个由括号和整数组成的字符串,采用前序遍历的方式。 我们可以使用深度优先搜索(DFS)来递归构造字符串。需要注意的是,要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。 具体规则如下: 1. 如果节点有左子树,则需要在左子树的字符串外加上括号。 2. 如果节点有右子树,则需要在右子树的字符串外加上括号。 3. 如果节点没有左子树但有右子树,则需要在左子树的位置加上空括号 `"()"`。 4. 如果节点既没有左子树也没有右子树,则不需要加括号。 #### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def tree2str(self, root: Optional[TreeNode]) -> str: if not root: return "" # 只有根节点 if not root.left and not root.right: return str(root.val) # 有左子树,没有右子树 if root.left and not root.right: return str(root.val) + "(" + self.tree2str(root.left) + ")" # 有右子树(无论是否有左子树) return str(root.val) + "(" + self.tree2str(root.left) + ")(" + self.tree2str(root.right) + ")" ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数。需要遍历所有节点。 - **空间复杂度**:$O(n)$。递归调用栈的深度最多为 $n$。 ================================================ FILE: docs/solutions/0600-0699/count-binary-substrings.md ================================================ # [0696. 计数二进制子串](https://leetcode.cn/problems/count-binary-substrings/) - 标签:双指针、字符串 - 难度:简单 ## 题目链接 - [0696. 计数二进制子串 - 力扣](https://leetcode.cn/problems/count-binary-substrings/) ## 题目大意 **描述**: 给定一个字符串 $s$。 **要求**: 统计并返回具有相同数量 $0$ 和 $1$ 的非空(连续)子字符串的数量,并且这些子字符串中的所有 $0$ 和所有 $1$ 都是成组连续的。 重复出现(不同位置)的子串也要统计它们出现的次数。 **说明**: - $1 \le s.length \le 10^{5}$。 - $s[i]$ 为 `'0'` 或 `'1'`。 **示例**: - 示例 1: ```python 输入:s = "00110011" 输出:6 解释:6 个子串满足具有相同数量的连续 1 和 0 :"0011"、"01"、"1100"、"10"、"0011" 和 "01" 。 注意,一些重复出现的子串(不同位置)要统计它们出现的次数。 另外,"00110011" 不是有效的子串,因为所有的 0(还有 1 )没有组合在一起。 ``` - 示例 2: ```python 输入:s = "10101" 输出:4 解释:有 4 个子串:"10"、"01"、"10"、"01" ,具有相同数量的连续 1 和 0 。 ``` ## 解题思路 ### 思路 1:双指针 #### 思路 1:算法描述 这道题目要求统计具有相同数量 $0$ 和 $1$ 的非空连续子字符串的数量,并且这些子字符串中的所有 $0$ 和所有 $1$ 都是成组连续的。 我们可以使用双指针的方法,统计连续的 $0$ 和 $1$ 的个数。 具体步骤如下: 1. 初始化 $prev = 0$(前一组字符的个数)和 $curr = 1$(当前组字符的个数)。 2. 初始化结果 $ans = 0$。 3. 从左到右遍历字符串 $s$,对于每个位置 $i$(从 $1$ 开始): - 如果 $s[i] = s[i - 1]$,说明当前字符与前一个字符相同,将 $curr$ 加 $1$。 - 否则,说明遇到了新的一组字符,此时可以形成的子字符串数量为 $\min(prev, curr)$,将其加到 $ans$ 中,然后更新 $prev = curr$,$curr = 1$。 4. 遍历结束后,还需要加上最后一组字符可以形成的子字符串数量 $\min(prev, curr)$。 5. 返回 $ans$。 #### 思路 1:代码 ```python class Solution: def countBinarySubstrings(self, s: str) -> int: prev = 0 # 前一组字符的个数 curr = 1 # 当前组字符的个数 ans = 0 # 结果 # 遍历字符串 for i in range(1, len(s)): if s[i] == s[i - 1]: # 当前字符与前一个字符相同 curr += 1 else: # 遇到新的一组字符 ans += min(prev, curr) prev = curr curr = 1 # 加上最后一组字符可以形成的子字符串数量 ans += min(prev, curr) return ans ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串的长度。只需要遍历一次字符串。 - **空间复杂度**:$O(1)$。只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0600-0699/course-schedule-iii.md ================================================ # [0630. 课程表 III](https://leetcode.cn/problems/course-schedule-iii/) - 标签:贪心、数组、排序、堆(优先队列) - 难度:困难 ## 题目链接 - [0630. 课程表 III - 力扣](https://leetcode.cn/problems/course-schedule-iii/) ## 题目大意 **描述**: 这里有 $n$ 门不同的在线课程,按从 $1$ 到 $n$ 编号。给你一个数组 $courses$ ,其中 $courses[i] = [duration_i, lastDay_i]$ 表示第 $i$ 门课将会持续上 $durationi$ 天课,并且必须在不晚于 $lastDayi$ 的时候完成。 你的学期从第 1 天开始。且不能同时修读两门及两门以上的课程。 **要求**: 返回你最多可以修读的课程数目。 **说明**: - $1 \le courses.length \le 10^{4}$。 - $1 \le duration_i, lastDay_i \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:courses = [[100, 200], [200, 1300], [1000, 1250], [2000, 3200]] 输出:3 解释: 这里一共有 4 门课程,但是你最多可以修 3 门: 首先,修第 1 门课,耗费 100 天,在第 100 天完成,在第 101 天开始下门课。 第二,修第 3 门课,耗费 1000 天,在第 1100 天完成,在第 1101 天开始下门课程。 第三,修第 2 门课,耗时 200 天,在第 1300 天完成。 第 4 门课现在不能修,因为将会在第 3300 天完成它,这已经超出了关闭日期。 ``` - 示例 2: ```python 输入:courses = [[1,2]] 输出:1 ``` ## 解题思路 ### 思路 1:贪心 + 优先队列 #### 思路 1:算法描述 这道题目要求在不晚于截止日期的情况下,最多可以修读多少门课程。 我们可以使用贪心算法结合优先队列(最大堆)来解决这个问题。 基本思路: 1. 按照课程的截止日期从小到大排序。 2. 依次考虑每门课程,如果当前时间加上课程持续时间不超过截止日期,就选择这门课程。 3. 如果超过了截止日期,但当前课程的持续时间比已选课程中持续时间最长的课程短,就替换掉那门课程。 具体步骤如下: 1. 将课程按照截止日期从小到大排序。 2. 初始化当前时间 $time = 0$ 和最大堆 $heap$(存储已选课程的持续时间)。 3. 遍历排序后的课程: - 如果 $time + duration \le lastDay$,选择这门课程,将持续时间加入堆中,更新 $time$。 - 否则,如果堆不为空且堆顶元素(最大持续时间)大于当前课程的持续时间,就替换掉堆顶课程。 4. 返回堆的大小,即最多可以修读的课程数。 #### 思路 1:代码 ```python class Solution: def scheduleCourse(self, courses: List[List[int]]) -> int: import heapq # 按照截止日期从小到大排序 courses.sort(key=lambda x: x[1]) time = 0 # 当前时间 heap = [] # 最大堆,存储已选课程的持续时间(取负数实现最大堆) for duration, lastDay in courses: # 如果可以在截止日期前完成这门课程 if time + duration <= lastDay: time += duration heapq.heappush(heap, -duration) # 加入堆中(取负数) # 如果不能完成,但当前课程的持续时间比已选课程中最长的短 elif heap and -heap[0] > duration: # 替换掉持续时间最长的课程 time += duration - (-heapq.heappop(heap)) heapq.heappush(heap, -duration) return len(heap) ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是课程的数量。排序需要 $O(n \log n)$,每门课程最多进出堆一次,堆操作需要 $O(\log n)$。 - **空间复杂度**:$O(n)$。堆中最多存储 $n$ 门课程。 ================================================ FILE: docs/solutions/0600-0699/cut-off-trees-for-golf-event.md ================================================ # [0675. 为高尔夫比赛砍树](https://leetcode.cn/problems/cut-off-trees-for-golf-event/) - 标签:广度优先搜索、数组、矩阵、堆(优先队列) - 难度:困难 ## 题目链接 - [0675. 为高尔夫比赛砍树 - 力扣](https://leetcode.cn/problems/cut-off-trees-for-golf-event/) ## 题目大意 **描述**: 你被请来给一个要举办高尔夫比赛的树林砍树。树林由一个 $m \times n$ 的矩阵表示,在这个矩阵中: - $0$ 表示障碍,无法触碰 - $1$ 表示地面,可以行走 - 比 $1$ 大的数表示有树的单元格,可以行走,数值表示树的高度 每一步,你都可以向上、下、左、右四个方向之一移动一个单位,如果你站的地方有一棵树,那么你可以决定是否要砍倒它。 你需要按照树的高度从低向高砍掉所有的树,每砍过一颗树,该单元格的值变为 $1$(即变为地面)。 **要求**: 你将从 $(0, 0)$ 点开始工作,返回你砍完所有树需要走的最小步数。 如果你无法砍完所有的树,返回 $-1$。 可以保证的是,没有两棵树的高度是相同的,并且你至少需要砍倒一棵树。 **说明**: - $m == forest.length$。 - $n == forest[i].length$。 - $1 \le m, n \le 50$。 - $0 \le forest[i][j] \le 10^{9}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/26/trees1.jpg) ```python 输入:forest = [[1,2,3],[0,0,4],[7,6,5]] 输出:6 解释:沿着上面的路径,你可以用 6 步,按从最矮到最高的顺序砍掉这些树。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/11/26/trees2.jpg) ```python 输入:forest = [[1,2,3],[0,0,0],[7,6,5]] 输出:-1 解释:由于中间一行被障碍阻塞,无法访问最下面一行中的树。 ``` ## 解题思路 ### 思路 1:BFS + 排序 #### 思路 1:算法描述 这道题目要求按照树的高度从低到高砍树,返回砍完所有树需要走的最小步数。 我们可以将问题分解为两个子问题: 1. 确定砍树的顺序:按照树的高度从低到高排序。 2. 计算从一个位置到另一个位置的最短路径:使用 BFS。 具体步骤如下: 1. 遍历矩阵,找到所有树的位置和高度,按照高度从低到高排序。 2. 从起点 $(0, 0)$ 开始,依次前往每棵树的位置。 3. 对于每次移动,使用 BFS 计算从当前位置到目标位置的最短路径。 4. 如果无法到达某棵树,返回 $-1$。 5. 累加所有移动的步数,返回总步数。 #### 思路 1:代码 ```python class Solution: def cutOffTree(self, forest: List[List[int]]) -> int: from collections import deque m, n = len(forest), len(forest[0]) # 找到所有树的位置和高度 trees = [] for i in range(m): for j in range(n): if forest[i][j] > 1: trees.append((forest[i][j], i, j)) # 按照高度从低到高排序 trees.sort() # BFS 计算从 (sr, sc) 到 (tr, tc) 的最短路径 def bfs(sr, sc, tr, tc): if sr == tr and sc == tc: return 0 queue = deque([(sr, sc, 0)]) visited = {(sr, sc)} directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] while queue: r, c, dist = queue.popleft() for dr, dc in directions: nr, nc = r + dr, c + dc # 检查边界和障碍物 if 0 <= nr < m and 0 <= nc < n and (nr, nc) not in visited and forest[nr][nc] != 0: if nr == tr and nc == tc: return dist + 1 queue.append((nr, nc, dist + 1)) visited.add((nr, nc)) return -1 # 无法到达 # 从起点开始,依次前往每棵树 total_steps = 0 sr, sc = 0, 0 for _, tr, tc in trees: steps = bfs(sr, sc, tr, tc) if steps == -1: return -1 total_steps += steps sr, sc = tr, tc return total_steps ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(m^2 \times n^2 \times t)$,其中 $m$ 和 $n$ 是矩阵的行数和列数,$t$ 是树的数量。每次 BFS 的时间复杂度为 $O(m \times n)$,需要进行 $t$ 次 BFS。 - **空间复杂度**:$O(m \times n)$。BFS 需要使用队列和访问标记。 ================================================ FILE: docs/solutions/0600-0699/decode-ways-ii.md ================================================ # [0639. 解码方法 II](https://leetcode.cn/problems/decode-ways-ii/) - 标签:字符串、动态规划 - 难度:困难 ## 题目链接 - [0639. 解码方法 II - 力扣](https://leetcode.cn/problems/decode-ways-ii/) ## 题目大意 **描述**:给定一个包含数字和字符 `'*'` 的字符串 $s$。该字符串已经按照下面的映射关系进行了编码: - `A` 映射为 $1$。 - `B` 映射为 $2$。 - ... - `Z` 映射为 $26$。 除了上述映射方法,字符串 $s$ 中可能包含字符 `'*'`,可以表示 $1$ ~ $9$ 的任一数字(不包括 $0$)。例如字符串 `"1*"` 可以表示为 `"11"`、`"12"`、…、`"18"`、`"19"` 中的任何一个编码。 基于上述映射的方法,现在对字符串 `s` 进行「解码」。即从数字到字母进行反向映射。比如 `"11106"` 可以映射为: - `"AAJF"`,将消息分组为 $(1 1 10 6)$。 - `"KJF"`,将消息分组为 $(11 10 6)$。 **要求**:计算出共有多少种可能的解码方案。 **说明**: - $1 \le s.length \le 100$。 - $s$ 只包含数字,并且可能包含前导零。 - 题目数据保证答案肯定是一个 $32$ 位的整数。 ```python 输入:s = "*" 输出:9 解释:这一条编码消息可以表示 "1"、"2"、"3"、"4"、"5"、"6"、"7"、"8" 或 "9" 中的任意一条。可以分别解码成字符串 "A"、"B"、"C"、"D"、"E"、"F"、"G"、"H" 和 "I" 。因此,"*" 总共有 9 种解码方法。 ``` ## 解题思路 ### 思路 1:动态规划 这道题是「[91. 解码方法 - 力扣](https://leetcode.cn/problems/decode-ways/)」的升级版,其思路是相似的,只不过本题的状态转移方程的条件和公式不太容易想全。 ###### 1. 阶段划分 按照字符串的结尾位置进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i]$ 表示为:字符串 $s$ 前 $i$ 个字符构成的字符串可能构成的翻译方案数。 ###### 3. 状态转移方程 $dp[i]$ 的来源有两种情况: 1. 使用了一个字符,对 $s[i]$ 进行翻译: 1. 如果 `s[i] == '*'`,则 `s[i]` 可以视作区间 `[1, 9]` 上的任意一个数字,可以被翻译为 `A` ~ `I`。此时当前位置上的方案数为 `9`,即 `dp[i] = dp[i - 1] * 9`。 2. 如果 `s[i] == '0'`,则无法被翻译,此时当前位置上的方案数为 `0`,即 `dp[i] = dp[i - 1] * 0`。 3. 如果是其他情况(即 `s[i]` 是区间 `[1, 9]` 上某一个数字),可以被翻译为 `A` ~ `I` 对应位置上的某个字母。此时当前位置上的方案数为 `1`,即 `dp[i] = dp[i - 1] * 1`。 2. 使用了两个字符,对 `s[i - 1]` 和 `s[i]` 进行翻译: 1. 如果 `s[i - 1] == '*'` 并且 `s[i] == '*'`,则 `s[i]` 可以视作区间 `[11, 19]` 或者 `[21, 26]` 上的任意一个数字。此时当前位置上的方案数为 `15`,即 `dp[i] = dp[i - 2] * 15`。 2. 如果 `s[i - 1] == '*'` 并且 `s[i] != '*'`,则: 1. 如果 `s[i]` 在区间 `[1, 6]` 内,`s[i - 1]` 可以选择 `1` 或 `2`。此时当前位置上的方案数为 `2`,即 `dp[i] = dp[i - 2] * 2`。 2. 如果 `s[i]` 不在区间 `[1, 6]` 内,`s[i - 1]` 只能选择 `1`。此时当前位置上的方案数为 `1`,即 `dp[i] = dp[i - 2] * 1`。 3. 如果 `s[i - 1] == '1'` 并且 `s[i] == '*'`,`s[i]` 可以视作区间 `[1, 9]` 上任意一个数字。此时当前位置上的方案数为 `9`,即 `dp[i] = dp[i - 2] * 9`。 4. 如果 `s[i - 1] == '1'` 并且 `s[i] != '*'`,`s[i]` 可以视作区间 `[1, 9]` 上的某一个数字。此时当前位置上的方案数为 `1`,即 `dp[i] = dp[i - 2] * 1`。 5. 如果 `s[i - 1] == '2'` 并且 `s[i] == '*'`,`s[i]` 可以视作区间 `[1, 6]` 上任意一个数字。此时当前位置上的方案数为 `6`,即 `dp[i] = dp[i - 2] * 6`。 6. 如果 `s[i - 1] == '2'` 并且 `s[i] != '*'`,则: 1. 如果 `s[i]` 在区间 `[1, 6]` 内,此时当前位置上的方案数为 `1`,即 `dp[i] = dp[i - 2] * 1`。 2. 如果 `s[i]` 不在区间 `[1, 6]` 内,此时当前位置上的方案数为 `0`,即 `dp[i] = dp[i - 2] * 0`。 7. 其他情况下(即 `s[i - 1]` 在区间 `[3, 9]` 内),则无法被翻译,此时当前位置上的方案数为 `0`,即 `dp[i] = dp[i - 2] * 0`。 在进行转移的时候,需要将使用一个字符的翻译方案数与使用两个字符的翻译方案数进行相加。同时还要注意对 $10^9 + 7$ 的取余。 这里我们可以单独写两个方法 `,分别来表示「单个字符 `s[i]` 的翻译方案数」和「两个字符 `s[i - 1]` 和 `s[i]` 的翻译方案数」,这样代码逻辑会更加清晰。 ###### 4. 初始条件 - 字符串为空时,只有一个翻译方案,翻译为空字符串,即 `dp[0] = 1`。 - 字符串只有一个字符时,单个字符 `s[i]` 的翻译方案数为转移条件的第一种求法,即`dp[1] = self.parse1(s[0])`。 ###### 5. 最终结果 根据我们之前定义的状态,`dp[i]` 表示为:字符串 `s` 前 `i` 个字符构成的字符串可能构成的翻译方案数。则最终结果为 `dp[size]`,`size` 为字符串长度。 ### 思路 1:动态规划代码 ```python class Solution: def parse1(self, ch): if ch == '*': return 9 if ch == '0': return 0 return 1 def parse2(self, ch1, ch2): if ch1 == '*' and ch2 == '*': return 15 if ch1 == '*' and ch2 != '*': return 2 if ch2 <= '6' else 1 if ch1 == '1' and ch2 == '*': return 9 if ch1 == '1' and ch2 != '*': return 1 if ch1 == '2' and ch2 == '*': return 6 if ch1 == '2' and ch2 != '*': return 1 if ch2 <= '6' else 0 return 0 def numDecodings(self, s: str) -> int: mod = 10 ** 9 + 7 size = len(s) dp = [0 for _ in range(size + 1)] dp[0] = 1 dp[1] = self.parse1(s[0]) for i in range(2, size + 1): dp[i] += dp[i - 1] * self.parse1(s[i - 1]) dp[i] += dp[i - 2] * self.parse2(s[i - 2], s[i - 1]) dp[i] %= mod return dp[size] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度是 $O(n)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 ================================================ FILE: docs/solutions/0600-0699/degree-of-an-array.md ================================================ # [0697. 数组的度](https://leetcode.cn/problems/degree-of-an-array/) - 标签:数组、哈希表 - 难度:简单 ## 题目链接 - [0697. 数组的度 - 力扣](https://leetcode.cn/problems/degree-of-an-array/) ## 题目大意 **描述**: 给定一个非空且只包含非负数的整数数组 $nums$,数组的「度」的定义是指数组里任一元素出现频数的最大值。 **要求**: 在 $nums$ 中找到与 $nums$ 拥有相同大小的度的最短连续子数组,返回其长度。 **说明**: - $nums.length$ 在 $1$ 到 $50,000$ 范围内。 - $nums[i]$ 是一个在 $0$ 到 $49,999$ 范围内的整数。 **示例**: - 示例 1: ```python 输入:nums = [1,2,2,3,1] 输出:2 解释: 输入数组的度是 2 ,因为元素 1 和 2 的出现频数最大,均为 2 。 连续子数组里面拥有相同度的有如下所示: [1, 2, 2, 3, 1], [1, 2, 2, 3], [2, 2, 3, 1], [1, 2, 2], [2, 2, 3], [2, 2] 最短连续子数组 [2, 2] 的长度为 2 ,所以返回 2 。 ``` - 示例 2: ```python 输入:nums = [1,2,2,3,1,4,2] 输出:6 解释: 数组的度是 3 ,因为元素 2 重复出现 3 次。 所以 [2,2,3,1,4,2] 是最短子数组,因此返回 6 。 ``` ## 解题思路 ### 思路 1:哈希表 #### 思路 1:算法描述 这道题目要求找到与原数组拥有相同度的最短连续子数组。数组的度定义为数组里任一元素出现频数的最大值。 我们可以使用哈希表来记录每个元素的出现次数、第一次出现的位置和最后一次出现的位置。 具体步骤如下: 1. 初始化三个哈希表: - $count$:记录每个元素的出现次数。 - $first$:记录每个元素第一次出现的位置。 - $last$:记录每个元素最后一次出现的位置。 2. 遍历数组 $nums$,更新三个哈希表。 3. 找到数组的度 $degree$,即 $count$ 中的最大值。 4. 遍历 $count$,找到所有出现次数等于 $degree$ 的元素,计算它们对应的子数组长度 $last[num] - first[num] + 1$,取最小值。 5. 返回最小值。 #### 思路 1:代码 ```python class Solution: def findShortestSubArray(self, nums: List[int]) -> int: count = {} # 记录每个元素的出现次数 first = {} # 记录每个元素第一次出现的位置 last = {} # 记录每个元素最后一次出现的位置 # 遍历数组,更新哈希表 for i, num in enumerate(nums): if num not in count: count[num] = 1 first[num] = i else: count[num] += 1 last[num] = i # 找到数组的度 degree = max(count.values()) # 找到最短子数组长度 min_len = len(nums) for num, cnt in count.items(): if cnt == degree: min_len = min(min_len, last[num] - first[num] + 1) return min_len ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。需要遍历两次数组。 - **空间复杂度**:$O(n)$。需要使用三个哈希表存储信息。 ================================================ FILE: docs/solutions/0600-0699/design-circular-deque.md ================================================ # [0641. 设计循环双端队列](https://leetcode.cn/problems/design-circular-deque/) - 标签:设计、队列、数组、链表 - 难度:中等 ## 题目链接 - [0641. 设计循环双端队列 - 力扣](https://leetcode.cn/problems/design-circular-deque/) ## 题目大意 **描述**: 设计实现双端队列。 **要求**: 实现 MyCircularDeque 类: - `MyCircularDeque(int k)`:构造函数,双端队列最大为 $k$。 - `boolean insertFront()`:将一个元素添加到双端队列头部。 如果操作成功返回 true,否则返回 false。 - `boolean insertLast()`:将一个元素添加到双端队列尾部。如果操作成功返回 true,否则返回 false。 - `boolean deleteFront()`:从双端队列头部删除一个元素。 如果操作成功返回 true,否则返回 false。 - `boolean deleteLast()`:从双端队列尾部删除一个元素。如果操作成功返回 true,否则返回 false。 - `int getFront()`:从双端队列头部获得一个元素。如果双端队列为空,返回 $-1$。 - `int getRear()`:获得双端队列的最后一个元素。 如果双端队列为空,返回 $-1$。 - `boolean isEmpty()`:若双端队列为空,则返回 true,否则返回 false。 - `boolean isFull()`:若双端队列满了,则返回 true,否则返回 false。 **说明**: - $1 \le k \le 10^{3}$。 - $0 \le value \le 10^{3}$。 - `insertFront`, `insertLast`, `deleteFront`, `deleteLast`, `getFront`, `getRear`, `isEmpty`, `isFull` 调用次数不大于 $2000$ 次。 **示例**: - 示例 1: ```python 输入 ["MyCircularDeque", "insertLast", "insertLast", "insertFront", "insertFront", "getRear", "isFull", "deleteLast", "insertFront", "getFront"] [[3], [1], [2], [3], [4], [], [], [], [4], []] 输出 [null, true, true, true, false, 2, true, true, true, 4] 解释 MyCircularDeque circularDeque = new MycircularDeque(3); // 设置容量大小为3 circularDeque.insertLast(1); // 返回 true circularDeque.insertLast(2); // 返回 true circularDeque.insertFront(3); // 返回 true circularDeque.insertFront(4); // 已经满了,返回 false circularDeque.getRear(); // 返回 2 circularDeque.isFull(); // 返回 true circularDeque.deleteLast(); // 返回 true circularDeque.insertFront(4); // 返回 true circularDeque.getFront(); // 返回 4 ``` ## 解题思路 ### 思路 1:数组实现循环双端队列 #### 思路 1:算法描述 这道题目要求设计实现一个循环双端队列。我们可以使用数组来实现。 需要维护以下变量: - $queue$:存储队列元素的数组,长度为 $k + 1$(多一个空间用于区分队列满和队列空)。 - $front$:队首指针,指向队首元素。 - $rear$:队尾指针,指向队尾元素的下一个位置。 - $capacity$:队列的容量,为 $k + 1$。 各个操作的实现: 1. **insertFront**:在队首插入元素。将 $front$ 向前移动一位(循环),然后在 $front$ 位置插入元素。 2. **insertLast**:在队尾插入元素。在 $rear$ 位置插入元素,然后将 $rear$ 向后移动一位(循环)。 3. **deleteFront**:删除队首元素。将 $front$ 向后移动一位(循环)。 4. **deleteLast**:删除队尾元素。将 $rear$ 向前移动一位(循环)。 5. **getFront**:获取队首元素。返回 $queue[front]$。 6. **getRear**:获取队尾元素。返回 $queue[(rear - 1 + capacity) \% capacity]$。 7. **isEmpty**:判断队列是否为空。当 $front = rear$ 时,队列为空。 8. **isFull**:判断队列是否已满。当 $(rear + 1) \% capacity = front$ 时,队列已满。 #### 思路 1:代码 ```python class MyCircularDeque: def __init__(self, k: int): self.capacity = k + 1 # 多一个空间用于区分队列满和队列空 self.queue = [0] * self.capacity self.front = 0 # 队首指针 self.rear = 0 # 队尾指针 def insertFront(self, value: int) -> bool: if self.isFull(): return False # 将 front 向前移动一位(循环) self.front = (self.front - 1 + self.capacity) % self.capacity self.queue[self.front] = value return True def insertLast(self, value: int) -> bool: if self.isFull(): return False self.queue[self.rear] = value # 将 rear 向后移动一位(循环) self.rear = (self.rear + 1) % self.capacity return True def deleteFront(self) -> bool: if self.isEmpty(): return False # 将 front 向后移动一位(循环) self.front = (self.front + 1) % self.capacity return True def deleteLast(self) -> bool: if self.isEmpty(): return False # 将 rear 向前移动一位(循环) self.rear = (self.rear - 1 + self.capacity) % self.capacity return True def getFront(self) -> int: if self.isEmpty(): return -1 return self.queue[self.front] def getRear(self) -> int: if self.isEmpty(): return -1 # 队尾元素在 rear 的前一个位置 return self.queue[(self.rear - 1 + self.capacity) % self.capacity] def isEmpty(self) -> bool: return self.front == self.rear def isFull(self) -> bool: return (self.rear + 1) % self.capacity == self.front # Your MyCircularDeque object will be instantiated and called as such: # obj = MyCircularDeque(k) # param_1 = obj.insertFront(value) # param_2 = obj.insertLast(value) # param_3 = obj.deleteFront() # param_4 = obj.deleteLast() # param_5 = obj.getFront() # param_6 = obj.getRear() # param_7 = obj.isEmpty() # param_8 = obj.isFull() ``` #### 思路 1:复杂度分析 - **时间复杂度**:所有操作的时间复杂度均为 $O(1)$。 - **空间复杂度**:$O(k)$。需要使用长度为 $k + 1$ 的数组存储队列元素。 ================================================ FILE: docs/solutions/0600-0699/design-circular-queue.md ================================================ # [0622. 设计循环队列](https://leetcode.cn/problems/design-circular-queue/) - 标签:设计、队列、数组、链表 - 难度:中等 ## 题目链接 - [0622. 设计循环队列 - 力扣](https://leetcode.cn/problems/design-circular-queue/) ## 题目大意 **要求**:设计实现一个循环队列,支持以下操作: - `MyCircularQueue(k)`: 构造器,设置队列长度为 `k`。 - `Front`: 从队首获取元素。如果队列为空,返回 `-1`。 - `Rear`: 获取队尾元素。如果队列为空,返回 `-1`。 - `enQueue(value)`: 向循环队列插入一个元素。如果成功插入则返回真。 - `deQueue()`: 从循环队列中删除一个元素。如果成功删除则返回真。 - `isEmpty()`: 检查循环队列是否为空。 - `isFull()`: 检查循环队列是否已满。 **说明**: - 所有的值都在 `0` 至 `1000` 的范围内。 - 操作数将在 `1` 至 `1000` 的范围内。 - 请不要使用内置的队列库。 **示例**: - 示例 1: ```python MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3 circularQueue.enQueue(1); // 返回 true circularQueue.enQueue(2); // 返回 true circularQueue.enQueue(3); // 返回 true circularQueue.enQueue(4); // 返回 false,队列已满 circularQueue.Rear(); // 返回 3 circularQueue.isFull(); // 返回 true circularQueue.deQueue(); // 返回 true circularQueue.enQueue(4); // 返回 true circularQueue.Rear(); // 返回 4 ``` ## 解题思路 这道题可以使用数组,也可以使用链表来实现循环队列。 ### 思路 1:使用数组模拟 建立一个容量为 `k + 1` 的数组 `queue`。并保存队头指针 `front`、队尾指针 `rear`,队列容量 `capacity` 为 `k + 1`(这里之所以用了 `k + 1` 的容量,是为了判断空和满,需要空出一个)。 然后实现循环队列的各个接口: 1. `MyCircularQueue(k)`: 1. 将数组 `queue` 初始化大小为 `k + 1` 的数组。 2. `front`、`rear` 初始化为 `0`。 2. `Front`: 1. 先检测队列是否为空。如果队列为空,返回 `-1`。 2. 如果不为空,则返回队头元素。 3. `Rear`: 1. 先检测队列是否为空。如果队列为空,返回 `-1`。 2. 如果不为空,则返回队尾元素。 4. `enQueue(value)`: 1. 如果队列已满,则无法插入,返回 `False`。 2. 如果队列未满,则将队尾指针 `rear` 向右循环移动一位,并进行插入操作。然后返回 `True`。 5. `deQueue()`: 1. 如果队列为空,则无法删除,返回 `False`。 2. 如果队列不空,则将队头指针 `front` 指向元素赋值为 `None`,并将 `front` 向右循环移动一位。然后返回 `True`。 6. `isEmpty()`: 如果 `rear` 等于 `front`,则说明队列为空,返回 `True`。否则,队列不为空,返回 `False`。 7. `isFull()`: 如果 `(rear + 1) % capacity` 等于 `front`,则说明队列已满,返回 `True`。否则,队列未满,返回 `False`。 ### 思路 1:代码 ```python class MyCircularQueue: def __init__(self, k: int): self.capacity = k + 1 self.queue = [0 for _ in range(k + 1)] self.front = 0 self.rear = 0 def enQueue(self, value: int) -> bool: if self.isFull(): return False self.rear = (self.rear + 1) % self.capacity self.queue[self.rear] = value return True def deQueue(self) -> bool: if self.isEmpty(): return False self.front = (self.front + 1) % self.capacity return True def Front(self) -> int: if self.isEmpty(): return -1 return self.queue[(self.front + 1) % self.capacity] def Rear(self) -> int: if self.isEmpty(): return -1 return self.queue[self.rear] def isEmpty(self) -> bool: return self.front == self.rear def isFull(self) -> bool: return (self.rear + 1) % self.capacity == self.front ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。初始化和每项操作的时间复杂度均为 $O(1)$。 - **空间复杂度**:$O(k)$。其中 $k$ 为给定队列的元素数目。 ================================================ FILE: docs/solutions/0600-0699/design-compressed-string-iterator.md ================================================ # [0604. 迭代压缩字符串](https://leetcode.cn/problems/design-compressed-string-iterator/) - 标签:设计、数组、字符串、迭代器 - 难度:简单 ## 题目链接 - [0604. 迭代压缩字符串 - 力扣](https://leetcode.cn/problems/design-compressed-string-iterator/) ## 题目大意 **要求**: 设计并实现一个迭代压缩字符串的数据结构。给定的压缩字符串的形式是,每个字母后面紧跟一个正整数,表示该字母在原始未压缩字符串中出现的次数。 设计一个数据结构,它支持如下两种操作: `next` 和 `hasNext`。 - `next()`:如果原始字符串中仍有未压缩字符,则返回下一个字符,否则返回空格。 - `hasNext()`:如果原始字符串中存在未压缩的的字母,则返回 true,否则返回 false。 **说明**: - $1 \le compressedString.length \le 10^{3}$。 - $compressedString$ 由小写字母、大写字母和数字组成。 - 在 $compressedString$ 中,单个字符的重复次数在 $[1, 10^9]$ 范围内。 - `next` 和 `hasNext` 的操作数最多为 $10^{3}$。 **示例**: - 示例 1: ```python 输入: ["StringIterator", "next", "next", "next", "next", "next", "next", "hasNext", "next", "hasNext"] [["L1e2t1C1o1d1e1"], [], [], [], [], [], [], [], [], []] 输出: [null, "L", "e", "e", "t", "C", "o", true, "d", true] 解释: StringIterator stringIterator = new StringIterator("L1e2t1C1o1d1e1"); stringIterator.next(); // 返回 "L" stringIterator.next(); // 返回 "e" stringIterator.next(); // 返回 "e" stringIterator.next(); // 返回 "t" stringIterator.next(); // 返回 "C" stringIterator.next(); // 返回 "o" stringIterator.hasNext(); // 返回 True stringIterator.next(); // 返回 "d" stringIterator.hasNext(); // 返回 True ``` - 示例 2: ```python 输入: 输出: ``` ## 解题思路 ### 思路 1:双指针解析 #### 思路 1:算法描述 这道题目要求设计一个迭代压缩字符串的数据结构。压缩字符串的格式是每个字母后面紧跟一个正整数,表示该字母的重复次数。 我们可以在初始化时解析压缩字符串,将字符和对应的重复次数存储起来,然后使用指针来跟踪当前位置。 具体步骤如下: 1. **初始化**:解析压缩字符串,提取每个字符和对应的重复次数,存储在列表中。维护两个变量: - $idx$:当前字符在列表中的索引。 - $count$:当前字符剩余的重复次数。 2. **next()**:返回下一个字符。 - 如果 $count > 0$,返回当前字符,并将 $count$ 减 $1$。 - 如果 $count = 0$,移动到下一个字符($idx$ 加 $1$),更新 $count$。 - 如果已经没有字符了,返回空格。 3. **hasNext()**:判断是否还有未压缩的字符。 - 如果 $idx < len(chars)$ 或 $count > 0$,返回 $True$。 - 否则返回 $False$。 #### 思路 1:代码 ```python class StringIterator: def __init__(self, compressedString: str): self.chars = [] # 存储字符和重复次数的列表 i = 0 n = len(compressedString) # 解析压缩字符串 while i < n: char = compressedString[i] i += 1 num_str = "" # 提取数字 while i < n and compressedString[i].isdigit(): num_str += compressedString[i] i += 1 self.chars.append((char, int(num_str))) self.idx = 0 # 当前字符在列表中的索引 self.count = self.chars[0][1] if self.chars else 0 # 当前字符剩余的重复次数 def next(self) -> str: if not self.hasNext(): return ' ' # 获取当前字符 char = self.chars[self.idx][0] self.count -= 1 # 如果当前字符已经用完,移动到下一个字符 if self.count == 0 and self.idx + 1 < len(self.chars): self.idx += 1 self.count = self.chars[self.idx][1] return char def hasNext(self) -> bool: return self.idx < len(self.chars) and self.count > 0 # Your StringIterator object will be instantiated and called as such: # obj = StringIterator(compressedString) # param_1 = obj.next() # param_2 = obj.hasNext() ``` #### 思路 1:复杂度分析 - **时间复杂度**: - 初始化:$O(n)$,其中 $n$ 是压缩字符串的长度。 - next():$O(1)$。 - hasNext():$O(1)$。 - **空间复杂度**:$O(m)$,其中 $m$ 是不同字符的数量。 ================================================ FILE: docs/solutions/0600-0699/design-excel-sum-formula.md ================================================ # [0631. 设计 Excel 求和公式](https://leetcode.cn/problems/design-excel-sum-formula/) - 标签:图、设计、拓扑排序、数组、哈希表、字符串、矩阵 - 难度:困难 ## 题目链接 - [0631. 设计 Excel 求和公式 - 力扣](https://leetcode.cn/problems/design-excel-sum-formula/) ## 题目大意 **描述**: 请你设计 Excel 中的基本功能,并实现求和公式。 **要求**: 实现 Excel 类: - `Excel(int height, char width)`:用高度 $height$ 和宽度 $width$ 初始化对象。该表格是一个大小为 $height \times width$ 的整数矩阵 $mat$,其中行下标范围是 $[1, height]$,列下标范围是 $['A', width]$。初始情况下,所有的值都应该为零。 - `void set(int row, char column, int val)`:将 $mat[row][column]$ 的值更改为 $val$。 - `int get(int row, char column)`:返回 $mat[row][column]$ 的值。 - `int sum(int row, char column, List numbers)`:将 $mat[row][column]$ 的值设为由 $numbers$ 表示的单元格的和,并返回 $mat[row][column]$ 的值。此求和公式应该 **长期作用于** 该单元格,直到该单元格被另一个值或另一个求和公式覆盖。其中,$numbers[i]$ 的格式可以为: - `"ColRow"`:表示某个单元格。例如,`"F7"` 表示单元格 $mat[7]['F']$。 - `"ColRow1:ColRow2"`:表示一组单元格。该范围将始终为一个矩形,其中 `"ColRow1"` 表示左上角单元格的位置,`"ColRow2"` 表示右下角单元格的位置。例如,`"B3:F7"` 表示 $3 \le i \le 7$ 和 $'B' \le j \le 'F'$ 的单元格 $mat[i][j]$。 **说明**: - 注意:可以假设不会出现循环求和引用。例如,$mat[1]['A'] == sum(1, "B")$,且 $mat[1]['B'] == sum(1, "A")$。 - $1 \le height \le 26$。 - $'A' \le width \le 'Z'$。 - $1 \le row \le height$。 - $'A' \le column \le width$。 - $-10^3 \le val \le 10^3$。 - $1 \le numbers.length \le 5$。 - $numbers[i]$ 的格式为 `"ColRow"` 或 `"ColRow1:ColRow2"`。 - 最多会对 `set`、`get` 和 `sum` 进行 $10^3$ 次调用。 **示例**: - 示例 1: ```python 输入: ["Excel", "set", "sum", "set", "get"] [[3, "C"], [1, "A", 2], [3, "C", ["A1", "A1:B2"]], [2, "B", 2], [3, "C"]] 输出: [null, null, 4, null, 6] 解释: 执行以下操作: Excel excel = new Excel(3, "C"); // 构造一个 3 * 3 的二维数组,所有值初始化为零。 // A B C // 1 0 0 0 // 2 0 0 0 // 3 0 0 0 excel.set(1, "A", 2); // 将 mat[1]["A"] 设置为 2 。 // A B C // 1 2 0 0 // 2 0 0 0 // 3 0 0 0 excel.sum(3, "C", ["A1", "A1:B2"]); // 返回 4 // 将 mat[3]["C"] 设置为 mat[1]["A"] 的值与矩形范围的单元格和的和,该范围的左上角单元格位置为 mat[1]["A"],右下角单元格位置为 mat[2]["B"]。 // A B C // 1 2 0 0 // 2 0 0 0 // 3 0 0 4 excel.set(2, "B", 2); // 将 mat[2]["B"] 设置为 2 。注意 mat[3]["C"] 也应该更改。 // A B C // 1 2 0 0 // 2 0 2 0 // 3 0 0 6 excel.get(3, "C"); // 返回 6 ``` ## 解题思路 ### 思路 1:哈希表 + 图 这道题目要求设计一个 Excel 表格,支持设置单元格值、获取单元格值和设置求和公式。关键在于求和公式需要 **长期作用**,即当依赖的单元格值改变时,公式单元格的值也要自动更新。 **核心思路**: - 使用哈希表存储每个单元格的值。 - 使用图结构记录单元格之间的依赖关系:如果单元格 $A$ 的值依赖于单元格 $B$,则 $B \to A$ 有一条边。 - 当某个单元格的值改变时,需要递归更新所有依赖它的单元格。 **算法步骤**: 1. **初始化**:创建哈希表存储单元格值,创建图存储依赖关系。 2. **set 操作**:设置单元格值,清除该单元格的依赖关系,递归更新依赖它的单元格。 3. **get 操作**:直接返回单元格的值。 4. **sum 操作**:解析 $numbers$,计算和,设置单元格值,建立依赖关系。 ### 思路 1:代码 ```python class Excel: def __init__(self, height: int, width: str): self.height = height self.width = ord(width) - ord('A') + 1 # formulas[r][c] = (cells_dict, val) # cells_dict: 依赖的单元格及其计数,val: 当前值 self.formulas = [[None] * self.width for _ in range(height)] def get(self, row: int, column: str) -> int: r, c = row - 1, ord(column) - ord('A') if self.formulas[r][c] is None: return 0 return self.formulas[r][c][1] def set(self, row: int, column: str, val: int) -> None: r, c = row - 1, ord(column) - ord('A') # 设置为纯值,清空依赖 self.formulas[r][c] = ({}, val) # 拓扑排序更新依赖此单元格的所有单元格 stack = [] self._topological_sort(r, c, stack, set()) self._execute_stack(stack) def sum(self, row: int, column: str, numbers: List[str]) -> int: r, c = row - 1, ord(column) - ord('A') cells = self._convert(numbers) summ = self._calculate_sum(r, c, cells) self.formulas[r][c] = (cells, summ) # 拓扑排序更新依赖此单元格的所有单元格 stack = [] self._topological_sort(r, c, stack, set()) self._execute_stack(stack) return summ def _convert(self, strs: List[str]) -> dict: """将公式字符串转换为单元格计数字典""" res = {} for st in strs: if ':' not in st: res[st] = res.get(st, 0) + 1 else: parts = st.split(':') si, ei = int(parts[0][1:]), int(parts[1][1:]) sj, ej = parts[0][0], parts[1][0] for i in range(si, ei + 1): for j in range(ord(sj), ord(ej) + 1): key = chr(j) + str(i) res[key] = res.get(key, 0) + 1 return res def _topological_sort(self, r: int, c: int, stack: list, visited: set) -> None: """拓扑排序:找出所有依赖 (r,c) 的单元格""" key = chr(ord('A') + c) + str(r + 1) for i in range(len(self.formulas)): for j in range(len(self.formulas[0])): if self.formulas[i][j] is not None and key in self.formulas[i][j][0]: if (i, j) not in visited: self._topological_sort(i, j, stack, visited) if (r, c) not in visited: visited.add((r, c)) stack.append((r, c)) def _execute_stack(self, stack: list) -> None: """按拓扑顺序更新单元格""" while stack: r, c = stack.pop() if self.formulas[r][c] is not None and self.formulas[r][c][0]: self._calculate_sum(r, c, self.formulas[r][c][0]) def _calculate_sum(self, r: int, c: int, cells: dict) -> int: """计算单元格的和""" total = 0 for s, cnt in cells.items(): x, y = int(s[1:]) - 1, ord(s[0]) - ord('A') val = self.formulas[x][y][1] if self.formulas[x][y] else 0 total += val * cnt self.formulas[r][c] = (cells, total) return total # Your Excel object will be instantiated and called as such: # obj = Excel(height, width) # obj.set(row,column,val) # param_2 = obj.get(row,column) # param_3 = obj.sum(row,column,numbers) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - $set$ 操作:$O(d)$,其中 $d$ 是依赖此单元格的单元格数量,需要更新所有依赖的单元格。 - $get$ 操作:$O(1)$,直接返回缓存的值。 - $sum$ 操作:$O(k + d)$,其中 $k$ 是公式中依赖的单元格数量,$d$ 是依赖此单元格的单元格数量。需要计算所有依赖的单元格,并更新依赖此单元格的其他单元格。 - **空间复杂度**:$O(n + m)$,其中 $n$ 是单元格的数量,$m$ 是依赖关系的数量。 ================================================ FILE: docs/solutions/0600-0699/design-log-storage-system.md ================================================ # [0635. 设计日志存储系统](https://leetcode.cn/problems/design-log-storage-system/) - 标签:设计、哈希表、字符串、有序集合 - 难度:中等 ## 题目链接 - [0635. 设计日志存储系统 - 力扣](https://leetcode.cn/problems/design-log-storage-system/) ## 题目大意 **描述**: 你将获得多条日志,每条日志都有唯一的 $id$ 和 $timestamp$,$timestamp$ 是形如 `Year:Month:Day:Hour:Minute:Second` 的字符串,例如 `2017:01:01:23:59:59`,所有值域都是零填充的十进制数。 **要求**: 实现 LogSystem 类: - `LogSystem()` 初始化 `LogSystem` 对象 - `void put(int id, string timestamp)` 给定日志的 $id$ 和 $timestamp$,将这个日志存入你的存储系统中。 - `int[] retrieve(string start, string end, string granularity)` 返回在给定时间区间 $[start, end]$(包含两端)内的所有日志的 $id$。$start$、$end$ 和 $timestamp$ 的格式相同,$granularity$ 表示考虑的时间粒度(例如,精确到 `Day`、`Minute` 等)。例如 `start = "2017:01:01:23:59:59"`、`end = "2017:01:02:23:59:59"` 且 `granularity = "Day"` 意味着需要查找从 Jan. 1st 2017 到 Jan. 2nd 2017 范围内的日志,可以忽略日志的 `Hour`、`Minute` 和 `Second`。 **说明**: - $1 \le id \le 500$。 - $2000 \le Year \le 2017$。 - $1 \le Month \le 12$。 - $1 \le Day \le 31$。 - $0 \le Hour \le 23$。 - $0 \le Minute, Second \le 59$。 - $granularity$ 是这些值 `["Year", "Month", "Day", "Hour", "Minute", "Second"]` 之一。 - 最多调用 $500$ 次 `put` 和 `retrieve`。 **示例**: - 示例 1: ```python 输入: ["LogSystem", "put", "put", "put", "retrieve", "retrieve"] [[], [1, "2017:01:01:23:59:59"], [2, "2017:01:01:22:59:59"], [3, "2016:01:01:00:00:00"], ["2016:01:01:01:01:01", "2017:01:01:23:00:00", "Year"], ["2016:01:01:01:01:01", "2017:01:01:23:00:00", "Hour"]] 输出: [null, null, null, null, [3, 2, 1], [2, 1]] 解释: LogSystem logSystem = new LogSystem(); logSystem.put(1, "2017:01:01:23:59:59"); logSystem.put(2, "2017:01:01:22:59:59"); logSystem.put(3, "2016:01:01:00:00:00"); // 返回 [3,2,1],返回从 2016 年到 2017 年所有的日志。 logSystem.retrieve("2016:01:01:01:01:01", "2017:01:01:23:00:00", "Year"); // 返回 [2,1],返回从 Jan. 1, 2016 01:XX:XX 到 Jan. 1, 2017 23:XX:XX 之间的所有日志 // 不返回日志 3 因为记录时间 Jan. 1, 2016 00:00:00 超过范围的起始时间 logSystem.retrieve("2016:01:01:01:01:01", "2017:01:01:23:00:00", "Hour"); ``` ## 解题思路 ### 思路 1:哈希表 + 字符串处理 #### 思路 1:算法描述 这道题目要求设计一个日志存储系统,支持存储日志和根据时间粒度检索日志。 我们可以使用哈希表来存储日志,键为日志 ID,值为时间戳。在检索时,根据时间粒度截取时间戳的相应部分进行比较。 **算法步骤**: 1. **初始化**:创建一个哈希表 `logs`,用于存储日志 ID 和时间戳。 2. **put(id, timestamp)**:将日志 ID 和时间戳存入哈希表。 3. **retrieve(start, end, granularity)**: - 根据时间粒度确定需要比较的时间戳长度。 - 截取 `start` 和 `end` 的相应部分。 - 遍历所有日志,截取时间戳的相应部分,判断是否在 `[start, end]` 范围内。 - 返回符合条件的日志 ID 列表。 时间粒度对应的截取长度: - Year: 4 - Month: 7 - Day: 10 - Hour: 13 - Minute: 16 - Second: 19 #### 思路 1:代码 ```python class LogSystem: def __init__(self): self.logs = {} # 存储日志 ID 和时间戳 # 时间粒度对应的截取长度 self.granularity_map = { "Year": 4, "Month": 7, "Day": 10, "Hour": 13, "Minute": 16, "Second": 19 } def put(self, id: int, timestamp: str) -> None: self.logs[id] = timestamp def retrieve(self, start: str, end: str, granularity: str) -> List[int]: # 根据时间粒度确定截取长度 length = self.granularity_map[granularity] # 截取 start 和 end 的相应部分 start_prefix = start[:length] end_prefix = end[:length] result = [] # 遍历所有日志 for log_id, timestamp in self.logs.items(): # 截取时间戳的相应部分 timestamp_prefix = timestamp[:length] # 判断是否在范围内 if start_prefix <= timestamp_prefix <= end_prefix: result.append(log_id) return result # Your LogSystem object will be instantiated and called as such: # obj = LogSystem() # obj.put(id,timestamp) # param_2 = obj.retrieve(start,end,granularity) ``` #### 思路 1:复杂度分析 - **时间复杂度**: - `put()`:$O(1)$。 - `retrieve()`:$O(n)$,其中 $n$ 是日志的数量。需要遍历所有日志。 - **空间复杂度**:$O(n)$。需要存储 $n$ 条日志。 ================================================ FILE: docs/solutions/0600-0699/design-search-autocomplete-system.md ================================================ # [0642. 设计搜索自动补全系统](https://leetcode.cn/problems/design-search-autocomplete-system/) - 标签:设计、字典树、字符串、数据流 - 难度:困难 ## 题目链接 - [0642. 设计搜索自动补全系统 - 力扣](https://leetcode.cn/problems/design-search-autocomplete-system/) ## 题目大意 要求:设计一个搜索自动补全系统。用户会输入一条语句(最少包含一个字母,以特殊字符 `#` 结尾)。除 `#` 以外用户输入的每个字符,返回历史中热度前三并以当前输入部分为前缀的句子。下面是详细规则: - 一条句子的热度定义为历史上用户输入这个句子的总次数。 - 返回前三的句子需要按照热度从高到低排序(第一个是最热门的)。如果有多条热度相同的句子,请按照 ASCII 码的顺序输出(ASCII 码越小排名越前)。 - 如果满足条件的句子个数少于 3,将它们全部输出。 - 如果输入了特殊字符,意味着句子结束了,请返回一个空集合。 你的工作是实现以下功能: - 构造函数: `AutocompleteSystem(String[] sentences, int[] times):` - 输入历史数据。 `sentences` 是之前输入过的所有句子,`times` 是每条句子输入的次数,你的系统需要记录这些历史信息。 - 输入函数(用户输入一条新的句子,下面的函数会提供用户输入的下一个字符):`List input(char c):` - 其中 `c` 是用户输入的下一个字符。字符只会是小写英文字母(`a` 到 `z` ),空格(` `)和特殊字符(`#`)。输出历史热度前三的具有相同前缀的句子。 ## 解题思路 使用字典树来保存输入过的所有句子 `sentences`,并且在字典树中维护每条句子的输入次数 `times`。 构造函数中: - 将所有句子及对应输入次数插入到字典树中。 输入函数中: - 使用 `path` 变量保存当前输入句子的前缀。 - 如果遇到 `#`,则将当前句子插入到字典树中。 - 如果遇到其他字符,用 `path` 保存当前字符 `c`。并在字典树中搜索以 `path` 为前缀的节点的所有分支,将每个分支对应的单词 `path` 和它们出现的次数 `times` 存入数组中。然后借助 `heapq` 进行堆排序,根据出现次数和 ASCII 码大小排序,找出 `times` 最多的前三个单词。 ## 代码 ```python import heapq class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False self.times = 0 def insert(self, word: str, times=1) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True cur.times += times def search(self, word: str): """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return [] cur = cur.children[ch] res = [] path = [word] cur.dfs(res, path) return res def dfs(self, res, path): cur = self if cur.isEnd: res.append((-cur.times, ''.join(path))) for ch in cur.children: node = cur.children[ch] path.append(ch) node.dfs(res, path) path.pop() class AutocompleteSystem: def __init__(self, sentences: List[str], times: List[int]): self.path = '' self.exists = True self.trie_tree = Trie() for i in range(len(sentences)): self.trie_tree.insert(sentences[i], times[i]) def input(self, c: str) -> List[str]: if c == '#': self.trie_tree.insert(self.path, 1) self.path = '' self.exists = True return [] else: self.path += c if not self.exists: return [] words = self.trie_tree.search(self.path) if words: heapq.heapify(words) res = [] while words and len(res) < 3: res.append(heapq.heappop(words)[1]) return res else: self.exists = False return [] ``` ================================================ FILE: docs/solutions/0600-0699/dota2-senate.md ================================================ # [0649. Dota2 参议院](https://leetcode.cn/problems/dota2-senate/) - 标签:贪心、队列、字符串 - 难度:中等 ## 题目链接 - [0649. Dota2 参议院 - 力扣](https://leetcode.cn/problems/dota2-senate/) ## 题目大意 **描述**: Dota2 的世界里有两个阵营:Radiant(天辉)和 Dire(夜魇) Dota2 参议院由来自两派的参议员组成。现在参议院希望对一个 Dota2 游戏里的改变作出决定。他们以一个基于轮为过程的投票进行。在每一轮中,每一位参议员都可以行使两项权利中的一项: - 禁止一名参议员的权利:参议员可以让另一位参议员在这一轮和随后的几轮中丧失 所有的权利 。 - 宣布胜利:如果参议员发现有权利投票的参议员都是 同一个阵营的 ,他可以宣布胜利并决定在游戏中的有关变化。 给定一个字符串 $senate$ 代表每个参议员的阵营。字母 `'R'` 和 `'D'` 分别代表了 Radiant(天辉)和 Dire(夜魇)。然后,如果有 $n$ 个参议员,给定字符串的大小将是 $n$。 以轮为基础的过程从给定顺序的第一个参议员开始到最后一个参议员结束。这一过程将持续到投票结束。所有失去权利的参议员将在过程中被跳过。 **要求**: 假设每一位参议员都足够聪明,会为自己的政党做出最好的策略,你需要预测哪一方最终会宣布胜利并在 Dota2 游戏中决定改变。输出应该是 `"Radiant"` 或 `"Dire"`。 **说明**: - $n == senate.length$。 - $1 \le n \le 10^{4}$。 - $senate[i]$ 为 `'R'` 或 `'D'`。 **示例**: - 示例 1: ```python 输入:senate = "RD" 输出:"Radiant" 解释: 第 1 轮时,第一个参议员来自 Radiant 阵营,他可以使用第一项权利让第二个参议员失去所有权利。 这一轮中,第二个参议员将会被跳过,因为他的权利被禁止了。 第 2 轮时,第一个参议员可以宣布胜利,因为他是唯一一个有投票权的人。 ``` - 示例 2: ```python 输入:senate = "RDD" 输出:"Dire" 解释: 第 1 轮时,第一个来自 Radiant 阵营的参议员可以使用第一项权利禁止第二个参议员的权利。 这一轮中,第二个来自 Dire 阵营的参议员会将被跳过,因为他的权利被禁止了。 这一轮中,第三个来自 Dire 阵营的参议员可以使用他的第一项权利禁止第一个参议员的权利。 因此在第二轮只剩下第三个参议员拥有投票的权利,于是他可以宣布胜利 ``` ## 解题思路 ### 思路 1:贪心 + 队列 #### 思路 1:算法描述 这道题目模拟 Dota2 参议院的投票过程。每个参议员都会采取最优策略,即优先禁止对方阵营中最早投票的参议员。 我们可以使用两个队列分别存储 Radiant 和 Dire 阵营参议员的位置索引,然后模拟投票过程。 具体步骤如下: 1. 初始化两个队列 $radiant$ 和 $dire$,分别存储两个阵营参议员的位置索引。 2. 遍历字符串 $senate$,将每个参议员的位置索引加入对应的队列。 3. 模拟投票过程: - 当两个队列都不为空时,取出两个队列的队首元素 $r$ 和 $d$。 - 比较 $r$ 和 $d$ 的大小: - 如果 $r < d$,说明 Radiant 阵营的参议员先投票,他会禁止 Dire 阵营的参议员。将 $r + n$($n$ 是参议员总数)加入 $radiant$ 队列,表示该参议员在下一轮还可以投票。 - 如果 $r > d$,说明 Dire 阵营的参议员先投票,他会禁止 Radiant 阵营的参议员。将 $d + n$ 加入 $dire$ 队列。 4. 最后,哪个队列不为空,哪个阵营获胜。 #### 思路 1:代码 ```python class Solution: def predictPartyVictory(self, senate: str) -> str: from collections import deque n = len(senate) radiant = deque() # Radiant 阵营参议员的位置索引 dire = deque() # Dire 阵营参议员的位置索引 # 初始化两个队列 for i, s in enumerate(senate): if s == 'R': radiant.append(i) else: dire.append(i) # 模拟投票过程 while radiant and dire: r = radiant.popleft() d = dire.popleft() # 位置靠前的参议员先投票,禁止对方阵营的参议员 if r < d: # Radiant 阵营的参议员先投票 radiant.append(r + n) else: # Dire 阵营的参议员先投票 dire.append(d + n) # 判断哪个阵营获胜 return "Radiant" if radiant else "Dire" ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是参议员的数量。每个参议员最多被处理常数次。 - **空间复杂度**:$O(n)$。需要使用两个队列存储参议员的位置索引。 ================================================ FILE: docs/solutions/0600-0699/employee-importance.md ================================================ # [0690. 员工的重要性](https://leetcode.cn/problems/employee-importance/) - 标签:深度优先搜索、广度优先搜索、哈希表 - 难度:中等 ## 题目链接 - [0690. 员工的重要性 - 力扣](https://leetcode.cn/problems/employee-importance/) ## 题目大意 给定一个公司的所有员工信息。其中每个员工信息包含:该员工 id,该员工重要度,以及该员工的所有下属 id。 再给定一个员工 id,要求返回该员工和他所有下属的重要度之和。 ## 解题思路 利用哈希表,以「员工 id: 员工数据结构」的形式将员工信息存入哈希表中。然后深度优先搜索该员工以及下属员工。在搜索的同时,计算重要度之和,最终返回结果即可。 ## 代码 ```python class Solution: def getImportance(self, employees: List['Employee'], id: int) -> int: employee_dict = dict() for employee in employees: employee_dict[employee.id] = employee def dfs(index: int) -> int: total = employee_dict[index].importance for sub_index in employee_dict[index].subordinates: total += dfs(sub_index) return total return dfs(id) ``` ================================================ FILE: docs/solutions/0600-0699/equal-tree-partition.md ================================================ # [0663. 均匀树划分](https://leetcode.cn/problems/equal-tree-partition/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0663. 均匀树划分 - 力扣](https://leetcode.cn/problems/equal-tree-partition/) ## 题目大意 **描述**:给你一棵二叉树的根节点 $root$,如果你可以通过去掉原始树上的一条边将树分成两棵节点值之和相等的子树,则返回 `true`。 **要求**:判断是否可以通过移除一条边将树分成两个节点值之和相等的子树。 **说明**: - 树中节点数目在 $[1, 10^4]$ 范围内。 - $-10^5 \le Node.val \le 10^5$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/05/03/split1-tree.jpg) ```python 输入:root = [5,10,10,null,null,2,3] 输出:true ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/05/03/split2-tree.jpg) ```python 输入:root = [1,2,10,null,null,2,20] 输出:false 解释:在树上移除一条边无法将树分成两棵节点值之和相等的子树。 ``` ## 解题思路 ### 思路 1:DFS + 后序遍历 #### 思路 1:算法描述 判断是否可以通过移除一条边将树分成两个节点值之和相等的子树。 **核心思路**: - 首先计算整棵树的总和 $total$。 - 如果 $total$ 是奇数,无法平分,返回 `False`。 - 使用 DFS 计算每个子树的和,如果某个子树的和等于 $total / 2$,说明可以分割。 - 注意:不能在根节点处分割(因为需要移除一条边)。 **算法步骤**: 1. 计算整棵树的总和 $total$。 2. 如果 $total$ 是奇数,返回 `False`。 3. 使用 DFS 遍历树,计算每个子树的和: - 如果某个非根子树的和等于 $total / 2$,返回 `True`。 4. 如果遍历完没有找到,返回 `False`。 #### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def checkEqualTree(self, root: Optional[TreeNode]) -> bool: # 计算整棵树的总和 def get_sum(node): if not node: return 0 return node.val + get_sum(node.left) + get_sum(node.right) total = get_sum(root) # 如果总和是奇数,无法平分 if total % 2 != 0: return False target = total // 2 self.found = False # DFS 检查每个子树的和 def dfs(node, is_root): if not node: return 0 left_sum = dfs(node.left, False) right_sum = dfs(node.right, False) current_sum = node.val + left_sum + right_sum # 如果不是根节点且子树和等于目标值 if not is_root and current_sum == target: self.found = True return current_sum dfs(root, True) return self.found ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树的节点数,需要遍历树两次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度,递归栈的深度。 ================================================ FILE: docs/solutions/0600-0699/exclusive-time-of-functions.md ================================================ # [0636. 函数的独占时间](https://leetcode.cn/problems/exclusive-time-of-functions/) - 标签:栈、数组 - 难度:中等 ## 题目链接 - [0636. 函数的独占时间 - 力扣](https://leetcode.cn/problems/exclusive-time-of-functions/) ## 题目大意 **描述**: 有一个单线程 CPU 正在运行一个含有 $n$ 道函数的程序。每道函数都有一个位于 $0$ 和 $n-1$ 之间的唯一标识符。 函数调用存储在一个「调用栈」上:当一个函数调用开始时,它的标识符将会推入栈中。而当一个函数调用结束时,它的标识符将会从栈中弹出。标识符位于栈顶的函数是 当前正在执行的函数。每当一个函数开始或者结束时,将会记录一条日志,包括函数标识符、是开始还是结束、以及相应的时间戳。 给定一个由日志组成的列表 $logs$,其中 $logs[i]$ 表示第 $i$ 条日志消息,该消息是一个按 `"{function_id}:{"start" | "end"}:{timestamp}"` 进行格式化的字符串。例如,`"0:start:3"` 意味着标识符为 $0$ 的函数调用在时间戳 $3$ 的起始开始执行;而 `"1:end:2"` 意味着标识符为 $1$ 的函数调用在时间戳 $2$ 的末尾结束执行。注意,函数可以调用多次,可能存在递归调用。 函数的「独占时间」定义是在这个函数在程序所有函数调用中执行时间的总和,调用其他函数花费的时间不算该函数的独占时间。例如,如果一个函数被调用两次,一次调用执行 $2$ 单位时间,另一次调用执行 $1$ 单位时间,那么该函数的 独占时间 为 $2 + 1 = 3$。 **要求**: 以数组形式返回每个函数的「独占时间」,其中第 $i$ 个下标对应的值表示标识符 $i$ 的函数的独占时间。 **说明**: - $1 \le n \le 10^{3}$。 - $2 \le logs.length \le 500$。 - $0 \le function_id \lt n$。 - $0 \le timestamp \le 10^{9}$。 - 两个开始事件不会在同一时间戳发生。 - 两个结束事件不会在同一时间戳发生。 - 每道函数都有一个对应 `"start"` 日志的 `"end"` 日志。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2019/04/05/diag1b.png) ```python 输入:n = 2, logs = ["0:start:0","1:start:2","1:end:5","0:end:6"] 输出:[3,4] 解释: 函数 0 在时间戳 0 的起始开始执行,执行 2 个单位时间,于时间戳 1 的末尾结束执行。 函数 1 在时间戳 2 的起始开始执行,执行 4 个单位时间,于时间戳 5 的末尾结束执行。 函数 0 在时间戳 6 的开始恢复执行,执行 1 个单位时间。 所以函数 0 总共执行 2 + 1 = 3 个单位时间,函数 1 总共执行 4 个单位时间。 ``` - 示例 2: ```python 输入:n = 1, logs = ["0:start:0","0:start:2","0:end:5","0:start:6","0:end:6","0:end:7"] 输出:[8] 解释: 函数 0 在时间戳 0 的起始开始执行,执行 2 个单位时间,并递归调用它自身。 函数 0(递归调用)在时间戳 2 的起始开始执行,执行 4 个单位时间。 函数 0(初始调用)恢复执行,并立刻再次调用它自身。 函数 0(第二次递归调用)在时间戳 6 的起始开始执行,执行 1 个单位时间。 函数 0(初始调用)在时间戳 7 的起始恢复执行,执行 1 个单位时间。 所以函数 0 总共执行 2 + 4 + 1 + 1 = 8 个单位时间。 ``` ## 解题思路 ### 思路 1:栈 这道题目需要模拟函数调用栈的执行过程,计算每个函数的独占时间。 1. 使用栈来模拟函数调用过程,栈中存储函数的 ID。 2. 使用数组 $ans$ 记录每个函数的独占时间。 3. 使用变量 $prev\underline{~}time$ 记录上一个时间戳。 4. 遍历日志列表: - 解析日志,获取函数 ID、操作类型(start/end)和时间戳。 - 如果是 start 操作: - 如果栈不为空,说明有函数正在执行,将当前时间与上一个时间的差值累加到栈顶函数的独占时间中。 - 将当前函数 ID 入栈。 - 更新 $prev\underline{~}time$ 为当前时间戳。 - 如果是 end 操作: - 将栈顶函数出栈,并将当前时间与上一个时间的差值加 1(因为 end 时刻也属于该函数)累加到该函数的独占时间中。 - 更新 $prev\underline{~}time$ 为当前时间戳加 1。 5. 返回结果数组 $ans$。 ### 思路 1:代码 ```python class Solution: def exclusiveTime(self, n: int, logs: List[str]) -> List[int]: ans = [0] * n # 记录每个函数的独占时间 stack = [] # 模拟函数调用栈 prev_time = 0 # 记录上一个时间戳 for log in logs: parts = log.split(':') func_id = int(parts[0]) op_type = parts[1] timestamp = int(parts[2]) if op_type == 'start': # 如果栈不为空,当前栈顶函数被暂停 if stack: ans[stack[-1]] += timestamp - prev_time # 当前函数入栈 stack.append(func_id) prev_time = timestamp else: # end # 当前函数出栈,累加独占时间 func_id = stack.pop() ans[func_id] += timestamp - prev_time + 1 prev_time = timestamp + 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m)$,其中 $m$ 是日志的数量。需要遍历所有日志。 - **空间复杂度**:$O(n)$,其中 $n$ 是函数的数量。需要使用栈来存储函数调用关系,最坏情况下所有函数都在栈中。 ================================================ FILE: docs/solutions/0600-0699/falling-squares.md ================================================ # [0699. 掉落的方块](https://leetcode.cn/problems/falling-squares/) - 标签:线段树、数组、有序集合 - 难度:困难 ## 题目链接 - [0699. 掉落的方块 - 力扣](https://leetcode.cn/problems/falling-squares/) ## 题目大意 **描述**: 在二维平面上的 x 轴上,放置着一些方块。 给定一个二维整数数组 $positions$,其中 $positions[i] = [left\_i, sideLength\_i]$ 表示:第 $i$ 个方块边长为 $sideLength\_i$ ,其左侧边与 x 轴上坐标点 $left\_i$ 对齐。 每个方块都从一个比目前所有的落地方块更高的高度掉落而下。方块沿 y 轴负方向下落,直到着陆到「另一个正方形的顶边」或者是 x 轴上。一个方块仅仅是擦过另一个方块的左侧边或右侧边不算着陆。一旦着陆,它就会固定在原地,无法移动。 在每个方块掉落后,你必须记录目前所有已经落稳的「方块堆叠的最高高度」。 **要求**: 返回一个整数数组 $ans$,其中 $ans[i]$ 表示在第 $i$ 块方块掉落后堆叠的最高高度。 **说明**: - $1 \le positions.length \le 10^{3}$。 - $1 \le left_i \le 10^{8}$。 - $1 \le sideLength_i \le 10^{6}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/28/fallingsq1-plane.jpg) ```python 输入:positions = [[1,2],[2,3],[6,1]] 输出:[2,5,5] 解释: 第 1 个方块掉落后,最高的堆叠由方块 1 组成,堆叠的最高高度为 2 。 第 2 个方块掉落后,最高的堆叠由方块 1 和 2 组成,堆叠的最高高度为 5 。 第 3 个方块掉落后,最高的堆叠仍然由方块 1 和 2 组成,堆叠的最高高度为 5 。 因此,返回 [2, 5, 5] 作为答案。 ``` - 示例 2: ```python 输入:positions = [[100,100],[200,100]] 输出:[100,100] 解释: 第 1 个方块掉落后,最高的堆叠由方块 1 组成,堆叠的最高高度为 100 。 第 2 个方块掉落后,最高的堆叠可以由方块 1 组成也可以由方块 2 组成,堆叠的最高高度为 100 。 因此,返回 [100, 100] 作为答案。 注意,方块 2 擦过方块 1 的右侧边,但不会算作在方块 1 上着陆。 ``` ## 解题思路 ### 思路 1:线段树 + 坐标压缩 这道题目要求模拟方块掉落的过程,记录每次掉落后的最大高度。由于坐标范围很大,需要使用坐标压缩。 1. 使用字典记录每个区间的当前高度。 2. 对于每个掉落的方块 $[left, sideLength]$: - 计算方块覆盖的区间 $[left, right]$,其中 $right = left + sideLength - 1$。 - 查询该区间内的最大高度 $max\_height$。 - 方块掉落后的高度为 $max\_height + sideLength$。 - 更新该区间的高度。 - 记录当前的最大高度。 3. 返回每次掉落后的最大高度列表。 ### 思路 1:代码 ```python class Solution: def fallingSquares(self, positions: List[List[int]]) -> List[int]: # 使用字典记录每个区间的高度 heights = {} result = [] max_height = 0 for left, side_length in positions: right = left + side_length - 1 # 查询 [left, right] 区间内的最大高度 base_height = 0 for (l, r), h in list(heights.items()): # 判断区间是否有重叠 if not (r < left or l > right): base_height = max(base_height, h) # 方块掉落后的高度 new_height = base_height + side_length # 更新区间高度 # 先删除被覆盖的区间 keys_to_remove = [] for (l, r) in list(heights.keys()): if left <= l and r <= right: keys_to_remove.append((l, r)) for key in keys_to_remove: del heights[key] # 添加新的区间 heights[(left, right)] = new_height # 更新最大高度 max_height = max(max_height, new_height) result.append(max_height) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是方块的数量。每次掉落需要遍历所有已有的区间。 - **空间复杂度**:$O(n)$,需要使用字典存储区间高度信息。 ================================================ FILE: docs/solutions/0600-0699/find-duplicate-file-in-system.md ================================================ # [0609. 在系统中查找重复文件](https://leetcode.cn/problems/find-duplicate-file-in-system/) - 标签:数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [0609. 在系统中查找重复文件 - 力扣](https://leetcode.cn/problems/find-duplicate-file-in-system/) ## 题目大意 **描述**: 给定一个目录信息列表 $paths$,包括目录路径,以及该目录中的所有文件及其内容。 一组重复的文件至少包括「两个」具有完全相同内容的文件。 「输入」列表中的单个目录信息字符串的格式如下: - `"root/d1/d2/.../dm f1.txt(f1_content) f2.txt(f2_content) ... fn.txt(fn_content)"` 这意味着,在目录 `root/d1/d2/.../dm` 下,有 $n$ 个文件 ($f1.txt, f2.txt ... fn.txt$) 的内容分别是 ($f1\_content$, $f2\_content$ ... $fn\_content$) 。注意:$n \ge 1$ 且 $m \ge 0$ 。如果 $m = 0$,则表示该目录是根目录。 「输出」是由「重复文件路径组」构成的列表。其中每个组由所有具有相同内容文件的文件路径组成。文件路径是具有下列格式的字符串: - `"directory_path/file_name.txt"` **要求**: 按路径返回文件系统中的所有重复文件。答案可按任意顺序返回。 **说明**: - $1 \le paths.length \le 2 \times 10^{4}$。 - $1 \le paths[i].length \le 3000$。 - $1 \le sum(paths[i].length) \le 5 \times 10^{5}$。 - $paths[i]$ 由英文字母、数字、字符 `'/'`、`'.'`、`'('`、`')'` 和 `' '` 组成。 - 你可以假设在同一目录中没有任何文件或目录共享相同的名称。 - 你可以假设每个给定的目录信息代表一个唯一的目录。目录路径和文件信息用单个空格分隔。 - 进阶: - 假设您有一个真正的文件系统,您将如何搜索文件?广度搜索还是宽度搜索? - 如果文件内容非常大(GB级别),您将如何修改您的解决方案? - 如果每次只能读取 1 kb 的文件,您将如何修改解决方案? - 修改后的解决方案的时间复杂度是多少?其中最耗时的部分和消耗内存的部分是什么?如何优化? - 如何确保您发现的重复文件不是误报? **示例**: - 示例 1: ```python 输入:paths = ["root/a 1.txt(abcd) 2.txt(efgh)","root/c 3.txt(abcd)","root/c/d 4.txt(efgh)","root 4.txt(efgh)"] 输出:[["root/a/2.txt","root/c/d/4.txt","root/4.txt"],["root/a/1.txt","root/c/3.txt"]] ``` - 示例 2: ```python 输入:paths = ["root/a 1.txt(abcd) 2.txt(efgh)","root/c 3.txt(abcd)","root/c/d 4.txt(efgh)"] 输出:[["root/a/2.txt","root/c/d/4.txt"],["root/a/1.txt","root/c/3.txt"]] ``` ## 解题思路 ### 思路 1:哈希表 这道题目要求找出具有相同内容的文件。使用哈希表,以文件内容为键,文件路径列表为值。 1. 创建哈希表 $content\_map$,键为文件内容,值为文件路径列表。 2. 遍历 $paths$ 中的每个目录信息: - 解析目录路径和文件信息。 - 对于每个文件,提取文件名和内容。 - 构造完整的文件路径 $directory\_path/file\_name.txt$。 - 将文件路径添加到对应内容的列表中。 3. 遍历哈希表,将包含多个文件路径的列表加入结果。 4. 返回结果。 ### 思路 1:代码 ```python class Solution: def findDuplicate(self, paths: List[str]) -> List[List[str]]: from collections import defaultdict content_map = defaultdict(list) for path in paths: parts = path.split() directory = parts[0] # 遍历该目录下的所有文件 for i in range(1, len(parts)): file_info = parts[i] # 找到文件名和内容的分隔位置 idx = file_info.index('(') file_name = file_info[:idx] content = file_info[idx + 1:-1] # 去掉括号 # 构造完整路径 full_path = directory + '/' + file_name content_map[content].append(full_path) # 筛选出有重复内容的文件组 result = [] for paths_list in content_map.values(): if len(paths_list) > 1: result.append(paths_list) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是目录信息的数量,$m$ 是每个目录中文件的平均数量。需要遍历所有文件。 - **空间复杂度**:$O(n \times m)$,需要使用哈希表存储所有文件的路径和内容。 ================================================ FILE: docs/solutions/0600-0699/find-duplicate-subtrees.md ================================================ # [0652. 寻找重复的子树](https://leetcode.cn/problems/find-duplicate-subtrees/) - 标签:树、深度优先搜索、哈希表、二叉树 - 难度:中等 ## 题目链接 - [0652. 寻找重复的子树 - 力扣](https://leetcode.cn/problems/find-duplicate-subtrees/) ## 题目大意 给定一个二叉树,返回所有重复的子树。对于重复的子树,只需返回其中任意一棵的根节点。 ## 解题思路 对二叉树进行先序遍历,对遍历的所有的子树进行序列化处理,将序列化处理后的字符串作为哈希表的键,记录每棵子树出现的次数。 当出现第二次时,则说明该子树是重复的子树,将其加入答案数组。最后返回答案数组即可。 ## 代码 ```python class Solution: def findDuplicateSubtrees(self, root: TreeNode) -> List[TreeNode]: tree_dict = dict() res = [] def preorder(node): if not node: return '#' sub_tree = str(node.val) + ',' + preorder(node.left) + ',' + preorder(node.right) if sub_tree in tree_dict: tree_dict[sub_tree] += 1 else: tree_dict[sub_tree] = 1 if tree_dict[sub_tree] == 2: res.append(node) return sub_tree preorder(root) return res ``` ================================================ FILE: docs/solutions/0600-0699/find-k-closest-elements.md ================================================ # [0658. 找到 K 个最接近的元素](https://leetcode.cn/problems/find-k-closest-elements/) - 标签:数组、双指针、二分查找、排序、滑动窗口、堆(优先队列) - 难度:中等 ## 题目链接 - [0658. 找到 K 个最接近的元素 - 力扣](https://leetcode.cn/problems/find-k-closest-elements/) ## 题目大意 **描述**:给定一个有序数组 $arr$,以及两个整数 $k$、$x$。 **要求**:从数组中找到最靠近 $x$(两数之差最小)的 $k$ 个数。返回包含这 $k$ 个数的有序数组。 **说明**: - 整数 $a$ 比整数 $b$ 更接近 $x$ 需要满足: - $|a - x| < |b - x|$ 或者 - $|a - x| == |b - x|$ 且 $a < b$。 - $1 \le k \le arr.length$。 - $1 \le arr.length \le 10^4$。 - $arr$ 按升序排列。 - $-10^4 \le arr[i], x \le 10^4$。 **示例**: - 示例 1: ```python 输入:arr = [1,2,3,4,5], k = 4, x = 3 输出:[1,2,3,4] ``` - 示例 2: ```python 输入:arr = [1,2,3,4,5], k = 4, x = -1 输出:[1,2,3,4] ``` ## 解题思路 ### 思路 1:二分查找算法 数组的区间为 $[0, n-1]$,查找的子区间长度为 $k$。我们可以通过查找子区间左端点位置,从而确定子区间。 查找子区间左端点可以通过二分查找来降低复杂度。 因为子区间为 $k$,所以左端点最多取到 $n - k$ 的位置。 设定两个指针 $left$,$right$。$left$ 指向 $0$,$right$ 指向 $n - k$。 每次取 $left$ 和 $right$ 中间位置,判断 $x$ 与左右边界的差值。$x$ 与左边的差值为 $x - arr[mid]$,$x$ 与右边界的差值为 $arr[mid + k] - x$。 - 如果 $x$ 与左边界的差值大于 $x$ 与右边界的差值,即 $x - arr[mid] > arr[mid + k] - x$,将 $left$ 右移,$left = mid + 1$,从右侧继续查找。 - 如果 $x$ 与左边界的差值小于等于 $x$ 与右边界的差值, 即 $x - arr[mid] \le arr[mid + k] - x$,则将 $right$ 向左侧靠拢,$right = mid$,从左侧继续查找。 最后返回 $arr[left, left + k]$ 即可。 ### 思路 1:代码 ```python class Solution: def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]: n = len(arr) left = 0 right = n - k while left < right: mid = left + (right - left) // 2 if x - arr[mid] > arr[mid + k] - x: left = mid + 1 else: right = mid return arr[left: left + k] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log (n - k) + k)$,其中 $n$ 为数组中的元素个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0600-0699/find-the-derangement-of-an-array.md ================================================ # [0634. 寻找数组的错位排列](https://leetcode.cn/problems/find-the-derangement-of-an-array/) - 标签:数学、动态规划、组合数学 - 难度:中等 ## 题目链接 - [0634. 寻找数组的错位排列 - 力扣](https://leetcode.cn/problems/find-the-derangement-of-an-array/) ## 题目大意 **描述**: 在组合数学中,如果一个排列中所有元素都不在原先的位置上,那么这个排列就被称为 **错位排列**。 给定一个从 $1$ 到 $n$ 升序排列的数组。 **要求**: 返回 **不同的错位排列** 的数量。 由于答案可能非常大,你只需要将答案对 $10^9 + 7$ 取余输出即可。 **说明**: - $1 \le n \le 10^6$。 **示例**: - 示例 1: ```python 输入:n = 3 输出:2 解释:原始的数组为 [1,2,3]。两个错位排列的数组为 [2,3,1] 和 [3,1,2]。 ``` - 示例 2: ```python 输入:n = 2 输出:1 ``` ## 解题思路 ### 思路 1:动态规划 #### 思路 1:算法描述 错位排列(Derangement)是组合数学中的经典问题。设 $D(n)$ 表示 $n$ 个元素的错位排列数量。 **递推公式**: $$D(n) = (n-1) \times [D(n-1) + D(n-2)]$$ **推导思路**: - 考虑第 $n$ 个元素,它不能放在第 $n$ 个位置。 - 假设第 $n$ 个元素放在第 $k$ 个位置($1 \le k \le n-1$)。 - 情况 1:第 $k$ 个元素放在第 $n$ 个位置,剩余 $n-2$ 个元素错位排列,方案数为 $D(n-2)$。 - 情况 2:第 $k$ 个元素不放在第 $n$ 个位置,相当于 $n-1$ 个元素的错位排列,方案数为 $D(n-1)$。 - 第 $n$ 个元素有 $n-1$ 种选择,所以 $D(n) = (n-1) \times [D(n-1) + D(n-2)]$。 **初始条件**: - $D(0) = 1$(空排列) - $D(1) = 0$(1 个元素无法错位) - $D(2) = 1$(只有 $[2, 1]$ 一种) #### 思路 1:代码 ```python class Solution: def findDerangement(self, n: int) -> int: MOD = 10**9 + 7 # 特殊情况 if n == 1: return 0 if n == 2: return 1 # 动态规划 dp_prev2 = 1 # D(0) dp_prev1 = 0 # D(1) for i in range(2, n + 1): dp_curr = ((i - 1) * (dp_prev1 + dp_prev2)) % MOD dp_prev2 = dp_prev1 dp_prev1 = dp_curr return dp_prev1 ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,需要计算从 $2$ 到 $n$ 的所有错位排列数。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0600-0699/image-smoother.md ================================================ # [0661. 图片平滑器](https://leetcode.cn/problems/image-smoother/) - 标签:数组、矩阵 - 难度:简单 ## 题目链接 - [0661. 图片平滑器 - 力扣](https://leetcode.cn/problems/image-smoother/) ## 题目大意 **描述**: 「图像平滑器」是大小为 $3 \times 3$ 的过滤器,用于对图像的每个单元格平滑处理,平滑处理后单元格的值为该单元格的平均灰度。 每个单元格的「平均灰度」定义为:该单元格自身及其周围的 $8$ 个单元格的平均值,结果需向下取整。(即,需要计算蓝色平滑器中 $9$ 个单元格的平均值)。 如果一个单元格周围存在单元格缺失的情况,则计算平均灰度时不考虑缺失的单元格(即,需要计算红色平滑器中 $4$ 个单元格的平均值)。 ![](https://assets.leetcode.com/uploads/2021/05/03/smoother-grid.jpg) 给定一个表示图像灰度的 $m \times n$ 整数矩阵 $img$。 **要求**: 返回对图像的每个单元格平滑处理后的图像。 **说明**: - $m == img.length$。 - $n == img[i].length$。 - $1 \le m, n \le 200$。 - $0 \le img[i][j] \le 255$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/05/03/smooth-grid.jpg) ```python 输入:img = [[1,1,1],[1,0,1],[1,1,1]] 输出:[[0, 0, 0],[0, 0, 0], [0, 0, 0]] 解释: 对于点 (0,0), (0,2), (2,0), (2,2): 平均(3/4) = 平均(0.75) = 0 对于点 (0,1), (1,0), (1,2), (2,1): 平均(5/6) = 平均(0.83333333) = 0 对于点 (1,1): 平均(8/9) = 平均(0.88888889) = 0 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/05/03/smooth2-grid.jpg) ```python 输入: img = [[100,200,100],[200,50,200],[100,200,100]] 输出: [[137,141,137],[141,138,141],[137,141,137]] 解释: 对于点 (0,0), (0,2), (2,0), (2,2): floor((100+200+200+50)/4) = floor(137.5) = 137 对于点 (0,1), (1,0), (1,2), (2,1): floor((200+200+50+200+100+100)/6) = floor(141.666667) = 141 对于点 (1,1): floor((50+200+200+200+200+100+100+100+100)/9) = floor(138.888889) = 138 ``` ## 解题思路 ### 思路 1:矩阵遍历 这道题目要求对图像的每个单元格进行平滑处理,计算该单元格及其周围 8 个单元格的平均值(向下取整)。 1. 创建一个与原图像大小相同的结果矩阵 $result$。 2. 定义 8 个方向的偏移量,用于访问当前单元格周围的 8 个单元格。 3. 遍历图像的每个单元格 $(i, j)$: - 初始化 $sum$ 为当前单元格的值,$count$ 为 1。 - 遍历 8 个方向,检查相邻单元格是否在边界内。 - 如果在边界内,将该单元格的值累加到 $sum$,并将 $count$ 加 1。 - 计算平均值 $sum // count$(向下取整),存入结果矩阵。 4. 返回结果矩阵。 ### 思路 1:代码 ```python class Solution: def imageSmoother(self, img: List[List[int]]) -> List[List[int]]: m, n = len(img), len(img[0]) result = [[0] * n for _ in range(m)] # 8 个方向的偏移量 directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] for i in range(m): for j in range(n): total = img[i][j] # 当前单元格的值 count = 1 # 计数器 # 遍历 8 个方向 for dx, dy in directions: ni, nj = i + dx, j + dy # 检查是否在边界内 if 0 <= ni < m and 0 <= nj < n: total += img[ni][nj] count += 1 # 计算平均值(向下取整) result[i][j] = total // count return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 和 $n$ 分别是图像的行数和列数。需要遍历每个单元格,每个单元格最多检查 8 个相邻单元格。 - **空间复杂度**:$O(m \times n)$,需要创建一个结果矩阵存储平滑后的图像。 ================================================ FILE: docs/solutions/0600-0699/implement-magic-dictionary.md ================================================ # [0676. 实现一个魔法字典](https://leetcode.cn/problems/implement-magic-dictionary/) - 标签:设计、字典树、哈希表、字符串 - 难度:中等 ## 题目链接 - [0676. 实现一个魔法字典 - 力扣](https://leetcode.cn/problems/implement-magic-dictionary/) ## 题目大意 **要求**:设计一个使用单词表进行初始化的数据结构。单词表中的单词互不相同。如果给出一个单词,要求判定能否将该单词中的一个字母替换成另一个字母,是的所形成的新单词已经在够构建的单词表中。 实现 MagicDictionary 类: - `MagicDictionary()` 初始化对象。 - `void buildDict(String[] dictionary)` 使用字符串数组 `dictionary` 设定该数据结构,`dictionary` 中的字符串互不相同。 - `bool search(String searchWord)` 给定一个字符串 `searchWord`,判定能否只将字符串中一个字母换成另一个字母,使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,返回 `True`;否则,返回 `False`。 **说明**: - $1 \le dictionary.length \le 100$。 - $1 \le dictionary[i].length \le 100$。 - `dictionary[i]` 仅由小写英文字母组成。 - `dictionary` 中的所有字符串互不相同。 - $1 \le searchWord.length \le 100$。 - `searchWord` 仅由小写英文字母组成。 - `buildDict` 仅在 `search` 之前调用一次。 - 最多调用 $100$ 次 `search`。 **示例**: - 示例 1: ```python 输入 ["MagicDictionary", "buildDict", "search", "search", "search", "search"] [[], [["hello", "leetcode"]], ["hello"], ["hhllo"], ["hell"], ["leetcoded"]] 输出 [null, null, false, true, false, false] 解释 MagicDictionary magicDictionary = new MagicDictionary(); magicDictionary.buildDict(["hello", "leetcode"]); magicDictionary.search("hello"); // 返回 False magicDictionary.search("hhllo"); // 将第二个 'h' 替换为 'e' 可以匹配 "hello" ,所以返回 True magicDictionary.search("hell"); // 返回 False magicDictionary.search("leetcoded"); // 返回 False ``` ## 解题思路 ### 思路 1:字典树 1. 构造一棵字典树。 2. `buildDict` 方法中将所有单词存入字典树中。 3. `search` 方法中替换 `searchWord` 每一个位置上的字符,然后在字典树中查询。 ### 思路 1:代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None and cur.isEnd class MagicDictionary: def __init__(self): """ Initialize your data structure here. """ self.trie_tree = Trie() def buildDict(self, dictionary: List[str]) -> None: for word in dictionary: self.trie_tree.insert(word) def search(self, searchWord: str) -> bool: size = len(searchWord) for i in range(size): for j in range(26): new_ch = chr(ord('a') + j) if searchWord[i] != new_ch: new_word = searchWord[:i] + new_ch + searchWord[i + 1:] if self.trie_tree.search(new_word): return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:初始化操作是 $O(1)$。构建操作是 $O(|dictionary|)$,搜索操作是 $O(|searchWord| \times |\sum|)$。其中 $|dictionary|$ 是字符串数组 `dictionary` 中的字符个数,$|searchWord|$ 是查询操作中字符串的长度,$|\sum|$ 是字符集的大小。 - **空间复杂度**:$O(|dicitonary|)$。 ================================================ FILE: docs/solutions/0600-0699/index.md ================================================ ## 本章内容 - [0600. 不含连续1的非负整数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/non-negative-integers-without-consecutive-ones.md) - [0604. 迭代压缩字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-compressed-string-iterator.md) - [0605. 种花问题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/can-place-flowers.md) - [0606. 根据二叉树创建字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/construct-string-from-binary-tree.md) - [0609. 在系统中查找重复文件](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/find-duplicate-file-in-system.md) - [0611. 有效三角形的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/valid-triangle-number.md) - [0616. 给字符串添加加粗标签](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/add-bold-tag-in-string.md) - [0617. 合并二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/merge-two-binary-trees.md) - [0621. 任务调度器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/task-scheduler.md) - [0622. 设计循环队列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-circular-queue.md) - [0623. 在二叉树中增加一行](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/add-one-row-to-tree.md) - [0624. 数组列表中的最大距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-distance-in-arrays.md) - [0625. 最小因式分解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/minimum-factorization.md) - [0628. 三个数的最大乘积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-product-of-three-numbers.md) - [0629. K 个逆序对数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/k-inverse-pairs-array.md) - [0630. 课程表 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/course-schedule-iii.md) - [0631. 设计 Excel 求和公式](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-excel-sum-formula.md) - [0632. 最小区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/smallest-range-covering-elements-from-k-lists.md) - [0633. 平方数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/sum-of-square-numbers.md) - [0634. 寻找数组的错位排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/find-the-derangement-of-an-array.md) - [0635. 设计日志存储系统](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-log-storage-system.md) - [0636. 函数的独占时间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/exclusive-time-of-functions.md) - [0637. 二叉树的层平均值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/average-of-levels-in-binary-tree.md) - [0638. 大礼包](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/shopping-offers.md) - [0639. 解码方法 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/decode-ways-ii.md) - [0640. 求解方程](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/solve-the-equation.md) - [0641. 设计循环双端队列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-circular-deque.md) - [0642. 设计搜索自动补全系统](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/design-search-autocomplete-system.md) - [0643. 子数组最大平均数 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-average-subarray-i.md) - [0644. 子数组最大平均数 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-average-subarray-ii.md) - [0645. 错误的集合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/set-mismatch.md) - [0646. 最长数对链](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-length-of-pair-chain.md) - [0647. 回文子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/palindromic-substrings.md) - [0648. 单词替换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/replace-words.md) - [0649. Dota2 参议院](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/dota2-senate.md) - [0650. 两个键的键盘](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/2-keys-keyboard.md) - [0651. 四个键的键盘](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/4-keys-keyboard.md) - [0652. 寻找重复的子树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/find-duplicate-subtrees.md) - [0653. 两数之和 IV - 输入二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/two-sum-iv-input-is-a-bst.md) - [0654. 最大二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-binary-tree.md) - [0655. 输出二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/print-binary-tree.md) - [0656. 成本最小路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/coin-path.md) - [0657. 机器人能否返回原点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/robot-return-to-origin.md) - [0658. 找到 K 个最接近的元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/find-k-closest-elements.md) - [0659. 分割数组为连续子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/split-array-into-consecutive-subsequences.md) - [0660. 移除 9](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/remove-9.md) - [0661. 图片平滑器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/image-smoother.md) - [0662. 二叉树最大宽度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-width-of-binary-tree.md) - [0663. 均匀树划分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/equal-tree-partition.md) - [0664. 奇怪的打印机](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/strange-printer.md) - [0665. 非递减数列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/non-decreasing-array.md) - [0666. 路径总和 IV](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/path-sum-iv.md) - [0667. 优美的排列 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/beautiful-arrangement-ii.md) - [0669. 修剪二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/trim-a-binary-search-tree.md) - [0670. 最大交换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-swap.md) - [0671. 二叉树中第二小的节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/second-minimum-node-in-a-binary-tree.md) - [0672. 灯泡开关 Ⅱ](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/bulb-switcher-ii.md) - [0673. 最长递增子序列的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/number-of-longest-increasing-subsequence.md) - [0674. 最长连续递增序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/longest-continuous-increasing-subsequence.md) - [0675. 为高尔夫比赛砍树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/cut-off-trees-for-golf-event.md) - [0676. 实现一个魔法字典](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/implement-magic-dictionary.md) - [0677. 键值映射](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/map-sum-pairs.md) - [0678. 有效的括号字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/valid-parenthesis-string.md) - [0679. 24 点游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/24-game.md) - [0680. 验证回文串 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/valid-palindrome-ii.md) - [0681. 最近时刻](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/next-closest-time.md) - [0682. 棒球比赛](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/baseball-game.md) - [0683. K 个关闭的灯泡](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/k-empty-slots.md) - [0684. 冗余连接](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/redundant-connection.md) - [0685. 冗余连接 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/redundant-connection-ii.md) - [0686. 重复叠加字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/repeated-string-match.md) - [0687. 最长同值路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/longest-univalue-path.md) - [0688. 骑士在棋盘上的概率](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/knight-probability-in-chessboard.md) - [0689. 三个无重叠子数组的最大和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/maximum-sum-of-3-non-overlapping-subarrays.md) - [0690. 员工的重要性](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/employee-importance.md) - [0691. 贴纸拼词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/stickers-to-spell-word.md) - [0692. 前K个高频单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/top-k-frequent-words.md) - [0693. 交替位二进制数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/binary-number-with-alternating-bits.md) - [0694. 不同岛屿的数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/number-of-distinct-islands.md) - [0695. 岛屿的最大面积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/max-area-of-island.md) - [0696. 计数二进制子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/count-binary-substrings.md) - [0697. 数组的度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/degree-of-an-array.md) - [0698. 划分为k个相等的子集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/partition-to-k-equal-sum-subsets.md) - [0699. 掉落的方块](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/falling-squares.md) ================================================ FILE: docs/solutions/0600-0699/k-empty-slots.md ================================================ # [0683. K 个关闭的灯泡](https://leetcode.cn/problems/k-empty-slots/) - 标签:树状数组、数组、有序集合、滑动窗口 - 难度:困难 ## 题目链接 - [0683. K 个关闭的灯泡 - 力扣](https://leetcode.cn/problems/k-empty-slots/) ## 题目大意 **描述**:$n$ 个灯泡排成一行,编号从 $1$ 到 $n$。最初,所有灯泡都关闭。每天只打开一个灯泡,直到 $n$ 天后所有灯泡都打开。 给定一个长度为 $n$ 的灯泡数组 $blubs$,其中 `bulls[i] = x` 意味着在第 $i + 1$ 天,我们会把在位置 $x$ 的灯泡打开,其中 $i$ 从 $0$ 开始,$x$ 从 $1$ 开始。 再给定一个整数 $k$。 **要求**:输出在第几天恰好有两个打开的灯泡,使得它们中间正好有 $k$ 个灯泡且这些灯泡全部是关闭的 。如果不存在这种情况,则返回 $-1$。如果有多天都出现这种情况,请返回最小的天数 。 **说明**: - $n == bulbs.length$。 - $1 \le n \le 2 \times 10^4$。 - $1 \le bulbs[i] \le n$。 - $bulbs$ 是一个由从 $1$ 到 $n$ 的数字构成的排列。 - $0 \le k \le 2 \times 10^4$。 **示例**: - 示例 1: ```python 输入: bulbs = [1,3,2],k = 1 输出:2 解释: 第一天 bulbs[0] = 1,打开第一个灯泡 [1,0,0] 第二天 bulbs[1] = 3,打开第三个灯泡 [1,0,1] 第三天 bulbs[2] = 2,打开第二个灯泡 [1,1,1] 返回2,因为在第二天,两个打开的灯泡之间恰好有一个关闭的灯泡。 ``` - 示例 2: ```python 输入:bulbs = [1,2,3],k = 1 输出:-1 ``` ## 解题思路 ### 思路 1:滑动窗口 $blubs[i]$ 记录的是第 $i + 1$ 天开灯的位置。我们将其转换一下,使用另一个数组 $days$ 来存储每个灯泡的开灯时间,其中 $days[i]$ 表示第 $i$ 个位置上的灯泡的开灯时间。 - 使用 $ans$ 记录最小满足条件的天数。维护一个窗口 $left$、$right$。其中 `right = left + k + 1`。使得区间 $(left, right)$ 中所有灯泡(总共为 $k$ 个)开灯时间都晚于 $days[left]$ 和 $days[right]$。 - 对于区间 $[left, right]$,$left < i < right$: - 如果出现 $days[i] < days[left]$ 或者 $days[i] < days[right]$,说明不符合要求。将 $left$、$right$ 移动到 $[i, i + k + 1]$,继续进行判断。 - 如果对于 $left < i < right$ 中所有的 $i$,都满足 $days[i] \ge days[left]$ 并且 $days[i] \ge days[right]$,说明此时满足要求。将当前答案与 $days[left]$ 和 $days[right]$ 中的较大值作比较。如果比当前答案更小,则更新答案。同时将窗口向右移动 $k $位。继续检测新的不相交间隔 $[right, right + k + 1]$。 - 注意:之所以检测新的不相交间隔,是因为如果检测的是相交间隔,原来的 $right$ 位置元素仍在区间中,肯定会出现 $days[right] < days[right_new]$,不满足要求。所以此时相交的区间可以直接跳过,直接检测不相交的间隔。 - 直到 $right \ge len(days)$ 时跳出循环,判断是否有符合要求的答案,并返回答案 $ans$。 ### 思路 1:代码 ```python class Solution: def kEmptySlots(self, bulbs: List[int], k: int) -> int: size = len(bulbs) days = [0 for _ in range(size)] for i in range(size): days[bulbs[i] - 1] = i + 1 left, right = 0, k + 1 ans = float('inf') while right < size: check_flag = True for i in range(left + 1, right): if days[i] < days[left] or days[i] < days[right]: left, right = i, i + k + 1 check_flag = False break if check_flag: ans = min(ans, max(days[left], days[right])) left, right = right, right + k + 1 if ans != float('inf'): return ans else: return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $bulbs$ 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0600-0699/k-inverse-pairs-array.md ================================================ # [0629. K 个逆序对数组](https://leetcode.cn/problems/k-inverse-pairs-array/) - 标签:动态规划 - 难度:困难 ## 题目链接 - [0629. K 个逆序对数组 - 力扣](https://leetcode.cn/problems/k-inverse-pairs-array/) ## 题目大意 **描述**: 对于一个整数数组 $nums$,逆序对是一对满足 $0 \le i < j < nums$.length$ 且 $nums[i] > nums[j]$ 的整数对 $[i, j]$。 给定两个整数 $n$ 和 $k$。 **要求**: 找出所有包含从 $1$ 到 $n$ 的数字,且恰好拥有 $k$ 个「逆序对」的不同的数组的个数。由于答案可能很大,只需要返回对 $10^9 + 7$ 取余的结果。 **说明**: - $1 \le n \le 10^{3}$。 - $0 \le k \le 10^{3}$。 **示例**: - 示例 1: ```python 输入:n = 3, k = 0 输出:1 解释: 只有数组 [1,2,3] 包含了从1到3的整数并且正好拥有 0 个逆序对。 ``` - 示例 2: ```python 输入:n = 3, k = 1 输出:2 解释: 数组 [1,3,2] 和 [2,1,3] 都有 1 个逆序对。 ``` ## 解题思路 ### 思路 1:动态规划 这道题目要求计算恰好有 $k$ 个逆序对的排列数量。使用动态规划求解。 定义 $dp[i][j]$ 表示使用数字 $1$ 到 $i$ 组成的排列中,恰好有 $j$ 个逆序对的排列数量。 状态转移: - 当我们在前 $i - 1$ 个数字的排列中插入数字 $i$ 时,可以插入到任意位置。 - 如果插入到最后,不产生新的逆序对。 - 如果插入到倒数第二个位置,产生 1 个新的逆序对。 - 如果插入到第一个位置,产生 $i - 1$ 个新的逆序对。 - 因此:$dp[i][j] = \sum_{t=0}^{\min(j, i-1)} dp[i-1][j-t]$ 优化:使用前缀和优化,避免重复计算。 ### 思路 1:代码 ```python class Solution: def kInversePairs(self, n: int, k: int) -> int: MOD = 10**9 + 7 # dp[i][j] 表示使用 1 到 i 的数字,恰好有 j 个逆序对的排列数 dp = [[0] * (k + 1) for _ in range(n + 1)] # 初始化:1 个数字,0 个逆序对 dp[0][0] = 1 for i in range(1, n + 1): dp[i][0] = 1 # 0 个逆序对只有一种排列(升序) for j in range(1, k + 1): # dp[i][j] = sum(dp[i-1][j-t]) for t in range(min(j, i-1) + 1) # 使用前缀和优化 dp[i][j] = (dp[i][j - 1] + dp[i - 1][j]) % MOD # 减去超出范围的部分 if j >= i: dp[i][j] = (dp[i][j] - dp[i - 1][j - i]) % MOD return dp[n][k] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times k)$,需要填充 $n \times k$ 的动态规划表。 - **空间复杂度**:$O(n \times k)$,需要使用二维数组存储动态规划状态。可以优化到 $O(k)$。 ================================================ FILE: docs/solutions/0600-0699/knight-probability-in-chessboard.md ================================================ # [0688. 骑士在棋盘上的概率](https://leetcode.cn/problems/knight-probability-in-chessboard/) - 标签:动态规划 - 难度:中等 ## 题目链接 - [0688. 骑士在棋盘上的概率 - 力扣](https://leetcode.cn/problems/knight-probability-in-chessboard/) ## 题目大意 **描述**:在一个 `n * n` 的国际象棋棋盘上,一个骑士从单元格 `(row, column)` 开始,尝试进行 `k` 次 移动。行和列是从 `0` 开始的,左上角的单元格是 `(0, 0)`,右下角的单元格是 `(n - 1, n - 1)`。 象棋骑士有 `8` 种可能的走法,如下图所示。每次移动在基本方向上是两个单元格,然后在正交方向上是一个单元格。 ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/10/12/knight.png) 每次骑士要移动时,它都会随机从 `8` 种可能的移动中选择一种(即使棋子会离开棋盘),然后移动到那里。骑士继续移动,直到它走了 `k` 步或离开了棋盘。 现在给定代表棋盘大小的整数 `n`、代表骑士移动次数的整数 `k`,以及代表骑士初始位置的坐标 `row` 和 `column`。 **要求**:返回骑士在棋盘停止移动后仍留在棋盘上的概率。 **说明**: - $1 \le n \le 25$。 - $0 \le k \le 100$。 - $0 \le row, column \le n$。 **示例**: - 示例 1: ```python 输入:n = 3, k = 2, row = 0, column = 0 输出:0.0625 解释:有两步(到(1,2),(2,1))可以让骑士留在棋盘上。在每一个位置上,也有两种移动可以让骑士留在棋盘上。骑士留在棋盘上的总概率是 0.0625。 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照骑士所在位置和所走步数进行阶段划分。 ###### 2. 定义状态 定义状态 `dp[i][j][p]` 表示为:从位置 `(i, j)` 出发,移动不超过 `p` 步的情况下,最后仍留在棋盘内的概率。 ###### 3. 状态转移方程 根据象棋骑士的 `8` 种可能的走法,`dp[i][j][p]` 的来源有八个方向(超出棋盘的无需再考虑): - 假设下一步的落点为 `(new_i, new_j)`。从当前步选择 `8` 个方向其中之一作为下一步方向的概率为 $\frac{1}{8}$。 - 而每个方向上落点仍在棋盘内的概率为 `dp[new_i][new_j][p - 1]`。所以从 `(i, j)` 走到 `(new_i, new_j)` 的可能性为 $dp[new_i][new_j] \times \frac{1}{8}$。 最终 $dp[i][j][p]$ 来源为 `8` 个方向上落点的概率之和,即:$dp[i][j][p] = \sum{ dp[new_i][new_j] \times \frac{1}{8} }$。 ###### 4. 初始条件 - 从位置 `(i, j)` 出发,移动不超过 `0` 步的情况下,最后仍留在棋盘内的概率为 `1`。 ###### 5. 最终结果 根据我们之前定义的状态,`dp[i][j][p]` 表示为:从位置 `(i, j)` 出发,移动不超过 `p` 步的情况下,最后仍留在棋盘内的概率。则最终结果为 `dp[row][column][k]`。 ### 思路 1:动态规划代码 ```python class Solution: def knightProbability(self, n: int, k: int, row: int, column: int) -> float: dp = [[[0 for _ in range(k + 1)] for _ in range(n)] for _ in range(n)] for i in range(n): for j in range(n): dp[i][j][0] = 1 directions = {(-1, -2), (-1, 2), (1, -2), (1, 2), (-2, -1), (-2, 1), (2, -1), (2, 1)} for p in range(1, k + 1): for i in range(n): for j in range(n): for direction in directions: new_i = i + direction[0] new_j = j + direction[1] if 0 <= new_i < n and 0 <= new_j < n: dp[i][j][p] += dp[new_i][new_j][p - 1] / 8 return dp[row][column][k] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 * k)$。外三层循环的时间复杂度为 $O(n^2 * k)$,内层关于 `directions` 的循环每次执行 `8` 次,可以看做是常数级时间复杂度。 - **空间复杂度**:$O(n^2 * k)$。用到了三维数组保存状态。 ================================================ FILE: docs/solutions/0600-0699/longest-continuous-increasing-subsequence.md ================================================ # [0674. 最长连续递增序列](https://leetcode.cn/problems/longest-continuous-increasing-subsequence/) - 标签:数组 - 难度:简单 ## 题目链接 - [0674. 最长连续递增序列 - 力扣](https://leetcode.cn/problems/longest-continuous-increasing-subsequence/) ## 题目大意 **描述**:给定一个未经排序的数组 $nums$。 **要求**:找到最长且连续递增的子序列,并返回该序列的长度。 **说明**: - **连续递增的子序列**:可以由两个下标 $l$ 和 $r$($l < r$)确定,如果对于每个 $l \le i < r$,都有 $nums[i] < nums[i + 1] $,那么子序列 $[nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]$ 就是连续递增子序列。 - $1 \le nums.length \le 10^4$。 - $-10^9 \le nums[i] \le 10^9$。 **示例**: - 示例 1: ```python 输入:nums = [1,3,5,4,7] 输出:3 解释:最长连续递增序列是 [1,3,5], 长度为 3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。 ``` - 示例 2: ```python 输入:nums = [2,2,2,2,2] 输出:1 解释:最长连续递增序列是 [2], 长度为 1。 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 定义状态 定义状态 $dp[i]$ 表示为:以 $nums[i]$ 结尾的最长且连续递增的子序列长度。 ###### 2. 状态转移方程 因为求解的是连续子序列,所以只需要考察相邻元素的状态转移方程。 如果一个较小的数右侧相邻元素为一个较大的数,则会形成一个更长的递增子序列。 对于相邻的数组元素 $nums[i - 1]$ 和 $nums[i]$ 来说: - 如果 $nums[i - 1] < nums[i]$,则 $nums[i]$ 可以接在 $nums[i - 1]$ 后面,此时以 $nums[i]$ 结尾的最长递增子序列长度会在「以 $nums[i - 1]$ 结尾的最长递增子序列长度」的基础上加 $1$,即 $dp[i] = dp[i - 1] + 1$。 - 如果 $nums[i - 1] >= nums[i]$,则 $nums[i]$ 不可以接在 $nums[i - 1]$ 后面,可以直接跳过。 综上,我们的状态转移方程为:$dp[i] = dp[i - 1] + 1$,$nums[i - 1] < nums[i]$。 ###### 3. 初始条件 默认状态下,把数组中的每个元素都作为长度为 $1$ 的最长且连续递增的子序列长度。即 $dp[i] = 1$。 ###### 4. 最终结果 根据我们之前定义的状态,$dp[i]$ 表示为:以 $nums[i]$ 结尾的最长且连续递增的子序列长度。则为了计算出最大值,则需要再遍历一遍 $dp$ 数组,求出最大值即为最终结果。 ### 思路 1:动态规划代码 ```python class Solution: def findLengthOfLCIS(self, nums: List[int]) -> int: size = len(nums) dp = [1 for _ in range(size)] for i in range(1, size): if nums[i - 1] < nums[i]: dp[i] = dp[i - 1] + 1 return max(dp) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$,最后求最大值的时间复杂度是 $O(n)$,所以总体时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 ### 思路 2:滑动窗口(不定长度) 1. 设定两个指针:$left$、$right$,分别指向滑动窗口的左右边界,保证窗口内为连续递增序列。使用 $window\_len$ 存储当前窗口大小,使用 $max\_len$ 维护最大窗口长度。 2. 一开始,$left$、$right$ 都指向 $0$。 3. 将最右侧元素 $nums[right]$ 加入当前连续递增序列中,即当前窗口长度加 $1$(`window_len += 1`)。 4. 判断当前元素 $nums[right]$ 是否满足连续递增序列。 5. 如果 $right > 0$ 并且 $nums[right - 1] \ge nums[right]$ ,说明不满足连续递增序列,则将 $left$ 移动到窗口最右侧,重置当前窗口长度为 $1$(`window_len = 1`)。 6. 记录当前连续递增序列的长度,并更新最长连续递增序列的长度。 7. 继续右移 $right$,直到 $right \ge len(nums)$ 结束。 8. 输出最长连续递增序列的长度 $max\_len$。 ### 思路 2:代码 ```python class Solution: def findLengthOfLCIS(self, nums: List[int]) -> int: size = len(nums) left, right = 0, 0 window_len = 0 max_len = 0 while right < size: window_len += 1 if right > 0 and nums[right - 1] >= nums[right]: left = right window_len = 1 max_len = max(max_len, window_len) right += 1 return max_len ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0600-0699/longest-univalue-path.md ================================================ # [0687. 最长同值路径](https://leetcode.cn/problems/longest-univalue-path/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0687. 最长同值路径 - 力扣](https://leetcode.cn/problems/longest-univalue-path/) ## 题目大意 **描述**:给定一个二叉树的根节点 $root$。 **要求**:返回二叉树中最长的路径的长度,该路径中每个节点具有相同值。 这条路径可以经过也可以不经过根节点。 **说明**: - 树的节点数的范围是 $[0, 10^4]$。 - $-1000 \le Node.val \le 1000$。 - 树的深度将不超过 $1000$。 - 两个节点之间的路径长度:由它们之间的边数表示。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/13/ex1.jpg) ```python 输入:root = [5,4,5,1,1,5] 输出:2 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/10/13/ex2.jpg) ```python 输入:root = [1,4,5,4,4,5] 输出:2 ``` ## 解题思路 ### 思路 1:树形 DP + 深度优先搜索 这道题如果先不考虑「路径中每个节点具有相同值」这个条件,那么这道题就是在求「二叉树的直径长度(最长路径的长度)」。 「二叉树的直径长度」的定义为:二叉树中任意两个节点路径长度中的最大值。并且这条路径可能穿过也可能不穿过根节点。 对于根为 $root$ 的二叉树来说,其直径长度并不简单等于「左子树高度」加上「右子树高度」。 根据路径是否穿过根节点,我们可以将二叉树分为两种: 1. 直径长度所对应的路径穿过根节点,这种情况下:$\text{二叉树的直径} = \text{左子树高度} + \text{右子树高度}$。 2. 直径长度所对应的路径不穿过根节点,这种情况下:$\text{二叉树的直径} = \text{所有子树中最大直径长度}$。 也就是说根为 $root$ 的二叉树的直径长度可能来自于 $\text{左子树高度} + \text{右子树高度}$,也可能来自于 $\text{子树中的最大直径}$,即 $\text{二叉树的直径} = max(\text{左子树高度} + \text{右子树高度}, \quad \text{所有子树中最大直径长度})$。 那么现在问题就变成为如何求「子树的高度」和「子树中的最大直径」。 1. 子树的高度:我们可以利用深度优先搜索方法,递归遍历左右子树,并分别返回左右子树的高度。 2. 子树中的最大直径:我们可以在递归求解子树高度的时候维护一个 $ans$ 变量,用于记录所有 $\text{左子树高度} + \text{右子树高度}$ 中的最大值。 最终 $ans$ 就是我们所求的该二叉树的最大直径。 接下来我们再来加上「路径中每个节点具有相同值」这个限制条件。 1. 「左子树高度」应变为「左子树最长同值路径长度」。 2. 「右子树高度」应变为「右子树最长同值路径长度」。 3. 题目变为求「二叉树的最长同值路径长度」,式子为:$\text{二叉树的最长同值路径长度} = max(\text{左子树最长同值路径长度} + \text{右子树最长同值路径长度}, \quad \text{所有子树中最长同值路径长度})$。 在递归遍历的时候,我们还需要当前节点与左右子节点的值的相同情况,来维护更新「包含当前节点的最长同值路径长度」。 1. 在递归遍历左子树时,如果当前节点与左子树的值相同,则:$\text{包含当前节点向左的最长同值路径长度} = \text{左子树最长同值路径长度} + 1$,否则为 $0$。 2. 在递归遍历左子树时,如果当前节点与左子树的值相同,则:$\text{包含当前节点向右的最长同值路径长度} = \text{右子树最长同值路径长度} + 1$,否则为 $0$。 则:$\text{包含当前节点向左的最长同值路径长度} = max(\text{包含当前节点向左的最长同值路径长度}, \quad \text{包含当前节点向右的最长同值路径长度})$。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def __init__(self): self.ans = 0 def dfs(self, node): if not node: return 0 left_len = self.dfs(node.left) # 左子树高度 right_len = self.dfs(node.right) # 右子树高度 if node.left and node.left.val == node.val: left_len += 1 else: left_len = 0 if node.right and node.right.val == node.val: right_len += 1 else: right_len = 0 self.ans = max(self.ans, left_len + right_len) return max(left_len, right_len) def longestUnivaluePath(self, root: Optional[TreeNode]) -> int: self.dfs(root) return self.ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为二叉树的节点个数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0600-0699/map-sum-pairs.md ================================================ # [0677. 键值映射](https://leetcode.cn/problems/map-sum-pairs/) - 标签:设计、字典树、哈希表、字符串 - 难度:中等 ## 题目链接 - [0677. 键值映射 - 力扣](https://leetcode.cn/problems/map-sum-pairs/) ## 题目大意 **要求**:实现一个 MapSum 类,支持两个方法,`insert` 和 `sum`: - `MapSum()` 初始化 MapSum 对象。 - `void insert(String key, int val)` 插入 `key-val` 键值对,字符串表示键 `key`,整数表示值 `val`。如果键 `key` 已经存在,那么原来的键值对将被替代成新的键值对。 - `int sum(string prefix)` 返回所有以该前缀 `prefix` 开头的键 `key` 的值的总和。 **说明**: - $1 \le key.length, prefix.length \le 50$。 - `key` 和 `prefix` 仅由小写英文字母组成。 - $1 \le val \le 1000$。 - 最多调用 $50$ 次 `insert` 和 `sum`。 **示例**: - 示例 1: ```python 输入: ["MapSum", "insert", "sum", "insert", "sum"] [[], ["apple", 3], ["ap"], ["app", 2], ["ap"]] 输出: [null, null, 3, null, 5] 解释: MapSum mapSum = new MapSum(); mapSum.insert("apple", 3); mapSum.sum("ap"); // 返回 3 (apple = 3) mapSum.insert("app", 2); mapSum.sum("ap"); // 返回 5 (apple + app = 3 + 2 = 5) ``` ## 解题思路 ### 思路 1:字典树 可以构造前缀树(字典树)解题。 - 初始化时,构建一棵前缀树(字典树),并增加 `val` 变量。 - 调用插入方法时,用字典树存储 `key`,并在对应字母节点存储对应的 `val`。 - 在调用查询总和方法时,先查找该前缀 `prefix` 对应的前缀树节点,从该节点开始,递归遍历该节点的子节点,并累积子节点的 `val`,进行求和,并返回求和累加结果。 ### 思路 1:代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False self.value = 0 def insert(self, word: str, value: int) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True cur.value = value def search(self, word: str) -> int: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return 0 cur = cur.children[ch] return self.dfs(cur) def dfs(self, root) -> int: if not root: return 0 res = root.value for node in root.children.values(): res += self.dfs(node) return res class MapSum: def __init__(self): """ Initialize your data structure here. """ self.trie_tree = Trie() def insert(self, key: str, val: int) -> None: self.trie_tree.insert(key, val) def sum(self, prefix: str) -> int: return self.trie_tree.search(prefix) ``` ### 思路 1:复杂度分析 - **时间复杂度**:`insert` 操作的时间复杂度为 $O(|key|)$。其中 $|key|$ 是每次插入字符串 `key` 的长度。`sum` 操作的时间复杂度是 $O(|prefix|)$,其中 $O(| prefix |)$ 是查询字符串 `prefix` 的长度。 - **空间复杂度**:$O(|T| \times m)$。其中 $|T|$ 表示字符串 `key` 的最大长度,$m$ 表示 `key - val` 的键值数目。 ================================================ FILE: docs/solutions/0600-0699/max-area-of-island.md ================================================ # [0695. 岛屿的最大面积](https://leetcode.cn/problems/max-area-of-island/) - 标签:深度优先搜索、广度优先搜索、并查集、数组、矩阵 - 难度:中等 ## 题目链接 - [0695. 岛屿的最大面积 - 力扣](https://leetcode.cn/problems/max-area-of-island/) ## 题目大意 **描述**:给定一个只包含 $0$、$1$ 元素的二维数组,$1$ 代表岛屿,$0$ 代表水。一座岛的面积就是上下左右相邻的 $1$ 所组成的连通块的数目。 **要求**:计算出最大的岛屿面积。 **说明**: - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 50$。 - $grid[i][j]$ 为 $0$ 或 $1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/05/01/maxarea1-grid.jpg) ```python 输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]] 输出:6 解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。 ``` - 示例 2: ```python 输入:grid = [[0,0,0,0,0,0,0,0]] 输出:0 ``` ## 解题思路 ### 思路 1:深度优先搜索 1. 遍历二维数组的每一个元素,对于每个值为 $1$ 的元素: 1. 将该位置上的值置为 $0$(防止二次重复计算)。 2. 递归搜索该位置上下左右四个位置,并统计搜到值为 $1$ 的元素个数。 3. 返回值为 $1$ 的元素个数(即为该岛的面积)。 2. 维护并更新最大的岛面积。 3. 返回最大的到面积。 ### 思路 1:代码 ```python class Solution: def dfs(self, grid, i, j): n = len(grid) m = len(grid[0]) if i < 0 or i >= n or j < 0 or j >= m or grid[i][j] == 0: return 0 ans = 1 grid[i][j] = 0 ans += self.dfs(grid, i + 1, j) ans += self.dfs(grid, i, j + 1) ans += self.dfs(grid, i - 1, j) ans += self.dfs(grid, i, j - 1) return ans def maxAreaOfIsland(self, grid: List[List[int]]) -> int: ans = 0 for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == 1: ans = max(ans, self.dfs(grid, i, j)) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $m$ 和 $n$ 分别为行数和列数。 - **空间复杂度**:$O(n \times m)$。 ### 思路 2:广度优先搜索 1. 使用 $ans$ 记录最大岛屿面积。 2. 遍历二维数组的每一个元素,对于每个值为 $1$ 的元素: 1. 将该元素置为 $0$。并使用队列 $queue$ 存储该节点位置。使用 $temp\_ans$ 记录当前岛屿面积。 2. 然后从队列 $queue$ 中取出第一个节点位置 $(i, j)$。遍历该节点位置上、下、左、右四个方向上的相邻节点。并将其置为 $0$(避免重复搜索)。并将其加入到队列中。并累加当前岛屿面积,即 `temp_ans += 1`。 3. 不断重复上一步骤,直到队列 $queue$ 为空。 4. 更新当前最大岛屿面积,即 `ans = max(ans, temp_ans)`。 3. 将 $ans$ 作为答案返回。 ### 思路 2:代码 ```python import collections class Solution: def maxAreaOfIsland(self, grid: List[List[int]]) -> int: directs = [(0, 1), (0, -1), (1, 0), (-1, 0)] rows, cols = len(grid), len(grid[0]) ans = 0 for i in range(rows): for j in range(cols): if grid[i][j] == 1: grid[i][j] = 0 temp_ans = 1 q = collections.deque([(i, j)]) while q: i, j = q.popleft() for direct in directs: new_i = i + direct[0] new_j = j + direct[1] if new_i < 0 or new_i >= rows or new_j < 0 or new_j >= cols or grid[new_i][new_j] == 0: continue grid[new_i][new_j] = 0 q.append((new_i, new_j)) temp_ans += 1 ans = max(ans, temp_ans) return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $m$ 和 $n$ 分别为行数和列数。 - **空间复杂度**:$O(n \times m)$。 ================================================ FILE: docs/solutions/0600-0699/maximum-average-subarray-i.md ================================================ # [0643. 子数组最大平均数 I](https://leetcode.cn/problems/maximum-average-subarray-i/) - 标签:数组、滑动窗口 - 难度:简单 ## 题目链接 - [0643. 子数组最大平均数 I - 力扣](https://leetcode.cn/problems/maximum-average-subarray-i/) ## 题目大意 **描述**:给定一个由 $n$ 个元素组成的整数数组 $nums$ 和一个整数 $k$。 **要求**:找出平均数最大且长度为 $k$ 的连续子数组,并输出该最大平均数。 **说明**: - 任何误差小于 $10^{-5}$ 的答案都将被视为正确答案。 - $n == nums.length$。 - $1 \le k \le n \le 10^5$。 - $-10^4 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [1,12,-5,-6,50,3], k = 4 输出:12.75 解释:最大平均数 (12-5-6+50)/4 = 51/4 = 12.75 ``` - 示例 2: ```python 输入:nums = [5], k = 1 输出:5.00000 ``` ## 解题思路 ### 思路 1:滑动窗口(固定长度) 这道题目是典型的固定窗口大小的滑动窗口题目。窗口大小为 $k$。具体做法如下: 1. $ans$ 用来维护子数组最大平均数,初始值为负无穷,即 `float('-inf')`。$window\_total$ 用来维护窗口中元素的和。 2. $left$ 、$right$ 都指向序列的第一个元素,即:`left = 0`,`right = 0`。 3. 向右移动 $right$,先将 $k$ 个元素填入窗口中。 4. 当窗口元素个数为 $k$ 时,即:$right - left + 1 >= k$ 时,计算窗口内的元素和平均值,并维护子数组最大平均数。 5. 然后向右移动 $left$,从而缩小窗口长度,即 `left += 1`,使得窗口大小始终保持为 $k$。 6. 重复 $4 \sim 5$ 步,直到 $right$ 到达数组末尾。 7. 最后输出答案 $ans$。 ### 思路 1:代码 ```python class Solution: def findMaxAverage(self, nums: List[int], k: int) -> float: left = 0 right = 0 window_total = 0 ans = float('-inf') while right < len(nums): window_total += nums[right] if right - left + 1 >= k: ans = max(window_total / k, ans) window_total -= nums[left] left += 1 # 向右侧增大窗口 right += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 为数组 $nums$ 的元素个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0600-0699/maximum-average-subarray-ii.md ================================================ # [0644. 子数组最大平均数 II](https://leetcode.cn/problems/maximum-average-subarray-ii/) - 标签:数组、二分查找、前缀和 - 难度:困难 ## 题目链接 - [0644. 子数组最大平均数 II - 力扣](https://leetcode.cn/problems/maximum-average-subarray-ii/) ## 题目大意 **描述**: 给你一个包含 $n$ 个整数的数组 $nums$,和一个整数 $k$。 **要求**: 找出 **长度大于等于** $k$ 且含最大平均值的连续子数组。并输出这个最大平均值。任何计算误差小于 $10^{-5}$ 的结果都将被视为正确答案。 **说明**: - $n == nums.length$。 - $1 \le k \le n \le 10^4$。 - $-10^4 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [1,12,-5,-6,50,3], k = 4 输出:12.75000 解释: - 当长度为 4 的时候,连续子数组平均值分别为 [0.5, 12.75, 10.5],其中最大平均值是 12.75。 - 当长度为 5 的时候,连续子数组平均值分别为 [10.4, 10.8],其中最大平均值是 10.8。 - 当长度为 6 的时候,连续子数组平均值分别为 [9.16667],其中最大平均值是 9.16667。 当取长度为 4 的子数组(即,子数组 [12, -5, -6, 50])的时候,可以得到最大的连续子数组平均值 12.75,所以返回 12.75。 根据题目要求,无需考虑长度小于 4 的子数组。 ``` - 示例 2: ```python 输入:nums = [5], k = 1 输出:5.00000 ``` ## 解题思路 ### 思路 1:二分查找 + 前缀和 这道题目要求找到长度大于等于 $k$ 的子数组的最大平均值。 **核心思路**: - 使用二分查找来确定最大平均值。 - 对于一个给定的平均值 $mid$,判断是否存在长度大于等于 $k$ 的子数组,其平均值大于等于 $mid$。 - 判断方法:将数组中每个元素减去 $mid$,如果存在长度大于等于 $k$ 的子数组和大于等于 $0$,则说明存在平均值大于等于 $mid$ 的子数组。 **算法步骤**: 1. 二分查找的范围是 $[min(nums), max(nums)]$。 2. 对于每个 $mid$,将数组中每个元素减去 $mid$,得到新数组 $arr$。 3. 使用前缀和 + 滑动窗口,判断是否存在长度大于等于 $k$ 的子数组和大于等于 $0$。 4. 如果存在,说明最大平均值在 $[mid, right]$ 范围内;否则在 $[left, mid]$ 范围内。 5. 重复步骤 2-4,直到 $left$ 和 $right$ 的差值小于 $10^{-5}$。 ### 思路 1:代码 ```python class Solution: def findMaxAverage(self, nums: List[int], k: int) -> float: def check(mid): """判断是否存在长度 >= k 的子数组,平均值 >= mid""" # 将数组中每个元素减去 mid,计算前缀和 n = len(nums) prefix_sum = 0 prev_sum = 0 # 前 i-k 个元素的前缀和 min_prev_sum = 0 # 前 i-k 个元素中的最小前缀和 for i in range(n): prefix_sum += nums[i] - mid # 长度至少为 k if i >= k - 1: # prefix_sum - min_prev_sum 表示某个长度 >= k 的子数组和 if prefix_sum - min_prev_sum >= 0: return True # 更新 prev_sum 和 min_prev_sum # prev_sum 是前 i-k+1 个元素的前缀和 prev_sum += nums[i - k + 1] - mid min_prev_sum = min(min_prev_sum, prev_sum) return False # 二分查找 left, right = min(nums), max(nums) while right - left > 1e-5: mid = (left + right) / 2 if check(mid): left = mid else: right = mid return left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log(max - min))$,其中 $n$ 是数组长度,$max$ 和 $min$ 分别是数组的最大值和最小值。二分查找的次数为 $O(\log(max - min))$,每次检查需要 $O(n)$ 时间。 - **空间复杂度**:$O(1)$。只使用了常数额外空间。 ================================================ FILE: docs/solutions/0600-0699/maximum-binary-tree.md ================================================ # [0654. 最大二叉树](https://leetcode.cn/problems/maximum-binary-tree/) - 标签:栈、树、数组、分治、二叉树、单调栈 - 难度:中等 ## 题目链接 - [0654. 最大二叉树 - 力扣](https://leetcode.cn/problems/maximum-binary-tree/) ## 题目大意 给定一个不含重复元素的整数数组 `nums`。一个以此数组构建的最大二叉树定义如下: - 二叉树的根是数组中的最大元素。 - 左子树是通过数组中最大值左边部分构造出的最大二叉树。 - 右子树是通过数组中最大值右边部分构造出的最大二叉树。 要求通过给定的数组构建最大二叉树,并且输出这个树的根节点。 ## 解题思路 根据题意可知,数组中最大元素位置为根节点,最大元素位置左右部分可分别作为左右子树。则我们可以通过递归的方式构建最大二叉树。 - 定义 left、right 分别表示当前数组的左右边界位置,定义 `max_value_index` 为当前数组中最大值位置。 - 遍历当前数组,找到最大值位置 `max_value_index`,并建立根节点 `root`,将数组 `nums` 分为 `[left, max_value_index]` 和 `[max_value_index, right]` 两部分,并分别递归建树。 - 将其赋值给 `root` 的左右子节点,最后返回 root 节点。 ## 代码 ```python class Solution: def createBinaryTree(self, nums: List[int], left: int, right: int) -> TreeNode: if left >= right: return None max_value_index = left for i in range(left + 1, right): if nums[i] > nums[max_value_index]: max_value_index = i root = TreeNode(nums[max_value_index]) root.left = self.createBinaryTree(nums, left, max_value_index) root.right = self.createBinaryTree(nums, max_value_index + 1, right) return root def constructMaximumBinaryTree(self, nums: List[int]) -> TreeNode: return self.createBinaryTree(nums, 0, len(nums)) ``` ================================================ FILE: docs/solutions/0600-0699/maximum-distance-in-arrays.md ================================================ # [0624. 数组列表中的最大距离](https://leetcode.cn/problems/maximum-distance-in-arrays/) - 标签:贪心、数组 - 难度:中等 ## 题目链接 - [0624. 数组列表中的最大距离 - 力扣](https://leetcode.cn/problems/maximum-distance-in-arrays/) ## 题目大意 **描述**: 给定 $m$ 个数组,每个数组都已经按照升序排好序了。 **要求**: 从两个不同的数组中选择两个整数(每个数组选一个)并且计算它们的距离。两个整数 $a$ 和 $b$ 之间的距离定义为它们差的绝对值 $|a-b|$。 返回最大距离。 **说明**: - $m == arrays.length$。 - $2 \le m \le 10^{5}$。 - $1 \le arrays[i].length \le 500$。 - $-10^{4} \le arrays[i][j] \le 10^{4}$。 - $arrays[i]$ 以升序排序。 - 所有数组中最多有 $10^{5}$ 个整数。 **示例**: - 示例 1: ```python 输入:[[1,2,3],[4,5],[1,2,3]] 输出:4 解释: 一种得到答案 4 的方法是从第一个数组或者第三个数组中选择 1,同时从第二个数组中选择 5 。 ``` - 示例 2: ```python 输入:arrays = [[1],[1]] 输出:0 ``` ## 解题思路 ### 思路 1:贪心 这道题目要求从不同数组中选择两个数,使得它们的距离最大。由于每个数组都是升序排列的,最大距离一定是某个数组的最大值减去另一个数组的最小值。 1. 初始化 $min\_val$ 为第一个数组的最小值,$max\_val$ 为第一个数组的最大值。 2. 初始化结果 $result = 0$。 3. 从第二个数组开始遍历: - 计算当前数组的最大值与之前所有数组的最小值的差值。 - 计算之前所有数组的最大值与当前数组的最小值的差值。 - 更新结果为这两个差值的最大值。 - 更新 $min\_val$ 和 $max\_val$。 4. 返回结果。 ### 思路 1:代码 ```python class Solution: def maxDistance(self, arrays: List[List[int]]) -> int: # 初始化为第一个数组的最小值和最大值 min_val = arrays[0][0] max_val = arrays[0][-1] result = 0 # 从第二个数组开始遍历 for i in range(1, len(arrays)): # 当前数组的最小值和最大值 curr_min = arrays[i][0] curr_max = arrays[i][-1] # 计算当前数组与之前数组的最大距离 result = max(result, abs(curr_max - min_val), abs(max_val - curr_min)) # 更新全局最小值和最大值 min_val = min(min_val, curr_min) max_val = max(max_val, curr_max) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m)$,其中 $m$ 是数组的个数。只需要遍历所有数组一次。 - **空间复杂度**:$O(1)$,只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0600-0699/maximum-length-of-pair-chain.md ================================================ # [0646. 最长数对链](https://leetcode.cn/problems/maximum-length-of-pair-chain/) - 标签:贪心、数组、动态规划、排序 - 难度:中等 ## 题目链接 - [0646. 最长数对链 - 力扣](https://leetcode.cn/problems/maximum-length-of-pair-chain/) ## 题目大意 **描述**: 给定一个由 $n$ 个数对组成的数对数组 $pairs$ ,其中 $pairs[i] = [left_i, right_i]$ 且 $left_i < right_i$ 。 现在,我们定义一种「跟随」关系,当且仅当 $b < c$ 时,数对 $p2 = [c, d]$ 才可以跟在 $p1 = [a, b]$ 后面。我们用这种形式来构造「数对链」。 **要求**: 找出并返回能够形成的 最长数对链的长度 。 你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。 **说明**: - $n == pairs.length$。 - $1 \le n \le 10^{3}$。 - $-10^{3} \le left_i \lt right_i \le 10^{3}$。 **示例**: - 示例 1: ```python 输入:pairs = [[1,2], [2,3], [3,4]] 输出:2 解释:最长的数对链是 [1,2] -> [3,4] 。 ``` - 示例 2: ```python 输入:pairs = [[1,2],[7,8],[4,5]] 输出:3 解释:最长的数对链是 [1,2] -> [4,5] -> [7,8] 。 ``` ## 解题思路 ### 思路 1:贪心 这道题目要求找到最长的数对链。可以使用贪心策略:按照数对的右端点排序,然后依次选择不冲突的数对。 1. 将数对按照右端点从小到大排序。 2. 初始化链的长度 $count = 1$,当前链的末尾为第一个数对的右端点 $curr\underline{~}end = pairs[0][1]$。 3. 从第二个数对开始遍历: - 如果当前数对的左端点大于 $curr\underline{~}end$,说明可以将当前数对加入链中。 - 更新 $curr\underline{~}end$ 为当前数对的右端点,$count$ 加 1。 4. 返回 $count$。 ### 思路 1:代码 ```python class Solution: def findLongestChain(self, pairs: List[List[int]]) -> int: # 按照右端点排序 pairs.sort(key=lambda x: x[1]) count = 1 curr_end = pairs[0][1] for i in range(1, len(pairs)): # 如果当前数对的左端点大于链的末尾,可以加入链中 if pairs[i][0] > curr_end: count += 1 curr_end = pairs[i][1] return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是数对的数量。主要时间消耗在排序上。 - **空间复杂度**:$O(\log n)$,排序所需的栈空间。 ================================================ FILE: docs/solutions/0600-0699/maximum-product-of-three-numbers.md ================================================ # [0628. 三个数的最大乘积](https://leetcode.cn/problems/maximum-product-of-three-numbers/) - 标签:数组、数学、排序 - 难度:简单 ## 题目链接 - [0628. 三个数的最大乘积 - 力扣](https://leetcode.cn/problems/maximum-product-of-three-numbers/) ## 题目大意 **描述**: 给定一个整型数组 $nums$。 **要求**: 在数组中找出由三个数组成的最大乘积,并输出这个乘积。 **说明**: - $3 \le nums.length \le 10^{4}$。 - $-10^{3} \le nums[i] \le 10^{3}$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3] 输出:6 ``` - 示例 2: ```python 输入:nums = [1,2,3,4] 输出:24 ``` ## 解题思路 ### 思路 1:排序 + 数学 这道题目要求找出三个数的最大乘积。需要考虑负数的情况。 最大乘积可能有两种情况: 1. 三个最大的正数相乘。 2. 两个最小的负数(绝对值最大)与最大的正数相乘。 因此,对数组排序后,比较这两种情况的结果即可。 ### 思路 1:代码 ```python class Solution: def maximumProduct(self, nums: List[int]) -> int: nums.sort() n = len(nums) # 情况 1:三个最大的数相乘 product1 = nums[n - 1] * nums[n - 2] * nums[n - 3] # 情况 2:两个最小的数(可能是负数)与最大的数相乘 product2 = nums[0] * nums[1] * nums[n - 1] return max(product1, product2) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组的长度。主要时间消耗在排序上。 - **空间复杂度**:$O(\log n)$,排序所需的栈空间。 ### 思路 2:线性扫描 不需要完整排序,只需要找到最大的三个数和最小的两个数即可。 ### 思路 2:代码 ```python class Solution: def maximumProduct(self, nums: List[int]) -> int: # 找到最大的三个数 max1 = max2 = max3 = float('-inf') # 找到最小的两个数 min1 = min2 = float('inf') for num in nums: # 更新最大的三个数 if num > max1: max3 = max2 max2 = max1 max1 = num elif num > max2: max3 = max2 max2 = num elif num > max3: max3 = num # 更新最小的两个数 if num < min1: min2 = min1 min1 = num elif num < min2: min2 = num return max(max1 * max2 * max3, min1 * min2 * max1) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。只需要遍历数组一次。 - **空间复杂度**:$O(1)$,只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0600-0699/maximum-sum-of-3-non-overlapping-subarrays.md ================================================ # [0689. 三个无重叠子数组的最大和](https://leetcode.cn/problems/maximum-sum-of-3-non-overlapping-subarrays/) - 标签:数组、动态规划、前缀和、滑动窗口 - 难度:困难 ## 题目链接 - [0689. 三个无重叠子数组的最大和 - 力扣](https://leetcode.cn/problems/maximum-sum-of-3-non-overlapping-subarrays/) ## 题目大意 **描述**: 给定一个整数数组 $nums$ 和一个整数 $k$。 **要求**: 找出三个长度为 $k$、互不重叠、且全部数字和最大的子数组,并返回这三个子数组。 以下标的数组形式返回结果,数组中的每一项分别指示每个子数组的起始位置(下标从 $0$ 开始)。如果有多个结果,返回字典序最小的一个。 **说明**: - $1 \le nums.length \le 2 \times 10^{4}$。 - $1 \le nums[i] \lt 216$。 - $1 \le k \le floor(nums.length / 3)$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,1,2,6,7,5,1], k = 2 输出:[0,3,5] 解释:子数组 [1, 2], [2, 6], [7, 5] 对应的起始下标为 [0, 3, 5]。 也可以取 [2, 1], 但是结果 [1, 3, 5] 在字典序上更大。 ``` - 示例 2: ```python 输入:nums = [1,2,1,2,1,2,1,2,1], k = 2 输出:[0,2,4] ``` ## 解题思路 ### 思路 1:动态规划 + 滑动窗口 这道题目要求找出三个长度为 $k$ 的无重叠子数组,使得它们的和最大。使用动态规划记录最优解。 1. 首先使用滑动窗口计算所有长度为 $k$ 的子数组的和,存储在数组 $sums$ 中。 2. 定义三个数组: - $left[i]$:表示在 $[0, i]$ 范围内,和最大的子数组的起始索引。 - $right[i]$:表示在 $[i, n-k]$ 范围内,和最大的子数组的起始索引。 - 中间的子数组通过遍历确定。 3. 遍历中间子数组的所有可能位置 $j$(范围 $[k, n-2k]$): - 左侧子数组的最优位置为 $left[j-k]$。 - 右侧子数组的最优位置为 $right[j+k]$。 - 计算三个子数组的总和,更新最大值和对应的索引。 4. 返回三个子数组的起始索引。 ### 思路 1:代码 ```python class Solution: def maxSumOfThreeSubarrays(self, nums: List[int], k: int) -> List[int]: n = len(nums) # 计算所有长度为 k 的子数组的和 sums = [] window_sum = sum(nums[:k]) sums.append(window_sum) for i in range(k, n): window_sum += nums[i] - nums[i - k] sums.append(window_sum) # left[i] 表示在 [0, i] 范围内和最大的子数组的起始索引 left = [0] * len(sums) best_idx = 0 for i in range(len(sums)): if sums[i] > sums[best_idx]: best_idx = i left[i] = best_idx # right[i] 表示在 [i, len(sums)-1] 范围内和最大的子数组的起始索引 right = [0] * len(sums) best_idx = len(sums) - 1 for i in range(len(sums) - 1, -1, -1): if sums[i] >= sums[best_idx]: best_idx = i right[i] = best_idx # 遍历中间子数组的位置 max_sum = 0 result = [-1, -1, -1] for j in range(k, len(sums) - k): l = left[j - k] r = right[j + k] total = sums[l] + sums[j] + sums[r] if total > max_sum: max_sum = total result = [l, j, r] return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。需要遍历数组多次,但每次都是线性时间。 - **空间复杂度**:$O(n)$,需要使用数组存储子数组的和以及左右最优位置。 ================================================ FILE: docs/solutions/0600-0699/maximum-swap.md ================================================ # [0670. 最大交换](https://leetcode.cn/problems/maximum-swap/) - 标签:贪心、数学 - 难度:中等 ## 题目链接 - [0670. 最大交换 - 力扣](https://leetcode.cn/problems/maximum-swap/) ## 题目大意 **描述**: 给定一个非负整数,你至多可以交换一次数字中的任意两位。 **要求**: 返回你能得到的最大值。 **说明**: - 给定数字的范围是 $[0, 10^8]$。 **示例**: - 示例 1: ```python 输入: 2736 输出: 7236 解释: 交换数字2和数字7。 ``` - 示例 2: ```python 输入: 9973 输出: 9973 解释: 不需要交换。 ``` ## 解题思路 ### 思路 1:贪心 这道题目要求交换一次数字中的任意两位,使得结果最大。贪心策略是:从高位到低位找到第一个可以被更大数字替换的位置。 1. 将数字转换为字符数组,方便操作。 2. 从右到左记录每个位置右侧的最大数字及其最右位置。 3. 从左到右遍历,找到第一个可以被右侧更大数字替换的位置: - 如果当前位置的数字小于右侧的最大数字,交换它们。 - 为了使结果最大,选择最右侧的最大数字进行交换。 4. 返回交换后的数字。 ### 思路 1:代码 ```python class Solution: def maximumSwap(self, num: int) -> int: # 将数字转换为字符数组 digits = list(str(num)) n = len(digits) # 记录每个位置右侧(包括自己)的最大数字的最右位置 max_idx = [0] * n max_idx[n - 1] = n - 1 for i in range(n - 2, -1, -1): if digits[i] > digits[max_idx[i + 1]]: max_idx[i] = i else: max_idx[i] = max_idx[i + 1] # 从左到右找到第一个可以交换的位置 for i in range(n): # 如果当前位置的数字小于右侧的最大数字 if digits[i] < digits[max_idx[i]]: # 交换 digits[i], digits[max_idx[i]] = digits[max_idx[i]], digits[i] break return int(''.join(digits)) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log num)$,其中 $num$ 是输入的数字。需要遍历数字的每一位。 - **空间复杂度**:$O(\log num)$,需要将数字转换为字符数组。 ================================================ FILE: docs/solutions/0600-0699/maximum-width-of-binary-tree.md ================================================ # [0662. 二叉树最大宽度](https://leetcode.cn/problems/maximum-width-of-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0662. 二叉树最大宽度 - 力扣](https://leetcode.cn/problems/maximum-width-of-binary-tree/) ## 题目大意 **描述**:给你一棵二叉树的根节点 `root`。 **要求**:返回树的最大宽度。 **说明**: - **每一层的宽度**:为该层最左和最右的非空节点(即两个端点)之间的长度。将这个二叉树视作与满二叉树结构相同,两端点间会出现一些延伸到这一层的 `null` 节点,这些 `null` 节点也计入长度。 - **树的最大宽度**:是所有层中最大的宽度。 - 题目数据保证答案将会在 32 位带符号整数范围内。 - 树中节点的数目范围是 $[1, 3000]$。 - $-100 \le Node.val \le 100$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/05/03/width1-tree.jpg) ```python 输入:root = [1,3,2,5,3,null,9] 输出:4 解释:最大宽度出现在树的第 3 层,宽度为 4 (5,3,null,9)。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2022/03/14/maximum-width-of-binary-tree-v3.jpg) ```python 输入:root = [1,3,2,5,null,null,9,6,null,7] 输出:7 解释:最大宽度出现在树的第 4 层,宽度为 7 (6,null,null,null,null,null,7) 。 ``` ## 解题思路 ### 思路 1:广度优先搜索 最直观的做法是,求出每一层的宽度,然后求出所有层高度的最大值。 在计算每一层宽度时,根据题意,两端点之间的 `null` 节点也计入长度,所以我们可以对包括 `null` 节点在内的该二叉树的所有节点进行编号。 也就是满二叉树的编号规则:如果当前节点的编号为 $i$,则左子节点编号记为 $i \times 2 + 1$,则右子节点编号为 $i \times 2 + 2$。 接下来我们使用广度优先搜索方法遍历每一层的节点,在向队列中添加节点时,将该节点与该节点对应的编号一同存入队列中。 这样在计算每一层节点的宽度时,我们可以通过队列中队尾节点的编号与队头节点的编号,快速计算出当前层的宽度。并计算出所有层宽度的最大值。 ### 思路 1:代码 ```python class Solution: def widthOfBinaryTree(self, root: Optional[TreeNode]) -> int: if not root: return False queue = collections.deque([[root, 0]]) ans = 0 while queue: ans = max(ans, queue[-1][1] - queue[0][1] + 1) size = len(queue) for _ in range(size): cur, index = queue.popleft() if cur.left: queue.append([cur.left, index * 2 + 1]) if cur.right: queue.append([cur.right, index * 2 + 2]) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为二叉树的节点数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0600-0699/merge-two-binary-trees.md ================================================ # [0617. 合并二叉树](https://leetcode.cn/problems/merge-two-binary-trees/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0617. 合并二叉树 - 力扣](https://leetcode.cn/problems/merge-two-binary-trees/) ## 题目大意 给定两个二叉树,将两个二叉树合并成一个新的二叉树。合并规则如下: - 如果两个二叉树对应节点重叠,则将两个节点的值相加并作为新的二叉树节点。 - 如果两个二叉树对应节点其中一个为空,另一个不为空,则将不为空的节点左心新的二叉树节点。 最终返回新的二叉树的根节点。 ## 解题思路 利用前序遍历二叉树,并按照规则递归建立二叉树。将其对应节点值相加或者取其中不为空的节点做为新节点。 ## 代码 ```python class Solution: def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode: if not root1: return root2 if not root2: return root1 merged = TreeNode(root1.val + root2.val) merged.left = self.mergeTrees(root1.left, root2.left) merged.right = self.mergeTrees(root1.right, root2.right) return merged ``` ================================================ FILE: docs/solutions/0600-0699/minimum-factorization.md ================================================ # [0625. 最小因式分解](https://leetcode.cn/problems/minimum-factorization/) - 标签:贪心、数学 - 难度:中等 ## 题目链接 - [0625. 最小因式分解 - 力扣](https://leetcode.cn/problems/minimum-factorization/) ## 题目大意 **描述**: 给定一个正整数 $num$。 **要求**: 找出最小的正整数 $x$ 使得 $x$ 的所有数位相乘恰好等于 $num$。 如果不存在这样的结果或者结果不是 32 位有符号整数,返回 $0$。 **说明**: - $1 \le num \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:num = 48 输出:68 ``` - 示例 2: ```python 输入:num = 15 输出:35 ``` ## 解题思路 ### 思路 1:贪心 + 因数分解 #### 思路 1:算法描述 要找到最小的正整数 $x$,使得 $x$ 的所有数位相乘等于 `num`。 **核心思路**: - 为了让 $x$ 最小,应该让 $x$ 的位数尽可能少,每一位尽可能大。 - 从 $9$ 到 $2$ 依次尝试分解 `num`,将因子从小到大排列(贪心:让结果最小)。 - 特殊情况:如果 $num < 10$,直接返回 `num`。 **算法步骤**: 1. 如果 $num < 10$,直接返回 `num`。 2. 从 $9$ 到 $2$ 依次尝试分解 `num`: - 如果 `num` 能被 $i$ 整除,将 $i$ 加入结果列表。 - 继续分解 $num / i$。 3. 如果最后 $num > 1$,说明无法分解(存在大于 $9$ 的质因子),返回 $0$。 4. 将结果列表从小到大排列并转换为整数,检查是否超过 32 位整数范围。 #### 思路 1:代码 ```python class Solution: def smallestFactorization(self, num: int) -> int: # 特殊情况 if num < 10: return num # 从 9 到 2 依次分解 digits = [] for i in range(9, 1, -1): while num % i == 0: digits.append(i) num //= i # 如果 num > 1,说明存在大于 9 的质因子,无法分解 if num > 1: return 0 # 将数字从小到大排列(贪心:让结果最小) digits.sort() # 转换为整数 result = 0 for digit in digits: result = result * 10 + digit # 检查是否超过 32 位整数范围 if result > 2**31 - 1: return 0 return result ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(\log num)$,需要分解 `num` 的所有因子。 - **空间复杂度**:$O(\log num)$,需要存储分解后的数字。 ================================================ FILE: docs/solutions/0600-0699/next-closest-time.md ================================================ # [0681. 最近时刻](https://leetcode.cn/problems/next-closest-time/) - 标签:哈希表、字符串、回溯、枚举 - 难度:中等 ## 题目链接 - [0681. 最近时刻 - 力扣](https://leetcode.cn/problems/next-closest-time/) ## 题目大意 **描述**: 给定一个形如 `"HH:MM"` 表示的时刻 `time`,利用当前出现过的数字构造下一个距离当前时间最近的时刻。每个出现数字都可以被无限次使用。 你可以认为给定的字符串一定是合法的。例如,`"01:34"` 和 `"12:09"` 是合法的,`"1:34"` 和 `"12:9"` 是不合法的。 **要求**: 返回下一个最近的时刻。 **说明**: - $time.length == 5$。 - $time$ 为有效时间,格式为 `"HH:MM"`。 - $0 \le HH < 24$。 - $0 \le MM < 60$。 **示例**: - 示例 1: ```python 输入:"19:34" 输出:"19:39" 解释:利用数字 1, 9, 3, 4 构造出来的最近时刻是 19:39,是 5 分钟之后。 结果不是 19:33 因为这个时刻是 23 小时 59 分钟之后。 ``` - 示例 2: ```python 输入:"23:59" 输出:"22:22" 解释:利用数字 2, 3, 5, 9 构造出来的最近时刻是 22:22。答案一定是第二天的某一时刻,所以选择可表示的最小时刻。 ``` ## 解题思路 ### 思路 1:枚举 + 模拟 #### 思路 1:算法描述 给定当前时间,需要找到下一个最近的时刻,且只能使用当前时间中出现的数字。 **核心思路**: - 从当前时间开始,逐分钟递增,检查每个时刻是否只包含当前时间中的数字。 - 最多检查 $24 \times 60 = 1440$ 个时刻(一天的分钟数)。 **算法步骤**: 1. 提取当前时间中出现的所有数字,存入集合。 2. 将时间转换为分钟数。 3. 从下一分钟开始,逐分钟递增: - 将分钟数转换为时间格式。 - 检查时间中的所有数字是否都在集合中。 - 如果是,返回该时间。 4. 循环最多 $1440$ 次(一天)。 #### 思路 1:代码 ```python class Solution: def nextClosestTime(self, time: str) -> str: # 提取当前时间中的数字 digits = set(time.replace(':', '')) # 将时间转换为分钟数 hour, minute = map(int, time.split(':')) current_minutes = hour * 60 + minute # 从下一分钟开始查找 for i in range(1, 24 * 60 + 1): next_minutes = (current_minutes + i) % (24 * 60) next_hour = next_minutes // 60 next_minute = next_minutes % 60 # 格式化时间 next_time = f"{next_hour:02d}:{next_minute:02d}" # 检查是否只包含原有数字 if all(d in digits for d in next_time.replace(':', '')): return next_time return time ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$,最多检查 $1440$ 个时刻,每次检查需要常数时间。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0600-0699/non-decreasing-array.md ================================================ # [0665. 非递减数列](https://leetcode.cn/problems/non-decreasing-array/) - 标签:数组 - 难度:中等 ## 题目链接 - [0665. 非递减数列 - 力扣](https://leetcode.cn/problems/non-decreasing-array/) ## 题目大意 给定一个整数数组 nums,问能否在最多改变 1 个元素的条件下,使数组变为非递减序列。如果能,返回 True,不能则返回 False。 ## 解题思路 循环遍历数组,寻找 nums[i] > nums[i+1] 的情况,一旦这种情况出现超过 2 次,则不可能最多改变 1 个元素,直接返回 False。 遇到 nums[i] > nums[i+1] 的情况,应该手动调节某位置上元素使数组有序。此时,有两种选择: - 将 nums[i] 调低,与 nums[i-1] 持平 - 将 nums[i+1] 调高,与 nums[i] 持平 如果选择第一种调节方式,如果调节前 nums[i-1] > nums[i+1],那么调节完 nums[i] 之后,nums[i-1] 还是比 nums[i+1] 大,不可取。 所以应选择第二种调节方式,如果调节前 nums[i-1] > nums[i+1],那么调节完 nums[i+1] 之后 nums[i-1] < nums[i] <= nums[i+1],满足非递减要求。 最终如果最多调整过一次,且 nums[i] > nums[i+1] 的情况也最多出现过一次,则返回 True。 ## 代码 ```python class Solution: def checkPossibility(self, nums: List[int]) -> bool: count = 0 for i in range(len(nums)-1): if nums[i] > nums[i+1]: count += 1 if count > 1: return False if i > 0 and nums[i-1] > nums[i+1]: nums[i+1] = nums[i] return True ``` ================================================ FILE: docs/solutions/0600-0699/non-negative-integers-without-consecutive-ones.md ================================================ # [0600. 不含连续1的非负整数](https://leetcode.cn/problems/non-negative-integers-without-consecutive-ones/) - 标签:动态规划 - 难度:困难 ## 题目链接 - [0600. 不含连续1的非负整数 - 力扣](https://leetcode.cn/problems/non-negative-integers-without-consecutive-ones/) ## 题目大意 **描述**:给定一个正整数 $n$。 **要求**:统计在 $[0, n]$ 范围的非负整数中,有多少个整数的二进制表示中不存在连续的 $1$。 **说明**: - $1 \le n \le 10^9$。 **示例**: - 示例 1: ```python 输入: n = 5 输出: 5 解释: 下面列出范围在 [0, 5] 的非负整数与其对应的二进制表示: 0 : 0 1 : 1 2 : 10 3 : 11 4 : 100 5 : 101 其中,只有整数 3 违反规则(有两个连续的 1 ),其他 5 个满足规则。 ``` - 示例 2: ```python 输入: n = 1 输出: 2 ``` ## 解题思路 ### 思路 1:动态规划 + 数位 DP 将 $n$ 转换为字符串 $s$,定义递归函数 `def dfs(pos, pre, isLimit):` 表示构造第 $pos$ 位及之后所有数位的合法方案数。其中: 1. $pos$ 表示当前枚举的数位位置。 2. $pre$ 表示前一位是否为 $1$,用于过滤连续 $1$ 的不合法方案。 3. $isLimit$ 表示前一位数位是否等于上界,用于限制本次搜索的数位范围。 接下来按照如下步骤进行递归。 1. 从 `dfs(0, False, True)` 开始递归。 `dfs(0, False, True)` 表示: 1. 从位置 $0$ 开始构造。 2. 开始时前一位不为 $1$。 3. 开始时受到数字 $n$ 对应最高位数位的约束。 2. 如果遇到 $pos == len(s)$,表示到达数位末尾,当前为合法方案,此时:直接返回方案数 $1$。 3. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:`ans = 0`。 4. 因为不需要考虑前导 $0$,所以当前所能选择的最小数字 $minX$ 为 $0$。 5. 根据 $isLimit$ 来决定填当前位数位所能选择的最大数字($maxX$)。 6. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $d$。 7. 如果前一位为 $1$ 并且当前为 $d$ 也为 $1$,则说明当前方案出现了连续的 $1$,则跳过。 8. 方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, d == 1, isLimit and d == maxX)`。 1. `d == 1` 表示下一位 $pos - 1$ 的前一位 $pos$ 是否为 $1$。 2. `isLimit and d == maxX` 表示 $pos + 1$ 位受到之前位限制和 $pos$ 位限制。 9. 最后的方案数为 `dfs(0, False, True)`,将其返回即可。 ### 思路 1:代码 ```python class Solution: def findIntegers(self, n: int) -> int: # 将 n 的二进制转换为字符串 s s = str(bin(n))[2:] @cache # pos: 第 pos 个数位 # pre: 第 pos - 1 位是否为 1 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 def dfs(pos, pre, isLimit): if pos == len(s): return 1 ans = 0 # 不需要考虑前导 0,则最小可选择数字为 0 minX = 0 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 1。 maxX = int(s[pos]) if isLimit else 1 # 枚举可选择的数字 for d in range(minX, maxX + 1): if pre and d == 1: continue ans += dfs(pos + 1, d == 1, isLimit and d == maxX) return ans return dfs(0, False, True) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0600-0699/number-of-distinct-islands.md ================================================ # [0694. 不同岛屿的数量](https://leetcode.cn/problems/number-of-distinct-islands/) - 标签:深度优先搜索、广度优先搜索、并查集、哈希表、哈希函数 - 难度:中等 ## 题目链接 - [0694. 不同岛屿的数量 - 力扣](https://leetcode.cn/problems/number-of-distinct-islands/) ## 题目大意 **描述**: 给定一个非空 01 二维数组表示的网格,一个岛屿由四连通(上、下、左、右四个方向)的 $1$ 组成,你可以认为网格的四周被海水包围。 **要求**: 计算这个网格中共有多少个形状不同的岛屿。两个岛屿被认为是相同的,当且仅当一个岛屿可以通过平移变换(不可以旋转、翻转)和另一个岛屿重合。 **说明**: - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 50$。 - $grid[i][j]$ 仅包含 $0$ 或 $1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/05/01/distinctisland1-1-grid.jpg) ```python 输入: grid = [[1,1,0,0,0],[1,1,0,0,0],[0,0,0,1,1],[0,0,0,1,1]] 输出:1 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/05/01/distinctisland1-2-grid.jpg) ```python 输入: grid = [[1,1,0,1,1],[1,0,0,0,0],[0,0,0,0,1],[1,1,0,1,1]] 输出: 3 ``` ## 解题思路 ### 思路 1:深度优先搜索 + 哈希表 这道题目要求计算形状不同的岛屿数量。关键在于如何表示岛屿的形状。 **核心思路**: - 使用深度优先搜索遍历每个岛屿。 - 记录岛屿的形状:以岛屿的第一个单元格为原点,记录其他单元格相对于原点的坐标。 - 将形状(坐标集合)转换为字符串或元组,存入哈希集合中。 - 最后返回哈希集合的大小。 **算法步骤**: 1. 遍历网格,找到每个岛屿的起点(值为 $1$ 的单元格)。 2. 对每个岛屿进行深度优先搜索,记录岛屿的形状(相对坐标)。 3. 将形状标准化(以第一个单元格为原点),转换为元组。 4. 将形状存入哈希集合。 5. 返回哈希集合的大小。 ### 思路 1:代码 ```python class Solution: def numDistinctIslands(self, grid: List[List[int]]) -> int: m, n = len(grid), len(grid[0]) visited = [[False] * n for _ in range(m)] shapes = set() def dfs(i, j, shape, base_i, base_j): """深度优先搜索,记录岛屿形状""" if i < 0 or i >= m or j < 0 or j >= n or visited[i][j] or grid[i][j] == 0: return visited[i][j] = True # 记录相对坐标 shape.append((i - base_i, j - base_j)) # 四个方向搜索 dfs(i + 1, j, shape, base_i, base_j) dfs(i - 1, j, shape, base_i, base_j) dfs(i, j + 1, shape, base_i, base_j) dfs(i, j - 1, shape, base_i, base_j) # 遍历网格 for i in range(m): for j in range(n): if grid[i][j] == 1 and not visited[i][j]: shape = [] dfs(i, j, shape, i, j) # 将形状转换为元组并存入集合 shapes.add(tuple(sorted(shape))) return len(shapes) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 和 $n$ 分别是网格的行数和列数。需要遍历整个网格。 - **空间复杂度**:$O(m \times n)$。需要使用 $visited$ 数组和递归栈空间。 ================================================ FILE: docs/solutions/0600-0699/number-of-longest-increasing-subsequence.md ================================================ # [0673. 最长递增子序列的个数](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/) - 标签:树状数组、线段树、数组、动态规划 - 难度:中等 ## 题目链接 - [0673. 最长递增子序列的个数 - 力扣](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/) ## 题目大意 **描述**:给定一个未排序的整数数组 `nums`。 **要求**:返回最长递增子序列的个数。 **说明**: - 子数列必须是严格递增的。 - $1 \le nums.length \le 2000$。 - $-10^6 \le nums[i] \le 10^6$。 **示例**: - 示例 1: ```python 输入:[1,3,5,4,7] 输出:2 解释:有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。 ``` ## 解题思路 ### 思路 1:动态规划 可以先做题目 [0300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/)。 动态规划的状态 `dp[i]` 表示为:以第 `i` 个数字结尾的前 `i` 个元素中最长严格递增子序列的长度。 两重循环遍历前 `i` 个数字,对于 $0 \le j \le i$: - 当 `nums[j] < nums[i]` 时,`nums[i]` 可以接在 `nums[j]` 后面,此时以第 `i` 个数字结尾的最长严格递增子序列长度 + 1,即 `dp[i] = dp[j] + 1`。 - 当 `nums[j] ≥ nums[i]` 时,可以直接跳过。 则状态转移方程为:`dp[i] = max(dp[i], dp[j] + 1)`,`0 ≤ j ≤ i`,`nums[j] < nums[i]`。 最后再遍历一遍 dp 数组,求出最大值即为最长递增子序列的长度。 现在求最长递增子序列的个数。则需要在求解的过程中维护一个 `count` 数组,用来保存以 `nums[i]` 结尾的最长递增子序列的个数。 对于 $0 \le j \le i$: - 当 `nums[j] < nums[i]`,而且 `dp[j] + 1 > dp[i]` 时,说明第一次找到 `dp[j] + 1`长度且以`nums[i]`结尾的最长递增子序列,则以 `nums[i]` 结尾的最长递增子序列的组合数就等于以 `nums[j]` 结尾的组合数,即 `count[i] = count[j]`。 - 当 `nums[j] < nums[i]`,而且 `dp[j] + 1 == dp[i]` 时,说明以 `nums[i]` 结尾且长度为 `dp[j] + 1` 的递增序列已找到过一次了,则以 `nums[i]` 结尾的最长递增子序列的组合数要加上以 `nums[j]` 结尾的组合数,即 `count[i] += count[j]`。 - 然后根据遍历 dp 数组得到的最长递增子序列的长度 Z,然后再一次遍历 dp 数组,将所有 `dp[i] == max_length` 情况下的组合数 `coun[i]` 累加起来,即为最长递增序列的个数。 ### 思路 1:动态规划代码 ```python class Solution: def findNumberOfLIS(self, nums: List[int]) -> int: size = len(nums) dp = [1 for _ in range(size)] count = [1 for _ in range(size)] for i in range(size): for j in range(i): if nums[j] < nums[i]: if dp[j] + 1 > dp[i]: dp[i] = dp[j] + 1 count[i] = count[j] elif dp[j] + 1 == dp[i]: count[i] += count[j] max_length = max(dp) res = 0 for i in range(size): if dp[i] == max_length: res += count[i] return res ``` ### 思路 2:线段树 题目中 `nums` 的长度 为 $[1, 2000]$,值域为 $[-10^6, 10^6]$。 值域范围不是特别大,我们可以直接用线段树保存整个值域区间。但因为数组的长度只有 `2000`,所以算法效率更高的做法是先对数组进行离散化处理。把数组中的元素按照大小依次映射到 `[0, len(nums) - 1]` 这个区间。 1. 构建一棵长度为 `len(nums)` 的线段树,其中每个线段树的节点保存一个二元组。这个二元组 `val = [length, count]` 用来表示:以当前节点为结尾的子序列所能达到的最长递增子序列长度 `length` 和最长递增子序列对应的数量 `count`。 2. 顺序遍历数组 `nums`。对于当前元素 `nums[i]`: 3. 查找 `[0, nums[i - 1]]` 离散化后对应区间节点的二元组,也就是查找以区间 `[0, nums[i - 1]]` 上的点为结尾的子序列所能达到的最长递增子序列长度和其对应的数量,即 `val = [length, count]`。 - 如果所能达到的最长递增子序列长度为 `0`,则加入 `nums[i]` 之后最长递增子序列长度变为 `1`,且数量也变为 `1`。 - 如果所能达到的最长递增子序列长度不为 `0`,则加入 `nums[i]` 之后最长递增子序列长度 +1,但数量不变。 4. 根据上述计算的 `val` 值更新 `nums[i]` 对应节点的 `val` 值。 5. 然后继续向后遍历,重复进行第 `3` ~ `4` 步操作。 6. 最后查询以区间 `[0, nums[len(nums) - 1]]` 上的点为结尾的子序列所能达到的最长递增子序列长度和其对应的数量。返回对应的数量即为答案。 ### 思路 2:线段树代码 ```python # 线段树的节点类 class SegTreeNode: def __init__(self, val=[0, 1]): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值) # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, size): self.size = size self.tree = [SegTreeNode() for _ in range(4 * self.size)] # 维护 SegTreeNode 数组 if self.size > 0: self.__build(0, 0, self.size - 1) # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.__update_point(i, val, 0) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, 0) # 以下为内部实现方法 # 构建线段树实现方法:节点的存储下标为 index,节点的区间为 [left, right] def __build(self, index, left, right): self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,节点值为对应位置的元素值 self.tree[index].val = [0, 0] return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.__build(left_index, left, mid) # 递归创建左子树 self.__build(right_index, mid + 1, right) # 递归创建右子树 self.tree[index].val = self.merge(self.tree[left_index].val, self.tree[right_index].val) # 向上更新节点的区间值 # 单点更新实现方法:将 nums[i] 更改为 val,节点的存储下标为 index def __update_point(self, i, val, index): left = self.tree[index].left right = self.tree[index].right if left == i and right == i: self.tree[index].val = self.merge(self.tree[index].val, val) return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if i <= mid: # 在左子树中更新节点值 self.__update_point(i, val, left_index) else: # 在右子树中更新节点值 self.__update_point(i, val, right_index) self.tree[index].val = self.merge(self.tree[left_index].val, self.tree[right_index].val) # 向上更新节点的区间值 # 区间查询实现方法:在线段树中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return self.tree[index].val # 直接返回节点值 if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return [0, 0] mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 res_left = [0, 0] res_right = [0, 0] if q_left <= mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, left_index) if q_right > mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, right_index) # 返回合并结果 return self.merge(res_left, res_right) # 向上合并实现方法 def merge(self, val1, val2): val = [0, 0] if val1[0] == val2[0]: # 递增子序列长度一致,则合并后最长递增子序列个数为之前两者之和 val = [val1[0], val1[1] + val2[1]] elif val1[0] < val2[0]: # 如果递增子序列长度不一致,则合并后最长递增子序列个数取较长一方的个数 val = [val2[0], val2[1]] else: val = [val1[0], val1[1]] return val class Solution: def findNumberOfLIS(self, nums: List[int]) -> int: # 离散化处理 num_dict = dict() nums_sort = sorted(nums) for i in range(len(nums_sort)): num_dict[nums_sort[i]] = i # 构造线段树 self.STree = SegmentTree(len(nums_sort)) for num in nums: index = num_dict[num] # 查询 [0, nums[index - 1]] 区间上以 nums[index - 1] 结尾的子序列所能达到的最长递增子序列长度和对应数量 val = self.STree.query_interval(0, index - 1) # 如果当前最长递增子序列长度为 0,则加入 num 之后最长递增子序列长度为 1,且数量为 1 # 如果当前最长递增子序列长度不为 0,则加入 num 之后最长递增子序列长度 +1,但数量不变 if val[0] == 0: val = [1, 1] else: val = [val[0] + 1, val[1]] self.STree.update_point(index, val) return self.STree.query_interval(0, len(nums_sort) - 1)[1] ``` ================================================ FILE: docs/solutions/0600-0699/palindromic-substrings.md ================================================ # [0647. 回文子串](https://leetcode.cn/problems/palindromic-substrings/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [0647. 回文子串 - 力扣](https://leetcode.cn/problems/palindromic-substrings/) ## 题目大意 给定一个字符串 `s`,计算 `s` 中有多少个回文子串。 ## 解题思路 动态规划求解。 先定义状态 `dp[i][j]` 表示为区间 `[i, j]` 的子串是否为回文子串,如果是,则 `dp[i][j] = True`,如果不是,则 `dp[i][j] = False`。 接下来确定状态转移共识: 如果 `s[i] == s[j]`,分为以下几种情况: - `i == j`,单字符肯定是回文子串,`dp[i][j] == True`。 - `j - i == 1`,比如 `aa` 肯定也是回文子串,`dp[i][j] = True`。 - 如果 `j - i > 1`,则需要看 `[i + 1, j - 1]` 区间是不是回文子串,`dp[i][j] = dp[i + 1][j - 1]`。 如果 `s[i] != s[j]`,那肯定不是回文子串,`dp[i][j] = False`。 下一步确定遍历方向。 由于 `dp[i][j]` 依赖于 `dp[i + 1][j - 1]`,所以我们可以从左下角向右上角遍历。 同时,在递推过程中记录下 `dp[i][j] == True` 的个数,即为最后结果。 ## 代码 ```python class Solution: def countSubstrings(self, s: str) -> int: size = len(s) dp = [[False for _ in range(size)] for _ in range(size)] res = 0 for i in range(size - 1, -1, -1): for j in range(i, size): if s[i] == s[j]: if j - i <= 1: dp[i][j] = True else: dp[i][j] = dp[i + 1][j - 1] else: dp[i][j] = False if dp[i][j]: res += 1 return res ``` ================================================ FILE: docs/solutions/0600-0699/partition-to-k-equal-sum-subsets.md ================================================ # [0698. 划分为k个相等的子集](https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/) - 标签:位运算、记忆化搜索、数组、动态规划、回溯、状态压缩 - 难度:中等 ## 题目链接 - [0698. 划分为k个相等的子集 - 力扣](https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 和一个正整数 $k$。 **要求**:找出是否有可能把这个数组分成 $k$ 个非空子集,其总和都相等。 **说明**: - $1 \le k \le len(nums) \le 16$。 - $0 < nums[i] < 10000$。 - 每个元素的频率在 $[1, 4]$ 范围内。 **示例**: - 示例 1: ```python 输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4 输出: True 说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。 ``` - 示例 2: ```python 输入: nums = [1,2,3,4], k = 3 输出: False ``` ## 解题思路 ### 思路 1:状态压缩 DP 根据题目要求,我们可以将几种明显不符合要求的情况过滤掉,比如:元素个数小于 $k$、元素总和不是 $k$ 的倍数、数组 $nums$ 中最大元素超过 $k$ 等分的目标和这几种情况。 然后再来考虑一般情况下,如何判断是否符合要求。 因为题目给定数组 $nums$ 的长度最多为 $16$,所以我们可以使用一个长度为 $16$ 位的二进制数来表示数组子集的选择状态。我们可以定义 $dp[state]$ 表示为当前选择状态下,是否可行。如果 $dp[state] == True$,表示可行;如果 $dp[state] == False$,则表示不可行。 接下来使用动态规划方法,进行求解。具体步骤如下: ###### 1. 阶段划分 按照数组元素选择情况进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[state]$ 表示为:当数组元素选择情况为 $state$ 时,是否存在一种方案,使得方案中的数字必定能分割成 $p(0 \le p \le k)$ 组恰好数字和等于目标和 $target$ 的集合和至多 $1$ 组数字和小于目标和 $target$ 的集合。 ###### 3. 状态转移方程 对于当前状态 $state$,如果: 1. 当数组元素选择情况为 $state$ 时可行,即 $dp[state] == True$; 2. 第 $i$ 位数字没有被使用; 3. 加上第 $i$ 位元素后的状态为 $next\_state$; 4. 加上第 $i$ 位元素后没有超出目标和。 则:$dp[next\_state] = True$。 ###### 4. 初始条件 - 当不选择任何元素时,可按照题目要求 ###### 5. 最终结果 根据我们之前定义的状态,$dp[state]$ 表示为:当数组元素选择情况为 $state$ 时,是否存在一种方案,使得方案中的数字必定能分割成 $p(0 \le p \le k)$ 组恰好数字和等于目标和 $target$ 的集合和至多 $1$ 组数字和小于目标和 $target$ 的集合。 所以当 $state == 1 << n - 1$ 时,状态就变为了:当数组元素都选上的情况下,是否存在一种方案,使得方案中的数字必定能分割成 $k$ 组恰好数字和等于目标和 $target$ 的集合。 这里之所以是 $k$ 组恰好数字和等于目标和 $target$ 的集合,是因为一开我们就限定了 $total \mod k == 0$ 这个条件,所以只能是 $k$ 组恰好数字和等于目标和 $target$ 的集合。 所以最终结果为 $dp[states - 1]$,其中 $states = 1 << n$。 ### 思路 1:代码 ```python class Solution: def canPartitionKSubsets(self, nums: List[int], k: int) -> bool: size = len(nums) if size < k: # 元素个数小于 k return False total = sum(nums) if total % k != 0: # 元素总和不是 k 的倍数 return False target = total // k if nums[-1] > target: # 最大元素超过 k 等分的目标和 return False nums.sort() states = 1 << size # 子集选择状态总数 cur_sum = [0 for _ in range(states)] dp = [False for _ in range(states)] dp[0] = True for state in range(states): if not dp[state]: # 基于 dp[state] == True 前提下进行转移 continue for i in range(size): if state & (1 << i) != 0: # 当前数字已被使用 continue if cur_sum[state] % target + nums[i] > target: break # 如果加入当前数字超出目标和,则后续不用继续遍历 next_state = state | (1 << i) # 加入当前数字 if dp[next_state]: # 如果新状态能划分,则跳过继续 continue cur_sum[next_state] = cur_sum[state] + nums[i] # 更新新状态下子集和 dp[next_state] = True # 更新新状态 if dp[states - 1]: # 找到一个符合要求的划分方案,提前返回 return True return dp[states - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 2^n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(2^n)$。 ## 参考资料 - 【题解】[状态压缩的定义理解 - 划分为k个相等的子集](https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/solution/zhuang-tai-ya-suo-de-ding-yi-li-jie-by-c-fo1b/) ================================================ FILE: docs/solutions/0600-0699/path-sum-iv.md ================================================ # [0666. 路径总和 IV](https://leetcode.cn/problems/path-sum-iv/) - 标签:树、深度优先搜索、数组、哈希表、二叉树 - 难度:中等 ## 题目链接 - [0666. 路径总和 IV - 力扣](https://leetcode.cn/problems/path-sum-iv/) ## 题目大意 **描述**: 对于一棵深度小于 $5$ 的树,可以用一组三位十进制整数来表示。给定一个由三位数组成的 **递增** 的数组 $nums$ 表示一棵深度小于 $5$ 的二叉树,对于每个整数: - 百位上的数字表示这个节点的深度 $d$,$1 \le d \le 4$。 - 十位上的数字表示这个节点在当前层所在的位置 $p$,$1 \le p \le 8$。位置编号与一棵 **满二叉树** 的位置编号相同。 - 个位上的数字表示这个节点的权值 $v$,$0 \le v \le 9$。 **要求**: 返回从 **根** 到所有 **叶子结点** 的 **路径之和**。 保证 **给定的数组表示一个有效的连接二叉树**。 **说明**: - $1 \le nums.length \le 15$。 - $110 \le nums[i] \le 489$。 - $nums$ 表示深度小于 $5$ 的有效二叉树。 - $nums$ 以升序排序。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/04/30/pathsum4-1-tree.jpg) ```python 输入:nums = [113, 215, 221] 输出:12 解释:列表所表示的树如上所示。 路径和 = (3 + 5) + (3 + 1) = 12。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/04/30/pathsum4-2-tree.jpg) ```python 输入:nums = [113, 221] 输出:4 解释:列表所表示的树如上所示。 路径和 = (3 + 1) = 4。 ``` ## 解题思路 ### 思路 1:DFS + 哈希表 #### 思路 1:算法描述 给定一个特殊编码的二叉树,需要计算从根到所有叶子节点的路径和。 **核心思路**: - 使用哈希表存储节点信息,键为 $(depth, position)$,值为节点的权值。 - 使用 DFS 遍历树,累加路径和。 - 判断叶子节点:左右子节点都不存在。 **算法步骤**: 1. 解析 `nums` 数组,构建哈希表存储节点信息。 2. 使用 DFS 从根节点开始遍历: - 累加当前路径和。 - 如果是叶子节点,将路径和加入总和。 - 否则递归遍历左右子节点。 3. 返回总和。 **关键点**: - 对于深度为 $d$、位置为 $p$ 的节点,其左子节点位置为 $2p - 1$,右子节点位置为 $2p$,深度为 $d + 1$。 #### 思路 1:代码 ```python class Solution: def pathSum(self, nums: List[int]) -> int: # 构建哈希表,键为 (depth, position),值为节点权值 tree = {} for num in nums: depth = num // 100 position = (num % 100) // 10 value = num % 10 tree[(depth, position)] = value self.total_sum = 0 def dfs(depth, position, current_sum): # 当前节点的值 if (depth, position) not in tree: return current_sum += tree[(depth, position)] # 计算左右子节点的位置 left_pos = 2 * position - 1 right_pos = 2 * position # 判断是否为叶子节点 has_left = (depth + 1, left_pos) in tree has_right = (depth + 1, right_pos) in tree if not has_left and not has_right: # 叶子节点,累加路径和 self.total_sum += current_sum else: # 递归遍历左右子树 if has_left: dfs(depth + 1, left_pos, current_sum) if has_right: dfs(depth + 1, right_pos, current_sum) # 从根节点开始 DFS dfs(1, 1, 0) return self.total_sum ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是节点数量,每个节点访问一次。 - **空间复杂度**:$O(n)$,哈希表和递归栈的空间开销。 ================================================ FILE: docs/solutions/0600-0699/print-binary-tree.md ================================================ # [0655. 输出二叉树](https://leetcode.cn/problems/print-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0655. 输出二叉树 - 力扣](https://leetcode.cn/problems/print-binary-tree/) ## 题目大意 **描述**: 给定一棵二叉树的根节点 $root$。 **要求**: 构造一个下标从 $0$ 开始、大小为 $m \times n$ 的字符串矩阵 $res$,用以表示树的「格式化布局」。 构造此格式化布局矩阵需要遵循以下规则: - 树的「高度」为 $height$,矩阵的行数 $m$ 应该等于 $height + 1$。 - 矩阵的列数 $n$ 应该等于 $2^{height+1} - 1$。 - 「根节点」需要放置在「顶行」的「正中间」,对应位置为 $res[0][(n-1)/2]$。 - 对于放置在矩阵中的每个节点,设对应位置为 $res[r][c]$,将其左子节点放置在 $res[r+1][c-2^{height-r-1}]$,右子节点放置在 $res[r+1][c+2^{height-r-1}]$。 - 继续这一过程,直到树中的所有节点都妥善放置。 - 任意空单元格都应该包含空字符串 `""`。 返回构造得到的矩阵 $res$。 **说明**: - 树中节点数在范围 $[1, 2^{10}]$ 内。 - $-99 \le Node.val \le 99$。 - 树的深度在范围 $[1, 10]$ 内。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/05/03/print1-tree.jpg) ```python 输入:root = [1,2] 输出: [["","1",""], ["2","",""]] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/05/03/print2-tree.jpg) ```python 输入:root = [1,2,3,null,4] 输出: [["","","","1","","",""], ["","2","","","","3",""], ["","","4","","","",""]] ``` ## 解题思路 ### 思路 1:深度优先搜索 这道题目要求将二叉树按照特定格式输出到矩阵中。首先需要计算树的高度,然后根据高度确定矩阵的大小,最后使用 DFS 填充矩阵。 1. 计算树的高度 $height$。 2. 根据题目要求,矩阵的行数 $m = height + 1$,列数 $n = 2^{height + 1} - 1$。 3. 初始化矩阵,所有位置填充空字符串。 4. 使用 DFS 填充矩阵: - 根节点放在第 0 行的中间位置 $(n - 1) / 2$。 - 对于位置 $(r, c)$ 的节点: - 左子节点放在 $(r + 1, c - 2^{height - r - 1})$。 - 右子节点放在 $(r + 1, c + 2^{height - r - 1})$。 5. 返回矩阵。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def printTree(self, root: Optional[TreeNode]) -> List[List[str]]: # 计算树的高度 def get_height(node): if not node: return -1 return 1 + max(get_height(node.left), get_height(node.right)) height = get_height(root) m = height + 1 n = 2 ** (height + 1) - 1 # 初始化矩阵 result = [[""] * n for _ in range(m)] # DFS 填充矩阵 def dfs(node, row, col): if not node: return result[row][col] = str(node.val) # 计算左右子节点的列偏移 offset = 2 ** (height - row - 1) # 递归处理左右子树 dfs(node.left, row + 1, col - offset) dfs(node.right, row + 1, col + offset) # 从根节点开始填充 dfs(root, 0, (n - 1) // 2) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(h \times 2^h)$,其中 $h$ 是树的高度。需要遍历所有节点,矩阵的大小为 $(h + 1) \times (2^{h + 1} - 1)$。 - **空间复杂度**:$O(h \times 2^h)$,需要创建矩阵存储结果,递归调用栈的深度为 $O(h)$。 ================================================ FILE: docs/solutions/0600-0699/redundant-connection-ii.md ================================================ # [0685. 冗余连接 II](https://leetcode.cn/problems/redundant-connection-ii/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:困难 ## 题目链接 - [0685. 冗余连接 II - 力扣](https://leetcode.cn/problems/redundant-connection-ii/) ## 题目大意 **描述**: 在本问题中,有根树指满足以下条件的「有向」图。该树只有一个根节点,所有其他节点都是该根节点的后继。 该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。 输入一个有向图,该图由一个有着 $n$ 个节点(节点值不重复,从 $1$ 到 $n$)的树及一条附加的有向边构成。附加的边包含在 $1$ 到 $n$ 中的两个不同顶点间,这条附加的边不属于树中已存在的边。 结果图是一个以边组成的二维数组 $edges$。 每个元素是一对 $[ui, vi]$,用以表示「有向」图中连接顶点 $ui$ 和顶点 $vi$ 的边,其中 $ui$ 是 $vi$ 的一个父节点。 **要求**: 返回一条能删除的边,使得剩下的图是有 $n$ 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。 **说明**: - $n == edges.length$。 - $3 \le n \le 10^{3}$。 - $edges[i].length == 2$。 - $1 \le ui, vi \le n$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/12/20/graph1.jpg) ```python 输入:edges = [[1,2],[1,3],[2,3]] 输出:[2,3] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/12/20/graph2.jpg) ```python 输入:edges = [[1,2],[2,3],[3,4],[4,1],[1,5]] 输出:[4,1] ``` ## 解题思路 ### 思路 1:并查集 这道题目是在有向图中找到一条可以删除的边,使得剩下的图是有根树。有向图中的有根树有以下特点: 1. 只有一个根节点(入度为 0)。 2. 除根节点外,其他节点的入度都为 1。 可能出现的情况: 1. 某个节点的入度为 2(有两个父节点)。 2. 图中存在环。 使用并查集来检测环,并记录入度为 2 的节点。 ### 思路 1:代码 ```python class Solution: def findRedundantDirectedConnection(self, edges: List[List[int]]) -> List[int]: n = len(edges) parent = list(range(n + 1)) def find(x): if parent[x] != x: parent[x] = find(parent[x]) return parent[x] def union(x, y): parent[find(x)] = find(y) # 记录每个节点的父节点 in_degree = {} conflict_edge = None cycle_edge = None for u, v in edges: # 如果 v 已经有父节点,说明有冲突 if v in in_degree: conflict_edge = [u, v] else: in_degree[v] = u # 检查是否形成环 if find(u) == find(v): cycle_edge = [u, v] else: union(u, v) # 情况 1:没有冲突边,只有环 if not conflict_edge: return cycle_edge # 情况 2:有冲突边 # 如果没有环,删除冲突边 if not cycle_edge: return conflict_edge # 情况 3:既有冲突边又有环 # 删除导致冲突的第一条边 return [in_degree[conflict_edge[1]], conflict_edge[1]] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \alpha(n))$,其中 $n$ 是边的数量,$\alpha(n)$ 是阿克曼函数的反函数,可以认为是常数。 - **空间复杂度**:$O(n)$,需要使用并查集和哈希表存储节点的父节点信息。 ================================================ FILE: docs/solutions/0600-0699/redundant-connection.md ================================================ # [0684. 冗余连接](https://leetcode.cn/problems/redundant-connection/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [0684. 冗余连接 - 力扣](https://leetcode.cn/problems/redundant-connection/) ## 题目大意 **描述**:一个 `n` 个节点的树(节点值为 `1~n`)添加一条边后就形成了图,添加的这条边不属于树中已经存在的边。图的信息记录存储与长度为 `n` 的二维数组 `edges`,`edges[i] = [ai, bi]` 表示图中在 `ai` 和 `bi` 之间存在一条边。 现在给定代表边信息的二维数组 `edges`。 **要求**:找到一条可以山区的边,使得删除后的剩余部分是一个有着 `n` 个节点的树。如果有多个答案,则返回数组 `edges` 中最后出现的边。 **说明**: - $n == edges.length$。 - $3 \le n \le 1000$。 - $edges[i].length == 2$。 - $1 \le ai < bi \le edges.length$。 - $ai ≠ bi$。 - $edges$ 中无重复元素。 - 给定的图是连通的。 **示例**: - 示例 1: ![img](https://pic.leetcode-cn.com/1626676174-hOEVUL-image.png) ```python 输入: edges = [[1,2], [1,3], [2,3]] 输出: [2,3] ``` - 示例 2: ![img](https://pic.leetcode-cn.com/1626676179-kGxcmu-image.png) ```python 输入: edges = [[1,2], [2,3], [3,4], [1,4], [1,5]] 输出: [1,4] ``` ## 解题思路 ### 思路 1:并查集 树可以看做是无环的图,这道题就是要找出那条添加边之后成环的边。可以考虑用并查集来做。 1. 从前向后遍历每一条边。 2. 如果边的两个节点不在同一个集合,就加入到一个集合(链接到同一个根节点)。 3. 如果边的节点已经出现在同一个集合里,说明边的两个节点已经连在一起了,再加入这条边一定会出现环,则这条边就是所求答案。 ### 思路 1:代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) self.parent[root_x] = root_y def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def findRedundantConnection(self, edges: List[List[int]]) -> List[int]: size = len(edges) union_find = UnionFind(size + 1) for edge in edges: if union_find.is_connected(edge[0], edge[1]): return edge union_find.union(edge[0], edge[1]) return None ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \alpha(n))$。其中 $n$ 是图中的节点个数,$\alpha$ 是反 `Ackerman` 函数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0600-0699/remove-9.md ================================================ # [0660. 移除 9](https://leetcode.cn/problems/remove-9/) - 标签:数学 - 难度:困难 ## 题目链接 - [0660. 移除 9 - 力扣](https://leetcode.cn/problems/remove-9/) ## 题目大意 **描述**: 从 $1$ 开始,移除包含数字 $9$ 的所有整数,例如 $9$,$19$,$29$,…… 这样就获得了一个新的整数数列:$1$,$2$,$3$,$4$,$5$,$6$,$7$,$8$,$10$,$11$,…… 给定一个整数 $n$。 **要求**: 返回新数列中第 $n$ 个数字(下标从 $1$ 开始)。 **说明**: - $1 \le n \le 8 \times 10^8$。 **示例**: - 示例 1: ```python 输入:n = 9 输出:10 ``` - 示例 2: ```python 输入:n = 10 输出:11 ``` ## 解题思路 ### 思路 1:进制转换 #### 思路 1:算法描述 移除所有包含数字 $9$ 的整数后,剩余的数字序列实际上是一个九进制数系统。 **核心思路**: - 原始序列:$1, 2, 3, 4, 5, 6, 7, 8, 10, 11, ...$(跳过所有包含 $9$ 的数字) - 这相当于九进制:$1, 2, 3, 4, 5, 6, 7, 8, 10, 11, ...$ - 第 $n$ 个数字就是 $n$ 的九进制表示。 **算法步骤**: 1. 将 $n$ 转换为九进制。 2. 返回转换后的结果。 #### 思路 1:代码 ```python class Solution: def newInteger(self, n: int) -> int: # 将 n 转换为九进制 result = 0 base = 1 while n > 0: result += (n % 9) * base n //= 9 base *= 10 return result ``` #### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$,需要将 $n$ 转换为九进制。 - **空间复杂度**:$O(1)$,只使用了常数额外空间。 ================================================ FILE: docs/solutions/0600-0699/repeated-string-match.md ================================================ # [0686. 重复叠加字符串匹配](https://leetcode.cn/problems/repeated-string-match/) - 标签:字符串、字符串匹配 - 难度:中等 ## 题目链接 - [0686. 重复叠加字符串匹配 - 力扣](https://leetcode.cn/problems/repeated-string-match/) ## 题目大意 **描述**:给定两个字符串 `a` 和 `b`。 **要求**:寻找重复叠加字符串 `a` 的最小次数,使得字符串 `b` 成为叠加后的字符串 `a` 的子串,如果不存在则返回 `-1`。 **说明**: - 字符串 `"abc"` 重复叠加 `0` 次是 `""`,重复叠加 `1` 次是 `"abc"`,重复叠加 `2` 次是 `"abcabc"`。 - $1 \le a.length \le 10^4$。 - $1 \le b.length \le 10^4$。 - `a` 和 `b` 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:a = "abcd", b = "cdabcdab" 输出:3 解释:a 重复叠加三遍后为 "abcdabcdabcd", 此时 b 是其子串。 ``` - 示例 2: ```python 输入:a = "a", b = "aa" 输出:2 ``` ## 解题思路 ### 思路 1:KMP 算法 假设字符串 `a` 的长度为 `n`,`b` 的长度为 `m`。 把 `b` 看做是模式串,把字符串 `a` 叠加后的字符串看做是文本串,这道题就变成了单模式串匹配问题。 我们可以模拟叠加字符串 `a` 后进行单模式串匹配问题。模拟叠加字符串可以通过在遍历字符串匹配时对字符串 `a` 的长度 `n` 取余来实现。 那么问题关键点就变为了如何高效的进行单模式串匹配,以及字符串循环匹配的退出条件是什么。 **单模式串匹配问题**:可以用 KMP 算法来做。 **循环匹配退出条件问题**:假设我们用 `i` 遍历 `a` 叠加后字符串,用 `j` 遍历字符串 `b`。如果字符串 `b` 是 `a` 叠加后字符串的子串,那么 `b` 有两种可能: 1. `b` 直接是原字符串 `a` 的子串:这种情况下,最多遍历到 `len(a)`。 2. `b` 是 `a` 叠加后的字符串的子串: 1. 最多遍历到 `len(a) + len(b)`,可以写为 `while i < len(a) + len(b):`,当 `i == len(a) + len(b)` 时跳出循环。 2. 也可以写为 `while i - j < len(a):`,这种写法中 `i - j ` 表示的是字符匹配开始的位置,如果匹配到 `len(a)` 时(即 `i - j == len(a)` 时)最开始位置的字符仍没有匹配,那么 `b` 也不可能是 `a` 叠加后的字符串的子串了,此时跳出循环。 最后我们需要计算一下重复叠加字符串 `a` 的最小次数。假设 `index` 使我们求出的匹配位置。 1. 如果 `index == -1`,则说明 `b` 不可能是 `a` 叠加后的字符串的子串,返回 `False`。 2. 如果 `len(a) - index >= len(b)`,则说明匹配位置未超过字符串 `a` 的长度,叠加 `1` 次(字符串 `a` 本身)就可以匹配。 3. 如果 `len(a) - index < len(b)`,则说明需要叠加才能匹配。此时最小叠加次数为 $\lfloor \frac{index + len(b) - 1}{len(a)} \rfloor + 1$。其中 `index` 代笔匹配开始前的字符串长度,加上 `len(b)` 后就是匹配到字符串 `b` 结束时最少需要的字符数,再 `-1` 是为了向下取整。 除以 `len(a)` 表示至少需要几个 `a`, 因为是向下取整,所以最后要加上 `1`。写成代码就是:`(index + len(b) - 1) // len(a) + 1`。 ### 思路 1:代码 ```python class Solution: # KMP 匹配算法,T 为文本串,p 为模式串 def kmp(self, T: str, p: str) -> int: n, m = len(T), len(p) next = self.generateNext(p) i, j = 0, 0 while i - j < n: while j > 0 and T[i % n] != p[j]: j = next[j - 1] if T[i % n] == p[j]: j += 1 if j == m: return i - m + 1 i += 1 return -1 def generateNext(self, p: str): m = len(p) next = [0 for _ in range(m)] left = 0 for right in range(1, m): while left > 0 and p[left] != p[right]: left = next[left - 1] if p[left] == p[right]: left += 1 next[right] = left return next def repeatedStringMatch(self, a: str, b: str) -> int: len_a = len(a) len_b = len(b) index = self.kmp(a, b) if index == -1: return -1 if len_a - index >= len_b: return 1 return (index + len(b) - 1) // len(a) + 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中文本串 $a$ 的长度为 $n$,模式串 $b$ 的长度为 $m$。 - **空间复杂度**:$O(m)$。 ================================================ FILE: docs/solutions/0600-0699/replace-words.md ================================================ # [0648. 单词替换](https://leetcode.cn/problems/replace-words/) - 标签:字典树、数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [0648. 单词替换 - 力扣](https://leetcode.cn/problems/replace-words/) ## 题目大意 **描述**:给定一个由许多词根组成的字典列表 `dictionary`,以及一个句子字符串 `sentence`。 **要求**:将句子中有词根的单词用词根替换掉。如果单词有很多词根,则用最短的词根替换掉他。最后输出替换之后的句子。 **说明**: - $1 \le dictionary.length \le 1000$。 - $1 \le dictionary[i].length \le 100$。 - `dictionary[i]` 仅由小写字母组成。 - $1 \le sentence.length \le 10^6$。 - `sentence` 仅由小写字母和空格组成。 - `sentence` 中单词的总量在范围 $[1, 1000]$ 内。 - `sentence` 中每个单词的长度在范围 $[1, 1000]$ 内。 - `sentence` 中单词之间由一个空格隔开。 - `sentence` 没有前导或尾随空格。 **示例**: - 示例 1: ```python 输入:dictionary = ["cat","bat","rat"], sentence = "the cattle was rattled by the battery" 输出:"the cat was rat by the bat" ``` - 示例 2: ```python 输入:dictionary = ["a","b","c"], sentence = "aadsfasf absbs bbab cadsfafs" 输出:"a a b c" ``` ## 解题思路 ### 思路 1:字典树 1. 构造一棵字典树。 2. 将所有的词根存入到前缀树(字典树)中。 3. 然后在树上查找每个单词的最短词根。 ### 思路 1:代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> str: """ Returns if the word is in the trie. """ cur = self index = 0 for ch in word: if ch not in cur.children: return word cur = cur.children[ch] index += 1 if cur.isEnd: break return word[:index] class Solution: def replaceWords(self, dictionary: List[str], sentence: str) -> str: trie_tree = Trie() for word in dictionary: trie_tree.insert(word) words = sentence.split(" ") size = len(words) for i in range(size): word = words[i] words[i] = trie_tree.search(word) return ' '.join(words) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(|dictionary| + |sentence|)$。其中 $|dictionary|$ 是字符串数组 `dictionary` 中的字符总数,$|sentence|$ 是字符串 `sentence` 的字符总数。 - **空间复杂度**:$O(|dictionary| + |sentence|)$。 ================================================ FILE: docs/solutions/0600-0699/robot-return-to-origin.md ================================================ # [0657. 机器人能否返回原点](https://leetcode.cn/problems/robot-return-to-origin/) - 标签:字符串、模拟 - 难度:简单 ## 题目链接 - [0657. 机器人能否返回原点 - 力扣](https://leetcode.cn/problems/robot-return-to-origin/) ## 题目大意 **描述**: 在二维平面上,有一个机器人从原点 $(0, 0)$ 开始。给出它的移动顺序,判断这个机器人在完成移动后是否在 $(0, 0)$ 处结束。 移动顺序由字符串 $moves$ 表示。字符 $move[i]$ 表示其第 $i$ 次移动。机器人的有效动作有 R(右),L(左),U(上)和 D(下)。 **要求**: 如果机器人在完成所有动作后返回原点,则返回 true。否则,返回 false。 **说明**: - 注意:机器人「面朝」的方向无关紧要。 `R` 将始终使机器人向右移动一次,`L` 将始终向左移动等。此外,假设每次移动机器人的移动幅度相同。 - $1 \le moves.length \le 2 \times 10^{4}$。 - moves 只包含字符 `'U'`, `'D'`, `'L'` 和 `'R'`。 **示例**: - 示例 1: ```python 输入: moves = "UD" 输出: true 解释:机器人向上移动一次,然后向下移动一次。所有动作都具有相同的幅度,因此它最终回到它开始的原点。因此,我们返回 true。 ``` - 示例 2: ```python 输入: moves = "LL" 输出: false 解释:机器人向左移动两次。它最终位于原点的左侧,距原点有两次 “移动” 的距离。我们返回 false,因为它在移动结束时没有返回原点。 ``` ## 解题思路 ### 思路 1:模拟 这道题目非常简单,只需要统计上下左右移动的次数,判断是否能回到原点。 1. 初始化坐标 $(x, y) = (0, 0)$。 2. 遍历移动序列中的每个字符: - 如果是 `'U'`,$y$ 加 1(向上移动)。 - 如果是 `'D'`,$y$ 减 1(向下移动)。 - 如果是 `'L'`,$x$ 减 1(向左移动)。 - 如果是 `'R'`,$x$ 加 1(向右移动)。 3. 判断最终坐标是否为 $(0, 0)$。 ### 思路 1:代码 ```python class Solution: def judgeCircle(self, moves: str) -> bool: x, y = 0, 0 for move in moves: if move == 'U': y += 1 elif move == 'D': y -= 1 elif move == 'L': x -= 1 elif move == 'R': x += 1 return x == 0 and y == 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是移动序列的长度。需要遍历所有移动指令。 - **空间复杂度**:$O(1)$,只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0600-0699/second-minimum-node-in-a-binary-tree copy.md ================================================ # [0671. 二叉树中第二小的节点](https://leetcode.cn/problems/second-minimum-node-in-a-binary-tree/) - 标签:树、深度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0671. 二叉树中第二小的节点 - 力扣](https://leetcode.cn/problems/second-minimum-node-in-a-binary-tree/) ## 题目大意 **描述**: 给定一个非空特殊的二叉树,每个节点都是正数,并且每个节点的子节点数量只能为 $2$ 或 $0$。如果一个节点有两个子节点的话,那么该节点的值等于两个子节点中较小的一个。 更正式地说,即 $root.val = min(root.left.val, root.right.val)$ 总成立。 **要求**: 给出这样的一个二叉树,你需要输出所有节点中的「第二小的值」。 如果第二小的值不存在的话,输出 $-1$。 **说明**: - 树中节点数目在范围 $[1, 25]$ 内。 - $1 \le Node.val \le 2^{31} - 1$。 - 对于树中每个节点 $root.val == min(root.left.val, root.right.val)$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/15/smbt1.jpg) ```python 输入:root = [2,2,5,null,null,5,7] 输出:5 解释:最小的值是 2 ,第二小的值是 5 。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/10/15/smbt2.jpg) ```python 输入:root = [2,2,2] 输出:-1 解释:最小的值是 2, 但是不存在第二小的值。 ``` ## 解题思路 ### 思路 1:深度优先搜索 根据题目描述,二叉树的特性是根节点的值等于两个子节点中较小的值。因此根节点一定是最小值。我们需要找到第二小的值。 1. 根节点的值 $root.val$ 是最小值。 2. 使用深度优先搜索遍历整棵树,寻找第一个大于 $root.val$ 的值。 3. 在遍历过程中: - 如果当前节点的值大于 $root.val$,说明找到了一个候选值,更新答案。 - 如果当前节点的值等于 $root.val$,继续递归搜索其子树。 4. 如果没有找到第二小的值,返回 $-1$。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def findSecondMinimumValue(self, root: Optional[TreeNode]) -> int: self.ans = float('inf') min_val = root.val def dfs(node): if not node: return # 如果当前节点值大于最小值且小于当前答案,更新答案 if min_val < node.val < self.ans: self.ans = node.val # 只有当节点值等于最小值时,才需要继续搜索其子树 elif node.val == min_val: dfs(node.left) dfs(node.right) dfs(root) return self.ans if self.ans != float('inf') else -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数。最坏情况下需要遍历所有节点。 - **空间复杂度**:$O(h)$,其中 $h$ 是二叉树的高度。递归调用栈的深度最多为树的高度。 ================================================ FILE: docs/solutions/0600-0699/second-minimum-node-in-a-binary-tree.md ================================================ # [0671. 二叉树中第二小的节点](https://leetcode.cn/problems/second-minimum-node-in-a-binary-tree/) - 标签:树、深度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0671. 二叉树中第二小的节点 - 力扣](https://leetcode.cn/problems/second-minimum-node-in-a-binary-tree/) ## 题目大意 **描述**: 给定一个非空特殊的二叉树,每个节点都是正数,并且每个节点的子节点数量只能为 $2$ 或 $0$。如果一个节点有两个子节点的话,那么该节点的值等于两个子节点中较小的一个。 更正式地说,即 $root.val = min(root.left.val, root.right.val)$ 总成立。 **要求**: 给出这样的一个二叉树,你需要输出所有节点中的「第二小的值」。 如果第二小的值不存在的话,输出 $-1$。 **说明**: - 树中节点数目在范围 $[1, 25]$ 内。 - $1 \le Node.val \le 2^{31} - 1$。 - 对于树中每个节点 $root.val == min(root.left.val, root.right.val)$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/10/15/smbt1.jpg) ```python 输入:root = [2,2,5,null,null,5,7] 输出:5 解释:最小的值是 2 ,第二小的值是 5 。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/10/15/smbt2.jpg) ```python 输入:root = [2,2,2] 输出:-1 解释:最小的值是 2, 但是不存在第二小的值。 ``` ## 解题思路 ### 思路 1:深度优先搜索 根据题目描述,二叉树的特性是根节点的值等于两个子节点中较小的值。因此根节点一定是最小值。我们需要找到第二小的值。 1. 根节点的值 $root.val$ 是最小值。 2. 使用深度优先搜索遍历整棵树,寻找第一个大于 $root.val$ 的值。 3. 在遍历过程中: - 如果当前节点的值大于 $root.val$,说明找到了一个候选值,更新答案。 - 如果当前节点的值等于 $root.val$,继续递归搜索其子树。 4. 如果没有找到第二小的值,返回 $-1$。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def findSecondMinimumValue(self, root: Optional[TreeNode]) -> int: self.ans = float('inf') min_val = root.val def dfs(node): if not node: return # 如果当前节点值大于最小值且小于当前答案,更新答案 if min_val < node.val < self.ans: self.ans = node.val # 只有当节点值等于最小值时,才需要继续搜索其子树 elif node.val == min_val: dfs(node.left) dfs(node.right) dfs(root) return self.ans if self.ans != float('inf') else -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数。最坏情况下需要遍历所有节点。 - **空间复杂度**:$O(h)$,其中 $h$ 是二叉树的高度。递归调用栈的深度最多为树的高度。 ================================================ FILE: docs/solutions/0600-0699/set-mismatch.md ================================================ # [0645. 错误的集合](https://leetcode.cn/problems/set-mismatch/) - 标签:位运算、数组、哈希表、排序 - 难度:简单 ## 题目链接 - [0645. 错误的集合 - 力扣](https://leetcode.cn/problems/set-mismatch/) ## 题目大意 **描述**: 集合 $s$ 包含从 $1$ 到 $n$ 的整数。不幸的是,因为数据错误,导致集合里面某一个数字复制了成了集合里面的另外一个数字的值,导致集合「丢失了一个数字」并且「有一个数字重复」。 给定一个数组 $nums$ 代表了集合 $S$ 发生错误后的结果。 **要求**: 找出重复出现的整数,再找到丢失的整数,将它们以数组的形式返回。 **说明**: - $2 \le nums.length \le 10^{4}$。 - $1 \le nums[i] \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,2,4] 输出:[2,3] ``` - 示例 2: ```python 输入:nums = [1,1] 输出:[1,2] ``` ## 解题思路 ### 思路 1:哈希表 这道题目要求找出重复的数字和丢失的数字。可以使用哈希表记录每个数字出现的次数。 1. 使用哈希表 $freq$ 记录数组中每个数字出现的次数。 2. 遍历 $1$ 到 $n$: - 如果 $freq[i] = 2$,说明 $i$ 是重复的数字。 - 如果 $freq[i] = 0$,说明 $i$ 是丢失的数字。 3. 返回 `[重复的数字, 丢失的数字]`。 ### 思路 1:代码 ```python class Solution: def findErrorNums(self, nums: List[int]) -> List[int]: from collections import Counter n = len(nums) freq = Counter(nums) duplicate, missing = 0, 0 for i in range(1, n + 1): if freq[i] == 2: duplicate = i elif freq[i] == 0: missing = i return [duplicate, missing] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。需要遍历数组一次统计频率,再遍历 $1$ 到 $n$ 找出重复和丢失的数字。 - **空间复杂度**:$O(n)$,需要使用哈希表存储每个数字的频率。 ### 思路 2:数学方法 利用数学方法,通过求和和平方和来找出重复和丢失的数字。 1. 设重复的数字为 $x$,丢失的数字为 $y$。 2. 计算数组的和 $sum\underline{~}nums$ 和 $1$ 到 $n$ 的和 $sum\underline{~}n$,有:$sum\underline{~}nums - sum\underline{~}n = x - y$。 3. 计算数组的平方和 $sum\underline{~}sq\underline{~}nums$ 和 $1$ 到 $n$ 的平方和 $sum\underline{~}sq\underline{~}n$,有:$sum\underline{~}sq\underline{~}nums - sum\underline{~}sq\underline{~}n = x^2 - y^2 = (x + y)(x - y)$。 4. 通过这两个方程可以求出 $x$ 和 $y$。 ### 思路 2:代码 ```python class Solution: def findErrorNums(self, nums: List[int]) -> List[int]: n = len(nums) # 计算数组的和与平方和 sum_nums = sum(nums) sum_sq_nums = sum(x * x for x in nums) # 计算 1 到 n 的和与平方和 sum_n = n * (n + 1) // 2 sum_sq_n = n * (n + 1) * (2 * n + 1) // 6 # x - y = sum_nums - sum_n diff = sum_nums - sum_n # x^2 - y^2 = sum_sq_nums - sum_sq_n # (x + y)(x - y) = sum_sq_nums - sum_sq_n # x + y = (sum_sq_nums - sum_sq_n) / (x - y) sum_xy = (sum_sq_nums - sum_sq_n) // diff # 求解 x 和 y duplicate = (diff + sum_xy) // 2 missing = sum_xy - duplicate return [duplicate, missing] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。需要遍历数组计算和与平方和。 - **空间复杂度**:$O(1)$,只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0600-0699/shopping-offers.md ================================================ # [0638. 大礼包](https://leetcode.cn/problems/shopping-offers/) - 标签:位运算、记忆化搜索、数组、动态规划、回溯、状态压缩 - 难度:中等 ## 题目链接 - [0638. 大礼包 - 力扣](https://leetcode.cn/problems/shopping-offers/) ## 题目大意 **描述**: 在 LeetCode 商店中,有 $n$ 件在售的物品。每件物品都有对应的价格。然而,也有一些大礼包,每个大礼包以优惠的价格捆绑销售一组物品。 给定一个整数数组 $price$ 表示物品价格,其中 $price[i]$ 是第 $i$ 件物品的价格。另有一个整数数组 $needs$ 表示购物清单,其中 $needs[i]$ 是需要购买第 $i$ 件物品的数量。 还有一个数组 $special$ 表示大礼包,$special[i]$ 的长度为 $n + 1$,其中 $special[i][j]$ 表示第 $i$ 个大礼包中内含第 $j$ 件物品的数量,且 $special[i][n]$ (也就是数组中的最后一个整数)为第 $i$ 个大礼包的价格。 **要求**: 返回「确切」满足购物清单所需花费的最低价格,你可以充分利用大礼包的优惠活动。你不能购买超出购物清单指定数量的物品,即使那样会降低整体价格。任意大礼包可无限次购买。 **说明**: - $n == price.length == needs.length$。 - $1 \le n \le 6$。 - $0 \le price[i], needs[i] \le 10$。 - $1 \le special.length \le 10^{3}$。 - $special[i].length == n + 1$。 - $0 \le special[i][j] \le 50$。 - 生成的输入对于 $0 \le j \le n - 1$ 至少有一个 $special[i][j]$ 非零。 **示例**: - 示例 1: ```python 输入:price = [2,5], special = [[3,0,5],[1,2,10]], needs = [3,2] 输出:14 解释:有 A 和 B 两种物品,价格分别为 ¥2 和 ¥5 。 大礼包 1 ,你可以以 ¥5 的价格购买 3A 和 0B 。 大礼包 2 ,你可以以 ¥10 的价格购买 1A 和 2B 。 需要购买 3 个 A 和 2 个 B , 所以付 ¥10 购买 1A 和 2B(大礼包 2),以及 ¥4 购买 2A 。 ``` - 示例 2: ```python 输入:price = [2,3,4], special = [[1,1,0,4],[2,2,1,9]], needs = [1,2,1] 输出:11 解释:A ,B ,C 的价格分别为 ¥2 ,¥3 ,¥4 。 可以用 ¥4 购买 1A 和 1B ,也可以用 ¥9 购买 2A ,2B 和 1C 。 需要买 1A ,2B 和 1C ,所以付 ¥4 买 1A 和 1B(大礼包 1),以及 ¥3 购买 1B , ¥4 购买 1C 。 不可以购买超出待购清单的物品,尽管购买大礼包 2 更加便宜。 ``` ## 解题思路 ### 思路 1:记忆化搜索 这道题目要求计算满足购物清单的最低价格。可以使用记忆化搜索(动态规划)来避免重复计算。 1. 首先过滤掉不划算的大礼包(大礼包价格大于等于单独购买的价格)。 2. 使用记忆化搜索,状态为当前的购物需求 $needs$。 3. 对于每个状态,有两种选择: - 不使用大礼包,直接按单价购买所有物品。 - 尝试使用每个大礼包,如果大礼包中的物品数量不超过需求,则使用该大礼包,并递归计算剩余需求的最低价格。 4. 返回所有选择中的最小值。 ### 思路 1:代码 ```python class Solution: def shoppingOffers(self, price: List[int], special: List[List[int]], needs: List[int]) -> int: from functools import lru_cache n = len(price) # 过滤掉不划算的大礼包 filtered_special = [] for offer in special: # 计算大礼包的原价 original_price = sum(offer[i] * price[i] for i in range(n)) # 如果大礼包价格更优惠,保留 if offer[n] < original_price: filtered_special.append(offer) @lru_cache(None) def dfs(needs_tuple): needs = list(needs_tuple) # 不使用大礼包,直接购买 min_cost = sum(needs[i] * price[i] for i in range(n)) # 尝试使用每个大礼包 for offer in filtered_special: # 检查是否可以使用该大礼包 can_use = True new_needs = [] for i in range(n): if needs[i] < offer[i]: can_use = False break new_needs.append(needs[i] - offer[i]) if can_use: # 使用该大礼包,递归计算剩余需求 min_cost = min(min_cost, offer[n] + dfs(tuple(new_needs))) return min_cost return dfs(tuple(needs)) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times k^n)$,其中 $m$ 是大礼包的数量,$n$ 是物品的种类,$k$ 是每种物品的最大需求量。状态数最多为 $k^n$,每个状态需要尝试 $m$ 个大礼包。 - **空间复杂度**:$O(k^n)$,需要使用记忆化存储所有状态的结果。 ================================================ FILE: docs/solutions/0600-0699/smallest-range-covering-elements-from-k-lists.md ================================================ # [0632. 最小区间](https://leetcode.cn/problems/smallest-range-covering-elements-from-k-lists/) - 标签:贪心、数组、哈希表、排序、滑动窗口、堆(优先队列) - 难度:困难 ## 题目链接 - [0632. 最小区间 - 力扣](https://leetcode.cn/problems/smallest-range-covering-elements-from-k-lists/) ## 题目大意 **描述**: 你有 $k$ 个「非递减排列」的整数列表。 **要求**: 找到一个 最小 区间,使得 $k$ 个列表中的每个列表至少有一个数包含在其中。 **说明**: - 我们定义如果 $b - a < d - c$ 或者在 $b - a == d - c$ 时 $a < c$,则区间 $[a, b]$ 比 $[c, d]$ 小。 - $nums.length == k$。 - $1 \le k \le 3500$。 - $1 \le nums[i].length \le 50$。 - $-10^{5} \le nums[i][j] \le 10^{5}$。 - $nums[i]$ 按非递减顺序排列。 **示例**: - 示例 1: ```python 输入:nums = [[4,10,15,24,26], [0,9,12,20], [5,18,22,30]] 输出:[20,24] 解释: 列表 1:[4, 10, 15, 24, 26],24 在区间 [20,24] 中。 列表 2:[0, 9, 12, 20],20 在区间 [20,24] 中。 列表 3:[5, 18, 22, 30],22 在区间 [20,24] 中。 ``` - 示例 2: ```python 输入:nums = [[1,2,3],[1,2,3],[1,2,3]] 输出:[1,1] ``` ## 解题思路 ### 思路 1:堆(优先队列) 这道题目要求找到一个最小区间,使得 $k$ 个列表中的每个列表至少有一个数包含在其中。使用最小堆维护当前区间。 1. 初始化最小堆,将每个列表的第一个元素加入堆中,元素格式为 $(value, list\_idx, element\_idx)$。 2. 记录当前堆中的最大值 $max\_val$。 3. 当堆中有 $k$ 个元素时(每个列表都有元素在堆中): - 取出堆顶元素(最小值)$min\_val$。 - 更新最小区间 $[min\_val, max\_val]$。 - 如果该元素所在列表还有下一个元素,将下一个元素加入堆,并更新 $max\_val$。 - 如果该元素所在列表没有下一个元素,结束循环。 4. 返回最小区间。 ### 思路 1:代码 ```python class Solution: def smallestRange(self, nums: List[List[int]]) -> List[int]: import heapq # 初始化堆,加入每个列表的第一个元素 heap = [] max_val = float('-inf') for i in range(len(nums)): heapq.heappush(heap, (nums[i][0], i, 0)) max_val = max(max_val, nums[i][0]) # 初始化结果区间 result = [float('-inf'), float('inf')] while len(heap) == len(nums): min_val, list_idx, element_idx = heapq.heappop(heap) # 更新最小区间 if max_val - min_val < result[1] - result[0]: result = [min_val, max_val] # 如果当前列表还有下一个元素 if element_idx + 1 < len(nums[list_idx]): next_val = nums[list_idx][element_idx + 1] heapq.heappush(heap, (next_val, list_idx, element_idx + 1)) max_val = max(max_val, next_val) else: # 当前列表已经遍历完,无法继续 break return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times k \times \log k)$,其中 $n$ 是所有列表的平均长度,$k$ 是列表的数量。需要遍历所有元素,每次堆操作的时间复杂度为 $O(\log k)$。 - **空间复杂度**:$O(k)$,堆中最多存储 $k$ 个元素。 ================================================ FILE: docs/solutions/0600-0699/solve-the-equation.md ================================================ # [0640. 求解方程](https://leetcode.cn/problems/solve-the-equation/) - 标签:数学、字符串、模拟 - 难度:中等 ## 题目链接 - [0640. 求解方程 - 力扣](https://leetcode.cn/problems/solve-the-equation/) ## 题目大意 **描述**: 给定一个方程 $equation$。 **要求**: 求解一个给定的方程,将 $x$ 以字符串 `"x=#value"` 的形式返回。该方程仅包含 `'+'`,`'-'` 操作,变量 $x$ 和其对应系数。 如果方程没有解或存在的解不为整数,请返回 `"No $solution$"` 。如果方程有无限解,则返回 `"Infinite solutions"`。 题目保证,如果方程中只有一个解,则 `'x'` 的值是一个整数。 **说明**: - $3 \le equation.length \le 10^{3}$。 - $equation$ 只有一个 `'='`。 - 方程由绝对值在 $[0, 10^{3}]$ 范围内且无任何前导零的整数和变量 `'x'` 组成。 **示例**: - 示例 1: ```python 输入: equation = "x+5-3+x=6+x-2" 输出: "x=2" ``` - 示例 2: ```python 输入: equation = "x=x" 输出: "Infinite solutions" ``` ## 解题思路 ### 思路 1:字符串解析 + 模拟 这道题目要求求解一元一次方程。需要解析方程字符串,分别统计等号左右两边的 $x$ 的系数和常数项。 1. 将方程按 '=' 分割成左右两部分。 2. 对于每一部分,解析出 $x$ 的系数和常数项: - 遍历字符串,识别数字、符号和变量 $x$。 - 如果是 $x$ 的系数,累加到 $x\_coef$。 - 如果是常数,累加到 $const$。 3. 将右边的系数和常数移到左边(符号取反)。 4. 根据最终的系数和常数判断: - 如果系数为 0 且常数为 0,返回 "Infinite solutions"。 - 如果系数为 0 且常数不为 0,返回 "No solution"。 - 否则,返回 "x=#value",其中 value = -常数 / 系数。 ### 思路 1:代码 ```python class Solution: def solveEquation(self, equation: str) -> str: def parse(s): """解析表达式,返回 x 的系数和常数项""" x_coef = 0 const = 0 i = 0 n = len(s) while i < n: # 读取符号 sign = 1 if s[i] == '+': i += 1 elif s[i] == '-': sign = -1 i += 1 # 读取数字 num = 0 has_num = False while i < n and s[i].isdigit(): num = num * 10 + int(s[i]) has_num = True i += 1 # 判断是 x 还是常数 if i < n and s[i] == 'x': # 如果没有数字,系数为 1 x_coef += sign * (num if has_num else 1) i += 1 else: # 常数项 const += sign * num return x_coef, const # 分割等号左右两边 left, right = equation.split('=') # 解析左右两边 left_x, left_const = parse(left) right_x, right_const = parse(right) # 移项:左边 - 右边 = 0 x_coef = left_x - right_x const = left_const - right_const # 判断解的情况 if x_coef == 0: if const == 0: return "Infinite solutions" else: return "No solution" else: # x = -const / x_coef return f"x={-const // x_coef}" ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是方程字符串的长度。需要遍历字符串解析表达式。 - **空间复杂度**:$O(1)$,只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0600-0699/split-array-into-consecutive-subsequences.md ================================================ # [0659. 分割数组为连续子序列](https://leetcode.cn/problems/split-array-into-consecutive-subsequences/) - 标签:贪心、数组、哈希表、堆(优先队列) - 难度:中等 ## 题目链接 - [0659. 分割数组为连续子序列 - 力扣](https://leetcode.cn/problems/split-array-into-consecutive-subsequences/) ## 题目大意 **描述**: 给定一个按「非递减顺序」排列的整数数组 $nums$。 **要求**: 判断是否能在将 $nums$ 分割成 一个或多个子序列 的同时满足下述两个条件: - 每个子序列都是一个「连续递增序列」(即,每个整数「恰好」比前一个整数大 $1$)。 - 所有子序列的长度「至少」为 $3$。 如果可以分割 $nums$ 并满足上述条件,则返回 true ;否则,返回 false。 **说明**: - $1 \le nums.length \le 10^{4}$。 - $-10^{3} \le nums[i] \le 10^{3}$。 - $nums$ 按非递减顺序排列。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,3,4,5] 输出:true 解释:nums 可以分割成以下子序列: [1,2,3,3,4,5] --> 1, 2, 3 [1,2,3,3,4,5] --> 3, 4, 5 ``` - 示例 2: ```python 输入:nums = [1,2,3,3,4,4,5,5] 输出:true 解释:nums 可以分割成以下子序列: [1,2,3,3,4,4,5,5] --> 1, 2, 3, 4, 5 [1,2,3,3,4,4,5,5] --> 3, 4, 5 ``` ## 解题思路 ### 思路 1:贪心 + 哈希表 这道题目要求将数组分割成若干个长度至少为 3 的连续递增子序列。使用贪心策略:优先将当前数字接到已有的子序列后面,如果不能接到已有子序列,则尝试创建新的子序列。 1. 使用哈希表 $freq$ 记录每个数字的剩余可用次数。 2. 使用哈希表 $need$ 记录以某个数字结尾的子序列需要的下一个数字的数量。 3. 遍历数组中的每个数字 $num$: - 如果 $freq[num] = 0$,说明该数字已经被使用完,跳过。 - 如果 $need[num] > 0$,说明存在以 $num - 1$ 结尾的子序列需要 $num$,将 $num$ 接到该子序列后面: - $need[num]$ 减 1,$freq[num]$ 减 1。 - $need[num + 1]$ 加 1(该子序列现在需要 $num + 1$)。 - 否则,尝试创建新的子序列 $[num, num + 1, num + 2]$: - 检查 $freq[num + 1]$ 和 $freq[num + 2]$ 是否大于 0。 - 如果可以创建,将这三个数字的频率减 1,并将 $need[num + 3]$ 加 1。 - 如果不能创建,返回 $False$。 4. 如果所有数字都能成功分配,返回 $True$。 ### 思路 1:代码 ```python class Solution: def isPossible(self, nums: List[int]) -> bool: from collections import defaultdict freq = defaultdict(int) # 记录每个数字的剩余可用次数 need = defaultdict(int) # 记录以某个数字结尾的子序列需要的下一个数字的数量 # 统计每个数字的频率 for num in nums: freq[num] += 1 for num in nums: if freq[num] == 0: continue # 优先将 num 接到已有的子序列后面 if need[num] > 0: need[num] -= 1 freq[num] -= 1 need[num + 1] += 1 # 尝试创建新的子序列 [num, num+1, num+2] elif freq[num + 1] > 0 and freq[num + 2] > 0: freq[num] -= 1 freq[num + 1] -= 1 freq[num + 2] -= 1 need[num + 3] += 1 else: # 无法分配当前数字 return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。需要遍历数组两次。 - **空间复杂度**:$O(n)$,需要使用两个哈希表存储频率和需求信息。 ================================================ FILE: docs/solutions/0600-0699/stickers-to-spell-word.md ================================================ # [0691. 贴纸拼词](https://leetcode.cn/problems/stickers-to-spell-word/) - 标签:位运算、数组、字符串、动态规划、回溯、状态压缩 - 难度:困难 ## 题目链接 - [0691. 贴纸拼词 - 力扣](https://leetcode.cn/problems/stickers-to-spell-word/) ## 题目大意 **描述**:给定一个字符串数组 $stickers$ 表示不同的贴纸,其中 $stickers[i]$ 表示第 $i$ 张贴纸上的小写英文单词。再给定一个字符串 $target$。为了拼出给定字符串 $target$,我们需要从贴纸中切割单个字母并重新排列它们。贴纸的数量是无限的,可以重复多次使用。 **要求**:返回需要拼出 $target$ 的最小贴纸数量。如果任务不可能,则返回 $-1$。 **说明**: - 在所有的测试用例中,所有的单词都是从 $1000$ 个最常见的美国英语单词中随机选择的,并且 $target$ 被选择为两个随机单词的连接。 - $n == stickers.length$。 - $1 \le n \le 50$。 - $1 \le stickers[i].length \le 10$。 - $1 \le target.length \le 15$。 - $stickers[i]$ 和 $target$ 由小写英文单词组成。 **示例**: - 示例 1: ```python 输入:stickers = ["with","example","science"], target = "thehat" 输出:3 解释: 我们可以使用 2 个 "with" 贴纸,和 1 个 "example" 贴纸。 把贴纸上的字母剪下来并重新排列后,就可以形成目标 “thehat“ 了。 此外,这是形成目标字符串所需的最小贴纸数量。 ``` - 示例 2: ```python 输入:stickers = ["notice","possible"], target = "basicbasic" 输出:-1 解释:我们不能通过剪切给定贴纸的字母来形成目标“basicbasic”。 ``` ## 解题思路 ### 思路 1:状态压缩 DP + 广度优先搜索 根据题意,$target$ 的长度最大为 $15$,所以我们可以使用一个长度最多为 $15$ 位的二进制数 $state$ 来表示 $target$ 的某个子序列,如果 $state$ 第 $i$ 位二进制值为 $1$,则说明 $target$ 的第 $i$ 个字母被选中。 然后我们从初始状态 $state = 0$(没有选中 $target$ 中的任何字母)开始进行广度优先搜索遍历。 在广度优先搜索过程中,对于当前状态 $cur\_state$,我们遍历所有贴纸的所有字母,如果当前字母可以拼到 $target$ 中的某个位置上,则更新状态 $next\_state$ 为「选中 $target$ 中对应位置上的字母」。 为了得到最小最小贴纸数量,我们可以使用动态规划的方法,定义 $dp[state]$ 表示为到达 $state$ 状态需要的最小贴纸数量。 那么在广度优先搜索中,在更新状态时,同时进行状态转移,即 $dp[next\_state] = dp[cur\_state] + 1$。 > 注意:在进行状态转移时,要跳过 $dp[next\_state]$ 已经有值的情况。 这样在到达状态 $1 \text{ <}\text{< } len(target) - 1$ 时,所得到的 $dp[1 \text{ <}\text{< } len(target) - 1]$ 即为答案。 如果最终到达不了 $dp[1 \text{ <}\text{< } len(target) - 1]$,则说明无法完成任务,返回 $-1$。 ### 思路 1:代码 ```python class Solution: def minStickers(self, stickers: List[str], target: str) -> int: size = len(target) states = 1 << size dp = [0 for _ in range(states)] queue = collections.deque([0]) while queue: cur_state = queue.popleft() for sticker in stickers: next_state = cur_state cnts = [0 for _ in range(26)] for ch in sticker: cnts[ord(ch) - ord('a')] += 1 for i in range(size): if cnts[ord(target[i]) - ord('a')] and next_state & (1 << i) == 0: next_state |= (1 << i) cnts[ord(target[i]) - ord('a')] -= 1 if dp[next_state] or next_state == 0: continue queue.append(next_state) dp[next_state] = dp[cur_state] + 1 if next_state == states - 1: return dp[next_state] return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n \times \sum_{i = 0}^{m - 1} len(stickers[i]) \times n$,其中 $n$ 为 $target$ 的长度,$m$ 为 $stickers$ 的元素个数。 - **空间复杂度**:$O(2^n)$。 ================================================ FILE: docs/solutions/0600-0699/strange-printer.md ================================================ # [0664. 奇怪的打印机](https://leetcode.cn/problems/strange-printer/) - 标签:字符串、动态规划 - 难度:困难 ## 题目链接 - [0664. 奇怪的打印机 - 力扣](https://leetcode.cn/problems/strange-printer/) ## 题目大意 **描述**:有一台奇怪的打印机,有以下两个功能: 1. 打印机每次只能打印由同一个字符组成的序列,比如:`"aaaa"`、`"bbb"`。 2. 每次可以从起始位置到结束的任意为止打印新字符,并且会覆盖掉原有字符。 现在给定一个字符串 $s$。 **要求**:计算这个打印机打印出字符串 $s$ 需要的最少打印次数。 **说明**: - $1 \le s.length \le 100$。 - $s$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "aaabbb" 输出:2 解释:首先打印 "aaa" 然后打印 "bbb"。 ``` - 示例 2: ```python 输入:s = "aba" 输出:2 解释:首先打印 "aaa" 然后在第二个位置打印 "b" 覆盖掉原来的字符 'a'。 ``` ## 解题思路 对于字符串 $s$,我们可以先考虑区间 $[i, j]$ 上的子字符串需要的最少打印次数。 1. 如果区间 $[i, j]$ 内只有 $1$ 种字符,则最少打印次数为 $1$,即:$dp[i][i] = 1$。 2. 如果区间 $[i, j]$ 内首尾字符相同,即 $s[i] == s[j]$,则我们在打印 $s[i]$ 的同时我们可以顺便打印 $s[j]$,这样我们可以忽略 $s[j]$,只考虑剩下区间 $[i, j - 1]$ 的打印情况,即:$dp[i][j] = dp[i][j - 1]$。 3. 如果区间 $[i, j]$ 上首尾字符不同,即 $s[i] \ne s[j]$,则枚举分割点 $k$,将区间 $[i, j]$ 分为区间 $[i, k]$ 与区间 $[k + 1, j]$,使得 $dp[i][k] + dp[k + 1][j]$ 的值最小即为 $dp[i][j]$。 ### 思路 1:动态规划 ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:打印第 $i$ 个字符到第 $j$ 个字符需要的最少打印次数。 ###### 3. 状态转移方程 1. 如果 $s[i] == s[j]$,则我们在打印 $s[i]$ 的同时我们可以顺便打印 $s[j]$,这样我们可以忽略 $s[j]$,只考虑剩下区间 $[i, j - 1]$ 的打印情况,即:$dp[i][j] = dp[i][j - 1]$。 2. 如果 $s[i] \ne s[j]$,则枚举分割点 $k$,将区间 $[i, j]$ 分为区间 $[i, k]$ 与区间 $[k + 1, j]$,使得 $dp[i][k] + dp[k + 1][j]$ 的值最小即为 $dp[i][j]$,即:$dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j])$。 ###### 4. 初始条件 - 初始时,打印单个字符的最少打印次数为 $1$,即 $dp[i][i] = 1$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:打印第 $i$ 个字符到第 $j$ 个字符需要的最少打印次数。 所以最终结果为 $dp[0][size - 1]$。 ### 思路 1:代码 ```python class Solution: def strangePrinter(self, s: str) -> int: size = len(s) dp = [[float('inf') for _ in range(size)] for _ in range(size)] for i in range(size): dp[i][i] = 1 for l in range(2, size + 1): for i in range(size): j = i + l - 1 if j >= size: break if s[i] == s[j]: dp[i][j] = dp[i][j - 1] else: for k in range(i, j): dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]) return dp[0][size - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/0600-0699/sum-of-square-numbers.md ================================================ # [0633. 平方数之和](https://leetcode.cn/problems/sum-of-square-numbers/) - 标签:数学、双指针、二分查找 - 难度:中等 ## 题目链接 - [0633. 平方数之和 - 力扣](https://leetcode.cn/problems/sum-of-square-numbers/) ## 题目大意 给定一个非负整数 c,判断是否存在两个整数 a 和 b,使得 $a^2 + b^2 = c$,如果存在则返回 True,不存在返回 False。 ## 解题思路 最直接的办法就是枚举 a、b 所有可能。这样遍历下来的时间复杂度为 $O(c^2)$。但是没必要进行二重遍历。可以只遍历 a,然后去判断 $\sqrt{c - b^2}$ 是否为整数,并且 a 只需遍历到 $\sqrt{c}$ 即可,时间复杂度为 $O(\sqrt{c})$。 另一种方法是双指针。定义两个指针 left,right 分别指向 0 和 $\sqrt{c}$。判断 $left^2 + right^2$ 与 c 之间的关系。 - 如果 $a^2 + b^2 == c$,则返回 True。 - 如果 $a^2 + b^2 < c$,则将 a 值加一,继续查找。 - 如果 $a^2 + b^2 > c$,则将 b 值减一,继续查找。 - 当 $a == b$ 时,结束查找。如果此时仍没有找到满足 $a^2 + b^2 == c$ 的 a、b 值,则返回 False。 ## 代码 ```python class Solution: def judgeSquareSum(self, c: int) -> bool: a, b = 0, int(c ** 0.5) while a <= b: sum = a*a + b*b if sum == c: return True elif sum < c: a += 1 else: b -= 1 return False ``` ================================================ FILE: docs/solutions/0600-0699/task-scheduler.md ================================================ # [0621. 任务调度器](https://leetcode.cn/problems/task-scheduler/) - 标签:贪心、数组、哈希表、计数、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [0621. 任务调度器 - 力扣](https://leetcode.cn/problems/task-scheduler/) ## 题目大意 给定一个字符数组 tasks 表示 CPU 需要执行的任务列表。tasks 中每个字母表示一种不同种类的任务。任务可以按任意顺序执行,并且每个任务执行时间为 1 个单位时间。在任何一个单位时间,CPU 可以完成一个任务,或者也可以处于待命状态。 但是两个相同种类的任务之间需要 n 个单位时间的冷却时间,所以不能在连续的 n 个单位时间内执行相同的任务。 要求计算出完成 tasks 中所有任务所需要的「最短时间」。 ## 解题思路 因为相同种类的任务之间最少需要 n 个单位时间间隔,所以为了最短时间,应该优先考虑任务出现此次最多的任务。 先找出出现次数最多的任务,然后中间间隔的单位来安排别的任务,或者处于待命状态。 然后将第二出现次数最多的任务,按照 n 个时间间隔安排起来。如果第二出现次数最多的任务跟第一出现次数最多的任务出现次数相同,则最短时间就会加一。 最后我们会发现:最短时间跟出现次数最多的任务正相关。 假设出现次数最多的任务为 "A"。与 "A" 出现次数相同的任务数为 count。则: - `最短时间 = (A 出现次数 - 1)* (n + 1)+ count`。 最后还应该比较一下总的任务个数跟计算出的最短时间答案。如果最短时间比总的任务个数还少,说明间隔中放不下所有的任务,会有任务「溢出」。则应该将多余任务插入间隔中,则答案应为总的任务个数。 ## 代码 ```python class Solution: def leastInterval(self, tasks: List[str], n: int) -> int: # 记录每个任务出现的次数 tasks_counts = [0 for _ in range(26)] for i in range(len(tasks)): num = ord(tasks[i]) - ord('A') tasks_counts[num] += 1 max_task_count = max(tasks_counts) # 统计多少个出现最多次的任务 count = 0 for task_count in tasks_counts: if task_count == max_task_count: count += 1 # 如果结果比任务数量少,则返回总任务数 return max((max_task_count - 1) * (n + 1) + count, len(tasks)) ``` ================================================ FILE: docs/solutions/0600-0699/top-k-frequent-words copy.md ================================================ # [0692. 前K个高频单词](https://leetcode.cn/problems/top-k-frequent-words/) - 标签:字典树、数组、哈希表、字符串、桶排序、计数、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [0692. 前K个高频单词 - 力扣](https://leetcode.cn/problems/top-k-frequent-words/) ## 题目大意 **描述**: 给定一个单词列表 $words$ 和一个整数 $k$。 **要求**: 返回前 $k$ 个出现次数最多的单词。 返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字典顺序排序。 **说明**: - $1 \le words.length \le 500$。 - $1 \le words[i].length \le 10$。 - $words[i]$ 由小写英文字母组成。 - $k$ 的取值范围是 $[1, \text{不同 words[i] 的数量}]$ - 进阶:尝试以 $O(n \log k)$ 时间复杂度和 $O(n)$ 空间复杂度解决。 **示例**: - 示例 1: ```python 输入: words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2 输出: ["i", "love"] 解析: "i" 和 "love" 为出现次数最多的两个单词,均为2次。 注意,按字母顺序 "i" 在 "love" 之前。 ``` - 示例 2: ```python 输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4 输出: ["the", "is", "sunny", "day"] 解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词, 出现次数依次为 4, 3, 2 和 1 次。 ``` ## 解题思路 ### 思路 1:哈希表 + 排序 这道题目要求找出前 $k$ 个出现频率最高的单词,频率相同时按字典序排序。 1. 使用哈希表统计每个单词的出现频率。 2. 将哈希表中的单词按照以下规则排序: - 首先按频率从高到低排序。 - 频率相同时按字典序从小到大排序。 3. 返回排序后的前 $k$ 个单词。 ### 思路 1:代码 ```python class Solution: def topKFrequent(self, words: List[str], k: int) -> List[str]: from collections import Counter # 统计单词频率 freq = Counter(words) # 按照频率从高到低,频率相同时按字典序从小到大排序 result = sorted(freq.keys(), key=lambda x: (-freq[x], x)) # 返回前 k 个单词 return result[:k] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是不同单词的数量。统计频率需要 $O(m)$($m$ 是单词总数),排序需要 $O(n \log n)$。 - **空间复杂度**:$O(n)$,需要使用哈希表存储单词频率。 ## 解题思路 ### 思路 2:堆(优先队列)+ 哈希表 使用最小堆来优化时间复杂度,只维护前 $k$ 个高频单词。 1. 使用哈希表统计每个单词的出现频率。 2. 使用最小堆维护前 $k$ 个高频单词: - 堆的大小最多为 $k$。 - 堆中元素按照频率从小到大排序,频率相同时按字典序从大到小排序(这样可以保证频率小的或字典序大的在堆顶,会被优先弹出)。 3. 遍历哈希表,将单词加入堆: - 直接将单词加入堆。 - 如果堆的大小超过 $k$,弹出堆顶元素(频率最小的,或频率相同时字典序最大的)。 4. 将堆中的元素按照频率从高到低、频率相同时按字典序从小到大排序后返回。 **关键点**:使用自定义类来实现堆的比较逻辑,确保频率相同时字典序大的在堆顶。 ### 思路 2:代码 ```python from collections import Counter import heapq # 自定义类,用于堆的比较 class Word: def __init__(self, word, freq): self.word = word self.freq = freq def __lt__(self, other): # 频率不同时,频率小的在堆顶 if self.freq != other.freq: return self.freq < other.freq # 频率相同时,字典序大的在堆顶(会被弹出) return self.word > other.word class Solution: def topKFrequent(self, words: List[str], k: int) -> List[str]: # 统计单词频率 freq = Counter(words) # 使用最小堆 heap = [] for word, count in freq.items(): heapq.heappush(heap, Word(word, count)) if len(heap) > k: heapq.heappop(heap) # 按照频率从高到低,频率相同时按字典序从小到大排序 heap.sort(key=lambda x: (-x.freq, x.word)) return [x.word for x in heap] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \log k)$,其中 $n$ 是不同单词的数量。统计频率需要 $O(m)$($m$ 是单词总数),维护大小为 $k$ 的堆需要 $O(n \log k)$,最后排序需要 $O(k \log k)$。 - **空间复杂度**:$O(n)$,需要使用哈希表存储单词频率,堆的大小为 $O(k)$。 ================================================ FILE: docs/solutions/0600-0699/top-k-frequent-words.md ================================================ # [0692. 前K个高频单词](https://leetcode.cn/problems/top-k-frequent-words/) - 标签:字典树、数组、哈希表、字符串、桶排序、计数、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [0692. 前K个高频单词 - 力扣](https://leetcode.cn/problems/top-k-frequent-words/) ## 题目大意 **描述**: 给定一个单词列表 $words$ 和一个整数 $k$。 **要求**: 返回前 $k$ 个出现次数最多的单词。 返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字典顺序排序。 **说明**: - $1 \le words.length \le 500$。 - $1 \le words[i].length \le 10$。 - $words[i]$ 由小写英文字母组成。 - $k$ 的取值范围是 $[1, \text{不同 words[i] 的数量}]$ - 进阶:尝试以 $O(n \log k)$ 时间复杂度和 $O(n)$ 空间复杂度解决。 **示例**: - 示例 1: ```python 输入: words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2 输出: ["i", "love"] 解析: "i" 和 "love" 为出现次数最多的两个单词,均为2次。 注意,按字母顺序 "i" 在 "love" 之前。 ``` - 示例 2: ```python 输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4 输出: ["the", "is", "sunny", "day"] 解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词, 出现次数依次为 4, 3, 2 和 1 次。 ``` ## 解题思路 ### 思路 1:哈希表 + 排序 这道题目要求找出前 $k$ 个出现频率最高的单词,频率相同时按字典序排序。 1. 使用哈希表统计每个单词的出现频率。 2. 将哈希表中的单词按照以下规则排序: - 首先按频率从高到低排序。 - 频率相同时按字典序从小到大排序。 3. 返回排序后的前 $k$ 个单词。 ### 思路 1:代码 ```python class Solution: def topKFrequent(self, words: List[str], k: int) -> List[str]: from collections import Counter # 统计单词频率 freq = Counter(words) # 按照频率从高到低,频率相同时按字典序从小到大排序 result = sorted(freq.keys(), key=lambda x: (-freq[x], x)) # 返回前 k 个单词 return result[:k] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是不同单词的数量。统计频率需要 $O(m)$($m$ 是单词总数),排序需要 $O(n \log n)$。 - **空间复杂度**:$O(n)$,需要使用哈希表存储单词频率。 ## 解题思路 ### 思路 2:堆(优先队列)+ 哈希表 使用最小堆来优化时间复杂度,只维护前 $k$ 个高频单词。 1. 使用哈希表统计每个单词的出现频率。 2. 使用最小堆维护前 $k$ 个高频单词: - 堆的大小最多为 $k$。 - 堆中元素按照频率从小到大排序,频率相同时按字典序从大到小排序(这样可以保证频率小的或字典序大的在堆顶,会被优先弹出)。 3. 遍历哈希表,将单词加入堆: - 直接将单词加入堆。 - 如果堆的大小超过 $k$,弹出堆顶元素(频率最小的,或频率相同时字典序最大的)。 4. 将堆中的元素按照频率从高到低、频率相同时按字典序从小到大排序后返回。 **关键点**:使用自定义类来实现堆的比较逻辑,确保频率相同时字典序大的在堆顶。 ### 思路 2:代码 ```python from collections import Counter import heapq # 自定义类,用于堆的比较 class Word: def __init__(self, word, freq): self.word = word self.freq = freq def __lt__(self, other): # 频率不同时,频率小的在堆顶 if self.freq != other.freq: return self.freq < other.freq # 频率相同时,字典序大的在堆顶(会被弹出) return self.word > other.word class Solution: def topKFrequent(self, words: List[str], k: int) -> List[str]: # 统计单词频率 freq = Counter(words) # 使用最小堆 heap = [] for word, count in freq.items(): heapq.heappush(heap, Word(word, count)) if len(heap) > k: heapq.heappop(heap) # 按照频率从高到低,频率相同时按字典序从小到大排序 heap.sort(key=lambda x: (-x.freq, x.word)) return [x.word for x in heap] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \log k)$,其中 $n$ 是不同单词的数量。统计频率需要 $O(m)$($m$ 是单词总数),维护大小为 $k$ 的堆需要 $O(n \log k)$,最后排序需要 $O(k \log k)$。 - **空间复杂度**:$O(n)$,需要使用哈希表存储单词频率,堆的大小为 $O(k)$。 ================================================ FILE: docs/solutions/0600-0699/trim-a-binary-search-tree.md ================================================ # [0669. 修剪二叉搜索树](https://leetcode.cn/problems/trim-a-binary-search-tree/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [0669. 修剪二叉搜索树 - 力扣](https://leetcode.cn/problems/trim-a-binary-search-tree/) ## 题目大意 给定一棵二叉搜索树的根节点 `root`,同时给定最小边界 `low` 和最大边界 `high`。通过修建二叉搜索树,使得所有节点值都在 `[low, high]` 中。修剪树不应该改变保留在树中的元素的相对结构(即如果没有移除节点,则该节点的父节点关系、子节点关系都应当保留)。 现在要求返回修建过后的二叉树的根节点。 ## 解题思路 递归修剪,函数返回值为修剪之后的树。 - 如果当前根节点为空,则直接返回 None。 - 如果当前根节点的值小于 `low`,则该节点左子树全部都小于最小边界,则删除左子树,然后递归遍历右子树,在右子树中寻找符合条件的节点。 - 如果当前根节点的值大于 `hight`,则该节点右子树全部都大于最大边界,则删除右子树,然后递归遍历左子树,在左子树中寻找符合条件的节点。 - 如果在最小边界和最大边界的区间内,则分别从左右子树寻找符合条件的节点作为根的左右子树。 ## 代码 ```python class Solution: def trimBST(self, root: TreeNode, low: int, high: int) -> TreeNode: if not root: return None if root.val < low: right = self.trimBST(root.right, low, high) return right if root.val > high: left = self.trimBST(root.left, low, high) return left root.left = self.trimBST(root.left, low, high) root.right = self.trimBST(root.right, low, high) return root ``` ================================================ FILE: docs/solutions/0600-0699/two-sum-iv-input-is-a-bst.md ================================================ # [0653. 两数之和 IV - 输入二叉搜索树](https://leetcode.cn/problems/two-sum-iv-input-is-a-bst/) - 标签:树、深度优先搜索、广度优先搜索、二叉搜索树、哈希表、双指针、二叉树 - 难度:简单 ## 题目链接 - [0653. 两数之和 IV - 输入二叉搜索树 - 力扣](https://leetcode.cn/problems/two-sum-iv-input-is-a-bst/) ## 题目大意 给定一个二叉搜索树的根节点 `root` 和一个整数 `k`。 要求:判断该二叉搜索树是否存在两个节点值的和等于 `k`。如果存在,则返回 `True`,不存在则返回 `False`。 ## 解题思路 二叉搜索树中序遍历的结果是从小到大排序,所以我们可以先对二叉搜索树进行中序遍历,将中序遍历结果存储到列表中。再使用左右指针查找节点值和为 `k` 的两个节点。 ## 代码 ```python class Solution: def inOrder(self, root, nums): if not root: return self.inOrder(root.left, nums) nums.append(root.val) self.inOrder(root.right, nums) def findTarget(self, root: TreeNode, k: int) -> bool: nums = [] self.inOrder(root, nums) left, right = 0, len(nums) - 1 while left < right: sum = nums[left] + nums[right] if sum == k: return True elif sum < k: left += 1 else: right -= 1 return False ``` ================================================ FILE: docs/solutions/0600-0699/valid-palindrome-ii.md ================================================ # [0680. 验证回文串 II](https://leetcode.cn/problems/valid-palindrome-ii/) - 标签:贪心、双指针、字符串 - 难度:简单 ## 题目链接 - [0680. 验证回文串 II - 力扣](https://leetcode.cn/problems/valid-palindrome-ii/) ## 题目大意 给定一个非空字符串 `s`。 要求:判断如果最多从字符串中删除一个字符能否得到一个回文字符串。 ## 解题思路 题目要求在最多删除一个字符的情况下是否能得到一个回文字符串。最直接的思路是遍历各个字符,判断将该字符删除之后,剩余字符串是否是回文串。但是这种思路的时间复杂度是 $O(n^2)$,解答的话会超时。 我们可以通过双指针 + 贪心算法来减少时间复杂度。具体做法如下: - 使用两个指针变量 `left`、`right` 分别指向字符串的开始和结束位置。 - 判断 `s[left]` 是否等于 `s[right]`。 - 如果等于,则 `left` 右移、`right`左移。 - 如果不等于,则判断 `s[left: right - 1]` 或 `s[left + 1, right]` 是为回文串。 - 如果是则返回 `True`。 - 如果不是则返回 `False`,然后继续判断。 - 如果 `right >= left`,则说明字符串 `s` 本身就是回文串,返回 `True`。 ## 代码 ```python class Solution: def checkPalindrome(self, s: str, left: int, right: int): i, j = left, right while i < j: if s[i] != s[j]: return False i += 1 j -= 1 return True def validPalindrome(self, s: str) -> bool: left, right = 0, len(s) - 1 while left < right: if s[left] == s[right]: left += 1 right -= 1 else: return self.checkPalindrome(s, left + 1, right) or self.checkPalindrome(s, left, right - 1) return True ``` ================================================ FILE: docs/solutions/0600-0699/valid-parenthesis-string.md ================================================ # [0678. 有效的括号字符串](https://leetcode.cn/problems/valid-parenthesis-string/) - 标签:栈、贪心、字符串、动态规划 - 难度:中等 ## 题目链接 - [0678. 有效的括号字符串 - 力扣](https://leetcode.cn/problems/valid-parenthesis-string/) ## 题目大意 **描述**:给定一个只包含三种字符的字符串:`(` ,`)` 和 `*`。有效的括号字符串具有如下规则: 1. 任何左括号 `(` 必须有相应的右括号 `)`。 2. 任何右括号 `)` 必须有相应的左括号 `(`。 3. 左括号 `(` 必须在对应的右括号之前 `)`。 4. `*` 可以被视为单个右括号 `)`,或单个左括号 `(`,或一个空字符串。 5. 一个空字符串也被视为有效字符串。 **要求**:验证这个字符串是否为有效字符串。如果是,则返回 `True`;否则,则返回 `False`。 **说明**: - 字符串大小将在 `[1, 100]` 范围内。 **示例**: - 示例 1: ```python 输入:"(*)" 输出:True ``` ## 解题思路 ### 思路 1:动态规划(时间复杂度为 $O(n^3)$) ###### 1. 阶段划分 按照子串的起始位置进行阶段划分。 ###### 2. 定义状态 定义状态 `dp[i][j]` 表示为:从下标 `i` 到下标 `j` 的子串是否为有效的括号字符串,其中 ($0 \le i < j < size$,$size$ 为字符串长度)。如果是则 `dp[i][j] = True`,否则,`dp[i][j] = False`。 ###### 3. 状态转移方程 长度大于 `2` 时,我们需要根据 `s[i]` 和 `s[j]` 的情况,以及子串中间的有效字符串情况来判断 `dp[i][j]`。 - 如果 `s[i]`、`s[j]` 分别表示左括号和右括号,或者为 `'*'`(此时 `s[i]`、`s[j]` 可以分别看做是左括号、右括号)。则如果 `dp[i + 1][j - 1] == True` 时,`dp[i][j] = True`。 - 如果可以将从下标 `i` 到下标 `j` 的子串从中间分开为两个有效字符串,则 `dp[i][j] = True`。即如果存在 $i \le k < j$,使得 `dp[i][k] == True` 并且 `dp[k + 1][j] == True`,则 `dp[i][j] = True`。 ###### 4. 初始条件 - 当子串的长度为 `1`,并且该字符串为 `'*'` 时,子串可看做是空字符串,此时子串是有效的括号字符串。 - 当子串的长度为 `2` 时,如果两个字符可以分别看做是左括号和右括号,子串可以看做是 `"()"`,此时子串是有效的括号字符串。 ###### 5. 最终结果 根据我们之前定义的状态,`dp[i][j]` 表示为:从下标 `i` 到下标 `j` 的子串是否为有效的括号字符串。则最终结果为 `dp[0][size - 1]`。 ### 思路 1:动态规划(时间复杂度为 $O(n^3)$)代码 ```python class Solution: def checkValidString(self, s: str) -> bool: size = len(s) dp = [[False for _ in range(size)] for _ in range(size)] for i in range(size): if s[i] == '*': dp[i][i] = True for i in range(1, size): if (s[i - 1] == '(' or s[i - 1] == '*') and (s[i] == ')' or s[i] == '*'): dp[i - 1][i] = True for i in range(size - 3, -1, -1): for j in range(i + 2, size): if (s[i] == '(' or s[i] == '*') and (s[j] == ')' or s[j] == '*'): dp[i][j] = dp[i + 1][j - 1] for k in range(i, j): if dp[i][j]: break dp[i][j] = dp[i][k] and dp[k + 1][j] return dp[0][size - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$。三重循环遍历的时间复杂度是 $O(n^3)$。 - **空间复杂度**:$O(n^2)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n^2)$。 ### 思路 2:动态规划(时间复杂度为 $O(n^2)$) ###### 1. 阶段划分 按照字符串的结束位置进行阶段划分。 ###### 2. 定义状态 定义状态 `dp[i][j]` 表示为:前 `i` 个字符能否通过补齐 `j` 个右括号成为有效的括号字符串。 ###### 3. 状态转移方程 1. 如果 `s[i] == '('`,则如果前 `i - 1` 个字符通过补齐 `j - 1` 个右括号成为有效的括号字符串,则前 `i` 个字符就能通过补齐 `j` 个右括号成为有效的括号字符串(比前 `i - 1` 个字符需要多补一个右括号)。也就是说,如果 `s[i] == '('` 并且 `dp[i - 1][j - 1] == True`,则 `dp[i][j] = True`。 2. 如果 `s[i] == ')'`,则如果前 `i - 1` 个字符通过补齐 `j + 1` 个右括号成为有效的括号字符串,则前 `i` 个字符就能通过补齐 `j` 个右括号成为有效的括号字符串(比前 `i - 1` 个字符需要少补一个右括号)。也就是说,如果 `s[i] == ')'` 并且 `dp[i - 1][j + 1] == True`,则 `dp[i][j] = True`。 3. 如果 `s[i] == '*'`,而 `'*'` 可以表示空字符串、左括号或者右括号,则 `dp[i][j]` 取决于这三种情况,只要有一种情况为 `True`,则 `dp[i][j] = True`。也就是说,如果 `s[i] == '*'`,则 `dp[i][j] = dp[i - 1][j] or dp[i - 1][j - 1]`。 ###### 4. 初始条件 - `0` 个字符可以通过补齐 `0` 个右括号成为有效的括号字符串(空字符串),即 `dp[0][0] = 0`。 ###### 5. 最终结果 根据我们之前定义的状态,`dp[i][j]` 表示为:前 `i` 个字符能否通过补齐 `j` 个右括号成为有效的括号字符串。。则最终结果为 `dp[size][0]`。 ### 思路 2:动态规划(时间复杂度为 $O(n^2)$)代码 ```python class Solution: def checkValidString(self, s: str) -> bool: size = len(s) dp = [[False for _ in range(size + 1)] for _ in range(size + 1)] dp[0][0] = True for i in range(1, size + 1): for j in range(i + 1): if s[i - 1] == '(': if j > 0: dp[i][j] = dp[i - 1][j - 1] elif s[i - 1] == ')': if j < i: dp[i][j] = dp[i - 1][j + 1] else: dp[i][j] = dp[i - 1][j] if j > 0: dp[i][j] = dp[i][j] or dp[i - 1][j - 1] if j < i: dp[i][j] = dp[i][j] or dp[i - 1][j + 1] return dp[size][0] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2)$。两重循环遍历的时间复杂度是 $O(n^2)$。 - **空间复杂度**:$O(n^2)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n^2)$。 ================================================ FILE: docs/solutions/0600-0699/valid-triangle-number.md ================================================ # [0611. 有效三角形的个数](https://leetcode.cn/problems/valid-triangle-number/) - 标签:贪心、数组、双指针、二分查找、排序 - 难度:中等 ## 题目链接 - [0611. 有效三角形的个数 - 力扣](https://leetcode.cn/problems/valid-triangle-number/) ## 题目大意 **描述**:给定一个包含非负整数的数组 $nums$,其中 $nums[i]$ 表示第 $i$ 条边的边长。 **要求**:统计数组中可以组成三角形三条边的三元组个数。 **说明**: - $1 \le nums.length \le 1000$。 - $0 \le nums[i] \le 1000$。 **示例**: - 示例 1: ```python 输入: nums = [2,2,3,4] 输出: 3 解释:有效的组合是: 2,3,4 (使用第一个 2) 2,3,4 (使用第二个 2) 2,2,3 ``` - 示例 2: ```python 输入: nums = [4,2,3,4] 输出: 4 ``` ## 解题思路 ### 思路 1:对撞指针 构成三角形的条件为:任意两边和大于第三边,或者任意两边差小于第三边。只要满足这两个条件之一就可以构成三角形。以任意两边和大于第三边为例,如果用 $a$、$b$、$c$ 来表示的话,应该同时满足 $a + b > c$、$a + c > b$、$b + c > a$。如果我们将三条边升序排序,假设 $a \le b \le c$,则如果满足 $a + b > c$,则 $a + c > b$ 和 $b + c > a$ 一定成立。 所以我们可以先对 $nums$ 进行排序。然后固定最大边 $i$,利用对撞指针 $left$、$right$ 查找较小的两条边。然后判断是否构成三角形并统计三元组个数。 为了避免重复计算和漏解,要严格保证三条边的序号关系为:$left < right < i$。具体做法如下: - 对数组从小到大排序,使用 $ans$ 记录三元组个数。 - 从 $i = 2$ 开始遍历数组的每一条边,$i$ 作为最大边。 - 使用双指针 $left$、$right$。$left$ 指向 $0$,$right$ 指向 $i - 1$。 - 如果 $nums[left] + nums[right] \le nums[i]$,说明第一条边太短了,可以增加第一条边长度,所以将 $left$ 右移,即 `left += 1`。 - 如果 $nums[left] + nums[right] > nums[i]$,说明可以构成三角形,并且第二条边固定为 $right$ 边的话,第一条边可以在 $[left, right - 1]$ 中任意选择。所以三元组个数要加上 $right - left$。即 `ans += (right - left)`。 - 直到 $left == right$ 跳出循环,输出三元组个数 $ans$。 ### 思路 1:代码 ```python class Solution: def triangleNumber(self, nums: List[int]) -> int: nums.sort() size = len(nums) ans = 0 for i in range(2, size): left = 0 right = i - 1 while left < right: if nums[left] + nums[right] <= nums[i]: left += 1 else: ans += (right - left) right -= 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为数组中的元素个数。 - **空间复杂度**:$O(\log n)$,排序需要 $\log n$ 的栈空间。 ================================================ FILE: docs/solutions/0700-0799/1-bit-and-2-bit-characters.md ================================================ # [0717. 1 比特与 2 比特字符](https://leetcode.cn/problems/1-bit-and-2-bit-characters/) - 标签:数组 - 难度:简单 ## 题目链接 - [0717. 1 比特与 2 比特字符 - 力扣](https://leetcode.cn/problems/1-bit-and-2-bit-characters/) ## 题目大意 **描述**: 有两种特殊字符: - 第一种字符可以用一比特 $0$ 表示 - 第二种字符可以用两比特($10$ 或 $11$)表示 给定一个以 $0$ 结尾的二进制数组 $bits$。 **要求**: 如果最后一个字符必须是一个一比特字符,则返回 true。 **说明**: - $1 \le bits.length \le 10^{3}$。 - $bits[i]$ 为 $0$ 或 $1$。 **示例**: - 示例 1: ```python 输入: bits = [1, 0, 0] 输出: true 解释: 唯一的解码方式是将其解析为一个两比特字符和一个一比特字符。 所以最后一个字符是一比特字符。 ``` - 示例 2: ```python 输入:bits = [1,1,1,0] 输出:false 解释:唯一的解码方式是将其解析为两比特字符和两比特字符。 所以最后一个字符不是一比特字符。 ``` ## 解题思路 ### 思路 1:贪心算法 这道题的关键在于理解字符的编码规则: - 一比特字符:`0` - 两比特字符:`10` 或 `11` 由于数组以 `0` 结尾,我们需要判断这个 `0` 是作为一比特字符单独存在,还是作为两比特字符的一部分。 **解题步骤**: 1. 从数组开头开始遍历,使用指针 $i$ 记录当前位置。 2. 如果 $bits[i] = 1$,说明当前是两比特字符,跳过两位($i$ 增加 $2$)。 3. 如果 $bits[i] = 0$,说明当前是一比特字符,跳过一位($i$ 增加 $1$)。 4. 当 $i$ 到达倒数第二个位置时停止遍历。 5. 如果 $i$ 正好等于 $n - 1$(最后一个位置),说明最后一个 `0` 是一比特字符,返回 `True`。 6. 如果 $i$ 超过了 $n - 1$,说明最后一个 `0` 是两比特字符的一部分,返回 `False`。 ### 思路 1:代码 ```python class Solution: def isOneBitCharacter(self, bits: List[int]) -> bool: n = len(bits) i = 0 # 遍历到倒数第二个位置 while i < n - 1: if bits[i] == 1: # 两比特字符 i += 2 else: # 一比特字符 i += 1 # 如果 i 正好等于 n - 1,说明最后一个 0 是一比特字符 return i == n - 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $bits$ 的长度。需要遍历整个数组一次。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0700-0799/accounts-merge.md ================================================ # [0721. 账户合并](https://leetcode.cn/problems/accounts-merge/) - 标签:深度优先搜索、广度优先搜索、并查集、数组、哈希表、字符串、排序 - 难度:中等 ## 题目链接 - [0721. 账户合并 - 力扣](https://leetcode.cn/problems/accounts-merge/) ## 题目大意 **描述**: 给定一个列表 $accounts$,每个元素 $accounts[i]$ 是一个字符串列表,其中第一个元素 $accounts[i][0]$ 是名称($name$),其余元素是 $emails$ 表示该账户的邮箱地址。 现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。 **要求**: 合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是按字符 ASCII 顺序排列的邮箱地址。账户本身可以以任意顺序返回。 **说明**: - $1 \le accounts.length \le 10^{3}$。 - $2 \le accounts[i].length \le 10$。 - $1 \le accounts[i][j].length \le 30$。 - $accounts[i][0]$ 由英文字母组成。 - $accounts[i][j] (for j \gt 0)$ 是有效的邮箱地址。 **示例**: - 示例 1: ```python 输入:accounts = [["John", "johnsmith@mail.com", "john00@mail.com"], ["John", "johnnybravo@mail.com"], ["John", "johnsmith@mail.com", "john_newyork@mail.com"], ["Mary", "mary@mail.com"]] 输出:[["John", 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com'], ["John", "johnnybravo@mail.com"], ["Mary", "mary@mail.com"]] 解释: 第一个和第三个 John 是同一个人,因为他们有共同的邮箱地址 "johnsmith@mail.com"。 第二个 John 和 Mary 是不同的人,因为他们的邮箱地址没有被其他帐户使用。 可以以任何顺序返回这些列表,例如答案 [['Mary','mary@mail.com'],['John','johnnybravo@mail.com'], ['John','john00@mail.com','john_newyork@mail.com','johnsmith@mail.com']] 也是正确的。 ``` - 示例 2: ```python 输入:accounts = [["Gabe","Gabe0@m.co","Gabe3@m.co","Gabe1@m.co"],["Kevin","Kevin3@m.co","Kevin5@m.co","Kevin0@m.co"],["Ethan","Ethan5@m.co","Ethan4@m.co","Ethan0@m.co"],["Hanzo","Hanzo3@m.co","Hanzo1@m.co","Hanzo0@m.co"],["Fern","Fern5@m.co","Fern1@m.co","Fern0@m.co"]] 输出:[["Ethan","Ethan0@m.co","Ethan4@m.co","Ethan5@m.co"],["Gabe","Gabe0@m.co","Gabe1@m.co","Gabe3@m.co"],["Hanzo","Hanzo0@m.co","Hanzo1@m.co","Hanzo3@m.co"],["Kevin","Kevin0@m.co","Kevin3@m.co","Kevin5@m.co"],["Fern","Fern0@m.co","Fern1@m.co","Fern5@m.co"]] ``` ## 解题思路 ### 思路 1:并查集 这道题的核心是将具有相同邮箱的账户合并在一起。可以使用并查集来解决。 **解题步骤**: 1. 将每个邮箱看作一个节点,如果两个邮箱属于同一个账户,就将它们连接起来。 2. 使用哈希表 $email\_to\_id$ 记录每个邮箱对应的账户索引。 3. 使用并查集将属于同一个人的邮箱合并到同一个集合中。 4. 遍历所有账户,对于每个账户中的邮箱: - 如果邮箱第一次出现,记录其账户索引。 - 如果邮箱已经出现过,将当前账户与之前的账户合并。 5. 最后,将同一集合中的所有邮箱归类到一起,并按字典序排序。 ### 思路 1:代码 ```python class Solution: def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]: # 并查集 parent = {} def find(x): if x not in parent: parent[x] = x if parent[x] != x: parent[x] = find(parent[x]) return parent[x] def union(x, y): root_x = find(x) root_y = find(y) if root_x != root_y: parent[root_x] = root_y # 邮箱到账户索引的映射 email_to_id = {} # 邮箱到姓名的映射 email_to_name = {} # 遍历所有账户,建立邮箱之间的连接 for account in accounts: name = account[0] for i in range(1, len(account)): email = account[i] email_to_name[email] = name if email not in email_to_id: email_to_id[email] = email # 将当前账户的所有邮箱合并到第一个邮箱的集合中 union(account[1], email) # 将同一集合的邮箱归类 merged = {} for email in email_to_id: root = find(email) if root not in merged: merged[root] = [] merged[root].append(email) # 构建结果 result = [] for emails in merged.values(): name = email_to_name[emails[0]] result.append([name] + sorted(emails)) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是所有邮箱的总数。并查集操作的时间复杂度接近 $O(1)$,主要时间消耗在排序上。 - **空间复杂度**:$O(n)$。需要存储并查集、哈希表等数据结构。 ================================================ FILE: docs/solutions/0700-0799/all-paths-from-source-to-target.md ================================================ # [0797. 所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/) - 标签:深度优先搜索、广度优先搜索、图、回溯 - 难度:中等 ## 题目链接 - [0797. 所有可能的路径 - 力扣](https://leetcode.cn/problems/all-paths-from-source-to-target/) ## 题目大意 给定一个有 `n` 个节点的有向无环图(DAG),用二维数组 `graph` 表示。 要求:找出所有从节点 `0` 到节点 `n - 1` 的路径并输出(不要求按特定顺序)。 二维数组 `graph` 的第 `i` 个数组 `graph[i]` 中的单元都表示有向图中 `i` 号节点所能到达的下一个节点,如果为空就是没有下一个结点了。 ## 解题思路 从第 `0` 个节点开始进行深度优先搜索遍历。在遍历的同时,通过回溯来寻找所有路径。具体做法如下: - 使用 `ans` 数组存放所有答案路径,使用 `path` 数组记录当前路径。 - 从第 `0` 个节点开始进行深度优先搜索遍历。 - 如果当前开始节点 `start` 等于目标节点 `target`。则将当前路径 `path` 添加到答案数组 `ans` 中,并返回。 - 然后遍历当前节点 `start` 所能达到的下一个节点。 - 将下一个节点加入到当前路径中。 - 从该节点出发进行深度优先搜索遍历。 - 然后将下一个节点从当前路径中移出,进行回退操作。 - 最后返回答案数组 `ans`。 ## 代码 ```python class Solution: def dfs(self, graph, start, target, path, ans): if start == target: ans.append(path[:]) return for end in graph[start]: path.append(end) self.dfs(graph, end, target, path, ans) path.remove(end) def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]: path = [0] ans = [] self.dfs(graph, 0, len(graph) - 1, path, ans) return ans ``` ================================================ FILE: docs/solutions/0700-0799/asteroid-collision.md ================================================ # [0735. 小行星碰撞](https://leetcode.cn/problems/asteroid-collision/) - 标签:栈、数组 - 难度:中等 ## 题目链接 - [0735. 小行星碰撞 - 力扣](https://leetcode.cn/problems/asteroid-collision/) ## 题目大意 给定一个整数数组 `asteroids`,表示在同一行的小行星。 数组中的每一个元素,其绝对值表示小行星的大小,正负表示小行星的移动方向(正表示向右移动,负表示向左移动)。每一颗小行星以相同的速度移动。小行星按照下面的规则发生碰撞。 - 碰撞规则:两个行星相互碰撞,较小的行星会爆炸。如果两颗行星大小相同,则两颗行星都会爆炸。两颗移动方向相同的行星,永远不会发生碰撞。 要求:找出碰撞后剩下的所有小行星,将答案存入数组并返回。 ## 解题思路 用栈模拟小行星碰撞,具体步骤如下: - 遍历数组 `asteroids`。 - 如果栈为空或者当前元素 `asteroid` 为正数,将其压入栈。 - 如果当前栈不为空并且当前元素 `asteroid` 为负数: - 与栈中元素发生碰撞,判断当前元素和栈顶元素的大小和方向,如果栈顶元素为正数,并且当前元素的绝对值大于栈顶元素,则将栈顶元素弹出,并继续与栈中元素发生碰撞。 - 碰撞完之后,如果栈为空并且栈顶元素为负数,则将当前元素 `asteroid` 压入栈,表示碰撞完剩下了 `asteroid`。 - 如果栈顶元素恰好与当前元素值大小相等、方向相反,则弹出栈顶元素,表示碰撞完两者都爆炸了。 - 最后返回栈作为答案。 ## 代码 ```python class Solution: def asteroidCollision(self, asteroids: List[int]) -> List[int]: stack = [] for asteroid in asteroids: if not stack or asteroid > 0: stack.append(asteroid) else: while stack and 0 < stack[-1] < -asteroid: stack.pop() if not stack or stack[-1] < 0: stack.append(asteroid) elif stack[-1] == -asteroid: stack.pop() return stack ``` ================================================ FILE: docs/solutions/0700-0799/basic-calculator-iv.md ================================================ # [0770. 基本计算器 IV](https://leetcode.cn/problems/basic-calculator-iv/) - 标签:栈、递归、哈希表、数学、字符串 - 难度:困难 ## 题目链接 - [0770. 基本计算器 IV - 力扣](https://leetcode.cn/problems/basic-calculator-iv/) ## 题目大意 **描述**: 给定一个表达式如 `expression = "e + 8 - a + 5"` 和一个求值映射,如 `{"e": 1}`(给定的形式为 `evalvars = ["e"]` 和 `evalints = [1]`。 **要求**: 返回表示简化表达式的标记列表,例如 `["-1*a","14"]`。 - 表达式交替使用块和符号,每个块和符号之间有一个空格。 - 块要么是括号中的表达式,要么是变量,要么是非负整数。 - 变量是一个由小写字母组成的字符串(不包括数字)。请注意,变量可以是多个字母,并注意变量从不具有像 `"2x"` 或 `"-x"` 这样的前导系数或一元运算符。 表达式按通常顺序进行求值:先是括号,然后求乘法,再计算加法和减法。 - 例如,`expression = "1 + 2 * 3"` 的答案是 ["7"]。 输出格式如下: - 对于系数非零的每个自变量项,我们按字典排序的顺序将自变量写在一个项中。 - 例如,我们永远不会写像 `"b*a*c"` 这样的项,只写 `"a*b*c"`。 - 项的次数等于被乘的自变量的数目,并计算重复项。我们先写出答案的最大次数项,用字典顺序打破关系,此时忽略词的前导系数。 - 例如,`"a*a*b*c"` 的次数为 $4$。 - 项的前导系数直接放在左边,用星号将它与变量分隔开(如果存在的话)。前导系数 $1$ 仍然要打印出来。 - 格式良好的一个示例答案是 `["-2*a*a*a", "3*a*a*b", "3*b*b", "4*a", "5*c", "-6"]`。 - 系数为 $0$ 的项(包括常数项)不包括在内。 - 例如,`"0"` 的表达式输出为 `[]`。 注意:你可以假设给定的表达式均有效。所有中间结果都在区间 $[-2^{31}, 2^{31} - 1]$ 内。 **说明**: - $1 \le expression.length \le 250$。 - expression 由小写英文字母,数字 '+', '-', '*', '(', ')', ' ' 组成。 - expression 不包含任何前空格或后空格。 - expression 中的所有符号都用一个空格隔开。 - $0 \le evalvars.length \le 10^{3}$。 - $1 \le evalvars[i].length \le 20$。 - $evalvars[i]$ 由小写英文字母组成。 - $evalints.length == evalvars.length$。 - $-10^{3} \le evalints[i] \le 10^{3}$。 **示例**: - 示例 1: ```python 输入:expression = "e + 8 - a + 5", evalvars = ["e"], evalints = [1] 输出:["-1*a","14"] ``` - 示例 2: ```python 输入:expression = "e - 8 + temperature - pressure", evalvars = ["e", "temperature"], evalints = [1, 12] 输出:["-1*pressure","5"] ``` ## 解题思路 ### 思路 1:递归 + 哈希表 + 多项式运算 这道题要求实现一个支持变量的计算器,需要处理加减乘运算、括号和变量替换。 核心思路: 1. 定义多项式类,支持加减乘运算。 2. 多项式用字典表示,键是变量的元组(按字典序排序),值是系数。 3. 使用递归下降解析表达式。 4. 先将给定的变量替换为常数,再进行计算。 算法步骤: 1. 创建多项式类 `Poly`,支持: - 加法:合并同类项。 - 减法:系数取反后加法。 - 乘法:分配律展开。 2. 解析表达式: - 使用递归下降解析器处理括号、加减乘运算。 - 遇到变量时,如果在求值映射中,替换为常数;否则保留为变量。 3. 格式化输出: - 按次数从高到低、字典序排序。 - 格式化每一项。 ### 思路 1:代码 ```python class Solution: def basicCalculatorIV(self, expression: str, evalvars: List[str], evalints: List[int]) -> List[str]: from collections import Counter # 创建变量求值映射 eval_map = dict(zip(evalvars, evalints)) # 多项式类 class Poly: def __init__(self, terms=None): # terms: {变量元组: 系数} self.terms = Counter(terms) if terms else Counter() def __add__(self, other): result = Poly(self.terms) for key, val in other.terms.items(): result.terms[key] += val return result def __sub__(self, other): result = Poly(self.terms) for key, val in other.terms.items(): result.terms[key] -= val return result def __mul__(self, other): result = Poly() for k1, v1 in self.terms.items(): for k2, v2 in other.terms.items(): # 合并变量(按字典序排序) key = tuple(sorted(k1 + k2)) result.terms[key] += v1 * v2 return result def to_list(self): # 转换为输出格式 # 删除系数为 0 的项 items = [(k, v) for k, v in self.terms.items() if v != 0] # 排序:先按次数降序,再按字典序 items.sort(key=lambda x: (-len(x[0]), x[0])) result = [] for vars_tuple, coef in items: if vars_tuple: result.append(f"{coef}*{'*'.join(vars_tuple)}") else: result.append(str(coef)) return result # 解析表达式 tokens = expression.replace('(', ' ( ').replace(')', ' ) ').split() def parse(): """解析加减表达式""" nonlocal idx left = parse_term() while idx < len(tokens) and tokens[idx] in ['+', '-']: op = tokens[idx] idx += 1 right = parse_term() if op == '+': left = left + right else: left = left - right return left def parse_term(): """解析乘法表达式""" nonlocal idx left = parse_factor() while idx < len(tokens) and tokens[idx] == '*': idx += 1 right = parse_factor() left = left * right return left def parse_factor(): """解析因子(数字、变量或括号表达式)""" nonlocal idx token = tokens[idx] idx += 1 if token == '(': result = parse() idx += 1 # 跳过 ')' return result elif token.lstrip('-').isdigit(): # 数字 return Poly({(): int(token)}) else: # 变量 if token in eval_map: return Poly({(): eval_map[token]}) else: return Poly({(token,): 1}) idx = 0 poly = parse() return poly.to_list() ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是表达式的长度,$m$ 是多项式项的数量。解析和计算都需要遍历表达式。 - **空间复杂度**:$O(m)$,需要存储多项式的所有项。 ================================================ FILE: docs/solutions/0700-0799/best-time-to-buy-and-sell-stock-with-transaction-fee.md ================================================ # [0714. 买卖股票的最佳时机含手续费](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) - 标签:贪心、数组、动态规划 - 难度:中等 ## 题目链接 - [0714. 买卖股票的最佳时机含手续费 - 力扣](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) ## 题目大意 给定一个整数数组 `prices`,其中第 `i` 个元素代表了第 `i` 天的股票价格 ;整数 `fee` 代表了交易股票的手续费用。 你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。 最后要求返回获得利润的最大值。 ## 解题思路 这道题的解题思路和「[0122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/)」类似,同样可以买卖多次。122 题是在跌入谷底的时候买入,在涨到波峰的时候卖出,这道题多了手续费,则在判断波峰波谷的时候还要考虑手续费。贪心策略如下: - 当股票价格小于当前最低股价时,更新最低股价,不卖出。 - 当股票价格大于最小价格 + 手续费时,累积股票利润(实质上暂未卖出,等到波峰卖出),同时最低股价减去手续费,以免重复计算。 ## 代码 ```python class Solution: def maxProfit(self, prices: List[int], fee: int) -> int: res = 0 min_price = prices[0] for i in range(1, len(prices)): if prices[i] < min_price: min_price = prices[i] elif prices[i] > min_price + fee: res += prices[i] - min_price - fee min_price = prices[i] - fee return res ``` ================================================ FILE: docs/solutions/0700-0799/binary-search.md ================================================ # [0704. 二分查找](https://leetcode.cn/problems/binary-search/) - 标签:数组、二分查找 - 难度:简单 ## 题目链接 - [0704. 二分查找 - 力扣](https://leetcode.cn/problems/binary-search/) ## 题目大意 **描述**:给定一个升序的数组 $nums$,和一个目标值 $target$。 **要求**:返回 $target$ 在数组中的位置,如果找不到,则返回 -1。 **说明**: - 你可以假设 $nums$ 中的所有元素是不重复的。 - $n$ 将在 $[1, 10000]$之间。 - $nums$ 的每个元素都将在 $[-9999, 9999]$之间。 **示例**: - 示例 1: ```python 输入: nums = [-1,0,3,5,9,12], target = 9 输出: 4 解释: 9 出现在 nums 中并且下标为 4 ``` - 示例 2: ```python 输入: nums = [-1,0,3,5,9,12], target = 2 输出: -1 解释: 2 不存在 nums 中因此返回 -1 ``` ## 解题思路 ### 思路 1:二分查找 设定左右节点为数组两端,即 `left = 0`,`right = len(nums) - 1`,代表待查找区间为 $[left, right]$(左闭右闭)。 取两个节点中心位置 $mid$,先比较中心位置值 $nums[mid]$ 与目标值 $target$ 的大小。 - 如果 $target == nums[mid]$,则返回中心位置。 - 如果 $target > nums[mid]$,则将左节点设置为 $mid + 1$,然后继续在右区间 $[mid + 1, right]$ 搜索。 - 如果中心位置值 $target < nums[mid]$,则将右节点设置为 $mid - 1$,然后继续在左区间 $[left, mid - 1]$ 搜索。 ### 思路 1:代码 ```python class Solution: def search(self, nums: List[int], target: int) -> int: left, right = 0, len(nums) - 1 # 在区间 [left, right] 内查找 target while left <= right: # 取区间中间节点 mid = (left + right) // 2 # 如果找到目标值,则直接返回中心位置 if nums[mid] == target: return mid # 如果 nums[mid] 小于目标值,则在 [mid + 1, right] 中继续搜索 elif nums[mid] < target: left = mid + 1 # 如果 nums[mid] 大于目标值,则在 [left, mid - 1] 中继续搜索 else: right = mid - 1 # 未搜索到元素,返回 -1 return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0700-0799/bold-words-in-string.md ================================================ # [0758. 字符串中的加粗单词](https://leetcode.cn/problems/bold-words-in-string/) - 标签:字典树、数组、哈希表、字符串、字符串匹配 - 难度:中等 ## 题目链接 - [0758. 字符串中的加粗单词 - 力扣](https://leetcode.cn/problems/bold-words-in-string/) ## 题目大意 给定一个关键词集合 `words` 和一个字符串 `s`。 要求:在所有 `s` 中出现的关键词前后位置上添加加粗闭合标签 `` 和 ``。如果两个子串有重叠部分,则将它们一起用一对闭合标签包围起来。同理,如果两个子字符串连续被加粗,那么你也需要把它们合起来用一对加粗标签包围。最后返回添加加粗标签后的字符串 `s`。 ## 解题思路 构建字典树,将字符串列表 `words` 中所有字符串添加到字典树中。 然后遍历字符串 `s`,从每一个位置开始查询字典树。在第一个符合要求的单词前面添加 ``。在连续符合要求的单词中的最后一个单词后面添加 ``。 最后返回添加加粗标签后的字符串 `s`。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None and cur.isEnd class Solution: def boldWords(self, words: List[str], s: str) -> str: trie_tree = Trie() for word in words: trie_tree.insert(word) size = len(s) bold_left, bold_right = -1, -1 ans = "" for i in range(size): cur = trie_tree if s[i] in cur.children: bold_left = i while bold_left < size and s[bold_left] in cur.children: cur = cur.children[s[bold_left]] bold_left += 1 if cur.isEnd: if bold_right == -1: ans += "" bold_right = max(bold_left, bold_right) if i == bold_right: ans += "" bold_right = -1 ans += s[i] if bold_right >= 0: ans += "
" return ans ``` ================================================ FILE: docs/solutions/0700-0799/champagne-tower.md ================================================ # [0799. 香槟塔](https://leetcode.cn/problems/champagne-tower/) - 标签:动态规划 - 难度:中等 ## 题目链接 - [0799. 香槟塔 - 力扣](https://leetcode.cn/problems/champagne-tower/) ## 题目大意 **描述**: 我们把玻璃杯摆成金字塔的形状,其中第一层有 $1$ 个玻璃杯,第二层有 $2$ 个,依次类推到第 $100$ 层,每个玻璃杯将盛有香槟。 从顶层的第一个玻璃杯开始倾倒一些香槟,当顶层的杯子满了,任何溢出的香槟都会立刻等流量的流向左右两侧的玻璃杯。当左右两边的杯子也满了,就会等流量的流向它们左右两边的杯子,依次类推。(当最底层的玻璃杯满了,香槟会流到地板上) 例如,在倾倒一杯香槟后,最顶层的玻璃杯满了。倾倒了两杯香槟后,第二层的两个玻璃杯各自盛放一半的香槟。在倒三杯香槟后,第二层的香槟满了 - 此时总共有三个满的玻璃杯。在倒第四杯后,第三层中间的玻璃杯盛放了一半的香槟,他两边的玻璃杯各自盛放了四分之一的香槟,如下图所示。 ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/03/09/tower.png) **要求**: 现在当倾倒了非负整数杯香槟后,返回第 $i$ 行 $j$ 个玻璃杯所盛放的香槟占玻璃杯容积的比例( $i$ 和 $j$ 都从 $0$ 开始)。 **说明**: - $0 \le poured \le 10^{9}$。 - $0 \le query\_glass \le query\_row \lt 10^{3}$。 **示例**: - 示例 1: ```python 示例 1: 输入: poured(倾倒香槟总杯数) = 1, query_glass(杯子的位置数) = 1, query_row(行数) = 1 输出: 0.00000 解释: 我们在顶层(下标是(0,0))倒了一杯香槟后,没有溢出,因此所有在顶层以下的玻璃杯都是空的。 ``` - 示例 2: ```python 输入: poured(倾倒香槟总杯数) = 2, query_glass(杯子的位置数) = 1, query_row(行数) = 1 输出: 0.50000 解释: 我们在顶层(下标是(0,0)倒了两杯香槟后,有一杯量的香槟将从顶层溢出,位于(1,0)的玻璃杯和(1,1)的玻璃杯平分了这一杯香槟,所以每个玻璃杯有一半的香槟。 ``` ## 解题思路 ### 思路 1:动态规划 + 模拟 这道题可以使用动态规划来模拟香槟倾倒的过程。 **解题步骤**: 1. 定义 $dp[i][j]$ 表示第 $i$ 行第 $j$ 个杯子中的香槟量(可能超过 $1$)。 2. 初始时,将所有香槟倒入顶层杯子:$dp[0][0] = poured$。 3. 对于每个杯子 $dp[i][j]$: - 如果 $dp[i][j] > 1$,说明有香槟溢出。 - 溢出的香槟量为 $overflow = (dp[i][j] - 1) / 2$。 - 将溢出的香槟平均分配给下一层的两个杯子:$dp[i+1][j]$ 和 $dp[i+1][j+1]$。 4. 最后返回 $\min(1, dp[query\_row][query\_glass])$,因为杯子最多只能装 $1$ 杯香槟。 **优化**:由于只需要计算到第 $query\_row$ 行,所以只需要模拟到该行即可。 ### 思路 1:代码 ```python class Solution: def champagneTower(self, poured: int, query_row: int, query_glass: int) -> float: # dp[i][j] 表示第 i 行第 j 个杯子中的香槟量 dp = [[0.0] * (query_row + 2) for _ in range(query_row + 2)] dp[0][0] = poured # 模拟香槟倾倒过程 for i in range(query_row + 1): for j in range(i + 1): if dp[i][j] > 1: # 计算溢出的香槟量 overflow = (dp[i][j] - 1) / 2.0 # 将溢出的香槟平均分配给下一层的两个杯子 dp[i + 1][j] += overflow dp[i + 1][j + 1] += overflow # 返回目标杯子中的香槟量,最多为 1 return min(1.0, dp[query_row][query_glass]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(query\_row^2)$。需要遍历前 $query\_row + 1$ 行的所有杯子。 - **空间复杂度**:$O(query\_row^2)$。需要存储前 $query\_row + 1$ 行的所有杯子的状态。 ================================================ FILE: docs/solutions/0700-0799/cheapest-flights-within-k-stops.md ================================================ # [0787. K 站中转内最便宜的航班](https://leetcode.cn/problems/cheapest-flights-within-k-stops/) - 标签:深度优先搜索、广度优先搜索、图、动态规划、最短路、堆(优先队列) - 难度:中等 ## 题目链接 - [0787. K 站中转内最便宜的航班 - 力扣](https://leetcode.cn/problems/cheapest-flights-within-k-stops/) ## 题目大意 **描述**: 有 $n$ 个城市通过一些航班连接。给你一个数组 $flights$,其中 $flights[i] = [from_i, to_i, price_i]$,表示该航班都从城市 $from_i$ 开始,以价格 $price_i$ 抵达 $to_i$。 现在给定所有的城市和航班,以及出发城市 $src$ 和目的地 $dst$。 **要求**: 找到出一条最多经过 $k$ 站中转的路线,使得从 $src$ 到 $dst$ 的「价格最便宜」,并返回该价格。如果不存在这样的路线,则输出 $-1$。 **说明**: - $1 \le n \le 10^{3}$。 - $0 \le flights.length \le (n * (n - 1) / 2)$。 - $flights[i].length == 3$。 - $0 \le from_i, to_i \lt n$。 - $from_i \ne to_i$。 - $1 \le price_i \le 10^{4}$。 - 航班没有重复,且不存在自环。 - $0 \le src, dst, k \lt n$。 - $src \ne dst$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2022/03/18/cheapest-flights-within-k-stops-3drawio.png) ```python 输入: n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1 输出: 700 解释: 城市航班图如上 从城市 0 到城市 3 经过最多 1 站的最佳路径用红色标记,费用为 100 + 600 = 700。 请注意,通过城市 [0, 1, 2, 3] 的路径更便宜,但无效,因为它经过了 2 站。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2022/03/18/cheapest-flights-within-k-stops-1drawio.png) ```python 输入: n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1 输出: 200 解释: 城市航班图如上 从城市 0 到城市 2 经过最多 1 站的最佳路径标记为红色,费用为 100 + 100 = 200。 ``` ## 解题思路 ### 思路 1:动态规划(Bellman-Ford 算法) 这是一个带限制条件的最短路径问题。可以使用动态规划或 Bellman-Ford 算法求解。 **状态定义**: - $dp[k][i]$ 表示经过最多 $k$ 次中转到达城市 $i$ 的最小花费。 **状态转移**: - $dp[k][i] = \min(dp[k][i], dp[k-1][j] + price_{j \to i})$,其中 $j$ 是 $i$ 的前驱节点。 **初始化**: - $dp[0][src] = 0$,其他为无穷大。 ### 思路 1:代码 ```python class Solution: def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int: # 初始化 dp 数组 INF = float('inf') dp = [INF] * n dp[src] = 0 # 最多 k+1 次飞行(k 次中转) for _ in range(k + 1): # 使用临时数组避免状态覆盖 new_dp = dp[:] for from_city, to_city, price in flights: if dp[from_city] != INF: new_dp[to_city] = min(new_dp[to_city], dp[from_city] + price) dp = new_dp return dp[dst] if dp[dst] != INF else -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k \times m)$,其中 $m$ 是航班数量。 - **空间复杂度**:$O(n)$,$dp$ 数组的空间。 ================================================ FILE: docs/solutions/0700-0799/cherry-pickup.md ================================================ # [0741. 摘樱桃](https://leetcode.cn/problems/cherry-pickup/) - 标签:数组、动态规划、矩阵 - 难度:困难 ## 题目链接 - [0741. 摘樱桃 - 力扣](https://leetcode.cn/problems/cherry-pickup/) ## 题目大意 **描述**: 给定一个 $n \times n$ 的网格 $grid$,代表一块樱桃地,每个格子由以下三种数字的一种来表示: - $0$ 表示这个格子是空的,所以你可以穿过它。 - $1$ 表示这个格子里装着一个樱桃,你可以摘到樱桃然后穿过它。 - $-1$ 表示这个格子里有荆棘,挡着你的路。 **要求**: 请你统计并返回:在遵守下列规则的情况下,能摘到的最多樱桃数: - 从位置 $(0, 0)$ 出发,最后到达 $(n - 1, n - 1)$,只能向下或向右走,并且只能穿越有效的格子(即只可以穿过值为 $0$ 或者 $1$ 的格子); - 当到达 $(n - 1, n - 1)$ 后,你要继续走,直到返回到 $(0, 0)$,只能向上或向左走,并且只能穿越有效的格子; - 当你经过一个格子且这个格子包含一个樱桃时,你将摘到樱桃并且这个格子会变成空的(值变为 0 ); - 如果在 $(0, 0)$ 和 $(n - 1, n - 1)$ 之间不存在一条可经过的路径,则无法摘到任何一个樱桃。 **说明**: - $n == grid.length$。 - $n == grid[i].length$。 - $1 \le n \le 50$。 - $grid[i][j]$ 为 $-1$、$0$ 或 $1$。 - $grid[0][0] != -1$。 - $grid[n - 1][n - 1] != -1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/12/14/grid.jpg) ```python 输入:grid = [[0,1,-1],[1,0,-1],[1,1,1]] 输出:5 解释:玩家从 (0, 0) 出发:向下、向下、向右、向右移动至 (2, 2) 。 在这一次行程中捡到 4 个樱桃,矩阵变成 [[0,1,-1],[0,0,-1],[0,0,0]] 。 然后,玩家向左、向上、向上、向左返回起点,再捡到 1 个樱桃。 总共捡到 5 个樱桃,这是最大可能值。 ``` - 示例 2: ```python 输入:grid = [[1,1,-1],[1,-1,1],[-1,1,1]] 输出:0 ``` ## 解题思路 ### 思路 1:动态规划 这道题的关键在于将「往返两次」转化为"两个人同时从起点出发到终点"。 **问题转化**: - 一个人从 $(0, 0)$ 走到 $(n-1, n-1)$ 再返回,等价于两个人同时从 $(0, 0)$ 走到 $(n-1, n-1)$。 - 两个人同时走,每次都向右或向下移动一步。 - 如果两个人在同一位置,樱桃只能摘一次。 **状态定义**: - 定义 $dp[k][i1][i2]$ 表示两个人都走了 $k$ 步,第一个人在第 $i1$ 行,第二个人在第 $i2$ 行时,能摘到的最多樱桃数。 - 由于 $i + j = k$,所以列数可以通过 $j = k - i$ 计算得出。 **状态转移**: - 两个人可以从四个方向转移过来:$(i1-1, i2-1)$、$(i1-1, i2)$、$(i1, i2-1)$、$(i1, i2)$。 - 如果两个人在同一位置,樱桃只计算一次;否则计算两次。 ### 思路 1:代码 ```python class Solution: def cherryPickup(self, grid: List[List[int]]) -> int: n = len(grid) # dp[k][i1][i2] 表示两个人都走了 k 步,第一个人在第 i1 行,第二个人在第 i2 行 dp = [[[-1] * n for _ in range(n)] for _ in range(2 * n - 1)] dp[0][0][0] = grid[0][0] for k in range(1, 2 * n - 1): for i1 in range(max(0, k - n + 1), min(k + 1, n)): for i2 in range(max(0, k - n + 1), min(k + 1, n)): j1, j2 = k - i1, k - i2 # 如果当前位置有荆棘,跳过 if grid[i1][j1] == -1 or grid[i2][j2] == -1: continue # 从四个方向转移 val = -1 for pi1 in range(i1 - 1, i1 + 1): for pi2 in range(i2 - 1, i2 + 1): if pi1 >= 0 and pi2 >= 0 and dp[k - 1][pi1][pi2] >= 0: val = max(val, dp[k - 1][pi1][pi2]) if val >= 0: # 如果两个人在同一位置,樱桃只计算一次 if i1 == i2: val += grid[i1][j1] else: val += grid[i1][j1] + grid[i2][j2] dp[k][i1][i2] = val return max(0, dp[2 * n - 2][n - 1][n - 1]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 是网格的边长。需要遍历 $O(n)$ 步,每步需要 $O(n^2)$ 的状态转移。 - **空间复杂度**:$O(n^3)$。需要存储 $O(n^3)$ 个状态。 ================================================ FILE: docs/solutions/0700-0799/contain-virus.md ================================================ # [0749. 隔离病毒](https://leetcode.cn/problems/contain-virus/) - 标签:深度优先搜索、广度优先搜索、数组、矩阵、模拟 - 难度:困难 ## 题目链接 - [0749. 隔离病毒 - 力扣](https://leetcode.cn/problems/contain-virus/) ## 题目大意 **描述**: 病毒扩散得很快,现在你的任务是尽可能地通过安装防火墙来隔离病毒。 假设世界由 $m \times n$ 的二维矩阵 $isInfected$ 组成,$isInfected[i][j] == 0$ 表示该区域未感染病毒,而 $isInfected[i][j] == 1$ 表示该区域已感染病毒。可以在任意 $2$ 个相邻单元之间的共享边界上安装一个防火墙(并且只有一个防火墙)。 每天晚上,病毒会从被感染区域向相邻未感染区域扩散,除非被防火墙隔离。现由于资源有限,每天你只能安装一系列防火墙来隔离其中一个被病毒感染的区域(一个区域或连续的一片区域),且该感染区域对未感染区域的威胁最大且 保证唯一 。 **要求**: 你需要努力使得最后有部分区域不被病毒感染,如果可以成功,那么返回需要使用的防火墙个数; 如果无法实现,则返回在世界被病毒全部感染时已安装的防火墙个数。 **说明**: - $m == isInfected.length$。 - $n == isInfected[i].length$。 - $1 \le m, n \le 50$。 - $isInfected[i][j]$ 为 $0$ 或者 $1$。 - 在整个描述的过程中,总有一个相邻的病毒区域,它将在下一轮「严格地感染更多未受污染的方块」。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/06/01/virus11-grid.jpg) ```python 输入: isInfected = [[0,1,0,0,0,0,0,1],[0,1,0,0,0,0,0,1],[0,0,0,0,0,0,0,1],[0,0,0,0,0,0,0,0]] 输出: 10 解释:一共有两块被病毒感染的区域。 在第一天,添加 5 墙隔离病毒区域的左侧。病毒传播后的状态是: ![](https://assets.leetcode.com/uploads/2021/06/01/virus12edited-grid.jpg) 第二天,在右侧添加 5 个墙来隔离病毒区域。此时病毒已经被完全控制住了。 ![](https://assets.leetcode.com/uploads/2021/06/01/virus13edited-grid.jpg) ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/06/01/virus2-grid.jpg) ```python 输入: isInfected = [[1,1,1],[1,0,1],[1,1,1]] 输出: 4 解释: 虽然只保存了一个小区域,但却有四面墙。 注意,防火墙只建立在两个不同区域的共享边界上。 ``` ## 解题思路 ### 思路 1:BFS + 模拟 这道题需要模拟病毒扩散和隔离的过程。 **解题步骤**: 1. 每天找出所有被感染的区域(连通块)。 2. 对于每个区域,计算其威胁值(能感染的未感染区域数量)和需要的防火墙数量。 3. 选择威胁值最大的区域进行隔离,累加防火墙数量。 4. 其他区域的病毒向相邻未感染区域扩散。 5. 重复以上步骤,直到没有区域可以扩散。 **实现细节**: - 使用 BFS 找出所有连通的感染区域。 - 对于每个区域,记录其能感染的未感染区域(去重)和需要的防火墙数量。 - 隔离后,将该区域标记为特殊值(如 $-1$),表示已隔离。 ### 思路 1:代码 ```python class Solution: def containVirus(self, isInfected: List[List[int]]) -> int: m, n = len(isInfected), len(isInfected[0]) directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] total_walls = 0 while True: visited = [[False] * n for _ in range(m)] regions = [] # 存储所有感染区域的信息 # 找出所有感染区域 for i in range(m): for j in range(n): if isInfected[i][j] == 1 and not visited[i][j]: # BFS 找出连通的感染区域 infected_cells = [] # 感染区域的所有单元格 threatened = set() # 能感染的未感染区域 walls = 0 # 需要的防火墙数量 queue = [(i, j)] visited[i][j] = True while queue: x, y = queue.pop(0) infected_cells.append((x, y)) for dx, dy in directions: nx, ny = x + dx, y + dy if 0 <= nx < m and 0 <= ny < n: if isInfected[nx][ny] == 1 and not visited[nx][ny]: queue.append((nx, ny)) visited[nx][ny] = True elif isInfected[nx][ny] == 0: walls += 1 threatened.add((nx, ny)) regions.append((len(threatened), walls, infected_cells, threatened)) # 如果没有感染区域,结束 if not regions: break # 找出威胁值最大的区域 regions.sort(reverse=True) threat_count, wall_count, infected_cells, threatened = regions[0] # 如果没有威胁,结束 if threat_count == 0: break # 隔离威胁最大的区域 total_walls += wall_count for x, y in infected_cells: isInfected[x][y] = -1 # 标记为已隔离 # 其他区域扩散 for i in range(1, len(regions)): for x, y in regions[i][3]: # threatened isInfected[x][y] = 1 return total_walls ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O((m \times n)^2)$,其中 $m$ 和 $n$ 是矩阵的行数和列数。最坏情况下需要进行 $O(m \times n)$ 轮模拟,每轮需要 $O(m \times n)$ 的时间。 - **空间复杂度**:$O(m \times n)$。需要存储访问标记和区域信息。 ================================================ FILE: docs/solutions/0700-0799/count-different-palindromic-subsequences.md ================================================ # [0730. 统计不同回文子序列](https://leetcode.cn/problems/count-different-palindromic-subsequences/) - 标签:字符串、动态规划 - 难度:困难 ## 题目链接 - [0730. 统计不同回文子序列 - 力扣](https://leetcode.cn/problems/count-different-palindromic-subsequences/) ## 题目大意 **描述**: 给定一个字符串 $s$。 **要求**: 返回 $s$ 中不同的非空回文子序列个数 。由于答案可能很大,请返回对 $10^9 + 7$ 取余的结果。 **说明**: - 字符串的子序列可以经由字符串删除 $0$ 个或多个字符获得。 - 如果一个序列与它反转后的序列一致,那么它是回文序列。 - 如果存在某个 $i$,满足 $ai \ne bi$,则两个序列 $a1, a2, ...$ 和 $b1, b2, ...$ 不同。 - $1 \le s.length \le 10^{3}$。 - $s[i]$ 仅包含 `'a'`, `'b'`, `'c'` 或 `'d'`。 **示例**: - 示例 1: ```python 输入:s = 'bccb' 输出:6 解释:6 个不同的非空回文子字符序列分别为:'b', 'c', 'bb', 'cc', 'bcb', 'bccb'。 注意:'bcb' 虽然出现两次但仅计数一次。 ``` - 示例 2: ```python 输入:s = 'abcdabcdabcdabcdabcdabcdabcdabcddcbadcbadcbadcbadcbadcbadcbadcba' 输出:104860361 解释:共有 3104860382 个不同的非空回文子序列,104860361 是对 109 + 7 取余后的值。 ``` ## 解题思路 ### 思路 1:区间动态规划 这道题要求统计不同的回文子序列个数。可以使用区间动态规划来解决。 **状态定义**: - 定义 $dp[i][j]$ 表示字符串 $s[i:j+1]$ 中不同回文子序列的个数。 **状态转移**: - 如果 $s[i] \neq s[j]$,则 $dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]$。 - 减去 $dp[i+1][j-1]$ 是因为它被重复计算了。 - 如果 $s[i] = s[j]$,需要考虑更复杂的情况: - 在 $s[i+1:j]$ 中找到第一个和最后一个与 $s[i]$ 相同的字符位置 $left$ 和 $right$。 - 如果 $left > right$:说明中间没有相同字符,$dp[i][j] = dp[i+1][j-1] \times 2 + 2$。 - 如果 $left = right$:说明中间只有一个相同字符,$dp[i][j] = dp[i+1][j-1] \times 2 + 1$。 - 如果 $left < right$:说明中间有多个相同字符,$dp[i][j] = dp[i+1][j-1] \times 2 - dp[left+1][right-1]$。 ### 思路 1:代码 ```python class Solution: def countPalindromicSubsequences(self, s: str) -> int: n = len(s) MOD = 10**9 + 7 dp = [[0] * n for _ in range(n)] # 初始化:单个字符是一个回文子序列 for i in range(n): dp[i][i] = 1 # 按区间长度从小到大遍历 for length in range(2, n + 1): for i in range(n - length + 1): j = i + length - 1 if s[i] == s[j]: # 在 s[i+1:j] 中找第一个和最后一个与 s[i] 相同的字符 left = i + 1 right = j - 1 while left <= right and s[left] != s[i]: left += 1 while left <= right and s[right] != s[i]: right -= 1 if left > right: # 中间没有相同字符 dp[i][j] = (dp[i + 1][j - 1] * 2 + 2) % MOD elif left == right: # 中间只有一个相同字符 dp[i][j] = (dp[i + 1][j - 1] * 2 + 1) % MOD else: # 中间有多个相同字符 dp[i][j] = (dp[i + 1][j - 1] * 2 - dp[left + 1][right - 1]) % MOD else: # s[i] != s[j] dp[i][j] = (dp[i + 1][j] + dp[i][j - 1] - dp[i + 1][j - 1]) % MOD return (dp[0][n - 1] + MOD) % MOD ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是字符串 $s$ 的长度。需要填充 $O(n^2)$ 个状态,每个状态的计算时间为 $O(1)$ 到 $O(n)$。 - **空间复杂度**:$O(n^2)$。需要存储 $dp$ 数组。 ================================================ FILE: docs/solutions/0700-0799/couples-holding-hands.md ================================================ # [0765. 情侣牵手](https://leetcode.cn/problems/couples-holding-hands/) - 标签:贪心、深度优先搜索、广度优先搜索、并查集、图 - 难度:困难 ## 题目链接 - [0765. 情侣牵手 - 力扣](https://leetcode.cn/problems/couples-holding-hands/) ## 题目大意 **描述**:$n$ 对情侣坐在连续排列的 $2 \times n$ 个座位上,想要牵对方的手。人和座位用 $0 \sim 2 \times n - 1$ 的整数表示。情侣按顺序编号,第一对是 $(0, 1)$,第二对是 $(2, 3)$,以此类推,最后一对是 $(2 \times n - 2, 2 \times n - 1)$。 给定代表情侣初始座位的数组 `row`,`row[i]` 表示第 `i` 个座位上的人的编号。 **要求**:计算最少交换座位的次数,以便每对情侣可以并肩坐在一起。每一次交换可以选择任意两人,让他们互换座位。 **说明**: - $2 \times n == row.length$。 - $2 \le n \le 30$。 - $n$ 是偶数。 - $0 \le row[i] < 2 \times n$。 - $row$ 中所有元素均无重复。 **示例**: - 示例 1: ```python 输入: row = [0,2,1,3] 输出: 1 解释: 只需要交换row[1]和row[2]的位置即可。 ``` - 示例 2: ```python 输入: row = [3,2,0,1] 输出: 0 解释: 无需交换座位,所有的情侣都已经可以手牵手了。 ``` ## 解题思路 ### 思路 1:并查集 先观察一下可以直接牵手的情侣特点: - 编号一定相邻。 - 编号为一个奇数一个偶数。 - 偶数 + 1 = 奇数。 将每对情侣的编号 `(0, 1) (2, 3) (4, 5) ...` 除以 `2` 可以得到 `(0, 0) (1, 1) (2, 2) ...`,这样相同编号就代表是一对情侣。 1. 按照 `2` 个一组的顺序,遍历一下所有编号。 1. 如果相邻的两人编号除以 `2` 相同,则两人是情侣,将其合并到一个集合中。 2. 如果相邻的两人编号不同,则将其合并到同一个集合中,而这两个人分别都有各自的对象,所以在后续遍历中两个人各自的对象和他们同组上的另一个人一定都会并到统一集合中,最终形成一个闭环。比如 `(0, 1) (1, 3) (2, 0) (3, 2)`。假设闭环对数为 `k`,最少需要交换 `k - 1` 次才能让情侣牵手。 2. 假设 `n` 对情侣中有 `m` 个闭环,则 `至少交换次数 = (n1 - 1) + (n2 - 1) + ... + (nn - 1) = n - m`。 ### 思路 1:代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return False self.parent[root_x] = root_y return True def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def minSwapsCouples(self, row: List[int]) -> int: size = len(row) n = size // 2 count = n union_find = UnionFind(n) for i in range(0, size, 2): if union_find.union(row[i] // 2, row[i + 1] // 2): count -= 1 return n - count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \alpha(n))$。其中 $n$ 是数组 $row$ 长度,$\alpha$ 是反 `Ackerman` 函数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0700-0799/cracking-the-safe.md ================================================ # [0753. 破解保险箱](https://leetcode.cn/problems/cracking-the-safe/) - 标签:深度优先搜索、图、欧拉回路 - 难度:困难 ## 题目链接 - [0753. 破解保险箱 - 力扣](https://leetcode.cn/problems/cracking-the-safe/) ## 题目大意 **描述**: 有一个需要密码才能打开的保险箱。密码是 $n$ 位数, 密码的每一位都是范围 $[0, k - 1]$ 中的一个数字。 保险箱有一种特殊的密码校验方法,你可以随意输入密码序列,保险箱会自动记住 最后 $n$ 位输入,如果匹配,则能够打开保险箱。 - 例如,正确的密码是 `"345"`,并且你输入的是 `"012345"`: - 输入 0 之后,最后 3 位输入是 `"0"`,不正确。 - 输入 1 之后,最后 3 位输入是 `"01"`,不正确。 - 输入 2 之后,最后 3 位输入是 `"012"`,不正确。 - 输入 3 之后,最后 3 位输入是 `"123"`,不正确。 - 输入 4 之后,最后 3 位输入是 `"234"`,不正确。 - 输入 5 之后,最后 3 位输入是 `"345"`,正确,打开保险箱。 **要求**: 在只知道密码位数 $n$ 和范围边界 $k$ 的前提下,请你找出并返回确保在输入的「某个时刻」能够打开保险箱的任一 最短「密码序列」。 **说明**: - $1 \le n \le 4$。 - $1 \le k \le 10$。 - $1 \le k^n \le 4096$。 **示例**: - 示例 1: ```python 输入:n = 1, k = 2 输出:"10" 解释:密码只有 1 位,所以输入每一位就可以。"01" 也能够确保打开保险箱。 ``` - 示例 2: ```python 输入:n = 2, k = 2 输出:"01100" 解释:对于每种可能的密码: - "00" 从第 4 位开始输入。 - "01" 从第 1 位开始输入。 - "10" 从第 3 位开始输入。 - "11" 从第 2 位开始输入。 因此 "01100" 可以确保打开保险箱。"01100"、"10011" 和 "11001" 也可以确保打开保险箱。 ``` ## 解题思路 ### 思路 1:欧拉回路(Hierholzer 算法) 这道题本质上是求欧拉回路问题。 **问题转化**: - 将每个 $n-1$ 位的数字序列看作一个节点。 - 如果在某个 $n-1$ 位序列后面添加一个数字 $d$,可以得到一个 $n$ 位密码,那么就从该节点连一条边到新的 $n-1$ 位序列(去掉第一位,加上 $d$)。 - 例如:$n=2, k=2$ 时,节点有 `0` 和 `1`,边有 `00`、`01`、`10`、`11`。 - 我们需要找到一条路径,经过所有边恰好一次(欧拉回路)。 **Hierholzer 算法**: 1. 从任意节点开始 DFS,每次选择一条未访问的边。 2. 将访问过的边标记,避免重复访问。 3. 当无法继续前进时,将当前节点加入结果(逆序)。 4. 最后将结果反转,得到欧拉回路。 ### 思路 1:代码 ```python class Solution: def crackSafe(self, n: int, k: int) -> str: if n == 1: return ''.join(map(str, range(k))) visited = set() result = [] start = '0' * (n - 1) def dfs(node): for digit in range(k): edge = node + str(digit) if edge not in visited: visited.add(edge) # 下一个节点是去掉第一位,加上当前数字 next_node = edge[1:] dfs(next_node) result.append(str(digit)) dfs(start) # 结果需要加上起始节点 return ''.join(result[::-1]) + start ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k^n)$。总共有 $k^n$ 个不同的 $n$ 位密码,需要遍历所有边。 - **空间复杂度**:$O(k^n)$。需要存储访问过的边和递归栈。 ================================================ FILE: docs/solutions/0700-0799/custom-sort-string.md ================================================ # [0791. 自定义字符串排序](https://leetcode.cn/problems/custom-sort-string/) - 标签:哈希表、字符串、排序 - 难度:中等 ## 题目链接 - [0791. 自定义字符串排序 - 力扣](https://leetcode.cn/problems/custom-sort-string/) ## 题目大意 **描述**: 给定两个字符串 $order$ 和 $s$。$order$ 的所有字母都是「唯一」的,并且以前按照一些自定义的顺序排序。 对 $s$ 的字符进行置换,使其与排序的 $order$ 相匹配。更具体地说,如果在 $order$ 中的字符 $x$ 出现字符 $y$ 之前,那么在排列后的字符串中,$x$ 也应该出现在 $y$ 之前。 **要求**: 返回满足这个性质的 $s$ 的任意一种排列。 **说明**: - $1 \le order.length \le 26$。 - $1 \le s.length \le 200$。 - $order$ 和 $s$ 由小写英文字母组成。 - $order$ 中的所有字符都不同。 **示例**: - 示例 1: ```python 输入: order = "cba", s = "abcd" 输出: "cbad" 解释: "a"、"b"、"c"是按顺序出现的,所以"a"、"b"、"c"的顺序应该是"c"、"b"、"a"。 因为"d"不是按顺序出现的,所以它可以在返回的字符串中的任何位置。"dcba"、"cdba"、"cbda"也是有效的输出。 ``` - 示例 2: ```python 输入: order = "cbafg", s = "abcd" 输出: "cbad" 解释:字符 "b"、"c" 和 "a" 规定了 s 中字符的顺序。s 中的字符 "d" 没有在 order 中出现,所以它的位置是弹性的。 按照出现的顺序,s 中的 "b"、"c"、"a" 应排列为"b"、"c"、"a"。"d" 可以放在任何位置,因为它没有按顺序排列。输出 "bcad" 遵循这一规则。其他排序如 "dbca" 或 "bcda" 也是有效的,只要维持 "b"、"c"、"a" 的顺序。 ``` ## 解题思路 ### 思路 1:自定义排序 这道题要求按照 $order$ 中的顺序对 $s$ 进行排序。 **解题步骤**: 1. 使用哈希表记录 $order$ 中每个字符的优先级(位置索引)。 2. 对于不在 $order$ 中的字符,赋予一个较大的优先级,使其排在后面。 3. 使用自定义排序函数,按照优先级对 $s$ 中的字符进行排序。 **优化方法**: - 可以先统计 $s$ 中每个字符的出现次数。 - 按照 $order$ 的顺序,依次将字符添加到结果中。 - 最后将不在 $order$ 中的字符添加到结果末尾。 ### 思路 1:代码 ```python class Solution: def customSortString(self, order: str, s: str) -> str: # 统计 s 中每个字符的出现次数 from collections import Counter count = Counter(s) result = [] # 按照 order 的顺序添加字符 for char in order: if char in count: result.append(char * count[char]) del count[char] # 添加不在 order 中的字符 for char, freq in count.items(): result.append(char * freq) return ''.join(result) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 是字符串 $s$ 的长度,$m$ 是字符串 $order$ 的长度。需要遍历两个字符串各一次。 - **空间复杂度**:$O(|\Sigma|)$,其中 $|\Sigma|$ 是字符集大小(这里是 $26$)。需要存储字符计数。 ================================================ FILE: docs/solutions/0700-0799/daily-temperatures.md ================================================ # [0739. 每日温度](https://leetcode.cn/problems/daily-temperatures/) - 标签:栈、数组、单调栈 - 难度:中等 ## 题目链接 - [0739. 每日温度 - 力扣](https://leetcode.cn/problems/daily-temperatures/) ## 题目大意 **描述**:给定一个列表 `temperatures`,`temperatures[i]` 表示第 `i` 天的气温。 **要求**:输出一个列表,列表上每个位置代表「如果要观测到更高的气温,至少需要等待的天数」。如果之后的气温不再升高,则用 `0` 来代替。 **说明**: - $1 \le temperatures.length \le 10^5$。 - $30 \le temperatures[i] \le 100$。 **示例**: - 示例 1: ```python 输入: temperatures = [73,74,75,71,69,72,76,73] 输出: [1,1,4,2,1,1,0,0] ``` - 示例 2: ```python 输入: temperatures = [30,40,50,60] 输出: [1,1,1,0] ``` ## 解题思路 题目的意思实际上就是给定一个数组,每个位置上有整数值。对于每个位置,在该位置右侧找到第一个比当前元素更大的元素。求「该元素」与「右侧第一个比当前元素更大的元素」之间的距离,将所有距离保存为数组返回结果。 最简单的思路是对于每个温度值,向后依次进行搜索,找到比当前温度更高的值。 更好的方式使用「单调递增栈」,栈中保存元素的下标。 ### 思路 1:单调栈 1. 首先,将答案数组 `ans` 全部赋值为 0。然后遍历数组每个位置元素。 2. 如果栈为空,则将当前元素的下标入栈。 3. 如果栈不为空,且当前数字大于栈顶元素对应数字,则栈顶元素出栈,并计算下标差。 4. 此时当前元素就是栈顶元素的下一个更高值,将其下标差存入答案数组 `ans` 中保存起来,判断栈顶元素。 5. 直到当前数字小于或等于栈顶元素,则停止出栈,将当前元素下标入栈。 6. 最后输出答案数组 `ans`。 ### 思路 1:代码 ```python class Solution: def dailyTemperatures(self, T: List[int]) -> List[int]: n = len(T) stack = [] ans = [0 for _ in range(n)] for i in range(n): while stack and T[i] > T[stack[-1]]: index = stack.pop() ans[index] = (i-index) stack.append(i) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0700-0799/delete-and-earn.md ================================================ # [0740. 删除并获得点数](https://leetcode.cn/problems/delete-and-earn/) - 标签:数组、哈希表、动态规划 - 难度:中等 ## 题目链接 - [0740. 删除并获得点数 - 力扣](https://leetcode.cn/problems/delete-and-earn/) ## 题目大意 **描述**: 给定一个整数数组 $nums$,你可以对它进行一些操作。 每次操作中,选择任意一个 $nums[i]$,删除它并获得 $nums[i]$ 的点数。之后,你必须删除所有等于 $nums[i] - 1$ 和 $nums[i] + 1$ 的元素。 开始你拥有 $0$ 个点数。 **要求**: 返回你能通过这些操作获得的最大点数。 **说明**: - $1 \le nums.length \le 2 \times 10^{4}$。 - $1 \le nums[i] \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:nums = [3,4,2] 输出:6 解释: 删除 4 获得 4 个点数,因此 3 也被删除。 之后,删除 2 获得 2 个点数。总共获得 6 个点数。 ``` - 示例 2: ```python 输入:nums = [2,2,3,3,3,4] 输出:9 解释: 删除 3 获得 3 个点数,接着要删除两个 2 和 4 。 之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。 总共获得 9 个点数。 ``` ## 解题思路 ### 思路 1:动态规划 这道题可以转化为「打家劫舍」问题。选择数字 $x$ 后,所有 $x - 1$ 和 $x + 1$ 都会被删除,相当于不能选择相邻的数字。 **实现步骤**: 1. 统计每个数字的总点数:$total[i]$ 表示选择所有数字 $i$ 能获得的总点数。 2. 问题转化为:在数组 $total$ 中选择一些不相邻的元素,使得和最大。 3. 定义 $dp[i]$ 表示考虑前 $i$ 个数字能获得的最大点数: - $dp[i] = \max(dp[i-1], dp[i-2] + total[i])$ - 不选择 $i$:$dp[i-1]$ - 选择 $i$:$dp[i-2] + total[i]$(不能选择 $i-1$) ### 思路 1:代码 ```python class Solution: def deleteAndEarn(self, nums: List[int]) -> int: if not nums: return 0 # 统计每个数字的总点数 max_num = max(nums) total = [0] * (max_num + 1) for num in nums: total[num] += num # 动态规划 if max_num == 0: return total[0] # dp[i] 表示考虑前 i 个数字能获得的最大点数 prev2 = total[0] # dp[i-2] prev1 = max(total[0], total[1]) # dp[i-1] for i in range(2, max_num + 1): curr = max(prev1, prev2 + total[i]) prev2 = prev1 prev1 = curr return prev1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 是 $nums$ 的长度,$m$ 是 $nums$ 中的最大值。 - **空间复杂度**:$O(m)$,$total$ 数组的空间。 ================================================ FILE: docs/solutions/0700-0799/design-hashmap.md ================================================ # [0706. 设计哈希映射](https://leetcode.cn/problems/design-hashmap/) - 标签:设计、数组、哈希表、链表、哈希函数 - 难度:简单 ## 题目链接 - [0706. 设计哈希映射 - 力扣](https://leetcode.cn/problems/design-hashmap/) ## 题目大意 **要求**:不使用任何内建的哈希表库设计一个哈希映射(`HashMap`)。 需要满足以下操作: - `MyHashMap()` 用空映射初始化对象。 - `void put(int key, int value) 向 HashMap` 插入一个键值对 `(key, value)` 。如果 `key` 已经存在于映射中,则更新其对应的值 `value`。 - `int get(int key)` 返回特定的 `key` 所映射的 `value`;如果映射中不包含 `key` 的映射,返回 `-1`。 - `void remove(key)` 如果映射中存在 key 的映射,则移除 `key` 和它所对应的 `value` 。 **说明**: - $0 \le key, value \le 10^6$。 - 最多调用 $10^4$ 次 `put`、`get` 和 `remove` 方法。 **示例**: - 示例 1: ```python 输入: ["MyHashMap", "put", "put", "get", "get", "put", "get", "remove", "get"] [[], [1, 1], [2, 2], [1], [3], [2, 1], [2], [2], [2]] 输出: [null, null, null, 1, -1, null, 1, null, -1] 解释: MyHashMap myHashMap = new MyHashMap(); myHashMap.put(1, 1); // myHashMap 现在为 [[1,1]] myHashMap.put(2, 2); // myHashMap 现在为 [[1,1], [2,2]] myHashMap.get(1); // 返回 1 ,myHashMap 现在为 [[1,1], [2,2]] myHashMap.get(3); // 返回 -1(未找到),myHashMap 现在为 [[1,1], [2,2]] myHashMap.put(2, 1); // myHashMap 现在为 [[1,1], [2,1]](更新已有的值) myHashMap.get(2); // 返回 1 ,myHashMap 现在为 [[1,1], [2,1]] myHashMap.remove(2); // 删除键为 2 的数据,myHashMap 现在为 [[1,1]] myHashMap.get(2); // 返回 -1(未找到),myHashMap 现在为 [[1,1]] ``` ## 解题思路 ### 思路 1:链地址法 和 [0705. 设计哈希集合](https://leetcode.cn/problems/design-hashset/) 类似。这里我们使用「链地址法」来解决哈希冲突。即利用「数组 + 链表」的方式实现哈希集合。 1. 定义哈希表长度 `buckets` 为 `1003`。 2. 定义一个一维长度为 `buckets` 的二维数组 `table`。其中第一维度用于计算哈希函数,为关键字 `key` 分桶。第二个维度用于存放 `key` 和对应的 `value`。第二维度的数组会根据 `key` 值动态增长,用数组模拟真正的链表。 3. 定义一个 `hash(key)` 的方法,将 `key` 转换为对应的地址 `hash_key`。 4. 进行 `put` 操作时,根据 `hash(key)` 方法,获取对应的地址 `hash_key`。然后遍历 `hash_key` 对应的数组元素,查找与 `key` 值一样的元素。 1. 如果找到与 `key` 值相同的元素,则更改该元素对应的 `value` 值。 2. 如果没找到与 `key` 值相同的元素,则在第二维数组 `table[hask_key]` 中增加元素,元素为 `(key, value)` 组成的元组。 5. 进行 `get` 操作跟 `put` 操作差不多。根据 `hash(key)` 方法,获取对应的地址 `hash_key`。然后遍历 `hash_key` 对应的数组元素,查找与 `key` 值一样的元素。 1. 如果找到与 `key` 值相同的元素,则返回该元素对应的 `value`。 2. 如果没找到与 `key` 值相同的元素,则返回 `-1`。 ### 思路 1:代码 ```python class MyHashMap: def __init__(self): self.buckets = 1003 self.table = [[] for _ in range(self.buckets)] def hash(self, key): return key % self.buckets def put(self, key: int, value: int) -> None: hash_key = self.hash(key) for item in self.table[hash_key]: if key == item[0]: item[1] = value return self.table[hash_key].append([key, value]) def get(self, key: int) -> int: hash_key = self.hash(key) for item in self.table[hash_key]: if key == item[0]: return item[1] return -1 def remove(self, key: int) -> None: hash_key = self.hash(key) for i, item in enumerate(self.table[hash_key]): if key == item[0]: self.table[hash_key].pop(i) return ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\frac{n}{b})$。其中 $n$ 为哈希表中元素数量,$b$ 为链表的数量。 - **空间复杂度**:$O(n + b)$。 ================================================ FILE: docs/solutions/0700-0799/design-hashset.md ================================================ # [0705. 设计哈希集合](https://leetcode.cn/problems/design-hashset/) - 标签:设计、数组、哈希表、链表、哈希函数 - 难度:简单 ## 题目链接 - [0705. 设计哈希集合 - 力扣](https://leetcode.cn/problems/design-hashset/) ## 题目大意 **要求**:不使用内建的哈希表库,自行实现一个哈希集合(HashSet)。 需要满足以下操作: - `void add(key)` 向哈希集合中插入值 $key$。 - `bool contains(key)` 返回哈希集合中是否存在这个值 $key$。 - `void remove(key)` 将给定值 $key$ 从哈希集合中删除。如果哈希集合中没有这个值,什么也不做。 **说明**: - $0 \le key \le 10^6$。 - 最多调用 $10^4$ 次 `add`、`remove` 和 `contains`。 **示例**: - 示例 1: ```python 输入: ["MyHashSet", "add", "add", "contains", "contains", "add", "contains", "remove", "contains"] [[], [1], [2], [1], [3], [2], [2], [2], [2]] 输出: [null, null, null, true, false, null, true, null, false] 解释: MyHashSet myHashSet = new MyHashSet(); myHashSet.add(1); // set = [1] myHashSet.add(2); // set = [1, 2] myHashSet.contains(1); // 返回 True myHashSet.contains(3); // 返回 False ,(未找到) myHashSet.add(2); // set = [1, 2] myHashSet.contains(2); // 返回 True myHashSet.remove(2); // set = [1] myHashSet.contains(2); // 返回 False ,(已移除) ``` ## 解题思路 ### 思路 1:数组 + 链表 定义一个一维长度为 $buckets$ 的二维数组 $table$。 第一维度用于计算哈希函数,为 $key$ 进行分桶。第二个维度用于寻找 $key$ 存放的具体位置。第二维度的数组会根据 $key$ 值动态增长,模拟真正的链表。 ### 思路 1:代码 ```python class MyHashSet: def __init__(self): self.buckets = 1003 self.table = [[] for _ in range(self.buckets)] def hash(self, key): return key % self.buckets def add(self, key: int) -> None: hash_key = self.hash(key) if key in self.table[hash_key]: return self.table[hash_key].append(key) def remove(self, key: int) -> None: hash_key = self.hash(key) if key not in self.table[hash_key]: return self.table[hash_key].remove(key) def contains(self, key: int) -> bool: hash_key = self.hash(key) return key in self.table[hash_key] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\frac{n}{m})$,其中 $n$ 为哈希表中的元素数量,$b$ 为 $table$ 的元素个数,也就是链表的数量。 - **空间复杂度**:$O(n + m)$。 ================================================ FILE: docs/solutions/0700-0799/design-linked-list.md ================================================ # [0707. 设计链表](https://leetcode.cn/problems/design-linked-list/) - 标签:设计、链表 - 难度:中等 ## 题目链接 - [0707. 设计链表 - 力扣](https://leetcode.cn/problems/design-linked-list/) ## 题目大意 **要求**:设计实现一个链表,需要支持以下操作: - `get(index)`:获取链表中第 `index` 个节点的值。如果索引无效,则返回 `-1`。 - `addAtHead(val)`:在链表的第一个元素之前添加一个值为 `val` 的节点。插入后,新节点将成为链表的第一个节点。 - `addAtTail(val)`:将值为 `val` 的节点追加到链表的最后一个元素。 - `addAtIndex(index, val)`:在链表中的第 `index` 个节点之前添加值为 `val` 的节点。如果 `index` 等于链表的长度,则该节点将附加到链表的末尾。如果 `index` 大于链表长度,则不会插入节点。如果 `index` 小于 `0`,则在头部插入节点。 - `deleteAtIndex(index)`:如果索引 `index` 有效,则删除链表中的第 `index` 个节点。 **说明**: - 所有`val`值都在 $[1, 1000]$ 之内。 - 操作次数将在 $[1, 1000]$ 之内。 - 请不要使用内置的 `LinkedList` 库。 **示例**: - 示例 1: ```python MyLinkedList linkedList = new MyLinkedList(); linkedList.addAtHead(1); linkedList.addAtTail(3); linkedList.addAtIndex(1,2); // 链表变为 1 -> 2 -> 3 linkedList.get(1); // 返回 2 linkedList.deleteAtIndex(1); // 现在链表是 1-> 3 linkedList.get(1); // 返回 3 ``` ## 解题思路 ### 思路 1:单链表 新建一个带有 `val` 值 和 `next` 指针的链表节点类, 然后按照要求对节点进行操作。 ### 思路 1:代码 ```python class ListNode: def __init__(self, x): self.val = x self.next = None class MyLinkedList: def __init__(self): """ Initialize your data structure here. """ self.size = 0 self.head = ListNode(0) def get(self, index: int) -> int: """ Get the value of the index-th node in the linked list. If the index is invalid, return -1. """ if index < 0 or index >= self.size: return -1 curr = self.head for _ in range(index + 1): curr = curr.next return curr.val def addAtHead(self, val: int) -> None: """ Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. """ self.addAtIndex(0, val) def addAtTail(self, val: int) -> None: """ Append a node of value val to the last element of the linked list. """ self.addAtIndex(self.size, val) def addAtIndex(self, index: int, val: int) -> None: """ Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. """ if index > self.size: return if index < 0: index = 0 self.size += 1 pre = self.head for _ in range(index): pre = pre.next add_node = ListNode(val) add_node.next = pre.next pre.next = add_node def deleteAtIndex(self, index: int) -> None: """ Delete the index-th node in the linked list, if the index is valid. """ if index < 0 or index >= self.size: return self.size -= 1 pre = self.head for _ in range(index): pre = pre.next pre.next = pre.next.next ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `addAtHead(val)`:$O(1)$。 - `get(index)`、`addAtTail(val)`、`del eteAtIndex(index)`:$O(k)$。$k$ 指的是元素的索引。 - `addAtIndex(index, val)`:$O(n)$。$n$ 指的是链表的元素个数。 - **空间复杂度**:$O(1)$。 ### 思路 2:双链表 新建一个带有 `val` 值和 `next` 指针、`prev` 指针的链表节点类,然后按照要求对节点进行操作。 ### 思路 2:代码 ```python class ListNode: def __init__(self, x): self.val = x self.next = None self.prev = None class MyLinkedList: def __init__(self): """ Initialize your data structure here. """ self.size = 0 self.head = ListNode(0) self.tail = ListNode(0) self.head.next = self.tail self.tail.prev = self.head def get(self, index: int) -> int: """ Get the value of the index-th node in the linked list. If the index is invalid, return -1. """ if index < 0 or index >= self.size: return -1 if index + 1 < self.size - index: curr = self.head for _ in range(index + 1): curr = curr.next else: curr = self.tail for _ in range(self.size - index): curr = curr.prev return curr.val def addAtHead(self, val: int) -> None: """ Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. """ self.addAtIndex(0, val) def addAtTail(self, val: int) -> None: """ Append a node of value val to the last element of the linked list. """ self.addAtIndex(self.size, val) def addAtIndex(self, index: int, val: int) -> None: """ Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. """ if index > self.size: return if index < 0: index = 0 if index < self.size - index: prev = self.head for _ in range(index): prev = prev.next next = prev.next else: next = self.tail for _ in range(self.size - index): next = next.prev prev = next.prev self.size += 1 add_node = ListNode(val) add_node.prev = prev add_node.next = next prev.next = add_node next.prev = add_node def deleteAtIndex(self, index: int) -> None: """ Delete the index-th node in the linked list, if the index is valid. """ if index < 0 or index >= self.size: return if index < self.size - index: prev = self.head for _ in range(index): prev = prev.next next = prev.next.next else: next = self.tail for _ in range(self.size - index - 1): next = next.prev prev = next.prev.prev self.size -= 1 prev.next = next next.prev = prev ``` ### 思路 2:复杂度分析 - **时间复杂度**: - `addAtHead(val)`、`addAtTail(val)`:$O(1)$。 - `get(index)`、`addAtIndex(index, val)`、`del eteAtIndex(index)`:$O(min(k, n - k))$。$n$ 指的是链表的元素个数,$k$ 指的是元素的索引。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0700-0799/domino-and-tromino-tiling.md ================================================ # [0790. 多米诺和托米诺平铺](https://leetcode.cn/problems/domino-and-tromino-tiling/) - 标签:动态规划 - 难度:中等 ## 题目链接 - [0790. 多米诺和托米诺平铺 - 力扣](https://leetcode.cn/problems/domino-and-tromino-tiling/) ## 题目大意 **描述**: 有两种形状的瓷砖:一种是 $2 \times 1$ 的多米诺形,另一种是形如 `"L"` 的托米诺形。两种形状都可以旋转。 ![](https://assets.leetcode.com/uploads/2021/07/15/lc-domino.jpg) 给定整数 $n$。 **要求**: 返回可以平铺 $2 \times n$ 的面板的方法的数量。返回对 $10^9 + 7$ 取模的值。 **说明**: - 平铺:指的是每个正方形都必须有瓷砖覆盖。两个平铺不同,当且仅当面板上有四个方向上的相邻单元中的两个,使得恰好有一个平铺有一个瓷砖占据两个正方形。 - $1 \le n \le 10^{3}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/07/15/lc-domino1.jpg) ```python 示例 1: 输入: n = 3 输出: 5 解释: 五种不同的方法如上所示。 ``` - 示例 2: ```python 输入: n = 1 输出: 1 ``` ## 解题思路 ### 思路 1:动态规划 这道题需要计算平铺 $2 \times n$ 面板的方法数。 **状态定义**: - 定义 $dp[i][0]$ 表示平铺到第 $i$ 列,且第 $i$ 列两行都被填满的方法数。 - 定义 $dp[i][1]$ 表示平铺到第 $i$ 列,且第 $i$ 列只有上行被填满的方法数。 - 定义 $dp[i][2]$ 表示平铺到第 $i$ 列,且第 $i$ 列只有下行被填满的方法数。 **状态转移**: - $dp[i][0]$ 可以从以下状态转移: - $dp[i-1][0]$:放置一个竖直的多米诺。 - $dp[i-2][0]$:放置两个水平的多米诺。 - $dp[i-1][1]$ 和 $dp[i-1][2]$:放置托米诺。 - $dp[i][1]$ 可以从 $dp[i-1][0]$ 或 $dp[i-1][2]$ 转移。 - $dp[i][2]$ 可以从 $dp[i-1][0]$ 或 $dp[i-1][1]$ 转移。 **递推公式**: - $dp[i][0] = (dp[i-1][0] + dp[i-2][0] + dp[i-1][1] + dp[i-1][2]) \mod (10^9 + 7)$ - $dp[i][1] = (dp[i-1][0] + dp[i-1][2]) \mod (10^9 + 7)$ - $dp[i][2] = (dp[i-1][0] + dp[i-1][1]) \mod (10^9 + 7)$ ### 思路 1:代码 ```python class Solution: def numTilings(self, n: int) -> int: MOD = 10**9 + 7 if n == 1: return 1 if n == 2: return 2 # dp[i][0]: 第 i 列两行都填满 # dp[i][1]: 第 i 列只有上行填满 # dp[i][2]: 第 i 列只有下行填满 dp = [[0] * 3 for _ in range(n + 1)] dp[0][0] = 1 dp[1][0] = 1 for i in range(2, n + 1): dp[i][0] = (dp[i-1][0] + dp[i-2][0] + dp[i-1][1] + dp[i-1][2]) % MOD dp[i][1] = (dp[i-2][0] + dp[i-1][2]) % MOD dp[i][2] = (dp[i-2][0] + dp[i-1][1]) % MOD return dp[n][0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。需要遍历 $n$ 列。 - **空间复杂度**:$O(n)$。可以优化到 $O(1)$,只保留最近的几个状态。 ================================================ FILE: docs/solutions/0700-0799/escape-the-ghosts.md ================================================ # [0789. 逃脱阻碍者](https://leetcode.cn/problems/escape-the-ghosts/) - 标签:数组、数学 - 难度:中等 ## 题目链接 - [0789. 逃脱阻碍者 - 力扣](https://leetcode.cn/problems/escape-the-ghosts/) ## 题目大意 **描述**: 你在进行一个简化版的吃豆人游戏。你从 $[0, 0]$ 点开始出发,你的目的地是 $target = [xtarget, ytarget]$。地图上有一些阻碍者,以数组 $ghosts$ 给出,第 $i$ 个阻碍者从 $ghosts[i] = [xi, yi]$ 出发。所有输入均为「整数坐标」。 每一回合,你和阻碍者们可以同时向东,西,南,北四个方向移动,每次可以移动到距离原位置 $1$ 个单位 的新位置。当然,也可以选择「不动」。所有动作「同时」发生。 如果你可以在任何阻碍者抓住你「之前」到达目的地(阻碍者可以采取任意行动方式),则被视为逃脱成功。如果你和阻碍者「同时」到达了一个位置(包括目的地)「都不算」是逃脱成功。 **要求**: 如果不管阻碍者怎么移动都可以成功逃脱时,输出 true;否则,输出 false。 **说明**: - $1 \le ghosts.length \le 10^{3}$。 - $ghosts[i].length == 2$。 - $-10^{4} \le xi, yi \le 10^{4}$。 - 同一位置可能有「多个阻碍者」。 - $target.length == 2$。 - $-10^{4} \le xtarget, ytarget \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:ghosts = [[1,0],[0,3]], target = [0,1] 输出:true 解释:你可以直接一步到达目的地 (0,1) ,在 (1, 0) 或者 (0, 3) 位置的阻碍者都不可能抓住你。 ``` - 示例 2: ```python 输入:ghosts = [[1,0]], target = [2,0] 输出:false 解释:你需要走到位于 (2, 0) 的目的地,但是在 (1, 0) 的阻碍者位于你和目的地之间。 ``` ## 解题思路 ### 思路 1:曼哈顿距离 这道题的关键在于理解:如果阻碍者能在你之前或同时到达目的地,你就无法逃脱。 **核心观察**: - 你和阻碍者都使用曼哈顿距离移动。 - 从 $(x_1, y_1)$ 到 $(x_2, y_2)$ 的曼哈顿距离为 $|x_1 - x_2| + |y_1 - y_2|$。 - 如果存在任何一个阻碍者到目的地的距离 $\leq$ 你到目的地的距离,你就无法逃脱。 - 因为阻碍者可以采取最优策略,直接朝目的地移动。 **解题步骤**: 1. 计算你从起点 $(0, 0)$ 到目的地 $target$ 的曼哈顿距离。 2. 对于每个阻碍者,计算其到目的地的曼哈顿距离。 3. 如果所有阻碍者到目的地的距离都严格大于你的距离,返回 `True`。 4. 否则返回 `False`。 ### 思路 1:代码 ```python class Solution: def escapeGhosts(self, ghosts: List[List[int]], target: List[int]) -> bool: # 计算你到目的地的曼哈顿距离 my_distance = abs(target[0]) + abs(target[1]) # 检查每个阻碍者到目的地的距离 for ghost in ghosts: ghost_distance = abs(ghost[0] - target[0]) + abs(ghost[1] - target[1]) # 如果任何阻碍者能在你之前或同时到达,返回 False if ghost_distance <= my_distance: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是阻碍者的数量。需要遍历所有阻碍者。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0700-0799/find-k-th-smallest-pair-distance.md ================================================ # [0719. 找出第 K 小的距离对](https://leetcode.cn/problems/find-k-th-smallest-pair-distance/) - 标签:数组、双指针、二分查找、排序 - 难度:困难 ## 题目链接 - [0719. 找出第 K 小的距离对 - 力扣](https://leetcode.cn/problems/find-k-th-smallest-pair-distance/) ## 题目大意 **描述**:给定一个整数数组 $nums$,对于数组中不同的数 $nums[i]$、$nums[j]$ 之间的距离定义为 $nums[i]$ 和 $nums[j]$ 的绝对差值,即 $dist(nums[i], nums[j]) = abs(nums[i] - nums[j])$。 **要求**:求所有数对之间第 $k$ 个最小距离。 **说明**: - $n == nums.length$ - $2 \le n \le 10^4$。 - $0 \le nums[i] \le 10^6$。 - $1 \le k \le n \times (n - 1) / 2$。 **示例**: - 示例 1: ```python 输入:nums = [1,3,1], k = 1 输出:0 解释:数对和对应的距离如下: (1,3) -> 2 (1,1) -> 0 (3,1) -> 2 距离第 1 小的数对是 (1,1) ,距离为 0。 ``` - 示例 2: ```python 输入:nums = [1,1,1], k = 2 输出:0 ``` ## 解题思路 ### 思路 1:二分查找算法 一般来说 topK 问题都可以用堆排序来解决。但是这道题使用堆排序超时了。所以需要换其他方法。 先来考虑第 $k$ 个最小距离的范围。这个范围一定在 $[0, max(nums) - min(nums)]$ 之间。 我们可以对 $nums$ 先进行排序,然后得到最小距离为 $0$,最大距离为 $nums[-1] - nums[0]$。我们可以在这个区间上进行二分,对于二分的位置 $mid$,统计距离小于等于 $mid$ 的距离对数,并根据它和 $k$ 的关系调整区间上下界。 统计对数可以使用双指针来计算出所有小于等于 $mid$ 的距离对数目。 1. 维护两个指针 $left$、$right$。$left$、$right$ 都指向数组开头位置。 2. 然后不断移动 $right$,计算 $nums[right]$ 和 $nums[left]$ 之间的距离。 3. 如果大于 $mid$,则 $left$ 向右移动,直到距离小于等于 $mid$ 时,统计当前距离对数为 $right - left$。 4. 最终将这些符合要求的距离对数累加,就得到了所有小于等于 $mid$ 的距离对数目。 ### 思路 1:代码 ```python class Solution: def smallestDistancePair(self, nums: List[int], k: int) -> int: def get_count(dist): left, count = 0, 0 for right in range(1, len(nums)): while nums[right] - nums[left] > dist: left += 1 count += (right - left) return count nums.sort() left, right = 0, nums[-1] - nums[0] while left < right: mid = left + (right - left) // 2 if get_count(mid) >= k: right = mid else: left = mid + 1 return left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 为数组 $nums$ 中的元素个数。 - **空间复杂度**:$O(\log n)$,排序算法所用到的空间复杂度为 $O(\log n)$。 ================================================ FILE: docs/solutions/0700-0799/find-pivot-index.md ================================================ # [0724. 寻找数组的中心下标](https://leetcode.cn/problems/find-pivot-index/) - 标签:数组、前缀和 - 难度:简单 ## 题目链接 - [0724. 寻找数组的中心下标 - 力扣](https://leetcode.cn/problems/find-pivot-index/) ## 题目大意 **描述**:给定一个数组 $nums$。 **要求**:找到「左侧元素和」与「右侧元素和相等」的位置,如果找不到,则返回 $-1$。 **说明**: - $1 \le nums.length \le 10^4$。 - $-1000 \le nums[i] \le 1000$。 **示例**: - 示例 1: ```python 输入:nums = [1, 7, 3, 6, 5, 6] 输出:3 解释: 中心下标是 3 。 左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11, 右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11,二者相等。 ``` - 示例 2: ```python 输入:nums = [1, 2, 3] 输出:-1 解释: 数组中不存在满足此条件的中心下标。 ``` ## 解题思路 ### 思路 1:两次遍历 两次遍历,第一次遍历先求出数组全部元素和。第二次遍历找到左侧元素和恰好为全部元素和一半的位置。 ### 思路 1:代码 ```python class Solution: def pivotIndex(self, nums: List[int]) -> int: sum = 0 for i in range(len(nums)): sum += nums[i] curr_sum = 0 for i in range(len(nums)): if curr_sum * 2 + nums[i] == sum: return i curr_sum += nums[i] return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。两次遍历的时间复杂度为 $O(2 \times n)$ ,$O(2 \times n) == O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0700-0799/find-smallest-letter-greater-than-target.md ================================================ # [0744. 寻找比目标字母大的最小字母](https://leetcode.cn/problems/find-smallest-letter-greater-than-target/) - 标签:数组、二分查找 - 难度:简单 ## 题目链接 - [0744. 寻找比目标字母大的最小字母 - 力扣](https://leetcode.cn/problems/find-smallest-letter-greater-than-target/) ## 题目大意 **描述**:给你一个字符数组 $letters$,该数组按非递减顺序排序,以及一个字符 $target$。$letters$ 里至少有两个不同的字符。 **要求**:找出 $letters$ 中大于 $target$ 的最小的字符。如果不存在这样的字符,则返回 $letters$ 的第一个字符。 **说明**: - $2 \le letters.length \le 10^4$。 - $letters[i]$$ 是一个小写字母。 - $letters$ 按非递减顺序排序。 - $letters$ 最少包含两个不同的字母。 - $target$ 是一个小写字母。 **示例**: - 示例 1: ```python 输入: letters = ["c", "f", "j"],target = "a" 输出: "c" 解释:letters 中字典上比 'a' 大的最小字符是 'c'。 ``` - 示例 2: ```python 输入: letters = ["c","f","j"], target = "c" 输出: "f" 解释:letters 中字典顺序上大于 'c' 的最小字符是 'f'。 ``` ## 解题思路 ### 思路 1:二分查找 利用二分查找,找到比 $target$ 大的字母。注意 $target$ 可能大于 $letters$ 的所有字符,此时应返回 $letters$ 的第一个字母。 我们可以假定 $target$ 的取值范围为 $[0, len(letters)]$。当 $target$ 取到 $len(letters)$ 时,说明 $target$ 大于 $letters$ 的所有字符,对 $len(letters)$ 取余即可得到 $letters[0]$。 ### 思路 1:代码 ```python class Solution: def nextGreatestLetter(self, letters: List[str], target: str) -> str: n = len(letters) left = 0 right = n while left < right: mid = left + (right - left) // 2 if letters[mid] <= target: left = mid + 1 else: right = mid return letters[left % n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 为字符数组 $letters$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0700-0799/flood-fill.md ================================================ # [0733. 图像渲染](https://leetcode.cn/problems/flood-fill/) - 标签:深度优先搜索、广度优先搜索、数组、矩阵 - 难度:简单 ## 题目链接 - [0733. 图像渲染 - 力扣](https://leetcode.cn/problems/flood-fill/) ## 题目大意 给定一个二维数组 image 表示图画,数组的每个元素值表示该位置的像素值大小。再给定一个坐标 (sr, sc) 表示图像渲染开始的位置。然后再给定一个新的颜色值 newColor。现在要求:将坐标 (sr, sc) 以及 (sr, sc) 相连的上下左右区域上与 (sr, sc) 原始颜色相同的区域染色为 newColor。返回染色后的二维数组。 ## 解题思路 从起点开始,对上下左右四个方向进行广度优先搜索。每次搜索到一个位置时,如果该位置上的像素值与初始位置像素值相同,则更新该位置像素值,并将该位置加入队列中。最后将二维数组返回。 - 注意:如果起点位置初始颜色和新颜色值 newColor 相同,则不需要染色,直接返回原数组即可。 ## 代码 ```python import collections class Solution: def floodFill(self, image: List[List[int]], sr: int, sc: int, newColor: int) -> List[List[int]]: if newColor == image[sr][sc]: return image directions = {(1, 0), (-1, 0), (0, 1), (0, -1)} queue = collections.deque([(sr, sc)]) oriColor = image[sr][sc] while queue: point = queue.popleft() image[point[0]][point[1]] = newColor for direction in directions: new_i = point[0] + direction[0] new_j = point[1] + direction[1] if 0 <= new_i < len(image) and 0 <= new_j < len(image[0]) and image[new_i][new_j] == oriColor: queue.append((new_i, new_j)) return image ``` ================================================ FILE: docs/solutions/0700-0799/global-and-local-inversions.md ================================================ # [0775. 全局倒置与局部倒置](https://leetcode.cn/problems/global-and-local-inversions/) - 标签:数组、数学 - 难度:中等 ## 题目链接 - [0775. 全局倒置与局部倒置 - 力扣](https://leetcode.cn/problems/global-and-local-inversions/) ## 题目大意 **描述**: 给定一个长度为 $n$ 的整数数组 $nums$ ,表示由范围 $[0, n - 1]$ 内所有整数组成的一个排列。 - 「全局倒置」的数目等于满足下述条件不同下标对 $(i, j)$ 的数目: - $0 \le i < j < n$ - $nums[i] > nums[j]$ - 「局部倒置」的数目等于满足下述条件的下标 i 的数目: - $0 <= i < n - 1$ - $nums[i] > nums[i + 1]$ **要求**: 当数组 $nums$ 中「全局倒置」的数量等于「局部倒置」的数量时,返回 true;否则,返回 false。 **说明**: - $n == nums.length$。 - $1 \le n \le 10^{5}$。 - $0 \le nums[i] \lt n$。 - $nums$ 中的所有整数 互不相同。 - $nums$ 是范围 $[0, n - 1]$ 内所有数字组成的一个排列。 **示例**: - 示例 1: ```python 输入:nums = [1,0,2] 输出:true 解释:有 1 个全局倒置,和 1 个局部倒置。 ``` - 示例 2: ```python 输入:nums = [1,2,0] 输出:false 解释:有 2 个全局倒置,和 1 个局部倒置。 ``` ## 解题思路 ### 思路 1:数学规律 这道题的关键在于观察全局倒置和局部倒置的关系。 **核心观察**: - 局部倒置是指相邻元素的倒置:$nums[i] > nums[i+1]$。 - 全局倒置是指任意两个元素的倒置:$i < j$ 且 $nums[i] > nums[j]$。 - 显然,所有局部倒置都是全局倒置。 - 要使全局倒置数量等于局部倒置数量,就要保证不存在非局部的全局倒置。 - 即:不存在 $i < j - 1$ 且 $nums[i] > nums[j]$ 的情况。 **等价条件**: - 对于每个位置 $i$,$nums[i]$ 与其原本应该在的位置 $i$ 的差距不能超过 $1$。 - 即:$|nums[i] - i| \leq 1$。 **解题步骤**: 1. 遍历数组,检查每个元素是否满足 $|nums[i] - i| \leq 1$。 2. 如果所有元素都满足,返回 `True`。 3. 否则返回 `False`。 ### 思路 1:代码 ```python class Solution: def isIdealPermutation(self, nums: List[int]) -> bool: # 检查每个元素是否与其索引的差距不超过 1 for i in range(len(nums)): if abs(nums[i] - i) > 1: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $nums$ 的长度。需要遍历数组一次。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0700-0799/index.md ================================================ ## 本章内容 - [0700. 二叉搜索树中的搜索](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/search-in-a-binary-search-tree.md) - [0701. 二叉搜索树中的插入操作](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/insert-into-a-binary-search-tree.md) - [0702. 搜索长度未知的有序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/search-in-a-sorted-array-of-unknown-size.md) - [0703. 数据流中的第 K 大元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/kth-largest-element-in-a-stream.md) - [0704. 二分查找](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/binary-search.md) - [0705. 设计哈希集合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/design-hashset.md) - [0706. 设计哈希映射](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/design-hashmap.md) - [0707. 设计链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/design-linked-list.md) - [0708. 循环有序列表的插入](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/insert-into-a-sorted-circular-linked-list.md) - [0709. 转换成小写字母](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/to-lower-case.md) - [0710. 黑名单中的随机数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/random-pick-with-blacklist.md) - [0712. 两个字符串的最小ASCII删除和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/minimum-ascii-delete-sum-for-two-strings.md) - [0713. 乘积小于 K 的子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/subarray-product-less-than-k.md) - [0714. 买卖股票的最佳时机含手续费](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/best-time-to-buy-and-sell-stock-with-transaction-fee.md) - [0715. Range 模块](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/range-module.md) - [0717. 1 比特与 2 比特字符](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/1-bit-and-2-bit-characters.md) - [0718. 最长重复子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/maximum-length-of-repeated-subarray.md) - [0719. 找出第 K 小的数对距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/find-k-th-smallest-pair-distance.md) - [0720. 词典中最长的单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/longest-word-in-dictionary.md) - [0721. 账户合并](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/accounts-merge.md) - [0722. 删除注释](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/remove-comments.md) - [0724. 寻找数组的中心下标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/find-pivot-index.md) - [0725. 分隔链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/split-linked-list-in-parts.md) - [0726. 原子的数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/number-of-atoms.md) - [0727. 最小窗口子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/minimum-window-subsequence.md) - [0728. 自除数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/self-dividing-numbers.md) - [0729. 我的日程安排表 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/my-calendar-i.md) - [0730. 统计不同回文子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/count-different-palindromic-subsequences.md) - [0731. 我的日程安排表 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/my-calendar-ii.md) - [0732. 我的日程安排表 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/my-calendar-iii.md) - [0733. 图像渲染](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/flood-fill.md) - [0735. 小行星碰撞](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/asteroid-collision.md) - [0736. Lisp 语法解析](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/parse-lisp-expression.md) - [0738. 单调递增的数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/monotone-increasing-digits.md) - [0739. 每日温度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/daily-temperatures.md) - [0740. 删除并获得点数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/delete-and-earn.md) - [0741. 摘樱桃](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/cherry-pickup.md) - [0743. 网络延迟时间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/network-delay-time.md) - [0744. 寻找比目标字母大的最小字母](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/find-smallest-letter-greater-than-target.md) - [0745. 前缀和后缀搜索](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/prefix-and-suffix-search.md) - [0746. 使用最小花费爬楼梯](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/min-cost-climbing-stairs.md) - [0747. 至少是其他数字两倍的最大数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/largest-number-at-least-twice-of-others.md) - [0748. 最短补全词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/shortest-completing-word.md) - [0749. 隔离病毒](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/contain-virus.md) - [0752. 打开转盘锁](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/open-the-lock.md) - [0753. 破解保险箱](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/cracking-the-safe.md) - [0754. 到达终点数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/reach-a-number.md) - [0756. 金字塔转换矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/pyramid-transition-matrix.md) - [0757. 设置交集大小至少为2](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/set-intersection-size-at-least-two.md) - [0758. 字符串中的加粗单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/bold-words-in-string.md) - [0762. 二进制表示中质数个计算置位](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/prime-number-of-set-bits-in-binary-representation.md) - [0763. 划分字母区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/partition-labels.md) - [0764. 最大加号标志](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/largest-plus-sign.md) - [0765. 情侣牵手](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/couples-holding-hands.md) - [0766. 托普利茨矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/toeplitz-matrix.md) - [0767. 重构字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/reorganize-string.md) - [0768. 最多能完成排序的块 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/max-chunks-to-make-sorted-ii.md) - [0769. 最多能完成排序的块](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/max-chunks-to-make-sorted.md) - [0770. 基本计算器 IV](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/basic-calculator-iv.md) - [0771. 宝石与石头](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/jewels-and-stones.md) - [0773. 滑动谜题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/sliding-puzzle.md) - [0775. 全局倒置与局部倒置](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/global-and-local-inversions.md) - [0777. 在 LR 字符串中交换相邻字符](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/swap-adjacent-in-lr-string.md) - [0778. 水位上升的泳池中游泳](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/swim-in-rising-water.md) - [0779. 第K个语法符号](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/k-th-symbol-in-grammar.md) - [0780. 到达终点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/reaching-points.md) - [0781. 森林中的兔子](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rabbits-in-forest.md) - [0782. 变为棋盘](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/transform-to-chessboard.md) - [0783. 二叉搜索树节点最小距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/minimum-distance-between-bst-nodes.md) - [0784. 字母大小写全排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/letter-case-permutation.md) - [0785. 判断二分图](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/is-graph-bipartite.md) - [0786. 第 K 个最小的质数分数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/k-th-smallest-prime-fraction.md) - [0787. K 站中转内最便宜的航班](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/cheapest-flights-within-k-stops.md) - [0788. 旋转数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotated-digits.md) - [0789. 逃脱阻碍者](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/escape-the-ghosts.md) - [0790. 多米诺和托米诺平铺](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/domino-and-tromino-tiling.md) - [0791. 自定义字符串排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/custom-sort-string.md) - [0792. 匹配子序列的单词数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/number-of-matching-subsequences.md) - [0793. 阶乘函数后 K 个零](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/preimage-size-of-factorial-zeroes-function.md) - [0794. 有效的井字游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/valid-tic-tac-toe-state.md) - [0795. 区间子数组个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/number-of-subarrays-with-bounded-maximum.md) - [0796. 旋转字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/rotate-string.md) - [0797. 所有可能的路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/all-paths-from-source-to-target.md) - [0798. 得分最高的最小轮调](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/smallest-rotation-with-highest-score.md) - [0799. 香槟塔](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/champagne-tower.md) ================================================ FILE: docs/solutions/0700-0799/insert-into-a-binary-search-tree.md ================================================ # [0701. 二叉搜索树中的插入操作](https://leetcode.cn/problems/insert-into-a-binary-search-tree/) - 标签:树、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [0701. 二叉搜索树中的插入操作 - 力扣](https://leetcode.cn/problems/insert-into-a-binary-search-tree/) ## 题目大意 **描述**:给定一个二叉搜索树的根节点和要插入树中的值 `val`。 **要求**:将 `val` 插入到二叉搜索树中,返回新的二叉搜索树的根节点。 **说明**: - 树中的节点数将在 $[0, 10^4]$ 的范围内。 - $-10^8 \le Node.val \le 10^8$ - 所有值 `Node.val` 是独一无二的。 - $-10^8 \le val \le 10^8$。 - **保证** $val$ 在原始 BST 中不存在。 **示例**: - 示例 1: ```python 输入:root = [4,2,7,1,3], val = 5 输出:[4,2,7,1,3,5] 解释:另一个满足题目要求可以通过的树是: ``` - 示例 2: ```python 输入:root = [40,20,60,10,30,50,70], val = 25 输出:[40,20,60,10,30,50,70,null,null,25] ``` ## 解题思路 ### 思路 1:递归 已知搜索二叉树的性质: - 左子树上任意节点值均小于根节点,即 `root.left.val < root.val`。 - 右子树上任意节点值均大于根节点,即 `root.left.val > root.val`。 那么根据 `val` 和当前节点的大小关系,则可以确定将 `val` 插入到当前节点的哪个子树上。具体步骤如下: 1. 从根节点 `root` 开始向下递归遍历。根据 `val` 值和当前子树节点 `cur` 的大小关系: 1. 如果 `val < cur.val`,则应在当前节点的左子树继续遍历判断。 1. 如果左子树为空,则新建节点,赋值为 `val`。链接到该子树的父节点上。并停止遍历。 2. 如果左子树不为空,则继续向左子树移动。 2. 如果 `val >= cur.val`,则应在当前节点的右子树继续遍历判断。 1. 如果右子树为空,则新建节点,赋值为 `val`。链接到该子树的父节点上。并停止遍历。 2. 如果右子树不为空,则继续向左子树移动。 2. 遍历完返回根节点 `root`。 ### 思路 1:代码 ```python class Solution: def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode: if not root: return TreeNode(val) cur = root while cur: if val < cur.val: if not cur.left: cur.left = TreeNode(val) break else: cur = cur.left else: if not cur.right: cur.right = TreeNode(val) break else: cur = cur.right return root ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉搜索树的节点数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0700-0799/insert-into-a-sorted-circular-linked-list.md ================================================ # [0708. 循环有序列表的插入](https://leetcode.cn/problems/insert-into-a-sorted-circular-linked-list/) - 标签:链表 - 难度:中等 ## 题目链接 - [0708. 循环有序列表的插入 - 力扣](https://leetcode.cn/problems/insert-into-a-sorted-circular-linked-list/) ## 题目大意 给定循环升序链表中的一个节点 `head` 和一个整数 `insertVal`。 要求:将整数 `insertVal` 插入循环升序链表中,并且满足链表仍为循环升序链表。最终返回原先给定的节点。 ## 解题思路 - 先判断所给节点 `head` 是否为空,为空直接创建一个值为 `insertVal` 的新节点,并指向自己,返回即可。 - 如果 `head` 不为空,把 `head` 赋值给 `node` ,方便最后返回原节点 `head`。 - 然后遍历 `node`,判断插入值 `insertVal` 与 `node.val` 和 `node.next.val` 的关系,找到插入位置,具体判断如下: - 如果新节点值在两个节点值中间, 即 `node.val <= insertVal <= node.next.val`。则说明新节点值在最大值最小值中间,应将新节点插入到当前位置,则应将 `insertVal` 插入到这个位置。 - 如果新节点值比当前节点值和当前节点下一节点值都大,并且当前节点值比当前节点值的下一节点值大,即 `node.next.val < node.val <= insertVal`,则说明 `insertVal` 比链表最大值都大,应插入最大值后边。 - 如果新节点值比当前节点值和当前节点下一节点值都小,并且当前节点值比当前节点值的下一节点值大,即 `insertVal < node.next.val < node.val`,则说明 `insertVal` 比链表中最小值都小,应插入最小值前边。 - 找到插入位置后,跳出循环,在插入位置插入值为 `insertVal` 的新节点。 ## 代码 ```python class Solution: def insert(self, head: 'Node', insertVal: int) -> 'Node': if not head: node = Node(insertVal) node.next = node return node node = head while node.next != head: if node.val <= insertVal <= node.next.val: break elif node.next.val < node.val <= insertVal: break elif insertVal < node.next.val < node.val: break else: node = node.next insert_node = Node(insertVal) insert_node.next = node.next node.next = insert_node return head ``` ================================================ FILE: docs/solutions/0700-0799/is-graph-bipartite.md ================================================ # [0785. 判断二分图](https://leetcode.cn/problems/is-graph-bipartite/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [0785. 判断二分图 - 力扣](https://leetcode.cn/problems/is-graph-bipartite/) ## 题目大意 给定一个代表 n 个节点的无向图的二维数组 `graph`,其中 `graph[u]` 是一个节点数组,由节点 `u` 的邻接节点组成。对于 `graph[u]` 中的每个 `v`,都存在一条位于节点 `u` 和节点 `v` 之间的无向边。 该无向图具有以下属性: - 不存在自环(`graph[u]` 不包含 `u`)。 - 不存在平行边(`graph[u]` 不包含重复值)。 - 如果 `v` 在 `graph[u]` 内,那么 `u` 也应该在 `graph[v]` 内(该图是无向图)。 - 这个图可能不是连通图,也就是说两个节点 `u` 和 `v` 之间可能不存在一条连通彼此的路径。 要求:判断该图是否是二分图,如果是二分图,则返回 `True`;否则返回 `False`。 - 二分图:如果能将一个图的节点集合分割成两个独立的子集 `A` 和 `B`,并使图中的每一条边的两个节点一个来自 `A` 集合,一个来自 `B` 集合,就将这个图称为 二分图 。 ## 解题思路 对于图中的任意节点 `u` 和 `v`,如果 `u` 和 `v` 之间有一条无向边,那么 `u` 和 `v` 必然属于不同的集合。 我们可以通过在深度优先搜索中对邻接点染色标记的方式,来识别该图是否是二分图。具体做法如下: - 找到一个没有染色的节点 `u`,将其染成红色。 - 然后遍历该节点直接相连的节点 `v`,如果该节点没有被染色,则将该节点直接相连的节点染成蓝色,表示两个节点不是同一集合。如果该节点已经被染色并且颜色跟 `u` 一样,则说明该图不是二分图,直接返回 `False`。 - 从上面染成蓝色的节点 `v` 出发,遍历该节点直接相连的节点。。。依次类推的递归下去。 - 如果所有节点都顺利染上色,则说明该图为二分图,返回 `True`。否则,如果在途中不能顺利染色,则返回 `False`。 ## 代码 ```python class Solution: def dfs(self, graph, colors, i, color): colors[i] = color for j in graph[i]: if colors[j] == colors[i]: return False if colors[j] == 0 and not self.dfs(graph, colors, j, -color): return False return True def isBipartite(self, graph: List[List[int]]) -> bool: size = len(graph) colors = [0 for _ in range(size)] for i in range(size): if colors[i] == 0 and not self.dfs(graph, colors, i, 1): return False return True ``` ================================================ FILE: docs/solutions/0700-0799/jewels-and-stones.md ================================================ # [0771. 宝石与石头](https://leetcode.cn/problems/jewels-and-stones/) - 标签:哈希表、字符串 - 难度:简单 ## 题目链接 - [0771. 宝石与石头 - 力扣](https://leetcode.cn/problems/jewels-and-stones/) ## 题目大意 **描述**:给定一个字符串 $jewels$ 代表石头中宝石的类型,再给定一个字符串 $stones$ 代表你拥有的石头。$stones$ 中每个字符代表了一种你拥有的石头的类型。 **要求**:计算出拥有的石头中有多少是宝石。 **说明**: - 字母区分大小写,因此 $a$ 和 $A$ 是不同类型的石头。 - $1 \le jewels.length, stones.length \le 50$。 - $jewels$ 和 $stones$ 仅由英文字母组成。 - $jewels$ 中的所有字符都是唯一的。 **示例**: - 示例 1: ```python 输入:jewels = "aA", stones = "aAAbbbb" 输出:3 ``` - 示例 2: ```python 输入:jewels = "z", stones = "ZZ" 输出:0 ``` ## 解题思路 ### 思路 1:哈希表 1. 用 $count$ 来维护石头中的宝石个数。 2. 先使用哈希表或者集合存储宝石。 3. 再遍历数组 $stones$,并统计每块石头是否在哈希表中或集合中。 1. 如果当前石头在哈希表或集合中,则令 $count$ 加 $1$。 2. 如果当前石头不在哈希表或集合中,则不统计。 4. 最后返回 $count$。 ### 思路 1:代码 ```python class Solution: def numJewelsInStones(self, jewels: str, stones: str) -> int: jewel_dict = dict() for jewel in jewels: jewel_dict[jewel] = 1 count = 0 for stone in stones: if stone in jewel_dict: count += 1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n)$,其中 $m$ 是字符串 $jewels$ 的长度,$n$ 是 $stones$ 的长度。 - **空间复杂度**:$O(m)$,其中 $m$ 是字符串 $jewels$ 的长度。 ================================================ FILE: docs/solutions/0700-0799/k-th-smallest-prime-fraction.md ================================================ # [0786. 第 K 个最小的质数分数](https://leetcode.cn/problems/k-th-smallest-prime-fraction/) - 标签:数组、双指针、二分查找、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [0786. 第 K 个最小的质数分数 - 力扣](https://leetcode.cn/problems/k-th-smallest-prime-fraction/) ## 题目大意 **描述**: 给定一个按递增顺序排序的数组 $arr$ 和一个整数 $k$。数组 $arr$ 由 $1$ 和若干质数组成,且其中所有整数互不相同。 对于每对满足 $0 \le i < j < arr.length$ 的 $i$ 和 $j$,可以得到分数 $arr[i] / arr[j]$。 **要求**: 计算第 $k$ 个最小的分数。以长度为 $2$ 的整数数组返回你的答案, 这里 $answer[0] == arr[i]$ 且 $answer[1] == arr[j]$。 **说明**: - $2 \le arr.length \le 10^{3}$。 - $1 \le arr[i] \le 3 * 10^{4}$。 - $arr[0] == 1$。 - $arr[i]$ 是一个「质数」,$i \gt 0$。 - arr 中的所有数字「互不相同」,且按「严格递增」排序。 - $1 \le k \le arr.length \times (arr.length - 1) / 2$。 - 进阶:你可以设计并实现时间复杂度小于 $O(n^2)$ 的算法解决此问题吗? **示例**: - 示例 1: ```python 输入:arr = [1,2,3,5], k = 3 输出:[2,5] 解释:已构造好的分数,排序后如下所示: 1/5, 1/3, 2/5, 1/2, 3/5, 2/3 很明显第三个最小的分数是 2/5 ``` - 示例 2: ```python 输入:arr = [1,7], k = 1 输出:[1,7] ``` ## 解题思路 ### 思路 1:优先队列(最小堆) 这道题要求找到第 $k$ 个最小的质数分数。可以使用优先队列来解决。 **解题步骤**: 1. 将所有可能的分数 $\frac{arr[i]}{arr[j]}$($i < j$)加入优先队列。 2. 由于数组是递增的,对于每个分母 $arr[j]$,最小的分数是 $\frac{arr[0]}{arr[j]}$。 3. 使用最小堆,初始时将所有 $\frac{arr[0]}{arr[j]}$($j > 0$)加入堆中。 4. 每次从堆中取出最小的分数,如果这是第 $k$ 个,返回结果。 5. 如果取出的分数是 $\frac{arr[i]}{arr[j]}$ 且 $i + 1 < j$,将 $\frac{arr[i+1]}{arr[j]}$ 加入堆中。 **优化**:使用索引而不是实际的分数值,避免浮点数比较的精度问题。 ### 思路 1:代码 ```python class Solution: def kthSmallestPrimeFraction(self, arr: List[int], k: int) -> List[int]: import heapq n = len(arr) # 最小堆,存储 (分数值, 分子索引, 分母索引) heap = [] # 初始化:将所有 arr[0]/arr[j] 加入堆 for j in range(1, n): heapq.heappush(heap, (arr[0] / arr[j], 0, j)) # 取出前 k-1 个最小的分数 for _ in range(k - 1): _, i, j = heapq.heappop(heap) # 如果还有更大的分子,加入堆中 if i + 1 < j: heapq.heappush(heap, (arr[i + 1] / arr[j], i + 1, j)) # 第 k 个最小的分数 _, i, j = heapq.heappop(heap) return [arr[i], arr[j]] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k \log n)$,其中 $n$ 是数组 $arr$ 的长度。初始化堆需要 $O(n \log n)$,取出 $k$ 个元素需要 $O(k \log n)$。 - **空间复杂度**:$O(n)$。堆中最多存储 $n$ 个元素。 ================================================ FILE: docs/solutions/0700-0799/k-th-symbol-in-grammar.md ================================================ # [0779. 第K个语法符号](https://leetcode.cn/problems/k-th-symbol-in-grammar/) - 标签:位运算、递归、数学 - 难度:中等 ## 题目链接 - [0779. 第K个语法符号 - 力扣](https://leetcode.cn/problems/k-th-symbol-in-grammar/) ## 题目大意 **描述**:给定两个整数 $n$ 和 $k$​。我们可以按照下面的规则来生成字符串: - 第一行写上一个 $0$。 - 从第二行开始,每一行将上一行的 $0$ 替换成 $01$,$1$ 替换为 $10$。 **要求**:输出第 $n$ 行字符串中的第 $k$ 个字符。 **说明**: - $1 \le n \le 30$。 - $1 \le k \le 2^{n - 1}$。 **示例**: - 示例 1: ```python 输入: n = 2, k = 1 输出: 0 解释: 第一行: 0 第二行: 01 ``` - 示例 2: ```python 输入: n = 4, k = 4 输出: 0 解释: 第一行:0 第二行:01 第三行:0110 第四行:01101001 ``` ## 解题思路 ### 思路 1:递归算法 + 找规律 每一行都是由上一行生成的。我们可以将多行写到一起找下规律。 可以发现:第 $k$ 个数字是由上一位对应位置上的数字生成的。 - $k$ 在奇数位时,由上一行 $(k + 1) / 2$ 位置的值生成。且与上一行 $(k + 1) / 2$ 位置的值相同; - $k$ 在偶数位时,由上一行 $k / 2$ 位置的值生成。且与上一行 $k / 2$ 位置的值相反。 接下来就是递归求解即可。 ### 思路 1:代码 ```python class Solution: def kthGrammar(self, n: int, k: int) -> int: if n == 0: return 0 if k % 2 == 1: return self.kthGrammar(n - 1, (k + 1) // 2) else: return abs(self.kthGrammar(n - 1, k // 2) - 1) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0700-0799/kth-largest-element-in-a-stream.md ================================================ # [0703. 数据流中的第 K 大元素](https://leetcode.cn/problems/kth-largest-element-in-a-stream/) - 标签:树、设计、二叉搜索树、二叉树、数据流、堆(优先队列) - 难度:简单 ## 题目链接 - [0703. 数据流中的第 K 大元素 - 力扣](https://leetcode.cn/problems/kth-largest-element-in-a-stream/) ## 题目大意 **要求**:设计一个 KthLargest 类,用于找到数据流中第 $k$ 大元素。 实现 KthLargest 类: - `KthLargest(int k, int[] nums)`:使用整数 $k$ 和整数流 $nums$ 初始化对象。 - `int add(int val)`:将 $val$ 插入数据流 $nums$ 后,返回当前数据流中第 $k$ 大的元素。 **说明**: - $1 \le k \le 10^4$。 - $0 \le nums.length \le 10^4$。 - $-10^4 \le nums[i] \le 10^4$。 - $-10^4 \le val \le 10^4$。 - 最多调用 `add` 方法 $10^4$ 次。 - 题目数据保证,在查找第 $k$ 大元素时,数组中至少有 $k$ 个元素。 **示例**: - 示例 1: ```python 输入: ["KthLargest", "add", "add", "add", "add", "add"] [[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]] 输出: [null, 4, 5, 5, 8, 8] 解释: KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]); kthLargest.add(3); // return 4 kthLargest.add(5); // return 5 kthLargest.add(10); // return 5 kthLargest.add(9); // return 8 kthLargest.add(4); // return 8 ``` ## 解题思路 ### 思路 1:堆 1. 建立大小为 $k$ 的大顶堆,堆中元素保证不超过 $k$ 个。 2. 每次 `add` 操作时,将新元素压入堆中,如果堆中元素超出了 $k$ 个,则将堆中最小元素(堆顶)移除。 - 此时堆中最小元素(堆顶)就是整个数据流中的第 $k$ 大元素。 ### 思路 1:代码 ```python import heapq class KthLargest: def __init__(self, k: int, nums: List[int]): self.min_heap = [] self.k = k for num in nums: heapq.heappush(self.min_heap, num) if len(self.min_heap) > k: heapq.heappop(self.min_heap) def add(self, val: int) -> int: heapq.heappush(self.min_heap, val) if len(self.min_heap) > self.k: heapq.heappop(self.min_heap) return self.min_heap[0] ``` ### 思路 1:复杂度分析 - **时间复杂度**: - 初始化时间复杂度:$O(n \times \log k)$,其中 $n$ 为 $nums$ 初始化时的元素个数。 - 单次插入时间复杂度:$O(\log k)$。 - **空间复杂度**:$O(k)$。 ================================================ FILE: docs/solutions/0700-0799/largest-number-at-least-twice-of-others.md ================================================ # [0747. 至少是其他数字两倍的最大数](https://leetcode.cn/problems/largest-number-at-least-twice-of-others/) - 标签:数组、排序 - 难度:简单 ## 题目链接 - [0747. 至少是其他数字两倍的最大数 - 力扣](https://leetcode.cn/problems/largest-number-at-least-twice-of-others/) ## 题目大意 **描述**: 给定一个整数数组 $nums$,其中总是存在「唯一的」一个最大整数。 **要求**: 找出数组中的最大元素并检查它是否 至少是数组中每个其他数字的两倍。如果是,则返回「最大元素的下标」,否则返回 $-1$。 **说明**: - $2 \le nums.length \le 50$。 - $0 \le nums[i] \le 10^{3}$。 - $nums$ 中的最大元素是唯一的。 **示例**: - 示例 1: ```python 输入:nums = [3,6,1,0] 输出:1 解释:6 是最大的整数,对于数组中的其他整数,6 至少是数组中其他元素的两倍。6 的下标是 1 ,所以返回 1 。 ``` - 示例 2: ```python 输入:nums = [1,2,3,4] 输出:-1 解释:4 没有超过 3 的两倍大,所以返回 -1 。 ``` ## 解题思路 ### 思路 1:一次遍历 这道题要求找到数组中的最大元素,并检查它是否至少是其他所有元素的两倍。 **解题步骤**: 1. 遍历数组,找到最大元素及其索引,同时记录第二大元素。 2. 检查最大元素是否至少是第二大元素的两倍。 3. 如果是,返回最大元素的索引;否则返回 $-1$。 **优化**:只需要一次遍历即可完成。 ### 思路 1:代码 ```python class Solution: def dominantIndex(self, nums: List[int]) -> int: if len(nums) == 1: return 0 max_val = -1 max_idx = -1 second_max = -1 # 找到最大值和第二大值 for i, num in enumerate(nums): if num > max_val: second_max = max_val max_val = num max_idx = i elif num > second_max: second_max = num # 检查最大值是否至少是第二大值的两倍 if max_val >= second_max * 2: return max_idx else: return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $nums$ 的长度。需要遍历数组一次。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0700-0799/largest-plus-sign.md ================================================ # [0764. 最大加号标志](https://leetcode.cn/problems/largest-plus-sign/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [0764. 最大加号标志 - 力扣](https://leetcode.cn/problems/largest-plus-sign/) ## 题目大意 **描述**: 在一个 $n \times n$ 的矩阵 $grid$ 中,除了在数组 $mines$ 中给出的元素为 $0$,其他每个元素都为 $1$。$mines[i] = [xi, yi]$ 表示 $grid[xi][yi] == 0$。 **要求**: 返回 $grid$ 中包含 $1$ 的最大的「轴对齐」加号标志的阶数。如果未找到加号标志,则返回 $0$。 **说明**: - 一个 $k$ 阶由 $1$ 组成的「轴对称加号标志」具有中心网格 $grid[r][c] == 1$,以及 $4$ 个从中心向上、向下、向左、向右延伸,长度为 $k - 1$,由 $1$ 组成的臂。注意,只有加号标志的所有网格要求为 $1$,别的网格可能为 $0$ 也可能为 $1$。 - $1 \le n \le 500$。 - $1 \le mines.length \le 5000$。 - $0 \le xi, yi \lt n$。 - 每一对 $(xi, yi)$ 都「不重复」。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/06/13/plus1-grid.jpg) ```python 输入: n = 5, mines = [[4, 2]] 输出: 2 解释: 在上面的网格中,最大加号标志的阶只能是2。一个标志已在图中标出。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/06/13/plus2-grid.jpg) ```python 输入: n = 1, mines = [[0, 0]] 输出: 0 解释: 没有加号标志,返回 0 。 ``` ## 解题思路 ### 思路 1:动态规划 这道题要求计算每个位置能形成的最大加号标志的阶数。 **解题步骤**: 1. 首先将所有位置初始化为 $1$,将 $mines$ 中的位置标记为 $0$。 2. 对于每个位置 $(i, j)$,计算其四个方向(上、下、左、右)连续 $1$ 的个数。 3. 定义 $dp[i][j]$ 表示位置 $(i, j)$ 能形成的最大加号标志的阶数。 4. $dp[i][j] = \min(\text{上}, \text{下}, \text{左}, \text{右})$,即四个方向中最小的连续 $1$ 的个数。 5. 返回所有 $dp[i][j]$ 中的最大值。 **优化**:可以在一次遍历中同时计算四个方向的连续 $1$ 的个数。 ### 思路 1:代码 ```python class Solution: def orderOfLargestPlusSign(self, n: int, mines: List[List[int]]) -> int: # 初始化所有位置为 n(表示最多可以延伸 n 个单位) dp = [[n] * n for _ in range(n)] # 将 mines 中的位置标记为 0 banned = set(map(tuple, mines)) for x, y in banned: dp[x][y] = 0 # 计算每个位置四个方向的最小连续 1 的个数 for i in range(n): # 从左到右 left = 0 # 从右到左 right = 0 # 从上到下 up = 0 # 从下到上 down = 0 for j in range(n): # 从左到右 left = 0 if (i, j) in banned else left + 1 dp[i][j] = min(dp[i][j], left) # 从右到左 right = 0 if (i, n - 1 - j) in banned else right + 1 dp[i][n - 1 - j] = min(dp[i][n - 1 - j], right) # 从上到下 up = 0 if (j, i) in banned else up + 1 dp[j][i] = min(dp[j][i], up) # 从下到上 down = 0 if (n - 1 - j, i) in banned else down + 1 dp[n - 1 - j][i] = min(dp[n - 1 - j][i], down) # 返回最大值 return max(max(row) for row in dp) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是矩阵的边长。需要遍历矩阵四次。 - **空间复杂度**:$O(n^2)$。需要存储 $dp$ 数组和 $banned$ 集合。 ================================================ FILE: docs/solutions/0700-0799/letter-case-permutation.md ================================================ # [0784. 字母大小写全排列](https://leetcode.cn/problems/letter-case-permutation/) - 标签:位运算、字符串、回溯 - 难度:中等 ## 题目链接 - [0784. 字母大小写全排列 - 力扣](https://leetcode.cn/problems/letter-case-permutation/) ## 题目大意 **描述**:给定一个字符串 $s$,通过将字符串 $s$ 中的每个字母转变大小写,我们可以获得一个新的字符串。 **要求**:返回所有可能得到的字符串集合。 **说明**: - 答案可以以任意顺序返回输出。 - $1 \le s.length \le 12$。 - $s$ 由小写英文字母、大写英文字母和数字组成。 **示例**: - 示例 1: ```python 输入:s = "a1b2" 输出:["a1b2", "a1B2", "A1b2", "A1B2"] ``` - 示例 2: ```python 输入: s = "3z4" 输出: ["3z4","3Z4"] ``` ## 解题思路 ### 思路 1:回溯算法 - $i$ 代表当前要处理的字符在字符串 $s$ 中的下标,$path$ 表示当前路径,$ans$ 表示答案数组。 - 如果处理到 $i == len(s)$ 时,将当前路径存入答案数组中返回,否则进行递归处理。 - 不修改当前字符,直接递归处理第 $i + 1$ 个字符。 - 如果当前字符是小写字符,则变为大写字符之后,递归处理第 $i + 1$ 个字符。 - 如果当前字符是大写字符,则变为小写字符之后,递归处理第 $i + 1$ 个字符。 ### 思路 1:代码 ```python class Solution: def dfs(self, s, path, i, ans): if i == len(s): ans.append(path) return self.dfs(s, path + s[i], i + 1, ans) if ord('a') <= ord(s[i]) <= ord('z'): self.dfs(s, path + s[i].upper(), i + 1, ans) elif ord('A') <= ord(s[i]) <= ord('Z'): self.dfs(s, path + s[i].lower(), i + 1, ans) def letterCasePermutation(self, s: str) -> List[str]: ans, path = [], "" self.dfs(s, path, 0, ans) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$n \times 2^n$,其中 $n$ 为字符串的长度。 - **空间复杂度**:$O(1)$,除返回值外不需要额外的空间。 ================================================ FILE: docs/solutions/0700-0799/longest-word-in-dictionary.md ================================================ # [0720. 词典中最长的单词](https://leetcode.cn/problems/longest-word-in-dictionary/) - 标签:字典树、数组、哈希表、字符串、排序 - 难度:中等 ## 题目链接 - [0720. 词典中最长的单词 - 力扣](https://leetcode.cn/problems/longest-word-in-dictionary/) ## 题目大意 给出一个字符串数组 `words` 组成的一本英语词典。 要求:从中找出最长的一个单词,该单词是由 `words` 词典中其他单词逐步添加一个字母组成。如果其中有多个可行的答案,则返回答案中字典序最小的单词。如果无答案,则返回空字符串。 ## 解题思路 使用字典树存储每一个单词。再在字典树中查找每一个单词,查找的时候判断是否有以当前单词为前缀的单词。如果有,则该单词可以由前缀构成的单词逐步添加字母获得。此时,如果该单词比答案单词更长,则维护更新答案单词。 最后输出答案单词。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children or not cur.children[ch].isEnd: return False cur = cur.children[ch] return cur is not None and cur.isEnd class Solution: def longestWord(self, words: List[str]) -> str: trie_tree = Trie() for word in words: trie_tree.insert(word) ans = "" for word in words: if trie_tree.search(word): if len(word) > len(ans): ans = word elif len(word) == len(ans) and word < ans: ans = word return ans ``` ================================================ FILE: docs/solutions/0700-0799/max-chunks-to-make-sorted-ii.md ================================================ # [0768. 最多能完成排序的块 II](https://leetcode.cn/problems/max-chunks-to-make-sorted-ii/) - 标签:栈、贪心、数组、排序、单调栈 - 难度:困难 ## 题目链接 - [0768. 最多能完成排序的块 II - 力扣](https://leetcode.cn/problems/max-chunks-to-make-sorted-ii/) ## 题目大意 **描述**: 给定一个整数数组 $arr$。将 $arr$ 分割成若干块,并将这些块分别进行排序。之后再连接起来,使得连接的结果和按升序排序后的原数组相同。 **要求**: 返回能将数组分成的最多块数。 **说明**: - $1 \le arr.length \le 2000$。 - $0 \le arr[i] \le 10^{8}$。 **示例**: - 示例 1: ```python 输入:arr = [5,4,3,2,1] 输出:1 解释: 将数组分成2块或者更多块,都无法得到所需的结果。 例如,分成 [5, 4], [3, 2, 1] 的结果是 [4, 5, 1, 2, 3],这不是有序的数组。 ``` - 示例 2: ```python 输入:arr = [2,1,3,4,4] 输出:4 解释: 可以把它分成两块,例如 [2, 1], [3, 4, 4]。 然而,分成 [2, 1], [3], [4], [4] 可以得到最多的块数。 ``` ## 解题思路 ### 思路 1:单调栈 这道题要求将数组分成若干块,每块单独排序后连接起来,结果与整体排序相同。 **核心观察**: - 如果可以在位置 $i$ 处分割,那么 $arr[0:i+1]$ 中的最大值必须 $\leq$ $arr[i+1:]$ 中的最小值。 - 等价于:前 $i+1$ 个元素排序后,应该恰好是整体排序后的前 $i+1$ 个元素。 **解题步骤**: 1. 从左到右遍历数组,维护当前的最大值 $max\_left$。 2. 同时预处理从右到左的最小值数组 $min\_right$。 3. 如果在位置 $i$ 处,$max\_left \leq min\_right[i+1]$,说明可以在此处分割。 4. 统计所有可以分割的位置数量。 **优化**:可以使用单调栈来解决,但对于这道题,简单的贪心方法更直观。 ### 思路 1:代码 ```python class Solution: def maxChunksToSorted(self, arr: List[int]) -> int: n = len(arr) # 预处理从右到左的最小值 min_right = [0] * (n + 1) min_right[n] = float('inf') for i in range(n - 1, -1, -1): min_right[i] = min(arr[i], min_right[i + 1]) chunks = 0 max_left = 0 # 从左到右遍历 for i in range(n): max_left = max(max_left, arr[i]) # 如果当前最大值 <= 右侧最小值,可以分割 if max_left <= min_right[i + 1]: chunks += 1 return chunks ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $arr$ 的长度。需要遍历数组两次。 - **空间复杂度**:$O(n)$。需要存储 $min\_right$ 数组。 ================================================ FILE: docs/solutions/0700-0799/max-chunks-to-make-sorted.md ================================================ # [0769. 最多能完成排序的块](https://leetcode.cn/problems/max-chunks-to-make-sorted/) - 标签:栈、贪心、数组、排序、单调栈 - 难度:中等 ## 题目链接 - [0769. 最多能完成排序的块 - 力扣](https://leetcode.cn/problems/max-chunks-to-make-sorted/) ## 题目大意 **描述**: 给定一个长度为 $n$ 的整数数组 $arr$,它表示在 $[0, n - 1]$ 范围内的整数的排列。 我们将 $arr$ 分割成若干块 (即分区),并对每个块单独排序。将它们连接起来后,使得连接的结果和按升序排序后的原数组相同。 **要求**: 返回数组能分成的最多块数量。 **说明**: - $n == arr.length$。 - $1 \le n \le 10$。 - $0 \le arr[i] \lt n$。 - $arr$ 中每个元素都「不同」。 **示例**: - 示例 1: ```python 输入: arr = [4,3,2,1,0] 输出: 1 解释: 将数组分成2块或者更多块,都无法得到所需的结果。 例如,分成 [4, 3], [2, 1, 0] 的结果是 [3, 4, 0, 1, 2],这不是有序的数组。 ``` - 示例 2: ```python 输入: arr = [1,0,2,3,4] 输出: 4 解释: 我们可以把它分成两块,例如 [1, 0], [2, 3, 4]。 然而,分成 [1, 0], [2], [3], [4] 可以得到最多的块数。 对每个块单独排序后,结果为 [0, 1], [2], [3], [4] ``` ## 解题思路 ### 思路 1:贪心算法 这道题是 0768 题的简化版本,数组元素是 $[0, n-1]$ 的排列。 **核心观察**: - 由于数组是 $[0, n-1]$ 的排列,如果前 $i+1$ 个元素的最大值等于 $i$,说明前 $i+1$ 个元素恰好是 $[0, i]$。 - 此时可以在位置 $i$ 处分割,因为前面的元素排序后不会影响后面的元素。 **解题步骤**: 1. 从左到右遍历数组,维护当前的最大值 $max\_val$。 2. 如果在位置 $i$ 处,$max\_val = i$,说明前 $i+1$ 个元素恰好是 $[0, i]$,可以分割。 3. 统计所有可以分割的位置数量。 ### 思路 1:代码 ```python class Solution: def maxChunksToSorted(self, arr: List[int]) -> int: chunks = 0 max_val = 0 for i in range(len(arr)): max_val = max(max_val, arr[i]) # 如果当前最大值等于索引,说明前面的元素恰好是 [0, i] if max_val == i: chunks += 1 return chunks ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $arr$ 的长度。需要遍历数组一次。 - **空间复杂度**:$O(1)$。只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0700-0799/maximum-length-of-repeated-subarray.md ================================================ # [0718. 最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) - 标签:数组、二分查找、动态规划、滑动窗口、哈希函数、滚动哈希 - 难度:中等 ## 题目链接 - [0718. 最长重复子数组 - 力扣](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) ## 题目大意 **描述**:给定两个整数数组 $nums1$、$nums2$。 **要求**:计算两个数组中公共的、长度最长的子数组长度。 **说明**: - $1 \le nums1.length, nums2.length \le 1000$。 - $0 \le nums1[i], nums2[i] \le 100$。 **示例**: - 示例 1: ```python 输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7] 输出:3 解释:长度最长的公共子数组是 [3,2,1] 。 ``` - 示例 2: ```python 输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0] 输出:5 ``` ## 解题思路 ### 思路 1:暴力(超时) 1. 枚举数组 $nums1$ 和 $nums2$ 的子数组开始位置 $i$、$j$。 2. 如果遇到相同项,即 $nums1[i] == nums2[j]$,则以 $nums1[i]$、$nums2[j]$ 为前缀,同时向后遍历,计算当前的公共子数组长度 $subLen$ 最长为多少。 3. 直到遇到超出数组范围或者 $nums1[i + subLen] == nums2[j + subLen]$ 情况时,停止遍历,并更新答案。 4. 继续执行 $1 \sim 3$ 步,直到遍历完,输出答案。 ### 思路 1:代码 ```python class Solution: def findLength(self, nums1: List[int], nums2: List[int]) -> int: size1, size2 = len(nums1), len(nums2) ans = 0 for i in range(size1): for j in range(size2): if nums1[i] == nums2[j]: subLen = 1 while i + subLen < size1 and j + subLen < size2 and nums1[i + subLen] == nums2[j + subLen]: subLen += 1 ans = max(ans, subLen) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m \times min(n, m))$。其中 $n$ 是数组 $nums1$ 的长度,$m$ 是数组 $nums2$ 的长度。 - **空间复杂度**:$O(1)$。 ### 思路 2:滑动窗口 暴力方法中,因为子数组在两个数组中的位置不同,所以会导致子数组之间会进行多次比较。 我们可以将两个数组分别看做是两把直尺。然后将数组 $nums1$ 固定, 让 $nums2$ 的尾部与 $nums1$ 的头部对齐,如下所示。 ```python nums1 = [1, 2, 3, 2, 1] nums2 = [3, 2, 1, 4, 7] ``` 然后逐渐向右移动直尺 $nums2$,比较 $nums1$ 与 $nums2$ 重叠部分中的公共子数组的长度,直到直尺 $nums2$ 的头部移动到 $nums1$ 的尾部。 ```python nums1 = [1, 2, 3, 2, 1] nums2 = [3, 2, 1, 4, 7] nums1 = [1, 2, 3, 2, 1] nums2 = [3, 2, 1, 4, 7] nums1 = [1, 2, 3, 2, 1] nums2 = [3, 2, 1, 4, 7] nums1 = [1, 2, 3, 2, 1] nums2 = [3, 2, 1, 4, 7] nums1 = [1, 2, 3, 2, 1] nums2 = [3, 2, 1, 4, 7] nums1 = [1, 2, 3, 2, 1] nums2 = [3, 2, 1, 4, 7] nums1 = [1, 2, 3, 2, 1] nums2 = [3, 2, 1, 4, 7] nums1 = [1, 2, 3, 2, 1] nums2 = [3, 2, 1, 4, 7] ``` 在这个过程中求得的 $nums1$ 与 $nums2$ 重叠部分中的最大的公共子数组的长度就是 $nums1$ 与 $nums2$ 数组中公共的、长度最长的子数组长度。 ### 思路 2:代码 ```python class Solution: def findMaxLength(self, nums1, nums2, i, j): size1, size2 = len(nums1), len(nums2) max_len = 0 cur_len = 0 while i < size1 and j < size2: if nums1[i] == nums2[j]: cur_len += 1 max_len = max(max_len, cur_len) else: cur_len = 0 i += 1 j += 1 return max_len def findLength(self, nums1: List[int], nums2: List[int]) -> int: size1, size2 = len(nums1), len(nums2) res = 0 for i in range(size1): res = max(res, self.findMaxLength(nums1, nums2, i, 0)) for i in range(size2): res = max(res, self.findMaxLength(nums1, nums2, 0, i)) return res ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n + m) \times min(n, m)$。其中 $n$ 是数组 $nums1$ 的长度,$m$ 是数组 $nums2$ 的长度。 - **空间复杂度**:$O(1)$。 ### 思路 3:动态规划 ###### 1. 阶段划分 按照子数组结尾位置进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 为:「以 $nums1$ 中前 $i$ 个元素为子数组($nums1[0]...nums2[i - 1]$)」和「以 $nums2$ 中前 $j$ 个元素为子数组($nums2[0]...nums2[j - 1]$)」的最长公共子数组长度。 ###### 3. 状态转移方程 1. 如果 $nums1[i - 1] = nums2[j - 1]$,则当前元素可以构成公共子数组,此时 $dp[i][j] = dp[i - 1][j - 1] + 1$。 2. 如果 $nums1[i - 1] \ne nums2[j - 1]$,则当前元素不能构成公共子数组,此时 $dp[i][j] = 0$。 ###### 4. 初始条件 - 当 $i = 0$ 时,$nums1[0]...nums1[i - 1]$ 表示的是空数组,空数组与 $nums2[0]...nums2[j - 1]$ 的最长公共子序列长度为 $0$,即 $dp[0][j] = 0$。 - 当 $j = 0$ 时,$nums2[0]...nums2[j - 1]$ 表示的是空数组,空数组与 $nums1[0]...nums1[i - 1]$ 的最长公共子序列长度为 $0$,即 $dp[i][0] = 0$。 ###### 5. 最终结果 - 根据状态定义, $dp[i][j]$ 为:「以 $nums1$ 中前 $i$ 个元素为子数组($nums1[0]...nums2[i - 1]$)」和「以 $nums2$ 中前 $j$ 个元素为子数组($nums2[0]...nums2[j - 1]$)」的最长公共子数组长度。在遍历过程中,我们可以使用 $res$ 记录下所有 $dp[i][j]$ 中最大值即为答案。 ### 思路 3:代码 ```python class Solution: def findLength(self, nums1: List[int], nums2: List[int]) -> int: size1 = len(nums1) size2 = len(nums2) dp = [[0 for _ in range(size2 + 1)] for _ in range(size1 + 1)] res = 0 for i in range(1, size1 + 1): for j in range(1, size2 + 1): if nums1[i - 1] == nums2[j - 1]: dp[i][j] = dp[i - 1][j - 1] + 1 if dp[i][j] > res: res = dp[i][j] return res ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n \times m)$。其中 $n$ 是数组 $nums1$ 的长度,$m$ 是数组 $nums2$ 的长度。 - **空间复杂度**:$O(n \times m)$。 ================================================ FILE: docs/solutions/0700-0799/min-cost-climbing-stairs.md ================================================ # [0746. 使用最小花费爬楼梯](https://leetcode.cn/problems/min-cost-climbing-stairs/) - 标签:数组、动态规划 - 难度:简单 ## 题目链接 - [0746. 使用最小花费爬楼梯 - 力扣](https://leetcode.cn/problems/min-cost-climbing-stairs/) ## 题目大意 给定一个数组 `cost` 代表一段楼梯,`cost[i]` 代表爬上第 `i` 阶楼梯醒酒药花费的体力值(下标从 `0` 开始)。 每爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。 要求:找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 `0` 或 `1` 的元素作为初始阶梯。 ## 解题思路 使用动态规划方法。 状态 `dp[i]` 表示为:到达第 `i` 个台阶所花费的最少体⼒。 则状态转移方程为: `dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]`。 表示为:到达第 `i` 个台阶所花费的最少体⼒ = 到达第 `i - 1` 个台阶所花费的最小体力 与 到达第 `i - 2` 个台阶所花费的最小体力中的最小值 + 到达第 `i` 个台阶所需要花费的体力值。 ## 代码 ```python class Solution: def minCostClimbingStairs(self, cost: List[int]) -> int: size = len(cost) dp = [0 for _ in range(size + 1)] for i in range(2, size+1): dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]) return dp[size] ``` ================================================ FILE: docs/solutions/0700-0799/minimum-ascii-delete-sum-for-two-strings.md ================================================ # [0712. 两个字符串的最小ASCII删除和](https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [0712. 两个字符串的最小ASCII删除和 - 力扣](https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings/) ## 题目大意 **描述**: 给定两个字符串 $s1$ 和 $s2$。 **要求**: 返回使两个字符串相等所需删除字符的 ASCII 值的最小。 **说明**: - $0 \le s1.length, s2.length \le 10^{3}$。 - $s1$ 和 $s2$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入: s1 = "sea", s2 = "eat" 输出: 231 解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。 在 "eat" 中删除 "t" 并将 116 加入总和。 结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。 ``` - 示例 2: ```python 输入: s1 = "delete", s2 = "leet" 输出: 403 解释: 在 "delete" 中删除 "dee" 字符串变成 "let", 将 100[d]+101[e]+101[e] 加入总和。在 "leet" 中删除 "e" 将 101[e] 加入总和。 结束时,两个字符串都等于 "let",结果即为 100+101+101+101 = 403 。 如果改为将两个字符串转换为 "lee" 或 "eet",我们会得到 433 或 417 的结果,比答案更大。 ``` ## 解题思路 ### 思路 1:动态规划 这道题类似于最长公共子序列(LCS)问题,但要求的是删除字符的 ASCII 值之和最小。 **状态定义**: - 定义 $dp[i][j]$ 表示使 $s1[0:i]$ 和 $s2[0:j]$ 相等所需删除字符的最小 ASCII 值之和。 **状态转移**: - 如果 $s1[i-1] = s2[j-1]$,不需要删除,$dp[i][j] = dp[i-1][j-1]$。 - 如果 $s1[i-1] \neq s2[j-1]$,有两种选择: - 删除 $s1[i-1]$:$dp[i][j] = dp[i-1][j] + \text{ord}(s1[i-1])$。 - 删除 $s2[j-1]$:$dp[i][j] = dp[i][j-1] + \text{ord}(s2[j-1])$。 - 取两者的最小值。 **初始化**: - $dp[0][0] = 0$。 - $dp[i][0] = \sum_{k=0}^{i-1} \text{ord}(s1[k])$,删除 $s1$ 的前 $i$ 个字符。 - $dp[0][j] = \sum_{k=0}^{j-1} \text{ord}(s2[k])$,删除 $s2$ 的前 $j$ 个字符。 ### 思路 1:代码 ```python class Solution: def minimumDeleteSum(self, s1: str, s2: str) -> int: m, n = len(s1), len(s2) # dp[i][j] 表示使 s1[0:i] 和 s2[0:j] 相等所需删除字符的最小 ASCII 值之和 dp = [[0] * (n + 1) for _ in range(m + 1)] # 初始化:删除 s1 的前 i 个字符 for i in range(1, m + 1): dp[i][0] = dp[i - 1][0] + ord(s1[i - 1]) # 初始化:删除 s2 的前 j 个字符 for j in range(1, n + 1): dp[0][j] = dp[0][j - 1] + ord(s2[j - 1]) # 状态转移 for i in range(1, m + 1): for j in range(1, n + 1): if s1[i - 1] == s2[j - 1]: # 字符相同,不需要删除 dp[i][j] = dp[i - 1][j - 1] else: # 字符不同,选择删除 s1[i-1] 或 s2[j-1] dp[i][j] = min( dp[i - 1][j] + ord(s1[i - 1]), # 删除 s1[i-1] dp[i][j - 1] + ord(s2[j - 1]) # 删除 s2[j-1] ) return dp[m][n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 和 $n$ 分别是字符串 $s1$ 和 $s2$ 的长度。 - **空间复杂度**:$O(m \times n)$。可以优化到 $O(\min(m, n))$。 ================================================ FILE: docs/solutions/0700-0799/minimum-distance-between-bst-nodes.md ================================================ # [0783. 二叉搜索树节点最小距离](https://leetcode.cn/problems/minimum-distance-between-bst-nodes/) - 标签:树、深度优先搜索、广度优先搜索、二叉搜索树、二叉树 - 难度:简单 ## 题目链接 - [0783. 二叉搜索树节点最小距离 - 力扣](https://leetcode.cn/problems/minimum-distance-between-bst-nodes/) ## 题目大意 **描述**:给定一个二叉搜索树的根节点 $root$。 **要求**:返回树中任意两不同节点值之间的最小差值。 **说明**: - **差值**:是一个正数,其数值等于两值之差的绝对值。 - 树中节点的数目范围是 $[2, 100]$。 - $0 \le Node.val \le 10^5$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/05/bst1.jpg) ```python 输入:root = [4,2,6,1,3] 输出:1 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/02/05/bst2.jpg) ```python 输入:root = [1,0,48,null,null,12,49] 输出:1 ``` ## 解题思路 ### 思路 1:中序遍历 先来看二叉搜索树的定义: - 如果左子树不为空,则左子树上所有节点值均小于它的根节点值; - 如果右子树不为空,则右子树上所有节点值均大于它的根节点值; - 任意节点的左、右子树也分别为二叉搜索树。 题目要求二叉搜索树上任意两节点的差的绝对值的最小值。 二叉树的中序遍历顺序是:左 -> 根 -> 右,二叉搜索树的中序遍历最终得到就是一个升序数组。而升序数组中绝对值差的最小值就是比较相邻两节点差值的绝对值,找出其中最小值。 那么我们就可以先对二叉搜索树进行中序遍历,并保存中序遍历的结果。然后再比较相邻节点差值的最小值,从而找出最小值。 ### 思路 1:代码 ```Python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def inorderTraversal(self, root: TreeNode) -> List[int]: res = [] def inorder(root): if not root: return inorder(root.left) res.append(root.val) inorder(root.right) inorder(root) return res def minDiffInBST(self, root: Optional[TreeNode]) -> int: inorder = self.inorderTraversal(root) ans = float('inf') for i in range(1, len(inorder)): ans = min(ans, abs(inorder[i - 1] - inorder[i])) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为二叉搜索树中的节点数量。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0700-0799/minimum-window-subsequence.md ================================================ # [0727. 最小窗口子序列](https://leetcode.cn/problems/minimum-window-subsequence/) - 标签:字符串、动态规划、滑动窗口 - 难度:困难 ## 题目链接 - [0727. 最小窗口子序列 - 力扣](https://leetcode.cn/problems/minimum-window-subsequence/) ## 题目大意 给定字符串 `s1` 和 `s2`。 要求:找出 `s1` 中最短的(连续)子串 `w`,使得 `s2` 是 `w` 的子序列 。如果 `s1` 中没有窗口可以包含 `s2` 中的所有字符,返回空字符串 `""`。如果有不止一个最短长度的窗口,返回开始位置最靠左的那个。 ## 解题思路 这道题跟「[76. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/)」有点类似。但这道题中字符的相对顺序需要保持一致。求解的思路如下: - 向右扩大窗口,匹配字符,直到匹配完 `s2` 的最后一个字符。 - 当满足条件时,缩小窗口,并更新最小窗口的起始位置和最短长度。 - 缩小窗口到不满足条件为止。 这道题的难点在于第二步中如何缩小窗口。当匹配到一个子序列时,可以采用逆向匹配的方式,从 `s2` 的最后一位字符匹配到 `s2` 的第一位字符。找到符合要求的最大下标,即是窗口的左边界。 整个算法的解题步骤如下: - 使用两个指针 `left`、`right` 代表窗口的边界,一开始都指向 `0` 。`min_len` 用来记录最小子序列的长度。`i`、`j` 作为索引,用于遍历字符串 `s1` 和 `s2`,一开始都为 `0`。 - 遍历字符串 `s1` 的每一个字符,如果 `s1[i] == s2[j]`,则说明 `s2` 中第 `j` 个字符匹配了,向右移动 `j`,即 `j += 1`,然后继续匹配。 - 如果 `j == len(s2)`,则说明 `s2` 中所有字符都匹配了。 - 此时确定了窗口的右边界 `right = i`,并令 `j` 指向 `s2` 最后一个字符位置。 - 从右至左逆向匹配字符串,找到窗口的左边界。 - 判断当前窗口长度和窗口的最短长度,并更新最小窗口的起始位置和最短长度。 - 令 `j = 0`,重新继续匹配 `s2`。 - 向右移动 `i`,继续匹配。 - 遍历完输出窗口的最短长度(需要判断是否有解)。 ## 代码 ```python class Solution: def minWindow(self, s1: str, s2: str) -> str: i, j = 0, 0 min_len = float('inf') left, right = 0, 0 while i < len(s1): if s1[i] == s2[j]: j += 1 # 完成了匹配 if j == len(s2): right = i j -= 1 while j >= 0: if s1[i] == s2[j]: j -= 1 i -= 1 i += 1 if right - i + 1 < min_len: left = i min_len = right - left + 1 j = 0 i += 1 if min_len != float('inf'): return s1[left: left + min_len] return "" ``` ## 参考资料 - 【题解】[c++ 简单好理解的 滑动窗口解法 和 动态规划解法 - 最小窗口子序列 - 力扣](https://leetcode.cn/problems/minimum-window-subsequence/solution/c-jian-dan-hao-li-jie-de-hua-dong-chuang-wguk/) - 【题解】[727. 最小窗口子序列 C++ 滑动窗口 - 最小窗口子序列 - 力扣](https://leetcode.cn/problems/minimum-window-subsequence/solution/727-zui-xiao-chuang-kou-zi-xu-lie-c-hua-dong-chuan/) ================================================ FILE: docs/solutions/0700-0799/monotone-increasing-digits.md ================================================ # [0738. 单调递增的数字](https://leetcode.cn/problems/monotone-increasing-digits/) - 标签:贪心、数学 - 难度:中等 ## 题目链接 - [0738. 单调递增的数字 - 力扣](https://leetcode.cn/problems/monotone-increasing-digits/) ## 题目大意 给定一个非负整数 n,找出小于等于 n 的最大整数,同时该整数需要满足其各个位数上的数字是单调递增的。 ## 解题思路 为了方便操作,我们先将整数 n 转为 list 数组,即 n_list。 题目要求这个整数尽可能的大,那么这个数从高位开始,就应该尽可能的保持不变。那么我们需要从高位到低位,找到第一个满足 `n_list[i - 1] > n_list[i]` 的位置,然后把 `n_list[i] - 1`,再把剩下的低位都变为 9。 ## 代码 ```python class Solution: def monotoneIncreasingDigits(self, n: int) -> int: n_list = list(str(n)) size = len(n_list) start_i = size for i in range(size - 1, 0, -1): if n_list[i - 1] > n_list[i]: start_i = i n_list[i - 1] = chr(ord(n_list[i - 1]) - 1) for i in range(start_i, size, 1): n_list[i] = '9' res = int(''.join(n_list)) return res ``` ================================================ FILE: docs/solutions/0700-0799/my-calendar-i.md ================================================ # [0729. 我的日程安排表 I](https://leetcode.cn/problems/my-calendar-i/) - 标签:设计、线段树、二分查找、有序集合 - 难度:中等 ## 题目链接 - [0729. 我的日程安排表 I - 力扣](https://leetcode.cn/problems/my-calendar-i/) ## 题目大意 **要求**:实现一个 `MyCalendar` 类来存放你的日程安排。如果要添加的日程安排不会造成重复预订 ,则可以存储这个新的日程安排。 日程可以用一对整数 $start$ 和 $end$ 表示,这里的时间是半开区间,即 $[start, end)$,实数 $x$ 的范围为 $start \le x < end$。 `MyCalendar` 类: - `MyCalendar()` 初始化日历对象。 - `boolean book(int start, int end)` 如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 `True` 。否则,返回 `False` 并且不要将该日程安排添加到日历中。 **说明**: - 重复预订:当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生重复预订 。 - $0 \le start < end \le 10^9$ - 每个测试用例,调用 `book` 方法的次数最多不超过 `1000` 次。 **示例**: - 示例 1: ```python 输入: ["MyCalendar", "book", "book", "book"] [[], [10, 20], [15, 25], [20, 30]] 输出: [null, true, false, true] 解释: MyCalendar myCalendar = new MyCalendar(); myCalendar.book(10, 20); // return True myCalendar.book(15, 25); // return False ,这个日程安排不能添加到日历中,因为时间 15 已经被另一个日程安排预订了。 myCalendar.book(20, 30); // return True ,这个日程安排可以添加到日历中,因为第一个日程安排预订的每个时间都小于 20 ,且不包含时间 20 。 ``` ## 解题思路 ### 思路 1:线段树 这道题可以使用线段树来做。 因为区间的范围是 $[0, 10^9]$,普通数组构成的线段树不满足要求。需要用到动态开点线段树。 - 构建一棵线段树。每个线段树的节点类存储当前区间中保存的日程区间个数。 - 在 `book` 方法中,从线段树中查询 `[start, end - 1]` 区间上保存的日程区间个数。 - 如果日程区间个数大于等于 `1`,则说明该日程添加到日历中会导致重复预订,则直接返回 `False`。 - 如果日程区间个数小于 `1`,则说明该日程添加到日历中不会导致重复预定,则在线段树中将区间 `[start, end - 1]` 的日程区间个数 + 1,然后返回 `True`。 ### 思路 1:线段树代码 ```python # 线段树的节点类 class SegTreeNode: def __init__(self, left=-1, right=-1, val=0, lazy_tag=None, leftNode=None, rightNode=None): self.left = left # 区间左边界 self.right = right # 区间右边界 self.mid = left + (right - left) // 2 self.leftNode = leftNode # 区间左节点 self.rightNode = rightNode # 区间右节点 self.val = val # 节点值(区间值) self.lazy_tag = lazy_tag # 区间问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, function): self.tree = SegTreeNode(0, int(1e9)) self.function = function # function 是一个函数,左右区间的聚合方法 # 单点更新,将 nums[i] 更改为 val def update_point(self, i, val): self.__update_point(i, val, self.tree) # 区间更新,将区间为 [q_left, q_right] 上的元素值修改为 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, self.tree) # 区间查询,查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, self.tree) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self, length): nums = [0 for _ in range(length)] for i in range(length): nums[i] = self.query_interval(i, i) return nums # 以下为内部实现方法 # 单点更新,将 nums[i] 更改为 val。node 节点的区间为 [node.left, node.right] def __update_point(self, i, val, node): if node.left == node.right: node.val = val # 叶子节点,节点值修改为 val return if i <= node.mid: # 在左子树中更新节点值 self.__update_point(i, val, node.leftNode) else: # 在右子树中更新节点值 self.__update_point(i, val, node.rightNode) self.__pushup(node) # 向上更新节点的区间值 # 区间更新 def __update_interval(self, q_left, q_right, val, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 if node.lazy_tag is not None: node.lazy_tag += val # 将当前节点的延迟标记增加 val else: node.lazy_tag = val # 将当前节点的延迟标记增加 val node.val += val # 当前节点所在区间每个元素值增加 val return if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 if q_left <= node.mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, node.leftNode) if q_right > node.mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, node.rightNode) self.__pushup(node) # 区间查询,在线段树的 [left, right] 区间范围中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return node.val # 直接返回节点值 if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= node.mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, node.leftNode) if q_right > node.mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, node.rightNode) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新 node 节点区间值,节点的区间值等于该节点左右子节点元素值的聚合计算结果 def __pushup(self, node): if node.leftNode and node.rightNode: node.val = self.function(node.leftNode.val, node.rightNode.val) # 向下更新 node 节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, node): if node.leftNode is None: node.leftNode = SegTreeNode(node.left, node.mid) if node.rightNode is None: node.rightNode = SegTreeNode(node.mid + 1, node.right) lazy_tag = node.lazy_tag if node.lazy_tag is None: return if node.leftNode.lazy_tag is not None: node.leftNode.lazy_tag += lazy_tag # 更新左子节点懒惰标记 else: node.leftNode.lazy_tag = lazy_tag # 更新左子节点懒惰标记 node.leftNode.val += lazy_tag # 左子节点每个元素值增加 lazy_tag if node.rightNode.lazy_tag is not None: node.rightNode.lazy_tag += lazy_tag # 更新右子节点懒惰标记 else: node.rightNode.lazy_tag = lazy_tag # 更新右子节点懒惰标记 node.rightNode.val += lazy_tag # 右子节点每个元素值增加 lazy_tag node.lazy_tag = None # 更新当前节点的懒惰标记 class MyCalendar: def __init__(self): self.STree = SegmentTree(lambda x, y: max(x, y)) def book(self, start: int, end: int) -> bool: if self.STree.query_interval(start, end - 1) >= 1: return False self.STree.update_interval(start, end - 1, 1) return True ``` ================================================ FILE: docs/solutions/0700-0799/my-calendar-ii.md ================================================ # [731. 我的日程安排表 II](https://leetcode.cn/problems/my-calendar-ii/) - 标签:设计、线段树、二分查找、有序集合 - 难度:中等 ## 题目链接 - [731. 我的日程安排表 II - 力扣](https://leetcode.cn/problems/my-calendar-ii/) ## 题目大意 **要求**:实现一个 `MyCalendar` 类来存放你的日程安排。如果要添加的时间内不会导致三重预订时,则可以存储这个新的日程安排。 日程可以用一对整数 $start$ 和 $end$ 表示,这里的时间是半开区间,即 $[start, end)$,实数 $x$ 的范围为 $start \le x < end$。 `MyCalendar` 类: - `MyCalendar()` 初始化日历对象。 - `boolean book(int start, int end)` 如果可以将日程安排成功添加到日历中而不会导致三重预订,返回 `True` 。否则,返回 `False` 并且不要将该日程安排添加到日历中。 **说明**: - 三重预定:当三个日程安排有一些时间上的交叉时(例如三个日程安排都在同一时间内),就会产生三重预订 。 - $0 \le start < end \le 10^9$。 - 每个测试用例,调用 `book` 方法的次数最多不超过 `1000` 次。 **示例**: - 示例 1: ```python 输入: ["MyCalendar", "book", "book", "book"] [[], [10, 20], [15, 25], [20, 30]] 输出: [null, true, false, true] 解释: MyCalendar myCalendar = new MyCalendar(); myCalendar.book(10, 20); // return True myCalendar.book(15, 25); // return False ,这个日程安排不能添加到日历中,因为时间 15 已经被另一个日程安排预订了。 myCalendar.book(20, 30); // return True ,这个日程安排可以添加到日历中,因为第一个日程安排预订的每个时间都小于 20 ,且不包含时间 20 。 ``` ## 解题思路 ### 思路 1:线段树 这道题可以使用线段树来做。 因为区间的范围是 $[0, 10^9]$,普通数组构成的线段树不满足要求。需要用到动态开点线段树。 - 构建一棵线段树。每个线段树的节点类存储当前区间中保存的日程区间个数。 - 在 `book` 方法中,从线段树中查询 `[start, end - 1]` 区间上保存的日程区间个数。 - 如果日程区间个数大于等于 `2`,则说明该日程添加到日历中会导致三重预订,则直接返回 `False`。 - 如果日程区间个数小于 `2`,则说明该日程添加到日历中不会导致三重预订,则在线段树中将区间 `[start, end - 1]` 的日程区间个数 + 1,然后返回 `True`。 ### 思路 1:线段树代码 ```python # 线段树的节点类 class SegTreeNode: def __init__(self, left=-1, right=-1, val=0, lazy_tag=None, leftNode=None, rightNode=None): self.left = left # 区间左边界 self.right = right # 区间右边界 self.mid = left + (right - left) // 2 self.leftNode = leftNode # 区间左节点 self.rightNode = rightNode # 区间右节点 self.val = val # 节点值(区间值) self.lazy_tag = lazy_tag # 区间问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, function): self.tree = SegTreeNode(0, int(1e9)) self.function = function # function 是一个函数,左右区间的聚合方法 # 单点更新,将 nums[i] 更改为 val def update_point(self, i, val): self.__update_point(i, val, self.tree) # 区间更新,将区间为 [q_left, q_right] 上的元素值修改为 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, self.tree) # 区间查询,查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, self.tree) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self, length): nums = [0 for _ in range(length)] for i in range(length): nums[i] = self.query_interval(i, i) return nums # 以下为内部实现方法 # 单点更新,将 nums[i] 更改为 val。node 节点的区间为 [node.left, node.right] def __update_point(self, i, val, node): if node.left == node.right: node.val = val # 叶子节点,节点值修改为 val return if i <= node.mid: # 在左子树中更新节点值 self.__update_point(i, val, node.leftNode) else: # 在右子树中更新节点值 self.__update_point(i, val, node.rightNode) self.__pushup(node) # 向上更新节点的区间值 # 区间更新 def __update_interval(self, q_left, q_right, val, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 if node.lazy_tag is not None: node.lazy_tag += val # 将当前节点的延迟标记增加 val else: node.lazy_tag = val # 将当前节点的延迟标记增加 val node.val += val # 当前节点所在区间增加 val return if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 if q_left <= node.mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, node.leftNode) if q_right > node.mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, node.rightNode) self.__pushup(node) # 区间查询,在线段树的 [left, right] 区间范围中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return node.val # 直接返回节点值 if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= node.mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, node.leftNode) if q_right > node.mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, node.rightNode) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新 node 节点区间值,节点的区间值等于该节点左右子节点元素值的聚合计算结果 def __pushup(self, node): if node.leftNode and node.rightNode: node.val = self.function(node.leftNode.val, node.rightNode.val) # 向下更新 node 节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, node): if node.leftNode is None: node.leftNode = SegTreeNode(node.left, node.mid) if node.rightNode is None: node.rightNode = SegTreeNode(node.mid + 1, node.right) lazy_tag = node.lazy_tag if node.lazy_tag is None: return if node.leftNode.lazy_tag is not None: node.leftNode.lazy_tag += lazy_tag # 更新左子节点懒惰标记 else: node.leftNode.lazy_tag = lazy_tag # 更新左子节点懒惰标记 node.leftNode.val += lazy_tag # 左子节点区间增加 lazy_tag if node.rightNode.lazy_tag is not None: node.rightNode.lazy_tag += lazy_tag # 更新右子节点懒惰标记 else: node.rightNode.lazy_tag = lazy_tag # 更新右子节点懒惰标记 node.rightNode.val += lazy_tag # 右子节点区间增加 lazy_tag node.lazy_tag = None # 更新当前节点的懒惰标记 class MyCalendarTwo: def __init__(self): self.STree = SegmentTree(lambda x, y: max(x, y)) def book(self, start: int, end: int) -> bool: if self.STree.query_interval(start, end - 1) >= 2: return False self.STree.update_interval(start, end - 1, 1) return True ``` ================================================ FILE: docs/solutions/0700-0799/my-calendar-iii.md ================================================ # [0732. 我的日程安排表 III](https://leetcode.cn/problems/my-calendar-iii/) - 标签:设计、线段树、二分查找、有序集合 - 难度:困难 ## 题目链接 - [0732. 我的日程安排表 III - 力扣](https://leetcode.cn/problems/my-calendar-iii/) ## 题目大意 **要求**:实现一个 `MyCalendarThree` 类来存放你的日程安排,你可以一直添加新的日程安排。 日程可以用一对整数 $start$ 和 $end$ 表示,这里的时间是半开区间,即 $[start, end)$,实数 $x$ 的范围为 $start \le x < end$。 `MyCalendarThree` 类: - `MyCalendarThree()` 初始化对象。 - `int book(int start, int end)` 返回一个整数 `k`,表示日历中存在的 `k` 次预订的最大值。 **说明**: - `k` 次预定:当 `k` 个日程安排有一些时间上的交叉时(例如 `k` 个日程安排都在同一时间内),就会产生 `k` 次预订。 - $0 \le start < end \le 10^9$ - 每个测试用例,调用 `book` 函数最多不超过 `400` 次。 **示例**: - 示例 1: ```python 输入 ["MyCalendarThree", "book", "book", "book", "book", "book", "book"] [[], [10, 20], [50, 60], [10, 40], [5, 15], [5, 10], [25, 55]] 输出 [null, 1, 1, 2, 3, 3, 3] 解释 MyCalendarThree myCalendarThree = new MyCalendarThree(); myCalendarThree.book(10, 20); // 返回 1 ,第一个日程安排可以预订并且不存在相交,所以最大 k 次预订是 1 次预订。 myCalendarThree.book(50, 60); // 返回 1 ,第二个日程安排可以预订并且不存在相交,所以最大 k 次预订是 1 次预订。 myCalendarThree.book(10, 40); // 返回 2 ,第三个日程安排 [10, 40) 与第一个日程安排相交,所以最大 k 次预订是 2 次预订。 myCalendarThree.book(5, 15); // 返回 3 ,剩下的日程安排的最大 k 次预订是 3 次预订。 myCalendarThree.book(5, 10); // 返回 3 myCalendarThree.book(25, 55); // 返回 3 ``` ## 解题思路 ### 思路 1:线段树 这道题可以使用线段树来做。 因为区间的范围是 $[0, 10^9]$,普通数组构成的线段树不满足要求。需要用到动态开点线段树。 - 构建一棵线段树。每个线段树的节点类存储当前区间中保存的日程区间个数。 - 在 `book` 方法中,在线段树中更新 `[start, end - 1]` 的交叉日程区间个数,即令其区间值整体加 `1`。 - 然后从线段树中查询区间 $[0, 10^9]$ 上保存的交叉日程区间个数,并返回。 ### 思路 1:代码 ```python # 线段树的节点类 class SegTreeNode: def __init__(self, left=-1, right=-1, val=0, lazy_tag=None, leftNode=None, rightNode=None): self.left = left # 区间左边界 self.right = right # 区间右边界 self.mid = left + (right - left) // 2 self.leftNode = leftNode # 区间左节点 self.rightNode = rightNode # 区间右节点 self.val = val # 节点值(区间值) self.lazy_tag = lazy_tag # 区间问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, function): self.tree = SegTreeNode(0, int(1e9)) self.function = function # function 是一个函数,左右区间的聚合方法 # 单点更新,将 nums[i] 更改为 val def update_point(self, i, val): self.__update_point(i, val, self.tree) # 区间更新,将区间为 [q_left, q_right] 上的元素值修改为 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, self.tree) # 区间查询,查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, self.tree) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self, length): nums = [0 for _ in range(length)] for i in range(length): nums[i] = self.query_interval(i, i) return nums # 以下为内部实现方法 # 单点更新,将 nums[i] 更改为 val。node 节点的区间为 [node.left, node.right] def __update_point(self, i, val, node): if node.left == node.right: node.val = val # 叶子节点,节点值修改为 val return if i <= node.mid: # 在左子树中更新节点值 self.__update_point(i, val, node.leftNode) else: # 在右子树中更新节点值 self.__update_point(i, val, node.rightNode) self.__pushup(node) # 向上更新节点的区间值 # 区间更新 def __update_interval(self, q_left, q_right, val, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 if node.lazy_tag is not None: node.lazy_tag += val # 将当前节点的延迟标记增加 val else: node.lazy_tag = val # 将当前节点的延迟标记增加 val node.val += val # 当前节点所在区间增加 val return if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 if q_left <= node.mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, node.leftNode) if q_right > node.mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, node.rightNode) self.__pushup(node) # 区间查询,在线段树的 [left, right] 区间范围中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return node.val # 直接返回节点值 if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= node.mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, node.leftNode) if q_right > node.mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, node.rightNode) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新 node 节点区间值,节点的区间值等于该节点左右子节点元素值的聚合计算结果 def __pushup(self, node): if node.leftNode and node.rightNode: node.val = self.function(node.leftNode.val, node.rightNode.val) # 向下更新 node 节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, node): if node.leftNode is None: node.leftNode = SegTreeNode(node.left, node.mid) if node.rightNode is None: node.rightNode = SegTreeNode(node.mid + 1, node.right) lazy_tag = node.lazy_tag if node.lazy_tag is None: return if node.leftNode.lazy_tag is not None: node.leftNode.lazy_tag += lazy_tag # 更新左子节点懒惰标记 else: node.leftNode.lazy_tag = lazy_tag # 更新左子节点懒惰标记 node.leftNode.val += lazy_tag # 左子节点区间增加 lazy_tag if node.rightNode.lazy_tag is not None: node.rightNode.lazy_tag += lazy_tag # 更新右子节点懒惰标记 else: node.rightNode.lazy_tag = lazy_tag # 更新右子节点懒惰标记 node.rightNode.val += lazy_tag # 右子节点区间增加 lazy_tag node.lazy_tag = None # 更新当前节点的懒惰标记 class MyCalendarThree: def __init__(self): self.STree = SegmentTree(lambda x, y: max(x, y)) def book(self, start: int, end: int) -> int: self.STree.update_interval(start, end - 1, 1) return self.STree.query_interval(0, int(1e9)) # Your MyCalendarThree object will be instantiated and called as such: # obj = MyCalendarThree() # param_1 = obj.book(start,end) ``` ================================================ FILE: docs/solutions/0700-0799/network-delay-time.md ================================================ # [0743. 网络延迟时间](https://leetcode.cn/problems/network-delay-time/) - 标签:深度优先搜索、广度优先搜索、图、最短路、堆(优先队列) - 难度:中等 ## 题目链接 - [0743. 网络延迟时间 - 力扣](https://leetcode.cn/problems/network-delay-time/) ## 题目大意 **描述**: 有 $n$ 个节点组成的网络,节点标记为 $1 \sim n$。 给定一个列表 $times[i] = (u_i, v_i, w_i)$,表示信号经过有向边的传递时间,其中 $u_i$ 是源节点,$v_i$ 是目标节点,$w_i$ 是一个信号从源节点传递到目标节点的时间。 给定一个整数 $n$,表示 $n$ 个节点。 给定一个节点 $k$,表示从节点 $k$ 发出一个信号。 **要求**: 需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 $-1$。 **说明**: - $1 \le k \le n \le 100$。 - $1 \le times.length \le 6000$。 - $times[i].length == 3$。 - $1 \le u_i, v_i \le n$。 - $u_i \ne v_i$。 - $0 \le w_i \le 100$。 - 所有 $(u_i, v_i)$ 对都互不相同(即不含重复边)。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2019/05/23/931_example_1.png) ```python 输入:times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2 输出:2 ``` - 示例 2: ```python 输入:times = [[1,2,1]], n = 2, k = 1 输出:1 ``` ## 解题思路 ### 思路 1:Bellman Ford 算法 Bellman Ford 算法核心思想:通过「松弛操作」来逐步更新从源节点 $k$ 到所有其他节点的最短距离。 **算法步骤**: 1. **初始化距离数组**:将源节点 $k$ 的距离 $dist[k]$ 设为 $0$,其他节点的距离设为无穷大。 2. **松弛操作**:进行 $n - 1$ 轮松弛,每轮遍历所有边 $(u_i, v_i, w_i)$,如果 $dist[v_i] > dist[u_i] + w_i$,则更新 $dist[v_i] = dist[u_i] + w_i$。 3. **检测负环**(本题不需要):再次遍历所有边,如果仍然存在 $dist[v_i] > dist[u_i] + w_i$,说明存在负权环。 4. **返回结果**:找出距离数组中的最大值,如果存在无法到达的节点(距离仍为无穷大),则返回 $-1$。 **关键点**: - 经过 $n - 1$ 轮松弛后,所有可达节点的最短距离已经确定。 - 由于本题没有负权边,不需要检测负环,但保留检测逻辑也无妨。 ### 思路 1:代码 ```python from typing import List class Solution: def bellmanFord(self, graph, n, source): """Bellman Ford 算法实现 Args: graph: 图的邻接表表示,graph[u][v] 表示边 (u, v) 的权重 n: 节点数量 source: 源节点 Returns: 距离数组,dist[i] 表示从源节点到节点 i 的最短距离 """ # 初始化距离数组,所有节点距离设为无穷大 dist = [float('inf') for _ in range(n + 1)] # 源节点距离设为 0 dist[source] = 0 # 进行 n - 1 轮松弛操作 for i in range(n - 1): # 遍历所有边进行松弛 for u in graph: for v in graph[u]: # 如果可以通过 u 更新 v 的距离,则更新 if dist[v] > graph[u][v] + dist[u]: dist[v] = graph[u][v] + dist[u] # 检测负权环(本题不需要,但保留检测逻辑) for u in graph: for v in graph[u]: if dist[v] > dist[u] + graph[u][v]: return None return dist def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int: """计算网络延迟时间 Args: times: 边列表,每个元素为 (u, v, w),表示从 u 到 v 的边权重为 w n: 节点数量 k: 源节点 Returns: 所有节点收到信号的最短时间,如果存在无法到达的节点则返回 -1 """ # 构建邻接表 graph = dict() for u, v, w in times: if u not in graph: graph[u] = dict() if v not in graph[u]: graph[u][v] = w # 使用 Bellman Ford 算法计算最短距离 dist = self.bellmanFord(graph, n, k) # 如果返回 None,说明存在负权环(本题不会出现) if dist is None: return -1 # 找出最大距离 ans = 0 for i in range(1, len(dist)): # 如果存在无法到达的节点,返回 -1 if dist[i] >= float('inf'): return -1 ans = max(ans, dist[i]) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(V \times E)$,其中 $V$ 是节点的数量,$E$ 是边的数量。需要进行 $V - 1$ 轮松弛,每轮遍历所有 $E$ 条边。 - **空间复杂度**:$O(V + E)$,其中 $V$ 是节点的数量,$E$ 是边的数量。需要存储邻接表和距离数组。 ### 思路 2:朴素 Dijkstra 算法 朴素 Dijkstra 算法:不使用优先队列,而是每次遍历所有未访问的节点来找到距离最小的节点。虽然时间复杂度较高,但实现更直观。 **算法步骤**: 1. **初始化距离数组**:将源节点 $k$ 的距离 $dist[k]$ 设为 $0$,其他节点的距离设为无穷大。 2. **维护访问数组**:使用 $visited$ 数组记录节点是否已经被访问。 3. **贪心选择**:每次从未访问的节点中找到距离 $dist[u]$ 最小的节点 $u$,标记为已访问。 4. **松弛操作**:更新节点 $u$ 的所有相邻节点 $v$ 的距离,如果 $dist[v] > dist[u] + w(u, v)$,则更新 $dist[v] = dist[u] + w(u, v)$。 5. **重复步骤**:重复步骤 $3 \sim 4$,直到所有节点都被访问。 6. **返回结果**:找出距离数组中的最大值,如果存在无法到达的节点(距离仍为无穷大),则返回 $-1$。 **关键点**: - Dijkstra 算法适用于非负权图,每次选择距离最小的节点进行松弛。 - 由于题目约束 $0 \le w_i \le 100$,所有边权非负,可以使用 Dijkstra 算法。 ### 思路 2:代码 ```python from typing import List class Solution: def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int: """计算网络延迟时间(朴素 Dijkstra 算法) Args: times: 边列表,每个元素为 (u, v, w),表示从 u 到 v 的边权重为 w n: 节点数量 k: 源节点 Returns: 所有节点收到信号的最短时间,如果存在无法到达的节点则返回 -1 """ # 使用哈希表构建邻接表 graph = dict() for u, v, w in times: if u not in graph: graph[u] = dict() graph[u][v] = w # 初始化距离数组和访问数组 dist = [float('inf')] * (n + 1) dist[k] = 0 # 源节点距离设为 0 visited = [False] * (n + 1) # 遍历所有节点,每次选择一个未访问的距离最小的节点 for _ in range(n): # 找到未访问的距离最小的节点 min_dist = float('inf') u = -1 for i in range(1, n + 1): if not visited[i] and dist[i] < min_dist: min_dist = dist[i] u = i # 如果没有找到可达的节点,说明存在无法到达的节点 if u == -1: return -1 # 标记当前节点为已访问 visited[u] = True # 更新相邻节点的距离 if u in graph: for v, w in graph[u].items(): # 如果通过 u 到达 v 的距离更短,则更新 if not visited[v] and dist[v] > dist[u] + w: dist[v] = dist[u] + w # 找出最大距离 return max(dist[1:]) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(V^2 + E)$,其中 $V$ 是节点的数量,$E$ 是边的数量。每次需要遍历所有节点找到最小距离节点,总共需要 $V$ 次,每次遍历需要 $O(V)$ 的时间。更新边的操作需要 $O(E)$ 的时间。 - **空间复杂度**:$O(V + E)$,其中 $V$ 是节点的数量,$E$ 是边的数量。需要存储邻接表、距离数组和访问数组。 ### 思路 3:Dijkstra 算法(堆优化) Dijkstra 算法是解决单源最短路径问题的经典算法。在这个问题中,我们可以使用 Dijkstra 算法来找到从节点 $k$ 到所有其他节点的最短路径。 堆优化版本的 Dijkstra 算法:使用优先队列(最小堆)来维护待处理的节点,避免每次遍历所有节点查找最小距离节点,从而降低时间复杂度。 **算法步骤**: 1. **构建邻接表**:使用邻接表存储图结构,方便遍历相邻节点。 2. **初始化距离数组**:将源节点 $k$ 的距离 $dist[k]$ 设为 $0$,其他节点的距离设为无穷大。 3. **初始化优先队列**:将源节点 $k$ 及其距离 $0$ 加入优先队列。 4. **贪心选择**:每次从优先队列中取出距离 $dist[u]$ 最小的节点 $u$。 5. **跳过无效节点**:如果当前距离大于已知最短距离,说明该节点已被更优路径访问过,跳过。 6. **松弛操作**:更新节点 $u$ 的所有相邻节点 $v$ 的距离,如果 $dist[v] > dist[u] + w(u, v)$,则更新 $dist[v] = dist[u] + w(u, v)$ 并将 $(dist[v], v)$ 加入优先队列。 7. **重复步骤**:重复步骤 $4 \sim 6$,直到优先队列为空。 8. **返回结果**:找出距离数组中的最大值,如果存在无法到达的节点(距离仍为无穷大),则返回 $-1$。 **关键点**: - 使用优先队列可以将查找最小距离节点的时间复杂度从 $O(V)$ 降低到 $O(\log V)$。 - 同一个节点可能被多次加入优先队列,但只有距离更小的才会被处理。 ### 思路 3:代码 ```python import heapq from typing import List class Solution: def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int: """计算网络延迟时间(Dijkstra 算法堆优化版本) Args: times: 边列表,每个元素为 (u, v, w),表示从 u 到 v 的边权重为 w n: 节点数量 k: 源节点 Returns: 所有节点收到信号的最短时间,如果存在无法到达的节点则返回 -1 """ # 构建邻接表 graph = [[] for _ in range(n + 1)] for u, v, w in times: graph[u].append((v, w)) # 初始化距离数组,所有节点距离设为无穷大 dist = [float('inf')] * (n + 1) dist[k] = 0 # 源节点距离设为 0 # 使用优先队列(最小堆)存储待处理的节点,格式为 (距离, 节点编号) pq = [(0, k)] while pq: # 取出距离最小的节点 d, u = heapq.heappop(pq) # 如果当前距离大于已知最短距离,说明该节点已被更优路径访问过,跳过 if d > dist[u]: continue # 遍历相邻节点 for v, w in graph[u]: # 如果通过 u 到达 v 的距离更短,则更新 if dist[v] > dist[u] + w: dist[v] = dist[u] + w # 将更新后的节点加入优先队列 heapq.heappush(pq, (dist[v], v)) # 找出最大距离 max_dist = max(dist[1:]) # 如果存在无法到达的节点,返回 -1 return max_dist if max_dist != float('inf') else -1 ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(E \log V)$,其中 $E$ 是边的数量,$V$ 是节点的数量。每次从优先队列中取出一个节点需要 $O(\log V)$ 的时间,总共需要处理 $E$ 条边。 - **空间复杂度**:$O(V + E)$,其中 $V$ 是节点的数量,$E$ 是边的数量。需要存储邻接表和优先队列。 ### 思路 4:SPFA 算法 SPFA(Shortest Path Faster Algorithm):Bellman-Ford 算法的一个优化版本。它使用队列来维护待更新的节点,只有当节点的距离被更新时,才将其加入队列,从而避免不必要的松弛操作。 **算法步骤**: 1. **初始化距离数组**:将源节点 $k$ 的距离 $dist[k]$ 设为 $0$,其他节点的距离设为无穷大。 2. **初始化队列**:将源节点 $k$ 加入队列,并使用 $in\_queue$ 数组标记节点是否在队列中。 3. **队列处理**:从队列中取出一个节点 $u$,标记其不在队列中。 4. **松弛操作**:遍历节点 $u$ 的所有相邻节点 $v$: - 如果 $dist[v] > dist[u] + w(u, v)$,则更新 $dist[v] = dist[u] + w(u, v)$。 - 如果节点 $v$ 不在队列中,则将其加入队列并标记。 5. **重复步骤**:重复步骤 $3 \sim 4$,直到队列为空。 6. **返回结果**:找出距离数组中的最大值,如果存在无法到达的节点(距离仍为无穷大),则返回 $-1$。 **关键点**: - SPFA 算法只对距离发生变化的节点进行松弛,避免了对所有边的重复检查。 - 在非负权图中,SPFA 算法的性能通常优于 Bellman-Ford 算法。 - 由于题目约束 $0 \le w_i \le 100$,所有边权非负,不会出现负权环,SPFA 算法可以正常使用。 ### 思路 4:代码 ```python from typing import List from collections import deque class Solution: def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int: """计算网络延迟时间(SPFA 算法) Args: times: 边列表,每个元素为 (u, v, w),表示从 u 到 v 的边权重为 w n: 节点数量 k: 源节点 Returns: 所有节点收到信号的最短时间,如果存在无法到达的节点则返回 -1 """ # 使用哈希表构建邻接表 graph = dict() for u, v, w in times: if u not in graph: graph[u] = dict() graph[u][v] = w # 初始化距离数组,所有节点距离设为无穷大 dist = [float('inf')] * (n + 1) dist[k] = 0 # 源节点距离设为 0 # 使用队列存储待更新的节点 queue = deque([k]) # 记录节点是否在队列中,避免重复入队 in_queue = [False] * (n + 1) in_queue[k] = True while queue: # 取出队头节点 u = queue.popleft() in_queue[u] = False # 遍历相邻节点 if u in graph: for v, w in graph[u].items(): # 如果通过 u 到达 v 的距离更短,则更新 if dist[v] > dist[u] + w: dist[v] = dist[u] + w # 如果节点不在队列中,则加入队列 if not in_queue[v]: queue.append(v) in_queue[v] = True # 找出最大距离 max_dist = max(dist[1:]) # 如果存在无法到达的节点,返回 -1 return max_dist if max_dist != float('inf') else -1 ``` ### 思路 4:复杂度分析 - **时间复杂度**:平均情况下为 $O(kE)$,其中 $E$ 是边的数量,$k$ 是每个节点入队的平均次数。在最坏情况下可能退化为 $O(VE)$。 - **空间复杂度**:$O(V + E)$,其中 $V$ 是节点的数量,$E$ 是边的数量。需要存储邻接表、距离数组和队列。 ================================================ FILE: docs/solutions/0700-0799/number-of-atoms.md ================================================ # [0726. 原子的数量](https://leetcode.cn/problems/number-of-atoms/) - 标签:栈、哈希表、字符串、排序 - 难度:困难 ## 题目链接 - [0726. 原子的数量 - 力扣](https://leetcode.cn/problems/number-of-atoms/) ## 题目大意 **描述**: 原子总是以一个大写字母开始,接着跟随 $0$ 个或任意个小写字母,表示原子的名字。 如果数量大于 $1$,原子后会跟着数字表示原子的数量。如果数量等于 $1$ 则不会跟数字。 - 例如,`"H2O"` 和 `"H2O2"` 是可行的,但 `"H1O2"` 这个表达是不可行的。 两个化学式连在一起可以构成新的化学式。 - 例如 `"H2O2He3Mg4"` 也是化学式。 由括号括起的化学式并佐以数字(可选择性添加)也是化学式。 - 例如 `"(H2O2)"` 和 `"(H2O2)3"` 是化学式。 给定一个字符串化学式 $formula$。 **要求**: 返回「每种原子的数量」,格式为:第一个(按字典序)原子的名字,跟着它的数量(如果数量大于 1),然后是第二个原子的名字(按字典序),跟着它的数量(如果数量大于 1),以此类推。 **说明**: - $1 \le formula.length \le 10^{3}$。 - $formula$ 由英文字母、数字、`'('` 和 `')'` 组成。 - $formula$ 总是有效的化学式。 **示例**: - 示例 1: ```python 输入:formula = "H2O" 输出:"H2O" 解释:原子的数量是 {'H': 2, 'O': 1}。 ``` - 示例 2: ```python 输入:formula = "Mg(OH)2" 输出:"H2MgO2" 解释:原子的数量是 {'H': 2, 'Mg': 1, 'O': 2}。 ``` ## 解题思路 ### 思路 1:栈 + 哈希表 这道题需要解析化学式并统计每种原子的数量。可以使用栈来处理括号嵌套的情况。 **解题步骤**: 1. 使用栈来存储当前层级的原子计数(哈希表)。 2. 遍历化学式字符串: - 遇到 `(`:将当前哈希表压入栈,创建新的哈希表。 - 遇到 `)`:弹出栈顶哈希表,读取后面的数字,将当前哈希表中的所有原子数量乘以该数字,然后合并到栈顶哈希表。 - 遇到大写字母:读取完整的原子名称和后面的数字,加入当前哈希表。 3. 最后将所有原子按字典序排序,构建结果字符串。 **实现细节**: - 使用栈存储嵌套的哈希表。 - 解析原子名称:大写字母开头,后面跟若干小写字母。 - 解析数字:连续的数字字符。 ### 思路 1:代码 ```python class Solution: def countOfAtoms(self, formula: str) -> str: from collections import defaultdict n = len(formula) i = 0 stack = [defaultdict(int)] while i < n: if formula[i] == '(': # 遇到左括号,压入新的哈希表 stack.append(defaultdict(int)) i += 1 elif formula[i] == ')': # 遇到右括号,读取后面的数字 i += 1 start = i while i < n and formula[i].isdigit(): i += 1 multiplier = int(formula[start:i]) if start < i else 1 # 弹出栈顶哈希表,乘以倍数后合并到新的栈顶 top = stack.pop() for atom, count in top.items(): stack[-1][atom] += count * multiplier else: # 读取原子名称 start = i i += 1 while i < n and formula[i].islower(): i += 1 atom = formula[start:i] # 读取数字 start = i while i < n and formula[i].isdigit(): i += 1 count = int(formula[start:i]) if start < i else 1 # 加入当前哈希表 stack[-1][atom] += count # 构建结果字符串 result = [] for atom in sorted(stack[-1].keys()): count = stack[-1][atom] result.append(atom) if count > 1: result.append(str(count)) return ''.join(result) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + k \log k)$,其中 $n$ 是化学式的长度,$k$ 是不同原子的种类数。需要遍历化学式一次,然后对原子排序。 - **空间复杂度**:$O(n)$。栈的深度最多为括号的嵌套层数,哈希表存储原子计数。 ================================================ FILE: docs/solutions/0700-0799/number-of-matching-subsequences.md ================================================ # [0792. 匹配子序列的单词数](https://leetcode.cn/problems/number-of-matching-subsequences/) - 标签:字典树、数组、哈希表、字符串、二分查找、动态规划、排序 - 难度:中等 ## 题目链接 - [0792. 匹配子序列的单词数 - 力扣](https://leetcode.cn/problems/number-of-matching-subsequences/) ## 题目大意 **描述**: 给定字符串 $s$ 和字符串数组 $words$。 **要求**: 返回 $words[i]$ 中是 $s$ 的子序列的单词个数。 **说明**: - 字符串的「子序列」:是从原始字符串中生成的新字符串,可以从中删去一些字符(可以是 none),而不改变其余字符的相对顺序。 - 例如,`"ace"` 是 `"abcde"` 的子序列。 - $1 \le s.length \le 5 * 10^{4}$。 - $1 \le words.length \le 5000$。 - $1 \le words[i].length \le 50$。 - $words[i]$ 和 $s$ 都只由小写字母组成。 **示例**: - 示例 1: ```python 输入: s = "abcde", words = ["a","bb","acd","ace"] 输出: 3 解释: 有三个是 s 的子序列的单词: "a", "acd", "ace"。 ``` - 示例 2: ```python 输入: s = "dsahjpjauf", words = ["ahjpjau","ja","ahbwzgqnuk","tnmlanowax"] 输出: 2 ``` ## 解题思路 ### 思路 1:哈希表 + 双指针 这道题要求统计有多少个单词是字符串 $s$ 的子序列。 **解题步骤**: 1. **优化方法**:为了避免对每个单词都遍历一次 $s$,可以使用哈希表将单词按首字母分组。 2. 对于 $s$ 中的每个字符 $c$,找到所有以 $c$ 开头的单词,尝试匹配。 3. 使用双指针判断单词是否是 $s$ 的子序列: - 对于每个单词,维护一个指针指向当前需要匹配的字符。 - 遍历 $s$,如果当前字符与单词的当前字符匹配,移动单词指针。 - 如果单词指针到达末尾,说明该单词是 $s$ 的子序列。 **优化**:将单词按首字母分组,每次只处理首字母匹配的单词。 ### 思路 1:代码 ```python class Solution: def numMatchingSubseq(self, s: str, words: List[str]) -> int: from collections import defaultdict # 将单词按首字母分组,存储 (单词, 当前匹配位置) waiting = defaultdict(list) for word in words: waiting[word[0]].append((word, 0)) count = 0 # 遍历 s 中的每个字符 for char in s: # 获取所有等待匹配当前字符的单词 current_waiting = waiting[char] waiting[char] = [] for word, index in current_waiting: index += 1 if index == len(word): # 单词匹配完成 count += 1 else: # 继续等待下一个字符 waiting[word[index]].append((word, index)) return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 是字符串 $s$ 的长度,$m$ 是所有单词的总长度。每个字符最多被访问一次。 - **空间复杂度**:$O(m)$。需要存储所有单词的状态。 ================================================ FILE: docs/solutions/0700-0799/number-of-subarrays-with-bounded-maximum.md ================================================ # [0795. 区间子数组个数](https://leetcode.cn/problems/number-of-subarrays-with-bounded-maximum/) - 标签:数组、双指针 - 难度:中等 ## 题目链接 - [0795. 区间子数组个数 - 力扣](https://leetcode.cn/problems/number-of-subarrays-with-bounded-maximum/) ## 题目大意 给定一个元素都是正整数的数组`A` ,正整数 `L` 以及 `R` (`L <= R`)。 求连续、非空且其中最大元素满足大于等于`L` 小于等于`R`的子数组个数。 ## 解题思路 最大元素满足大于等于`L` 小于等于`R`的子数组个数 = 最大元素小于等于 `R` 的子数组个数 - 最大元素小于 `L` 的子数组个数。 其中「最大元素小于 `L` 的子数组个数」也可以转变为「最大元素小于等于 `L - 1` 的子数组个数」。那么现在的问题就变为了如何计算最大元素小于等于 `k` 的子数组个数。 我们使用 `count` 记录 小于等于 `k` 的连续元素数量,遍历一遍数组,如果遇到 `nums[i] <= k` 时,`count` 累加,表示在此位置上结束的有效子数组数量为 `count + 1`。如果遇到 `nums[i] > k` 时,`count` 重新开始计算。每次遍历完将有效子数组数量累加到答案中。 ## 代码 ```python class Solution: def numSubarrayMaxK(self, nums, k): ans = 0 count = 0 for i in range(len(nums)): if nums[i] <= k: count += 1 else: count = 0 ans += count return ans def numSubarrayBoundedMax(self, nums: List[int], left: int, right: int) -> int: return self.numSubarrayMaxK(nums, right) - self.numSubarrayMaxK(nums, left - 1) ``` ================================================ FILE: docs/solutions/0700-0799/open-the-lock.md ================================================ # [0752. 打开转盘锁](https://leetcode.cn/problems/open-the-lock/) - 标签:广度优先搜索、数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [0752. 打开转盘锁 - 力扣](https://leetcode.cn/problems/open-the-lock/) ## 题目大意 **描述**:有一把带有四个数字的密码锁,每个位置上有 `0` ~ `9` 共 `10` 个数字。每次只能将其中一个位置上的数字转动一下。可以向上转,也可以向下转。比如:`1 -> 2`、`2 -> 1`。 密码锁的初始数字为:`0000`。现在给定一组表示死亡数字的字符串数组 `deadends`,和一个带有四位数字的目标字符串 `target`。 如果密码锁转动到 `deadends` 中任一字符串状态,则锁就会永久锁定,无法再次旋转。 **要求**:给出使得锁的状态由 `0000` 转动到 `target` 的最小的选择次数。如果无论如何不能解锁,返回 `-1` 。 **说明**: - $1 \le deadends.length \le 500$ $deadends[i].length == 4$ $target.length == 4$ $target$ 不在 $deadends$ 之中 $target$ 和 $deadends[i]$ 仅由若干位数字组成。 **示例**: - 示例 1: ```python 输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202" 输出:6 解释: 可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。 注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的, 因为当拨动到 "0102" 时这个锁就会被锁定。 ``` - 示例 2: ```python 输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888" 输出:-1 解释:无法旋转到目标数字且不被锁定。 ``` ## 解题思路 ### 思路 1:广度优先搜索 1. 定义 `visited` 为标记访问节点的 set 集合变量,`queue` 为存放节点的队列。 2. 将`0000` 状态标记为访问,并将其加入队列 `queue`。 3. 将当前队列中的所有状态依次出队,判断这些状态是否为死亡字符串。 1. 如果为死亡字符串,则跳过该状态,否则继续执行。 2. 如果为目标字符串,则返回当前路径长度,否则继续执行。 4. 枚举当前状态所有位置所能到达的所有状态(通过向上或者向下旋转),并判断是否访问过该状态。 5. 如果之前出现过该状态,则继续执行,否则将其存入队列,并标记访问。 6. 遍历完步骤 3 中当前队列中的所有状态,令路径长度加 `1`,继续执行 3 ~ 5 步,直到队列为空。 7. 如果队列为空,也未能到达目标状态,则返回 `-1`。 ### 思路 1:代码 ```python import collections class Solution: def openLock(self, deadends: List[str], target: str) -> int: queue = collections.deque(['0000']) visited = set(['0000']) deadset = set(deadends) level = 0 while queue: size = len(queue) for _ in range(size): cur = queue.popleft() if cur in deadset: continue if cur == target: return level for i in range(len(cur)): up = self.upward_adjust(cur, i) if up not in visited: queue.append(up) visited.add(up) down = self.downward_adjust(cur, i) if down not in visited: queue.append(down) visited.add(down) level += 1 return -1 def upward_adjust(self, s, i): s_list = list(s) if s_list[i] == '9': s_list[i] = '0' else: s_list[i] = chr(ord(s_list[i]) + 1) return "".join(s_list) def downward_adjust(self, s, i): s_list = list(s) if s_list[i] == '0': s_list[i] = '9' else: s_list[i] = chr(ord(s_list[i]) - 1) return "".join(s_list) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(10^d \times d^2 + m \times d)$。其中 $d$ 是数字的位数,$m$ 是数组 $deadends$ 的长度。 - **空间复杂度**:$O(10^D \times d + m)$。 ================================================ FILE: docs/solutions/0700-0799/parse-lisp-expression.md ================================================ # [0736. Lisp 语法解析](https://leetcode.cn/problems/parse-lisp-expression/) - 标签:栈、递归、哈希表、字符串 - 难度:困难 ## 题目链接 - [0736. Lisp 语法解析 - 力扣](https://leetcode.cn/problems/parse-lisp-expression/) ## 题目大意 **描述**: 给定一个类似 Lisp 语句的字符串表达式 $expression$。 表达式语法如下所示: - 表达式可以为整数,let 表达式,add 表达式,mult 表达式,或赋值的变量。表达式的结果总是一个整数。 - (整数可以是正整数、负整数、0) - **let** 表达式采用 `"(let v1 e1 v2 e2 ... vn en expr)"` 的形式,其中 let 总是以字符串 `"let"` 来表示,接下来会跟随一对或多对交替的变量和表达式,也就是说,第一个变量 v1 被分配为表达式 e1 的值,第二个变量 v2 被分配为表达式 e2 的值,依次类推;最终 let 表达式的值为 expr 表达式的值。 - **add** 表达式表示为 `"(add e1 e2)"`,其中 add 总是以字符串 `"add"` 来表示,该表达式总是包含两个表达式 e1、e2,最终结果是 e1 表达式的值与 e2 表达式的值之「和」。 - **mult** 表达式表示为 `"(mult e1 e2)"`,其中 mult 总是以字符串 `"mult"` 表示,该表达式总是包含两个表达式 e1、e2,最终结果是 e1 表达式的值与 e2 表达式的值之「积」。 - 在该题目中,变量名以小写字符开始,之后跟随 0 个或多个小写字符或数字。为了方便,`"add"`,`"let"`,`"mult"` 会被定义为 `"关键字"`,不会用作变量名。 - 最后,要说一下作用域的概念。计算变量名所对应的表达式时,在计算上下文中,首先检查最内层作用域(按括号计),然后按顺序依次检查外部作用域。测试用例中每一个表达式都是合法的。有关作用域的更多详细信息,请参阅示例。 **要求**: 求出其计算结果。 **说明**: - $1 \le expression.length \le 2000$。 - $exprssion$ 中不含前导和尾随空格。 - $expressoin$ 中的不同部分(token)之间用单个空格进行分隔。 - 答案和所有中间计算结果都符合 32-bit 整数范围。 - 测试用例中的表达式均为合法的且最终结果为整数。 **示例**: - 示例 1: ```python 输入:expression = "(let x 2 (mult x (let x 3 y 4 (add x y))))" 输出:14 解释: 计算表达式 (add x y), 在检查变量 x 值时, 在变量的上下文中由最内层作用域依次向外检查。 首先找到 x = 3, 所以此处的 x 值是 3 。 ``` - 示例 2: ```python 输入:expression = "(let x 3 x 2 x)" 输出:2 解释:let 语句中的赋值运算按顺序处理即可。 ``` ## 解题思路 ### 思路 1:递归 + 哈希表 这道题需要解析和计算 Lisp 表达式。可以使用递归来处理嵌套的表达式。 **解题步骤**: 1. 使用递归函数 `evaluate` 来计算表达式的值。 2. 使用哈希表(作用域)来存储变量的值。 3. 根据表达式的类型进行处理: - 如果是整数,直接返回其值。 - 如果是变量,从作用域中查找其值。 - 如果是 `let` 表达式,依次赋值变量,然后计算最后一个表达式。 - 如果是 `add` 表达式,计算两个子表达式的和。 - 如果是 `mult` 表达式,计算两个子表达式的积。 **实现细节**: - 使用栈来解析表达式,处理括号嵌套。 - 每个 `let` 表达式创建新的作用域(复制父作用域)。 - 递归计算子表达式的值。 ### 思路 1:代码 ```python class Solution: def evaluate(self, expression: str) -> int: def parse(expr): """解析表达式,返回 token 列表""" tokens = [] i = 0 while i < len(expr): if expr[i] in '()': tokens.append(expr[i]) i += 1 elif expr[i] == ' ': i += 1 else: j = i while j < len(expr) and expr[j] not in '() ': j += 1 tokens.append(expr[i:j]) i = j return tokens def evaluate_helper(tokens, index, scope): """递归计算表达式的值""" token = tokens[index] if token == '(': # 处理括号表达式 index += 1 op = tokens[index] index += 1 if op == 'let': # 创建新的作用域 new_scope = scope.copy() # 处理变量赋值 while True: # 检查是否是最后一个表达式 if tokens[index] == ')': # 没有最后的表达式,返回 0 return 0, index + 1 # 先保存当前位置 saved_index = index # 尝试解析下一个表达式 value, next_index = evaluate_helper(tokens, index, new_scope) # 检查是否还有下一个 token if tokens[next_index] == ')': # 这是最后一个表达式 return value, next_index + 1 # 这是一个变量名,读取其值 var_name = tokens[saved_index] index = next_index # 计算变量的值 var_value, index = evaluate_helper(tokens, index, new_scope) new_scope[var_name] = var_value elif op == 'add': # 计算两个子表达式的和 val1, index = evaluate_helper(tokens, index, scope) val2, index = evaluate_helper(tokens, index, scope) index += 1 # 跳过 ')' return val1 + val2, index elif op == 'mult': # 计算两个子表达式的积 val1, index = evaluate_helper(tokens, index, scope) val2, index = evaluate_helper(tokens, index, scope) index += 1 # 跳过 ')' return val1 * val2, index elif token.lstrip('-').isdigit(): # 整数 return int(token), index + 1 else: # 变量 return scope.get(token, 0), index + 1 tokens = parse(expression) result, _ = evaluate_helper(tokens, 0, {}) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是表达式的长度。需要遍历表达式一次。 - **空间复杂度**:$O(n)$。递归栈和作用域的空间消耗。 ================================================ FILE: docs/solutions/0700-0799/partition-labels.md ================================================ # [0763. 划分字母区间](https://leetcode.cn/problems/partition-labels/) - 标签:贪心、哈希表、双指针、字符串 - 难度:中等 ## 题目链接 - [0763. 划分字母区间 - 力扣](https://leetcode.cn/problems/partition-labels/) ## 题目大意 给定一个由小写字母组成的字符串 `s`。要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。 要求:返回一个表示每个字符串片段的长度的列表。 ## 解题思路 因为同一字母最多出现在一个片段中,则同一字母第一次出现的下标位置和最后一次出现的下标位置肯定在同一个片段中。 我们先遍历一遍字符串,用哈希表 letter_map 存储下每一个字母最后一次出现的下标位置。 为了得到尽可能的片段,我们使用贪心的思想: - 从头开始遍历字符串,遍历同时维护当前片段的开始位置 start 和结束位置 end。 - 对于字符串中的每个字符 `s[i]`,得到当前字母的最后一次出现的下标位置 `letter_map[s[i]]`,则当前片段的结束位置一定不会早于 `letter_map[s[i]]`,所以更新 end 值为 `end = max(end, letter_map[s[i]])`。 - 当访问到 `i == end` 时,当前片段访问结束,当前片段的下标范围为 `[start, end]`,长度为 `end - start + 1`,将其长度加入答案数组,并更新 start 值为 `i + 1`,继续遍历。 - 最终返回答案数组。 ## 代码 ```python class Solution: def partitionLabels(self, s: str) -> List[int]: letter_map = dict() for i in range(len(s)): letter_map[s[i]] = i res = [] start, end = 0, 0 for i in range(len(s)): end = max(end, letter_map[s[i]]) if i == end: res.append(end - start + 1) start = i + 1 return res ``` ================================================ FILE: docs/solutions/0700-0799/prefix-and-suffix-search.md ================================================ # [0745. 前缀和后缀搜索](https://leetcode.cn/problems/prefix-and-suffix-search/) - 标签:设计、字典树、数组、哈希表、字符串 - 难度:困难 ## 题目链接 - [0745. 前缀和后缀搜索 - 力扣](https://leetcode.cn/problems/prefix-and-suffix-search/) ## 题目大意 **要求**: 设计一个包含一些单词的特殊词典,并能够通过前缀和后缀来检索单词。 实现 WordFilter 类: - `WordFilter(string[] words)`:使用词典中的单词 $words$ 初始化对象。 - `f(string pref, string suff)`:返回词典中具有前缀 $pref$ 和后缀 $suff$ 的单词的下标。如果存在不止一个满足要求的下标,返回其中「最大的下标」。如果不存在这样的单词,返回 $-1$。 **说明**: - $1 \le words.length \le 10^{4}$。 - $1 \le words[i].length \le 7$。 - $1 \le pref.length, suff.length \le 7$。 - $words[i]$、$pref$ 和 $suff$ 仅由小写英文字母组成。 - 最多对函数 $f$ 执行 $10^{4}$ 次调用。 **示例**: - 示例 1: ```python 输入 ["WordFilter", "f"] [[["apple"]], ["a", "e"]] 输出 [null, 0] 解释 WordFilter wordFilter = new WordFilter(["apple"]); wordFilter.f("a", "e"); // 返回 0 ,因为下标为 0 的单词:前缀 prefix = "a" 且 后缀 suffix = "e" 。 ``` ## 解题思路 ### 思路 1:字典树(Trie) 使用字典树存储所有单词,同时为每个节点存储前缀和后缀的组合。 **实现步骤**: 1. 对于每个单词 $word$,将所有可能的 `{suffix}#{word}` 形式插入字典树。 - 例如,对于单词 `"apple"`,插入 `"e#apple"`, `"le#apple"`, `"ple#apple"`, `"pple#apple"`, `"apple#apple"`, `"#apple"`。 2. 查询时,将 `pref` 和 `suff` 组合成 `{suff}#{pref}`,在字典树中查找。 3. 每个节点存储经过该节点的最大索引。 ### 思路 1:代码 ```python class TrieNode: def __init__(self): self.children = {} self.weight = -1 # 存储最大索引 class WordFilter: def __init__(self, words: List[str]): self.root = TrieNode() # 对于每个单词,插入所有可能的 {suffix}#{word} 形式 for index, word in enumerate(words): word_len = len(word) # 遍历所有后缀(包括空后缀) for i in range(word_len + 1): suffix = word[i:] # 插入 suffix#word key = suffix + '#' + word node = self.root for char in key: if char not in node.children: node.children[char] = TrieNode() node = node.children[char] node.weight = index # 更新最大索引 def f(self, pref: str, suff: str) -> int: # 查找 suff#pref key = suff + '#' + pref node = self.root for char in key: if char not in node.children: return -1 node = node.children[char] return node.weight # Your WordFilter object will be instantiated and called as such: # obj = WordFilter(words) # param_1 = obj.f(pref,suff) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - 初始化:$O(n \times L^2)$,其中 $n$ 是单词数量,$L$ 是单词的平均长度。 - 查询:$O(L)$,$L$ 是前缀和后缀的长度之和。 - **空间复杂度**:$O(n \times L^2)$,字典树的空间。 ================================================ FILE: docs/solutions/0700-0799/preimage-size-of-factorial-zeroes-function.md ================================================ # [0793. 阶乘函数后 K 个零](https://leetcode.cn/problems/preimage-size-of-factorial-zeroes-function/) - 标签:数学、二分查找 - 难度:困难 ## 题目链接 - [0793. 阶乘函数后 K 个零 - 力扣](https://leetcode.cn/problems/preimage-size-of-factorial-zeroes-function/) ## 题目大意 **描述**: $f(x)$ 是 $x!$ 末尾是 $0$ 的数量。回想一下 $x! = 1 \times 2 \times 3 \times ... \times x$,且 $0! = 1$。 - 例如,$f(3) = 0$,因为 $3! = 6$ 的末尾没有 $0$;而 $f(11) = 2$,因为 $11! = 39916800$ 末端有 $2$ 个 $0$。 给定 k。 **要求**: 找出返回能满足 $f(x) = k$ 的非负整数 $x$ 的数量。 **说明**: - $0 \le k \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:k = 0 输出:5 解释:0!, 1!, 2!, 3!, 和 4! 均符合 k = 0 的条件。 ``` - 示例 2: ```python 输入:k = 5 输出:0 解释:没有匹配到这样的 x!,符合 k = 5 的条件。 ``` ## 解题思路 ### 思路 1:二分查找 $n!$ 末尾 $0$ 的个数取决于因子中 $5$ 的个数(因为 $2$ 的个数总是比 $5$ 多)。设 $f(x)$ 表示 $x!$ 末尾 $0$ 的个数,则: $$f(x) = \lfloor \frac{x}{5} \rfloor + \lfloor \frac{x}{25} \rfloor + \lfloor \frac{x}{125} \rfloor + ...$$ **性质**: - $f(x)$ 是单调非递减的。 - 对于某个 $k$,要么不存在 $x$ 使得 $f(x) = k$(返回 $0$),要么存在连续的 $5$ 个 $x$ 使得 $f(x) = k$(返回 $5$)。 **原因**: - 当 $x$ 不是 $5$ 的倍数时,$f(x) = f(x-1)$。 - 当 $x$ 是 $5$ 的倍数但不是 $25$ 的倍数时,$f(x) = f(x-1) + 1$。 - 因此,$f(x)$ 每次增加至少 $1$,且连续 $5$ 个数中至少有一个是 $5$ 的倍数。 ### 思路 1:代码 ```python class Solution: def preimageSizeFZF(self, k: int) -> int: def count_zeros(x): """计算 x! 末尾 0 的个数""" count = 0 while x > 0: x //= 5 count += x return count def binary_search(k): """二分查找最小的 x 使得 f(x) >= k""" left, right = 0, 5 * (k + 1) while left < right: mid = (left + right) // 2 if count_zeros(mid) < k: left = mid + 1 else: right = mid return left # 找到最小的 x 使得 f(x) = k x = binary_search(k) # 如果 f(x) = k,则存在 5 个这样的 x if count_zeros(x) == k: return 5 else: return 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log^2 k)$,二分查找的时间复杂度为 $O(\log k)$,每次计算 $f(x)$ 的时间复杂度为 $O(\log k)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0700-0799/prime-number-of-set-bits-in-binary-representation.md ================================================ # [0762. 二进制表示中质数个计算置位](https://leetcode.cn/problems/prime-number-of-set-bits-in-binary-representation/) - 标签:位运算、数学 - 难度:简单 ## 题目链接 - [0762. 二进制表示中质数个计算置位 - 力扣](https://leetcode.cn/problems/prime-number-of-set-bits-in-binary-representation/) ## 题目大意 **描述**: 给定两个整数 $left$ 和 $right$。 **要求**: 在闭区间 $[left, right]$ 范围内,统计并返回「计算置位位数为质数」的整数个数。 **说明**: - 「计算置位位数」就是二进制表示中 $1$ 的个数。 - 例如,$21$ 的二进制表示 $10101$ 有 $3$ 个计算置位。 - $1 \le left \le right \le 10^{6}$。 - $0 \le right - left \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:left = 6, right = 10 输出:4 解释: 6 -> 110 (2 个计算置位,2 是质数) 7 -> 111 (3 个计算置位,3 是质数) 9 -> 1001 (2 个计算置位,2 是质数) 10-> 1010 (2 个计算置位,2 是质数) 共计 4 个计算置位为质数的数字。 ``` - 示例 2: ```python 输入:left = 10, right = 15 输出:5 解释: 10 -> 1010 (2 个计算置位, 2 是质数) 11 -> 1011 (3 个计算置位, 3 是质数) 12 -> 1100 (2 个计算置位, 2 是质数) 13 -> 1101 (3 个计算置位, 3 是质数) 14 -> 1110 (3 个计算置位, 3 是质数) 15 -> 1111 (4 个计算置位, 4 不是质数) 共计 5 个计算置位为质数的数字。 ``` ## 解题思路 ### 思路 1:位运算 + 质数判断 计算置位位数就是计算二进制表示中 $1$ 的个数。我们需要统计范围内有多少个数的二进制表示中 $1$ 的个数是质数。 **实现步骤**: 1. 由于 $right \le 10^6 < 2^{20}$,所以二进制表示最多有 $20$ 位,$1$ 的个数最多为 $20$。 2. 预先计算 $20$ 以内的质数:$2, 3, 5, 7, 11, 13, 17, 19$。 3. 遍历 $[left, right]$ 范围内的每个数字: - 使用 `bin(num).count('1')` 或位运算计算 $1$ 的个数。 - 判断 $1$ 的个数是否是质数。 4. 统计满足条件的数字个数。 ### 思路 1:代码 ```python class Solution: def countPrimeSetBits(self, left: int, right: int) -> int: # 20 以内的质数集合 primes = {2, 3, 5, 7, 11, 13, 17, 19} count = 0 for num in range(left, right + 1): # 计算二进制表示中 1 的个数 set_bits = bin(num).count('1') # 判断是否是质数 if set_bits in primes: count += 1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O((right - left) \times \log(right))$,其中 $\log(right)$ 是计算二进制位数的时间。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0700-0799/pyramid-transition-matrix.md ================================================ # [0756. 金字塔转换矩阵](https://leetcode.cn/problems/pyramid-transition-matrix/) - 标签:位运算、深度优先搜索、广度优先搜索、哈希表、字符串 - 难度:中等 ## 题目链接 - [0756. 金字塔转换矩阵 - 力扣](https://leetcode.cn/problems/pyramid-transition-matrix/) ## 题目大意 **描述**: 你正在把积木堆成金字塔。每个块都有一个颜色,用一个字母表示。每一行的块比它下面的行 少一个块 ,并且居中。 为了使金字塔美观,只有特定的「三角形图案」是允许的。一个三角形的图案由「两个块」和叠在上面的「单个块」组成。模式是以三个字母字符串的列表形式 $allowed$ 给出的,其中模式的前两个字符分别表示左右底部块,第三个字符表示顶部块。 - 例如,`"ABC"` 表示一个三角形图案,其中一个 `"C"` 块堆叠在一个 `"A"` 块(左)和一个 `"B"` 块(右)之上。请注意,这与 `"BAC"` 不同,`"B"` 在左下角,`"A"` 在右下角。 **要求**: 你从作为单个字符串给出的底部的一排积木 $bottom$ 开始,必须「将其作为金字塔的底部」。 在给定 $bottom$ 和 $allowed$ 的情况下,如果你能一直构建到金字塔顶部,使金字塔中的 每个三角形图案 都是在 $allowed$ 中的,则返回 true,否则返回 false。 **说明**: - $2 \le bottom.length \le 6$。 - $0 \le allowed.length \le 216$。 - $allowed[i].length == 3$。 - 所有输入字符串中的字母来自集合 `{'A', 'B', 'C', 'D', 'E', 'F'}`。 - $allowed$ 中所有值都是唯一的。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/08/26/pyramid1-grid.jpg) ```python 输入:bottom = "BCD", allowed = ["BCC","CDE","CEA","FFF"] 输出:true 解释:允许的三角形图案显示在右边。 从最底层(第 3 层)开始,我们可以在第 2 层构建“CE”,然后在第 1 层构建“E”。 金字塔中有三种三角形图案,分别是 “BCC”、“CDE” 和 “CEA”。都是允许的。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/08/26/pyramid2-grid.jpg) ```python 输入:bottom = "AAAA", allowed = ["AAB","AAC","BCD","BBE","DEF"] 输出:false 解释:允许的三角形图案显示在右边。 从最底层(即第 4 层)开始,创造第 3 层有多种方法,但如果尝试所有可能性,你便会在创造第 1 层前陷入困境。 ``` ## 解题思路 ### 思路 1:DFS + 哈希表 使用深度优先搜索(DFS)和哈希表来构建金字塔。 **实现步骤**: 1. 将 $allowed$ 转换为哈希表,键为底部两个字符,值为可能的顶部字符列表。 2. 使用 DFS 递归构建金字塔: - 如果当前层只有一个字符,返回 `True`。 - 否则,尝试所有可能的下一层组合。 3. 对于当前层的每个相邻字符对,查找可能的顶部字符。 4. 递归检查是否能构建到顶部。 ### 思路 1:代码 ```python class Solution: def pyramidTransition(self, bottom: str, allowed: List[str]) -> bool: from collections import defaultdict # 构建哈希表:底部两个字符 -> 可能的顶部字符列表 mapping = defaultdict(list) for pattern in allowed: mapping[pattern[:2]].append(pattern[2]) def dfs(current): """递归构建金字塔""" # 如果当前层只有一个字符,成功 if len(current) == 1: return True # 尝试构建下一层 return build_next_layer(current, 0, []) def build_next_layer(current, index, next_layer): """构建下一层""" # 如果已经构建完下一层,递归检查 if index == len(current) - 1: return dfs(''.join(next_layer)) # 尝试当前位置的所有可能字符 base = current[index:index+2] for char in mapping[base]: next_layer.append(char) if build_next_layer(current, index + 1, next_layer): return True next_layer.pop() return False return dfs(bottom) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(A^n)$,其中 $A$ 是字母表大小,$n$ 是 $bottom$ 的长度。最坏情况下需要尝试所有可能的组合。 - **空间复杂度**:$O(n^2)$,递归栈的深度为 $O(n)$,每层需要 $O(n)$ 空间。 ================================================ FILE: docs/solutions/0700-0799/rabbits-in-forest.md ================================================ # [0781. 森林中的兔子](https://leetcode.cn/problems/rabbits-in-forest/) - 标签:贪心、数组、哈希表、数学 - 难度:中等 ## 题目链接 - [0781. 森林中的兔子 - 力扣](https://leetcode.cn/problems/rabbits-in-forest/) ## 题目大意 **描述**: 森林中有未知数量的兔子。提问其中若干只兔子 `"还有多少只兔子与你(指被提问的兔子)颜色相同?"`,将答案收集到一个整数数组 $answers$ 中,其中 $answers[i]$ 是第 $i$ 只兔子的回答。 给定数组 $answers$。 **要求**: 返回森林中兔子的最少数量。 **说明**: - $1 \le answers.length \le 10^{3}$。 - $0 \le answers[i] \lt 10^{3}$。 **示例**: - 示例 1: ```python 输入:answers = [1,1,2] 输出:5 解释: 两只回答了 "1" 的兔子可能有相同的颜色,设为红色。 之后回答了 "2" 的兔子不会是红色,否则他们的回答会相互矛盾。 设回答了 "2" 的兔子为蓝色。 此外,森林中还应有另外 2 只蓝色兔子的回答没有包含在数组中。 因此森林中兔子的最少数量是 5 只:3 只回答的和 2 只没有回答的。 ``` - 示例 2: ```python 输入:answers = [10,10,10] 输出:11 ``` ## 解题思路 ### 思路 1:贪心 + 哈希表 如果一只兔子回答 $x$,说明包括它自己在内,有 $x + 1$ 只相同颜色的兔子。为了使兔子总数最少,我们应该让回答相同的兔子尽可能属于同一组。 **实现步骤**: 1. 使用哈希表统计每个回答出现的次数。 2. 对于回答 $x$ 的兔子: - 每 $x + 1$ 只回答 $x$ 的兔子可以是同一颜色。 - 如果有 $count$ 只兔子回答 $x$,则至少需要 $\lceil \frac{count}{x + 1} \rceil$ 组,每组有 $x + 1$ 只兔子。 - 总数为 $\lceil \frac{count}{x + 1} \rceil \times (x + 1)$。 3. 将所有颜色的兔子数量相加。 ### 思路 1:代码 ```python class Solution: def numRabbits(self, answers: List[int]) -> int: from collections import Counter # 统计每个回答出现的次数 count = Counter(answers) total = 0 for x, cnt in count.items(): # 每组有 x + 1 只兔子 group_size = x + 1 # 需要的组数(向上取整) groups = (cnt + group_size - 1) // group_size # 总兔子数 total += groups * group_size return total ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是 $answers$ 的长度。 - **空间复杂度**:$O(n)$,哈希表的空间。 ================================================ FILE: docs/solutions/0700-0799/random-pick-with-blacklist.md ================================================ # [0710. 黑名单中的随机数](https://leetcode.cn/problems/random-pick-with-blacklist/) - 标签:数组、哈希表、数学、二分查找、排序、随机化 - 难度:困难 ## 题目链接 - [0710. 黑名单中的随机数 - 力扣](https://leetcode.cn/problems/random-pick-with-blacklist/) ## 题目大意 **描述**: 给定一个整数 $n$ 和一个「无重复」黑名单整数数组 $blacklist$。设计一种算法,从 $[0, n - 1]$ 范围内的任意整数中选取一个「未加入」黑名单 $blacklist$ 的整数。任何在上述范围内且不在黑名单 $blacklist$ 中的整数都应该有「同等的可能性」被返回。 优化你的算法,使它最小化调用语言「内置」随机函数的次数。 **要求**: 实现 Solution 类: - `Solution(int n, int[] blacklist)` 初始化整数 $n$ 和被加入黑名单 $blacklist$ 的整数。 - `int pick()` 返回一个范围为 $[0, n - 1]$ 且不在黑名单 $blacklist$ 中的随机整数。 **说明**: - $1 \le n \le 10^{9}$。 - $0 \le blacklist.length \le min(10^{5}, n - 1)$。 - $0 \le blacklist[i] \lt n$。 - $blacklist$ 中所有值都不同。 - $pick$ 最多被调用 $2 \times 10^{4}$ 次。 **示例**: - 示例 1: ```python 输入 ["Solution", "pick", "pick", "pick", "pick", "pick", "pick", "pick"] [[7, [2, 3, 5]], [], [], [], [], [], [], []] 输出 [null, 0, 4, 1, 6, 1, 0, 4] 解释 Solution solution = new Solution(7, [2, 3, 5]); solution.pick(); // 返回0,任何[0,1,4,6]的整数都可以。注意,对于每一个pick的调用, // 0、1、4和6的返回概率必须相等(即概率为1/4)。 solution.pick(); // 返回 4 solution.pick(); // 返回 1 solution.pick(); // 返回 6 solution.pick(); // 返回 1 solution.pick(); // 返回 0 solution.pick(); // 返回 4 ``` ## 解题思路 ### 思路 1:哈希表 + 随机映射 将黑名单中的数字映射到 $[n - len(blacklist), n)$ 范围内的白名单数字。 **实现步骤**: 1. 计算白名单的大小:$white\_count = n - len(blacklist)$。 2. 将黑名单中 $\ge white\_count$ 的数字放入集合 $black\_set$。 3. 对于黑名单中 $< white\_count$ 的数字,将其映射到 $[white\_count, n)$ 范围内不在黑名单中的数字。 4. 随机生成 $[0, white\_count)$ 范围内的数字: - 如果在映射表中,返回映射后的值。 - 否则,直接返回该数字。 ### 思路 1:代码 ```python class Solution: def __init__(self, n: int, blacklist: List[int]): import random self.random = random # 白名单的大小 self.white_count = n - len(blacklist) # 黑名单中 >= white_count 的数字 black_set = set() for b in blacklist: if b >= self.white_count: black_set.add(b) # 映射表:将黑名单中 < white_count 的数字映射到 [white_count, n) 范围内的白名单数字 self.mapping = {} white = self.white_count for b in blacklist: if b < self.white_count: # 找到下一个不在黑名单中的数字 while white in black_set or white in blacklist: white += 1 self.mapping[b] = white white += 1 def pick(self) -> int: # 随机生成 [0, white_count) 范围内的数字 rand = self.random.randint(0, self.white_count - 1) # 如果在映射表中,返回映射后的值 return self.mapping.get(rand, rand) # Your Solution object will be instantiated and called as such: # obj = Solution(n, blacklist) # param_1 = obj.pick() ``` ### 思路 1:复杂度分析 - **时间复杂度**: - 初始化:$O(B)$,其中 $B$ 是黑名单的长度。 - 查询:$O(1)$。 - **空间复杂度**:$O(B)$,映射表的空间。 ================================================ FILE: docs/solutions/0700-0799/range-module.md ================================================ # [0715. Range 模块](https://leetcode.cn/problems/range-module/) - 标签:设计、线段树、有序集合 - 难度:困难 ## 题目链接 - [0715. Range 模块 - 力扣](https://leetcode.cn/problems/range-module/) ## 题目大意 **描述**:`Range` 模块是跟踪数字范围的模块。 **要求**: - 设计一个数据结构来跟踪查询半开区间 `[left, right)` 内的数字是否被跟踪。 - 实现 `RangeModule` 类: - `RangeModule()` 初始化数据结构的对象。 - `void addRange(int left, int right)` 添加半开区间 `[left, right)`,跟踪该区间中的每个实数。添加与当前跟踪的数字部分重叠的区间时,应当添加在区间 `[left, right)` 中尚未跟踪的任何数字到该区间中。 - `boolean queryRange(int left, int right)` 只有在当前正在跟踪区间 `[left, right)` 中的每一个实数时,才返回 `True` ,否则返回 `False`。 - `void removeRange(int left, int right)` 停止跟踪半开区间 `[left, right)` 中当前正在跟踪的每个实数。 **说明**: - $1 \le left < right \le 10^9$。 **示例**: - 示例 1: ``` rangeModule = RangeModule() -> null rangeModule.addRange(10, 20) -> null rangeModule.removeRange(14, 16) -> null rangeModule.queryRange(10, 14) -> True rangeModule.queryRange(13, 15) -> False rangeModule.queryRange(16, 17) -> True ``` ## 解题思路 ### 思路 1:线段树 这道题可以使用线段树来做,但是效率比较差。 区间的范围是 $[0, 10^9]$,普通数组构成的线段树不满足要求。需要用到动态开点线段树。题目要求的是半开区间 `[left, right)` ,而线段树中常用的是闭合区间。但是我们可以将半开区间 `[left, right)` 转为 `[left, right - 1]` 的闭合空间。 这样构建线段树的时间复杂度为 $O(\log n)$,单次区间更新的时间复杂度为 $O(\log n)$,单次区间查询的时间复杂度为 $O(\log n)$。总体时间复杂度为 $O(\log n)$。 ## 代码 ### 思路 1 代码: ```python # 线段树的节点类 class TreeNode: def __init__(self, left, right, val=False, lazy_tag=None, letNode=None, rightNode=None): self.left = left # 区间左边界 self.right = right # 区间右边界 self.mid = (left + right) >> 1 self.leftNode = letNode # 区间左节点 self.rightNode = rightNode # 区间右节点 self.val = val # 节点值(区间值) self.lazy_tag = lazy_tag # 区间问题的延迟更新标记 class RangeModule: def __init__(self): self.tree = TreeNode(0, int(1e9)) # 向上更新 node 节点区间值,节点的区间值等于该节点左右子节点元素值的聚合计算结果 def __pushup(self, node): if node.leftNode and node.rightNode: node.val = node.leftNode.val and node.rightNode.val else: node.val = False # 向下更新 node 节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, node): if not node.leftNode: node.leftNode = TreeNode(node.left, node.mid) if not node.rightNode: node.rightNode = TreeNode(node.mid + 1, node.right) if node.lazy_tag is not None: node.leftNode.lazy_tag = node.lazy_tag # 更新左子节点懒惰标记 node.leftNode.val = node.lazy_tag # 左子节点每个元素值增加 lazy_tag node.rightNode.lazy_tag = node.lazy_tag # 更新右子节点懒惰标记 node.rightNode.val = node.lazy_tag # 右子节点每个元素值增加 lazy_tag node.lazy_tag = None # 更新当前节点的懒惰标记 # 区间更新 def __update_interval(self, q_left, q_right, val, node): if q_left <= node.left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 node.lazy_tag = val # 将当前节点的延迟标记增加 val node.val = val # 当前节点所在区间每个元素值增加 val return self.__pushdown(node) if q_left <= node.mid: self.__update_interval(q_left, q_right, val, node.leftNode) if q_right > node.mid: self.__update_interval(q_left, q_right, val, node.rightNode) self.__pushup(node) # 区间查询,在线段树的 [left, right] 区间范围中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, node): if q_left <= node.left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return node.val # 直接返回节点值 # 需要向下更新节点所在区间的左右子节点的值和懒惰标记 self.__pushdown(node) if q_right <= node.mid: return self.__query_interval(q_left, q_right, node.leftNode) if q_left > node.mid: return self.__query_interval(q_left, q_right, node.rightNode) return self.__query_interval(q_left, q_right, node.leftNode) and self.__query_interval(q_left, q_right, node.rightNode) # 返回左右子树元素值的聚合计算结果 def addRange(self, left: int, right: int) -> None: self.__update_interval(left, right - 1, True, self.tree) def queryRange(self, left: int, right: int) -> bool: return self.__query_interval(left, right - 1, self.tree) def removeRange(self, left: int, right: int) -> None: self.__update_interval(left, right - 1, False, self.tree) ``` ================================================ FILE: docs/solutions/0700-0799/reach-a-number.md ================================================ # [0754. 到达终点数字](https://leetcode.cn/problems/reach-a-number/) - 标签:数学、二分查找 - 难度:中等 ## 题目链接 - [0754. 到达终点数字 - 力扣](https://leetcode.cn/problems/reach-a-number/) ## 题目大意 **描述**: 在一根无限长的数轴上,你站在 $0$ 的位置。终点在 $target$ 的位置。 你可以做一些数量的移动 $numMoves$: - 每次你可以选择向左或向右移动。 - 第 $i$ 次移动(从 $i == 1$ 开始,到 $i == numMoves$),在选择的方向上走 $i$ 步。 给定整数 $target$。 **要求**: 返回 到达目标所需的 最小 移动次数(即最小 $numMoves$ ) 。 **说明**: - $-10^{9} \le target \le 10^{9}$。 - target != 0。 **示例**: - 示例 1: ```python 示例 1: 输入: target = 2 输出: 3 解释: 第一次移动,从 0 到 1 。 第二次移动,从 1 到 -1 。 第三次移动,从 -1 到 2 。 ``` - 示例 2: ```python 输入: 输出: ``` ## 解题思路 ### 思路 1:数学 在数轴上,从 $0$ 开始,第 $i$ 步可以向左或向右移动 $i$ 步。要到达 $target$,需要找到最小的移动次数。 **分析**: - 如果一直向右移动,第 $n$ 步后位置为 $1 + 2 + ... + n = \frac{n(n+1)}{2}$。 - 如果某些步向左移动,相当于从总和中减去这些步的两倍。 - 设向右移动的步为正,向左移动的步为负,则:$sum - 2 \times neg = target$。 - 即:$neg = \frac{sum - target}{2}$。 **步骤**: 1. 由于对称性,可以只考虑 $target$ 的绝对值。 2. 找到最小的 $n$,使得 $sum = \frac{n(n+1)}{2} \ge |target|$。 3. 如果 $sum - |target|$ 是偶数,则可以通过翻转某些步到达 $target$,返回 $n$。 4. 否则,继续增加步数,直到差值为偶数。 ### 思路 1:代码 ```python class Solution: def reachNumber(self, target: int) -> int: # 由于对称性,只考虑绝对值 target = abs(target) n = 0 sum_n = 0 # 找到最小的 n,使得 sum >= target while sum_n < target: n += 1 sum_n += n # 如果差值是偶数,可以直接到达 diff = sum_n - target if diff % 2 == 0: return n # 否则,继续增加步数 # 如果 n 是奇数,再走 2 步(n+1 和 n+2),差值增加 2n+3(奇数) # 如果 n 是偶数,再走 1 步(n+1),差值增加 n+1(奇数) # 总之,最多再走 2 步就能使差值为偶数 while diff % 2 != 0: n += 1 sum_n += n diff = sum_n - target return n ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\sqrt{target})$,需要找到 $n$ 使得 $\frac{n(n+1)}{2} \ge target$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0700-0799/reaching-points.md ================================================ # [0780. 到达终点](https://leetcode.cn/problems/reaching-points/) - 标签:数学 - 难度:困难 ## 题目链接 - [0780. 到达终点 - 力扣](https://leetcode.cn/problems/reaching-points/) ## 题目大意 **描述**: 给定四个整数 $sx$,$sy$,$tx$ 和 $ty$。 **要求**: 如果通过一系列的转换可以从起点 $(sx, sy)$ 到达终点 $(tx, ty)$,则返回 true,否则返回 false。 从点 $(x, y)$ 可以转换到 $(x, x+y)$ 或者 $(x+y, y)$。 **说明**: - $1 \le sx, sy, tx, ty \le 10^{9}$。 **示例**: - 示例 1: ```python 输入: sx = 1, sy = 1, tx = 3, ty = 5 输出: true 解释: 可以通过以下一系列转换从起点转换到终点: (1, 1) -> (1, 2) (1, 2) -> (3, 2) (3, 2) -> (3, 5) ``` - 示例 2: ```python 输入: sx = 1, sy = 1, tx = 2, ty = 2 输出: false ``` ## 解题思路 ### 思路 1:数学 从起点 $(sx, sy)$ 到终点 $(tx, ty)$,每次可以将 $(x, y)$ 转换为 $(x, x+y)$ 或 $(x+y, y)$。我们可以反向思考:从 $(tx, ty)$ 逆推到 $(sx, sy)$。 **逆向操作**: - 如果 $tx > ty$,则上一步是 $(tx - ty, ty)$。 - 如果 $ty > tx$,则上一步是 $(tx, ty - tx)$。 **优化**: - 当 $tx \gg ty$ 时,可以一次性减去多个 $ty$:$tx = tx \% ty$(如果 $ty > sy$)。 - 否则,需要逐步减去,确保不会跳过 $(sx, sy)$。 ### 思路 1:代码 ```python class Solution: def reachingPoints(self, sx: int, sy: int, tx: int, ty: int) -> bool: # 从终点逆推到起点 while tx >= sx and ty >= sy: if tx == sx and ty == sy: return True if tx > ty: # 上一步是 (tx - ty, ty) if ty == sy: # 检查是否能通过减去若干个 ty 到达 sx return (tx - sx) % ty == 0 # 一次性减去多个 ty tx %= ty else: # 上一步是 (tx, ty - tx) if tx == sx: # 检查是否能通过减去若干个 tx 到达 sy return (ty - sy) % tx == 0 # 一次性减去多个 tx ty %= tx return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log(\max(tx, ty)))$,类似辗转相除法。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0700-0799/remove-comments.md ================================================ # [0722. 删除注释](https://leetcode.cn/problems/remove-comments/) - 标签:数组、字符串 - 难度:中等 ## 题目链接 - [0722. 删除注释 - 力扣](https://leetcode.cn/problems/remove-comments/) ## 题目大意 **描述**: 给一个 C++ 程序,删除程序中的注释。这个程序 $source$ 是一个数组,其中 $source[i]$ 表示第 $i$ 行源码。 这表示每行源码由 `'\n'` 分隔。 在 C++ 中有两种注释风格,行内注释和块注释。 - 字符串 `//` 表示行注释,表示//和其右侧的其余字符应该被忽略。 - 字符串 `/*` 表示一个块注释,它表示直到下一个(非重叠)出现的 `*/` 之间的所有字符都应该被忽略。(阅读顺序为从左到右)非重叠是指,字符串 `/*/` 并没有结束块注释,因为注释的结尾与开头相重叠。 第一个有效注释优先于其他注释。 - 如果字符串 `//` 出现在块注释中会被忽略。 - 同样,如果字符串 `/*` 出现在行或块注释中也会被忽略。 如果一行在删除注释之后变为空字符串,那么不要输出该行。即,答案列表中的每个字符串都是非空的。 样例中没有控制字符,单引号或双引号字符。 - 比如,`source = $string$ $s$ = "/* Not a $comment$. */";" 不会出现在测试样例里。 此外,没有其他内容(如定义或宏)会干扰注释。 我们保证每一个块注释最终都会被闭合, 所以在行或块注释之外的/*总是开始新的注释。 最后,隐式换行符可以通过块注释删除。 有关详细信息,请参阅下面的示例。 从源代码中删除注释后,需要以相同的格式返回源代码。 **要求**: 从源代码中删除注释,以相同的格式返回源代码。 **说明**: - $1 \le source.length \le 10^{3}$。 - $0 \le source[i].length \le 80$。 - $source[i]$ 由可打印的 ASCII 字符组成。 - 每个块注释都会被闭合。 - 给定的源码中不会有单引号、双引号或其他控制字符。 **示例**: - 示例 1: ```python 输入: source = ["/*Test program */", "int main()", "{ ", " // variable declaration ", "int a, b, c;", "/* This is a test", " multiline ", " comment for ", " testing */", "a = b + c;", "}"] 输出: ["int main()","{ "," ","int a, b, c;","a = b + c;","}"] 解释: 示例代码可以编排成这样: /*Test program */ int main() { // variable declaration int a, b, c; /* This is a test multiline comment for testing */ a = b + c; } 第 1 行和第 6-9 行的字符串 /* 表示块注释。第 4 行的字符串 // 表示行注释。 编排后: int main() { int a, b, c; a = b + c; } ``` - 示例 2: ```python 输入: source = ["a/*comment", "line", "more_comment*/b"] 输出: ["ab"] 解释: 原始的 source 字符串是 "a/*comment\nline\nmore_comment*/b", 其中我们用粗体显示了换行符。删除注释后,隐含的换行符被删除,留下字符串 "ab" 用换行符分隔成数组时就是 ["ab"]. ``` ## 解题思路 ### 思路 1:模拟 删除注释需要处理两种注释:行注释 `//` 和块注释 `/* */`。 **实现步骤**: 1. 使用一个标志 `in_block` 表示当前是否在块注释中。 2. 遍历每一行的每个字符: - 如果在块注释中,查找 `*/` 结束块注释。 - 如果不在块注释中: - 遇到 `/*`,进入块注释。 - 遇到 `//`,忽略该行剩余部分。 - 否则,将字符加入当前行。 3. 如果当前行不为空且不在块注释中,将其加入结果。 ### 思路 1:代码 ```python class Solution: def removeComments(self, source: List[str]) -> List[str]: result = [] in_block = False # 是否在块注释中 current_line = [] # 当前行的内容 for line in source: i = 0 while i < len(line): if in_block: # 在块注释中,查找 */ if i + 1 < len(line) and line[i:i+2] == '*/': in_block = False i += 2 else: i += 1 else: # 不在块注释中 if i + 1 < len(line) and line[i:i+2] == '/*': # 进入块注释 in_block = True i += 2 elif i + 1 < len(line) and line[i:i+2] == '//': # 行注释,忽略该行剩余部分 break else: # 普通字符 current_line.append(line[i]) i += 1 # 如果不在块注释中且当前行不为空,加入结果 if not in_block and current_line: result.append(''.join(current_line)) current_line = [] return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是源代码的行数,$m$ 是每行的平均长度。 - **空间复杂度**:$O(n \times m)$,存储结果的空间。 ================================================ FILE: docs/solutions/0700-0799/reorganize-string.md ================================================ # [0767. 重构字符串](https://leetcode.cn/problems/reorganize-string/) - 标签:贪心、哈希表、字符串、计数、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [0767. 重构字符串 - 力扣](https://leetcode.cn/problems/reorganize-string/) ## 题目大意 **描述**: 给定一个字符串 $s$。 **要求**: 检查是否能重新排布其中的字母,使得两相邻的字符不同。 返回 $s$ 的任意可能的重新排列。若不可行,返回空字符串 `""`。 **说明**: - $1 \le s.length \le 500$。 - $s$ 只包含小写字母。 **示例**: - 示例 1: ```python 输入: s = "aab" 输出: "aba" ``` - 示例 2: ```python 输入: s = "aaab" 输出: "" ``` ## 解题思路 ### 思路 1:贪心 + 堆 要使相邻字符不同,我们应该优先放置出现次数最多的字符。使用最大堆来维护字符的出现次数。 **实现步骤**: 1. 统计每个字符的出现次数。 2. 如果某个字符的出现次数超过 $\lceil \frac{n}{2} \rceil$,则无法重排,返回空字符串。 3. 使用最大堆,每次取出出现次数最多的字符: - 如果结果字符串为空或最后一个字符与当前字符不同,直接添加。 - 否则,取出次多的字符添加,然后将之前的字符放回堆中。 4. 重复直到所有字符都被添加。 ### 思路 1:代码 ```python class Solution: def reorganizeString(self, s: str) -> str: from collections import Counter import heapq # 统计字符出现次数 count = Counter(s) n = len(s) # 如果某个字符出现次数超过 (n + 1) // 2,无法重排 if max(count.values()) > (n + 1) // 2: return "" # 使用最大堆(Python 的 heapq 是最小堆,所以用负数) heap = [(-cnt, char) for char, cnt in count.items()] heapq.heapify(heap) result = [] while heap: # 取出出现次数最多的字符 first_cnt, first_char = heapq.heappop(heap) # 如果结果为空或最后一个字符与当前字符不同 if not result or result[-1] != first_char: result.append(first_char) # 如果还有剩余,放回堆中 if first_cnt + 1 < 0: heapq.heappush(heap, (first_cnt + 1, first_char)) else: # 需要取次多的字符 if not heap: return "" second_cnt, second_char = heapq.heappop(heap) result.append(second_char) # 放回第一个字符 heapq.heappush(heap, (first_cnt, first_char)) # 如果第二个字符还有剩余,放回堆中 if second_cnt + 1 < 0: heapq.heappush(heap, (second_cnt + 1, second_char)) return ''.join(result) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log k)$,其中 $n$ 是字符串长度,$k$ 是不同字符的个数(最多 $26$)。 - **空间复杂度**:$O(k)$,堆的空间。 ================================================ FILE: docs/solutions/0700-0799/rotate-string.md ================================================ # [0796. 旋转字符串](https://leetcode.cn/problems/rotate-string/) - 标签:字符串、字符串匹配 - 难度:简单 ## 题目链接 - [0796. 旋转字符串 - 力扣](https://leetcode.cn/problems/rotate-string/) ## 题目大意 **描述**:给定两个字符串 `s` 和 `goal`。 **要求**:如果 `s` 在若干次旋转之后,能变为 `goal`,则返回 `True`,否则返回 `False`。 **说明**: - `s` 的旋转操作:将 `s` 最左侧的字符移动到最右边。 - 比如:`s = "abcde"`,在旋转一次之后结果就是 `s = "bcdea"`。 - $1 \le s.length, goal.length \le 100$。 - `s` 和 `goal` 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入: s = "abcde", goal = "cdeab" 输出: true ``` - 示例 2: ```python 输入: s = "abcde", goal = "abced" 输出: false ``` ## 解题思路 ### 思路 1:KMP 算法 其实将两个字符串 `s` 拼接在一起,就包含了所有从 `s` 进行旋转后的字符串。那么我们只需要判断一下 `goal` 是否为 `s + s` 的子串即可。可以用 KMP 算法来做。 1. 先排除掉几种不可能的情况,比如 `s` 为空串的情况,`goal` 为空串的情况,`len(s) != len(goal)` 的情况。 2. 然后使用 KMP 算法计算出 `goal` 在 `s + s` 中的下标位置 `index`(`s + s` 可用取余运算模拟)。 3. 如果 `index == -1`,则说明 `s` 在若干次旋转之后,不能能变为 `goal`,则返回 `False`。 4. 如果 `index != -1`,则说明 `s` 在若干次旋转之后,能变为 `goal`,则返回 `True`。 ### 思路 1:代码 ```python class Solution: def kmp(self, T: str, p: str) -> int: n, m = len(T), len(p) next = self.generateNext(p) i, j = 0, 0 while i - j < n: while j > 0 and T[i % n] != p[j]: j = next[j - 1] if T[i % n] == p[j]: j += 1 if j == m: return i - m + 1 i += 1 return -1 def generateNext(self, p: str): m = len(p) next = [0 for _ in range(m)] left = 0 for right in range(1, m): while left > 0 and p[left] != p[right]: left = next[left - 1] if p[left] == p[right]: left += 1 next[right] = left return next def rotateString(self, s: str, goal: str) -> bool: if not s or not goal or len(s) != len(goal): return False index = self.kmp(s, goal) if index == -1: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中文本串 $s$ 的长度为 $n$,模式串 $goal$ 的长度为 $m$。 - **空间复杂度**:$O(m)$。 ================================================ FILE: docs/solutions/0700-0799/rotated-digits.md ================================================ # [0788. 旋转数字](https://leetcode.cn/problems/rotated-digits/) - 标签:数学、动态规划 - 难度:中等 ## 题目链接 - [0788. 旋转数字 - 力扣](https://leetcode.cn/problems/rotated-digits/) ## 题目大意 **描述**:给定搞一个正整数 $n$。 **要求**:计算从 $1$ 到 $n$ 中有多少个数 $x$ 是好数。 **说明**: - **好数**:如果一个数 $x$ 的每位数字逐个被旋转 180 度之后,我们仍可以得到一个有效的,且和 $x$ 不同的数,则成该数为好数。 - 如果一个数的每位数字被旋转以后仍然还是一个数字, 则这个数是有效的。$0$、$1$ 和 $8$ 被旋转后仍然是它们自己;$2$ 和 $5$ 可以互相旋转成对方(在这种情况下,它们以不同的方向旋转,换句话说,$2$ 和 $5$ 互为镜像);$6$ 和 $9$ 同理,除了这些以外其他的数字旋转以后都不再是有效的数字。 - $n$ 的取值范围是 $[1, 10000]$。 **示例**: - 示例 1: ```python 输入: 10 输出: 4 解释: 在 [1, 10] 中有四个好数: 2, 5, 6, 9。 注意 1 和 10 不是好数, 因为他们在旋转之后不变。 ``` ## 解题思路 ### 思路 1:枚举算法 根据题目描述,一个数满足:数中没有出现 $3$、$4$、$7$,并且至少出现一次 $2$、$5$、$6$ 或 $9$,就是好数。 因此,我们可以枚举 $[1, n]$ 中的每一个正整数 $x$,并判断该正整数 $x$ 的数位中是否满足没有出现 $3$、$4$、$7$,并且至少一次出现了 $2$、$5$、$6$ 或 $9$,如果满足,则该正整数 $x$ 位好数,否则不是好数。 最后统计好数的方案个数并将其返回即可。 ### 思路 1:代码 ```python class Solution: def rotatedDigits(self, n: int) -> int: check = [0, 0, 1, -1, -1, 1, 1, -1, 0, 1] ans = 0 for i in range(1, n + 1): flag = False num = i while num: digit = num % 10 num //= 10 if check[digit] == 1: flag = True elif check[digit] == -1: flag = False break if flag: ans += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(\log n)$。 ### 思路 2:动态规划 + 数位 DP 将 $n$ 转换为字符串 $s$,定义递归函数 `def dfs(pos, hasDiff, isLimit):` 表示构造第 $pos$ 位及之后所有数位的合法方案数。其中: 1. $pos$ 表示当前枚举的数位位置。 2. $hasDiff$ 表示当前是否用到 $2$、$5$、$6$ 或 $9$ 中任何一个数字。 3. $isLimit$ 表示前一位数位是否等于上界,用于限制本次搜索的数位范围。 接下来按照如下步骤进行递归。 1. 从 `dfs(0, False, True)` 开始递归。 `dfs(0, False, True)` 表示: 1. 从位置 $0$ 开始构造。 2. 初始没有用到 $2$、$5$、$6$ 或 $9$ 中任何一个数字。 3. 开始时受到数字 $n$ 对应最高位数位的约束。 2. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时: 1. 如果 $hasDiff == True$,说明当前方案符合要求,则返回方案数 $1$。 2. 如果 $hasDiff == False$,说明当前方案不符合要求,则返回方案数 $0$。 3. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:`ans = 0`。 4. 因为不需要考虑前导 $0$,所以当前所能选择的最小数字 $minX$ 为 $0$。 5. 根据 $isLimit$ 来决定填当前位数位所能选择的最大数字($maxX$)。 6. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $d$。 7. 如果当前数位与之前数位没有出现 $3$、$4$、$7$,则方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, hasDiff or check[d], isLimit and d == maxX)`。 1. `hasDiff or check[d]` 表示当前是否用到 $2$、$5$、$6$ 或 $9$ 中任何一个数字或者没有用到 $3$、$4$、$7$。 2. `isLimit and d == maxX` 表示 $pos + 1$ 位受到之前位限制和 $pos$ 位限制。 8. 最后的方案数为 `dfs(0, False, True)`,将其返回即可。 ### 思路 2:代码 ```python class Solution: def rotatedDigits(self, n: int) -> int: check = [0, 0, 1, -1, -1, 1, 1, -1, 0, 1] # 将 n 转换为字符串 s s = str(n) @cache # pos: 第 pos 个数位 # hasDiff: 之前选过的数字是否包含 2,5,6,9 中至少一个。 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 def dfs(pos, hasDiff, isLimit): if pos == len(s): # isNum 为 True,则表示当前方案符合要求 return int(hasDiff) ans = 0 # 不需要考虑前导 0,则最小可选择数字为 0 minX = 0 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s[pos]) if isLimit else 9 # 枚举可选择的数字 for d in range(minX, maxX + 1): # d 不在选择的数字集合中,即之前没有选择过 d if check[d] != -1: ans += dfs(pos + 1, hasDiff or check[d], isLimit and d == maxX) return ans return dfs(0, False, True) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0700-0799/search-in-a-binary-search-tree.md ================================================ # [0700. 二叉搜索树中的搜索](https://leetcode.cn/problems/search-in-a-binary-search-tree/) - 标签:树、二叉搜索树、二叉树 - 难度:简单 ## 题目链接 - [0700. 二叉搜索树中的搜索 - 力扣](https://leetcode.cn/problems/search-in-a-binary-search-tree/) ## 题目大意 **描述**:给定一个二叉搜索树和一个值 `val`。 **要求**:在二叉搜索树中查找节点值等于 `val` 的节点,并返回该节点。 **说明**: - 数中节点数在 $[1, 5000]$ 范围内。 - $1 \le Node.val \le 10^7$。 - `root` 是二叉搜索树。 - $1 \le val \le 10^7$。 **示例**: - 示例 1: ![img](https://assets.leetcode.com/uploads/2021/01/12/tree1.jpg) ```python 输入:root = [4,2,7,1,3], val = 2 输出:[2,1,3] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/01/12/tree2.jpg) ```python 输入:root = [4,2,7,1,3], val = 5 输出:[] ``` ## 解题思路 ### 思路 1:递归 1. 从根节点 `root` 开始向下递归遍历。 1. 如果 `val` 等于当前节点的值,即 `val == root.val`,则返回 `root`; 2. 如果 `val` 小于当前节点的值 ,即 `val < root.val`,则递归遍历左子树,继续查找; 3. 如果 `val` 大于当前节点的值 ,即 `val > root.val`,则递归遍历右子树,继续查找。 2. 如果遍历到最后也没有找到,则返回空节点。 ### 思路 1:代码 ```python class Solution: def searchBST(self, root: TreeNode, val: int) -> TreeNode: if not root or val == root.val: return root if val < root.val: return self.searchBST(root.left, val) else: return self.searchBST(root.right, val) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉搜索树的节点数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0700-0799/search-in-a-sorted-array-of-unknown-size.md ================================================ # [0702. 搜索长度未知的有序数组](https://leetcode.cn/problems/search-in-a-sorted-array-of-unknown-size/) - 标签:数组、二分查找、交互 - 难度:中等 ## 题目链接 - [0702. 搜索长度未知的有序数组 - 力扣](https://leetcode.cn/problems/search-in-a-sorted-array-of-unknown-size/) ## 题目大意 **描述**:给定一个升序数组 $secret$,但是数组的大小是未知的。我们无法直接访问数组,智能通过 `ArrayReader` 接口去访问他。我们可以通过接口 `reader.get(k)`: 1. 如果数组访问未越界,则返回数组 $secret$ 中第 $k$ 个下标位置的元素值。 2. 如果数组访问越界,则接口返回 $2^{31} - 1$。 现在再给定一个数字 $target$。 **要求**:从 $secret$ 中找出 $secret[k] == target$ 的下标位置 $k$,如果 $secret$ 中不存在 $target$,则返回 $-1$。 **说明**: - $1 \le secret.length \le 10^4$。 - $-10^4 \le secret[i], target \le 10^4$。 - $secret$ 严格递增。 **示例**: - 示例 1: ```python 输入: secret = [-1,0,3,5,9,12], target = 9 输出: 4 解释: 9 存在在 nums 中,下标为 4 ``` - 示例 2: ```python 输入: secret = [-1,0,3,5,9,12], target = 2 输出: -1 解释: 2 不在数组中所以返回 -1 ``` ## 解题思路 ### 思路 1:二分查找算法 这道题的关键点在于找到数组的大小,以便确定查找的右边界位置。右边界可以通过倍增的方式快速查找。在查找右边界的同时,也能将左边界的范围进一步缩小。等确定了左右边界,就可以使用二分查找算法快速查找 $target$。 ### 思路 1:代码 ```python class Solution: def binarySearch(self, reader, left, right, target): while left < right: mid = left + (right - left) // 2 if target > reader.get(mid): left = mid + 1 else: right = mid if reader.get(left) == target: return left else: return -1 def search(self, reader, target): left = 0 right = 1 while reader.get(right) < target: left = right right <<= 1 return self.binarySearch(reader, left, right, target) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$,其中 $n$ 为数组长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0700-0799/self-dividing-numbers.md ================================================ # [0728. 自除数](https://leetcode.cn/problems/self-dividing-numbers/) - 标签:数学 - 难度:简单 ## 题目链接 - [0728. 自除数 - 力扣](https://leetcode.cn/problems/self-dividing-numbers/) ## 题目大意 **描述**: 「自除数」是指可以被它包含的每一位数整除的数。 - 例如,$128$ 是一个自除数,因为 $128 \% 1 == 0, 128 \% 2 == 0, 128 \% 8 == 0$。 「自除数」不允许包含 $0$。 给定两个整数 $left$ 和 $right$。 **要求**: 返回一个列表,列表的元素是范围 $[left, right]$(包括两个端点)内所有的自除数。 **说明**: - $1 \le left \le right \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:left = 1, right = 22 输出:[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 15, 22] ``` - 示例 2: ```python 输入:left = 47, right = 85 输出:[48,55,66,77] ``` ## 解题思路 ### 思路 1:模拟 自除数是指可以被它包含的每一位数整除的数。我们可以遍历范围内的每个数字,检查它是否是自除数。 **实现步骤**: 1. 遍历 $[left, right]$ 范围内的每个数字 $num$。 2. 对于每个数字,检查它的每一位数字: - 如果某一位是 $0$,则不是自除数。 - 如果 $num$ 不能被某一位整除,则不是自除数。 3. 如果所有位数字都能整除 $num$,则将其加入结果列表。 ### 思路 1:代码 ```python class Solution: def selfDividingNumbers(self, left: int, right: int) -> List[int]: def isSelfDividing(num): """判断一个数是否是自除数""" temp = num while temp > 0: digit = temp % 10 # 如果某一位是 0,或者 num 不能被该位整除 if digit == 0 or num % digit != 0: return False temp //= 10 return True result = [] # 遍历范围内的每个数字 for num in range(left, right + 1): if isSelfDividing(num): result.append(num) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O((right - left) \times \log_{10}(right))$,其中 $\log_{10}(right)$ 是数字的位数。 - **空间复杂度**:$O(1)$,不考虑结果数组的空间。 ================================================ FILE: docs/solutions/0700-0799/set-intersection-size-at-least-two.md ================================================ # [0757. 设置交集大小至少为2](https://leetcode.cn/problems/set-intersection-size-at-least-two/) - 标签:贪心、数组、排序 - 难度:困难 ## 题目链接 - [0757. 设置交集大小至少为2 - 力扣](https://leetcode.cn/problems/set-intersection-size-at-least-two/) ## 题目大意 **描述**: 给定一个二维整数数组 $intervals$ ,其中 $intervals[i] = [starti, endi]$ 表示从 $starti$ 到 $endi$ 的所有整数,包括 $starti$ 和 $endi$。 「包含集合」是一个名为 $nums$ 的数组,并满足 $intervals$ 中的每个区间都「至少」有「两个」整数在 $nums$ 中。 - 例如,如果 $intervals = [[1,3], [3,7], [8,9]]$,那么 $[1,2,4,7,8,9]$ 和 $[2,3,4,8,9]$ 都符合「包含集合」的定义。 **要求**: 返回包含集合可能的最小大小。 **说明**: - $1 \le intervals.length \le 3000$。 - $intervals[i].length == 2$。 - $0 \le starti \lt endi \le 10^{8}$。 **示例**: - 示例 1: ```python 输入:intervals = [[1,3],[3,7],[8,9]] 输出:5 解释:nums = [2, 3, 4, 8, 9]. 可以证明不存在元素数量为 4 的包含集合。 ``` - 示例 2: ```python 输入:intervals = [[1,3],[1,4],[2,5],[3,5]] 输出:3 解释:nums = [2, 3, 4]. 可以证明不存在元素数量为 2 的包含集合。 ``` ## 解题思路 ### 思路 1:贪心 + 排序 贪心策略:按照区间的右端点排序,优先选择右端点较小的区间。 **实现步骤**: 1. 按照区间的右端点升序排序,如果右端点相同,按左端点降序排序。 2. 维护一个集合 $S$,初始为空。 3. 遍历每个区间 $[start, end]$: - 统计 $S$ 中在该区间内的元素个数 $count$。 - 如果 $count < 2$,需要添加 $2 - count$ 个元素。 - 贪心地选择尽可能靠右的元素($end - 1$ 和 $end$),以便覆盖更多后续区间。 4. 返回集合的大小。 ### 思路 1:代码 ```python class Solution: def intersectionSizeTwo(self, intervals: List[List[int]]) -> int: # 按右端点升序排序,右端点相同时按左端点降序排序 intervals.sort(key=lambda x: (x[1], -x[0])) result = [] for start, end in intervals: # 统计 result 中在 [start, end] 范围内的元素个数 count = sum(1 for x in result if start <= x <= end) # 需要添加的元素个数 need = 2 - count if need <= 0: continue # 贪心地选择尽可能靠右的元素 if need == 1: # 添加 end result.append(end) else: # need == 2 # 添加 end-1 和 end result.append(end - 1) result.append(end) return len(result) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是区间的数量。排序需要 $O(n \log n)$,遍历每个区间需要 $O(n)$,每次统计需要 $O(n)$。 - **空间复杂度**:$O(n)$,存储结果的空间。 ================================================ FILE: docs/solutions/0700-0799/shortest-completing-word.md ================================================ # [0748. 最短补全词](https://leetcode.cn/problems/shortest-completing-word/) - 标签:数组、哈希表、字符串 - 难度:简单 ## 题目链接 - [0748. 最短补全词 - 力扣](https://leetcode.cn/problems/shortest-completing-word/) ## 题目大意 **描述**: 「补全词」是一个包含 $licensePlate$ 中所有字母的单词。忽略 $licensePlate$ 中的「数字和空格」。不区分大小写。如果某个字母在 $licensePlate$ 中出现不止一次,那么该字母在补全词中的出现次数应当一致或者更多。 例如:`licensePlate = "aBc 12c"`,那么它的补全词应当包含字母 `'a'`、`'b'` (忽略大写)和两个 `'c'`。可能的「补全词」有 `"abccdef"`、`"caaacab"` 以及 `"cbca"`。 给定一个字符串 $licensePlate$ 和一个字符串数组 $words$。 **要求**: 请你找出 $words$ 中的「最短补全词」。 题目数据保证一定存在一个最短补全词。当有多个单词都符合最短补全词的匹配条件时取 $words$ 中 第一个 出现的那个。 **说明**: - $1 \le licensePlate.length \le 7$。 - $licensePlate$ 由数字、大小写字母或空格 `' '` 组成。 - $1 \le words.length \le 10^{3}$。 - $1 \le words[i].length \le 15$。 - $words[i]$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:licensePlate = "1s3 PSt", words = ["step", "steps", "stripe", "stepple"] 输出:"steps" 解释:最短补全词应该包括 "s"、"p"、"s"(忽略大小写) 以及 "t"。 "step" 包含 "t"、"p",但只包含一个 "s",所以它不符合条件。 "steps" 包含 "t"、"p" 和两个 "s"。 "stripe" 缺一个 "s"。 "stepple" 缺一个 "s"。 因此,"steps" 是唯一一个包含所有字母的单词,也是本例的答案。 ``` - 示例 2: ```python 输入:licensePlate = "1s3 456", words = ["looks", "pest", "stew", "show"] 输出:"pest" 解释:licensePlate 只包含字母 "s" 。所有的单词都包含字母 "s" ,其中 "pest"、"stew"、和 "show" 三者最短。答案是 "pest" ,因为它是三个单词中在 words 里最靠前的那个。 ``` ## 解题思路 ### 思路 1:哈希表 补全词是包含 $licensePlate$ 中所有字母的单词(忽略数字、空格和大小写)。我们需要找到最短的补全词。 **实现步骤**: 1. 统计 $licensePlate$ 中每个字母(转为小写)的出现次数,忽略数字和空格。 2. 遍历 $words$ 中的每个单词: - 统计单词中每个字母的出现次数。 - 检查单词是否包含 $licensePlate$ 中所有字母(次数要足够)。 3. 在所有补全词中,返回长度最短的那个(如果有多个,返回第一个)。 ### 思路 1:代码 ```python class Solution: def shortestCompletingWord(self, licensePlate: str, words: List[str]) -> str: from collections import Counter # 统计 licensePlate 中字母的出现次数(忽略数字和空格,转为小写) plate_count = Counter() for char in licensePlate: if char.isalpha(): plate_count[char.lower()] += 1 result = None min_length = float('inf') # 遍历每个单词 for word in words: word_count = Counter(word) # 检查是否是补全词 is_completing = True for char, count in plate_count.items(): if word_count[char] < count: is_completing = False break # 更新最短补全词 if is_completing and len(word) < min_length: result = word min_length = len(word) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是 $words$ 的长度,$m$ 是单词的平均长度。 - **空间复杂度**:$O(1)$,字母表大小固定为 $26$。 ================================================ FILE: docs/solutions/0700-0799/sliding-puzzle.md ================================================ # [0773. 滑动谜题](https://leetcode.cn/problems/sliding-puzzle/) - 标签:广度优先搜索、记忆化搜索、数组、动态规划、回溯、矩阵 - 难度:困难 ## 题目链接 - [0773. 滑动谜题 - 力扣](https://leetcode.cn/problems/sliding-puzzle/) ## 题目大意 **描述**: 在一个 $2 \times 3$ 的板上(board)有 $5$ 块砖瓦,用数字 $1 \sim 5$ 来表示, 以及一块空缺用 $0$ 来表示。一次「移动」定义为选择 $0$ 与一个相邻的数字(上下左右)进行交换. 最终当板 $board$ 的结果是 $[[1,2,3],[4,5,0]]$ 谜板被解开。 给定一个谜板的初始状态 $board$。 **要求**: 返回最少可以通过多少次移动解开谜板,如果不能解开谜板,则返回 $-1$。 **说明**: - $board.length == 2$。 - $board[i].length == 3$。 - $0 \le board[i][j] \le 5$。 - $board[i][j]$ 中每个值都不同。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/06/29/slide1-grid.jpg) ```python 输入:board = [[1,2,3],[4,0,5]] 输出:1 解释:交换 0 和 5 ,1 步完成 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/06/29/slide2-grid.jpg) ```python 输入:board = [[1,2,3],[5,4,0]] 输出:-1 解释:没有办法完成谜板 ``` ## 解题思路 ### 思路 1:BFS(广度优先搜索) 将滑动谜题看作状态搜索问题,使用 BFS 找到从初始状态到目标状态的最短路径。 **实现步骤**: 1. 将二维数组转换为字符串表示状态。 2. 目标状态为 `"123450"`。 3. 使用 BFS,每次找到 `0` 的位置,尝试与相邻位置交换: - 预先定义每个位置可以移动到的相邻位置。 - 位置 $0, 1, 2, 3, 4, 5$ 分别对应二维数组的 $(0,0), (0,1), (0,2), (1,0), (1,1), (1,2)$。 4. 使用集合记录访问过的状态,避免重复。 5. 返回到达目标状态的最小步数。 ### 思路 1:代码 ```python class Solution: def slidingPuzzle(self, board: List[List[int]]) -> int: from collections import deque # 将二维数组转换为字符串 start = ''.join(str(board[i][j]) for i in range(2) for j in range(3)) target = "123450" if start == target: return 0 # 每个位置可以移动到的相邻位置 neighbors = { 0: [1, 3], 1: [0, 2, 4], 2: [1, 5], 3: [0, 4], 4: [1, 3, 5], 5: [2, 4] } # BFS queue = deque([(start, 0)]) # (状态, 步数) visited = {start} while queue: state, steps = queue.popleft() # 找到 0 的位置 zero_pos = state.index('0') # 尝试移动到相邻位置 for next_pos in neighbors[zero_pos]: # 交换 0 和相邻位置 state_list = list(state) state_list[zero_pos], state_list[next_pos] = state_list[next_pos], state_list[zero_pos] next_state = ''.join(state_list) # 如果到达目标状态 if next_state == target: return steps + 1 # 如果未访问过,加入队列 if next_state not in visited: visited.add(next_state) queue.append((next_state, steps + 1)) return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O((m \times n)! \times m \times n)$,状态总数为 $(m \times n)!$,每个状态需要 $O(m \times n)$ 时间处理。对于 $2 \times 3$ 的棋盘,状态数为 $6! = 720$。 - **空间复杂度**:$O((m \times n)!)$,存储访问过的状态。 ================================================ FILE: docs/solutions/0700-0799/smallest-rotation-with-highest-score.md ================================================ # [0798. 得分最高的最小轮调](https://leetcode.cn/problems/smallest-rotation-with-highest-score/) - 标签:数组、前缀和 - 难度:困难 ## 题目链接 - [0798. 得分最高的最小轮调 - 力扣](https://leetcode.cn/problems/smallest-rotation-with-highest-score/) ## 题目大意 **描述**: 给定一个数组 $nums$,我们可以将它按一个非负整数 $k$ 进行轮调,这样可以使数组变为 $[nums[k], nums[k + 1], ... nums[nums.length - 1], nums[0], nums[1], ..., nums[k-1]]$ 的形式。此后,任何值小于或等于其索引的项都可以记作一分。 - 例如,数组为 $nums = [2,4,1,3,0]$,我们按 $k = 2$ 进行轮调后,它将变成 $[1,3,0,2,4]$。这将记为 $3$ 分,因为 $1 > 0$ [不计分]、$3 > 1$ [不计分]、$0 \le 2$ [计 $1$ 分]、$2 \le 3$ [计 $1$ 分],$4 \le 4$ [计 $1$ 分]。 **要求**: 在所有可能的轮调中,返回我们所能得到的最高分数对应的轮调下标 $k$。如果有多个答案,返回满足条件的最小的下标 $k$ 。 **说明**: - $1 \le nums.length \le 10^{5}$。 - $0 \le nums[i] \lt nums.length$。 **示例**: - 示例 1: ```python 输入:nums = [2,3,1,4,0] 输出:3 解释: 下面列出了每个 k 的得分: k = 0, nums = [2,3,1,4,0], score 2 k = 1, nums = [3,1,4,0,2], score 3 k = 2, nums = [1,4,0,2,3], score 3 k = 3, nums = [4,0,2,3,1], score 4 k = 4, nums = [0,2,3,1,4], score 3 所以我们应当选择 k = 3,得分最高。 ``` - 示例 2: ```python 输入:nums = [1,3,0,2,4] 输出:0 解释: nums 无论怎么变化总是有 3 分。 所以我们将选择最小的 k,即 0。 ``` ## 解题思路 ### 思路 1:差分数组 对于每次轮调 $k$,计算得分的变化。使用差分数组优化。 **分析**: - 对于元素 $nums[i]$,在哪些轮调 $k$ 下能得分? - 轮调 $k$ 后,$nums[i]$ 移动到位置 $(i - k + n) \% n$。 - 要得分,需要 $nums[i] \le (i - k + n) \% n$。 **计算得分区间**: - 对于 $nums[i]$,它在轮调 $k$ 时能得分的条件是: - 如果 $i \ge nums[i]$:在 $k \in [0, i - nums[i]]$ 和 $k \in [i + 1, n - 1]$ 时得分。 - 如果 $i < nums[i]$:在 $k \in [i + 1, n - 1 - (nums[i] - i - 1)]$ 时得分(如果该区间有效)。 使用差分数组记录每个 $k$ 的得分变化。 ### 思路 1:代码 ```python class Solution: def bestRotation(self, nums: List[int]) -> int: n = len(nums) diff = [0] * n # 差分数组 for i in range(n): # 计算 nums[i] 不能得分的区间 # nums[i] 在位置 j 时,如果 nums[i] > j,则不能得分 # 轮调 k 后,nums[i] 在位置 (i - k + n) % n # 不能得分的条件:nums[i] > (i - k + n) % n # 简化:nums[i] 不能得分的轮调区间为 [(i - nums[i] + 1 + n) % n, i] # 使用差分数组标记不能得分的区间 left = (i - nums[i] + 1 + n) % n right = i if left <= right: diff[left] -= 1 if right + 1 < n: diff[right + 1] += 1 else: # 区间跨越了边界 diff[0] -= 1 if right + 1 < n: diff[right + 1] += 1 diff[left] -= 1 # 计算每个 k 的得分 max_score = 0 current_score = n # 初始得分为 n(假设所有元素都得分) result = 0 for k in range(n): current_score += diff[k] if current_score > max_score: max_score = current_score result = k return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。 - **空间复杂度**:$O(n)$,差分数组的空间。 ================================================ FILE: docs/solutions/0700-0799/split-linked-list-in-parts.md ================================================ # [0725. 分隔链表](https://leetcode.cn/problems/split-linked-list-in-parts/) - 标签:链表 - 难度:中等 ## 题目链接 - [0725. 分隔链表 - 力扣](https://leetcode.cn/problems/split-linked-list-in-parts/) ## 题目大意 **描述**: 给定一个头结点为 $head$ 的单链表和一个整数 $k$。 **要求**: 请你设计一个算法将链表分隔为 $k$ 个连续的部分。 每部分的长度应该尽可能的相等:任意两部分的长度差距不能超过 $1$。这可能会导致有些部分为 null。 这 $k$ 个部分应该按照在链表中出现的顺序排列,并且排在前面的部分的长度应该大于或等于排在后面的长度。 返回一个由上述 $k$ 部分组成的数组。 **说明**: - 链表中节点的数目在范围 $[0, 10^{3}]$。 - $0 \le Node.val \le 10^{3}$。 - $1 \le k \le 50$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/06/13/split1-lc.jpg) ```python 输入:head = [1,2,3], k = 5 输出:[[1],[2],[3],[],[]] 解释: 第一个元素 output[0] 为 output[0].val = 1 ,output[0].next = null 。 最后一个元素 output[4] 为 null ,但它作为 ListNode 的字符串表示是 [] 。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/06/13/split2-lc.jpg) ```python 输入:head = [1,2,3,4,5,6,7,8,9,10], k = 3 输出:[[1,2,3,4],[5,6,7],[8,9,10]] 解释: 输入被分成了几个连续的部分,并且每部分的长度相差不超过 1 。前面部分的长度大于等于后面部分的长度。 ``` ## 解题思路 ### 思路 1:链表遍历 将链表分隔为 $k$ 个连续的部分,每部分的长度应该尽可能相等。 **实现步骤**: 1. 先遍历链表,计算链表的总长度 $n$。 2. 计算每部分的基本长度:$size = n // k$。 3. 计算有多少部分需要多一个节点:$extra = n \% k$。 4. 前 $extra$ 个部分的长度为 $size + 1$,其余部分的长度为 $size$。 5. 遍历链表,按照计算的长度分隔链表: - 对于每一部分,遍历相应数量的节点。 - 断开当前部分与下一部分的连接。 - 将当前部分的头节点加入结果数组。 ### 思路 1:代码 ```python # Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution: def splitListToParts(self, head: Optional[ListNode], k: int) -> List[Optional[ListNode]]: # 计算链表长度 length = 0 curr = head while curr: length += 1 curr = curr.next # 计算每部分的长度 size = length // k # 每部分的基本长度 extra = length % k # 前 extra 个部分需要多一个节点 result = [] curr = head # 分隔链表 for i in range(k): # 当前部分的头节点 part_head = curr # 当前部分的长度 part_size = size + (1 if i < extra else 0) # 遍历当前部分 for j in range(part_size - 1): if curr: curr = curr.next # 断开连接 if curr: next_part = curr.next curr.next = None curr = next_part result.append(part_head) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + k)$,其中 $n$ 是链表的长度。需要遍历链表两次。 - **空间复杂度**:$O(1)$,不考虑结果数组的空间。 ================================================ FILE: docs/solutions/0700-0799/subarray-product-less-than-k.md ================================================ # [0713. 乘积小于 K 的子数组](https://leetcode.cn/problems/subarray-product-less-than-k/) - 标签:数组、滑动窗口 - 难度:中等 ## 题目链接 - [0713. 乘积小于 K 的子数组 - 力扣](https://leetcode.cn/problems/subarray-product-less-than-k/) ## 题目大意 **描述**:给定一个正整数数组 $nums$ 和整数 $k$。 **要求**:找出该数组内乘积小于 $k$ 的连续的子数组的个数。 **说明**: - $1 \le nums.length \le 3 * 10^4$。 - $1 \le nums[i] \le 1000$。 - $0 \le k \le 10^6$。 **示例**: - 示例 1: ```python 输入:nums = [10,5,2,6], k = 100 输出:8 解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2],、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。 ``` - 示例 2: ```python 输入:nums = [1,2,3], k = 0 输出:0 ``` ## 解题思路 ### 思路 1:滑动窗口(不定长度) 1. 设定两个指针:$left$、$right$,分别指向滑动窗口的左右边界,保证窗口内所有数的乘积 $window\_product$ 都小于 $k$。使用 $window\_product$ 记录窗口中的乘积值,使用 $count$ 记录符合要求的子数组个数。 2. 一开始,$left$、$right$ 都指向 $0$。 3. 向右移动 $right$,将最右侧元素加入当前子数组乘积 $window\_product$ 中。 4. 如果 $window\_product \ge k$,则不断右移 $left$,缩小滑动窗口长度,并更新当前乘积值 $window\_product$ 直到 $window\_product < k$。 5. 记录累积答案个数加 $1$,继续右移 $right$,直到 $right \ge len(nums)$ 结束。 6. 输出累积答案个数。 ### 思路 1:代码 ```python class Solution: def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int: if k <= 1: return 0 size = len(nums) left = 0 right = 0 window_product = 1 count = 0 while right < size: window_product *= nums[right] while window_product >= k: window_product /= nums[left] left += 1 count += (right - left + 1) right += 1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0700-0799/swap-adjacent-in-lr-string.md ================================================ # [0777. 在 LR 字符串中交换相邻字符](https://leetcode.cn/problems/swap-adjacent-in-lr-string/) - 标签:双指针、字符串 - 难度:中等 ## 题目链接 - [0777. 在 LR 字符串中交换相邻字符 - 力扣](https://leetcode.cn/problems/swap-adjacent-in-lr-string/) ## 题目大意 **描述**: 在一个由 `'L'`, `'R'` 和 `'X'` 三个字符组成的字符串(例如 `"RXXLRXRXL"`)中进行移动操作。一次移动操作指用一个 `"LX"` 替换一个 `"XL"`,或者用一个 `"XR"` 替换一个 `"RX"`。 现给定起始字符串 $start$ 和结束字符串 $result$。 **要求**: 请编写代码,当且仅当存在一系列移动操作使得 $start$ 可以转换成 $result$ 时,返回 True。 **说明**: - $1 \le start.length \le 10^{4}$。 - $start.length == result.length$。 - $start$ 和 $result$ 都只包含 `'L'`, `'R'` 或 `'X'`。 **示例**: - 示例 1: ```python 输入:start = "RXXLRXRXL", result = "XRLXXRRLX" 输出:true 解释:通过以下步骤我们可以将 start 转化为 result: RXXLRXRXL -> XRXLRXRXL -> XRLXRXRXL -> XRLXXRRXL -> XRLXXRRLX ``` - 示例 2: ```python 输入:start = "X", result = "L" 输出:false ``` ## 解题思路 ### 思路 1:双指针 观察移动规则: - `XL` → `LX`:`L` 可以向左移动。 - `RX` → `XR`:`R` 可以向右移动。 - `L` 只能向左移动,`R` 只能向右移动,`X` 不影响相对顺序。 **判断条件**: 1. 去掉所有 `X` 后,$start$ 和 $result$ 的字符序列必须相同。 2. 对于每个 `L`,在 $start$ 中的位置必须 $\ge$ 在 $result$ 中的位置(只能向左移)。 3. 对于每个 `R$,在 $start$ 中的位置必须 $\le$ 在 $result$ 中的位置(只能向右移)。 ### 思路 1:代码 ```python class Solution: def canTransform(self, start: str, result: str) -> bool: # 去掉 X 后的字符序列必须相同 if start.replace('X', '') != result.replace('X', ''): return False n = len(start) i = j = 0 # 使用双指针检查位置关系 while i < n and j < n: # 跳过 X while i < n and start[i] == 'X': i += 1 while j < n and result[j] == 'X': j += 1 # 如果一个到达末尾,另一个也必须到达末尾 if (i < n) != (j < n): return False if i < n and j < n: # 字符必须相同 if start[i] != result[j]: return False # L 只能向左移动,start 中的位置必须 >= result 中的位置 if start[i] == 'L' and i < j: return False # R 只能向右移动,start 中的位置必须 <= result 中的位置 if start[i] == 'R' and i > j: return False i += 1 j += 1 return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串的长度。 - **空间复杂度**:$O(n)$,字符串替换操作的空间。 ================================================ FILE: docs/solutions/0700-0799/swim-in-rising-water.md ================================================ # [0778. 水位上升的泳池中游泳](https://leetcode.cn/problems/swim-in-rising-water/) - 标签:深度优先搜索、广度优先搜索、并查集、数组、二分查找、矩阵、堆(优先队列) - 难度:困难 ## 题目链接 - [0778. 水位上升的泳池中游泳 - 力扣](https://leetcode.cn/problems/swim-in-rising-water/) ## 题目大意 **描述**:给定一个 $n \times n$ 大小的二维数组 $grid$,每一个方格的值 $grid[i][j]$ 表示为位置 $(i, j)$ 的高度。 现在要从左上角 $(0, 0)$ 位置出发,经过方格的一些点,到达右下角 $(n - 1, n - 1)$ 位置上。其中所经过路径的花费为这条路径上所有位置的最大高度。 **要求**:计算从 $(0, 0)$ 位置到 $(n - 1, n - 1)$ 的最优路径的花费。 **说明**: - **最优路径**:路径上最大高度最小的那条路径。 - $n == grid.length$。 - $n == grid[i].length$。 - $1 \le n \le 50$。 - $0 \le grid[i][j] < n^2$。 - $grid[i][j]$ 中每个值均无重复。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/06/29/swim1-grid.jpg) ```python 输入: grid = [[0,2],[1,3]] 输出: 3 解释: 时间为 0 时,你位于坐标方格的位置为 (0, 0)。 此时你不能游向任意方向,因为四个相邻方向平台的高度都大于当前时间为 0 时的水位。 等时间到达 3 时,你才可以游向平台 (1, 1). 因为此时的水位是 3,坐标方格中的平台没有比水位 3 更高的,所以你可以游向坐标方格中的任意位置。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/06/29/swim2-grid-1.jpg) ```python 输入: grid = [[0,1,2,3,4],[24,23,22,21,5],[12,13,14,15,16],[11,17,18,19,20],[10,9,8,7,6]] 输出: 16 解释: 最终的路线用加粗进行了标记。 我们必须等到时间为 16,此时才能保证平台 (0, 0) 和 (4, 4) 是连通的。 ``` ## 解题思路 ### 思路 1:并查集 将整个网络抽象为一个无向图,每个点与相邻的点(上下左右)之间都存在一条无向边,边的权重为两个点之间的最大高度。 我们要找到左上角到右下角的最优路径,可以遍历所有的点,将所有的边存储到数组中,每条边的存储格式为 $[x, y, h]$,意思是编号 $x$ 的点和编号为 $y$ 的点之间的权重为 $h$。 然后按照权重从小到大的顺序,对所有边进行排序。 再按照权重大小遍历所有边,将其依次加入并查集中。并且每次都需要判断 $(0, 0)$ 点和 $(n - 1, n - 1)$ 点是否连通。 如果连通,则该边的权重即为答案。 ### 思路 1:代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] self.count = n def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.count -= 1 def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def swimInWater(self, grid: List[List[int]]) -> int: row_size = len(grid) col_size = len(grid[0]) size = row_size * col_size edges = [] for row in range(row_size): for col in range(col_size): if row < row_size - 1: x = row * col_size + col y = (row + 1) * col_size + col h = max(grid[row][col], grid[row + 1][col]) edges.append([x, y, h]) if col < col_size - 1: x = row * col_size + col y = row * col_size + col + 1 h = max(grid[row][col], grid[row][col + 1]) edges.append([x, y, h]) edges.sort(key=lambda x: x[2]) union_find = UnionFind(size) for edge in edges: x, y, h = edge[0], edge[1], edge[2] union_find.union(x, y) if union_find.is_connected(0, size - 1): return h return 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \log n)$。其中 $n$ 为网格的边长,$n^2$ 为总格子数。枚举所有相邻格子的边,边数为 $O(n^2)$,排序所有边的时间复杂度为 $O(n^2 \log n^2) = O(n^2 \log n)$。并查集的合并与查找操作均摊 $O(\alpha(n^2))$,$\alpha$ 为反 Ackermann 函数,极慢增长,可视为常数。因此整体复杂度为 $O(n^2 \log n)$。 - **空间复杂度**:$O(n^2)$。主要用于存储并查集的父节点数组和所有边的信息,均为 $O(n^2)$ 级别。 ================================================ FILE: docs/solutions/0700-0799/to-lower-case.md ================================================ # [0709. 转换成小写字母](https://leetcode.cn/problems/to-lower-case/) - 标签:字符串 - 难度:简单 ## 题目链接 - [0709. 转换成小写字母 - 力扣](https://leetcode.cn/problems/to-lower-case/) ## 题目大意 **描述**:给定一个字符串 $s$。 **要求**:将该字符串中的大写字母转换成相同的小写字母,返回新的字符串。 **说明**: - $1 \le s.length \le 100$。 - $s$ 由 ASCII 字符集中的可打印字符组成。 **示例**: - 示例 1: ```python 输入:s = "Hello" 输出:"hello" ``` - 示例 2: ```python 输入:s = "LOVELY" 输出:"lovely" ``` ## 解题思路 ### 思路 1:直接模拟 - 大写字母 $A \sim Z$ 的 ASCII 码范围为 $[65, 90]$。 - 小写字母 $a \sim z$ 的 ASCII 码范围为 $[97, 122]$。 将大写字母的 ASCII 码加 $32$,就得到了对应的小写字母,则解决步骤如下: 1. 使用一个字符串变量 $ans$ 存储最终答案字符串。 2. 遍历字符串 $s$,对于当前字符 $ch$: 1. 如果 $ch$ 的 ASCII 码范围在 $[65, 90]$,则说明 $ch$ 为大写字母。将 $ch$ 的 ASCII 码增加 $32$,再转换为对应的字符,存入字符串 $ans$ 的末尾。 2. 如果 $ch$ 的 ASCII 码范围不在 $[65, 90]$,则说明 $ch$ 为小写字母。直接将 $ch$ 存入字符串 $ans$ 的末尾。 3. 遍历完字符串 $s$,返回答案字符串 $ans$。 ### 思路 1:代码 ```python class Solution: def toLowerCase(self, s: str) -> str: ans = "" for ch in s: if ord('A') <= ord(ch) <= ord('Z'): ans += chr(ord(ch) + 32) else: ans += ch return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。如果算上答案数组的空间占用,则空间复杂度为 $O(n)$。不算上则空间复杂度为 $O(1)$。 ### 思路 2:使用 API Python 语言中自带大写字母转小写字母的 API:`lower()`,用 API 转换完成之后,直接返回新的字符串。 ### 思路 2:代码 ```python class Solution: def toLowerCase(self, s: str) -> str: return s.lower() ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。如果算上答案数组的空间占用,则空间复杂度为 $O(n)$。不算上则空间复杂度为 $O(1)$。 ================================================ FILE: docs/solutions/0700-0799/toeplitz-matrix.md ================================================ # [0766. 托普利茨矩阵](https://leetcode.cn/problems/toeplitz-matrix/) - 标签:数组、矩阵 - 难度:简单 ## 题目链接 - [0766. 托普利茨矩阵 - 力扣](https://leetcode.cn/problems/toeplitz-matrix/) ## 题目大意 **描述**:给定一个 $m \times n$ 大小的矩阵 $matrix$。 **要求**:如果 $matrix$ 是托普利茨矩阵,则返回 `True`;否则返回 `False`。 **说明**: - **托普利茨矩阵**:矩阵上每一条由左上到右下的对角线上的元素都相同。 - $m == matrix.length$。 - $n == matrix[i].length$。 - $1 \le m, n \le 20$。 - $0 \le matrix[i][j] \le 99$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/04/ex1.jpg) ```python 输入:matrix = [[1,2,3,4],[5,1,2,3],[9,5,1,2]] 输出:true 解释: 在上述矩阵中, 其对角线为: "[9]", "[5, 5]", "[1, 1, 1]", "[2, 2, 2]", "[3, 3]", "[4]"。 各条对角线上的所有元素均相同, 因此答案是 True。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/11/04/ex2.jpg) ```python 输入:matrix = [[1,2],[2,2]] 输出:false 解释: 对角线 "[1, 2]" 上的元素不同。 ``` ## 解题思路 ### 思路 1:简单模拟 1. 两层循环遍历矩阵,依次判断矩阵当前位置 $(i, j)$ 上的值 $matrix[i][j]$ 与其左上角位置 $(i - 1, j - 1)$ 位置上的值 $matrix[i - 1][j - 1]$ 是否相等。 2. 如果不相等,则返回 `False`。 3. 遍历完,则返回 `True`。 ### 思路 1:代码 ```python class Solution: def isToeplitzMatrix(self, matrix: List[List[int]]) -> bool: for i in range(1, len(matrix)): for j in range(1, len(matrix[0])): if matrix[i][j] != matrix[i - 1][j - 1]: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$、$n$ 分别是矩阵 $matrix$ 的行数、列数。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/0700-0799/transform-to-chessboard.md ================================================ # [0782. 变为棋盘](https://leetcode.cn/problems/transform-to-chessboard/) - 标签:位运算、数组、数学、矩阵 - 难度:困难 ## 题目链接 - [0782. 变为棋盘 - 力扣](https://leetcode.cn/problems/transform-to-chessboard/) ## 题目大意 **描述**: 一个 $n \times n$ 的二维网络 $board$ 仅由 $0$ 和 $1$ 组成。每次移动,你能交换任意两列或是两行的位置。 **要求**: 返回 将这个矩阵变为「棋盘」所需的最小移动次数 。如果不存在可行的变换,输出 $-1$。 **说明**: - 「棋盘」是指任意一格的上下左右四个方向的值均与本身不同的矩阵。 - $n == board.length$。 - $n == board[i].length$。 - $2 \le n \le 30$。 - $board[i][j]$ 将只包含 $0$ 或 $1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/06/29/chessboard1-grid.jpg) ```python 输入: board = [[0,1,1,0],[0,1,1,0],[1,0,0,1],[1,0,0,1]] 输出: 2 解释:一种可行的变换方式如下,从左到右: 第一次移动交换了第一列和第二列。 第二次移动交换了第二行和第三行。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/06/29/chessboard2-grid.jpg) ```python 输入: board = [[0, 1], [1, 0]] 输出: 0 解释: 注意左上角的格值为0时也是合法的棋盘,也是合法的棋盘. ``` ## 解题思路 ### 思路 1:位运算 + 贪心 这道题要求将一个 $n \times n$ 的 01 矩阵通过交换行或列变成棋盘(相邻格子值不同)。 棋盘的性质: 1. 只有两种不同的行(或列)模式,且它们互补(0 和 1 互换)。 2. 这两种行(或列)的数量相差不超过 1。 3. 每一行(或列)中 0 和 1 的数量相差不超过 1。 算法步骤: 1. 检查矩阵是否可以变成棋盘: - 检查第一行和其他行的关系,只能有两种模式。 - 检查第一列和其他列的关系,只能有两种模式。 - 检查 0 和 1 的数量是否满足条件。 2. 计算最少交换次数: - 对于行:计算将行变成 `010101...` 或 `101010...` 需要的交换次数,选择较小的。 - 对于列:同样计算。 - 如果 $n$ 是奇数,只有一种合法的目标模式。 3. 返回行和列的最少交换次数之和。 ### 思路 1:代码 ```python class Solution: def movesToChessboard(self, board: List[List[int]]) -> int: n = len(board) # 检查第一行和第一列 row_mask = 0 col_mask = 0 for i in range(n): row_mask = (row_mask << 1) | board[0][i] col_mask = (col_mask << 1) | board[i][0] # 检查是否只有两种行模式 row_cnt = {row_mask: 0, row_mask ^ ((1 << n) - 1): 0} for i in range(n): mask = 0 for j in range(n): mask = (mask << 1) | board[i][j] if mask not in row_cnt: return -1 row_cnt[mask] += 1 # 检查是否只有两种列模式 col_cnt = {col_mask: 0, col_mask ^ ((1 << n) - 1): 0} for j in range(n): mask = 0 for i in range(n): mask = (mask << 1) | board[i][j] if mask not in col_cnt: return -1 col_cnt[mask] += 1 # 检查两种模式的数量是否合法 if abs(row_cnt[row_mask] - row_cnt[row_mask ^ ((1 << n) - 1)]) > 1: return -1 if abs(col_cnt[col_mask] - col_cnt[col_mask ^ ((1 << n) - 1)]) > 1: return -1 # 检查第一行和第一列的 0 和 1 数量 row_ones = bin(row_mask).count('1') col_ones = bin(col_mask).count('1') if row_ones < n // 2 or row_ones > (n + 1) // 2: return -1 if col_ones < n // 2 or col_ones > (n + 1) // 2: return -1 # 计算最少交换次数 def min_swaps(mask, ones): """计算将 mask 变成棋盘模式的最少交换次数""" # 目标模式:010101... 或 101010... target1 = 0 target2 = 0 for i in range(n): if i % 2 == 0: target1 = (target1 << 1) | 1 target2 = (target2 << 1) | 0 else: target1 = (target1 << 1) | 0 target2 = (target2 << 1) | 1 diff1 = bin(mask ^ target1).count('1') diff2 = bin(mask ^ target2).count('1') # 如果 n 是奇数,只有一种合法模式 if n % 2 == 1: if ones * 2 > n: return diff1 // 2 else: return diff2 // 2 else: return min(diff1, diff2) // 2 row_swaps = min_swaps(row_mask, row_ones) col_swaps = min_swaps(col_mask, col_ones) return row_swaps + col_swaps ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,需要遍历整个矩阵检查合法性。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0700-0799/valid-tic-tac-toe-state.md ================================================ # [0794. 有效的井字游戏](https://leetcode.cn/problems/valid-tic-tac-toe-state/) - 标签:数组、矩阵 - 难度:中等 ## 题目链接 - [0794. 有效的井字游戏 - 力扣](https://leetcode.cn/problems/valid-tic-tac-toe-state/) ## 题目大意 **描述**: 井字游戏的棋盘是一个 $3 \times 3$ 数组,由字符 `' '`,`'X'` 和 `'O'` 组成。字符 `' '` 代表一个空位。 以下是井字游戏的规则: - 玩家轮流将字符放入空位 `' '` 中。 - 玩家 1 总是放字符 `'X'`,而玩家 2 总是放字符 `'O'`。 - `'X'` 和 `'O'` 只允许放置在空位中,不允许对已放有字符的位置进行填充。 - 当有 3 个相同(且非空)的字符填充任何行、列或对角线时,游戏结束。 - 当所有位置非空时,也算为游戏结束。 - 如果游戏结束,玩家不允许再放置字符。 给定一个字符串数组 $board$ 表示井字游戏的棋盘。 **要求**: 当且仅当在井字游戏过程中,棋盘有可能达到 $board$ 所显示的状态时,才返回 true。 **说明**: - $board.length == 3$。 - $board[i].length == 3$。 - $board[i][j]$ 为 `'X'`、`'O'` 或 `' '`。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/05/15/tictactoe1-grid.jpg) ```python 输入:board = ["O "," "," "] 输出:false 解释:玩家 1 总是放字符 "X" 。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/05/15/tictactoe2-grid.jpg) ```python 输入:board = ["XOX"," X "," "] 输出:false 解释:玩家应该轮流放字符。 ``` ## 解题思路 ### 思路 1:模拟 判断井字游戏的状态是否有效,需要检查以下条件: 1. **数量关系**:玩家 1(`X`)先手,所以 `X` 的数量应该等于或比 `O` 多 $1$。 2. **获胜条件**: - 如果 `X` 获胜,则 `O` 不能获胜,且 `X` 的数量应该比 `O` 多 $1$(因为 `X` 最后一步获胜)。 - 如果 `O` 获胜,则 `X` 不能获胜,且 `X` 的数量应该等于 `O` 的数量(因为 `O` 最后一步获胜)。 ### 思路 1:代码 ```python class Solution: def validTicTacToe(self, board: List[str]) -> bool: def check_win(player): """检查某个玩家是否获胜""" # 检查行 for i in range(3): if all(board[i][j] == player for j in range(3)): return True # 检查列 for j in range(3): if all(board[i][j] == player for i in range(3)): return True # 检查对角线 if all(board[i][i] == player for i in range(3)): return True if all(board[i][2-i] == player for i in range(3)): return True return False # 统计 X 和 O 的数量 x_count = sum(row.count('X') for row in board) o_count = sum(row.count('O') for row in board) # X 的数量应该等于或比 O 多 1 if x_count < o_count or x_count > o_count + 1: return False # 检查获胜情况 x_win = check_win('X') o_win = check_win('O') # X 和 O 不能同时获胜 if x_win and o_win: return False # 如果 X 获胜,X 的数量应该比 O 多 1 if x_win and x_count != o_count + 1: return False # 如果 O 获胜,X 和 O 的数量应该相等 if o_win and x_count != o_count: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$,棋盘大小固定为 $3 \times 3$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/advantage-shuffle.md ================================================ # [0870. 优势洗牌](https://leetcode.cn/problems/advantage-shuffle/) - 标签:贪心、数组、双指针、排序 - 难度:中等 ## 题目链接 - [0870. 优势洗牌 - 力扣](https://leetcode.cn/problems/advantage-shuffle/) ## 题目大意 **描述**: 给定两个长度相等的数组 $nums1$ 和 $nums2$,$nums1$ 相对于 $nums2$ 的优势可以用满足 $nums1[i] > nums2[i]$ 的索引 $i$ 的数目来描述。 **要求**: 返回 $nums1$ 的 任意 排列,使其相对于 $nums2$ 的优势最大化。 **说明**: - $1 \le nums1.length \le 10^{5}$。 - $nums2.length == nums1.length$。 - $0 \le nums1[i], nums2[i] \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:nums1 = [2,7,11,15], nums2 = [1,10,4,11] 输出:[2,11,7,15] ``` - 示例 2: ```python 输入:nums1 = [12,24,8,32], nums2 = [13,25,32,11] 输出:[24,32,8,12] ``` ## 解题思路 ### 思路 1:贪心 + 双指针(田忌赛马) 这道题本质上是经典的"田忌赛马"问题。目标是最大化 $nums1$ 相对于 $nums2$ 的优势。 贪心策略: 1. 将 $nums1$ 排序。 2. 将 $nums2$ 的元素及其索引一起排序。 3. 使用双指针: - 对于 $nums2$ 中的每个元素(从大到小),尝试用 $nums1$ 中最大的能战胜它的元素。 - 如果 $nums1$ 中最大的元素都无法战胜,则用 $nums1$ 中最小的元素(反正也赢不了,不如浪费最小的)。 具体实现: 1. 对 $nums1$ 排序。 2. 对 $nums2$ 的索引按值从大到小排序。 3. 使用左右指针遍历 $nums1$,对于 $nums2$ 中的每个元素(从大到小): - 如果 $nums1$ 的右指针元素能战胜,使用它,右指针左移。 - 否则,使用 $nums1$ 的左指针元素(最小的),左指针右移。 ### 思路 1:代码 ```python class Solution: def advantageCount(self, nums1: List[int], nums2: List[int]) -> List[int]: n = len(nums1) # 对 nums1 排序 nums1.sort() # 对 nums2 的索引按值从大到小排序 idx2 = sorted(range(n), key=lambda i: nums2[i], reverse=True) # 结果数组 result = [0] * n # 双指针 left, right = 0, n - 1 # 从大到小遍历 nums2 for i in idx2: # 如果 nums1 的最大值能战胜 nums2[i],使用它 if nums1[right] > nums2[i]: result[i] = nums1[right] right -= 1 else: # 否则用 nums1 的最小值(反正也赢不了) result[i] = nums1[left] left += 1 return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组的长度。排序需要 $O(n \log n)$。 - **空间复杂度**:$O(n)$,需要存储排序后的索引和结果数组。 ================================================ FILE: docs/solutions/0800-0899/all-nodes-distance-k-in-binary-tree.md ================================================ # [0863. 二叉树中所有距离为 K 的结点](https://leetcode.cn/problems/all-nodes-distance-k-in-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、哈希表、二叉树 - 难度:中等 ## 题目链接 - [0863. 二叉树中所有距离为 K 的结点 - 力扣](https://leetcode.cn/problems/all-nodes-distance-k-in-binary-tree/) ## 题目大意 **描述**: 给定一个二叉树(具有根结点 $root$), 一个目标结点 $target$,和一个整数值 $k$。 **要求**: 返回到目标结点 $target$ 距离为 $k$ 的所有结点的值的数组。 答案可以以「任何顺序」返回。 **说明**: - 节点数在 $[1, 500]$ 范围内。 - $0 \le Node.val \le 500$。 - $Node.val$ 中所有值「不同」。 - 目标结点 $target$ 是树上的结点。 - $0 \le k \le 10^{3}$。 **示例**: - 示例 1: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/06/28/sketch0.png) ```python 输入:root = [3,5,1,6,2,0,8,null,null,7,4], target = 5, k = 2 输出:[7,4,1] 解释:所求结点为与目标结点(值为 5)距离为 2 的结点,值分别为 7,4,以及 1 ``` - 示例 2: ```python 输入: root = [1], target = 1, k = 3 输出: [] ``` ## 解题思路 ### 思路 1:DFS + BFS 这道题要求找到二叉树中距离目标节点 $target$ 为 $k$ 的所有节点。由于二叉树只能从父节点访问子节点,无法直接从子节点访问父节点,因此需要先建立父节点的映射关系。 算法步骤: 1. 使用 DFS 遍历整棵树,建立每个节点到其父节点的映射关系。 2. 从目标节点 $target$ 开始,使用 BFS 向四周扩散(左子节点、右子节点、父节点)。 3. 使用 $visited$ 集合记录已访问的节点,避免重复访问。 4. 当扩散距离达到 $k$ 时,返回当前层的所有节点值。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val = x # self.left = None # self.right = None class Solution: def distanceK(self, root: TreeNode, target: TreeNode, k: int) -> List[int]: from collections import deque, defaultdict # 建立父节点映射 parent = defaultdict(lambda: None) def dfs(node, par=None): """DFS 遍历,建立父节点映射""" if not node: return parent[node] = par dfs(node.left, node) dfs(node.right, node) # 建立父节点映射 dfs(root) # BFS 从 target 开始扩散 queue = deque([target]) visited = {target} distance = 0 while queue: # 如果距离达到 k,返回当前层的所有节点值 if distance == k: return [node.val for node in queue] # 遍历当前层 for _ in range(len(queue)): node = queue.popleft() # 向左子节点扩散 if node.left and node.left not in visited: visited.add(node.left) queue.append(node.left) # 向右子节点扩散 if node.right and node.right not in visited: visited.add(node.right) queue.append(node.right) # 向父节点扩散 if parent[node] and parent[node] not in visited: visited.add(parent[node]) queue.append(parent[node]) distance += 1 return [] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数。需要遍历整棵树建立父节点映射,然后进行 BFS。 - **空间复杂度**:$O(n)$,需要存储父节点映射和 BFS 队列。 ================================================ FILE: docs/solutions/0800-0899/all-possible-full-binary-trees.md ================================================ # [0894. 所有可能的真二叉树](https://leetcode.cn/problems/all-possible-full-binary-trees/) - 标签:树、递归、记忆化搜索、动态规划、二叉树 - 难度:中等 ## 题目链接 - [0894. 所有可能的真二叉树 - 力扣](https://leetcode.cn/problems/all-possible-full-binary-trees/) ## 题目大意 **描述**: 给定一个整数 $n$。 **要求**: 请你找出所有可能含 $n$ 个节点的 真二叉树 ,并以列表形式返回。 **说明**: - 答案中每棵树的每个节点都必须符合 $Node$.$val == 0$。 - 答案的每个元素都是一棵真二叉树的根节点。你可以按 任意顺序 返回最终的真二叉树列表。 - 「真二叉树」是一类二叉树,树中每个节点恰好有 0 或 2 个子节点。 - $1 \le n \le 20$。 **示例**: - 示例 1: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/08/22/fivetrees.png) ```python 输入:n = 7 输出:[[0,0,0,null,null,0,0,null,null,0,0],[0,0,0,null,null,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,null,null,null,null,0,0],[0,0,0,0,0,null,null,0,0]] ``` - 示例 2: ```python 输入:n = 3 输出:[[0,0,0]] ``` ## 解题思路 ### 思路 1:递归 + 记忆化搜索 真二叉树的特点是每个节点要么有 0 个子节点(叶子节点),要么有 2 个子节点。因此,真二叉树的节点数必须是奇数。 关键观察: - 如果 $n$ 是偶数,无法构成真二叉树,返回空列表。 - 如果 $n = 1$,只有一个节点,返回包含单个节点的列表。 - 如果 $n > 1$,根节点占 1 个节点,剩余 $n-1$ 个节点分配给左右子树。 - 左子树可以有 $1, 3, 5, \ldots, n-2$ 个节点,相应地右子树有 $n-2, n-4, \ldots, 1$ 个节点。 算法步骤: 1. 使用记忆化搜索避免重复计算。 2. 递归构建所有可能的左右子树组合。 3. 对于每种组合,创建一个新的根节点,连接左右子树。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def allPossibleFBT(self, n: int) -> List[Optional[TreeNode]]: # 记忆化字典 memo = {} def helper(n): """返回所有可能的 n 个节点的真二叉树""" # 如果已经计算过,直接返回 if n in memo: return memo[n] # 偶数个节点无法构成真二叉树 if n % 2 == 0: return [] # 只有一个节点 if n == 1: return [TreeNode(0)] result = [] # 枚举左子树的节点数(必须是奇数) for left_count in range(1, n, 2): right_count = n - 1 - left_count # 递归构建所有可能的左右子树 left_trees = helper(left_count) right_trees = helper(right_count) # 组合所有可能的左右子树 for left in left_trees: for right in right_trees: root = TreeNode(0) root.left = left root.right = right result.append(root) memo[n] = result return result return helper(n) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n)$,生成所有可能的真二叉树需要指数时间。 - **空间复杂度**:$O(2^n)$,需要存储所有可能的树结构。 ================================================ FILE: docs/solutions/0800-0899/ambiguous-coordinates.md ================================================ # [0816. 模糊坐标](https://leetcode.cn/problems/ambiguous-coordinates/) - 标签:字符串、回溯、枚举 - 难度:中等 ## 题目链接 - [0816. 模糊坐标 - 力扣](https://leetcode.cn/problems/ambiguous-coordinates/) ## 题目大意 **描述**: 我们有一些二维坐标,如 `"(1, 3)"` 或 `"(2, 0.5)"`,然后我们移除所有逗号,小数点和空格,得到一个字符串 $S$。 **要求**: 返回所有可能的原始字符串到一个列表中。 **说明**: - 原始的坐标表示法不会存在多余的零,所以不会出现类似于 `"00"`, `"0.0"`, `"0.00"`, `"1.0"`, `"001"`, `"00.01"` 或一些其他更小的数来表示坐标。此外,一个小数点前至少存在一个数,所以也不会出现 `".1"` 形式的数字。 - 最后返回的列表可以是任意顺序的。而且注意返回的两个数字中间(逗号之后)都有一个空格。 - $4 \le S.length \le 12$。 - `S[0] = "(", S[S.length - 1] = ")"`, 且字符串 S$ 中的其他元素都是数字。 **示例**: - 示例 1: ```python 示例 1: 输入: "(123)" 输出: ["(1, 23)", "(12, 3)", "(1.2, 3)", "(1, 2.3)"] ``` - 示例 2: ```python 输入: "(00011)" 输出: ["(0.001, 1)", "(0, 0.011)"] 解释: 0.0, 00, 0001 或 00.01 是不被允许的。 ``` ## 解题思路 ### 思路 1:枚举 + 字符串处理 这道题要求将去掉括号和逗号的坐标字符串还原为所有可能的原始坐标。 算法步骤: 1. 去掉字符串首尾的括号,得到数字字符串。 2. 枚举逗号的位置,将字符串分为两部分($x$ 坐标和 $y$ 坐标)。 3. 对于每部分,枚举小数点的位置(包括不加小数点的情况)。 4. 检查生成的数字是否合法: - 不能有前导零(除了 `"0"` 本身)。 - 小数部分不能有后导零。 5. 组合所有合法的 $x$ 和 $y$ 坐标,生成结果。 辅助函数:生成所有可能的合法数字表示。 ### 思路 1:代码 ```python class Solution: def ambiguousCoordinates(self, s: str) -> List[str]: def get_valid_numbers(num_str): """生成字符串的所有合法数字表示""" n = len(num_str) result = [] # 不加小数点 if n == 1 or num_str[0] != '0': result.append(num_str) # 加小数点 for i in range(1, n): left = num_str[:i] right = num_str[i:] # 检查是否合法: # 1. 整数部分不能有前导零(除非是 "0") # 2. 小数部分不能有后导零 if (left == '0' or left[0] != '0') and right[-1] != '0': result.append(left + '.' + right) return result # 去掉首尾括号 s = s[1:-1] n = len(s) result = [] # 枚举逗号的位置 for i in range(1, n): x_str = s[:i] y_str = s[i:] # 生成所有可能的 x 和 y 坐标 x_coords = get_valid_numbers(x_str) y_coords = get_valid_numbers(y_str) # 组合所有可能的坐标 for x in x_coords: for y in y_coords: result.append(f"({x}, {y})") return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 是字符串的长度。需要枚举逗号位置 $O(n)$,对于每个位置枚举小数点位置 $O(n)$,生成结果需要 $O(n)$。 - **空间复杂度**:$O(n^2)$,需要存储所有可能的坐标表示。 ================================================ FILE: docs/solutions/0800-0899/backspace-string-compare.md ================================================ # [0844. 比较含退格的字符串](https://leetcode.cn/problems/backspace-string-compare/) - 标签:栈、双指针、字符串、模拟 - 难度:简单 ## 题目链接 - [0844. 比较含退格的字符串 - 力扣](https://leetcode.cn/problems/backspace-string-compare/) ## 题目大意 **描述**:给定 $s$ 和 $t$ 两个字符串。字符串中的 `#` 代表退格字符。 **要求**:当它们分别被输入到空白的文本编辑器后,判断二者是否相等。如果相等,返回 $True$;否则,返回 $False$。 **说明**: - 如果对空文本输入退格字符,文本继续为空。 - $1 \le s.length, t.length \le 200$。 - $s$ 和 $t$ 只含有小写字母以及字符 `#`。 **示例**: - 示例 1: ```python 输入:s = "ab#c", t = "ad#c" 输出:true 解释:s 和 t 都会变成 "ac"。 ``` - 示例 2: ```python 输入:s = "ab##", t = "c#d#" 输出:true 解释:s 和 t 都会变成 ""。 ``` ## 解题思路 这道题的第一个思路是用栈,第二个思路是使用分离双指针。 ### 思路 1:栈 - 定义一个构建方法,用来将含有退格字符串构建为删除退格的字符串。构建方法如下。 - 使用一个栈存放删除退格的字符串。 - 遍历字符串,如果遇到的字符不是 `#`,则将其插入到栈中。 - 如果遇到的字符是 `#`,且当前栈不为空,则将当前栈顶元素弹出。 - 分别使用构建方法处理字符串 $s$ 和 $t$,如果处理完的字符串 $s$ 和 $t$ 相等,则返回 $True$,否则返回 $False$。 ### 思路 1:代码 ```python class Solution: def build(self, s: str): stack = [] for ch in s: if ch != '#': stack.append(ch) elif stack: stack.pop() return stack def backspaceCompare(self, s: str, t: str) -> bool: return self.build(s) == self.build(t) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 和 $m$ 分别为字符串 $s$、$t$ 的长度。 - **空间复杂度**:$O(n + m)$。 ### 思路 2:分离双指针 由于 `#` 会消除左侧字符,而不会影响右侧字符,所以我们选择从字符串尾端遍历 $s$、$t$ 字符串。具体做法如下: - 使用分离双指针 $left\_1$、$left\_2$。$left\_1$ 指向字符串 $s$ 末尾,$left\_2$ 指向字符串 $t$ 末尾。使用 $sign\_1$、$sign\_2$ 标记字符串 $s$、$t$ 中当前退格字符个数。 - 从后到前遍历字符串 $s$、$t$。 - 先来循环处理字符串 $s$ 尾端 `#` 的影响,具体如下: - 如果当前字符是 `#`,则更新 $s$ 当前退格字符个数,即 `sign_1 += 1`。同时将 $left\_1$ 左移。 - 如果 $s$ 当前退格字符个数大于 $0$,则退格数减一,即 `sign_1 -= 1`。同时将 $left\_1$ 左移。 - 如果 $s$ 当前为普通字符,则跳出循环。 - 同理再来处理字符串 $t$ 尾端 `#` 的影响,具体如下: - 如果当前字符是 `#`,则更新 $t$ 当前退格字符个数,即 `sign_2 += 1`。同时将 $left\_2$ 左移。 - 如果 $t$ 当前退格字符个数大于 $0$,则退格数减一,即 `sign_2 -= 1`。同时将 $left\_2$ 左移。 - 如果 $t$ 当前为普通字符,则跳出循环。 - 处理完,如果两个字符串为空,则说明匹配,直接返回 $True$。 - 再先排除长度不匹配的情况,直接返回 $False$。 - 最后判断 $s[left\_1]$ 是否等于 $s[left\_2]$。不等于则直接返回 $False$,等于则令 $left\_1$、$left\_2$ 左移,继续遍历。 - 遍历完没有出现不匹配的情况,则返回 $True$。 ### 思路 2:代码 ```python class Solution: def backspaceCompare(self, s: str, t: str) -> bool: left_1, left_2 = len(s) - 1, len(t) - 1 sign_1, sign_2 = 0, 0 while left_1 >= 0 or left_2 >= 0: while left_1 >= 0: if s[left_1] == '#': sign_1 += 1 left_1 -= 1 elif sign_1 > 0: sign_1 -= 1 left_1 -= 1 else: break while left_2 >= 0: if t[left_2] == '#': sign_2 += 1 left_2 -= 1 elif sign_2 > 0: sign_2 -= 1 left_2 -= 1 else: break if left_1 < 0 and left_2 < 0: return True if left_1 >= 0 and left_2 < 0: return False if left_1 < 0 and left_2 >= 0: return False if s[left_1] != t[left_2]: return False left_1 -= 1 left_2 -= 1 return True ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 和 $m$ 分别为字符串 $s$、$t$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/binary-gap.md ================================================ # [0868. 二进制间距](https://leetcode.cn/problems/binary-gap/) - 标签:位运算 - 难度:简单 ## 题目链接 - [0868. 二进制间距 - 力扣](https://leetcode.cn/problems/binary-gap/) ## 题目大意 **描述**:给定一个正整数 $n$。 **要求**:找到并返回 $n$ 的二进制表示中两个相邻 $1$ 之间的最长距离。如果不存在两个相邻的 $1$,返回 $0$。 **说明**: - $1 \le n \le 10^9$。 **示例**: - 示例 1: ```python 输入:n = 22 输出:2 解释:22 的二进制是 "10110"。 在 22 的二进制表示中,有三个 1,组成两对相邻的 1。 第一对相邻的 1 中,两个 1 之间的距离为 2。 第二对相邻的 1 中,两个 1 之间的距离为 1。 答案取两个距离之中最大的,也就是 2。 ``` - 示例 2: ```python 输入:n = 8 输出:0 解释:8 的二进制是 "1000"。 在 8 的二进制表示中没有相邻的两个 1,所以返回 0。 ``` ## 解题思路 ### 思路 1:遍历 1. 将正整数 $n$ 转为二进制字符串形式 $bin\_n$。 2. 使用变量 $pre$ 记录二进制字符串中上一个 $1$ 的位置,使用变量 $ans$ 存储两个相邻 $1$ 之间的最长距离。 3. 遍历二进制字符串形式 $bin\_n$ 的每一位,遇到 $1$ 时判断并更新两个相邻 $1$ 之间的最长距离。 4. 遍历完返回两个相邻 $1$ 之间的最长距离,即 $ans$。 ### 思路 1:代码 ```Python class Solution: def binaryGap(self, n: int) -> int: bin_n = bin(n) pre, ans = 2, 0 for i in range(2, len(bin_n)): if bin_n[i] == '1': ans = max(ans, i - pre) pre = i return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/binary-tree-pruning.md ================================================ # [0814. 二叉树剪枝](https://leetcode.cn/problems/binary-tree-pruning/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0814. 二叉树剪枝 - 力扣](https://leetcode.cn/problems/binary-tree-pruning/) ## 题目大意 给定一棵二叉树的根节点 `root`,树的每个节点值要么是 `0`,要么是 `1`。 要求:剪除该二叉树中所有节点值为 `0` 的子树。 - 节点 `node` 的子树为: `node` 本身,以及所有 `node` 的后代。 ## 解题思路 定义辅助方法 `containsOnlyZero(root)` 递归判断以 `root` 为根的子树中是否只包含 `0`。如果子树中只包含 `0`,则返回 `True`。如果子树中含有 `1`,则返回 `False`。当 `root` 为空时,也返回 `True`。 然后递归遍历二叉树,判断当前节点 `root` 是否只包含 `0`。如果只包含 `0`,则将其置空,返回 `None`。否则递归遍历左右子树,并设置对应的左右指针。 最后返回根节点 `root`。 ## 代码 ```python class Solution: def containsOnlyZero(self, root: TreeNode): if not root: return True if root.val == 1: return False return self.containsOnlyZero(root.left) and self.containsOnlyZero(root.right) def pruneTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]: if not root: return root if self.containsOnlyZero(root): return None root.left = self.pruneTree(root.left) root.right = self.pruneTree(root.right) return root ``` ================================================ FILE: docs/solutions/0800-0899/binary-trees-with-factors.md ================================================ # [0823. 带因子的二叉树](https://leetcode.cn/problems/binary-trees-with-factors/) - 标签:数组、哈希表、动态规划、排序 - 难度:中等 ## 题目链接 - [0823. 带因子的二叉树 - 力扣](https://leetcode.cn/problems/binary-trees-with-factors/) ## 题目大意 **描述**: 给出一个含有不重复整数元素的数组 $arr$ ,每个整数 $arr[i]$ 均大于 1。 用这些整数来构建二叉树,每个整数可以使用任意次数。其中:每个非叶结点的值应等于它的两个子结点的值的乘积。 **要求**: 计算出满足条件的二叉树的数量。答案可能很大,返回 对 $10^9 + 7$ 取余的结果。 **说明**: - $1 \le arr.length \le 10^{3}$。 - $2 \le arr[i] \le 10^{9}$。 - $arr$ 中的所有值互不相同。 **示例**: - 示例 1: ```python 输入: arr = [2, 4] 输出: 3 解释: 可以得到这些二叉树: [2], [4], [4, 2, 2] ``` - 示例 2: ```python 输入: arr = [2, 4, 5, 10] 输出: 7 解释: 可以得到这些二叉树: [2], [4], [5], [10], [4, 2, 2], [10, 2, 5], [10, 5, 2]. ``` ## 解题思路 ### 思路 1:动态规划 + 哈希表 这道题要求计算用数组中的元素构建二叉树的方案数,其中每个非叶节点的值等于其两个子节点值的乘积。 关键观察: - 对于一个值 $arr[i]$,如果它是非叶节点,那么它的两个子节点值 $left$ 和 $right$ 必须满足 $left \times right = arr[i]$,且 $left$ 和 $right$ 都在数组中。 - 使用动态规划:$dp[x]$ 表示以 $x$ 为根节点的二叉树的方案数。 算法步骤: 1. 将数组排序,方便从小到大处理。 2. 使用哈希表 $dp$,$dp[x]$ 表示以 $x$ 为根节点的二叉树方案数。 3. 初始化:每个元素本身可以作为叶节点,$dp[x] = 1$。 4. 对于每个元素 $x$,枚举所有可能的因子对 $(left, right)$: - 如果 $left$ 和 $right$ 都在数组中,则 $dp[x] += dp[left] \times dp[right]$。 5. 返回所有 $dp[x]$ 的和。 ### 思路 1:代码 ```python class Solution: def numFactoredBinaryTrees(self, arr: List[int]) -> int: MOD = 10**9 + 7 # 排序,从小到大处理 arr.sort() # dp[x] 表示以 x 为根节点的二叉树方案数 dp = {} # 将数组元素存入集合,方便查找 arr_set = set(arr) for x in arr: # 初始化:x 本身可以作为叶节点 dp[x] = 1 # 枚举所有可能的左子节点 for left in arr: # 如果 left > sqrt(x),后面的 left 更大,right 会更小,会重复计算 if left * left > x: break # 检查是否能整除 if x % left == 0: right = x // left # 如果 right 在数组中 if right in arr_set: if left == right: # 左右子树相同 dp[x] = (dp[x] + dp[left] * dp[right]) % MOD else: # 左右子树不同,有两种组合方式 dp[x] = (dp[x] + 2 * dp[left] * dp[right]) % MOD # 返回所有方案数的和 return sum(dp.values()) % MOD ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是数组的长度。需要对每个元素枚举所有可能的因子。 - **空间复杂度**:$O(n)$,需要使用哈希表存储 DP 状态。 ================================================ FILE: docs/solutions/0800-0899/bitwise-ors-of-subarrays.md ================================================ # [0898. 子数组按位或操作](https://leetcode.cn/problems/bitwise-ors-of-subarrays/) - 标签:位运算、数组、动态规划 - 难度:中等 ## 题目链接 - [0898. 子数组按位或操作 - 力扣](https://leetcode.cn/problems/bitwise-ors-of-subarrays/) ## 题目大意 **描述**: 给定一个整数数组 $arr$。 **要求**: 返回所有 $arr$ 的非空子数组的不同按位或的数量。 **说明**: - 「子数组的按位或」是子数组中每个整数的按位或。含有一个整数的子数组的按位或就是该整数。 - 「子数组」是数组内连续的非空元素序列。 - $1 \le nums.length \le 5 \times 10^{4}$。 - $0 \le nums[i] \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:arr = [0] 输出:1 解释: 只有一个可能的结果 0 。 ``` - 示例 2: ```python 输入:arr = [1,1,2] 输出:3 解释: 可能的子数组为 [1],[1],[2],[1, 1],[1, 2],[1, 1, 2]。 产生的结果为 1,1,2,1,3,3 。 有三个唯一值,所以答案是 3 。 ``` ## 解题思路 ### 思路 1:动态规划 + 哈希表 这道题要求计算所有子数组的按位或结果的不同值数量。 关键观察: - 对于以位置 $i$ 结尾的所有子数组,它们的按位或结果最多只有 $O(\log C)$ 个不同值($C$ 是数组中的最大值)。 - 原因:按位或操作只会增加 1 的位数,而整数最多有 $\log C$ 位。 算法步骤: 1. 使用集合 $result$ 存储所有不同的按位或结果。 2. 使用集合 $cur$ 存储以当前位置结尾的所有子数组的按位或结果。 3. 遍历数组: - 对于每个元素 $arr[i]$,计算它与 $cur$ 中所有值的按位或结果,加上它本身。 - 更新 $cur$ 为新的按位或结果集合。 - 将 $cur$ 中的所有值加入 $result$。 4. 返回 $result$ 的大小。 ### 思路 1:代码 ```python class Solution: def subarrayBitwiseORs(self, arr: List[int]) -> int: result = set() # 存储所有不同的按位或结果 cur = set() # 存储以当前位置结尾的所有子数组的按位或结果 for num in arr: # 计算以当前元素结尾的所有子数组的按位或结果 cur = {num | x for x in cur} | {num} # 将结果加入总集合 result |= cur return len(result) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log C)$,其中 $n$ 是数组的长度,$C$ 是数组中的最大值。对于每个元素,$cur$ 集合的大小最多为 $O(\log C)$。 - **空间复杂度**:$O(n \log C)$,需要存储所有不同的按位或结果。 ================================================ FILE: docs/solutions/0800-0899/boats-to-save-people.md ================================================ # [0881. 救生艇](https://leetcode.cn/problems/boats-to-save-people/) - 标签:贪心、数组、双指针、排序 - 难度:中等 ## 题目链接 - [0881. 救生艇 - 力扣](https://leetcode.cn/problems/boats-to-save-people/) ## 题目大意 **描述**:给定一个整数数组 `people` 代表每个人的体重,其中第 `i` 个人的体重为 `people[i]`。再给定一个整数 `limit`,代表每艘船可以承载的最大重量。每艘船最多可同时载两人,但条件是这些人的重量之和最多为 `limit`。 **要求**:返回载到每一个人所需的最小船数(保证每个人都能被船载)。 **说明**: - $1 \le people.length \le 5 \times 10^4$。 - $1 \le people[i] \le limit \le 3 \times 10^4$。 **示例**: - 示例 1: ```python 输入:people = [1,2], limit = 3 输出:1 解释:1 艘船载 (1, 2) ``` - 示例 2: ```python 输入:people = [3,2,2,1], limit = 3 输出:3 解释:3 艘船分别载 (1, 2), (2) 和 (3) ``` ## 解题思路 ### 思路 1:贪心算法 + 双指针 暴力枚举的时间复杂度为 $O(n^2)$。使用双指针可以减少循环内的时间复杂度。 我们可以利用贪心算法的思想,让最重的和最轻的人一起走。这样一只船就可以尽可能的带上两个人。 具体做法如下: 1. 先对数组进行升序排序,使用 `ans` 记录所需最小船数。 2. 使用两个指针 `left`、`right`。`left` 指向数组开始位置,`right` 指向数组结束位置。 3. 判断 `people[left]` 和 `people[right]` 加一起是否超重。 1. 如果 `people[left] + people[right] > limit`,则让重的人上船,船数量 + 1,令 `right` 左移,继续判断。 2. 如果 `people[left] + people[right] <= limit`,则两个人都上船,船数量 + 1,并令 `left` 右移,`right` 左移,继续判断。 4. 如果 `lefft == right`,则让最后一个人上船,船数量 + 1。并返回答案。 ### 思路 1:代码 ```python class Solution: def numRescueBoats(self, people: List[int], limit: int) -> int: people.sort() size = len(people) left, right = 0, size - 1 ans = 0 while left < right: if people[left] + people[right] > limit: right -= 1 else: left += 1 right -= 1 ans += 1 if left == right: ans += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 是数组 `people` 的长度。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0800-0899/bricks-falling-when-hit.md ================================================ # [0803. 打砖块](https://leetcode.cn/problems/bricks-falling-when-hit/) - 标签:并查集、数组、矩阵 - 难度:困难 ## 题目链接 - [0803. 打砖块 - 力扣](https://leetcode.cn/problems/bricks-falling-when-hit/) ## 题目大意 **描述**:给定一个 $m \times n$ 大小的二元网格,其中 $1$ 表示砖块,$0$ 表示空白。砖块稳定(不会掉落)的前提是: - 一块砖直接连接到网格的顶部。 - 或者至少有一块相邻(4 个方向之一)砖块稳定不会掉落时。 再给定一个数组 $hits$,这是需要依次消除砖块的位置。每当消除 $hits[i] = (row_i, col_i)$ 位置上的砖块时,对应位置的砖块(如果存在)会消失,然后其他的砖块可能因为这一消除操作而掉落。一旦砖块掉落,它会立即从网格中消失(即,它不会落在其他稳定的砖块上)。 **要求**:返回一个数组 $result$,其中 $result[i]$ 表示第 $i$ 次消除操作对应掉落的砖块数目。 **说明**: - 消除可能指向是没有砖块的空白位置,如果发生这种情况,则没有砖块掉落。 - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 200$。 - $grid[i][j]$ 为 $0$ 或 $1$。 - $1 \le hits.length \le 4 \times 10^4$。 - $hits[i].length == 2$。 - $0 \le xi \le m - 1$。 - $0 \le yi \le n - 1$。 - 所有 $(xi, yi)$ 互不相同。 **示例**: - 示例 1: ```python 输入:grid = [[1,0,0,0],[1,1,1,0]], hits = [[1,0]] 输出:[2] 解释:网格开始为: [[1,0,0,0], [1,1,1,0]] 消除 (1,0) 处加粗的砖块,得到网格: [[1,0,0,0] [0,1,1,0]] 两个加粗的砖不再稳定,因为它们不再与顶部相连,也不再与另一个稳定的砖相邻,因此它们将掉落。得到网格: [[1,0,0,0], [0,0,0,0]] 因此,结果为 [2]。 ``` - 示例 2: ```python 输入:grid = [[1,0,0,0],[1,1,0,0]], hits = [[1,1],[1,0]] 输出:[0,0] 解释:网格开始为: [[1,0,0,0], [1,1,0,0]] 消除 (1,1) 处加粗的砖块,得到网格: [[1,0,0,0], [1,0,0,0]] 剩下的砖都很稳定,所以不会掉落。网格保持不变: [[1,0,0,0], [1,0,0,0]] 接下来消除 (1,0) 处加粗的砖块,得到网格: [[1,0,0,0], [0,0,0,0]] 剩下的砖块仍然是稳定的,所以不会有砖块掉落。 因此,结果为 [0,0]。 ``` ## 解题思路 ### 思路 1:并查集 一个很直观的想法: - 将所有砖块放入一个集合中。 - 根据 $hits$ 数组的顺序,每敲掉一块砖。则将这块砖与相邻(4 个方向)的砖块断开集合。 - 然后判断哪些砖块会掉落,从集合中删除会掉落的砖块,并统计掉落砖块的数量。 - **掉落砖块的数目 = 击碎砖块之前与屋顶相连的砖块数目 - 击碎砖块之后与屋顶相连的砖块数目 - 1**。 涉及集合问题,很容易想到用并查集来做。但是并查集主要用于合并查找集合,不适合断开集合。我们可以反向思考问题: - 先将 $hits$ 中的所有位置上的砖块敲掉。 - 将剩下的砖块建立并查集。 - 逆序填回被敲掉的砖块,并与相邻(4 个方向)的砖块合并。这样问题就变为了 **补上砖块会新增多少个砖块粘到屋顶**。 整个算法步骤具体如下: 1. 先将二维数组 $grid$ 复制一份到二维数组 $copy\_gird$ 上。这是因为遍历 $hits$ 元素时需要判断原网格是空白还是被打碎的砖块。 2. 在 $copy\_grid$ 中将 $hits$ 中打碎的砖块赋值为 $0$。 3. 建立并查集,将房顶上的砖块合并到一个集合中。 4. 逆序遍历 $hits$,将 $hits$ 中的砖块补到 $copy\_grid$ 中,并计算每一步中有多少个砖块粘到屋顶上(与屋顶砖块在一个集合中),并存入答案数组对应位置。 5. 最后输出答案数组。 ### 思路 1:代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] self.size = [1 for _ in range(n)] def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return False self.parent[root_x] = root_y self.size[root_y] += self.size[root_x] return True def is_connected(self, x, y): return self.find(x) == self.find(y) def get_size(self, x): root_x = self.find(x) return self.size[root_x] class Solution: def hitBricks(self, grid: List[List[int]], hits: List[List[int]]) -> List[int]: directions = {(0, 1), (1, 0), (-1, 0), (0, -1)} rows, cols = len(grid), len(grid[0]) def is_area(x, y): return 0 <= x < rows and 0 <= y < cols def get_index(x, y): return x * cols + y copy_grid = [[grid[i][j] for j in range(cols)] for i in range(rows)] for hit in hits: copy_grid[hit[0]][hit[1]] = 0 union_find = UnionFind(rows * cols + 1) for j in range(cols): if copy_grid[0][j] == 1: union_find.union(j, rows * cols) for i in range(1, rows): for j in range(cols): if copy_grid[i][j] == 1: if copy_grid[i - 1][j] == 1: union_find.union(get_index(i - 1, j), get_index(i, j)) if j > 0 and copy_grid[i][j - 1] == 1: union_find.union(get_index(i, j - 1), get_index(i, j)) size_hits = len(hits) res = [0 for _ in range(size_hits)] for i in range(size_hits - 1, -1, -1): x, y = hits[i][0], hits[i][1] if grid[x][y] == 0: continue origin = union_find.get_size(rows * cols) if x == 0: union_find.union(y, rows * cols) for direction in directions: new_x = x + direction[0] new_y = y + direction[1] if is_area(new_x, new_y) and copy_grid[new_x][new_y] == 1: union_find.union(get_index(x, y), get_index(new_x, new_y)) curr = union_find.get_size(rows * cols) res[i] = max(0, curr - origin - 1) copy_grid[x][y] = 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n \times \alpha(m \times n))$,其中 $\alpha$ 是反 Ackerman 函数。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/0800-0899/buddy-strings.md ================================================ # [0859. 亲密字符串](https://leetcode.cn/problems/buddy-strings/) - 标签:哈希表、字符串 - 难度:简单 ## 题目链接 - [0859. 亲密字符串 - 力扣](https://leetcode.cn/problems/buddy-strings/) ## 题目大意 **描述**: 给定两个字符串 $s$ 和 $goal$。 **要求**: 只要我们可以通过交换 $s$ 中的两个字母得到与 $goal$ 相等的结果,就返回 true;否则返回 false。 **说明**: - 交换字母的定义是:取两个下标 $i$ 和 $j$(下标从 0 开始)且满足 $i \ne j$ ,接着交换 $s[i]$ 和 $s[j]$ 处的字符。 - 例如,在 `"abcd"` 中交换下标 0 和下标 2 的元素可以生成 `"cbad"`。 - $1 \le s.length, goal.length \le 2 \times 10^{4}$。 - $s$ 和 $goal$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "ab", goal = "ba" 输出:true 解释:你可以交换 s[0] = 'a' 和 s[1] = 'b' 生成 "ba",此时 s 和 goal 相等。 ``` - 示例 2: ```python 输入:s = "ab", goal = "ab" 输出:false 解释:你只能交换 s[0] = 'a' 和 s[1] = 'b' 生成 "ba",此时 s 和 goal 不相等。 ``` ## 解题思路 ### 思路 1:分情况讨论 这道题要求判断是否可以通过交换 $s$ 中的两个字母得到 $goal$。需要分情况讨论: 1. **长度不同**:如果 $s$ 和 $goal$ 长度不同,直接返回 $False$。 2. **字符串相同**:如果 $s$ 和 $goal$ 完全相同,需要判断 $s$ 中是否有重复字符。如果有重复字符,可以交换这两个相同的字符,返回 $True$;否则返回 $False$。 3. **字符串不同**:找出所有不同的位置,如果不同位置的数量不等于 2,返回 $False$。如果恰好有 2 个不同位置,检查交换后是否能得到 $goal$。 ### 思路 1:代码 ```python class Solution: def buddyStrings(self, s: str, goal: str) -> bool: # 长度不同,直接返回 False if len(s) != len(goal): return False # 如果两个字符串相同 if s == goal: # 检查是否有重复字符,有则可以交换 return len(set(s)) < len(s) # 找出所有不同的位置 diff = [] for i in range(len(s)): if s[i] != goal[i]: diff.append(i) # 不同位置必须恰好为 2 个 if len(diff) != 2: return False # 检查交换后是否能得到 goal i, j = diff[0], diff[1] return s[i] == goal[j] and s[j] == goal[i] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串的长度。需要遍历字符串一次。 - **空间复杂度**:$O(n)$,使用集合存储字符需要 $O(n)$ 空间。 ================================================ FILE: docs/solutions/0800-0899/bus-routes.md ================================================ # [0815. 公交路线](https://leetcode.cn/problems/bus-routes/) - 标签:广度优先搜索、数组、哈希表 - 难度:困难 ## 题目链接 - [0815. 公交路线 - 力扣](https://leetcode.cn/problems/bus-routes/) ## 题目大意 **描述**: 给定一个数组 $routes$ ,表示一系列公交线路,其中每个 $routes[i]$ 表示一条公交线路,第 $i$ 辆公交车将会在上面循环行驶。 - 例如,路线 $routes[0] = [1, 5, 7]$ 表示第 0 辆公交车会一直按序列 $1 \rightarrow 5 \rightarrow 7 \rightarrow 1 \rightarrow 5 \rightarrow 7 \rightarrow 1 \rightarrow ...$ 这样的车站路线行驶。 现在从 $source$ 车站出发(初始时不在公交车上),要前往 $target$ 车站。 期间仅可乘坐公交车。 **要求**: 求出「最少乘坐的公交车数量」。如果不可能到达终点车站,返回 $-1$。 **说明**: - $1 \le routes.length \le 500.$。 - $1 \le routes[i].length \le 10^{5}$。 - $routes[i]$ 中的所有值互不相同。 - $sum(routes[i].length) \le 10^{5}$。 - $0 \le routes[i][j] \lt 10^{6}$。 - $0 \le source, target \lt 10^{6}$。 **示例**: - 示例 1: ```python 输入:routes = [[1,2,7],[3,6,7]], source = 1, target = 6 输出:2 解释:最优策略是先乘坐第一辆公交车到达车站 7 , 然后换乘第二辆公交车到车站 6 。 ``` - 示例 2: ```python 输入:routes = [[7,12],[4,5,15],[6],[15,19],[9,12,13]], source = 15, target = 12 输出:-1 ``` ## 解题思路 ### 思路 1:BFS(广度优先搜索) 这道题要求找到从起点到终点的最少乘坐公交车数量。可以将问题转化为图的最短路径问题: - 每个公交站是一个节点。 - 如果两个站在同一条公交线路上,它们之间有边相连。 但直接建图会导致边数过多。更好的方法是: - 将公交线路作为节点。 - 从起点站开始,找到所有经过该站的公交线路。 - 对于每条线路,找到该线路上的所有站点,再找到这些站点对应的其他线路。 - 使用 BFS 搜索最短路径。 算法步骤: 1. 如果起点等于终点,返回 0。 2. 建立站点到公交线路的映射。 3. 使用 BFS,从起点站开始: - 找到所有经过该站的公交线路。 - 对于每条线路,遍历该线路上的所有站点。 - 如果到达终点站,返回当前乘坐的公交车数量。 - 否则,将新站点加入队列。 4. 使用 $\text{visited}$ 集合记录已访问的线路和站点,避免重复访问。 ### 思路 1:代码 ```python class Solution: def numBusesToDestination(self, routes: List[List[int]], source: int, target: int) -> int: from collections import defaultdict, deque # 如果起点等于终点,直接返回 0 if source == target: return 0 # 建立站点到公交线路的映射 stop_to_routes = defaultdict(set) for i, route in enumerate(routes): for stop in route: stop_to_routes[stop].add(i) # BFS queue = deque([source]) visited_stops = {source} visited_routes = set() buses = 0 while queue: buses += 1 # 遍历当前层的所有站点 for _ in range(len(queue)): stop = queue.popleft() # 找到所有经过该站的公交线路 for route_idx in stop_to_routes[stop]: # 如果该线路已访问,跳过 if route_idx in visited_routes: continue visited_routes.add(route_idx) # 遍历该线路上的所有站点 for next_stop in routes[route_idx]: # 如果到达终点,返回结果 if next_stop == target: return buses # 如果该站点未访问,加入队列 if next_stop not in visited_stops: visited_stops.add(next_stop) queue.append(next_stop) # 无法到达终点 return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(N \times S)$,其中 $N$ 是公交线路的数量,$S$ 是每条线路的平均站点数。每条线路最多被访问一次,每次访问需要遍历该线路上的所有站点。 - **空间复杂度**:$O(N \times S)$,需要存储站点到线路的映射和 BFS 队列。 ================================================ FILE: docs/solutions/0800-0899/car-fleet.md ================================================ # [0853. 车队](https://leetcode.cn/problems/car-fleet/) - 标签:栈、数组、排序、单调栈 - 难度:中等 ## 题目链接 - [0853. 车队 - 力扣](https://leetcode.cn/problems/car-fleet/) ## 题目大意 **描述**: 在一条单行道上,有 $n$ 辆车开往同一目的地。目的地是几英里以外的 $target$。 给定两个整数数组 $position$ 和 $speed$,长度都是 $n$,其中 $position[i]$ 是第 $i$ 辆车的位置,$speed[i]$ 是第 $i$ 辆车的速度(单位是英里/小时)。 一辆车永远不会超过前面的另一辆车,但它可以追上去,并以较慢车的速度在另一辆车旁边行驶。 车队 是指并排行驶的一辆或几辆汽车。车队的速度是车队中「最慢」的车的速度。 即便一辆车在 $target$ 才赶上了一个车队,它们仍然会被视作是同一个车队。 **要求**: 返回到达目的地的车队数量。 **说明**: - $n == position.length == speed.length$。 - $1 \le n \le 10^{5}$。 - $0 \lt target \le 10^{6}$。 - $0 \le position[i] \lt target$。 - $position$ 中每个值都「不同」。 - $0 \lt speed[i] \le 10^{6}$。 **示例**: - 示例 1: ```python 输入:target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3] 输出:3 解释: * 从 10(速度为 2)和 8(速度为 4)开始的车会组成一个车队,它们在 12 相遇。车队在 target 形成。 * 从 0(速度为 1)开始的车不会追上其它任何车,所以它自己是一个车队。 * 从 5(速度为 1) 和 3(速度为 3)开始的车组成一个车队,在 6 相遇。车队以速度 1 移动直到它到达 target。 ``` - 示例 2: ```python 输入:target = 10, position = [3], speed = [3] 输出:1 解释:只有一辆车,因此只有一个车队。 ``` ## 解题思路 ### 思路 1:排序 + 单调栈 这道题的关键是理解车队的形成:后面的车如果能在到达终点前追上前面的车,它们就会形成车队,以较慢的速度行驶。 算法步骤: 1. 将车按照位置从大到小排序(从离终点近到远)。 2. 计算每辆车到达终点所需的时间。 3. 使用单调栈(或变量)维护车队: - 从离终点最近的车开始遍历。 - 如果当前车的到达时间大于栈顶车队的到达时间,说明它无法追上前面的车队,形成新的车队。 - 否则,它会追上前面的车队,合并为一个车队。 ### 思路 1:代码 ```python class Solution: def carFleet(self, target: int, position: List[int], speed: List[int]) -> int: # 将车按位置从大到小排序 cars = sorted(zip(position, speed), reverse=True) # 单调栈,存储每个车队到达终点的时间 stack = [] for pos, spd in cars: # 计算当前车到达终点的时间 time = (target - pos) / spd # 如果栈为空,或当前车无法追上前面的车队,形成新车队 if not stack or time > stack[-1]: stack.append(time) # 否则,当前车会追上前面的车队,合并(不需要操作) # 栈的大小就是车队数量 return len(stack) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是车的数量。排序需要 $O(n \log n)$,遍历需要 $O(n)$。 - **空间复杂度**:$O(n)$,需要存储排序后的车辆信息和栈。 ================================================ FILE: docs/solutions/0800-0899/card-flipping-game.md ================================================ # [0822. 翻转卡片游戏](https://leetcode.cn/problems/card-flipping-game/) - 标签:数组、哈希表 - 难度:中等 ## 题目链接 - [0822. 翻转卡片游戏 - 力扣](https://leetcode.cn/problems/card-flipping-game/) ## 题目大意 **描述**: 在桌子上有 $n$ 张卡片,每张卡片的正面和背面都写着一个正数(正面与背面上的数有可能不一样)。 我们可以先翻转任意张卡片,然后选择其中一张卡片。 如果选中的那张卡片背面的数字 $x$ 与任意一张卡片的正面的数字都不同,那么这个数字是我们想要的数字。 如果我们通过翻转卡片来交换正面与背面上的数,那么当初在正面的数就变成背面的数,背面的数就变成正面的数。 给定两个长度为 $n$ 的数组 $fronts[i]$ 和 $backs[i]$,分别代表第 $i$ 张卡片的正面和背面的数字。 **要求**: 计算出哪个数是这些想要的数字中最小的数(找到这些数中的最小值)。如果没有一个数字符合要求的,输出 $0$。 **说明**: - $n == fronts.length == backs.length$。 - $1 \le n \le 10^{3}$。 - $1 \le fronts[i], backs[i] \le 2000$。 **示例**: - 示例 1: ```python 输入:fronts = [1,2,4,4,7], backs = [1,3,4,1,3] 输出:2 解释:假设我们翻转第二张卡片,那么在正面的数变成了 [1,3,4,4,7] , 背面的数变成了 [1,2,4,1,3]。 接着我们选择第二张卡片,因为现在该卡片的背面的数是 2,2 与任意卡片上正面的数都不同,所以 2 就是我们想要的数字。 ``` - 示例 2: ```python 输入:fronts = [1], backs = [1] 输出:0 解释: 无论如何翻转都无法得到想要的数字,所以返回 0 。 ``` ## 解题思路 ### 思路 1:哈希表 这道题的关键是理解:如果一张卡片的正面和背面数字相同,那么这个数字永远不可能成为答案(因为无论怎么翻转,这个数字都会出现在某张卡片的正面)。 算法步骤: 1. 找出所有正反面数字相同的卡片,将这些数字加入「禁止集合」。 2. 遍历所有卡片的正面和背面,找出不在禁止集合中的最小数字。 ### 思路 1:代码 ```python class Solution: def flipgame(self, fronts: List[int], backs: List[int]) -> int: n = len(fronts) # 找出所有正反面相同的数字,这些数字不能作为答案 forbidden = set() for i in range(n): if fronts[i] == backs[i]: forbidden.add(fronts[i]) # 找出所有可能的数字中的最小值 result = float('inf') # 遍历所有正面和背面的数字 for num in fronts + backs: if num not in forbidden: result = min(result, num) # 如果没有找到合适的数字,返回 0 return result if result != float('inf') else 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是卡片的数量。需要遍历所有卡片两次。 - **空间复杂度**:$O(n)$,需要使用哈希表存储禁止的数字。 ================================================ FILE: docs/solutions/0800-0899/chalkboard-xor-game.md ================================================ # [0810. 黑板异或游戏](https://leetcode.cn/problems/chalkboard-xor-game/) - 标签:位运算、脑筋急转弯、数组、数学、博弈 - 难度:困难 ## 题目链接 - [0810. 黑板异或游戏 - 力扣](https://leetcode.cn/problems/chalkboard-xor-game/) ## 题目大意 **描述**: 黑板上写着一个非负整数数组 $nums[i]$。 Alice 和 Bob 轮流从黑板上擦掉一个数字,Alice 先手。如果擦除一个数字后,剩余的所有数字按位异或运算得出的结果等于 0 的话,当前玩家游戏失败。另外,如果只剩一个数字,按位异或运算得到它本身;如果无数字剩余,按位异或运算结果为 0。 并且,轮到某个玩家时,如果当前黑板上所有数字按位异或运算结果等于 0,这个玩家获胜。 **要求**: 假设两个玩家每步都使用最优解,当且仅当 Alice 获胜时返回 true,$Bob$ 获胜时返回 false。 **说明**: - $1 \le nums.length \le 10^{3}$。 - $0 \le nums[i] \lt 2^{16}$。 **示例**: - 示例 1: ```python 输入: nums = [1,1,2] 输出: false 解释: Alice 有两个选择: 擦掉数字 1 或 2。 如果擦掉 1, 数组变成 [1, 2]。剩余数字按位异或得到 1 XOR 2 = 3。那么 Bob 可以擦掉任意数字,因为 Alice 会成为擦掉最后一个数字的人,她总是会输。 如果 Alice 擦掉 2,那么数组变成[1, 1]。剩余数字按位异或得到 1 XOR 1 = 0。Alice 仍然会输掉游戏。 ``` - 示例 2: ```python 输入: nums = [0,1] 输出: true ``` ## 解题思路 ### 思路 1:博弈论 + 数学 这道题是一个博弈问题,需要分析在最优策略下谁会获胜。 关键观察: 1. 如果初始状态所有数字的异或结果为 0,Alice 直接获胜。 2. 如果数组长度为偶数,Alice 必胜。原因如下: - 假设所有数字的异或结果为 $x \neq 0$。 - 由于异或运算的性质,至少有一个数字 $nums[i]$ 使得 $x \oplus nums[i] \neq 0$(否则所有数字异或结果为 0)。 - 在 Alice 的回合,如果她无法找到一个数字使得擦除后异或结果不为 0,说明擦除任何数字后异或结果都为 0,这意味着所有数字都相同且等于 $x$。 - 但如果所有数字都相同,数组长度为偶数时,异或结果应该为 0,矛盾。 - 因此,Alice 总能找到一个数字擦除,使得游戏继续,最终 Bob 会面临奇数个数字的局面。 3. 如果数组长度为奇数且异或结果不为 0,Bob 必胜。 结论:Alice 获胜的条件是:初始异或结果为 0,或数组长度为偶数。 ### 思路 1:代码 ```python class Solution: def xorGame(self, nums: List[int]) -> bool: # 计算所有数字的异或结果 xor_sum = 0 for num in nums: xor_sum ^= num # Alice 获胜的条件:异或结果为 0,或数组长度为偶数 return xor_sum == 0 or len(nums) % 2 == 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。需要遍历数组计算异或结果。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0800-0899/consecutive-numbers-sum.md ================================================ # [0829. 连续整数求和](https://leetcode.cn/problems/consecutive-numbers-sum/) - 标签:数学、枚举 - 难度:困难 ## 题目链接 - [0829. 连续整数求和 - 力扣](https://leetcode.cn/problems/consecutive-numbers-sum/) ## 题目大意 **描述**: 给定一个正整数 $n$。 **要求**: 返回连续正整数满足所有数字之和为 $n$ 的组数。 **说明**: - $1 \le n \le 10^{9}$。 **示例**: - 示例 1: ```python 输入: n = 5 输出: 2 解释: 5 = 2 + 3,共有两组连续整数([5],[2,3])求和后为 5。 ``` - 示例 2: ```python 输入: n = 9 输出: 3 解释: 9 = 4 + 5 = 2 + 3 + 4 ``` ## 解题思路 ### 思路 1:数学 + 枚举 假设有 $k$ 个连续正整数,首项为 $x$,则它们的和为: $$x + (x+1) + (x+2) + \cdots + (x+k-1) = k \times x + \frac{k \times (k-1)}{2} = n$$ 化简得: $$x = \frac{n - \frac{k \times (k-1)}{2}}{k}$$ 要使 $x$ 为正整数,需要满足: 1. $n - \frac{k \times (k-1)}{2} > 0$,即 $k < \sqrt{2n} + 1$。 2. $n - \frac{k \times (k-1)}{2}$ 能被 $k$ 整除。 算法步骤: 1. 枚举 $k$ 从 1 到 $\sqrt{2n}$。 2. 对于每个 $k$,检查是否满足上述两个条件。 3. 如果满足,计数加 1。 ### 思路 1:代码 ```python class Solution: def consecutiveNumbersSum(self, n: int) -> int: count = 0 # 枚举连续整数的个数 k # k 的上界为 sqrt(2n) k = 1 while k * (k - 1) // 2 < n: # 检查 n - k*(k-1)/2 是否能被 k 整除 if (n - k * (k - 1) // 2) % k == 0: count += 1 k += 1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\sqrt{n})$,需要枚举 $k$ 从 1 到 $\sqrt{2n}$。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0800-0899/construct-binary-tree-from-preorder-and-postorder-traversal.md ================================================ # [0889. 根据前序和后序遍历构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal/) - 标签:树、数组、哈希表、分治、二叉树 - 难度:中等 ## 题目链接 - [0889. 根据前序和后序遍历构造二叉树 - 力扣](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-postorder-traversal/) ## 题目大意 **描述**:给定一棵无重复值二叉树的前序遍历结果 `preorder` 和后序遍历结果 `postorder`。 **要求**:构造出该二叉树并返回其根节点。如果存在多个答案,则可以返回其中任意一个。 **说明**: - $1 \le preorder.length \le 30$。 - $1 \le preorder[i] \le preorder.length$。 - `preorder` 中所有值都不同。 - `postorder.length == preorder.length`。 - $1 \le postorder[i] \le postorder.length$。 - `postorder` 中所有值都不同。 - 保证 `preorder` 和 `postorder` 是同一棵二叉树的前序遍历和后序遍历。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/07/24/lc-prepost.jpg) ```python 输入:preorder = [1,2,4,5,3,6,7], postorder = [4,5,2,6,7,3,1] 输出:[1,2,3,4,5,6,7] ``` - 示例 2: ```python 输入: preorder = [1], postorder = [1] 输出: [1] ``` ## 解题思路 ### 思路 1:递归 如果已知二叉树的前序遍历序列和后序遍历序列,是不能唯一地确定一棵二叉树的。这是因为没有中序遍历序列无法确定左右部分,也就无法进行子序列的分割。 只有二叉树中每个节点度为 `2` 或者 `0` 的时候,已知前序遍历序列和后序遍历序列,才能唯一地确定一颗二叉树,如果二叉树中存在度为 `1` 的节点时是无法唯一地确定一棵二叉树的,这是因为我们无法判断该节点是左子树还是右子树。 而这道题说明了,如果存在多个答案,则可以返回其中任意一个。 我们可以默认指定前序遍历序列的第 `2` 个值为左子树的根节点,由此递归划分左右子序列。具体操作步骤如下: 1. 从前序遍历序列中可知当前根节点的位置在 `preorder[0]`。 2. 前序遍历序列的第 `2` 个值为左子树的根节点,即 `preorder[1]`。通过在后序遍历中查找上一步根节点对应的位置 `postorder[k]`(该节点右侧为右子树序列),从而将二叉树的左右子树分隔开,并得到左右子树节点的个数。 3. 从上一步得到的左右子树个数将后序遍历结果中的左右子树分开。 4. 构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。 ### 思路 1:代码 ```python class Solution: def constructFromPrePost(self, preorder: List[int], postorder: List[int]) -> TreeNode: def createTree(preorder, postorder, n): if n == 0: return None node = TreeNode(preorder[0]) if n == 1: return node k = 0 while postorder[k] != preorder[1]: k += 1 node.left = createTree(preorder[1: k + 2], postorder[: k + 1], k + 1) node.right = createTree(preorder[k + 2: ], postorder[k + 1: -1], n - k - 2) return node return createTree(preorder, postorder, len(preorder)) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/0800-0899/count-unique-characters-of-all-substrings-of-a-given-string.md ================================================ # [0828. 统计子串中的唯一字符](https://leetcode.cn/problems/count-unique-characters-of-all-substrings-of-a-given-string/) - 标签:哈希表、字符串、动态规划 - 难度:困难 ## 题目链接 - [0828. 统计子串中的唯一字符 - 力扣](https://leetcode.cn/problems/count-unique-characters-of-all-substrings-of-a-given-string/) ## 题目大意 **描述**: 我们定义了一个函数 `countUniqueChars(s)` 来统计字符串 $s$ 中的唯一字符,并返回唯一字符的个数。 例如:`s = "LEETCODE"`,则其中 `"L"`, `"T"`, `"C"`, "O","D" 都是唯一字符,因为它们只出现一次,所以 `countUniqueChars(s) = 5`。 给定一个字符串 $s$。 **要求**: 返回 `countUniqueChars(t)` 的总和,其中 $t$ 是 $s$ 的子字符串。输入用例保证返回值为 32 位整数。 **说明**: - 注意:某些子字符串可能是重复的,但你统计时也必须算上这些重复的子字符串(也就是说,你必须统计 $s$ 的所有子字符串中的唯一字符)。 - $1 \le s.length \le 10^{5}$。 - $s$ 只包含大写英文字符。 **示例**: - 示例 1: ```python 输入: s = "ABC" 输出: 10 解释: 所有可能的子串为:"A","B","C","AB","BC" 和 "ABC"。 其中,每一个子串都由独特字符构成。 所以其长度总和为:1 + 1 + 1 + 2 + 2 + 3 = 10 ``` - 示例 2: ```python 输入: s = "ABA" 输出: 8 解释: 除了 countUniqueChars("ABA") = 1 之外,其余与示例 1 相同。 ``` ## 解题思路 ### 思路 1:贡献法 这道题如果暴力枚举所有子串会超时。我们需要换个角度思考:计算每个字符作为唯一字符对答案的贡献。 关键观察: - 对于字符串中的某个字符 $s[i]$,它在哪些子串中是唯一字符? - 答案是:子串的左边界在 $s[i]$ 左侧最近的相同字符之后,右边界在 $s[i]$ 右侧最近的相同字符之前。 算法步骤: 1. 对于每个字符,记录它在字符串中所有出现的位置。 2. 对于每个位置 $i$ 的字符 $s[i]$: - 找到它左侧最近的相同字符位置 $left$(如果没有,则为 $-1$)。 - 找到它右侧最近的相同字符位置 $right$(如果没有,则为 $n$)。 - 该字符作为唯一字符的贡献为:$(i - left) \times (right - i)$。 3. 累加所有字符的贡献。 ### 思路 1:代码 ```python class Solution: def uniqueLetterString(self, s: str) -> int: from collections import defaultdict n = len(s) # 记录每个字符出现的所有位置 pos = defaultdict(list) for i, ch in enumerate(s): pos[ch].append(i) result = 0 # 遍历每个字符 for ch, indices in pos.items(): # 在位置列表前后添加哨兵 indices = [-1] + indices + [n] # 计算每个位置的贡献 for i in range(1, len(indices) - 1): left = indices[i - 1] # 左侧最近的相同字符位置 mid = indices[i] # 当前位置 right = indices[i + 1] # 右侧最近的相同字符位置 # 当前字符作为唯一字符的贡献 result += (mid - left) * (right - mid) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串的长度。需要遍历字符串两次。 - **空间复杂度**:$O(n)$,需要存储每个字符的位置列表。 ================================================ FILE: docs/solutions/0800-0899/decoded-string-at-index.md ================================================ # [0880. 索引处的解码字符串](https://leetcode.cn/problems/decoded-string-at-index/) - 标签:栈、字符串 - 难度:中等 ## 题目链接 - [0880. 索引处的解码字符串 - 力扣](https://leetcode.cn/problems/decoded-string-at-index/) ## 题目大意 **描述**: 给定一个编码字符串 $s$。请你找出「解码字符串」并将其写入磁带。解码时,从编码字符串中 每次读取一个字符 ,并采取以下步骤: - 如果所读的字符是字母,则将该字母写在磁带上。 - 如果所读的字符是数字(例如 $d$),则整个当前磁带总共会被重复写 $d-1$ 次。 **要求**: 现在,对于给定的编码字符串 $s$ 和索引 $k$,查找并返回解码字符串中的第 $k$ 个字母。 **说明**: - $2 \le s.length \le 10^{3}$。 - $s$ 只包含小写字母与数字 2 到 9 。 - $s$ 以字母开头。 - $1 \le k \le 10^{9}$。 - 题目保证 $k$ 小于或等于解码字符串的长度。 - 解码后的字符串保证少于 263 个字母。 **示例**: - 示例 1: ```python 输入:s = "leet2code3", k = 10 输出:"o" 解释: 解码后的字符串为 "leetleetcodeleetleetcodeleetleetcode"。 字符串中的第 10 个字母是 "o"。 ``` - 示例 2: ```python 输入:s = "ha22", k = 5 输出:"h" 解释: 解码后的字符串为 "hahahaha"。第 5 个字母是 "h"。 ``` ## 解题思路 ### 思路 1:逆向思维 这道题如果直接构建解码字符串会超时(解码后的字符串可能非常长)。我们需要逆向思考: 关键观察: - 如果当前字符是数字 $d$,解码后的长度会变为原来的 $d$ 倍。 - 如果当前字符是字母,解码后的长度加 1。 算法步骤: 1. 先正向遍历,计算解码后的总长度 $size$。 2. 然后逆向遍历: - 如果遇到数字 $d$,说明当前段是前面内容重复 $d$ 次,将 $size$ 除以 $d$,同时 $k$ 对 $size$ 取模(因为是重复的)。 - 如果遇到字母,$size$ 减 1。如果此时 $k$ 等于 0 或 $k$ 等于 $size$,说明找到了答案。 ### 思路 1:代码 ```python class Solution: def decodeAtIndex(self, s: str, k: int) -> str: # 计算解码后的总长度 size = 0 for c in s: if c.isdigit(): size *= int(c) else: size += 1 # 逆向遍历 for i in range(len(s) - 1, -1, -1): c = s[i] # k 对 size 取模 k %= size # 如果 k 为 0 且当前是字母,返回该字母 if k == 0 and c.isalpha(): return c # 更新 size if c.isdigit(): size //= int(c) else: size -= 1 return "" ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。需要遍历字符串两次。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0800-0899/exam-room.md ================================================ # [0855. 考场就座](https://leetcode.cn/problems/exam-room/) - 标签:设计、有序集合、堆(优先队列) - 难度:中等 ## 题目链接 - [0855. 考场就座 - 力扣](https://leetcode.cn/problems/exam-room/) ## 题目大意 **描述**: 在考场里,有 $n$ 个座位排成一行,编号为 0 到 $n - 1$。 当学生进入考场后,他必须坐在离最近的人最远的座位上。如果有多个这样的座位,他会坐在编号最小的座位上。(另外,如果考场里没有人,那么学生就坐在 0 号座位上) **要求**: 设计一个模拟所述考场的类。 实现 ExamRoom 类: - `ExamRoom(int n)` 用座位的数量 $n$ 初始化考场对象。 - `int seat()` 返回下一个学生将会入座的座位编号。 - `void leave(int p)` 指定坐在座位 $p$ 的学生将离开教室。保证座位 $p$ 上会有一位学生。 **说明**: - $1 \le n \le 10^{9}$ - 保证有学生正坐在座位 $p$ 上。 - $seat$ 和 $leave$ 最多被调用 $10^{4}$ 次。 **示例**: - 示例 1: ```python 输入: ["ExamRoom", "seat", "seat", "seat", "seat", "leave", "seat"] [[10], [], [], [], [], [4], []] 输出: [null, 0, 9, 4, 2, null, 5] 解释: ExamRoom examRoom = new ExamRoom(10); examRoom.seat(); // 返回 0,房间里没有人,学生坐在 0 号座位。 examRoom.seat(); // 返回 9,学生最后坐在 9 号座位。 examRoom.seat(); // 返回 4,学生最后坐在 4 号座位。 examRoom.seat(); // 返回 2,学生最后坐在 2 号座位。 examRoom.leave(4); examRoom.seat(); // 返回 5,学生最后坐在 5 号座位。 ``` ## 解题思路 ### 思路 1:有序集合 + 贪心 这道题要求设计一个考场座位系统,学生入座时要坐在离最近的人最远的位置。 关键思路: - 使用有序集合(如 Python 的 `list` 或 `SortedList`)维护已坐学生的位置。 - 入座时,找到最大间隔的中点: - 检查第一个座位(0 号)到第一个学生的距离。 - 检查相邻两个学生之间的中点到最近学生的距离。 - 检查最后一个学生到最后一个座位($n-1$ 号)的距离。 - 选择距离最大的位置,如果有多个,选择编号最小的。 ### 思路 1:代码 ```python class ExamRoom: def __init__(self, n: int): self.n = n self.students = [] # 有序列表,存储已坐学生的位置 def seat(self) -> int: # 如果没有学生,坐在 0 号位置 if not self.students: self.students.append(0) return 0 # 计算最大距离和对应的座位 max_dist = self.students[0] # 第一个座位到第一个学生的距离 seat_pos = 0 # 检查相邻学生之间的中点 for i in range(len(self.students) - 1): left = self.students[i] right = self.students[i + 1] # 中点到最近学生的距离 dist = (right - left) // 2 if dist > max_dist: max_dist = dist seat_pos = left + dist # 检查最后一个学生到最后一个座位的距离 if self.n - 1 - self.students[-1] > max_dist: seat_pos = self.n - 1 # 将新学生插入有序列表 import bisect bisect.insort(self.students, seat_pos) return seat_pos def leave(self, p: int) -> None: # 移除学生 self.students.remove(p) # Your ExamRoom object will be instantiated and called as such: # obj = ExamRoom(n) # param_1 = obj.seat() # obj.leave(p) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - `seat` 操作:$O(n)$,需要遍历所有已坐学生,插入操作需要 $O(n)$。 - `leave` 操作:$O(n)$,需要查找并删除学生。 - **空间复杂度**:$O(n)$,需要存储所有已坐学生的位置。 ================================================ FILE: docs/solutions/0800-0899/expressive-words.md ================================================ # [0809. 情感丰富的文字](https://leetcode.cn/problems/expressive-words/) - 标签:数组、双指针、字符串 - 难度:中等 ## 题目链接 - [0809. 情感丰富的文字 - 力扣](https://leetcode.cn/problems/expressive-words/) ## 题目大意 **描述**: 有时候人们会用重复写一些字母来表示额外的感受,比如 `"hello"` -> `"heeellooo"`, `"hi"` -> `"hiii"`。我们将相邻字母都相同的一串字符定义为相同字母组,例如:`"h"`, `"eee"`,` "ll"`, `"ooo"`。 对于一个给定的字符串 $S$ ,如果另一个单词能够通过将一些字母组扩张从而使其和 $S$ 相同,我们将这个单词定义为可扩张的(stretchy)。 扩张操作定义如下:选择一个字母组(包含字母 $c$ ),然后往其中添加相同的字母 $c$ 使其长度达到 3 或以上。 - 例如,以 `"hello"` 为例,我们可以对字母组 `"o"` 扩张得到 `"hellooo"`,但是无法以同样的方法得到 `"helloo"` 因为字母组 `"oo"` 长度小于 3。此外,我们可以进行另一种扩张 `"ll" -> "lllll"` 以获得 `"helllllooo"`。如果 ·,那么查询词 "hello" 是可扩张的,因为可以对它执行这两种扩张操作使得 `query = "hello"` -> `"hellooo"` -> `"helllllooo" = s`。 给定字符串 $S$ 和一组查询单词 $words$。 **要求**: 输出其中可扩张的单词数量。 **说明**: - $1 \le s.length, words.length \le 10^{3}$。 - $1 \le words[i].length \le 10^{3}$。 - $s$ 和所有在 $words$ 中的单词都只由小写字母组成。 **示例**: - 示例 1: ```python 输入: s = "heeellooo" words = ["hello", "hi", "helo"] 输出:1 解释: 我们能通过扩张 "hello" 的 "e" 和 "o" 来得到 "heeellooo"。 我们不能通过扩张 "helo" 来得到 "heeellooo" 因为 "ll" 的长度小于 3 。 ``` ## 解题思路 ### 思路 1:双指针 这道题要求判断一个单词是否可以通过扩张字母组得到目标字符串 $s$。 关键规则: - 扩张操作只能将字母组扩张到长度 3 或以上。 - 如果原字母组长度已经是 3 或以上,可以继续扩张。 - 如果原字母组长度小于 3,不能扩张。 算法步骤: 1. 使用双指针分别遍历 $s$ 和 $word$。 2. 对于每个字母组,统计在 $s$ 和 $word$ 中的连续出现次数。 3. 判断是否可以扩张: - 如果字母不同,返回 $False$。 - 如果 $s$ 中的次数小于 $word$ 中的次数,无法扩张,返回 $False$。 - 如果 $s$ 中的次数大于 $word$ 中的次数,但 $s$ 中的次数小于 3,无法扩张,返回 $False$。 4. 如果所有字母组都满足条件,返回 $True$。 ### 思路 1:代码 ```python class Solution: def expressiveWords(self, s: str, words: List[str]) -> int: def check(word): """检查 word 是否可以扩张得到 s""" i, j = 0, 0 n, m = len(s), len(word) while i < n and j < m: # 如果字母不同,无法扩张 if s[i] != word[j]: return False # 统计 s 中当前字母的连续出现次数 ch = s[i] cnt_s = 0 while i < n and s[i] == ch: cnt_s += 1 i += 1 # 统计 word 中当前字母的连续出现次数 cnt_word = 0 while j < m and word[j] == ch: cnt_word += 1 j += 1 # 判断是否可以扩张 if cnt_s < cnt_word: # s 中的次数少于 word,无法扩张 return False if cnt_s > cnt_word and cnt_s < 3: # s 中的次数多于 word,但少于 3,无法扩张 return False # 检查是否都遍历完 return i == n and j == m # 统计可扩张的单词数量 count = 0 for word in words: if check(word): count += 1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是 $\text{words}$ 的长度,$m$ 是字符串 $s$ 的长度。需要对每个单词进行检查。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0800-0899/fair-candy-swap.md ================================================ # [0888. 公平的糖果交换](https://leetcode.cn/problems/fair-candy-swap/) - 标签:数组、哈希表、二分查找、排序 - 难度:简单 ## 题目链接 - [0888. 公平的糖果交换 - 力扣](https://leetcode.cn/problems/fair-candy-swap/) ## 题目大意 **描述**: 爱丽丝和鲍勃拥有不同总数量的糖果。给你两个数组 $aliceSizes$ 和 $bobSizes$,$aliceSizes[i]$ 是爱丽丝拥有的第 $i$ 盒糖果中的糖果数量,$bobSizes[j]$ 是鲍勃拥有的第 $j$ 盒糖果中的糖果数量。 两人想要互相交换一盒糖果,这样在交换之后,他们就可以拥有相同总数量的糖果。一个人拥有的糖果总数量是他们每盒糖果数量的总和。 **要求**: 返回一个整数数组 $answer$,其中 $answer[0]$ 是爱丽丝必须交换的糖果盒中的糖果的数目,$answer[1]$ 是鲍勃必须交换的糖果盒中的糖果的数目。 如果存在多个答案,你可以返回其中「任何一个」。题目测试用例保证存在与输入对应的答案。 **说明**: - $1 \le aliceSizes.length, bobSizes.length \le 10^{4}$。 - $1 \le aliceSizes[i], bobSizes[j] \le 10^{5}$。 - 爱丽丝和鲍勃的糖果总数量不同。 - 题目数据保证对于给定的输入至少存在一个有效答案。 **示例**: - 示例 1: ```python 输入:aliceSizes = [1,1], bobSizes = [2,2] 输出:[1,2] ``` - 示例 2: ```python 输入:aliceSizes = [1,2], bobSizes = [2,3] 输出:[1,2] ``` ## 解题思路 ### 思路 1:哈希表 + 数学 这道题要求找到一对糖果盒,使得交换后两人的糖果总数相等。 设爱丽丝的糖果总数为 $sumA$,鲍勃的糖果总数为 $sumB$。交换后两人糖果总数相等,即: $$sumA - x + y = sumB - y + x$$ 其中 $x$ 是爱丽丝交换出去的糖果数,$y$ 是鲍勃交换出去的糖果数。 化简得:$y = x + \frac{sumB - sumA}{2}$ 算法步骤: 1. 计算爱丽丝和鲍勃的糖果总数 $sumA$ 和 $sumB$。 2. 计算差值 $diff = \frac{sumB - sumA}{2}$。 3. 将鲍勃的糖果数存入哈希表,方便快速查找。 4. 遍历爱丽丝的糖果盒,对于每个 $x$,检查 $y = x + diff$ 是否在鲍勃的糖果盒中。 5. 如果找到,返回 $[x, y]$。 ### 思路 1:代码 ```python class Solution: def fairCandySwap(self, aliceSizes: List[int], bobSizes: List[int]) -> List[int]: # 计算爱丽丝和鲍勃的糖果总数 sumA = sum(aliceSizes) sumB = sum(bobSizes) # 计算差值 diff = (sumB - sumA) // 2 # 将鲍勃的糖果数存入哈希表 bobSet = set(bobSizes) # 遍历爱丽丝的糖果盒 for x in aliceSizes: y = x + diff # 如果 y 在鲍勃的糖果盒中,返回结果 if y in bobSet: return [x, y] return [] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 和 $m$ 分别是 $\text{aliceSizes}$ 和 $\text{bobSizes}$ 的长度。需要遍历两个数组。 - **空间复杂度**:$O(m)$,需要使用哈希表存储鲍勃的糖果数。 ================================================ FILE: docs/solutions/0800-0899/find-and-replace-in-string.md ================================================ # [0833. 字符串中的查找与替换](https://leetcode.cn/problems/find-and-replace-in-string/) - 标签:数组、哈希表、字符串、排序 - 难度:中等 ## 题目链接 - [0833. 字符串中的查找与替换 - 力扣](https://leetcode.cn/problems/find-and-replace-in-string/) ## 题目大意 **描述**: 给定一个字符串 $s$ (索引从 0 开始),你必须对它执行 $k$ 个替换操作。替换操作以三个长度均为 $k$ 的并行数组给出:$indices$, $sources$, $targets$。 要完成第 $i$ 个替换操作: 1. 检查「子字符串 $sources[i]$」是否出现在「原字符串 $s$」的索引 $indices[i]$ 处。 2. 如果没有出现,什么也不做。 3. 如果出现,则用 $targets[i]$「替换」该子字符串。 例如,如果 `s = "abcd"`, `indices[i] = 0`, `sources[i] = "ab"`,`targets[i] = "eee"`,那么替换的结果将是 `"eeecd"`。 所有替换操作必须「同时」发生,这意味着替换操作不应该影响彼此的索引。测试用例保证元素间不会重叠 。 - 例如,一个 `s = "abc"`,`indices = [0,1]`,`sources = ["ab","bc"]` 的测试用例将不会生成,因为 `"ab"` 和 `"bc"` 替换重叠。 **要求**: 在对 $s$ 执行所有替换操作后返回「结果字符串」。 **说明**: - 「子字符串」是字符串中连续的字符序列。 - $1 \le s.length \le 10^{3}$。 - $k == indices.length == sources.length == targets.length$。 - $1 \le k \le 10^{3}$。 - $0 \le indices[i] \lt s.length$。 - $1 \le sources[i].length, targets[i].length \le 50$。 - $s$ 仅由小写英文字母组成。 - $sources[i]$ 和 $targets[i]$ 仅由小写英文字母组成。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/06/12/833-ex1.png) ```python 输入:s = "abcd", indices = [0,2], sources = ["a","cd"], targets = ["eee","ffff"] 输出:"eeebffff" 解释: "a" 从 s 中的索引 0 开始,所以它被替换为 "eee"。 "cd" 从 s 中的索引 2 开始,所以它被替换为 "ffff"。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/06/12/833-ex2-1.png) ```python 输入:s = "abcd", indices = [0,2], sources = ["ab","ec"], targets = ["eee","ffff"] 输出:"eeecd" 解释: "ab" 从 s 中的索引 0 开始,所以它被替换为 "eee"。 "ec" 没有从原始的 S 中的索引 2 开始,所以它没有被替换。 ``` ## 解题思路 ### 思路 1:排序 + 模拟 这道题要求对字符串进行多次替换操作,关键是所有替换操作必须同时发生,不能相互影响。 1. 将所有替换操作按照索引从大到小排序,这样从后往前替换时,前面的索引不会受到影响。 2. 对于每个替换操作,检查在指定位置 $indices[i]$ 处是否匹配 $sources[i]$。 3. 如果匹配,则用 $targets[i]$ 替换该子串;否则跳过。 4. 由于从后往前处理,可以直接在原字符串上进行替换操作。 ### 思路 1:代码 ```python class Solution: def findReplaceString(self, s: str, indices: List[int], sources: List[str], targets: List[str]) -> str: # 将替换操作按索引从大到小排序,从后往前替换不会影响前面的索引 operations = sorted(zip(indices, sources, targets), reverse=True) # 将字符串转为列表,方便操作 s_list = list(s) for idx, source, target in operations: # 检查在位置 idx 处是否匹配 source if s[idx:idx + len(source)] == source: # 替换:删除原来的子串,插入新的子串 s_list[idx:idx + len(source)] = list(target) return ''.join(s_list) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k \log k + n)$,其中 $k$ 是替换操作的数量,$n$ 是字符串 $s$ 的长度。排序需要 $O(k \log k)$,每次替换操作最多需要 $O(n)$。 - **空间复杂度**:$O(n)$,需要将字符串转换为列表进行操作。 ================================================ FILE: docs/solutions/0800-0899/find-and-replace-pattern.md ================================================ # [0890. 查找和替换模式](https://leetcode.cn/problems/find-and-replace-pattern/) - 标签:数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [0890. 查找和替换模式 - 力扣](https://leetcode.cn/problems/find-and-replace-pattern/) ## 题目大意 **描述**: 你有一个单词列表 $words$ 和一个模式 $pattern$,你想知道 $words$ 中的哪些单词与模式匹配。 如果存在字母的排列 $p$ ,使得将模式中的每个字母 $x$ 替换为 $p$($x$) 之后,我们就得到了所需的单词,那么单词与模式是匹配的。(回想一下,字母的排列是从字母到字母的双射:每个字母映射到另一个字母,没有两个字母映射到同一个字母。) **要求**: 返回 $words$ 中与给定模式匹配的单词列表。 你可以按任何顺序返回答案。 **说明**: - $1 \le words.length \le 50$。 - $1 \le pattern.length = words[i].length \le 20$。 **示例**: - 示例 1: ```python 示例: 输入:words = ["abc","deq","mee","aqq","dkd","ccc"], pattern = "abb" 输出:["mee","aqq"] 解释: "mee" 与模式匹配,因为存在排列 {a -> m, b -> e, ...}。 "ccc" 与模式不匹配,因为 {a -> c, b -> c, ...} 不是排列。 因为 a 和 b 映射到同一个字母。 ``` ## 解题思路 ### 思路 1:哈希表 + 双射映射 这道题要求找出与给定模式匹配的单词。单词与模式匹配的条件是:存在一个字母的排列(双射),使得将模式中的每个字母替换后得到该单词。 双射的含义是: - 模式中的每个字母映射到单词中的唯一字母。 - 单词中的每个字母也只能由模式中的唯一字母映射而来。 算法步骤: 1. 对于每个单词,检查它是否与模式匹配。 2. 使用两个哈希表分别记录从模式到单词、从单词到模式的映射关系。 3. 遍历模式和单词的每个字符: - 如果模式字符已有映射,检查是否与当前单词字符一致。 - 如果单词字符已有映射,检查是否与当前模式字符一致。 - 如果不一致,说明不匹配。 4. 如果所有字符都匹配,将该单词加入结果。 ### 思路 1:代码 ```python class Solution: def findAndReplacePattern(self, words: List[str], pattern: str) -> List[str]: def match(word, pattern): # 检查 word 是否与 pattern 匹配 if len(word) != len(pattern): return False # 两个哈希表分别记录双向映射 map_p_to_w = {} # 模式到单词的映射 map_w_to_p = {} # 单词到模式的映射 for w_char, p_char in zip(word, pattern): # 检查模式到单词的映射 if p_char in map_p_to_w: if map_p_to_w[p_char] != w_char: return False else: map_p_to_w[p_char] = w_char # 检查单词到模式的映射 if w_char in map_w_to_p: if map_w_to_p[w_char] != p_char: return False else: map_w_to_p[w_char] = p_char return True # 筛选出与模式匹配的单词 result = [] for word in words: if match(word, pattern): result.append(word) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是单词列表的长度,$m$ 是单词的平均长度。需要遍历每个单词并检查是否匹配。 - **空间复杂度**:$O(m)$,需要使用两个哈希表存储映射关系。 ================================================ FILE: docs/solutions/0800-0899/find-eventual-safe-states.md ================================================ # [0802. 找到最终的安全状态](https://leetcode.cn/problems/find-eventual-safe-states/) - 标签:深度优先搜索、广度优先搜索、图、拓扑排序 - 难度:中等 ## 题目链接 - [0802. 找到最终的安全状态 - 力扣](https://leetcode.cn/problems/find-eventual-safe-states/) ## 题目大意 **描述**:给定一个有向图 $graph$,其中 $graph[i]$ 是与节点 $i$ 相邻的节点列表,意味着从节点 $i$ 到节点 $graph[i]$ 中的每个节点都有一条有向边。 **要求**:找出图中所有的安全节点,将其存入数组作为答案返回,答案数组中的元素应当按升序排列。 **说明**: - **终端节点**:如果一个节点没有连出的有向边,则它是终端节点。或者说,如果没有出边,则节点为终端节点。 - **安全节点**:如果从该节点开始的所有可能路径都通向终端节点,则该节点为安全节点。 - $n == graph.length$。 - $1 \le n \le 10^4$。 - $0 \le graph[i].length \le n$。 - $0 \le graph[i][j] \le n - 1$。 - $graph[i]$ 按严格递增顺序排列。 - 图中可能包含自环。 - 图中边的数目在范围 $[1, 4 \times 10^4]$ 内。 **示例**: - 示例 1: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/03/17/picture1.png) ```python 输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]] 输出:[2,4,5,6] 解释:示意图如上。 节点 5 和节点 6 是终端节点,因为它们都没有出边。 从节点 2、4、5 和 6 开始的所有路径都指向节点 5 或 6。 ``` - 示例 2: ```python 输入:graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]] 输出:[4] 解释: 只有节点 4 是终端节点,从节点 4 开始的所有路径都通向节点 4。 ``` ## 解题思路 ### 思路 1:拓扑排序 1. 根据题意可知,安全节点所对应的终点,一定是出度为 $0$ 的节点。而安全节点一定能在有限步内到达终点,则说明安全节点一定不在「环」内。 2. 我们可以利用拓扑排序来判断顶点是否在环中。 3. 为了找出安全节点,可以采取逆序建图的方式,将所有边进行反向。这样出度为 $0$ 的终点就变为了入度为 $0$ 的点。 4. 然后通过拓扑排序不断移除入度为 $0$ 的点之后,如果不在「环」中的点,最后入度一定为 $0$,这些点也就是安全节点。而在「环」中的点,最后入度一定不为 $0$。 5. 最后将所有安全的起始节点存入数组作为答案返回。 ### 思路 1:代码 ```python class Solution: # 拓扑排序,graph 中包含所有顶点的有向边关系(包括无边顶点) def topologicalSortingKahn(self, graph: dict): indegrees = {u: 0 for u in graph} # indegrees 用于记录所有节点入度 for u in graph: for v in graph[u]: indegrees[v] += 1 # 统计所有节点入度 # 将入度为 0 的顶点存入集合 S 中 S = collections.deque([u for u in indegrees if indegrees[u] == 0]) while S: u = S.pop() # 从集合中选择一个没有前驱的顶点 0 for v in graph[u]: # 遍历顶点 u 的邻接顶点 v indegrees[v] -= 1 # 删除从顶点 u 出发的有向边 if indegrees[v] == 0: # 如果删除该边后顶点 v 的入度变为 0 S.append(v) # 将其放入集合 S 中 res = [] for u in indegrees: if indegrees[u] == 0: res.append(u) return res def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]: graph_dict = {u: [] for u in range(len(graph))} for u in range(len(graph)): for v in graph[u]: graph_dict[v].append(u) # 逆序建图 return self.topologicalSortingKahn(graph_dict) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 是图中节点数目,$m$ 是图中边数目。 - **空间复杂度**:$O(n + m)$。 ================================================ FILE: docs/solutions/0800-0899/flipping-an-image.md ================================================ # [0832. 翻转图像](https://leetcode.cn/problems/flipping-an-image/) - 标签:数组、双指针、矩阵、模拟 - 难度:简单 ## 题目链接 - [0832. 翻转图像 - 力扣](https://leetcode.cn/problems/flipping-an-image/) ## 题目大意 给定一个二进制矩阵 `A` 代表图像,先将矩阵进行水平翻转,再进行翻转(将 0 变为 1,1 变为 0)。 ## 解题思路 两重 for 循环,第二层 for 循环遍历到一半即可。对于 `image[i][j]`、`image[i][n-1-j]` 先水平翻转操作,再进行翻转。 ## 代码 ```python class Solution: def flipAndInvertImage(self, image: List[List[int]]) -> List[List[int]]: n = len(image) for i in range(n): for j in range((n+1)//2): image[i][j], image[i][n-1-j] = image[i][n-1-j], image[i][j] image[i][j] = 0 if image[i][j] == 1 else 1 if j != n-1-j: image[i][n-1-j] = 0 if image[i][n-1-j] == 1 else 1 return image ``` ================================================ FILE: docs/solutions/0800-0899/friends-of-appropriate-ages.md ================================================ # [0825. 适龄的朋友](https://leetcode.cn/problems/friends-of-appropriate-ages/) - 标签:数组、双指针、二分查找、排序 - 难度:中等 ## 题目链接 - [0825. 适龄的朋友 - 力扣](https://leetcode.cn/problems/friends-of-appropriate-ages/) ## 题目大意 **描述**: 在社交媒体网站上有 $n$ 个用户。给你一个整数数组 $ages$,其中 $ages[i]$ 是第 $i$ 个用户的年龄。 如果下述任意一个条件为真,那么用户 $x$ 将不会向用户 $y$($x \ne y$)发送好友请求: - $ages[y] \le 0.5 \times ages[x] + 7$ - $ages[y] > ages[x]$ - $ages[y] > 100 && ages[x] < 100$ 否则,$x$ 将会向 $y$ 发送一条好友请求。 注意,如果 $x$ 向 $y$ 发送一条好友请求,$y$ 不必也向 $x$ 发送一条好友请求。另外,用户不会向自己发送好友请求。 **要求**: 返回在该社交媒体网站上产生的好友请求总数。 **说明**: - $n == ages.length$。 - $1 \le n \le 2 \times 10^{4}$。 - $1 \le ages[i] \le 120$。 **示例**: - 示例 1: ```python 输入:ages = [16,16] 输出:2 解释:2 人互发好友请求。 ``` - 示例 2: ```python 输入:ages = [16,17,18] 输出:2 解释:产生的好友请求为 17 -> 16 ,18 -> 17 。 ``` ## 解题思路 ### 思路 1:计数 + 数学 这道题要求计算好友请求的总数。根据题意,用户 $x$ 不会向用户 $y$ 发送好友请求的条件是: 1. $ages[y] \le 0.5 \times ages[x] + 7$ 2. $ages[y] > ages[x]$ 3. $ages[y] > 100$ 且 $ages[x] < 100$ 反过来,$x$ 会向 $y$ 发送好友请求的条件是: - $0.5 \times ages[x] + 7 < ages[y] \le ages[x]$ 由于年龄范围只有 $1$ 到 $120$,我们可以使用计数数组来统计每个年龄的人数,然后枚举所有可能的年龄对。 算法步骤: 1. 使用计数数组 $count$ 统计每个年龄的人数。 2. 枚举所有可能的年龄对 $(ageX, ageY)$。 3. 如果满足发送好友请求的条件,累加请求数: - 如果 $ageX == ageY$,则请求数为 $count[ageX] \times (count[ageX] - 1)$(同年龄的人互相发送,但不向自己发送)。 - 如果 $ageX \ne ageY$,则请求数为 $count[ageX] \times count[ageY]$。 ### 思路 1:代码 ```python class Solution: def numFriendRequests(self, ages: List[int]) -> int: # 统计每个年龄的人数 count = [0] * 121 for age in ages: count[age] += 1 result = 0 # 枚举所有可能的年龄对 for ageX in range(1, 121): if count[ageX] == 0: continue for ageY in range(1, 121): if count[ageY] == 0: continue # 判断 x 是否会向 y 发送好友请求 if ageY <= 0.5 * ageX + 7: continue if ageY > ageX: continue if ageY > 100 and ageX < 100: continue # 累加请求数 if ageX == ageY: result += count[ageX] * (count[ageX] - 1) else: result += count[ageX] * count[ageY] return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + C^2)$,其中 $n$ 是数组 $ages$ 的长度,$C = 120$ 是年龄的范围。需要遍历数组统计年龄,然后枚举所有年龄对。 - **空间复杂度**:$O(C)$,需要使用计数数组存储每个年龄的人数。 ================================================ FILE: docs/solutions/0800-0899/goat-latin.md ================================================ # [0824. 山羊拉丁文](https://leetcode.cn/problems/goat-latin/) - 标签:字符串 - 难度:简单 ## 题目链接 - [0824. 山羊拉丁文 - 力扣](https://leetcode.cn/problems/goat-latin/) ## 题目大意 **描述**:给定一个由若干单词组成的句子 $sentence$,单词之间由空格分隔。每个单词仅由大写和小写字母组成。 **要求**:将句子转换为「山羊拉丁文(Goat Latin)」,并返回将 $sentence$ 转换为山羊拉丁文后的句子。 **说明**: - 山羊拉丁文的规则如下: - 如果单词以元音开头(`a`,`e`,`i`,`o`,`u`),在单词后添加 `"ma"`。 - 例如,单词 `"apple"` 变为 `"applema"`。 - 如果单词以辅音字母开头(即,非元音字母),移除第一个字符并将它放到末尾,之后再添加 `"ma"`。 - 例如,单词 `"goat"` 变为 `"oatgma"`。 - 根据单词在句子中的索引,在单词最后添加与索引相同数量的字母 `a`,索引从 $1$ 开始。 - 例如,在第一个单词后添加 `"a"` ,在第二个单词后添加 `"aa"`,以此类推。 - $1 \le sentence.length \le 150$。 - $sentence$ 由英文字母和空格组成。 - $sentence$ 不含前导或尾随空格。 - $sentence$ 中的所有单词由单个空格分隔。 **示例**: - 示例 1: ```python 输入:sentence = "I speak Goat Latin" 输出:"Imaa peaksmaaa oatGmaaaa atinLmaaaaa" ``` - 示例 2: ```python 输入:sentence = "The quick brown fox jumped over the lazy dog" 输出:"heTmaa uickqmaaa rownbmaaaa oxfmaaaaa umpedjmaaaaaa overmaaaaaaa hetmaaaaaaaa azylmaaaaaaaaa ogdmaaaaaaaaaa" ``` ## 解题思路 ### 思路 1:模拟 1. 使用集合 $vowels$ 存储元音字符,然后将 $sentence$ 按照空格分隔成单词数组 $words$。 2. 遍历单词数组 $words$,对于当前单词 $word$,根据山羊拉丁文的规则,将其转为山羊拉丁文的单词,并存入答案数组 $res$ 中。 3. 遍历完之后将答案数组拼接为字符串并返回。 ### 思路 1:代码 ```python class Solution: def toGoatLatin(self, sentence: str) -> str: vowels = set(['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U']) words = sentence.split(' ') res = [] for i in range(len(words)): word = words[i] ans = "" if word[0] in vowels: ans += word + "ma" else: ans += word[1:] + word[0] + "ma" ans += 'a' * (i + 1) res.append(ans) return " ".join(res) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/groups-of-special-equivalent-strings.md ================================================ # [0893. 特殊等价字符串组](https://leetcode.cn/problems/groups-of-special-equivalent-strings/) - 标签:数组、哈希表、字符串、排序 - 难度:中等 ## 题目链接 - [0893. 特殊等价字符串组 - 力扣](https://leetcode.cn/problems/groups-of-special-equivalent-strings/) ## 题目大意 **描述**: 给定一个字符串数组 $words$。 一步操作中,你可以交换字符串 $words[i]$ 的任意两个偶数下标对应的字符或任意两个奇数下标对应的字符。 对两个字符串 $words[i]$ 和 $words[j]$ 而言,如果经过任意次数的操作,$words[i] == words[j]$,那么这两个字符串是 特殊等价 的。 - 例如,$words[i] = "zzxy"$ 和 $words[j] = "xyzz"$ 是一对「特殊等价」字符串,因为可以按 `"zzxy"` -> `"xzzy"` -> `"xyzz"` 的操作路径使 $words[i] == words[j]$。 现在规定,$words$ 的「一组特殊等价字符串」就是 $words$ 的一个同时满足下述条件的非空子集: - 该组中的每一对字符串都是 特殊等价 的 - 该组字符串已经涵盖了该类别中的所有特殊等价字符串,容量达到理论上的最大值(也就是说,如果一个字符串不在该组中,那么这个字符串就 不会 与该组内任何字符串特殊等价) **要求**: 返回 $words$ 中「特殊等价字符串组」的数量。 **说明**: - $1 \le words.length \le 10^{3}$。 - $1 \le words[i].length \le 20$。 - 所有 $words[i]$ 都只由小写字母组成。 - 所有 $words[i]$ 都具有相同的长度。 **示例**: - 示例 1: ```python 输入:words = ["abcd","cdab","cbad","xyzz","zzxy","zzyx"] 输出:3 解释: 其中一组为 ["abcd", "cdab", "cbad"],因为它们是成对的特殊等价字符串,且没有其他字符串与这些字符串特殊等价。 另外两组分别是 ["xyzz", "zzxy"] 和 ["zzyx"]。特别需要注意的是,"zzxy" 不与 "zzyx" 特殊等价。 ``` - 示例 2: ```python 输入:words = ["abc","acb","bac","bca","cab","cba"] 输出:3 解释:3 组 ["abc","cba"],["acb","bca"],["bac","cab"] ``` ## 解题思路 ### 思路 1:哈希表 这道题要求统计特殊等价字符串组的数量。两个字符串特殊等价的条件是:可以任意交换偶数位置的字符,也可以任意交换奇数位置的字符。 关键观察: - 如果两个字符串特殊等价,那么它们的偶数位置字符集合相同,奇数位置字符集合也相同。 - 可以将字符串的偶数位置字符排序后和奇数位置字符排序后拼接,作为该字符串的"签名"。 - 特殊等价的字符串具有相同的签名。 算法步骤: 1. 对于每个字符串,提取偶数位置和奇数位置的字符。 2. 分别对偶数位置和奇数位置的字符排序。 3. 将排序后的字符拼接作为签名。 4. 使用哈希集合统计不同签名的数量。 ### 思路 1:代码 ```python class Solution: def numSpecialEquivGroups(self, words: List[str]) -> int: def get_signature(word): # 提取偶数位置和奇数位置的字符 even_chars = [] odd_chars = [] for i, char in enumerate(word): if i % 2 == 0: even_chars.append(char) else: odd_chars.append(char) # 排序后拼接作为签名 even_chars.sort() odd_chars.sort() return ''.join(even_chars) + '|' + ''.join(odd_chars) # 使用哈希集合统计不同签名的数量 signatures = set() for word in words: signatures.add(get_signature(word)) return len(signatures) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m \log m)$,其中 $n$ 是字符串数组的长度,$m$ 是字符串的平均长度。需要遍历每个字符串并对其字符排序。 - **空间复杂度**:$O(n \times m)$,需要使用哈希集合存储签名。 ================================================ FILE: docs/solutions/0800-0899/guess-the-word.md ================================================ # [0843. 猜猜这个单词](https://leetcode.cn/problems/guess-the-word/) - 标签:数组、数学、字符串、博弈、交互 - 难度:困难 ## 题目链接 - [0843. 猜猜这个单词 - 力扣](https://leetcode.cn/problems/guess-the-word/) ## 题目大意 **描述**: 给定一个由「不同」字符串组成的单词列表 $words$,其中 $words[i]$ 长度均为 6。$words$ 中的一个单词将被选作秘密单词 $secret$。 另给你一个辅助对象 Master,你可以调用 `Master.guess(word)` 来猜单词,其中参数 $word$ 长度为 6 且必须是 $words$ 中的字符串。 `Master.guess(word)` 将会返回如下结果: - 如果 $word$ 不是 $words$ 中的字符串,返回 -1,或者 - 一个整数,表示你所猜测的单词 $word$ 与「秘密单词 $secret$」的准确匹配(值和位置同时匹配)的数目。 每组测试用例都会包含一个参数 $allowedGuesses$,其中 $allowedGuesses$ 是你可以调用 `Master.guess(word)` 的最大次数。 **要求**: 对于每组测试用例,如果在不超过允许猜测的次数的前提下: - 如果能够通过调用 `Master.guess` 来猜出秘密单词,则返回 `"You guessed the secret word correctly."`。 - 如果猜不出或者超过允许猜测的次数,则返回 `"Either you took too many guesses, or you did not find the secret word."`。 **说明**: - 生成的测试用例保证你可以利用某种合理的策略(而不是暴力)猜到秘密单词。 - $1 \le words.length \le 10^{3}$。 - $words[i].length == 6$。 - $words[i]$ 仅由小写英文字母组成。 - $words$ 中所有字符串「互不相同」。 - $secret$ 存在于 $words$ 中。 - $10 \le allowedGuesses \le 30$。 **示例**: - 示例 1: ```python 输入:secret = "acckzz", words = ["acckzz","ccbazz","eiowzz","abcczz"], allowedGuesses = 10 输出:You guessed the secret word correctly. 解释: master.guess("aaaaaa") 返回 -1 ,因为 "aaaaaa" 不在 words 中。 master.guess("acckzz") 返回 6 ,因为 "acckzz" 是秘密单词 secret ,共有 6 个字母匹配。 master.guess("ccbazz") 返回 3 ,因为 "ccbazz" 共有 3 个字母匹配。 master.guess("eiowzz") 返回 2 ,因为 "eiowzz" 共有 2 个字母匹配。 master.guess("abcczz") 返回 4 ,因为 "abcczz" 共有 4 个字母匹配。 一共调用 5 次 master.guess ,其中一个为秘密单词,所以通过测试用例。 ``` - 示例 2: ```python 输入:secret = "hamada", words = ["hamada","khaled"], allowedGuesses = 10 输出:You guessed the secret word correctly. 解释:共有 2 个单词,且其中一个为秘密单词,可以通过测试用例。 ``` ## 解题思路 ### 思路 1:MinMax 策略 + 预计算匹配矩阵 这道题要求通过调用 API 猜测秘密单词。关键是设计一个好的策略来选择每次猜测的单词。 **核心思想**: - 每次猜测后,根据返回的匹配数,可以过滤掉大量不可能的候选单词。 - 我们希望选择一个单词,使得无论返回什么匹配数,剩余的候选单词数量都尽可能少。 - 这是一个 MinMax 策略:最小化最坏情况下的候选单词数量。 **关键优化**: 1. **预计算匹配矩阵**:提前计算所有单词对之间的匹配数,避免重复计算。 2. **排除已猜单词**:在选择猜测单词时,排除已经猜过的单词。 3. **MinMax 选择**:对于每个候选单词,统计不同匹配数对应的候选单词分组,选择最大分组最小的单词。 **算法步骤**: 1. 预计算所有单词对之间的匹配数矩阵 $H[i][j]$,表示单词 $i$ 和单词 $j$ 的匹配数。 2. 初始化候选单词索引列表和已猜单词集合。 3. 每次使用 MinMax 策略选择最优的猜测单词: - 对于每个未猜过的候选单词,统计不同匹配数对应的候选单词分组。 - 选择最大分组最小的单词(即最坏情况下剩余候选单词最少的单词)。 4. 调用 `master.guess()` 获取匹配数。 5. 如果匹配数为 $6$,说明猜中,直接返回。 6. 否则,过滤候选单词:只保留与猜测单词有相同匹配数的单词。 7. 重复步骤 3-6,直到猜中。 ### 思路 1:代码 ```python # """ # This is Master's API interface. # You should not implement it, or speculate about its implementation # """ # class Master: # def guess(self, word: str) -> int: class Solution: def findSecretWord(self, words: List[str], master: 'Master') -> None: n = len(words) # 预计算所有单词对之间的匹配数矩阵 H = [[sum(a == b for a, b in zip(words[i], words[j])) for j in range(n)] for i in range(n)] # 候选单词索引列表 possible = list(range(n)) # 已猜单词集合 guessed = set() while possible: # 使用 MinMax 策略选择最优单词 guess_idx = self.solve(H, possible, guessed) # 猜测单词 matches = master.guess(words[guess_idx]) # 如果猜中,直接返回 if matches == len(words[0]): return # 将当前猜测加入已猜集合 guessed.add(guess_idx) # 过滤候选单词:只保留与猜测单词有相同匹配数的单词 possible = [j for j in possible if H[guess_idx][j] == matches] def solve(self, H, possible, guessed): # 如果候选单词数量很少,直接返回第一个 if len(possible) <= 2: return possible[0] min_max_group_size = float('inf') best_guess = possible[0] # 遍历所有未猜过的单词 for guess_idx in range(len(H)): if guess_idx in guessed: continue # 统计不同匹配数对应的候选单词分组 groups = [[] for _ in range(7)] # 匹配数从 0 到 6 for j in possible: if j != guess_idx: groups[H[guess_idx][j]].append(j) # 找到最大的分组(最坏情况) max_group = max(groups, key=len) # 选择最坏情况最好的单词 if len(max_group) < min_max_group_size: min_max_group_size = len(max_group) best_guess = guess_idx return best_guess ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \times m + n^2 \times k)$,其中 $n$ 是单词数量,$m$ 是单词长度,$k$ 是猜测次数(最多 $10$ 次)。预计算匹配矩阵需要 $O(n^2 \times m)$ 时间,每次选择最优单词需要 $O(n^2)$ 时间。 - **空间复杂度**:$O(n^2)$,需要存储匹配矩阵。 ================================================ FILE: docs/solutions/0800-0899/hand-of-straights.md ================================================ # [0846. 一手顺子](https://leetcode.cn/problems/hand-of-straights/) - 标签:贪心、数组、哈希表、排序 - 难度:中等 ## 题目链接 - [0846. 一手顺子 - 力扣](https://leetcode.cn/problems/hand-of-straights/) ## 题目大意 **描述**:`Alice` 手中有一把牌,她想要重新排列这些牌,分成若干组,使每一组的牌都是顺子(即由连续的牌构成),并且每一组的牌数都是 `groupSize`。现在给定一个整数数组 `hand`,其中 `hand[i]` 是表示第 `i` 张牌的数值,和一个整数 `groupSize`。 **要求**:如果 `Alice` 能将这些牌重新排列成若干组、并且每组都是 `goupSize` 张牌的顺子,则返回 `True`;否则,返回 `False`。 **说明**: - $1 \le hand.length \le 10^4$。 - $0 \le hand[i] \le 10^9$。 - $1 \le groupSize \le hand.length$。 **示例**: - 示例 1: ```python 输入:hand = [1,2,3,6,2,3,4,7,8], groupSize = 3 输出:True 解释:Alice 手中的牌可以被重新排列为 [1,2,3],[2,3,4],[6,7,8]。 ``` ## 解题思路 ### 思路 1:哈希表 + 排序 1. 使用哈希表存储每个数出现的次数。 2. 将哈希表中每个键从小到大排序。 3. 从哈希表中最小的数开始,以它作为当前顺子的开头,然后依次判断顺子里的数是否在哈希表中,如果在的话,则将哈希表中对应数的数量减 `1`。不在的话,说明无法满足题目要求,直接返回 `False`。 4. 重复执行 2 ~ 3 步,直到哈希表为空。最后返回 `True`。 ### 思路 1:哈希表 + 排序代码 ```python class Solution: def isPossibleDivide(self, nums: List[int], k: int) -> bool: hand_map = collections.defaultdict(int) for i in range(len(nums)): hand_map[nums[i]] += 1 for key in sorted(hand_map.keys()): value = hand_map[key] if value == 0: continue count = 0 for i in range(k): hand_map[key + count] -= value if hand_map[key + count] < 0: return False count += 1 return True ``` ================================================ FILE: docs/solutions/0800-0899/image-overlap.md ================================================ # [0835. 图像重叠](https://leetcode.cn/problems/image-overlap/) - 标签:数组、矩阵 - 难度:中等 ## 题目链接 - [0835. 图像重叠 - 力扣](https://leetcode.cn/problems/image-overlap/) ## 题目大意 **描述**: 给定两个图像 $img1$ 和 $img2$,两个图像的大小都是 $n \times n$,用大小相同的二进制正方形矩阵表示。二进制矩阵仅由若干 0 和若干 1 组成。 「转换」其中一个图像,将所有的 1 向左,右,上,或下滑动任何数量的单位;然后把它放在另一个图像的上面。该转换的「重叠」是指两个图像「都」具有 1 的位置的数目。 请注意,转换「不包括」向任何方向旋转。越过矩阵边界的 1 都将被清除。 **要求**: 计算最大可能的重叠数量。 **说明**: - $n == img1.length == img1[i].length$。 - $n == img2.length == img2[i].length$。 - $1 \le n \le 30$。 - $img1[i][j]$ 为 0 或 1。 - $img2[i][j]$ 为 0 或 1。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/09/09/overlap1.jpg) ```python 输入:img1 = [[1,1,0],[0,1,0],[0,1,0]], img2 = [[0,0,0],[0,1,1],[0,0,1]] 输出:3 解释:将 img1 向右移动 1 个单位,再向下移动 1 个单位。 ``` ![](https://assets.leetcode.com/uploads/2020/09/09/overlap_step1.jpg) ```python 两个图像都具有 1 的位置的数目是 3(用红色标识)。 ``` ![](https://assets.leetcode.com/uploads/2020/09/09/overlap_step2.jpg) - 示例 2: ```python 输入:img1 = [[1]], img2 = [[1]] 输出:1 ``` ## 解题思路 ### 思路 1:哈希表 + 偏移量统计 这道题要求计算两个二进制矩阵的最大重叠数。可以通过平移其中一个矩阵来实现重叠。 关键观察: - 只有值为 $1$ 的位置才会对重叠产生贡献。 - 可以枚举 $img1$ 中所有值为 $1$ 的位置和 $img2$ 中所有值为 $1$ 的位置,计算它们之间的偏移量。 - 相同偏移量出现的次数就是该偏移下的重叠数。 算法步骤: 1. 提取 $img1$ 和 $img2$ 中所有值为 $1$ 的位置。 2. 对于 $img1$ 中的每个 $1$ 和 $img2$ 中的每个 $1$,计算偏移量 $(dx, dy)$。 3. 使用哈希表统计每个偏移量出现的次数。 4. 返回最大的出现次数。 ### 思路 1:代码 ```python class Solution: def largestOverlap(self, img1: List[List[int]], img2: List[List[int]]) -> int: n = len(img1) # 提取 img1 和 img2 中所有值为 1 的位置 ones1 = [] ones2 = [] for i in range(n): for j in range(n): if img1[i][j] == 1: ones1.append((i, j)) if img2[i][j] == 1: ones2.append((i, j)) # 统计每个偏移量出现的次数 offset_count = {} for x1, y1 in ones1: for x2, y2 in ones2: # 计算偏移量 dx = x1 - x2 dy = y1 - y2 offset = (dx, dy) offset_count[offset] = offset_count.get(offset, 0) + 1 # 返回最大重叠数 return max(offset_count.values()) if offset_count else 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^4)$,其中 $n$ 是矩阵的边长。最坏情况下,两个矩阵都全为 $1$,需要枚举 $O(n^2) \times O(n^2)$ 对位置。 - **空间复杂度**:$O(n^2)$,需要存储所有值为 $1$ 的位置和偏移量统计。 ================================================ FILE: docs/solutions/0800-0899/increasing-order-search-tree.md ================================================ # [0897. 递增顺序搜索树](https://leetcode.cn/problems/increasing-order-search-tree/) - 标签:栈、树、深度优先搜索、二叉搜索树、二叉树 - 难度:简单 ## 题目链接 - [0897. 递增顺序搜索树 - 力扣](https://leetcode.cn/problems/increasing-order-search-tree/) ## 题目大意 给定一棵二叉搜索树的根节点 `root`。 要求:按中序遍历顺序将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。 ## 解题思路 可以分为两步: 1. 中序遍历二叉搜索树,将节点先存储到列表中。 2. 将列表中的节点构造成一棵递增顺序搜索树。 中序遍历直接按照 `左 -> 根 -> 右` 的顺序递归遍历,然后将遍历的节点存储到 `res` 中。 构造递增顺序搜索树,则用 `head` 保存头节点位置。遍历列表中的每个节点,将其左右指针先置空,再将其连接在上一个节点的右子节点上。 最后返回 `head.right` 即可。 ## 代码 ```python class Solution: def inOrder(self, root, res): if not root: return self.inOrder(root.left, res) res.append(root) self.inOrder(root.right, res) def increasingBST(self, root: TreeNode) -> TreeNode: res = [] self.inOrder(root, res) if not res: return head = TreeNode(-1) cur = head for node in res: node.left = node.right = None cur.right = node cur = cur.right return head.right ``` ================================================ FILE: docs/solutions/0800-0899/index.md ================================================ ## 本章内容 - [0800. 相似 RGB 颜色](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/similar-rgb-color.md) - [0801. 使序列递增的最小交换次数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/minimum-swaps-to-make-sequences-increasing.md) - [0802. 找到最终的安全状态](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/find-eventual-safe-states.md) - [0803. 打砖块](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/bricks-falling-when-hit.md) - [0804. 唯一摩尔斯密码词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/unique-morse-code-words.md) - [0805. 数组的均值分割](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/split-array-with-same-average.md) - [0806. 写字符串需要的行数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/number-of-lines-to-write-string.md) - [0807. 保持城市天际线](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/max-increase-to-keep-city-skyline.md) - [0808. 分汤](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/soup-servings.md) - [0809. 情感丰富的文字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/expressive-words.md) - [0810. 黑板异或游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/chalkboard-xor-game.md) - [0811. 子域名访问计数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/subdomain-visit-count.md) - [0812. 最大三角形面积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/largest-triangle-area.md) - [0813. 最大平均值和的分组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/largest-sum-of-averages.md) - [0814. 二叉树剪枝](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/binary-tree-pruning.md) - [0815. 公交路线](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/bus-routes.md) - [0816. 模糊坐标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/ambiguous-coordinates.md) - [0817. 链表组件](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/linked-list-components.md) - [0818. 赛车](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/race-car.md) - [0819. 最常见的单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/most-common-word.md) - [0820. 单词的压缩编码](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/short-encoding-of-words.md) - [0821. 字符的最短距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-distance-to-a-character.md) - [0822. 翻转卡片游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/card-flipping-game.md) - [0823. 带因子的二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/binary-trees-with-factors.md) - [0824. 山羊拉丁文](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/goat-latin.md) - [0825. 适龄的朋友](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/friends-of-appropriate-ages.md) - [0826. 安排工作以达到最大收益](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/most-profit-assigning-work.md) - [0827. 最大人工岛](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/making-a-large-island.md) - [0828. 统计子串中的唯一字符](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/count-unique-characters-of-all-substrings-of-a-given-string.md) - [0829. 连续整数求和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/consecutive-numbers-sum.md) - [0830. 较大分组的位置](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/positions-of-large-groups.md) - [0831. 隐藏个人信息](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/masking-personal-information.md) - [0832. 翻转图像](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/flipping-an-image.md) - [0833. 字符串中的查找与替换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/find-and-replace-in-string.md) - [0834. 树中距离之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/sum-of-distances-in-tree.md) - [0835. 图像重叠](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/image-overlap.md) - [0836. 矩形重叠](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/rectangle-overlap.md) - [0837. 新 21 点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/new-21-game.md) - [0838. 推多米诺](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/push-dominoes.md) - [0839. 相似字符串组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/similar-string-groups.md) - [0840. 矩阵中的幻方](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/magic-squares-in-grid.md) - [0841. 钥匙和房间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/keys-and-rooms.md) - [0842. 将数组拆分成斐波那契序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/split-array-into-fibonacci-sequence.md) - [0843. 猜猜这个单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/guess-the-word.md) - [0844. 比较含退格的字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/backspace-string-compare.md) - [0845. 数组中的最长山脉](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/longest-mountain-in-array.md) - [0846. 一手顺子](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/hand-of-straights.md) - [0847. 访问所有节点的最短路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-path-visiting-all-nodes.md) - [0848. 字母移位](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shifting-letters.md) - [0849. 到最近的人的最大距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/maximize-distance-to-closest-person.md) - [0850. 矩形面积 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/rectangle-area-ii.md) - [0851. 喧闹和富有](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/loud-and-rich.md) - [0852. 山脉数组的峰顶索引](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/peak-index-in-a-mountain-array.md) - [0853. 车队](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/car-fleet.md) - [0854. 相似度为 K 的字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/k-similar-strings.md) - [0855. 考场就座](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/exam-room.md) - [0856. 括号的分数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/score-of-parentheses.md) - [0857. 雇佣 K 名工人的最低成本](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/minimum-cost-to-hire-k-workers.md) - [0858. 镜面反射](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/mirror-reflection.md) - [0859. 亲密字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/buddy-strings.md) - [0860. 柠檬水找零](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/lemonade-change.md) - [0861. 翻转矩阵后的得分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/score-after-flipping-matrix.md) - [0862. 和至少为 K 的最短子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-subarray-with-sum-at-least-k.md) - [0863. 二叉树中所有距离为 K 的结点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/all-nodes-distance-k-in-binary-tree.md) - [0864. 获取所有钥匙的最短路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/shortest-path-to-get-all-keys.md) - [0865. 具有所有最深节点的最小子树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/smallest-subtree-with-all-the-deepest-nodes.md) - [0866. 回文质数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/prime-palindrome.md) - [0867. 转置矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/transpose-matrix.md) - [0868. 二进制间距](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/binary-gap.md) - [0869. 重新排序得到 2 的幂](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/reordered-power-of-2.md) - [0870. 优势洗牌](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/advantage-shuffle.md) - [0871. 最低加油次数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/minimum-number-of-refueling-stops.md) - [0872. 叶子相似的树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/leaf-similar-trees.md) - [0873. 最长的斐波那契子序列的长度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/length-of-longest-fibonacci-subsequence.md) - [0874. 模拟行走机器人](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/walking-robot-simulation.md) - [0875. 爱吃香蕉的珂珂](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/koko-eating-bananas.md) - [0876. 链表的中间结点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/middle-of-the-linked-list.md) - [0877. 石子游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/stone-game.md) - [0878. 第 N 个神奇数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/nth-magical-number.md) - [0879. 盈利计划](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/profitable-schemes.md) - [0880. 索引处的解码字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/decoded-string-at-index.md) - [0881. 救生艇](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/boats-to-save-people.md) - [0882. 细分图中的可到达节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/reachable-nodes-in-subdivided-graph.md) - [0883. 三维形体投影面积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/projection-area-of-3d-shapes.md) - [0884. 两句话中的不常见单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/uncommon-words-from-two-sentences.md) - [0885. 螺旋矩阵 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/spiral-matrix-iii.md) - [0886. 可能的二分法](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/possible-bipartition.md) - [0887. 鸡蛋掉落](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/super-egg-drop.md) - [0888. 公平的糖果交换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/fair-candy-swap.md) - [0889. 根据前序和后序遍历构造二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/construct-binary-tree-from-preorder-and-postorder-traversal.md) - [0890. 查找和替换模式](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/find-and-replace-pattern.md) - [0891. 子序列宽度之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/sum-of-subsequence-widths.md) - [0892. 三维形体的表面积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/surface-area-of-3d-shapes.md) - [0893. 特殊等价字符串组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/groups-of-special-equivalent-strings.md) - [0894. 所有可能的真二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/all-possible-full-binary-trees.md) - [0895. 最大频率栈](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/maximum-frequency-stack.md) - [0896. 单调数列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/monotonic-array.md) - [0897. 递增顺序搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/increasing-order-search-tree.md) - [0898. 子数组按位或操作](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/bitwise-ors-of-subarrays.md) - [0899. 有序队列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/orderly-queue.md) ================================================ FILE: docs/solutions/0800-0899/k-similar-strings.md ================================================ # [0854. 相似度为 K 的字符串](https://leetcode.cn/problems/k-similar-strings/) - 标签:广度优先搜索、哈希表、字符串 - 难度:困难 ## 题目链接 - [0854. 相似度为 K 的字符串 - 力扣](https://leetcode.cn/problems/k-similar-strings/) ## 题目大意 **描述**: 对于某些非负整数 $k$,如果交换 $s1$ 中两个字母的位置恰好 $k$ 次,能够使结果字符串等于 $s2$ ,则认为字符串 $s1$ 和 $s2$ 的 相似度为 $k$ 。 给你两个字母异位词 $s1$ 和 $s2$。 **要求**: 返回 $s1$ 和 $s2$ 的相似度 $k$ 的最小值。 **说明**: - $1 \le s1.length \le 20$。 - $s2.length == s1.length$。 - $s1$ 和 $s2$ 只包含集合 `{'a', 'b', 'c', 'd', 'e', 'f'}` 中的小写字母。 - $s2$ 是 $s1$ 的一个字母异位词。 **示例**: - 示例 1: ```python 输入:s1 = "ab", s2 = "ba" 输出:1 ``` - 示例 2: ```python 输入:s1 = "abc", s2 = "bca" 输出:2 ``` ## 解题思路 ### 思路 1:BFS(广度优先搜索) 这道题要求计算将字符串 $s1$ 通过最少的交换次数变成 $s2$。这是一个典型的 BFS 最短路径问题。 算法步骤: 1. 使用 BFS 从 $s1$ 开始搜索,每次尝试交换两个字符。 2. 使用哈希集合记录已访问的字符串,避免重复搜索。 3. 优化:只交换能让字符串更接近 $s2$ 的位置,即只交换那些当前位置字符与 $s2$ 不匹配的位置。 4. 当搜索到 $s2$ 时,返回交换次数。 具体优化: - 对于当前字符串,找到第一个与 $s2$ 不匹配的位置 $i$。 - 尝试将位置 $i$ 与后面所有位置 $j$ 交换,其中 $s[j] == s2[i]$ 且 $s[i] \ne s2[j]$(避免无效交换)。 ### 思路 1:代码 ```python class Solution: def kSimilarity(self, s1: str, s2: str) -> int: from collections import deque if s1 == s2: return 0 # BFS queue = deque([(s1, 0)]) # (当前字符串, 交换次数) visited = {s1} while queue: curr, swaps = queue.popleft() # 找到第一个与 s2 不匹配的位置 i = 0 while i < len(curr) and curr[i] == s2[i]: i += 1 # 尝试交换位置 i 与后面的位置 for j in range(i + 1, len(curr)): # 只交换能让位置 i 匹配的字符 if curr[j] == s2[i]: # 交换位置 i 和 j next_str = list(curr) next_str[i], next_str[j] = next_str[j], next_str[i] next_str = ''.join(next_str) # 如果到达目标字符串 if next_str == s2: return swaps + 1 # 如果未访问过,加入队列 if next_str not in visited: visited.add(next_str) queue.append((next_str, swaps + 1)) return -1 # 理论上不会到达这里 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \times n!)$,其中 $n$ 是字符串的长度。最坏情况下需要遍历所有可能的字符串排列,但实际上由于剪枝和字符集较小,运行时间会快很多。 - **空间复杂度**:$O(n!)$,需要存储访问过的字符串。 ================================================ FILE: docs/solutions/0800-0899/keys-and-rooms.md ================================================ # [0841. 钥匙和房间](https://leetcode.cn/problems/keys-and-rooms/) - 标签:深度优先搜索、广度优先搜索、图 - 难度:中等 ## 题目链接 - [0841. 钥匙和房间 - 力扣](https://leetcode.cn/problems/keys-and-rooms/) ## 题目大意 **描述**:有 `n` 个房间,编号为 `0` ~ `n - 1`,每个房间都有若干把钥匙,每把钥匙上都有一个编号,可以开启对应房间号的门。最初,除了 `0` 号房间外其他房间的门都是锁着的。 现在给定一个二维数组 `rooms`,`rooms[i][j]` 表示第 `i` 个房间的第 `j` 把钥匙所能开启的房间号。 **要求**:判断是否能开启所有房间的门。如果能开启,则返回 `True`。否则返回 `False`。 **说明**: - $n == rooms.length$。 - $2 \le n \le 1000$。 - $0 \le rooms[i].length \le 1000$。 - $1 \le sum(rooms[i].length) \le 3000$。 - $0 \le rooms[i][j] < n$。 - 所有 $rooms[i]$ 的值互不相同。 **示例**: - 示例 1: ```python 输入:rooms = [[1],[2],[3],[]] 输出:True 解释: 我们从 0 号房间开始,拿到钥匙 1。 之后我们去 1 号房间,拿到钥匙 2。 然后我们去 2 号房间,拿到钥匙 3。 最后我们去了 3 号房间。 由于我们能够进入每个房间,我们返回 true。 ``` - 示例 2: ```python 输入:rooms = [[1,3],[3,0,1],[2],[0]] 输出:False 解释:我们不能进入 2 号房间。 ``` ## 解题思路 ### 思路 1:深度优先搜索 当 `x` 号房间有 `y` 号房间的钥匙时,就可以认为我们可以通过 `x` 号房间去往 `y` 号房间。现在把 `n` 个房间看做是拥有 `n` 个节点的图,则上述关系可以看做是 `x` 与 `y` 点之间有一条有向边。 那么问题就变为了给定一张有向图,从 `0` 节点开始出发,问是否能到达所有的节点。 我们可以使用深度优先搜索的方式来解决这道题,具体做法如下: 1. 使用 set 集合变量 `visited` 来统计遍历到的节点个数。 2. 从 `0` 节点开始,使用深度优先搜索的方式遍历整个图。 3. 将当前节点 `x` 加入到集合 `visited` 中,遍历当前节点的邻接点。 1. 如果邻接点不再集合 `visited` 中,则继续递归遍历。 4. 最后深度优先搜索完毕,判断一下遍历到的节点个数是否等于图的节点个数(即集合 `visited` 中的元素个数是否等于节点个数)。 1. 如果等于,则返回 `True` 2. 如果不等于,则返回 `False`。 ### 思路 1:代码 ```python class Solution: def canVisitAllRooms(self, rooms: List[List[int]]) -> bool: def dfs(x): visited.add(x) for key in rooms[x]: if key not in visited: dfs(key) visited = set() dfs(0) return len(visited) == len(rooms) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 是房间的数量,$m$ 是所有房间中的钥匙数量的总数。 - **空间复杂度**:$O(n)$,递归调用的栈空间深度不超过 $n$。 ================================================ FILE: docs/solutions/0800-0899/koko-eating-bananas.md ================================================ # [0875. 爱吃香蕉的珂珂](https://leetcode.cn/problems/koko-eating-bananas/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [0875. 爱吃香蕉的珂珂 - 力扣](https://leetcode.cn/problems/koko-eating-bananas/) ## 题目大意 **描述**:给定一个数组 $piles$ 代表 $n$ 堆香蕉。其中 $piles[i]$ 表示第 $i$ 堆香蕉的个数。再给定一个整数 $h$ ,表示最多可以在 $h$ 小时内吃完所有香蕉。珂珂决定以速度每小时 $k$(未知)根的速度吃香蕉。每一个小时,她讲选择其中一堆香蕉,从中吃掉 $k$ 根。如果这堆香蕉少于 $k$ 根,珂珂将在这一小时吃掉这堆的所有香蕉,并且这一小时不会再吃其他堆的香蕉。 **要求**:返回珂珂可以在 $h$ 小时内吃掉所有香蕉的最小速度 $k$($k$ 为整数)。 **说明**: - $1 \le piles.length \le 10^4$。 - $piles.length \le h \le 10^9$。 - $1 \le piles[i] \le 10^9$。 **示例**: - 示例 1: ```python 输入:piles = [3,6,7,11], h = 8 输出:4 ``` - 示例 2: ```python 输入:piles = [30,11,23,4,20], h = 5 输出:30 ``` ## 解题思路 ### 思路 1:二分查找算法 先来看 $k$ 的取值范围,因为 $k$ 是整数,且速度肯定不能为 $0$ 吧,为 $0$ 的话就永远吃不完了。所以$k$ 的最小值可以取 $1$。$k$ 的最大值根香蕉中最大堆的香蕉个数有关,因为 $1$ 个小时内只能选择一堆吃,不能再吃其他堆的香蕉,则 $k$ 的最大值取香蕉堆的最大值即可。即 $k$ 的最大值为 $max(piles)$。 我们的目标是求出 $h$ 小时内吃掉所有香蕉的最小速度 $k$。现在有了区间「$[1, max(piles)]$」,有了目标「最小速度 $k$」。接下来使用二分查找算法来查找「最小速度 $k$」。至于计算 $h$ 小时内能否以 $k$ 的速度吃完香蕉,我们可以再写一个方法 $canEat$ 用于判断。如果能吃完就返回 $True$,不能吃完则返回 $False$。下面说一下算法的具体步骤。 - 使用两个指针 $left$、$right$。令 $left$ 指向 $1$,$right$ 指向 $max(piles)$。代表待查找区间为 $[left, right]$ - 取两个节点中心位置 $mid$,判断是否能在 $h$ 小时内以 $k$ 的速度吃完香蕉。 - 如果不能吃完,则将区间 $[left, mid]$ 排除掉,继续在区间 $[mid + 1, right]$ 中查找。 - 如果能吃完,说明 $k$ 还可以继续减小,则继续在区间 $[left, mid]$ 中查找。 - 当 $left == right$ 时跳出循环,返回 $left$。 ### 思路 1:代码 ```python class Solution: def canEat(self, piles, hour, speed): time = 0 for pile in piles: time += (pile + speed - 1) // speed return time <= hour def minEatingSpeed(self, piles: List[int], h: int) -> int: left, right = 1, max(piles) while left < right: mid = left + (right - left) // 2 if not self.canEat(piles, h, mid): left = mid + 1 else: right = mid return left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log max(piles))$,$n$ 表示数组 $piles$ 中的元素个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/largest-sum-of-averages.md ================================================ # [0813. 最大平均值和的分组](https://leetcode.cn/problems/largest-sum-of-averages/) - 标签:数组、动态规划、前缀和 - 难度:中等 ## 题目链接 - [0813. 最大平均值和的分组 - 力扣](https://leetcode.cn/problems/largest-sum-of-averages/) ## 题目大意 **描述**: 给定数组 $nums$ 和一个整数 $k$。我们将给定的数组 $nums$ 分成「最多 $k$ 个非空子数组」,且数组内部是连续的 。 「分数」由每个子数组内的平均值的总和构成。 注意我们必须使用 $nums$ 数组中的每一个数进行分组,并且分数不一定需要是整数。 **要求**: 返回我们所能得到的最大 分数 是多少。答案误差在 $10^{-6}$ 内被视为是正确的。 **说明**: - $1 \le nums.length \le 10^{3}$。 - $1 \le nums[i] \le 10^{4}$。 - $1 \le k \le nums.length$。 **示例**: - 示例 1: ```python 输入: nums = [9,1,2,3,9], k = 3 输出: 20.00000 解释: nums 的最优分组是[9], [1, 2, 3], [9]. 得到的分数是 9 + (1 + 2 + 3) / 3 + 9 = 20. 我们也可以把 nums 分成[9, 1], [2], [3, 9]. 这样的分组得到的分数为 5 + 2 + 6 = 13, 但不是最大值. ``` - 示例 2: ```python 输入: nums = [1,2,3,4,5,6,7], k = 4 输出: 20.50000 ``` ## 解题思路 ### 思路 1:动态规划 这道题要求将数组分成最多 $k$ 个连续子数组,使得每个子数组的平均值之和最大。 定义状态: - $dp[i][j]$ 表示将前 $i$ 个元素分成 $j$ 个子数组时的最大平均值和。 状态转移方程: - $dp[i][j] = \max(dp[p][j-1] + \frac{\sum_{t=p+1}^{i} nums[t]}{i-p})$,其中 $j-1 \le p < i$。 边界条件: - $dp[i][1] = \frac{\sum_{t=1}^{i} nums[t]}{i}$,即前 $i$ 个元素分成 $1$ 个子数组。 为了优化计算,可以使用前缀和数组 $prefix$ 来快速计算子数组的和。 算法步骤: 1. 计算前缀和数组 $prefix$。 2. 初始化 $dp$ 数组。 3. 填充 $dp$ 数组,枚举分组数 $j$ 和元素数 $i$,以及分割点 $p$。 4. 返回 $dp[n][k]$。 ### 思路 1:代码 ```python class Solution: def largestSumOfAverages(self, nums: List[int], k: int) -> float: n = len(nums) # 计算前缀和 prefix = [0] * (n + 1) for i in range(n): prefix[i + 1] = prefix[i] + nums[i] # 初始化 dp 数组 dp = [[0.0] * (k + 1) for _ in range(n + 1)] # 边界条件:前 i 个元素分成 1 个子数组 for i in range(1, n + 1): dp[i][1] = prefix[i] / i # 状态转移 for j in range(2, k + 1): # 枚举分组数 for i in range(j, n + 1): # 枚举元素数(至少需要 j 个元素) for p in range(j - 1, i): # 枚举分割点 # 前 p 个元素分成 j-1 组,后面 i-p 个元素作为第 j 组 avg = (prefix[i] - prefix[p]) / (i - p) dp[i][j] = max(dp[i][j], dp[p][j - 1] + avg) return dp[n][k] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \times k)$,其中 $n$ 是数组的长度。需要填充 $n \times k$ 个状态,每个状态需要枚举 $O(n)$ 个分割点。 - **空间复杂度**:$O(n \times k)$,需要使用 $dp$ 数组存储状态。 ================================================ FILE: docs/solutions/0800-0899/largest-triangle-area.md ================================================ # [0812. 最大三角形面积](https://leetcode.cn/problems/largest-triangle-area/) - 标签:几何、数组、数学 - 难度:简单 ## 题目链接 - [0812. 最大三角形面积 - 力扣](https://leetcode.cn/problems/largest-triangle-area/) ## 题目大意 **描述**: 给定一个由 X-Y 平面上的点组成的数组 $points$,其中 $points[i] = [xi, yi]$。 **要求**: 从其中取任意三个不同的点组成三角形,返回能组成的最大三角形的面积。与真实值误差在 $10^{-5}$ 内的答案将会视为正确答案。 **说明**: - $3 \le points.length \le 50$。 - $-50 \le xi, yi \le 50$。 - 给出的所有点「互不相同」。 **示例**: - 示例 1: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/04/04/1027.png) ```python 输入:points = [[0,0],[0,1],[1,0],[0,2],[2,0]] 输出:2.00000 解释:输入中的 5 个点如上图所示,红色的三角形面积最大。 ``` - 示例 2: ```python 输入:points = [[1,0],[0,0],[0,1]] 输出:0.50000 ``` ## 解题思路 ### 思路 1:枚举 + 几何 这道题要求计算由给定点组成的最大三角形面积。 对于三个点 $(x_1, y_1)$、$(x_2, y_2)$、$(x_3, y_3)$,三角形的面积可以用以下公式计算(鞋带公式): $$S = \frac{1}{2} |x_1(y_2 - y_3) + x_2(y_3 - y_1) + x_3(y_1 - y_2)|$$ 算法步骤: 1. 枚举所有可能的三个点的组合。 2. 对于每个组合,使用鞋带公式计算三角形面积。 3. 记录最大面积。 ### 思路 1:代码 ```python class Solution: def largestTriangleArea(self, points: List[List[int]]) -> float: n = len(points) max_area = 0 # 枚举所有可能的三个点 for i in range(n): for j in range(i + 1, n): for k in range(j + 1, n): # 获取三个点的坐标 x1, y1 = points[i] x2, y2 = points[j] x3, y3 = points[k] # 使用鞋带公式计算三角形面积 area = 0.5 * abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) # 更新最大面积 max_area = max(max_area, area) return max_area ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 是点的数量。需要枚举所有三个点的组合。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0800-0899/leaf-similar-trees.md ================================================ # [0872. 叶子相似的树](https://leetcode.cn/problems/leaf-similar-trees/) - 标签:树、深度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0872. 叶子相似的树 - 力扣](https://leetcode.cn/problems/leaf-similar-trees/) ## 题目大意 将一棵二叉树树上所有的叶子,按照从左到右的顺序排列起来就形成了一个「叶值序列」。如果两棵二叉树的叶值序列是相同的,我们就认为它们是叶相似的。 现在给定两棵二叉树的根节点 `root1`、`root2`。如果两棵二叉是叶相似的,则返回 `True`,否则返回 `False`。 ## 解题思路 分别 DFS 遍历两棵树,得到对应的叶值序列,判断两个叶值序列是否相等。 ## 代码 ```python class Solution: def leafSimilar(self, root1: TreeNode, root2: TreeNode) -> bool: def dfs(node: TreeNode, res: List[int]): if not node: return if not node.left and not node.right: res.append(node.val) dfs(node.left, res) dfs(node.right, res) res1 = [] dfs(root1, res1) res2 = [] dfs(root2, res2) return res1 == res2 ``` ================================================ FILE: docs/solutions/0800-0899/lemonade-change.md ================================================ # [0860. 柠檬水找零](https://leetcode.cn/problems/lemonade-change/) - 标签:贪心、数组 - 难度:简单 ## 题目链接 - [0860. 柠檬水找零 - 力扣](https://leetcode.cn/problems/lemonade-change/) ## 题目大意 **描述**:一杯柠檬水的售价是 $5$ 美元。现在有 $n$ 个顾客排队购买柠檬水,每人只能购买一杯。顾客支付的钱面额有 $5$ 美元、$10$ 美元、$20$ 美元。必须给每个顾客正确找零(就是每位顾客需要向你支付 $5$ 美元,多出的钱要找还回顾客)。 现在给定 $n$ 个顾客支付的钱币面额数组 `bills`。 **要求**:如果能给每位顾客正确找零,则返回 `True`,否则返回 `False`。 **说明**: - 一开始的时候手头没有任何零钱。 - $1 \le bills.length \le 10^5$。 - `bills[i]` 不是 $5$ 就是 $10$ 或是 $20$。 **示例**: - 示例 1: ```python 输入:bills = [5,5,5,10,20] 输出:True 解释: 前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。 第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。 第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。 由于所有客户都得到了正确的找零,所以我们输出 True。 ``` - 示例 2: ```python 输入:bills = [5,5,10,10,20] 输出:False 解释: 前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。 对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。 对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。 由于不是每位顾客都得到了正确的找零,所以答案是 False。 ``` ## 解题思路 ### 思路 1:贪心算法 由于顾客只能给我们 $5$、$10$、$20$ 三种面额的钞票,且一开始我们手头没有任何钞票,所以我们手中所能拥有的钞票面额只能是 $5$、$10$、$20$。因此可以采取下面的策略: 1. 如果顾客支付 $5$ 美元,直接收下。 2. 如果顾客支付 $10$ 美元,如果我们手头有 $5$ 美元面额的钞票,则找给顾客,否则无法正确找零,返回 `False`。 3. 如果顾客支付 $20$ 美元,如果我们手头有 $1$ 张 $10$ 美元和 $1$ 张 $5$ 美元的钞票,或者有 $3$ 张 $5$ 美元的钞票,则可以找给顾客。如果两种组合方式同时存在,倾向于第 $1$ 种方式找零,因为使用 $5$ 美元的场景比使用 $10$ 美元的场景多,要尽可能的保留 $5$ 美元的钞票。如果这两种组合方式都不通知,则无法正确找零,返回 `False`。 所以,我们可以使用两个变量 `five` 和 `ten` 来维护手中 $5$ 美元、$10$ 美团的钞票数量, 然后遍历一遍根据上述条件分别判断即可。 ### 思路 1:代码 ```python class Solution: def lemonadeChange(self, bills: List[int]) -> bool: five, ten, twenty = 0, 0, 0 for bill in bills: if bill == 5: five += 1 if bill == 10: if five <= 0: return False ten += 1 five -= 1 if bill == 20: if five > 0 and ten > 0: five -= 1 ten -= 1 twenty += 1 elif five >= 3: five -= 3 twenty += 1 else: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 `bill` 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/length-of-longest-fibonacci-subsequence.md ================================================ # [0873. 最长的斐波那契子序列的长度](https://leetcode.cn/problems/length-of-longest-fibonacci-subsequence/) - 标签:数组、哈希表、动态规划 - 难度:中等 ## 题目链接 - [0873. 最长的斐波那契子序列的长度 - 力扣](https://leetcode.cn/problems/length-of-longest-fibonacci-subsequence/) ## 题目大意 **描述**:给定一个严格递增的正整数数组 $arr$。 **要求**:从数组 $arr$ 中找出最长的斐波那契式的子序列的长度。如果不存斐波那契式的子序列,则返回 0。 **说明**: - **斐波那契式序列**:如果序列 $X_1, X_2, ..., X_n$ 满足: - $n \ge 3$; - 对于所有 $i + 2 \le n$,都有 $X_i + X_{i+1} = X_{i+2}$。 则称该序列为斐波那契式序列。 - **斐波那契式子序列**:从序列 $A$ 中挑选若干元素组成子序列,并且子序列满足斐波那契式序列,则称该序列为斐波那契式子序列。例如:$A = [3, 4, 5, 6, 7, 8]$。则 $[3, 5, 8]$ 是 $A$ 的一个斐波那契式子序列。 - $3 \le arr.length \le 1000$。 - $1 \le arr[i] < arr[i + 1] \le 10^9$。 **示例**: - 示例 1: ```python 输入: arr = [1,2,3,4,5,6,7,8] 输出: 5 解释: 最长的斐波那契式子序列为 [1,2,3,5,8]。 ``` - 示例 2: ```python 输入: arr = [1,3,7,11,12,14,18] 输出: 3 解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18]。 ``` ## 解题思路 ### 思路 1: 暴力枚举(超时) 假设 $arr[i]$、$arr[j]$、$arr[k]$ 是序列 $arr$ 中的 $3$ 个元素,且满足关系:$arr[i] + arr[j] == arr[k]$,则 $arr[i]$、$arr[j]$、$arr[k]$ 就构成了 $arr$ 的一个斐波那契式子序列。 通过 $arr[i]$、$arr[j]$,我们可以确定下一个斐波那契式子序列元素的值为 $arr[i] + arr[j]$。 因为给定的数组是严格递增的,所以对于一个斐波那契式子序列,如果确定了 $arr[i]$、$arr[j]$,则可以顺着 $arr$ 序列,从第 $j + 1$ 的元素开始,查找值为 $arr[i] + arr[j]$ 的元素 。找到 $arr[i] + arr[j]$ 之后,然后再顺着查找子序列的下一个元素。 简单来说,就是确定了 $arr[i]$、$arr[j]$,就能尽可能的得到一个长的斐波那契式子序列,此时我们记录下子序列长度。然后对于不同的 $arr[i]$、$arr[j]$,统计不同的斐波那契式子序列的长度。 最后将这些长度进行比较,其中最长的长度就是答案。 ### 思路 1:代码 ```python class Solution: def lenLongestFibSubseq(self, arr: List[int]) -> int: size = len(arr) ans = 0 for i in range(size): for j in range(i + 1, size): temp_ans = 0 temp_i = i temp_j = j k = j + 1 while k < size: if arr[temp_i] + arr[temp_j] == arr[k]: temp_ans += 1 temp_i = temp_j temp_j = k k += 1 if temp_ans > ans: ans = temp_ans if ans > 0: return ans + 2 else: return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 为数组 $arr$ 的元素个数。 - **空间复杂度**:$O(1)$。 ### 思路 2:哈希表 对于 $arr[i]$、$arr[j]$,要查找的元素 $arr[i] + arr[j]$ 是否在 $arr$ 中,我们可以预先建立一个反向的哈希表。键值对关系为 $value : idx$,这样就能在 $O(1)$ 的时间复杂度通过 $arr[i] + arr[j]$ 的值查找到对应的 $arr[k]$,而不用像原先一样线性查找 $arr[k]$ 了。 ### 思路 2:代码 ```python class Solution: def lenLongestFibSubseq(self, arr: List[int]) -> int: size = len(arr) ans = 0 idx_map = dict() for idx, value in enumerate(arr): idx_map[value] = idx for i in range(size): for j in range(i + 1, size): temp_ans = 0 temp_i = i temp_j = j while arr[temp_i] + arr[temp_j] in idx_map: temp_ans += 1 k = idx_map[arr[temp_i] + arr[temp_j]] temp_i = temp_j temp_j = k if temp_ans > ans: ans = temp_ans if ans > 0: return ans + 2 else: return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为数组 $arr$ 的元素个数。 - **空间复杂度**:$O(n)$。 ### 思路 3:动态规划 + 哈希表 ###### 1. 阶段划分 按照斐波那契式子序列相邻两项的结尾位置进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:以 $arr[i]$、$arr[j]$ 为结尾的斐波那契式子序列的最大长度。 ###### 3. 状态转移方程 以 $arr[j]$、$arr[k]$ 结尾的斐波那契式子序列的最大长度 = 满足 $arr[i] + arr[j] = arr[k]$ 条件下,以 $arr[i]$、$arr[j]$ 结尾的斐波那契式子序列的最大长度加 $1$。即状态转移方程为:$dp[j][k] = max_{(A[i] + A[j] = A[k], i < j < k)}(dp[i][j] + 1)$。 ###### 4. 初始条件 默认状态下,数组中任意相邻两项元素都可以作为长度为 $2$ 的斐波那契式子序列,即 $dp[i][j] = 2$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:以 $arr[i]$、$arr[j]$ 为结尾的斐波那契式子序列的最大长度。那为了计算出最大的最长递增子序列长度,则需要在进行状态转移时,求出最大值 $ans$ 即为最终结果。 因为题目定义中,斐波那契式中 $n \ge 3$,所以只有当 $ans \ge 3$ 时,返回 $ans$。如果 $ans < 3$,则返回 $0$。 > **注意**:在进行状态转移的同时,我们应和「思路 2:哈希表」一样采用哈希表优化的方式来提高效率,降低算法的时间复杂度。 ### 思路 3:代码 ```python class Solution: def lenLongestFibSubseq(self, arr: List[int]) -> int: size = len(arr) dp = [[0 for _ in range(size)] for _ in range(size)] ans = 0 # 初始化 dp for i in range(size): for j in range(i + 1, size): dp[i][j] = 2 idx_map = {} # 将 value : idx 映射为哈希表,这样可以快速通过 value 获取到 idx for idx, value in enumerate(arr): idx_map[value] = idx for i in range(size): for j in range(i + 1, size): if arr[i] + arr[j] in idx_map: # 获取 arr[i] + arr[j] 的 idx,即斐波那契式子序列下一项元素 k = idx_map[arr[i] + arr[j]] dp[j][k] = max(dp[j][k], dp[i][j] + 1) ans = max(ans, dp[j][k]) if ans >= 3: return ans return 0 ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为数组 $arr$ 的元素个数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0800-0899/linked-list-components.md ================================================ # [0817. 链表组件](https://leetcode.cn/problems/linked-list-components/) - 标签:数组、哈希表、链表 - 难度:中等 ## 题目链接 - [0817. 链表组件 - 力扣](https://leetcode.cn/problems/linked-list-components/) ## 题目大意 **描述**: 给定链表头结点 $head$,该链表上的每个结点都有一个「唯一的整型值」。同时给定列表 $nums$,该列表是上述链表中整型值的一个子集。 **要求**: 返回列表 $nums$ 中组件的个数,这里对组件的定义为:链表中一段最长连续结点的值(该值必须在列表 $nums$ 中)构成的集合。 **说明**: - 链表中节点数为n。 - $1 \le n \le 10^{4}$。 - $0 \le Node.val \lt n$。 - Node.val 中所有值 不同。 - $1 \le nums.length \le n$。 - $0 \le nums[i] \lt n$。 - $nums$ 中所有值「不同」。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/07/22/lc-linkedlistcom1.jpg) ```python 输入: head = [0,1,2,3], nums = [0,1,3] 输出: 2 解释: 链表中,0 和 1 是相连接的,且 nums 中不包含 2,所以 [0, 1] 是 nums 的一个组件,同理 [3] 也是一个组件,故返回 2。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/07/22/lc-linkedlistcom2.jpg) ```python 输入: head = [0,1,2,3,4], nums = [0,3,1,4] 输出: 2 解释: 链表中,0 和 1 是相连接的,3 和 4 是相连接的,所以 [0, 1] 和 [3, 4] 是两个组件,故返回 2。 ``` ## 解题思路 ### 思路 1:哈希表 + 链表遍历 这道题要求统计链表中组件的数量。组件是链表中连续的、值都在 $nums$ 中的节点序列。 算法步骤: 1. 将 $nums$ 转换为哈希集合,方便快速查找。 2. 遍历链表,统计组件数量: - 如果当前节点的值在 $nums$ 中,且前一个节点的值不在 $nums$ 中(或当前是第一个节点),则组件数加 $1$。 - 换句话说,每次遇到一个新组件的开始时,计数器加 $1$。 ### 思路 1:代码 ```python # Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution: def numComponents(self, head: Optional[ListNode], nums: List[int]) -> int: # 将 nums 转换为哈希集合 num_set = set(nums) count = 0 in_component = False # 标记当前是否在组件中 # 遍历链表 curr = head while curr: if curr.val in num_set: # 如果当前节点在 nums 中 if not in_component: # 如果之前不在组件中,说明这是一个新组件的开始 count += 1 in_component = True else: # 如果当前节点不在 nums 中,标记不在组件中 in_component = False curr = curr.next return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 是链表的长度,$m$ 是数组 $nums$ 的长度。需要遍历链表和构建哈希集合。 - **空间复杂度**:$O(m)$,需要使用哈希集合存储 $nums$ 中的元素。 ================================================ FILE: docs/solutions/0800-0899/longest-mountain-in-array.md ================================================ # [0845. 数组中的最长山脉](https://leetcode.cn/problems/longest-mountain-in-array/) - 标签:数组、双指针、动态规划、枚举 - 难度:中等 ## 题目链接 - [0845. 数组中的最长山脉 - 力扣](https://leetcode.cn/problems/longest-mountain-in-array/) ## 题目大意 **描述**:给定一个整数数组 $arr$。 **要求**:返回最长山脉子数组的长度。如果不存在山脉子数组,返回 $0$。 **说明**: - **山脉数组**:符合下列属性的数组 $arr$ 称为山脉数组。 - $arr.length \ge 3$。 - 存在下标 $i(0 < i < arr.length - 1)$ 满足: - $arr[0] < arr[1] < … < arr[i]$ - $arr[i] > arr[i + 1] > … > arr[arr.length - 1]$ - $1 \le arr.length \le 10^4$。 - $0 \le arr[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:arr = [2,1,4,7,3,2,5] 输出:5 解释:最长的山脉子数组是 [1,4,7,3,2],长度为 5。 ``` - 示例 2: ```python 输入:arr = [2,2,2] 输出:0 解释:不存在山脉子数组。 ``` ## 解题思路 ### 思路 1:快慢指针 1. 使用变量 $ans$ 保存最长山脉长度。 2. 遍历数组,假定当前节点为山峰。 3. 使用双指针 $left$、$right$ 分别向左、向右查找山脉的长度。 4. 如果当前山脉的长度比最长山脉长度更长,则更新最长山脉长度。 5. 最后输出 $ans$。 ### 思路 1:代码 ```python class Solution: def longestMountain(self, arr: List[int]) -> int: size = len(arr) res = 0 for i in range(1, size - 1): if arr[i] > arr[i - 1] and arr[i] > arr[i + 1]: left = i - 1 right = i + 1 while left > 0 and arr[left - 1] < arr[left]: left -= 1 while right < size - 1 and arr[right + 1] < arr[right]: right += 1 if right - left + 1 > res: res = right - left + 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $arr$ 中的元素数量。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/loud-and-rich.md ================================================ # [0851. 喧闹和富有](https://leetcode.cn/problems/loud-and-rich/) - 标签:深度优先搜索、图、拓扑排序、数组 - 难度:中等 ## 题目链接 - [0851. 喧闹和富有 - 力扣](https://leetcode.cn/problems/loud-and-rich/) ## 题目大意 **描述**:有一组 `n` 个人作为实验对象,从 `0` 到 `n - 1` 编号,其中每个人都有不同数目的钱,以及不同程度的安静值 `quietness`。 现在给定一个数组 `richer`,其中 `richer[i] = [ai, bi]` 表示第 `ai` 个人比第 `bi` 个人更有钱。另给你一个整数数组 `quiet`,其中 `quiet[i]` 是第 `i` 个人的安静值。数组 `richer` 中所给出的数据逻辑自洽(也就是说,在第 `ai` 个人比第 `bi` 个人更有钱的同时,不会出现第 `bi` 个人比第 `ai` 个人更有钱的情况 )。 **要求**:返回一个长度为 `n` 的整数数组 `answer` 作为答案,其中 `answer[i]` 表示在所有比第 `i` 个人更有钱或者和他一样有钱的人中,安静值最小的那个人的编号。 **说明**: - $n == quiet.length$ - $1 \le n \le 500$。 - $0 \le quiet[i] \le n$。 - $quiet$ 的所有值互不相同。 - $0 \le richer.length \le n * (n - 1) / 2$。 - $0 \le ai, bi < n$。 - $ai != bi$。 - $richer$ 中的所有数对 互不相同。 - 对 $richer$ 的观察在逻辑上是一致的。 **示例**: - 示例 1: ```python 输入:richer = [[1,0],[2,1],[3,1],[3,7],[4,3],[5,3],[6,3]], quiet = [3,2,5,4,6,1,7,0] 输出:[5,5,2,5,4,5,6,7] 解释: answer[0] = 5, person 5 比 person 3 有更多的钱,person 3 比 person 1 有更多的钱,person 1 比 person 0 有更多的钱。 唯一较为安静(有较低的安静值 quiet[x])的人是 person 7, 但是目前还不清楚他是否比 person 0 更有钱。 answer[7] = 7, 在所有拥有的钱肯定不少于 person 7 的人中(这可能包括 person 3,4,5,6 以及 7), 最安静(有较低安静值 quiet[x])的人是 person 7。 其他的答案也可以用类似的推理来解释。 ``` ## 解题思路 ### 思路 1:拓扑排序 对于第 `i` 个人,我们要求解的是比第 `i` 个人更有钱或者和他一样有钱的人中,安静值最小的那个人的编号。 我们可以建立一张有向无环图,由富人指向穷人。这样,对于任意一点来说(比如 `x`),通过有向边链接的点(比如 `y`),拥有的钱都没有 `x` 多。则我们可以根据 `answer[x]` 去更新所有 `x` 能连接到的点的 `answer` 值。 我们可以先将数组 `answer` 元素初始化为当前元素编号。然后对建立的有向无环图进行拓扑排序,按照拓扑排序的顺序去更新 `x` 能连接到的点的 `answer` 值。 ### 思路 1:拓扑排序代码 ```python import collections class Solution: def loudAndRich(self, richer: List[List[int]], quiet: List[int]) -> List[int]: size = len(quiet) indegrees = [0 for _ in range(size)] edges = collections.defaultdict(list) for x, y in richer: edges[x].append(y) indegrees[y] += 1 res = [i for i in range(size)] queue = collections.deque([]) for i in range(size): if not indegrees[i]: queue.append(i) while queue: x = queue.popleft() size -= 1 for y in edges[x]: if quiet[res[x]] < quiet[res[y]]: res[y] = res[x] indegrees[y] -= 1 if not indegrees[y]: queue.append(y) return res ``` ================================================ FILE: docs/solutions/0800-0899/magic-squares-in-grid.md ================================================ # [0840. 矩阵中的幻方](https://leetcode.cn/problems/magic-squares-in-grid/) - 标签:数组、哈希表、数学、矩阵 - 难度:中等 ## 题目链接 - [0840. 矩阵中的幻方 - 力扣](https://leetcode.cn/problems/magic-squares-in-grid/) ## 题目大意 **描述**: $3 \times 3$ 的幻方是一个填充有 从 1 到 9 的不同数字的 $3 \times 3$ 矩阵,其中每行,每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的 $row \times col$ 的 $grid$。 **要求**: 计算其中有多少个 $3 \times 3$ 的「幻方」子矩阵。 注意:虽然幻方只能包含 1 到 9 的数字,但 $grid$ 可以包含最多 15 的数字。 **说明**: - $row == grid.length$。 - $col == grid[i].length$。 - $1 \le row, col \le 10$。 - $0 \le grid[i][j] \le 15$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/09/11/magic_main.jpg) ```python 输入: grid = [[4,3,8,4],[9,5,1,9],[2,7,6,2] 输出: 1 解释: 下面的子矩阵是一个 3 x 3 的幻方: ``` ![](https://assets.leetcode.com/uploads/2020/09/11/magic_valid.jpg) ```python 而这一个不是: ``` ![](https://assets.leetcode.com/uploads/2020/09/11/magic_invalid.jpg) ```python 总的来说,在本示例所给定的矩阵中只有一个 3 x 3 的幻方子矩阵。 ``` - 示例 2: ```python 输入: grid = [[8]] 输出: 0 ``` ## 解题思路 ### 思路 1:枚举 + 模拟 这道题要求统计矩阵中 $3 \times 3$ 幻方的数量。幻方需要满足: 1. 包含 $1$ 到 $9$ 的所有数字,且每个数字只出现一次。 2. 每行、每列、两条对角线的和都相等(都等于 $15$)。 算法步骤: 1. 枚举所有可能的 $3 \times 3$ 子矩阵的左上角位置。 2. 对于每个子矩阵,检查是否为幻方: - 检查是否包含 $1$ 到 $9$ 的所有数字。 - 检查每行、每列、两条对角线的和是否都等于 $15$。 3. 统计满足条件的幻方数量。 ### 思路 1:代码 ```python class Solution: def numMagicSquaresInside(self, grid: List[List[int]]) -> int: def is_magic(square): # 检查是否为幻方 # 1. 检查是否包含 1 到 9 的所有数字 nums = [] for row in square: for num in row: if num < 1 or num > 9: return False nums.append(num) if sorted(nums) != list(range(1, 10)): return False # 2. 检查每行、每列、两条对角线的和是否都等于 15 # 检查行 for row in square: if sum(row) != 15: return False # 检查列 for col in range(3): if sum(square[row][col] for row in range(3)) != 15: return False # 检查对角线 if sum(square[i][i] for i in range(3)) != 15: return False if sum(square[i][2 - i] for i in range(3)) != 15: return False return True rows = len(grid) cols = len(grid[0]) count = 0 # 枚举所有可能的 3x3 子矩阵 for i in range(rows - 2): for j in range(cols - 2): # 提取 3x3 子矩阵 square = [grid[i + r][j:j + 3] for r in range(3)] if is_magic(square): count += 1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 和 $n$ 分别是矩阵的行数和列数。需要枚举所有可能的 $3 \times 3$ 子矩阵,每个子矩阵的检查时间为常数。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0800-0899/making-a-large-island.md ================================================ # [0827. 最大人工岛](https://leetcode.cn/problems/making-a-large-island/) - 标签:深度优先搜索、广度优先搜索、并查集、数组、矩阵 - 难度:困难 ## 题目链接 - [0827. 最大人工岛 - 力扣](https://leetcode.cn/problems/making-a-large-island/) ## 题目大意 **描述**: 给定一个大小为 $n \times n$ 二进制矩阵 $grid$。最多 只能将一格 0 变成 1 。 **要求**: 返回执行此操作后,$grid$ 中最大的岛屿面积是多少? **说明**: - 「岛屿」由一组上、下、左、右四个方向相连的 1 形成。 - $n == grid.length$。 - $n == grid[i].length$。 - $1 \le n \le 500$。 - grid[i][j] 为 0 或 1。 **示例**: - 示例 1: ```python 输入: grid = [[1, 0], [0, 1]] 输出: 3 解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。 ``` - 示例 2: ```python 输入: grid = [[1, 1], [1, 0]] 输出: 4 解释: 将一格0变成1,岛屿的面积扩大为 4。 ``` ## 解题思路 ### 思路 1:DFS(深度优先搜索)+ 并查集 这道题要求在最多将一个 $0$ 变成 $1$ 的情况下,计算最大的岛屿面积。 算法步骤: 1. 使用 DFS 标记每个岛屿,并计算每个岛屿的面积。 2. 为每个岛屿分配一个唯一的 ID。 3. 遍历所有的 $0$,尝试将其变成 $1$: - 统计该位置四周相邻的不同岛屿。 - 计算将该 $0$ 变成 $1$ 后的总面积($1$ + 相邻岛屿面积之和)。 4. 返回最大面积。 5. 特殊情况:如果没有 $0$,返回整个网格的面积。 ### 思路 1:代码 ```python class Solution: def largestIsland(self, grid: List[List[int]]) -> int: n = len(grid) # 四个方向 directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] # DFS 标记岛屿并计算面积 def dfs(i, j, island_id): if i < 0 or i >= n or j < 0 or j >= n or grid[i][j] != 1: return 0 grid[i][j] = island_id # 标记为岛屿 ID area = 1 for di, dj in directions: ni, nj = i + di, j + dj area += dfs(ni, nj, island_id) return area # 标记所有岛屿并记录面积 island_id = 2 # 从 2 开始,因为 0 和 1 已被使用 area_map = {} # 记录每个岛屿的面积 for i in range(n): for j in range(n): if grid[i][j] == 1: area = dfs(i, j, island_id) area_map[island_id] = area island_id += 1 # 如果没有 0,返回整个网格的面积 max_area = max(area_map.values()) if area_map else 0 # 尝试将每个 0 变成 1 for i in range(n): for j in range(n): if grid[i][j] == 0: # 统计相邻的不同岛屿 adjacent_islands = set() for di, dj in directions: ni, nj = i + di, j + dj if 0 <= ni < n and 0 <= nj < n and grid[ni][nj] > 1: adjacent_islands.add(grid[ni][nj]) # 计算总面积 total_area = 1 + sum(area_map[island] for island in adjacent_islands) max_area = max(max_area, total_area) return max_area ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是网格的边长。需要遍历网格两次,一次标记岛屿,一次尝试填充 $0$。 - **空间复杂度**:$O(n^2)$,递归调用栈的深度最多为 $O(n^2)$。 ================================================ FILE: docs/solutions/0800-0899/masking-personal-information.md ================================================ # [0831. 隐藏个人信息](https://leetcode.cn/problems/masking-personal-information/) - 标签:字符串 - 难度:中等 ## 题目链接 - [0831. 隐藏个人信息 - 力扣](https://leetcode.cn/problems/masking-personal-information/) ## 题目大意 **描述**: 给定一条个人信息字符串 $s$,可能表示一个「邮箱地址」,也可能表示一串「电话号码」。 一个「有效」的电子邮件地址由以下部分组成: - 一个 `名字`,由大小写英文字母组成,后面跟着 - 一个 `'@'` 字符,后面跟着 - 一个 `域名`,由大小写英文字母和一个位于中间的 `'.'` 字符组成。`'.'` 不会是域名的第一个或者最后一个字符。 要想隐藏电子邮件地址中的个人信息: - `名字` 和 `域名` 部分的大写英文字母应当转换成小写英文字母。 - `名字` 中间的字母(即,除第一个和最后一个字母外)必须用 5 个 `"*****"` 替换。 一个「有效」的电话号码应当按下述格式组成: - 电话号码可以由 $10 \sim 13$ 位数字组成 - 后 10 位构成 本地号码 - 前面剩下的 $0 \sim 3$ 位,构成「国家代码」 - 利用 `{'+', '-', '(', ')', ' '}` 这些「分隔字符」按某种形式对上述数字进行分隔 要想隐藏电话号码中的个人信息: - 移除所有 `分隔字符` - 隐藏个人信息后的电话号码应该遵从这种格式: - `"***-***-XXXX"` 如果国家代码为 0 位数字 - `"+*-***-***-XXXX"` 如果国家代码为 1 位数字 - `"+**-***-***-XXXX"` 如果国家代码为 2 位数字 - `"+***-***-***-XXXX"` 如果国家代码为 3 位数字 - `"XXXX"` 是最后 4 位 本地号码 **要求**: 返回按规则「隐藏」个人信息后的结果。 **说明**: - $s$ 是一个「有效」的电子邮件或者电话号码。 - 如果 $s$ 是一个电子邮件: - $8 \le s.length \le 40$。 - s 是由大小写英文字母,恰好一个 `'@'` 字符,以及 `'.'` 字符组成。 - 如果 $s$ 是一个电话号码: - $10 \le s.length \le 20$。 - s 是由数字、空格、字符 `'('`、`')'`、`'-'` 和 `'+'` 组成。 **示例**: - 示例 1: ```python 输入:s = "LeetCode@LeetCode.com" 输出:"l*****e@leetcode.com" 解释:s 是一个电子邮件地址。 名字和域名都转换为小写,名字的中间用 5 个 * 替换。 ``` - 示例 2: ```python 输入:s = "AB@qq.com" 输出:"a*****b@qq.com" 解释:s 是一个电子邮件地址。 名字和域名都转换为小写,名字的中间用 5 个 * 替换。 注意,尽管 "ab" 只有两个字符,但中间仍然必须有 5 个 * 。 ``` ## 解题思路 ### 思路 1:字符串处理 + 模拟 这道题要求根据规则隐藏个人信息。需要分别处理邮箱和电话号码两种情况。 **邮箱处理规则**: 1. 将名字和域名转换为小写。 2. 名字中间的字母用 $5$ 个 `*` 替换(保留首尾字母)。 **电话号码处理规则**: 1. 移除所有分隔字符,只保留数字。 2. 后 $10$ 位是本地号码,前面的是国家代码。 3. 根据国家代码位数格式化输出。 算法步骤: 1. 判断是邮箱还是电话号码(包含 `@` 则为邮箱)。 2. 根据类型应用相应的处理规则。 ### 思路 1:代码 ```python class Solution: def maskPII(self, s: str) -> str: if '@' in s: # 处理邮箱 s = s.lower() name, domain = s.split('@') # 名字首尾字母 + 5个* + 域名 return name[0] + '*****' + name[-1] + '@' + domain else: # 处理电话号码 # 移除所有非数字字符 digits = ''.join(c for c in s if c.isdigit()) # 本地号码是后 10 位 local = digits[-10:] # 国家代码是前面的部分 country_code_len = len(digits) - 10 # 格式化本地号码 masked_local = '***-***-' + local[-4:] # 根据国家代码位数添加前缀 if country_code_len == 0: return masked_local else: return '+' + '*' * country_code_len + '-' + masked_local ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串的长度。需要遍历字符串进行处理。 - **空间复杂度**:$O(n)$,需要存储处理后的字符串。 ================================================ FILE: docs/solutions/0800-0899/max-increase-to-keep-city-skyline.md ================================================ # [0807. 保持城市天际线](https://leetcode.cn/problems/max-increase-to-keep-city-skyline/) - 标签:贪心、数组、矩阵 - 难度:中等 ## 题目链接 - [0807. 保持城市天际线 - 力扣](https://leetcode.cn/problems/max-increase-to-keep-city-skyline/) ## 题目大意 **描述**: 给定一座由 $n \times n$ 个街区组成的城市,每个街区都包含一座立方体建筑。给你一个下标从 0 开始的 $n \times n$ 整数矩阵 $grid$,其中 $grid[r][c]$ 表示坐落于 $r$ 行 $c$ 列的建筑物的「高度」。 城市的「天际线」是从远处观察城市时,所有建筑物形成的外部轮廓。从东、南、西、北四个主要方向观测到的「天际线」可能不同。 我们被允许为「任意数量的建筑物」的高度增加「任意增量(不同建筑物的增量可能不同)」。高度为 0 的建筑物的高度也可以增加。然而,增加的建筑物高度「不能影响」从任何主要方向观察城市得到的「天际线」。 **要求**: 在「不改变」从任何主要方向观测到的城市「天际线」的前提下,返回建筑物可以增加的「最大高度增量总和」。 **说明**: - $n == grid.length$。 - $n == grid[r].length$。 - $2 \le n \le 50$。 - $0 \le grid[r][c] \le 10^{3}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/06/21/807-ex1.png) ```python 输入:grid = [[3,0,8,4],[2,4,5,7],[9,2,6,3],[0,3,1,0]] 输出:35 解释:建筑物的高度如上图中心所示。 用红色绘制从不同方向观看得到的天际线。 在不影响天际线的情况下,增加建筑物的高度: gridNew = [ [8, 4, 8, 7], [7, 4, 7, 7], [9, 4, 8, 7], [3, 3, 3, 3] ] ``` - 示例 2: ```python 输入:grid = [[0,0,0],[0,0,0],[0,0,0]] 输出:0 解释:增加任何建筑物的高度都会导致天际线的变化。 ``` ## 解题思路 ### 思路 1:贪心 + 矩阵 这道题要求在不改变天际线的前提下,计算建筑物可以增加的最大高度增量总和。 关键观察: - 从东西方向看,天际线由每行的最大值决定。 - 从南北方向看,天际线由每列的最大值决定。 - 对于位置 $(i, j)$ 的建筑物,它的最大高度不能超过第 $i$ 行的最大值和第 $j$ 列的最大值中的较小值。 算法步骤: 1. 计算每行的最大值 $row\_max[i]$。 2. 计算每列的最大值 $col\_max[j]$。 3. 对于每个位置 $(i, j)$,建筑物可以增加的高度为 $\min(row\_max[i], col\_max[j]) - grid[i][j]$。 4. 累加所有位置的增量。 ### 思路 1:代码 ```python class Solution: def maxIncreaseKeepingSkyline(self, grid: List[List[int]]) -> int: n = len(grid) # 计算每行的最大值 row_max = [max(grid[i]) for i in range(n)] # 计算每列的最大值 col_max = [max(grid[i][j] for i in range(n)) for j in range(n)] # 计算总增量 total_increase = 0 for i in range(n): for j in range(n): # 当前位置的最大高度 max_height = min(row_max[i], col_max[j]) # 累加增量 total_increase += max_height - grid[i][j] return total_increase ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是矩阵的边长。需要遍历矩阵计算行列最大值和增量。 - **空间复杂度**:$O(n)$,需要存储每行和每列的最大值。 ================================================ FILE: docs/solutions/0800-0899/maximize-distance-to-closest-person.md ================================================ # [0849. 到最近的人的最大距离](https://leetcode.cn/problems/maximize-distance-to-closest-person/) - 标签:数组 - 难度:中等 ## 题目链接 - [0849. 到最近的人的最大距离 - 力扣](https://leetcode.cn/problems/maximize-distance-to-closest-person/) ## 题目大意 **描述**: 给定一个数组 $seats$ 表示一排座位,其中 $seats[i] = 1$ 代表有人坐在第 i 个座位上,$seats[i] = 0$ 代表座位 $i$ 上是空的(下标从 0 开始)。 至少有一个空座位,且至少有一人已经坐在座位上。 亚历克斯希望坐在一个能够使他与离他最近的人之间的距离达到最大化的座位上。 **要求**: 返回他到离他最近的人的最大距离。 **说明**: - $2 \le seats.length \le 2 * 10^{4}$。 - $seats[i]$ 为 0 或 1。 - 至少有一个「空座位」。 - 至少有一个「座位上有人」。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/09/10/distance.jpg) ```python 输入:seats = [1,0,0,0,1,0,1] 输出:2 解释: 如果亚历克斯坐在第二个空位(seats[2])上,他到离他最近的人的距离为 2 。 如果亚历克斯坐在其它任何一个空位上,他到离他最近的人的距离为 1 。 因此,他到离他最近的人的最大距离是 2 。 ``` - 示例 2: ```python 输入:seats = [1,0,0,0] 输出:3 解释: 如果亚历克斯坐在最后一个座位上,他离最近的人有 3 个座位远。 这是可能的最大距离,所以答案是 3 。 ``` ## 解题思路 ### 思路 1:一次遍历 这道题要求找到使亚历克斯到最近的人的距离最大化的座位。 关键观察: - 最大距离可能出现在三种情况: 1. 两个有人座位之间的中点。 2. 最左边的空座位(如果最左边是空的)。 3. 最右边的空座位(如果最右边是空的)。 算法步骤: 1. 遍历座位数组,记录上一个有人座位的位置 $prev$。 2. 对于每个空座位: - 如果 $prev == -1$(左边没有人),距离为当前位置到第一个有人座位的距离。 - 否则,距离为 $(i - prev) // 2$(两个有人座位之间的中点)。 3. 特殊处理最右边的空座位。 4. 返回最大距离。 ### 思路 1:代码 ```python class Solution: def maxDistToClosest(self, seats: List[int]) -> int: n = len(seats) max_dist = 0 prev = -1 # 上一个有人座位的位置 for i in range(n): if seats[i] == 1: # 如果当前座位有人 if prev == -1: # 如果左边没有人,距离为从开始到当前位置 max_dist = i else: # 否则,距离为两个有人座位之间的中点 max_dist = max(max_dist, (i - prev) // 2) prev = i # 特殊处理最右边的空座位 max_dist = max(max_dist, n - 1 - prev) return max_dist ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是座位数组的长度。只需遍历一次数组。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0800-0899/maximum-frequency-stack.md ================================================ # [0895. 最大频率栈](https://leetcode.cn/problems/maximum-frequency-stack/) - 标签:栈、设计、哈希表、有序集合 - 难度:困难 ## 题目链接 - [0895. 最大频率栈 - 力扣](https://leetcode.cn/problems/maximum-frequency-stack/) ## 题目大意 **要求**: 设计一个类似堆栈的数据结构,将元素推入堆栈,并从堆栈中弹出出现频率最高的元素。 实现 FreqStack 类: - `FreqStack$()` 构造一个空的堆栈。 - `void push(int val)` 将一个整数 $val$ 压入栈顶。 - `int pop()` 删除并返回堆栈中出现频率最高的元素。 - 如果出现频率最高的元素不只一个,则移除并返回最接近栈顶的元素。 **说明**: - $0 \le val \le 10^{9}$。 - `push` 和 `pop` 的操作数不大于 $2 \times 10^{4}$。 - 输入保证在调用 `pop` 之前堆栈中至少有一个元素。 **示例**: - 示例 1: ```python 输入: ["FreqStack","push","push","push","push","push","push","pop","pop","pop","pop"], [[],[5],[7],[5],[7],[4],[5],[],[],[],[]] 输出:[null,null,null,null,null,null,null,5,7,5,4] 解释: FreqStack = new FreqStack(); freqStack.push (5);//堆栈为 [5] freqStack.push (7);//堆栈是 [5,7] freqStack.push (5);//堆栈是 [5,7,5] freqStack.push (7);//堆栈是 [5,7,5,7] freqStack.push (4);//堆栈是 [5,7,5,7,4] freqStack.push (5);//堆栈是 [5,7,5,7,4,5] freqStack.pop ();//返回 5 ,因为 5 出现频率最高。堆栈变成 [5,7,5,7,4]。 freqStack.pop ();//返回 7 ,因为 5 和 7 出现频率最高,但7最接近顶部。堆栈变成 [5,7,5,4]。 freqStack.pop ();//返回 5 ,因为 5 出现频率最高。堆栈变成 [5,7,4]。 freqStack.pop ();//返回 4 ,因为 4, 5 和 7 出现频率最高,但 4 是最接近顶部的。堆栈变成 [5,7]。 ``` ## 解题思路 ### 思路 1:哈希表 + 频率栈 这道题要求设计一个数据结构,支持 `push` 和 `pop` 操作,其中 `pop` 操作返回频率最高的元素(如果有多个,返回最接近栈顶的)。 关键数据结构: 1. $freq$ 哈希表:记录每个元素的出现频率。 2. $group$ 哈希表:记录每个频率对应的元素栈。$group[f]$ 存储所有频率为 $f$ 的元素。 3. $max\_freq$:记录当前最大频率。 算法步骤: - **push 操作**: 1. 更新元素的频率 $freq[val]$。 2. 将元素加入对应频率的栈 $group[freq[val]]$。 3. 更新最大频率 $max\_freq$。 - **pop 操作**: 1. 从最大频率的栈 $group[max\_freq]$ 中弹出元素。 2. 更新该元素的频率 $freq[val]$。 3. 如果最大频率的栈为空,减小 $max\_freq$。 ### 思路 1:代码 ```python class FreqStack: def __init__(self): self.freq = {} # 记录每个元素的频率 self.group = {} # 记录每个频率对应的元素栈 self.max_freq = 0 # 当前最大频率 def push(self, val: int) -> None: # 更新元素的频率 self.freq[val] = self.freq.get(val, 0) + 1 f = self.freq[val] # 将元素加入对应频率的栈 if f not in self.group: self.group[f] = [] self.group[f].append(val) # 更新最大频率 self.max_freq = max(self.max_freq, f) def pop(self) -> int: # 从最大频率的栈中弹出元素 val = self.group[self.max_freq].pop() # 更新元素的频率 self.freq[val] -= 1 # 如果最大频率的栈为空,减小最大频率 if not self.group[self.max_freq]: self.max_freq -= 1 return val # Your FreqStack object will be instantiated and called as such: # obj = FreqStack() # obj.push(val) # param_2 = obj.pop() ``` ### 思路 1:复杂度分析 - **时间复杂度**:`push` 和 `pop` 操作的时间复杂度都是 $O(1)$。 - **空间复杂度**:$O(n)$,其中 $n$ 是元素的总数。需要存储频率和分组信息。 ================================================ FILE: docs/solutions/0800-0899/middle-of-the-linked-list.md ================================================ # [0876. 链表的中间结点](https://leetcode.cn/problems/middle-of-the-linked-list/) - 标签:链表、双指针 - 难度:简单 ## 题目链接 - [0876. 链表的中间结点 - 力扣](https://leetcode.cn/problems/middle-of-the-linked-list/) ## 题目大意 **描述**:给定一个单链表的头节点 `head`。 **要求**:返回链表的中间节点。如果有两个中间节点,则返回第二个中间节点。 **说明**: - 给定链表的结点数介于 `1` 和 `100` 之间。 **示例**: - 示例 1: ```python 输入:[1,2,3,4,5] 输出:此列表中的结点 3 (序列化形式:[3,4,5]) 解释:返回的结点值为 3 。 注意,我们返回了一个 ListNode 类型的对象 ans,这样: ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL. ``` - 示例 2: ```python 输入:[1,2,3,4,5,6] 输出:此列表中的结点 4 (序列化形式:[4,5,6]) 解释:由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。 ``` ## 解题思路 ### 思路 1:单指针 先遍历一遍链表,统计一下节点个数为 `n`,再遍历到 `n / 2` 的位置,返回中间节点。 ### 思路 1:代码 ```python class Solution: def middleNode(self, head: ListNode) -> ListNode: n = 0 curr = head while curr: n += 1 curr = curr.next k = 0 curr = head while k < n // 2: k += 1 curr = curr.next return curr ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:快慢指针 使用步长不一致的快慢指针进行一次遍历找到链表的中间节点。具体做法如下: 1. 使用两个指针 `slow`、`fast`。`slow`、`fast` 都指向链表的头节点。 2. 在循环体中将快、慢指针同时向右移动。其中慢指针每次移动 `1` 步,即 `slow = slow.next`。快指针每次移动 `2` 步,即 `fast = fast.next.next`。 3. 等到快指针移动到链表尾部(即 `fast == Node`)时跳出循环体,此时 `slow` 指向链表中间位置。 4. 返回 `slow` 指针。 ### 思路 2:代码 ```python class Solution: def middleNode(self, head: ListNode) -> ListNode: fast = head slow = head while fast and fast.next: slow = slow.next fast = fast.next.next return slow ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/minimum-cost-to-hire-k-workers.md ================================================ # [0857. 雇佣 K 名工人的最低成本](https://leetcode.cn/problems/minimum-cost-to-hire-k-workers/) - 标签:贪心、数组、排序、堆(优先队列) - 难度:困难 ## 题目链接 - [0857. 雇佣 K 名工人的最低成本 - 力扣](https://leetcode.cn/problems/minimum-cost-to-hire-k-workers/) ## 题目大意 **描述**: 有 $n$ 名工人。 给定两个数组 $quality$ 和 $wage$ ,其中,$quality[i]$ 表示第 $i$ 名工人的工作质量,其最低期望工资为 $wage[i]$。 现在我们想雇佣 $k$ 名工人组成一个 工资组。在雇佣 一组 $k$ 名工人时,我们必须按照下述规则向他们支付工资: 1. 对工资组中的每名工人,应当按其工作质量与同组其他工人的工作质量的比例来支付工资。 2. 工资组中的每名工人至少应当得到他们的最低期望工资。 给定整数 $k$。 **要求**: 返回「组成满足上述条件的付费群体所需的最小金额」。与实际答案误差相差在 $10^{-5}$ 以内的答案将被接受。 **说明**: - $n == quality.length == wage.length$。 - $1 \le k \le n \le 10^{4}$。 - $1 \le quality[i], wage[i] \le 10^{4}$。 **示例**: - 示例 1: ```python 输入: quality = [10,20,5], wage = [70,50,30], k = 2 输出: 105.00000 解释: 我们向 0 号工人支付 70,向 2 号工人支付 35。 ``` - 示例 2: ```python 输入: quality = [3,1,10,10,1], wage = [4,8,2,2,7], k = 3 输出: 30.66667 解释: 我们向 0 号工人支付 4,向 2 号和 3 号分别支付 13.33333。 ``` ## 解题思路 ### 思路 1:贪心 + 堆 关键观察:如果我们选定了某个工人作为"基准",其工资恰好等于最低期望工资,那么其他工人的工资由他们的工作质量比例决定。 设基准工人的工资期望比为 $r = \frac{wage}{quality}$,那么所有工人的工资期望比都不能超过 $r$,否则无法满足最低工资要求。 算法步骤: 1. 计算每个工人的工资期望比 $ratio[i] = \frac{wage[i]}{quality[i]}$ 2. 按照 $ratio$ 从小到大排序 3. 枚举每个工人作为工资期望比最大的工人(基准) 4. 对于当前基准,选择前面 $k-1$ 个工作质量最小的工人,使用大顶堆维护 5. 计算总成本:$ratio \times \sum quality$ ### 思路 1:代码 ```python class Solution: def mincostToHireWorkers(self, quality: List[int], wage: List[int], k: int) -> float: import heapq n = len(quality) # 计算每个工人的工资期望比,并按比例排序 workers = sorted([(wage[i] / quality[i], quality[i]) for i in range(n)]) min_cost = float('inf') quality_sum = 0 # 当前选中的工人的工作质量之和 max_heap = [] # 大顶堆,存储工作质量(取负数实现大顶堆) for ratio, q in workers: # 将当前工人加入堆 heapq.heappush(max_heap, -q) quality_sum += q # 如果堆中元素超过 k 个,移除工作质量最大的 if len(max_heap) > k: quality_sum += heapq.heappop(max_heap) # 注意是负数,所以用加法 # 如果堆中恰好有 k 个工人,计算成本 if len(max_heap) == k: min_cost = min(min_cost, ratio * quality_sum) return min_cost ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是工人数量。排序需要 $O(n \log n)$,每个工人最多入堆出堆一次,堆操作需要 $O(n \log k)$。 - **空间复杂度**:$O(n)$,需要存储排序后的工人信息和堆。 ================================================ FILE: docs/solutions/0800-0899/minimum-number-of-refueling-stops.md ================================================ # [0871. 最低加油次数](https://leetcode.cn/problems/minimum-number-of-refueling-stops/) - 标签:贪心、数组、动态规划、堆(优先队列) - 难度:困难 ## 题目链接 - [0871. 最低加油次数 - 力扣](https://leetcode.cn/problems/minimum-number-of-refueling-stops/) ## 题目大意 **描述**: 汽车从起点出发驶向目的地,该目的地位于出发位置东面 $target$ 英里处。 沿途有加油站,用数组 $stations$ 表示。其中 $stations[i] = [position_i, fuel_i] 表示第 $i$ 个加油站位于出发位置东面 $position_i$ 英里处,并且有 $fuel_i$ 升汽油。 假设汽车油箱的容量是无限的,其中最初有 $startFuel$ 升燃料。它每行驶 1 英里就会用掉 1 升汽油。当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。 **要求**: 为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 -1。 **说明**: - 注意:如果汽车到达加油站时剩余燃料为 0,它仍然可以在那里加油。如果汽车到达目的地时剩余燃料为 0,仍然认为它已经到达目的地。 - $1 \le target, startFuel \le 10^{9}$。 - $0 \le stations.length \le 500$。 - $1 \le position_i \lt position_i+1 \lt target$。 - $1 \le fuel_i \lt 10^{9}$。 **示例**: - 示例 1: ```python 输入:target = 1, startFuel = 1, stations = [] 输出:0 解释:可以在不加油的情况下到达目的地。 ``` - 示例 2: ```python 输入:target = 100, startFuel = 10, stations = [[10,60],[20,30],[30,30],[60,40]] 输出:2 解释: 出发时有 10 升燃料。 开车来到距起点 10 英里处的加油站,消耗 10 升燃料。将汽油从 0 升加到 60 升。 然后,从 10 英里处的加油站开到 60 英里处的加油站(消耗 50 升燃料), 并将汽油从 10 升加到 50 升。然后开车抵达目的地。 沿途在两个加油站停靠,所以返回 2 。 ``` ## 解题思路 ### 思路 1:贪心 + 堆(优先队列) 这道题要求计算到达目的地所需的最低加油次数。 贪心策略: - 尽可能少加油,每次加油时选择之前经过的加油站中油量最多的。 - 使用最大堆记录经过的加油站的油量。 算法步骤: 1. 初始化当前位置 $pos = 0$,当前油量 $fuel = startFuel$,加油次数 $count = 0$。 2. 使用最大堆存储经过的加油站的油量。 3. 遍历加油站: - 如果当前油量不足以到达下一个加油站,从堆中取出最大油量加油,直到能到达或堆为空。 - 如果堆为空仍无法到达,返回 $-1$。 - 将当前加油站的油量加入堆。 4. 检查是否能到达目的地,如果不能,继续加油。 5. 返回加油次数。 ### 思路 1:代码 ```python class Solution: def minRefuelStops(self, target: int, startFuel: int, stations: List[List[int]]) -> int: import heapq # 最大堆(Python 的 heapq 是最小堆,所以存储负值) max_heap = [] fuel = startFuel count = 0 prev = 0 # 遍历所有加油站 for position, gas in stations: # 尝试到达当前加油站 fuel -= (position - prev) # 如果油量不足,从之前经过的加油站中选择油量最多的加油 while fuel < 0 and max_heap: fuel += -heapq.heappop(max_heap) count += 1 # 如果仍然无法到达,返回 -1 if fuel < 0: return -1 # 将当前加油站的油量加入堆 heapq.heappush(max_heap, -gas) prev = position # 尝试到达目的地 fuel -= (target - prev) while fuel < 0 and max_heap: fuel += -heapq.heappop(max_heap) count += 1 # 如果仍然无法到达,返回 -1 if fuel < 0: return -1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是加油站的数量。每个加油站最多入堆和出堆一次。 - **空间复杂度**:$O(n)$,需要使用堆存储加油站的油量。 ================================================ FILE: docs/solutions/0800-0899/minimum-swaps-to-make-sequences-increasing.md ================================================ # [0801. 使序列递增的最小交换次数](https://leetcode.cn/problems/minimum-swaps-to-make-sequences-increasing/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [0801. 使序列递增的最小交换次数 - 力扣](https://leetcode.cn/problems/minimum-swaps-to-make-sequences-increasing/) ## 题目大意 给定两个长度相等的整形数组 A 和 B。可以交换两个数组相同位置上的元素,比如 A[i] 与 B[i] 交换,可以交换多个位置,但要保证交换之后保证数组 A和数组 B 是严格递增的。 要求:返回使得数组 A和数组 B 保持严格递增状态的最小交换次数。假设给定的输入一定有效。 ## 解题思路 可以用动态规划来做。 对于两个数组每一个位置上的元素 A[i] 和 B[i] 来说,只有两种情况:换或者不换。 动态规划的状态 `dp[i][j]` 表示为:第 i 个位置元素,不交换(j = 0)、交换(j = 1)状态时的最小交换次数。 如果数组元素个数只有一个,则: - `dp[0][0] = 0` ,第 0 个元素不做交换,交换次数为 0。 - `dp[0][1] = 1`,第 0 个元素做交换,交换次数为 1。 如果有 2 个元素,为了保证两个数组中的相邻元素都为递增元素,则第 2 个元素交换与否与第 1 个元素有关。同理如果有多个元素,那么第 i 个元素交换与否,只与第 i - 1 个元素有关。现在来考虑第 i 个元素与第 i - 1 的元素的情况。 先按原本数组当前是否满足递增关系来划分,可以划分为: - 原本数组都满足递增关系,即 `A[i - 1] < A[i]` 并且 `B[i - 1] < B[i]`。 - 不满足上述递增关系的情况,即 `A[i - 1] >= A[i]` 或者 `B[i - 1] >= B[i]`。 可以看出,不满足递增关系的情况下是肯定要交换的。只需要考虑交换第 i 位元素,还是第 i - 1 位元素。 - `dp[i][0] = dp[i - 1][1]`,第 i 位如果不交换,则第 i - 1 位必须交换。 - `dp[i][1] = dp[i - 1][0] + 1`,第 i 位交换,则第 i - 1 位不能交换。 下面再来考虑原本数组都满足递增关系的情况。考虑两个数组间相邻元素的关系。 - `A[i - 1] < B[i]` 并且 `B[i - 1] < A[i]`。 - `A[i - 1] >= B[i]` 或者 `B[i - 1] >= A[i]`。 如果是 `A[i - 1] < B[i]` 并且 `B[i - 1] < A[i]` 情况下,第 i 位交换,与第 i - 1 位交换与否无关,则 `dp[i][j]` 只需取 `dp[i-1][j]` 上较小结果进行计算即可,即: - `dp[i][0] = min(dp[i-1][0], dp[i-1][1])` - `dp[i][1] = min(dp[i-1][0], dp[i-1][1]) + 1` 如果是 `A[i - 1] >= B[i]` 或者 `B[i - 1] >= A[i]` 情况下,则如果第 i 位交换,则第 i - 1 位必须跟着交换。如果第 i 位不交换,则第 i - 1 为也不能交换,即: - `dp[i][0] = dp[i - 1][0]`,如果第 i 位不交换,则第 i - 1 位也不交换。 - `dp[i][1] = dp[i - 1][1] + 1`,如果第 i 位交换,则第 i - 1 位也必须交换。 这样就考虑了所有的情况,最终返回最后一个元素,(交换、不交换)状态下的最小值即可。 ## 代码 ```python class Solution: def minSwap(self, nums1: List[int], nums2: List[int]) -> int: size = len(nums1) dp = [[0 for _ in range(size)] for _ in range(size)] dp[0][1] = 1 for i in range(1, size): if nums1[i - 1] < nums1[i] and nums2[i - 1] < nums2[i]: if nums1[i - 1] < nums2[i] and nums2[i - 1] < nums1[i]: # 第 i 位交换,与第 i - 1 位交换与否无关 dp[i][0] = min(dp[i-1][0], dp[i-1][1]) dp[i][1] = min(dp[i-1][0], dp[i-1][1]) + 1 else: # 如果第 i 位不交换,则第 i - 1 位也不交换 # 如果第 i 位交换,则第 i - 1 位也必须交换 dp[i][0] = dp[i - 1][0] dp[i][1] = dp[i - 1][1] + 1 else: dp[i][0] = dp[i - 1][1] # 如果第 i 位如果不交换,则第 i - 1 位必须交换 dp[i][1] = dp[i - 1][0] + 1 # 如果第 i 位交换,则第 i - 1 位不能交换 return min(dp[size - 1][0], dp[size - 1][1]) ``` ================================================ FILE: docs/solutions/0800-0899/mirror-reflection.md ================================================ # [0858. 镜面反射](https://leetcode.cn/problems/mirror-reflection/) - 标签:几何、数学、数论 - 难度:中等 ## 题目链接 - [0858. 镜面反射 - 力扣](https://leetcode.cn/problems/mirror-reflection/) ## 题目大意 **描述**: 有一个特殊的正方形房间,每面墙上都有一面镜子。除西南角以外,每个角落都放有一个接受器,编号为 0, 1,以及 2。 正方形房间的墙壁长度为 $p$,一束激光从西南角射出,首先会与东墙相遇,入射点到接收器 0 的距离为 $q$。 **要求**: 返回光线最先遇到的接收器的编号(保证光线最终会遇到一个接收器)。 **说明**: - $1 \le q \le p \le 10^{3}$。 **示例**: - 示例 1: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/06/18/reflection.png) ```python 输入:p = 2, q = 1 输出:2 解释:这条光线在第一次被反射回左边的墙时就遇到了接收器 2 。 ``` - 示例 2: ```python 输入:p = 3, q = 1 输入:1 ``` ## 解题思路 ### 思路 1:数学 + 最大公约数 这道题要求计算激光在正方形房间中经过镜面反射后最先遇到的接收器编号。 关键观察: - 可以将镜面反射问题转换为"展开"问题:将房间沿着镜面展开,激光沿直线传播。 - 激光从 $(0, 0)$ 出发,斜率为 $\frac{q}{p}$,最终会到达某个点 $(m \times p, n \times q)$,其中 $m$ 和 $n$ 是整数。 - 需要找到最小的 $m$ 和 $n$,使得 $(m \times p, n \times q)$ 对应某个接收器。 接收器位置: - 接收器 $0$:东南角 $(p, 0)$,对应 $m$ 为奇数,$n$ 为偶数。 - 接收器 $1$:东北角 $(p, p)$,对应 $m$ 和 $n$ 都为奇数。 - 接收器 $2$:西北角 $(0, p)$,对应 $m$ 为偶数,$n$ 为奇数。 算法步骤: 1. 计算 $p$ 和 $q$ 的最大公约数 $g$。 2. 化简 $m = \frac{p}{g}$,$n = \frac{q}{g}$。 3. 根据 $m$ 和 $n$ 的奇偶性判断接收器编号。 ### 思路 1:代码 ```python class Solution: def mirrorReflection(self, p: int, q: int) -> int: from math import gcd # 计算最大公约数 g = gcd(p, q) # 化简 m = p // g # 水平方向的反射次数 n = q // g # 垂直方向的反射次数 # 根据奇偶性判断接收器编号 if m % 2 == 1 and n % 2 == 1: return 1 # 东北角 elif m % 2 == 1 and n % 2 == 0: return 0 # 东南角 else: # m % 2 == 0 and n % 2 == 1 return 2 # 西北角 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log \min(p, q))$,计算最大公约数的时间复杂度。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0800-0899/monotonic-array.md ================================================ # [0896. 单调数列](https://leetcode.cn/problems/monotonic-array/) - 标签:数组 - 难度:简单 ## 题目链接 - [0896. 单调数列 - 力扣](https://leetcode.cn/problems/monotonic-array/) ## 题目大意 **描述**: 如果数组是单调递增或单调递减的,那么它是「单调」的。 如果对于所有 $i \le j$,$nums[i] \le nums[j]$,那么数组 $nums$ 是单调递增的。如果对于所有 $i \le j$,$nums[i] \ge nums[j]$,那么数组 $nums$ 是单调递减的。 **要求**: 当给定的数组 $nums$ 是单调数组时返回 true,否则返回 false。 **说明**: - $1 \le nums.length \le 10^{5}$。 - $-10^{5} \le nums[i] \le 10^{5}$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,2,3] 输出:true ``` - 示例 2: ```python 输入:nums = [6,5,4,4] 输出:true ``` ## 解题思路 ### 思路 1:一次遍历 这道题要求判断数组是否单调(单调递增或单调递减)。 算法步骤: 1. 使用两个布尔变量 $increasing$ 和 $decreasing$ 分别表示数组是否单调递增和单调递减,初始值都为 $True$。 2. 遍历数组,比较相邻元素: - 如果 $nums[i] > nums[i-1]$,说明不是单调递减,设置 $decreasing = False$。 - 如果 $nums[i] < nums[i-1]$,说明不是单调递增,设置 $increasing = False$。 3. 最后返回 $increasing$ 或 $decreasing$ 是否为 $True$。 ### 思路 1:代码 ```python class Solution: def isMonotonic(self, nums: List[int]) -> bool: n = len(nums) if n <= 1: return True # 标记是否单调递增和单调递减 increasing = True decreasing = True # 遍历数组,比较相邻元素 for i in range(1, n): if nums[i] > nums[i - 1]: decreasing = False # 不是单调递减 if nums[i] < nums[i - 1]: increasing = False # 不是单调递增 # 只要满足其中一个条件即可 return increasing or decreasing ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组的长度。只需遍历一次数组。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0800-0899/most-common-word.md ================================================ # [0819. 最常见的单词](https://leetcode.cn/problems/most-common-word/) - 标签:哈希表、字符串、计数 - 难度:简单 ## 题目链接 - [0819. 最常见的单词 - 力扣](https://leetcode.cn/problems/most-common-word/) ## 题目大意 **描述**:给定一个字符串 $paragraph$ 表示段落,再给定搞一个禁用单词列表 $banned$。 **要求**:返回出现次数最多,同时不在禁用列表中的单词。 **说明**: - 题目保证至少有一个词不在禁用列表中,而且答案唯一。 - 禁用列表 $banned$ 中的单词用小写字母表示,不含标点符号。 - 段落 $paragraph$ 只包含字母、空格和下列标点符号`!?',;.` - 段落中的单词不区分大小写。 - $1 \le \text{段落长度} \le 1000$。 - $0 \le \text{禁用单词个数} \le 100$。 - $1 \le \text{禁用单词长度} \le 10$。 - 答案是唯一的,且都是小写字母(即使在 $paragraph$ 里是大写的,即使是一些特定的名词,答案都是小写的)。 - 不存在没有连字符或者带有连字符的单词。 - 单词里只包含字母,不会出现省略号或者其他标点符号。 **示例**: - 示例 1: ```python 输入: paragraph = "Bob hit a ball, the hit BALL flew far after it was hit." banned = ["hit"] 输出: "ball" 解释: "hit" 出现了3次,但它是一个禁用的单词。 "ball" 出现了2次 (同时没有其他单词出现2次),所以它是段落里出现次数最多的,且不在禁用列表中的单词。 注意,所有这些单词在段落里不区分大小写,标点符号需要忽略(即使是紧挨着单词也忽略, 比如 "ball,"), "hit"不是最终的答案,虽然它出现次数更多,但它在禁用单词列表中。 ``` - 示例 2: ```python 输入: paragraph = "a." banned = [] 输出:"a" ``` ## 解题思路 ### 思路 1:哈希表 1. 将禁用词列表转为集合 $banned\_set$。 2. 遍历段落 $paragraph$,获取段落中的所有单词。 3. 判断当前单词是否在禁用词集合中,如果不在禁用词集合中,则使用哈希表对该单词进行计数。 4. 遍历完,找出哈希表中频率最大的单词,将该单词作为答案进行返回。 ### 思路 1:代码 ```python class Solution: def mostCommonWord(self, paragraph: str, banned: List[str]) -> str: banned_set = set(banned) cnts = Counter() word = "" for ch in paragraph: if ch.isalpha(): word += ch.lower() else: if word and word not in banned_set: cnts[word] += 1 word = "" if word and word not in banned_set: cnts[word] += 1 max_cnt, ans = 0, "" for word, cnt in cnts.items(): if cnt > max_cnt: max_cnt = cnt ans = word return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 为段落 $paragraph$ 的长度,$m$ 是禁用词 $banned$ 的长度。 - **空间复杂度**:$O(n + m)$。 ================================================ FILE: docs/solutions/0800-0899/most-profit-assigning-work.md ================================================ # [0826. 安排工作以达到最大收益](https://leetcode.cn/problems/most-profit-assigning-work/) - 标签:贪心、数组、双指针、二分查找、排序 - 难度:中等 ## 题目链接 - [0826. 安排工作以达到最大收益 - 力扣](https://leetcode.cn/problems/most-profit-assigning-work/) ## 题目大意 **描述**: 你有 $n$ 个工作和 $m$ 个工人。给定三个数组:$difficulty$, $profit$ 和 $worker$,其中: - $difficulty[i]$ 表示第 $i$ 个工作的难度,$profit[i]$ 表示第 $i$ 个工作的收益。 - $worker[i]$ 是第 $i$ 个工人的能力,即该工人只能完成难度小于等于 $worker[i]$ 的工作。 每个工人「最多」只能安排「一个」工作,但是一个工作可以「完成多次」。 - 举个例子,如果 3 个工人都尝试完成一份报酬为 1 的同样工作,那么总收益为 $3$。如果一个工人不能完成任何工作,他的收益为 0。 **要求**: 返回「在把工人分配到工作岗位后,我们所能获得的最大利润」。 **说明**: - $n == difficulty.length$。 - $n == profit.length$。 - $m == worker.length$。 - $1 \le n, m \le 10^{4}$。 - $1 \le difficulty[i], profit[i], worker[i] \le 10^{5}$。 **示例**: - 示例 1: ```python 输入: difficulty = [2,4,6,8,10], profit = [10,20,30,40,50], worker = [4,5,6,7] 输出: 100 解释: 工人被分配的工作难度是 [4,4,6,6] ,分别获得 [20,20,30,30] 的收益。 ``` - 示例 2: ```python 输入: difficulty = [85,47,57], profit = [24,66,99], worker = [40,25,25] 输出: 0 ``` ## 解题思路 ### 思路 1:贪心 + 排序 + 双指针 这道题要求安排工人完成工作以达到最大收益。每个工人只能完成一个工作,且工作难度不能超过工人能力。 贪心策略: - 对于每个工人,选择他能完成的工作中收益最大的。 算法步骤: 1. 将工作按难度排序,同时记录对应的收益。 2. 预处理:对于每个难度,记录到该难度为止的最大收益(因为难度更高的工作收益不一定更高)。 3. 将工人按能力排序。 4. 使用双指针遍历工人和工作: - 对于每个工人,找到他能完成的所有工作中收益最大的。 - 累加收益。 ### 思路 1:代码 ```python class Solution: def maxProfitAssignment(self, difficulty: List[int], profit: List[int], worker: List[int]) -> int: # 将工作按难度排序 jobs = sorted(zip(difficulty, profit)) # 预处理:记录到每个难度为止的最大收益 max_profit = 0 for i in range(len(jobs)): max_profit = max(max_profit, jobs[i][1]) jobs[i] = (jobs[i][0], max_profit) # 将工人按能力排序 worker.sort() total_profit = 0 job_idx = 0 # 遍历每个工人 for ability in worker: # 找到该工人能完成的所有工作中收益最大的 while job_idx < len(jobs) and jobs[job_idx][0] <= ability: job_idx += 1 # 如果该工人能完成至少一个工作 if job_idx > 0: total_profit += jobs[job_idx - 1][1] return total_profit ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n + m \log m)$,其中 $n$ 是工作数量,$m$ 是工人数量。需要对工作和工人排序。 - **空间复杂度**:$O(n)$,需要存储排序后的工作列表。 ================================================ FILE: docs/solutions/0800-0899/new-21-game.md ================================================ # [0837. 新 21 点](https://leetcode.cn/problems/new-21-game/) - 标签:数学、动态规划、滑动窗口、概率与统计 - 难度:中等 ## 题目链接 - [0837. 新 21 点 - 力扣](https://leetcode.cn/problems/new-21-game/) ## 题目大意 **描述**: 爱丽丝参与一个大致基于纸牌游戏「21点」规则的游戏,描述如下: 爱丽丝以 0 分开始,并在她的得分少于 $k$ 分时抽取数字。 抽取时,她从 $[1, maxPts]$ 的范围中随机获得一个整数作为分数进行累计,其中 $maxPts$ 是一个整数。每次抽取都是独立的,其结果具有相同的概率。 当爱丽丝获得 $k$ 分或更多分时,她就停止抽取数字。 **要求**: 计算爱丽丝的分数不超过 $n$ 的概率。 **说明**: - 与实际答案误差不超过 $10^{-5}$ 的答案将被视为正确答案。 - $0 \le k \le n \le 10^{4}$。 - $1 \le maxPts \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:n = 10, k = 1, maxPts = 10 输出:1.00000 解释:爱丽丝得到一张牌,然后停止。 ``` - 示例 2: ```python 输入:n = 6, k = 1, maxPts = 10 输出:0.60000 解释:爱丽丝得到一张牌,然后停止。 在 10 种可能性中的 6 种情况下,她的得分不超过 6 分。 ``` ## 解题思路 ### 思路 1:动态规划 + 滑动窗口 定义 $dp[x]$ 表示从分数为 $x$ 的情况开始,最终得分不超过 $n$ 的概率。 状态转移: - 当 $x \ge k$ 时,游戏停止,如果 $x \le n$,则 $dp[x] = 1$,否则 $dp[x] = 0$ - 当 $x < k$ 时,可以抽取 $[1, maxPts]$ 中的任意数字,每个数字的概率为 $\frac{1}{maxPts}$: $$dp[x] = \frac{1}{maxPts} \sum_{i=1}^{maxPts} dp[x+i]$$ 为了优化计算,我们可以维护一个滑动窗口的和 $sum$,表示 $\sum_{i=x+1}^{x+maxPts} dp[i]$。 ### 思路 1:代码 ```python class Solution: def new21Game(self, n: int, k: int, maxPts: int) -> float: # 特判:如果 k = 0 或 n >= k + maxPts - 1,概率为 1 if k == 0 or n >= k + maxPts - 1: return 1.0 # dp[x] 表示从分数 x 开始,最终得分不超过 n 的概率 dp = [0.0] * (n + 1) dp[0] = 1.0 # sum 表示滑动窗口的和 window_sum = 1.0 for x in range(1, n + 1): # 当前分数的概率 dp[x] = window_sum / maxPts if x < k: # 如果还可以继续抽牌,将当前概率加入窗口 window_sum += dp[x] # 移除窗口左边界 if x >= maxPts: window_sum -= dp[x - maxPts] # 答案是从 0 分开始,最终得分在 [k, n] 范围内的概率 return dp[0] if k == 0 else sum(dp[k:n+1]) / maxPts * maxPts if k > 0 else dp[0] ``` 实际上,我们需要重新理解题意。让我重写: ```python class Solution: def new21Game(self, n: int, k: int, maxPts: int) -> float: # 特判 if k == 0 or n >= k + maxPts - 1: return 1.0 # dp[x] 表示得分为 x 时,最终得分不超过 n 的概率 dp = [0.0] * (k + maxPts) # 初始化:得分在 [k, n] 范围内的概率为 1 for i in range(k, min(n + 1, k + maxPts)): dp[i] = 1.0 # 滑动窗口和 window_sum = min(n - k + 1, maxPts) # 从 k-1 倒推到 0 for x in range(k - 1, -1, -1): dp[x] = window_sum / maxPts # 更新窗口:加入 dp[x+1],移除 dp[x+maxPts+1] window_sum += dp[x] if x + maxPts < len(dp): window_sum -= dp[x + maxPts] return dp[0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(k + maxPts)$,需要计算 $k + maxPts$ 个状态。 - **空间复杂度**:$O(k + maxPts)$,需要存储 $dp$ 数组。 ================================================ FILE: docs/solutions/0800-0899/nth-magical-number.md ================================================ # [0878. 第 N 个神奇数字](https://leetcode.cn/problems/nth-magical-number/) - 标签:数学、二分查找 - 难度:困难 ## 题目链接 - [0878. 第 N 个神奇数字 - 力扣](https://leetcode.cn/problems/nth-magical-number/) ## 题目大意 **描述**: - 一个正整数如果能被 $a$ 或 $b$ 整除,那么它是神奇的。 给定三个整数 $n$ , $a$, $b$。 **要求**: 返回第 $n$ 个神奇的数字。因为答案可能很大,所以返回答案对 $10^9 + 7$ 取模后的值。 **说明**: - $1 \le n \le 10^{9}$。 - $2 \le a, b \le 4 \times 10^{4}$。 **示例**: - 示例 1: ```python 输入:n = 1, a = 2, b = 3 输出:2 ``` - 示例 2: ```python 输入:n = 4, a = 2, b = 3 输出:6 ``` ## 解题思路 ### 思路 1:二分查找 + 容斥原理 要找第 $n$ 个神奇数字,我们可以使用二分查找。关键是如何计算不超过 $x$ 的神奇数字个数。 根据容斥原理,不超过 $x$ 的神奇数字个数为: $$count(x) = \lfloor \frac{x}{a} \rfloor + \lfloor \frac{x}{b} \rfloor - \lfloor \frac{x}{lcm(a,b)} \rfloor$$ 其中 $lcm(a,b)$ 是 $a$ 和 $b$ 的最小公倍数,可以通过 $lcm(a,b) = \frac{a \times b}{gcd(a,b)}$ 计算。 二分查找的范围: - 左边界:$1$ - 右边界:$n \times \min(a, b)$ ### 思路 1:代码 ```python class Solution: def nthMagicalNumber(self, n: int, a: int, b: int) -> int: import math MOD = 10**9 + 7 # 计算最小公倍数 lcm = a * b // math.gcd(a, b) # 计算不超过 x 的神奇数字个数 def count(x): return x // a + x // b - x // lcm # 二分查找 left, right = 1, n * min(a, b) while left < right: mid = (left + right) // 2 if count(mid) < n: left = mid + 1 else: right = mid return left % MOD ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log(n \times \min(a, b)))$,二分查找的时间复杂度。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0800-0899/number-of-lines-to-write-string.md ================================================ # [0806. 写字符串需要的行数](https://leetcode.cn/problems/number-of-lines-to-write-string/) - 标签:数组、字符串 - 难度:简单 ## 题目链接 - [0806. 写字符串需要的行数 - 力扣](https://leetcode.cn/problems/number-of-lines-to-write-string/) ## 题目大意 **描述**:给定一个数组 $widths$,其中 $words[0]$ 代表 `'a'` 需要的单位,$words[1]$ 代表 `'b'` 需要的单位,…,$words[25]$ 代表 `'z'` 需要的单位。再给定一个字符串 $s$,现在需要将字符串 $s$ 从左到右写到每一行上,每一行的最大宽度为 $100$ 个单位,如果在写某个字符的时候使改行超过了 $100$ 个单位,那么我们应该将这个字母写到下一行。 **要求**:计算出能放下 $s$ 的最少行数,以及最后一行使用的宽度单位。 **说明**: - 字符串 $s$ 的长度在 $[1, 1000]$ 的范围。 - $s$ 只包含小写字母。 - $widths$ 是长度为 $26$ 的数组。 - $widths[i]$ 值的范围在 $[2, 10]$。 **示例**: - 示例 1: ```python 输入: widths = [10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10] S = "abcdefghijklmnopqrstuvwxyz" 输出: [3, 60] 解释: 所有的字符拥有相同的占用单位10。所以书写所有的26个字母, 我们需要2个整行和占用60个单位的一行。 ``` - 示例 2: ```python 输入: widths = [4,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10] S = "bbbcccdddaaa" 输出: [2, 4] 解释: 除去字母'a'所有的字符都是相同的单位10,并且字符串 "bbbcccdddaa" 将会覆盖 9 * 10 + 2 * 4 = 98 个单位. 最后一个字母 'a' 将会被写到第二行,因为第一行只剩下2个单位了。 所以,这个答案是2行,第二行有4个单位宽度。 ``` ## 解题思路 ### 思路 1:模拟 1. 使用变量 $line\_cnt$ 记录行数,使用变量 $last\_cnt$ 记录最后一行使用的单位数。 2. 遍历字符串,如果当前最后一行使用的单位数 + 当前字符需要的单位超过了 $100$,则: 1. 另起一行填充字符。(即行数加 $1$,最后一行使用的单位数为当前字符宽度)。 3. 如果当前最后一行使用的单位数 + 当前字符需要的单位没有超过 $100$,则: 1. 在当前行填充字符。(即最后一行使用的单位数累加上当前字符宽度)。 ### 思路 1:代码 ```python class Solution: def numberOfLines(self, widths: List[int], s: str) -> List[int]: line_cnt, last_cnt = 1, 0 for ch in s: width = widths[ord(ch) - ord('a')] if last_cnt + width > 100: line_cnt += 1 last_cnt = width else: last_cnt += width return [line_cnt, last_cnt] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/orderly-queue.md ================================================ # [0899. 有序队列](https://leetcode.cn/problems/orderly-queue/) - 标签:数学、字符串、排序 - 难度:困难 ## 题目链接 - [0899. 有序队列 - 力扣](https://leetcode.cn/problems/orderly-queue/) ## 题目大意 **描述**: 给定一个字符串 $s$ 和一个整数 $k$。你可以从 $s$ 的前 $k$ 个字母中选择一个,并把它加到字符串的末尾。 **要求**: 返回在应用上述步骤的任意数量的移动后,字典序最小的字符串。 **说明**: - $1 \le k \le S.length \le 10^{3}$。 - $s$ 只由小写字母组成。 **示例**: - 示例 1: ```python 输入:s = "cba", k = 1 输出:"acb" 解释: 在第一步中,我们将第一个字符(“c”)移动到最后,获得字符串 “bac”。 在第二步中,我们将第一个字符(“b”)移动到最后,获得最终结果 “acb”。 ``` - 示例 2: ```python 输入:s = "baaca", k = 3 输出:"aaabc" 解释: 在第一步中,我们将第一个字符(“b”)移动到最后,获得字符串 “aacab”。 在第二步中,我们将第三个字符(“c”)移动到最后,获得最终结果 “aaabc”。 ``` ## 解题思路 ### 思路 1:数学 + 排序 这道题的关键在于分析 $k$ 的不同取值: 1. **当 $k = 1$ 时**:只能将第一个字符移到末尾,相当于字符串的循环移位。我们需要枚举所有可能的循环移位,找到字典序最小的。 2. **当 $k \ge 2$ 时**:可以实现任意两个字符的交换,因此可以得到任意排列。此时答案就是将字符串排序后的结果。 证明 $k \ge 2$ 时可以交换任意两个字符: - 假设要交换位置 $i$ 和 $j$ 的字符($i < j$) - 可以通过一系列操作,将这两个字符移到相邻位置,然后交换它们 ### 思路 1:代码 ```python class Solution: def orderlyQueue(self, s: str, k: int) -> str: if k == 1: # k = 1 时,枚举所有循环移位 result = s for i in range(len(s)): # 将前 i 个字符移到末尾 rotated = s[i:] + s[:i] if rotated < result: result = rotated return result else: # k >= 2 时,可以得到任意排列,返回排序后的字符串 return ''.join(sorted(s)) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是字符串的长度。当 $k = 1$ 时,需要枚举 $n$ 种循环移位,每次比较需要 $O(n)$;当 $k \ge 2$ 时,排序需要 $O(n \log n)$。 - **空间复杂度**:$O(n)$,需要存储结果字符串。 ================================================ FILE: docs/solutions/0800-0899/peak-index-in-a-mountain-array.md ================================================ # [0852. 山脉数组的峰顶索引](https://leetcode.cn/problems/peak-index-in-a-mountain-array/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [0852. 山脉数组的峰顶索引 - 力扣](https://leetcode.cn/problems/peak-index-in-a-mountain-array/) ## 题目大意 **描述**:给定由整数组成的山脉数组 $arr$。 **要求**:返回任何满足 $arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[len(arr) - 1] $ 的下标 $i$。 **说明**: - **山脉数组**:满足以下属性的数组: 1. $len(arr) \ge 3$; 2. 存在 $i$($0 < i < len(arr) - 1$),使得: 1. $arr[0] < arr[1] < ... arr[i-1] < arr[i]$; 2. $arr[i] > arr[i+1] > ... > arr[len(arr) - 1]$。 - $3 <= arr.length <= 105$ - $0 <= arr[i] <= 106$ - 题目数据保证 $arr$ 是一个山脉数组 **示例**: - 示例 1: ```python 输入:arr = [0,1,0] 输出:1 ``` - 示例 2: ```python 输入:arr = [0,2,1,0] 输出:1 ``` ## 解题思路 ### 思路 1:二分查找 1. 使用两个指针 $left$、$right$ 。$left$ 指向数组第一个元素,$right$ 指向数组最后一个元素。 2. 取区间中间节点 $mid$,并比较 $nums[mid]$ 和 $nums[mid + 1]$ 的值大小。 1. 如果 $nums[mid]< nums[mid + 1]$,则右侧存在峰值,令 `left = mid + 1`。 2. 如果 $nums[mid] \ge nums[mid + 1]$,则左侧存在峰值,令 `right = mid`。 3. 最后,当 $left == right$ 时,跳出循环,返回 $left$。 ### 思路 1:代码 ```python class Solution: def peakIndexInMountainArray(self, arr: List[int]) -> int: left = 0 right = len(arr) - 1 while left < right: mid = left + (right - left) // 2 if arr[mid] < arr[mid + 1]: left = mid + 1 else: right = mid return left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/positions-of-large-groups.md ================================================ # [0830. 较大分组的位置](https://leetcode.cn/problems/positions-of-large-groups/) - 标签:字符串 - 难度:简单 ## 题目链接 - [0830. 较大分组的位置 - 力扣](https://leetcode.cn/problems/positions-of-large-groups/) ## 题目大意 **描述**:给定由小写字母构成的字符串 $s$。字符串 $s$ 包含一些连续的相同字符所构成的分组。 **要求**:找到每一个较大分组的区间,按起始位置下标递增顺序排序后,返回结果。 **说明**: - **较大分组**:我们称所有包含大于或等于三个连续字符的分组为较大分组。 **示例**: - 示例 1: ```python 输入:s = "abbxxxxzzy" 输出:[[3,6]] 解释:"xxxx" 是一个起始于 3 且终止于 6 的较大分组。 ``` - 示例 2: ```python 输入:s = "abc" 输出:[] 解释:"a","b" 和 "c" 均不是符合要求的较大分组。 ``` ## 解题思路 ### 思路 1:简单模拟 遍历字符串 $s$,统计出所有大于等于 $3$ 个连续字符的子字符串的开始位置与结束位置。具体步骤如下: 1. 令 $cnt = 1$,然后从下标 $1$ 位置开始遍历字符串 $s$。 1. 如果 $s[i - 1] == s[i]$,则令 $cnt$ 加 $1$。 2. 如果 $s[i - 1] \ne s[i]$,说明出现了不同字符,则判断之前连续字符个数 $cnt$ 是否大于等于 $3$。 3. 如果 $cnt \ge 3$,则将对应包含 $cnt$ 个连续字符的子字符串的开始位置与结束位置存入答案数组中。 4. 令 $cnt = 1$,重新开始记录连续字符个数。 2. 遍历完字符串 $s$,输出答案数组。 ### 思路 1:代码 ```python class Solution: def largeGroupPositions(self, s: str) -> List[List[int]]: res = [] cnt = 1 size = len(s) for i in range(1, size): if s[i] == s[i - 1]: cnt += 1 else: if cnt >= 3: res.append([i - cnt, i - 1]) cnt = 1 if cnt >= 3: res.append([size - cnt, size - 1]) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/possible-bipartition.md ================================================ # [0886. 可能的二分法](https://leetcode.cn/problems/possible-bipartition/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [0886. 可能的二分法 - 力扣](https://leetcode.cn/problems/possible-bipartition/) ## 题目大意 把 n 个人(编号为 1, 2, ... , n)分为任意大小的两组。每个人都可能不喜欢其他人,那么他们不应该属于同一组。 给定表示不喜欢关系的数组 `dislikes`,其中 `dislikes[i] = [a, b]` 表示 `a` 和 `b` 互相不喜欢,不允许将编号 `a` 和 `b` 的人归入同一组。 要求:如果可以以这种方式将所有人分为两组,则返回 `True`;如果不能则返回 `False`。 ## 解题思路 先构建图,对于 `dislikes[i] = [a, b]`,在节点 `a` 和 `b` 之间建立一条无向边,然后判断该图是否为二分图。具体做法如下: - 找到一个没有染色的节点 `u`,将其染成红色。 - 然后遍历该节点直接相连的节点 `v`,如果该节点没有被染色,则将该节点直接相连的节点染成蓝色,表示两个节点不是同一集合。如果该节点已经被染色并且颜色跟 `u` 一样,则说明该图不是二分图,直接返回 `False`。 - 从上面染成蓝色的节点 `v` 出发,遍历该节点直接相连的节点。。。依次类推的递归下去。 - 如果所有节点都顺利染上色,则说明该图为二分图,可以将所有人分为两组,返回 `True`。否则,如果在途中不能顺利染色,不能将所有人分为两组,则返回 `False`。 ## 代码 ```python class Solution: def dfs(self, graph, colors, i, color): colors[i] = color for j in graph[i]: if colors[j] == colors[i]: return False if colors[j] == 0 and not self.dfs(graph, colors, j, -color): return False return True def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool: graph = [[] for _ in range(n + 1)] colors = [0 for _ in range(n + 1)] for x, y in dislikes: graph[x].append(y) graph[y].append(x) for i in range(1, n + 1): if colors[i] == 0 and not self.dfs(graph, colors, i, 1): return False return True ``` ================================================ FILE: docs/solutions/0800-0899/prime-palindrome.md ================================================ # [0866. 回文质数](https://leetcode.cn/problems/prime-palindrome/) - 标签:数学、数论 - 难度:中等 ## 题目链接 - [0866. 回文质数 - 力扣](https://leetcode.cn/problems/prime-palindrome/) ## 题目大意 **描述**: 给定一个整数 $n$。 **要求**: 返回大于或等于 $n$ 的最小 回文质数。 **说明**: - 一个整数如果恰好有两个除数:1 和它本身,那么它是「质数」。注意,1 不是质数。 - 例如,2、3、5、7、11 和 13 都是质数。 - 一个整数如果从左向右读和从右向左读是相同的,那么它是「回文数」。 - 例如,101 和 12321 都是回文数。 - 测试用例保证答案总是存在,并且在 $[2, 2 \times 10^8]$ 范围内。 - $1 \le n \le 10^{8}$。 **示例**: - 示例 1: ```python 输入:n = 6 输出:7 ``` - 示例 2: ```python 输入:n = 8 输出:11 ``` ## 解题思路 ### 思路 1:数学 + 枚举 关键观察:除了 11 之外,所有偶数位的回文数都能被 11 整除,因此不是质数。 所以我们只需要检查奇数位的回文数即可。 1. 编写判断质数的函数 `is_prime(x)`。 2. 编写判断回文数的函数 `is_palindrome(x)`。 3. 从 $n$ 开始枚举: - 如果当前数是偶数位且大于 11,直接跳到下一个奇数位的起始位置 - 否则检查是否同时是回文数和质数 4. 返回第一个满足条件的数。 ### 思路 1:代码 ```python class Solution: def primePalindrome(self, n: int) -> int: # 判断是否为质数 def is_prime(x): if x < 2: return False if x == 2: return True if x % 2 == 0: return False # 只需检查到 sqrt(x) i = 3 while i * i <= x: if x % i == 0: return False i += 2 return True # 判断是否为回文数 def is_palindrome(x): s = str(x) return s == s[::-1] # 特判 if n == 1: return 2 # 从 n 开始枚举 x = n while True: # 如果是偶数位且大于 11,跳到下一个奇数位 s = str(x) if len(s) % 2 == 0 and x > 11: # 跳到下一个奇数位的起始位置 x = 10 ** len(s) continue # 检查是否同时是回文数和质数 if is_palindrome(x) and is_prime(x): return x x += 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(N \sqrt{N})$,其中 $N$ 是答案的大小。需要枚举并检查每个数是否为质数。 - **空间复杂度**:$O(\log N)$,字符串转换需要的空间。 ================================================ FILE: docs/solutions/0800-0899/profitable-schemes.md ================================================ # [0879. 盈利计划](https://leetcode.cn/problems/profitable-schemes/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [0879. 盈利计划 - 力扣](https://leetcode.cn/problems/profitable-schemes/) ## 题目大意 **描述**: 集团里有 $n$ 名员工,他们可以完成各种各样的工作创造利润。 第 $i$ 种工作会产生 $profit[i]$ 的利润,它要求 $group[i]$ 名成员共同参与。如果成员参与了其中一项工作,就不能参与另一项工作。 工作的任何至少产生 $minProfit$ 利润的子集称为「盈利计划」。并且工作的成员总数最多为 $n$。 **要求**: 计算出有多少种计划可以选择?因为答案很大,所以 返回结果模 $10^9 + 7$ 的值。 **说明**: - $1 \le n \le 10^{3}$。 - $0 \le minProfit \le 10^{3}$。 - $1 \le group.length \le 10^{3}$。 - $1 \le group[i] \le 10^{3}$。 - $profit.length == group.length$。 - $0 \le profit[i] \le 10^{3}$。 **示例**: - 示例 1: ```python 输入:n = 5, minProfit = 3, group = [2,2], profit = [2,3] 输出:2 解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1 ,或仅完成工作 1 。 总的来说,有两种计划。 ``` - 示例 2: ```python 输入:n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8] 输出:7 解释:至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。 有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。 ``` ## 解题思路 ### 思路 1:三维动态规划 这是一个多维约束的 01 背包问题。我们需要在员工数量不超过 $n$ 的约束下,选择若干工作,使得利润至少为 $minProfit$。 **状态定义**: 定义 $dp[i][j][k]$ 表示考虑前 $i$ 个工作,使用了 $j$ 个员工,获得的利润至少为 $k$ 的方案数。 **状态转移**: 对于第 $i$ 个工作(需要 $group[i-1]$ 个员工,产生 $profit[i-1]$ 利润): 1. **不选择第 $i$ 个工作**:$dp[i][j][k] = dp[i-1][j][k]$ 2. **选择第 $i$ 个工作**(前提是 $j \ge group[i-1]$): - 需要从 $dp[i-1][j-group[i-1]][k-profit[i-1]]$ 转移过来 - 注意:当 $k - profit[i-1] \le 0$ 时,说明利润已经足够,应该从 $dp[i-1][j-group[i-1]][0]$ 转移 - 因此:$dp[i][j][k] += dp[i-1][j-group[i-1]][\max(0, k-profit[i-1])]$ **初始状态**: $dp[0][0][0] = 1$,表示不选择任何工作,使用 0 个员工,获得 0 利润的方案数为 1。 **答案**: $\sum_{j=0}^{n} dp[length][j][minProfit]$,即考虑所有工作后,使用不超过 $n$ 个员工,获得至少 $minProfit$ 利润的方案数之和。 ### 思路 1:代码 ```python class Solution: def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int: MOD = 10**9 + 7 length = len(group) # dp[i][j][k] 表示考虑前 i 个工作,使用 j 个员工,获得至少 k 利润的方案数 dp = [[[0] * (minProfit + 1) for _ in range(n + 1)] for _ in range(length + 1)] # 初始状态:不选择任何工作,使用 0 个员工,获得 0 利润 dp[0][0][0] = 1 # 枚举每个工作 for i in range(1, length + 1): members = group[i - 1] # 第 i 个工作需要的员工数 earn = profit[i - 1] # 第 i 个工作产生的利润 # 枚举员工数 for j in range(n + 1): # 枚举利润 for k in range(minProfit + 1): # 不选择第 i 个工作 dp[i][j][k] = dp[i - 1][j][k] # 选择第 i 个工作(需要足够的员工) if j >= members: # 利润超过 minProfit 的部分都算作 minProfit prev_profit = max(0, k - earn) dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j - members][prev_profit]) % MOD # 统计答案:使用不超过 n 个员工,获得至少 minProfit 利润的方案数 result = 0 for j in range(n + 1): result = (result + dp[length][j][minProfit]) % MOD return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(length \times n \times minProfit)$,其中 $length$ 是工作数量。需要填充三维 $dp$ 数组。 - **空间复杂度**:$O(length \times n \times minProfit)$,需要存储三维 $dp$ 数组。可以使用滚动数组优化到 $O(n \times minProfit)$。 ================================================ FILE: docs/solutions/0800-0899/projection-area-of-3d-shapes.md ================================================ # [0883. 三维形体投影面积](https://leetcode.cn/problems/projection-area-of-3d-shapes/) - 标签:几何、数组、数学、矩阵 - 难度:简单 ## 题目链接 - [0883. 三维形体投影面积 - 力扣](https://leetcode.cn/problems/projection-area-of-3d-shapes/) ## 题目大意 **描述**: 在 $n \times n$ 的网格 $grid$ 中,我们放置了一些与 $x$,$y$,$z$ 三轴对齐的 $1 \times 1 \times 1$ 立方体。 每个值 $v = grid[i][j]$ 表示有一列 $v$ 个正方体叠放在格子 $(i, j)$ 上。 现在,我们查看这些立方体在 $xy$、$yz$ 和 $zx$ 平面上的投影。 「投影」就像影子,将「三维」形体映射到一个「二维」平面上。从顶部、前面和侧面看立方体时,我们会看到「影子」。 **要求**: 返回「所有三个投影的总面积」。 **说明**: - $n == grid.length == grid[i].length$。 - $1 \le n \le 50$。 - $0 \le grid[i][j] \le 50$。 **示例**: - 示例 1: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/08/02/shadow.png) ```python 输入:[[1,2],[3,4]] 输出:17 解释:这里有该形体在三个轴对齐平面上的三个投影(“阴影部分”)。 ``` - 示例 2: ```python 输入:grid = [[2]] 输出:5 ``` ## 解题思路 ### 思路 1:数学 + 模拟 三维形体在三个平面上的投影面积分别为: 1. **xy 平面(俯视图)**:每个非零格子贡献 1 的面积,即统计非零格子的个数。 2. **yz 平面(正视图)**:每一行的最大值之和,因为从前面看,每一行的高度由该行最高的立方体决定。 3. **zx 平面(侧视图)**:每一列的最大值之和,因为从侧面看,每一列的高度由该列最高的立方体决定。 ### 思路 1:代码 ```python class Solution: def projectionArea(self, grid: List[List[int]]) -> int: n = len(grid) xy_area = 0 # xy 平面投影面积 yz_area = 0 # yz 平面投影面积 zx_area = 0 # zx 平面投影面积 for i in range(n): row_max = 0 # 第 i 行的最大值 col_max = 0 # 第 i 列的最大值 for j in range(n): # 统计非零格子 if grid[i][j] > 0: xy_area += 1 # 更新行最大值 row_max = max(row_max, grid[i][j]) # 更新列最大值 col_max = max(col_max, grid[j][i]) yz_area += row_max zx_area += col_max return xy_area + yz_area + zx_area ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是网格的边长。需要遍历整个网格。 - **空间复杂度**:$O(1)$,只使用常数额外空间。 ================================================ FILE: docs/solutions/0800-0899/push-dominoes.md ================================================ # [0838. 推多米诺](https://leetcode.cn/problems/push-dominoes/) - 标签:双指针、字符串、动态规划 - 难度:中等 ## 题目链接 - [0838. 推多米诺 - 力扣](https://leetcode.cn/problems/push-dominoes/) ## 题目大意 **描述**: $n$ 张多米诺骨牌排成一行,将每张多米诺骨牌垂直竖立。在开始时,同时把一些多米诺骨牌向左或向右推。 每过一秒,倒向左边的多米诺骨牌会推动其左侧相邻的多米诺骨牌。同样地,倒向右边的多米诺骨牌也会推动竖立在其右侧的相邻多米诺骨牌。 如果一张垂直竖立的多米诺骨牌的两侧同时有多米诺骨牌倒下时,由于受力平衡,「该骨牌仍然保持不变」。 就这个问题而言,我们会认为一张正在倒下的多米诺骨牌不会对其它正在倒下或已经倒下的多米诺骨牌施加额外的力。 给定一个字符串 $dominoes$ 表示这一行多米诺骨牌的初始状态,其中: - $dominoes[i] = 'L'$,表示第 $i$ 张多米诺骨牌被推向左侧, - $dominoes[i] = 'R'$,表示第 $i$ 张多米诺骨牌被推向右侧, - $dominoes[i] = '.'$,表示没有推动第 $i$ 张多米诺骨牌。 **要求**: 返回表示最终状态的字符串。 **说明**: - $n == dominoes.length$。 - $1 \le n \le 10^{5}$。 - $dominoes[i]$ 为 `'L'`、`'R'` 或 `'.'`。 **示例**: - 示例 1: ```python 输入:dominoes = "RR.L" 输出:"RR.L" 解释:第一张多米诺骨牌没有给第二张施加额外的力。 ``` - 示例 2: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/05/18/domino.png) ```python 输入:dominoes = ".L.R...LR..L.." 输出:"LL.RR.LLRRLL.." ``` ## 解题思路 ### 思路 1:双指针 + 模拟 多米诺骨牌的最终状态取决于相邻的 `R` 和 `L` 之间的关系。我们可以用双指针来标记相邻的两个非 `.` 字符,然后根据它们的组合来决定中间部分的状态。 1. 在字符串首尾分别添加虚拟的 `L` 和 `R`,方便处理边界情况。 2. 使用两个指针 $left$ 和 $right$,分别指向相邻的两个非 `.` 字符。 3. 根据 $dominoes[left]$ 和 $dominoes[right]$ 的组合,分四种情况处理: - `R...R`:中间全部变为 `R` - `L...L`:中间全部变为 `L` - `R...L`:中间部分向两边倒,中心位置保持不变(如果距离为奇数) - `L...R`:中间部分保持不变 ### 思路 1:代码 ```python class Solution: def pushDominoes(self, dominoes: str) -> str: # 在首尾添加虚拟字符,方便处理边界 dominoes = 'L' + dominoes + 'R' n = len(dominoes) res = [] left = 0 # 左指针 for right in range(1, n): # 找到下一个非 '.' 的字符 if dominoes[right] == '.': continue # 计算中间 '.' 的个数 middle = right - left - 1 # 添加左边界字符(跳过最开始的虚拟 'L') if left > 0: res.append(dominoes[left]) # 根据左右字符的组合处理中间部分 if dominoes[left] == dominoes[right]: # R...R 或 L...L,中间全部变为相同字符 res.append(dominoes[left] * middle) elif dominoes[left] == 'R' and dominoes[right] == 'L': # R...L,向中间倒 res.append('R' * (middle // 2)) if middle % 2 == 1: res.append('.') # 中心位置保持不变 res.append('L' * (middle // 2)) else: # L...R,中间保持不变 res.append('.' * middle) left = right return ''.join(res) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $dominoes$ 的长度。只需遍历一次字符串。 - **空间复杂度**:$O(n)$,需要存储结果字符串。 ================================================ FILE: docs/solutions/0800-0899/race-car.md ================================================ # [0818. 赛车](https://leetcode.cn/problems/race-car/) - 标签:动态规划 - 难度:困难 ## 题目链接 - [0818. 赛车 - 力扣](https://leetcode.cn/problems/race-car/) ## 题目大意 **描述**: 你的赛车可以从位置 0 开始,并且速度为 +1,在一条无限长的数轴上行驶。赛车也可以向负方向行驶。赛车可以按照由加速指令 `'A'` 和倒车指令 `'R'` 组成的指令序列自动行驶。 - 当收到指令 `'A'` 时,赛车这样行驶: - $position += speed$ - $speed *= 2$ - 当收到指令 `'R'` 时,赛车这样行驶: - 如果速度为正数,那么 $speed = -1$ - 否则 $speed = 1$ 当前所处位置不变。 例如,在执行指令 `"AAR"` 后,赛车位置变化为 $0 \rightarrow 1 \rightarrow 3 \rightarrow 3$,速度变化为 $1 \rightarrow 2 \rightarrow 4 \rightarrow -1$。 给定一个目标位置 $target$。 **要求**: 返回能到达目标位置的最短指令序列的长度。 **说明**: - $1 \le target \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:target = 3 输出:2 解释: 最短指令序列是 "AA" 。 位置变化 0 --> 1 --> 3 。 ``` - 示例 2: ```python 输入:target = 6 输出:5 解释: 最短指令序列是 "AAARA" 。 位置变化 0 --> 1 --> 3 --> 7 --> 7 --> 6 。 ``` ## 解题思路 ### 思路 1:动态规划 定义 $dp[i]$ 表示到达位置 $i$ 所需的最少指令数。 对于位置 $target$,有两种策略: 1. **直接到达或超过**:连续执行 $n$ 次 `A`,可以到达位置 $2^n - 1$。 - 如果 $2^n - 1 = target$,则 $dp[target] = n$ - 如果 $2^n - 1 > target$,需要先到达 $2^n - 1$,然后倒车回到 $target$ 2. **先前进再倒车**:先执行 $n$ 次 `A` 到达 $2^n - 1$,然后执行 `R` 倒车,再执行 $m$ 次 `A` 后退 $2^m - 1$,最后再执行 `R` 前进,到达剩余距离。 - 总指令数为:$n + 1 + m + 1 + dp[2^n - 1 - (2^m - 1) - target]$ ### 思路 1:代码 ```python class Solution: def racecar(self, target: int) -> int: dp = [0] * (target + 1) for i in range(1, target + 1): # 找到最小的 n,使得 2^n - 1 >= i n = i.bit_length() # 情况 1:恰好到达 if (1 << n) - 1 == i: dp[i] = n continue # 情况 2:超过后倒车回来 # 先走 n 步到达 2^n - 1,然后倒车,再走到 i dp[i] = n + 1 + dp[(1 << n) - 1 - i] # 情况 3:先走 n-1 步,然后倒车,再前进 # 枚举倒车时前进的步数 m for m in range(n - 1): # 先走 n-1 步到达 2^(n-1) - 1 # 倒车(R) # 前进 m 步到达 2^(n-1) - 1 - (2^m - 1) # 再倒车(R) # 继续前进到 i pos = (1 << (n - 1)) - 1 - ((1 << m) - 1) dp[i] = min(dp[i], (n - 1) + 1 + m + 1 + dp[i - pos]) return dp[target] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(target \log target)$,对于每个位置,需要枚举 $O(\log target)$ 种倒车策略。 - **空间复杂度**:$O(target)$,需要存储 $dp$ 数组。 ================================================ FILE: docs/solutions/0800-0899/reachable-nodes-in-subdivided-graph.md ================================================ # [0882. 细分图中的可到达节点](https://leetcode.cn/problems/reachable-nodes-in-subdivided-graph/) - 标签:图、最短路、堆(优先队列) - 难度:困难 ## 题目链接 - [0882. 细分图中的可到达节点 - 力扣](https://leetcode.cn/problems/reachable-nodes-in-subdivided-graph/) ## 题目大意 **描述**: 给定一个无向图(原始图),图中有 $n$ 个节点,编号从 0 到 $n - 1$ 。你决定将图中的每条边「细分」为一条节点链,每条边之间的新节点数各不相同。 图用由边组成的二维数组 $edges$ 表示,其中 $edges[i] = [u_i, v_i, cnt_i]$ 表示原始图中节点 $u_i$ 和 $v_i$ 之间存在一条边,$cnt_i$ 是将边「细分」后的新节点总数。注意,$cnt_i == 0$ 表示边不可细分。 要「」边 $[u_i, v_i]$ ,需要将其替换为 $(cnt_i + 1)$ 条新边,和 $cnt_i$ 个新节点。新节点为 $x1, x2, ..., xcnt_i$,新边为 $[ui, x1], [x1, x2], [x2, x3], ..., [xcnt_i-1, xcnt_i], [xcnt_i, v_i]$。 现在得到一个「新的细分图」,请你计算从节点 0 出发,可以到达多少个节点?如果节点间距离是 $maxMoves$ 或更少,则视为 可以到达。 给定原始图和 $maxMoves$。 **要求**: 返回「新的细分图中从节点 0 出发 可到达的节点数」。 **说明**: - $0 \le edges.length \le min(n \times (n - 1) / 2, 10^{4})$。 - $edges[i].length == 3$。 - $0 \le u_i \lt v_i \lt n$。 - 图中「不存在平行边」。 - $0 \le cnt_i \le 10^{4}$。 - $0 \le maxMoves \le 10^{9}$。 - $1 \le n \le 3000$。 **示例**: - 示例 1: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/08/01/origfinal.png) ```python 输入:edges = [[0,1,10],[0,2,1],[1,2,2]], maxMoves = 6, n = 3 输出:13 解释:边的细分情况如上图所示。 可以到达的节点已经用黄色标注出来。 ``` - 示例 2: ```python 输入:edges = [[0,1,4],[1,2,6],[0,2,8],[1,3,1]], maxMoves = 10, n = 4 输出:23 ``` ## 解题思路 ### 思路 1:Dijkstra 最短路 这道题的关键是:从节点 0 出发,在 $maxMoves$ 步内能到达多少个节点(包括原始节点和细分后的新节点)。 使用 Dijkstra 算法计算从节点 0 到其他所有节点的最短距离。对于每条边: - 如果能从两端都到达这条边,需要避免重复计数中间的新节点 - 从节点 $u$ 出发,能到达边上的新节点数为 $\min(cnt, maxMoves - dist[u])$ - 从节点 $v$ 出发,能到达边上的新节点数为 $\min(cnt, maxMoves - dist[v])$ - 这条边上能到达的新节点总数为 $\min(cnt, \text{从u到达的数量} + \text{从v到达的数量})$ ### 思路 1:代码 ```python class Solution: def reachableNodes(self, edges: List[List[int]], maxMoves: int, n: int) -> int: import heapq from collections import defaultdict # 构建图 graph = defaultdict(dict) for u, v, cnt in edges: graph[u][v] = cnt graph[v][u] = cnt # Dijkstra 算法 dist = [float('inf')] * n dist[0] = 0 heap = [(0, 0)] # (距离, 节点) visited = set() while heap: d, u = heapq.heappop(heap) if u in visited: continue visited.add(u) # 更新相邻节点的距离 for v, cnt in graph[u].items(): # 到达节点 v 需要的步数:到达 u 的步数 + 边上的新节点数 + 1 new_dist = d + cnt + 1 if new_dist < dist[v]: dist[v] = new_dist heapq.heappush(heap, (new_dist, v)) # 统计可到达的节点数 result = 0 # 统计原始节点 for i in range(n): if dist[i] <= maxMoves: result += 1 # 统计边上的新节点 for u, v, cnt in edges: # 从 u 出发能到达的新节点数 from_u = max(0, maxMoves - dist[u]) if dist[u] <= maxMoves else 0 # 从 v 出发能到达的新节点数 from_v = max(0, maxMoves - dist[v]) if dist[v] <= maxMoves else 0 # 这条边上能到达的新节点总数(避免重复计数) result += min(cnt, from_u + from_v) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O((n + m) \log n)$,其中 $n$ 是节点数,$m$ 是边数。Dijkstra 算法的时间复杂度。 - **空间复杂度**:$O(n + m)$,需要存储图和距离数组。 ================================================ FILE: docs/solutions/0800-0899/rectangle-area-ii.md ================================================ # [0850. 矩形面积 II](https://leetcode.cn/problems/rectangle-area-ii/) - 标签:线段树、数组、有序集合、扫描线 - 难度:困难 ## 题目链接 - [0850. 矩形面积 II - 力扣](https://leetcode.cn/problems/rectangle-area-ii/) ## 题目大意 **描述**:给定一个二维矩形列表 `rectangles`,其中 `rectangle[i] = [x1, y1, x2, y2]` 表示第 `i` 个矩形,`(x1, y1)` 是第 `i` 个矩形左下角的坐标,`(x2, y2)` 是第 `i` 个矩形右上角的坐标。。 **要求**:计算 `rectangles` 中所有矩形所覆盖的总面积,并返回总面积。 **说明**: - 任何被两个或多个矩形覆盖的区域应只计算一次 。 - 因为答案可能太大,返回 $10^9 + 7$ 的模。 - $1 \le rectangles.length \le 200$。 - $rectanges[i].length = 4$。 - $0 \le x_1, y_1, x_2, y_2 \le 10^9$。 - 矩形叠加覆盖后的总面积不会超越 $2^63 - 1$,这意味着可以用一个 $64$ 位有符号整数来保存面积结果。 **示例**: - 示例 1: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/06/06/rectangle_area_ii_pic.png) ```python 输入:rectangles = [[0,0,2,2],[1,0,2,3],[1,0,3,1]] 输出:6 解释:如图所示,三个矩形覆盖了总面积为6的区域。 从 (1,1) 到 (2,2),绿色矩形和红色矩形重叠。 从 (1,0) 到 (2,3),三个矩形都重叠。 ``` ## 解题思路 ### 思路 1:扫描线 + 动态开点线段树 ### 思路 1:扫描线 + 动态开点线段树代码 ```python # 线段树的节点类 class SegTreeNode: def __init__(self, left=-1, right=-1, cnt=0, height=0, leftNode=None, rightNode=None): self.left = left # 区间左边界 self.right = right # 区间右边界 self.mid = left + (right - left) // 2 self.leftNode = leftNode # 区间左节点 self.rightNode = rightNode # 区间右节点 self.cnt = cnt # 节点值(区间值) self.height = height # 区间问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self): self.tree = SegTreeNode(0, int(1e9)) # 区间更新接口:将区间为 [q_left, q_right] 上的元素值修改为 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, self.tree) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, self.tree) # 以下为内部实现方法 # 区间更新实现方法 def __update_interval(self, q_left, q_right, val, node): if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 node.cnt += val # 当前节点所在区间每个元素值改为 val self.__pushup(node) return self.__pushdown(node) if q_left <= node.mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, node.leftNode) if q_right > node.mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, node.rightNode) self.__pushup(node) # 区间查询实现方法:在线段树的 [left, right] 区间范围中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, node): if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return node.height # 直接返回节点值 self.__pushdown(node) res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= node.mid: # 在左子树中查询 res_left = self.__query_interval(q_left, node.mid, node.leftNode) if q_right > node.mid: # 在右子树中查询 res_right = self.__query_interval(node.mid + 1, q_right, node.rightNode) return res_left + res_right # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:更新 node 节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, node): if node.cnt > 0: node.height = node.right - node.left + 1 else: if node.leftNode and node.rightNode: node.height = node.leftNode.height + node.rightNode.height else: node.height = 0 # 向下更新实现方法:更新 node 节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, node): if node.leftNode is None: node.leftNode = SegTreeNode(node.left, node.mid) if node.rightNode is None: node.rightNode = SegTreeNode(node.mid + 1, node.right) class Solution: def rectangleArea(self, rectangles) -> int: # lines 存储每个矩阵的上下两条边 lines = [] for rectangle in rectangles: x1, y1, x2, y2 = rectangle lines.append([x1, y1 + 1, y2, 1]) lines.append([x2, y1 + 1, y2, -1]) lines.sort(key=lambda line: line[0]) # 建立线段树 self.STree = SegmentTree() ans = 0 mod = 10 ** 9 + 7 prev_x = lines[0][0] for i in range(len(lines)): x, y1, y2, val = lines[i] height = self.STree.query_interval(0, int(1e9)) ans += height * (x - prev_x) ans %= mod self.STree.update_interval(y1, y2, val) prev_x = x return ans ``` ## 参考资料 - 【文章】[【hdu1542】线段树求矩形面积并 - 拦路雨偏似雪花](https://www.cnblogs.com/KonjakJuruo/p/6024266.html) ================================================ FILE: docs/solutions/0800-0899/rectangle-overlap.md ================================================ # [0836. 矩形重叠](https://leetcode.cn/problems/rectangle-overlap/) - 标签:几何、数学 - 难度:简单 ## 题目链接 - [0836. 矩形重叠 - 力扣](https://leetcode.cn/problems/rectangle-overlap/) ## 题目大意 给定两个矩形的左下角、右上角坐标:[x1, y1, x2, y2]。[x1, y1] 表示左下角坐标,[x2, y2] 表示右上角坐标。如果两个矩形相交面积大于 0,则称两矩形重叠。 要求:根据给定的矩形 rec1 和 rec2 的左下角、右上角坐标,如果重叠,则返回 True,否则返回 False。 ## 解题思路 如果两个矩形重叠,则两个矩形的水平边投影到 x 轴上的线段会有交集,同理竖直边投影到 y 轴上的线段也会有交集。因此我们可以把问题看做是:判断两条线段是否有交集。 矩形 rec1 和 rec2 水平边投影到 x 轴上的线段为 `(rec1[0], rec1[2])` 和 `(rec2[0], rec2[2])`。如果两条线段有交集,则 `min(rec1[2], rec2[2]) > max(rec1[0], rec2[0])`。 矩形 rec1 和 rec2 竖直边投影到 y 轴上的线段为 `(rec1[1], rec1[3])` 和 `(rec2[1], rec2[3])`。如果两条线段有交集,则 `min(rec1[3], rec2[3]) > max(rec1[1], rec2[1])`。 判断是否满足上述条件,如果满足则说明两个矩形重叠,返回 True,如果不满足则返回 False。 ## 代码 ```python class Solution: def isRectangleOverlap(self, rec1: List[int], rec2: List[int]) -> bool: return min(rec1[2], rec2[2]) > max(rec1[0], rec2[0]) and min(rec1[3], rec2[3]) > max(rec1[1], rec2[1]) ``` ================================================ FILE: docs/solutions/0800-0899/reordered-power-of-2.md ================================================ # [0869. 重新排序得到 2 的幂](https://leetcode.cn/problems/reordered-power-of-2/) - 标签:哈希表、数学、计数、枚举、排序 - 难度:中等 ## 题目链接 - [0869. 重新排序得到 2 的幂 - 力扣](https://leetcode.cn/problems/reordered-power-of-2/) ## 题目大意 **描述**: 给定正整数 $n$,我们按任何顺序(包括原始顺序)将数字重新排序,注意其前导数字不能为零。 **要求**: 如果我们可以通过上述方式得到 2 的幂,返回 true;否则,返回 false。 **说明**: - $1 \le n \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:n = 1 输出:true ``` - 示例 2: ```python 输入:n = 10 输出:false ``` ## 解题思路 ### 思路 1:计数 + 枚举 判断一个数能否重新排列成 2 的幂,关键在于判断它的数字组成是否与某个 2 的幂相同。 由于 $1 \le n \le 10^9$,所以 2 的幂最多到 $2^{29} = 536870912$。我们可以: 1. 统计 $n$ 中每个数字出现的次数。 2. 枚举所有在范围内的 2 的幂 $2^i$($i = 0, 1, 2, \ldots, 29$)。 3. 统计每个 2 的幂中各数字出现的次数。 4. 如果某个 2 的幂的数字计数与 $n$ 的数字计数完全相同,返回 `True`。 ### 思路 1:代码 ```python class Solution: def reorderedPowerOf2(self, n: int) -> bool: # 统计 n 中每个数字出现的次数 def count_digits(num): cnt = [0] * 10 while num > 0: cnt[num % 10] += 1 num //= 10 return cnt n_count = count_digits(n) # 枚举所有可能的 2 的幂(2^0 到 2^29) for i in range(30): power = 1 << i # 2^i if count_digits(power) == n_count: return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n + 30 \times \log C)$,其中 $C = 10^9$。统计 $n$ 的数字需要 $O(\log n)$,枚举 30 个 2 的幂并统计数字需要 $O(30 \times \log C)$。 - **空间复杂度**:$O(1)$,只需要常数大小的数组来存储数字计数。 ================================================ FILE: docs/solutions/0800-0899/score-after-flipping-matrix.md ================================================ # [0861. 翻转矩阵后的得分](https://leetcode.cn/problems/score-after-flipping-matrix/) - 标签:贪心、位运算、数组、矩阵 - 难度:中等 ## 题目链接 - [0861. 翻转矩阵后的得分 - 力扣](https://leetcode.cn/problems/score-after-flipping-matrix/) ## 题目大意 **描述**:给定一个二维矩阵 `A`,其中每个元素的值为 `0` 或 `1`。 我们可以选择任一行或列,并转换该行或列中的每一个值:将所有 `0` 都更改为 `1`,将所有 `1` 都更改为 `0`。 在做出任意次数的移动后,将该矩阵的每一行都按照二进制数来解释,矩阵的得分就是这些数字的总和。 **要求**:返回尽可能高的分数。 **说明**: - $1 \le A.length \le 20$。 - $1 \le A[0].length \le 20$。 - `A[i][j]` 值为 `0` 或 `1`。 **示例**: - 示例 1: ```python 输入:[[0,0,1,1],[1,0,1,0],[1,1,0,0]] 输出:39 解释: 转换为 [[1,1,1,1],[1,0,0,1],[1,1,1,1]] 0b1111 + 0b1001 + 0b1111 = 15 + 9 + 15 = 39 ``` ## 解题思路 ### 思路 1:贪心算法 对于一个二进制数来说,应该优先保证高位(靠前的列)尽可能的大,也就是保证高位尽可能值为 `1`。 - 我们先来看矩阵的第一列数,只要第一列的某一行为 `0`,则将这一行的值进行翻转。这样就保证了最高位一定为 `1`。 - 接下来,我们再来关注除了第一列的其他列,这里因为有最高位限制,所以我们不能随意再将某一行的值进行翻转,只能选择某一列进行翻转。 - 为了保证当前位上有尽可能多的 `1`。我们可以用两个变量 `one_cnt`、`zeo_cnt` 来记录当前列上 `1` 的个数和 `0` 的个数。如果 `0` 的个数多于 `1` 的个数,那么我们就将当前列进行翻转。从而保证当前位上有尽可能多的 `1`。 - 当所有列都遍历完成后,我们会得到加和最大的情况。 ### 思路 1:贪心算法代码 ```python class Solution: def matrixScore(self, grid: List[List[int]]) -> int: zero_cnt, one_cnt = 0, 0 res = 0 rows, cols = len(grid), len(grid[0]) for col in range(cols): for row in range(rows): if col == 0 and grid[row][col] == 0: for j in range(cols): grid[row][j] = 1 - grid[row][j] else: if grid[row][col] == 1: one_cnt += 1 else: zero_cnt += 1 if zero_cnt > one_cnt: for row in range(rows): grid[row][col] = 1 - grid[row][col] for row in range(rows): if grid[row][col] == 1: res += pow(2, cols - col - 1) zero_cnt = 0 one_cnt = 0 return res ``` ================================================ FILE: docs/solutions/0800-0899/score-of-parentheses.md ================================================ # [0856. 括号的分数](https://leetcode.cn/problems/score-of-parentheses/) - 标签:栈、字符串 - 难度:中等 ## 题目链接 - [0856. 括号的分数 - 力扣](https://leetcode.cn/problems/score-of-parentheses/) ## 题目大意 **描述**: 给定一个平衡括号字符串 $S$,按下述规则计算该字符串的分数: - `()` 得 1 分。 - `AB` 得 $A + B$ 分,其中 $A$ 和 $B$ 是平衡括号字符串。 - `(A)` 得 $2 \times A$ 分,其中 $A$ 是平衡括号字符串。 **要求**: 返回该字符串的分数。 **说明**: - $S$ 是平衡括号字符串,且只含有 `(` 和 `)`。 - $2 \le S.length \le 50$。 **示例**: - 示例 1: ```python 输入: "()()" 输出: 2 ``` - 示例 2: ```python 输入: "(()(()))" 输出: 6 ``` ## 解题思路 ### 思路 1:栈 根据题意,括号的分数计算规则为: - `()` 得 1 分 - `AB` 得 $A + B$ 分 - `(A)` 得 $2 \times A$ 分 我们可以使用栈来模拟这个过程: 1. 遍历字符串 $s$,用栈来记录每一层的分数。 2. 遇到 `(` 时,将 0 压入栈中,表示新的一层开始。 3. 遇到 `)` 时: - 弹出栈顶元素 $cur$ - 如果 $cur = 0$,说明是 `()`,得 1 分 - 否则说明是 `(A)`,得 $2 \times cur$ 分 - 将得分加到新的栈顶(上一层) 4. 最后栈中只剩一个元素,即为总分数。 ### 思路 1:代码 ```python class Solution: def scoreOfParentheses(self, s: str) -> int: stack = [0] # 初始化栈,栈底为 0 for ch in s: if ch == '(': # 遇到左括号,新的一层开始 stack.append(0) else: # 遇到右括号,计算当前层的分数 cur = stack.pop() if cur == 0: # () 的情况,得 1 分 score = 1 else: # (A) 的情况,得 2 * A 分 score = 2 * cur # 将分数加到上一层 stack[-1] += score return stack[0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。需要遍历一次字符串。 - **空间复杂度**:$O(n)$,栈的空间复杂度最坏情况下为 $O(n)$。 ================================================ FILE: docs/solutions/0800-0899/shifting-letters copy.md ================================================ # [0848. 字母移位](https://leetcode.cn/problems/shifting-letters/) - 标签:数组、字符串、前缀和 - 难度:中等 ## 题目链接 - [0848. 字母移位 - 力扣](https://leetcode.cn/problems/shifting-letters/) ## 题目大意 **描述**: 有一个由小写字母组成的字符串 $s$,和一个长度相同的整数数组 $shifts$。 我们将字母表中的下一个字母称为原字母的「移位 `shift()`」 (由于字母表是环绕的, `'z'` 将会变成 `'a'`)。 - 例如,`shift('a') = 'b'`, `shift('t') = 'u'`, 以及 `shift('z') = 'a'`。 对于每个 $shifts[i] = x$,我们会将 $s$ 中的前 $i + 1$ 个字母移位 $x$ 次。 **要求**: 返回「将所有这些移位都应用到 $s$ 后最终得到的字符串」。 **说明**: - $1 \le s.length \le 10^{5}$。 - $s$ 由小写英文字母组成。 - $shifts.length == s.length$。 - $0 \le shifts[i] \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:s = "abc", shifts = [3,5,9] 输出:"rpl" 解释: 我们以 "abc" 开始。 将 S 中的第 1 个字母移位 3 次后,我们得到 "dbc"。 再将 S 中的前 2 个字母移位 5 次后,我们得到 "igc"。 最后将 S 中的这 3 个字母移位 9 次后,我们得到答案 "rpl"。 ``` - 示例 2: ```python 输入: 输出: ``` ## 解题思路 ### 思路 1:后缀和 根据题意,对于位置 $i$ 的字符,它需要移位的总次数为 $\sum_{j=i}^{n-1} shifts[j]$。 如果直接计算每个位置的移位次数,时间复杂度为 $O(n^2)$,会超时。我们可以使用后缀和来优化: 1. 从后向前遍历 $shifts$ 数组,计算后缀和 $total$。 2. 对于位置 $i$,其移位次数为 $total$。 3. 将字符 $s[i]$ 移位 $total \bmod 26$ 次(因为字母表是循环的)。 4. 更新 $total$ 为下一个位置的后缀和。 ### 思路 1:代码 ```python class Solution: def shiftingLetters(self, s: str, shifts: List[int]) -> str: n = len(s) res = [] total = 0 # 后缀和 # 从后向前遍历 for i in range(n - 1, -1, -1): total += shifts[i] # 计算移位后的字符 # ord(s[i]) - ord('a') 得到字符相对于 'a' 的偏移量 # 加上移位次数后对 26 取模,得到新的偏移量 new_char = chr((ord(s[i]) - ord('a') + total) % 26 + ord('a')) res.append(new_char) # 因为是从后向前遍历,需要反转结果 return ''.join(res[::-1]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。只需遍历一次数组。 - **空间复杂度**:$O(n)$,需要存储结果字符串。 ================================================ FILE: docs/solutions/0800-0899/shifting-letters.md ================================================ # [0848. 字母移位](https://leetcode.cn/problems/shifting-letters/) - 标签:数组、字符串、前缀和 - 难度:中等 ## 题目链接 - [0848. 字母移位 - 力扣](https://leetcode.cn/problems/shifting-letters/) ## 题目大意 **描述**: 有一个由小写字母组成的字符串 $s$,和一个长度相同的整数数组 $shifts$。 我们将字母表中的下一个字母称为原字母的「移位 `shift()`」 (由于字母表是环绕的, `'z'` 将会变成 `'a'`)。 - 例如,`shift('a') = 'b'`, `shift('t') = 'u'`, 以及 `shift('z') = 'a'`。 对于每个 $shifts[i] = x$,我们会将 $s$ 中的前 $i + 1$ 个字母移位 $x$ 次。 **要求**: 返回「将所有这些移位都应用到 $s$ 后最终得到的字符串」。 **说明**: - $1 \le s.length \le 10^{5}$。 - $s$ 由小写英文字母组成。 - $shifts.length == s.length$。 - $0 \le shifts[i] \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:s = "abc", shifts = [3,5,9] 输出:"rpl" 解释: 我们以 "abc" 开始。 将 S 中的第 1 个字母移位 3 次后,我们得到 "dbc"。 再将 S 中的前 2 个字母移位 5 次后,我们得到 "igc"。 最后将 S 中的这 3 个字母移位 9 次后,我们得到答案 "rpl"。 ``` - 示例 2: ```python 输入: 输出: ``` ## 解题思路 ### 思路 1:后缀和 根据题意,对于位置 $i$ 的字符,它需要移位的总次数为 $\sum_{j=i}^{n-1} shifts[j]$。 如果直接计算每个位置的移位次数,时间复杂度为 $O(n^2)$,会超时。我们可以使用后缀和来优化: 1. 从后向前遍历 $shifts$ 数组,计算后缀和 $total$。 2. 对于位置 $i$,其移位次数为 $total$。 3. 将字符 $s[i]$ 移位 $total \bmod 26$ 次(因为字母表是循环的)。 4. 更新 $total$ 为下一个位置的后缀和。 ### 思路 1:代码 ```python class Solution: def shiftingLetters(self, s: str, shifts: List[int]) -> str: n = len(s) res = [] total = 0 # 后缀和 # 从后向前遍历 for i in range(n - 1, -1, -1): total += shifts[i] # 计算移位后的字符 # ord(s[i]) - ord('a') 得到字符相对于 'a' 的偏移量 # 加上移位次数后对 26 取模,得到新的偏移量 new_char = chr((ord(s[i]) - ord('a') + total) % 26 + ord('a')) res.append(new_char) # 因为是从后向前遍历,需要反转结果 return ''.join(res[::-1]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。只需遍历一次数组。 - **空间复杂度**:$O(n)$,需要存储结果字符串。 ================================================ FILE: docs/solutions/0800-0899/short-encoding-of-words.md ================================================ # [0820. 单词的压缩编码](https://leetcode.cn/problems/short-encoding-of-words/) - 标签:字典树、数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [0820. 单词的压缩编码 - 力扣](https://leetcode.cn/problems/short-encoding-of-words/) ## 题目大意 给定一个单词数组 `words`。要求对 `words` 进行编码成一个助记字符串,用来帮助记忆。`words` 中拥有相同字符后缀的单词可以合并成一个单词,比如`time` 和 `me` 可以合并成 `time`。同时每个不能再合并的单词末尾以 `#` 为结束符,将所有合并后的单词排列起来就是一个助记字符串。 要求:返回对 `words` 进行编码的最小助记字符串 `s` 的长度。 ## 解题思路 构建一个字典树。然后对字符串长度进行从小到大排序。 再依次将去重后的所有单词插入到字典树中。如果出现比当前单词更长的单词,则将短单词的结尾置为 `False`,意为替换掉短单词。 然后再依次在字典树中查询所有单词,「单词长度 + 1」就是当前不能在合并的单词,累加起来就是答案。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = False cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None and cur.isEnd class Solution: def minimumLengthEncoding(self, words: List[str]) -> int: trie_tree = Trie() words = list(set(words)) words.sort(key=lambda i: len(i)) ans = 0 for word in words: trie_tree.insert(word[::-1]) for word in words: if trie_tree.search(word[::-1]): ans += len(word) + 1 return ans ``` ================================================ FILE: docs/solutions/0800-0899/shortest-distance-to-a-character.md ================================================ # [0821. 字符的最短距离](https://leetcode.cn/problems/shortest-distance-to-a-character/) - 标签:数组、双指针、字符串 - 难度:简单 ## 题目链接 - [0821. 字符的最短距离 - 力扣](https://leetcode.cn/problems/shortest-distance-to-a-character/) ## 题目大意 **描述**:给定一个字符串 $s$ 和一个字符 $c$,并且 $c$ 是字符串 $s$ 中出现过的字符。 **要求**:返回一个长度与字符串 $s$ 想通的整数数组 $answer$,其中 $answer[i]$ 是字符串 $s$ 中从下标 $i$ 到离下标 $i$ 最近的字符 $c$ 的距离。 **说明**: - 两个下标 $i$ 和 $j$ 之间的 **距离** 为 $abs(i - j)$ ,其中 $abs$ 是绝对值函数。 - $1 \le s.length \le 10^4$。 - $s[i]$ 和 $c$ 均为小写英文字母 - 题目数据保证 $c$ 在 $s$ 中至少出现一次。 **示例**: - 示例 1: ```python 输入:s = "loveleetcode", c = "e" 输出:[3,2,1,0,1,0,0,1,2,2,1,0] 解释:字符 'e' 出现在下标 3、5、6 和 11 处(下标从 0 开始计数)。 距下标 0 最近的 'e' 出现在下标 3,所以距离为 abs(0 - 3) = 3。 距下标 1 最近的 'e' 出现在下标 3,所以距离为 abs(1 - 3) = 2。 对于下标 4,出现在下标 3 和下标 5 处的 'e' 都离它最近,但距离是一样的 abs(4 - 3) == abs(4 - 5) = 1。 距下标 8 最近的 'e' 出现在下标 6,所以距离为 abs(8 - 6) = 2。 ``` - 示例 2: ```python 输入:s = "aaab", c = "b" 输出:[3,2,1,0] ``` ## 解题思路 ### 思路 1:两次遍历 第一次从左到右遍历,记录每个 $i$ 左边最近的 $c$ 的位置,并将其距离记录到 $answer[i]$ 中。 第二次从右到左遍历,记录每个 $i$ 右侧最近的 $c$ 的位置,并将其与第一次遍历左侧最近的 $c$ 的位置相比较,并将较小的距离记录到 $answer[i]$ 中。 ### 思路 1:代码 ```python class Solution: def shortestToChar(self, s: str, c: str) -> List[int]: size = len(s) ans = [size + 1 for _ in range(size)] pos = -1 for i in range(size): if s[i] == c: pos = i if pos != -1: ans[i] = i - pos pos = -1 for i in range(size - 1, -1, -1): if s[i] == c: pos = i if pos != -1: ans[i] = min(ans[i], pos - i) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/shortest-path-to-get-all-keys.md ================================================ # [0864. 获取所有钥匙的最短路径](https://leetcode.cn/problems/shortest-path-to-get-all-keys/) - 标签:位运算、广度优先搜索、数组、矩阵 - 难度:困难 ## 题目链接 - [0864. 获取所有钥匙的最短路径 - 力扣](https://leetcode.cn/problems/shortest-path-to-get-all-keys/) ## 题目大意 **描述**: 给定一个二维网格 $grid$,其中: - `'.'` 代表一个空房间 - `'#'` 代表一堵墙 - `'@'` 是起点 - 小写字母代表钥匙 - 大写字母代表锁 我们从起点开始出发,一次移动是指向四个基本方向之一行走一个单位空间。我们不能在网格外面行走,也无法穿过一堵墙。如果途经一个钥匙,我们就把它捡起来。除非我们手里有对应的钥匙,否则无法通过锁。 假设 $k$ 为 钥匙/锁 的个数,且满足 $1 \le k \le 6$,字母表中的前 $k$ 个字母在网格中都有自己对应的一个小写和一个大写字母。换言之,每个锁有唯一对应的钥匙,每个钥匙也有唯一对应的锁。另外,代表钥匙和锁的字母互为大小写并按字母顺序排列。 **要求**: 返回获取所有钥匙所需要的移动的最少次数。如果无法获取所有钥匙,返回 -1。 **说明**: - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 30$。 - $grid[i][j]$ 只含有 `'.'`, `'#'`, `'@'`, `'a'-'f'` 以及 `'A'-'F'`。 - 钥匙的数目范围是 $[1, 6]$。 - 每个钥匙都对应一个「不同」的字母。 - 每个钥匙正好打开一个对应的锁。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/07/23/lc-keys2.jpg) ```python 输入:grid = ["@.a..","###.#","b.A.B"] 输出:8 解释:目标是获得所有钥匙,而不是打开所有锁。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/07/23/lc-key2.jpg) ```python 输入:grid = ["@..aA","..B#.","....b"] 输出:6 ``` ## 解题思路 ### 思路 1:BFS + 状态压缩 这是一个带状态的最短路径问题。状态包括:当前位置 $(x, y)$ 和已收集的钥匙集合。 由于钥匙最多 6 个,可以用 6 位二进制数表示钥匙的收集状态,第 $i$ 位为 1 表示已收集第 $i$ 把钥匙。 算法步骤: 1. 找到起点位置和钥匙总数 2. 使用 BFS,状态为 $(x, y, keys)$,其中 $keys$ 是钥匙的二进制状态 3. 对于每个状态,尝试向四个方向移动: - 如果是墙,不能通过 - 如果是锁,需要有对应的钥匙才能通过 - 如果是钥匙,收集它并更新状态 4. 当收集到所有钥匙时,返回步数 ### 思路 1:代码 ```python class Solution: def shortestPathAllKeys(self, grid: List[str]) -> int: from collections import deque m, n = len(grid), len(grid[0]) start_x, start_y = 0, 0 key_count = 0 # 找到起点和钥匙总数 for i in range(m): for j in range(n): if grid[i][j] == '@': start_x, start_y = i, j elif grid[i][j].islower(): key_count += 1 # 所有钥匙都收集完的状态 all_keys = (1 << key_count) - 1 # BFS queue = deque([(start_x, start_y, 0, 0)]) # (x, y, keys, steps) visited = set() visited.add((start_x, start_y, 0)) directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] while queue: x, y, keys, steps = queue.popleft() # 如果收集到所有钥匙,返回步数 if keys == all_keys: return steps # 尝试四个方向 for dx, dy in directions: nx, ny = x + dx, y + dy # 检查边界 if nx < 0 or nx >= m or ny < 0 or ny >= n: continue cell = grid[nx][ny] # 如果是墙,跳过 if cell == '#': continue # 如果是锁,检查是否有钥匙 if cell.isupper(): key_bit = ord(cell.lower()) - ord('a') if not (keys & (1 << key_bit)): continue # 更新钥匙状态 new_keys = keys if cell.islower(): key_bit = ord(cell) - ord('a') new_keys = keys | (1 << key_bit) # 如果状态未访问过,加入队列 if (nx, ny, new_keys) not in visited: visited.add((nx, ny, new_keys)) queue.append((nx, ny, new_keys, steps + 1)) return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n \times 2^k)$,其中 $m$ 和 $n$ 是网格的大小,$k$ 是钥匙的数量。每个状态最多访问一次。 - **空间复杂度**:$O(m \times n \times 2^k)$,需要存储访问状态。 ================================================ FILE: docs/solutions/0800-0899/shortest-path-visiting-all-nodes.md ================================================ # [0847. 访问所有节点的最短路径](https://leetcode.cn/problems/shortest-path-visiting-all-nodes/) - 标签:位运算、广度优先搜索、图、动态规划、状态压缩 - 难度:困难 ## 题目链接 - [0847. 访问所有节点的最短路径 - 力扣](https://leetcode.cn/problems/shortest-path-visiting-all-nodes/) ## 题目大意 **描述**:存在一个由 $n$ 个节点组成的无向连通图,图中节点编号为 $0 \sim n - 1$。现在给定一个数组 $graph$ 表示这个图。其中,$graph[i]$ 是一个列表,由所有与节点 $i$ 直接相连的节点组成。 **要求**:返回能够访问所有节点的最短路径长度。可以在任一节点开始和停止,也可以多次重访节点,并且可以重用边。 **说明**: - $n == graph.length$。 - $1 \le n \le 12$。 - $0 \le graph[i].length < n$。 - $graph[i]$ 不包含 $i$。 - 如果 $graph[a]$ 包含 $b$,那么 $graph[b]$ 也包含 $a$。 - 输入的图总是连通图。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/05/12/shortest1-graph.jpg) ```python 输入:graph = [[1,2,3],[0],[0],[0]] 输出:4 解释:一种可能的路径为 [1,0,2,0,3] ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/05/12/shortest2-graph.jpg) ```python 输入:graph = [[1],[0,2,4],[1,3,4],[2],[1,2]] 输出:4 解释:一种可能的路径为 [0,1,4,2,3] ``` ## 解题思路 ### 思路 1:状态压缩 + 广度优先搜索 题目需要求解的是「能够访问所有节点的最短路径长度」,并且每个节点都可以作为起始点。 如果对于一个特定的起点,我们可以将该起点放入队列中,然后对其进行广度优先搜索,并使用访问数组 $visited$ 标记访问过的节点,直到所有节点都已经访问过时,返回路径长度即为「从某点开始出发,所能够访问所有节点的最短路径长度」。 而本题中,每个节点都可以作为起始点,则我们可以直接将所有节点放入队列中,然后对所有节点进行广度优先搜索。 因为本题中节点数目 $n$ 的范围为 $[1, 12]$,所以我们可以采用「状态压缩」的方式,标记节点的访问情况。每个点的初始状态可以表示为 `(u, 1 << u)`。当状态 $state == 1 \text{ <}\text{< } n - 1$ 时,表示所有节点都已经访问过了,此时返回其对应路径长度即为「能够访问所有节点的最短路径长度」。 为了方便在广度优先搜索的同事,记录当前的「路径长度」以及「节点的访问情况」。我们可以使用一个三元组 $(u, state, dist)$ 来表示当前节点情况,其中: - $u$:表示当前节点编号。 - $state$:一个 $n$ 位的二进制数,表示 $n$ 个节点的访问情况。$state$ 第 $i$ 位为 $0$ 时表示未访问过,$state$ 第 $i$ 位为 $1$ 时表示访问过。 - $dist$ 表示当前的「路径长度」。 同时为了避免重复搜索同一个节点 $u$ 以及相同节点的访问情况,我们可以使用集合记录 $(u, state)$ 是否已经被搜索过。 整个算法步骤如下: 1. 将所有节点的 `(节点编号, 起始状态, 路径长度)` 作为三元组存入队列,并使用集合 $visited$ 记录所有节点的访问情况。 2. 对所有点开始进行广度优先搜索: 1. 从队列中弹出队头节点。 2. 判断节点的当前状态,如果所有节点都已经访问过,则返回答案。 3. 如果没有全访问过,则遍历当前节点的邻接节点。 4. 将邻接节点的访问状态标记为访问过。 5. 如果节点即当前路径没有访问过,则加入队列继续遍历,并标记为访问过。 3. 重复进行第 $2$ 步,直到队列为空。 ### 思路 1:代码 ```python import collections class Solution: def shortestPathLength(self, graph: List[List[int]]) -> int: size = len(graph) queue = collections.deque([]) visited = set() for u in range(size): queue.append((u, 1 << u, 0)) # 将 (节点编号, 起始状态, 路径长度) 存入队列 visited.add((u, 1 << u)) # 标记所有节点的节点编号,以及当前状态 while queue: # 对所有点开始进行广度优先搜索 u, state, dist = queue.popleft() # 弹出队头节点 if state == (1 << size) - 1: # 所有节点都访问完,返回答案 return dist for v in graph[u]: # 遍历邻接节点 next_state = state | (1 << v) # 标记邻接节点的访问状态 if (v, next_state) not in visited: # 如果节点即当前路径没有访问过,则加入队列继续遍历,并标记为访问过 queue.append((v, next_state, dist + 1)) visited.add((v, next_state)) return 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \times 2^n)$,其中 $n$ 为图的节点数量。 - **空间复杂度**:$O(n \times 2^n)$。 ================================================ FILE: docs/solutions/0800-0899/shortest-subarray-with-sum-at-least-k.md ================================================ # [0862. 和至少为 K 的最短子数组](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/) - 标签:队列、数组、二分查找、前缀和、滑动窗口、单调队列、堆(优先队列) - 难度:困难 ## 题目链接 - [0862. 和至少为 K 的最短子数组 - 力扣](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 和一个整数 $k$。 **要求**:找出 $nums$ 中和至少为 $k$ 的最短非空子数组,并返回该子数组的长度。如果不存在这样的子数组,返回 $-1$。 **说明**: - **子数组**:数组中连续的一部分。 - $1 \le nums.length \le 10^5$。 - $-10^5 \le nums[i] \le 10^5$。 - $1 \le k \le 10^9$。 **示例**: - 示例 1: ```python 输入:nums = [1], k = 1 输出:1 ``` - 示例 2: ```python 输入:nums = [1,2], k = 4 输出:-1 ``` ## 解题思路 ### 思路 1:前缀和 + 单调队列 题目要求得到满足和至少为 $k$ 的子数组的最短长度。 先来考虑暴力做法。如果使用两重循环分别遍历子数组的开始和结束位置,则可以直接求出所有满足条件的子数组,以及对应长度。但是这种做法的时间复杂度为 $O(n^2)$。我们需要对其进行优化。 #### 1. 前缀和优化 首先对于子数组和,我们可以使用「前缀和」的方式,方便快速的得到某个子数组的和。 对于区间 $[left, right]$,通过 $pre\_sum[right + 1] - prefix\_cnts[left]$ 即可快速求解出区间 $[left, right]$ 的子数组和。 此时问题就转变为:是否能找到满足 $i > j$ 且 $pre\_sum[i] - pre\_sum[j] \ge k$ 两个条件的子数组 $[j, i)$?如果能找到,则找出 $i - j$ 差值最小的作为答案。 #### 2. 单调队列优化 对于区间 $[j, i)$ 来说,我们应该尽可能的减少不成立的区间枚举。 1. 对于某个区间 $[j, i)$ 来说,如果 $pre\_sum[i] - pre\_sum[j] \ge k$,那么大于 $i$ 的索引值就不用再进行枚举了,不可能比 $i - j$ 的差值更优了。此时我们应该尽可能的向右移动 $j$,从而使得 $i - j$ 更小。 2. 对于某个区间 $[j, i)$ 来说,如果 $pre\_sum[j] \ge pre\_sum[i]$,对于任何大于等于 $i$ 的索引值 $r$ 来说,$pre\_sum[r] - pre\_sum[i]$ 一定比 $pre\_sum[i] - pre\_sum[j]$ 更小且长度更小,此时 $pre\_sum[j]$ 可以直接忽略掉。 因此,我们可以使用单调队列来维护单调递增的前缀数组 $pre\_sum$。其中存放了下标 $x:x_0, x_1, …$,满足 $pre\_sum[x_0] < pre\_sum[x_1] < …$ 单调递增。 1. 使用一重循环遍历位置 $i$,将当前位置 $i$ 存入倒掉队列中。 2. 对于每一个位置 $i$,如果单调队列不为空,则可以判断其之前存入在单调队列中的 $pre\_sum[j]$ 值,如果 $pre\_sum[i] - pre\_sum[j] \ge k$,则更新答案,并将 $j$ 从队头位置弹出。直到不再满足 $pre\_sum[i] - pre\_sum[j] \ge k$ 时为止(即 $pre\_sum[i] - pre\_sum[j] < k$)。 3. 如果队尾 $pre\_sum[j] \ge pre\_sum[i]$,那么说明以后无论如何都不会再考虑 $pre\_sum[j]$ 了,则将其从队尾弹出。 4. 最后遍历完返回答案。 ### 思路 1:代码 ```Python class Solution: def shortestSubarray(self, nums: List[int], k: int) -> int: size = len(nums) # 优化 1 pre_sum = [0 for _ in range(size + 1)] for i in range(size): pre_sum[i + 1] = pre_sum[i] + nums[i] ans = float('inf') queue = collections.deque() for i in range(size + 1): # 优化 2 while queue and pre_sum[i] - pre_sum[queue[0]] >= k: ans = min(ans, i - queue.popleft()) while queue and pre_sum[queue[-1]] >= pre_sum[i]: queue.pop() queue.append(i) if ans == float('inf'): return -1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(n)$。 ## 参考资料 - 【题解】[862. 和至少为 K 的最短子数组 - 力扣](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/solutions/1925036/liang-zhang-tu-miao-dong-dan-diao-dui-li-9fvh/) - 【题解】[Leetcode 862:和至少为 K 的最短子数组 - 掘金](https://juejin.cn/post/7076316608460750856) - 【题解】[LeetCode 862. 和至少为 K 的最短子数组 - AcWing](https://www.acwing.com/solution/leetcode/content/612/) - 【题解】[0862. Shortest Subarray With Sum at Least K | LeetCode Cookbook](https://books.halfrost.com/leetcode/ChapterFour/0800~0899/0862.Shortest-Subarray-with-Sum-at-Least-K/) ================================================ FILE: docs/solutions/0800-0899/similar-rgb-color.md ================================================ # [0800. 相似 RGB 颜色](https://leetcode.cn/problems/similar-rgb-color/) - 标签:数学、字符串、枚举 - 难度:简单 ## 题目链接 - [0800. 相似 RGB 颜色 - 力扣](https://leetcode.cn/problems/similar-rgb-color/) ## 题目大意 **描述**:RGB 颜色 `"#AABBCC"` 可以简写成 `"#ABC"` 。例如,`"#1155cc"` 可以简写为 `"#15c"`。现在给定一个按 `"#ABCDEF"` 形式定义的字符串 `color` 表示 RGB 颜色。 **要求**:返回一个与 `color` 相似度最大并且可以简写的颜色。 **说明**: - 两个颜色 `"#ABCDEF"` 和 `"#UVWXYZ"` 的相似度计算公式为:$-(AB - UV)^2 - (CD - WX)^2 - (EF - YZ)^2$。 **示例**: - 示例 1: ```python 输入 color = "#09f166" 输出 "#11ee66" 解释: 因为相似度计算得出 -(0x09 - 0x11)^2 -(0xf1 - 0xee)^2 - (0x66 - 0x66)^2 = -64 -9 -0 = -73,这是所有可以简写的颜色中与 color 最相似的颜色 ``` ## 解题思路 ### 思路 1:枚举算法 所有可以简写的颜色范围是 `"#000"` ~ `"#fff"`,共 $16^3 = 4096$ 种颜色。因此,我们可以枚举这些可以简写的颜色,并计算出其与 $color$的相似度,从而找出与 $color$ 最相似的颜色。具体做法如下: - 将 $color$ 转换为十六进制数,即 `hex_color = int(color[1:], 16)`。 - 三重循环遍历 $R$、$G$、$B$ 三个通道颜色,每一重循环范围为 $0 \sim 15$。 - 计算出每一种可以简写的颜色对应的十六进制,即 $17 \times R \times (1 << 16) + 17 \times G \times (1 << 8) + 17 \times B$,$17$ 是 $0x11 = 16 + 1 = 17$,$(1 << 16)$ 为 $R$ 左移的位数,$17 \times R \times (1 << 16)$ 就表示 $R$ 通道上对应的十六进制数。$(1 << 8)$ 为 $G$ 左移的位数,$17 \times G \times (1 << 8)$ 就表示 $G$ 通道上对应的十六进制数。$17 \times B$ 就表示 $B$ 通道上对应的十六进制数。 - 然后我们根据 $color$ 的十六进制数,与每一个可以简写的颜色对应的十六进制数,计算出相似度,并找出大相似对应的颜色。将其转换为字符串,并输出。 ### 思路 1:枚举算法代码 ```python class Solution: def similar(self, hex1, hex2): r1, g1, b1 = hex1 >> 16, (hex1 >> 8) % 256, hex1 % 256 r2, g2, b2 = hex2 >> 16, (hex2 >> 8) % 256, hex2 % 256 return - (r1 - r2) ** 2 - (g1 - g2) ** 2 - (b1 - b2) ** 2 def similarRGB(self, color: str) -> str: ans = 0 hex_color = int(color[1:], 16) for r in range(16): for g in range(16): for b in range(16): hex_cur = 17 * r * (1 << 16) + 17 * g * (1 << 8) + 17 * b if self.similar(hex_color, hex_cur) > self.similar(hex_color, ans): ans = hex_cur return "#{:06x}".format(ans) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(16^3)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/similar-string-groups.md ================================================ # [0839. 相似字符串组](https://leetcode.cn/problems/similar-string-groups/) - 标签:深度优先搜索、广度优先搜索、并查集、数组、哈希表、字符串 - 难度:困难 ## 题目链接 - [0839. 相似字符串组 - 力扣](https://leetcode.cn/problems/similar-string-groups/) ## 题目大意 **描述**: 如果交换字符串 $X$ 中的两个不同位置的字母,使得它和字符串 $Y$ 相等,那么称 $X$ 和 $Y$ 两个字符串相似。如果这两个字符串本身是相等的,那它们也是相似的。 例如,`"tars"` 和 `"rats"` 是相似的 (交换 0 与 2 的位置); `"rats"` 和 `"arts"` 也是相似的,但是 `"star"` 不与 `"tars"`,`"rats"`,或 `"arts"` 相似。 总之,它们通过相似性形成了两个关联组:`{"tars", "rats", "arts"}` 和 `{"star"}`。注意,`"tars"` 和 `"arts"` 是在同一组中,即使它们并不相似。形式上,对每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。 给你一个字符串列表 $strs$。列表中的每个字符串都是 $strs$ 中其它所有字符串的一个字母异位词。 **要求**: 计算 $strs$ 中有多少个相似字符串组。 **说明**: - $1 \le strs.length \le 300$。 - $1 \le strs[i].length \le 300$。 - $strs[i]$ 只包含小写字母。 - $strs$ 中的所有单词都具有相同的长度,且是彼此的字母异位词。 **示例**: - 示例 1: ```python 输入:strs = ["tars","rats","arts","star"] 输出:2 ``` - 示例 2: ```python 输入:strs = ["omv","ovm"] 输出:1 ``` ## 解题思路 ### 思路 1:并查集 判断两个字符串是否相似:只需检查它们有多少个位置的字符不同。如果不同位置的个数为 0 或 2,则相似。 使用并查集来维护相似字符串组: 1. 初始化并查集,每个字符串自成一组 2. 遍历所有字符串对,如果两个字符串相似,将它们合并到同一组 3. 最后统计并查集中有多少个不同的根节点 ### 思路 1:代码 ```python class Solution: def numSimilarGroups(self, strs: List[str]) -> int: n = len(strs) # 并查集 parent = list(range(n)) def find(x): if parent[x] != x: parent[x] = find(parent[x]) return parent[x] def union(x, y): root_x = find(x) root_y = find(y) if root_x != root_y: parent[root_x] = root_y # 判断两个字符串是否相似 def is_similar(s1, s2): diff_count = 0 for i in range(len(s1)): if s1[i] != s2[i]: diff_count += 1 if diff_count > 2: return False # 相同或恰好两个位置不同 return diff_count == 0 or diff_count == 2 # 遍历所有字符串对 for i in range(n): for j in range(i + 1, n): if is_similar(strs[i], strs[j]): union(i, j) # 统计不同的根节点数量 return len(set(find(i) for i in range(n))) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \times m + n \times \alpha(n))$,其中 $n$ 是字符串数量,$m$ 是字符串长度,$\alpha$ 是并查集的反阿克曼函数。需要检查 $O(n^2)$ 对字符串,每次比较需要 $O(m)$。 - **空间复杂度**:$O(n)$,并查集需要 $O(n)$ 空间。 ================================================ FILE: docs/solutions/0800-0899/smallest-subtree-with-all-the-deepest-nodes.md ================================================ # [0865. 具有所有最深节点的最小子树](https://leetcode.cn/problems/smallest-subtree-with-all-the-deepest-nodes/) - 标签:树、深度优先搜索、广度优先搜索、哈希表、二叉树 - 难度:中等 ## 题目链接 - [0865. 具有所有最深节点的最小子树 - 力扣](https://leetcode.cn/problems/smallest-subtree-with-all-the-deepest-nodes/) ## 题目大意 **描述**: 给定一个根为 $root$ 的二叉树,每个节点的深度是「该节点到根的最短距离」。 **要求**: 返回包含原始树中所有「最深节点」的「最小子树」。 **说明**: - 如果一个节点在「整个树」的任意节点之间具有最大的深度,则该节点是「最深的」。 - 一个节点的「子树」是该节点加上它的所有后代的集合。 - 树中节点的数量在 $[1, 500]$ 范围内。 - $0 \le Node.val \le 500$。 - 每个节点的值都是「独一无二」的。 - 注意:本题与力扣 1123 重复:https://leetcode-cn.com/problems/lowest-common-ancestor-of-deepest-leaves [https://leetcode-cn.com/problems/lowest-common-ancestor-of-deepest-leaves/] **示例**: - 示例 1: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/07/01/sketch1.png) ```python 输入:root = [3,5,1,6,2,0,8,null,null,7,4] 输出:[2,7,4] 解释: 我们返回值为 2 的节点,在图中用黄色标记。 在图中用蓝色标记的是树的最深的节点。 注意,节点 5、3 和 2 包含树中最深的节点,但节点 2 的子树最小,因此我们返回它。 ``` - 示例 2: ```python 输入:root = [1] 输出:[1] 解释:根节点是树中最深的节点。 ``` ## 解题思路 ### 思路 1:DFS 对于每个节点,我们需要知道: 1. 左子树的最大深度 2. 右子树的最大深度 如果左右子树的最大深度相同,说明当前节点就是包含所有最深节点的最小子树的根。 递归函数返回:(节点, 深度),表示以当前节点为根的子树中,包含所有最深节点的最小子树的根节点和深度。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def subtreeWithAllDeepest(self, root: Optional[TreeNode]) -> Optional[TreeNode]: def dfs(node): # 返回 (包含所有最深节点的最小子树根节点, 深度) if not node: return None, 0 # 递归处理左右子树 left_node, left_depth = dfs(node.left) right_node, right_depth = dfs(node.right) # 如果左右子树深度相同,当前节点就是答案 if left_depth == right_depth: return node, left_depth + 1 # 如果左子树更深,答案在左子树中 elif left_depth > right_depth: return left_node, left_depth + 1 # 如果右子树更深,答案在右子树中 else: return right_node, right_depth + 1 return dfs(root)[0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树中节点的数量。需要遍历每个节点一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度。递归栈的深度为树的高度。 ================================================ FILE: docs/solutions/0800-0899/soup-servings.md ================================================ # [0808. 分汤](https://leetcode.cn/problems/soup-servings/) - 标签:数学、动态规划、概率与统计 - 难度:中等 ## 题目链接 - [0808. 分汤 - 力扣](https://leetcode.cn/problems/soup-servings/) ## 题目大意 **描述**: 你有两种汤,A 和 B,每种初始为 $n$ 毫升。在每一轮中,会随机选择以下四种操作中的一种,每种操作的概率为 0.25,且与之前的所有轮次 无关: 1. 从汤 A 取 100 毫升,从汤 B 取 0 毫升 2. 从汤 A 取 75 毫升,从汤 B 取 25 毫升 3. 从汤 A 取 50 毫升,从汤 B 取 50 毫升 4. 从汤 A 取 25 毫升,从汤 B 取 75 毫升 注意: - 不存在从汤 A 取 0ml 和从汤 B 取 100ml 的操作。 - 汤 A 和 B 在每次操作中同时被取出。 - 如果一次操作要求你取出比剩余的汤更多的量,请取出该汤剩余的所有部分。 操作过程在任何回合中任一汤被取完后立即停止。 **要求**: 返回汤 A 在 B 前取完的概率,加上两种汤在「同一回合」取完概率的一半。返回值在正确答案 $10^{-5}$ 的范围内将被认为是正确的。 **说明**: - $0 \le n \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:n = 50 输出:0.62500 解释: 如果我们选择前两个操作,A 首先将变为空。 对于第三个操作,A 和 B 会同时变为空。 对于第四个操作,B 首先将变为空。 所以 A 变为空的总概率加上 A 和 B 同时变为空的概率的一半是 0.25 *(1 + 1 + 0.5 + 0)= 0.625。 ``` - 示例 2: ```python 输入:n = 100 输出:0.71875 解释: 如果我们选择第一个操作,A 首先将变为空。 如果我们选择第二个操作,A 将在执行操作 [1, 2, 3] 时变为空,然后 A 和 B 在执行操作 4 时同时变空。 如果我们选择第三个操作,A 将在执行操作 [1, 2] 时变为空,然后 A 和 B 在执行操作 3 时同时变空。 如果我们选择第四个操作,A 将在执行操作 1 时变为空,然后 A 和 B 在执行操作 2 时同时变空。 所以 A 变为空的总概率加上 A 和 B 同时变为空的概率的一半是 0.71875。 ``` ## 解题思路 ### 思路 1:动态规划 + 记忆化搜索 定义 $dp(a, b)$ 表示汤 A 剩余 $a$ 毫升、汤 B 剩余 $b$ 毫升时,满足条件的概率。 状态转移: - 如果 $a \le 0$ 且 $b \le 0$,返回 $0.5$(同时取完) - 如果 $a \le 0$,返回 $1$(A 先取完) - 如果 $b \le 0$,返回 $0$(B 先取完) - 否则: $$dp(a, b) = 0.25 \times [dp(a-100, b) + dp(a-75, b-25) + dp(a-50, b-50) + dp(a-25, b-75)]$$ 优化: 1. 由于每次操作都是 25 的倍数,可以将 $n$ 除以 25 来缩小规模。 2. 当 $n$ 很大时($n \ge 5000$),概率会非常接近 1,可以直接返回 1。 ### 思路 1:代码 ```python class Solution: def soupServings(self, n: int) -> float: # 当 n >= 5000 时,概率非常接近 1 if n >= 5000: return 1.0 # 将 n 转换为以 25 为单位(向上取整) n = (n + 24) // 25 # 记忆化搜索 memo = {} def dp(a, b): # 如果已经计算过,直接返回 if (a, b) in memo: return memo[(a, b)] # 边界条件 if a <= 0 and b <= 0: return 0.5 if a <= 0: return 1.0 if b <= 0: return 0.0 # 状态转移 result = 0.25 * ( dp(a - 4, b) + # 操作 1: A 取 100ml, B 取 0ml dp(a - 3, b - 1) + # 操作 2: A 取 75ml, B 取 25ml dp(a - 2, b - 2) + # 操作 3: A 取 50ml, B 取 50ml dp(a - 1, b - 3) # 操作 4: A 取 25ml, B 取 75ml ) memo[(a, b)] = result return result return dp(n, n) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是转换后的汤的容量。需要计算 $O(n^2)$ 个状态。 - **空间复杂度**:$O(n^2)$,记忆化搜索需要存储 $O(n^2)$ 个状态。 ================================================ FILE: docs/solutions/0800-0899/spiral-matrix-iii.md ================================================ # [0885. 螺旋矩阵 III](https://leetcode.cn/problems/spiral-matrix-iii/) - 标签:数组、矩阵、模拟 - 难度:中等 ## 题目链接 - [0885. 螺旋矩阵 III - 力扣](https://leetcode.cn/problems/spiral-matrix-iii/) ## 题目大意 **描述**: 在 $rows \times cols$ 的网格上,你从单元格 $(rStart, cStart)$ 面朝东面开始。网格的西北角位于第一行第一列,网格的东南角位于最后一行最后一列。 你需要以顺时针按螺旋状行走,访问此网格中的每个位置。每当移动到网格的边界之外时,需要继续在网格之外行走(但稍后可能会返回到网格边界)。 最终,我们到过网格的所有 $rows \times cols$ 个空间。 **要求**: 按照访问顺序返回表示网格位置的坐标列表。 **说明**: - $1 \le rows, cols \le 10^{3}$。 - $0 \le rStart \lt rows$。 - $0 \le cStart \lt cols$。 **示例**: - 示例 1: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/08/24/example_1.png) ```python 输入:rows = 1, cols = 4, rStart = 0, cStart = 0 输出:[[0,0],[0,1],[0,2],[0,3]] ``` - 示例 2: ![](https://s3-lc-upload.s3.amazonaws.com/uploads/2018/08/24/example_2.png) ```python 输入:rows = 5, cols = 6, rStart = 1, cStart = 4 输出:[[1,4],[1,5],[2,5],[2,4],[2,3],[1,3],[0,3],[0,4],[0,5],[3,5],[3,4],[3,3],[3,2],[2,2],[1,2],[0,2],[4,5],[4,4],[4,3],[4,2],[4,1],[3,1],[2,1],[1,1],[0,1],[4,0],[3,0],[2,0],[1,0],[0,0]] ``` ## 解题思路 ### 思路 1:模拟 按照螺旋顺序模拟机器人的行走过程: 1. 螺旋行走的规律是:向右走 1 步,向下走 1 步,向左走 2 步,向上走 2 步,向右走 3 步,向下走 3 步... 2. 可以发现,每个方向走的步数规律为:1, 1, 2, 2, 3, 3, 4, 4... 3. 方向顺序为:东、南、西、北,循环往复。 4. 在行走过程中,如果当前位置在网格内,就将其加入结果。 5. 当结果数组的长度等于 $rows \times cols$ 时,说明已经访问完所有格子。 ### 思路 1:代码 ```python class Solution: def spiralMatrixIII(self, rows: int, cols: int, rStart: int, cStart: int) -> List[List[int]]: # 方向数组:东、南、西、北 directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] result = [[rStart, cStart]] # 起始位置 if rows * cols == 1: return result r, c = rStart, cStart steps = 1 # 当前方向要走的步数 while len(result) < rows * cols: # 每两个方向,步数增加 1 for i in range(4): dr, dc = directions[i] # 在当前方向走 steps 步 for _ in range(steps): r += dr c += dc # 如果在网格内,加入结果 if 0 <= r < rows and 0 <= c < cols: result.append([r, c]) if len(result) == rows * cols: return result # 每走完两个方向,步数加 1 if i % 2 == 1: steps += 1 return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\max(rows, cols)^2)$,最坏情况下需要走出网格很远才能访问完所有格子。 - **空间复杂度**:$O(1)$,不考虑结果数组的空间。 ================================================ FILE: docs/solutions/0800-0899/split-array-into-fibonacci-sequence.md ================================================ # [0842. 将数组拆分成斐波那契序列](https://leetcode.cn/problems/split-array-into-fibonacci-sequence/) - 标签:字符串、回溯 - 难度:中等 ## 题目链接 - [0842. 将数组拆分成斐波那契序列 - 力扣](https://leetcode.cn/problems/split-array-into-fibonacci-sequence/) ## 题目大意 **描述**: 给定一个数字字符串 $num$,比如 `"123456579"`,我们可以将它分成「斐波那契式」的序列 $[123, 456, 579]$。 形式上,「斐波那契式」序列是一个非负整数列表 $f$,且满足: - $0 \le f[i] < 2^{31}$,(也就是说,每个整数都符合 32 位 有符号整数类型) - $f.length \ge 3$ - 对于所有的 $0 \le i < f.length - 2$,都有 $f[i] + f[i + 1] = f[i + 2]$ 另外,请注意,将字符串拆分成小块时,每个块的数字一定不要以零开头,除非这个块是数字 0 本身。 **要求**: 返回从 $num$ 拆分出来的任意一组斐波那契式的序列块,如果不能拆分则返回 []。 **说明**: - $1 \le num.length \le 200$。 - $num$ 中只含有数字。 **示例**: - 示例 1: ```python 输入:num = "1101111" 输出:[11,0,11,11] 解释:输出 [110,1,111] 也可以。 ``` - 示例 2: ```python 输入: num = "112358130" 输出: [] 解释: 无法拆分。 ``` ## 解题思路 ### 思路 1:回溯 斐波那契序列的特点是:前两个数确定后,后续所有数都确定了。因此我们可以: 1. 枚举前两个数的所有可能划分。 2. 对于每种划分,尝试按照斐波那契规则继续划分剩余部分。 3. 如果能成功划分完整个字符串,返回结果;否则尝试下一种划分。 注意事项: - 数字不能有前导零(除非数字本身是 0) - 每个数必须在 32 位有符号整数范围内($< 2^{31}$) - 序列至少要有 3 个数 ### 思路 1:代码 ```python class Solution: def splitIntoFibonacci(self, num: str) -> List[int]: n = len(num) def backtrack(index, path): # 如果已经遍历完字符串且序列长度 >= 3,返回 True if index == n and len(path) >= 3: return True # 枚举当前数字的长度 for i in range(index, n): # 如果有前导零,只能是单独的 0 if num[index] == '0' and i > index: break # 截取当前数字 cur_str = num[index:i+1] cur_num = int(cur_str) # 检查是否超过 32 位整数范围 if cur_num >= 2**31: break # 如果序列长度 < 2,直接加入 if len(path) < 2: path.append(cur_num) if backtrack(i + 1, path): return True path.pop() # 如果序列长度 >= 2,检查是否满足斐波那契规则 elif cur_num == path[-1] + path[-2]: path.append(cur_num) if backtrack(i + 1, path): return True path.pop() # 如果当前数字已经大于前两数之和,后面更长的数字更不可能满足 elif cur_num > path[-1] + path[-2]: break return False path = [] if backtrack(0, path): return path return [] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是字符串的长度。最坏情况下需要枚举前两个数的所有可能组合,每种组合最多需要 $O(n)$ 时间验证。 - **空间复杂度**:$O(n)$,递归栈的深度最多为 $O(n)$。 ================================================ FILE: docs/solutions/0800-0899/split-array-with-same-average.md ================================================ # [0805. 数组的均值分割](https://leetcode.cn/problems/split-array-with-same-average/) - 标签:位运算、数组、数学、动态规划、状态压缩 - 难度:困难 ## 题目链接 - [0805. 数组的均值分割 - 力扣](https://leetcode.cn/problems/split-array-with-same-average/) ## 题目大意 **描述**: 给定你一个整数数组 $nums$。 我们要将 $nums$ 数组中的每个元素移动到 A 数组 或者 B 数组中,使得 A 数组和 B 数组不为空,并且 `average(A) == average(B)`。 **要求**: 如果可以完成则返回 true,否则返回 false。 **说明**: - 注意:对于数组 $arr$, `average(arr)` 是 $arr$ 的所有元素的和除以 $arr$ 长度。 - $1 \le nums.length \le 30$。 - $0 \le nums[i] \le 10^{4}$。 **示例**: - 示例 1: ```python 输入: nums = [1,2,3,4,5,6,7,8] 输出: true 解释: 我们可以将数组分割为 [1,4,5,8] 和 [2,3,6,7], 他们的平均值都是4.5。 ``` - 示例 2: ```python 输入: nums = [3,1] 输出: false ``` ## 解题思路 ### 思路 1:动态规划 + 状态压缩 设数组总和为 $sum$,长度为 $n$。如果能将数组分成两部分 $A$ 和 $B$,使得它们的平均值相等,则: $$\frac{\sum A}{|A|} = \frac{\sum B}{|B|} = \frac{sum}{n}$$ 即:$\sum A \times n = sum \times |A|$ 因此,我们需要找到一个子集 $A$,使得 $\sum A = \frac{sum \times |A|}{n}$。 枚举子集大小 $k$(从 1 到 $n/2$),使用动态规划判断是否存在大小为 $k$、和为 $\frac{sum \times k}{n}$ 的子集。 定义 $dp[k]$ 为所有大小为 $k$ 的子集的和的集合。 ### 思路 1:代码 ```python class Solution: def splitArraySameAverage(self, nums: List[int]) -> bool: n = len(nums) total = sum(nums) # 特判 if n == 1: return False # dp[k] 存储所有大小为 k 的子集的和 dp = [set() for _ in range(n // 2 + 1)] dp[0].add(0) for num in nums: # 倒序遍历,避免重复使用 for k in range(n // 2, 0, -1): for s in list(dp[k - 1]): dp[k].add(s + num) # 检查是否存在满足条件的子集 for k in range(1, n // 2 + 1): # 检查 sum * k 是否能被 n 整除 if (total * k) % n == 0: target = (total * k) // n if target in dp[k]: return True return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \times sum)$,其中 $n$ 是数组长度,$sum$ 是数组总和。需要枚举所有子集。 - **空间复杂度**:$O(n \times sum)$,需要存储所有可能的子集和。 ================================================ FILE: docs/solutions/0800-0899/stone-game.md ================================================ # [0877. 石子游戏](https://leetcode.cn/problems/stone-game/) - 标签:数组、数学、动态规划、博弈 - 难度:中等 ## 题目链接 - [0877. 石子游戏 - 力扣](https://leetcode.cn/problems/stone-game/) ## 题目大意 亚历克斯和李在玩石子游戏。总共有偶数堆石子,每堆都有正整数颗石子 `piles[i]`,总共的石子数为奇数 。每回合,玩家从开始位置或者结束位置取走一整堆石子。直到没有石子堆为止结束游戏,最终手中石子颗数多的玩家获胜。假设亚历克斯和李每回合都能发挥出最佳水平,并且亚历克斯先开始。 给定代表每个位置石子颗数的数组 `piles`。 要求:判断亚历克斯是否能赢得比赛。如果亚历克斯赢得比赛,则返回 `True`。如果李赢得比赛返回 `False`。 ## 解题思路 能取的次数是偶数个,总数是奇数个。 - 如果亚历克斯开始取了开始偶数位置 `0`,那么李只能取奇数位置 `1` 或者末尾位置 `len(piles) - 1`。然后亚历克斯可以j接着取偶数位。 - 或者亚历克斯开始取了最后奇数位置 `len(piles) - 1`,那么李只能取偶数位置 `0` 或 `len(piles) - 2`。然后亚历克斯可以接着取奇数位。 - 这样亚历克斯只要一开始计算好奇数位置上的石子总数多,还是偶数位置上的石子总数多,然后就可以选择一开始取奇数位置还是偶数位置。所以最后肯定会赢 - 游戏一开始,其实就没李啥事了。。。 ## 代码 ```python class Solution: def stoneGame(self, piles: List[int]) -> bool: return True ``` ================================================ FILE: docs/solutions/0800-0899/subdomain-visit-count.md ================================================ # [0811. 子域名访问计数](https://leetcode.cn/problems/subdomain-visit-count/) - 标签:数组、哈希表、字符串、计数 - 难度:中等 ## 题目链接 - [0811. 子域名访问计数 - 力扣](https://leetcode.cn/problems/subdomain-visit-count/) ## 题目大意 **描述**:网站域名是由多个子域名构成的。 - 例如 `"discuss.leetcode.com"` 的顶级域名为 `"com"`,二级域名为 `"leetcode.com"`,三级域名为 `"discuss.leetcode.com"`。 当访问 `"discuss.leetcode.com"` 时,也会隐式访问其父域名 `"leetcode.com"` 以及 `"com"`。 计算机配对域名的格式为 `"rep d1.d2.d3"` 或 `"rep d1.d2"`。其中 `rep` 表示访问域名的次数,`d1.d2.d3` 或 `d1.d2` 为域名本身。 - 例如:`"9001 discuss.leetcode.com"` 就是一个 计数配对域名 ,表示 `discuss.leetcode.com` 被访问了 `9001` 次。 现在给定一个由计算机配对域名组成的数组 `cpdomains`。 **要求**:解析每一个计算机配对域名,计算出所有域名的访问次数,并以数组形式返回。可以按任意顺序返回答案。 ## 解题思路 这道题求解的是不同层级的域名的次数汇总,很容易想到使用哈希表。我们可以使用哈希表来统计不同层级的域名访问次数。具体做如下: 1. 如果数组 `cpdomains` 为空,直接返回空数组。 2. 使用哈希表 `times_dict` 存储不同层级的域名访问次数。 3. 遍历数组 `cpdomains`。对于每一个计算机配对域名 `cpdomain`: 1. 先将计算机配对域名的访问次数 `times` 和域名 `domain` 进行分割。 2. 然后将域名转为子域名数组 `domain_list`,逆序拼接不同等级的子域名 `sub_domain`。 3. 如果子域名 `sub_domain` 没有出现在哈希表 `times_dict` 中,则在哈希表中存入 `sub_domain` 和访问次数 `times` 的键值对。 4. 如果子域名 `sub_domain` 曾经出现在哈希表 `times_dict` 中,则在哈希表对应位置加上 `times`。 4. 遍历完之后,遍历哈希表 `times_dict`,将所有域名和访问次数拼接为字符串,存入答案数组中。 5. 最后返回答案数组。 ## 代码 ```python class Solution: def subdomainVisits(self, cpdomains: List[str]) -> List[str]: if not cpdomains: return [] times_dict = dict() for cpdomain in cpdomains: tiems, domain = cpdomain.split() tiems = int(tiems) domain_list = domain.split('.') for i in range(len(domain_list) - 1, -1, -1): sub_domain = '.'.join(domain_list[i:]) if sub_domain not in times_dict: times_dict[sub_domain] = tiems else: times_dict[sub_domain] += tiems res = [] for key in times_dict.keys(): res.append(str(times_dict[key]) + ' ' + key) return res ``` ================================================ FILE: docs/solutions/0800-0899/sum-of-distances-in-tree.md ================================================ # [0834. 树中距离之和](https://leetcode.cn/problems/sum-of-distances-in-tree/) - 标签:树、深度优先搜索、图、动态规划 - 难度:困难 ## 题目链接 - [0834. 树中距离之和 - 力扣](https://leetcode.cn/problems/sum-of-distances-in-tree/) ## 题目大意 **描述**:给定一个无向、连通的树。树中有 $n$ 个标记为 $0 \sim n - 1$ 的节点以及 $n - 1$ 条边 。 给定整数 $n$ 和数组 $edges$,其中 $edges[i] = [ai, bi]$ 表示树中的节点 $ai$ 和 $bi$ 之间有一条边。 **要求**:返回长度为 $n$ 的数组 $answer$,其中 $answer[i]$ 是树中第 $i$ 个节点与所有其他节点之间的距离之和。 **说明**: - $1 \le n \le 3 \times 10^4$。 - $edges.length == n - 1$。 - $edges[i].length == 2$。 - $0 \le ai, bi < n$。 - $ai \ne bi$。 - 给定的输入保证为有效的树。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/07/23/lc-sumdist1.jpg) ```python 输入: n = 6, edges = [[0,1],[0,2],[2,3],[2,4],[2,5]] 输出: [8,12,6,10,10,10] 解释: 树如图所示。 我们可以计算出 dist(0,1) + dist(0,2) + dist(0,3) + dist(0,4) + dist(0,5) 也就是 1 + 1 + 2 + 2 + 2 = 8。 因此,answer[0] = 8,以此类推。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/07/23/lc-sumdist3.jpg) ```python 输入: n = 2, edges = [[1,0]] 输出: [1,1] ``` ## 解题思路 ### 思路 1:树形 DP + 二次遍历换根法 最容易想到的做法是:枚举 $n$ 个节点,以每个节点为根节点进行树形 DP。 对于节点 $u$,定义 $dp[u]$ 为:以节点 $u$ 为根节点的树,它的所有子节点到它的距离之和。 然后进行一轮深度优先搜索,在搜索的过程中得到以节点 $v$ 为根节点的树,节点 $v$ 与所有其他子节点之间的距离之和 $dp[v]$。还能得到子树的节点个数 $sizes[v]$。 对于节点 $v$ 来说,其对 $dp[u]$ 的贡献为:节点 $v$ 与所有其他子节点之间的距离之和,再加上需要经过 $u \rightarrow v$ 这条边的节点个数,即 $dp[v] + sizes[v]$。 可得到状态转移方程为:$dp[u] = \sum_{v \in graph[u]}(dp[v] + sizes[v])$。 这样,对于 $n$ 个节点来说,需要进行 $n$ 次树形 DP,这种做法的时间复杂度为 $O(n^2)$,而 $n$ 的范围为 $[1, 3 \times 10^4]$,这样做会导致超时,因此需要进行优化。 我们可以使用「二次遍历换根法」进行优化,从而在 $O(n)$ 的时间复杂度内解决这道题。 以编号为 $0$ 的节点为根节点,进行两次深度优先搜索。 1. 第一次遍历:从编号为 $0$ 的根节点开始,自底向上地计算出节点 $0$ 到其他的距离之和,记录在 $ans[0]$ 中。并且统计出以子节点为根节点的子树节点个数 $sizes[v]$。 2. 第二次遍历:从编号为 $0$ 的根节点开始,自顶向下地枚举每个点,计算出将每个点作为新的根节点时,其他节点到根节点的距离之和。如果当前节点为 $v$,其父节点为 $u$,则自顶向下计算出 $ans[u]$ 之后,我们将根节点从 $u$ 换为节点 $v$,子树上的点到新根节点的距离比原来都小了 $1$,非子树上剩下所有点到新根节点的距离比原来都大了 $1$。则可以据此计算出节点 $v$ 与其他节点的距离和为:$ans[v] = ans[u] + n - 2 \times sizes[u]$。 ### 思路 1:代码 ```python class Solution: def sumOfDistancesInTree(self, n: int, edges: List[List[int]]) -> List[int]: graph = [[] for _ in range(n)] for u, v in edges: graph[u].append(v) graph[v].append(u) ans = [0 for _ in range(n)] sizes = [1 for _ in range(n)] def dfs(u, fa, depth): ans[0] += depth for v in graph[u]: if v == fa: continue dfs(v, u, depth + 1) sizes[u] += sizes[v] def reroot(u, fa): for v in graph[u]: if v == fa: continue ans[v] = ans[u] + n - 2 * size[v] reroot(v, u) dfs(0, -1, 0) reroot(0, -1) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为树的节点个数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0800-0899/sum-of-subsequence-widths.md ================================================ # [0891. 子序列宽度之和](https://leetcode.cn/problems/sum-of-subsequence-widths/) - 标签:数组、数学、排序 - 难度:困难 ## 题目链接 - [0891. 子序列宽度之和 - 力扣](https://leetcode.cn/problems/sum-of-subsequence-widths/) ## 题目大意 **描述**: 一个序列的「宽度」定义为该序列中最大元素和最小元素的差值。 给定一个整数数组 $nums$。 **要求**: 返回 $nums$ 的所有非空「子序列」的「宽度之和」。由于答案可能非常大,请返回对 $10^9 + 7$ 取余后的结果。 **说明**: - 「子序列」定义为从一个数组里删除一些(或者不删除)元素,但不改变剩下元素的顺序得到的数组。例如,$[3,6,2,7]$ 就是数组 $[0,3,1,6,2,2,7]$ 的一个子序列。 - $1 \le nums.length \le 10^{5}$。 - $1 \le nums[i] \le 10^{5}$。 **示例**: - 示例 1: ```python 输入:nums = [2,1,3] 输出:6 解释:子序列为 [1], [2], [3], [2,1], [2,3], [1,3], [2,1,3] 。 相应的宽度是 0, 0, 0, 1, 1, 2, 2 。 宽度之和是 6 。 ``` - 示例 2: ```python 输入:nums = [2] 输出:0 ``` ## 解题思路 ### 思路 1:排序 + 数学 关键观察:子序列的宽度只与最大值和最小值有关,与中间元素无关。 对数组排序后,对于元素 $nums[i]$: - 作为最大值时,有 $2^i$ 个子序列(从前 $i$ 个元素中任选) - 作为最小值时,有 $2^{n-1-i}$ 个子序列(从后 $n-1-i$ 个元素中任选) 因此,元素 $nums[i]$ 对答案的贡献为: $$nums[i] \times (2^i - 2^{n-1-i})$$ 总答案为: $$\sum_{i=0}^{n-1} nums[i] \times (2^i - 2^{n-1-i})$$ ### 思路 1:代码 ```python class Solution: def sumSubseqWidths(self, nums: List[int]) -> int: MOD = 10**9 + 7 n = len(nums) # 排序 nums.sort() # 预计算 2 的幂次 pow2 = [1] * n for i in range(1, n): pow2[i] = (pow2[i - 1] * 2) % MOD result = 0 # 计算每个元素的贡献 for i in range(n): # 作为最大值的贡献 - 作为最小值的贡献 contribution = (nums[i] * pow2[i] - nums[i] * pow2[n - 1 - i]) % MOD result = (result + contribution) % MOD return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组长度。排序需要 $O(n \log n)$,计算贡献需要 $O(n)$。 - **空间复杂度**:$O(n)$,需要存储 2 的幂次数组。 ================================================ FILE: docs/solutions/0800-0899/super-egg-drop.md ================================================ # [0887. 鸡蛋掉落](https://leetcode.cn/problems/super-egg-drop/) - 标签:数学、二分查找、动态规划 - 难度:困难 ## 题目链接 - [0887. 鸡蛋掉落 - 力扣](https://leetcode.cn/problems/super-egg-drop/) ## 题目大意 **描述**:给定一个整数 `k` 和整数 `n`,分别代表 `k` 枚鸡蛋和可以使用的一栋从第 `1` 层到第 `n` 层楼的建筑。 已知存在楼层 `f`,满足 `0 <= f <= n`,任何从高于 `f` 的楼层落下的鸡蛋都会碎,从 `f` 楼层或比它低的楼层落下的鸡蛋都不会碎。 每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 `x` 扔下(满足 `1 <= x <= n`),如果鸡蛋碎了,就不能再次使用它。如果鸡蛋没碎,则可以再次使用。 **要求**:计算并返回要确定 `f` 确切值的最小操作次数是多少。 **说明**: - $1 \le k \le 100$。 - $1 \le n \le 10^4$。 **示例**: - 示例 1: ```python 输入:k = 1, n = 2 输入:2 解释:鸡蛋从 1 楼掉落。如果它碎了,肯定能得出 f = 0。否则,鸡蛋从 2 楼掉落。如果它碎了,肯定能得出 f = 1。如果它没碎,那么肯定能得出 f = 2。因此,在最坏的情况下我们需要移动 2 次以确定 f 是多少。 ``` ## 解题思路 这道题目的题意不是很容易理解,我们先把题目简化一下,忽略一些限制条件,理解简单情况下的题意。然后再一步步增加限制条件,从而弄明白这道题目的意思,以及思考清楚这道题的解题思路。 我们先忽略 `k` 个鸡蛋这个条件,假设有无限个鸡蛋。 现在有 `1` ~ `n` 一共 `n` 层楼。已知存在楼层 `f`,低于等于 `f` 层的楼层扔下去的鸡蛋都不会碎,高于 `f` 的楼层扔下去的鸡蛋都会碎。 当然这个楼层 `f` 的确切值题目没有给出,需要我们一次次去测试鸡蛋最高会在哪一层不会摔碎。 在每次操作中,我们可以选定一个楼层,将鸡蛋扔下去: - 如果鸡蛋没摔碎,则可以继续选择其他楼层进行测试。 - 如果鸡蛋摔碎了,则该鸡蛋无法继续测试。 现在题目要求:**已知有 `n` 层楼,无限个鸡蛋,求出至少需要扔几次鸡蛋,才能保证无论 `f` 是多少层,都能将 `f` 找出来?** 最简单且直观的想法: 1. 从第 `1` 楼开始扔鸡蛋。`1` 楼不碎,再去 `2` 楼扔。 2. `2` 楼还不碎,就去 `3` 楼扔。 3. …… 4. 直到鸡蛋碎了,也就找到了鸡蛋不会摔碎的最高层 `f`。 用这种方法,最坏情况下,鸡蛋在第 `n` 层也没摔碎。这种情况下我们总共试了 `n` 次才确定鸡蛋不会摔碎的最高楼层 `f`。 下面再来说一下比 `n` 次要少的情况。 如果我们可以通过二分查找的方法,先从 `1` ~ `n` 层的中间层开始扔鸡蛋。 - 如果鸡蛋碎了,则从第 `1` 层到中间层这个区间中去扔鸡蛋。 - 如果鸡蛋没碎,则从中间层到第 `n` 层这个区间中去扔鸡蛋。 每次扔鸡蛋都从区间的中间层去扔,这样每次都能排除当前区间一半的答案,从而最终确定鸡蛋不会摔碎的最高楼层 `f`。 通过这种二分查找的方法,可以优化到 $\log n$ 次就能确定鸡蛋不会摔碎的最高楼层 `f`。 因为 $\log n \le n$,所以通过二分查找的方式,「至少」比线性查找的次数要少。 同样,我们还可以通过三分查找、五分查找等等方式减少次数。 这是在不限制鸡蛋个数的情况下,现在我们来限制一下鸡蛋个数为 `k`。 现在题目要求:**已知有 `n` 层楼,`k` 个鸡蛋,求出至少需要扔几次鸡蛋,才能保证无论 `f` 是多少层,都能将 `f` 找出来?** 如果鸡蛋足够多(大于等于 $\log_2 n$ 个),可以通过二分查找的方法来测试。如果鸡蛋不够多,可能二分查找过程中,鸡蛋就用没了,则不能通过二分查找的方法来测试。 那么这时候为了找出 `f` ,我们应该如何求出最少的扔鸡蛋次数? ### 思路 1:动态规划(超时) 可以这样考虑。题目限定了 `n` 层楼,`k` 个鸡蛋。 如果我们尝试在 `1` ~ `n` 层中的任意一层 `x` 扔鸡蛋: 1. 如果鸡蛋没碎,则说明 `1` ~ `x` 层都不用再考虑了,我们需要用 `k` 个鸡蛋去考虑剩下的 `n - x` 层,问题就从 `(n, k)` 转变为了 `(n - x, k)`。 2. 如果鸡蛋碎了,则说明 `x + 1` ~ `n` 层都不用再考虑了,我们需要去剩下的 `k - 1` 个鸡蛋考虑剩下的 `x - 1` 层,问题就从 `(n, k)` 转变为了 `(x - 1, k - 1)`。 这样一来,我们就可以根据上述关系使用动态规划方法来解决这道题目了。具体步骤如下: ###### 1. 阶段划分 按照楼层数量、剩余鸡蛋个数进行阶段划分。 ###### 2. 定义状态 定义状态 `dp[i][j]` 表示为:一共有 `i` 层楼,`j` 个鸡蛋的条件下,为了找出 `f` ,最坏情况下的最少扔鸡蛋次数。 ###### 3. 状态转移方程 根据之前的描述,`dp[i][j]` 有两个来源,其状态转移方程为: $dp[i][j] = min_{1 \le x \le n} (max(dp[i - x][j], dp[x - 1][j - 1])) + 1$ ###### 4. 初始条件 给定鸡蛋 `k` 的取值范围为 `[1, 100]`,`f` 值取值范围为 `[0, n]`,初始化时,可以考虑将所有值设置为当前拥有的楼层数。 - 当鸡蛋数为 `1` 时,`dp[i][1] = i`。这是如果唯一的蛋碎了,则无法测试了。只能从低到高,一步步进行测试,最终最少测试数为当前拥有的楼层数(如果刚开始初始化时已经将所有值设置为当前拥有的楼层数,其实这一步可省略)。 - 当楼层为 `1` 时,在 `1` 层扔鸡蛋,`dp[1][j] = 1`。这是因为: - 如果在 `1` 层扔鸡蛋碎了,则 `f < 1`。同时因为 `f` 的取值范围为 `[0, n]`。所以能确定 `f = 0`。 - 如果在 `1` 层扔鸡蛋没碎,则 `f >= 1`。同时因为 `f` 的取值范围为 `[0, n]`。所以能确定 `f = 0`。 ###### 5. 最终结果 根据我们之前定义的状态,`dp[i][j]` 表示为:一共有 `i` 层楼,`j` 个鸡蛋的条件下,为了找出 `f` ,最坏情况下的最少扔鸡蛋次数。则最终结果为 `dp[n][k]`。 ### 思路 1:代码 ```python class Solution: def superEggDrop(self, k: int, n: int) -> int: dp = [[0 for _ in range(k + 1)] for i in range(n + 1)] for i in range(1, n + 1): for j in range(1, k + 1): dp[i][j] = i # for i in range(1, n + 1): # dp[i][1] = i for j in range(1, k + 1): dp[1][j] = 1 for i in range(2, n + 1): for j in range(2, k + 1): for x in range(1, i + 1): dp[i][j] = min(dp[i][j], max(dp[i - x][j], dp[x - 1][j - 1]) + 1) return dp[n][k] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \times k)$。三重循环的时间复杂度为 $O(n^2 \times k)$。 - **空间复杂度**:$O(n \times k)$。 ### 思路 2:动态规划优化 上一步中时间复杂度为 $O(n^2 \times k)$。根据 $n$ 的规模,提交上去不出意外的超时了。 我们可以观察一下上面的状态转移方程:$dp[i][j] = min_{1 \le x \le n} (max(dp[i - x][j], dp[x - 1][j - 1])) + 1$ 。 这里最外两层循环的 `i`、`j` 分别为状态的阶段,可以先将 `i`、`j` 看作固定值。最里层循环的 `x` 代表选择的任意一层 `x` ,值从 `1` 遍历到 `i`。 此时我们把 `dp[i - x][j]` 和 `dp[x - 1][j - 1]` 分别单独来看。可以看出: - 对于 `dp[i - x][j]`:当 `x` 增加时,`i - x` 的值减少,`dp[i - x][j]` 的值跟着减小。自变量 `x` 与函数 `dp[i - x][j]` 是一条单调非递增函数。 - 对于 `dp[x - 1][j - 1]`:当 `x` 增加时, `x - 1` 的值增加,`dp[x - 1][j - 1]` 的值跟着增加。自变量 `x` 与函数 `dp[x - 1][j - 1]` 是一条单调非递减函数。 两条函数的交点处就是两个函数较大值的最小值位置。即 `dp[i][j]` 所取位置。而这个位置可以通过二分查找满足 `dp[x - 1][j - 1] >= dp[i - x][j]` 最大的那个 `x`。这样时间复杂度就从 $O(n^2 \times k)$ 优化到了 $O(n \log n \times k)$。 ### 思路 2:代码 ```python class Solution: def superEggDrop(self, k: int, n: int) -> int: dp = [[0 for _ in range(k + 1)] for i in range(n + 1)] for i in range(1, n + 1): for j in range(1, k + 1): dp[i][j] = i # for i in range(1, n + 1): # dp[i][1] = i for j in range(1, k + 1): dp[1][j] = 1 for i in range(2, n + 1): for j in range(2, k + 1): left, right = 1, i while left < right: mid = left + (right - left) // 2 if dp[mid - 1][j - 1] < dp[i - mid][j]: left = mid + 1 else: right = mid dp[i][j] = max(dp[left - 1][j - 1], dp[i - left][j]) + 1 return dp[n][k] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \log n \times k)$。两重循环的时间复杂度为 $O(n \times k)$,二分查找的时间复杂度为 $O(\log n)$。 - **空间复杂度**:$O(n \times k)$。 ### 思路 3:动态规划 + 逆向思维 再看一下我们现在的题目要求:已知有 `n` 层楼,`k` 个鸡蛋,求出至少需要扔几次鸡蛋,才能保证无论 `f` 是多少层,都能将 `f` 找出来? 我们可以逆向转换一下思维,将题目转变为:**已知有 `k` 个鸡蛋,最多扔 `x` 次鸡蛋(碎没碎都算 `1` 次),求最多可以检测的多少层?** 我们把未知条件「扔鸡蛋的次数」变为了已知条件,将「检测的楼层个数」变为了未知条件。 这样如果求出来的「检测的楼层个数」大于等于 `n`,则说明 `1` ~ `n` 层楼都考虑全了,`f` 值也就明确了。我们只需要从符合条件的情况中,找出「扔鸡蛋次数」最少的次数即可。 动态规划的具体步骤如下: ###### 1. 阶段划分 按照鸡蛋个数、扔鸡蛋的次数进行阶段划分。 ###### 2. 定义状态 定义状态 `dp[i][j]` 表示为:一共有 `i` 个鸡蛋,最多扔 `j` 次鸡蛋(碎没碎都算 `1` 次)的条件下,最多可以检测的楼层个数。 ###### 3. 状态转移方程 我们现在有 `i` 个鸡蛋,`j` 次扔鸡蛋的机会,现在尝试在 `1` ~ `n` 层中的任意一层 `x` 扔鸡蛋: 1. 如果鸡蛋没碎,剩下 `i` 个鸡蛋,还有 `j - 1` 次扔鸡蛋的机会,最多可以检测 `dp[i][j - 1]` 层楼层。 2. 如果鸡蛋碎了,剩下 `i - 1` 个鸡蛋,还有 `j - 1` 次扔鸡蛋的机会,最多可以检测 `dp[i - 1][j - 1]` 层楼层。 3. 再加上我们扔鸡蛋的第 `x` 层,`i` 个鸡蛋,`j` 次扔鸡蛋的机会最多可以检测 `dp[i][j - 1] + dp[i - 1][j - 1] + 1` 层。 则状态转移方程为:$dp[i][j] = dp[i][j - 1] + dp[i - 1][j - 1] + 1$。 ###### 4. 初始条件 - 当鸡蛋数为 `1` 时,只有 `1` 次扔鸡蛋的机会时,最多可以检测 `1` 层,即 `dp[1][1] = 1`。 ###### 5. 最终结果 根据我们之前定义的状态,`dp[i][j]` 表示为:一共有 `i` 个鸡蛋,最多扔 `j` 次鸡蛋(碎没碎都算 `1` 次)的条件下,最多可以检测的楼层个数。则我们需要从满足 `i == k` 并且 `dp[i][j] >= n`(即 `k` 个鸡蛋,`j` 次扔鸡蛋,一共检测出 `n` 层楼)的情况中,找出最小的 ` j`,将其返回。 ### 思路 3:代码 ```python class Solution: def superEggDrop(self, k: int, n: int) -> int: dp = [[0 for _ in range(n + 1)] for i in range(k + 1)] dp[1][1] = 1 for i in range(1, k + 1): for j in range(1, n + 1): dp[i][j] = dp[i][j - 1] + dp[i - 1][j - 1] + 1 if i == k and dp[i][j] >= n: return j return n ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n \times k)$。两重循环的时间复杂度为 $O(n \times k)$。 - **空间复杂度**:$O(n \times k)$。 ## 参考资料 - 【题解】[题目理解 + 基本解法 + 进阶解法 - 鸡蛋掉落 - 力扣](https://leetcode.cn/problems/super-egg-drop/solution/ji-ben-dong-tai-gui-hua-jie-fa-by-labuladong/) - 【题解】[动态规划(只解释官方题解方法一)(Java) - 鸡蛋掉落 - 力扣](https://leetcode.cn/problems/super-egg-drop/solution/dong-tai-gui-hua-zhi-jie-shi-guan-fang-ti-jie-fang/) - 【题解】[动态规划 & 记忆化搜索 2000ms -> 32ms 的过程 - 鸡蛋掉落 - 力扣](https://leetcode.cn/problems/super-egg-drop/solution/python-dong-tai-gui-hua-ji-yi-hua-sou-su-hnj9/) ================================================ FILE: docs/solutions/0800-0899/surface-area-of-3d-shapes.md ================================================ # [0892. 三维形体的表面积](https://leetcode.cn/problems/surface-area-of-3d-shapes/) - 标签:几何、数组、数学、矩阵 - 难度:简单 ## 题目链接 - [0892. 三维形体的表面积 - 力扣](https://leetcode.cn/problems/surface-area-of-3d-shapes/) ## 题目大意 **描述**:给定一个 $n \times n$ 的网格 $grid$,上面放置着一些 $1 \times 1 \times 1$ 的正方体。每个值 $v = grid[i][j]$ 表示 $v$ 个正方体叠放在对应单元格 $(i, j)$ 上。 放置好正方体后,任何直接相邻的正方体都会互相粘在一起,形成一些不规则的三维形体。 **要求**:返回最终这些形体的总面积。 **说明**: - 每个形体的底面也需要计入表面积中。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/01/08/tmp-grid2.jpg) ```python 输入:grid = [[1,2],[3,4]] 输出:34 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/01/08/tmp-grid4.jpg) ```python 输入:grid = [[1,1,1],[1,0,1],[1,1,1]] 输出:32 ``` ## 解题思路 ### 思路 1:模拟 使用二重循环遍历所有的正方体,计算每一个正方体所贡献的表面积,将其累积起来即为答案。 而每一个正方体所贡献的表面积,可以通过枚举当前正方体前后左右相邻四个方向上的正方体的个数,从而通过判断计算得出。 - 如果当前位置 $(row, col)$ 存在正方体,则正方体在上下位置上起码贡献了 $2$ 的表面积。 - 如果当前位置 $(row, col)$ 的相邻位置 $(new\_row, new\_col)$ 上不存在正方体,说明当前正方体在该方向为最外侧,则 $(row, col)$ 位置所贡献的表面积为当前位置上的正方体个数,即 $grid[row][col]$。 - 如果当前位置 $(row, col)$ 的相邻位置 $(new\_row, new\_col)$ 上存在正方体: - 如果 $grid[row][col] > grid[new\_row][new\_col]$,说明 $grid[row][col]$ 在该方向上底面一部分被 $grid[new\_row][new\_col]$ 遮盖了,则 $(row, col)$ 位置所贡献的表面积为 $grid[row][col] - grid[new_row][new_col]$。 - 如果 $grid[row][col] \le grid[new\_row][new\_col]$,说明 $grid[row][col]$ 在该方向上完全被 $grid[new\_row][new\_col]$ 遮盖了,则 $(row, col)$ 位置所贡献的表面积为 $0$。 ### 思路 1:代码 ```Python class Solution: def surfaceArea(self, grid: List[List[int]]) -> int: directions = [(-1, 0), (0, 1), (1, 0), (0, -1)] size = len(grid) ans = 0 for row in range(size): for col in range(size): if grid[row][col]: # 底部、顶部贡献表面积 ans += 2 for direction in directions: new_row = row + direction[0] new_col = col + direction[1] if 0 <= new_row < size and 0 <= new_col < size: if grid[row][col] > grid[new_row][new_col]: add = grid[row][col] - grid[new_row][new_col] else: add = 0 else: add = grid[row][col] ans += add return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为二位数组 $grid$ 的行数或列数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0800-0899/transpose-matrix.md ================================================ # [0867. 转置矩阵](https://leetcode.cn/problems/transpose-matrix/) - 标签:数组、矩阵、模拟 - 难度:简单 ## 题目链接 - [0867. 转置矩阵 - 力扣](https://leetcode.cn/problems/transpose-matrix/) ## 题目大意 给定一个二维数组 matrix。返回 matrix 的转置矩阵。 ## 解题思路 直接模拟求解即可。先求出 matrix 的规模。如果 matrix 是 m * n 的矩阵。则创建一个 n * m 大小的矩阵 transposed。根据转置的规则对 transposed 的每个元素进行赋值。最终返回 transposed。 ## 代码 ```python class Solution: def transpose(self, matrix: List[List[int]]) -> List[List[int]]: m = len(matrix) n = len(matrix[0]) transposed = [[0 for _ in range(m)] for _ in range(n)] for i in range(m): for j in range(n): transposed[j][i] = matrix[i][j] return transposed ``` ================================================ FILE: docs/solutions/0800-0899/uncommon-words-from-two-sentences.md ================================================ # [0884. 两句话中的不常见单词](https://leetcode.cn/problems/uncommon-words-from-two-sentences/) - 标签:哈希表、字符串 - 难度:简单 ## 题目链接 - [0884. 两句话中的不常见单词 - 力扣](https://leetcode.cn/problems/uncommon-words-from-two-sentences/) ## 题目大意 **描述**:给定两个字符串 $s1$ 和 $s2$ ,分别表示两个句子。 **要求**:返回所有不常用单词的列表。返回列表中单词可以按任意顺序组织。 **说明**: - **句子**:是一串由空格分隔的单词。 - **单词**:仅由小写字母组成的子字符串。 - **不常见单词**:如果某个单词在其中一个句子中恰好出现一次,在另一个句子中却没有出现,那么这个单词就是不常见的。 - $1 \le s1.length, s2.length \le 200$。 - $s1$ 和 $s2$ 由小写英文字母和空格组成。 - $s1$ 和 $s2$ 都不含前导或尾随空格。 - $s1$ 和 $s2$ 中的所有单词间均由单个空格分隔。 **示例**: - 示例 1: ```python 输入:s1 = "this apple is sweet", s2 = "this apple is sour" 输出:["sweet","sour"] ``` - 示例 2: ```python 输入:s1 = "apple apple", s2 = "banana" 输出:["banana"] ``` ## 解题思路 ### 思路 1:哈希表 题目要求找出在其中一个句子中恰好出现一次,在另一个句子中却没有出现的单词,其实就是找出在两个句子中只出现过一次的单词,我们可以用哈希表统计两个句子中每个单词的出现频次,然后将出现频次为 $1$ 的单词就是不常见单词,将其加入答案数组即可。 具体步骤如下: 1. 遍历字符串 $s1$、$s2$,使用哈希表 $table$ 统计字符串 $s1$、$s2$ 各个单词的出现频次。 2. 遍历哈希表,找出出现频次为 $1$ 的单词,将其加入答案数组 $res$ 中。 3. 遍历完返回答案数组 $res$。 ### 思路 1:代码 ```python class Solution: def uncommonFromSentences(self, s1: str, s2: str) -> List[str]: table = dict() for word in s1.split(' '): if word not in table: table[word] = 1 else: table[word] += 1 for word in s2.split(' '): if word not in table: table[word] = 1 else: table[word] += 1 res = [] for word in table: if table[word] == 1: res.append(word) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n)$,其中 $m$、$n$ 分别为字符串 $s1$、$s2$ 的长度。 - **空间复杂度**:$O(m + n)$。 ================================================ FILE: docs/solutions/0800-0899/unique-morse-code-words.md ================================================ # [0804. 唯一摩尔斯密码词](https://leetcode.cn/problems/unique-morse-code-words/) - 标签:数组、哈希表、字符串 - 难度:简单 ## 题目链接 - [0804. 唯一摩尔斯密码词 - 力扣](https://leetcode.cn/problems/unique-morse-code-words/) ## 题目大意 **描述**:国际摩尔斯密码定义一种标准编码方式,将每个字母对应于一个由一系列点和短线组成的字符串, 比如: - `'a'` 对应 `".-"`, - `'b'` 对应 `"-..."`, - `'c'` 对应 `"-.-."` ,以此类推。 为了方便,所有 $26$ 个英文字母的摩尔斯密码表如下: `[".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."]` 给定一个字符串数组 $words$,每个单词可以写成每个字母对应摩尔斯密码的组合。 - 例如,`"cab"` 可以写成 `"-.-..--..."` ,(即 `"-.-."` + `".-"` + `"-..."` 字符串的结合)。我们将这样一个连接过程称作单词翻译。 **要求**:对 $words$ 中所有单词进行单词翻译,返回不同单词翻译的数量。 **说明**: - $1 \le words.length \le 100$。 - $1 \le words[i].length \le 12$。 - $words[i]$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入: words = ["gin", "zen", "gig", "msg"] 输出: 2 解释: 各单词翻译如下: "gin" -> "--...-." "zen" -> "--...-." "gig" -> "--...--." "msg" -> "--...--." 共有 2 种不同翻译, "--...-." 和 "--...--.". ``` - 示例 2: ```python 输入:words = ["a"] 输出:1 ``` ## 解题思路 ### 思路 1:模拟 + 哈希表 1. 根据题目要求,将所有单词都转换为对应摩斯密码。 2. 使用哈希表存储所有转换后的摩斯密码。 3. 返回哈希表中不同的摩斯密码个数(脊哈希表的长度)作为答案。 ### 思路 1:代码 ```Python class Solution: def uniqueMorseRepresentations(self, words: List[str]) -> int: table = [".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."] word_set = set() for word in words: word_mose = "" for ch in word: word_mose += table[ord(ch) - ord('a')] word_set.add(word_mose) return len(word_set) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(s)$,其中 $s$ 为数组 $words$ 中所有单词的长度之和。 - **空间复杂度**:$O(s)$。 ================================================ FILE: docs/solutions/0800-0899/walking-robot-simulation.md ================================================ # [0874. 模拟行走机器人](https://leetcode.cn/problems/walking-robot-simulation/) - 标签:数组、哈希表、模拟 - 难度:中等 ## 题目链接 - [0874. 模拟行走机器人 - 力扣](https://leetcode.cn/problems/walking-robot-simulation/) ## 题目大意 **描述**: 机器人在一个无限大小的 XY 网格平面上行走,从点 $(0, 0)$ 处开始出发,面向北方。该机器人可以接收以下三种类型的命令 $commands$: - $-2$:向左转 90 度 - $-1$:向右转 90 度 - $1 \le x \le 9$:向前移动 $x$ 个单位长度 在网格上有一些格子被视为障碍物 $obstacles$。第 $i$ 个障碍物位于网格点 $obstacles[i] = (xi, yi)$。 机器人无法走到障碍物上,它将会停留在障碍物的前一个网格方块上,并继续执行下一个命令。 **要求**: 返回机器人距离原点的「最大欧式距离」的「平方」。(即,如果距离为 5 ,则返回 25 ) **说明**: - 注意: - 北方表示 +Y 方向。 - 东方表示 +X 方向。 - 南方表示 -Y 方向。 - 西方表示 -X 方向。 - 原点 $[0,0]$ 可能会有障碍物。 - $1 \le commands.length \le 10^{4}$。 - $commands[i]$ 的值可以取 -2、-1 或者是范围 $[1, 9]$ 内的一个整数。 - $0 \le obstacles.length \le 10^{4}$。 - $-3 \times 10^{4} \le xi, yi \le 3 \times 10^{4}$。 - 答案保证小于 $2^{31}$。 **示例**: - 示例 1: ```python 输入:commands = [4,-1,3], obstacles = [] 输出:25 解释: 机器人开始位于 (0, 0): 1. 向北移动 4 个单位,到达 (0, 4) 2. 右转 3. 向东移动 3 个单位,到达 (3, 4) 距离原点最远的是 (3, 4) ,距离为 32 + 42 = 25 ``` - 示例 2: ```python 输入:commands = [4,-1,4,-2,4], obstacles = [[2,4]] 输出:65 解释:机器人开始位于 (0, 0): 1. 向北移动 4 个单位,到达 (0, 4) 2. 右转 3. 向东移动 1 个单位,然后被位于 (2, 4) 的障碍物阻挡,机器人停在 (1, 4) 4. 左转 5. 向北走 4 个单位,到达 (1, 8) 距离原点最远的是 (1, 8) ,距离为 12 + 82 = 65 ``` ## 解题思路 ### 思路 1:模拟 + 哈希表 模拟机器人的行走过程: 1. 用方向数组表示四个方向:北(0, 1)、东(1, 0)、南(0, -1)、西(-1, 0)。 2. 用变量 $direction$ 表示当前方向的索引。 3. 将障碍物坐标存入哈希集合,方便快速查询。 4. 遍历命令: - 如果是 -2(左转),方向索引减 1 - 如果是 -1(右转),方向索引加 1 - 如果是移动命令,逐步移动,每次移动一格并检查是否有障碍物 5. 记录过程中距离原点的最大欧式距离的平方。 ### 思路 1:代码 ```python class Solution: def robotSim(self, commands: List[int], obstacles: List[List[int]]) -> int: # 方向数组:北、东、南、西 directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] direction = 0 # 初始方向为北 # 将障碍物转换为集合,方便查询 obstacle_set = set(map(tuple, obstacles)) x, y = 0, 0 # 初始位置 max_dist = 0 # 最大距离的平方 for cmd in commands: if cmd == -2: # 左转 direction = (direction - 1) % 4 elif cmd == -1: # 右转 direction = (direction + 1) % 4 else: # 前进 cmd 步 dx, dy = directions[direction] for _ in range(cmd): # 尝试移动一步 next_x, next_y = x + dx, y + dy # 检查是否有障碍物 if (next_x, next_y) not in obstacle_set: x, y = next_x, next_y # 更新最大距离 max_dist = max(max_dist, x * x + y * y) else: # 遇到障碍物,停止移动 break return max_dist ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(N + K)$,其中 $N$ 是命令数组的长度,$K$ 是所有移动命令的步数之和。将障碍物加入集合需要 $O(M)$,其中 $M$ 是障碍物数量。 - **空间复杂度**:$O(M)$,需要存储障碍物的哈希集合。 ================================================ FILE: docs/solutions/0900-0999/3sum-with-multiplicity.md ================================================ # [0923. 三数之和的多种可能](https://leetcode.cn/problems/3sum-with-multiplicity/) - 标签:数组、哈希表、双指针、计数、排序 - 难度:中等 ## 题目链接 - [0923. 三数之和的多种可能 - 力扣](https://leetcode.cn/problems/3sum-with-multiplicity/) ## 题目大意 **描述**: 给定一个整数数组 $arr$,以及一个整数 $target$ 作为目标值。 **要求**: 返回满足 $i < j < k$ 且 $arr[i] + arr[j] + arr[k] == target$ 的元组 $i, j, k$ 的数量。 由于结果会非常大,请返回 $10^9 + 7$ 的模。 **说明**: - $3 \le arr.length \le 3000$。 - $0 \le arr[i] \le 10^{3}$。 - $0 \le target \le 300$。 **示例**: - 示例 1: ```python 输入:arr = [1,1,2,2,3,3,4,4,5,5], target = 8 输出:20 解释: 按值枚举(arr[i], arr[j], arr[k]): (1, 2, 5) 出现 8 次; (1, 3, 4) 出现 8 次; (2, 2, 4) 出现 2 次; (2, 3, 3) 出现 2 次。 ``` - 示例 2: ```python 输入:arr = [1,1,2,2,2,2], target = 5 输出:12 解释: arr[i] = 1, arr[j] = arr[k] = 2 出现 12 次: 我们从 [1,1] 中选择一个 1,有 2 种情况, 从 [2,2,2,2] 中选出两个 2,有 6 种情况。 ``` ## 解题思路 ### 思路 1:哈希表 + 双指针 #### 思路 这道题要求统计满足 $i < j < k$ 且 $arr[i] + arr[j] + arr[k] = target$ 的三元组数量。 由于数组元素范围较小($0 \le arr[i] \le 100$),我们可以使用哈希表统计每个数字的出现次数,然后枚举所有可能的三元组: 1. **统计频次**:使用哈希表 $count$ 统计每个数字的出现次数。 2. **枚举三元组**:枚举所有可能的 $(i, j, k)$ 组合,其中 $i \le j \le k$: - 如果 $i + j + k = target$,计算该组合的方案数。 - 方案数的计算需要考虑三个数是否相同: - 如果三个数都相同:$C(count[i], 3) = \frac{count[i] \times (count[i] - 1) \times (count[i] - 2)}{6}$ - 如果两个数相同:$C(count[i], 2) \times count[k] = \frac{count[i] \times (count[i] - 1)}{2} \times count[k]$ - 如果三个数都不同:$count[i] \times count[j] \times count[k]$ 3. 返回总方案数对 $10^9 + 7$ 取模的结果。 #### 代码 ```python class Solution: def threeSumMulti(self, arr: List[int], target: int) -> int: MOD = 10**9 + 7 from collections import Counter # 统计每个数字的出现次数 count = Counter(arr) res = 0 # 枚举所有可能的三元组 (i, j, k),其中 i <= j <= k for i in range(101): for j in range(i, 101): k = target - i - j if k < 0 or k > 100: continue if i == j == k: # 三个数都相同:C(count[i], 3) res += count[i] * (count[i] - 1) * (count[i] - 2) // 6 elif i == j: # 前两个数相同:C(count[i], 2) * count[k] if k > j: res += count[i] * (count[i] - 1) // 2 * count[k] elif j == k: # 后两个数相同:count[i] * C(count[j], 2) if k > i: res += count[i] * count[j] * (count[j] - 1) // 2 else: # 三个数都不同 if k > j: res += count[i] * count[j] * count[k] res %= MOD return res ``` #### 复杂度分析 - **时间复杂度**:$O(n + C^2)$,其中 $n$ 是数组长度,$C = 101$ 是数字的范围。统计频次需要 $O(n)$,枚举三元组需要 $O(C^2)$。 - **空间复杂度**:$O(C)$,需要哈希表存储每个数字的频次。 ================================================ FILE: docs/solutions/0900-0999/add-to-array-form-of-integer.md ================================================ # [0989. 数组形式的整数加法](https://leetcode.cn/problems/add-to-array-form-of-integer/) - 标签:数组、数学 - 难度:简单 ## 题目链接 - [0989. 数组形式的整数加法 - 力扣](https://leetcode.cn/problems/add-to-array-form-of-integer/) ## 题目大意 **描述**: 整数的「数组形式」$num$ 是按照从左到右的顺序表示其数字的数组。 - 例如,对于 $num = 1321$ ,数组形式是 $[1,3,2,1]$。 给定 $num$,整数的「数组形式」,和整数 $k$。 **要求**: 返回 整数 $num + k$ 的「数组形式」。 **说明**: - $1 \le num.length \le 10^{4}$。 - $0 \le num[i] \le 9$。 - $num$ 不包含任何前导零,除了零本身。 - $1 \le k \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:num = [1,2,0,0], k = 34 输出:[1,2,3,4] 解释:1200 + 34 = 1234 ``` - 示例 2: ```python 输入:num = [2,7,4], k = 181 输出:[4,5,5] 解释:274 + 181 = 455 ``` ## 解题思路 ### 思路 1:模拟加法 #### 思路 这道题要求将数组形式的整数与整数 $k$ 相加,返回数组形式的结果。我们可以模拟加法的过程: 1. 从数组 $num$ 的最后一位开始,与 $k$ 的个位相加。 2. 计算当前位的和以及进位。 3. 将当前位的结果插入到结果数组的开头。 4. 更新 $k$ 为进位值($k$ 除以 $10$)。 5. 继续处理下一位,直到数组遍历完且 $k$ 为 $0$。 #### 代码 ```python class Solution: def addToArrayForm(self, num: List[int], k: int) -> List[int]: res = [] n = len(num) i = n - 1 # 从数组最后一位开始 # 当数组还有位数或 k 不为 0 时继续 while i >= 0 or k > 0: # 获取当前位的数字 digit = num[i] if i >= 0 else 0 # 计算当前位的和 total = digit + k % 10 # 将当前位结果插入到结果数组开头 res.append(total % 10) # 计算进位 k = k // 10 + total // 10 i -= 1 # 结果是逆序的,需要反转 return res[::-1] ``` #### 复杂度分析 - **时间复杂度**:$O(\max(n, \log k))$,其中 $n$ 是数组 $num$ 的长度,$\log k$ 是整数 $k$ 的位数。需要遍历所有位数。 - **空间复杂度**:$O(1)$,不考虑结果数组的空间,只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0900-0999/array-of-doubled-pairs.md ================================================ # [0954. 二倍数对数组](https://leetcode.cn/problems/array-of-doubled-pairs/) - 标签:贪心、数组、哈希表、排序 - 难度:中等 ## 题目链接 - [0954. 二倍数对数组 - 力扣](https://leetcode.cn/problems/array-of-doubled-pairs/) ## 题目大意 **描述**: 给定一个长度为偶数的整数数组 $arr$。 **要求**: 只有对 $arr$ 进行重组后可以满足「对于每个 $0 \le i < len(arr) / 2$,都有 $arr[2 \times i + 1] = 2 \times arr[2 \times i]$」时,返回 true;否则,返回 false。 **说明**: - $0 \le arr.length \le 3 \times 10^{4}$。 - $arr.length$ 是偶数。 - $-10^{5} \le arr[i] \le 10^{5}$。 **示例**: - 示例 1: ```python 输入:arr = [3,1,3,6] 输出:false ``` - 示例 2: ```python 输入:arr = [2,1,2,6] 输出:false ``` ## 解题思路 ### 思路 1:贪心 + 哈希表 #### 思路 这道题要求判断数组能否重组为二倍数对的形式,即 $arr[2 \times i + 1] = 2 \times arr[2 \times i]$。 我们可以使用贪心策略: 1. **统计频次**:使用哈希表统计每个数字出现的次数。 2. **排序**:按绝对值从小到大排序。这样可以保证先处理较小的数,避免遗漏。 3. **贪心匹配**:对于每个数 $x$,如果它还有剩余次数,就尝试找它的二倍 $2x$: - 如果 $2x$ 的次数不足,返回 `False`。 - 否则,将 $x$ 和 $2x$ 的次数都减 $1$。 4. 如果所有数都能成功匹配,返回 `True`。 **注意**:需要按绝对值排序,因为负数的二倍关系是 $-4$ 和 $-2$,而不是 $-2$ 和 $-4$。 #### 代码 ```python class Solution: def canReorderDoubled(self, arr: List[int]) -> bool: from collections import Counter # 统计每个数字的出现次数 count = Counter(arr) # 按绝对值从小到大排序 for x in sorted(count, key=abs): # 如果 x 还有剩余次数 if count[x] > 0: # 检查 2x 的次数是否足够 if count[2 * x] < count[x]: return False # 匹配 x 和 2x count[2 * x] -= count[x] return True ``` #### 复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组长度。主要时间消耗在排序上。 - **空间复杂度**:$O(n)$,需要哈希表存储每个数字的频次。 ================================================ FILE: docs/solutions/0900-0999/available-captures-for-rook.md ================================================ # [0999. 可以被一步捕获的棋子数](https://leetcode.cn/problems/available-captures-for-rook/) - 标签:数组、矩阵、模拟 - 难度:简单 ## 题目链接 - [0999. 可以被一步捕获的棋子数 - 力扣](https://leetcode.cn/problems/available-captures-for-rook/) ## 题目大意 **描述**:在一个 $8 \times 8$ 的棋盘上,有一个白色的车(Rook),用字符 `'R'` 表示。棋盘上还可能存在空方块,白色的象(Bishop)以及黑色的卒(pawn),分别用字符 `'.'`,`'B'` 和 `'p'` 表示。不难看出,大写字符表示的是白棋,小写字符表示的是黑棋。 **要求**:你现在可以控制车移动一次,请你统计有多少敌方的卒处于你的捕获范围内(即,可以被一步捕获的棋子数)。 **说明**: - 车按国际象棋中的规则移动。东,西,南,北四个基本方向任选其一,然后一直向选定的方向移动,直到满足下列四个条件之一: - 棋手选择主动停下来。 - 棋子因到达棋盘的边缘而停下。 - 棋子移动到某一方格来捕获位于该方格上敌方(黑色)的卒,停在该方格内。 - 车不能进入/越过已经放有其他友方棋子(白色的象)的方格,停在友方棋子前。 - $board.length == board[i].length == 8$ - $board[i][j]$ 可以是 `'R'`,`'.'`,`'B'` 或 `'p'`。 - 只有一个格子上存在 $board[i][j] == 'R'$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/02/23/1253_example_1_improved.PNG) ```python 输入:[[".",".",".",".",".",".",".","."],[".",".",".","p",".",".",".","."],[".",".",".","R",".",".",".","p"],[".",".",".",".",".",".",".","."],[".",".",".",".",".",".",".","."],[".",".",".","p",".",".",".","."],[".",".",".",".",".",".",".","."],[".",".",".",".",".",".",".","."]] 输出:3 解释:在本例中,车能够捕获所有的卒。 ``` - 示例 2: ![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/02/23/1253_example_2_improved.PNG) ```python 输入:[[".",".",".",".",".",".",".","."],[".","p","p","p","p","p",".","."],[".","p","p","B","p","p",".","."],[".","p","B","R","B","p",".","."],[".","p","p","B","p","p",".","."],[".","p","p","p","p","p",".","."],[".",".",".",".",".",".",".","."],[".",".",".",".",".",".",".","."]] 输出:0 解释:象阻止了车捕获任何卒。 ``` ## 解题思路 ### 思路 1:模拟 1. 双重循环遍历确定白色车的位置 $(pos\_i,poss\_j)$。 2. 让车向上、下、左、右四个方向进行移动,直到超出边界 / 碰到白色象 / 碰到卒为止。使用计数器 $cnt$ 记录捕获的卒的数量。 3. 返回答案 $cnt$。 ### 思路 1:代码 ```Python class Solution: def numRookCaptures(self, board: List[List[str]]) -> int: directions = {(1, 0), (-1, 0), (0, 1), (0, -1)} pos_i, pos_j = -1, -1 for i in range(len(board)): if pos_i != -1 and pos_j != -1: break for j in range(len(board[i])): if board[i][j] == 'R': pos_i, pos_j = i, j break cnt = 0 for direction in directions: setp = 0 while True: new_i = pos_i + setp * direction[0] new_j = pos_j + setp * direction[1] if new_i < 0 or new_i >= 8 or new_j < 0 or new_j >= 8 or board[new_i][new_j] == 'B': break if board[new_i][new_j] == 'p': cnt += 1 break setp += 1 return cnt ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为棋盘的边长。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0900-0999/bag-of-tokens.md ================================================ # [0948. 令牌放置](https://leetcode.cn/problems/bag-of-tokens/) - 标签:贪心、数组、双指针、排序 - 难度:中等 ## 题目链接 - [0948. 令牌放置 - 力扣](https://leetcode.cn/problems/bag-of-tokens/) ## 题目大意 **描述**: 你的初始「能量」为 $power$,初始「分数」为 0,只有一包令牌以整数数组 $tokens$ 给出。其中 $tokens[i]$ 是第 $i$ 个令牌的值(下标从 0 开始)。 你的目标是通过有策略地使用这些令牌以「最大化」总「分数」。在一次行动中,你可以用两种方式中的一种来使用一个 未被使用的 令牌(但不是对同一个令牌使用两种方式): - 朝上:如果你当前「至少」有 $tokens[i]$ 点 能量 ,可以使用令牌 $i$,失去 $tokens[i]$ 点「能量」,并得到 1 分。 - 朝下:如果你当前至少有 1 分,可以使用令牌 $i$ ,获得 $tokens[i]$ 点「能量」,并失去 1 分。 **要求**: 在使用「任意」数量的令牌后,返回我们可以得到的最大「分数」。 **说明**: - $0 \le tokens.length \le 10^{3}$。 - $0 \le tokens[i], power \lt 10^{4}$。 **示例**: - 示例 1: ```python 输入:tokens = [100], power = 50 输出:0 解释:因为你的初始分数为 0,无法使令牌朝下。你也不能使令牌朝上因为你的能量(50)比 tokens[0] 少(100)。 ``` - 示例 2: ```python 输入:tokens = [200,100], power = 150 输出:1 解释:使令牌 1 正面朝上,能量变为 50,分数变为 1 。 不必使用令牌 0,因为你无法使用它来提高分数。可得到的最大分数是 1。 ``` ## 解题思路 ### 思路 1:贪心 + 双指针 #### 思路 这道题要求通过使用令牌来最大化分数。我们有两种操作: - **朝上**:消耗 $tokens[i]$ 点能量,获得 $1$ 分。 - **朝下**:消耗 $1$ 分,获得 $tokens[i]$ 点能量。 贪心策略: 1. **排序**:将令牌按值从小到大排序。 2. **双指针**:使用左右指针分别指向最小和最大的令牌。 3. **策略**: - 优先使用能量较少的令牌朝上(左指针),获得分数。 - 当能量不足时,如果有分数,可以使用能量较大的令牌朝下(右指针),获得能量。 - 记录过程中的最大分数。 #### 代码 ```python class Solution: def bagOfTokensScore(self, tokens: List[int], power: int) -> int: tokens.sort() # 排序 left, right = 0, len(tokens) - 1 score = 0 # 当前分数 max_score = 0 # 最大分数 while left <= right: if power >= tokens[left]: # 能量足够,使用最小的令牌朝上 power -= tokens[left] score += 1 max_score = max(max_score, score) left += 1 elif score > 0: # 能量不足但有分数,使用最大的令牌朝下 power += tokens[right] score -= 1 right -= 1 else: # 能量不足且没有分数,无法继续 break return max_score ``` #### 复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是令牌数量。主要时间消耗在排序上。 - **空间复杂度**:$O(1)$,只使用了常数个额外变量(不考虑排序的空间)。 ================================================ FILE: docs/solutions/0900-0999/beautiful-array.md ================================================ # [0932. 漂亮数组](https://leetcode.cn/problems/beautiful-array/) - 标签:数组、数学、分治 - 难度:中等 ## 题目链接 - [0932. 漂亮数组 - 力扣](https://leetcode.cn/problems/beautiful-array/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:返回长度为 $n$ 的任一漂亮数组。 **说明**: - **漂亮数组**(长度为 $n$ 的数组 $nums$ 满足下述条件): - $nums$ 是由范围 $[1, n]$ 的整数组成的一个排列。 - 对于每个 $0 \le i < j < n$,均不存在下标 $k$($i < k < j$)使得 $2 \times nums[k] == nums[i] + nums[j]$。 - $1 \le n \le 1000$。 - 本题保证对于给定的 $n$ 至少存在一个有效答案。 **示例**: - 示例 1: ```python 输入:n = 4 输出:[2,1,4,3] ``` - 示例 2: ```python 输入:n = 5 输出:[3,1,2,5,4] ``` ## 解题思路 ### 思路 1:分治算法 根据题目要求,我们可以得到以下信息: 1. 题目要求 $2 \times nums[k] == nums[i] + nums[j], (0 \le i < k < j < n)$ 不能成立,可知:等式左侧必为偶数,只要右侧和为奇数则等式不成立。 2. 已知:奇数 + 偶数 = 奇数,则令 $nums[i]$ 和 $nums[j]$ 其中一个为奇数,另一个为偶数,即可保证 $nums[i] + nums[j]$ 一定为奇数。这里我们不妨令 $nums[i]$ 为奇数,令 $nums[j]$ 为偶数。 3. 如果数组 $nums$ 是漂亮数组,那么对数组 $nums$ 的每一位元素乘以一个常数或者加上一个常数之后,$nums$ 仍是漂亮数组。 - 即如果 $[a_1, a_2, ..., a_n]$ 是一个漂亮数组,那么 $[k \times a_1 + b, k \times a_2 + b, ..., k \times a_n + b]$ 也是漂亮数组。 那么,我们可以按照下面的规则构建长度为 $n$ 的漂亮数组。 1. 当 $n = 1$ 时,返回 $[1]$。此时数组 $nums$ 中仅有 $1$ 个元素,并且满足漂亮数组的条件。 2. 当 $n > 1$ 时,我们将 $nums$ 分解为左右两个部分:`left_nums`、`right_nums`。如果左右两个部分满足: 1. 数组 `left_nums` 中元素全为奇数(可以通过 `nums[i] * 2 - 1` 将 `left_nums` 中元素全部映射为奇数)。 2. 数组 `right_nums` 中元素全为偶数(可以通过 `nums[i] * 2` 将 `right_nums` 中元素全部映射为偶数)。 3. `left_nums` 和 `right_nums` 都是漂亮数组。 3. 那么 `left_nums + right_nums` 构成的数组一定也是漂亮数组,即 $nums$ 为漂亮数组,将 $nums$ 返回即可。 ### 思路 1:代码 ```python class Solution: def beautifulArray(self, n: int) -> List[int]: if n == 1: return [1] nums = [0 for _ in range(n)] left_cnt = (n + 1) // 2 right_cnt = n - left_cnt left_nums = self.beautifulArray(left_cnt) right_nums = self.beautifulArray(right_cnt) for i in range(left_cnt): nums[i] = 2 * left_nums[i] - 1 for i in range(right_cnt): nums[left_cnt + i] = 2 * right_nums[i] return nums ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(n \times \log n)$。 ================================================ FILE: docs/solutions/0900-0999/binary-subarrays-with-sum.md ================================================ # [0930. 和相同的二元子数组](https://leetcode.cn/problems/binary-subarrays-with-sum/) - 标签:数组、哈希表、前缀和、滑动窗口 - 难度:中等 ## 题目链接 - [0930. 和相同的二元子数组 - 力扣](https://leetcode.cn/problems/binary-subarrays-with-sum/) ## 题目大意 **描述**: 给定一个二元数组 $nums$,和一个整数 $goal$。 **要求**: 请你统计并返回有多少个和为 $goal$ 的「非空」子数组。 **说明**: - 「子数组」是数组的一段连续部分。 - $1 \le nums.length \le 3 * 10^{4}$。 - $nums[i]$ 不是 0 就是 1。 - $0 \le goal \le nums.length$。 **示例**: - 示例 1: ```python 输入:nums = [1,0,1,0,1], goal = 2 输出:4 解释: 有 4 个满足题目要求的子数组:[1,0,1]、[1,0,1,0]、[0,1,0,1]、[1,0,1] ``` - 示例 2: ```python 输入:nums = [0,0,0,0,0], goal = 0 输出:15 ``` ## 解题思路 ### 思路 1:前缀和 + 哈希表 #### 思路 这道题要求统计和为 $goal$ 的非空子数组数量。我们可以使用前缀和的思想: 1. **前缀和**:定义 $preSum[i]$ 为数组前 $i$ 个元素的和。 2. **子数组和**:子数组 $[i, j]$ 的和为 $preSum[j] - preSum[i - 1]$。 3. **转化问题**:要找满足 $preSum[j] - preSum[i - 1] = goal$ 的 $(i, j)$ 对数,即找满足 $preSum[i - 1] = preSum[j] - goal$ 的数量。 4. **哈希表优化**:使用哈希表 $count$ 记录每个前缀和出现的次数,遍历数组时: - 如果 $preSum - goal$ 在哈希表中,说明存在以当前位置为结尾、和为 $goal$ 的子数组,累加对应的次数。 - 将当前前缀和加入哈希表。 #### 代码 ```python class Solution: def numSubarraysWithSum(self, nums: List[int], goal: int) -> int: from collections import defaultdict # 哈希表记录前缀和出现的次数 count = defaultdict(int) count[0] = 1 # 前缀和为 0 的情况,初始化为 1 preSum = 0 # 当前前缀和 res = 0 # 结果 for num in nums: preSum += num # 如果 preSum - goal 存在,说明存在和为 goal 的子数组 if preSum - goal in count: res += count[preSum - goal] # 记录当前前缀和 count[preSum] += 1 return res ``` #### 复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组长度。只需要遍历一次数组。 - **空间复杂度**:$O(n)$,哈希表最多存储 $n$ 个不同的前缀和。 ================================================ FILE: docs/solutions/0900-0999/binary-tree-cameras.md ================================================ # [0968. 监控二叉树](https://leetcode.cn/problems/binary-tree-cameras/) - 标签:树、深度优先搜索、动态规划、二叉树 - 难度:困难 ## 题目链接 - [0968. 监控二叉树 - 力扣](https://leetcode.cn/problems/binary-tree-cameras/) ## 题目大意 给定一个二叉树,需要在树的节点上安装摄像头。节点上的每个摄影头都可以监视其父节点、自身及其直接子节点。 计算监控树的所有节点所需的最小摄像头数量。 - 示例 1: ![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/29/bst_cameras_01.png) ``` 输入:[0,0,null,0,0] 输出:1 解释:如图所示,一台摄像头足以监控所有节点。 ``` - 示例 2: ![img](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/29/bst_cameras_02.png) ``` 输入:[0,0,null,0,null,0,null,null,0] 输出:2 解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。 ``` ## 解题思路 根据题意可知,一个摄像头的有效范围为 3 层:父节点、自身及其直接子节点。而约是下层的节点就越多,所以摄像头应该优先满足下层节点。可以使用后序遍历的方式遍历二叉树的节点,这样就可以优先遍历叶子节点。 对于每个节点,利用贪心思想,可以确定三种状态: - 第一种状态:该节点无覆盖 - 第二种状态:该节点已经装上了摄像头 - 第三种状态:该节点已经覆盖 为了让摄像头数量最少,我们要尽量让叶⼦节点的⽗节点安装摄像头,这样才能摄像头的数量最少。对此我们应当分析当前节点和左右两侧子节点的覆盖情况。 先来考虑空节点,空节点应该算作已经覆盖状态。 再来考虑左右两侧子覆盖情况: - 如果左节点或者右节点都无覆盖,则当前节点需要装上摄像头,答案 res 需要 + 1。 - 如果左节点已经覆盖或者右节点已经装上了摄像头,则当前节点已经覆盖。 - 如果左节点右节点都已经覆盖,则当前节点无覆盖。 根据以上条件就可以写出对应的后序遍历代码。 ## 代码 ```python class Solution: res = 0 def traversal(self, cur: TreeNode) -> int: if not cur: return 3 left = self.traversal(cur.left) right = self.traversal(cur.right) if left == 1 or right == 1: self.res += 1 return 2 if left == 2 or right == 2: return 3 if left == 3 and right == 3: return 1 return -1 def minCameraCover(self, root: TreeNode) -> int: self.res = 0 if self.traversal(root) == 1: self.res += 1 return self.res ``` ================================================ FILE: docs/solutions/0900-0999/broken-calculator.md ================================================ # [0991. 坏了的计算器](https://leetcode.cn/problems/broken-calculator/) - 标签:贪心、数学 - 难度:中等 ## 题目链接 - [0991. 坏了的计算器 - 力扣](https://leetcode.cn/problems/broken-calculator/) ## 题目大意 **描述**: 在显示着数字 $startValue$ 的坏计算器上,我们可以执行以下两种操作: - 双倍(Double):将显示屏上的数字乘 2; - 递减(Decrement):将显示屏上的数字减 1。 给定两个整数 $startValue$ 和 $target$。 **要求**: 返回显示数字 $target$ 所需的最小操作数。 **说明**: - $1 \le startValue, target \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:startValue = 2, target = 3 输出:2 解释:先进行双倍运算,然后再进行递减运算 {2 -> 4 -> 3}. ``` - 示例 2: ```python 输入:startValue = 5, target = 8 输出:2 解释:先递减,再双倍 {5 -> 4 -> 8}. ``` ## 解题思路 ### 思路 1:逆向贪心 #### 思路 这道题可以进行两种操作:乘 $2$(Double)和减 $1$(Decrement),要求从 $startValue$ 到 $target$ 的最少操作数。 如果正向思考,每一步有两种选择,会导致搜索空间很大。我们可以 **逆向思考**: - 从 $target$ 出发,逆向操作回到 $startValue$。 - 逆向操作为:除以 $2$(对应正向的乘 $2$)和加 $1$(对应正向的减 $1$)。 贪心策略: 1. 如果 $target \le startValue$,只能一直减 $1$,操作数为 $startValue - target$。 2. 如果 $target$ 是偶数,除以 $2$ 更优(一次操作减少一半)。 3. 如果 $target$ 是奇数,必须先加 $1$ 变成偶数,然后再除以 $2$。 #### 代码 ```python class Solution: def brokenCalc(self, startValue: int, target: int) -> int: count = 0 # 从 target 逆向到 startValue while target > startValue: if target % 2 == 0: # target 是偶数,除以 2 target //= 2 else: # target 是奇数,加 1 target += 1 count += 1 # 如果 target < startValue,需要减 1 操作 count += startValue - target return count ``` #### 复杂度分析 - **时间复杂度**:$O(\log target)$,每次除以 $2$ 会使 $target$ 减半,最多需要 $O(\log target)$ 次操作。 - **空间复杂度**:$O(1)$,只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0900-0999/cat-and-mouse.md ================================================ # [0913. 猫和老鼠](https://leetcode.cn/problems/cat-and-mouse/) - 标签:图、拓扑排序、记忆化搜索、数学、动态规划、博弈 - 难度:困难 ## 题目链接 - [0913. 猫和老鼠 - 力扣](https://leetcode.cn/problems/cat-and-mouse/) ## 题目大意 **描述**: 两位玩家分别扮演猫和老鼠,在一张「无向」图上进行游戏,两人轮流行动。 图的形式是:$graph[a]$ 是一个列表,由满足 $ab$ 是图中的一条边的所有节点 $b$ 组成。 老鼠从节点 1 开始,第一个出发;猫从节点 2 开始,第二个出发。在节点 0 处有一个洞。 在每个玩家的行动中,他们 必须 沿着图中与所在当前位置连通的一条边移动。例如,如果老鼠在节点 1 ,那么它必须移动到 $graph[1]$ 中的任一节点。 此外,猫无法移动到洞中(节点 0)。 然后,游戏在出现以下三种情形之一时结束: - 如果猫和老鼠出现在同一个节点,猫获胜。 - 如果老鼠到达洞中,老鼠获胜。 - 如果某一位置重复出现(即,玩家的位置和移动顺序都与上一次行动相同),游戏平局。 给定一张图 $graph$ ,并假设两位玩家都都以最佳状态参与游戏。 **要求**: - 如果老鼠获胜,则返回 1; - 如果猫获胜,则返回 2; - 如果平局,则返回 0 。 **说明**: - $3 \le graph.length \le 50$。 - $1 \le graph[i].length \lt graph.length$。 - $0 \le graph[i][j] \lt graph.length$。 - $graph[i][j] \ne i$。 - $graph[i]$ 互不相同。 - 猫和老鼠在游戏中总是可以移动。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/11/17/cat1.jpg) ```python 输入:graph = [[2,5],[3],[0,4,5],[1,4,5],[2,3],[0,2,3]] 输出:0 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2020/11/17/cat2.jpg) ```python 输入:graph = [[1,3],[0],[3],[0,2]] 输出:1 ``` ## 解题思路 ### 思路 1:博弈论 + 拓扑排序 #### 思路 这是一个博弈问题,需要判断在双方都采取最优策略的情况下,谁会获胜。 我们可以使用 **拓扑排序 + 博弈状态** 来解决: - 状态定义:$(mouse, cat, turn)$ 表示老鼠在位置 $mouse$,猫在位置 $cat$,当前轮到 $turn$($1$ 表示老鼠,$2$ 表示猫)。 - 状态结果: - $0$:平局 - $1$:老鼠获胜 - $2$:猫获胜 使用逆向思维,从已知结果的状态开始,逐步推导其他状态: 1. **初始化必胜/必败状态**: - 老鼠到达洞口($mouse = 0$):老鼠获胜。 - 猫抓到老鼠($mouse = cat$):猫获胜。 2. **拓扑排序**:从已知状态出发,更新前驱状态: - 如果某个状态的所有后继状态都对对手有利,则该状态对当前玩家不利。 - 如果某个状态存在一个后继状态对当前玩家有利,则该状态对当前玩家有利。 3. **返回初始状态**:$(1, 2, 1)$ 的结果。 #### 代码 ```python class Solution: def catMouseGame(self, graph: List[List[int]]) -> int: n = len(graph) DRAW, MOUSE_WIN, CAT_WIN = 0, 1, 2 # 状态:(mouse, cat, turn),turn=1 表示老鼠,turn=2 表示猫 # 结果:0=平局,1=老鼠赢,2=猫赢 result = [[[DRAW] * 3 for _ in range(n)] for _ in range(n)] degree = [[[0] * 3 for _ in range(n)] for _ in range(n)] # 计算每个状态的出度 for mouse in range(n): for cat in range(n): degree[mouse][cat][1] = len(graph[mouse]) degree[mouse][cat][2] = len([node for node in graph[cat] if node != 0]) # 初始化队列:已知结果的状态 from collections import deque queue = deque() for cat in range(n): for turn in [1, 2]: # 老鼠到达洞口,老鼠赢 result[0][cat][turn] = MOUSE_WIN queue.append((0, cat, turn)) # 猫抓到老鼠(但猫不能在洞口),猫赢 if cat > 0: result[cat][cat][turn] = CAT_WIN queue.append((cat, cat, turn)) # 拓扑排序 while queue: mouse, cat, turn = queue.popleft() current_result = result[mouse][cat][turn] if turn == 1: # 当前是老鼠的回合,推导上一步猫的状态 for prev_cat in graph[cat]: if prev_cat == 0: # 猫不能进洞 continue if result[mouse][prev_cat][2] != DRAW: continue if current_result == CAT_WIN: # 如果老鼠这步后猫赢,说明猫的上一步可以导致猫赢 result[mouse][prev_cat][2] = CAT_WIN queue.append((mouse, prev_cat, 2)) else: # 否则,减少出度 degree[mouse][prev_cat][2] -= 1 if degree[mouse][prev_cat][2] == 0: # 所有后继状态都对猫不利,猫输 result[mouse][prev_cat][2] = MOUSE_WIN queue.append((mouse, prev_cat, 2)) else: # 当前是猫的回合,推导上一步老鼠的状态 for prev_mouse in graph[mouse]: if result[prev_mouse][cat][1] != DRAW: continue if current_result == MOUSE_WIN: # 如果猫这步后老鼠赢,说明老鼠的上一步可以导致老鼠赢 result[prev_mouse][cat][1] = MOUSE_WIN queue.append((prev_mouse, cat, 1)) else: # 否则,减少出度 degree[prev_mouse][cat][1] -= 1 if degree[prev_mouse][cat][1] == 0: # 所有后继状态都对老鼠不利,老鼠输 result[prev_mouse][cat][1] = CAT_WIN queue.append((prev_mouse, cat, 1)) return result[1][2][1] ``` #### 复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 是图中节点的数量。需要遍历所有状态和边。 - **空间复杂度**:$O(n^2)$,需要存储所有状态的结果和出度。 ================================================ FILE: docs/solutions/0900-0999/check-completeness-of-a-binary-tree.md ================================================ # [0958. 二叉树的完全性检验](https://leetcode.cn/problems/check-completeness-of-a-binary-tree/) - 标签:树、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0958. 二叉树的完全性检验 - 力扣](https://leetcode.cn/problems/check-completeness-of-a-binary-tree/) ## 题目大意 **描述**:给定一个二叉树的根节点 `root`。 **要求**:判断该二叉树是否是一个完全二叉树。 **说明**: - **完全二叉树**: - 树的结点数在范围 $[1, 100]$ 内。 - $1 \le Node.val \le 1000$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/15/complete-binary-tree-1.png) ```python 输入:root = [1,2,3,4,5,6] 输出:true 解释:最后一层前的每一层都是满的(即,结点值为 {1} 和 {2,3} 的两层),且最后一层中的所有结点({4,5,6})都尽可能地向左。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/15/complete-binary-tree-2.png) ```python 输入:root = [1,2,3,4,5,null,7] 输出:false 解释:值为 7 的结点没有尽可能靠向左侧。 ``` ## 解题思路 ### 思路 1:广度优先搜索 对于一个完全二叉树,按照「层序遍历」的顺序进行广度优先搜索,在遇到第一个空节点之后,整个完全二叉树的遍历就已结束了。不应该在后续遍历过程中再次出现非空节点。 如果在遍历过程中在遇到第一个空节点之后,又出现了非空节点,则该二叉树不是完全二叉树。 利用这一点,我们可以在广度优先搜索的过程中,维护一个布尔变量 `is_empty` 用于标记是否遇见了空节点。 ### 思路 1:代码 ```python class Solution: def isCompleteTree(self, root: Optional[TreeNode]) -> bool: if not root: return False queue = collections.deque([root]) is_empty = False while queue: size = len(queue) for _ in range(size): cur = queue.popleft() if not cur: is_empty = True else: if is_empty: return False queue.append(cur.left) queue.append(cur.right) return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为二叉树的节点数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0900-0999/complete-binary-tree-inserter.md ================================================ # [0919. 完全二叉树插入器](https://leetcode.cn/problems/complete-binary-tree-inserter/) - 标签:树、广度优先搜索、设计、二叉树 - 难度:中等 ## 题目链接 - [0919. 完全二叉树插入器 - 力扣](https://leetcode.cn/problems/complete-binary-tree-inserter/) ## 题目大意 要求:设计一个用完全二叉树初始化的数据结构 `CBTInserter`,并支持以下几种操作: - `CBTInserter(TreeNode root)` 使用根节点为 `root` 的给定树初始化该数据结构; - `CBTInserter.insert(int v)` 向树中插入一个新节点,节点类型为 `TreeNode`,值为 `v`。使树保持完全二叉树的状态,并返回插入的新节点的父节点的值; - `CBTInserter.get_root()` 返回树的根节点。 ## 解题思路 使用数组标记完全二叉树中节点的序号,初始化数组为 `[None]`。完全二叉树中节点的序号从 `1` 开始,对于序号为 `k` 的节点,其左子节点序号为 `2k`,右子节点的序号为 `2k + 1`,其父节点的序号为 `k // 2`。 然后在初始化和插入节点的同时,按顺序向数组中插入节点。 ## 代码 ```python class CBTInserter: def __init__(self, root: TreeNode): self.queue = [root] self.nodelist = [None] while self.queue: node = self.queue.pop(0) self.nodelist.append(node) if node.left: self.queue.append(node.left) if node.right: self.queue.append(node.right) def insert(self, v: int) -> int: self.nodelist.append(TreeNode(v)) index = len(self.nodelist) - 1 father = self.nodelist[index // 2] if index % 2 == 0: father.left = self.nodelist[-1] else: father.right = self.nodelist[-1] return father.val def get_root(self) -> TreeNode: return self.nodelist[1] ``` ================================================ FILE: docs/solutions/0900-0999/cousins-in-binary-tree.md ================================================ # [0993. 二叉树的堂兄弟节点](https://leetcode.cn/problems/cousins-in-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0993. 二叉树的堂兄弟节点 - 力扣](https://leetcode.cn/problems/cousins-in-binary-tree/) ## 题目大意 给定一个二叉树,和两个值 x,y。从二叉树中找出 x 和 y 对应的节点 node_x,node_y。如果两个节点是堂兄弟节点,则返回 True,否则返回 False。 - 堂兄弟节点:两个节点的深度相同,父节点不同。 ## 解题思路 广度优先搜索或者深度优先搜索都可。以深度优先搜索为例,递归遍历查找节点值为 x,y 的两个节点。在递归的同时,需要传入递归函数当前节点的深度和父节点信息。如果找到对应的节点,则保存两节点对应深度和父节点信息。最后判断两个节点是否是深度相同,父节点不同。如果是,则返回 True,不是则返回 False。 ## 代码 ```python class Solution: def isCousins(self, root: TreeNode, x: int, y: int) -> bool: depths = [0, 0] parents = [None, None] def dfs(node, depth, parent): if not node: return if node.val == x: depths[0] = depth parents[0] = parent elif node.val == y: depths[1] = depth parents[1] = parent dfs(node.left, depth+1, node) dfs(node.right, depth+1, node) dfs(root, 0, None) return depths[0] == depths[1] and parents[0] != parents[1] ``` ================================================ FILE: docs/solutions/0900-0999/delete-columns-to-make-sorted-ii.md ================================================ # [0955. 删列造序 II](https://leetcode.cn/problems/delete-columns-to-make-sorted-ii/) - 标签:贪心、数组、字符串 - 难度:中等 ## 题目链接 - [0955. 删列造序 II - 力扣](https://leetcode.cn/problems/delete-columns-to-make-sorted-ii/) ## 题目大意 **描述**: 给定由 $n$ 个字符串组成的数组 $strs$,其中每个字符串长度相等。 选取一个删除索引序列,对于 $strs$ 中的每个字符串,删除对应每个索引处的字符。 比如,有 $strs = ["abcdef", "uvwxyz"]$,删除索引序列 ${0, 2, 3}$,删除后 $strs$ 为 $["bef", "vyz"]$。 假设,我们选择了一组删除索引 $answer$,那么在执行删除操作之后,最终得到的数组的元素是按 字典序($strs[0] \le strs[1] \le strs[2] ... \le strs[n - 1]$)排列的。 **要求**: 返回 $answer.length$ 的最小可能值。 **说明**: - $n == strs.length$。 - $1 \le n \le 10^{3}$。 - $1 \le strs[i].length \le 10^{3}$。 - $strs[i]$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:strs = ["ca","bb","ac"] 输出:1 解释: 删除第一列后,strs = ["a", "b", "c"]。 现在 strs 中元素是按字典排列的 (即,strs[0] <= strs[1] <= strs[2])。 我们至少需要进行 1 次删除,因为最初 strs 不是按字典序排列的,所以答案是 1。 ``` - 示例 2: ```python 输入:strs = ["xc","yb","za"] 输出:0 解释: strs 的列已经是按字典序排列了,所以我们不需要删除任何东西。 注意 strs 的行不需要按字典序排列。 也就是说,strs[0][0] <= strs[0][1] <= ... 不一定成立。 ``` ## 解题思路 ### 思路 1:贪心算法 #### 思路 这道题要求删除最少的列,使得删除后的字符串数组按字典序排列。与删列造序 I 不同,这里要求的是整个字符串的字典序,而不是每一列的字典序。 我们可以使用贪心策略: 1. 维护一个数组 $sorted$,记录每一行是否已经确定了字典序关系(即前面的列已经使得该行严格小于下一行)。 2. 遍历每一列: - 检查该列是否会破坏字典序:对于未确定字典序的相邻行,如果当前列使得前一行大于后一行,则需要删除该列。 - 如果该列不需要删除,更新 $sorted$ 数组:对于未确定字典序的相邻行,如果当前列使得前一行小于后一行,则标记为已确定。 3. 返回删除的列数。 #### 代码 ```python class Solution: def minDeletionSize(self, strs: List[str]) -> int: n = len(strs) # 行数 m = len(strs[0]) # 列数 count = 0 # 需要删除的列数 # sorted[i] 表示第 i 行和第 i+1 行是否已经确定了字典序关系 sorted_rows = [False] * (n - 1) # 遍历每一列 for j in range(m): # 检查当前列是否需要删除 need_delete = False for i in range(n - 1): # 如果该行还未确定字典序,且当前列破坏了字典序 if not sorted_rows[i] and strs[i][j] > strs[i + 1][j]: need_delete = True break if need_delete: # 删除当前列 count += 1 else: # 更新已确定字典序的行 for i in range(n - 1): if strs[i][j] < strs[i + 1][j]: sorted_rows[i] = True return count ``` #### 复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是字符串数组的长度,$m$ 是每个字符串的长度。需要遍历所有字符。 - **空间复杂度**:$O(n)$,需要一个长度为 $n - 1$ 的数组记录字典序关系。 ================================================ FILE: docs/solutions/0900-0999/delete-columns-to-make-sorted-iii.md ================================================ # [0960. 删列造序 III](https://leetcode.cn/problems/delete-columns-to-make-sorted-iii/) - 标签:数组、字符串、动态规划 - 难度:困难 ## 题目链接 - [0960. 删列造序 III - 力扣](https://leetcode.cn/problems/delete-columns-to-make-sorted-iii/) ## 题目大意 **描述**: 给定由 $n$ 个小写字母字符串组成的数组 $strs$,其中每个字符串长度相等。 选取一个删除索引序列,对于 $strs$ 中的每个字符串,删除对应每个索引处的字符。 比如,有 $strs = ["abcdef","uvwxyz"]$,删除索引序列 ${0, 2, 3}$,删除后为 $["bef", "vyz"]$。 假设,我们选择了一组删除索引 $answer$,那么在执行删除操作之后,最终得到的数组的行中的「每个元素」都是按字典序排列的(即 ($strs[0][0] \le strs[0][1] \le ... \le strs[0][strs[0].length - 1]$) 和 ($strs[1][0] \le strs[1][1] \le ... \le strs[1][strs[1].length - 1]$),依此类推)。 **要求**: 请返回 $answer.length$ 的最小可能值。 **说明**: - $n == strs.length$。 - $1 \le n \le 10^{3}$。 - $1 \le strs[i].length \le 10^{3}$。 - $strs[i]$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:strs = ["babca","bbazb"] 输出:3 解释: 删除 0、1 和 4 这三列后,最终得到的数组是 strs = ["bc", "az"]。 这两行是分别按字典序排列的(即,strs[0][0] <= strs[0][1] 且 strs[1][0] <= strs[1][1])。 注意,strs[0] > strs[1] —— 数组 strs 不一定是按字典序排列的。 ``` - 示例 2: ```python 输入:strs = ["edcba"] 输出:4 解释:如果删除的列少于 4 列,则剩下的行都不会按字典序排列。 ``` ## 解题思路 ### 思路 1:动态规划 + 最长公共子序列(LCS) #### 思路 这道题要求删除最少的列,使得删除后每一行都按字典序非严格递增。这等价于保留最多的列,使得保留的列满足条件。 我们可以使用动态规划求解最长递增子序列(LIS)的变体: - 定义 $dp[j]$ 表示以第 $j$ 列结尾的最长合法列数。 - 对于每一列 $j$,检查它能否接在之前的某一列 $i$ 后面: - 如果对于所有行,都有 $strs[row][i] \le strs[row][j]$,则可以接在后面。 - $dp[j] = \max(dp[j], dp[i] + 1)$ - 最终答案为 $m - \max(dp)$,其中 $m$ 是列数。 #### 代码 ```python class Solution: def minDeletionSize(self, strs: List[str]) -> int: n = len(strs) # 行数 m = len(strs[0]) # 列数 # dp[j] 表示以第 j 列结尾的最长合法列数 dp = [1] * m # 对于每一列 j for j in range(1, m): # 检查能否接在之前的某一列 i 后面 for i in range(j): # 检查所有行是否满足 strs[row][i] <= strs[row][j] valid = True for row in range(n): if strs[row][i] > strs[row][j]: valid = False break if valid: dp[j] = max(dp[j], dp[i] + 1) # 最少删除的列数 = 总列数 - 最长合法列数 return m - max(dp) ``` #### 复杂度分析 - **时间复杂度**:$O(n \times m^2)$,其中 $n$ 是字符串数组的长度,$m$ 是每个字符串的长度。需要枚举所有列对,并检查所有行。 - **空间复杂度**:$O(m)$,需要一个长度为 $m$ 的 $dp$ 数组。 ================================================ FILE: docs/solutions/0900-0999/delete-columns-to-make-sorted.md ================================================ # [0944. 删列造序](https://leetcode.cn/problems/delete-columns-to-make-sorted/) - 标签:数组、字符串 - 难度:简单 ## 题目链接 - [0944. 删列造序 - 力扣](https://leetcode.cn/problems/delete-columns-to-make-sorted/) ## 题目大意 **描述**: 给定由 $n$ 个小写字母字符串组成的数组 $strs$,其中每个字符串长度相等。 这些字符串可以每个一行,排成一个网格。例如,$strs = [$"abc", "bce", "cae"] 可以排列为: ```python abc bce cae ``` 你需要找出并删除「不是按字典序非严格递增排列的」列。在上面的例子(下标从 0 开始)中,列 0 `('a', 'b', 'c')`和列 2 `('c', 'e', 'e')` 都是按字典序非严格递增排列的,而列 1 `('b', 'c', 'a')` 不是,所以要删除列 1。 **要求**: 返回你需要删除的列数。 **说明**: - $n == strs.length$。 - $1 \le n \le 10^{3}$。 - $1 \le strs[i].length \le 10^{3}$。 - $strs[i]$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:strs = ["cba","daf","ghi"] 输出:1 解释:网格示意如下: cba daf ghi 列 0 和列 2 按升序排列,但列 1 不是,所以只需要删除列 1 。 ``` - 示例 2: ```python 输入:strs = ["a","b"] 输出:0 解释:网格示意如下: a b 只有列 0 这一列,且已经按升序排列,所以不用删除任何列。 ``` ## 解题思路 ### 思路 1:模拟 #### 思路 这道题要求找出不按字典序非严格递增排列的列数。我们可以逐列检查,对于每一列,从上到下遍历,如果发现某个字符小于前一个字符,说明这一列不满足要求,需要删除。 1. 遍历每一列(列索引从 $0$ 到 $m - 1$,其中 $m$ 是字符串长度)。 2. 对于每一列,遍历每一行(行索引从 $1$ 到 $n - 1$,其中 $n$ 是字符串数组长度)。 3. 如果当前行的字符小于前一行的字符,说明这一列不满足递增要求,计数器加 $1$,并跳出内层循环。 4. 返回需要删除的列数。 #### 代码 ```python class Solution: def minDeletionSize(self, strs: List[str]) -> int: n = len(strs) # 行数 m = len(strs[0]) # 列数 count = 0 # 需要删除的列数 # 遍历每一列 for j in range(m): # 检查当前列是否按字典序递增 for i in range(1, n): if strs[i][j] < strs[i - 1][j]: # 当前列不满足递增要求 count += 1 break return count ``` #### 复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是字符串数组的长度,$m$ 是每个字符串的长度。需要遍历所有字符。 - **空间复杂度**:$O(1)$,只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0900-0999/di-string-match.md ================================================ # [0942. 增减字符串匹配](https://leetcode.cn/problems/di-string-match/) - 标签:贪心、数组、双指针、字符串 - 难度:简单 ## 题目链接 - [0942. 增减字符串匹配 - 力扣](https://leetcode.cn/problems/di-string-match/) ## 题目大意 **描述**: 由范围 $[0, n]$ 内所有整数组成的 $n + 1$ 个整数的排列序列可以表示为长度为 $n$ 的字符串 $s$,其中: - 如果 $perm[i] < perm[i + 1]$,那么 $s[i] == 'I'$ - 如果 $perm[i] > perm[i + 1]$,那么 $s[i] == 'D'$ 给定一个字符串 $s$。 **要求**: 重构排列 $perm$ 并返回它。如果有多个有效排列 $perm$,则返回其中任何一个。 **说明**: - $1 \le s.length \le 10^{5}$。 - $s$ 只包含字符 `"I"` 或 `"D"`。 **示例**: - 示例 1: ```python 输入:s = "IDID" 输出:[0,4,1,3,2] ``` - 示例 2: ```python 输入:s = "III" 输出:[0,1,2,3] ``` ## 解题思路 ### 思路 1:贪心算法 #### 思路 这道题要求根据字符串 $s$ 构造一个排列 $perm$,使得相邻元素的大小关系符合 $s$ 中的 `I`(递增)和 `D`(递减)。 我们可以使用贪心策略: - 维护两个指针 $low$ 和 $high$,分别指向当前可用的最小值和最大值。 - 遍历字符串 $s$: - 如果遇到 `I`,说明下一个数要比当前数大,我们选择当前最小的可用数 $low$,然后 $low$ 加 $1$。 - 如果遇到 `D`,说明下一个数要比当前数小,我们选择当前最大的可用数 $high$,然后 $high$ 减 $1$。 - 最后还剩一个数,此时 $low$ 和 $high$ 相等,将其加入结果。 #### 代码 ```python class Solution: def diStringMatch(self, s: str) -> List[int]: n = len(s) low, high = 0, n # 初始化最小值和最大值 res = [] # 遍历字符串 s for ch in s: if ch == 'I': # 递增,选择当前最小值 res.append(low) low += 1 else: # 递减,选择当前最大值 res.append(high) high -= 1 # 最后剩下一个数,low 和 high 相等 res.append(low) return res ``` #### 复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。只需要遍历一次字符串。 - **空间复杂度**:$O(1)$,不考虑结果数组的空间,只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0900-0999/distinct-subsequences-ii.md ================================================ # [0940. 不同的子序列 II](https://leetcode.cn/problems/distinct-subsequences-ii/) - 标签:字符串、动态规划 - 难度:困难 ## 题目链接 - [0940. 不同的子序列 II - 力扣](https://leetcode.cn/problems/distinct-subsequences-ii/) ## 题目大意 **描述**: 给定一个字符串 $s$。 **要求**: 计算 $s$ 的 不同非空子序列 的个数。因为结果可能很大,所以返回答案需要对 $10^9 + 7$ 取余。 **说明**: - 字符串的「子序列」是经由原字符串删除一些(也可能不删除)字符但不改变剩余字符相对位置的一个新字符串。 - 例如,`"ace"` 是 `"abcde"` 的一个子序列,但 `"aec"` 不是。 - $1 \le s.length \le 2000$。 - $s$ 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "abc" 输出:7 解释:7 个不同的子序列分别是 "a", "b", "c", "ab", "ac", "bc", 以及 "abc"。 ``` - 示例 2: ```python 输入:s = "aba" 输出:6 解释:6 个不同的子序列分别是 "a", "b", "ab", "ba", "aa" 以及 "aba"。 ``` ## 解题思路 ### 思路 1:动态规划 #### 思路 这道题要求计算字符串 $s$ 的不同非空子序列的个数。 我们可以使用动态规划: - 定义 $dp[i]$ 表示字符串前 $i$ 个字符的不同子序列个数。 - 对于第 $i$ 个字符 $s[i]$: - 如果不选择 $s[i]$,子序列个数为 $dp[i - 1]$。 - 如果选择 $s[i]$,可以将 $s[i]$ 添加到前面所有子序列的末尾,得到 $dp[i - 1]$ 个新子序列,再加上单独的 $s[i]$,共 $dp[i - 1] + 1$ 个。 - 但如果 $s[i]$ 之前出现过(在位置 $j$),那么以 $s[i]$ 结尾的子序列中,有 $dp[j - 1]$ 个是重复的,需要减去。 状态转移方程: - $dp[i] = dp[i - 1] \times 2 + 1$(不考虑重复) - 如果 $s[i]$ 在位置 $j$ 出现过,$dp[i] = dp[i - 1] \times 2 + 1 - dp[j - 1] - 1 = dp[i - 1] \times 2 - dp[j - 1]$ #### 代码 ```python class Solution: def distinctSubseqII(self, s: str) -> int: MOD = 10**9 + 7 n = len(s) # last[c] 记录字符 c 最后一次出现时的 dp 值 last = {} dp = 0 # 当前的不同子序列个数(不包括空序列) for ch in s: # 新增的子序列个数 new_dp = (dp * 2 + 1) % MOD # 如果字符之前出现过,减去重复的部分 if ch in last: new_dp = (new_dp - last[ch]) % MOD # 记录当前字符的 dp 值 last[ch] = dp + 1 dp = new_dp return dp ``` #### 复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度。只需要遍历一次字符串。 - **空间复杂度**:$O(|\Sigma|)$,其中 $|\Sigma|$ 是字符集大小(这里是 $26$)。需要哈希表记录每个字符的最后出现位置。 ================================================ FILE: docs/solutions/0900-0999/distribute-coins-in-binary-tree.md ================================================ # [0979. 在二叉树中分配硬币](https://leetcode.cn/problems/distribute-coins-in-binary-tree/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0979. 在二叉树中分配硬币 - 力扣](https://leetcode.cn/problems/distribute-coins-in-binary-tree/) ## 题目大意 **描述**: 给定一个有 $n$ 个结点的二叉树的根结点 $root$,其中树中每个结点 $node$ 都对应有 $node$.$val$ 枚硬币。整棵树上一共有 $n$ 枚硬币。 在一次移动中,我们可以选择两个相邻的结点,然后将一枚硬币从其中一个结点移动到另一个结点。移动可以是从父结点到子结点,或者从子结点移动到父结点。 **要求**: 返回使每个结点上「只有」一枚硬币所需的「最少」移动次数。 **说明**: - 树中节点的数目为 $n$。 - $1 \le n \le 10^{3}$。 - $0 \le Node.val \le n$。 - 所有 $Node.val$ 的值之和是 $n$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2019/01/18/tree1.png) ```python 输入:root = [3,0,0] 输出:2 解释:一枚硬币从根结点移动到左子结点,一枚硬币从根结点移动到右子结点。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2019/01/18/tree2.png) ```python 输入:root = [0,3,0] 输出:3 解释:将两枚硬币从根结点的左子结点移动到根结点(两次移动)。然后,将一枚硬币从根结点移动到右子结点。 ``` ## 解题思路 ### 思路 1:深度优先搜索(DFS) #### 思路 这道题要求计算使每个节点都恰好有一枚硬币所需的最少移动次数。 关键观察:对于每个节点,我们需要计算它的「盈余」或「亏损」: - 如果一个节点有 $k$ 枚硬币,它需要 $1$ 枚,那么它有 $k - 1$ 枚盈余(或 $k - 1$ 枚亏损)。 - 这些盈余或亏损需要通过父节点传递给其他节点。 我们可以使用后序遍历(先处理子节点,再处理父节点): 1. 对于每个节点,计算其左右子树的盈余/亏损。 2. 当前节点的盈余 / 亏损 = 左子树盈余 + 右子树盈余 + 当前节点硬币数 - 1。 3. 移动次数 = 所有盈余 / 亏损的绝对值之和(因为每次移动都需要经过边)。 #### 代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def distributeCoins(self, root: Optional[TreeNode]) -> int: self.moves = 0 # 记录移动次数 def dfs(node): """返回当前节点的盈余/亏损""" if not node: return 0 # 递归处理左右子树 left_surplus = dfs(node.left) right_surplus = dfs(node.right) # 计算移动次数:左右子树的盈余/亏损都需要通过当前节点传递 self.moves += abs(left_surplus) + abs(right_surplus) # 返回当前节点的盈余/亏损 # = 左子树盈余 + 右子树盈余 + 当前节点硬币数 - 1 return left_surplus + right_surplus + node.val - 1 dfs(root) return self.moves ``` #### 复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树中节点的数量。每个节点被访问一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度。递归调用栈的深度最多为树的高度。 ================================================ FILE: docs/solutions/0900-0999/equal-rational-numbers.md ================================================ # [0972. 相等的有理数](https://leetcode.cn/problems/equal-rational-numbers/) - 标签:数学、字符串 - 难度:困难 ## 题目链接 - [0972. 相等的有理数 - 力扣](https://leetcode.cn/problems/equal-rational-numbers/) ## 题目大意 **描述**: 给定两个字符串 $s$ 和 $t$,每个字符串代表一个非负有理数。 有理数 最多可以用三个部分来表示:整数部分 ``、小数非重复部分 `` 和小数重复部分 `<(><)>`。数字可以用以下三种方法之一来表示: - ` ` - 例: 0, 12 和 123 - `<.>` - 例: 0.5, 1., 2.12 和 123.0001 - `<.><(><)>` - 例: 0.1(6), 1.(9), 123.00(1212) 十进制展开的重复部分通常在一对圆括号内表示。例如: `1 / 6 = 0.16666666... = 0.1(6) = 0.1666(6) = 0.166(66)` **要求**: 只有当它们表示相同的数字时才返回 true,否则返回 false。字符串中可以使用括号来表示有理数的重复部分。 **说明**: - 每个部分仅由数字组成。 - 整数部分 `` 不会以零开头。(零本身除外) - $1 \le$ `.length` $\le 4$。 - $0 \le$ `.length` $\le 4$。 - $1 \le$ `.length` $\le 4$。 **示例**: - 示例 1: ```python 输入:s = "0.(52)", t = "0.5(25)" 输出:true 解释:因为 "0.(52)" 代表 0.52525252...,而 "0.5(25)" 代表 0.52525252525.....,则这两个字符串表示相同的数字。 ``` - 示例 2: ```python 输入:s = "0.1666(6)", t = "0.166(66)" 输出:true ``` ## 解题思路 ### 思路 1:数学 + 字符串处理 #### 思路 这道题要求判断两个有理数字符串是否相等。有理数可以表示为:整数部分 + 非重复小数部分 + 重复小数部分。 关键思路: 1. **解析字符串**:将字符串解析为整数部分、非重复部分和重复部分。 2. **转换为分数**:将有理数转换为分数形式进行比较。 - 对于重复小数,可以使用数学公式:$0.\overline{abc} = \frac{abc}{999}$,$0.d\overline{abc} = \frac{d}{10} + \frac{abc}{9990}$ 3. **比较**:将两个分数化简后比较是否相等。 由于实现较复杂,我们可以使用一个简化的方法:将有理数展开为足够长的小数(例如 $20$ 位),然后比较。 #### 代码 ```python class Solution: def isRationalEqual(self, s: str, t: str) -> bool: def parse(s): """将有理数字符串转换为浮点数""" # 查找小数点和括号 if '.' not in s: return float(s) integer_part, decimal_part = s.split('.') if '(' not in decimal_part: # 没有重复部分 return float(s) # 有重复部分 non_repeat, repeat = decimal_part.split('(') repeat = repeat.rstrip(')') # 构造足够长的小数(20 位) # 重复部分重复多次 repeat_count = (20 - len(non_repeat)) // len(repeat) + 1 full_decimal = non_repeat + repeat * repeat_count full_decimal = full_decimal[:20] # 截取 20 位 return float(integer_part + '.' + full_decimal) # 比较两个有理数(使用足够小的误差) return abs(parse(s) - parse(t)) < 1e-9 ``` #### 复杂度分析 - **时间复杂度**:$O(1)$,字符串长度有限,处理时间为常数。 - **空间复杂度**:$O(1)$,只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0900-0999/find-the-shortest-superstring.md ================================================ # [0943. 最短超级串](https://leetcode.cn/problems/find-the-shortest-superstring/) - 标签:位运算、数组、字符串、动态规划、状态压缩 - 难度:困难 ## 题目链接 - [0943. 最短超级串 - 力扣](https://leetcode.cn/problems/find-the-shortest-superstring/) ## 题目大意 **描述**: 给定一个字符串数组 $words$。 我们可以假设 $words$ 中没有字符串是 $words$ 中另一个字符串的子字符串。 **要求**: 找到以 $words$ 中每个字符串作为子字符串的最短字符串。如果有多个有效最短字符串满足题目条件,返回其中「任意一个」即可。 **说明**: - $1 \le words.length \le 12$。 - $1 \le words[i].length \le 20$。 - $words[i]$ 由小写英文字母组成。 - $words$ 中的所有字符串互不相同。 **示例**: - 示例 1: ```python 输入:words = ["alex","loves","leetcode"] 输出:"alexlovesleetcode" 解释:"alex","loves","leetcode" 的所有排列都会被接受。 ``` - 示例 2: ```python 输入:words = ["catg","ctaagt","gcta","ttca","atgcatc"] 输出:"gctaagttcatgcatc" ``` ## 解题思路 ### 思路 1:状态压缩动态规划 #### 思路 这道题要求找到包含所有字符串的最短超级串。这是一个 NP 难问题,但由于字符串数量最多只有 $12$ 个,我们可以使用状态压缩动态规划。 核心思想: 1. **预处理**:计算任意两个字符串的重叠长度 $overlap[i][j]$,表示将字符串 $j$ 接在字符串 $i$ 后面时可以节省的长度。 2. **状态压缩 DP**: - 定义 $dp[mask][i]$ 表示已经使用了 $mask$ 中的字符串,且最后一个字符串是 $i$ 时的最短长度。 - 状态转移:$dp[mask | (1 << j)][j] = \min(dp[mask | (1 << j)][j], dp[mask][i] + len(words[j]) - overlap[i][j])$ 3. **路径重建**:记录转移路径,最后重建最短超级串。 #### 代码 ```python class Solution: def shortestSuperstring(self, words: List[str]) -> str: n = len(words) # 计算重叠长度 overlap = [[0] * n for _ in range(n)] for i in range(n): for j in range(n): if i != j: # 计算 words[j] 接在 words[i] 后面的重叠长度 max_overlap = min(len(words[i]), len(words[j])) for k in range(max_overlap, 0, -1): if words[i][-k:] == words[j][:k]: overlap[i][j] = k break # 状态压缩 DP # dp[mask][i] 表示使用了 mask 中的字符串,最后一个是 i 时的最短长度 INF = float('inf') dp = [[INF] * n for _ in range(1 << n)] parent = [[-1] * n for _ in range(1 << n)] # 初始化:只使用一个字符串 for i in range(n): dp[1 << i][i] = len(words[i]) # 状态转移 for mask in range(1, 1 << n): for i in range(n): if not (mask & (1 << i)) or dp[mask][i] == INF: continue for j in range(n): if mask & (1 << j): continue new_mask = mask | (1 << j) new_len = dp[mask][i] + len(words[j]) - overlap[i][j] if new_len < dp[new_mask][j]: dp[new_mask][j] = new_len parent[new_mask][j] = i # 找到最短长度和对应的最后一个字符串 full_mask = (1 << n) - 1 min_len = INF last = -1 for i in range(n): if dp[full_mask][i] < min_len: min_len = dp[full_mask][i] last = i # 重建路径 path = [] mask = full_mask while last != -1: path.append(last) new_last = parent[mask][last] mask ^= (1 << last) last = new_last path.reverse() # 构建结果 result = words[path[0]] for i in range(1, len(path)): prev = path[i - 1] curr = path[i] result += words[curr][overlap[prev][curr]:] return result ``` #### 复杂度分析 - **时间复杂度**:$O(n^2 \times 2^n + n^2 \times L)$,其中 $n$ 是字符串数量,$L$ 是字符串的平均长度。预处理重叠需要 $O(n^2 \times L)$,DP 需要 $O(n^2 \times 2^n)$。 - **空间复杂度**:$O(n \times 2^n)$,需要存储 DP 数组和路径信息。 ================================================ FILE: docs/solutions/0900-0999/find-the-town-judge.md ================================================ # [0997. 找到小镇的法官](https://leetcode.cn/problems/find-the-town-judge/) - 标签:图、数组、哈希表 - 难度:简单 ## 题目链接 - [0997. 找到小镇的法官 - 力扣](https://leetcode.cn/problems/find-the-town-judge/) ## 题目大意 **描述**: 小镇里有 $n$ 个人,按从 1 到 $n$ 的顺序编号。传言称,这些人中有一个暗地里是小镇法官。 如果小镇法官真的存在,那么: 1. 小镇法官不会信任任何人。 2. 每个人(除了小镇法官)都信任这位小镇法官。 3. 只有一个人同时满足属性 1 和属性 2。 给你一个数组 $trust$,其中 $trust[i] = [ai, bi]$ 表示编号为 $ai$ 的人信任编号为 $bi$ 的人。 **要求**: 如果小镇法官存在并且可以确定他的身份,请返回该法官的编号;否则,返回 -1。 **说明**: - $1 \le n \le 10^{3}$。 - $0 \le trust.length \le 10^{4}$。 - $trust[i].length == 2$。 - $trust$ 中的所有 $trust[i] = [ai, bi]$ 互不相同。 - $ai \ne bi$。 - $1 \le ai, bi \le n$。 **示例**: - 示例 1: ```python 输入:n = 2, trust = [[1,2]] 输出:2 ``` - 示例 2: ```python 输入:n = 3, trust = [[1,3],[2,3]] 输出:3 ``` ## 解题思路 ### 思路 1:入度和出度统计 #### 思路 这道题可以看作一个有向图问题。法官的特点是: 1. 法官不信任任何人(出度为 $0$)。 2. 所有其他人都信任法官(入度为 $n - 1$)。 我们可以用一个数组 $degree$ 来统计每个人的「信任度」: - 如果 $a$ 信任 $b$,则 $degree[a]$ 减 $1$(出度),$degree[b]$ 加 $1$(入度)。 - 最后遍历数组,找到 $degree$ 值为 $n - 1$ 的人,即为法官。 #### 代码 ```python class Solution: def findJudge(self, n: int, trust: List[List[int]]) -> int: # degree[i] 表示 i 的信任度(入度 - 出度) degree = [0] * (n + 1) # 统计每个人的信任度 for a, b in trust: degree[a] -= 1 # a 信任别人,出度 +1 degree[b] += 1 # b 被信任,入度 +1 # 查找法官:信任度为 n - 1 的人 for i in range(1, n + 1): if degree[i] == n - 1: return i return -1 ``` #### 复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 是人数,$m$ 是信任关系的数量。需要遍历所有信任关系和所有人。 - **空间复杂度**:$O(n)$,需要一个长度为 $n + 1$ 的数组来存储信任度。 ================================================ FILE: docs/solutions/0900-0999/flip-binary-tree-to-match-preorder-traversal.md ================================================ # [0971. 翻转二叉树以匹配先序遍历](https://leetcode.cn/problems/flip-binary-tree-to-match-preorder-traversal/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0971. 翻转二叉树以匹配先序遍历 - 力扣](https://leetcode.cn/problems/flip-binary-tree-to-match-preorder-traversal/) ## 题目大意 **描述**: 给定一棵二叉树的根节点 $root$,树中有 $n$ 个节点,每个节点都有一个不同于其他节点且处于 1 到 $n$ 之间的值。 另给你一个由 $n$ 个值组成的行程序列 $voyage$,表示「预期」的二叉树「先序遍历」结果。 通过交换节点的左右子树,可以「翻转」该二叉树中的任意节点。例,翻转节点 1 的效果如下: ![](https://assets.leetcode.com/uploads/2021/02/15/fliptree.jpg) 请翻转「最少」的树中节点,使二叉树的「先序遍历」与预期的遍历行程 $voyage$ 相匹配。 **要求**: 如果可以,则返回「翻转的」所有节点的值的列表。你可以按任何顺序返回答案。如果不能,则返回列表 $[-1]$。 **说明**: - 树中的节点数目为 n。 - $n == voyage.length$。 - $1 \le n \le 10^{3}$。 - $1 \le Node.val, voyage[i] \le n$。 - 树中的所有值 互不相同。 - voyage 中的所有值 互不相同。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2019/01/02/1219-01.png) ```python 输入:root = [1,2], voyage = [2,1] 输出:[-1] 解释:翻转节点无法令先序遍历匹配预期行程。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2019/01/02/1219-02.png) ```python 输入:root = [1,2,3], voyage = [1,3,2] 输出:[1] 解释:交换节点 2 和 3 来翻转节点 1 ,先序遍历可以匹配预期行程。 ``` ## 解题思路 ### 思路 1:深度优先搜索(DFS) #### 思路 这道题要求翻转二叉树的某些节点,使得先序遍历结果与给定的 $voyage$ 匹配。 我们可以使用深度优先搜索,同时维护一个指针 $index$ 指向 $voyage$ 中当前应该匹配的位置: 1. **基本情况**:如果当前节点为空,返回 `True`。 2. **检查值**:如果当前节点的值与 $voyage[index]$ 不匹配,返回 `False`。 3. **递归处理**: - 如果左子节点存在且值与 $voyage[index + 1]$ 匹配,按正常顺序遍历(先左后右)。 - 否则,需要翻转当前节点(先右后左),并记录当前节点的值。 4. 如果任何一步失败,返回 `[-1]`。 #### 代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def flipMatchVoyage(self, root: Optional[TreeNode], voyage: List[int]) -> List[int]: self.flipped = [] # 记录翻转的节点 self.index = 0 # 当前匹配的位置 def dfs(node): if not node: return True # 检查当前节点的值是否匹配 if node.val != voyage[self.index]: return False self.index += 1 # 如果左子节点存在且值不匹配,需要翻转 if node.left and node.left.val != voyage[self.index]: # 记录翻转的节点 self.flipped.append(node.val) # 先遍历右子树,再遍历左子树 return dfs(node.right) and dfs(node.left) # 正常顺序:先左后右 return dfs(node.left) and dfs(node.right) # 如果匹配失败,返回 [-1] if dfs(root): return self.flipped else: return [-1] ``` #### 复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树中节点的数量。每个节点最多被访问一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度。递归调用栈的深度最多为树的高度。 ================================================ FILE: docs/solutions/0900-0999/flip-equivalent-binary-trees.md ================================================ # [0951. 翻转等价二叉树](https://leetcode.cn/problems/flip-equivalent-binary-trees/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [0951. 翻转等价二叉树 - 力扣](https://leetcode.cn/problems/flip-equivalent-binary-trees/) ## 题目大意 **描述**: 我们可以为二叉树 $T$ 定义一个「翻转操作」,如下所示:选择任意节点,然后交换它的左子树和右子树。 只要经过一定次数的翻转操作后,能使 $X$ 等于 $Y$,我们就称二叉树 $X$ 翻转「等价」于二叉树 $Y$。 这些树由根节点 $root1$ 和 $root2$ 给出。 **要求**: 如果两个二叉树是否是翻转「等价」的树,则返回 true,否则返回 false。 **说明**: - 每棵树节点数在 $[0, 10^{3}]$ 范围内。 - 每棵树中的每个值都是唯一的、在 $[0, 99]$ 范围内的整数。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2018/11/29/tree_ex.png) ```python 输入:root1 = [1,2,3,4,5,6,null,null,null,7,8], root2 = [1,3,2,null,6,4,5,null,null,null,null,8,7] 输出:true 解释:我们翻转值为 1,3 以及 5 的三个节点。 ``` - 示例 2: ```python 输入: root1 = [], root2 = [] 输出: true ``` ## 解题思路 ### 思路 1:深度优先搜索(DFS) #### 思路 这道题要求判断两棵二叉树是否翻转等价。翻转等价的定义是:通过若干次翻转操作(交换左右子树),可以使两棵树相同。 我们可以使用递归的方式判断: 1. **基本情况**: - 如果两个节点都为空,返回 `True`。 - 如果只有一个节点为空,或者两个节点的值不同,返回 `False`。 2. **递归情况**:对于两个节点,有两种可能: - **不翻转**:左子树对应左子树,右子树对应右子树。 - **翻转**:左子树对应右子树,右子树对应左子树。 - 只要其中一种情况成立,就返回 `True`。 #### 代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def flipEquiv(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> bool: # 基本情况:两个节点都为空 if not root1 and not root2: return True # 只有一个节点为空,或者值不同 if not root1 or not root2 or root1.val != root2.val: return False # 递归判断:不翻转或翻转 # 不翻转:左对左,右对右 no_flip = self.flipEquiv(root1.left, root2.left) and self.flipEquiv(root1.right, root2.right) # 翻转:左对右,右对左 flip = self.flipEquiv(root1.left, root2.right) and self.flipEquiv(root1.right, root2.left) return no_flip or flip ``` #### 复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树中节点的数量。每个节点最多被访问一次。 - **空间复杂度**:$O(h)$,其中 $h$ 是树的高度。递归调用栈的深度最多为树的高度。 ================================================ FILE: docs/solutions/0900-0999/flip-string-to-monotone-increasing.md ================================================ # [0926. 将字符串翻转到单调递增](https://leetcode.cn/problems/flip-string-to-monotone-increasing/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [0926. 将字符串翻转到单调递增 - 力扣](https://leetcode.cn/problems/flip-string-to-monotone-increasing/) ## 题目大意 **描述**: 如果一个二进制字符串,是以一些 0(可能没有 0)后面跟着一些 1(也可能没有 1)的形式组成的,那么该字符串是「单调递增」的。 给定一个二进制字符串 $s$,你可以将任何 0 翻转为 1 或者将 1 翻转为 0。 **要求**: 返回使 $s$ 单调递增的最小翻转次数。 **说明**: - $1 \le s.length \le 10^{5}$。 - $s[i]$ 为 `'0'` 或 `'1'`。 **示例**: - 示例 1: ```python 输入:s = "00110" 输出:1 解释:翻转最后一位得到 00111. ``` - 示例 2: ```python 输入:s = "010110" 输出:2 解释:翻转得到 011111,或者是 000111。 ``` ## 解题思路 ### 思路 1:动态规划 #### 思路 这道题要求将二进制字符串翻转为单调递增(先 $0$ 后 $1$)的最小翻转次数。 我们可以使用动态规划: - 定义 $dp0$ 表示当前位置结尾为 $0$ 的最小翻转次数。 - 定义 $dp1$ 表示当前位置结尾为 $1$ 的最小翻转次数。 状态转移: - 如果当前字符是 `'0'`: - $dp0$ 不变(不需要翻转)。 - $dp1 = dp1 + 1$(需要将 `'0'` 翻转为 `'1'`)。 - 如果当前字符是 `'1'`: - $dp0 = dp0 + 1$(需要将 `'1'` 翻转为 `'0'`,但这会破坏单调性,所以实际上不能再添加 $0$)。 - $dp1 = \min(dp0, dp1)$(可以保持 `'1'` 不变,或者从前面的 $0$ 序列转换过来)。 最终答案是 $\min(dp0, dp1)$,但由于单调递增的字符串可以全是 $0$ 或全是 $1$,所以答案是 $dp1$。 #### 代码 ```python class Solution: def minFlipsMonoIncr(self, s: str) -> int: dp0 = 0 # 以 0 结尾的最小翻转次数 dp1 = 0 # 以 1 结尾的最小翻转次数 for ch in s: if ch == '0': # 当前是 0,保持 0 不需要翻转,变成 1 需要翻转 dp1 = min(dp0, dp1) + 1 # dp0 不变 else: # 当前是 1,变成 0 需要翻转(但会破坏单调性),保持 1 不需要翻转 dp1 = min(dp0, dp1) dp0 = dp0 + 1 return min(dp0, dp1) ``` #### 复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度。只需要遍历一次字符串。 - **空间复杂度**:$O(1)$,只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0900-0999/fruit-into-baskets.md ================================================ # [0904. 水果成篮](https://leetcode.cn/problems/fruit-into-baskets/) - 标签:数组、哈希表、滑动窗口 - 难度:中等 ## 题目链接 - [0904. 水果成篮 - 力扣](https://leetcode.cn/problems/fruit-into-baskets/) ## 题目大意 给定一个数组 `fruits`。其中 `fruits[i]` 表示第 `i` 棵树会产生 `fruits[i]` 型水果。 你可以从你选择的任何树开始,然后重复执行以下步骤: - 把这棵树上的水果放进你的篮子里。如果你做不到,就停下来。 - 移动到当前树右侧的下一棵树。如果右边没有树,就停下来。 - 请注意,在选择一棵树后,你没有任何选择:你必须执行步骤 1,然后执行步骤 2,然后返回步骤 1,然后执行步骤 2,依此类推,直至停止。 你有 `2` 个篮子,每个篮子可以携带任何数量的水果,但你希望每个篮子只携带一种类型的水果。 要求:返回你能收集的水果树的最大总量。 ## 解题思路 只有 `2` 个篮子,要求在连续子数组中装最多 `2` 种不同水果。可以理解为维护一个水果种类数为 `2` 的滑动数组,求窗口中最大的水果树数目。具体做法如下: - 用滑动窗口 `window` 来维护不同种类水果树数目。`window` 为哈希表类型。`ans` 用来维护能收集的水果树的最大总量。设定两个指针:`left`、`right`,分别指向滑动窗口的左右边界,保证窗口中水果种类数不超过 `2` 种。 - 一开始,`left`、`right` 都指向 `0`。 - 将最右侧数组元素 `fruits[right]` 加入当前窗口 `window` 中,该水果树数目 +1。 - 如果该窗口中该水果树种类多于 `2` 种,即 `len(window) > 2`,则不断右移 `left`,缩小滑动窗口长度,并更新窗口中对应水果树的个数,直到 `len(window) <= 2`。 - 维护更新能收集的水果树的最大总量。然后右移 `right`,直到 `right >= len(fruits)` 结束。 - 输出能收集的水果树的最大总量。 ## 代码 ```python class Solution: def totalFruit(self, fruits: List[int]) -> int: window = dict() window_size = 2 ans = 0 left, right = 0, 0 while right < len(fruits): if fruits[right] in window: window[fruits[right]] += 1 else: window[fruits[right]] = 1 while len(window) > window_size: window[fruits[left]] -= 1 if window[fruits[left]] == 0: del window[fruits[left]] left += 1 ans = max(ans, right - left + 1) right += 1 return ans ``` ================================================ FILE: docs/solutions/0900-0999/index.md ================================================ ## 本章内容 - [0900. RLE 迭代器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/rle-iterator.md) - [0901. 股票价格跨度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/online-stock-span.md) - [0902. 最大为 N 的数字组合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/numbers-at-most-n-given-digit-set.md) - [0903. DI 序列的有效排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/valid-permutations-for-di-sequence.md) - [0904. 水果成篮](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/fruit-into-baskets.md) - [0905. 按奇偶排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-array-by-parity.md) - [0906. 超级回文数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/super-palindromes.md) - [0907. 子数组的最小值之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sum-of-subarray-minimums.md) - [0908. 最小差值 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/smallest-range-i.md) - [0909. 蛇梯棋](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/snakes-and-ladders.md) - [0910. 最小差值 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/smallest-range-ii.md) - [0911. 在线选举](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/online-election.md) - [0912. 排序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-an-array.md) - [0913. 猫和老鼠](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/cat-and-mouse.md) - [0914. 卡牌分组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/x-of-a-kind-in-a-deck-of-cards.md) - [0915. 分割数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/partition-array-into-disjoint-intervals.md) - [0916. 单词子集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/word-subsets.md) - [0917. 仅仅反转字母](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/reverse-only-letters.md) - [0918. 环形子数组的最大和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/maximum-sum-circular-subarray.md) - [0919. 完全二叉树插入器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/complete-binary-tree-inserter.md) - [0920. 播放列表的数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/number-of-music-playlists.md) - [0921. 使括号有效的最少添加](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-add-to-make-parentheses-valid.md) - [0922. 按奇偶排序数组 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sort-array-by-parity-ii.md) - [0923. 三数之和的多种可能](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/3sum-with-multiplicity.md) - [0924. 尽量减少恶意软件的传播](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimize-malware-spread.md) - [0925. 长按键入](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/long-pressed-name.md) - [0926. 将字符串翻转到单调递增](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/flip-string-to-monotone-increasing.md) - [0927. 三等分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/three-equal-parts.md) - [0928. 尽量减少恶意软件的传播 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimize-malware-spread-ii.md) - [0929. 独特的电子邮件地址](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/unique-email-addresses.md) - [0930. 和相同的二元子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/binary-subarrays-with-sum.md) - [0931. 下降路径最小和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-falling-path-sum.md) - [0932. 漂亮数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/beautiful-array.md) - [0933. 最近的请求次数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/number-of-recent-calls.md) - [0934. 最短的桥](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/shortest-bridge.md) - [0935. 骑士拨号器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/knight-dialer.md) - [0936. 戳印序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/stamping-the-sequence.md) - [0937. 重新排列日志文件](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/reorder-data-in-log-files.md) - [0938. 二叉搜索树的范围和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/range-sum-of-bst.md) - [0939. 最小面积矩形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-area-rectangle.md) - [0940. 不同的子序列 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/distinct-subsequences-ii.md) - [0941. 有效的山脉数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/valid-mountain-array.md) - [0942. 增减字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/di-string-match.md) - [0943. 最短超级串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/find-the-shortest-superstring.md) - [0944. 删列造序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/delete-columns-to-make-sorted.md) - [0945. 使数组唯一的最小增量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-increment-to-make-array-unique.md) - [0946. 验证栈序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/validate-stack-sequences.md) - [0947. 移除最多的同行或同列石头](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/most-stones-removed-with-same-row-or-column.md) - [0948. 令牌放置](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/bag-of-tokens.md) - [0949. 给定数字能组成的最大时间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/largest-time-for-given-digits.md) - [0950. 按递增顺序显示卡牌](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/reveal-cards-in-increasing-order.md) - [0951. 翻转等价二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/flip-equivalent-binary-trees.md) - [0952. 按公因数计算最大组件大小](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/largest-component-size-by-common-factor.md) - [0953. 验证外星语词典](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/verifying-an-alien-dictionary.md) - [0954. 二倍数对数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/array-of-doubled-pairs.md) - [0955. 删列造序 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/delete-columns-to-make-sorted-ii.md) - [0956. 最高的广告牌](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/tallest-billboard.md) - [0957. N 天后的牢房](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/prison-cells-after-n-days.md) - [0958. 二叉树的完全性检验](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/check-completeness-of-a-binary-tree.md) - [0959. 由斜杠划分区域](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/regions-cut-by-slashes.md) - [0960. 删列造序 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/delete-columns-to-make-sorted-iii.md) - [0961. 在长度 2N 的数组中找出重复 N 次的元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/n-repeated-element-in-size-2n-array.md) - [0962. 最大宽度坡](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/maximum-width-ramp.md) - [0963. 最小面积矩形 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-area-rectangle-ii.md) - [0964. 表示数字的最少运算符](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/least-operators-to-express-number.md) - [0965. 单值二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/univalued-binary-tree.md) - [0966. 元音拼写检查器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/vowel-spellchecker.md) - [0967. 连续差相同的数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/numbers-with-same-consecutive-differences.md) - [0968. 监控二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/binary-tree-cameras.md) - [0969. 煎饼排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/pancake-sorting.md) - [0970. 强整数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/powerful-integers.md) - [0971. 翻转二叉树以匹配先序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/flip-binary-tree-to-match-preorder-traversal.md) - [0972. 相等的有理数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/equal-rational-numbers.md) - [0973. 最接近原点的 K 个点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/k-closest-points-to-origin.md) - [0974. 和可被 K 整除的子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/subarray-sums-divisible-by-k.md) - [0975. 奇偶跳](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/odd-even-jump.md) - [0976. 三角形的最大周长](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/largest-perimeter-triangle.md) - [0977. 有序数组的平方](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/squares-of-a-sorted-array.md) - [0978. 最长湍流子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/longest-turbulent-subarray.md) - [0979. 在二叉树中分配硬币](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/distribute-coins-in-binary-tree.md) - [0980. 不同路径 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/unique-paths-iii.md) - [0981. 基于时间的键值存储](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/time-based-key-value-store.md) - [0982. 按位与为零的三元组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/triples-with-bitwise-and-equal-to-zero.md) - [0983. 最低票价](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-cost-for-tickets.md) - [0984. 不含 AAA 或 BBB 的字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/string-without-aaa-or-bbb.md) - [0985. 查询后的偶数和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/sum-of-even-numbers-after-queries.md) - [0986. 区间列表的交集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/interval-list-intersections.md) - [0987. 二叉树的垂序遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/vertical-order-traversal-of-a-binary-tree.md) - [0988. 从叶结点开始的最小字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/smallest-string-starting-from-leaf.md) - [0989. 数组形式的整数加法](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/add-to-array-form-of-integer.md) - [0990. 等式方程的可满足性](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/satisfiability-of-equality-equations.md) - [0991. 坏了的计算器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/broken-calculator.md) - [0992. K 个不同整数的子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/subarrays-with-k-different-integers.md) - [0993. 二叉树的堂兄弟节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/cousins-in-binary-tree.md) - [0994. 腐烂的橘子](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/rotting-oranges.md) - [0995. K 连续位的最小翻转次数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/minimum-number-of-k-consecutive-bit-flips.md) - [0996. 平方数组的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/number-of-squareful-arrays.md) - [0997. 找到小镇的法官](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/find-the-town-judge.md) - [0998. 最大二叉树 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/maximum-binary-tree-ii.md) - [0999. 可以被一步捕获的棋子数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/available-captures-for-rook.md) ================================================ FILE: docs/solutions/0900-0999/interval-list-intersections.md ================================================ # [0986. 区间列表的交集](https://leetcode.cn/problems/interval-list-intersections/) - 标签:数组、双指针、扫描线 - 难度:中等 ## 题目链接 - [0986. 区间列表的交集 - 力扣](https://leetcode.cn/problems/interval-list-intersections/) ## 题目大意 **描述**: 给定两个由一些「闭区间」组成的列表,$firstList$ 和 $secondList$,其中 $firstList[i] = [start_i, end_i]$ 而 $secondList[j] = [start_j, end_j]$。每个区间列表都是成对「不相交」的,并且「已经排序」。 **要求**: 返回这 两个区间列表的交集 。 **说明**: - 形式上,闭区间 $[a, b]$(其中 $a \le b$)表示实数 $x$ 的集合,而 $a \le x \le b$。 - 两个闭区间的「交集」是一组实数,要么为空集,要么为闭区间。例如,$[1, 3]$ 和 $[2, 4]$ 的交集为 $[2, 3]$。 - $0 \le firstList.length, secondList.length \le 10^{3}$。 - $firstList.length + secondList.length \ge 1$。 - $0 \le start_i \lt end_i \le 10^{9}$。 - $end_i \lt start_i+1$。 - $0 \le start_j \lt end_j \le 10^{9}$。 - $end_j \lt start_j+1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2019/01/30/interval1.png) ```python 输入:firstList = [[0,2],[5,10],[13,23],[24,25]], secondList = [[1,5],[8,12],[15,24],[25,26]] 输出:[[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]] ``` - 示例 2: ```python 输入:firstList = [[1,3],[5,9]], secondList = [] 输出:[] ``` ## 解题思路 ### 思路 1:双指针 #### 思路 这道题要求找到两个区间列表的交集。我们可以使用双指针分别遍历两个列表: 1. 初始化两个指针 $i$ 和 $j$,分别指向 $firstList$ 和 $secondList$ 的起始位置。 2. 对于当前的两个区间 $[start1, end1]$ 和 $[start2, end2]$: - 计算交集:$[\max(start1, start2), \min(end1, end2)]$。 - 如果交集有效(即 $\max(start1, start2) \le \min(end1, end2)$),将其加入结果。 - 移动指针:如果 $end1 < end2$,说明第一个区间已经处理完,移动 $i$;否则移动 $j$。 3. 返回所有交集区间。 #### 代码 ```python class Solution: def intervalIntersection(self, firstList: List[List[int]], secondList: List[List[int]]) -> List[List[int]]: res = [] i, j = 0, 0 # 双指针遍历两个列表 while i < len(firstList) and j < len(secondList): start1, end1 = firstList[i] start2, end2 = secondList[j] # 计算交集 start = max(start1, start2) end = min(end1, end2) # 如果交集有效,加入结果 if start <= end: res.append([start, end]) # 移动指针:结束时间较早的区间已经处理完 if end1 < end2: i += 1 else: j += 1 return res ``` #### 复杂度分析 - **时间复杂度**:$O(m + n)$,其中 $m$ 和 $n$ 分别是两个列表的长度。每个区间最多被访问一次。 - **空间复杂度**:$O(1)$,不考虑结果数组的空间,只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0900-0999/k-closest-points-to-origin.md ================================================ # [0973. 最接近原点的 K 个点](https://leetcode.cn/problems/k-closest-points-to-origin/) - 标签:几何、数组、数学、分治、快速选择、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [0973. 最接近原点的 K 个点 - 力扣](https://leetcode.cn/problems/k-closest-points-to-origin/) ## 题目大意 给定一个由由平面上的点组成的列表 `points`,再给定一个整数 `K`。 要求:从中找出 `K` 个距离原点` (0, 0)` 最近的点。(这里,平面上两点之间的距离是欧几里德距离。)可以按任何顺序返回答案。除了点坐标的顺序之外,答案确保是唯一的。 ## 解题思路 1. 使用二叉堆构建优先队列,优先级为距离原点的距离。此时堆顶元素即为距离原点最近的元素。 2. 将堆顶元素加入到答案数组中,进行出队操作。时间复杂度 $O(log{n})$。 - 出队操作:交换堆顶元素与末尾元素,将末尾元素已移出堆。继续调整大顶堆。 3. 不断重复第 2 步,直到 `K` 次结束。 ## 代码 ```python class Heapq: def compare(self, a, b): dist_a = a[0] * a[0] + a[1] * a[1] dist_b = b[0] * b[0] + b[1] * b[1] if dist_a < dist_b: return -1 elif dist_a == dist_b: return 0 else: return 1 # 堆调整方法:调整为小顶堆 def heapAdjust(self, nums: [int], index: int, end: int): left = index * 2 + 1 right = left + 1 while left <= end: # 当前节点为非叶子结点 max_index = index if self.compare(nums[left], nums[max_index]) == -1: max_index = left if right <= end and self.compare(nums[right], nums[max_index]) == -1: max_index = right if index == max_index: # 如果不用交换,则说明已经交换结束 break nums[index], nums[max_index] = nums[max_index], nums[index] # 继续调整子树 index = max_index left = index * 2 + 1 right = left + 1 # 将数组构建为二叉堆 def heapify(self, nums: [int]): size = len(nums) # (size - 2) // 2 是最后一个非叶节点,叶节点不用调整 for i in range((size - 2) // 2, -1, -1): # 调用调整堆函数 self.heapAdjust(nums, i, size - 1) # 入队操作 def heappush(self, nums: list, value): nums.append(value) size = len(nums) i = size - 1 # 寻找插入位置 while (i - 1) // 2 >= 0: cur_root = (i - 1) // 2 # value 大于当前根节点,则插入到当前位置 if self.compare(nums[cur_root], value) == -1: break # 继续向上查找 nums[i] = nums[cur_root] i = cur_root # 找到插入位置或者到达根位置,将其插入 nums[i] = value # 出队操作 def heappop(self, nums: list) -> int: size = len(nums) nums[0], nums[-1] = nums[-1], nums[0] # 得到最小值(堆顶元素)然后调整堆 top = nums.pop() if size > 0: self.heapAdjust(nums, 0, size - 2) return top # 升序堆排序 def heapSort(self, nums: [int]): self.heapify(nums) size = len(nums) for i in range(size): nums[0], nums[size - i - 1] = nums[size - i - 1], nums[0] self.heapAdjust(nums, 0, size - i - 2) return nums class Solution: def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]: heap = Heapq() queue = [] for point in points: heap.heappush(queue, point) res = [] for i in range(k): res.append(heap.heappop(queue)) return res ``` ================================================ FILE: docs/solutions/0900-0999/knight-dialer.md ================================================ # [0935. 骑士拨号器](https://leetcode.cn/problems/knight-dialer/) - 标签:动态规划 - 难度:中等 ## 题目链接 - [0935. 骑士拨号器 - 力扣](https://leetcode.cn/problems/knight-dialer/) ## 题目大意 **描述**:象棋骑士可以垂直移动两个方格,水平移动一个方格,或者水平移动两个方格,垂直移动一个方格(两者都形成一个 $L$ 的形状),如下图所示。 ![](https://assets.leetcode.com/uploads/2020/08/18/chess.jpg) 现在我们有一个象棋其实和一个电话垫,如下图所示,骑士只能站在一个数字单元格上($0 \sim 9$)。 ![](https://assets.leetcode.com/uploads/2020/08/18/phone.jpg) 现在给定一个整数 $n$。 **要求**:返回我们可以拨多少个长度为 $n$ 的不同电话号码。因为答案可能很大,所以最终答案需要对 $10^9 + 7$ 进行取模。 **说明**: - 可以将骑士放在任何数字单元格上,然后执行 $n - 1$ 次移动来获得长度为 $n$ 的电话号码。 - $1 \le n \le 5000$。 **示例**: - 示例 1: ```python 输入:n = 1 输出:10 解释:我们需要拨一个长度为1的数字,所以把骑士放在10个单元格中的任何一个数字单元格上都能满足条件。 ``` - 示例 2: ```python 输入:n = 2 输出:20 解释:我们可以拨打的所有有效号码为[04, 06, 16, 18, 27, 29, 34, 38, 40, 43, 49, 60, 61, 67, 72, 76, 81, 83, 92, 94] ``` ## 解题思路 ### 思路 1:动态规划 根据象棋骑士的跳跃规则,以及电话键盘的样式,我们可以预先处理一下象棋骑士当前位置与下一步能跳跃到的位置关系,将其存入哈希表中,方便查询。 接下来我们可以用动态规划的方式,计算出跳跃 $n - 1$ 次总共能得到多少个长度为 $n$ 的不同电话号码。 ###### 1. 阶段划分 按照步数、所处数字位置进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][v]$ 表示为:第 $i$ 步到达键位 $u$ 总共能到的长度为 $i + 1$ 的不同电话号码个数。 ###### 3. 状态转移方程 第 $i$ 步到达键位 $v$ 所能得到的不同电话号码个数,取决于 $i - 1$ 步中所有能到达 $v$ 的键位 $u$ 的不同电话号码个数总和。 呢状态转移方程为:$dp[i][v] = \sum dp[i - 1][u]$(可以从 $u$ 跳到 $v$)。 ###### 4. 初始条件 - 第 $0$ 步(位于开始位置)所能得到的电话号码个数为 $1$,因为开始时可以将骑士放在任何数字单元格上,所以所有的 $dp[0][v] = 1$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][v]$ 表示为:第 $i$ 步到达键位 $u$ 总共能到的长度为 $i + 1$ 的不同电话号码个数。 所以最终结果为第 $n - 1$ 行所有的 $dp[n - 1][v]$ 的总和。 ### 思路 1:代码 ```python class Solution: def knightDialer(self, n: int) -> int: graph = { 0: [4, 6], 1: [6, 8], 2: [7, 9], 3: [4, 8], 4: [0, 3, 9], 5: [], 6: [0, 1, 7], 7: [2, 6], 8: [1, 3], 9: [2, 4] } MOD = 10 ** 9 + 7 dp = [[0 for _ in range(10)] for _ in range(n)] for v in range(10): dp[0][v] = 1 for i in range(1, n): for u in range(10): for v in graph[u]: dp[i][v] = (dp[i][v] + dp[i - 1][u]) % MOD return sum(dp[n - 1]) % MOD ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 10)$,其中 $n$ 为给定整数。 - **空间复杂度**:$O(n \times 10)$。 ================================================ FILE: docs/solutions/0900-0999/largest-component-size-by-common-factor.md ================================================ # [0952. 按公因数计算最大组件大小](https://leetcode.cn/problems/largest-component-size-by-common-factor/) - 标签:并查集、数组、哈希表、数学、数论 - 难度:困难 ## 题目链接 - [0952. 按公因数计算最大组件大小 - 力扣](https://leetcode.cn/problems/largest-component-size-by-common-factor/) ## 题目大意 **描述**: 给定一个由不同正整数的组成的非空数组 $nums$,考虑下面的图: - 有 $nums.length$ 个节点,按从 $nums[0]$ 到 $nums[nums.length - 1]$ 标记; - 只有当 $nums[i]$ 和 $nums[j]$ 共用一个大于 1 的公因数时,$nums[i]$ 和 $nums[j]$ 之间才有一条边。 **要求**: 返回「图中最大连通组件的大小」。 **说明**: - $1 \le nums.length \le 2 * 10^{4}$。 - $1 \le nums[i] \le 10^{5}$。 - $nums$ 中所有值都不同。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2018/12/01/ex1.png) ```python 输入:nums = [4,6,15,35] 输出:4 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2018/12/01/ex2.png) ```python 输入:nums = [20,50,9,63] 输出:2 ``` ## 解题思路 ### 思路 1:并查集 #### 思路 这道题要求找到图中最大连通组件的大小,其中两个数如果有大于 $1$ 的公因数,则它们之间有边。 直接判断两两之间的公因数会超时。我们可以通过 **质因数分解** 来优化: - 如果两个数有公因数 $p$,那么它们都能被 $p$ 整除。 - 我们可以将每个数分解为质因数,然后将所有包含相同质因数的数连接起来。 使用并查集: 1. 对每个数进行质因数分解。 2. 将每个数与其所有质因数连接(使用并查集的 $union$ 操作)。 3. 统计每个连通分量的大小,返回最大值。 为了避免直接连接数字和质因数(范围不同),我们可以用一个哈希表记录每个质因数第一次出现时对应的数字,后续出现相同质因数的数字都与这个数字连接。 #### 代码 ```python class Solution: def largestComponentSize(self, nums: List[int]) -> int: # 并查集 parent = {} def find(x): if x not in parent: parent[x] = x if parent[x] != x: parent[x] = find(parent[x]) return parent[x] def union(x, y): root_x = find(x) root_y = find(y) if root_x != root_y: parent[root_x] = root_y # 质因数分解 def get_prime_factors(n): factors = [] # 处理因子 2 if n % 2 == 0: factors.append(2) while n % 2 == 0: n //= 2 # 处理奇数因子 i = 3 while i * i <= n: if n % i == 0: factors.append(i) while n % i == 0: n //= i i += 2 # 如果 n 是质数 if n > 2: factors.append(n) return factors # prime_to_num[p] 记录质因数 p 第一次出现时对应的数字 prime_to_num = {} # 对每个数进行质因数分解,并连接 for num in nums: primes = get_prime_factors(num) for prime in primes: if prime in prime_to_num: union(num, prime_to_num[prime]) else: prime_to_num[prime] = num # 统计每个连通分量的大小 from collections import Counter count = Counter(find(num) for num in nums) return max(count.values()) ``` #### 复杂度分析 - **时间复杂度**:$O(n \sqrt{m})$,其中 $n$ 是数组长度,$m$ 是数组中的最大值。每个数的质因数分解需要 $O(\sqrt{m})$ 时间。 - **空间复杂度**:$O(n + k)$,其中 $k$ 是不同质因数的数量。需要并查集和哈希表存储。 ================================================ FILE: docs/solutions/0900-0999/largest-perimeter-triangle.md ================================================ # [0976. 三角形的最大周长](https://leetcode.cn/problems/largest-perimeter-triangle/) - 标签:贪心、数组、数学、排序 - 难度:简单 ## 题目链接 - [0976. 三角形的最大周长 - 力扣](https://leetcode.cn/problems/largest-perimeter-triangle/) ## 题目大意 **描述**:给定一些由正数(代表长度)组成的数组 `nums`。 **要求**:返回由其中 `3` 个长度组成的、面积不为 `0` 的三角形的最大周长。如果不能形成任何面积不为 `0` 的三角形,则返回 `0`。 **说明**: - $3 \le nums.length \le 10^4$。 - $1 \le nums[i] \le 10^6$。 **示例**: - 示例 1: ```python 输入:nums = [2,1,2] 输出:5 解释:长度为 2, 1, 2 的边组成的三角形周长为 5,为最大周长 ``` ## 解题思路 ### 思路 1: 要想三角形的周长最大,则每一条边都要尽可能的长,并且还要满足三角形的边长条件,即 `a + b > c`,其中 `a`、`b`、`c` 分别是三角形的 `3` 条边长。 所以,我们可以先对所有边长进行排序。然后倒序枚举最长边 `nums[i]`,判断前两个边长相加是否大于最长边,即 `nums[i - 2] + nums[i - 1] > nums[i]`。如果满足,则返回 `3` 条边长的和,否则的话继续枚举最长边。 ## 代码 ### 思路 1 代码: ```python class Solution: def largestPerimeter(self, nums: List[int]) -> int: nums.sort() for i in range(len(nums) - 1, 1, -1): if nums[i - 2] + nums[i - 1] > nums[i]: return nums[i - 2] + nums[i - 1] + nums[i] return 0 ``` ================================================ FILE: docs/solutions/0900-0999/largest-time-for-given-digits.md ================================================ # [0949. 给定数字能组成的最大时间](https://leetcode.cn/problems/largest-time-for-given-digits/) - 标签:数组、字符串、回溯、枚举 - 难度:中等 ## 题目链接 - [0949. 给定数字能组成的最大时间 - 力扣](https://leetcode.cn/problems/largest-time-for-given-digits/) ## 题目大意 **描述**: 24 小时格式为 `"HH:MM"`,其中 $HH$ 在 00 到 23 之间,$MM$ 在 00 到 59 之间。最小的 24 小时制时间是 `00:00`,而最大的是 `23:59`。从 `00:00` (午夜)开始算起,过得越久,时间越大。 给定一个由 4 位数字组成的数组。 **要求**: 返回可以设置的符合 24 小时制的最大时间。 长度为 5 的字符串,按 `"HH:MM"` 格式返回答案。如果不能确定有效时间,则返回空字符串。 **说明**: - $arr.length == 4$。 - $0 \le arr[i] \le 9$。 **示例**: - 示例 1: ```python 输入:arr = [1,2,3,4] 输出:"23:41" 解释:有效的 24 小时制时间是 "12:34","12:43","13:24","13:42","14:23","14:32","21:34","21:43","23:14" 和 "23:41" 。这些时间中,"23:41" 是最大时间。 ``` - 示例 2: ```python 输入:arr = [5,5,5,5] 输出:"" 解释:不存在有效的 24 小时制时间,因为 "55:55" 无效。 ``` ## 解题思路 ### 思路 1:枚举 #### 思路 这道题要求用 $4$ 个数字组成符合 $24$ 小时制的最大时间。由于只有 $4$ 个数字,我们可以枚举所有可能的排列(共 $4! = 24$ 种),然后检查每个排列是否能组成有效的时间,并记录最大的时间。 有效时间的条件: - 小时部分:$00 \sim 23$,即第一位 $\le 2$,如果第一位是 $2$,第二位 $\le 3$。 - 分钟部分:$00 \sim 59$,即第三位 $\le 5$。 #### 代码 ```python class Solution: def largestTimeFromDigits(self, arr: List[int]) -> str: from itertools import permutations max_time = -1 # 记录最大时间(用分钟数表示) # 枚举所有排列 for perm in permutations(arr): hour = perm[0] * 10 + perm[1] minute = perm[2] * 10 + perm[3] # 检查是否是有效时间 if hour < 24 and minute < 60: # 转换为分钟数进行比较 time_in_minutes = hour * 60 + minute max_time = max(max_time, time_in_minutes) # 如果没有有效时间,返回空字符串 if max_time == -1: return "" # 将分钟数转换为时间格式 hour = max_time // 60 minute = max_time % 60 return f"{hour:02d}:{minute:02d}" ``` #### 复杂度分析 - **时间复杂度**:$O(1)$,因为数组长度固定为 $4$,排列数固定为 $24$。 - **空间复杂度**:$O(1)$,只使用了常数个额外变量。 ================================================ FILE: docs/solutions/0900-0999/least-operators-to-express-number.md ================================================ # [0964. 表示数字的最少运算符](https://leetcode.cn/problems/least-operators-to-express-number/) - 标签:记忆化搜索、数学、动态规划 - 难度:困难 ## 题目链接 - [0964. 表示数字的最少运算符 - 力扣](https://leetcode.cn/problems/least-operators-to-express-number/) ## 题目大意 **描述**: 给定一个正整数 $x$,我们将会写出一个形如 $x (op1) x (op2) x (op3) x ...$ 的表达式,其中每个运算符 $op1, op2, …$ 可以是加、减、乘、除(`+`,`-`,`*`,或是 `/`)之一。例如,对于 $x = 3$,我们可以写出表达式 $3 * 3 / 3 + 3 - 3$,该式的值为 3。 在写这样的表达式时,我们需要遵守下面的惯例: - 除运算符(`/`)返回有理数。 - 任何地方都没有括号。 - 我们使用通常的操作顺序:乘法和除法发生在加法和减法之前。 - 不允许使用一元否定运算符(`-`)。例如,$x - x$ 是一个有效的表达式,因为它只使用减法,但是 $-x + x$ 不是,因为它使用了否定运算符。 我们希望编写一个能使表达式等于给定的目标值 $target$ 且运算符最少的表达式。 **要求**: 返回所用运算符的最少数量。 **说明**: - $2 \le x \le 10^{3}$。 - $1 \le target \le 2 * 10^{8}$。 **示例**: - 示例 1: ```python 输入:x = 3, target = 19 输出:5 解释:3 * 3 + 3 * 3 + 3 / 3 。表达式包含 5 个运算符。 ``` - 示例 2: ```python 输入:x = 5, target = 501 输出:8 解释:5 * 5 * 5 * 5 - 5 * 5 * 5 + 5 / 5 。表达式包含 8 个运算符。 ``` ## 解题思路 ### 思路 1:记忆化搜索 + 动态规划 #### 思路 这道题要求用最少的运算符表示目标数字 $target$。我们可以将 $target$ 看作 $x$ 进制数,每一位可以是 $0$ 到 $x$(允许进位)。 **关键观察**: - $x^0 = 1$ 需要 $x / x$,即 $2$ 个运算符(一个除号一个乘号,但连接时算作 $2$ 个代价)。 - $x^1 = x$ 不需要运算符(直接使用 $x$,但连接时需要 $1$ 个加号/减号)。 - $x^i$ 需要 $i$ 个运算符($i - 1$ 个乘号 + $1$ 个加号/减号用于连接)。 **状态定义**: - 定义 $dp(i, target)$ 表示用 $x^i$ 及更高次幂表示 $target$ 的最少运算符数。 - 对于每个位置 $i$,计算 $target$ 除以 $x$ 的商 $q$ 和余数 $r$: - **方案 1**:使用 $r$ 个 $x^i$,然后递归处理 $q$。 - **方案 2**:使用 $(x - r)$ 个 $-x^i$(相当于凑到下一个 $x^{i+1}$),然后递归处理 $q + 1$。 **代价计算**: - 预先计算 $cost[i]$,表示使用一个 $x^i$ 需要的运算符数: - $cost[0] = 2$($x / x$ 需要 $2$ 个运算符) - $cost[i] = i$($i \ge 1$ 时,$x^i$ 需要 $i$ 个运算符) #### 代码 ```python class Solution: def leastOpsExpressTarget(self, x: int, target: int) -> int: from functools import lru_cache # cost[i] 表示使用一个 x^i 需要的运算符数 # cost[0] = 2 (x/x),cost[i] = i (i >= 1) cost = [2] + list(range(1, 40)) @lru_cache(None) def dp(i, targ): """用 x^i 及更高次幂表示 targ 的最少运算符数""" # 基本情况 if targ == 0: return 0 if targ == 1: return cost[i] if i >= 39: # 防止溢出 return float('inf') # 计算 targ 除以 x 的商和余数 quotient, remainder = divmod(targ, x) # 方案 1:使用 remainder 个 x^i,然后处理 quotient # 每个 x^i 需要 cost[i] 个运算符 ans1 = remainder * cost[i] + dp(i + 1, quotient) # 方案 2:使用 (x - remainder) 个 -x^i,然后处理 quotient + 1 # 相当于凑到下一个 x^(i+1) ans2 = (x - remainder) * cost[i] + dp(i + 1, quotient + 1) return min(ans1, ans2) # 从 x^0 开始,最后减 1 是因为最外层不需要加号 return dp(0, target) - 1 ``` #### 复杂度分析 - **时间复杂度**:$O(\log_x target \times \log target)$,递归深度最多为 $O(\log_x target)$,每个状态最多被计算一次。 - **空间复杂度**:$O(\log_x target \times \log target)$,记忆化搜索的缓存空间。 ================================================ FILE: docs/solutions/0900-0999/long-pressed-name.md ================================================ # [0925. 长按键入](https://leetcode.cn/problems/long-pressed-name/) - 标签:双指针、字符串 - 难度:简单 ## 题目链接 - [0925. 长按键入 - 力扣](https://leetcode.cn/problems/long-pressed-name/) ## 题目大意 **描述**:你的朋友正在使用键盘输入他的名字 $name$。偶尔,在键入字符时,按键可能会被长按,而字符可能被输入 $1$ 次或多次。 现在给定代表名字的字符串 $name$,以及实际输入的字符串 $typed$。 **要求**:检查键盘输入的字符 $typed$。如果它对应的可能是你的朋友的名字(其中一些字符可能被长按),就返回 `True`。否则返回 `False`。 **说明**: - $1 \le name.length, typed.length \le 1000$。 - $name$ 和 $typed$ 的字符都是小写字母。 **示例**: - 示例 1: ```python 输入:name = "alex", typed = "aaleex" 输出:true 解释:'alex' 中的 'a' 和 'e' 被长按。 ``` - 示例 2: ```python 输入:name = "saeed", typed = "ssaaedd" 输出:false 解释:'e' 一定需要被键入两次,但在 typed 的输出中不是这样。 ``` ## 解题思路 ### 思路 1:分离双指针 这道题目的意思是在 $typed$ 里边匹配 $name$,同时要考虑字符重复问题,以及不匹配的情况。可以使用分离双指针来做。具体做法如下: 1. 使用两个指针 $left\_1$、$left\_2$,$left\_1$ 指向字符串 $name$ 开始位置,$left\_2$ 指向字符串 $type$ 开始位置。 2. 如果 $name[left\_1] == name[left\_2]$,则将 $left\_1$、$left\_2$ 同时右移。 3. 如果 $nmae[left\_1] \ne name[left\_2]$,则: 1. 如果 $typed[left\_2]$ 和前一个位置元素 $typed[left\_2 - 1]$ 相等,则说明出现了重复元素,将 $left\_2$ 右移,过滤重复元素。 2. 如果 $typed[left\_2]$ 和前一个位置元素 $typed[left\_2 - 1]$ 不等,则说明出现了多余元素,不匹配。直接返回 `False` 即可。 4. 当 $left\_1 == len(name)$ 或者 $left\_2 == len(typed)$ 时跳出循环。然后过滤掉 $typed$ 末尾的重复元素。 5. 最后判断,如果 $left\_1 == len(name)$ 并且 $left\_2 == len(typed)$,则说明匹配,返回 `True`,否则返回 `False`。 ### 思路 1:代码 ```python class Solution: def isLongPressedName(self, name: str, typed: str) -> bool: left_1, left_2 = 0, 0 while left_1 < len(name) and left_2 < len(typed): if name[left_1] == typed[left_2]: left_1 += 1 left_2 += 1 elif left_2 > 0 and typed[left_2 - 1] == typed[left_2]: left_2 += 1 else: return False while 0 < left_2 < len(typed) and typed[left_2] == typed[left_2 - 1]: left_2 += 1 if left_1 == len(name) and left_2 == len(typed): return True else: return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$。其中 $n$、$m$ 分别为字符串 $name$、$typed$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0900-0999/longest-turbulent-subarray.md ================================================ # [0978. 最长湍流子数组](https://leetcode.cn/problems/longest-turbulent-subarray/) - 标签:数组、动态规划、滑动窗口 - 难度:中等 ## 题目链接 - [0978. 最长湍流子数组 - 力扣](https://leetcode.cn/problems/longest-turbulent-subarray/) ## 题目大意 **描述**:给定一个数组 $arr$。当 $arr$ 的子数组 $arr[i]$,$arr[i + 1]$,$...$, $arr[j]$ 满足下列条件时,我们称其为湍流子数组: - 如果 $i \le k < j$,当 $k$ 为奇数时, $arr[k] > arr[k + 1]$,且当 $k$ 为偶数时,$arr[k] < arr[k + 1]$; - 或如果 $i \le k < j$,当 $k$ 为偶数时,$arr[k] > arr[k + 1]$ ,且当 $k$ 为奇数时,$arr[k] < arr[k + 1]$。 - 也就是说,如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是湍流子数组。 **要求**:返回给定数组 $arr$ 的最大湍流子数组的长度。 **说明**: - $1 \le arr.length \le 4 \times 10^4$。 - $0 \le arr[i] \le 10^9$。 **示例**: - 示例 1: ```python 输入:arr = [9,4,2,10,7,8,8,1,9] 输出:5 解释:arr[1] > arr[2] < arr[3] > arr[4] < arr[5] ``` - 示例 2: ```python 输入:arr = [4,8,12,16] 输出:2 ``` ## 解题思路 ### 思路 1:快慢指针 湍流子数组实际上像波浪一样,比如 $arr[i - 2] > arr[i - 1] < arr[i] > arr[i + 1] < arr[i + 2]$。所以我们可以使用双指针的做法。具体做法如下: - 使用两个指针 $left$、$right$。$left$ 指向湍流子数组的左端,$right$ 指向湍流子数组的右端。 - 如果 $arr[right - 1] == arr[right]$,则更新 `left = right`,重新开始计算最长湍流子数组大小。 - 如果 $arr[right - 2] < arr[right - 1] < arr[right]$,此时为递增数组,则 $left$ 从 $right - 1$ 开始重新计算最长湍流子数组大小。 - 如果 $arr[right - 2] > arr[right - 1] > arr[right]$,此时为递减数组,则 $left$ 从 $right - 1$ 开始重新计算最长湍流子数组大小。 - 其他情况(即 $arr[right - 2] < arr[right - 1] > arr[right]$ 或 $arr[right - 2] > arr[right - 1] < arr[right]$)时,不用更新 $left$值。 - 更新最大湍流子数组的长度,并向右移动 $right$。直到 $right \ge len(arr)$ 时,返回答案 $ans$。 ### 思路 1:代码 ```python class Solution: def maxTurbulenceSize(self, arr: List[int]) -> int: left, right = 0, 1 ans = 1 while right < len(arr): if arr[right - 1] == arr[right]: left = right elif right != 1 and arr[right - 2] < arr[right - 1] and arr[right - 1] < arr[right]: left = right - 1 elif right != 1 and arr[right - 2] > arr[right - 1] and arr[right - 1] > arr[right]: left = right - 1 ans = max(ans, right - left + 1) right += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $arr$ 中的元素数量。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0900-0999/maximum-binary-tree-ii.md ================================================ # [0998. 最大二叉树 II](https://leetcode.cn/problems/maximum-binary-tree-ii/) - 标签:树、二叉树 - 难度:中等 ## 题目链接 - [0998. 最大二叉树 II - 力扣](https://leetcode.cn/problems/maximum-binary-tree-ii/) ## 题目大意 **描述**: 「最大树」定义:一棵树,并满足:其中每个节点的值都大于其子树中的任何其他值。 给你最大树的根节点 $root$ 和一个整数 $val$。 就像 [之前的问题](https://leetcode.cn/problems/maximum-binary-tree/) 那样,给定的树是利用 `Construct(a)` 例程从列表 $a$(`root = Construct(a)`)递归地构建的: - 如果 $a$ 为空,返回 $null$。 - 否则,令 $a[i]$ 作为 $a$ 的最大元素。创建一个值为 $a[i]$ 的根节点 $root$。 - $root$ 的左子树将被构建为 `Construct([a[0], a[1], ..., a[i - 1]])`。 - $root$ 的右子树将被构建为 `Construct([a[i + 1], a[i + 2], ..., a[a.length - 1]])`。 - 返回 $root$。 请注意,题目没有直接给出 $a$,只是给出一个根节点 `root = Construct(a)`。 假设 $b$ 是 $a$ 的副本,并在末尾附加值 $val$。题目数据保证 $b$ 中的值互不相同。 **要求**: 返回 `Construct(b)`。 **说明**: - 树中节点数目在范围 $[1, 10^{3}]$ 内。 - $1 \le Node.val \le 10^{3}$。 - 树中的所有值「互不相同」。 - $1 \le val \le 10^{3}$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/02/23/maximum-binary-tree-1-1.png) ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/02/23/maximum-binary-tree-1-2.png) ```python 输入:root = [4,1,3,null,null,2], val = 5 输出:[5,4,null,1,3,null,null,2] 解释:a = [1,4,2,3], b = [1,4,2,3,5] ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/02/23/maximum-binary-tree-2-1.png) ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/02/23/maximum-binary-tree-2-2.png) ```python 输入:root = [5,2,4,null,1], val = 3 输出:[5,2,4,null,1,null,3] 解释:a = [2,1,5,4], b = [2,1,5,4,3] ``` ## 解题思路 ### 思路 1:递归 #### 思路 这道题要求在最大二叉树的末尾插入一个新值 $val$。根据最大二叉树的构造规则: - 如果 $val$ 大于根节点的值,那么 $val$ 应该成为新的根节点,原来的树成为新根的左子树。 - 如果 $val$ 小于根节点的值,那么 $val$ 应该插入到右子树中(因为 $val$ 是在数组末尾添加的)。 我们可以使用递归的方式: 1. 如果当前节点为空,创建一个新节点返回。 2. 如果 $val$ 大于当前节点的值,创建新节点作为根,当前节点作为左子树。 3. 否则,递归地将 $val$ 插入到右子树中。 #### 代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def insertIntoMaxTree(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]: # 如果当前节点为空,创建新节点 if not root: return TreeNode(val) # 如果 val 大于当前节点的值,val 成为新的根节点 if val > root.val: new_root = TreeNode(val) new_root.left = root return new_root # 否则,递归地插入到右子树 root.right = self.insertIntoMaxTree(root.right, val) return root ``` #### 复杂度分析 - **时间复杂度**:$O(h)$,其中 $h$ 是树的高度。最坏情况下需要遍历到最右边的叶子节点。 - **空间复杂度**:$O(h)$,递归调用栈的深度最多为树的高度。 ================================================ FILE: docs/solutions/0900-0999/maximum-sum-circular-subarray.md ================================================ # [0918. 环形子数组的最大和](https://leetcode.cn/problems/maximum-sum-circular-subarray/) - 标签:队列、数组、分治、动态规划、单调队列 - 难度:中等 ## 题目链接 - [0918. 环形子数组的最大和 - 力扣](https://leetcode.cn/problems/maximum-sum-circular-subarray/) ## 题目大意 给定一个环形整数数组 nums,数组 nums 的尾部和头部是相连状态。求环形数组 nums 的非空子数组的最大和(子数组中每个位置元素最多出现一次)。 ## 解题思路 构成环形整数数组 nums 的非空子数组的最大和的子数组有两种情况: - 最大和的子数组为一个子区间:$nums[i] + nums[i+1] + nums[i+2] + ... + num[j]$。 - 最大和的子数组为首尾的两个子区间:$(nums[0] + nums[1] + ... + nums[i]) + (nums[j] + nums[j+1] + ... + num[N-1])$。 第一种情况其实就是无环情况下的整数数组的非空子数组最大和问题,跟「[53. 最大子序和](https://leetcode.cn/problems/maximum-subarray/)」问题是一致的,我们假设求解结果为 `max_num`。 下来来思考第二种情况,第二种情况下,要使首尾两个子区间的和尽可能的大,则中间的子区间的和应该尽可能的小。 使得中间子区间的和尽可能小的问题,可以转变为求解:整数数组 nums 的非空子数组最小和问题。求解思路跟上边是相似的,只不过最大变为了最小。我们假设求解结果为 `min_num`。 而首尾两个区间和尽可能大的结果为数组 nums 的和减去中间最小子数组和,即 `sum(nums) - min_num`。 最终的结果就是比较 `sum(nums) - min_num` 和 `max_num`的大小,返回较大值即可。 ## 代码 ```python class Solution: def maxSubarraySumCircular(self, nums: List[int]) -> int: size = len(nums) dp_max, dp_min = nums[0], nums[0] max_num, min_num = nums[0], nums[0] for i in range(1, size): dp_max = max(dp_max + nums[i], nums[i]) dp_min = min(dp_min + nums[i], nums[i]) max_num = max(dp_max, max_num) min_num = min(dp_min, min_num) sum_num = sum(nums) if max_num < 0: return max_num return max(sum_num - min_num, max_num) ``` ================================================ FILE: docs/solutions/0900-0999/maximum-width-ramp.md ================================================ # [0962. 最大宽度坡](https://leetcode.cn/problems/maximum-width-ramp/) - 标签:栈、数组、双指针、单调栈 - 难度:中等 ## 题目链接 - [0962. 最大宽度坡 - 力扣](https://leetcode.cn/problems/maximum-width-ramp/) ## 题目大意 **描述**: 给定一个整数数组 A,坡是元组 $(i, j)$,其中 $i < j$ 且 $A[i] \le A[j]$。这样的坡的宽度为 $j - i$。 **要求**: 找出 A 中的坡的最大宽度,如果不存在,返回 0。 **说明**: - $2 \le A.length \le 50000$。 - $0 \le A[i] \le 50000$。 **示例**: - 示例 1: ```python 输入:[6,0,8,2,1,5] 输出:4 解释: 最大宽度的坡为 (i, j) = (1, 5): A[1] = 0 且 A[5] = 5. ``` - 示例 2: ```python 输入:[9,8,1,0,1,9,4,0,4,1] 输出:7 解释: 最大宽度的坡为 (i, j) = (2, 9): A[2] = 1 且 A[9] = 1. ``` ## 解题思路 ### 思路 1:单调栈 #### 思路 这道题要求找到最大宽度的坡,即满足 $i < j$ 且 $nums[i] \le nums[j]$ 的最大 $j - i$。 我们可以使用单调栈来解决: 1. **构建单调递减栈**:从左到右遍历数组,将索引压入栈中,保持栈中索引对应的值单调递减。这样栈中存储的是可能作为坡起点的候选位置。 2. **从右向左查找最大宽度**:从右向左遍历数组,对于每个位置 $j$,尝试从栈顶弹出满足 $nums[stack[-1]] \le nums[j]$ 的索引 $i$,计算宽度 $j - i$ 并更新最大值。 为什么从右向左遍历?因为对于栈顶的索引 $i$,我们希望找到最远的 $j$ 使得 $nums[i] \le nums[j]$,从右向左可以保证找到的是最大宽度。 #### 代码 ```python class Solution: def maxWidthRamp(self, nums: List[int]) -> int: n = len(nums) stack = [] # 单调递减栈,存储索引 # 构建单调递减栈 for i in range(n): if not stack or nums[i] < nums[stack[-1]]: stack.append(i) max_width = 0 # 从右向左遍历,寻找最大宽度 for j in range(n - 1, -1, -1): # 当栈不为空且当前值大于等于栈顶索引对应的值 while stack and nums[j] >= nums[stack[-1]]: i = stack.pop() max_width = max(max_width, j - i) return max_width ``` #### 复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组长度。每个元素最多入栈和出栈各一次。 - **空间复杂度**:$O(n)$,栈的空间最多存储 $n$ 个元素。 ================================================ FILE: docs/solutions/0900-0999/minimize-malware-spread-ii.md ================================================ # [0928. 尽量减少恶意软件的传播 II](https://leetcode.cn/problems/minimize-malware-spread-ii/) - 标签:深度优先搜索、广度优先搜索、并查集、图、数组、哈希表 - 难度:困难 ## 题目链接 - [0928. 尽量减少恶意软件的传播 II - 力扣](https://leetcode.cn/problems/minimize-malware-spread-ii/) ## 题目大意 **描述**: 给定一个由 $n$ 个节点组成的网络,用 $n$ $x$ $n$ 个邻接矩阵 $graph$ 表示。在节点网络中,只有当 $graph[i][j] = 1$ 时,节点 $i$ 能够直接连接到另一个节点 $j$。 一些节点 $initial$ 最初被恶意软件感染。只要两个节点直接连接,且其中至少一个节点受到恶意软件的感染,那么两个节点都将被恶意软件感染。这种恶意软件的传播将继续,直到没有更多的节点可以被这种方式感染。 假设 $M(initial)$ 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。 我们可以从 $initial$ 中 删除一个节点,并完全移除该节点以及从该节点到任何其他节点的任何连接。 **要求**: 请返回移除后能够使 $M(initial)$ 最小化的节点。如果有多个节点满足条件,返回索引「最小的节点」。 **说明**: - $n == graph.length$。 - $n == graph[i].length$。 - $2 \le n \le 300$。 - $graph[i][j]$ 是 $0$ 或 $1$。 - $graph[i][j] == graph[j][i]$。 - $graph[i][i] == 1$。 - $1 \le initial.length \lt n$。 - $0 \le initial[i] \le n - 1$。 - $initial$ 中每个整数都不同。 **示例**: - 示例 1: ```python 输入:graph = [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1] 输出:0 ``` - 示例 2: ```python 输入:graph = [[1,1,0],[1,1,1],[0,1,1]], initial = [0,1] 输出:1 ``` ## 解题思路 ### 思路 1:并查集 这道题与 0924 类似,但区别在于删除节点后,该节点不会传播病毒。 1. **枚举删除节点**:对于每个初始感染节点,假设删除它,计算剩余节点的感染范围。 2. **BFS/DFS 计算感染范围**:从剩余的初始感染节点开始,使用 BFS 或 DFS 计算能感染的节点数。 3. **选择最优节点**:选择删除后感染节点数最少的节点,如果有多个,选择索引最小的。 **优化**:可以使用并查集预处理连通分量,然后对每个初始感染节点计算影响。 ### 思路 1:代码 ```python class Solution: def minMalwareSpread(self, graph: List[List[int]], initial: List[int]) -> int: n = len(graph) def bfs(removed): """计算删除 removed 节点后的感染节点数""" infected = set() queue = collections.deque() # 将剩余的初始感染节点加入队列 for node in initial: if node != removed: queue.append(node) infected.add(node) # BFS 扩散 while queue: curr = queue.popleft() for next_node in range(n): if next_node != removed and graph[curr][next_node] == 1 and next_node not in infected: infected.add(next_node) queue.append(next_node) return len(infected) # 枚举删除每个初始感染节点 min_infected = n + 1 result = min(initial) for node in sorted(initial): infected_count = bfs(node) if infected_count < min_infected: min_infected = infected_count result = node return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n^2)$,其中 $m$ 是初始感染节点数量,$n$ 是节点总数。对于每个初始感染节点,需要进行一次 BFS,每次 BFS 需要 $O(n^2)$ 时间。 - **空间复杂度**:$O(n)$,需要使用队列和集合存储访问状态。 ================================================ FILE: docs/solutions/0900-0999/minimize-malware-spread.md ================================================ # [0924. 尽量减少恶意软件的传播](https://leetcode.cn/problems/minimize-malware-spread/) - 标签:深度优先搜索、广度优先搜索、并查集、图、数组、哈希表 - 难度:困难 ## 题目链接 - [0924. 尽量减少恶意软件的传播 - 力扣](https://leetcode.cn/problems/minimize-malware-spread/) ## 题目大意 **描述**: 给定一个由 $n$ 个节点组成的网络,用 $n \times n$ 个邻接矩阵图 $graph$ 表示。在节点网络中,当 $graph[i][j] = 1$ 时,表示节点 $i$ 能够直接连接到另一个节点 $j$。 一些节点 $initial$ 最初被恶意软件感染。只要两个节点直接连接,且其中至少一个节点受到恶意软件的感染,那么两个节点都将被恶意软件感染。这种恶意软件的传播将继续,直到没有更多的节点可以被这种方式感染。 假设 $M(initial)$ 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。 **要求**: 如果从 $initial$ 中移除某一节点能够最小化 $M(initial)$,返回该节点。如果有多个节点满足条件,就返回索引最小的节点。 **说明**: - 注意:如果某个节点已从受感染节点的列表 $initial$ 中删除,它以后仍有可能因恶意软件传播而受到感染。 - $n == graph.length$。 - $n == graph[i].length$。 - $2 \le n \le 300$。 - $graph[i][j] == 0$ 或 $1$。 - $graph[i][j] == graph[j][i]$。 - $graph[i][i] == 1$。 - $1 \le initial.length \le n$。 - $0 \le initial[i] \le n - 1$。 - $initial$ 中所有整数均不重复。 **示例**: - 示例 1: ```python 输入:graph = [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1] 输出:0 ``` - 示例 2: ```python 输入:graph = [[1,0,0],[0,1,0],[0,0,1]], initial = [0,2] 输出:0 ``` ## 解题思路 ### 思路 1:并查集 这道题的关键是找到删除哪个初始感染节点能最大程度减少感染范围。 1. **构建连通分量**:使用并查集将所有节点连接起来,形成连通分量。 2. **统计影响**:对于每个连通分量,统计其中包含的初始感染节点数量和连通分量大小。 3. **选择最优节点**: - 如果一个连通分量只包含一个初始感染节点,删除该节点可以拯救整个连通分量 - 如果一个连通分量包含多个初始感染节点,删除任何一个都无法拯救它 - 选择能拯救最多节点的初始感染节点,如果有多个,选择索引最小的 ### 思路 1:代码 ```python class Solution: def minMalwareSpread(self, graph: List[List[int]], initial: List[int]) -> int: n = len(graph) initial_set = set(initial) # 并查集 parent = list(range(n)) def find(x): if parent[x] != x: parent[x] = find(parent[x]) return parent[x] def union(x, y): px, py = find(x), find(y) if px != py: parent[px] = py # 将所有相连的节点连接起来 for i in range(n): for j in range(i + 1, n): if graph[i][j] == 1: union(i, j) # 统计每个连通分量的大小和包含的初始感染节点 component_size = collections.defaultdict(int) component_malware = collections.defaultdict(list) for i in range(n): root = find(i) component_size[root] += 1 if i in initial_set: component_malware[root].append(i) # 统计删除每个初始感染节点能拯救的节点数 saved = collections.defaultdict(int) for root, malware_list in component_malware.items(): # 只有一个初始感染节点时,删除它可以拯救整个连通分量(除了它自己) if len(malware_list) == 1: m = malware_list[0] saved[m] = component_size[root] # 选择能拯救最多节点的初始感染节点 initial.sort() max_saved = max(saved.values()) if saved else 0 for node in initial: if saved[node] == max_saved: return node return initial[0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是节点数量。需要遍历邻接矩阵构建并查集。 - **空间复杂度**:$O(n)$,需要使用并查集和哈希表存储信息。 ================================================ FILE: docs/solutions/0900-0999/minimum-add-to-make-parentheses-valid.md ================================================ # [0921. 使括号有效的最少添加](https://leetcode.cn/problems/minimum-add-to-make-parentheses-valid/) - 标签:栈、贪心、字符串 - 难度:中等 ## 题目链接 - [0921. 使括号有效的最少添加 - 力扣](https://leetcode.cn/problems/minimum-add-to-make-parentheses-valid/) ## 题目大意 **描述**:给定一个括号字符串 `s`,可以在字符串的任何位置插入一个括号。 **要求**:返回为使结果字符串 `s` 有效而必须添加的最少括号数。 **说明**: - $1 \le s.length \le 1000$。 - `s` 只包含 `'('` 和 `')'` 字符。 只有满足下面几点之一,括号字符串才是有效的: - 它是一个空字符串,或者 - 它可以被写成 AB (A 与 B 连接), 其中 A 和 B 都是有效字符串,或者 - 它可以被写作 (A),其中 A 是有效字符串。 例如,如果 `s = "()))"`,你可以插入一个开始括号为 `"(()))"` 或结束括号为 `"())))"`。 **示例**: - 示例 1: ```python 输入:s = "())" 输出:1 ``` ## 解题思路 ### 思路 1:贪心算法 为了最终添加的最少括号数,我们应该尽可能将当前能够匹配的括号先进行配对。则剩余的未完成配对的括号数量就是答案。 我们使用变量 `left_cnt` 来记录当前左括号的数量。使用 `res` 来记录添加的最少括号数量。 - 遍历字符串,判断当前字符。 - 如果当前字符为左括号 `(`,则令 `left_cnt` 加 `1`。 - 如果当前字符为右括号 `)`,则令 `left_cnt` 减 `1`。如果 `left_cnt` 减到 `-1`,说明当前有右括号不能完成匹配,则答案数量 `res` 加 `1`,并令 `left_cnt` 重新赋值为 `0`。 - 遍历完之后,令 `res` 加上剩余不匹配的 `left_cnt` 数量。 - 最后输出 `res`。 ### 思路 1:贪心算法代码 ```python class Solution: def minAddToMakeValid(self, s: str) -> int: res = 0 left_cnt = 0 for ch in s: if ch == '(': left_cnt += 1 elif ch == ')': left_cnt -= 1 if left_cnt == -1: left_cnt = 0 res += 1 res += left_cnt return res ``` ================================================ FILE: docs/solutions/0900-0999/minimum-area-rectangle-ii.md ================================================ # [0963. 最小面积矩形 II](https://leetcode.cn/problems/minimum-area-rectangle-ii/) - 标签:几何、数组、哈希表、数学 - 难度:中等 ## 题目链接 - [0963. 最小面积矩形 II - 力扣](https://leetcode.cn/problems/minimum-area-rectangle-ii/) ## 题目大意 **描述**: 给定一个 X-Y 平面上的点数组 $points$,其中 $points[i] = [x_i, y_i]$。 **要求**: 返回由这些点形成的任意矩形的最小面积,矩形的边「不一定」平行于 X 轴和 Y 轴。如果不存在这样的矩形,则返回 0。 答案只需在$10^{-5}$ 的误差范围内即可被视作正确答案。 **说明**: - $1 \le points.length \le 50$。 - $points[i].length == 2$。 - $0 \le x_i, y_i \le 4 \times 10^{4}$。 - 所有给定的点都是「唯一」的。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2018/12/21/1a.png) ```python 输入: points = [[1,2],[2,1],[1,0],[0,1]] 输出: 2.00000 解释: 最小面积矩形由 [1,2]、[2,1]、[1,0]、[0,1] 组成,其面积为 2。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2018/12/22/2.png) ```python 输入: points = [[0,1],[2,1],[1,1],[1,0],[2,0]] 输出: 1.00000 解释: 最小面积矩形由 [1,0]、[1,1]、[2,1]、[2,0] 组成,其面积为 1。 ``` ## 解题思路 ### 思路 1:哈希表 + 几何 对于任意矩形,可以由对角线的两个点和中心点唯一确定。我们可以枚举所有点对作为对角线,然后检查是否存在另外两个点构成矩形。 1. **枚举对角线**:枚举所有点对 $(p_1, p_2)$ 作为对角线。 2. **计算中心和边长**: - 中心点:$(\frac{x_1 + x_2}{2}, \frac{y_1 + y_2}{2})$ - 对角线长度:$d = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$ 3. **哈希存储**:使用哈希表存储 (中心点, 对角线长度) 到点对的映射。 4. **验证矩形**:对于相同中心和对角线长度的两对点,验证它们是否构成矩形(对角线互相垂直)。 5. **计算面积**:使用向量叉积计算矩形面积。 ### 思路 1:代码 ```python class Solution: def minAreaFreeRect(self, points: List[List[int]]) -> float: from collections import defaultdict n = len(points) if n < 4: return 0.0 # 哈希表:(中心点, 对角线长度平方) -> [(点1, 点2), ...] diagonals = defaultdict(list) # 枚举所有点对作为对角线 for i in range(n): for j in range(i + 1, n): x1, y1 = points[i] x2, y2 = points[j] # 计算中心点(使用 2 倍坐标避免浮点数) cx, cy = x1 + x2, y1 + y2 # 计算对角线长度的平方 dist_sq = (x2 - x1) ** 2 + (y2 - y1) ** 2 # 存储到哈希表 diagonals[(cx, cy, dist_sq)].append((i, j)) min_area = float('inf') # 检查相同中心和对角线长度的点对 for key, pairs in diagonals.items(): if len(pairs) < 2: continue # 枚举所有点对组合 for k in range(len(pairs)): for l in range(k + 1, len(pairs)): i1, j1 = pairs[k] i2, j2 = pairs[l] # 获取四个点 p1, p2 = points[i1], points[j1] p3, p4 = points[i2], points[j2] # 计算两条边的向量 v1 = (p3[0] - p1[0], p3[1] - p1[1]) v2 = (p4[0] - p1[0], p4[1] - p1[1]) # 检查是否垂直(点积为 0) if v1[0] * v2[0] + v1[1] * v2[1] == 0: # 计算面积 len1 = (v1[0] ** 2 + v1[1] ** 2) ** 0.5 len2 = (v2[0] ** 2 + v2[1] ** 2) ** 0.5 area = len1 * len2 min_area = min(min_area, area) return min_area if min_area != float('inf') else 0.0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 + m^2)$,其中 $n$ 是点的数量,$m$ 是相同中心和对角线长度的点对数量。枚举点对需要 $O(n^2)$,验证矩形需要 $O(m^2)$。 - **空间复杂度**:$O(n^2)$,哈希表最多存储 $O(n^2)$ 个点对。 ================================================ FILE: docs/solutions/0900-0999/minimum-area-rectangle.md ================================================ # [0939. 最小面积矩形](https://leetcode.cn/problems/minimum-area-rectangle/) - 标签:几何、数组、哈希表、数学、排序 - 难度:中等 ## 题目链接 - [0939. 最小面积矩形 - 力扣](https://leetcode.cn/problems/minimum-area-rectangle/) ## 题目大意 **描述**: 给定一个 X-Y 平面上的点数组 $points$,其中 $points[i] = [x_i, y_i]$。 **要求**: 返回由这些点形成的矩形的最小面积,矩形的边与 X 轴和 Y 轴平行。如果不存在这样的矩形,则返回 0。 **说明**: - $1 \le points.length \le 500$。 - $points[i].length == 2$。 - $0 \le x_i, y_i \le 4 \times 10^{4}$。 - 所有给定的点都是「唯一」的。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/08/03/rec1.JPG) ```python 输入: points = [[1,1],[1,3],[3,1],[3,3],[2,2]] 输出: 4 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/08/03/rec2.JPG) ```python 输入: points = [[1,1],[1,3],[3,1],[3,3],[4,1],[4,3]] 输出: 2 ``` ## 解题思路 ### 思路 1:哈希表 要找到边平行于坐标轴的最小面积矩形,我们需要找到四个点构成的矩形。 1. **枚举对角线**:矩形可以由对角线上的两个点确定。对于边平行于坐标轴的矩形,对角线的两个点 $(x_1, y_1)$ 和 $(x_2, y_2)$ 满足 $x_1 \ne x_2$ 且 $y_1 \ne y_2$。 2. **验证矩形**:对于每对对角线点,检查另外两个点 $(x_1, y_2)$ 和 $(x_2, y_1)$ 是否存在。 3. **计算面积**:如果四个点都存在,计算面积 $|x_2 - x_1| \times |y_2 - y_1|$,更新最小值。 4. **优化**:使用哈希集合存储所有点,快速判断点是否存在。 ### 思路 1:代码 ```python class Solution: def minAreaRect(self, points: List[List[int]]) -> int: point_set = set(map(tuple, points)) min_area = float('inf') # 枚举所有点对作为对角线 for i in range(len(points)): x1, y1 = points[i] for j in range(i + 1, len(points)): x2, y2 = points[j] # 必须是对角线(不在同一行或同一列) if x1 != x2 and y1 != y2: # 检查另外两个点是否存在 if (x1, y2) in point_set and (x2, y1) in point_set: area = abs(x2 - x1) * abs(y2 - y1) min_area = min(min_area, area) return min_area if min_area != float('inf') else 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是点的数量,需要枚举所有点对。 - **空间复杂度**:$O(n)$,需要使用哈希集合存储所有点。 ================================================ FILE: docs/solutions/0900-0999/minimum-cost-for-tickets.md ================================================ # [0983. 最低票价](https://leetcode.cn/problems/minimum-cost-for-tickets/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [0983. 最低票价 - 力扣](https://leetcode.cn/problems/minimum-cost-for-tickets/) ## 题目大意 **描述**: 在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 $days$ 的数组给出。每一项是一个从 1 到 365 的整数。 火车票有「三种不同的销售方式」: - 一张「为期一天」的通行证售价为 $costs[0]$ 美元; - 一张「为期七天」的通行证售价为 $costs[1]$ 美元; - 一张「为期三十天」的通行证售价为 $costs[2]$ 美元。 通行证允许数天无限制的旅行。例如,如果我们在第 2 天获得一张 为期 7 天 的通行证,那么我们可以连着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。 **要求**: 返回你想要完成在给定的列表 $days$ 中列出的每一天的旅行所需要的最低消费。 **说明**: - $1 \le days.length \le 365$。 - $1 \le days[i] \le 365$。 - $days$ 按顺序严格递增。 - $costs.length == 3$。 - $1 \le costs[i] \le 10^{3}$。 **示例**: - 示例 1: ```python 输入:days = [1,4,6,7,8,20], costs = [2,7,15] 输出:11 解释: 例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划: 在第 1 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 1 天生效。 在第 3 天,你花了 costs[1] = $7 买了一张为期 7 天的通行证,它将在第 3, 4, ..., 9 天生效。 在第 20 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 20 天生效。 你总共花了 $11,并完成了你计划的每一天旅行。 ``` - 示例 2: ```python 输入:days = [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15] 输出:17 解释: 例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划: 在第 1 天,你花了 costs[2] = $15 买了一张为期 30 天的通行证,它将在第 1, 2, ..., 30 天生效。 在第 31 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 31 天生效。 你总共花了 $17,并完成了你计划的每一天旅行。 ``` ## 解题思路 ### 思路 1:动态规划 这是一个经典的动态规划问题,需要考虑三种购票方案的最优组合。 1. **状态定义**:$dp[i]$ 表示到第 $i$ 天为止的最低消费。 2. **状态转移**:对于旅行日 $days[i]$,可以选择三种购票方案: - 购买 $1$ 天通行证:$dp[i] = dp[i-1] + costs[0]$ - 购买 $7$ 天通行证:$dp[i] = dp[j] + costs[1]$,其中 $j$ 是 $7$ 天前的最后一个旅行日 - 购买 $30$ 天通行证:$dp[i] = dp[k] + costs[2]$,其中 $k$ 是 $30$ 天前的最后一个旅行日 3. **优化**:可以使用数组索引而不是日期作为状态,避免处理非旅行日。 ### 思路 1:代码 ```python class Solution: def mincostTickets(self, days: List[int], costs: List[int]) -> int: n = len(days) dp = [0] * n for i in range(n): # 方案 1:购买 1 天通行证 dp[i] = (dp[i - 1] if i > 0 else 0) + costs[0] # 方案 2:购买 7 天通行证 j = i while j >= 0 and days[i] - days[j] < 7: j -= 1 cost_7 = (dp[j] if j >= 0 else 0) + costs[1] dp[i] = min(dp[i], cost_7) # 方案 3:购买 30 天通行证 k = i while k >= 0 and days[i] - days[k] < 30: k -= 1 cost_30 = (dp[k] if k >= 0 else 0) + costs[2] dp[i] = min(dp[i], cost_30) return dp[n - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \cdot d)$,其中 $n$ 是旅行天数,$d$ 是通行证的最大天数($30$)。对于每个旅行日,最多向前查找 $30$ 天。 - **空间复杂度**:$O(n)$,需要使用数组存储 DP 状态。 ================================================ FILE: docs/solutions/0900-0999/minimum-falling-path-sum.md ================================================ # [0931. 下降路径最小和](https://leetcode.cn/problems/minimum-falling-path-sum/) - 标签:数组、动态规划、矩阵 - 难度:中等 ## 题目链接 - [0931. 下降路径最小和 - 力扣](https://leetcode.cn/problems/minimum-falling-path-sum/) ## 题目大意 **描述**: 给定一个 $n \times n$ 的方形整数数组 $matrix$。 **要求**: 请你找出并返回通过 $matrix$ 的下降路径的「最小和」。 **说明**: - 「下降路径」可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 $(row, col)$ 的下一个元素应当是 $(row + 1, col - 1)$、$(row + 1, col)$ 或者 $(row + 1, col + 1)$。 - $n == matrix.length == matrix[i].length$。 - $1 \le n \le 10^{3}$。 - $-10^{3} \le matrix[i][j] \le 10^{3}$。 **示例**: - 示例 1: ![](https://pic.leetcode.cn/1729566253-aneDag-image.png) ```python 输入:matrix = [[2,1,3],[6,5,4],[7,8,9]] 输出:13 解释:如图所示,为和最小的两条下降路径 ``` - 示例 2: ![](https://pic.leetcode.cn/1729566282-dtXwRd-image.png) ```python 输入:matrix = [[-19,57],[-40,-5]] 输出:-59 解释:如图所示,为和最小的下降路径 ``` ## 解题思路 ### 思路 1:动态规划 这是一个经典的动态规划问题,类似于"最小路径和"。 1. **状态定义**:$dp[i][j]$ 表示到达位置 $(i, j)$ 的最小路径和。 2. **状态转移**:对于位置 $(i, j)$,可以从三个位置转移而来: - 正上方:$(i-1, j)$ - 左上方:$(i-1, j-1)$ - 右上方:$(i-1, j+1)$ 转移方程:$dp[i][j] = matrix[i][j] + \min(dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1])$ 3. **初始化**:第一行的值就是 $matrix[0][j]$。 4. **边界处理**:注意处理列的边界情况。 5. **返回结果**:最后一行的最小值。 **空间优化**:可以直接在原数组上修改,或使用滚动数组优化空间。 ### 思路 1:代码 ```python class Solution: def minFallingPathSum(self, matrix: List[List[int]]) -> int: n = len(matrix) # 从第二行开始,逐行计算最小路径和 for i in range(1, n): for j in range(n): # 计算从上一行三个位置转移的最小值 min_prev = matrix[i - 1][j] # 正上方 if j > 0: # 左上方 min_prev = min(min_prev, matrix[i - 1][j - 1]) if j < n - 1: # 右上方 min_prev = min(min_prev, matrix[i - 1][j + 1]) # 更新当前位置的最小路径和 matrix[i][j] += min_prev # 返回最后一行的最小值 return min(matrix[n - 1]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是矩阵的边长,需要遍历整个矩阵。 - **空间复杂度**:$O(1)$,直接在原数组上修改。如果不能修改原数组,需要 $O(n^2)$ 的额外空间。 ================================================ FILE: docs/solutions/0900-0999/minimum-increment-to-make-array-unique.md ================================================ # [0945. 使数组唯一的最小增量](https://leetcode.cn/problems/minimum-increment-to-make-array-unique/) - 标签:贪心、数组、计数、排序 - 难度:中等 ## 题目链接 - [0945. 使数组唯一的最小增量 - 力扣](https://leetcode.cn/problems/minimum-increment-to-make-array-unique/) ## 题目大意 **描述**: 给定一个整数数组 $nums$。每次 $move$ 操作将会选择任意一个满足 $0 \le i < nums.length$ 的下标 $i$,并将 $nums[i]$ 递增 1。 **要求**: 返回使 $nums$ 中的每个值都变成唯一的所需要的最少操作次数。 **说明**: - 生成的测试用例保证答案在 32 位整数范围内。 - $1 \le nums.length \le 10^{5}$。 - $0 \le nums[i] \le 10^{5}$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,2] 输出:1 解释:经过一次 move 操作,数组将变为 [1, 2, 3]。 ``` - 示例 2: ```python 输入:nums = [3,2,1,2,1,7] 输出:6 解释:经过 6 次 move 操作,数组将变为 [3, 4, 1, 2, 5, 7]。 可以看出 5 次或 5 次以下的 move 操作是不能让数组的每个值唯一的。 ``` ## 解题思路 ### 思路 1:贪心 + 排序 要使数组中的每个值都唯一,我们可以先排序,然后从左到右遍历,确保每个元素都大于前一个元素。 1. **排序**:首先对数组进行排序。 2. **贪心策略**:从左到右遍历,对于每个元素: - 如果当前元素小于或等于前一个元素,需要将其增加到 $\text{prev} + 1$ - 累加操作次数 3. **维护最大值**:使用变量 $\text{need}$ 记录当前位置需要的最小值。 ### 思路 1:代码 ```python class Solution: def minIncrementForUnique(self, nums: List[int]) -> int: nums.sort() moves = 0 need = 0 # 当前位置需要的最小值 for num in nums: # 如果当前数字小于需要的最小值,需要增加 if num < need: moves += need - num need += 1 else: # 当前数字已经足够大,更新需要的最小值 need = num + 1 return moves ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组长度,主要是排序的时间复杂度。 - **空间复杂度**:$O(\log n)$,排序所需的栈空间。 ================================================ FILE: docs/solutions/0900-0999/minimum-number-of-k-consecutive-bit-flips.md ================================================ # [0995. K 连续位的最小翻转次数](https://leetcode.cn/problems/minimum-number-of-k-consecutive-bit-flips/) - 标签:位运算、队列、数组、前缀和、滑动窗口 - 难度:困难 ## 题目链接 - [0995. K 连续位的最小翻转次数 - 力扣](https://leetcode.cn/problems/minimum-number-of-k-consecutive-bit-flips/) ## 题目大意 **描述**:给定一个仅包含 $0$ 和 $1$ 的数组 $nums$,再给定一个整数 $k$。进行一次 $k$ 位翻转包括选择一个长度为 $k$ 的(连续)子数组,同时将子数组中的每个 $0$ 更改为 $1$,而每个 $1$ 更改为 $0$。 **要求**:返回所需的 $k$ 位翻转的最小次数,以便数组没有值为 $0$ 的元素。如果不可能,返回 $-1$。 **说明**: - **子数组**:数组的连续部分。 - $1 <= nums.length <= 105$。 - $1 <= k <= nums.length$。 **示例**: - 示例 1: ```python 输入:nums = [0,1,0], K = 1 输出:2 解释:先翻转 A[0],然后翻转 A[2]。 ``` - 示例 2: ```python 输入:nums = [0,0,0,1,0,1,1,0], K = 3 输出:3 解释: 翻转 A[0],A[1],A[2]: A变成 [1,1,1,1,0,1,1,0] 翻转 A[4],A[5],A[6]: A变成 [1,1,1,1,1,0,0,0] 翻转 A[5],A[6],A[7]: A变成 [1,1,1,1,1,1,1,1] ``` ## 解题思路 ### 思路 1:滑动窗口 每次需要翻转的起始位置肯定是遇到第一个元素为 $0$ 的位置开始反转,如果能够使得整个数组不存在 $0$,即返回 $ans$ 作为反转次数。 同时我们还可以发现: - 如果某个元素反转次数为奇数次,元素会由 $0 \rightarrow 1$,$1 \rightarrow 0$。 - 如果某个元素反转次数为偶数次,元素不会发生变化。 每个第 $i$ 位置上的元素只会被前面 $[i - k + 1, i - 1]$ 的元素影响。所以我们只需要知道前面 $k - 1$ 个元素翻转次数的奇偶性就可以了。 同时如果我们知道了前面 $k - 1$ 个元素的翻转次数就可以直接修改 $nums[i]$ 了。 我们使用 $flip\_count$ 记录第 $i$ 个元素之前 $k - 1$ 个位置总共被反转了多少次,或者 $flip\_count$ 是大小为 $k - 1$ 的滑动窗口。 - 如果前面第 $k - 1$ 个元素翻转了奇数次,则如果 $nums[i] == 1$,则 $nums[i]$ 也被翻转成了 $0$,需要再翻转 $1$ 次。 - 如果前面第 $k - 1$ 个元素翻转了偶数次,则如果 $nums[i] == 0$,则 $nums[i]$ 也被翻转成为了 $0$,需要再翻转 $1$ 次。 这两句写成判断语句可以写为:`if (flip_count + nums[i]) % 2 == 0:`。 因为 $0 <= nums[i] <= 1$,所以我们可以用 $0$ 和 $1$ 以外的数,比如 $2$ 来标记第 $i$ 个元素发生了翻转,即 `nums[i] = 2`。这样在遍历到第 $i$ 个元素时,如果有 $nums[i - k] == 2$,则说明 $nums[i - k]$ 发生了翻转。同时根据 $flip\_count$ 和 $nums[i]$ 来判断第 $i$ 位是否需要进行翻转。 整个算法的具体步骤如下: - 使用 $res$ 记录最小翻转次数。使用 $flip\_count$ 记录窗口内前 $k - 1 $ 位元素的翻转次数。 - 遍历数组 $nums$,对于第 $i$ 位元素: - 如果 $i - k >= 0$,并且 $nums[i - k] == 2$,需要缩小窗口,将翻转次数减一。(此时窗口范围为 $[i - k + 1, i - 1]$)。 - 如果 $(flip\_count + nums[i]) \mod 2 == 0$,则说明 $nums[i]$ 还需要再翻转一次,将 $nums[i]$ 标记为 $2$,同时更新窗口内翻转次数 $flip\_count$ 和答案最小翻转次数 $ans$。 - 遍历完之后,返回 $res$。 ### 思路 1:代码 ```python class Solution: def minKBitFlips(self, nums: List[int], k: int) -> int: ans = 0 flip_count = 0 for i in range(len(nums)): if i - k >= 0 and nums[i - k] == 2: flip_count -= 1 if (flip_count + nums[i]) % 2 == 0: if i + k > len(nums): return -1 nums[i] = 2 flip_count += 1 ans += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0900-0999/most-stones-removed-with-same-row-or-column.md ================================================ # [0947. 移除最多的同行或同列石头](https://leetcode.cn/problems/most-stones-removed-with-same-row-or-column/) - 标签:深度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [0947. 移除最多的同行或同列石头 - 力扣](https://leetcode.cn/problems/most-stones-removed-with-same-row-or-column/) ## 题目大意 **描述**:二维平面中有 $n$ 块石头,每块石头都在整数坐标点上,且每个坐标点上最多只能有一块石头。如果一块石头的同行或者同列上有其他石头存在,那么就可以移除这块石头。 给你一个长度为 $n$ 的数组 $stones$ ,其中 $stones[i] = [xi, yi]$ 表示第 $i$ 块石头的位置。 **要求**:返回可以移除的石子的最大数量。 **说明**: - $1 \le stones.length \le 1000$。 - $0 \le xi, yi \le 10^4$。 - 不会有两块石头放在同一个坐标点上。 **示例**: - 示例 1: ```python 输入:stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]] 输出:5 解释:一种移除 5 块石头的方法如下所示: 1. 移除石头 [2,2] ,因为它和 [2,1] 同行。 2. 移除石头 [2,1] ,因为它和 [0,1] 同列。 3. 移除石头 [1,2] ,因为它和 [1,0] 同行。 4. 移除石头 [1,0] ,因为它和 [0,0] 同列。 5. 移除石头 [0,1] ,因为它和 [0,0] 同行。 石头 [0,0] 不能移除,因为它没有与另一块石头同行/列。 ``` - 示例 2: ```python 输入:stones = [[0,0],[0,2],[1,1],[2,0],[2,2]] 输出:3 解释:一种移除 3 块石头的方法如下所示: 1. 移除石头 [2,2] ,因为它和 [2,0] 同行。 2. 移除石头 [2,0] ,因为它和 [0,0] 同列。 3. 移除石头 [0,2] ,因为它和 [0,0] 同行。 石头 [0,0] 和 [1,1] 不能移除,因为它们没有与另一块石头同行/列。 ``` ## 解题思路 ### 思路 1:并查集 题目「求最多可以移走的石头数目」也可以换一种思路:「求最少留下的石头数目」。 - 如果两个石头 $A$、$B$ 处于同一行或者同一列,我们就可以删除石头 $A$ 或 $B$,最少留下 $1$ 个石头。 - 如果三个石头 $A$、$B$、$C$,其中 $A$、$B$ 处于同一行,$B$、$C$ 处于同一列,则我们可以先删除石头 $A$,再删除石头 $C$,最少留下 $1$ 个石头。 - 如果有 $n$ 个石头,其中每个石头都有一个同行或者同列的石头,则我们可以将 $n - 1$ 个石头都删除,最少留下 $1$ 个石头。 通过上面的分析,我们可以利用并查集,将同行、同列的石头都加入到一个集合中。这样「最少可以留下的石头」就是并查集中集合的个数。 则答案为:**最多可以移走的石头数目 = 所有石头个数 - 最少可以留下的石头(并查集的集合个数)**。 因为石子坐标是二维的,在使用并查集的时候要区分横纵坐标,因为 $0 <= xi, yi <= 10^4$,可以取 $n = 10010$,将纵坐标映射到 $[n, n + 10000]$ 的范围内,这样就可以得到所有节点的标号。 最后计算集合个数,可以使用 set 集合去重,然后统计数量。 整体步骤如下: 1. 定义一个 $10010 \times 2$ 大小的并查集。 2. 遍历每块石头的横纵坐标: 1. 将纵坐标映射到 $[10010, 10010 + 10000]$ 的范围内。 2. 然后将当前石头的横纵坐标相连接(加入到并查集中)。 3. 建立一个 set 集合,查找每块石头横坐标所在集合对应的并查集编号,将编号加入到 set 集合中。 4. 最后,返回「所有石头个数 - 并查集集合个数」即为答案。 ### 思路 1:代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] self.count = n def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.count -= 1 def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def removeStones(self, stones: List[List[int]]) -> int: size = len(stones) n = 10010 union_find = UnionFind(n * 2) for i in range(size): union_find.union(stones[i][0], stones[i][1] + n) stones_set = set() for i in range(size): stones_set.add(union_find.find(stones[i][0])) return size - len(stones_set) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \alpha(n))$。其中 $n$ 是石子个数。$\alpha$ 是反 Ackerman 函数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0900-0999/n-repeated-element-in-size-2n-array.md ================================================ # [0961. 在长度 2N 的数组中找出重复 N 次的元素](https://leetcode.cn/problems/n-repeated-element-in-size-2n-array/) - 标签:数组、哈希表 - 难度:简单 ## 题目链接 - [0961. 在长度 2N 的数组中找出重复 N 次的元素 - 力扣](https://leetcode.cn/problems/n-repeated-element-in-size-2n-array/) ## 题目大意 **描述**: 给定一个整数数组 $nums$,该数组具有以下属性: - $nums.length == 2 \times n$。 - $nums$ 包含 $n + 1$ 个「不同的」元素。 - $nums$ 中恰有一个元素重复 $n$ 次。 **要求**: 找出并返回重复了 $n$ 次的那个元素。 **说明**: - $2 \le n \le 5000$。 - $nums.length == 2 \times n$。 - $0 \le nums[i] \le 10^{4}$。 - $nums$ 由 $n + 1$ 个「不同的」元素组成,且其中一个元素恰好重复 $n$ 次。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,3] 输出:3 ``` - 示例 2: ```python 输入:nums = [2,1,2,5,3,2] 输出:2 ``` ## 解题思路 ### 思路 1:哈希表 由于数组长度为 $2 \times n$,包含 $n+1$ 个不同元素,且恰有一个元素重复 $n$ 次,所以只需要找到出现次数最多的元素即可。 1. **使用哈希表**:遍历数组,统计每个元素的出现次数。 2. **返回结果**:返回出现次数为 $n$ 的元素。 **优化**:由于重复元素占据了一半的位置,我们可以使用更简单的方法:只要发现某个元素出现第二次,就返回它。 ### 思路 1:代码 ```python class Solution: def repeatedNTimes(self, nums: List[int]) -> int: seen = set() for num in nums: if num in seen: return num seen.add(num) return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组长度,最多遍历一次数组。 - **空间复杂度**:$O(n)$,需要使用哈希集合存储已访问的元素。 ================================================ FILE: docs/solutions/0900-0999/number-of-music-playlists.md ================================================ # [0920. 播放列表的数量](https://leetcode.cn/problems/number-of-music-playlists/) - 标签:数学、动态规划、组合数学 - 难度:困难 ## 题目链接 - [0920. 播放列表的数量 - 力扣](https://leetcode.cn/problems/number-of-music-playlists/) ## 题目大意 **描述**: 你的音乐播放器里有 $n$ 首不同的歌,在旅途中,你计划听 $goal$ 首歌(不一定不同,即,允许歌曲重复)。你将会按如下规则创建播放列表: - 每首歌「至少播放一次」。 - 一首歌只有在其他 $k$ 首歌播放完之后才能再次播放。 给定 $n$、$goal$ 和 $k$。 **要求**: 返回可以满足要求的播放列表的数量。由于答案可能非常大,请返回对 $10^9 + 7$ 取余 的结果。 **说明**: - $0 \le k \lt n \le goal \le 10^{3}$。 **示例**: - 示例 1: ```python 输入:n = 3, goal = 3, k = 1 输出:6 解释:有 6 种可能的播放列表。[1, 2, 3],[1, 3, 2],[2, 1, 3],[2, 3, 1],[3, 1, 2],[3, 2, 1] 。 ``` - 示例 2: ```python 输入:n = 2, goal = 3, k = 0 输出:6 解释:有 6 种可能的播放列表。[1, 1, 2],[1, 2, 1],[2, 1, 1],[2, 2, 1],[2, 1, 2],[1, 2, 2] 。 ``` ## 解题思路 ### 思路 1:动态规划 这是一个经典的组合计数问题,需要使用动态规划来解决。 1. **状态定义**:$dp[i][j]$ 表示长度为 $i$、包含 $j$ 首不同歌曲的播放列表数量。 2. **状态转移**:对于 $dp[i][j]$,考虑第 $i$ 首歌的选择: - **选择新歌**:从剩余的 $n - j + 1$ 首歌中选择,转移方程:$dp[i][j] = dp[i-1][j-1] \times (n - j + 1)$ - **选择旧歌**:必须是 $k$ 首歌之前播放过的,即至少有 $j - k$ 首歌可选,转移方程:$dp[i][j] = dp[i-1][j] \times \max(j - k, 0)$ 3. **初始化**:$dp[0][0] = 1$(空播放列表)。 4. **返回结果**:$dp[goal][n]$。 ### 思路 1:代码 ```python class Solution: def numMusicPlaylists(self, n: int, goal: int, k: int) -> int: MOD = 10**9 + 7 # dp[i][j] 表示长度为 i、包含 j 首不同歌曲的播放列表数量 dp = [[0] * (n + 1) for _ in range(goal + 1)] dp[0][0] = 1 for i in range(1, goal + 1): for j in range(1, min(i, n) + 1): # 选择新歌:从 n - j + 1 首歌中选择 dp[i][j] = dp[i - 1][j - 1] * (n - j + 1) % MOD # 选择旧歌:从已播放的 j 首歌中选择(需要满足 k 首歌的限制) if j > k: dp[i][j] = (dp[i][j] + dp[i - 1][j] * (j - k)) % MOD return dp[goal][n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(goal \times n)$,需要填充 $goal \times n$ 的 DP 表。 - **空间复杂度**:$O(goal \times n)$,需要使用二维数组存储 DP 状态。可以优化为 $O(n)$(滚动数组)。 ================================================ FILE: docs/solutions/0900-0999/number-of-recent-calls.md ================================================ # [0933. 最近的请求次数](https://leetcode.cn/problems/number-of-recent-calls/) - 标签:设计、队列、数据流 - 难度:简单 ## 题目链接 - [0933. 最近的请求次数 - 力扣](https://leetcode.cn/problems/number-of-recent-calls/) ## 题目大意 要求:实现一个用来计算特定时间范围内的最近请求的 `RecentCounter` 类: - `RecentCounter()` 初始化计数器,请求数为 0 。 - `int ping(int t)` 在时间 `t` 时添加一个新请求,其中 `t` 表示以毫秒为单位的某个时间,并返回在 `[t-3000, t]` 内发生的请求数。 ## 解题思路 使用一个队列,用于存储 `[t - 3000, t]` 范围内的请求。 获取请求数时,将队首所有小于 `t - 3000` 时间的请求将其从队列中移除,然后返回队列的长度即可。 ## 代码 ```python class RecentCounter: def __init__(self): self.queue = [] def ping(self, t: int) -> int: self.queue.append(t) while self.queue[0] < t - 3000: self.queue.pop(0) return len(self.queue) ``` ================================================ FILE: docs/solutions/0900-0999/number-of-squareful-arrays.md ================================================ # [0996. 平方数组的数目](https://leetcode.cn/problems/number-of-squareful-arrays/) - 标签:位运算、数组、哈希表、数学、动态规划、回溯、状态压缩 - 难度:困难 ## 题目链接 - [0996. 平方数组的数目 - 力扣](https://leetcode.cn/problems/number-of-squareful-arrays/) ## 题目大意 **描述**: 如果一个数组的任意两个相邻元素之和都是「完全平方数」,则该数组称为「平方数组」。 给定一个整数数组 $nums$。 **要求**: 返回所有属于「平方数组」的 $nums$ 的排列数量。 **说明**: - 如果存在某个索引 $i$ 使得 $perm1[i] \ne perm2[i]$,则认为两个排列 $perm1$ 和 $perm2$ 不同。 - $1 \le nums.length \le 12$。 - $0 \le nums[i] \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:nums = [1,17,8] 输出:2 解释:[1,8,17] 和 [17,8,1] 是有效的排列。 ``` - 示例 2: ```python 输入:nums = [2,2,2] 输出:1 ``` ## 解题思路 ### 思路 1:回溯 + 剪枝 这道题需要找到所有满足条件的排列,可以使用回溯算法。 1. **判断完全平方数**:首先实现一个函数判断两个数之和是否为完全平方数。 2. **回溯搜索**: - 使用回溯算法生成所有排列 - 在添加新元素时,检查与前一个元素的和是否为完全平方数 - 使用访问标记避免重复使用元素 3. **剪枝优化**: - 对数组进行排序,方便去重 - 如果当前元素与前一个元素相同且前一个元素未被使用,跳过(避免重复排列) - 预先计算哪些数字对可以相邻,构建图结构 ### 思路 1:代码 ```python class Solution: def numSquarefulPerms(self, nums: List[int]) -> int: import math def is_square(n): """判断是否为完全平方数""" root = int(math.sqrt(n)) return root * root == n nums.sort() n = len(nums) visited = [False] * n self.count = 0 def backtrack(path): if len(path) == n: self.count += 1 return for i in range(n): # 跳过已使用的元素 if visited[i]: continue # 去重:如果当前元素与前一个元素相同,且前一个元素未被使用,跳过 if i > 0 and nums[i] == nums[i - 1] and not visited[i - 1]: continue # 检查是否满足平方数条件 if path and not is_square(path[-1] + nums[i]): continue # 选择当前元素 visited[i] = True path.append(nums[i]) backtrack(path) # 撤销选择 path.pop() visited[i] = False backtrack([]) return self.count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n! \times n)$,其中 $n$ 是数组长度。最坏情况下需要遍历所有排列,每次检查需要 $O(n)$ 时间。 - **空间复杂度**:$O(n)$,递归栈和访问标记数组的空间。 ================================================ FILE: docs/solutions/0900-0999/numbers-at-most-n-given-digit-set.md ================================================ # [0902. 最大为 N 的数字组合](https://leetcode.cn/problems/numbers-at-most-n-given-digit-set/) - 标签:数组、数学、字符串、二分查找、动态规划 - 难度:困难 ## 题目链接 - [0902. 最大为 N 的数字组合 - 力扣](https://leetcode.cn/problems/numbers-at-most-n-given-digit-set/) ## 题目大意 **描述**:给定一个按非递减序列排列的数字数组 $digits$。我们可以使用任意次数的 $digits[i]$ 来写数字。例如,如果 `digits = ["1", "3", "5"]`,我们可以写数字,如 `"13"`, `"551"`, 和 `"1351315"`。 **要求**:返回可以生成的小于等于给定整数 $n$ 的正整数个数。 **说明**: - $1 \le digits.length \le 9$。 - $digits[i].length == 1$。 - $digits[i]$ 是从 `'1'` 到 `'9'` 的数。 - $digits$ 中的所有值都不同。 - $digits$ 按非递减顺序排列。 - $1 \le n \le 10^9$。 **示例**: - 示例 1: ```python 输入:digits = ["1","3","5","7"], n = 100 输出:20 解释: 可写出的 20 个数字是: 1, 3, 5, 7, 11, 13, 15, 17, 31, 33, 35, 37, 51, 53, 55, 57, 71, 73, 75, 77。 ``` - 示例 2: ```python 输入:digits = ["1","4","9"], n = 1000000000 输出:29523 解释: 我们可以写 3 个一位数字,9 个两位数字,27 个三位数字, 81 个四位数字,243 个五位数字,729 个六位数字, 2187 个七位数字,6561 个八位数字和 19683 个九位数字。 总共,可以使用D中的数字写出 29523 个整数。 ``` ## 解题思路 ### 思路 1:动态规划 + 数位 DP 数位 DP 模板的应用。因为这道题目中可以使用任意次数的 $digits[i]$,所以不需要用状态压缩的方式来表示数字集合。 这道题的具体步骤如下: 将 $n$ 转换为字符串 $s$,定义递归函数 `def dfs(pos, isLimit, isNum):` 表示构造第 $pos$ 位及之后所有数位的合法方案数。接下来按照如下步骤进行递归。 1. 从 `dfs(0, True, False)` 开始递归。 `dfs(0, True, False)` 表示: 1. 从位置 $0$ 开始构造。 2. 开始时受到数字 $n$ 对应最高位数位的约束。 3. 开始时没有填写数字。 2. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时: 1. 如果 $isNum == True$,说明当前方案符合要求,则返回方案数 $1$。 2. 如果 $isNum == False$,说明当前方案不符合要求,则返回方案数 $0$。 3. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:`ans = 0`。 4. 如果遇到 $isNum == False$,说明之前位数没有填写数字,当前位可以跳过,这种情况下方案数等于 $pos + 1$ 位置上没有受到 $pos$ 位的约束,并且之前没有填写数字时的方案数,即:`ans = dfs(i + 1, False, False)`。 5. 如果 $isNum == True$,则当前位必须填写一个数字。此时: 1. 根据 $isNum$ 和 $isLimit$ 来决定填当前位数位所能选择的最大数字($maxX$)。 2. 然后枚举 $digits$ 数组中所有能够填入的数字 $d$。 3. 如果 $d$ 超过了所能选择的最大数字 $maxX$ 则直接跳出循环。 4. 如果 $d$ 是合法数字,则方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, isLimit and d == maxX, True)`。 1. `isLimit and d == maxX` 表示 $pos + 1$ 位受到之前位限制和 $pos$ 位限制。 2. $isNum == True$ 表示 $pos$ 位选择了数字。 6. 最后的方案数为 `dfs(0, True, False)`,将其返回即可。 ### 思路 1:代码 ```python class Solution: def atMostNGivenDigitSet(self, digits: List[str], n: int) -> int: # 将 n 转换为字符串 s s = str(n) @cache # pos: 第 pos 个数位 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 # isNum: 表示 pos 前面的数位是否填了数字。 # 如果为真,则当前位不可跳过;如果为假,则当前位可跳过。 def dfs(pos, isLimit, isNum): if pos == len(s): # isNum 为 True,则表示当前方案符合要求 return int(isNum) ans = 0 if not isNum: # 如果 isNumb 为 False,则可以跳过当前数位 ans = dfs(pos + 1, False, False) # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = s[pos] if isLimit else '9' # 枚举可选择的数字 for d in digits: if d > maxX: break ans += dfs(pos + 1, isLimit and d == maxX, True) return ans return dfs(0, True, False) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times \log n)$,其中 $m$ 是数组 $digits$ 的长度,$\log n$ 是 $n$ 转为字符串之后的位数长度。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0900-0999/numbers-with-same-consecutive-differences.md ================================================ # [0967. 连续差相同的数字](https://leetcode.cn/problems/numbers-with-same-consecutive-differences/) - 标签:广度优先搜索、回溯 - 难度:中等 ## 题目链接 - [0967. 连续差相同的数字 - 力扣](https://leetcode.cn/problems/numbers-with-same-consecutive-differences/) ## 题目大意 **描述**: 给定两个整数 $n$ 和 $k$。 **要求**: 返回所有长度为 $n$ 且满足其每两个连续位上的数字之间的差的绝对值为 $k$ 的 非负整数。 你可以按「任何顺序」返回答案。 **说明**: - 注意:除了「数字 0」本身之外,答案中的每个数字都「不能」有前导零。例如,01 有一个前导零,所以是无效的;但 0 是有效的。 - $2 \le n \le 9$。 - $0 \le k \le 9$。 **示例**: - 示例 1: ```python 输入:n = 3, k = 7 输出:[181,292,707,818,929] 解释:注意,070 不是一个有效的数字,因为它有前导零。 ``` - 示例 2: ```python 输入:n = 2, k = 1 输出:[10,12,21,23,32,34,43,45,54,56,65,67,76,78,87,89,98] ``` ## 解题思路 ### 思路 1:广度优先搜索 使用 BFS 逐层构建满足条件的数字。 1. **初始化**:从 $1 \sim 9$ 开始(不能有前导零),将它们加入队列。 2. **BFS 扩展**:对于队列中的每个数字,尝试在末尾添加新的数字: - 新数字与当前数字的最后一位的差的绝对值必须等于 $k$ - 即可以添加 $\text{lastDigit} + k$ 或 $\text{lastDigit} - k$(如果在 $[0, 9]$ 范围内) 3. **长度控制**:当数字长度达到 $n$ 时,加入结果集。 4. **去重**:注意当 $k = 0$ 时,$\text{lastDigit} + k$ 和 $\text{lastDigit} - k$ 相同,需要避免重复添加。 ### 思路 1:代码 ```python class Solution: def numsSameConsecDiff(self, n: int, k: int) -> List[int]: # 初始化:从 1-9 开始 queue = list(range(1, 10)) # BFS 构建 n 位数 for _ in range(n - 1): next_queue = [] for num in queue: last_digit = num % 10 # 尝试添加 last_digit + k if last_digit + k <= 9: next_queue.append(num * 10 + last_digit + k) # 尝试添加 last_digit - k(避免重复) if k != 0 and last_digit - k >= 0: next_queue.append(num * 10 + last_digit - k) queue = next_queue return queue ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n)$,每个数字最多可以扩展出 $2$ 个新数字,最多有 $n$ 层。 - **空间复杂度**:$O(2^n)$,队列中最多存储 $O(2^n)$ 个数字。 ================================================ FILE: docs/solutions/0900-0999/odd-even-jump.md ================================================ # [0975. 奇偶跳](https://leetcode.cn/problems/odd-even-jump/) - 标签:栈、数组、动态规划、有序集合、排序、单调栈 - 难度:困难 ## 题目链接 - [0975. 奇偶跳 - 力扣](https://leetcode.cn/problems/odd-even-jump/) ## 题目大意 **描述**: 给定一个整数数组 $arr$,你可以从某一起始索引出发,跳跃一定次数。在你跳跃的过程中,第 1、3、5... 次跳跃称为奇数跳跃,而第 2、4、6... 次跳跃称为偶数跳跃。 你可以按以下方式从索引 $i$ 向后跳转到索引 $j$(其中 $i < j$): - 在进行奇数跳跃时(如,第 1,3,5... 次跳跃),你将会跳到索引 $j$,使得 $arr[i] \le arr[j]$,且 $arr[j]$ 的值尽可能小。如果存在多个这样的索引 $j$,你只能跳到满足要求的最小索引 $j$ 上。 - 在进行偶数跳跃时(如,第 2,4,6... 次跳跃),你将会跳到索引 $j$,使得 $arr[i] \ge arr[j]$,且 $arr[j]$ 的值尽可能大。如果存在多个这样的索引 $j$,你只能跳到满足要求的最小索引 $j$ 上。 - (对于某些索引 $i$,可能无法进行合乎要求的跳跃。) 如果从某一索引开始跳跃一定次数(可能是 0 次或多次),就可以到达数组的末尾(索引 $arr.length - 1$),那么该索引就会被认为是好的起始索引。 **要求**: 返回好的起始索引的数量。 **说明**: - $1 \le arr.length \le 20000$。 - $0 \le arr[i] \lt 10^{0000}$。 **示例**: - 示例 1: ```python 输入:[10,13,12,14,15] 输出:2 解释: 从起始索引 i = 0 出发,我们可以跳到 i = 2,(因为 arr[2] 是 arr[1],arr[2],arr[3],arr[4] 中大于或等于 arr[0] 的最小值),然后我们就无法继续跳下去了。 从起始索引 i = 1 和 i = 2 出发,我们可以跳到 i = 3,然后我们就无法继续跳下去了。 从起始索引 i = 3 出发,我们可以跳到 i = 4,到达数组末尾。 从起始索引 i = 4 出发,我们已经到达数组末尾。 总之,我们可以从 2 个不同的起始索引(i = 3, i = 4)出发,通过一定数量的跳跃到达数组末尾。 ``` - 示例 2: ```python 输入:[2,3,1,1,4] 输出:3 解释: 从起始索引 i=0 出发,我们依次可以跳到 i = 1,i = 2,i = 3: 在我们的第一次跳跃(奇数)中,我们先跳到 i = 1,因为 arr[1] 是(arr[1],arr[2],arr[3],arr[4])中大于或等于 arr[0] 的最小值。 在我们的第二次跳跃(偶数)中,我们从 i = 1 跳到 i = 2,因为 arr[2] 是(arr[2],arr[3],arr[4])中小于或等于 arr[1] 的最大值。arr[3] 也是最大的值,但 2 是一个较小的索引,所以我们只能跳到 i = 2,而不能跳到 i = 3。 在我们的第三次跳跃(奇数)中,我们从 i = 2 跳到 i = 3,因为 arr[3] 是(arr[3],arr[4])中大于或等于 arr[2] 的最小值。 我们不能从 i = 3 跳到 i = 4,所以起始索引 i = 0 不是好的起始索引。 类似地,我们可以推断: 从起始索引 i = 1 出发, 我们跳到 i = 4,这样我们就到达数组末尾。 从起始索引 i = 2 出发, 我们跳到 i = 3,然后我们就不能再跳了。 从起始索引 i = 3 出发, 我们跳到 i = 4,这样我们就到达数组末尾。 从起始索引 i = 4 出发,我们已经到达数组末尾。 总之,我们可以从 3 个不同的起始索引(i = 1, i = 3, i = 4)出发,通过一定数量的跳跃到达数组末尾。 ``` ## 解题思路 ### 思路 1:动态规划 + 单调栈 这是一道困难的动态规划问题,需要结合单调栈来优化。 1. **状态定义**: - $odd[i]$:从位置 $i$ 开始,第一次跳跃是奇数跳,能否到达终点 - $even[i]$:从位置 $i$ 开始,第一次跳跃是偶数跳,能否到达终点 2. **状态转移**: - 奇数跳:跳到满足 $arr[j] \ge arr[i]$ 且 $arr[j]$ 最小的位置 $j$ - 偶数跳:跳到满足 $arr[j] \le arr[i]$ 且 $arr[j]$ 最大的位置 $j$ 3. **单调栈优化**:使用单调栈预处理每个位置的下一跳位置。 4. **从后向前 DP**:从最后一个位置开始,逐步计算每个位置的状态。 ### 思路 1:代码 ```python class Solution: def oddEvenJumps(self, arr: List[int]) -> int: n = len(arr) # 计算下一跳位置 def make_next(sorted_indices): """使用单调栈计算下一跳位置""" result = [None] * n stack = [] for i in sorted_indices: while stack and i > stack[-1]: result[stack.pop()] = i stack.append(i) return result # 奇数跳:按值升序,索引升序排序 odd_next = make_next(sorted(range(n), key=lambda i: (arr[i], i))) # 偶数跳:按值降序,索引升序排序 even_next = make_next(sorted(range(n), key=lambda i: (-arr[i], i))) # DP:从后向前计算 odd = [False] * n even = [False] * n odd[n - 1] = even[n - 1] = True for i in range(n - 2, -1, -1): if odd_next[i] is not None: odd[i] = even[odd_next[i]] if even_next[i] is not None: even[i] = odd[even_next[i]] # 统计从奇数跳开始能到达终点的位置数 return sum(odd) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组长度。排序需要 $O(n \log n)$,单调栈和 DP 都是 $O(n)$。 - **空间复杂度**:$O(n)$,需要存储下一跳位置和 DP 状态。 ================================================ FILE: docs/solutions/0900-0999/online-election.md ================================================ # [0911. 在线选举](https://leetcode.cn/problems/online-election/) - 标签:设计、数组、哈希表、二分查找 - 难度:中等 ## 题目链接 - [0911. 在线选举 - 力扣](https://leetcode.cn/problems/online-election/) ## 题目大意 **描述**: 给定两个整数数组 $persons$ 和 $times$。在选举中,第 $i$ 张票是在时刻为 $times[i]$ 时投给候选人 $persons[i]$ 的。 对于发生在时刻 $t$ 的每个查询,需要找出在 $t$ 时刻在选举中领先的候选人的编号。 在 $t$ 时刻投出的选票也将被计入我们的查询之中。在平局的情况下,最近获得投票的候选人将会获胜。 **要求**: 实现 TopVotedCandidate 类: - `TopVotedCandidate(int[] persons, int[] times)` 使用 $persons$ 和 $times$ 数组初始化对象。 - `int q(int t)` 根据前面描述的规则,返回在时刻 $t$ 在选举中领先的候选人的编号。 **说明**: - $1 \le persons.length \le 5000$。 - $times.length == persons.length$。 - $0 \le persons[i] \lt persons.length$。 - $0 \le times[i] \le 10^{9}$。 - $times$ 是一个严格递增的有序数组。 - $times[0] \le t \le 10^{9}$。 - 每个测试用例最多调用 $10^{4}$ 次 `q`。 **示例**: - 示例 1: ```python 输入: ["TopVotedCandidate", "q", "q", "q", "q", "q", "q"] [[[0, 1, 1, 0, 0, 1, 0], [0, 5, 10, 15, 20, 25, 30]], [3], [12], [25], [15], [24], [8]] 输出: [null, 0, 1, 1, 0, 0, 1] 解释: TopVotedCandidate topVotedCandidate = new TopVotedCandidate([0, 1, 1, 0, 0, 1, 0], [0, 5, 10, 15, 20, 25, 30]); topVotedCandidate.q(3); // 返回 0 ,在时刻 3 ,票数分布为 [0] ,编号为 0 的候选人领先。 topVotedCandidate.q(12); // 返回 1 ,在时刻 12 ,票数分布为 [0,1,1] ,编号为 1 的候选人领先。 topVotedCandidate.q(25); // 返回 1 ,在时刻 25 ,票数分布为 [0,1,1,0,0,1] ,编号为 1 的候选人领先。(在平局的情况下,1 是最近获得投票的候选人)。 topVotedCandidate.q(15); // 返回 0 topVotedCandidate.q(24); // 返回 0 topVotedCandidate.q(8); // 返回 1 ``` ## 解题思路 ### 思路 1:预处理 + 二分查找 这道题需要快速查询某个时刻的领先候选人,可以通过预处理和二分查找来优化。 1. **预处理**:在初始化时,遍历所有投票记录,计算每个时刻的领先候选人。 - 使用哈希表记录每个候选人的票数 - 维护当前领先的候选人和最高票数 - 在平局时,选择最近获得投票的候选人 2. **二分查找**:查询时,使用二分查找找到不超过 $t$ 的最大时刻,返回该时刻的领先候选人。 ### 思路 1:代码 ```python class TopVotedCandidate: def __init__(self, persons: List[int], times: List[int]): self.times = times self.leaders = [] # 记录每个时刻的领先者 vote_count = collections.defaultdict(int) leader = -1 max_votes = 0 for person in persons: vote_count[person] += 1 # 票数更多,或票数相同但是最近获得投票 if vote_count[person] >= max_votes: leader = person max_votes = vote_count[person] self.leaders.append(leader) def q(self, t: int) -> int: # 二分查找:找到不超过 t 的最大时刻 left, right = 0, len(self.times) - 1 while left < right: mid = (left + right + 1) // 2 if self.times[mid] <= t: left = mid else: right = mid - 1 return self.leaders[left] # Your TopVotedCandidate object will be instantiated and called as such: # obj = TopVotedCandidate(persons, times) # param_1 = obj.q(t) ``` ### 思路 1:复杂度分析 - **时间复杂度**:初始化 $O(n)$,查询 $O(\log n)$,其中 $n$ 是投票记录的数量。 - **空间复杂度**:$O(n)$,需要存储每个时刻的领先者。 ================================================ FILE: docs/solutions/0900-0999/online-stock-span.md ================================================ # [0901. 股票价格跨度](https://leetcode.cn/problems/online-stock-span/) - 标签:栈、设计、数据流、单调栈 - 难度:中等 ## 题目链接 - [0901. 股票价格跨度 - 力扣](https://leetcode.cn/problems/online-stock-span/) ## 题目大意 要求:编写一个 `StockSpanner` 类,用于收集某些股票的每日报价,并返回该股票当日价格的跨度。 - 今天股票价格的跨度:股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。 例如:如果未来 7 天股票的价格是 `[100, 80, 60, 70, 60, 75, 85]`,那么股票跨度将是 `[1, 1, 1, 2, 1, 4, 6]`。 ## 解题思路 「求解小于或等于今天价格的最大连续日」等价于「求出左侧第一个比当前股票价格大的股票,并计算距离」。求出左侧第一个比当前股票价格大的股票我们可以使用「单调递减栈」来做。具体步骤如下: - 初始化方法:初始化一个空栈,即 `self.stack = []` - 求解今天股票价格的跨度: - 初始化跨度 `span` 为 `1`。 - 如果今日股票价格 `price` 大于等于栈顶元素 `self.stack[-1][0]`,则: - 将其弹出,即 `top = self.stack.pop()`。 - 跨度累加上弹出栈顶元素的跨度,即 `span += top[1]`。 - 继续判断,直到遇到一个今日股票价格 `price` 小于栈顶元素的元素位置,再将 `[price, span]` 压入栈中。 - 如果今日股票价格 `price` 小于栈顶元素 `self.stack[-1][0]`,则直接将 `[price, span]` 压入栈中。 - 最后输出今天股票价格的跨度 `span`。 ## 代码 ```python class StockSpanner: def __init__(self): self.stack = [] def next(self, price: int) -> int: span = 1 while self.stack and price >= self.stack[-1][0]: top = self.stack.pop() span += top[1] self.stack.append([price, span]) return span ``` ================================================ FILE: docs/solutions/0900-0999/pancake-sorting.md ================================================ # [0969. 煎饼排序](https://leetcode.cn/problems/pancake-sorting/) - 标签:贪心、数组、双指针、排序 - 难度:中等 ## 题目链接 - [0969. 煎饼排序 - 力扣](https://leetcode.cn/problems/pancake-sorting/) ## 题目大意 **描述**: 给定一个整数数组 $arr$,请使用「煎饼翻转」完成对数组的排序。 一次煎饼翻转的执行过程如下: - 选择一个整数 $k$,$1 \le k \le arr$.length$。 - 反转子数组 $arr[...k-1]$(下标从 0 开始)。 例如,$arr = [3,2,1,4]$,选择 $k = 3$ 进行一次煎饼翻转,反转子数组 $[3,2,1]$,得到 $arr = [1,2,3,4]$。 **要求**: 以数组形式返回能使 $arr$ 有序的煎饼翻转操作所对应的 $k$ 值序列。任何将数组排序且翻转次数在 $10 \times arr.length$ 范围内的有效答案都将被判断为正确。 **说明**: - $1 \le arr.length \le 10^{3}$。 - $1 \le arr[i] \le arr.length$。 - $arr$ 中的所有整数互不相同(即,$arr$ 是从 $1$ 到 $arr.length$ 整数的一个排列)。 **示例**: - 示例 1: ```python 输入:[3,2,4,1] 输出:[4,2,4,3] 解释: 我们执行 4 次煎饼翻转,k 值分别为 4,2,4,和 3。 初始状态 arr = [3, 2, 4, 1] 第一次翻转后(k = 4):arr = [1, 4, 2, 3] 第二次翻转后(k = 2):arr = [4, 1, 2, 3] 第三次翻转后(k = 4):arr = [3, 2, 1, 4] 第四次翻转后(k = 3):arr = [1, 2, 3, 4],此时已完成排序。 ``` - 示例 2: ```python 输入:[1,2,3] 输出:[] 解释: 输入已经排序,因此不需要翻转任何内容。 请注意,其他可能的答案,如 [3,3] ,也将被判断为正确。 ``` ## 解题思路 ### 思路 1:贪心 煎饼排序的核心思想是:每次找到未排序部分的最大值,将其翻转到最前面,再翻转到正确的位置。 1. **从后向前排序**:从数组末尾开始,每次将最大的未排序元素放到正确位置。 2. **两次翻转**: - 第一次翻转:将最大元素翻转到数组开头(翻转 $[0, maxIndex]$) - 第二次翻转:将最大元素翻转到目标位置(翻转 $[0, i]$) 3. **重复过程**:对剩余未排序部分重复上述过程。 **优化**:如果当前最大元素已经在正确位置,则跳过。 ### 思路 1:代码 ```python class Solution: def pancakeSort(self, arr: List[int]) -> List[int]: result = [] n = len(arr) # 从后向前排序 for target in range(n, 0, -1): # 找到目标值的位置 max_index = arr.index(target) # 如果已经在正确位置,跳过 if max_index == target - 1: continue # 如果不在开头,先翻转到开头 if max_index != 0: result.append(max_index + 1) arr[:max_index + 1] = arr[:max_index + 1][::-1] # 翻转到目标位置 result.append(target) arr[:target] = arr[:target][::-1] return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是数组长度。外层循环 $O(n)$ 次,每次查找和翻转需要 $O(n)$ 时间。 - **空间复杂度**:$O(1)$,不考虑返回结果的空间。 ================================================ FILE: docs/solutions/0900-0999/partition-array-into-disjoint-intervals.md ================================================ # [0915. 分割数组](https://leetcode.cn/problems/partition-array-into-disjoint-intervals/) - 标签:数组 - 难度:中等 ## 题目链接 - [0915. 分割数组 - 力扣](https://leetcode.cn/problems/partition-array-into-disjoint-intervals/) ## 题目大意 **描述**: 给定一个数组 $nums$,将其划分为两个连续子数组 $left$ 和 $right$,使得: - $left$ 中的每个元素都小于或等于 $right$ 中的每个元素。 - $left$ 和 $right$ 都是非空的。 - $left$ 的长度要尽可能小。 **要求**: 在完成这样的分组后返回 $left$ 的「长度」。 用例可以保证存在这样的划分方法。 **说明**: - $2 \le nums.length \le 10^{5}$。 - $0 \le nums[i] \le 10^{6}$。 - 可以保证至少有一种方法能够按题目所描述的那样对 $nums$ 进行划分。 **示例**: - 示例 1: ```python 输入:nums = [5,0,3,8,6] 输出:3 解释:left = [5,0,3],right = [8,6] ``` - 示例 2: ```python 输入:nums = [1,1,1,0,6,12] 输出:4 解释:left = [1,1,1,0],right = [6,12] ``` ## 解题思路 ### 思路 1:前缀最大值 + 后缀最小值 要满足 $left$ 中的每个元素都小于或等于 $right$ 中的每个元素,即 $left$ 的最大值要小于或等于 $right$ 的最小值。 1. **预处理**: - 计算前缀最大值数组 $max\_left[i]$:表示 $[0, i]$ 范围内的最大值 - 计算后缀最小值数组 $min\_right[i]$:表示 $[i, n-1]$ 范围内的最小值 2. **查找分割点**:从左到右遍历,找到第一个满足 $max\_left[i] \le min\_right[i+1]$ 的位置 $i$,返回 $i+1$(即 $left$ 的长度)。 ### 思路 1:代码 ```python class Solution: def partitionDisjoint(self, nums: List[int]) -> int: n = len(nums) # 计算前缀最大值 max_left = [0] * n max_left[0] = nums[0] for i in range(1, n): max_left[i] = max(max_left[i - 1], nums[i]) # 计算后缀最小值 min_right = [0] * n min_right[n - 1] = nums[n - 1] for i in range(n - 2, -1, -1): min_right[i] = min(min_right[i + 1], nums[i]) # 查找分割点 for i in range(n - 1): if max_left[i] <= min_right[i + 1]: return i + 1 return n - 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组长度,需要遍历三次数组。 - **空间复杂度**:$O(n)$,需要存储前缀最大值和后缀最小值数组。 ================================================ FILE: docs/solutions/0900-0999/powerful-integers.md ================================================ # [0970. 强整数](https://leetcode.cn/problems/powerful-integers/) - 标签:哈希表、数学、枚举 - 难度:中等 ## 题目链接 - [0970. 强整数 - 力扣](https://leetcode.cn/problems/powerful-integers/) ## 题目大意 **描述**: 给定三个整数 $x$、$y$ 和 $bound$。 **要求**: 返回值小于或等于 $bound$ 的所有「强整数」组成的列表。 **说明**: - 如果某一整数可以表示为 $x^i + y^j$ ,其中整数 $i \le 0$ 且 $j \le 0$,那么我们认为该整数是一个「强整数」。 - 你可以按任何顺序返回答案。在你的回答中,每个值「最多」出现一次。 - $1 \le x, y \le 10^{3}$。 - $0 \le bound \le 10^{6}$。 **示例**: - 示例 1: ```python 输入:x = 2, y = 3, bound = 10 输出:[2,3,4,5,7,9,10] 解释: 2 = 20 + 30 3 = 21 + 30 4 = 20 + 31 5 = 21 + 31 7 = 22 + 31 9 = 23 + 30 10 = 20 + 32 ``` - 示例 2: ```python 输入:x = 3, y = 5, bound = 15 输出:[2,4,6,8,10,14] ``` ## 解题思路 ### 思路 1:枚举 根据题意,强整数可以表示为 $x^i + y^j$,其中 $i \ge 0$,$j \ge 0$。由于结果要小于等于 $bound$,我们可以枚举所有可能的 $i$ 和 $j$。 1. **确定枚举范围**: - 当 $x = 1$ 时,$x^i = 1$(对所有 $i$) - 当 $x > 1$ 时,$x^i$ 最多枚举到 $\log_x(bound)$ - 同理,$y^j$ 也有类似的范围 2. **枚举所有组合**:使用两层循环枚举所有可能的 $i$ 和 $j$,计算 $x^i + y^j$。 3. **去重**:使用集合存储结果,自动去重。 4. **边界处理**:当 $x = 1$ 或 $y = 1$ 时,只需要枚举一次即可。 ### 思路 1:代码 ```python class Solution: def powerfulIntegers(self, x: int, y: int, bound: int) -> List[int]: result = set() # 确定 x 的幂次上限 x_limit = 20 if x > 1 else 1 # x^20 > 10^6 y_limit = 20 if y > 1 else 1 # y^20 > 10^6 # 枚举所有可能的 i 和 j for i in range(x_limit): x_power = x ** i if x_power > bound: break for j in range(y_limit): y_power = y ** j total = x_power + y_power if total <= bound: result.add(total) # 如果 y = 1,只需要枚举一次 if y == 1: break # 如果 x = 1,只需要枚举一次 if x == 1: break return list(result) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log^2 \text{bound})$,最多枚举 $\log_x(\text{bound}) \times \log_y(\text{bound})$ 次。 - **空间复杂度**:$O(\log^2 \text{bound})$,集合中最多存储 $O(\log^2 \text{bound})$ 个元素。 ================================================ FILE: docs/solutions/0900-0999/prison-cells-after-n-days.md ================================================ # [0957. N 天后的牢房](https://leetcode.cn/problems/prison-cells-after-n-days/) - 标签:位运算、数组、哈希表、数学 - 难度:中等 ## 题目链接 - [0957. N 天后的牢房 - 力扣](https://leetcode.cn/problems/prison-cells-after-n-days/) ## 题目大意 **描述**: 监狱中 8 间牢房排成一排,每间牢房可能被占用或空置。 每天,无论牢房是被占用或空置,都会根据以下规则进行变更: - 如果一间牢房的两个相邻的房间都被占用或都是空的,那么该牢房就会被占用。 - 否则,它就会被空置。 注意:由于监狱中的牢房排成一行,所以行中的第一个和最后一个牢房不存在两个相邻的房间。 给你一个整数数组 $cells$,用于表示牢房的初始状态:如果第 $i$ 间牢房被占用,则 $cell[i]==1$,否则 $cell[i]==0$。另给你一个整数 $n$。 **要求**: 请你返回 $n$ 天后监狱的状况(即,按上文描述进行 $n$ 次变更)。 **说明**: - $cells.length == 8$。 - $cells[i]$ 为 0 或 1。 - $1 \le n \le 10^{9}$。 **示例**: - 示例 1: ```python 输入:cells = [0,1,0,1,1,0,0,1], n = 7 输出:[0,0,1,1,0,0,0,0] 解释:下表总结了监狱每天的状况: Day 0: [0, 1, 0, 1, 1, 0, 0, 1] Day 1: [0, 1, 1, 0, 0, 0, 0, 0] Day 2: [0, 0, 0, 0, 1, 1, 1, 0] Day 3: [0, 1, 1, 0, 0, 1, 0, 0] Day 4: [0, 0, 0, 0, 0, 1, 0, 0] Day 5: [0, 1, 1, 1, 0, 1, 0, 0] Day 6: [0, 0, 1, 0, 1, 1, 0, 0] Day 7: [0, 0, 1, 1, 0, 0, 0, 0] ``` - 示例 2: ```python 输入:cells = [1,0,0,1,0,0,1,0], n = 1000000000 输出:[0,0,1,1,1,1,1,0] ``` ## 解题思路 ### 思路 1:哈希表 + 找规律 由于牢房只有 $8$ 间,状态数量有限(最多 $2^8 = 256$ 种),必然会出现循环。我们可以利用这个特性来优化。 1. **模拟变化**:按照规则模拟牢房状态的变化。 2. **检测循环**:使用哈希表记录每个状态第一次出现的天数,当出现重复状态时,说明进入循环。 3. **计算结果**: - 如果 $n$ 天内没有循环,直接返回第 $n$ 天的状态 - 如果出现循环,计算循环周期,通过取模快速得到第 $n$ 天的状态 4. **边界处理**:注意第一个和最后一个牢房始终为空。 ### 思路 1:代码 ```python class Solution: def prisonAfterNDays(self, cells: List[int], n: int) -> List[int]: def next_day(cells): """计算下一天的状态""" new_cells = [0] * 8 for i in range(1, 7): # 两个相邻房间状态相同,则被占用 new_cells[i] = 1 if cells[i - 1] == cells[i + 1] else 0 return new_cells seen = {} day = 0 while day < n: # 将状态转换为元组(可哈希) state = tuple(cells) # 检测循环 if state in seen: # 计算循环周期 cycle_length = day - seen[state] # 跳过完整的循环 remaining_days = (n - day) % cycle_length # 继续模拟剩余天数 for _ in range(remaining_days): cells = next_day(cells) return cells # 记录当前状态 seen[state] = day # 模拟下一天 cells = next_day(cells) day += 1 return cells ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\min(n, 2^k))$,其中 $k = 8$ 是牢房数量。最多模拟 $2^8 = 256$ 天就会出现循环。 - **空间复杂度**:$O(2^k)$,哈希表最多存储 $256$ 个状态。 ================================================ FILE: docs/solutions/0900-0999/range-sum-of-bst.md ================================================ # [0938. 二叉搜索树的范围和](https://leetcode.cn/problems/range-sum-of-bst/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:简单 ## 题目链接 - [0938. 二叉搜索树的范围和 - 力扣](https://leetcode.cn/problems/range-sum-of-bst/) ## 题目大意 给定一个二叉搜索树,和一个范围 [low, high]。求范围 [low, high] 之间所有节点的值的和。 ## 解题思路 二叉搜索树的定义: - 如果左子树不为空,则左子树上所有节点值均小于它的根节点值; - 如果右子树不为空,则右子树上所有节点值均大于它的根节点值; - 任意节点的左、右子树也分别为二叉搜索树。 这道题求解 [low, high] 之间所有节点的值的和,需要递归求解。 - 当前节点为 None 时返回 0; - 当前节点值 val > high 时,则返回左子树之和; - 当前节点值 val < low 时,则返回右子树之和; - 当前节点 val <= high,且 val >= low 时,则返回当前节点值 + 左子树之和 + 右子树之和。 ## 代码 ```python class Solution: def rangeSumBST(self, root: TreeNode, low: int, high: int) -> int: if not root: return 0 if root.val > high: return self.rangeSumBST(root.left, low, high) if root.val < low: return self.rangeSumBST(root.right, low, high) return root.val + self.rangeSumBST(root.left, low, high) + self.rangeSumBST(root.right, low, high) ``` ================================================ FILE: docs/solutions/0900-0999/regions-cut-by-slashes.md ================================================ # [0959. 由斜杠划分区域](https://leetcode.cn/problems/regions-cut-by-slashes/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [0959. 由斜杠划分区域 - 力扣](https://leetcode.cn/problems/regions-cut-by-slashes/) ## 题目大意 **描述**:在由 $1 \times 1$ 方格组成的 $n \times n$ 网格 $grid$ 中,每个 $1 \times 1$ 方块由 `'/'`、`'\'` 或 `' '` 构成。这些字符会将方块划分为一些共边的区域。 现在给定代表网格的二维数组 $grid$。 **要求**:返回区域的数目。 **说明**: - 反斜杠字符是转义的,因此 `'\'` 用 `'\\'` 表示。 - $n == grid.length == grid[i].length$。 - $1 \le n \le 30$。 - $grid[i][j]$ 是 `'/'`、`'\'` 或 `' '`。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2018/12/15/1.png) ```python 输入:grid = [" /","/ "] 输出:2 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2018/12/15/4.png) ```python 输入:grid = ["/\\","\\/"] 输出:5 解释:回想一下,因为 \ 字符是转义的,所以 "/\\" 表示 /\,而 "\\/" 表示 \/。 ``` ## 解题思路 ### 思路 1:并查集 我们把一个 $1 \times 1$ 的单元格分割成逻辑上的 $4$ 个部分,则 `' '`、`'/'`、`'\'` 可以将 $1 \times 1$ 的方格分割为以下三种形态: ![](http://qcdn.itcharge.cn/images/20210827142447.png) 在进行遍历的时候,需要将联通的部分进行合并,并统计出联通的块数。这就需要用到了并查集。 遍历二维数组 $gird$,然后在「单元格内」和「单元格间」进行合并。 现在我们为单元格的每个小三角部分按顺时针方向都编上编号,起始位置为左边。然后单元格间的编号按照从左到右,从上到下的位置进行编号,如下图所示: ![](http://qcdn.itcharge.cn/images/20210827143836.png) 假设当前单元格的起始位置为 $index$,则合并策略如下: - 如果是单元格内: - 如果是空格:合并 $index$、$index + 1$、$index + 2$、$index + 3$。 - 如果是 `'/'`:合并 $index$ 和 $index + 1$,合并 $index + 2$ 和 $index + 3$。 - 如果是 `'\'`:合并 $index$ 和 $index + 3$,合并 $index + 1$ 和 $index + 2$。 - 如果是单元格间,则向下向右进行合并: - 向下:合并 $index + 3$ 和 $index + 4 * size + 1 $。 - 向右:合并 $index + 2$ 和 $index + 4$。 最后合并完成之后,统计并查集中连通分量个数即为答案。 ### 思路 1:代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] self.count = n def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.count -= 1 def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def regionsBySlashes(self, grid: List[str]) -> int: size = len(grid) m = 4 * size * size union_find = UnionFind(m) for i in range(size): for j in range(size): index = 4 * (i * size + j) ch = grid[i][j] if ch == '/': union_find.union(index, index + 1) union_find.union(index + 2, index + 3) elif ch == '\\': union_find.union(index, index + 3) union_find.union(index + 1, index + 2) else: union_find.union(index, index + 1) union_find.union(index + 1, index + 2) union_find.union(index + 2, index + 3) if j + 1 < size: union_find.union(index + 2, index + 4) if i + 1 < size: union_find.union(index + 3, index + 4 * size + 1) return union_find.count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \times \alpha(n^2))$,其中 $\alpha$ 是反 `Ackerman` 函数。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/0900-0999/reorder-data-in-log-files.md ================================================ # [0937. 重新排列日志文件](https://leetcode.cn/problems/reorder-data-in-log-files/) - 标签:数组、字符串、排序 - 难度:中等 ## 题目链接 - [0937. 重新排列日志文件 - 力扣](https://leetcode.cn/problems/reorder-data-in-log-files/) ## 题目大意 **描述**: 给定一个日志数组 $logs$。每条日志都是以空格分隔的字串,其第一个字为字母与数字混合的「标识符」。 有两种不同类型的日志: - 字母日志:除标识符之外,所有字均由小写字母组成 - 数字日志:除标识符之外,所有字均由数字组成 请按下述规则将日志重新排序: - 所有「字母日志」都排在「数字日志」之前。 - 「字母日志」在内容不同时,忽略标识符后,按内容字母顺序排序;在内容相同时,按标识符排序。 - 「数字日志」应该保留原来的相对顺序。 **要求**: 返回日志的最终顺序。 **说明**: - $1 \le logs.length \le 10^{3}$。 - $3 \le logs[i].length \le 10^{3}$。 - $logs[i]$ 中,字与字之间都用 单个 空格分隔。 - 题目数据保证 $logs[i]$ 都有一个标识符,并且在标识符之后至少存在一个字。 **示例**: - 示例 1: ```python 输入:logs = ["dig1 8 1 5 1","let1 art can","dig2 3 6","let2 own kit dig","let3 art zero"] 输出:["let1 art can","let3 art zero","let2 own kit dig","dig1 8 1 5 1","dig2 3 6"] 解释: 字母日志的内容都不同,所以顺序为 "art can", "art zero", "own kit dig" 。 数字日志保留原来的相对顺序 "dig1 8 1 5 1", "dig2 3 6" 。 ``` - 示例 2: ```python 输入:logs = ["a1 9 2 3 1","g1 act car","zo4 4 7","ab1 off key dog","a8 act zoo"] 输出:["g1 act car","a8 act zoo","ab1 off key dog","a1 9 2 3 1","zo4 4 7"] ``` ## 解题思路 ### 思路 1:自定义排序 根据题目要求,需要对日志进行自定义排序。 1. **分类日志**:将日志分为字母日志和数字日志。 2. **排序规则**: - 字母日志排在数字日志之前 - 字母日志按内容排序,内容相同时按标识符排序 - 数字日志保持原有顺序 3. **实现方式**:使用自定义排序函数,返回排序键。 ### 思路 1:代码 ```python class Solution: def reorderLogFiles(self, logs: List[str]) -> List[str]: def sort_key(log): identifier, content = log.split(' ', 1) # 判断是字母日志还是数字日志 if content[0].isalpha(): # 字母日志:返回 (0, 内容, 标识符) return (0, content, identifier) else: # 数字日志:返回 (1,),保持原有顺序 return (1,) return sorted(logs, key=sort_key) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n \cdot m)$,其中 $n$ 是日志数量,$m$ 是日志的平均长度。排序需要 $O(n \log n)$ 次比较,每次比较需要 $O(m)$ 时间。 - **空间复杂度**:$O(n \cdot m)$,排序需要的额外空间。 ================================================ FILE: docs/solutions/0900-0999/reveal-cards-in-increasing-order.md ================================================ # [0950. 按递增顺序显示卡牌](https://leetcode.cn/problems/reveal-cards-in-increasing-order/) - 标签:队列、数组、排序、模拟 - 难度:中等 ## 题目链接 - [0950. 按递增顺序显示卡牌 - 力扣](https://leetcode.cn/problems/reveal-cards-in-increasing-order/) ## 题目大意 **描述**: 给定一个数组 $A$ 代表牌组。牌组中的每张卡牌都对应有一个唯一的整数。你可以按你想要的顺序对这套卡片进行排序。 最初,这些卡牌在牌组里是正面朝下的(即,未显示状态)。 现在,重复执行以下步骤,直到显示所有卡牌为止: 1. 从牌组顶部抽一张牌,显示它,然后将其从牌组中移出。 2. 如果牌组中仍有牌,则将下一张处于牌组顶部的牌放在牌组的底部。 3. 如果仍有未显示的牌,那么返回步骤 1。否则,停止行动。 **要求**: 返回能以递增顺序显示卡牌的牌组顺序。 答案中的第一张牌被认为处于牌堆顶部。 **说明**: - $1 \le A.length \le 10^{3}$。 - $1 \le A[i] \le 10^6$。 - 对于所有的 $i \ne j$,$A[i] \ne A[j]$。 **示例**: - 示例 1: ```python 输入:[17,13,11,2,3,5,7] 输出:[2,13,3,11,5,17,7] 解释: 我们得到的牌组顺序为 [17,13,11,2,3,5,7](这个顺序不重要),然后将其重新排序。 重新排序后,牌组以 [2,13,3,11,5,17,7] 开始,其中 2 位于牌组的顶部。 我们显示 2,然后将 13 移到底部。牌组现在是 [3,11,5,17,7,13]。 我们显示 3,并将 11 移到底部。牌组现在是 [5,17,7,13,11]。 我们显示 5,然后将 17 移到底部。牌组现在是 [7,13,11,17]。 我们显示 7,并将 13 移到底部。牌组现在是 [11,17,13]。 我们显示 11,然后将 17 移到底部。牌组现在是 [13,17]。 我们展示 13,然后将 17 移到底部。牌组现在是 [17]。 我们显示 17。 由于所有卡片都是按递增顺序排列显示的,所以答案是正确的。 ``` ## 解题思路 ### 思路 1:队列模拟 这道题需要逆向思考:从最终的递增顺序反推初始的牌组顺序。 1. **逆向模拟**: - 将排序后的牌从小到大依次放入结果 - 模拟逆向操作:如果正向是"抽牌、移底",逆向就是"放底、移顶" 2. **使用队列**: - 初始化一个索引队列 $[0, 1, 2, ..., n-1]$ - 模拟抽牌过程,记录每次抽牌的位置 - 将排序后的牌按照抽牌顺序放入对应位置 3. **具体步骤**: - 每次从队列头部取出一个索引(对应抽牌位置) - 如果队列不为空,将队列头部的下一个索引移到队列尾部(对应移底操作) ### 思路 1:代码 ```python class Solution: def deckRevealedIncreasing(self, deck: List[int]) -> List[int]: n = len(deck) deck.sort() # 先排序 # 使用队列模拟索引 queue = collections.deque(range(n)) result = [0] * n for card in deck: # 当前卡牌放到队列头部对应的位置 idx = queue.popleft() result[idx] = card # 如果队列不为空,将下一个索引移到队列尾部 if queue: queue.append(queue.popleft()) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是牌的数量。排序需要 $O(n \log n)$,模拟过程需要 $O(n)$。 - **空间复杂度**:$O(n)$,需要使用队列存储索引。 ================================================ FILE: docs/solutions/0900-0999/reverse-only-letters.md ================================================ # [0917. 仅仅反转字母](https://leetcode.cn/problems/reverse-only-letters/) - 标签:双指针、字符串 - 难度:简单 ## 题目链接 - [0917. 仅仅反转字母 - 力扣](https://leetcode.cn/problems/reverse-only-letters/) ## 题目大意 **描述**: 给定一个字符串 $s$ ,根据下述规则反转字符串: - 所有非英文字母保留在原有位置。 - 所有英文字母(小写或大写)位置反转。 **要求**: 返回反转后的 $s$。 **说明**: - $1 \le s.length \le 100$ - $s$ 仅由 ASCII 值在范围 $[33, 122]$ 的字符组成 - $s$ 不含 `'\"'` 或 `'\\'` **示例**: - 示例 1: ```python 输入:s = "ab-cd" 输出:"dc-ba" ``` - 示例 2: ```python 输入:s = "a-bC-dEf-ghIj" 输出:"j-Ih-gfE-dCba" ``` ## 解题思路 ### 思路 1:双指针 使用双指针分别从字符串的两端向中间移动,只交换英文字母,跳过非英文字母。 1. **初始化**:将字符串转换为列表(Python 字符串不可变),使用左右指针 $left$ 和 $right$。 2. **双指针移动**: - 如果 $s[left]$ 不是字母,$left$ 右移 - 如果 $s[right]$ 不是字母,$right$ 左移 - 如果两者都是字母,交换它们,然后同时移动 3. **返回结果**:将列表转换回字符串。 ### 思路 1:代码 ```python class Solution: def reverseOnlyLetters(self, s: str) -> str: s = list(s) left, right = 0, len(s) - 1 while left < right: # 左指针找到字母 if not s[left].isalpha(): left += 1 # 右指针找到字母 elif not s[right].isalpha(): right -= 1 # 交换两个字母 else: s[left], s[right] = s[right], s[left] left += 1 right -= 1 return ''.join(s) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是字符串长度,每个字符最多访问一次。 - **空间复杂度**:$O(n)$,需要将字符串转换为列表。 ================================================ FILE: docs/solutions/0900-0999/rle-iterator.md ================================================ # [0900. RLE 迭代器](https://leetcode.cn/problems/rle-iterator/) - 标签:设计、数组、计数、迭代器 - 难度:中等 ## 题目链接 - [0900. RLE 迭代器 - 力扣](https://leetcode.cn/problems/rle-iterator/) ## 题目大意 **描述**:我们可以使用游程编码(即 RLE)来编码一个整数序列。在偶数长度 $encoding$ ( 从 $0$ 开始 )的游程编码数组中,对于所有偶数 $i$,$encoding[i]$ 告诉我们非负整数 $encoding[i + 1]$ 在序列中重复的次数。 - 例如,序列 $arr = [8,8,8,5,5]$ 可以被编码为 $encoding =[3,8,2,5]$。$encoding =[3,8,0,9,2,5]$ 和 $encoding =[2,8,1,8,2,5]$ 也是 $arr$ 有效的 RLE。 给定一个游程长度的编码数组 $encoding$。 **要求**:设计一个迭代器来遍历它。 实现 `RLEIterator` 类: - `RLEIterator(int[] encoded)` 用编码后的数组初始化对象。 - `int next(int n)` 以这种方式耗尽后 $n$ 个元素并返回最后一个耗尽的元素。如果没有剩余的元素要耗尽,则返回 $-1$。 **说明**: - $2 \le encoding.length \le 1000$。 - $encoding.length$ 为偶。 - $0 \le encoding[i] \le 10^9$。 - $1 \le n \le 10^9$。 - 每个测试用例调用 `next` 不高于 $1000$ 次。 **示例**: - 示例 1: ```python 输入: ["RLEIterator","next","next","next","next"] [[[3,8,0,9,2,5]],[2],[1],[1],[2]] 输出: [null,8,8,5,-1] 解释: RLEIterator rLEIterator = new RLEIterator([3, 8, 0, 9, 2, 5]); // 这映射到序列 [8,8,8,5,5]。 rLEIterator.next(2); // 耗去序列的 2 个项,返回 8。现在剩下的序列是 [8, 5, 5]。 rLEIterator.next(1); // 耗去序列的 1 个项,返回 8。现在剩下的序列是 [5, 5]。 rLEIterator.next(1); // 耗去序列的 1 个项,返回 5。现在剩下的序列是 [5]。 rLEIterator.next(2); // 耗去序列的 2 个项,返回 -1。 这是由于第一个被耗去的项是 5, 但第二个项并不存在。由于最后一个要耗去的项不存在,我们返回 -1。 ``` ## 解题思路 ### 思路 1:模拟 1. 初始化时: 1. 保存数组 $encoding$ 作为成员变量。 2. 保存当前位置 $index$,表示当前迭代器指向元素 $encoding[index + 1]$。初始化赋值为 $0$。 3. 保存当前指向元素 $encoding[index + 1]$ 已经被删除的元素个数 $d\_cnt$。初始化赋值为 $0$。 2. 调用 `next(n)` 时: 1. 对于当前元素,先判断当前位置是否超出 $encoding$ 范围,超过则直接返回 $-1$。 2. 如果未超过,再判断当前元素剩余个数 $encoding[index] - d\_cnt$ 是否小于 $n$ 个。 1. 如果小于 $n$ 个,则删除当前元素剩余所有个数,并指向下一位置继续删除剩余元素。 2. 如果等于大于等于 $n$ 个,则令当前指向元素 $encoding[index + 1]$ 已经被删除的元素个数 $d\_cnt$ 加上 $n$。 ### 思路 1:代码 ```Python class RLEIterator: def __init__(self, encoding: List[int]): self.encoding = encoding self.index = 0 self.d_cnt = 0 def next(self, n: int) -> int: while self.index < len(self.encoding): if self.d_cnt + n > self.encoding[self.index]: n -= self.encoding[self.index] - self.d_cnt self.d_cnt = 0 self.index += 2 else: self.d_cnt += n return self.encoding[self.index + 1] return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 为数组 $encoding$ 的长度,$m$ 是调用 `next(n)` 的次数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0900-0999/rotting-oranges.md ================================================ # [0994. 腐烂的橘子](https://leetcode.cn/problems/rotting-oranges/) - 标签:广度优先搜索、数组、矩阵 - 难度:中等 ## 题目链接 - [0994. 腐烂的橘子 - 力扣](https://leetcode.cn/problems/rotting-oranges/) ## 题目大意 **描述**: $m \times n$ 网格 $grid$ 中,每个单元格可以有以下三个值之一: - 值 0 代表空单元格; - 值 1 代表新鲜橘子; - 值 2 代表腐烂的橘子。 每分钟,腐烂的橘子「周围 4 个方向上相邻」的新鲜橘子都会腐烂。 **要求**: 返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。 **说明**: - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 10$。 - $grid[i][j]$ 仅为 0、1 或 2。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/02/16/oranges.png) ```python 输入:grid = [[2,1,1],[1,1,0],[0,1,1]] 输出:4 ``` - 示例 2: ```python 输入:grid = [[2,1,1],[0,1,1],[1,0,1]] 输出:-1 解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个方向上。 ``` ## 解题思路 ### 思路 1:广度优先搜索 这是一个多源 BFS 问题,所有腐烂的橘子同时开始扩散。 1. **初始化**: - 遍历矩阵,统计新鲜橘子的数量 - 将所有腐烂橘子的位置加入队列 2. **BFS 扩展**: - 每一轮 BFS 代表一分钟 - 从队列中取出所有腐烂橘子,向四个方向扩散 - 如果相邻位置是新鲜橘子,将其变为腐烂,加入队列,新鲜橘子数量减 $1$ 3. **判断结果**: - 如果最后还有新鲜橘子,返回 $-1$ - 否则返回经过的分钟数 ### 思路 1:代码 ```python class Solution: def orangesRotting(self, grid: List[List[int]]) -> int: m, n = len(grid), len(grid[0]) queue = collections.deque() fresh_count = 0 # 统计新鲜橘子数量,收集腐烂橘子位置 for i in range(m): for j in range(n): if grid[i][j] == 1: fresh_count += 1 elif grid[i][j] == 2: queue.append((i, j)) # 如果没有新鲜橘子,直接返回 0 if fresh_count == 0: return 0 # BFS 扩散 directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] minutes = 0 while queue: size = len(queue) for _ in range(size): x, y = queue.popleft() for dx, dy in directions: nx, ny = x + dx, y + dy # 如果相邻位置是新鲜橘子 if 0 <= nx < m and 0 <= ny < n and grid[nx][ny] == 1: grid[nx][ny] = 2 # 变为腐烂 fresh_count -= 1 queue.append((nx, ny)) # 如果队列不为空,说明这一轮有橘子腐烂 if queue: minutes += 1 # 如果还有新鲜橘子,返回 -1 return minutes if fresh_count == 0 else -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$ 和 $n$ 是矩阵的行数和列数,每个格子最多访问一次。 - **空间复杂度**:$O(m \times n)$,队列中最多存储所有格子。 ================================================ FILE: docs/solutions/0900-0999/satisfiability-of-equality-equations.md ================================================ # [0990. 等式方程的可满足性](https://leetcode.cn/problems/satisfiability-of-equality-equations/) - 标签:并查集、图、数组、字符串 - 难度:中等 ## 题目链接 - [0990. 等式方程的可满足性 - 力扣](https://leetcode.cn/problems/satisfiability-of-equality-equations/) ## 题目大意 **描述**:给定一个由字符串方程组成的数组 `equations`,每个字符串方程 `equations[i]` 的长度为 `4`,有以下两种形式组成:`a==b` 或 `a!=b`。`a` 和 `b` 是小写字母,表示单字母变量名。 **要求**:判断所有的字符串方程是否能同时满足,如果能同时满足,返回 `True`,否则返回 `False`。 **说明**: - $1 \le equations.length \le 500$。 - $equations[i].length == 4$。 - $equations[i][0]$ 和 $equations[i][3]$ 是小写字母。 - $equations[i][1]$ 要么是 `'='`,要么是 `'!'`。 - `equations[i][2]` 是 `'='`。 **示例**: - 示例 1: ```python 输入:["a==b","b!=a"] 输出:False 解释:如果我们指定,a = 1 且 b = 1 那么可以满足第一个方程,但无法满足第二个方程。 没有办法分配变量同时满足这两个方程。 ``` ## 解题思路 ### 思路 1:并查集 字符串方程只有 `==` 或者 `!=`,可以考虑将相等的遍历划分到相同集合中,然后再遍历所有不等式方程,看方程的两个变量是否在之前划分的相同集合中,如果在则说明不满足。 这就需要用到并查集,具体操作如下: - 遍历所有等式方程,将等式两边的单字母变量顶点进行合并。 - 遍历所有不等式方程,检查不等式两边的单字母遍历是不是在一个连通分量中,如果在则返回 `False`,否则继续扫描。如果所有不等式检查都没有矛盾,则返回 `True`。 ### 思路 1:并查集代码 ```python class UnionFind: def __init__(self, n): # 初始化 self.fa = [i for i in range(n)] # 每个元素的集合编号初始化为数组 fa 的下标索引 def __find(self, x): # 查找元素根节点的集合编号内部实现方法 while self.fa[x] != x: # 递归查找元素的父节点,直到根节点 self.fa[x] = self.fa[self.fa[x]] # 隔代压缩优化 x = self.fa[x] return x # 返回元素根节点的集合编号 def union(self, x, y): # 合并操作:令其中一个集合的树根节点指向另一个集合的树根节点 root_x = self.__find(x) root_y = self.__find(y) if root_x == root_y: # x 和 y 的根节点集合编号相同,说明 x 和 y 已经同属于一个集合 return False self.fa[root_x] = root_y # x 的根节点连接到 y 的根节点上,成为 y 的根节点的子节点 return True def is_connected(self, x, y): # 查询操作:判断 x 和 y 是否同属于一个集合 return self.__find(x) == self.__find(y) class Solution: def equationsPossible(self, equations: List[str]) -> bool: union_find = UnionFind(26) for eqation in equations: if eqation[1] == "=": index1 = ord(eqation[0]) - 97 index2 = ord(eqation[3]) - 97 union_find.union(index1, index2) for eqation in equations: if eqation[1] == "!": index1 = ord(eqation[0]) - 97 index2 = ord(eqation[3]) - 97 if union_find.is_connected(index1, index2): return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + C \times \log C)$。其中 $n$ 是方程组 $equations$ 中的等式数量。$C$ 是字母变量的数量。本题中变量都是小写字母,即 $C \le 26$。 - **空间复杂度**:$O(C)$。 ================================================ FILE: docs/solutions/0900-0999/shortest-bridge.md ================================================ # [0934. 最短的桥](https://leetcode.cn/problems/shortest-bridge/) - 标签:深度优先搜索、广度优先搜索、数组、矩阵 - 难度:中等 ## 题目链接 - [0934. 最短的桥 - 力扣](https://leetcode.cn/problems/shortest-bridge/) ## 题目大意 **描述**: 给定一个大小为 $n \times n$ 的二元矩阵 $grid$,其中 1 表示陆地,0 表示水域。 「岛」是由四面相连的 1 形成的一个最大组,即不会与非组内的任何其他 1 相连。$grid$ 中 恰好存在两座岛 。 你可以将任意数量的 0 变为 1,以使两座岛连接起来,变成「一座岛」。 **要求**: 返回必须翻转的 0 的最小数目。 **说明**: - $n == grid.length == grid[i].length$。 - $2 \le n \le 10^{3}$。 - $grid[i][j]$ 为 0 或 1。 - $grid$ 中恰有两个岛。 **示例**: - 示例 1: ```python 输入:grid = [[0,1],[1,0]] 输出:1 ``` - 示例 2: ```python 输入:grid = [[0,1,0],[0,0,0],[0,0,1]] 输出:2 ``` ## 解题思路 ### 思路 1:广度优先搜索 这道题需要找到连接两个岛屿的最短桥,可以分为两步: 1. **找到第一个岛屿**:使用 DFS 找到第一个岛屿的所有格子,并将它们加入队列。 2. **BFS 扩展**:从第一个岛屿的所有格子开始,使用 BFS 向外扩展,直到遇到第二个岛屿。 **具体步骤**: - 遍历矩阵,找到第一个值为 $1$ 的格子,从这里开始 DFS - DFS 标记第一个岛屿的所有格子(改为 $2$),并将它们加入队列 - BFS 从队列中的所有格子开始扩展,每次将水域($0$)改为 $2$,直到遇到第二个岛屿($1$) ### 思路 1:代码 ```python class Solution: def shortestBridge(self, grid: List[List[int]]) -> int: n = len(grid) directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] # DFS 找到第一个岛屿 def dfs(i, j): if i < 0 or i >= n or j < 0 or j >= n or grid[i][j] != 1: return grid[i][j] = 2 # 标记为第一个岛屿 queue.append((i, j)) for di, dj in directions: dfs(i + di, j + dj) # 找到第一个岛屿并标记 queue = collections.deque() found = False for i in range(n): if found: break for j in range(n): if grid[i][j] == 1: dfs(i, j) found = True break # BFS 扩展,寻找第二个岛屿 steps = 0 while queue: size = len(queue) for _ in range(size): x, y = queue.popleft() for dx, dy in directions: nx, ny = x + dx, y + dy if 0 <= nx < n and 0 <= ny < n: if grid[nx][ny] == 1: # 找到第二个岛屿 return steps elif grid[nx][ny] == 0: # 水域,继续扩展 grid[nx][ny] = 2 queue.append((nx, ny)) steps += 1 return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是矩阵的边长。DFS 和 BFS 都最多访问每个格子一次。 - **空间复杂度**:$O(n^2)$,队列和递归栈的空间。 ================================================ FILE: docs/solutions/0900-0999/smallest-range-i.md ================================================ # [0908. 最小差值 I](https://leetcode.cn/problems/smallest-range-i/) - 标签:数组、数学 - 难度:简单 ## 题目链接 - [0908. 最小差值 I - 力扣](https://leetcode.cn/problems/smallest-range-i/) ## 题目大意 **描述**:给定一个整数数组 `nums`,和一个整数 `k`。给数组中的每个元素 `nums[i]` 都加上一个任意数字 `x` (`-k <= x <= k`),从而得到一个新数组 `result`。 **要求**:返回数组 `result` 的最大值和最小值之间可能存在的最小差值。 **说明**: - $1 \le nums.length \le 10^4$。 - $0 \le nums[i] \le 10^4$。 - $0 \le k \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [1], k = 0 输出:0 解释:分数是 max(nums) - min(nums) = 1 - 1 = 0。 ``` - 示例 2: ```python 输入:nums = [0,10], k = 2 输出:6 解释:将 nums 改为 [2,8]。分数是 max(nums) - min(nums) = 8 - 2 = 6。 ``` ## 解题思路 ### 思路 1:数学 `nums` 中的每个元素可以波动 `[-k, k]`。最小的差值就是「最大值减去 `k`」和「最小值加上 `k`」之间的差值。而如果差值小于 `0`,则说明每个数字都可以波动成相等的数字,此时直接返回 `0` 即可。 ### 思路 1:代码 ```python class Solution: def smallestRangeI(self, nums: List[int], k: int) -> int: return max(0, max(nums) - min(nums) - 2*k) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/0900-0999/smallest-range-ii.md ================================================ # [0910. 最小差值 II](https://leetcode.cn/problems/smallest-range-ii/) - 标签:贪心、数组、数学、排序 - 难度:中等 ## 题目链接 - [0910. 最小差值 II - 力扣](https://leetcode.cn/problems/smallest-range-ii/) ## 题目大意 **描述**: 给定一个整数数组 $nums$,和一个整数 $k$。 对于每个下标 $i$($0 \le i < nums.length$),将 $nums[i]$ 变成 $nums[i] + k$ 或 $nums[i] - k$。 $nums$ 的「分数」是 $nums$ 中最大元素和最小元素的差值。 **要求**: 在更改每个下标对应的值之后,返回 $nums$ 的最小「分数」。 **说明**: - $1 \le nums.length \le 10^{4}$。 - $0 \le nums[i] \le 10^{4}$。 - $0 \le k \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:nums = [1], k = 0 输出:0 解释:分数 = max(nums) - min(nums) = 1 - 1 = 0 。 ``` - 示例 2: ```python 输入:nums = [0,10], k = 2 输出:6 解释:将数组变为 [2, 8] 。分数 = max(nums) - min(nums) = 8 - 2 = 6 。 ``` ## 解题思路 ### 思路 1:贪心 + 排序 这道题的关键是理解:对于排序后的数组,最优策略是将数组分为两部分,前一部分都加 $k$,后一部分都减 $k$。 1. **排序**:首先对数组进行排序。 2. **贪心策略**:排序后,我们尝试在每个位置 $i$ 进行分割,使得 $[0, i]$ 的元素都加 $k$,$[i+1, n-1]$ 的元素都减 $k$。 3. **计算分数**:对于每个分割点,计算可能的最大值和最小值: - 最大值可能是 $\text{nums}[i] + k$ 或 $\text{nums}[n-1] - k$ - 最小值可能是 $\text{nums}[0] + k$ 或 $\text{nums}[i+1] - k$ 4. **更新答案**:取所有分割点中的最小分数。 **特殊情况**:如果所有元素都加 $k$ 或都减 $k$,分数为 $\text{nums}[n-1] - \text{nums}[0]$。 ### 思路 1:代码 ```python class Solution: def smallestRangeII(self, nums: List[int], k: int) -> int: nums.sort() n = len(nums) # 初始答案:所有元素都加 k 或都减 k ans = nums[n - 1] - nums[0] # 尝试在每个位置分割 for i in range(n - 1): # [0, i] 加 k,[i+1, n-1] 减 k max_val = max(nums[i] + k, nums[n - 1] - k) min_val = min(nums[0] + k, nums[i + 1] - k) ans = min(ans, max_val - min_val) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组长度,主要是排序的时间复杂度。 - **空间复杂度**:$O(\log n)$,排序所需的栈空间。 ================================================ FILE: docs/solutions/0900-0999/smallest-string-starting-from-leaf.md ================================================ # [0988. 从叶结点开始的最小字符串](https://leetcode.cn/problems/smallest-string-starting-from-leaf/) - 标签:树、深度优先搜索、字符串、回溯、二叉树 - 难度:中等 ## 题目链接 - [0988. 从叶结点开始的最小字符串 - 力扣](https://leetcode.cn/problems/smallest-string-starting-from-leaf/) ## 题目大意 **描述**: 给定一颗根结点为 $root$ 的二叉树,树中的每一个结点都有一个 $[0, 25]$ 范围内的值,分别代表字母 `'a'` 到 `'z'`。 **要求**: 返回「按字典序最小」的字符串,该字符串从这棵树的一个叶结点开始,到根结点结束。 **说明**: - 注意:字符串中任何较短的前缀在「字典序上」都是「较小」的: - 例如,在字典序上 `"ab"` 比 `"aba"` 要小。叶结点是指没有子结点的结点。 - 节点的叶节点是没有子节点的节点。 - 给定树的结点数在 $[1, 8500]$ 范围内。 - $0 \le Node.val \le 25$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/02/02/tree1.png) ```python 输入:root = [0,1,2,3,4,3,4] 输出:"dba" ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2019/01/30/tree2.png) ```python 输入:root = [25,1,3,1,3,0,2] 输出:"adz" ``` ## 解题思路 ### 思路 1:深度优先搜索 使用 DFS 遍历二叉树,从叶子节点到根节点构建字符串,然后比较所有路径的字典序。 1. **DFS 遍历**:从根节点开始,递归遍历到叶子节点。 2. **构建字符串**:在递归过程中,将节点值转换为字符并拼接。 3. **叶子节点判断**:当到达叶子节点时,将当前路径(反转后)与最小字符串比较。 4. **字典序比较**:使用 Python 的字符串比较功能,更新最小字符串。 **注意**:由于是从叶子到根,需要在递归返回时构建字符串,或者在递归过程中构建后反转。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def smallestFromLeaf(self, root: Optional[TreeNode]) -> str: self.result = None def dfs(node, path): if not node: return # 将当前节点值转换为字符并添加到路径 path = chr(ord('a') + node.val) + path # 如果是叶子节点,更新结果 if not node.left and not node.right: if self.result is None or path < self.result: self.result = path return # 递归遍历左右子树 if node.left: dfs(node.left, path) if node.right: dfs(node.right, path) dfs(root, "") return self.result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是树中节点的数量。需要遍历所有节点,每次字符串拼接需要 $O(n)$ 时间。 - **空间复杂度**:$O(n)$,递归栈的深度最多为树的高度,字符串长度最多为 $n$。 ================================================ FILE: docs/solutions/0900-0999/snakes-and-ladders.md ================================================ # [0909. 蛇梯棋](https://leetcode.cn/problems/snakes-and-ladders/) - 标签:广度优先搜索、数组、矩阵 - 难度:中等 ## 题目链接 - [0909. 蛇梯棋 - 力扣](https://leetcode.cn/problems/snakes-and-ladders/) ## 题目大意 **描述**: 给定一个大小为 $n \times n$ 的整数矩阵 $board$,方格按从 1 到 $n^2$ 编号,编号遵循「转行交替方式」,从左下角开始 (即,从 $board[n - 1][0]$ 开始)的每一行改变方向。 你一开始位于棋盘上的方格 1。每一回合,玩家需要从当前方格 $curr$ 开始出发,按下述要求前进: - 选定目标方格 $next$,目标方格的编号在范围 $[curr + 1, min(curr + 6, n^2)]$。 - 该选择模拟了掷「六面体骰子」的情景,无论棋盘大小如何,玩家最多只能有 6 个目的地。 - 传送玩家:如果目标方格 $next$ 处存在蛇或梯子,那么玩家会传送到蛇或梯子的目的地。否则,玩家传送到目标方格 $next$。 - 当玩家到达编号 $n^2$ 的方格时,游戏结束。 如果 $board[r][c] \ne -1$,位于 $r$ 行 $c$ 列的棋盘格中可能存在「蛇」或「梯子」。那个蛇或梯子的目的地将会是 $board[r][c]$。编号为 1 和 $n^2$ 的方格不是任何蛇或梯子的起点。 注意,玩家在每次掷骰的前进过程中最多只能爬过蛇或梯子一次:就算目的地是另一条蛇或梯子的起点,玩家也 不能 继续移动。 - 举个例子,假设棋盘是 $[[-1,4],[-1,3]]$,第一次移动,玩家的目标方格是 2。那么这个玩家将会顺着梯子到达方格 3,但「不能」顺着方格 3 上的梯子前往方格 4。(简单来说,类似飞行棋,玩家掷出骰子点数后移动对应格数,遇到单向的路径(即梯子或蛇)可以直接跳到路径的终点,但如果多个路径首尾相连,也不能连续跳多个路径) **要求**: 返回达到编号为 $n^2$ 的方格所需的最少掷骰次数,如果不可能,则返回 -1。 **说明**: - $n == board.length == board[i].length$。 - $2 \le n \le 20$。 - $board[i][j]$ 的值是 -1 或在范围 $[1, n^2]$ 内。 - 编号为 1 和 $n^2$ 的方格上没有蛇或梯子。 **示例**: - 示例 1: ```python ![](https://assets.leetcode.com/uploads/2018/09/23/snakes.png) 输入:board = [[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,35,-1,-1,13,-1],[-1,-1,-1,-1,-1,-1],[-1,15,-1,-1,-1,-1]] 输出:4 解释: 首先,从方格 1 [第 5 行,第 0 列] 开始。 先决定移动到方格 2 ,并必须爬过梯子移动到到方格 15 。 然后决定移动到方格 17 [第 3 行,第 4 列],必须爬过蛇到方格 13 。 接着决定移动到方格 14 ,且必须通过梯子移动到方格 35 。 最后决定移动到方格 36 , 游戏结束。 可以证明需要至少 4 次移动才能到达最后一个方格,所以答案是 4 。 ``` - 示例 2: ```python 输入:board = [[-1,-1],[-1,3]] 输出:1 ``` ## 解题思路 ### 思路 1:广度优先搜索 这道题是一个典型的最短路径问题,可以使用广度优先搜索(BFS)来解决。 1. **坐标转换**:首先需要将编号转换为二维坐标。由于棋盘是"牛耕式转行"编号,即奇数行从左到右,偶数行从右到左。 2. **BFS 搜索**:从方格 $1$ 开始,每次可以移动 $1 \sim 6$ 步(模拟掷骰子),如果目标方格有蛇或梯子,则传送到对应位置。 3. **记录步数**:使用队列记录当前位置和步数,使用集合记录已访问的方格,避免重复访问。 4. **终止条件**:当到达方格 $n^2$ 时,返回当前步数。 ### 思路 1:代码 ```python class Solution: def snakesAndLadders(self, board: List[List[int]]) -> int: n = len(board) # 将编号转换为坐标 def num_to_pos(num): num -= 1 # 转换为 0 索引 row = n - 1 - num // n # 从下往上 col = num % n # 奇数行(从下往上数)需要反转列 if (n - 1 - row) % 2 == 1: col = n - 1 - col return row, col # BFS queue = collections.deque([(1, 0)]) # (当前位置, 步数) visited = {1} while queue: curr, steps = queue.popleft() # 尝试掷骰子 1-6 for i in range(1, 7): next_num = curr + i if next_num > n * n: break # 获取目标位置 row, col = num_to_pos(next_num) # 如果有蛇或梯子,传送到目标位置 if board[row][col] != -1: next_num = board[row][col] # 到达终点 if next_num == n * n: return steps + 1 # 未访问过则加入队列 if next_num not in visited: visited.add(next_num) queue.append((next_num, steps + 1)) return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是棋盘的边长。每个方格最多访问一次。 - **空间复杂度**:$O(n^2)$,需要使用队列和集合存储访问状态。 ================================================ FILE: docs/solutions/0900-0999/sort-an-array.md ================================================ # [0912. 排序数组](https://leetcode.cn/problems/sort-an-array/) - 标签:数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序 - 难度:中等 ## 题目链接 - [0912. 排序数组 - 力扣](https://leetcode.cn/problems/sort-an-array/) ## 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:将该数组升序排列。 **说明**: - $1 \le nums.length \le 5 * 10^4$。 - $-5 * 10^4 \le nums[i] \le 5 * 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [5,2,3,1] 输出:[1,2,3,5] ``` - 示例 2: ```python 输入:nums = [5,1,1,2,0,0] 输出:[0,0,1,1,2,5] ``` ## 解题思路 这道题是一道用来复习排序算法,测试算法时间复杂度的好题。我试过了十种排序算法。得到了如下结论: - 超时算法(时间复杂度为 $O(n^2)$):冒泡排序、选择排序、插入排序。 - 通过算法(时间复杂度为 $O(n \times \log n)$):希尔排序、归并排序、快速排序、堆排序。 - 通过算法(时间复杂度为 $O(n)$):计数排序、桶排序。 - 解答错误算法(普通基数排序只适合非负数):基数排序。 ### 思路 1:冒泡排序(超时) > **冒泡排序(Bubble Sort)基本思想**:经过多次迭代,通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面。 假设数组的元素个数为 $n$ 个,则冒泡排序的算法步骤如下: 1. 第 $1$ 趟「冒泡」:对前 $n$ 个元素执行「冒泡」,从而使第 $1$ 个值最大的元素放置在正确位置上。 1. 先将序列中第 $1$ 个元素与第 $2$ 个元素进行比较,如果前者大于后者,则两者交换位置,否则不交换。 2. 然后将第 $2$ 个元素与第 $3$ 个元素比较,如果前者大于后者,则两者交换位置,否则不交换。 3. 依次类推,直到第 $n - 1$ 个元素与第 $n$ 个元素比较(或交换)为止。 4. 经过第 $1$ 趟排序,使得 $n$ 个元素中第 $i$ 个值最大元素被安置在第 $n$ 个位置上。 2. 第 $2$ 趟「冒泡」:对前 $n - 1$ 个元素执行「冒泡」,从而使第 $2$ 个值最大的元素放置在正确位置上。 1. 先将序列中第 $1$ 个元素与第 $2$ 个元素进行比较,如果前者大于后者,则两者交换位置,否则不交换。 2. 然后将第 $2$ 个元素与第 $3$ 个元素比较,如果前者大于后者,则两者交换位置,否则不交换。 3. 依次类推,直到第 $n - 2$ 个元素与第 $n - 1$ 个元素比较(或交换)为止。 4. 经过第 $2$ 趟排序,使得数组中第 $2$ 个值最大元素被安置在第 $n$ 个位置上。 3. 依次类推,重复上述「冒泡」过程,直到某一趟排序过程中不出现元素交换位置的动作,则排序结束。 ### 思路 1:代码 ```python class Solution: def bubbleSort(self, nums: [int]) -> [int]: # 第 i 趟「冒泡」 for i in range(len(nums) - 1): flag = False # 是否发生交换的标志位 # 对数组未排序区间 [0, n - i - 1] 的元素执行「冒泡」 for j in range(len(nums) - i - 1): # 相邻两个元素进行比较,如果前者大于后者,则交换位置 if nums[j] > nums[j + 1]: nums[j], nums[j + 1] = nums[j + 1], nums[j] flag = True if not flag: # 此趟遍历未交换任何元素,直接跳出 break return nums def sortArray(self, nums: [int]) -> [int]: return self.bubbleSort(nums) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:选择排序(超时) >**选择排序(Selection Sort)基本思想**:将数组分为两个区间,左侧为已排序区间,右侧为未排序区间。每趟从未排序区间中选择一个值最小的元素,放到已排序区间的末尾,从而将该元素划分到已排序区间。 假设数组的元素个数为 $n$ 个,则选择排序的算法步骤如下: 1. 初始状态下,无已排序区间,未排序区间为 $[0, n - 1]$。 2. 第 $1$ 趟选择: 1. 遍历未排序区间 $[0, n - 1]$,使用变量 $min\_i$ 记录区间中值最小的元素位置。 2. 将 $min\_i$ 与下标为 $0$ 处的元素交换位置。如果下标为 $0$ 处元素就是值最小的元素位置,则不用交换。 3. 此时,$[0, 0]$ 为已排序区间,$[1, n - 1]$(总共 $n - 1$ 个元素)为未排序区间。 3. 第 $2$ 趟选择: 1. 遍历未排序区间 $[1, n - 1]$,使用变量 $min\_i$ 记录区间中值最小的元素位置。 2. 将 $min\_i$ 与下标为 $1$ 处的元素交换位置。如果下标为 $1$ 处元素就是值最小的元素位置,则不用交换。 3. 此时,$[0, 1]$ 为已排序区间,$[2, n - 1]$(总共 $n - 2$ 个元素)为未排序区间。 4. 依次类推,对剩余未排序区间重复上述选择过程,直到所有元素都划分到已排序区间,排序结束。 ### 思路 2:代码 ```python class Solution: def selectionSort(self, nums: [int]) -> [int]: for i in range(len(nums) - 1): # 记录未排序区间中最小值的位置 min_i = i for j in range(i + 1, len(nums)): if nums[j] < nums[min_i]: min_i = j # 如果找到最小值的位置,将 i 位置上元素与最小值位置上的元素进行交换 if i != min_i: nums[i], nums[min_i] = nums[min_i], nums[i] return nums def sortArray(self, nums: [int]) -> [int]: return self.selectionSort(nums) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ### 思路 3:插入排序(超时) >**插入排序(Insertion Sort)基本思想**:将数组分为两个区间,左侧为有序区间,右侧为无序区间。每趟从无序区间取出一个元素,然后将其插入到有序区间的适当位置。 假设数组的元素个数为 $n$ 个,则插入排序的算法步骤如下: 1. 初始状态下,有序区间为 $[0, 0]$,无序区间为 $[1, n - 1]$。 2. 第 $1$ 趟插入: 1. 取出无序区间 $[1, n - 1]$ 中的第 $1$ 个元素,即 $nums[1]$。 2. 从右到左遍历有序区间中的元素,将比 $nums[1]$ 小的元素向后移动 $1$ 位。 3. 如果遇到大于或等于 $nums[1]$ 的元素时,说明找到了插入位置,将 $nums[1]$ 插入到该位置。 4. 插入元素后有序区间变为 $[0, 1]$,无序区间变为 $[2, n - 1]$。 3. 第 $2$ 趟插入: 1. 取出无序区间 $[2, n - 1]$ 中的第 $1$ 个元素,即 $nums[2]$。 2. 从右到左遍历有序区间中的元素,将比 $nums[2]$ 小的元素向后移动 $1$ 位。 3. 如果遇到大于或等于 $nums[2]$ 的元素时,说明找到了插入位置,将 $nums[2]$ 插入到该位置。 4. 插入元素后有序区间变为 $[0, 2]$,无序区间变为 $[3, n - 1]$。 4. 依次类推,对剩余无序区间中的元素重复上述插入过程,直到所有元素都插入到有序区间中,排序结束。 ### 思路 3:代码 ```python class Solution: def insertionSort(self, nums: [int]) -> [int]: # 遍历无序区间 for i in range(1, len(nums)): temp = nums[i] j = i # 从右至左遍历有序区间 while j > 0 and nums[j - 1] > temp: # 将有序区间中插入位置右侧的所有元素依次右移一位 nums[j] = nums[j - 1] j -= 1 # 将该元素插入到适当位置 nums[j] = temp return nums def sortArray(self, nums: [int]) -> [int]: return self.insertionSort(nums) ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ### 思路 4:希尔排序(通过) > **希尔排序(Shell Sort)基本思想**:将整个数组切按照一定的间隔取值划分为若干个子数组,每个子数组分别进行插入排序。然后逐渐缩小间隔进行下一轮划分子数组和对子数组进行插入排序。直至最后一轮排序间隔为 $1$,对整个数组进行插入排序。 假设数组的元素个数为 $n$ 个,则希尔排序的算法步骤如下: 1. 确定一个元素间隔数 $gap$。 2. 将参加排序的数组按此间隔数从第 $1$ 个元素开始一次分成若干个子数组,即分别将所有位置相隔为 $gap$ 的元素视为一个子数组。 3. 在各个子数组中采用某种排序算法(例如插入排序算法)进行排序。 4. 减少间隔数,并重新将整个数组按新的间隔数分成若干个子数组,再分别对各个子数组进行排序。 5. 依次类推,直到间隔数 $gap$ 值为 $1$,最后进行一次排序,排序结束。 ### 思路 4:代码 ```python class Solution: def shellSort(self, nums: [int]) -> [int]: size = len(nums) gap = size // 2 # 按照 gap 分组 while gap > 0: # 对每组元素进行插入排序 for i in range(gap, size): # temp 为每组中无序数组第 1 个元素 temp = nums[i] j = i # 从右至左遍历每组中的有序数组元素 while j >= gap and nums[j - gap] > temp: # 将每组有序数组中插入位置右侧的元素依次在组中右移一位 nums[j] = nums[j - gap] j -= gap # 将该元素插入到适当位置 nums[j] = temp # 缩小 gap 间隔 gap = gap // 2 return nums def sortArray(self, nums: [int]) -> [int]: return self.shellSort(nums) ``` ### 思路 4:复杂度分析 - **时间复杂度**:介于 $O(n \times \log n)$ 与 $O(n^2)$ 之间。 - **空间复杂度**:$O(1)$。 ### 思路 5:归并排序(通过) > **归并排序(Merge Sort)基本思想**:采用经典的分治策略,先递归地将当前数组平均分成两半,然后将有序数组两两合并,最终合并成一个有序数组。 假设数组的元素个数为 $n$ 个,则归并排序的算法步骤如下: 1. **分解过程**:先递归地将当前数组平均分成两半,直到子数组长度为 $1$。 1. 找到数组中心位置 $mid$,从中心位置将数组分成左右两个子数组 $left\_nums$、$right\_nums$。 2. 对左右两个子数组 $left\_nums$、$right\_nums$ 分别进行递归分解。 3. 最终将数组分解为 $n$ 个长度均为 $1$ 的有序子数组。 2. **归并过程**:从长度为 $1$ 的有序子数组开始,依次将有序数组两两合并,直到合并成一个长度为 $n$ 的有序数组。 1. 使用数组变量 $nums$ 存放合并后的有序数组。 2. 使用两个指针 $left\_i$、$right\_i$ 分别指向两个有序子数组 $left\_nums$、$right\_nums$ 的开始位置。 3. 比较两个指针指向的元素,将两个有序子数组中较小元素依次存入到结果数组 $nums$ 中,并将指针移动到下一位置。 4. 重复步骤 $3$,直到某一指针到达子数组末尾。 5. 将另一个子数组中的剩余元素存入到结果数组 $nums$ 中。 6. 返回合并后的有序数组 $nums$。 ### 思路 5:代码 ```python class Solution: # 合并过程 def merge(self, left_nums: [int], right_nums: [int]): nums = [] left_i, right_i = 0, 0 while left_i < len(left_nums) and right_i < len(right_nums): # 将两个有序子数组中较小元素依次插入到结果数组中 if left_nums[left_i] < right_nums[right_i]: nums.append(left_nums[left_i]) left_i += 1 else: nums.append(right_nums[right_i]) right_i += 1 # 如果左子数组有剩余元素,则将其插入到结果数组中 while left_i < len(left_nums): nums.append(left_nums[left_i]) left_i += 1 # 如果右子数组有剩余元素,则将其插入到结果数组中 while right_i < len(right_nums): nums.append(right_nums[right_i]) right_i += 1 # 返回合并后的结果数组 return nums # 分解过程 def mergeSort(self, nums: [int]) -> [int]: # 数组元素个数小于等于 1 时,直接返回原数组 if len(nums) <= 1: return nums mid = len(nums) // 2 # 将数组从中间位置分为左右两个数组 left_nums = self.mergeSort(nums[0: mid]) # 递归将左子数组进行分解和排序 right_nums = self.mergeSort(nums[mid:]) # 递归将右子数组进行分解和排序 return self.merge(left_nums, right_nums) # 把当前数组组中有序子数组逐层向上,进行两两合并 def sortArray(self, nums: [int]) -> [int]: return self.mergeSort(nums) ``` ### 思路 5:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(n)$。 ### 思路 6:快速排序(通过) > **快速排序(Quick Sort)基本思想**:采用经典的分治策略,选择数组中某个元素作为基准数,通过一趟排序将数组分为独立的两个子数组,一个子数组中所有元素值都比基准数小,另一个子数组中所有元素值都比基准数大。然后再按照同样的方式递归的对两个子数组分别进行快速排序,以达到整个数组有序。 假设数组的元素个数为 $n$ 个,则快速排序的算法步骤如下: 1. **哨兵划分**:选取一个基准数,将数组中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。 1. 从当前数组中找到一个基准数 $pivot$(这里以当前数组第 $1$ 个元素作为基准数,即 $pivot = nums[low]$)。 2. 使用指针 $i$ 指向数组开始位置,指针 $j$ 指向数组末尾位置。 3. 从右向左移动指针 $j$,找到第 $1$ 个小于基准值的元素。 4. 从左向右移动指针 $i$,找到第 $1$ 个大于基准数的元素。 5. 交换指针 $i$、指针 $j$ 指向的两个元素位置。 6. 重复第 $3 \sim 5$ 步,直到指针 $i$ 和指针 $j$ 相遇时停止,最后将基准数放到两个子数组交界的位置上。 2. **递归分解**:完成哨兵划分之后,对划分好的左右子数组分别进行递归排序。 1. 按照基准数的位置将数组拆分为左右两个子数组。 2. 对每个子数组分别重复「哨兵划分」和「递归分解」,直到各个子数组只有 $1$ 个元素,排序结束。 ### 思路 6:代码 ```python import random class Solution: # 随机哨兵划分:从 nums[low: high + 1] 中随机挑选一个基准数,并进行移位排序 def randomPartition(self, nums: [int], low: int, high: int) -> int: # 随机挑选一个基准数 i = random.randint(low, high) # 将基准数与最低位互换 nums[i], nums[low] = nums[low], nums[i] # 以最低位为基准数,然后将数组中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上 return self.partition(nums, low, high) # 哨兵划分:以第 1 位元素 nums[low] 为基准数,然后将比基准数小的元素移动到基准数左侧,将比基准数大的元素移动到基准数右侧,最后将基准数放到正确位置上 def partition(self, nums: [int], low: int, high: int) -> int: # 以第 1 位元素为基准数 pivot = nums[low] i, j = low, high while i < j: # 从右向左找到第 1 个小于基准数的元素 while i < j and nums[j] >= pivot: j -= 1 # 从左向右找到第 1 个大于基准数的元素 while i < j and nums[i] <= pivot: i += 1 # 交换元素 nums[i], nums[j] = nums[j], nums[i] # 将基准数放到正确位置上 nums[j], nums[low] = nums[low], nums[j] return j def quickSort(self, nums: [int], low: int, high: int) -> [int]: if low < high: # 按照基准数的位置,将数组划分为左右两个子数组 pivot_i = self.partition(nums, low, high) # 对左右两个子数组分别进行递归快速排序 self.quickSort(nums, low, pivot_i - 1) self.quickSort(nums, pivot_i + 1, high) return nums def sortArray(self, nums: [int]) -> [int]: return self.quickSort(nums, 0, len(nums) - 1) ``` ### 思路 6:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(n)$。 ### 思路 7:堆排序(通过) > **堆排序(Heap sort)基本思想**:借用「堆结构」所设计的排序算法。将数组转化为大顶堆,重复从大顶堆中取出数值最大的节点,并让剩余的堆结构继续维持大顶堆性质。 假设数组的元素个数为 $n$ 个,则堆排序的算法步骤如下: 1. **构建初始大顶堆**: 1. 定义一个数组实现的堆结构,将原始数组的元素依次存入堆结构的数组中(初始顺序不变)。 2. 从数组的中间位置开始,从右至左,依次通过「下移调整」将数组转换为一个大顶堆。 2. **交换元素,调整堆**: 1. 交换堆顶元素(第 $1$ 个元素)与末尾(最后 $1$ 个元素)的位置,交换完成后,堆的长度减 $1$。 2. 交换元素之后,由于堆顶元素发生了改变,需要从根节点开始,对当前堆进行「下移调整」,使其保持堆的特性。 3. **重复交换和调整堆**: 1. 重复第 $2$ 步,直到堆的大小为 $1$ 时,此时大顶堆的数组已经完全有序。 ### 思路 7:代码 ```python class Solution: # 调整为大顶堆 def heapify(self, arr, index, end): left = index * 2 + 1 right = left + 1 while left <= end: # 当前节点为非叶子节点 max_index = index if arr[left] > arr[max_index]: max_index = left if right <= end and arr[right] > arr[max_index]: max_index = right if index == max_index: # 如果不用交换,则说明已经交换结束 break arr[index], arr[max_index] = arr[max_index], arr[index] # 继续调整子树 index = max_index left = index * 2 + 1 right = left + 1 # 初始化大顶堆 def buildMaxHeap(self, arr): size = len(arr) # (size-2) // 2 是最后一个非叶节点,叶节点不用调整 for i in range((size - 2) // 2, -1, -1): self.heapify(arr, i, size - 1) return arr # 升序堆排序,思路如下: # 1. 先建立大顶堆 # 2. 让堆顶最大元素与最后一个交换,然后调整第一个元素到倒数第二个元素,这一步获取最大值 # 3. 再交换堆顶元素与倒数第二个元素,然后调整第一个元素到倒数第三个元素,这一步获取第二大值 # 4. 以此类推,直到最后一个元素交换之后完毕。 def maxHeapSort(self, arr): self.buildMaxHeap(arr) size = len(arr) for i in range(size): arr[0], arr[size-i-1] = arr[size-i-1], arr[0] self.heapify(arr, 0, size-i-2) return arr def sortArray(self, nums: List[int]) -> List[int]: return self.maxHeapSort(nums) ``` ### 思路 7:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(1)$。 ### 思路 8:计数排序(通过) > **计数排序(Counting Sort)基本思想**:通过统计数组中每个元素在数组中出现的次数,根据这些统计信息将数组元素有序的放置到正确位置,从而达到排序的目的。 假设数组的元素个数为 $n$ 个,则计数排序的算法步骤如下: 1. **计算排序范围**:遍历数组,找出待排序序列中最大值元素 $nums\_max$ 和最小值元素 $nums\_min$,计算出排序范围为 $nums\_max - nums\_min + 1$。 2. **定义计数数组**:定义一个大小为排序范围的计数数组 $counts$,用于统计每个元素的出现次数。其中: 1. 数组的索引值 $num - nums\_min$ 表示元素的值为 $num$。 2. 数组的值 $counts[num - nums\_min]$ 表示元素 $num$ 的出现次数。 3. **对数组元素进行计数统计**:遍历待排序数组 $nums$,对每个元素在计数数组中进行计数,即将待排序数组中「每个元素值减去最小值」作为索引,将「对计数数组中的值」加 $1$,即令 $counts[num - nums\_min]$ 加 $1$。 4. **生成累积计数数组**:从 $counts$ 中的第 $1$ 个元素开始,每一项累家前一项和。此时 $counts[num - nums\_min]$ 表示值为 $num$ 的元素在排序数组中最后一次出现的位置。 5. **逆序填充目标数组**:逆序遍历数组 $nums$,将每个元素 $num$ 填入正确位置。 6. 将其填充到结果数组 $res$ 的索引 $counts[num - nums\_min]$ 处。 7. 放入后,令累积计数数组中对应索引减 $1$,从而得到下个元素 $num$ 的放置位置。 ### 思路 8:代码 ```python class Solution: def countingSort(self, nums: [int]) -> [int]: # 计算待排序数组中最大值元素 nums_max 和最小值元素 nums_min nums_min, nums_max = min(nums), max(nums) # 定义计数数组 counts,大小为 最大值元素 - 最小值元素 + 1 size = nums_max - nums_min + 1 counts = [0 for _ in range(size)] # 统计值为 num 的元素出现的次数 for num in nums: counts[num - nums_min] += 1 # 生成累积计数数组 for i in range(1, size): counts[i] += counts[i - 1] # 反向填充目标数组 res = [0 for _ in range(len(nums))] for i in range(len(nums) - 1, -1, -1): num = nums[i] # 根据累积计数数组,将 num 放在数组对应位置 res[counts[num - nums_min] - 1] = num # 将 num 的对应放置位置减 1,从而得到下个元素 num 的放置位置 counts[nums[i] - nums_min] -= 1 return res def sortArray(self, nums: [int]) -> [int]: return self.countingSort(nums) ``` ### 思路 8:复杂度分析 - **时间复杂度**:$O(n + k)$。其中 $k$ 代表待排序序列的值域。 - **空间复杂度**:$O(k)$。其中 $k$ 代表待排序序列的值域。 ### 思路 9:桶排序(通过) > **桶排序(Bucket Sort)基本思想**:将待排序数组中的元素分散到若干个「桶」中,然后对每个桶中的元素再进行单独排序。 假设数组的元素个数为 $n$ 个,则桶排序的算法步骤如下: 1. **确定桶的数量**:根据待排序数组的值域范围,将数组划分为 $k$ 个桶,每个桶可以看做是一个范围区间。 2. **分配元素**:遍历待排序数组元素,将每个元素根据大小分配到对应的桶中。 3. **对每个桶进行排序**:对每个非空桶内的元素单独排序(使用插入排序、归并排序、快排排序等算法)。 4. **合并桶内元素**:将排好序的各个桶中的元素按照区间顺序依次合并起来,形成一个完整的有序数组。 ### 思路 9:代码 ```python class Solution: def insertionSort(self, nums: [int]) -> [int]: # 遍历无序区间 for i in range(1, len(nums)): temp = nums[i] j = i # 从右至左遍历有序区间 while j > 0 and nums[j - 1] > temp: # 将有序区间中插入位置右侧的元素依次右移一位 nums[j] = nums[j - 1] j -= 1 # 将该元素插入到适当位置 nums[j] = temp return nums def bucketSort(self, nums: [int], bucket_size=5) -> [int]: # 计算待排序序列中最大值元素 nums_max、最小值元素 nums_min nums_min, nums_max = min(nums), max(nums) # 定义桶的个数为 (最大值元素 - 最小值元素) // 每个桶的大小 + 1 bucket_count = (nums_max - nums_min) // bucket_size + 1 # 定义桶数组 buckets buckets = [[] for _ in range(bucket_count)] # 遍历待排序数组元素,将每个元素根据大小分配到对应的桶中 for num in nums: buckets[(num - nums_min) // bucket_size].append(num) # 对每个非空桶内的元素单独排序,排序之后,按照区间顺序依次合并到 res 数组中 res = [] for bucket in buckets: self.insertionSort(bucket) res.extend(bucket) # 返回结果数组 return res def sortArray(self, nums: [int]) -> [int]: return self.bucketSort(nums) ``` ### 思路 9:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n + m)$。$m$ 为桶的个数。 ### 思路 10:基数排序(提交解答错误,普通基数排序只适合非负数) > **基数排序(Radix Sort)基本思想**:将整数按位数切割成不同的数字,然后从低位开始,依次到高位,逐位进行排序,从而达到排序的目的。 我们以最低位优先法为例,讲解一下基数排序的算法步骤。 1. **确定排序的最大位数**:遍历数组元素,获取数组最大值元素,并取得对应位数。 2. **从最低位(个位)开始,到最高位为止,逐位对每一位进行排序**: 1. 定义一个长度为 $10$ 的桶数组 $buckets$,每个桶分别代表 $0 \sim 9$ 中的 $1$ 个数字。 2. 按照每个元素当前位上的数字,将元素放入对应数字的桶中。 3. 清空原始数组,然后按照桶的顺序依次取出对应元素,重新加入到原始数组中。 ### 思路 10:代码 ```python class Solution: def radixSort(self, nums: [int]) -> [int]: # 桶的大小为所有元素的最大位数 size = len(str(max(nums))) # 从最低位(个位)开始,逐位遍历每一位 for i in range(size): # 定义长度为 10 的桶数组 buckets,每个桶分别代表 0 ~ 9 中的 1 个数字。 buckets = [[] for _ in range(10)] # 遍历数组元素,按照每个元素当前位上的数字,将元素放入对应数字的桶中。 for num in nums: buckets[num // (10 ** i) % 10].append(num) # 清空原始数组 nums.clear() # 按照桶的顺序依次取出对应元素,重新加入到原始数组中。 for bucket in buckets: for num in bucket: nums.append(num) # 完成排序,返回结果数组 return nums def sortArray(self, nums: [int]) -> [int]: return self.radixSort(nums) ``` ### 思路 10:复杂度分析 - **时间复杂度**:$O(n \times k)$。其中 $n$ 是待排序元素的个数,$k$ 是数字位数。$k$ 的大小取决于数字位的选择(十进制位、二进制位)和待排序元素所属数据类型全集的大小。 - **空间复杂度**:$O(n + k)$。 ================================================ FILE: docs/solutions/0900-0999/sort-array-by-parity-ii.md ================================================ # [0922. 按奇偶排序数组 II](https://leetcode.cn/problems/sort-array-by-parity-ii/) - 标签:数组、双指针、排序 - 难度:简单 ## 题目链接 - [0922. 按奇偶排序数组 II - 力扣](https://leetcode.cn/problems/sort-array-by-parity-ii/) ## 题目大意 **描述**: 给定一个非负整数数组 $nums$,$nums$ 中一半整数是「奇数」,一半整数是「偶数」。 对数组进行排序,以便当 $nums[i]$ 为奇数时,i 也是「奇数」;当 $nums[i]$ 为偶数时, $i$ 也是「偶数」。 **要求**: 你可以返回「任何满足上述条件的数组作为答案」。 **说明**: - $2 \le nums.length \le 2 * 10^{4}$。 - $nums.length$ 是偶数。 - $nums$ 中一半是偶数。 - $0 \le nums[i] \le 10^{3}$。 - 进阶:可以不使用额外空间解决问题吗? **示例**: - 示例 1: ```python 输入:nums = [4,2,5,7] 输出:[4,5,2,7] 解释:[4,7,2,5],[2,5,4,7],[2,7,4,5] 也会被接受。 ``` - 示例 2: ```python 输入:nums = [2,3] 输出:[2,3] ``` ## 解题思路 ### 思路 1:双指针 使用两个指针 $even$ 和 $odd$,分别指向偶数索引和奇数索引。 1. $even$ 指针从 $0$ 开始,每次移动 $2$ 步,寻找偶数索引上的奇数。 2. $odd$ 指针从 $1$ 开始,每次移动 $2$ 步,寻找奇数索引上的偶数。 3. 当 $even$ 指向奇数且 $odd$ 指向偶数时,交换两个元素。 4. 重复上述过程,直到遍历完整个数组。 ### 思路 1:代码 ```python class Solution: def sortArrayByParityII(self, nums: List[int]) -> List[int]: n = len(nums) even, odd = 0, 1 # even 指向偶数索引,odd 指向奇数索引 while even < n and odd < n: # 偶数索引找到奇数 while even < n and nums[even] % 2 == 0: even += 2 # 奇数索引找到偶数 while odd < n and nums[odd] % 2 == 1: odd += 2 # 交换位置不对的元素 if even < n and odd < n: nums[even], nums[odd] = nums[odd], nums[even] return nums ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $nums$ 的长度。 - **空间复杂度**:$O(1)$,只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0900-0999/sort-array-by-parity.md ================================================ # [0905. 按奇偶排序数组](https://leetcode.cn/problems/sort-array-by-parity/) - 标签:数组、双指针、排序 - 难度:简单 ## 题目链接 - [0905. 按奇偶排序数组 - 力扣](https://leetcode.cn/problems/sort-array-by-parity/) ## 题目大意 **描述**: 给定一个整数数组 $nums$,将 $nums$ 中的的所有偶数元素移动到数组的前面,后跟所有奇数元素。 **要求**: 返回满足此条件的「任一数组」作为答案。 **说明**: - $1 \le nums.length \le 5000$。 - $0 \le nums[i] \le 5000$。 **示例**: - 示例 1: ```python 输入:nums = [3,1,2,4] 输出:[2,4,3,1] 解释:[4,2,3,1]、[2,4,1,3] 和 [4,2,1,3] 也会被视作正确答案。 ``` - 示例 2: ```python 输入:nums = [0] 输出:[0] ``` ## 解题思路 ### 思路 1:双指针 使用双指针法,一个指针 $left$ 指向数组开头,一个指针 $right$ 指向数组末尾。 1. 当 $left$ 指向的元素为偶数时,$left$ 右移。 2. 当 $right$ 指向的元素为奇数时,$right$ 左移。 3. 当 $left$ 指向奇数且 $right$ 指向偶数时,交换两个元素。 4. 重复上述过程,直到 $left \ge right$。 ### 思路 1:代码 ```python class Solution: def sortArrayByParity(self, nums: List[int]) -> List[int]: left, right = 0, len(nums) - 1 # 双指针,左边放偶数,右边放奇数 while left < right: # 左指针找到奇数 while left < right and nums[left] % 2 == 0: left += 1 # 右指针找到偶数 while left < right and nums[right] % 2 == 1: right -= 1 # 交换奇数和偶数 if left < right: nums[left], nums[right] = nums[right], nums[left] left += 1 right -= 1 return nums ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $nums$ 的长度。 - **空间复杂度**:$O(1)$,只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0900-0999/squares-of-a-sorted-array.md ================================================ # [0977. 有序数组的平方](https://leetcode.cn/problems/squares-of-a-sorted-array/) - 标签:数组、双指针、排序 - 难度:简单 ## 题目链接 - [0977. 有序数组的平方 - 力扣](https://leetcode.cn/problems/squares-of-a-sorted-array/) ## 题目大意 **描述**:给定一个按「非递减顺序」排序的整数数组 $nums$。 **要求**:返回「每个数字的平方」组成的新数组,要求也按「非递减顺序」排序。 **说明**: - 要求使用时间复杂度为 $O(n)$ 的算法解决本问题。 - $1 \le nums.length \le 10^4$。 - $-10^4 \le nums[i] \le 10^4$。 - $nums$ 已按非递减顺序排序。 **示例**: - 示例 1: ```python 输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100] 排序后,数组变为 [0,1,9,16,100] ``` - 示例 2: ```python 输入:nums = [-7,-3,2,3,11] 输出:[4,9,9,49,121] ``` ## 解题思路 ### 思路 1:对撞指针 原数组是按「非递减顺序」排序的,可能会存在负数元素。但是无论是否存在负数,数字的平方最大值一定在原数组的两端。题目要求返回的新数组也要按照「非递减顺序」排序。那么,我们可以利用双指针,从两端向中间移动,然后不断将数的平方最大值填入数组。具体做法如下: - 使用两个指针 $left$、$right$。$left$ 指向数组第一个元素位置,$right$ 指向数组最后一个元素位置。再定义 $index = len(nums) - 1$ 作为答案数组填入顺序的索引值。$res$ 作为答案数组。 - 比较 $nums[left]$ 与 $nums[right]$ 的绝对值大小。大的就是平方最大的的那个数。 - 如果 $abs(nums[right])$ 更大,则将其填入答案数组对应位置,并令 `right -= 1`。 - 如果 $abs(nums[left])$ 更大,则将其填入答案数组对应位置,并令 `left += 1`。 - 令 $index -= 1$。 - 直到 $left == right$,最后将 $nums[left]$ 填入答案数组对应位置。 返回答案数组 $res$。 ### 思路 1:代码 ```python class Solution: def sortedSquares(self, nums: List[int]) -> List[int]: size = len(nums) left, right = 0, size - 1 index = size - 1 res = [0 for _ in range(size)] while left < right: if abs(nums[left]) < abs(nums[right]): res[index] = nums[right] * nums[right] right -= 1 else: res[index] = nums[left] * nums[left] left += 1 index -= 1 res[index] = nums[left] * nums[left] return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 中的元素数量。 - **空间复杂度**:$O(1)$,不考虑最终返回值的空间占用。 ### 思路 2:排序算法 可以通过各种排序算法来对平方后的数组进行排序。以快速排序为例,具体步骤如下: 1. 遍历数组,将数组中各个元素变为平方项。 2. 从数组中找到一个基准数。 3. 然后将数组中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧,从而把数组拆分为左右两个部分。 4. 再对左右两个部分分别重复第 2、3 步,直到各个部分只有一个数,则排序结束。 ### 思路 2:代码 ```python import random class Solution: def randomPartition(self, arr: [int], low: int, high: int): i = random.randint(low, high) arr[i], arr[high] = arr[high], arr[i] return self.partition(arr, low, high) def partition(self, arr: [int], low: int, high: int): i = low - 1 pivot = arr[high] for j in range(low, high): if arr[j] <= pivot: i += 1 arr[i], arr[j] = arr[j], arr[i] arr[i + 1], arr[high] = arr[high], arr[i + 1] return i + 1 def quickSort(self, arr, low, high): if low < high: pi = self.randomPartition(arr, low, high) self.quickSort(arr, low, pi - 1) self.quickSort(arr, pi + 1, high) return arr def sortedSquares(self, nums: List[int]) -> List[int]: for i in range(len(nums)): nums[i] = nums[i] * nums[i] return self.quickSort(nums, 0, len(nums) - 1) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 为数组 $nums$ 中的元素数量。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/0900-0999/stamping-the-sequence.md ================================================ # [0936. 戳印序列](https://leetcode.cn/problems/stamping-the-sequence/) - 标签:栈、贪心、队列、字符串 - 难度:困难 ## 题目链接 - [0936. 戳印序列 - 力扣](https://leetcode.cn/problems/stamping-the-sequence/) ## 题目大意 **描述**: 你想要用小写字母组成一个目标字符串 $target$。 开始的时候,序列由 $target.length$ 个 `'?'` 记号组成。而你有一个小写字母印章 $stamp$。 在每个回合,你可以将印章放在序列上,并将序列中的每个字母替换为印章上的相应字母。你最多可以进行 $10 \times target.length$ 个回合。 举个例子,如果初始序列为 `"?????"`,而你的印章 $stamp$ 是 `"abc"`,那么在第一回合,你可以得到 `"abc??"`、`"?abc?"`、`"??abc"`。(请注意,印章必须完全包含在序列的边界内才能盖下去。) **要求**: 如果可以印出序列,那么返回一个数组,该数组由每个回合中被印下的最左边字母的索引组成。如果不能印出序列,就返回一个空数组。 例如,如果序列是 `"ababc"`,印章是 `"abc"`,那么我们就可以返回与操作 `"?????"` -> `"abc??"` -> `"ababc"` 相对应的答案 $[0, 2]$; 另外,如果可以印出序列,那么需要保证可以在 $10 \times target.length$ 个回合内完成。任何超过此数字的答案将不被接受。 **说明**: - $1 \le stamp.length \le target.length \le 10^{3}$ - $stamp$ 和 $target$ 只包含小写字母。 **示例**: - 示例 1: ```python 输入:stamp = "abc", target = "ababc" 输出:[0,2] ([1,0,2] 以及其他一些可能的结果也将作为答案被接受) ``` - 示例 2: ```python 输入:stamp = "abca", target = "aabcaca" 输出:[3,0,1] ``` ## 解题思路 ### 思路 1:逆向思维 + 贪心 这道题正向思考比较困难,我们可以逆向思考:从目标字符串 $target$ 逆推回全 `?` 的初始状态。 1. 将 $target$ 转换为字符数组,方便修改。 2. 从后往前,每次找到一个可以被"擦除"的位置(即该位置的字符可以被替换为 `?`)。 3. 一个位置可以被擦除的条件是:该位置的字符与 $stamp$ 匹配,或者已经是 `?`。 4. 每次擦除后,记录擦除的起始位置。 5. 重复上述过程,直到所有字符都变成 `?`。 6. 最后将记录的位置反转,即为答案。 ### 思路 1:代码 ```python class Solution: def movesToStamp(self, stamp: str, target: str) -> List[int]: m, n = len(stamp), len(target) target = list(target) # 转换为列表方便修改 result = [] visited = [False] * n # 标记是否已经被戳印覆盖 stars = 0 # 记录 '?' 的数量 # 检查从位置 pos 开始是否可以戳印 def canStamp(pos): changed = False for i in range(m): if target[pos + i] == '?': continue if target[pos + i] != stamp[i]: return False changed = True return changed # 从位置 pos 开始戳印(将字符替换为 '?') def doStamp(pos): nonlocal stars for i in range(m): if target[pos + i] != '?': target[pos + i] = '?' stars += 1 # 不断尝试戳印,直到所有字符都变成 '?' while stars < n: stamped = False for i in range(n - m + 1): if not visited[i] and canStamp(i): doStamp(i) result.append(i) visited[i] = True stamped = True # 如果一轮下来没有任何戳印,说明无法完成 if not stamped: return [] # 反转结果(因为是逆向推导) return result[::-1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times (n - m))$,其中 $n$ 是 $target$ 的长度,$m$ 是 $stamp$ 的长度。最多需要 $n$ 次戳印,每次需要检查 $O(n - m)$ 个位置。 - **空间复杂度**:$O(n)$,需要存储 $target$ 的字符数组和访问标记数组。 ================================================ FILE: docs/solutions/0900-0999/string-without-aaa-or-bbb.md ================================================ # [0984. 不含 AAA 或 BBB 的字符串](https://leetcode.cn/problems/string-without-aaa-or-bbb/) - 标签:贪心、字符串 - 难度:中等 ## 题目链接 - [0984. 不含 AAA 或 BBB 的字符串 - 力扣](https://leetcode.cn/problems/string-without-aaa-or-bbb/) ## 题目大意 **描述**: 给定两个整数 $a$ 和 $b$。 **要求**: 返回 任意 字符串 $s$,要求满足: - $s$ 的长度为 $a + b$,且正好包含 $a$ 个 `'a'` 字母与 $b$ 个 `'b'` 字母; - 子串 `'aaa'` 没有出现在 $s$ 中; - 子串 `'bbb'` 没有出现在 $s$ 中。 **说明**: - $0 \le a, b \le 10^{3}$。 - 对于给定的 $a$ 和 $b$,保证存在满足要求的 $s$。 **示例**: - 示例 1: ```python 输入:a = 1, b = 2 输出:"abb" 解释:"abb", "bab" 和 "bba" 都是正确答案。 ``` - 示例 2: ```python 输入:a = 4, b = 1 输出:"aabaa" ``` ## 解题思路 ### 思路 1:贪心 使用贪心策略构造字符串:优先放置数量较多的字符,但要避免连续三个相同字符。 1. 每次选择剩余数量较多的字符。 2. 如果该字符已经连续出现两次,则必须放置另一个字符。 3. 否则,如果该字符数量较多,可以连续放置两个;如果数量相近,则只放置一个。 4. 重复上述过程,直到所有字符都放置完毕。 ### 思路 1:代码 ```python class Solution: def strWithout3a3b(self, a: int, b: int) -> str: result = [] while a > 0 or b > 0: # 判断是否需要写入 'a' write_a = False n = len(result) # 如果最后两个字符是 'bb',必须写 'a' if n >= 2 and result[-1] == result[-2] == 'b': write_a = True # 如果最后两个字符是 'aa',不能写 'a' elif n >= 2 and result[-1] == result[-2] == 'a': write_a = False # 否则,选择剩余数量较多的字符 else: write_a = a >= b if write_a: result.append('a') a -= 1 else: result.append('b') b -= 1 return ''.join(result) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(a + b)$,需要构造长度为 $a + b$ 的字符串。 - **空间复杂度**:$O(1)$,不考虑结果字符串的空间。 ================================================ FILE: docs/solutions/0900-0999/subarray-sums-divisible-by-k.md ================================================ # [974. 和可被 K 整除的子数组](https://leetcode.cn/problems/subarray-sums-divisible-by-k/) - 标签:数组、哈希表、前缀和 - 难度:中等 ## 题目链接 - [974. 和可被 K 整除的子数组 - 力扣](https://leetcode.cn/problems/subarray-sums-divisible-by-k/) ## 题目大意 给定一个整数数组 `nums` 和一个整数 `k`。 要求:返回其中元素之和可被 `k` 整除的(连续、非空)子数组的数目。 ## 解题思路 先考虑暴力计算子数组和,外层两重循环,遍历所有连续子数组,然后最内层再计算一下子数组的和。部分代码如下: ```python for i in range(len(nums)): for j in range(i + 1): sum = countSum(i, j) ``` 这样下来时间复杂度就是 $O(n^3)$ 了。下一步是想办法降低时间复杂度。 先用一重循环遍历数组,计算出数组 `nums` 中前 i 个元素的和(前缀和),保存到一维数组 `pre_sum` 中,那么对于任意 `[j..i]` 的子数组 的和为 `pre_sum[i] - pre_sum[j - 1]`。这样计算子数组和的时间复杂度降为了 $O(1)$。总体时间复杂度为 $O(n^2)$。 由于我们只关心和为 `k` 出现的次数,不关心具体的解,可以使用哈希表来加速运算。 `pre_sum[i]` 的定义是前 `i` 个元素和,则 `[j..i]` 子数组和可以被 `k` 整除可以转换为:`(pre_sum[i] - pre_sum[j - 1])% k == 0`。再转换一下:`pre_sum[i] % k == pre_sum[j - 1] % k`。 所以,我们只需要统计满足 `pre_sum[i] % k == pre_sum[j - 1] % k` 条件的组合个数。具体做法如下: 使用 `pre_sum` 变量记录前缀和(代表 `pre_sum[i]`)。使用哈希表 `pre_dic` 记录 `pre_sum[i] % k` 出现的次数。键值对为 `pre_sum[i] : count`。 - 从左到右遍历数组,计算当前前缀和并对 `k` 取余,即 `pre_sum = (pre_sum + nums[i]) % k`。 - 如果 `pre_sum` 在哈希表中,则答案个数累加上 `pre_dic[pre_sum]`。同时 `pre_sum` 个数累加 1,即 `pre_dic[pre_sum] += 1`。 - 如果 `pre_sum` 不在哈希表中,则 `pre_sum` 个数记为 1,即 `pre_dic[pre_sum] += 1`。 - 最后输出答案个数。 ## 代码 ```python class Solution: def subarraysDivByK(self, nums: List[int], k: int) -> int: pre_sum = 0 ans = 0 nums_dict = {0: 1} for i in range(len(nums)): pre_sum = (pre_sum + nums[i]) % k if pre_sum < 0: pre_sum += k if pre_sum in nums_dict: ans += nums_dict[pre_sum] nums_dict[pre_sum] += 1 else: nums_dict[pre_sum] = 1 return ans ``` ================================================ FILE: docs/solutions/0900-0999/subarrays-with-k-different-integers.md ================================================ # [0992. K 个不同整数的子数组](https://leetcode.cn/problems/subarrays-with-k-different-integers/) - 标签:数组、哈希表、计数、滑动窗口 - 难度:困难 ## 题目链接 - [0992. K 个不同整数的子数组 - 力扣](https://leetcode.cn/problems/subarrays-with-k-different-integers/) ## 题目大意 给定一个正整数数组 `nums`,再给定一个整数 `k`。如果 `nums` 的某个子数组中不同整数的个数恰好为 `k`,则称 `nums` 的这个连续、不一定不同的子数组为「好子数组」。 - 例如,`[1, 2, 3, 1, 2]` 中有 3 个不同的整数:`1`,`2` 以及 `3`。 要求:返回 `nums` 中好子数组的数目。 ## 解题思路 这道题转换一下思路会更简单。 恰好包含 `k` 个不同整数的连续子数组数量 = 包含小于等于 `k` 个不同整数的连续子数组数量 - 包含小于等于 `k - 1` 个不同整数的连续子数组数量 可以专门写一个方法计算包含小于等于 `k` 个不同整数的连续子数组数量。 计算包含小于等于 `k` 个不同整数的连续子数组数量的方法具体步骤如下: 用滑动窗口 `windows` 来记录不同的整数个数,`windows` 为哈希表类型。 设定两个指针:`left`、`right`,分别指向滑动窗口的左右边界,保证窗口内不超过 `k` 个不同整数。 - 一开始,`left`、`right` 都指向 `0`。 - 将最右侧整数 `nums[right]` 加入当前窗口 `windows` 中,记录该整数个数。 - 如果该窗口中该整数的个数多于 `k` 个,即 `len(windows) > k`,则不断右移 `left`,缩小滑动窗口长度,并更新窗口中对应整数的个数,直到 `len(windows) <= k`。 - 维护更新包含小于等于 `k` 个不同整数的连续子数组数量。每次累加数量为 `right - left + 1`,表示以 `nums[right]` 为结尾的小于等于 `k` 个不同整数的连续子数组数量。 - 然后右移 `right`,直到 `right >= len(nums)` 结束。 - 返回包含小于等于 `k` 个不同整数的连续子数组数量。 ## 代码 ```python class Solution: def subarraysMostKDistinct(self, nums, k): windows = dict() left, right = 0, 0 ans = 0 while right < len(nums): if nums[right] in windows: windows[nums[right]] += 1 else: windows[nums[right]] = 1 while len(windows) > k: windows[nums[left]] -= 1 if windows[nums[left]] == 0: del windows[nums[left]] left += 1 ans += right - left + 1 right += 1 return ans def subarraysWithKDistinct(self, nums: List[int], k: int) -> int: return self.subarraysMostKDistinct(nums, k) - self.subarraysMostKDistinct(nums, k - 1) ``` ================================================ FILE: docs/solutions/0900-0999/sum-of-even-numbers-after-queries.md ================================================ # [0985. 查询后的偶数和](https://leetcode.cn/problems/sum-of-even-numbers-after-queries/) - 标签:数组、模拟 - 难度:中等 ## 题目链接 - [0985. 查询后的偶数和 - 力扣](https://leetcode.cn/problems/sum-of-even-numbers-after-queries/) ## 题目大意 **描述**: 给定一个整数数组 A 和一个查询数组 $queries$。 对于第 $i$ 次查询,有 $val = queries[i][0]$, $index = queries[i][1]$,我们会把 $val$ 加到 $A[index]$ 上。然后,第 $i$ 次查询的答案是 A 中偶数值的和。 (此处给定的 $index = queries[i][1]$ 是从 0 开始的索引,每次查询都会永久修改数组 A。) **要求**: 返回所有查询的答案。你的答案应当以数组 $answer$ 给出,$answer[i]$ 为第 $i$ 次查询的答案。 **说明**: - $1 \le A.length \le 10^{000}$。 - $-10^{000} \le A[i] \le 10^{000}$。 - $1 \le queries.length \le 10^{000}$。 - $-10^{000} \le queries[i][0] \le 10^{000}$。 - $0 \le queries[i][1] \lt A.length$。 **示例**: - 示例 1: ```python 输入:A = [1,2,3,4], queries = [[1,0],[-3,1],[-4,0],[2,3]] 输出:[8,6,2,4] 解释: 开始时,数组为 [1,2,3,4]。 将 1 加到 A[0] 上之后,数组为 [2,2,3,4],偶数值之和为 2 + 2 + 4 = 8。 将 -3 加到 A[1] 上之后,数组为 [2,-1,3,4],偶数值之和为 2 + 4 = 6。 将 -4 加到 A[0] 上之后,数组为 [-2,-1,3,4],偶数值之和为 -2 + 4 = 2。 将 2 加到 A[3] 上之后,数组为 [-2,-1,3,6],偶数值之和为 -2 + 6 = 4。 ``` ## 解题思路 ### 思路 1:模拟 先计算初始的偶数和,然后对每次查询进行模拟。 1. 先遍历数组 $nums$,计算初始的偶数和 $even\_sum$。 2. 对于每次查询 $[val, index]$: - 如果 $nums[index]$ 是偶数,从 $even\_sum$ 中减去它。 - 更新 $nums[index] = nums[index] + val$。 - 如果更新后的 $nums[index]$ 是偶数,将它加到 $even\_sum$ 中。 - 将当前的 $even\_sum$ 加入结果数组。 3. 返回结果数组。 ### 思路 1:代码 ```python class Solution: def sumEvenAfterQueries(self, nums: List[int], queries: List[List[int]]) -> List[int]: # 计算初始偶数和 even_sum = sum(x for x in nums if x % 2 == 0) result = [] for val, index in queries: # 如果原来是偶数,先从和中减去 if nums[index] % 2 == 0: even_sum -= nums[index] # 更新值 nums[index] += val # 如果更新后是偶数,加到和中 if nums[index] % 2 == 0: even_sum += nums[index] result.append(even_sum) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + q)$,其中 $n$ 是数组 $nums$ 的长度,$q$ 是查询数组 $queries$ 的长度。 - **空间复杂度**:$O(1)$,不考虑结果数组的空间。 ================================================ FILE: docs/solutions/0900-0999/sum-of-subarray-minimums.md ================================================ # [0907. 子数组的最小值之和](https://leetcode.cn/problems/sum-of-subarray-minimums/) - 标签:栈、数组、动态规划、单调栈 - 难度:中等 ## 题目链接 - [0907. 子数组的最小值之和 - 力扣](https://leetcode.cn/problems/sum-of-subarray-minimums/) ## 题目大意 **描述**: 给定一个整数数组 $arr$。 **要求**: 找到 `min(b)` 的总和,其中 $b$ 的范围为 $arr$ 的每个(连续)子数组。 由于答案可能很大,因此 返回答案模 $10^9 + 7$。 **说明**: - $1 \le arr.length \le 3 \times 10^{4}$。 - $1 \le arr[i] \le 3 \times 10^{4}$。 **示例**: - 示例 1: ```python 输入:arr = [3,1,2,4] 输出:17 解释: 子数组为 [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]。 最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。 ``` - 示例 2: ```python 输入:arr = [11,81,94,43,3] 输出:444 ``` ## 解题思路 ### 思路 1:单调栈 对于每个元素 $arr[i]$,我们需要找到它作为最小值的所有子数组。使用单调栈可以高效地找到每个元素左边和右边第一个比它小的元素。 1. 使用单调栈找到每个元素 $arr[i]$ 左边第一个比它小的元素位置 $left[i]$。 2. 使用单调栈找到每个元素 $arr[i]$ 右边第一个比它小的元素位置 $right[i]$。 3. 对于元素 $arr[i]$,它作为最小值的子数组个数为 $(i - left[i]) \times (right[i] - i)$。 4. 累加所有元素的贡献:$arr[i] \times (i - left[i]) \times (right[i] - i)$。 ### 思路 1:代码 ```python class Solution: def sumSubarrayMins(self, arr: List[int]) -> int: MOD = 10**9 + 7 n = len(arr) # left[i] 表示左边第一个小于 arr[i] 的位置 left = [-1] * n # right[i] 表示右边第一个小于 arr[i] 的位置 right = [n] * n # 单调递增栈,找左边第一个更小的元素 stack = [] for i in range(n): while stack and arr[stack[-1]] > arr[i]: stack.pop() if stack: left[i] = stack[-1] stack.append(i) # 单调递增栈,找右边第一个更小的元素 stack = [] for i in range(n - 1, -1, -1): while stack and arr[stack[-1]] >= arr[i]: # 注意这里用 >= 避免重复计算 stack.pop() if stack: right[i] = stack[-1] stack.append(i) # 计算每个元素的贡献 result = 0 for i in range(n): # arr[i] 作为最小值的子数组个数 count = (i - left[i]) * (right[i] - i) result = (result + arr[i] * count) % MOD return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $arr$ 的长度。每个元素最多入栈和出栈各一次。 - **空间复杂度**:$O(n)$,需要使用栈和辅助数组。 ================================================ FILE: docs/solutions/0900-0999/super-palindromes.md ================================================ # [0906. 超级回文数](https://leetcode.cn/problems/super-palindromes/) - 标签:数学、字符串、枚举 - 难度:困难 ## 题目链接 - [0906. 超级回文数 - 力扣](https://leetcode.cn/problems/super-palindromes/) ## 题目大意 **描述**: 如果一个正整数自身是回文数,而且它也是一个回文数的平方,那么我们称这个数为「超级回文数」。 现在,给定两个以字符串形式表示的正整数 $left$ 和 $right$, **要求**: 统计并返回区间 $[left, right]$ 中的「超级回文数」的数目。 **说明**: - $1 \le left.length, right.length \le 18$。 - $left$ 和 $right$ 仅由数字(0 - 9)组成。 - $left$ 和 $right$ 不含前导零。 - $left$ 和 $right$ 表示的整数在区间 $[1, 10^{18} - 1]$ 内。 - $left$ 小于等于 $right$。 **示例**: - 示例 1: ```python 输入:left = "4", right = "1000" 输出:4 解释:4、9、121 和 484 都是超级回文数。 注意 676 不是超级回文数:26 * 26 = 676 ,但是 26 不是回文数。 ``` - 示例 2: ```python 输入:left = "1", right = "2" 输出:1 ``` ## 解题思路 ### 思路 1:枚举 + 回文数构造 超级回文数是回文数的平方,且平方后仍是回文数。我们可以枚举所有可能的回文数,然后检查其平方是否也是回文数。 1. 由于 $left$ 和 $right$ 最大为 $10^{18}$,所以回文数最大为 $\sqrt{10^{18}} = 10^9$。 2. 我们可以通过构造前半部分来生成回文数,这样只需要枚举到 $10^5$ 左右。 3. 对于每个构造的回文数,计算其平方,检查是否在范围内且是回文数。 ### 思路 1:代码 ```python class Solution: def superpalindromesInRange(self, left: str, right: str) -> int: L, R = int(left), int(right) # 判断是否为回文数 def is_palindrome(x): s = str(x) return s == s[::-1] count = 0 # 枚举回文数的前半部分(长度为 1 到 5) for length in range(1, 6): # 枚举前半部分的所有可能值 for i in range(10**(length - 1), 10**length): s = str(i) # 构造奇数长度的回文数 for root in [int(s + s[-2::-1]), int(s + s[::-1])]: square = root * root # 检查平方是否在范围内且是回文数 if L <= square <= R and is_palindrome(square): count += 1 return count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\sqrt[4]{R})$,其中 $R$ 是右边界。需要枚举所有可能的回文数根。 - **空间复杂度**:$O(\log R)$,用于存储字符串。 ================================================ FILE: docs/solutions/0900-0999/tallest-billboard.md ================================================ # [0956. 最高的广告牌](https://leetcode.cn/problems/tallest-billboard/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [0956. 最高的广告牌 - 力扣](https://leetcode.cn/problems/tallest-billboard/) ## 题目大意 **描述**: 你正在安装一个广告牌,并希望它高度最大。这块广告牌将有两个钢制支架,两边各一个。每个钢支架的高度必须相等。 你有一堆可以焊接在一起的钢筋 $rods$。举个例子,如果钢筋的长度为 1、2 和 3,则可以将它们焊接在一起形成长度为 6 的支架。 **要求**: 返回「广告牌的最大可能安装高度」。如果没法安装广告牌,请返回 0。 **说明**: - $0 \le rods.length \le 20$。 - $1 \le rods[i] \le 10^{3}$。 - $sum(rods[i]) \le 5000$。 **示例**: - 示例 1: ```python 输入:[1,2,3,6] 输出:6 解释:我们有两个不相交的子集 {1,2,3} 和 {6},它们具有相同的和 sum = 6。 ``` - 示例 2: ```python 输入:[1,2,3,4,5,6] 输出:10 解释:我们有两个不相交的子集 {2,3,5} 和 {4,6},它们具有相同的和 sum = 10。 ``` ## 解题思路 ### 思路 1:动态规划 这道题可以转化为:将钢筋分成两组,使得两组的和相等,求最大的和。 使用动态规划,$dp[diff]$ 表示当两组差值为 $diff$ 时,较小组的最大高度。 1. 初始化 $dp[0] = 0$,表示差值为 $0$ 时,较小组高度为 $0$。 2. 对于每根钢筋 $rod$,有三种选择: - 不选:不更新 $dp$。 - 放入较大组:$dp[diff + rod] = \max(dp[diff + rod], dp[diff])$。 - 放入较小组:$dp[|diff - rod|] = \max(dp[|diff - rod|], dp[diff] + \min(diff, rod))$。 3. 最终答案为 $dp[0]$。 ### 思路 1:代码 ```python class Solution: def tallestBillboard(self, rods: List[int]) -> int: # dp[diff] 表示差值为 diff 时,较小组的最大高度 dp = {0: 0} for rod in rods: new_dp = dp.copy() for diff, smaller in dp.items(): # 将 rod 加到较大组 new_diff = diff + rod new_dp[new_diff] = max(new_dp.get(new_diff, 0), smaller) # 将 rod 加到较小组 if rod > diff: # 较小组变成较大组 new_diff = rod - diff new_smaller = smaller + diff else: # 较小组仍是较小组 new_diff = diff - rod new_smaller = smaller + rod new_dp[new_diff] = max(new_dp.get(new_diff, 0), new_smaller) dp = new_dp return dp[0] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times S)$,其中 $n$ 是钢筋数量,$S$ 是所有钢筋长度之和。 - **空间复杂度**:$O(S)$,需要存储所有可能的差值状态。 ================================================ FILE: docs/solutions/0900-0999/three-equal-parts.md ================================================ # [0927. 三等分](https://leetcode.cn/problems/three-equal-parts/) - 标签:数组、数学 - 难度:困难 ## 题目链接 - [0927. 三等分 - 力扣](https://leetcode.cn/problems/three-equal-parts/) ## 题目大意 **描述**: 给定一个由 0 和 1 组成的数组 $arr$ ,将数组分成 3 个非空的部分 ,使得所有这些部分表示相同的二进制值。 **要求**: 如果可以做到,请返回任何 $[i, j]$,其中 $i+1 < j$,这样一来: - $arr[0], arr[1], ..., arr[i]$ 为第一部分; - $arr[i + 1], arr[i + 2], ..., arr[j - 1]$ 为第二部分; - $arr[j], arr[j + 1], ..., arr[arr.length - 1]$ 为第三部分。 - 这三个部分所表示的二进制值相等。 如果无法做到,就返回 $[-1, -1]$。 注意,在考虑每个部分所表示的二进制时,应当将其看作一个整体。例如,$[1,1,0]$ 表示十进制中的 6,而不会是 3。此外,前导零也是被允许的,所以 $[0,1,1]$ 和 $[1,1]$ 表示相同的值。 **说明**: - $3 \le arr.length \le 3 * 10^{4}$。 - $arr[i]$ 是 0 或 1。 **示例**: - 示例 1: ```python 输入:arr = [1,0,1,0,1] 输出:[0,3] ``` - 示例 2: ```python 输入:arr = [1,1,0,1,1] 输出:[-1,-1] 示例 3: 输入:arr = [1,1,0,0,1] 输出:[0,2] ``` ## 解题思路 ### 思路 1:数学 + 双指针 要将数组分成三个表示相同二进制值的部分,首先需要统计 $1$ 的个数。 1. 统计数组中 $1$ 的总个数 $ones$。 2. 如果 $ones$ 不能被 $3$ 整除,返回 $[-1, -1]$。 3. 如果 $ones = 0$,说明全是 $0$,返回 $[0, n - 1]$。 4. 每部分应该有 $ones / 3$ 个 $1$。 5. 找到三部分的起始位置(第一个 $1$ 的位置)。 6. 从最后一部分的第一个 $1$ 开始,向前匹配三部分,确保它们表示相同的二进制值。 7. 最后一部分的尾部 $0$ 决定了前两部分的尾部 $0$ 的数量。 ### 思路 1:代码 ```python class Solution: def threeEqualParts(self, arr: List[int]) -> List[int]: n = len(arr) ones = sum(arr) # 如果 1 的个数不能被 3 整除,无法分成三等分 if ones % 3 != 0: return [-1, -1] # 如果全是 0,任意分割都可以 if ones == 0: return [0, n - 1] # 每部分应该有 k 个 1 k = ones // 3 # 找到三部分的第一个 1 的位置 first = second = third = -1 count = 0 for i in range(n): if arr[i] == 1: count += 1 if count == 1: first = i elif count == k + 1: second = i elif count == 2 * k + 1: third = i break # 从第三部分开始,向前匹配 while third < n: if arr[first] != arr[second] or arr[second] != arr[third]: return [-1, -1] first += 1 second += 1 third += 1 # 返回分割点 return [first - 1, second] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $arr$ 的长度。 - **空间复杂度**:$O(1)$,只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0900-0999/time-based-key-value-store.md ================================================ # [0981. 基于时间的键值存储](https://leetcode.cn/problems/time-based-key-value-store/) - 标签:设计、哈希表、字符串、二分查找 - 难度:中等 ## 题目链接 - [0981. 基于时间的键值存储 - 力扣](https://leetcode.cn/problems/time-based-key-value-store/) ## 题目大意 **描述**: 设计一个基于时间的键值数据结构,该结构可以在不同时间戳存储对应同一个键的多个值,并针对特定时间戳检索键对应的值。 **要求**: 实现 TimeMap 类: - `TimeMap()` 初始化数据结构对象 - `void set(String key, String value, int timestamp)` 存储给定时间戳 $timestamp$ 时的键 $key$ 和值 $value$。 - `String get(String key, int timestamp)` 返回一个值,该值在之前调用了 `set`,其中 $timestamp\_prev \le timestamp$。如果有多个这样的值,它将返回与最大 $timestamp\_prev$ 关联的值。如果没有值,则返回空字符串(`""`)。 **说明**: - $1 \le key.length, value.length \le 10^{3}$。 - key 和 value 由小写英文字母和数字组成。 - $1 \le timestamp \le 10^{7}$。 - `set` 操作中的时间戳 timestamp 都是严格递增的。 - 最多调用 `set` 和 `get` 操作 $2 \times 10^{5}$ 次。 **示例**: - 示例 1: ```python 输入: ["TimeMap", "set", "get", "get", "set", "get", "get"] [[], ["foo", "bar", 1], ["foo", 1], ["foo", 3], ["foo", "bar2", 4], ["foo", 4], ["foo", 5]] 输出: [null, null, "bar", "bar", null, "bar2", "bar2"] 解释: TimeMap timeMap = new TimeMap(); timeMap.set("foo", "bar", 1); // 存储键 "foo" 和值 "bar" ,时间戳 timestamp = 1 timeMap.get("foo", 1); // 返回 "bar" timeMap.get("foo", 3); // 返回 "bar", 因为在时间戳 3 和时间戳 2 处没有对应 "foo" 的值,所以唯一的值位于时间戳 1 处(即 "bar") 。 timeMap.set("foo", "bar2", 4); // 存储键 "foo" 和值 "bar2" ,时间戳 timestamp = 4 timeMap.get("foo", 4); // 返回 "bar2" timeMap.get("foo", 5); // 返回 "bar2" ``` ## 解题思路 ### 思路 1:哈希表 + 二分查找 使用哈希表存储每个键对应的时间戳和值的列表,由于时间戳是递增的,可以使用二分查找。 1. 使用哈希表 $data$,键为字符串 $key$,值为列表,列表中每个元素是 $(timestamp, value)$ 元组。 2. `set` 操作:将 $(timestamp, value)$ 添加到 $data[key]$ 的列表末尾。 3. `get` 操作:在 $data[key]$ 的列表中二分查找小于等于 $timestamp$ 的最大时间戳。 ### 思路 1:代码 ```python class TimeMap: def __init__(self): # 哈希表,key -> [(timestamp, value), ...] self.data = collections.defaultdict(list) def set(self, key: str, value: str, timestamp: int) -> None: # 直接添加到列表末尾(时间戳递增) self.data[key].append((timestamp, value)) def get(self, key: str, timestamp: int) -> str: if key not in self.data: return "" pairs = self.data[key] # 二分查找小于等于 timestamp 的最大时间戳 left, right = 0, len(pairs) - 1 result = "" while left <= right: mid = (left + right) // 2 if pairs[mid][0] <= timestamp: result = pairs[mid][1] left = mid + 1 else: right = mid - 1 return result # Your TimeMap object will be instantiated and called as such: # obj = TimeMap() # obj.set(key,value,timestamp) # param_2 = obj.get(key,timestamp) ``` ### 思路 1:复杂度分析 - **时间复杂度**:`set` 操作为 $O(1)$,`get` 操作为 $O(\log n)$,其中 $n$ 是该键对应的时间戳数量。 - **空间复杂度**:$O(N)$,其中 $N$ 是所有 `set` 操作的总次数。 ================================================ FILE: docs/solutions/0900-0999/triples-with-bitwise-and-equal-to-zero.md ================================================ # [0982. 按位与为零的三元组](https://leetcode.cn/problems/triples-with-bitwise-and-equal-to-zero/) - 标签:位运算、数组、哈希表 - 难度:困难 ## 题目链接 - [0982. 按位与为零的三元组 - 力扣](https://leetcode.cn/problems/triples-with-bitwise-and-equal-to-zero/) ## 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:返回其中「按位与三元组」的数目。 **说明**: - **按位与三元组**:由下标 $(i, j, k)$ 组成的三元组,并满足下述全部条件: - $0 \le i < nums.length$。 - $0 \le j < nums.length$。 - $0 \le k < nums.length$。 - $nums[i] \text{ \& } nums[j] \text{ \& } nums[k] == 0$ ,其中 $\text{ \& }$ 表示按位与运算符。 - $1 \le nums.length \le 1000$。 - $0 \le nums[i] < 2^{16}$。 **示例**: - 示例 1: ```python 输入:nums = [2,1,3] 输出:12 解释:可以选出如下 i, j, k 三元组: (i=0, j=0, k=1) : 2 & 2 & 1 (i=0, j=1, k=0) : 2 & 1 & 2 (i=0, j=1, k=1) : 2 & 1 & 1 (i=0, j=1, k=2) : 2 & 1 & 3 (i=0, j=2, k=1) : 2 & 3 & 1 (i=1, j=0, k=0) : 1 & 2 & 2 (i=1, j=0, k=1) : 1 & 2 & 1 (i=1, j=0, k=2) : 1 & 2 & 3 (i=1, j=1, k=0) : 1 & 1 & 2 (i=1, j=2, k=0) : 1 & 3 & 2 (i=2, j=0, k=1) : 3 & 2 & 1 (i=2, j=1, k=0) : 3 & 1 & 2 ``` - 示例 2: ```python 输入:nums = [0,0,0] 输出:27 ``` ## 解题思路 ### 思路 1:枚举 最直接的方法是使用三重循环直接枚举 $(i, j, k)$,然后再判断 $nums[i] \text{ \& } nums[j] \text{ \& } nums[k]$ 是否为 $0$。但是这样做的时间复杂度为 $O(n^3)$。 从题目中可以看出 $nums[i]$ 的值域范围为 $[0, 2^{16}]$,而 $2^{16} = 65536$。所以我们可以按照下面步骤优化时间复杂度: 1. 先使用两重循环枚举 $(i, j)$,计算出 $nums[i] \text{ \& } nums[j]$ 的值,将其存入一个大小为 $2^{16}$ 的数组或者哈希表 $cnts$ 中,并记录每个 $nums[i] \text{ \& } nums[j]$ 值出现的次数。 2. 然后遍历该数组或哈希表,再使用一重循环遍历 $k$,找出所有满足 $nums[k] \text{ \& } x == 0$ 的 $x$,并将其对应数量 $cnts[x]$ 累积到答案 $ans$ 中。 3. 最后返回答案 $ans$ 即可。 ### 思路 1:代码 ```python class Solution: def countTriplets(self, nums: List[int]) -> int: states = 1 << 16 cnts = [0 for _ in range(states)] for num_x in nums: for num_y in nums: cnts[num_x & num_y] += 1 ans = 0 for num in nums: for x in range(states): if num & x == 0: ans += cnts[x] return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 + 2^{16} \times n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(2^{16})$。 ### 思路 2:枚举 + 优化 第一步跟思路 1 一样,我们先使用两重循环枚举 $(i, j)$,计算出 $nums[i] \text{ \& } nums[j]$ 的值,将其存入一个大小为 $2^{16}$ 的数组或者哈希表 $cnts$ 中,并记录每个 $nums[i] \text{ \& } nums[j]$ 值出现的次数。 接下来我们对思路 1 中的第二步进行优化,在思路 1 中,我们是通过枚举数组或哈希表的方式得到 $x$ 的,这里我们换一种方法。 使用一重循环遍历 $k$,对于 $nums[k]$,我们先计算出 $nums[k]$ 的补集,即将 $nums[k]$ 与 $2^{16} - 1$(二进制中 $16$ 个 $1$)进行按位异或操作,得到 $nums[k]$ 的补集 $com$。如果 $nums[k] \text{ \& } x == 0$,则 $x$ 一定是 $com$ 的子集。 换句话说,$x$ 中 $1$ 的位置一定与 $nums[k]$ 中 $1$ 的位置不同,如果 $nums[k]$ 中第 $m$ 位为 $1$,则 $x$ 中第 $m$ 位一定为 $0$。 接下来我们通过下面的方式来枚举子集: 1. 定义子集为 $sub$,初始时赋值为 $com$,即:$sub = com$。 2. 令 $sub$ 减 $1$,然后与 $com$ 做按位与操作,得到下一个子集,即:$sub = (sub - 1) \text{ \& } com$。 3. 不断重复第 $2$ 步,直到 $sub$ 为空集时为止。 这种方法能枚举子集的原理是:$sub$ 减 $1$ 会将最低位的 $1$ 改为 $0$,而比这个 $1$ 更低位的 $0$ 都改为了 $1$。此时再与 $com$ 做按位与操作,就会过保留原本高位上的 $1$,滤掉当前最低位的 $1$,并且保留比这个 $1$ 更低位上的原有的 $1$,也就得到嘞下一个子集。 举个例子,比如补集 $com$ 为 $(00010110)_2$: 1. 初始 $sub = (00010110)_2$。 2. 令其减 $1$ 后为 $(00010101)_2$,然后与 $com$ 做按位与操作,得到下一个子集 $sub = (00010100)_2$,即:$(00010101)_2 \text{ \& } (00010110)_2$)。 3. 令其减 $1$ 后为 $(00010011)_2$,然后与 $com$ 做按位与操作,得到下一个子集 $sub = (00010010)_2$,即: $(00010011)_2 \text{ \& } (00010110)_2$。 4. 令其减 $1$ 后为 $(00010001)_2$,然后与 $com$ 做按位与操作,得到下一个子集 $sub = (00010000)_2$,即:$(00010001)_2 \text{ \& } (00010110)_2$。 5. 令其减 $1$ 后为 $(00001111)_2$,然后与 $com$ 做按位与操作,得到下一个子集 $sub = (00000110)_2$,即:$(00001111)_2 \text{ \& } (00010110)_2$。 6. 令其减 $1$ 后为 $(00000101)_2$,然后与 $com$ 做按位与操作,得到下一个子集 $sub = (00000100)_2$,即:$(00000101)_2 \text{ \& } (00010110)_2$。 7. 令其减 $1$ 后为 $(00000011)_2$,然后与 $com$ 做按位与操作,得到下一个子集 $sub = (00000010)_2$,即:$(00000011)_2 \text{ \& } (00010110)_2$。 8. 令其减 $1$ 后为 $(00000001)_2$,然后与 $com$ 做按位与操作,得到下一个子集 $sub = (00000000)_2$,即:$(00000001)_2 \text{ \& } (00010110)_2$。 9. $sub$ 变为了空集。 ### 思路 2:代码 ```python class Solution: def countTriplets(self, nums: List[int]) -> int: states = 1 << 16 cnts = [0 for _ in range(states)] for num_x in nums: for num_y in nums: cnts[num_x & num_y] += 1 ans = 0 for num in nums: com = num ^ 0xffff # com: num 的补集 sub = com # sub: 子集 while True: ans += cnts[sub] if sub == 0: break sub = (sub - 1) & com return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2 + 2^{16} \times n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(2^{16})$。 ## 参考资料 - 【题解】[按位与为零的三元组 - 按位与为零的三元组](https://leetcode.cn/problems/triples-with-bitwise-and-equal-to-zero/solution/an-wei-yu-wei-ling-de-san-yuan-zu-by-lee-gjud/) - 【题解】[有技巧的枚举 + 常数优化(Python/Java/C++/Go) - 按位与为零的三元组](https://leetcode.cn/problems/triples-with-bitwise-and-equal-to-zero/solution/you-ji-qiao-de-mei-ju-chang-shu-you-hua-daxit/) ================================================ FILE: docs/solutions/0900-0999/unique-email-addresses.md ================================================ # [0929. 独特的电子邮件地址](https://leetcode.cn/problems/unique-email-addresses/) - 标签:数组、哈希表、字符串 - 难度:简单 ## 题目链接 - [0929. 独特的电子邮件地址 - 力扣](https://leetcode.cn/problems/unique-email-addresses/) ## 题目大意 **描述**: 每个「有效电子邮件地址」都由一个「本地名」和一个「域名」组成,以 `'@'` 符号分隔。除小写字母之外,电子邮件地址还可以含有一个或多个 `'.'` 或 `'+'`。 - 例如,在 `alice@leetcode.com` 中, $alice$ 是 本地名 ,而 `leetcode.com` 是「域名」。 如果在电子邮件地址的「本地名」部分中的某些字符之间添加句点(`'.'`),则发往那里的邮件将会转发到本地名中没有点的同一地址。请注意,此规则「不适用于域名」。 - 例如,`"alice.z@leetcode.com"` 和 `"alicez@leetcode.com"` 会转发到同一电子邮件地址。 如果在「本地名」中添加加号(`'+'`),则会忽略第一个加号后面的所有内容。这允许过滤某些电子邮件。同样,此规则「不适用于域名」。 - 例如 `m.y+name@email.com` 将转发到 `my@email.com`。 可以同时使用这两个规则。 给定一个字符串数组 $emails$,我们会向每个 $emails[i]$ 发送一封电子邮件。 **要求**: 返回实际收到邮件的不同地址数目。 **说明**: - $1 \le emails.length \le 10^{3}$。 - $1 \le emails[i].length \le 10^{3}$。 - $emails[i]$ 由小写英文字母、`'+'`、`'.'` 和 `'@'` 组成。 - 每个 $emails[i]$ 都包含有且仅有一个 `'@'` 字符。 - 所有本地名和域名都不为空。 - 本地名不会以 `'+'` 字符作为开头。 - 域名以 `".com"` 后缀结尾。 - 域名在 `".com"` 后缀前至少包含一个字符。 **示例**: - 示例 1: ```python 输入:emails = ["test.email+alex@leetcode.com","test.e.mail+bob.cathy@leetcode.com","testemail+david@lee.tcode.com"] 输出:2 解释:实际收到邮件的是 "testemail@leetcode.com" 和 "testemail@lee.tcode.com"。 ``` - 示例 2: ```python 输入:emails = ["a@leetcode.com","b@leetcode.com","c@leetcode.com"] 输出:3 ``` ## 解题思路 ### 思路 1:哈希表 + 字符串处理 对每个邮箱地址进行规范化处理,然后使用哈希表统计不同的邮箱地址数量。 1. 将邮箱地址按 `@` 分割成本地名和域名。 2. 对本地名进行处理: - 遇到 `+` 时,忽略 `+` 及其后面的所有字符。 - 忽略所有 `.` 字符。 3. 将处理后的本地名和域名组合成规范化的邮箱地址。 4. 使用哈希表统计不同的邮箱地址数量。 ### 思路 1:代码 ```python class Solution: def numUniqueEmails(self, emails: List[str]) -> int: unique_emails = set() for email in emails: # 分割本地名和域名 local, domain = email.split('@') # 处理本地名 # 1. 遇到 '+' 时截断 if '+' in local: local = local[:local.index('+')] # 2. 移除所有 '.' local = local.replace('.', '') # 组合规范化的邮箱地址 normalized = local + '@' + domain unique_emails.add(normalized) return len(unique_emails) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$ 是邮箱地址的数量,$m$ 是每个邮箱地址的平均长度。 - **空间复杂度**:$O(n \times m)$,需要存储所有规范化后的邮箱地址。 ================================================ FILE: docs/solutions/0900-0999/unique-paths-iii.md ================================================ # [0980. 不同路径 III](https://leetcode.cn/problems/unique-paths-iii/) - 标签:位运算、数组、回溯、矩阵 - 难度:困难 ## 题目链接 - [0980. 不同路径 III - 力扣](https://leetcode.cn/problems/unique-paths-iii/) ## 题目大意 **描述**: 在二维网格 $grid$ 上,有 4 种类型的方格: - 1 表示起始方格。且只有一个起始方格。 - 2 表示结束方格,且只有一个结束方格。 - 0 表示我们可以走过的空方格。 - -1 表示我们无法跨越的障碍。 **要求**: 返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目。 **说明**: - 每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格。 - $1 \le grid.length \times grid[0].length \le 20$。 **示例**: - 示例 1: ```python 输入:[[1,0,0,0],[0,0,0,0],[0,0,2,-1]] 输出:2 解释:我们有以下两条路径: 1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2) 2. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2) ``` - 示例 2: ```python 输入:[[1,0,0,0],[0,0,0,0],[0,0,0,2]] 输出:4 解释:我们有以下四条路径: 1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2),(2,3) 2. (0,0),(0,1),(1,1),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,3),(1,3),(2,3) 3. (0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(1,1),(0,1),(0,2),(0,3),(1,3),(2,3) 4. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2),(2,3) ``` ## 解题思路 ### 思路 1:回溯 使用回溯法遍历所有可能的路径,统计从起点到终点且经过所有空方格的路径数量。 1. 首先遍历网格,找到起点位置,统计空方格的数量(包括起点和终点)。 2. 从起点开始进行深度优先搜索(DFS): - 标记当前方格为已访问。 - 如果到达终点,检查是否经过了所有空方格。 - 否则,向四个方向继续搜索。 - 回溯时,恢复当前方格的状态。 3. 返回满足条件的路径数量。 ### 思路 1:代码 ```python class Solution: def uniquePathsIII(self, grid: List[List[int]]) -> int: m, n = len(grid), len(grid[0]) start_x = start_y = 0 empty_count = 0 # 找到起点,统计空方格数量 for i in range(m): for j in range(n): if grid[i][j] == 1: start_x, start_y = i, j if grid[i][j] != -1: empty_count += 1 self.result = 0 def dfs(x, y, count): # 到达终点 if grid[x][y] == 2: # 检查是否经过了所有空方格 if count == empty_count: self.result += 1 return # 标记为已访问 temp = grid[x][y] grid[x][y] = -1 # 向四个方向搜索 for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]: nx, ny = x + dx, y + dy if 0 <= nx < m and 0 <= ny < n and grid[nx][ny] != -1: dfs(nx, ny, count + 1) # 回溯 grid[x][y] = temp dfs(start_x, start_y, 1) return self.result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(4^{m \times n})$,其中 $m$ 和 $n$ 是网格的行数和列数。最坏情况下需要遍历所有可能的路径。 - **空间复杂度**:$O(m \times n)$,递归调用栈的深度最多为网格中方格的数量。 ================================================ FILE: docs/solutions/0900-0999/univalued-binary-tree.md ================================================ # [0965. 单值二叉树](https://leetcode.cn/problems/univalued-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [0965. 单值二叉树 - 力扣](https://leetcode.cn/problems/univalued-binary-tree/) ## 题目大意 **描述**: 给定一棵二叉树的根节点 $root$。如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树。 **要求**: 如果给定的树是单值二叉树,返回 true;否则返回 false。 **说明**: - 给定树的节点数范围是 $[1, 10^{3}]$。 - 每个节点的值都是整数,范围为 $[0, 99]$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/29/screen-shot-2018-12-25-at-50104-pm.png) ```python 输入:[1,1,1,1,1,null,1] 输出:true ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/29/screen-shot-2018-12-25-at-50050-pm.png) ```python 输入:[2,2,2,5,2] 输出:false ``` ## 解题思路 ### 思路 1:深度优先搜索 使用深度优先搜索遍历整棵树,检查每个节点的值是否与根节点的值相同。 1. 如果当前节点为空,返回 $True$。 2. 如果当前节点的值与根节点的值不同,返回 $False$。 3. 递归检查左子树和右子树。 4. 只有当左右子树都是单值二叉树时,整棵树才是单值二叉树。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def isUnivalTree(self, root: Optional[TreeNode]) -> bool: if not root: return True def dfs(node, val): # 空节点返回 True if not node: return True # 当前节点值不等于目标值,返回 False if node.val != val: return False # 递归检查左右子树 return dfs(node.left, val) and dfs(node.right, val) return dfs(root, root.val) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是二叉树的节点数量。需要遍历所有节点。 - **空间复杂度**:$O(h)$,其中 $h$ 是二叉树的高度。递归调用栈的深度为树的高度。 ================================================ FILE: docs/solutions/0900-0999/valid-mountain-array.md ================================================ # [0941. 有效的山脉数组](https://leetcode.cn/problems/valid-mountain-array/) - 标签:数组 - 难度:简单 ## 题目链接 - [0941. 有效的山脉数组 - 力扣](https://leetcode.cn/problems/valid-mountain-array/) ## 题目大意 **描述**: 如果 $arr$ 满足下述条件,那么它是一个山脉数组: - $arr.length \ge 3$ - 在 0 < i < arr.length - 1$ 条件下,存在 $i$ 使得: - $arr[0] < arr[1] < ... arr[i-1] < arr[i]$ - $arr[i] > arr[i+1] > ... > arr[arr.length - 1]$ ![](https://assets.leetcode.com/uploads/2019/10/20/hint_valid_mountain_array.png) 给定一个整数数组 $arr$。 **要求**: 如果 $arr$ 是有效的山脉数组就返回 true,否则返回 false。 **说明**: - $1 \le arr.length \le 10^{4}$。 - $0 \le arr[i] \le 10^{4}$。 **示例**: - 示例 1: ```python 输入:arr = [2,1] 输出:false ``` - 示例 2: ```python 输入:arr = [3,5,5] 输出:false ``` ## 解题思路 ### 思路 1:双指针 使用双指针分别从左右两端向中间移动,找到山峰位置。 1. 如果数组长度小于 $3$,直接返回 $False$。 2. 使用指针 $left$ 从左向右移动,找到第一个不再递增的位置。 3. 使用指针 $right$ 从右向左移动,找到第一个不再递减的位置。 4. 检查 $left$ 和 $right$ 是否相等,且不在数组的两端(必须有上升和下降部分)。 ### 思路 1:代码 ```python class Solution: def validMountainArray(self, arr: List[int]) -> bool: n = len(arr) # 长度小于 3,不可能是山脉数组 if n < 3: return False left = 0 # 从左向右找到山峰 while left < n - 1 and arr[left] < arr[left + 1]: left += 1 # 山峰不能在两端 if left == 0 or left == n - 1: return False # 从山峰向右检查是否递减 while left < n - 1 and arr[left] > arr[left + 1]: left += 1 # 如果到达数组末尾,说明是有效的山脉数组 return left == n - 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是数组 $arr$ 的长度。 - **空间复杂度**:$O(1)$,只使用了常数级别的额外空间。 ================================================ FILE: docs/solutions/0900-0999/valid-permutations-for-di-sequence.md ================================================ # [0903. DI 序列的有效排列](https://leetcode.cn/problems/valid-permutations-for-di-sequence/) - 标签:字符串、动态规划、前缀和 - 难度:困难 ## 题目链接 - [0903. DI 序列的有效排列 - 力扣](https://leetcode.cn/problems/valid-permutations-for-di-sequence/) ## 题目大意 **描述**: 给定一个长度为 $n$ 的字符串 $s$,其中 $s[i]$ 是: - `"D"` 意味着减少,或者 - `"I"` 意味着增加 「有效排列」是对有 $n + 1$ 个在 $[0, n]$ 范围内的整数的一个排列 $perm$,使得对所有的 $i$: - 如果 $s[i] == 'D'$,那么 $perm[i] > perm[i+1]$,以及; - 如果 $s[i] == 'I'$,那么 $perm[i] < perm[i+1]$。 **要求**: 返回「有效排列 $perm$」的数量。因为答案可能很大,所以请返回你的答案对 $10^9 + 7$ 取余。 **说明**: - $n == s.length$。 - $1 \le n \le 200$。 - $s[i]$ 不是 `'I'` 就是 `'D'`。 **示例**: - 示例 1: ```python 输入:s = "DID" 输出:5 解释: (0, 1, 2, 3) 的五个有效排列是: (1, 0, 3, 2) (2, 0, 3, 1) (2, 1, 3, 0) (3, 0, 2, 1) (3, 1, 2, 0) ``` - 示例 2: ```python 输入: s = "D" 输出: 1 ``` ## 解题思路 ### 思路 1:动态规划 使用动态规划,$dp[i][j]$ 表示长度为 $i + 1$ 的排列中,最后一个数字在这 $i + 1$ 个数字中排名第 $j$(从小到大排序后的位置)的方案数。 关键理解: - 对于长度为 $i + 1$ 的排列,我们只关心相对大小关系,可以用 $0$ 到 $i$ 的排列表示。 - $dp[i][j]$ 表示前 $i + 1$ 个位置,最后一个位置的数字在这 $i + 1$ 个数字中排第 $j$ 小的方案数。 状态转移: - 如果 $s[i - 1] == 'D'$(第 $i - 1$ 个位置大于第 $i$ 个位置): - 前一个位置的数字必须大于当前位置,即前一个位置排名 $\ge j$ - $dp[i][j] = \sum_{k=j}^{i-1} dp[i-1][k]$ - 如果 $s[i - 1] == 'I'$(第 $i - 1$ 个位置小于第 $i$ 个位置): - 前一个位置的数字必须小于当前位置,即前一个位置排名 $< j$ - $dp[i][j] = \sum_{k=0}^{j-1} dp[i-1][k]$ ### 思路 1:代码 ```python class Solution: def numPermsDISequence(self, s: str) -> int: MOD = 10**9 + 7 n = len(s) # dp[i][j] 表示长度为 i+1 的排列,最后一个数字排名第 j 的方案数 dp = [[0] * (n + 2) for _ in range(n + 2)] dp[0][0] = 1 # 长度为 1 的排列,只有一个数字,排名第 0 for i in range(n): for j in range(i + 2): # 长度为 i+2 的排列,排名范围是 0 到 i+1 if s[i] == 'D': # 前一个位置的数字要大于当前位置 # 前一个位置排名 >= j(因为插入新数字后,排名会变化) for k in range(j, i + 1): dp[i + 1][j] = (dp[i + 1][j] + dp[i][k]) % MOD else: # s[i] == 'I' # 前一个位置的数字要小于当前位置 # 前一个位置排名 < j for k in range(j): dp[i + 1][j] = (dp[i + 1][j] + dp[i][k]) % MOD # 统计所有可能的结果 result = 0 for j in range(n + 1): result = (result + dp[n][j]) % MOD return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 是字符串 $s$ 的长度。 - **空间复杂度**:$O(n^2)$,需要存储动态规划数组。 ### 思路 2:动态规划 + 前缀和优化 在思路 1 的基础上,使用前缀和优化求和过程,将时间复杂度降低到 $O(n^2)$。 ### 思路 2:代码 ```python class Solution: def numPermsDISequence(self, s: str) -> int: MOD = 10**9 + 7 n = len(s) # dp[j] 表示当前长度的排列,最后一个数字排名第 j 的方案数 dp = [1] # 初始长度为 1,只有一种方案 for i in range(n): new_dp = [0] * (i + 2) if s[i] == 'D': # 从右向左累加(前缀和) cumsum = 0 for j in range(i, -1, -1): cumsum = (cumsum + dp[j]) % MOD new_dp[j] = cumsum else: # s[i] == 'I' # 从左向右累加(前缀和) cumsum = 0 for j in range(i + 1): cumsum = (cumsum + dp[j]) % MOD new_dp[j + 1] = cumsum dp = new_dp # 返回所有方案数之和 return sum(dp) % MOD ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 是字符串 $s$ 的长度。 - **空间复杂度**:$O(n)$,使用滚动数组优化空间。 ================================================ FILE: docs/solutions/0900-0999/validate-stack-sequences.md ================================================ # [0946. 验证栈序列](https://leetcode.cn/problems/validate-stack-sequences/) - 标签:栈、数组、模拟 - 难度:中等 ## 题目链接 - [0946. 验证栈序列 - 力扣](https://leetcode.cn/problems/validate-stack-sequences/) ## 题目大意 **描述**:给定两个整数序列 `pushed` 和 `popped`,每个序列中的值都不重复。 **要求**:如果第一个序列为空栈的压入顺序,而第二个序列 `popped` 为该栈的压出序列,则返回 `True`,否则返回 `False`。 **说明**: - $1 \le pushed.length \le 1000$。 - $0 \le pushed[i] \le 1000$。 - $pushed$ 的所有元素互不相同。 - $popped.length == pushed.length$。 - $popped$ 是 $pushed$ 的一个排列。 **示例**: - 示例 1: ```python 输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1] 输出:true 解释:我们可以按以下顺序执行: push(1), push(2), push(3), push(4), pop() -> 4, push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1 ``` - 示例 2: ```python 输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2] 输出:false 解释:1 不能在 2 之前弹出。 ``` ## 解题思路 ### 思路 1:栈 借助一个栈来模拟压入、压出的操作。检测最后是否能模拟成功。 ### 思路 1:代码 ```python class Solution: def validateStackSequences(self, pushed: List[int], popped: List[int]) -> bool: stack = [] index = 0 for item in pushed: stack.append(item) while (stack and stack[-1] == popped[index]): stack.pop() index += 1 return len(stack) == 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/0900-0999/verifying-an-alien-dictionary.md ================================================ # [0953. 验证外星语词典](https://leetcode.cn/problems/verifying-an-alien-dictionary/) - 标签:数组、哈希表、字符串 - 难度:简单 ## 题目链接 - [0953. 验证外星语词典 - 力扣](https://leetcode.cn/problems/verifying-an-alien-dictionary/) ## 题目大意 给定一组用外星语书写的单词字符串数组 `words`,以及表示外星字母表的顺序的字符串 `order` 。 要求:判断 `words` 中的单词是否都是按照 `order` 来排序的。如果是,则返回 `True`,否则返回 `False`。 ## 解题思路 如果所有单词是按照 `order` 的规则升序排列,则所有单词都符合规则。而判断所有单词是升序排列,只需要两两比较相邻的单词即可。所以我们可以先用哈希表存储所有字母的顺序,然后对所有相邻单词进行两两比较,如果最终是升序排列,则符合要求。具体步骤如下: - 使用哈希表 `order_map` 存储字母的顺序。 - 遍历单词数组 `words`,比较相邻单词 `word1` 和 `word2` 中所有字母在 `order_map` 中的下标,看是否满足 `word1 <= word2`。 - 如果全部满足,则返回 `True`。如果有不满足的情况,则直接返回 `False`。 ## 代码 ```python class Solution: def isAlienSorted(self, words: List[str], order: str) -> bool: order_map = dict() for i in range(len(order)): order_map[order[i]] = i for i in range(len(words) - 1): word1 = words[i] word2 = words[i + 1] flag = True for j in range(min(len(word1), len(word2))): if word1[j] != word2[j]: if order_map[word1[j]] > order_map[word2[j]]: return False else: flag = False break if flag and len(word1) > len(word2): return False return True ``` ================================================ FILE: docs/solutions/0900-0999/vertical-order-traversal-of-a-binary-tree.md ================================================ # [0987. 二叉树的垂序遍历](https://leetcode.cn/problems/vertical-order-traversal-of-a-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、哈希表、二叉树、排序 - 难度:困难 ## 题目链接 - [0987. 二叉树的垂序遍历 - 力扣](https://leetcode.cn/problems/vertical-order-traversal-of-a-binary-tree/) ## 题目大意 **描述**: 给定二叉树的根结点 $root$ ,请你设计算法计算二叉树的「垂序遍历」序列。 对位于 $(row, col)$ 的每个结点而言,其左右子结点分别位于 $(row + 1, col - 1)$ 和 $(row + 1, col + 1)$。树的根结点位于 $(0, 0)$。 二叉树的「垂序遍历」从最左边的列开始直到最右边的列结束,按列索引每一列上的所有结点,形成一个按出现位置从上到下排序的有序列表。如果同行同列上有多个结点,则按结点的值从小到大进行排序。 **要求**: 返回二叉树的「垂序遍历」序列。 **说明**: - 树中结点数目总数在范围 $[1, 10^{3}]$ 内。 - $0 \le Node.val \le 10^{3}$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/01/29/vtree1.jpg) ```python 输入:root = [3,9,20,null,null,15,7] 输出:[[9],[3,15],[20],[7]] 解释: 列 -1 :只有结点 9 在此列中。 列 0 :只有结点 3 和 15 在此列中,按从上到下顺序。 列 1 :只有结点 20 在此列中。 列 2 :只有结点 7 在此列中。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/01/29/vtree2.jpg) ```python 输入:root = [1,2,3,4,5,6,7] 输出:[[4],[2],[1,5,6],[3],[7]] 解释: 列 -2 :只有结点 4 在此列中。 列 -1 :只有结点 2 在此列中。 列 0 :结点 1 、5 和 6 都在此列中。 1 在上面,所以它出现在前面。 5 和 6 位置都是 (2, 0) ,所以按值从小到大排序,5 在 6 的前面。 列 1 :只有结点 3 在此列中。 列 2 :只有结点 7 在此列中。 ``` ## 解题思路 ### 思路 1:深度优先搜索 + 排序 使用深度优先搜索遍历二叉树,记录每个节点的位置 $(row, col)$ 和值,然后按照题目要求排序。 1. 使用 DFS 遍历二叉树,记录每个节点的 $(col, row, val)$。 2. 按照以下规则排序: - 首先按列 $col$ 从小到大排序。 - 列相同时,按行 $row$ 从小到大排序。 - 行和列都相同时,按值 $val$ 从小到大排序。 3. 将排序后的节点按列分组,返回结果。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def verticalTraversal(self, root: Optional[TreeNode]) -> List[List[int]]: nodes = [] # 存储 (col, row, val) def dfs(node, row, col): if not node: return nodes.append((col, row, node.val)) dfs(node.left, row + 1, col - 1) dfs(node.right, row + 1, col + 1) dfs(root, 0, 0) # 按照 col, row, val 排序 nodes.sort() # 按列分组 result = [] prev_col = float('-inf') for col, row, val in nodes: if col != prev_col: result.append([]) prev_col = col result[-1].append(val) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \log n)$,其中 $n$ 是二叉树的节点数量。需要遍历所有节点并排序。 - **空间复杂度**:$O(n)$,需要存储所有节点的位置信息。 ================================================ FILE: docs/solutions/0900-0999/vowel-spellchecker.md ================================================ # [0966. 元音拼写检查器](https://leetcode.cn/problems/vowel-spellchecker/) - 标签:数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [0966. 元音拼写检查器 - 力扣](https://leetcode.cn/problems/vowel-spellchecker/) ## 题目大意 **描述**: 在给定单词列表 $wordlist$ 的情况下,我们希望实现一个拼写检查器,将查询单词转换为正确的单词。 对于给定的查询单词 $query$,拼写检查器将会处理两类拼写错误: - 大小写:如果查询匹配单词列表中的某个单词(不区分大小写),则返回的正确单词与单词列表中的大小写相同。 - 例如:`wordlist = ["yellow"]`, `query = "YellOw"`: `correct = "yellow"` - 例如:`wordlist = ["yellow"]`, `query = "yellow"`: `correct = "Yellow"` - 例如:`wordlist = ["yellow"]`, `query = "yellow"`: `correct = "yellow"` - 元音错误:如果在将查询单词中的元音 (`'a'`, `'e'`, `'i'`, `'o'`, `'u'`) 分别替换为任何元音后,能与单词列表中的单词匹配(不区分大小写),则返回的正确单词与单词列表中的匹配项大小写相同。 - 例如:`wordlist = ["yellow"]`, `query = "yollow"`: `correct = "YellOw"` - 例如:`wordlist = ["yellow"]`, `query = "yeellow"`: `correct = ""` (无匹配项) - 例如:`wordlist = ["yellow"]`, `query = "yllw"`: `correct = ""` (无匹配项) 此外,拼写检查器还按照以下优先级规则操作: - 当查询完全匹配单词列表中的某个单词(区分大小写)时,应返回相同的单词。 - 当查询匹配到大小写问题的单词时,您应该返回单词列表中的第一个这样的匹配项。 - 当查询匹配到元音错误的单词时,您应该返回单词列表中的第一个这样的匹配项。 - 如果该查询在单词列表中没有匹配项,则应返回空字符串。 给定一些查询 $queries$。 **要求**: 返回一个单词列表 $answer$,其中 $answer[i]$ 是由查询 $query = queries[i]$ 得到的正确单词。 **说明**: - $1 \le wordlist.length, queries.length \le 5000$。 - $1 \le wordlist[i].length, queries[i].length \le 7$。 - $wordlist[i]$ 和 $queries[i]$ 只包含英文字母。 **示例**: - 示例 1: ```python 输入:wordlist = ["KiTe","kite","hare","Hare"], queries = ["kite","Kite","KiTe","Hare","HARE","Hear","hear","keti","keet","keto"] 输出:["kite","KiTe","KiTe","Hare","hare","","","KiTe","","KiTe"] ``` - 示例 2: ```python 输入:wordlist = ["yellow"], queries = ["YellOw"] 输出:["yellow"] ``` ## 解题思路 ### 思路 1:哈希表 使用三个哈希表分别处理三种匹配情况:完全匹配、大小写匹配、元音匹配。 1. 建立三个哈希表: - $exact$:存储完全匹配的单词。 - $lower$:存储小写形式的单词(用于大小写匹配)。 - $vowel$:存储元音替换后的单词(用于元音匹配)。 2. 对于每个查询,按优先级依次查找: - 首先查找完全匹配。 - 其次查找大小写匹配。 - 最后查找元音匹配。 3. 如果都没有匹配,返回空字符串。 ### 思路 1:代码 ```python class Solution: def spellchecker(self, wordlist: List[str], queries: List[str]) -> List[str]: # 将元音替换为统一字符 def devowel(word): return ''.join('*' if c in 'aeiouAEIOU' else c for c in word) # 三个哈希表 exact = set(wordlist) # 完全匹配 lower = {} # 大小写匹配 vowel = {} # 元音匹配 for word in wordlist: lower_word = word.lower() vowel_word = devowel(lower_word) # 只保留第一个匹配的单词 if lower_word not in lower: lower[lower_word] = word if vowel_word not in vowel: vowel[vowel_word] = word result = [] for query in queries: # 1. 完全匹配 if query in exact: result.append(query) # 2. 大小写匹配 elif query.lower() in lower: result.append(lower[query.lower()]) # 3. 元音匹配 elif devowel(query.lower()) in vowel: result.append(vowel[devowel(query.lower())]) # 4. 无匹配 else: result.append("") return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m + q \times m)$,其中 $n$ 是单词列表的长度,$q$ 是查询列表的长度,$m$ 是单词的平均长度。 - **空间复杂度**:$O(n \times m)$,需要存储所有单词的不同形式。 ================================================ FILE: docs/solutions/0900-0999/word-subsets.md ================================================ # [0916. 单词子集](https://leetcode.cn/problems/word-subsets/) - 标签:数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [0916. 单词子集 - 力扣](https://leetcode.cn/problems/word-subsets/) ## 题目大意 **描述**: 给定两个字符串数组 $words1$ 和 $words2$。 现在,如果 $b$ 中的每个字母都出现在 $a$ 中,包括重复出现的字母,那么称字符串 $b$ 是字符串 $a$ 的「子集」。 - 例如,`"wrr"` 是 `"warrior"` 的子集,但不是 `"world"` 的子集。 如果对 $words2$ 中的每一个单词 $b$,$b$ 都是 $a$ 的子集,那么我们称 $words1$ 中的单词 $a$ 是「通用单词」。 **要求**: 以数组形式返回 $words1$ 中所有的「通用」单词。你可以按任意顺序返回答案。 **说明**: - $1 \le words1.length, words2.length \le 10^{4}$。 - $1 \le words1[i].length, words2[i].length \le 10$。 - $words1[i]$ 和 $words2[i]$ 仅由小写英文字母组成。 - $words1$ 中的所有字符串「互不相同」。 **示例**: - 示例 1: ```python 输入:words1 = ["amazon","apple","facebook","google","leetcode"], words2 = ["e","o"] 输出:["facebook","google","leetcode"] ``` - 示例 2: ```python 输入:words1 = ["amazon","apple","facebook","google","leetcode"], words2 = ["lc","eo"] 输出:["leetcode"] ``` ## 解题思路 ### 思路 1:哈希表 对于 $words1$ 中的每个单词,检查它是否包含 $words2$ 中所有单词的字母(包括重复)。 1. 首先统计 $words2$ 中每个字母的最大需求量(对所有单词取并集)。 2. 对于 $words1$ 中的每个单词,统计其字母频率。 3. 检查该单词是否满足 $words2$ 的所有字母需求。 4. 如果满足,将该单词加入结果列表。 ### 思路 1:代码 ```python class Solution: def wordSubsets(self, words1: List[str], words2: List[str]) -> List[str]: # 统计 words2 中每个字母的最大需求量 max_freq = collections.Counter() for word in words2: freq = collections.Counter(word) for char, count in freq.items(): max_freq[char] = max(max_freq[char], count) result = [] # 检查 words1 中的每个单词 for word in words1: freq = collections.Counter(word) # 检查是否满足所有字母需求 is_universal = True for char, count in max_freq.items(): if freq[char] < count: is_universal = False break if is_universal: result.append(word) return result ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n_1 \times m_1 + n_2 \times m_2)$,其中 $n_1$ 和 $n_2$ 分别是 $words1$ 和 $words2$ 的长度,$m_1$ 和 $m_2$ 是单词的平均长度。 - **空间复杂度**:$O(1)$,字母表大小固定为 $26$。 ================================================ FILE: docs/solutions/0900-0999/x-of-a-kind-in-a-deck-of-cards.md ================================================ # [0914. 卡牌分组](https://leetcode.cn/problems/x-of-a-kind-in-a-deck-of-cards/) - 标签:数组、哈希表、数学、计数、数论 - 难度:简单 ## 题目链接 - [0914. 卡牌分组 - 力扣](https://leetcode.cn/problems/x-of-a-kind-in-a-deck-of-cards/) ## 题目大意 **描述**: 给定一副牌,每张牌上都写着一个整数。 此时,你需要选定一个数字 $X$,使我们可以将整副牌按下述规则分成 1 组或更多组: - 每组都有 $X$ 张牌。 - 组内所有的牌上都写着相同的整数。 **要求**: 仅当你可选的 $X \ge 2$ 时返回 true,否则返回 false。 **说明**: - $1 \le deck.length \le 10^{4}$。 - $0 \le deck[i] \lt 10^{4}$。 **示例**: - 示例 1: ```python 输入:deck = [1,2,3,4,4,3,2,1] 输出:true 解释:可行的分组是 [1,1],[2,2],[3,3],[4,4] ``` - 示例 2: ```python 输入:deck = [1,1,1,2,2,2,3,3] 输出:false 解释:没有满足要求的分组。 ``` ## 解题思路 ### 思路 1:最大公约数 要将卡牌分组,每组有 $X$ 张牌且组内所有牌相同,$X \ge 2$。这等价于找到所有牌的出现次数的最大公约数,且该最大公约数 $\ge 2$。 1. 使用哈希表统计每张牌的出现次数。 2. 计算所有出现次数的最大公约数 $g$。 3. 如果 $g \ge 2$,返回 $True$;否则返回 $False$。 ### 思路 1:代码 ```python class Solution: def hasGroupsSizeX(self, deck: List[int]) -> bool: from math import gcd from functools import reduce # 统计每张牌的出现次数 count = collections.Counter(deck) # 计算所有出现次数的最大公约数 g = reduce(gcd, count.values()) # 最大公约数至少为 2 return g >= 2 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + k \log C)$,其中 $n$ 是卡牌数量,$k$ 是不同卡牌的种类数,$C$ 是最大的出现次数。 - **空间复杂度**:$O(k)$,需要存储每张牌的出现次数。 ================================================ FILE: docs/solutions/1000-1099/best-sightseeing-pair.md ================================================ # [1014. 最佳观光组合](https://leetcode.cn/problems/best-sightseeing-pair/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [1014. 最佳观光组合 - 力扣](https://leetcode.cn/problems/best-sightseeing-pair/) ## 题目大意 给你一个正整数数组 `values`,其中 `values[i]` 表示第 `i` 个观光景点的评分,并且两个景点 `i` 和 `j` 之间的距离 为 `j - i`。一对景点(`i < j`)组成的观光组合的得分为 `values[i] + values[j] + i - j`,也就是景点的评分之和减去它们两者之间的距离。 要求:返回一对观光景点能取得的最高分。 ## 解题思路 求解的是 `ans = max(values[i] + values[j] + i - j)`。对于当前第 `j` 个位置上的元素来说,`values[j] - j` 的值是固定的,求解 `ans` 就是在求解 `values[i] + i` 的最大值。我们使用一个变量 `max_score` 来存储当前第 `j` 个位置元素之前 `values[i] + i` 的最大值。然后遍历数组,求出每一个元素位置之前 `values[i] + i` 的最大值,并找出其中最大的 `ans`。 ## 代码 ```python class Solution: def maxScoreSightseeingPair(self, values: List[int]) -> int: ans = 0 max_score = values[0] for i in range(1, len(values)): ans = max(ans, max_score + values[i] - i) max_score = max(max_score, values[i] + i) return ans ``` ================================================ FILE: docs/solutions/1000-1099/binary-search-tree-to-greater-sum-tree.md ================================================ # [1038. 从二叉搜索树到更大和树](https://leetcode.cn/problems/binary-search-tree-to-greater-sum-tree/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [1038. 从二叉搜索树到更大和树 - 力扣](https://leetcode.cn/problems/binary-search-tree-to-greater-sum-tree/) ## 题目大意 给定一棵二叉搜索树(BST)的根节点,且二叉搜索树的节点值各不相同。 要求:将它的每个节点的值替换成树中大于或者等于该节点值的所有节点值之和。 二叉搜索树的定义: - 如果左子树不为空,则左子树上所有节点值均小于它的根节点值; - 如果右子树不为空,则右子树上所有节点值均大于它的根节点值; - 任意节点的左、右子树也分别为二叉搜索树。 ## 解题思路 题目要求将每个节点的值修改为原来的节点值加上大于它的节点值之和。已知二叉搜索树的中序遍历可以得到一个升序数组。 题目就可以变为:修改升序数组中每个节点值为末尾元素累加和。由于末尾元素累加和的求和过程和遍历顺序相反,所以我们可以考虑换种思路。 二叉搜索树的中序遍历顺序为:左 -> 根 -> 右,从而可以得到一个升序数组,那么我们将左右反着遍历,即顺序为:右 -> 根 -> 左,就可以得到一个降序数组,这样就可以在遍历的同时求前缀和。 当然我们在计算前缀和的时候,需要用到前一个节点的值,所以需要用变量 `pre` 存储前一节点的值。 ## 代码 ```python class Solution: pre = 0 def createBinaryTree(self, root: TreeNode): if not root: return self.createBinaryTree(root.right) root.val += self.pre self.pre = root.val self.createBinaryTree(root.left) def bstToGst(self, root: TreeNode) -> TreeNode: self.pre = 0 self.createBinaryTree(root) return root ``` ================================================ FILE: docs/solutions/1000-1099/camelcase-matching.md ================================================ # [1023. 驼峰式匹配](https://leetcode.cn/problems/camelcase-matching/) - 标签:字典树、双指针、字符串、字符串匹配 - 难度:中等 ## 题目链接 - [1023. 驼峰式匹配 - 力扣](https://leetcode.cn/problems/camelcase-matching/) ## 题目大意 **描述**:给定待查询列表 `queries`,和模式串 `pattern`。如果我们可以将小写字母(0 个或多个)插入模式串 `pattern` 中间(任意位置)得到待查询项 `queries[i]`,那么待查询项与给定模式串匹配。如果匹配,则对应答案为 `True`,否则为 `False`。 **要求**:将匹配结果存入由布尔值组成的答案列表中,并返回。 **说明**: - $1 \le queries.length \le 100$。 - $1 \le queries[i].length \le 100$。 - $1 \le pattern.length \le 100$。 - 所有字符串都仅由大写和小写英文字母组成。 **示例**: - 示例 1: ```python 输入:queries = ["FooBar","FooBarTest","FootBall","FrameBuffer","ForceFeedBack"], pattern = "FB" 输出:[true,false,true,true,false] 示例: "FooBar" 可以这样生成:"F" + "oo" + "B" + "ar"。 "FootBall" 可以这样生成:"F" + "oot" + "B" + "all". "FrameBuffer" 可以这样生成:"F" + "rame" + "B" + "uffer". ``` - 示例 2: ```python 输入:queries = ["FooBar","FooBarTest","FootBall","FrameBuffer","ForceFeedBack"], pattern = "FoBa" 输出:[true,false,true,false,false] 解释: "FooBar" 可以这样生成:"Fo" + "o" + "Ba" + "r". "FootBall" 可以这样生成:"Fo" + "ot" + "Ba" + "ll". ``` ## 解题思路 ### 思路 1:字典树 构建一棵字典树,将 `pattern` 存入字典树中。 1. 对于 `queries[i]` 中的每个字符串。逐个字符与 `pattern` 进行匹配。 1. 如果遇见小写字母,直接跳过。 2. 如果遇见大写字母,但是不能匹配,返回 `False`。 3. 如果遇见大写字母,且可以匹配,继续查找。 4. 如果到达末尾仍然匹配,则返回 `True`。 2. 最后将所有结果存入答案数组中返回。 ### 思路 1:代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ord(ch) > 96: if ch not in cur.children: continue else: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None and cur.isEnd class Solution: def camelMatch(self, queries: List[str], pattern: str) -> List[bool]: trie_tree = Trie() trie_tree.insert(pattern) res = [] for query in queries: res.append(trie_tree.search(query)) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times |T| + |pattern|)$。其中 $n$ 是待查询项的数目,$|T|$ 是最长的待查询项的字符串长度,$|pattern|$ 是字符串 `pattern` 的长度。 - **空间复杂度**:$O(|pattern|)$。 ================================================ FILE: docs/solutions/1000-1099/capacity-to-ship-packages-within-d-days.md ================================================ # [1011. 在 D 天内送达包裹的能力](https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [1011. 在 D 天内送达包裹的能力 - 力扣](https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days/) ## 题目大意 **描述**:传送带上的包裹必须在 $D$ 天内从一个港口运送到另一个港口。给定所有包裹的重量数组 $weights$,货物必须按照给定的顺序装运。且每天船上装载的重量不会超过船的最大运载重量。 **要求**:求能在 $D$ 天内将所有包裹送达的船的最低运载量。 **说明**: - $1 \le days \le weights.length \le 5 * 10^4$。 - $1 \le weights[i] \le 500$。 **示例**: - 示例 1: ```python 输入:weights = [1,2,3,4,5,6,7,8,9,10], days = 5 输出:15 解释: 船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示: 第 1 天:1, 2, 3, 4, 5 第 2 天:6, 7 第 3 天:8 第 4 天:9 第 5 天:10 请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。 ``` - 示例 2: ```python 输入:weights = [3,2,2,4,1,4], days = 3 输出:6 解释: 船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示: 第 1 天:3, 2 第 2 天:2, 4 第 3 天:1, 4 ``` ## 解题思路 ### 思路 1:二分查找 船最小的运载能力,最少也要等于或大于最重的那件包裹,即 $max(weights)$。最多的话,可以一次性将所有包裹运完,即 $sum(weights)$。船的运载能力介于 $[max(weights), sum(weights)]$ 之间。 我们现在要做的就是从这个区间内,找到满足可以在 $D$ 天内运送完所有包裹的最小载重量。 可以通过二分查找的方式,找到满足要求的最小载重量。 ### 思路 1:代码 ```python class Solution: def shipWithinDays(self, weights: List[int], D: int) -> int: left = max(weights) right = sum(weights) while left < right: mid = (left + right) >> 1 days = 1 cur = 0 for weight in weights: if cur + weight > mid: days += 1 cur = 0 cur += weight if days <= D: right = mid else: left = mid + 1 return left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。二分查找算法的时间复杂度为 $O(\log n)$。 - **空间复杂度**:$O(1)$。只用到了常数空间存放若干变量。 ================================================ FILE: docs/solutions/1000-1099/coloring-a-border.md ================================================ # [1034. 边界着色](https://leetcode.cn/problems/coloring-a-border/) - 标签:深度优先搜索、广度优先搜索、数组、矩阵 - 难度:中等 ## 题目链接 - [1034. 边界着色 - 力扣](https://leetcode.cn/problems/coloring-a-border/) ## 题目大意 给定一个二维整数矩阵 `grid`,其中 `grid[i][j]` 表示矩阵第 `i` 行、第 `j` 列上网格块的颜色值。再给定一个起始位置 `(row, col)`,以及一个目标颜色 `color`。 要求:对起始位置 `(row, col)` 所在的连通分量边界填充颜色为 `color`。并返回最终的二维整数矩阵 `grid`。 - 连通分量:当两个相邻(上下左右四个方向上)网格块的颜色值相同时,它们属于同一连通分量。 - 连通分量边界:当前连通分量最外圈的所有网格块,这些网格块与连通分量的颜色相同,与其他周围网格块颜色不同。边界上的网格块也是连通分量边界。 ## 解题思路 深度优先搜索。使用二维数组 `visited` 标记访问过的节点。遍历上、下、左、右四个方向上的点。如果下一个点位置越界,或者当前位置与下一个点位置颜色不一样,则对该节点进行染色。 在遍历的过程中注意使用 `visited` 标记访问过的节点,以免重复遍历。 ## 代码 ```python class Solution: directs = [(0, 1), (0, -1), (1, 0), (-1, 0)] def dfs(self, grid, i, j, origin_color, color, visited): rows, cols = len(grid), len(grid[0]) for direct in self.directs: new_i = i + direct[0] new_j = j + direct[1] # 下一个位置越界,则当前点在边界,对其进行着色 if new_i < 0 or new_i >= rows or new_j < 0 or new_j >= cols: grid[i][j] = color continue # 如果访问过,则跳过 if visited[new_i][new_j]: continue # 如果下一个位置颜色与当前颜色相同,则继续搜索 if grid[new_i][new_j] == origin_color: visited[new_i][new_j] = True self.dfs(grid, new_i, new_j, origin_color, color, visited) # 下一个位置颜色与当前颜色不同,则当前位置为连通区域边界,对其进行着色 else: grid[i][j] = color def colorBorder(self, grid: List[List[int]], row: int, col: int, color: int) -> List[List[int]]: if not grid: return grid rows, cols = len(grid), len(grid[0]) visited = [[False for _ in range(cols)] for _ in range(rows)] visited[row][col] = True self.dfs(grid, row, col, grid[row][col], color, visited) return grid ``` ================================================ FILE: docs/solutions/1000-1099/complement-of-base-10-integer.md ================================================ # [1009. 十进制整数的反码](https://leetcode.cn/problems/complement-of-base-10-integer/) - 标签:位运算 - 难度:简单 ## 题目链接 - [1009. 十进制整数的反码 - 力扣](https://leetcode.cn/problems/complement-of-base-10-integer/) ## 题目大意 **描述**:给定一个十进制数 $n$。 **要求**:返回其二进制表示的反码对应的十进制整数。 **说明**: - $0 \le N < 10^9$。 **示例**: - 示例 1: ```python 输入:5 输出:2 解释:5 的二进制表示为 "101",其二进制反码为 "010",也就是十进制中的 2 。 ``` - 示例 2: ```python 输入:7 输出:0 解释:7 的二进制表示为 "111",其二进制反码为 "000",也就是十进制中的 0 。 ``` ## 解题思路 ### 思路 1:模拟 1. 将十进制数 $n$ 转为二进制 $binary$。 2. 遍历二进制 $binary$ 的每一个数位 $digit$。 1. 如果 $digit$ 为 $0$,则将其转为 $1$,存入答案 $res$ 中。 2. 如果 $digit$ 为 $1$,则将其转为 $0$,存入答案 $res$ 中。 3. 返回答案 $res$。 ### 思路 1:代码 ```python class Solution: def bitwiseComplement(self, n: int) -> int: binary = "" while n: binary += str(n % 2) n //= 2 if binary == "": binary = "0" else: binary = binary[::-1] res = 0 for digit in binary: if digit == '0': res = res * 2 + 1 else: res = res * 2 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(len(n))$,其中 $len(n)$ 为 $n$ 对应二进制的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1000-1099/construct-binary-search-tree-from-preorder-traversal.md ================================================ # [1008. 前序遍历构造二叉搜索树](https://leetcode.cn/problems/construct-binary-search-tree-from-preorder-traversal/) - 标签:栈、树、二叉搜索树、数组、二叉树、单调栈 - 难度:中等 ## 题目链接 - [1008. 前序遍历构造二叉搜索树 - 力扣](https://leetcode.cn/problems/construct-binary-search-tree-from-preorder-traversal/) ## 题目大意 给定一棵二叉搜索树的前序遍历结果 `preorder`。 要求:返回与给定前序遍历 `preorder` 相匹配的二叉搜索树的根节点。题目保证,对于给定的测试用例,总能找到满足要求的二叉搜索树。 ## 解题思路 二叉搜索树的中序遍历是升序序列。而题目又给了我们二叉搜索树的前序遍历,那么通过对前序遍历结果的排序,我们也可以得到二叉搜索树的中序遍历结果。这样就能根据二叉树的前序、中序遍历序列构造二叉树了。就变成了了「[0105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/)」题。 此外,我们还有另一种方法求解。前序遍历的顺序是:根 -> 左 -> 右。并且在二叉搜索树中,左子树的值小于根节点,右子树的值大于根节点。 根据以上性质,我们可以递归地构造二叉搜索树。 首先,以前序遍历的开始位置元素构造为根节点。从开始位置的下一个位置开始,找到序列中第一个大于等于根节点值的位置 `mid`。该位置左侧的值都小于根节点,右侧的值都大于等于根节点。以此位置为中心,递归的构造左子树和右子树。 最后再将根节点进行返回。 ## 代码 ```python class Solution: def buildTree(self, preorder, start, end): if start == end: return None root = preorder[start] mid = start + 1 while mid < end and preorder[mid] < root: mid += 1 node = TreeNode(root) node.left = self.buildTree(preorder, start + 1, mid) node.right = self.buildTree(preorder, mid, end) return node def bstFromPreorder(self, preorder: List[int]) -> TreeNode: return self.buildTree(preorder, 0, len(preorder)) ``` ================================================ FILE: docs/solutions/1000-1099/divisor-game.md ================================================ # [1025. 除数博弈](https://leetcode.cn/problems/divisor-game/) - 标签:脑筋急转弯、数学、动态规划、博弈 - 难度:简单 ## 题目链接 - [1025. 除数博弈 - 力扣](https://leetcode.cn/problems/divisor-game/) ## 题目大意 爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。最初,黑板上有一个数字 `n`。在每个玩家的回合,玩家需要执行以下操作: - 选出任一 `x`,满足 `0 < x < n` 且 `n % x == 0`。 - 用 `n - x` 替换黑板上的数字 `n` 。 - 如果玩家无法执行这些操作,就会输掉游戏。 只有在爱丽丝在游戏中取得胜利时才返回 `True`,否则返回 `False`。假设两个玩家都以最佳状态参与游戏。 ## 解题思路 - 如果 `n` 为奇数,则 `n` 的约数必然都是奇数;如果 `n` 为偶数,则 `n` 的约数可能为奇数也可能为偶数。 - 无论 `n` 为奇数还是偶数,都可以选择 `1` 作为约数。 - 无论 `n` 初始为多大的数,游戏到最终只能到 `n == 2` 结束,只要谁先到 `n == 2`,谁就赢得胜利。 - 当初始 `n` 为偶数时,爱丽丝只要一直选 `1`,那么鲍勃必然会一直面临 `n` 为奇数的情况,这样最后爱丽丝肯定能先到 `n == 2`,稳赢。 - 当初始 `n` 为奇数时,因为奇数的约数只能是奇数,奇数 - 奇数 必然是偶数,所以给鲍勃的数一定是偶数,鲍勃只需一直选 `1` 就会稳赢,此时爱丽丝稳输。 所以,当 `n` 为偶数时,爱丽丝稳赢。当 `n` 为奇数时,爱丽丝稳输。 ## 代码 ```python class Solution: def divisorGame(self, n: int) -> bool: return n & 1 == 0 ``` ================================================ FILE: docs/solutions/1000-1099/duplicate-zeros.md ================================================ # [1089. 复写零](https://leetcode.cn/problems/duplicate-zeros/) - 标签:数组、双指针 - 难度:简单 ## 题目链接 - [1089. 复写零 - 力扣](https://leetcode.cn/problems/duplicate-zeros/) ## 题目大意 **描述**:给定搞一个长度固定的整数数组 $arr$。 **要求**:键改改数组中出现的每一个 $0$ 都复写一遍,并将其余的元素向右平移。 **说明**: - 注意:不要在超过该数组长度的位置写上元素。请对输入的数组就地进行上述修改,不要从函数返回任何东西。 - $1 \le arr.length \le 10^4$。 - $0 \le arr[i] \le 9$。 **示例**: - 示例 1: ```python 输入:arr = [1,0,2,3,0,4,5,0] 输出:[1,0,0,2,3,0,0,4] 解释:调用函数后,输入的数组将被修改为:[1,0,0,2,3,0,0,4] ``` - 示例 2: ```python 输入:arr = [1,2,3] 输出:[1,2,3] 解释:调用函数后,输入的数组将被修改为:[1,2,3] ``` ## 解题思路 ### 思路 1:两次遍历 + 快慢指针 因为数组中出现的 $0$ 需要复写为 $00$,占用空间从一个单位变成两个单位空间,那么右侧必定会有一部分元素丢失。我们可以先遍历一遍数组,找出复写后需要保留的有效数字部分与需要丢失部分的分界点。则从分界点开始,分界点右侧的元素都可以丢失。 我们再次逆序遍历数组, 1. 使用两个指针 $slow$、$fast$,$slow$ 表示当前有效字符位置,$fast$ 表示当前遍历字符位置。一开始 $slow$ 和 $fast$ 都指向数组开始位置。 2. 正序扫描数组: 1. 如果遇到 $arr[slow] == 0$,则让 $fast$ 指针多走一步。 2. 然后 $fast$、$slow$ 各自向右移动 $1$ 位,直到 $fast$ 指针移动到数组末尾。此时 $slow$ 左侧数字 $arr[0]... arr[slow - 1]$ 为需要保留的有效数字部分, $arr[slow]...arr[fast - 1]$ 为需要丢失部分。 3. 令 $slow$、$fast$ 分别左移 $1$ 位,此时 $slow$ 指向最后一个有效数字,$fast$ 指向丢失部分的最后一个数字。此时 $fast$ 可能等于 $size - 1$,也可能等于 $size$(比如输入 $[0, 0, 0]$)。 4. 逆序遍历数组: 1. 将 $slow$ 位置元素移动到 $fast$ 位置。 2. 如果遇到 $arr[slow] == 0$,则令 $fast$ 减 $1$,然后再复制 $1$ 个 $0$ 到 $fast$ 位置。 3. 令 $slow$、$fast$ 分别左移 $1$ 位。 ### 思路 1:代码 ```python class Solution: def duplicateZeros(self, arr: List[int]) -> None: """ Do not return anything, modify arr in-place instead. """ size = len(arr) slow, fast = 0, 0 while fast < size: if arr[slow] == 0: fast += 1 slow += 1 fast += 1 slow -= 1 # slow 指向最后一个有效数字 fast -= 1 # fast 指向丢失部分的最后一个数字(可能在减 1 之后为 size,比如输入 [0, 0, 0]) while slow >= 0: if fast < size: # 防止 fast 越界 arr[fast] = arr[slow] # 将 slow 位置元素移动到 fast 位置 if arr[slow] == 0 and fast >= 0: # 遇见 0 则复制 0 到 fast - 1 位置 fast -= 1 arr[fast] = arr[slow] fast -= 1 slow -= 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $arr$ 中的元素个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1000-1099/find-common-characters.md ================================================ # [1002. 查找共用字符](https://leetcode.cn/problems/find-common-characters/) - 标签:数组、哈希表、字符串 - 难度:简单 ## 题目链接 - [1002. 查找共用字符 - 力扣](https://leetcode.cn/problems/find-common-characters/) ## 题目大意 **描述**:给定一个字符串数组 $words$。 **要求**:找出所有在 $words$ 的每个字符串中都出现的公用字符(包括重复字符),并以数组形式返回。可以按照任意顺序返回答案。 **说明**: - $1 \le words.length \le 100$。 - $1 \le words[i].length \le 100$。 - $words[i]$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:words = ["bella","label","roller"] 输出:["e","l","l"] ``` - 示例 2: ```python 输入:words = ["cool","lock","cook"] 输出:["c","o"] ``` ## 解题思路 ### 思路 1:哈希表 如果某个字符 $ch$ 在所有字符串中都出现了 $k$ 次以上,则最终答案中需要包含 $k$ 个 $ch$。因此,我们可以使用哈希表 $minfreq[ch]$ 记录字符 $ch$ 在所有字符串中出现的最小次数。具体步骤如下: 1. 定义长度为 $26$ 的哈希表 $minfreq$,初始化所有字符出现次数为无穷大,$minfreq[ch] = float('inf')$。 2. 遍历字符串数组中的所有字符串 $word$,对于字符串 $word$: 1. 记录 $word$ 中所有字符串的出现次数 $freq[ch]$。 2. 取 $freq[ch]$ 与 $minfreq[ch]$ 中的较小值更新 $minfreq[ch]$。 3. 遍历完之后,再次遍历 $26$ 个字符,将所有最小出现次数大于零的字符按照出现次数存入答案数组中。 4. 最后将答案数组返回。 ### 思路 1:代码 ```python class Solution: def commonChars(self, words: List[str]) -> List[str]: minfreq = [float('inf') for _ in range(26)] for word in words: freq = [0 for _ in range(26)] for ch in word: freq[ord(ch) - ord('a')] += 1 for i in range(26): minfreq[i] = min(minfreq[i], freq[i]) res = [] for i in range(26): while minfreq[i]: res.append(chr(i + ord('a'))) minfreq[i] -= 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times (|\sum| + m))$,其中 $n$ 为字符串数组 $words$ 的长度,$m$ 为每个字符串的平均长度,$|\sum|$ 为字符集。 - **空间复杂度**:$O(|\sum|)$。 ================================================ FILE: docs/solutions/1000-1099/find-in-mountain-array.md ================================================ # [1095. 山脉数组中查找目标值](https://leetcode.cn/problems/find-in-mountain-array/) - 标签:数组、二分查找、交互 - 难度:困难 ## 题目链接 - [1095. 山脉数组中查找目标值 - 力扣](https://leetcode.cn/problems/find-in-mountain-array/) ## 题目大意 **描述**:给定一个山脉数组 $mountainArr$。 **要求**:返回能够使得 `mountainArr.get(index)` 等于 $target$ 最小的下标 $index$ 值。如果不存在这样的下标 $index$,就请返回 $-1$。 **说明**: - 山脉数组:满足以下属性的数组: - $len(arr) \ge 3$; - 存在 $i$($0 < i < len(arr) - 1$),使得: - $arr[0] < arr[1] < ... arr[i-1] < arr[i]$; - $arr[i] > arr[i+1] > ... > arr[len(arr) - 1]$。 - 不能直接访问该山脉数组,必须通过 `MountainArray` 接口来获取数据: - `MountainArray.get(index)`:会返回数组中索引为 $k$ 的元素(下标从 $0$ 开始)。 - `MountainArray.length()`:会返回该数组的长度。 - 对 `MountainArray.get` 发起超过 $100$ 次调用的提交将被视为错误答案。 - $3 \le mountain_arr.length() \le 10000$。 - $0 \le target \le 10^9$。 - $0 \le mountain_arr.get(index) \le 10^9$。 **示例**: - 示例 1: ```python 输入:array = [1,2,3,4,5,3,1], target = 3 输出:2 解释:3 在数组中出现了两次,下标分别为 2 和 5,我们返回最小的下标 2。 ``` - 示例 2: ```python 输入:array = [0,1,2,4,2,1], target = 3 输出:-1 解释:3 在数组中没有出现,返回 -1。 ``` ## 解题思路 ### 思路 1:二分查找 因为题目要求不能对 `MountainArray.get` 发起超过 $100$ 次调用。所以遍历数组进行查找是不可行的。 根据山脉数组的性质,我们可以把山脉数组分为两部分:「前半部分的升序数组」和「后半部分的降序数组」。在有序数组中查找目标值可以使用二分查找来减少查找次数。 而山脉的峰顶元素索引也可以通过二分查找来做。所以这道题我们可以分为三步: 1. 通过二分查找找到山脉数组的峰顶元素索引。 2. 通过二分查找在前半部分的升序数组中查找目标元素。 3. 通过二分查找在后半部分的降序数组中查找目标元素。 最后,通过对查找结果的判断来输出最终答案。 ### 思路 1:代码 ```python #class MountainArray: # def get(self, index: int) -> int: # def length(self) -> int: class Solution: def binarySearchPeak(self, mountain_arr) -> int: left, right = 0, mountain_arr.length() - 1 while left < right: mid = left + (right - left) // 2 if mountain_arr.get(mid) < mountain_arr.get(mid + 1): left = mid + 1 else: right = mid return left def binarySearchAscending(self, mountain_arr, left, right, target): while left < right: mid = left + (right - left) // 2 if mountain_arr.get(mid) < target: left = mid + 1 else: right = mid return left if mountain_arr.get(left) == target else -1 def binarySearchDescending(self, mountain_arr, left, right, target): while left < right: mid = left + (right - left) // 2 if mountain_arr.get(mid) > target: left = mid + 1 else: right = mid return left if mountain_arr.get(left) == target else -1 def findInMountainArray(self, target: int, mountain_arr: 'MountainArray') -> int: size = mountain_arr.length() peek_i = self.binarySearchPeak(mountain_arr) res_left = self.binarySearchAscending(mountain_arr, 0, peek_i, target) res_right = self.binarySearchDescending(mountain_arr, peek_i + 1, size - 1, target) return res_left if res_left != -1 else res_right ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1000-1099/grumpy-bookstore-owner.md ================================================ # [1052. 爱生气的书店老板](https://leetcode.cn/problems/grumpy-bookstore-owner/) - 标签:数组、滑动窗口 - 难度:中等 ## 题目链接 - [1052. 爱生气的书店老板 - 力扣](https://leetcode.cn/problems/grumpy-bookstore-owner/) ## 题目大意 **描述**:书店老板有一家店打算试营业 $len(customers)$ 分钟。每一分钟都有一些顾客 $customers[i]$ 会进入书店,这些顾客会在这一分钟结束后离开。 在某些时候,书店老板会生气。如果书店老板在第 $i$ 分钟生气,则 `grumpy[i] = 1`,如果第 $i$ 分钟不生气,则 `grumpy[i] = 0`。当书店老板生气时,这一分钟的顾客会不满意。当书店老板不生气时,这一分钟的顾客是满意的。 假设老板知道一个秘密技巧,能保证自己连续 $minutes$ 分钟不生气,但只能使用一次。 现在给定代表每分钟进入书店的顾客数量的数组 $customes$,和代表老板生气状态的数组 $grumpy$,以及老板保证连续不生气的分钟数 $minutes$。 **要求**:计算出试营业下来,最多有多少客户能够感到满意。 **说明**: - $n == customers.length == grumpy.length$。 - $1 \le minutes \le n \le 2 \times 10^4$。 - $0 \le customers[i] \le 1000$。 - $grumpy[i] == 0 \text{ or } 1$。 **示例**: - 示例 1: ```python 输入:customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], minutes = 3 输出:16 解释:书店老板在最后 3 分钟保持冷静。 感到满意的最大客户数量 = 1 + 1 + 1 + 1 + 7 + 5 = 16. ``` - 示例 2: ```python 输入:customers = [1], grumpy = [0], minutes = 1 输出:1 ``` ## 解题思路 ### 思路 1:滑动窗口 固定长度的滑动窗口题目。我们可以维护一个窗口大小为 $minutes$ 的滑动窗口。使用 $window_count$ 记录当前窗口内生气的顾客人数。然后滑动求出窗口中最大顾客数,然后累加上老板未生气时的顾客数,就是答案。具体做法如下: 1. $ans$ 用来维护答案数目。$window\_count$ 用来维护窗口中生气的顾客人数。 2. $left$ 、$right$ 都指向序列的第一个元素,即:`left = 0`,`right = 0`。 3. 如果书店老板生气,则将这一分钟的顾客数量加入到 $window\_count$ 中,然后向右移动 $right$。 4. 当窗口元素个数大于 $minutes$ 时,即:$right - left + 1 > count$ 时,如果最左侧边界老板处于生气状态,则向右移动 $left$,从而缩小窗口长度,即 `left += 1`,使得窗口大小始终保持为小于 $minutes$。 5. 重复 $3 \sim 4$ 步,直到 $right$ 到达数组末尾。 6. 然后累加上老板未生气时的顾客数,最后输出答案。 ### 思路 1:代码 ```python class Solution: def maxSatisfied(self, customers: List[int], grumpy: List[int], minutes: int) -> int: left = 0 right = 0 window_count = 0 ans = 0 while right < len(customers): if grumpy[right] == 1: window_count += customers[right] if right - left + 1 > minutes: if grumpy[left] == 1: window_count -= customers[left] left += 1 right += 1 ans = max(ans, window_count) for i in range(len(customers)): if grumpy[i] == 0: ans += customers[i] return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $coustomer$、$grumpy$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1000-1099/height-checker.md ================================================ # [1051. 高度检查器](https://leetcode.cn/problems/height-checker/) - 标签:数组、计数排序、排序 - 难度:简单 ## 题目链接 - [1051. 高度检查器 - 力扣](https://leetcode.cn/problems/height-checker/) ## 题目大意 **描述**:学校打算为全体学生拍一张年度纪念照。根据要求,学生需要按照 非递减 的高度顺序排成一行。 排序后的高度情况用整数数组 $expected$ 表示,其中 $expected[i]$ 是预计排在这一行中第 $i$ 位的学生的高度(下标从 $0$ 开始)。 给定一个整数数组 $heights$ ,表示当前学生站位的高度情况。$heights[i]$ 是这一行中第 $i$ 位学生的高度(下标从 $0$ 开始)。 **要求**:返回满足 $heights[i] \ne expected[i]$ 的下标数量 。 **说明**: - $1 \le heights.length \le 100$。 - $1 \le heights[i] \le 100$。 **示例**: - 示例 1: ```python 输入:heights = [1,1,4,2,1,3] 输出:3 解释: 高度:[1,1,4,2,1,3] 预期:[1,1,1,2,3,4] 下标 2 、4 、5 处的学生高度不匹配。 ``` - 示例 2: ```python 输入:heights = [5,1,2,3,4] 输出:5 解释: 高度:[5,1,2,3,4] 预期:[1,2,3,4,5] 所有下标的对应学生高度都不匹配。 ``` ## 解题思路 ### 思路 1:排序算法 1. 将数组 $heights$ 复制一份,记为 $expected$。 2. 对数组 $expected$ 进行排序。 3. 排序之后,对比并统计 $heights[i] \ne expected[i]$ 的下标数量,记为 $ans$。 4. 返回 $ans$。 ### 思路 1:代码 ```Python class Solution: def heightChecker(self, heights: List[int]) -> int: expected = sorted(heights) ans = 0 for i in range(len(heights)): if expected[i] != heights[i]: ans += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 为数组 $heights$ 的长度。 - **空间复杂度**:$O(n)$。 ### 思路 2:计数排序 题目中 $heights[i]$ 的数据范围为 $[1, 100]$,所以我们可以使用计数排序。 ### 思路 2:代码 ```python class Solution: def heightChecker(self, heights: List[int]) -> int: # 待排序数组中最大值元素 heights_max = 100 和最小值元素 heights_min = 1 heights_min, heights_max = 1, 100 # 定义计数数组 counts,大小为 最大值元素 - 最小值元素 + 1 size = heights_max - heights_min + 1 counts = [0 for _ in range(size)] # 统计值为 height 的元素出现的次数 for height in heights: counts[height - heights_min] += 1 ans = 0 idx = 0 # 从小到大遍历 counts 的元素值范围 for height in range(heights_min, heights_max + 1): while counts[height - heights_min]: # 对于每个元素值,判断是否与对应位置上的 heights[idx] 相等 if heights[idx] != height: ans += 1 idx += 1 counts[height - heights_min] -= 1 return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n + k)$,其中 $n$ 为数组 $heights$ 的长度,$k$ 为数组 $heights$ 的值域范围。 - **空间复杂度**:$O(k)$。 ================================================ FILE: docs/solutions/1000-1099/index-pairs-of-a-string.md ================================================ # [1065. 字符串的索引对](https://leetcode.cn/problems/index-pairs-of-a-string/) - 标签:字典树、数组、字符串、排序 - 难度:简单 ## 题目链接 - [1065. 字符串的索引对 - 力扣](https://leetcode.cn/problems/index-pairs-of-a-string/) ## 题目大意 给定字符串 `text` 和单词列表 `words`。 要求:在 `text` 中找出所有属于单词列表 `words` 中的单词,并返回该单词在 `text` 中的索引对位置 `[i, j]`。将所有索引对存入列表中返回,并且返回的索引对可以交叉。 ## 解题思路 构建字典树,将所有单词存入字典树中。 然后一重循环遍历 `text`,表示从第 `i` 位置开始的字符串 `text[i:]`。然后在字符串前缀中搜索对应的单词,将所有符合要求的单词末尾位置存入列表中,返回所有位置列表。对于列表中每个单词末尾位置 `index` 和 `text` 来说,每个 `[i, i + index]` 都构成了单词在 `text` 中的索引对位置,将其存入答案数组并返回即可。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, text: str) -> list: """ Returns if the word is in the trie. """ cur = self res = [] for i in range(len(text)): ch = text[i] if ch not in cur.children: return res cur = cur.children[ch] if cur.isEnd: res.append(i) return res class Solution: def indexPairs(self, text: str, words: List[str]) -> List[List[int]]: trie_tree = Trie() for word in words: trie_tree.insert(word) res = [] for i in range(len(text)): for index in trie_tree.search(text[i:]): res.append([i, i + index]) return res ``` ================================================ FILE: docs/solutions/1000-1099/index.md ================================================ ## 本章内容 - [1000. 合并石头的最低成本](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/minimum-cost-to-merge-stones.md) - [1002. 查找共用字符](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/find-common-characters.md) - [1004. 最大连续1的个数 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/max-consecutive-ones-iii.md) - [1005. K 次取反后最大化的数组和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/maximize-sum-of-array-after-k-negations.md) - [1008. 前序遍历构造二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/construct-binary-search-tree-from-preorder-traversal.md) - [1009. 十进制整数的反码](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/complement-of-base-10-integer.md) - [1011. 在 D 天内送达包裹的能力](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/capacity-to-ship-packages-within-d-days.md) - [1012. 至少有 1 位重复的数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/numbers-with-repeated-digits.md) - [1014. 最佳观光组合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/best-sightseeing-pair.md) - [1020. 飞地的数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/number-of-enclaves.md) - [1021. 删除最外层的括号](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/remove-outermost-parentheses.md) - [1023. 驼峰式匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/camelcase-matching.md) - [1025. 除数博弈](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/divisor-game.md) - [1028. 从先序遍历还原二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/recover-a-tree-from-preorder-traversal.md) - [1029. 两地调度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/two-city-scheduling.md) - [1032. 字符流](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/stream-of-characters.md) - [1034. 边界着色](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/coloring-a-border.md) - [1035. 不相交的线](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/uncrossed-lines.md) - [1037. 有效的回旋镖](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/valid-boomerang.md) - [1038. 从二叉搜索树到更大和树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/binary-search-tree-to-greater-sum-tree.md) - [1039. 多边形三角剖分的最低得分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/minimum-score-triangulation-of-polygon.md) - [1041. 困于环中的机器人](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/robot-bounded-in-circle.md) - [1047. 删除字符串中的所有相邻重复项](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/remove-all-adjacent-duplicates-in-string.md) - [1049. 最后一块石头的重量 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/last-stone-weight-ii.md) - [1051. 高度检查器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/height-checker.md) - [1052. 爱生气的书店老板](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/grumpy-bookstore-owner.md) - [1065. 字符串的索引对](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/index-pairs-of-a-string.md) - [1079. 活字印刷](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/letter-tile-possibilities.md) - [1081. 不同字符的最小子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/smallest-subsequence-of-distinct-characters.md) - [1089. 复写零](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/duplicate-zeros.md) - [1091. 二进制矩阵中的最短路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/shortest-path-in-binary-matrix.md) - [1095. 山脉数组中查找目标值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/find-in-mountain-array.md) - [1099. 小于 K 的两数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/two-sum-less-than-k.md) ================================================ FILE: docs/solutions/1000-1099/last-stone-weight-ii.md ================================================ # [1049. 最后一块石头的重量 II](https://leetcode.cn/problems/last-stone-weight-ii/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [1049. 最后一块石头的重量 II - 力扣](https://leetcode.cn/problems/last-stone-weight-ii/) ## 题目大意 **描述**:有一堆石头,用整数数组 $stones$ 表示,其中 $stones[i]$ 表示第 $i$​ 块石头的重量。每一回合,从石头中选出任意两块石头,将这两块石头一起粉碎。假设石头的重量分别为 $x$ 和 $y$。且 $x \le y$,则结果如下: - 如果 $x = y$,则两块石头都会被完全粉碎; - 如果 $x < y$,则重量为 $x$ 的石头被完全粉碎,而重量为 $y$ 的石头新重量为 $y - x$。 **要求**:最后,最多只会剩下一块石头,返回此石头的最小可能重量。如果没有石头剩下,则返回 $0$。 **说明**: - $1 \le stones.length \le 30$。 - $1 \le stones[i] \le 100$。 **示例**: - 示例 1: ```python 输入:stones = [2,7,4,1,8,1] 输出:1 解释: 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1], 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1], 组合 2 和 1,得到 1,所以数组转化为 [1,1,1], 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。 ``` - 示例 2: ```python 输入:stones = [31,26,33,21,40] 输出:5 ``` ## 解题思路 ### 思路 1:动态规划 选取两块石头,重新放回去的重量是两块石头的差值绝对值。重新放回去的石头还会进行选取,然后进行粉碎,直到最后只剩一块或者不剩石头。 这个问题其实可以转化为:把一堆石头尽量平均的分成两对,求两堆石头重量差的最小值。 这就和「[0416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/)」有点相似。两堆石头的重量要尽可能的接近数组总数量和的一半。 进一步可以变为:「0-1 背包问题」。 1. 假设石头总重量和为 $sum$,将一堆石头放进载重上限为 $sum / 2$ 的背包中,获得的最大价值为 $max\_weight$(即其中一堆石子的重量)。另一堆石子的重量为 $sum - max\_weight$。 2. 则两者的差值为 $sum - 2 \times max\_weight$,即为答案。 ###### 1. 阶段划分 按照石头的序号进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[w]$ 表示为:将石头放入载重上限为 $w$ 的背包中可以获得的最大价值。 ###### 3. 状态转移方程 $dp[w] = max \lbrace dp[w], dp[w - stones[i - 1]] + stones[i - 1] \rbrace$。 ###### 4. 初始条件 - 无论背包载重上限为多少,只要不选择石头,可以获得的最大价值一定是 $0$,即 $dp[w] = 0, 0 \le w \le W$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[w]$ 表示为:将石头放入载重上限为 $w$ 的背包中可以获得的最大价值,即第一堆石头的价值为 $dp[size]$,第二堆石头的价值为 $sum - dp[size]$,最终答案为两者的差值,即 $sum - dp[size] \times 2$。 ### 思路 1:代码 ```python class Solution: def lastStoneWeightII(self, stones: List[int]) -> int: W = 1500 size = len(stones) dp = [0 for _ in range(W + 1)] target = sum(stones) // 2 for i in range(1, size + 1): for w in range(target, stones[i - 1] - 1, -1): dp[w] = max(dp[w], dp[w - stones[i - 1]] + stones[i - 1]) return sum(stones) - dp[target] * 2 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times W)$,其中 $n$ 为数组 $stones$ 的元素个数,$W$ 为数组 $stones$ 中元素和的一半。 - **空间复杂度**:$O(W)$。 ================================================ FILE: docs/solutions/1000-1099/letter-tile-possibilities.md ================================================ # [1079. 活字印刷](https://leetcode.cn/problems/letter-tile-possibilities/) - 标签:哈希表、字符串、回溯、计数 - 难度:中等 ## 题目链接 - [1079. 活字印刷 - 力扣](https://leetcode.cn/problems/letter-tile-possibilities/) ## 题目大意 **描述**:给定一个代表活字字模的字符串 $tiles$,其中 $tiles[i]$ 表示第 $i$ 个字模上刻的字母。 **要求**:返回你可以印出的非空字母序列的数目。 **说明**: - 本题中,每个活字字模只能使用一次。 - $1 <= tiles.length <= 7$。 - $tiles$ 由大写英文字母组成。 **示例**: - 示例 1: ```python 输入:"AAB" 输出:8 解释:可能的序列为 "A", "B", "AA", "AB", "BA", "AAB", "ABA", "BAA"。 ``` - 示例 2: ```python 输入:"AAABBC" 输出:188 ``` ## 解题思路 ### 思路 1:哈希表 + 回溯算法 1. 使用哈希表存储每个字符的个数。 2. 然后依次从哈希表中取出对应字符,统计排列个数,并进行回溯。 3. 如果当前字符个数为 $0$,则不再进行回溯。 4. 回溯之后将状态回退。 ### 思路 1:代码 ```python class Solution: ans = 0 def backtrack(self, tile_map): for key, value in tile_map.items(): if value == 0: continue self.ans += 1 tile_map[key] -= 1 self.backtrack(tile_map) tile_map[key] += 1 def numTilePossibilities(self, tiles: str) -> int: tile_map = dict() for tile in tiles: if tile not in tile_map: tile_map[tile] = 1 else: tile_map[tile] += 1 self.backtrack(tile_map) return self.ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times n!)$,其中 $n$ 表示 $tiles$ 的长度最小值。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1000-1099/max-consecutive-ones-iii.md ================================================ # [1004. 最大连续1的个数 III](https://leetcode.cn/problems/max-consecutive-ones-iii/) - 标签:数组、二分查找、前缀和、滑动窗口 - 难度:中等 ## 题目链接 - [1004. 最大连续1的个数 III - 力扣](https://leetcode.cn/problems/max-consecutive-ones-iii/) ## 题目大意 **描述**:给定一个由 $0$、$1$ 组成的数组 $nums$,再给定一个整数 $k$。最多可以将 $k$ 个值从 $0$ 变到 $1$。 **要求**:返回仅包含 $1$ 的最长连续子数组的长度。 **说明**: - $1 \le nums.length \le 10^5$。 - $nums[i]$ 不是 $0$ 就是 $1$。 - $0 \le k \le nums.length$。 **示例**: - 示例 1: ```python 输入:nums = [1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0], K = 2 输出:6 解释:[1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1] 将 nums[5]、nums[10] 从 0 翻转到 1,最长的子数组长度为 6。 ``` - 示例 2: ```python 输入:nums = [0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1], K = 3 输出:10 解释:[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1] 将 nums[4]、nums[5]、nums[9] 从 0 翻转到 1,最长的子数组长度为 10。 ``` ## 解题思路 ### 思路 1:滑动窗口(不定长度) 1. 使用两个指针 $left$、$right$ 指向数组开始位置。使用 $max\_count$ 来维护仅包含 $1$ 的最长连续子数组的长度。 2. 不断右移 $right$ 指针,扩大滑动窗口范围,并统计窗口内 $0$ 元素的个数。 3. 直到 $0$ 元素的个数超过 $k$ 时将 $left$ 右移,缩小滑动窗口范围,并减小 $0$ 元素的个数,同时维护 $max\_count$。 4. 最后输出最长连续子数组的长度 $max\_count$。 ### 思路 1:代码 ```python class Solution: def longestOnes(self, nums: List[int], k: int) -> int: max_count = 0 zero_count = 0 left, right = 0, 0 while right < len(nums): if nums[right] == 0: zero_count += 1 right += 1 if zero_count > k: if nums[left] == 0: zero_count -= 1 left += 1 max_count = max(max_count, right - left) return max_count ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1000-1099/maximize-sum-of-array-after-k-negations.md ================================================ # [1005. K 次取反后最大化的数组和](https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/) - 标签:贪心、数组、排序 - 难度:简单 ## 题目链接 - [1005. K 次取反后最大化的数组和 - 力扣](https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/) ## 题目大意 给定一个整数数组 nums 和一个整数 k。只能用下面的方法修改数组: - 将数组上第 i 个位置上的值取相反数,即将 `nums[i]` 变为 `-nums[i]`。 用这种方式进行 K 次修改(可以多次修改同一个位置 i) 后,返回数组可能的最大和。 ## 解题思路 - 先将数组按绝对值大小进行排序 - 从绝对值大的数开始遍历数组,如果 nums[i] < 0,并且 k > 0: - 则对 nums[i] 取相反数,并将 k 值 -1。 - 如果最后 k 还有余值,则判断奇偶性: - 如果 k 为奇数,则将数组绝对值最小的数进行取反。 - 如果 k 为偶数,则说明可将某一位数进行偶数次取反,和原数值一致,则不需要进行操作。 - 最后返回数组和。 ## 代码 ```python class Solution: def largestSumAfterKNegations(self, nums: List[int], k: int) -> int: nums.sort(key=lambda x: abs(x), reverse = True) for i in range(len(nums)): if nums[i] < 0 and k > 0: nums[i] *= -1 k -= 1 if k % 2 == 1: nums[-1] *= -1 return sum(nums) ``` ================================================ FILE: docs/solutions/1000-1099/minimum-cost-to-merge-stones.md ================================================ # [1000. 合并石头的最低成本](https://leetcode.cn/problems/minimum-cost-to-merge-stones/) - 标签:数组、动态规划、前缀和 - 难度:困难 ## 题目链接 - [1000. 合并石头的最低成本 - 力扣](https://leetcode.cn/problems/minimum-cost-to-merge-stones/) ## 题目大意 **描述**:给定一个代表 $n$ 堆石头的整数数组 $stones$,其中 $stones[i]$ 代表第 $i$ 堆中的石头个数。再给定一个整数 $k$, 每次移动需要将连续的 $k$ 堆石头合并为一堆,而这次移动的成本为这 $k$ 堆中石头的总数。 **要求**:返回把所有石头合并成一堆的最低成本。如果无法合并成一堆,则返回 $-1$。 **说明**: - $n == stones.length$。 - $1 \le n \le 30$。 - $1 \le stones[i] \le 100$。 - $2 \le k \le 30$。 **示例**: - 示例 1: ```python 输入:stones = [3,2,4,1], K = 2 输出:20 解释: 从 [3, 2, 4, 1] 开始。 合并 [3, 2],成本为 5,剩下 [5, 4, 1]。 合并 [4, 1],成本为 5,剩下 [5, 5]。 合并 [5, 5],成本为 10,剩下 [10]。 总成本 20,这是可能的最小值。 ``` - 示例 2: ```python 输入:stones = [3,5,1,2,6], K = 3 输出:25 解释: 从 [3, 5, 1, 2, 6] 开始。 合并 [5, 1, 2],成本为 8,剩下 [3, 8, 6]。 合并 [3, 8, 6],成本为 17,剩下 [17]。 总成本 25,这是可能的最小值。 ``` ## 解题思路 ### 思路 1:动态规划 + 前缀和 每次将 $k$ 堆连续的石头合并成 $1$ 堆,石头堆数就会减少 $k - 1$ 堆。总共有 $n$ 堆石子,则: 1. 当 $(n - 1) \mod (k - 1) == 0$ 时,一定可以经过 $\frac{n - 1}{k - 1}$ 次合并,将 $n$ 堆石头合并为 $1$ 堆。 2. 当 $(n - 1) \mod (k - 1) \ne 0$ 时,则无法将所有的石头合并成一堆。 根据以上情况,我们可以先将无法将所有的石头合并成一堆的情况排除出去,接下来只考虑合法情况。 由于每次合并石头的成本为合并的 $k$ 堆的石子总数,即数组 $stones$ 中长度为 $k$ 的连续子数组和,因此为了快速计算数组 $stones$ 的连续子数组和,我们可以使用「前缀和」的方式,预先计算出「前 $i$ 堆的石子总数」,从而可以在 $O(1)$ 的时间复杂度内得到数组 $stones$ 的连续子数组和。 $k$ 堆石头合并为 $1$ 堆石头的过程,可以看做是长度为 $k$ 的连续子数组合并为长度为 $1$ 的子数组的过程,也可以看做是将长度为 $k$ 的区间合并为长度为 $1$ 的区间。 接下来我们就可以按照「区间 DP 问题」的基本思路来做。 ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j][m]$ 表示为:将区间 $[i, j]$ 的石堆合并成 $m$ 堆的最低成本,其中 $m$ 的取值为 $[1,k]$。 ###### 3. 状态转移方程 我们将区间 $[i, j]$ 的石堆合并成 $m$ 堆,可以枚举 $i \le n \le j$,将区间 $[i, j]$ 拆分为两个区间 $[i, n]$ 和 $[n + 1, j]$。然后将 $[i, n]$ 中的石头合并为 $1$ 堆,将 $[n + 1, j]$ 中的石头合并成 $m - 1$ 堆。最后将 $1$ 堆石头和 $m - 1$ 堆石头合并成 $1$ 堆,这样就可以将 $[i, j]$ 的石堆合并成 $k$ 堆。则状态转移方程为:$dp[i][j][m] = min_{i \le n < j} \lbrace dp[i][n][1] + dp[n + 1][j][m - 1] \rbrace$。 我们再将区间 $[i, j]$ 的 $k$ 堆石头合并成 $1$ 堆,其成本为 区间 $[i, j]$ 的石堆合并成 $k$ 堆的成本,加上将这 $k$ 堆石头合并成 $1$ 堆的成本,即状态转移方程为:$dp[i][j][1] = dp[i][j][k] + \sum_{t = i}^{t = j} stones[t]$。 ###### 4. 初始条件 - 长度为 $1$ 的区间 $[i, i]$ 合并为 $1$ 堆成本为 $0$,即:$dp[i][i][1] = 0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j][m]$ 表示为:将区间 $[i, j]$ 的石堆合并成 $m$ 堆的最低成本,其中 $m$ 的取值为 $[1,k]$。 所以最终结果为 $dp[1][size][1]$,其中 $size$ 为数组 $stones$ 的长度。 ### 思路 1:代码 ```python class Solution: def mergeStones(self, stones: List[int], k: int) -> int: size = len(stones) if (size - 1) % (k - 1) != 0: return -1 prefix = [0 for _ in range(size + 1)] for i in range(1, size + 1): prefix[i] = prefix[i - 1] + stones[i - 1] dp = [[[float('inf') for _ in range(k + 1)] for _ in range(size)] for _ in range(size)] for i in range(size): dp[i][i][1] = 0 for l in range(2, size + 1): for i in range(size): j = i + l - 1 if j >= size: break for m in range(2, k + 1): for n in range(i, j, k - 1): dp[i][j][m] = min(dp[i][j][m], dp[i][n][1] + dp[n + 1][j][m - 1]) dp[i][j][1] = dp[i][j][k] + prefix[j + 1] - prefix[i] return dp[0][size - 1][1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3 \times k)$,其中 $n$ 是数组 $stones$ 的长度。 - **空间复杂度**:$O(n^2 \times k)$。 ### 思路 2:动态规划 + 状态优化 在思路 1 中,我们使用定义状态 $dp[i][j][m]$ 表示为:将区间 $[i, j]$ 的石堆合并成 $m$ 堆的最低成本,其中 $m$ 的取值为 $[1,k]$。 事实上,对于固定区间 $[i, j]$,初始时堆数为 $j - i + 1$,每次合并都会减少 $k - 1$ 堆,合并到无法合并时的堆数固定为 $(j - i) \mod (k - 1) + 1$。 所以,我们可以直接定义状态 $dp[i][j]$ 表示为:将区间 $[i, j]$ 的石堆合并到无法合并时的最低成本。 具体步骤如下: ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:将区间 $[i, j]$ 的石堆合并到无法合并时的最低成本。 ###### 3. 状态转移方程 枚举 $i \le n \le j$,将区间 $[i, j]$ 拆分为两个区间 $[i, n]$ 和 $[n + 1, j]$。然后将区间 $[i, n]$ 合并成 $1$ 堆,$[n + 1, j]$ 合并成 $m$ 堆。 $dp[i][j] = min_{i \le n < j} \lbrace dp[i][n] + dp[n + 1][j] \rbrace$。 如果 $(j - i) \mod (k - 1) == 0$,则说明区间 $[i, j]$ 能狗合并为 1 堆,则加上区间子数组和,即 $dp[i][j] += prefix[j + 1] - prefix[i]$。 ###### 4. 初始条件 - 长度为 $1$ 的区间 $[i, i]$ 合并到无法合并时的最低成本为 $0$,即:$dp[i][i] = 0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:将区间 $[i, j]$ 的石堆合并到无法合并时的最低成本。所以最终结果为 $dp[0][size - 1]$,其中 $size$ 为数组 $stones$ 的长度。 ### 思路 2:代码 ```python class Solution: def mergeStones(self, stones: List[int], k: int) -> int: size = len(stones) if (size - 1) % (k - 1) != 0: return -1 prefix = [0 for _ in range(size + 1)] for i in range(1, size + 1): prefix[i] = prefix[i - 1] + stones[i - 1] dp = [[float('inf') for _ in range(size)] for _ in range(size)] for i in range(size): dp[i][i] = 0 for l in range(2, size + 1): for i in range(size): j = i + l - 1 if j >= size: break # 遍历每一个可以组成 k 堆石子的分割点 n,每次递增 k - 1 个 for n in range(i, j, k - 1): # 判断 [i, n] 到 [n + 1, j] 是否比之前花费小 dp[i][j] = min(dp[i][j], dp[i][n] + dp[n + 1][j]) # 如果 [i, j] 能狗合并为 1 堆,则加上区间子数组和 if (l - 1) % (k - 1) == 0: dp[i][j] += prefix[j + 1] - prefix[i] return dp[0][size - 1] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 是数组 $stones$ 的长度。 - **空间复杂度**:$O(n^2)$。 ## 参考资料 - 【题解】[一题一解:动态规划(区间 DP)+ 前缀和(清晰题解) - 合并石头的最低成本](https://leetcode.cn/problems/minimum-cost-to-merge-stones/solution/python3javacgo-yi-ti-yi-jie-dong-tai-gui-lr9q/) ================================================ FILE: docs/solutions/1000-1099/minimum-score-triangulation-of-polygon.md ================================================ # [1039. 多边形三角剖分的最低得分](https://leetcode.cn/problems/minimum-score-triangulation-of-polygon/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [1039. 多边形三角剖分的最低得分 - 力扣](https://leetcode.cn/problems/minimum-score-triangulation-of-polygon/) ## 题目大意 **描述**:有一个凸的 $n$ 边形,其每个顶点都有一个整数值。给定一个整数数组 $values$,其中 $values[i]$ 是第 $i$ 个顶点的值(即顺时针顺序)。 现在要将 $n$ 边形剖分为 $n - 2$ 个三角形,对于每个三角形,该三角形的值是顶点标记的乘积,$n$ 边形三角剖分的分数是进行三角剖分后所有 $n - 2$ 个三角形的值之和。 **要求**:返回多边形进行三角剖分可以得到的最低分。 **说明**: - $n == values.length$。 - $3 \le n \le 50$。 - $1 \le values[i] \le 100$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/25/shape1.jpg) ```python 输入:values = [1,2,3] 输出:6 解释:多边形已经三角化,唯一三角形的分数为 6。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/02/25/shape2.jpg) ```python 输入:values = [3,7,4,5] 输出:144 解释:有两种三角剖分,可能得分分别为:3*7*5 + 4*5*7 = 245,或 3*4*5 + 3*4*7 = 144。最低分数为 144。 ``` ## 解题思路 ### 思路 1:动态规划 对于 $0 \sim n - 1$ 个顶点组成的凸多边形进行三角剖分,我们可以在 $[0, n - 1]$ 中任选 $1$ 个点 $k$,从而将凸多边形划分为: 1. 顶点 $0 \sim k$ 组成的凸多边形。 2. 顶点 $0$、$k$、$n - 1$ 组成的三角形。 3. 顶点 $k \sim n - 1$ 组成的凸多边形。 对于顶点 $0$、$k$、$n - 1$ 组成的三角形,我们可以直接计算对应的三角剖分分数为 $values[0] \times values[k] \times values[n - 1]$。 而对于顶点 $0 \sim k$ 组成的凸多边形和顶点 $k \sim n - 1$ 组成的凸多边形,我们可以利用递归或者动态规划的思想,定义一个 $dp[i][j]$ 用于计算顶点 $i$ 到顶点 $j$ 组成的多边形三角剖分的最小分数。 具体做法如下: ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:区间 $[i, j]$ 内三角剖分后的最小分数。 ###### 3. 状态转移方程 对于区间 $[i, j]$,枚举分割点 $k$,最小分数为 $min(dp[i][k] + dp[k][j] + values[i] \times values[k] \times values[j])$,即:$dp[i][j] = min(dp[i][k] + dp[k][j] + values[i] \times values[k] \times values[j])$。 ###### 4. 初始条件 - 默认情况下,所有区间 $[i, j]$ 的最小分数为无穷大。 - 当区间 $[i, j]$ 长度小于 $3$ 时,无法进行三角剖分,其最小分数为 $0$。 - 当区间 $[i, j]$ 长度等于 $3$ 时,其三角剖分的最小分数为 $values[i] * values[i + 1] * values[i + 2]$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:区间 $[i, j]$ 内三角剖分后的最小分数。。 所以最终结果为 $dp[0][size - 1]$。 ### 思路 1:代码 ```python class Solution: def minScoreTriangulation(self, values: List[int]) -> int: size = len(values) dp = [[float('inf') for _ in range(size)] for _ in range(size)] for l in range(1, size + 1): for i in range(size): j = i + l - 1 if j >= size: break if l < 3: dp[i][j] = 0 elif l == 3: dp[i][j] = values[i] * values[i + 1] * values[i + 2] else: for k in range(i + 1, j): dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + values[i] * values[j] * values[k]) return dp[0][size - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 为顶点个数。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/1000-1099/number-of-enclaves.md ================================================ # [1020. 飞地的数量](https://leetcode.cn/problems/number-of-enclaves/) - 标签:深度优先搜索、广度优先搜索、并查集、数组、矩阵 - 难度:中等 ## 题目链接 - [1020. 飞地的数量 - 力扣](https://leetcode.cn/problems/number-of-enclaves/) ## 题目大意 **描述**:给定一个二维数组 `grid`,每个单元格为 `0`(代表海)或 `1`(代表陆地)。我们可以从一个陆地走到另一个陆地上(朝四个方向之一),然后从边界上的陆地离开网络的边界。 **要求**:返回网格中无法在任意次数的移动中离开网格边界的陆地单元格的数量。 **说明**: - $m == grid.length$。 - $n == grid[i].length$。 - $1 \le m, n \le 500$。 - $grid[i][j]$ 的值为 $0$ 或 $1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/02/18/enclaves1.jpg) ```python 输入:grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]] 输出:3 解释:有三个 1 被 0 包围。一个 1 没有被包围,因为它在边界上。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/02/18/enclaves2.jpg) ```python 输入:grid = [[0,1,1,0],[0,0,1,0],[0,0,1,0],[0,0,0,0]] 输出:0 解释:所有 1 都在边界上或可以到达边界。 ``` ## 解题思路 ### 思路 1:深度优先搜索 与四条边界相连的陆地单元是肯定能离开网络边界的。 我们可以先通过深度优先搜索将与四条边界相关的陆地全部变为海(赋值为 `0`)。 然后统计网格中 `1` 的数量,即为答案。 ### 思路 1:代码 ```python class Solution: directs = [(0, 1), (0, -1), (1, 0), (-1, 0)] def dfs(self, grid, i, j): rows = len(grid) cols = len(grid[0]) if i < 0 or i >= rows or j < 0 or j >= cols or grid[i][j] == 0: return grid[i][j] = 0 for direct in self.directs: new_i = i + direct[0] new_j = j + direct[1] self.dfs(grid, new_i, new_j) def numEnclaves(self, grid: List[List[int]]) -> int: rows = len(grid) cols = len(grid[0]) for i in range(rows): if grid[i][0] == 1: self.dfs(grid, i, 0) if grid[i][cols - 1] == 1: self.dfs(grid, i, cols - 1) for j in range(cols): if grid[0][j] == 1: self.dfs(grid, 0, j) if grid[rows - 1][j] == 1: self.dfs(grid, rows - 1, j) ans = 0 for i in range(rows): for j in range(cols): if grid[i][j] == 1: ans += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。其中 $m$ 和 $n$ 分别为行数和列数。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/1000-1099/numbers-with-repeated-digits.md ================================================ # [1012. 至少有 1 位重复的数字](https://leetcode.cn/problems/numbers-with-repeated-digits/) - 标签:数学、动态规划 - 难度:困难 ## 题目链接 - [1012. 至少有 1 位重复的数字 - 力扣](https://leetcode.cn/problems/numbers-with-repeated-digits/) ## 题目大意 **描述**:给定一个正整数 $n$。 **要求**:返回在 $[1, n]$ 范围内具有至少 $1$ 位重复数字的正整数的个数。 **说明**: - $1 \le n \le 10^9$。 **示例**: - 示例 1: ```python 输入:n = 20 输出:1 解释:具有至少 1 位重复数字的正数(<= 20)只有 11。 ``` - 示例 2: ```python 输入:n = 100 输出:10 解释:具有至少 1 位重复数字的正数(<= 100)有 11,22,33,44,55,66,77,88,99 和 100。 ``` ## 解题思路 ### 思路 1:动态规划 + 数位 DP 正向求解在 $[1, n]$ 范围内具有至少 $1$ 位重复数字的正整数的个数不太容易,我们可以反向思考,先求解出在 $[1, n]$ 范围内各位数字都不重复的正整数的个数 $ans$,然后 $n - ans$ 就是题目答案。 将 $n$ 转换为字符串 $s$,定义递归函数 `def dfs(pos, state, isLimit, isNum):` 表示构造第 $pos$ 位及之后所有数位的合法方案数。接下来按照如下步骤进行递归。 1. 从 `dfs(0, 0, True, False)` 开始递归。 `dfs(0, 0, True, False)` 表示: 1. 从位置 $0$ 开始构造。 2. 初始没有使用数字(即前一位所选数字集合为 $0$)。 3. 开始时受到数字 $n$ 对应最高位数位的约束。 4. 开始时没有填写数字。 2. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时: 1. 如果 $isNum == True$,说明当前方案符合要求,则返回方案数 $1$。 2. 如果 $isNum == False$,说明当前方案不符合要求,则返回方案数 $0$。 3. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:`ans = 0`。 4. 如果遇到 $isNum == False$,说明之前位数没有填写数字,当前位可以跳过,这种情况下方案数等于 $pos + 1$ 位置上没有受到 $pos$ 位的约束,并且之前没有填写数字时的方案数,即:`ans = dfs(i + 1, state, False, False)`。 5. 如果 $isNum == True$,则当前位必须填写一个数字。此时: 1. 根据 $isNum$ 和 $isLimit$ 来决定填当前位数位所能选择的最小数字($minX$)和所能选择的最大数字($maxX$), 2. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $d$。 3. 如果之前没有选择 $d$,即 $d$ 不在之前选择的数字集合 $state$ 中,则方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, state | (1 << d), isLimit and d == maxX, True)`。 1. `state | (1 << d)` 表示之前选择的数字集合 $state$ 加上 $d$。 2. `isLimit and d == maxX` 表示 $pos + 1$ 位受到之前位限制和 $pos$ 位限制。 3. $isNum == True$ 表示 $pos$ 位选择了数字。 6. 最后的方案数为 `n - dfs(0, 0, True, False)`,将其返回即可。 ### 思路 1:代码 ```python class Solution: def numDupDigitsAtMostN(self, n: int) -> int: # 将 n 转换为字符串 s s = str(n) @cache # pos: 第 pos 个数位 # state: 之前选过的数字集合。 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 # isNum: 表示 pos 前面的数位是否填了数字。 # 如果为真,则当前位不可跳过;如果为假,则当前位可跳过。 def dfs(pos, state, isLimit, isNum): if pos == len(s): # isNum 为 True,则表示当前方案符合要求 return int(isNum) ans = 0 if not isNum: # 如果 isNumb 为 False,则可以跳过当前数位 ans = dfs(pos + 1, state, False, False) # 如果前一位没有填写数字,则最小可选择数字为 0,否则最少为 1(不能含有前导 0)。 minX = 0 if isNum else 1 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s[pos]) if isLimit else 9 # 枚举可选择的数字 for d in range(minX, maxX + 1): # d 不在选择的数字集合中,即之前没有选择过 d if (state >> d) & 1 == 0: ans += dfs(pos + 1, state | (1 << d), isLimit and d == maxX, True) return ans return n - dfs(0, 0, True, False) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n \times 10 \times 2^{10})$。 - **空间复杂度**:$O(\log n \times 2^{10})$。 ================================================ FILE: docs/solutions/1000-1099/recover-a-tree-from-preorder-traversal.md ================================================ # [1028. 从先序遍历还原二叉树](https://leetcode.cn/problems/recover-a-tree-from-preorder-traversal/) - 标签:树、深度优先搜索、字符串、二叉树 - 难度:困难 ## 题目链接 - [1028. 从先序遍历还原二叉树 - 力扣](https://leetcode.cn/problems/recover-a-tree-from-preorder-traversal/) ## 题目大意 对一棵二叉树进行深度优先搜索。在遍历的过程中,遇到节点,先输出与该节点深度相同数量的短线,再输出该节点的值。如果节点深度为 `D`,则子节点深度为 `D + 1`。根节点的深度为 `0`。如果节点只有一个子节点,则该子节点一定为左子节点。 现在给定深度优先搜索输出的字符串 `traversal`。 要求:还原二叉树,并返回其根节点 `root`。 ## 解题思路 用栈存储需要构建子树的节点。并记录下上一节点深度和当前节点深度。 然后遍历深度优先搜索的输出字符串。 - 先将开始部分的数字作为根节点值,构建一个根节点 `root`,并将根节点插入到栈中。 - 如果遇到 `-`,则更新当前节点深度。 - 然后如果遇到数字,则将数字逐位转为整数。并且在最后进行判断。 - 如果当前节点深度 > 前一节点深度: - 将栈顶节点出栈。 - 构建一个新节点,值为当前整数。将新节点插入到栈顶节点的左子树上。 - 将当前节点和新节点插入到栈中。 - 如果当前节点深度 <= 前一节点深度: - 将当前节点深度个数的节点从栈中弹出。 - 构建一个新节点,值为当前整数。并将新节点插入到最后弹出节点的右子树上。 - 将当前节点和新节点插入到栈中。 - 最后输出根节点 `root`。 ## 代码 ```python class Solution: def recoverFromPreorder(self, traversal: str) -> Optional[TreeNode]: stack = [] index, num = 0, 0 pre_level, cur_level = 0, 0 size = len(traversal) while index < size and traversal[index] != '-': num = num * 10 + ord(traversal[index]) - ord('0') index += 1 root = TreeNode(num) stack.append(root) while index < size: if traversal[index] == '-': cur_level += 1 index += 1 else: num = 0 while index < size and traversal[index] != '-': num = num * 10 + ord(traversal[index]) - ord('0') index += 1 if cur_level > pre_level: node = stack.pop() node.left = TreeNode(num) stack.append(node) stack.append(node.left) pre_level = cur_level cur_level = 0 else: while len(stack) > cur_level: stack.pop() node = stack.pop() node.right = TreeNode(num) stack.append(node) stack.append(node.right) pre_level = cur_level cur_level = 0 return root ``` ================================================ FILE: docs/solutions/1000-1099/remove-all-adjacent-duplicates-in-string.md ================================================ # [1047. 删除字符串中的所有相邻重复项](https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/) - 标签:栈、字符串 - 难度:简单 ## 题目链接 - [1047. 删除字符串中的所有相邻重复项 - 力扣](https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/) ## 题目大意 给定一个全部由小写字母组成的字符串 S,重复的删除相邻且相同的字母,直到相邻字母不再有相同的。 比如 "abbaca"。先删除相邻且相同的字母 "bb",变为 "aaca",再删除相邻且相同的字母 "aa",变为 "ca",无相邻且相同的字母,即 "ca" 为最终结果。 ## 解题思路 跟括号匹配有点类似。我们可以利用「栈」来做这道题。遍历字符串,如果当前字符与栈顶字符相同,则将栈顶所有相同字符删除,否则就将当前字符入栈。 ## 代码 ```python class Solution: def removeDuplicates(self, S: str) -> str: stack = [] for ch in S: if stack and stack[-1] == ch: stack.pop() else: stack.append(ch) return "".join(stack) ``` ================================================ FILE: docs/solutions/1000-1099/remove-outermost-parentheses.md ================================================ # [1021. 删除最外层的括号](https://leetcode.cn/problems/remove-outermost-parentheses/) - 标签:栈、字符串 - 难度:简单 ## 题目链接 - [1021. 删除最外层的括号 - 力扣](https://leetcode.cn/problems/remove-outermost-parentheses/) ## 题目大意 **描述**:有效括号字符串为空 `""`、`"("` + $A$ + `")"` 或 $A + B$ ,其中 $A$ 和 $B$ 都是有效的括号字符串,$+$ 代表字符串的连接。 - 例如,`""`,`"()"`,`"(())()"` 和 `"(()(()))"` 都是有效的括号字符串。 如果有效字符串 $s$ 非空,且不存在将其拆分为 $s = A + B$ 的方法,我们称其为原语(primitive),其中 $A$ 和 $B$ 都是非空有效括号字符串。 给定一个非空有效字符串 $s$,考虑将其进行原语化分解,使得:$s = P_1 + P_2 + ... + P_k$,其中 $P_i$ 是有效括号字符串原语。 **要求**:对 $s$ 进行原语化分解,删除分解中每个原语字符串的最外层括号,返回 $s$。 **说明**: - $1 \le s.length \le 10^5$。 - $s[i]$ 为 `'('` 或 `')'`。 - $s$ 是一个有效括号字符串。 **示例**: - 示例 1: ```python 输入:s = "(()())(())" 输出:"()()()" 解释: 输入字符串为 "(()())(())",原语化分解得到 "(()())" + "(())", 删除每个部分中的最外层括号后得到 "()()" + "()" = "()()()"。 ``` - 示例 2: ```python 输入:s = "(()())(())(()(()))" 输出:"()()()()(())" 解释: 输入字符串为 "(()())(())(()(()))",原语化分解得到 "(()())" + "(())" + "(()(()))", 删除每个部分中的最外层括号后得到 "()()" + "()" + "()(())" = "()()()()(())"。 ``` ## 解题思路 ### 思路 1:计数遍历 题目要求我们对 $s$ 进行原语化分解,并且删除分解中每个原语字符串的最外层括号。 通过观察可以发现,每个原语其实就是一组有效的括号对(左右括号匹配时),此时我们需要删除这组有效括号对的最外层括号。 我们可以使用一个计数器 $cnt$ 来进行原语化分解,并删除每个原语的最外层括号。 当计数器遇到左括号时,令计数器 $cnt$ 加 $1$,当计数器遇到右括号时,令计数器 $cnt$ 减 $1$。这样当计数器为 $0$ 时表示当前左右括号匹配。 为了删除每个原语的最外层括号,当遇到每个原语最外侧的左括号时(此时 $cnt$ 必然等于 $0$,因为之前字符串为空或者为上一个原语字符串),因为我们不需要最外层的左括号,所以此时我们不需要将其存入答案字符串中。只有当 $cnt > 0$ 时,才将其存入答案字符串中。 同理,当遇到每个原语最外侧的右括号时(此时 $cnt$ 必然等于 $1$,因为之前字符串差一个右括号匹配),因为我们不需要最外层的右括号,所以此时我们不需要将其存入答案字符串中。只有当 $cnt > 1$ 时,才将其存入答案字符串中。 具体步骤如下: 1. 遍历字符串 $s$。 2. 如果遇到 `'('`,判断当前计数器是否大于 $0$: 1. 如果 $cnt > 0$,则将 `'('` 存入答案字符串中,并令计数器加 $1$,即:`cnt += 1`。 2. 如果 $cnt == 0$,则令计数器加 $1$,即:`cnt += 1`。 3. 如果遇到 `')'`,判断当前计数器是否大于 $1$: 1. 如果 $cnt > 1$,则将 `')'` 存入答案字符串中,并令计数器减 $1$,即:`cnt -= 1`。 2. 如果 $cnt == 1$,则令计数器减 $1$,即:`cnt -= 1`。 4. 遍历完返回答案字符串 $ans$。 ### 思路 1:代码 ```Python class Solution: def removeOuterParentheses(self, s: str) -> str: cnt, ans = 0, "" for ch in s: if ch == '(': if cnt > 0: ans += ch cnt += 1 else: if cnt > 1: ans += ch cnt -= 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1000-1099/robot-bounded-in-circle.md ================================================ # [1041. 困于环中的机器人](https://leetcode.cn/problems/robot-bounded-in-circle/) - 标签:数学、字符串、模拟 - 难度:中等 ## 题目链接 - [1041. 困于环中的机器人 - 力扣](https://leetcode.cn/problems/robot-bounded-in-circle/) ## 题目大意 **描述**:在无限的平面上,机器人最初位于 $(0, 0)$ 处,面朝北方。注意: - 北方向 是 $y$ 轴的正方向。 - 南方向 是 $y$ 轴的负方向。 - 东方向 是 $x$ 轴的正方向。 - 西方向 是 $x$ 轴的负方向。 机器人可以接受下列三条指令之一: - `"G"`:直走 $1$ 个单位 - `"L"`:左转 $90$ 度 - `"R"`:右转 $90$ 度 给定一个字符串 $instructions$,机器人按顺序执行指令 $instructions$,并一直重复它们。 **要求**:只有在平面中存在环使得机器人永远无法离开时,返回 $True$。否则,返回 $False$。 **说明**: - $1 \le instructions.length \le 100$。 - $instructions[i]$ 仅包含 `'G'`,`'L'`,`'R'`。 **示例**: - 示例 1: ```python 输入:instructions = "GGLLGG" 输出:True 解释:机器人最初在(0,0)处,面向北方。 “G”:移动一步。位置:(0,1)方向:北。 “G”:移动一步。位置:(0,2).方向:北。 “L”:逆时针旋转90度。位置:(0,2).方向:西。 “L”:逆时针旋转90度。位置:(0,2)方向:南。 “G”:移动一步。位置:(0,1)方向:南。 “G”:移动一步。位置:(0,0)方向:南。 重复指令,机器人进入循环:(0,0)——>(0,1)——>(0,2)——>(0,1)——>(0,0)。 在此基础上,我们返回 True。 ``` - 示例 2: ```python 输入:instructions = "GG" 输出:False 解释:机器人最初在(0,0)处,面向北方。 “G”:移动一步。位置:(0,1)方向:北。 “G”:移动一步。位置:(0,2).方向:北。 重复这些指示,继续朝北前进,不会进入循环。 在此基础上,返回 False。 ``` ## 解题思路 ### 思路 1:模拟 设定初始位置为 $(0, 0)$,初始方向 $direction = 0$,假设按照给定字符串 $instructions$ 执行一遍之后,位于 $(x, y)$ 处,且方向为 $direction$,则可能出现的所有情况为: 1. 方向不变($direction == 0$),且 $(x, y) == (0, 0)$,则会一直在原点,无法走出去。 2. 方向不变($direction == 0$),且 $(x, y) \ne (0, 0)$,则可以走出去。 3. 方向相反($direction == 2$),无论是否产生位移,则再执行 $1$ 遍将会回到原点。 4. 方向逆时针 / 顺时针改变 $90°$($direction == 1 \text{ or } 3$),无论是否产生位移,则再执行 $3$ 遍将会回到原点。 综上所述,最多模拟 $4$ 次即可知道能否回到原点。 从上面也可以等出结论:如果不产生位移,则一定会回到原点。如果改变方向,同样一定会回到原点。 我们只需要根据以上结论,按照 $instructions$ 执行一遍之后,通过判断是否产生位移和改变方向,即可判断是否一定会回到原点。 ### 思路 1:代码 ```Python class Solution: def isRobotBounded(self, instructions: str) -> bool: # 分别代表北、东、南、西 directions = [(0, 1), (-1, 0), (0, -1), (1, 0)] x, y = 0, 0 # 初始方向为北 direction = 0 for step in instructions: if step == 'G': x += directions[direction][0] y += directions[direction][1] elif step == 'L': direction = (direction + 1) % 4 else: direction = (direction + 3) % 4 return (x == 0 and y == 0) or direction != 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串 $instructions$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1000-1099/shortest-path-in-binary-matrix.md ================================================ # [1091. 二进制矩阵中的最短路径](https://leetcode.cn/problems/shortest-path-in-binary-matrix/) - 标签:广度优先搜索、数组、矩阵 - 难度:中等 ## 题目链接 - [1091. 二进制矩阵中的最短路径 - 力扣](https://leetcode.cn/problems/shortest-path-in-binary-matrix/) ## 题目大意 给定一个 `n * n` 的二进制矩阵 `grid`。 `grid` 中只含有 `0` 或者 `1`。`grid` 中的畅通路径是一条从左上角 `(0, 0)` 位置上到右下角 `(n - 1, n - 1)`位置上的路径。该路径同时满足以下要求: - 路径途径的所有单元格的值都是 `0`。 - 路径中所有相邻的单元格应该在 `8` 个方向之一上连通(即相邻两单元格之间彼此不同且共享一条边或者一个角)。 - 畅通路径的长度是该路径途径的单元格总数。 要求:计算出矩阵中最短畅通路径的长度。如果不存在这样的路径,返回 `-1`。 ## 解题思路 使用广度优先搜索查找最短路径。具体做法如下: 1. 使用队列 `queue` 存放当前节点位置,使用 set 集合 `visited` 存放遍历过的节点位置。使用 `count` 记录最短路径。将起始位置 `(0, 0)` 加入到 `queue` 中,并标记为访问过。 2. 如果队列不为空,则令 `count += 1`,并将队列中的节点位置依次取出。对于每一个节点位置: - 先判断是否为右下角节点,即 `(n - 1, n - 1)`。如果是则返回当前最短路径长度 `count`。 - 如果不是,则继续遍历 `8` 个方向上、没有访问过、并且值为 `0` 的相邻单元格。 - 将其加入到队列 `queue` 中,并标记为访问过。 3. 重复进行第 2 步骤,直到队列为空时,返回 `-1`。 ## 代码 ```python class Solution: def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int: if grid[0][0] == 1: return -1 size = len(grid) directions = {(1, 0), (1, -1), (0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1)} visited = set((0, 0)) queue = [(0, 0)] count = 0 while queue: count += 1 for _ in range(len(queue)): row, col = queue.pop(0) if row == size - 1 and col == size - 1: return count for direction in directions: new_row = row + direction[0] new_col = col + direction[1] if 0 <= new_row < size and 0 <= new_col < size and grid[new_row][new_col] == 0 and (new_row, new_col) not in visited: queue.append((new_row, new_col)) visited.add((new_row, new_col)) return -1 ``` ================================================ FILE: docs/solutions/1000-1099/smallest-subsequence-of-distinct-characters.md ================================================ # [1081. 不同字符的最小子序列](https://leetcode.cn/problems/smallest-subsequence-of-distinct-characters/) - 标签:栈、贪心、字符串、单调栈 - 难度:中等 ## 题目链接 - [1081. 不同字符的最小子序列 - 力扣](https://leetcode.cn/problems/smallest-subsequence-of-distinct-characters/) ## 题目大意 **描述**:给定一个字符串 `s`。 **要求**:去除字符串中重复的字母,使得每个字母只出现一次。需要保证 **「返回结果的字典序最小(要求不能打乱其他字符的相对位置)」**。 **说明**: - $1 \le s.length \le 10^4$。 - `s` 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "bcabc" 输出:"abc" ``` - 示例 2: ```python 输入:s = "cbacdcbc" 输出:"acdb" ``` ## 解题思路 ### 思路 1:哈希表 + 单调栈 针对题目的三个要求:去重、不能打乱其他字符顺序、字典序最小。我们来一一分析。 1. **去重**:可以通过 **「使用哈希表存储字母出现次数」** 的方式,将每个字母出现的次数统计起来,再遍历一遍,去除重复的字母。 2. **不能打乱其他字符顺序**:按顺序遍历,将非重复的字母存储到答案数组或者栈中,最后再拼接起来,就能保证不打乱其他字符顺序。 3. **字典序最小**:意味着字典序小的字母应该尽可能放在前面。 1. 对于第 `i` 个字符 `s[i]` 而言,如果第 `0` ~ `i - 1` 之间的某个字符 `s[j]` 在 `s[i]` 之后不再出现了,那么 `s[j]` 必须放到 `s[i]` 之前。 2. 而如果 `s[j]` 在之后还会出现,并且 `s[j]` 的字典序大于 `s[i]`,我们则可以先舍弃 `s[j]`,把 `s[i]` 尽可能的放到前面。后边再考虑使用 `s[j]` 所对应的字符。 要满足第 3 条需求,我们可以使用 **「单调栈」** 来解决。我们使用单调栈存储 `s[i]` 之前出现的非重复、并且字典序最小的字符序列。整个算法步骤如下: 1. 先遍历一遍字符串,用哈希表 `letter_counts` 统计出每个字母出现的次数。 2. 然后使用单调递减栈保存当前字符之前出现的非重复、并且字典序最小的字符序列。 3. 当遍历到 `s[i]` 时,如果 `s[i]` 没有在栈中出现过: 1. 比较 `s[i]` 和栈顶元素 `stack[-1]` 的字典序。如果 `s[i]` 的字典序小于栈顶元素 `stack[-1]`,并且栈顶元素之后的出现次数大于 `0`,则将栈顶元素弹出。 2. 然后继续判断 `s[i]` 和栈顶元素 `stack[-1]`,并且知道栈顶元素出现次数为 `0` 时停止弹出。此时将 `s[i]` 添加到单调栈中。 4. 从哈希表 `letter_counts` 中减去 `s[i]` 出现的次数,继续遍历。 5. 最后将单调栈中的字符依次拼接为答案字符串,并返回。 ### 思路 1:代码 ```python class Solution: def removeDuplicateLetters(self, s: str) -> str: stack = [] letter_counts = dict() for ch in s: if ch in letter_counts: letter_counts[ch] += 1 else: letter_counts[ch] = 1 for ch in s: if ch not in stack: while stack and ch < stack[-1] and stack[-1] in letter_counts and letter_counts[stack[-1]] > 0: stack.pop() stack.append(ch) letter_counts[ch] -= 1 return ''.join(stack) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(|\sum|)$,其中 $\sum$ 为字符集合,$|\sum|$ 为字符种类个数。由于栈中字符不能重复,因此栈中最多有 $|\sum|$ 个字符。 ## 参考资料 - 【题解】[去除重复数组 - 去除重复字母 - 力扣(LeetCode)](https://leetcode.cn/problems/remove-duplicate-letters/solution/qu-chu-zhong-fu-shu-zu-by-lu-shi-zhe-sokp/) ================================================ FILE: docs/solutions/1000-1099/stream-of-characters.md ================================================ # [1032. 字符流](https://leetcode.cn/problems/stream-of-characters/) - 标签:设计、字典树、数组、字符串、数据流 - 难度:困难 ## 题目链接 - [1032. 字符流 - 力扣](https://leetcode.cn/problems/stream-of-characters/) ## 题目大意 **描述**:设计一个算法:接收一个字符流,并检查这些字符的后缀是否是字符串数组 $words$ 中的一个字符串。 **要求**: 按下述要求实现 StreamChecker 类: - `StreamChecker(String[] words):` 构造函数,用字符串数组 $words$ 初始化数据结构。 - `boolean query(char letter):` 从字符流中接收一个新字符,如果字符流中的任一非空后缀能匹配 $words$ 中的某一字符串,返回 $True$;否则,返回 $False$。 **说明**: - $1 \le words.length \le 2000$。 - $1 <= words[i].length <= 200$。 - $words[i]$ 由小写英文字母组成。 - $letter$ 是一个小写英文字母。 - 最多调用查询 $4 \times 10^4$ 次。 **示例**: - 示例 1: ```python 输入: ["StreamChecker", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query"] [[["cd", "f", "kl"]], ["a"], ["b"], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"], ["i"], ["j"], ["k"], ["l"]] 输出: [null, false, false, false, true, false, true, false, false, false, false, false, true] 解释: StreamChecker streamChecker = new StreamChecker(["cd", "f", "kl"]); streamChecker.query("a"); // 返回 False streamChecker.query("b"); // 返回 False streamChecker.query("c"); // 返回n False streamChecker.query("d"); // 返回 True ,因为 'cd' 在 words 中 streamChecker.query("e"); // 返回 False streamChecker.query("f"); // 返回 True ,因为 'f' 在 words 中 streamChecker.query("g"); // 返回 False streamChecker.query("h"); // 返回 False streamChecker.query("i"); // 返回 False streamChecker.query("j"); // 返回 False streamChecker.query("k"); // 返回 False streamChecker.query("l"); // 返回 True ,因为 'kl' 在 words 中 ``` ## 解题思路 这道题要求设计一个数据结构,能够实时检查字符流中的后缀是否匹配给定的单词集合。由于字符流是动态的,我们需要高效地处理每个新字符的查询。 ### 思路 1:字典树 + 字符串反转 **问题分析**: - 需要检查字符流中的后缀是否匹配单词集合中的任意单词 - 字符流是动态添加的,直接存储所有可能的后缀会非常低效 - 字典树适合前缀匹配,但我们需要后缀匹配 **核心思想**: 将后缀匹配问题转化为前缀匹配问题:将所有单词反转后插入字典树,这样检查后缀就变成了检查前缀。 **算法步骤**: 1. **初始化**:将所有单词反转后插入字典树中 2. **查询处理**:每次接收到新字符时,将其添加到字符流的前面 3. **匹配检查**:在字典树中搜索当前字符流,找到匹配的单词就返回 `True` **关键优化**: - 使用反转的单词构建字典树,将后缀匹配转化为前缀匹配 - 在搜索过程中,一旦找到匹配的单词就立即返回,避免不必要的继续搜索 **示例分析**: - 单词集合:`["cd", "f", "kl"]` → 插入字典树:`["dc", "f", "lk"]` - 字符流 `"cd"` → 检查 `"dc"` 是否在字典树中 → 匹配成功,返回 `True` ### 思路 1:代码 ```python class Node: # 字符节点 def __init__(self): # 初始化字符节点 self.children = dict() # 初始化子节点 self.isEnd = False # isEnd 用于标记单词结束 class Trie: # 字典树 # 初始化字典树 def __init__(self): # 初始化字典树 self.root = Node() # 初始化根节点(根节点不保存字符) # 向字典树中插入一个单词 def insert(self, word: str) -> None: cur = self.root for ch in word: # 遍历单词中的字符 if ch not in cur.children: # 如果当前节点的子节点中,不存在键为 ch 的节点 cur.children[ch] = Node() # 建立一个节点,并将其保存到当前节点的子节点 cur = cur.children[ch] # 令当前节点指向新建立的节点,继续处理下一个字符 cur.isEnd = True # 单词处理完成时,将当前节点标记为单词结束 # 查找字典树中是否存在一个单词 def search(self, word: str) -> bool: cur = self.root for ch in word: # 遍历单词中的字符 if ch not in cur.children: # 如果当前节点的子节点中,不存在键为 ch 的节点 return False # 直接返回 False cur = cur.children[ch] # 令当前节点指向新建立的节点,然后继续查找下一个字符 if cur.isEnd: return True return False # 查找字典树中是否存在一个前缀 def startsWith(self, prefix: str) -> bool: cur = self.root for ch in prefix: # 遍历前缀中的字符 if ch not in cur.children: # 如果当前节点的子节点中,不存在键为 ch 的节点 return False # 直接返回 False cur = cur.children[ch] # 令当前节点指向新建立的节点,然后继续查找下一个字符 return cur is not None # 判断当前节点是否为空,不为空则查找成功 class StreamChecker: def __init__(self, words: List[str]): self.trie = Trie() self.stream = "" for word in words: self.trie.insert(word[::-1]) def query(self, letter: str) -> bool: self.stream = letter + self.stream size = len(letter) return self.trie.search(self.stream) # Your StreamChecker object will be instantiated and called as such: # obj = StreamChecker(words) # param_1 = obj.query(letter) ``` ### 思路 1:复杂度分析 - **时间复杂度**: - 初始化:$O(m \times n)$,其中 $m$ 是单词数量,$n$ 是单词的平均长度。 - 查询:$O(k)$,其中 $k$ 是当前字符流的长度。最坏情况下,每次查询都需要遍历整个字符流。 - **空间复杂度**:$O(m \times n)$,字典树的空间复杂度,其中 $m$ 是单词数量,$n$ 是单词的平均长度。 ### 思路 2:AC 自动机 **问题分析**: - 需要处理多模式串匹配问题,适合使用 AC 自动机 - 字符流查询频率高,需要优化查询时间复杂度 - 不需要存储完整的字符流历史,只需要维护当前匹配状态 **核心思想**: 使用 AC 自动机(Aho-Corasick Automaton)进行多模式串匹配: 1. 将所有单词构建成AC自动机,利用字典树共享公共前缀 2. 为每个节点设置失配指针,实现匹配失败时的快速跳转 3. 维护当前匹配状态,每次接收新字符时更新状态并检查匹配 **算法步骤**: 1. **构建AC自动机**:将所有单词插入字典树,并构建失配指针 2. **维护匹配状态**:使用变量记录当前在AC自动机中的位置 3. **字符流处理**:每次接收新字符时,沿着AC自动机进行状态转移 4. **匹配检测**:检查当前状态及其失配链上是否有单词结尾 **关键优势**: - **时间复杂度优秀**:构建 $O(m)$,查询平均 $O(1)$ - **空间效率高**:共享公共前缀,节省存储空间 - **适合流式处理**:不需要存储整个字符流历史 ### 思路 2:代码 ```python class TrieNode: def __init__(self): self.children = {} # 子节点,key 为字符,value 为 TrieNode self.fail = None # 失配指针,指向当前节点最长可用后缀的节点 self.is_end = False # 是否为某个模式串的结尾 self.word = "" # 如果是结尾,存储完整的单词 class AC_Automaton: def __init__(self): self.root = TrieNode() # 初始化根节点 def add_word(self, word): """ 向Trie树中插入一个模式串 """ node = self.root for char in word: if char not in node.children: node.children[char] = TrieNode() # 新建子节点 node = node.children[char] node.is_end = True # 标记单词结尾 node.word = word # 存储完整单词 def build_fail_pointers(self): """ 构建失配指针(fail指针),采用BFS广度优先遍历 """ from collections import deque queue = deque() # 1. 根节点的所有子节点的 fail 指针都指向根节点 for child in self.root.children.values(): child.fail = self.root queue.append(child) # 2. 广度优先遍历,依次为每个节点建立 fail 指针 while queue: current = queue.popleft() for char, child in current.children.items(): # 从当前节点的 fail 指针开始,向上寻找有无相同字符的子节点 fail = current.fail while fail and char not in fail.children: fail = fail.fail # 如果找到了,child的fail指针指向该节点,否则指向根节点 child.fail = fail.children[char] if fail and char in fail.children else self.root queue.append(child) class StreamChecker: def __init__(self, words): self.ac = AC_Automaton() # 将所有单词插入AC自动机 for word in words: self.ac.add_word(word) # 构建失配指针 self.ac.build_fail_pointers() # 当前匹配状态 self.current_node = self.ac.root def query(self, letter): """ 处理新字符,检查是否匹配到任何单词 """ # 如果当前节点没有该字符的子节点,则沿 fail 指针向上跳转 while self.current_node is not self.ac.root and letter not in self.current_node.children: self.current_node = self.current_node.fail # 如果有该字符的子节点,则转移到该子节点 if letter in self.current_node.children: self.current_node = self.current_node.children[letter] # 否则仍然停留在根节点 # 检查当前节点以及沿 fail 链上的所有节点是否为单词结尾 temp = self.current_node while temp is not self.ac.root: if temp.is_end: return True # 找到匹配的单词 temp = temp.fail return False # 没有找到匹配的单词 # Your StreamChecker object will be instantiated and called as such: # obj = StreamChecker(words) # param_1 = obj.query(letter) ``` ### 思路 2:复杂度分析 - **时间复杂度**: - 初始化:$O(m)$,其中 $m$ 是所有单词的总长度。构建字典树和失配指针都是线性时间。 - 查询:$O(1)$ 平均情况,$O(k)$ 最坏情况,其中 $k$ 是单词的最大长度。由于失配指针的存在,大部分情况下可以快速跳转。 - **空间复杂度**:$O(m)$,其中 $m$ 是所有单词的总长度。AC自动机的空间复杂度主要由字典树决定。 ================================================ FILE: docs/solutions/1000-1099/two-city-scheduling.md ================================================ # [1029. 两地调度](https://leetcode.cn/problems/two-city-scheduling/) - 标签:贪心、数组、排序 - 难度:中等 ## 题目链接 - [1029. 两地调度 - 力扣](https://leetcode.cn/problems/two-city-scheduling/) ## 题目大意 **描述**:公司计划面试 `2 * n` 人。给你一个数组 `costs`,其中 `costs[i] = [aCosti, bCosti]`,表示第 `i` 人飞往 `a` 市的费用为 `aCosti` ,飞往 `b` 市的费用为 `bCosti`。 **要求**:返回将每个人都飞到 `a`、`b` 中某座城市的最低费用,要求每个城市都有 `n` 人抵达。 **说明**: - $2 * n == costs.length$。 - $2 \le costs.length \le 100$。 - $costs.length$ 为偶数。 - $1 \le aCosti, bCosti \le 1000$。 **示例**: - 示例 1: ```python 输入:costs = [[10,20],[30,200],[400,50],[30,20]] 输出:110 解释: 第一个人去 a 市,费用为 10。 第二个人去 a 市,费用为 30。 第三个人去 b 市,费用为 50。 第四个人去 b 市,费用为 20。 最低总费用为 10 + 30 + 50 + 20 = 110,每个城市都有一半的人在面试。 ``` ## 解题思路 ### 思路 1:贪心算法 我们先假设所有人都去了城市 `a`。然后令一半的人再去城市 `b`。现在的问题就变成了,让一半的人改变城市去向,从原本的 `a` 城市改成 `b` 城市的最低费用为多少。 已知第 `i` 个人更换去向的费用为「去城市 `b` 的费用 - 去城市 `a` 的费用」。所以我们可以根据「去城市 `b` 的费用 - 去城市 `a` 的费用」对数组 `costs` 进行排序,让前 `n` 个改变方向去城市 `b`,后 `n` 个人去城市 `a`。 最后统计所有人员的费用,将其返回即可。 ### 思路 1:贪心算法代码 ```python class Solution: def twoCitySchedCost(self, costs: List[List[int]]) -> int: costs.sort(key=lambda x:x[1] - x[0]) cost = 0 size = len(costs) // 2 for i in range(size): cost += costs[i][ 1] cost += costs[i + size][0] return cost ``` ================================================ FILE: docs/solutions/1000-1099/two-sum-less-than-k.md ================================================ # [1099. 小于 K 的两数之和](https://leetcode.cn/problems/two-sum-less-than-k/) - 标签:数组、双指针、二分查找、排序 - 难度:简单 ## 题目链接 - [1099. 小于 K 的两数之和 - 力扣](https://leetcode.cn/problems/two-sum-less-than-k/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 和整数 $k$。 **要求**:返回最大和 $sum$,满足存在 $i < j$ 使得 $nums[i] + nums[j] = sum$ 且 $sum < k$。如果没有满足此等式的 $i$, $j$ 存在,则返回 $-1$。 **说明**: - $1 \le nums.length \le 100$。 - $1 \le nums[i] \le 1000$。 - $1 \le k \le 2000$。 **示例**: - 示例 1: ```python 输入:nums = [34,23,1,24,75,33,54,8], k = 60 输出:58 解释:34 和 24 相加得到 58,58 小于 60,满足题意。 ``` - 示例 2: ```python 输入:nums = [10,20,30], k = 15 输出:-1 解释:我们无法找到和小于 15 的两个元素。 ``` ## 解题思路 ### 思路 1:对撞指针 常规暴力枚举时间复杂度为 $O(n^2)$。可以通过双指针降低时间复杂度。具体做法如下: - 先对数组进行排序(时间复杂度为 $O(n \log n$),使用 $res$ 记录答案,初始赋值为最小值 `float('-inf')`。 - 使用两个指针 $left$、$right$。$left$ 指向第 $0$ 个元素位置,$right$ 指向数组的最后一个元素位置。 - 计算 $nums[left] + nums[right]$,与 $k$ 进行比较。 - 如果 $nums[left] + nums[right] \ge k$,则将 $right$ 左移,继续查找。 - 如果 $nums[left] + nums[rigth] < k$,则将 $left$ 右移,并更新答案值。 - 当 $left == right$ 时,区间搜索完毕,判断 $res$ 是否等于 `float('-inf')`,如果等于,则返回 $-1$,否则返回 $res$。 ### 思路 1:代码 ```python class Solution: def twoSumLessThanK(self, nums: List[int], k: int) -> int: nums.sort() res = float('-inf') left, right = 0, len(nums) - 1 while left < right: total = nums[left] + nums[right] if total >= k: right -= 1 else: res = max(res, total) left += 1 return res if res != float('-inf') else -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为数组中元素的个数。 - **空间复杂度**:$O(\log n)$,排序需要 $\log n$ 的栈空间。 ================================================ FILE: docs/solutions/1000-1099/uncrossed-lines.md ================================================ # [1035. 不相交的线](https://leetcode.cn/problems/uncrossed-lines/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [1035. 不相交的线 - 力扣](https://leetcode.cn/problems/uncrossed-lines/) ## 题目大意 有两条独立平行的水平线,按照给定的顺序写下 `nums1` 和 `nums2` 的整数。 现在,我们可以绘制一些直线,只要满足以下要求: - `nums1[i] == nums2[j]`。 - 绘制的直线不与其他任何直线相交。 例如:![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/04/28/142.png) 现在要求:计算出能绘制的最大直线数目。 ## 解题思路 动态规划求解。 定义状态 `dp[i][j]` 表示:`nums1` 中前 `i` 个数与 `nums2` 中前 `j` 个数的最大连接数,则: 状态转移方程为: - 如果 `nums1[i] == nums[j]`,则 `nums1[i]` 与 `nums2[j]` 可连线,此时 `dp[i][j] = dp[i - 1][j - 1] + 1`。 - 如果 `nums1[i] != nums[j]`,则 `nums1[i]` 与 `nums2[j]` 不可连线,此时最大连线数取决于 `dp[i - 1][j]` 和 `dp[i][j - 1]` 的较大值,即:`dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])`。 最后输出 `dp[size1][size2]` 即可。 ## 代码 ```python class Solution: def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int: size1 = len(nums1) size2 = len(nums2) dp = [[0 for _ in range(size2 + 1)] for _ in range(size1 + 1)] for i in range(1, size1 + 1): for j in range(1, size2 + 1): if nums1[i - 1] == nums2[j - 1]: dp[i][j] = dp[i - 1][j - 1] + 1 else: dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) return dp[size1][size2] ``` ================================================ FILE: docs/solutions/1000-1099/valid-boomerang.md ================================================ # [1037. 有效的回旋镖](https://leetcode.cn/problems/valid-boomerang/) - 标签:几何、数组、数学 - 难度:简单 ## 题目链接 - [1037. 有效的回旋镖 - 力扣](https://leetcode.cn/problems/valid-boomerang/) ## 题目大意 **描述**:给定一个数组 $points$,其中 $points[i] = [xi, yi]$ 表示平面上的一个点。 **要求**:如果这些点构成一个回旋镖,则返回 `True`,否则,则返回 `False`。 **说明**: - **回旋镖**:定义为一组三个点,这些点各不相同且不在一条直线上。 - $points.length == 3$。 - $points[i].length == 2$。 - $0 \le xi, yi \le 100$。 **示例**: - 示例 1: ```python 输入:points = [[1,1],[2,3],[3,2]] 输出:True ``` - 示例 2: ```python 输入:points = [[1,1],[2,2],[3,3]] 输出:False ``` ## 解题思路 ### 思路 1: 设三点坐标为 $A = (x1, y1)$,$B = (x2, y2)$,$C = (x3, y3)$,则向量 $\overrightarrow{AB} = (x2 - x1, y2 - y1)$,$\overrightarrow{BC} = (x3 - x2, y3 - y2)$。 如果三点共线,则应满足:$\overrightarrow{AB} \times \overrightarrow{BC} = (x2 − x1) \times (y3 − y2) - (x3 − x2) \times (y2 − y1) = 0$。 如果三点不共线,则应满足:$\overrightarrow{AB} \times \overrightarrow{BC} = (x2 − x1) \times (y3 − y2) - (x3 − x2) \times (y2 − y1) \ne 0$。 ### 思路 1:代码 ```python class Solution: def isBoomerang(self, points: List[List[int]]) -> bool: x1, y1 = points[0] x2, y2 = points[1] x3, y3 = points[2] cross1 = (x2 - x1) * (y3 - y2) cross2 = (x3 - x2) * (y2 - y1) return cross1 - cross2 != 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1100-1199/corporate-flight-bookings.md ================================================ # [1109. 航班预订统计](https://leetcode.cn/problems/corporate-flight-bookings/) - 标签:数组、前缀和 - 难度:中等 ## 题目链接 - [1109. 航班预订统计 - 力扣](https://leetcode.cn/problems/corporate-flight-bookings/) ## 题目大意 **描述**:给定整数 `n`,代表 `n` 个航班。再给定一个包含三元组的数组 `bookings`,代表航班预订表。表中第 `i` 条预订记录 $bookings[i] = [first_i, last_i, seats_i]$ 意味着在从 $first_i$ 到 $last_i$ (包含 $first_i$ 和 $last_i$)的 每个航班上预订了 $seats_i$ 个座位。 **要求**:返回一个长度为 `n` 的数组 `answer`,里面元素是每个航班预定的座位总数。 **说明**: - $1 \le n \le 2 * 10^4$。 - $1 \le bookings.length \le 2 * 10^4$。 - $bookings[i].length == 3$。 - $1 \le first_i \le last_i \le n$。 - $1 \le seats_i \le 10^4$ **示例**: - 示例 1: ```python 给定 n = 5。初始 answer = [0, 0, 0, 0, 0] 航班编号 1 2 3 4 5 预订记录 1 : 10 10 预订记录 2 : 20 20 预订记录 3 : 25 25 25 25 总座位数: 10 55 45 25 25 最终 answer = [10, 55, 45, 25, 25] ``` ## 解题思路 ### 思路 1:线段树 - 初始化一个长度为 `n`,值全为 `0` 的 `nums` 数组。 - 然后根据 `nums` 数组构建一棵线段树。每个线段树的节点类存储当前区间的左右边界和该区间的和。并且线段树使用延迟标记。 - 然后遍历三元组操作,进行区间累加运算。 - 最后从线段树中查询数组所有元素,返回该数组即可。 这样构建线段树的时间复杂度为 $O(\log n)$,单次区间更新的时间复杂度为 $O(\log n)$,单次区间查询的时间复杂度为 $O(\log n)$。总体时间复杂度为 $O(\log n)$。 ### 思路 1 线段树代码: ```python # 线段树的节点类 class SegTreeNode: def __init__(self, val=0): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值) self.lazy_tag = None # 区间和问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, nums, function): self.size = len(nums) self.tree = [SegTreeNode() for _ in range(4 * self.size)] # 维护 SegTreeNode 数组 self.nums = nums # 原始数据 self.function = function # function 是一个函数,左右区间的聚合方法 if self.size > 0: self.__build(0, 0, self.size - 1) # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.nums[i] = val self.__update_point(i, val, 0) # 区间更新接口:将区间为 [q_left, q_right] 上的所有元素值加上 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, 0) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, 0) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self): for i in range(self.size): self.nums[i] = self.query_interval(i, i) return self.nums # 以下为内部实现方法 # 构建线段树实现方法:节点的存储下标为 index,节点的区间为 [left, right] def __build(self, index, left, right): self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,节点值为对应位置的元素值 self.tree[index].val = self.nums[left] return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.__build(left_index, left, mid) # 递归创建左子树 self.__build(right_index, mid + 1, right) # 递归创建右子树 self.__pushup(index) # 向上更新节点的区间值 # 单点更新实现方法:将 nums[i] 更改为 val,节点的存储下标为 index def __update_point(self, i, val, index): left = self.tree[index].left right = self.tree[index].right if left == right: self.tree[index].val = val # 叶子节点,节点值修改为 val return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if i <= mid: # 在左子树中更新节点值 self.__update_point(i, val, left_index) else: # 在右子树中更新节点值 self.__update_point(i, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间更新实现方法 def __update_interval(self, q_left, q_right, val, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 if self.tree[index].lazy_tag is not None: self.tree[index].lazy_tag += val # 将当前节点的延迟标记增加 val else: self.tree[index].lazy_tag = val # 将当前节点的延迟标记增加 val interval_size = (right - left + 1) # 当前节点所在区间大小 self.tree[index].val += val * interval_size # 当前节点所在区间每个元素值增加 val return if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return self.__pushdown(index) # 向下更新节点的区间值 mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if q_left <= mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, left_index) if q_right > mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间查询实现方法:在线段树中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return self.tree[index].val # 直接返回节点值 if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(index) mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, left_index) if q_right > mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, right_index) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:更新下标为 index 的节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, index): left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.tree[index].val = self.function(self.tree[left_index].val, self.tree[right_index].val) # 向下更新实现方法:更新下标为 index 的节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, index): lazy_tag = self.tree[index].lazy_tag if lazy_tag is None: return left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if self.tree[left_index].lazy_tag is not None: self.tree[left_index].lazy_tag += lazy_tag # 更新左子节点懒惰标记 else: self.tree[left_index].lazy_tag = lazy_tag left_size = (self.tree[left_index].right - self.tree[left_index].left + 1) self.tree[left_index].val += lazy_tag * left_size # 左子节点每个元素值增加 lazy_tag if self.tree[right_index].lazy_tag is not None: self.tree[right_index].lazy_tag += lazy_tag # 更新右子节点懒惰标记 else: self.tree[right_index].lazy_tag = lazy_tag right_size = (self.tree[right_index].right - self.tree[right_index].left + 1) self.tree[right_index].val += lazy_tag * right_size # 右子节点每个元素值增加 lazy_tag self.tree[index].lazy_tag = None # 更新当前节点的懒惰标记 class Solution: def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]: nums = [0 for _ in range(n)] self.STree = SegmentTree(nums, lambda x, y: x + y) for booking in bookings: self.STree.update_interval(booking[0] - 1, booking[1] - 1, booking[2]) return self.STree.get_nums() ``` ================================================ FILE: docs/solutions/1100-1199/defanging-an-ip-address.md ================================================ # [1108. IP 地址无效化](https://leetcode.cn/problems/defanging-an-ip-address/) - 标签:字符串 - 难度:简单 ## 题目链接 - [1108. IP 地址无效化 - 力扣](https://leetcode.cn/problems/defanging-an-ip-address/) ## 题目大意 **描述**:给定一个有效的 IPv4 的地址 `address`。。 **要求**:返回这个 IP 地址的无效化版本。 **说明**: - **无效化 IP 地址**:其实就是用 `"[.]"` 代替了每个 `"."`。 **示例**: - 示例 1: ```python 输入:address = "255.100.50.0" 输出:"255[.]100[.]50[.]0" ``` ## 解题思路 ### 思路 1:字符串替换 依次将字符串 `address` 中的 `"."` 替换为 `"[.]"`。这里为了方便,直接调用了 `replace` 方法。 ### 思路 1:字符串替换代码 ```python class Solution: def defangIPaddr(self, address: str) -> str: return address.replace('.', '[.]') ``` ================================================ FILE: docs/solutions/1100-1199/delete-nodes-and-return-forest.md ================================================ # [1110. 删点成林](https://leetcode.cn/problems/delete-nodes-and-return-forest/) - 标签:树、深度优先搜索、数组、哈希表、二叉树 - 难度:中等 ## 题目链接 - [1110. 删点成林 - 力扣](https://leetcode.cn/problems/delete-nodes-and-return-forest/) ## 题目大意 **描述**:给定二叉树的根节点 $root$,树上每个节点都有一个不同的值。 如果节点值在 $to\_delete$ 中出现,我们就把该节点从树上删去,最后得到一个森林(一些不相交的树构成的集合)。 **要求**:返回森林中的每棵树。你可以按任意顺序组织答案。 **说明**: - 树中的节点数最大为 $1000$。 - 每个节点都有一个介于 $1$ 到 $1000$ 之间的值,且各不相同。 - $to\_delete.length \le 1000$。 - $to\_delete$ 包含一些从 $1$ 到 $1000$、各不相同的值。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/07/05/screen-shot-2019-07-01-at-53836-pm.png) ```python 输入:root = [1,2,3,4,5,6,7], to_delete = [3,5] 输出:[[1,2,null,4],[6],[7]] ``` - 示例 2: ```python 输入:root = [1,2,4,null,3], to_delete = [3] 输出:[[1,2,4]] ``` ## 解题思路 ### 思路 1:深度优先搜索 将待删除节点数组 $to\_delete$ 转为集合 $deletes$,则每次能以 $O(1)$ 的时间复杂度判断节点值是否在待删除节点数组中。 如果当前节点值在待删除节点数组中,则删除当前节点后,我们还需要判断其左右子节点是否也在待删除节点数组中。 以此类推,还需要判断左右子节点的左右子节点。。。 因此,我们应该递归遍历处理完所有的左右子树,再判断当前节点的左右子节点是否在待删除节点数组中。如果在,则将其加入到答案数组中。 为此我们可以写一个深度优先搜索算法,具体步骤如下: 1. 如果当前根节点为空,则返回 `None`。 2. 递归遍历处理完当前根节点的左右子树,更新当前节点的左右子树(子节点被删除的情况下需要更新当前根节点的左右子树)。 3. 如果当前根节点值在待删除节点数组中: 1. 如果当前根节点的左子树没有在被删除节点数组中,将左子树节点加入到答案数组中。 2. 如果当前根节点的右子树没有在被删除节点数组中,将右子树节点加入到答案数组中。 3. 返回 `None`,表示当前节点被删除。 4. 如果当前根节点值不在待删除节点数组中: 1. 返回根节点,表示当前节点没有被删除。 ### 思路 1:代码 ```Python class Solution: def delNodes(self, root: Optional[TreeNode], to_delete: List[int]) -> List[TreeNode]: forest = [] deletes = set(to_delete) def dfs(root): if not root: return None root.left = dfs(root.left) root.right = dfs(root.right) if root.val in deletes: if root.left: forest.append(root.left) if root.right: forest.append(root.right) return None else: return root if dfs(root): forest.append(root) return forest ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为二叉树中节点个数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1100-1199/diet-plan-performance.md ================================================ # [1176. 健身计划评估](https://leetcode.cn/problems/diet-plan-performance/) - 标签:数组、滑动窗口 - 难度:简单 ## 题目链接 - [1176. 健身计划评估 - 力扣](https://leetcode.cn/problems/diet-plan-performance/) ## 题目大意 **描述**:好友给自己制定了一份健身计划。想请你帮他评估一下这份计划是否合理。 给定一个数组 $calories$,其中 $calories[i]$ 代表好友第 $i$ 天需要消耗的卡路里总量。再给定 $lower$ 代表较低消耗的卡路里,$upper$ 代表较高消耗的卡路里。再给定一个整数 $k$,代表连续 $k$ 天。 - 如果你的好友在这一天以及之后连续 $k$ 天内消耗的总卡路里 $T$ 小于 $lower$,则这一天的计划相对糟糕,并失去 $1$ 分。 - 如果你的好友在这一天以及之后连续 $k$ 天内消耗的总卡路里 $T$ 高于 $upper$,则这一天的计划相对优秀,并得到 $1$ 分。 - 如果你的好友在这一天以及之后连续 $k$ 天内消耗的总卡路里 $T$ 大于等于 $lower$,并且小于等于 $upper$,则这份计划普普通通,分值不做变动。 **要求**:输出最后评估的得分情况。 **说明**: - $1 \le k \le calories.length \le 10^5$。 - $0 \le calories[i] \le 20000$。 - $0 \le lower \le upper$。 **示例**: - 示例 1: ```python 输入:calories = [1,2,3,4,5], k = 1, lower = 3, upper = 3 输出:0 解释:calories[0], calories[1] < lower 而 calories[3], calories[4] > upper, 总分 = 0. ``` - 示例 2: ```python 输入:calories = [3,2], k = 2, lower = 0, upper = 1 输出:1 解释:calories[0] + calories[1] > upper, 总分 = 1. ``` ## 解题思路 ### 思路 1:滑动窗口 固定长度为 $k$ 的滑动窗口题目。具体做法如下: 1. $score$ 用来维护得分情况,初始值为 $0$。$window\_sum$ 用来维护窗口中卡路里总量。 2. $left$ 、$right$ 都指向数组的第一个元素,即:`left = 0`,`right = 0`。 3. 向右移动 $right$,先将 $k$ 个元素填入窗口中。 4. 当窗口元素个数为 $k$ 时,即:$right - left + 1 \ge k$ 时,计算窗口内的卡路里总量,并判断和 $upper$、$lower$ 的关系。同时维护得分情况。 5. 然后向右移动 $left$,从而缩小窗口长度,即 `left += 1`,使得窗口大小始终保持为 $k$。 6. 重复 $4 \sim 5$ 步,直到 $right$ 到达数组末尾。 最后输出得分情况 $score$。 ### 思路 1:代码 ```python class Solution: def dietPlanPerformance(self, calories: List[int], k: int, lower: int, upper: int) -> int: left, right = 0, 0 window_sum = 0 score = 0 while right < len(calories): window_sum += calories[right] if right - left + 1 >= k: if window_sum < lower: score -= 1 elif window_sum > upper: score += 1 window_sum -= calories[left] left += 1 right += 1 return score ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $calories$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1100-1199/distance-between-bus-stops.md ================================================ # [1184. 公交站间的距离](https://leetcode.cn/problems/distance-between-bus-stops/) - 标签:数组 - 难度:简单 ## 题目链接 - [1184. 公交站间的距离 - 力扣](https://leetcode.cn/problems/distance-between-bus-stops/) ## 题目大意 **描述**:环形公交路线上有 $n$ 个站,序号为 $0 \sim n - 1$。给定一个数组 $distance$ 表示每一对相邻公交站之间的距离,其中 $distance[i]$ 表示编号为 $i$ 的车站与编号为 $(i + 1) \mod n$ 的车站之间的距离。再给定乘客的出发点编号 $start$ 和目的地编号 $destination$。 **要求**:返回乘客从出发点 $start$ 到目的地 $destination$ 之间的最短距离。 **说明**: - $1 \le n \le 10^4$。 - $distance.length == n$。 - $0 \le start, destination < n$。 - $0 \le distance[i] \le 10^4$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/09/08/untitled-diagram-1.jpg) ```python 输入:distance = [1,2,3,4], start = 0, destination = 1 输出:1 解释:公交站 0 和 1 之间的距离是 1 或 9,最小值是 1。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/09/08/untitled-diagram-1-1.jpg) ```python 输入:distance = [1,2,3,4], start = 0, destination = 2 输出:3 解释:公交站 0 和 2 之间的距离是 3 或 7,最小值是 3。 ``` ## 解题思路 ### 思路 1:简单模拟 1. 因为 $start$ 和 $destination$ 的先后顺序不影响结果,为了方便计算,我们先令 $start \le destination$。 2. 遍历数组 $distance$,计算出 $[start, destination]$ 之间的距离和 $dist$。 3. 计算出环形路线中 $[destination, start]$ 之间的距离和为 $sum(distance) - dist$。 4. 比较 $2 \sim 3$ 中两个距离的大小,将距离最小值作为答案返回。 ### 思路 1:代码 ```python class Solution: def distanceBetweenBusStops(self, distance: List[int], start: int, destination: int) -> int: start, destination = min(start, destination), max(start, destination) dist = 0 for i in range(len(distance)): if start <= i < destination: dist += distance[i] return min(dist, sum(distance) - dist) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1100-1199/distribute-candies-to-people.md ================================================ # [1103. 分糖果 II](https://leetcode.cn/problems/distribute-candies-to-people/) - 标签:数学、模拟 - 难度:简单 ## 题目链接 - [1103. 分糖果 II - 力扣](https://leetcode.cn/problems/distribute-candies-to-people/) ## 题目大意 **描述**:给定一个整数 $candies$,代表糖果的数量。再给定一个整数 $num\_people$,代表小朋友的数量。 现在开始分糖果,给第 $1$ 个小朋友分 $1$ 颗糖果,第 $2$ 个小朋友分 $2$ 颗糖果,以此类推,直到最后一个小朋友分 $n$ 颗糖果。 然后回到第 $1$ 个小朋友,给第 $1$ 个小朋友分 $n + 1$ 颗糖果,第 $2$ 个小朋友分 $n + 2$ 颗糖果,一次类推,直到最后一个小朋友分 $n + n$ 颗糖果。 重复上述过程(每次都比上一次多给出 $1$ 颗糖果,当分完第 $n$ 个小朋友时回到第 $1$ 个小朋友),直到我们分完所有的糖果。 > 注意:如果我们手中剩下的糖果数不够(小于等于前一次发的糖果数),则将剩下的糖果全部发给当前的小朋友。 **要求**:返回一个长度为 $num\_people$、元素之和为 $candies$ 的数组,以表示糖果的最终分发情况(即 $ans[i]$ 表示第 $i$ 个小朋友分到的糖果数)。 **说明**: - $1 \le candies \le 10^9$。 - $1 \le num\_people \le 1000$。 **示例**: - 示例 1: ```python 输入:candies = 7, num_people = 4 输出:[1,2,3,1] 解释: 第一次,ans[0] += 1,数组变为 [1,0,0,0]。 第二次,ans[1] += 2,数组变为 [1,2,0,0]。 第三次,ans[2] += 3,数组变为 [1,2,3,0]。 第四次,ans[3] += 1(因为此时只剩下 1 颗糖果),最终数组变为 [1,2,3,1]。 ``` - 示例 2: ```python 输入:candies = 10, num_people = 3 输出:[5,2,3] 解释: 第一次,ans[0] += 1,数组变为 [1,0,0]。 第二次,ans[1] += 2,数组变为 [1,2,0]。 第三次,ans[2] += 3,数组变为 [1,2,3]。 第四次,ans[0] += 4,最终数组变为 [5,2,3]。 ``` ## 解题思路 ### 思路 1:暴力模拟 不断遍历数组,将对应糖果数分给当前小朋友,直到糖果数为 $0$ 时停止。 ### 思路 1:代码 ```python class Solution: def distributeCandies(self, candies: int, num_people: int) -> List[int]: ans = [0 for _ in range(num_people)] idx = 0 while candies: ans[idx % num_people] += min(idx + 1, candies) candies -= min(idx + 1, candies) idx += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(max(\sqrt{m}, n))$,其中 $m$ 为糖果数量,$n$ 为小朋友数量。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1100-1199/find-k-length-substrings-with-no-repeated-characters.md ================================================ # [1100. 长度为 K 的无重复字符子串](https://leetcode.cn/problems/find-k-length-substrings-with-no-repeated-characters/) - 标签:哈希表、字符串、滑动窗口 - 难度:中等 ## 题目链接 - [1100. 长度为 K 的无重复字符子串 - 力扣](https://leetcode.cn/problems/find-k-length-substrings-with-no-repeated-characters/) ## 题目大意 **描述**:给定一个字符串 `s`。 **要求**:找出所有长度为 `k` 且不含重复字符的子串,返回全部满足要求的子串的数目。 **说明**: - $1 \le s.length \le 10^4$。 - $s$ 中的所有字符均为小写英文字母。 - $1 <= k <= 10^4$。 **示例**: - 示例 1: ```python 输入:s = "havefunonleetcode", k = 5 输出:6 解释: 这里有 6 个满足题意的子串,分别是:'havef','avefu','vefun','efuno','etcod','tcode'。 ``` - 示例 2: ```python 输入:s = "home", K = 5 输出:0 解释: 注意:k 可能会大于 s 的长度。在这种情况下,就无法找到任何长度为 k 的子串。 ``` ## 解题思路 ### 思路 1:滑动窗口 固定长度滑动窗口的题目。维护一个长度为 `k` 的滑动窗口。用 `window_count` 来表示窗口内所有字符个数。可以用字典、数组来实现,也可以直接用 `collections.Counter()` 实现。然后不断向右滑动,然后进行比较。如果窗口内字符无重复,则答案数目 + 1。然后继续滑动。直到末尾时。整个解题步骤具体如下: 1. `window_count` 用来维护窗口中 `2` 对应子串的各个字符数量。 2. `left` 、`right` 都指向序列的第一个元素,即:`left = 0`,`right = 0`。 3. 向右移动 `right`,先将 `k` 个元素填入窗口中。 4. 当窗口元素个数为 `k` 时,即:`right - left + 1 >= k` 时,判断窗口内各个字符数量 `window_count` 是否等于 `k`。 1. 如果等于,则答案 + 1。 2. 如果不等于,则向右移动 `left`,从而缩小窗口长度,即 `left += 1`,使得窗口大小始终保持为 `k`。 5. 重复 3 ~ 4 步,直到 `right` 到达数组末尾。返回答案。 ### 思路 1:代码 ```python import collections class Solution: def numKLenSubstrNoRepeats(self, s: str, k: int) -> int: left, right = 0, 0 window_count = collections.Counter() ans = 0 while right < len(s): window_count[s[right]] += 1 if right - left + 1 >= k: if len(window_count) == k: ans += 1 window_count[s[left]] -= 1 if window_count[s[left]] == 0: del window_count[s[left]] left += 1 right += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(|\sum|)$,其中 $\sum$ 是字符集。 ================================================ FILE: docs/solutions/1100-1199/index.md ================================================ ## 本章内容 - [1100. 长度为 K 的无重复字符子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/find-k-length-substrings-with-no-repeated-characters.md) - [1103. 分糖果 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/distribute-candies-to-people.md) - [1108. IP 地址无效化](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/defanging-an-ip-address.md) - [1109. 航班预订统计](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/corporate-flight-bookings.md) - [1110. 删点成林](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/delete-nodes-and-return-forest.md) - [1122. 数组的相对排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/relative-sort-array.md) - [1136. 并行课程](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/parallel-courses.md) - [1137. 第 N 个泰波那契数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/n-th-tribonacci-number.md) - [1143. 最长公共子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/longest-common-subsequence.md) - [1151. 最少交换次数来组合所有的 1](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/minimum-swaps-to-group-all-1s-together.md) - [1155. 掷骰子等于目标和的方法数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/number-of-dice-rolls-with-target-sum.md) - [1161. 最大层内元素和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/maximum-level-sum-of-a-binary-tree.md) - [1176. 健身计划评估](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/diet-plan-performance.md) - [1184. 公交站间的距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/distance-between-bus-stops.md) ================================================ FILE: docs/solutions/1100-1199/longest-common-subsequence.md ================================================ # [1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [1143. 最长公共子序列 - 力扣](https://leetcode.cn/problems/longest-common-subsequence/) ## 题目大意 **描述**:给定两个字符串 $text1$ 和 $text2$。 **要求**:返回两个字符串的最长公共子序列的长度。如果不存在公共子序列,则返回 $0$。 **说明**: - **子序列**:原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 - **公共子序列**:两个字符串所共同拥有的子序列。 - $1 \le text1.length, text2.length \le 1000$。 - $text1$ 和 $text2$ 仅由小写英文字符组成。 **示例**: - 示例 1: ```python 输入:text1 = "abcde", text2 = "ace" 输出:3 解释:最长公共子序列是 "ace",它的长度为 3。 ``` - 示例 2: ```python 输入:text1 = "abc", text2 = "abc" 输出:3 解释:最长公共子序列是 "abc",它的长度为 3。 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照两个字符串的结尾位置进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:「以 $text1$ 中前 $i$ 个元素组成的子字符串 $str1$ 」与「以 $text2$ 中前 $j$ 个元素组成的子字符串 $str2$」的最长公共子序列长度为 $dp[i][j]$。 ###### 3. 状态转移方程 双重循环遍历字符串 $text1$ 和 $text2$,则状态转移方程为: 1. 如果 $text1[i - 1] = text2[j - 1]$,说明两个子字符串的最后一位是相同的,所以最长公共子序列长度加 $1$。即:$dp[i][j] = dp[i - 1][j - 1] + 1$。 2. 如果 $text1[i - 1] \ne text2[j - 1]$,说明两个子字符串的最后一位是不同的,则 $dp[i][j]$ 需要考虑以下两种情况,取两种情况中最大的那种:$dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])$。 1. 「以 $text1$ 中前 $i - 1$ 个元素组成的子字符串 $str1$ 」与「以 $text2$ 中前 $j$ 个元素组成的子字符串 $str2$」的最长公共子序列长度,即 $dp[i - 1][j]$。 2. 「以 $text1$ 中前 $i$ 个元素组成的子字符串 $str1$ 」与「以 $text2$ 中前 $j - 1$ 个元素组成的子字符串 $str2$」的最长公共子序列长度,即 $dp[i][j - 1]$。 ###### 4. 初始条件 1. 当 $i = 0$ 时,$str1$ 表示的是空串,空串与 $str2$ 的最长公共子序列长度为 $0$,即 $dp[0][j] = 0$。 2. 当 $j = 0$ 时,$str2$ 表示的是空串,$str1$ 与 空串的最长公共子序列长度为 $0$,即 $dp[i][0] = 0$。 ###### 5. 最终结果 根据状态定义,最后输出 $dp[sise1][size2]$(即 $text1$ 与 $text2$ 的最长公共子序列长度)即可,其中 $size1$、$size2$ 分别为 $text1$、$text2$ 的字符串长度。 ### 思路 1:代码 ```python class Solution: def longestCommonSubsequence(self, text1: str, text2: str) -> int: size1 = len(text1) size2 = len(text2) dp = [[0 for _ in range(size2 + 1)] for _ in range(size1 + 1)] for i in range(1, size1 + 1): for j in range(1, size2 + 1): if text1[i - 1] == text2[j - 1]: dp[i][j] = dp[i - 1][j - 1] + 1 else: dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) return dp[size1][size2] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m)$,其中 $n$、$m$ 分别是字符串 $text1$、$text2$ 的长度。两重循环遍历的时间复杂度是 $O(n \times m)$,所以总的时间复杂度为 $O(n \times m)$。 - **空间复杂度**:$O(n \times m)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n \times m)$。 ================================================ FILE: docs/solutions/1100-1199/maximum-level-sum-of-a-binary-tree.md ================================================ # [1161. 最大层内元素和](https://leetcode.cn/problems/maximum-level-sum-of-a-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [1161. 最大层内元素和 - 力扣](https://leetcode.cn/problems/maximum-level-sum-of-a-binary-tree/) ## 题目大意 **描述**:给你一个二叉树的根节点 $root$。设根节点位于二叉树的第 $1$ 层,而根节点的子节点位于第 $2$ 层,依此类推。 **要求**:返回层内元素之和最大的那几层(可能只有一层)的层号,并返回其中层号最小的那个。 **说明**: - 树中的节点数在 $[1, 10^4]$ 范围内。 - $-10^5 \le Node.val \le 10^5$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/08/17/capture.jpeg) ```python 输入:root = [1,7,0,7,-8,null,null] 输出:2 解释: 第 1 层各元素之和为 1, 第 2 层各元素之和为 7 + 0 = 7, 第 3 层各元素之和为 7 + -8 = -1, 所以我们返回第 2 层的层号,它的层内元素之和最大。 ``` - 示例 2: ```python 输入:root = [989,null,10250,98693,-89388,null,null,null,-32127] 输出:2 ``` ## 解题思路 ### 思路 1:二叉树的层序遍历 1. 利用广度优先搜索,在二叉树的层序遍历的基础上,统计每一层节点和,并存入数组 $levels$ 中。 2. 遍历 $levels$ 数组,从 $levels$ 数组中找到最大层和 $max\_sum$。 3. 再次遍历 $levels$ 数组,找出等于最大层和 $max\_sum$ 的那一层,并返回该层序号。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def levelOrder(self, root: TreeNode) -> List[List[int]]: if not root: return [] queue = [root] levels = [] while queue: level = 0 size = len(queue) for _ in range(size): curr = queue.pop(0) level += curr.val if curr.left: queue.append(curr.left) if curr.right: queue.append(curr.right) levels.append(level) return levels def maxLevelSum(self, root: Optional[TreeNode]) -> int: levels = self.levelOrder(root) max_sum = max(levels) for i in range(len(levels)): if levels[i] == max_sum: return i + 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中 $n$ 是二叉树的节点数目。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1100-1199/minimum-swaps-to-group-all-1s-together.md ================================================ # [1151. 最少交换次数来组合所有的 1](https://leetcode.cn/problems/minimum-swaps-to-group-all-1s-together/) - 标签:数组、滑动窗口 - 难度:中等 ## 题目链接 - [1151. 最少交换次数来组合所有的 1 - 力扣](https://leetcode.cn/problems/minimum-swaps-to-group-all-1s-together/) ## 题目大意 **描述**:给定一个二进制数组 $data$。 **要求**:通过交换位置,将数组中任何位置上的 $1$ 组合到一起,并返回所有可能中所需的最少交换次数。c **说明**: - $1 \le data.length \le 10^5$。 - $data[i] == 0 \text{ or } 1$。 **示例**: - 示例 1: ```python 输入: data = [1,0,1,0,1] 输出: 1 解释: 有三种可能的方法可以把所有的 1 组合在一起: [1,1,1,0,0],交换 1 次; [0,1,1,1,0],交换 2 次; [0,0,1,1,1],交换 1 次。 所以最少的交换次数为 1。 ``` - 示例 2: ```python 输入:data = [0,0,0,1,0] 输出:0 解释: 由于数组中只有一个 1,所以不需要交换。 ``` ## 解题思路 ### 思路 1:滑动窗口 将数组中任何位置上的 $1$ 组合到一起,并要求最少的交换次数。也就是说交换之后,某个连续子数组中全是 $1$,数组其他位置全是 $0$。为此,我们可以维护一个固定长度为 $1$ 的个数的滑动窗口,找到滑动窗口中 $0$ 最少的个数,这样最终交换出去的 $0$ 最少,交换次数也最少。 求最少交换次数,也就是求滑动窗口中最少的 $0$ 的个数。具体做法如下: 1. 统计 $1$ 的个数,并设置为窗口长度 $window\_size$。使用 $window\_count$ 维护窗口中 $0$ 的个数。使用 $ans$ 维护窗口中最少的 $0$ 的个数,也可以叫做最少交换次数。 2. 如果 $window\_size$ 为 $0$,则说明不用交换,直接返回 $0$。 3. 使用两个指针 $left$、$right$。$left$、$right$ 都指向数组的第一个元素,即:`left = 0`,`right = 0`。 4. 如果 $data[right] == 0$,则更新窗口中 $0$ 的个数,即 `window_count += 1`。然后向右移动 $right$。 5. 当窗口元素个数为 $window\_size$ 时,即:$right - left + 1 \ge window\_size$ 时,更新窗口中最少的 $0$ 的个数。 6. 然后如果左侧 $data[left] == 0$,则更新窗口中 $0$ 的个数,即 `window_count -= 1`。然后向右移动 $left$,从而缩小窗口长度,即 `left += 1`,使得窗口大小始终保持为 $window\_size$。 7. 重复 4 ~ 6 步,直到 $right$ 到达数组末尾。返回答案 $ans$。 ### 思路 1:代码 ```python class Solution: def minSwaps(self, data: List[int]) -> int: window_size = 0 for item in data: if item == 1: window_size += 1 if window_size == 0: return 0 left, right = 0, 0 window_count = 0 ans = float('inf') while right < len(data): if data[right] == 0: window_count += 1 if right - left + 1 >= window_size: ans = min(ans, window_count) if data[left] == 0: window_count -= 1 left += 1 right += 1 return ans if ans != float('inf') else 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $data$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1100-1199/n-th-tribonacci-number.md ================================================ # [1137. 第 N 个泰波那契数](https://leetcode.cn/problems/n-th-tribonacci-number/) - 标签:记忆化搜索、数学、动态规划 - 难度:简单 ## 题目链接 - [1137. 第 N 个泰波那契数 - 力扣](https://leetcode.cn/problems/n-th-tribonacci-number/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:返回第 $n$ 个泰波那契数。 **说明**: - **泰波那契数**:$T_0 = 0, T_1 = 1, T_2 = 1$,且在 $n >= 0$ 的条件下,$T_{n + 3} = T_{n} + T_{n+1} + T_{n+2}$。 - $0 \le n \le 37$。 - 答案保证是一个 32 位整数,即 $answer \le 2^{31} - 1$。 **示例**: - 示例 1: ```python 输入:n = 4 输出:4 解释: T_3 = 0 + 1 + 1 = 2 T_4 = 1 + 1 + 2 = 4 ``` - 示例 2: ```python 输入:n = 25 输出:1389537 ``` ## 解题思路 ### 思路 1:记忆化搜索 1. 问题的状态定义为:第 $n$ 个泰波那契数。其状态转移方程为:$T_0 = 0, T_1 = 1, T_2 = 1$,且在 $n >= 0$ 的条件下,$T_{n + 3} = T_{n} + T_{n+1} + T_{n+2}$。 2. 定义一个长度为 $n + 1$ 数组 `memo` 用于保存一斤个计算过的泰波那契数。 3. 定义递归函数 `my_tribonacci(n, memo)`。 1. 当 $n = 0$ 或者 $n = 1$,或者 $n = 2$ 时直接返回结果。 2. 当 $n > 2$ 时,首先检查是否计算过 $T(n)$,即判断 $memo[n]$ 是否等于 $0$。 1. 如果 $memo[n] \ne 0$,说明已经计算过 $T(n)$,直接返回 $memo[n]$。 2. 如果 $memo[n] = 0$,说明没有计算过 $T(n)$,则递归调用 `my_tribonacci(n - 3, memo)`、`my_tribonacci(n - 2, memo)`、`my_tribonacci(n - 1, memo)`,并将计算结果存入 $memo[n]$ 中,并返回 $memo[n]$。 ### 思路 1:代码 ```python class Solution: def tribonacci(self, n: int) -> int: # 使用数组保存已经求解过的 T(k) 的结果 memo = [0 for _ in range(n + 1)] return self.my_tribonacci(n, memo) def my_tribonacci(self, n: int, memo: List[int]) -> int: if n == 0: return 0 if n == 1 or n == 2: return 1 if memo[n] != 0: return memo[n] memo[n] = self.my_tribonacci(n - 3, memo) + self.my_tribonacci(n - 2, memo) + self.my_tribonacci(n - 1, memo) return memo[n] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ### 思路 2:动态规划 ###### 1. 阶段划分 我们可以按照整数顺序进行阶段划分,将其划分为整数 $0 \sim n$。 ###### 2. 定义状态 定义状态 `dp[i]` 为:第 `i` 个泰波那契数。 ###### 3. 状态转移方程 根据题目中所给的泰波那契数的定义:$T_0 = 0, T_1 = 1, T_2 = 1$,且在 $n >= 0$ 的条件下,$T_{n + 3} = T_{n} + T_{n+1} + T_{n+2}$。,则直接得出状态转移方程为 $dp[i] = dp[i - 3] + dp[i - 2] + dp[i - 1]$(当 $i > 2$ 时)。 ###### 4. 初始条件 根据题目中所给的初始条件 $T_0 = 0, T_1 = 1, T_2 = 1$ 确定动态规划的初始条件,即 `dp[0] = 0, dp[1] = 1, dp[2] = 1`。 ###### 5. 最终结果 根据状态定义,最终结果为 `dp[n]`,即第 `n` 个泰波那契数为 `dp[n]`。 ### 思路 2:代码 ```python class Solution: def tribonacci(self, n: int) -> int: if n == 0: return 0 if n == 1 or n == 2: return 1 dp = [0 for _ in range(n + 1)] dp[1] = dp[2] = 1 for i in range(3, n + 1): dp[i] = dp[i - 3] + dp[i - 2] + dp[i - 1] return dp[n] ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1100-1199/number-of-dice-rolls-with-target-sum.md ================================================ # [1155. 掷骰子等于目标和的方法数](https://leetcode.cn/problems/number-of-dice-rolls-with-target-sum/) - 标签:动态规划 - 难度:中等 ## 题目链接 - [1155. 掷骰子等于目标和的方法数 - 力扣](https://leetcode.cn/problems/number-of-dice-rolls-with-target-sum/) ## 题目大意 **描述**:有 $n$ 个一样的骰子,每个骰子上都有 $k$ 个面,分别标号为 $1 \sim k$。现在给定三个整数 $n$、$k$ 和 $target$,滚动 $n$ 个骰子。 **要求**:计算出使所有骰子正面朝上的数字和等于 $target$ 的方案数量。 **说明**: - $1 \le n, k \le 30$。 - $1 \le target \le 1000$。 **示例**: - 示例 1: ```python 输入:n = 1, k = 6, target = 3 输出:1 解释:你扔一个有 6 个面的骰子。 得到 3 的和只有一种方法。 ``` - 示例 2: ```python 输入:n = 2, k = 6, target = 7 输出:6 解释:你扔两个骰子,每个骰子有 6 个面。 得到 7 的和有 6 种方法 1+6 2+5 3+4 4+3 5+2 6+1。 ``` ## 解题思路 ### 思路 1:动态规划 我们可以将这道题转换为「分组背包问题」中求方案总数的问题。将每个骰子看做是一组物品,骰子每一个面上的数值当做是每组物品中的一个物品。这样问题就转换为:用 $n$ 个骰子($n$ 组物品)进行投掷,投掷出总和(总价值)为 $target$ 的方案数。 ###### 1. 阶段划分 按照总价值 $target$ 进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[w]$ 表示为:用 $n$ 个骰子($n$ 组物品)进行投掷,投掷出总和(总价值)为 $w$ 的方案数。 ###### 3. 状态转移方程 用 $n$ 个骰子($n$ 组物品)进行投掷,投掷出总和(总价值)为 $w$ 的方案数,等于用 $n$ 个骰子($n$ 组物品)进行投掷,投掷出总和(总价值)为 $w - d$ 的方案数累积值,其中 $d$ 为当前骰子掷出的价值,即:$dp[w] = dp[w] + dp[w - d]$。 ###### 4. 初始条件 - 用 $n$ 个骰子($n$ 组物品)进行投掷,投掷出总和(总价值)为 $0$ 的方案数为 $1$。 ###### 5. 最终结果 根据我们之前定义的状态, $dp[w]$ 表示为:用 $n$ 个骰子($n$ 组物品)进行投掷,投掷出总和(总价值)为 $w$ 的方案数。则最终结果为 $dp[target]$。 ### 思路 1:代码 ```python class Solution: def numRollsToTarget(self, n: int, k: int, target: int) -> int: dp = [0 for _ in range(target + 1)] dp[0] = 1 MOD = 10 ** 9 + 7 # 枚举前 i 组物品 for i in range(1, n + 1): # 逆序枚举背包装载重量 for w in range(target, -1, -1): dp[w] = 0 # 枚举第 i - 1 组物品能取个数 for d in range(1, k + 1): if w >= d: dp[w] = (dp[w] + dp[w - d]) % MOD return dp[target] % MOD ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times m \times target)$。 - **空间复杂度**:$O(target)$。 ================================================ FILE: docs/solutions/1100-1199/parallel-courses.md ================================================ # [1136. 并行课程](https://leetcode.cn/problems/parallel-courses/) - 标签:图、拓扑排序 - 难度:中等 ## 题目链接 - [1136. 并行课程 - 力扣](https://leetcode.cn/problems/parallel-courses/) ## 题目大意 有 N 门课程,分别以 1 到 N 进行编号。现在给定一份课程关系表 `relations[i] = [X, Y]`,用以表示课程 `X` 和课程 `Y` 之间的先修关系:课程 `X` 必须在课程 `Y` 之前修完。假设在一个学期里,你可以学习任何数量的课程,但前提是你已经学习了将要学习的这些课程的所有先修课程。 要求:返回学完全部课程所需的最少学期数。如果没有办法做到学完全部这些课程的话,就返回 `-1`。 ## 解题思路 拓扑排序。具体解法如下: 1. 使用列表 `edges` 存放课程关系图,并统计每门课程节点的入度,存入入度列表 `indegrees`。使用 `ans` 表示学期数。 2. 借助队列 `queue`,将所有入度为 `0` 的节点入队。 3. 将队列中所有节点依次取出,学期数 +1。对于取出的每个节点: 1. 对应课程数 -1。 2. 将该顶点以及该顶点为出发点的所有边的另一个节点入度 -1。如果入度 -1 后的节点入度不为 0,则将其加入队列 `queue`。 4. 重复 3~4 的步骤,直到队列中没有节点。 5. 最后判断剩余课程数是否为 0,如果为 0,则返回 `ans`,否则,返回 `-1`。 ## 代码 ```python import collections class Solution: def minimumSemesters(self, n: int, relations: List[List[int]]) -> int: indegrees = [0 for _ in range(n + 1)] edges = collections.defaultdict(list) for x, y in relations: edges[x].append(y) indegrees[y] += 1 queue = collections.deque([]) for i in range(1, n + 1): if not indegrees[i]: queue.append(i) ans = 0 while queue: size = len(queue) for i in range(size): x = queue.popleft() n -= 1 for y in edges[x]: indegrees[y] -= 1 if not indegrees[y]: queue.append(y) ans += 1 return ans if n == 0 else -1 ``` ================================================ FILE: docs/solutions/1100-1199/relative-sort-array.md ================================================ # [1122. 数组的相对排序](https://leetcode.cn/problems/relative-sort-array/) - 标签:数组、哈希表、计数排序、排序 - 难度:简单 ## 题目链接 - [1122. 数组的相对排序 - 力扣](https://leetcode.cn/problems/relative-sort-array/) ## 题目大意 **描述**:给定两个数组,$arr1$ 和 $arr2$,其中 $arr2$ 中的元素各不相同,$arr2$ 中的每个元素都出现在 $arr1$ 中。 **要求**:对 $arr1$ 中的元素进行排序,使 $arr1$ 中项的相对顺序和 $arr2$ 中的相对顺序相同。未在 $arr2$ 中出现过的元素需要按照升序放在 $arr1$ 的末尾。 **说明**: - $1 \le arr1.length, arr2.length \le 1000$。 - $0 \le arr1[i], arr2[i] \le 1000$。 **示例**: - 示例 1: ```python 输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6] 输出:[2,2,2,1,4,3,3,9,6,7,19] ``` - 示例 2: ```python 输入:arr1 = [28,6,22,8,44,17], arr2 = [22,28,8,6] 输出:[22,28,8,6,17,44] ``` ## 解题思路 ### 思路 1:计数排序 因为元素值范围在 $[0, 1000]$,所以可以使用计数排序的思路来解题。 1. 使用数组 $count$ 统计 $arr1$ 各个元素个数。 2. 遍历 $arr2$ 数组,将对应元素$num2$ 按照个数 $count[num2]$ 添加到答案数组 $ans$ 中,同时在 $count$ 数组中减去对应个数。 3. 然后在处理 $count$ 中剩余元素,将 $count$ 中大于 $0$ 的元素下标依次添加到答案数组 $ans$ 中。 4. 最后返回答案数组 $ans$。 ### 思路 1:代码 ```python class Solution: def relativeSortArray(self, arr1: List[int], arr2: List[int]) -> List[int]: # 计算待排序序列中最大值元素 arr_max 和最小值元素 arr_min arr1_min, arr1_max = min(arr1), max(arr1) # 定义计数数组 counts,大小为 最大值元素 - 最小值元素 + 1 size = arr1_max - arr1_min + 1 counts = [0 for _ in range(size)] # 统计值为 num 的元素出现的次数 for num in arr1: counts[num - arr1_min] += 1 res = [] for num in arr2: while counts[num - arr1_min] > 0: res.append(num) counts[num - arr1_min] -= 1 for i in range(size): while counts[i] > 0: num = i + arr1_min res.append(num) counts[i] -= 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n + max(arr_1))$。其中 $m$ 是数组 $arr_1$ 的长度,$n$ 是数组 $arr_2$ 的长度,$max(arr_1)$ 是数组 $arr_1$ 的最大值。 - **空间复杂度**:$O(max(arr_1))$。 ================================================ FILE: docs/solutions/1200-1299/airplane-seat-assignment-probability.md ================================================ # [1227. 飞机座位分配概率](https://leetcode.cn/problems/airplane-seat-assignment-probability/) - 标签:脑筋急转弯、数学、动态规划、概率与统计 - 难度:中等 ## 题目链接 - [1227. 飞机座位分配概率 - 力扣](https://leetcode.cn/problems/airplane-seat-assignment-probability/) ## 题目大意 **描述**:给定一个整数 $n$,代表 $n$ 位乘客即将登飞机。飞机上刚好有 $n$ 个座位。第一位乘客的票丢了,他随便选择了一个座位坐下。则剩下的乘客将会: - 如果自己的座位还空着,就坐到自己的座位上。 - 如果自己的座位被占用了,就随机选择其他座位。 **要求**:计算出第 $n$ 位乘客坐在自己座位上的概率是多少。 **说明**: - $1 \le n \le 10^5$。 **示例**: - 示例 1: ```python 输入:n = 1 输出:1.00000 解释:第一个人只会坐在自己的位置上。 ``` - 示例 2: ```python 输入: n = 2 输出: 0.50000 解释:在第一个人选好座位坐下后,第二个人坐在自己的座位上的概率是 0.5。 ``` ## 解题思路 ### 思路 1:数学 我们按照乘客的登机顺序为乘客编下号:$1 \sim n$,我们用 $f(n)$ 来表示第 $n$ 位乘客登机时,坐在自己座位上的概率。先从简单的情况开始考虑: 当 $n = 1$ 时: - 第 $1$ 位乘客只能坐在第 $1$ 个座位上,$f(1) = 1$。 当 $n = 2$ 时: - 第 $1$ 位乘客有 $\frac{1}{2}$ 的概率选中自己的位置,第 $2$ 位乘客一定能坐到自己的位置上,则第 $2$ 位乘客坐在自己座位上的概率为 $\frac{1}{2} * 1.0$。 - 第 $1$ 位乘客有 $\frac{1}{2}$ 的概率坐在第 $2$ 位乘客的位置上,第 $2$ 位乘客只能坐到第 $1$ 位乘客的位置上,那么第 $2$ 位乘客坐在自己座位上的概率为 $\frac{1}{2} * 0.0$。 - 综上,$f(2) = \frac{1}{2} * 1.0 + \frac{1}{2} * 0.0 = 0.5$。 当 $n \ge 3$ 时: - 先来考虑第 $1$ 位乘客登机情况: - 第 $1$ 位乘客有 $\frac{1}{n}$ 的概率选择坐在自己位置上,这样第 $1$ 位到第 $n - 1$ 位乘客的座位都不会被占,第 n 位乘客一定能坐到自己位置上。那么第 n 位乘客坐在自己座位上的概率为 $\frac{1}{n} * 1.0$。 - 第 $1$ 位乘客有 $\frac{1}{n}$ 的概率选择坐在第 $n$ 位乘客的位置上,这样第 $2$ 位到第 $n - 1$ 位乘客的座位都不会被占,第 $n$ 位乘客只能坐到第 $1$ 位乘客的位置上,那么第 $n$ 位乘客坐在自己座位上的概率为 $\frac{1}{n} * 0.0$。 - 第 $1$ 位乘客有 $\frac{n-2}{n}$ 的概率坐在第 $i$ 号座位上,$2 \le i \le n - 1$,每个座位被选中概率为 $\frac{1}{n}$。这样第 $2$ 位到第 $i - 1$ 位乘客的座位都不会被占。此时第 $i$ 位乘客,会在剩下的 $n - (i - 1)$ 个座位中进行选择: - 坐在第 $1$ 位乘客的位置上,这样后面的乘客座位都不会被占,第 $n$ 位乘客一定能坐到自己位置上。 - 坐在第 $n$ 个乘客的位置上,这样第 $n$ 个乘客肯定无法坐到自己的位置上。 - 在第 $[i + 1, n - 1]$ 之间找个位置坐。 - 再来考虑第 $i$ 位乘客登机情况: - 第 $i$ 为乘客所面临的情况跟第 $1$ 位乘客所面临的情况类似,只不过问题的规模数从 $n$ 减小到了 $n - (i - 1)$。 那么综合上面情况,可以得到 $f(n),(n \ge 3)$ 的递推式: $\begin{aligned} f(n) & = \frac{1}{n} * 1.0 + \frac{1}{n} * 0.0 + \frac{1}{n} * \sum_{i = 2}^{n-1} f(n - i + 1) \cr & = \frac{1}{n} (1.0 + \sum_{i = 2}^{n-1} f(n - i + 1)) \end{aligned}$ 接下来我们从等式中寻找规律,消去 $\sum_{i = 2}^{n-1} f(n - i + 1)$ 部分。 将 $n$ 换为 $n - 1$,得: $\begin{aligned} f(n - 1) & = \frac{1}{n - 1} * 1.0 + \frac{1}{n - 1} * 0.0 + \frac{1}{n - 1} * \sum_{i = 2}^{n-2} f(n - i) \cr & = \frac{1}{n - 1} (1.0 + \sum_{i = 2}^{n-2} f(n - i)) \end{aligned} $ 将 $f(n) * n$ 与 $f(n - 1) * (n - 1)$ 进行比较: $\begin{aligned} f(n) * n & = 1.0 + \sum_{i = 2}^{n-1} f(n - i + 1) & (1) \cr f(n - 1) * (n - 1) & = 1.0 + \sum_{i = 2}^{n-2} f(n - i) & (2) \end{aligned}$ 将上述 (1)、(2) 式相减得: $\begin{aligned} & f(n) * n - f(n - 1) * (n - 1) & \cr = & \sum_{i = 2}^{n-1} f(n - i + 1) - \sum_{i = 2}^{n-2} f(n - i) \cr = & f(n-1) \end{aligned}$ 整理后得:$f(n) = f(n - 1)$。 已知 $f(1) = 1$,$f(2) = 0.5$,因此当 $n \ge 3$ 时,$f(n) = 0.5$。 所以可以得出结论: $f(n) = \begin{cases} 1.0 & n = 1 \cr 0.5 & n \ge 2 \end{cases}$ ### 思路 1:代码 ```python class Solution: def nthPersonGetsNthSeat(self, n: int) -> float: if n == 1: return 1.0 else: return 0.5 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。 - **空间复杂度**:$O(1)$。 ## 参考资料 - [飞机座位分配概率 - 力扣(LeetCode)](https://leetcode.cn/problems/airplane-seat-assignment-probability/solution/fei-ji-zuo-wei-fen-pei-gai-lu-by-leetcod-gyw4/) ================================================ FILE: docs/solutions/1200-1299/check-if-it-is-a-straight-line.md ================================================ # [1232. 缀点成线](https://leetcode.cn/problems/check-if-it-is-a-straight-line/) - 标签:几何、数组、数学 - 难度:简单 ## 题目链接 - [1232. 缀点成线 - 力扣](https://leetcode.cn/problems/check-if-it-is-a-straight-line/) ## 题目大意 给定一系列的二维坐标点的坐标 `(xi, yi)`,判断这些点是否属于同一条直线。如果属于同一条直线,则返回 True,否则返回 False。 ## 解题思路 如果根据斜率来判断点是否处于同一条直线,需要处理斜率不存在(无穷大)的情况。我们可以使用叉乘来判断三个点构成的两个向量是否处于同一条直线上。 叉乘原理: 设向量 P 为 `(x1, y1)` 向量,Q 为 `(x2, y2)`,则向量 P、Q 的叉积定义为:$P × Q = x_1y_2 - x_2y_1$,其几何意义表示为如果以向量 P 和向量 Q 为边构成一个平行四边形,那么这两个向量叉乘的模长与这个平行四边形的正面积相等。 ![向量叉积](https://img.geek-docs.com/mathematical-basis/linear-algebra/220px-Cross_product_parallelogram.png) - 如果 `P × Q = 0`,则 P 与 Q 共线,有可能同向,也有可能反向。 - 如果 `P × Q > 0`,则 P 在 Q 的顺时针方向。 - 如果 `P × Q < 0`,则 P 在 Q 的逆时针方向。 具体求解方法: - 先求出第一个坐标与第二个坐标构成的向量 P。 - 遍历所有坐标,求出所有坐标与第一个坐标构成的向量 Q。 - 如果 `P × Q ≠ 0`,则返回 False。 - 如果遍历完仍没有发现 `P × Q ≠ 0`,则返回 True。 ## 代码 ```python class Solution: def checkStraightLine(self, coordinates: List[List[int]]) -> bool: x1 = coordinates[1][0] - coordinates[0][0] y1 = coordinates[1][1] - coordinates[0][1] for i in range(len(coordinates)): x2 = coordinates[i][0] - coordinates[0][0] y2 = coordinates[i][1] - coordinates[0][1] if x1 * y2 != x2 * y1: return False return True ``` ================================================ FILE: docs/solutions/1200-1299/count-vowels-permutation.md ================================================ # [1220. 统计元音字母序列的数目](https://leetcode.cn/problems/count-vowels-permutation/) - 标签:动态规划 - 难度:困难 ## 题目链接 - [1220. 统计元音字母序列的数目 - 力扣](https://leetcode.cn/problems/count-vowels-permutation/) ## 题目大意 **描述**:给定一个整数 `n`,我们可以按照以下规则生成长度为 `n` 的字符串: - 字符串中的每个字符都应当是小写元音字母(`'a'`、`'e'`、`'i'`、`'o'`、`'u'`)。 - 每个元音 `'a'` 后面都只能跟着 `'e'`。 - 每个元音 `'e'` 后面只能跟着 `'a'` 或者是 `'i'`。 - 每个元音 `'i'` 后面不能再跟着另一个 `'i'`。 - 每个元音 `'o'` 后面只能跟着 `'i'` 或者是 `'u'`。 - 每个元音 `'u'` 后面只能跟着 `'a'`。 **要求**:统计一下我们可以按上述规则形成多少个长度为 `n` 的字符串。由于答案可能会很大,所以请返回模 $10^9 + 7$ 之后的结果。 **说明**: - $1 \le n \le 2 * 10^4$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:10 解释:所有可能的字符串分别是:"ae", "ea", "ei", "ia", "ie", "io", "iu", "oi", "ou" 和 "ua"。 ``` ## 解题思路 ### 思路 1:动态规划 根据题目给定的字符串规则,我们可以将其整理一下: - 元音字母 `'a'` 前面只能跟着 `'e'`、`'i'`、`'u'`。 - 元音字母 `'e'` 前面只能跟着 `'a'`、`'i'`。 - 元音字母 `'i'` 前面只能跟着 `'e'`、`'o'`。 - 元音字母 `'o'` 前面只能跟着 `'i'`。 - 元音字母 `'u'` 前面只能跟着 `'o'`、`'i'`。 现在我们可以按照字符串的长度以及字符结尾进行阶段划分,并按照上述规则推导状态转移方程。 ###### 1. 阶段划分 按照字符串的结尾位置和结尾位置上的字符进行阶段划分。 ###### 2. 定义状态 定义状态 `dp[i][j]` 表示为:长度为 `i` 并且以字符 `j` 结尾的字符串数量。这里 $j = 0, 1, 2, 3, 4$ 分别代表元音字母 `'a'`、`'e'`、`'i'`、`'o'`、`'u'`。 ###### 3. 状态转移方程 通过上面的字符规则,可以得到状态转移方程为: $\begin{cases} dp[i][0] = dp[i - 1][1] + dp[i - 1][2] + dp[i - 1][4] \cr dp[i][1] = dp[i - 1][0] + dp[i - 1][2] \cr dp[i][2] = dp[i - 1][1] + dp[i - 1][3] \cr dp[i][3] = dp[i - 1][2] \cr dp[i][4] = dp[i - 1][2] + dp[i - 1][3] \end{cases}$ ###### 4. 初始条件 - 长度为 `1` 并且以字符 `j` 结尾的字符串数量为 `1`,即 `dp[1][j] = 1`。 ###### 5. 最终结果 根据我们之前定义的状态,`dp[i]` 表示为:长度为 `i` 并且以字符 `j` 结尾的字符串数量。则将 `dp[n]` 行所有列相加,就是长度为 `n` 的字符串数量。 ### 思路 1:动态规划代码 ```python class Solution: def countVowelPermutation(self, n: int) -> int: mod = 10 ** 9 + 7 dp = [[0 for _ in range(5)] for _ in range(n + 1)] for j in range(5): dp[1][j] = 1 for i in range(2, n + 1): dp[i][0] = (dp[i - 1][1] + dp[i - 1][2] + dp[i - 1][4]) % mod dp[i][1] = (dp[i - 1][0] + dp[i - 1][2]) % mod dp[i][2] = (dp[i - 1][1] + dp[i - 1][3]) % mod dp[i][3] = dp[i - 1][2] % mod dp[i][4] = (dp[i - 1][2] + dp[i - 1][3]) % mod ans = 0 for j in range(5): ans += dp[n][j] % mod ans %= mod return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1200-1299/divide-array-in-sets-of-k-consecutive-numbers.md ================================================ # [1296. 划分数组为连续数字的集合](https://leetcode.cn/problems/divide-array-in-sets-of-k-consecutive-numbers/) - 标签:贪心、数组、哈希表、排序 - 难度:中等 ## 题目链接 - [1296. 划分数组为连续数字的集合 - 力扣](https://leetcode.cn/problems/divide-array-in-sets-of-k-consecutive-numbers/) ## 题目大意 **描述**:给定一个整数数组 `nums` 和一个正整数 `k`。 **要求**:判断是否可以把这个数组划分成一些由 `k` 个连续数字组成的集合。如果可以,则返回 `True`;否则,返回 `False`。 **说明**: - $1 \le k \le nums.length \le 10^5$。 - $1 \le nums[i] \le 10^9$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,3,4,4,5,6], k = 4 输出:True 解释:数组可以分成 [1,2,3,4] 和 [3,4,5,6]。 ``` ## 解题思路 ### 思路 1:哈希表 + 排序 1. 使用哈希表存储每个数出现的次数。 2. 将哈希表中每个键从小到大排序。 3. 从哈希表中最小的数开始,以它作为当前连续数字的开头,然后依次判断连续的 `k` 个数是否在哈希表中,如果在的话,则将哈希表中对应数的数量减 `1`。不在的话,说明无法满足题目要求,直接返回 `False`。 4. 重复执行 2 ~ 3 步,直到哈希表为空。最后返回 `True`。 ### 思路 1:哈希表 + 排序代码 ```python class Solution: def isPossibleDivide(self, nums: List[int], k: int) -> bool: hand_map = collections.defaultdict(int) for i in range(len(nums)): hand_map[nums[i]] += 1 for key in sorted(hand_map.keys()): value = hand_map[key] if value == 0: continue count = 0 for i in range(k): hand_map[key + count] -= value if hand_map[key + count] < 0: return False count += 1 return True ``` ================================================ FILE: docs/solutions/1200-1299/find-elements-in-a-contaminated-binary-tree.md ================================================ # [1261. 在受污染的二叉树中查找元素](https://leetcode.cn/problems/find-elements-in-a-contaminated-binary-tree/) - 标签:树、深度优先搜索、广度优先搜索、设计、哈希表、二叉树 - 难度:中等 ## 题目链接 - [1261. 在受污染的二叉树中查找元素 - 力扣](https://leetcode.cn/problems/find-elements-in-a-contaminated-binary-tree/) ## 题目大意 **描述**:给出一满足下属规则的二叉树的根节点 $root$: 1. $root.val == 0$。 2. 如果 $node.val == x$ 且 $node.left \ne None$,那么 $node.left.val == 2 \times x + 1$。 3. 如果 $node.val == x$ 且 $node.right \ne None$,那么 $node.left.val == 2 \times x + 2$​。 现在这个二叉树受到「污染」,所有的 $node.val$ 都变成了 $-1$。 **要求**:请你先还原二叉树,然后实现 `FindElements` 类: - `FindElements(TreeNode* root)` 用受污染的二叉树初始化对象,你需要先把它还原。 - `bool find(int target)` 判断目标值 $target$ 是否存在于还原后的二叉树中并返回结果。 **说明**: - $node.val == -1$ - 二叉树的高度不超过 $20$。 - 节点的总数在 $[1, 10^4]$ 之间。 - 调用 `find()` 的总次数在 $[1, 10^4]$ 之间。 - $0 \le target \le 10^6$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/11/16/untitled-diagram-4-1.jpg) ```python 输入: ["FindElements","find","find"] [[[-1,null,-1]],[1],[2]] 输出: [null,false,true] 解释: FindElements findElements = new FindElements([-1,null,-1]); findElements.find(1); // return False findElements.find(2); // return True ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/11/16/untitled-diagram-4.jpg) ```python 输入: ["FindElements","find","find","find"] [[[-1,-1,-1,-1,-1]],[1],[3],[5]] 输出: [null,true,true,false] 解释: FindElements findElements = new FindElements([-1,-1,-1,-1,-1]); findElements.find(1); // return True findElements.find(3); // return True findElements.find(5); // return False ``` ## 解题思路 ### 思路 1:哈希表 + 深度优先搜索 1. 从根节点开始进行还原。 2. 然后使用深度优先搜索的方式,依次递归还原左右两个孩子节点。 3. 递归还原的同时,将还原之后的所有节点值,存入集合 $val\_set$ 中。 这样就可以在 $O(1)$ 的时间复杂度内判断目标值 $target$ 是否在还原后的二叉树中了。 ### 思路 1:代码 ```Python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class FindElements: def __init__(self, root: Optional[TreeNode]): self.val_set = set() def dfs(node, val): if not node: return self.val_set.add(val) dfs(node.left, val * 2 + 1) dfs(node.right, val * 2 + 2) dfs(root, 0) def find(self, target: int) -> bool: return target in self.val_set # Your FindElements object will be instantiated and called as such: # obj = FindElements(root) # param_1 = obj.find(target) ``` ### 思路 1:复杂度分析 - **时间复杂度**:还原二叉树:$O(n)$,其中 $n$ 为二叉树中的节点个数。查找目标值:$O(1)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1200-1299/get-equal-substrings-within-budget.md ================================================ # [1208. 尽可能使字符串相等](https://leetcode.cn/problems/get-equal-substrings-within-budget/) - 标签:字符串、二分查找、前缀和、滑动窗口 - 难度:中等 ## 题目链接 - [1208. 尽可能使字符串相等 - 力扣](https://leetcode.cn/problems/get-equal-substrings-within-budget/) ## 题目大意 **描述**:给定两个长度相同的字符串,$s$ 和 $t$。将 $s$ 中的第 $i$ 个字符变到 $t$ 中的第 $i$ 个字符需要 $| s[i] - t[i] |$ 的开销(开销可能为 $0$),也就是两个字符的 ASCII 码值的差的绝对值。用于变更字符串的最大预算是 $maxCost$。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。 **要求**:如果你可以将 $s$ 的子字符串转化为它在 $t$ 中对应的子字符串,则返回可以转化的最大长度。如果 $s$ 中没有子字符串可以转化成 $t$ 中对应的子字符串,则返回 $0$。 **说明**: - $1 \le s.length, t.length \le 10^5$。 - $0 \le maxCost \le 10^6$。 - $s$ 和 $t$ 都只含小写英文字母。 **示例**: - 示例 1: ```python 输入:s = "abcd", t = "bcdf", maxCost = 3 输出:3 解释:s 中的 "abc" 可以变为 "bcd"。开销为 3,所以最大长度为 3。 ``` - 示例 2: ```python 输入:s = "abcd", t = "cdef", maxCost = 3 输出:1 解释:s 中的任一字符要想变成 t 中对应的字符,其开销都是 2。因此,最大长度为 1。 ``` ## 解题思路 ### 思路 1:滑动窗口 维护一个滑动窗口 $window\_sum$ 用于记录窗口内的开销总和,保证窗口内的开销总和小于等于 $maxCost$。使用 $ans$ 记录可以转化的最大长度。具体做法如下: 使用两个指针 $left$、$right$。分别指向滑动窗口的左右边界,保证窗口内所有元素转化开销总和小于等于 $maxCost$。 - 先统计出 $s$ 中第 $i$ 个字符变为 $t$ 的第 $i$ 个字符的开销,用数组 $costs$ 保存。 - 一开始,$left$、$right$ 都指向 $0$。 - 将最右侧字符的转变开销填入窗口中,向右移动 $right$。 - 直到窗口内开销总和 $window\_sum$ 大于 $maxCost$。则不断右移 $left$,缩小窗口长度。直到 $window\_sum \le maxCost$ 时,更新可以转换的最大长度 $ans$。 - 向右移动 $right$,直到 $right \ge len(s)$ 为止。 - 输出答案 $ans$。 ### 思路 1:代码 ```python class Solution: def equalSubstring(self, s: str, t: str, maxCost: int) -> int: size = len(s) costs = [0 for _ in range(size)] for i in range(size): costs[i] = abs(ord(s[i]) - ord(t[i])) left, right = 0, 0 ans = 0 window_sum = 0 while right < size: window_sum += costs[right] while window_sum > maxCost: window_sum -= costs[left] left += 1 ans = max(ans, right - left + 1) right += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**: - **空间复杂度**: ================================================ FILE: docs/solutions/1200-1299/index.md ================================================ ## 本章内容 - [1202. 交换字符串中的元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/smallest-string-with-swaps.md) - [1208. 尽可能使字符串相等](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/get-equal-substrings-within-budget.md) - [1217. 玩筹码](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/minimum-cost-to-move-chips-to-the-same-position.md) - [1220. 统计元音字母序列的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/count-vowels-permutation.md) - [1227. 飞机座位分配概率](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/airplane-seat-assignment-probability.md) - [1229. 安排会议日程](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/meeting-scheduler.md) - [1232. 缀点成线](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/check-if-it-is-a-straight-line.md) - [1245. 树的直径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/tree-diameter.md) - [1247. 交换字符使得字符串相同](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/minimum-swaps-to-make-strings-equal.md) - [1253. 重构 2 行二进制矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/reconstruct-a-2-row-binary-matrix.md) - [1254. 统计封闭岛屿的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/number-of-closed-islands.md) - [1261. 在受污染的二叉树中查找元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/find-elements-in-a-contaminated-binary-tree.md) - [1266. 访问所有点的最小时间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/minimum-time-visiting-all-points.md) - [1268. 搜索推荐系统](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/search-suggestions-system.md) - [1281. 整数的各位积和之差](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/subtract-the-product-and-sum-of-digits-of-an-integer.md) - [1296. 划分数组为连续数字的集合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/divide-array-in-sets-of-k-consecutive-numbers.md) ================================================ FILE: docs/solutions/1200-1299/meeting-scheduler.md ================================================ # [1229. 安排会议日程](https://leetcode.cn/problems/meeting-scheduler/) - 标签:数组、双指针、排序 - 难度:中等 ## 题目链接 - [1229. 安排会议日程 - 力扣](https://leetcode.cn/problems/meeting-scheduler/) ## 题目大意 **描述**:给定两位客户的空闲时间表:$slots1$ 和 $slots2$,再给定会议的预计持续时间 $duration$。 其中 $slots1[i] = [start_i, end_i]$ 表示空闲时间第从 $start_i$ 开始,到 $end_i$ 结束。$slots2$ 也是如此。 **要求**:为他们安排合适的会议时间,如果有合适的会议时间,则返回该时间的起止时刻。如果没有满足要求的会议时间,就请返回一个 空数组。 **说明**: - **会议时间**:两位客户都有空参加,并且持续时间能够满足预计时间 $duration$ 的最早的时间间隔。 - 题目保证数据有效。同一个人的空闲时间不会出现交叠的情况,也就是说,对于同一个人的两个空闲时间 $[start1, end1]$ 和 $[start2, end2]$,要么 $start1 > end2$,要么 $start2 > end1$。 - $1 \le slots1.length, slots2.length \le 10^4$。 - $slots1[i].length, slots2[i].length == 2$。 - $slots1[i][0] < slots1[i][1]$。 - $slots2[i][0] < slots2[i][1]$。 - $0 \le slots1[i][j], slots2[i][j] \le 10^9$。 - $1 \le duration \le 10^6$。 **示例**: - 示例 1: ```python 输入:slots1 = [[10,50],[60,120],[140,210]], slots2 = [[0,15],[60,70]], duration = 8 输出:[60,68] ``` - 示例 2: ```python 输入:slots1 = [[10,50],[60,120],[140,210]], slots2 = [[0,15],[60,70]], duration = 12 输出:[] ``` ## 解题思路 ### 思路 1:分离双指针 题目保证了同一个人的空闲时间不会出现交叠。那么可以先直接对两个客户的空间时间表按照开始时间从小到大排序。然后使用分离双指针来遍历两个数组,求出重合部分,并判断重合区间是否大于等于 $duration$。具体做法如下: 1. 先对两个数组排序。 2. 然后使用两个指针 $left\_1$、$left\_2$。$left\_1$ 指向第一个数组开始位置,$left\_2$ 指向第二个数组开始位置。 3. 遍历两个数组。计算当前两个空闲时间区间的重叠范围。 1. 如果重叠范围大于等于 $duration$,直接返回当前重叠范围开始时间和会议结束时间,即 $[start, start + duration]$,$start$ 为重叠范围开始时间。 2. 如果第一个客户的空闲结束时间小于第二个客户的空闲结束时间,则令 $left\_1$ 右移,即 `left_1 += 1`,继续比较重叠范围。 3. 如果第一个客户的空闲结束时间大于等于第二个客户的空闲结束时间,则令 $left\_2$ 右移,即 `left_2 += 1`,继续比较重叠范围。 4. 直到 $left\_1 == len(slots1)$ 或者 $left\_2 == len(slots2)$ 时跳出循环,返回空数组 $[]$。 ### 思路 1:代码 ```python class Solution: def minAvailableDuration(self, slots1: List[List[int]], slots2: List[List[int]], duration: int) -> List[int]: slots1.sort() slots2.sort() size1 = len(slots1) size2 = len(slots2) left_1, left_2 = 0, 0 while left_1 < size1 and left_2 < size2: start_1, end_1 = slots1[left_1] start_2, end_2 = slots2[left_2] start = max(start_1, start_2) end = min(end_1, end_2) if end - start >= duration: return [start, start + duration] if end_1 < end_2: left_1 += 1 else: left_2 += 1 return [] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n + m \times \log m)$,其中 $n$、$m$ 分别为数组 $slots1$、$slots2$ 中的元素个数。 - **空间复杂度**:$O(\log n + \log m)$。 ================================================ FILE: docs/solutions/1200-1299/minimum-cost-to-move-chips-to-the-same-position.md ================================================ # [1217. 玩筹码](https://leetcode.cn/problems/minimum-cost-to-move-chips-to-the-same-position/) - 标签:贪心、数组、数学 - 难度:简单 ## 题目链接 - [1217. 玩筹码 - 力扣](https://leetcode.cn/problems/minimum-cost-to-move-chips-to-the-same-position/) ## 题目大意 **描述**:给定一个数组 $position$ 代表 $n$ 个筹码的位置,其中 $position[i]$ 代表第 $i$ 个筹码的位置。现在需要把所有筹码移到同一个位置。在一步中,我们可以将第 $i$ 个芯片的位置从 $position[i]$ 改变为: - $position[i] + 2$ 或 $position[i] - 2$,此时 $cost = 0$; - $position[i] + 1$ 或 $position[i] - 1$,此时 $cost = 1$。 即移动偶数位长度的代价为 $0$,移动奇数位长度的代价为 $1$。 **要求**:返回将所有筹码移动到同一位置上所需要的 最小代价 。 **说明**: - $1 \le chips.length \le 100$。 - $1 \le chips[i] \le 10^9$。 **示例**: - 示例 1: ```python 输入:position = [2,2,2,3,3] 输出:2 解释:我们可以把位置3的两个芯片移到位置 2。每一步的成本为 1。总成本 = 2。 ``` ## 解题思路 ### 思路 1:贪心算法 题目中移动偶数位长度是不需要代价的,所以奇数位移动到奇数位不需要代价,偶数位移动到偶数位也不需要代价。 则我们可以想将所有偶数位都移动到下标为 $0$ 的位置,奇数位都移动到下标为 $1$ 的位置。 这样,所有的奇数位、偶数位上的人都到相同或相邻位置了。 我们只需要统计一下奇数位和偶数位的数字个数。将少的数移动到多的数上边就是最小代价。 则这道题就可以通过以下步骤求解: - 遍历数组,统计数组中奇数个数和偶数个数。 - 返回奇数个数和偶数个数中较小的数即为答案。 ### 思路 1:贪心算法代码 ```python class Solution: def minCostToMoveChips(self, position: List[int]) -> int: odd, even = 0, 0 for p in position: if p & 1: odd += 1 else: even += 1 return min(odd, even) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $poition$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1200-1299/minimum-swaps-to-make-strings-equal.md ================================================ # [1247. 交换字符使得字符串相同](https://leetcode.cn/problems/minimum-swaps-to-make-strings-equal/) - 标签:贪心、数学、字符串 - 难度:中等 ## 题目链接 - [1247. 交换字符使得字符串相同 - 力扣](https://leetcode.cn/problems/minimum-swaps-to-make-strings-equal/) ## 题目大意 **描述**:给定两个长度相同的字符串 $s1$ 和 $s2$,并且两个字符串中只含有字符 `'x'` 和 `'y'`。现在需要通过「交换字符」的方式使两个字符串相同。 - 每次「交换字符」,需要分别从两个字符串中各选一个字符进行交换。 - 「交换字符」只能发生在两个不同的字符串之间,不能发生在同一个字符串内部。 **要求**:返回使 $s1$ 和 $s2$ 相同的最小交换次数,如果没有方法能够使得这两个字符串相同,则返回 $-1$。 **说明**: - $1 \le s1.length, s2.length \le 1000$。 - $s1$、$s2$ 只包含 `'x'` 或 `'y'`。 **示例**: - 示例 1: ```python 输入:s1 = "xy", s2 = "yx" 输出:2 解释: 交换 s1[0] 和 s2[0],得到 s1 = "yy",s2 = "xx" 。 交换 s1[0] 和 s2[1],得到 s1 = "xy",s2 = "xy" 。 注意,你不能交换 s1[0] 和 s1[1] 使得 s1 变成 "yx",因为我们只能交换属于两个不同字符串的字符。 ``` ## 解题思路 ### 思路 1:贪心算法 - 如果 $s1 == s2$,则不需要交换。 - 如果 `s1 = "xx"`,`s2 = "yy"`,则最少需要交换一次,才可以使两个字符串相等。 - 如果 `s1 = "yy"`,`s2 = "xx"`,则最少需要交换一次,才可以使两个字符串相等。 - 如果 `s1 = "xy"`,`s2 = "yx"`,则最少需要交换两次,才可以使两个字符串相等。 - 如果 `s1 = "yx"`,`s2 = "xy"`,则最少需要交换两次,才可以使两个字符串相等。 则可以总结为: - `"xx"` 与 `"yy"`、`"yy"` 与 `"xx"` 只需要交换一次。 - `"xy"` 与 `"yx"`、`"yx"` 与 `"xy"` 需要交换两次。 我们把这两种情况分别进行统计。 - 当遇到 $s1[i] == s2[i]$ 时直接跳过。 - 当遇到 $s1[i] \ne s2[i]$ 时: - 如果 `s1[i] == 'x'`,`s2[i] == 'y'`,则统计数量到变量 $xyCnt$ 中。 - 如果 `s1[i] == 'y'`,`s2[i] == 'y'`,则统计数量到变量 $yxCnt$ 中。 则最后我们只需要判断 $xyCnt$ 和 $yxCnt$ 的个数即可。 - 如果 $xyCnt + yxCnt$ 是奇数,则说明最终会有一个位置上的两个字符无法通过交换相匹配。 - 如果 $xyCnt + yxCnt$ 是偶数,并且 $xyCnt$ 为偶数,则 $yxCnt$ 也为偶数。则优先交换 `"xx"` 与 `"yy"`、`"yy"` 与 `"xx"`。即每两个 $xyCnt$ 对应一次交换,每两个 $yxCnt$ 对应交换一次,则结果为 $xyCnt \div 2 + yxCnt \div 2$。 - 如果 $xyCnt + yxCnt$ 是偶数,并且 $xyCnt$ 为奇数,则 $yxCnt$ 也为奇数。则优先交换 `"xx"` 与 `"yy"`、`"yy"` 与 `"xx"`。即每两个 $xyCnt$ 对应一次交换,每两个 $yxCnt$ 对应交换一次,则结果为 $xyCnt \div 2 + yxCnt \div 2$。最后还剩一组 `"xy"` 与 `"yx"` 或者 `"yx"` 与 `"xy"`,则再交换一次,则结果为 $xyCnt \div 2 + yxCnt \div 2 + 2$。 以上结果可以统一写成 $xyCnt \div 2 + yxCnt \div 2 + xyCnt \mod 2 \times 2$。 ### 思路 1:贪心算法代码 ```python class Solution: def minimumSwap(self, s1: str, s2: str) -> int: xyCnt, yxCnt = 0, 0 for i in range(len(s1)): if s1[i] == s2[i]: continue if s1[i] == 'x': xyCnt += 1 else: yxCnt += 1 if (xyCnt + yxCnt) & 1: return -1 return xyCnt // 2 + yxCnt // 2 + (xyCnt % 2 * 2) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1200-1299/minimum-time-visiting-all-points.md ================================================ # [1266. 访问所有点的最小时间](https://leetcode.cn/problems/minimum-time-visiting-all-points/) - 标签:几何、数组、数学 - 难度:简单 ## 题目链接 - [1266. 访问所有点的最小时间 - 力扣](https://leetcode.cn/problems/minimum-time-visiting-all-points/) ## 题目大意 **描述**:给定 $n$ 个点的整数坐标数组 $points$。其中 $points[i] = [xi, yi]$,表示第 $i$ 个点坐标为 $(xi, yi)$。可以按照以下规则在平面上移动: 1. 每一秒内,可以: 1. 沿着水平方向移动一个单位长度。 2. 沿着竖直方向移动一个单位长度。 3. 沿着对角线移动 $\sqrt 2$ 个单位长度(可看做在一秒内沿着水平方向和竖直方向各移动一个单位长度)。 2. 必须按照坐标数组 $points$ 中的顺序来访问这些点。 3. 在访问某个点时,可以经过该点后面出现的点,但经过的那些点不算作有效访问。 **要求**:计算出访问这些点需要的最小时间(以秒为单位)。 **说明**: - $points.length == n$。 - $1 \le n \le 100$。 - $points[i].length == 2$。 - $-1000 \le points[i][0], points[i][1] \le 1000$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/11/24/1626_example_1.png) ```python 输入:points = [[1,1],[3,4],[-1,0]] 输出:7 解释:一条最佳的访问路径是: [1,1] -> [2,2] -> [3,3] -> [3,4] -> [2,3] -> [1,2] -> [0,1] -> [-1,0] 从 [1,1] 到 [3,4] 需要 3 秒 从 [3,4] 到 [-1,0] 需要 4 秒 一共需要 7 秒 ``` ```python 输入:points = [[3,2],[-2,2]] 输出:5 ``` ## 解题思路 ### 思路 1:数学 根据题意,每一秒可以沿着水平方向移动一个单位长度、或者沿着竖直方向移动一个单位长度、或者沿着对角线移动 $\sqrt 2$ 个单位长度。而沿着对角线移动 $\sqrt 2$ 个单位长度可以看做是先沿着水平方向移动一个单位长度,又沿着竖直方向移动一个单位长度,算是一秒走了两步距离。 现在假设从 A 点(坐标为 $(x1, y1)$)移动到 B 点(坐标为 $(x2, y2)$)。 那么从 A 点移动到 B 点如果要想得到最小时间,我们应该计算出沿着水平方向走的距离为 $dx = |x2 - x1|$,沿着竖直方向走的距离为 $dy = |y2 - y1|$。 然后比较沿着水平方向的移动距离和沿着竖直方向的移动距离。 - 如果 $dx > dy$,则我们可以先沿着对角线移动 $dy$ 次,再水平移动 $dx - dy$ 次,总共 $dx$ 次。 - 如果 $dx == dy$,则我们可以直接沿着对角线移动 $dx$ 次,总共 $dx$ 次。 - 如果 $dx < dy$,则我们可以先沿着对角线移动 $dx$ 次,再水平移动 $dy - dx$ 次,,总共 $dy$ 次。 根据上面观察可以发现:最小时间取决于「走的步数较多的那个方向所走的步数」,即 $max(dx, dy)$。 根据题目要求,需要按照坐标数组 $points$ 中的顺序来访问这些点,则我们需要按顺序遍历整个数组,计算出相邻点之间的 $max(dx, dy)$,将其累加到答案中。 最后将答案输出即可。 ### 思路 1:代码 ```python class Solution: def minTimeToVisitAllPoints(self, points: List[List[int]]) -> int: ans = 0 x1, y1 = points[0] for point in points: x2, y2 = point ans += max(abs(x2 - x1), abs(y2 - y1)) x1, y1 = point return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1200-1299/number-of-closed-islands.md ================================================ # [1254. 统计封闭岛屿的数目](https://leetcode.cn/problems/number-of-closed-islands/) - 标签:深度优先搜索、广度优先搜索、并查集、数组、矩阵 - 难度:中等 ## 题目链接 - [1254. 统计封闭岛屿的数目 - 力扣](https://leetcode.cn/problems/number-of-closed-islands/) ## 题目大意 **描述**:给定一个二维矩阵 `grid`,每个位置要么是陆地(记号为 `0`)要么是水域(记号为 `1`)。 我们从一块陆地出发,每次可以往上下左右 `4` 个方向相邻区域走,能走到的所有陆地区域,我们将其称为一座「岛屿」。 如果一座岛屿完全由水域包围,即陆地边缘上下左右所有相邻区域都是水域,那么我们将其称为「封闭岛屿」。 **要求**:返回封闭岛屿的数目。 **说明**: - $1 \le grid.length, grid[0].length \le 100$。 - $0 \le grid[i][j] \le 1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2019/10/31/sample_3_1610.png) ```python 输入:grid = [[1,1,1,1,1,1,1,0],[1,0,0,0,0,1,1,0],[1,0,1,0,1,1,1,0],[1,0,0,0,0,1,0,1],[1,1,1,1,1,1,1,0]] 输出:2 解释:灰色区域的岛屿是封闭岛屿,因为这座岛屿完全被水域包围(即被 1 区域包围)。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/11/07/sample_4_1610.png) ```python 输入:grid = [[0,0,1,0,0],[0,1,0,1,0],[0,1,1,1,0]] 输出:1 ``` ## 解题思路 ### 思路 1:深度优先搜索 1. 从 `grid[i][j] == 0` 的位置出发,使用深度优先搜索的方法遍历上下左右四个方向上相邻区域情况。 1. 如果上下左右都是 `grid[i][j] == 1`,则返回 `True`。 2. 如果有一个以上方向的 `grid[i][j] == 0`,则返回 `False`。 3. 遍历之后将当前陆地位置置为 `1`,表示该位置已经遍历过了。 2. 最后统计出上下左右都满足 `grid[i][j] == 1` 的情况数量,即为答案。 ### 思路 1:代码 ```python class Solution: directs = [(0, 1), (0, -1), (1, 0), (-1, 0)] def dfs(self, grid, i, j): n, m = len(grid), len(grid[0]) if i < 0 or i >= n or j < 0 or j >= m: return False if grid[i][j] == 1: return True grid[i][j] = 1 res = True for direct in self.directs: new_i = i + direct[0] new_j = j + direct[1] if not self.dfs(grid, new_i, new_j): res = False return res def closedIsland(self, grid: List[List[int]]) -> int: res = 0 for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == 0 and self.dfs(grid, i, j): res += 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。其中 $m$ 和 $n$ 分别为行数和列数。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/1200-1299/reconstruct-a-2-row-binary-matrix.md ================================================ # [1253. 重构 2 行二进制矩阵](https://leetcode.cn/problems/reconstruct-a-2-row-binary-matrix/) - 标签:贪心、数组、矩阵 - 难度:中等 ## 题目链接 - [1253. 重构 2 行二进制矩阵 - 力扣](https://leetcode.cn/problems/reconstruct-a-2-row-binary-matrix/) ## 题目大意 **描述**:给定一个 $2$ 行 $n$ 列的二进制数组: - 矩阵是一个二进制矩阵,这意味着矩阵中的每个元素不是 $0$ 就是 $1$。 - 第 $0$ 行的元素之和为 $upper$。 - 第 $1$ 行的元素之和为 $lowe$r。 - 第 $i$ 列(从 $0$ 开始编号)的元素之和为 $colsum[i]$,$colsum$ 是一个长度为 $n$ 的整数数组。 **要求**:你需要利用 $upper$,$lower$ 和 $colsum$ 来重构这个矩阵,并以二维整数数组的形式返回它。 **说明**: - 如果有多个不同的答案,那么任意一个都可以通过本题。 - 如果不存在符合要求的答案,就请返回一个空的二维数组。 - $1 \le colsum.length \le 10^5$。 - $0 \le upper, lower \le colsum.length$。 - $0 \le colsum[i] \le 2$。 **示例**: - 示例 1: ```python 输入:upper = 2, lower = 1, colsum = [1,1,1] 输出:[[1,1,0],[0,0,1]] 解释:[[1,0,1],[0,1,0]] 和 [[0,1,1],[1,0,0]] 也是正确答案。 ``` - 示例 2: ```python 输入:upper = 2, lower = 3, colsum = [2,2,1,1] 输出:[] ``` ## 解题思路 ### 思路 1:贪心算法 1. 先构建一个 $2 \times n$ 的答案数组 $ans$,其中 $ans[0]$ 表示矩阵的第 $0$ 行,$ans[1]$ 表示矩阵的第 $1$​ 行。 2. 遍历数组 $colsum$,对于当前列的和 $colsum[i]$ 来说: 1. 如果 $colsum[i] == 2$,则需要将 $ans[0][i]$ 和 $ans[1][i]$ 都置为 $1$,此时 $upper$ 和 $lower$ 各自减去 $1$。 2. 如果 $colsum[i] == 1$,则需要将 $ans[0][i]$ 置为 $1$ 或将 $ans[1][i]$ 置为 $1$。我们优先使用元素和多的那一项。 1. 如果 $upper > lower$,则优先使用 $upper$,将 $ans[0][i]$ 置为 $1$,并且令 $upper$ 减去 $1$。 2. 如果 $upper \le lower$,则优先使用 $lower$,将 $ans[1][i]$ 置为 $1$,并且令 $lower$ 减去 $1$。 3. 如果 $colsum[i] == 0$,则需要将 $ans[0][i]$ 和 $ans[1][i]$ 都置为 $0$。 3. 在遍历过程中,如果出现 $upper < 0$ 或者 $lower < 0$,则说明无法构造出满足要求的矩阵,则直接返回空数组。 4. 遍历结束后,如果 $upper$ 和 $lower$ 都为 $0$,则返回答案数组 $ans$;否则返回空数组。 ### 思路 1:代码 ```Python class Solution: def reconstructMatrix(self, upper: int, lower: int, colsum: List[int]) -> List[List[int]]: size = len(colsum) ans = [[0 for _ in range(size)] for _ in range(2)] for i in range(size): if colsum[i] == 2: ans[0][i] = ans[1][i] = 1 upper -= 1 lower -= 1 elif colsum[i] == 1: if upper > lower: ans[0][i] = 1 upper -= 1 else: ans[1][i] = 1 lower -= 1 if upper < 0 or lower < 0: return [] if lower != 0 or upper != 0: return [] return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1200-1299/search-suggestions-system.md ================================================ # [1268. 搜索推荐系统](https://leetcode.cn/problems/search-suggestions-system/) - 标签:字典树、数组、字符串 - 难度:中等 ## 题目链接 - [1268. 搜索推荐系统 - 力扣](https://leetcode.cn/problems/search-suggestions-system/) ## 题目大意 给定一个产品数组 `products` 和一个字符串 `searchWord` ,`products` 数组中每个产品都是一个字符串。 要求:设计一个推荐系统,在依次输入单词 `searchWord` 的每一个字母后,推荐 `products` 数组中前缀与 `searchWord` 相同的最多三个产品(如果前缀相同的可推荐产品超过三个,请按字典序返回最小的三个)。 - 请你以二维列表的形式,返回在输入 `searchWord` 每个字母后相应的推荐产品的列表。 ## 解题思路 先将产品数组按字典序排序。 然后使用字典树结构存储每个产品,并在字典树中维护一个数组,用于表示当前前缀所对应的产品列表(只保存最多 3 个产品)。 在查询的时候,将不同前缀所对应的产品列表加入到答案数组中。 最后输出答案数组。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False self.words = list() def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] if len(cur.words) < 3: cur.words.append(word) cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self res = [] flag = False for ch in word: if flag or ch not in cur.children: res.append([]) flag = True else: cur = cur.children[ch] res.append(cur.words) return res class Solution: def suggestedProducts(self, products: List[str], searchWord: str) -> List[List[str]]: products.sort() trie_tree = Trie() for product in products: trie_tree.insert(product) return trie_tree.search(searchWord) ``` ================================================ FILE: docs/solutions/1200-1299/smallest-string-with-swaps.md ================================================ # [1202. 交换字符串中的元素](https://leetcode.cn/problems/smallest-string-with-swaps/) - 标签:深度优先搜索、广度优先搜索、并查集、哈希表、字符串 - 难度:中等 ## 题目链接 - [1202. 交换字符串中的元素 - 力扣](https://leetcode.cn/problems/smallest-string-with-swaps/) ## 题目大意 **描述**:给定一个字符串 `s`,再给定一个数组 `pairs`,其中 `pairs[i] = [a, b]` 表示字符串的第 `a` 个字符可以跟第 `b` 个字符交换。只要满足 `pairs` 中的交换关系,可以任意多次交换字符串中的字符。 **要求**:返回 `s` 经过若干次交换之后,可以变成的字典序最小的字符串。 **说明**: - $1 \le s.length \le 10^5$。 - $0 \le pairs.length \le 10^5$。 - $0 \le pairs[i][0], pairs[i][1] < s.length$。 - `s` 中只含有小写英文字母。 **示例**: - 示例 1: ```python 输入:s = "dcab", pairs = [[0,3],[1,2]] 输出:"bacd" 解释: 交换 s[0] 和 s[3], s = "bcad" 交换 s[1] 和 s[2], s = "bacd" ``` - 示例 2: ```python 输入:s = "dcab", pairs = [[0,3],[1,2],[0,2]] 输出:"abcd" 解释: 交换 s[0] 和 s[3], s = "bcad" 交换 s[0] 和 s[2], s = "acbd" 交换 s[1] 和 s[2], s = "abcd" ``` ## 解题思路 ### 思路 1:并查集 如果第 `a` 个字符可以跟第 `b` 个字符交换,第 `b` 个字符可以跟第 `c` 个字符交换,那么第 `a` 个字符、第 `b` 个字符、第 `c` 个字符之间就可以相互交换。我们可以把可以相互交换的「位置」都放入一个集合中。然后对每个集合中的字符进行排序。然后将其放置回在字符串中原有位置即可。 ### 思路 1:代码 ```python import collections class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] self.count = n def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.count -= 1 def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def smallestStringWithSwaps(self, s: str, pairs: List[List[int]]) -> str: size = len(s) union_find = UnionFind(size) for pair in pairs: union_find.union(pair[0], pair[1]) mp = collections.defaultdict(list) for i, ch in enumerate(s): mp[union_find.find(i)].append(ch) for vec in mp.values(): vec.sort(reverse=True) ans = [] for i in range(size): x = union_find.find(i) ans.append(mp[x][-1]) mp[x].pop() return "".join(ans) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log_2 n + m * \alpha(n))$。其中 $n$ 是字符串的长度,$m$ 为 $pairs$ 的索引对数量,$\alpha$ 是反 `Ackerman` 函数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1200-1299/subtract-the-product-and-sum-of-digits-of-an-integer.md ================================================ # [1281. 整数的各位积和之差](https://leetcode.cn/problems/subtract-the-product-and-sum-of-digits-of-an-integer/) - 标签:数学 - 难度:简单 ## 题目链接 - [1281. 整数的各位积和之差 - 力扣](https://leetcode.cn/problems/subtract-the-product-and-sum-of-digits-of-an-integer/) ## 题目大意 **描述**:给定一个整数 `n`。 **要求**:计算并返回该整数「各位数字之积」与「各位数字之和」的差。 **说明**: - $1 <= n <= 10^5$。 **示例**: - 示例 1: ```python 输入:n = 234 输出:15 解释: 各位数之积 2 * 3 * 4 = 24 各位数之和 2 + 3 + 4 = 9 结果 24 - 9 = 15 ``` ## 解题思路 ### 思路 1:数学 - 通过取模运算得到 `n` 的最后一位,即 `n %= 10`。 - 然后去除 `n` 的最后一位,及`n //= 10`。 - 一次求出各位数字之积与各位数字之和,并返回其差值。 ### 思路 1:数学代码 ```python class Solution: def subtractProductAndSum(self, n: int) -> int: product = 1 total = 0 while n: digit = n % 10 product *= digit total += digit n //= 10 return product - total ``` ================================================ FILE: docs/solutions/1200-1299/tree-diameter.md ================================================ # [1245. 树的直径](https://leetcode.cn/problems/tree-diameter/) - 标签:树、深度优先搜索、广度优先搜索、图、拓扑排序 - 难度:中等 ## 题目链接 - [1245. 树的直径 - 力扣](https://leetcode.cn/problems/tree-diameter/) ## 题目大意 **描述**:给定一个数组 $edges$,用来表示一棵无向树。其中 $edges[i] = [u, v]$ 表示节点 $u$ 和节点 $v$ 之间的双向边。书上的节点编号为 $0 \sim edges.length$,共 $edges.length + 1$ 个节点。 **要求**:求出这棵无向树的直径。 **说明**: - $0 \le edges.length < 10^4$。 - $edges[i][0] \ne edges[i][1]$。 - $0 \le edges[i][j] \le edges.length$。 - $edges$ 会形成一棵无向树。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/10/31/1397_example_1.png) ```python 输入:edges = [[0,1],[0,2]] 输出:2 解释: 这棵树上最长的路径是 1 - 0 - 2,边数为 2。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/10/31/1397_example_2.png) ```python 输入:edges = [[0,1],[1,2],[2,3],[1,4],[4,5]] 输出:4 解释: 这棵树上最长的路径是 3 - 2 - 1 - 4 - 5,边数为 4。 ``` ## 解题思路 ### 思路 1:树形 DP + 深度优先搜索 对于根节点为 $u$ 的树来说: 1. 如果其最长路径经过根节点 $u$,则:**最长路径长度 = 某子树中的最长路径长度 + 另一子树中的最长路径长度 + 1**。 2. 如果其最长路径不经过根节点 $u$,则:**最长路径长度 = 某个子树中的最长路径长度**。 即:**最长路径长度 = max(某子树中的最长路径长度 + 另一子树中的最长路径长度 + 1,某个子树中的最长路径长度)**。 对此,我们可以使用深度优先搜索递归遍历 $u$ 的所有相邻节点 $v$,并在递归遍历的同时,维护一个全局最大路径和变量 $ans$,以及当前节点 $u$ 的最大路径长度变量 $u\_len$。 1. 先计算出从相邻节点 $v$ 出发的最长路径长度 $v\_len$。 2. 更新维护全局最长路径长度为 $self.ans = max(self.ans, \quad u\_len + v\_len + 1)$。 3. 更新维护当前节点 $u$ 的最长路径长度为 $u\_len = max(u\_len, \quad v\_len + 1)$。 > 注意:在遍历邻接节点的过程中,为了避免造成重复遍历,我们在使用深度优先搜索时,应过滤掉父节点。 ### 思路 1:代码 ```python class Solution: def __init__(self): self.ans = 0 def dfs(self, graph, u, fa): u_len = 0 for v in graph[u]: if v != fa: v_len = self.dfs(graph, v, u) self.ans = max(self.ans, u_len + v_len + 1) u_len = max(u_len, v_len + 1) return u_len def treeDiameter(self, edges: List[List[int]]) -> int: size = len(edges) + 1 graph = [[] for _ in range(size)] for u, v in edges: graph[u].append(v) graph[v].append(u) self.dfs(graph, 0, -1) return self.ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为无向树中的节点个数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1300-1399/all-elements-in-two-binary-search-trees.md ================================================ # [1305. 两棵二叉搜索树中的所有元素](https://leetcode.cn/problems/all-elements-in-two-binary-search-trees/) - 标签:树、深度优先搜索、二叉搜索树、二叉树、排序 - 难度:中等 ## 题目链接 - [1305. 两棵二叉搜索树中的所有元素 - 力扣](https://leetcode.cn/problems/all-elements-in-two-binary-search-trees/) ## 题目大意 **描述**:给定两棵二叉搜索树的根节点 $root1$ 和 $root2$。 **要求**:返回一个列表,其中包含两棵树中所有整数并按升序排序。 **说明**: - 每棵树的节点数在 $[0, 5000]$ 范围内。 - $-10^5 \le Node.val \le 10^5$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/12/29/q2-e1.png) ```python 输入:root1 = [2,1,4], root2 = [1,0,3] 输出:[0,1,1,2,3,4] ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/12/29/q2-e5-.png) ```python 输入:root1 = [1,null,8], root2 = [8,1] 输出:[1,1,8,8] ``` ## 解题思路 ### 思路 1:二叉树的中序遍历 + 快慢指针 根据二叉搜索树的特性,如果我们以中序遍历的方式遍历整个二叉搜索树时,就会得到一个有序递增列表。我们按照这样的方式分别对两个二叉搜索树进行中序遍历,就得到了两个有序数组,那么问题就变成了:两个有序数组的合并问题。 两个有序数组的合并可以参考归并排序中的归并过程,使用快慢指针将两个有序数组合并为一个有序数组。 具体步骤如下: 1. 分别使用中序遍历的方式遍历两个二叉搜索树,得到两个有序数组 $nums1$、$nums2$。 2. 使用两个指针 $index1$、$index2$ 分别指向两个有序数组的开始位置。 3. 比较两个指针指向的元素,将两个有序数组中较小元素依次存入结果数组 $nums$ 中,并将指针移动到下一个位置。 4. 重复步骤 $3$,直到某一指针到达数组末尾。 5. 将另一个数组中的剩余元素依次存入结果数组 $nums$ 中。 6. 返回结果数组 $nums$。 ### 思路 1:代码 ```python # Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def inorderTraversal(self, root: TreeNode) -> List[int]: res = [] def inorder(root): if not root: return inorder(root.left) res.append(root.val) inorder(root.right) inorder(root) return res def getAllElements(self, root1: TreeNode, root2: TreeNode) -> List[int]: nums1 = self.inorderTraversal(root1) nums2 = self.inorderTraversal(root2) nums = [] index1, index2 = 0, 0 while index1 < len(nums1) and index2 < len(nums2): if nums1[index1] < nums2[index2]: nums.append(nums1[index1]) index1 += 1 else: nums.append(nums2[index2]) index2 += 1 while index1 < len(nums1): nums.append(nums1[index1]) index1 += 1 while index2 < len(nums2): nums.append(nums2[index2]) index2 += 1 return nums ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m)$,其中 $n$ 和 $m$ 分别为两棵二叉搜索树的节点个数。 - **空间复杂度**:$O(n + m)$。 ================================================ FILE: docs/solutions/1300-1399/angle-between-hands-of-a-clock.md ================================================ # [1344. 时钟指针的夹角](https://leetcode.cn/problems/angle-between-hands-of-a-clock/) - 标签:数学 - 难度:中等 ## 题目链接 - [1344. 时钟指针的夹角 - 力扣](https://leetcode.cn/problems/angle-between-hands-of-a-clock/) ## 题目大意 **描述**:给定两个数 $hour$ 和 $minutes$。 **要求**:请你返回在时钟上,由给定时间的时针和分针组成的较小角的角度($60$ 单位制)。 **说明**: - $1 \le hour \le 12$。 - $0 \le minutes \le 59$。 - 与标准答案误差在 $10^{-5}$ 以内的结果都被视为正确结果。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/02/08/sample_1_1673.png) ```python 输入:hour = 12, minutes = 30 输出:165 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/02/08/sample_2_1673.png) ```python 输入:hour = 3, minutes = 30 输出;75 ``` ## 解题思路 ### 思路 1:数学 1. 我们以 $00:00$ 为基准,分别计算出分针与 $00:00$ 中垂线的夹角,以及时针与 $00:00$ 中垂线的夹角。 2. 然后计算出两者差值的绝对值 $diff$。当前差值可能为较小的角(小于 $180°$ 的角),也可能为较大的角(大于等于 $180°$ 的角)。 3. 将差值的绝对值 $diff$ 与 $360 - diff$ 进行比较,取较小值作为答案。 ### 思路 1:代码 ```Python class Solution: def angleClock(self, hour: int, minutes: int) -> float: mins_angle = 6 * minutes hours_angle = (hour % 12 + minutes / 60) * 30 diff = abs(hours_angle - mins_angle) return min(diff, 360 - diff) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1300-1399/closest-divisors.md ================================================ # [1362. 最接近的因数](https://leetcode.cn/problems/closest-divisors/) - 标签:数学 - 难度:中等 ## 题目链接 - [1362. 最接近的因数 - 力扣](https://leetcode.cn/problems/closest-divisors/) ## 题目大意 **描述**:给定一个整数 $num$。 **要求**:找出同时满足下面全部要求的两个整数: - 两数乘积等于 $num + 1$ 或 $num + 2$。 - 以绝对差进行度量,两数大小最接近。 你可以按照任意顺序返回这两个整数。 **说明**: - $1 \le num \le 10^9$。 **示例**: - 示例 1: ```python 输入:num = 8 输出:[3,3] 解释:对于 num + 1 = 9,最接近的两个因数是 3 & 3;对于 num + 2 = 10, 最接近的两个因数是 2 & 5,因此返回 3 & 3。 ``` - 示例 2: ```python 输入:num = 123 输出:[5,25] ``` ## 解题思路 ### 思路 1:数学 对于整数的任意一个范围在 $[\sqrt{n}, n]$ 的因数而言,一定存在一个范围在 $[1, \sqrt{n}]$ 的因数与其对应。因此,我们在遍历整数因数时,我们只需遍历 $[1, \sqrt{n}]$ 范围内的因数即可。 则这道题的具体解题步骤如下: 1. 对于整数 $num + 1$、从 $\sqrt{num + 1}$ 的位置开始,到 $1$ 为止,以递减的顺序在 $[1, \sqrt{num + 1}]$ 范围内找到最接近的小因数 $a1$,并根据 $num // a1$ 获得另一个因数 $a2$。 2. 用同样的方式,对于整数 $num + 2$、从 $\sqrt{num + 2}$ 的位置开始,到 $1$ 为止,以递减的顺序在 $[1, \sqrt{num + 2}]$ 范围内找到最接近的小因数 $b1$,并根据 $num // b1$ 获得另一个因数 $b2$。 3. 判断 $abs(a1 - a2)$ 与 $abs(b1 - b2)$ 的大小,返回差值绝对值较小的一对因子数作为答案。 ### 思路 1:代码 ```Python class Solution: def disassemble(self, num): for i in range(int(sqrt(num) + 1), 1, -1): if num % i == 0: return (i, num // i) return (1, num) def closestDivisors(self, num: int) -> List[int]: a1, a2 = self.disassemble(num + 1) b1, b2 = self.disassemble(num + 2) if abs(a1 - a2) <= abs(b1 - b2): return [a1, a2] return [b1, b2] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$(\sqrt{n})$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1300-1399/convert-integer-to-the-sum-of-two-no-zero-integers.md ================================================ # [1317. 将整数转换为两个无零整数的和](https://leetcode.cn/problems/convert-integer-to-the-sum-of-two-no-zero-integers/) - 标签:数学 - 难度:简单 ## 题目链接 - [1317. 将整数转换为两个无零整数的和 - 力扣](https://leetcode.cn/problems/convert-integer-to-the-sum-of-two-no-zero-integers/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:返回一个由两个整数组成的列表 $[A, B]$,满足: - $A$ 和 $B$ 都是无零整数。 - $A + B = n$。 **说明**: - **无零整数**:十进制表示中不含任何 $0$ 的正整数。 - 题目数据保证至少一个有效的解决方案。 - 如果存在多个有效解决方案,可以返回其中任意一个。 - $2 \le n \le 10^4$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:[1,1] 解释:A = 1, B = 1. A + B = n 并且 A 和 B 的十进制表示形式都不包含任何 0。 ``` - 示例 2: ```python 输入:n = 11 输出:[2,9] ``` ## 解题思路 ### 思路 1:枚举 1. 由于给定的 $n$ 范围为 $[1, 10000]$,比较小,我们可以直接在 $[1, n)$ 的范围内枚举 $A$,并通过 $n - A$ 得到 $B$。 2. 在判断 $A$ 和 $B$ 中是否都不包含 $0$。如果都不包含 $0$,则返回 $[A, B]$。 ### 思路 1:代码 ```python class Solution: def getNoZeroIntegers(self, n: int) -> List[int]: for A in range(1, n): B = n - A if '0' not in str(A) and '0' not in str(B): return [A, B] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1300-1399/decompress-run-length-encoded-list.md ================================================ # [1313. 解压缩编码列表](https://leetcode.cn/problems/decompress-run-length-encoded-list/) - 标签:数组 - 难度:简单 ## 题目链接 - [1313. 解压缩编码列表 - 力扣](https://leetcode.cn/problems/decompress-run-length-encoded-list/) ## 题目大意 **描述**:给定一个以行程长度编码压缩的整数列表 $nums$。 考虑每对相邻的两个元素 $[freq, val] = [nums[2 \times i], nums[2 \times i + 1]]$ (其中 $i \ge 0$ ),每一对都表示解压后子列表中有 $freq$ 个值为 $val$ 的元素,你需要从左到右连接所有子列表以生成解压后的列表。 **要求**:返回解压后的列表。 **说明**: - $2 \le nums.length \le 100$。 - $nums.length \mod 2 == 0$。 - $1 \le nums[i] \le 100$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,4] 输出:[2,4,4,4] 解释:第一对 [1,2] 代表着 2 的出现频次为 1,所以生成数组 [2]。 第二对 [3,4] 代表着 4 的出现频次为 3,所以生成数组 [4,4,4]。 最后将它们串联到一起 [2] + [4,4,4] = [2,4,4,4]。 ``` - 示例 2: ```python 输入:nums = [1,1,2,3] 输出:[1,3,3] ``` ## 解题思路 ### 思路 1:模拟 1. 以步长为 $2$,遍历数组 $nums$。 2. 对于遍历到的元素 $nums[i]$、$nnums[i + 1]$,将 $nums[i]$ 个 $nums[i + 1]$ 存入答案数组中。 3. 返回答案数组。 ### 思路 1:代码 ```Python class Solution: def decompressRLElist(self, nums: List[int]) -> List[int]: res = [] for i in range(0, len(nums), 2): cnts = nums[i] for cnt in range(cnts): res.append(nums[i + 1]) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + s)$,其中 $n$ 为数组 $nums$ 的长度,$s$ 是数组 $nums$ 中所有偶数下标对应元素之和。 - **空间复杂度**:$O(s)$。 ================================================ FILE: docs/solutions/1300-1399/design-a-stack-with-increment-operation.md ================================================ # [1381. 设计一个支持增量操作的栈](https://leetcode.cn/problems/design-a-stack-with-increment-operation/) - 标签:栈、设计、数组 - 难度:中等 ## 题目链接 - [1381. 设计一个支持增量操作的栈 - 力扣](https://leetcode.cn/problems/design-a-stack-with-increment-operation/) ## 题目大意 **要求**:设计一个支持对其元素进行增量操作的栈。 实现自定义栈类 $CustomStack$: - `CustomStack(int maxSize)`:用 $maxSize$ 初始化对象,$maxSize$ 是栈中最多能容纳的元素数量。 - `void push(int x)`:如果栈还未增长到 $maxSize$,就将 $x$ 添加到栈顶。 - `int pop()`:弹出栈顶元素,并返回栈顶的值,或栈为空时返回 $-1$。 - `void inc(int k, int val)`:栈底的 $k$ 个元素的值都增加 $val$。如果栈中元素总数小于 $k$,则栈中的所有元素都增加 $val$。 **说明**: - $1 \le maxSize, x, k \le 1000$。 - $0 \le val \le 100$。 - 每种方法 `increment`,`push` 以及 `pop` 分别最多调用 $1000$ 次。 **示例**: - 示例 1: ```python 输入: ["CustomStack","push","push","pop","push","push","push","increment","increment","pop","pop","pop","pop"] [[3],[1],[2],[],[2],[3],[4],[5,100],[2,100],[],[],[],[]] 输出: [null,null,null,2,null,null,null,null,null,103,202,201,-1] 解释: CustomStack stk = new CustomStack(3); // 栈是空的 [] stk.push(1); // 栈变为 [1] stk.push(2); // 栈变为 [1, 2] stk.pop(); // 返回 2 --> 返回栈顶值 2,栈变为 [1] stk.push(2); // 栈变为 [1, 2] stk.push(3); // 栈变为 [1, 2, 3] stk.push(4); // 栈仍然是 [1, 2, 3],不能添加其他元素使栈大小变为 4 stk.increment(5, 100); // 栈变为 [101, 102, 103] stk.increment(2, 100); // 栈变为 [201, 202, 103] stk.pop(); // 返回 103 --> 返回栈顶值 103,栈变为 [201, 202] stk.pop(); // 返回 202 --> 返回栈顶值 202,栈变为 [201] stk.pop(); // 返回 201 --> 返回栈顶值 201,栈变为 [] stk.pop(); // 返回 -1 --> 栈为空,返回 -1 ``` ## 解题思路 ### 思路 1:模拟 1. 初始化: 1. 使用空数组 $stack$ 用于表示栈。 2. 使用 $size$ 用于表示当前栈中元素个数, 3. 使用 $maxSize$ 用于表示栈中允许的最大元素个数。 4. 使用另一个空数组 $increments$ 用于增量操作。 2. `push(x)` 操作: 1. 判断当前元素个数与栈中允许的最大元素个数关系。 2. 如果当前元素个数小于栈中允许的最大元素个数,则: 1. 将 $x$ 添加到数组 $stack$ 中,即:`self.stack.append(x)`。 2. 当前元素个数加 $1$,即:`self.size += 1`。 3. 将 $0$ 添加到增量数组 $increments$ 中,即:`self.increments.append(0)`。 3. `increment(k, val)` 操作: 1. 如果增量数组不为空,则取 $k$ 与元素个数 `self.size` 的较小值,令增量数组对应位置加上 `val`(等 `pop()` 操作时,再计算出准确值)。 4. `pop()` 操作: 1. 如果当前元素个数为 $0$,则直接返回 $-1$。 2. 如果当前元素个数大于等于 $2$,则更新弹出元素后的增量数组(保证剩余元素弹出时能够正确计算出),即:`self.increments[-2] += self.increments[-1]` 3. 令元素个数减 $1$,即:`self.size -= 1`。 4. 弹出数组 $stack$ 中的栈顶元素和增量数组 $increments$ 中的栈顶元素,令其相加,即为弹出元素值,将其返回。 ### 思路 1:代码 ```python class CustomStack: def __init__(self, maxSize: int): self.maxSize = maxSize self.stack = [] self.increments = [] self.size = 0 def push(self, x: int) -> None: if self.size < self.maxSize: self.stack.append(x) self.increments.append(0) self.size += 1 def pop(self) -> int: if self.size == 0: return -1 if self.size >= 2: self.increments[-2] += self.increments[-1] self.size -= 1 val = self.stack.pop() + self.increments.pop() return val def increment(self, k: int, val: int) -> None: if self.increments: self.increments[min(k, self.size) - 1] += val # Your CustomStack object will be instantiated and called as such: # obj = CustomStack(maxSize) # obj.push(x) # param_2 = obj.pop() # obj.increment(k,val) ``` ### 思路 1:复杂度分析 - **时间复杂度**:初始化、`push` 操作、`pop` 操作、`increment` 操作的时间复杂度为 $O(1)$。 - **空间复杂度**:$O(maxSize)$。 ================================================ FILE: docs/solutions/1300-1399/index.md ================================================ ## 本章内容 - [1300. 转变数组后最接近目标值的数组和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/sum-of-mutated-array-closest-to-target.md) - [1305. 两棵二叉搜索树中的所有元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/all-elements-in-two-binary-search-trees.md) - [1310. 子数组异或查询](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/xor-queries-of-a-subarray.md) - [1313. 解压缩编码列表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/decompress-run-length-encoded-list.md) - [1317. 将整数转换为两个无零整数的和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/convert-integer-to-the-sum-of-two-no-zero-integers.md) - [1319. 连通网络的操作次数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/number-of-operations-to-make-network-connected.md) - [1324. 竖直打印单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/print-words-vertically.md) - [1338. 数组大小减半](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/reduce-array-size-to-the-half.md) - [1343. 大小为 K 且平均值大于等于阈值的子数组数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/number-of-sub-arrays-of-size-k-and-average-greater-than-or-equal-to-threshold.md) - [1344. 时钟指针的夹角](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/angle-between-hands-of-a-clock.md) - [1347. 制造字母异位词的最小步骤数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/minimum-number-of-steps-to-make-two-strings-anagram.md) - [1349. 参加考试的最大学生数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/maximum-students-taking-exam.md) - [1358. 包含所有三种字符的子字符串数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/number-of-substrings-containing-all-three-characters.md) - [1362. 最接近的因数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/closest-divisors.md) - [1381. 设计一个支持增量操作的栈](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/design-a-stack-with-increment-operation.md) ================================================ FILE: docs/solutions/1300-1399/maximum-students-taking-exam.md ================================================ # [1349. 参加考试的最大学生数](https://leetcode.cn/problems/maximum-students-taking-exam/) - 标签:位运算、数组、动态规划、状态压缩、矩阵 - 难度:困难 ## 题目链接 - [1349. 参加考试的最大学生数 - 力扣](https://leetcode.cn/problems/maximum-students-taking-exam/) ## 题目大意 **描述**:给定一个 $m \times n$ 大小的矩阵 $seats$ 表示教室中的座位分布,其中如果座位是坏的(不可用),就用 `'#'` 表示,如果座位是好的,就用 `'.'` 表示。 学生可以看到左侧、右侧、左上方、右上方这四个方向上紧邻他的学生答卷,但是看不到直接坐在他前面或者后面的学生答卷。 **要求**:计算并返回该考场可以容纳的一期参加考试且无法作弊的最大学生人数。 **说明**: - 学生必须坐在状况良好的座位上。 - $seats$ 只包含字符 `'.'` 和 `'#'`。 - $m == seats.length$。 - $n == seats[i].length$。 - $1 \le m \le 8$。 - $1 \le n \le 8$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/02/09/image.png) ```python 输入:seats = [["#",".","#","#",".","#"], [".","#","#","#","#","."], ["#",".","#","#",".","#"]] 输出:4 解释:教师可以让 4 个学生坐在可用的座位上,这样他们就无法在考试中作弊。 ``` - 示例 2: ```python 输入:seats = [[".","#"], ["#","#"], ["#","."], ["#","#"], [".","#"]] 输出:3 解释:让所有学生坐在可用的座位上。 ``` ## 解题思路 ### 思路 1:状态压缩 DP 题目中给定的 $m$、$n$ 范围为 $1 \le m, n \le 8$,每一排最多有 $8$ 个座位,那么我们可以使用一个 $8$ 位长度的二进制数来表示当前排座位的选择情况(也就是「状态压缩」的方式)。 同时从题目中可以看出,当前排的座位与当前行左侧、右侧座位有关,并且也与上一排中左上方、右上方的座位有关,则我们可以使用一个二维数组来表示状态。其中第一维度为排数,第二维度为当前排的座位选择情况。 具体做法如下: ###### 1. 阶段划分 按照排数、当前排的座位选择情况进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][state]$ 表示为:前 $i$ 排,并且最后一排座位选择状态为 $state$ 时,可以参加考试的最大学生数。 ###### 3. 状态转移方程 因为学生可以看到左侧、右侧、左上方、右上方这四个方向上紧邻他的学生答卷,所以对于当前排的某个座位来说,其左侧、右侧、左上方、右上方都不应有人坐。我们可以根据当前排的座位选取状态 $cur\_state$,并通过枚举的方式,找出符合要求的上一排座位选取状态 $pre\_state$,并计算出当前排座位选择个数,即 $f(cur\_state)$,则状态转移方程为: $dp[i][state] = \max \lbrace dp[i - 1][pre\_state] \rbrace + f(state)$ 因为所给座位中还有坏座位(不可用)的情况,我们可以使用一个 $8$ 位的二进制数 $bad\_seat$ 来表示当前排的坏座位情况,如果 $cur\_state \text{ \& } bad\_seat == 1$,则说明当前状态下,选择了坏椅子,则可直接跳过这种状态。 我们还可以通过 $cur\_state \text{ \& } (cur\_state \text{ <}\text{< } 1)$ 和 $cur\_state \& (cur\_state \text{ >}\text{> } 1)$ 来判断当前排选择状态下,左右相邻座位上是否有人,如果有人,则可直接跳过这种状态。 同理,我们还可以通过 $cur\_state \text{ \& } (pre\_state \text{ <}\text{< } 1)$ 和 $cur\_state \text{ \& } (pre\_state \text{ >}\text{> } 1)$ 来判断当前排选择状态下,上一行左上、右上相邻座位上是否有人,如果有人,则可直接跳过这种状态。 ###### 4. 初始条件 - 默认情况下,前 $0$ 排所有选择状态下,可以参加考试的最大学生数为 $0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][state]$ 表示为:前 $i$ 排,并且最后一排座位选择状态为 $state$ 时,可以参加考试的最大学生数。 所以最终结果为最后一排 $dp[rows]$ 中的最大值。 ### 思路 1:代码 ```python class Solution: def maxStudents(self, seats: List[List[str]]) -> int: rows, cols = len(seats), len(seats[0]) states = 1 << cols dp = [[0 for _ in range(states)] for _ in range(rows + 1)] for i in range(1, rows + 1): # 模拟 1 ~ rows 排分配座位 bad_seat = 0 # 当前排的坏座位情况 for j in range(cols): if seats[i - 1][j] == '#': # 记录坏座位情况 bad_seat |= 1 << j for cur_state in range(states): # 枚举当前排的座位选取状态 if cur_state & bad_seat: # 当前排的座位选择了换座位,跳过 continue if cur_state & (cur_state << 1): # 当前排左侧座位有人,跳过 continue if cur_state & (cur_state >> 1): # 当前排右侧座位有人,跳过 continue count = bin(cur_state).count('1') # 计算当前排最多可以坐多少人 for pre_state in range(states): # 枚举前一排情况 if cur_state & (pre_state << 1): # 左上座位有人,跳过 continue if cur_state & (pre_state >> 1): # 右上座位有人,跳过 continue # dp[i][cur_state] 取自上一排分配情况为 pre_state 的最大值 + 当前排最多可以坐的人数 dp[i][cur_state] = max(dp[i][cur_state], dp[i - 1][pre_state] + count) return max(dp[rows]) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times 2^{2n})$,其中 $m$、$n$ 分别为所给矩阵的行数、列数。 - **空间复杂度**:$O(m \times 2^n)$。 ================================================ FILE: docs/solutions/1300-1399/minimum-number-of-steps-to-make-two-strings-anagram.md ================================================ # [1347. 制造字母异位词的最小步骤数](https://leetcode.cn/problems/minimum-number-of-steps-to-make-two-strings-anagram/) - 标签:哈希表、字符串、计数 - 难度:中等 ## 题目链接 - [1347. 制造字母异位词的最小步骤数 - 力扣](https://leetcode.cn/problems/minimum-number-of-steps-to-make-two-strings-anagram/) ## 题目大意 **描述**:给定两个长度相等的字符串 $s$ 和 $t$。每一个步骤中,你可以选择将 $t$ 中任一个字符替换为另一个字符。 **要求**:返回使 $t$ 成为 $s$ 的字母异位词的最小步骤数。 **说明**: - **字母异位词**:指字母相同,但排列不同(也可能相同)的字符串。 - $1 \le s.length \le 50000$。 - $s.length == t.length$。 - $s$ 和 $t$ 只包含小写英文字母。 **示例**: - 示例 1: ```python 输出:s = "bab", t = "aba" 输出:1 提示:用 'b' 替换 t 中的第一个 'a',t = "bba" 是 s 的一个字母异位词。 ``` - 示例 2: ```python 输出:s = "leetcode", t = "practice" 输出:5 提示:用合适的字符替换 t 中的 'p', 'r', 'a', 'i' 和 'c',使 t 变成 s 的字母异位词。 ``` ## 解题思路 ### 思路 1:哈希表 题目要求使 $t$ 成为 $s$ 的字母异位词,则只需要 $t$ 和 $s$ 对应的每种字符数量相一致即可,无需考虑字符位置。 因为每一次转换都会减少一个字符,并增加另一个字符。 1. 我们使用两个哈希表 $cnts\_s$、$cnts\_t$ 分别对 $t$ 和 $s$ 中的字符进行计数,并求出两者的交集。 2. 遍历交集中的字符种类,以及对应的字符数量。 3. 对于当前字符 $key$,如果当前字符串 $s$ 中的字符 $key$ 的数量小于字符串 $t$ 中字符 $key$ 的数量,即 $cnts\_s[key] < cnts\_t[key]$。则 $s$ 中需要补齐的字符数量就是需要的最小步数,将其累加到答案中。 4. 遍历完返回答案。 ### 思路 1:代码 ```Python class Solution: def minSteps(self, s: str, t: str) -> int: cnts_s, cnts_t = Counter(s), Counter(t) cnts = cnts_s | cnts_t ans = 0 for key, cnt in cnts.items(): if cnts_s[key] < cnts_t[key]: ans += cnts_t[key] - cnts_s[key] return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n)$,其中 $m$、$n$ 分别为字符串 $s$、$t$ 的长度。 - **空间复杂度**:$O(|\sum|)$,其中 $\sum$ 是字符集,本题中 $| \sum | = 26$。 ================================================ FILE: docs/solutions/1300-1399/number-of-operations-to-make-network-connected.md ================================================ # [1319. 连通网络的操作次数](https://leetcode.cn/problems/number-of-operations-to-make-network-connected/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [1319. 连通网络的操作次数 - 力扣](https://leetcode.cn/problems/number-of-operations-to-make-network-connected/) ## 题目大意 **描述**:$n$ 台计算机通过网线连接成一个网络,计算机的编号从 $0$ 到 $n - 1$。线缆用 $comnnections$ 表示,其中 $connections[i] = [a, b]$ 表示连接了计算机 $a$ 和 $b$。 给定这个计算机网络的初始布线 $connections$,可以拔除任意两台直接相连的计算机之间的网线,并用这根网线连接任意一对未直接连接的计算机。 **要求**:计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 $-1$。 **说明**: - $1 \le n \le 10^5$。 - $1 \le connections.length \le min( \frac{n \times (n-1)}{2}, 10^5)$。 - $connections[i].length == 2$。 - $0 \le connections[i][0], connections[i][1] < n$。 - $connections[i][0] != connections[i][1]$。 - 没有重复的连接。 - 两台计算机不会通过多条线缆连接。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/01/11/sample_1_1677.png) ```python 输入:n = 4, connections = [[0,1],[0,2],[1,2]] 输出:1 解释:拔下计算机 1 和 2 之间的线缆,并将它插到计算机 1 和 3 上。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/01/11/sample_2_1677.png) ```python 输入:n = 6, connections = [[0,1],[0,2],[0,3],[1,2],[1,3]] 输出:2 ``` ## 解题思路 ### 思路 1:并查集 $n$ 台计算机至少需要 $n - 1$ 根线才能进行连接,如果网线的数量少于 $n - 1$,那么就不可能将其连接。接下来计算最少操作次数。 把 $n$ 台计算机看做是 $n$ 个节点,每条网线看做是一条无向边。维护两个变量:多余电线数 $removeCount$、需要电线数 $needConnectCount$。初始 $removeCount = 1, needConnectCount = n - 1$。 遍历网线数组,将相连的节点 $a$ 和 $b$ 利用并查集加入到一个集合中(调用 `union` 操作)。 - 如果 $a$ 和 $b$ 已经在同一个集合中,说明该连接线多余,多余电线数加 $1$。 - 如果 $a$ 和 $b$ 不在一个集合中,则将其合并,则 $a$ 和 $b$ 之间不再需要用额外的电线连接了,所以需要电线数减 $1$。 最后,判断多余的电线数是否满足需要电线数,不满足返回 $-1$,如果满足,则返回需要电线数。 ### 思路 1:代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return False self.parent[root_x] = root_y return True def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def makeConnected(self, n: int, connections: List[List[int]]) -> int: union_find = UnionFind(n) removeCount = 0 needConnectCount = n - 1 for connection in connections: if union_find.union(connection[0], connection[1]): needConnectCount -= 1 else: removeCount += 1 if removeCount < needConnectCount: return -1 return needConnectCount ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times \alpha(n))$,其中 $m$ 是数组 $connections$ 的长度,$\alpha$ 是反 `Ackerman` 函数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1300-1399/number-of-sub-arrays-of-size-k-and-average-greater-than-or-equal-to-threshold.md ================================================ # [1343. 大小为 K 且平均值大于等于阈值的子数组数目](https://leetcode.cn/problems/number-of-sub-arrays-of-size-k-and-average-greater-than-or-equal-to-threshold/) - 标签:数组、滑动窗口 - 难度:中等 ## 题目链接 - [1343. 大小为 K 且平均值大于等于阈值的子数组数目 - 力扣](https://leetcode.cn/problems/number-of-sub-arrays-of-size-k-and-average-greater-than-or-equal-to-threshold/) ## 题目大意 **描述**:给定一个整数数组 $arr$ 和两个整数 $k$ 和 $threshold$。 **要求**:返回长度为 $k$ 且平均值大于等于 $threshold$ 的子数组数目。 **说明**: - $1 \le arr.length \le 10^5$。 - $1 \le arr[i] \le 10^4$。 - $1 \le k \le arr.length$。 - $0 \le threshold \le 10^4$。 **示例**: - 示例 1: ```python 输入:arr = [2,2,2,2,5,5,5,8], k = 3, threshold = 4 输出:3 解释:子数组 [2,5,5],[5,5,5] 和 [5,5,8] 的平均值分别为 4,5 和 6 。其他长度为 3 的子数组的平均值都小于 4 (threshold 的值)。 ``` - 示例 2: ```python 输入:arr = [11,13,17,23,29,31,7,5,2,3], k = 3, threshold = 5 输出:6 解释:前 6 个长度为 3 的子数组平均值都大于 5 。注意平均值不是整数。 ``` ## 解题思路 ### 思路 1:滑动窗口(固定长度) 这道题目是典型的固定窗口大小的滑动窗口题目。窗口大小为 `k`。具体做法如下: 1. `ans` 用来维护答案数目。`window_sum` 用来维护窗口中元素的和。 2. `left` 、`right` 都指向序列的第一个元素,即:`left = 0`,`right = 0`。 3. 向右移动 `right`,先将 `k` 个元素填入窗口中。 4. 当窗口元素个数为 `k` 时,即:`right - left + 1 >= k` 时,判断窗口内的元素和平均值是否大于等于阈值 `threshold`。 1. 如果满足,则答案数目 + 1。 2. 然后向右移动 `left`,从而缩小窗口长度,即 `left += 1`,使得窗口大小始终保持为 `k`。 5. 重复 3 ~ 4 步,直到 `right` 到达数组末尾。 6. 最后输出答案数目。 ### 思路 1:代码 ```python class Solution: def numOfSubarrays(self, arr: List[int], k: int, threshold: int) -> int: left = 0 right = 0 window_sum = 0 ans = 0 while right < len(arr): window_sum += arr[right] if right - left + 1 >= k: if window_sum >= k * threshold: ans += 1 window_sum -= arr[left] left += 1 right += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1300-1399/number-of-substrings-containing-all-three-characters.md ================================================ # [1358. 包含所有三种字符的子字符串数目](https://leetcode.cn/problems/number-of-substrings-containing-all-three-characters/) - 标签:哈希表、字符串、滑动窗口 - 难度:中等 ## 题目链接 - [1358. 包含所有三种字符的子字符串数目 - 力扣](https://leetcode.cn/problems/number-of-substrings-containing-all-three-characters/) ## 题目大意 给你一个字符串 `s` ,`s` 只包含三种字符 `a`, `b` 和 `c`。 请你返回 `a`,`b` 和 `c` 都至少出现过一次的子字符串数目。 ## 解题思路 只要找到首个 `a`、`b`、`c` 同时存在的子字符串,则在该子字符串后面追加字符构成的新字符串还是满足题意的。假设该子串末尾字母的位置为 `i`,则以此字符串构建的新字符串有 `len(s) - i`个。所以题目可以转换为找出 `a`、`b`、`c` 同时存在的最短子串,并记录所有满足题意的字符串数量。具体做法如下: 用滑动窗口 `window` 来记录各个字符个数,`window` 为哈希表类型。用 `ans` 来维护 `a`,`b` 和 `c` 都至少出现过一次的子字符串数目。 设定两个指针:`left`、`right`,分别指向滑动窗口的左右边界,保证窗口中不超过 `k` 种字符。 - 一开始,`left`、`right` 都指向 `0`。 - 将最右侧字符 `s[right]` 加入当前窗口 `window_counts` 中,记录该字符个数,向右移动 `right`。 - 如果该窗口中字符的种数大于等于 `3` 种,即 `len(window) >= 3`,则累积答案个数为 `len(s) - right`,并不断右移 `left`,缩小滑动窗口长度,并更新窗口中对应字符的个数,直到 `len(window) < 3`。 - 然后继续右移 `right`,直到 `right >= len(nums)` 结束。 - 输出答案 `ans`。 ## 代码 ```python class Solution: def numberOfSubstrings(self, s: str) -> int: window = dict() ans = 0 left, right = 0, 0 while right < len(s): if s[right] in window: window[s[right]] += 1 else: window[s[right]] = 1 while len(window) >= 3: ans += len(s) - right window[s[left]] -= 1 if window[s[left]] == 0: del window[s[left]] left += 1 right += 1 return ans ``` ================================================ FILE: docs/solutions/1300-1399/print-words-vertically.md ================================================ # [1324. 竖直打印单词](https://leetcode.cn/problems/print-words-vertically/) - 标签:数组、字符串、模拟 - 难度:中等 ## 题目链接 - [1324. 竖直打印单词 - 力扣](https://leetcode.cn/problems/print-words-vertically/) ## 题目大意 **描述**:给定一个字符串 $s$。 **要求**:按照单词在 $s$ 中出现顺序将它们全部竖直返回。 **说明**: - 单词应该以字符串列表的形式返回,必要时用空格补位,但输出尾部的空格需要删除(不允许尾随空格)。 - 每个单词只能放在一列上,每一列中也只能有一个单词。 - $1 \le s.length \le 200$。 - $s$ 仅含大写英文字母。 - 题目数据保证两个单词之间只有一个空格。 **示例**: - 示例 1: ```python 输入:s = "HOW ARE YOU" 输出:["HAY","ORO","WEU"] 解释:每个单词都应该竖直打印。 "HAY" "ORO" "WEU" ``` - 示例 2: ```python 输入:s = "TO BE OR NOT TO BE" 输出:["TBONTB","OEROOE"," T"] 解释:题目允许使用空格补位,但不允许输出末尾出现空格。 "TBONTB" "OEROOE" " T" ``` ## 解题思路 ### 思路 1:模拟 1. 将字符串 $s$ 按空格分割为单词数组 $words$。 2. 计算出单词数组 $words$ 中单词的最大长度 $max\_len$。 3. 第一重循环遍历竖直单词的每个单词位置 $i$,第二重循环遍历当前第 $j$ 个单词。 1. 如果当前单词没有第 $i$ 个字符(当前单词的长度超过了单词位置 $i$),则将空格插入到竖直单词中。 2. 如果当前单词有第 $i$ 个字符,泽讲当前单词的第 $i$ 个字符插入到竖直单词中。 4. 第二重循环遍历完,将竖直单词去除尾随空格,并加入到答案数组中。 5. 第一重循环遍历完,则返回答案数组。 ### 思路 1:代码 ```Python class Solution: def printVertically(self, s: str) -> List[str]: words = s.split(' ') max_len = 0 for word in words: max_len = max(len(word), max_len) res = [] for i in range(max_len): ans = "" for j in range(len(words)): if i + 1 > len(words[j]): ans += ' ' else: ans += words[j][i] res.append(ans.rstrip()) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times max(|word|))$,其中 $n$ 为字符串 $s$ 中的单词个数,$max(|word|)$ 是最长的单词长度。。 - **空间复杂度**:$O(n \times max(|word|))$。 ================================================ FILE: docs/solutions/1300-1399/reduce-array-size-to-the-half.md ================================================ - [1338. 数组大小减半](https://leetcode.cn/problems/reduce-array-size-to-the-half/) - 标签:贪心、数组、哈希表、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [1338. 数组大小减半 - 力扣](https://leetcode.cn/problems/reduce-array-size-to-the-half/) ## 题目大意 **描述**:给定过一个整数数组 $arr$。你可以从中选出一个整数集合,并在数组 $arr$ 删除所有整数集合对应的数。 **要求**:返回至少能删除数组中的一半整数的整数集合的最小大小。 **说明**: - $1 \le arr.length \le 10^5$。 - $arr.length$ 为偶数。 - $1 \le arr[i] \le 10^5$。 **示例**: - 示例 1: ```python 输入:arr = [3,3,3,3,5,5,5,2,2,7] 输出:2 解释:选择 {3,7} 使得结果数组为 [5,5,5,2,2]、长度为 5(原数组长度的一半)。 大小为 2 的可行集合有 {3,5},{3,2},{5,2}。 选择 {2,7} 是不可行的,它的结果数组为 [3,3,3,3,5,5,5],新数组长度大于原数组的二分之一。 ``` - 示例 2: ```python 输入:arr = [7,7,7,7,7,7] 输出:1 解释:我们只能选择集合 {7},结果数组为空。 ``` ## 解题思路 ### 思路 1:贪心算法 对于选出的整数集合中每一个数 $x$ 来说,我们会删除数组 $arr$ 中所有值为 $x$ 的整数。 因为题目要求我们选出的整数集合最小,所以在每一次选择整数 $x$ 加入整数集合时,我们都应该选择数组 $arr$ 中出现次数最多的数。 因此,我们可以统计出数组 $arr$ 中每个整数的出现次数,用哈希表存储,并依照出现次数进行降序排序。 然后,依次选择出现次数最多的数进行删除,并统计个数,直到删除了至少一半的数时停止。 最后,将统计个数作为答案返回。 ### 思路 1:代码 ```Python class Solution: def minSetSize(self, arr: List[int]) -> int: cnts = Counter(arr) ans, cnt = 0, 0 for num, freq in cnts.most_common(): cnt += freq ans += 1 if cnt * 2 >= len(arr): break return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 为数组 $arr$ 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1300-1399/sum-of-mutated-array-closest-to-target.md ================================================ # [1300. 转变数组后最接近目标值的数组和](https://leetcode.cn/problems/sum-of-mutated-array-closest-to-target/) - 标签:数组、二分查找、排序 - 难度:中等 ## 题目链接 - [1300. 转变数组后最接近目标值的数组和 - 力扣](https://leetcode.cn/problems/sum-of-mutated-array-closest-to-target/) ## 题目大意 **描述**:给定一个整数数组 $arr$ 和一个目标值 $target$。 **要求**:返回一个整数 $value$,使得将数组中所有大于 $value$ 的值变成 $value$ 后,数组的和最接近 $target$(最接近表示两者之差的绝对值最小)。如果有多种使得和最接近 $target$ 的方案,请你返回这些整数中的最小值。 **说明**: - 答案 $value$ 不一定是 $arr$ 中的数字。 - $1 \le arr.length \le 10^4$。 - $1 \le arr[i], target \le 10^5$。 **示例**: - 示例 1: ```python 输入:arr = [4,9,3], target = 10 输出:3 解释:当选择 value 为 3 时,数组会变成 [3, 3, 3],和为 9 ,这是最接近 target 的方案。 ``` - 示例 2: ```python 输入:arr = [60864,25176,27249,21296,20204], target = 56803 输出:11361 ``` ## 解题思路 ### 思路 1:二分查找 题目可以理解为:在 $[0, max(arr)]$ 的区间中,查找一个值 $value$。使得「转变后的数组和」与 $target$ 最接近。 - 转变规则:将数组中大于 $value$ 的值变为 $value$。 在 $[0, max(arr)]$ 的区间中,查找一个值 $value$ 可以使用二分查找答案的方式减少时间复杂度。但是这个最接近 $target$ 应该怎么理解,或者说怎么衡量接近程度。 最接近 $target$ 的肯定是数组和等于 $target$ 的时候。不过更可能是出现数组和恰好比 $target$ 大一点,或数组和恰好比 $target$ 小一点。我们可以将 $target$ 上下两个值相对应的数组和与 $target$ 进行比较,输出差值更小的那一个 $value$。 在根据查找的值 $value$ 计算数组和时,也可以通过二分查找方法查找出数组刚好大于等于 $value$ 元素下标。还可以根据事先处理过的前缀和数组,快速得到转变后的数组和。 最后输出使得数组和与 $target$ 差值更小的 $value$。 整个算法步骤如下: - 先对数组排序,并计算数组的前缀和 $pre\_sum$。 - 通过二分查找在 $[0, arr[-1]]$ 中查找使得转变后数组和刚好大于等于 $target$ 的值 $value$。 - 计算 $value$ 对应的数组和 $sum\_1$,以及 $value - 1$ 对应的数组和 $sum\_2$。并分别计算与 $target$ 的差值 $diff\_1$、$diff\_2$。 - 输出差值小的那个值。 ### 思路 1:代码 ```python class Solution: # 计算 value 对应的转变后的数组 def calc_sum(self, arr, value, pre_sum): size = len(arr) left, right = 0, size - 1 while left < right: mid = left + (right - left) // 2 if arr[mid] < value: left = mid + 1 else: right = mid return pre_sum[left] + (size - left) * value # 查找使得转变后的数组和刚好大于等于 target 的 value def binarySearchValue(self, arr, target, pre_sum): left, right = 0, arr[-1] while left < right: mid = left + (right - left) // 2 if self.calc_sum(arr, mid, pre_sum) < target: left = mid + 1 else: right = mid return left def findBestValue(self, arr: List[int], target: int) -> int: size = len(arr) arr.sort() pre_sum = [0 for _ in range(size + 1)] for i in range(size): pre_sum[i + 1] = pre_sum[i] + arr[i] value = self.binarySearchValue(arr, target, pre_sum) sum_1 = self.calc_sum(arr, value, pre_sum) sum_2 = self.calc_sum(arr, value - 1, pre_sum) diff_1 = abs(sum_1 - target) diff_2 = abs(sum_2 - target) return value if diff_1 < diff_2 else value - 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O((n + k) \times \log n)$。其中 $n$ 是数组 $arr$ 的长度,$k$ 是数组 $arr$ 中的最大值。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1300-1399/xor-queries-of-a-subarray.md ================================================ # [1310. 子数组异或查询](https://leetcode.cn/problems/xor-queries-of-a-subarray/) - 标签:位运算、数组、前缀和 - 难度:中等 ## 题目链接 - [1310. 子数组异或查询 - 力扣](https://leetcode.cn/problems/xor-queries-of-a-subarray/) ## 题目大意 **描述**:给定一个正整数数组 `arr`,再给定一个对应的查询数组 `queries`,其中 `queries[i] = [Li, Ri]`。 **要求**:对于每个查询 `queries[i]`,要求计算从 `Li` 到 `Ri` 的异或值(即 `arr[Li] ^ arr[Li+1] ^ ... ^ arr[Ri]`)作为本次查询的结果。并返回一个包含给定查询 `queries` 所有结果的数组。 **说明**: - $1 \le arr.length \le 3 * 10^4$。 - $1 \le arr[i] \le 10^9$。 - $1 \le queries.length \le 3 * 10^4$。 - $queries[i].length == 2$。 - $0 \le queries[i][0] \le queries[i][1] < arr.length$。 **示例**: - 示例 1: ```python 输入:arr = [1,3,4,8], queries = [[0,1],[1,2],[0,3],[3,3]] 输出:[2,7,14,8] 解释 数组中元素的二进制表示形式是: 1 = 0001 3 = 0011 4 = 0100 8 = 1000 查询的 XOR 值为: [0,1] = 1 xor 3 = 2 [1,2] = 3 xor 4 = 7 [0,3] = 1 xor 3 xor 4 xor 8 = 14 [3,3] = 8 ``` ## 解题思路 ### 思路 1:线段树 - 使用数组 `res` 作为答案数组,用于存放每个查询的结果值。 - 根据 `nums` 数组构建一棵线段树。 - 然后遍历查询数组 `queries`。对于每个查询 `queries[i]`,在线段树中查询对应区间的异或值,将其结果存入答案数组 `res` 中。 - 返回答案数组 `res` 即可。 这样构建线段树的时间复杂度为 $O(\log n)$,单次区间查询的时间复杂度为 $O(\log n)$。总体时间复杂度为 $O(k * \log n)$,其中 $k$ 是查询次数。 ### 思路 1:线段树代码 ```python # 线段树的节点类 class SegTreeNode: def __init__(self, val=0): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值) self.lazy_tag = None # 区间和问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, nums, function): self.size = len(nums) self.tree = [SegTreeNode() for _ in range(4 * self.size)] # 维护 SegTreeNode 数组 self.nums = nums # 原始数据 self.function = function # function 是一个函数,左右区间的聚合方法 if self.size > 0: self.__build(0, 0, self.size - 1) # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.nums[i] = val self.__update_point(i, val, 0) # 区间更新接口:将区间为 [q_left, q_right] 上的所有元素值加上 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, 0) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, 0) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self): for i in range(self.size): self.nums[i] = self.query_interval(i, i) return self.nums # 以下为内部实现方法 # 构建线段树实现方法:节点的存储下标为 index,节点的区间为 [left, right] def __build(self, index, left, right): self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,节点值为对应位置的元素值 self.tree[index].val = self.nums[left] return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.__build(left_index, left, mid) # 递归创建左子树 self.__build(right_index, mid + 1, right) # 递归创建右子树 self.__pushup(index) # 向上更新节点的区间值 # 区间查询实现方法:在线段树中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return self.tree[index].val # 直接返回节点值 if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, left_index) if q_right > mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, right_index) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:更新下标为 index 的节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, index): left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.tree[index].val = self.function(self.tree[left_index].val, self.tree[right_index].val) class Solution: def xorQueries(self, arr: List[int], queries: List[List[int]]) -> List[int]: self.STree = SegmentTree(arr, lambda x, y: (x ^ y)) res = [] for query in queries: ans = self.STree.query_interval(query[0], query[1]) res.append(ans) return res ``` ================================================ FILE: docs/solutions/1400-1499/average-salary-excluding-the-minimum-and-maximum-salary.md ================================================ # [1491. 去掉最低工资和最高工资后的工资平均值](https://leetcode.cn/problems/average-salary-excluding-the-minimum-and-maximum-salary/) - 标签:数组、排序 - 难度:简单 ## 题目链接 - [1491. 去掉最低工资和最高工资后的工资平均值 - 力扣](https://leetcode.cn/problems/average-salary-excluding-the-minimum-and-maximum-salary/) ## 题目大意 **描述**:给定一个整数数组 `salary`,数组中的每一个数都是唯一的,其中 `salary[i]` 是第 `i` 个员工的工资。 **要求**:返回去掉最低工资和最高工资之后,剩下员工工资的平均值。 **说明**: - $3 \le salary.length \le 100$。 - $10^3 \le salary[i] \le 10^6$。 - $salary[i]$ 是唯一的。 - 与真实值误差在 $10^{-5}$ 以内的结果都将视为正确答案。 **示例**: - 示例 1: ```python 给定 salary = [1000,2000,3000] 输出 2000.00000 解释 最低工资为 1000,最高工资为 3000,去除最低工资和最高工资之后,剩下员工工资的平均值为 2000 / 1 = 2000 ``` ## 解题思路 ### 思路 1: 因为给定 $salary.length \ge 3$,并且 $salary[i]$ 是唯一的,所以无需考虑最低工资和最高工资是同一个。接下来就是按照题意模拟过程: - 计算出最小工资为 `min_s`,即 `min_s = min(salary)`。 - 计算出最大工资为 `max_s`,即 `max_s = max(salary)`。 - 计算出所有工资和之后再减去最小工资和最大工资,即 `total = sum(salary) - min_s - max_s`。 - 求剩下工资的平均值,并返回,即 `return total / (len(salary) - 2)`。 ## 代码 ### 思路 1 代码: ```python class Solution: def average(self, salary: List[int]) -> float: min_s, max_s = min(salary), max(salary) total = sum(salary) - min_s - max_s return total / (len(salary) - 2) ``` ================================================ FILE: docs/solutions/1400-1499/consecutive-characters.md ================================================ # [1446. 连续字符](https://leetcode.cn/problems/consecutive-characters/) - 标签:字符串 - 难度:简单 ## 题目链接 - [1446. 连续字符 - 力扣](https://leetcode.cn/problems/consecutive-characters/) ## 题目大意 给你一个字符串 `s` ,字符串的「能量」定义为:只包含一种字符的最长非空子字符串的长度。 要求:返回字符串的能量。 注意: - `1 <= s.length <= 500` - `s` 只包含小写英文字母。 ## 解题思路 使用 `count` 统计连续不重复子串的长度,使用 `ans` 记录最长连续不重复子串的长度。 ## 代码 ```python class Solution: def maxPower(self, s: str) -> int: ans = 1 count = 1 for i in range(1, len(s)): if s[i] == s[i - 1]: count += 1 else: count = 1 ans = max(ans, count) return ans ``` ================================================ FILE: docs/solutions/1400-1499/construct-k-palindrome-strings.md ================================================ # [1400. 构造 K 个回文字符串](https://leetcode.cn/problems/construct-k-palindrome-strings/) - 标签:贪心、哈希表、字符串、计数 - 难度:中等 ## 题目链接 - [1400. 构造 K 个回文字符串 - 力扣](https://leetcode.cn/problems/construct-k-palindrome-strings/) ## 题目大意 **描述**:给定一个字符串 $s$ 和一个整数 $k$。 **要求**:用 $s$ 字符串中所有字符构造 $k$ 个非空回文串。如果可以用 $s$ 中所有字符构造 $k$ 个回文字符串,那么请你返回 `True`,否则返回 `False`。 **说明**: - $1 \le s.length \le 10^5$。 - $s$ 中所有字符都是小写英文字母。 - $1 \le k \le 10^5$。 **示例**: - 示例 1: ```python 输入:s = "annabelle", k = 2 输出:True 解释:可以用 s 中所有字符构造 2 个回文字符串。 一些可行的构造方案包括:"anna" + "elble","anbna" + "elle","anellena" + "b" ``` ## 解题思路 ### 思路 1:贪心算法 - 用字符串 $s$ 中所有字符构造回文串最多可以构造 $len(s)$ 个(将每个字符当做一个回文串)。所以如果 $len(s) < k$,则说明字符数量不够,无法构成 $k$ 个回文串,直接返回 `False`。 - 如果 $len(s) == k$,则可以直接使用单个字符构建回文串,直接返回 `True`。 - 如果 $len(s) > k$,则需要判断一下字符串 $s$ 中每个字符的个数。因为当字符是偶数个时,可以直接构造成回文串。所以我们只需要考虑个数为奇数的字符即可。如果个位为奇数的字符种类小于等于 $k$,则说明可以构造 $k$ 个回文串,返回 `True`。如果个位为奇数的字符种类大于 $k$,则说明无法构造 $k$ 个回文串,返回 `Fasle`。 ### 思路 1:贪心算法代码 ```python import collections class Solution: def canConstruct(self, s: str, k: int) -> bool: size = len(s) if size < k: return False if size == k: return True letter_dict = dict() for i in range(size): if s[i] in letter_dict: letter_dict[s[i]] += 1 else: letter_dict[s[i]] = 1 odd = 0 for key in letter_dict: if letter_dict[key] % 2 == 1: odd += 1 return odd <= k ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + |\sum|)$,其中 $n$ 为字符串 $s$ 的长度,$\sum$ 是字符集,本题中 $|\sum| = 26$。 - **空间复杂度**:$O(|\sum|)$。 ================================================ FILE: docs/solutions/1400-1499/form-largest-integer-with-digits-that-add-up-to-target.md ================================================ # [1449. 数位成本和为目标值的最大数字](https://leetcode.cn/problems/form-largest-integer-with-digits-that-add-up-to-target/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [1449. 数位成本和为目标值的最大数字 - 力扣](https://leetcode.cn/problems/form-largest-integer-with-digits-that-add-up-to-target/) ## 题目大意 **描述**:给定一个整数数组 $cost$ 和一个整数 $target$。现在从 `""` 开始,不断通过以下规则得到一个新的整数: 1. 给当前结果添加一个数位($i + 1$)的成本为 $cost[i]$($cost$ 数组下标从 $0$ 开始)。 2. 总成本必须恰好等于 $target$。 3. 添加的数位中没有数字 $0$。 **要求**:找到按照上述规则可以得到的最大整数。 **说明**: - 由于答案可能会很大,请你以字符串形式返回。 - 如果按照上述要求无法得到任何整数,请你返回 `"0"`。 - $cost.length == 9$。 - $1 \le cost[i] \le 5000$。 - $1 \le target \le 5000$。 **示例**: - 示例 1: ```python 输入:cost = [4,3,2,5,6,7,2,5,5], target = 9 输出:"7772" 解释:添加数位 '7' 的成本为 2 ,添加数位 '2' 的成本为 3 。所以 "7772" 的代价为 2*3+ 3*1 = 9 。 "977" 也是满足要求的数字,但 "7772" 是较大的数字。 数字 成本 1 -> 4 2 -> 3 3 -> 2 4 -> 5 5 -> 6 6 -> 7 7 -> 2 8 -> 5 9 -> 5 ``` - 示例 2: ```python 输入:cost = [7,6,5,5,5,6,8,7,8], target = 12 输出:"85" 解释:添加数位 '8' 的成本是 7 ,添加数位 '5' 的成本是 5 。"85" 的成本为 7 + 5 = 12。 数字 成本 1 -> 7 2 -> 6 3 -> 5 4 -> 5 5 -> 5 6 -> 6 7 -> 8 8 -> 7 9 -> 8 ``` ## 解题思路 把每个数位($1 \sim 9$)看做是一件物品,$cost[i]$ 看做是物品的重量,一共有无数件物品可以使用,$target$ 看做是背包的载重上限,得到的最大整数可以看做是背包的最大价值。那么问题就变为了「完全背包问题」中的「恰好装满背包的最大价值问题」。 因为答案可能会很大,要求以字符串形式返回。这里我们可以直接令 $dp[w]$ 为字符串形式,然后定义一个 `def maxInt(a, b):` 方法用于判断两个字符串代表的数字大小。 ### 思路 1:动态规划 ###### 1. 阶段划分 按照背包载重上限进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[w]$ 表示为:将物品装入一个最多能装重量为 $w$ 的背包中,恰好装满背包的情况下,能装入背包的最大整数。 ###### 3. 状态转移方程 $dp[w] = maxInt(dp[w], str(i) + dp[w - cost[i - 1]])$ ###### 4. 初始条件 1. 只有载重上限为 $0$ 的背包,在不放入物品时,能够恰好装满背包(有合法解),此时背包所含物品的最大价值为空字符串,即 `dp[0] = ""`。 2. 其他载重上限下的背包,在放入物品的时,都不能恰好装满背包(都没有合法解),此时背包所含物品的最大价值属于未定义状态,值为自定义字符 `"#"`,即 ,`dp[w] = "#"`,$0 \le w \le target$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[w]$ 表示为:将物品装入一个最多能装重量为 $w$ 的背包中,恰好装满背包的情况下,能装入背包的最大价值总和。 所以最终结果为 $dp[target]$。 ### 思路 1:代码 ```python class Solution: def largestNumber(self, cost: List[int], target: int) -> str: def maxInt(a, b): if len(a) == len(b): return max(a, b) if len(a) > len(b): return a return b size = len(cost) dp = ["#" for _ in range(target + 1)] dp[0] = "" for i in range(1, size + 1): for w in range(cost[i - 1], target + 1): if dp[w - cost[i - 1]] != "#": dp[w] = maxInt(dp[w], str(i) + dp[w - cost[i - 1]]) if dp[target] == "#": return "0" return dp[target] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times target)$,其中 $n$ 为数组 $cost$ 的元素个数,$target$ 为所给整数。 - **空间复杂度**:$O(target)$。 ================================================ FILE: docs/solutions/1400-1499/index.md ================================================ ## 本章内容 - [1400. 构造 K 个回文字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/construct-k-palindrome-strings.md) - [1408. 数组中的字符串匹配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/string-matching-in-an-array.md) - [1422. 分割字符串的最大得分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/maximum-score-after-splitting-a-string.md) - [1423. 可获得的最大点数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/maximum-points-you-can-obtain-from-cards.md) - [1438. 绝对差不超过限制的最长连续子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit.md) - [1446. 连续字符](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/consecutive-characters.md) - [1447. 最简分数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/simplified-fractions.md) - [1449. 数位成本和为目标值的最大数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/form-largest-integer-with-digits-that-add-up-to-target.md) - [1450. 在既定时间做作业的学生人数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/number-of-students-doing-homework-at-a-given-time.md) - [1451. 重新排列句子中的单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/rearrange-words-in-a-sentence.md) - [1456. 定长子串中元音的最大数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/maximum-number-of-vowels-in-a-substring-of-given-length.md) - [1476. 子矩形查询](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/subrectangle-queries.md) - [1480. 一维数组的动态和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/running-sum-of-1d-array.md) - [1482. 制作 m 束花所需的最少天数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/minimum-number-of-days-to-make-m-bouquets.md) - [1486. 数组异或操作](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/xor-operation-in-an-array.md) - [1491. 去掉最低工资和最高工资后的工资平均值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/average-salary-excluding-the-minimum-and-maximum-salary.md) - [1493. 删掉一个元素以后全为 1 的最长子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/longest-subarray-of-1s-after-deleting-one-element.md) - [1496. 判断路径是否相交](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/path-crossing.md) ================================================ FILE: docs/solutions/1400-1499/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit.md ================================================ # [1438. 绝对差不超过限制的最长连续子数组](https://leetcode.cn/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/) - 标签:队列、数组、有序集合、滑动窗口、单调队列、堆(优先队列) - 难度:中等 ## 题目链接 - [1438. 绝对差不超过限制的最长连续子数组 - 力扣](https://leetcode.cn/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/) ## 题目大意 给定一个整数数组 `nums`,和一个表示限制的整数 `limit`。 要求:返回最长连续子数组的长度,该子数组中的任意两个元素之间的绝对差必须小于或者等于 `limit`。 如果不存在满足条件的子数组,则返回 `0`。 ## 解题思路 求最长连续子数组,可以使用滑动窗口来解决。这道题目的难点在于如何维护滑动窗口内的最大值和最小值的差值。遍历滑动窗口求最大值和最小值,每次计算的时间复杂度为 $O(k)$,时间复杂度过高。考虑使用特殊的数据结构来降低时间复杂度。可以使用堆(优先队列)来解决。这里使用 `Python` 中 `heapq` 实现。具体做法如下: - 使用 `left`、`right` 两个指针,分别指向滑动窗口的左右边界,保证窗口中最大值和最小值的差值不超过 `limit`。 - 一开始,`left`、`right` 都指向 `0`。 - 向右移动 `right`,将最右侧元素加入当前窗口和大顶堆、小顶堆中。 - 如果大顶堆堆顶元素和小顶堆堆顶元素大于 `limit`,则不断右移 `left`,缩小滑动窗口长度,并更新窗口内的大顶堆、小顶堆。 - 如果大顶堆堆顶元素和小顶堆堆顶元素小于等于 `limit`,则更新最长连续子数组长度。 - 然后继续右移 `right`,直到 `right >= len(nums)` 结束。 - 输出答案。 ## 代码 ```python import heapq class Solution: def longestSubarray(self, nums: List[int], limit: int) -> int: size = len(nums) heap_max = [] heap_min = [] ans = 0 left, right = 0, 0 while right < size: heapq.heappush(heap_max, [-nums[right], right]) heapq.heappush(heap_min, [nums[right], right]) while -heap_max[0][0] - heap_min[0][0] > limit: while heap_min[0][1] <= left: heapq.heappop(heap_min) while heap_max[0][1] <= left: heapq.heappop(heap_max) left += 1 ans = max(ans, right - left + 1) right += 1 return ans ``` ================================================ FILE: docs/solutions/1400-1499/longest-subarray-of-1s-after-deleting-one-element.md ================================================ # [1493. 删掉一个元素以后全为 1 的最长子数组](https://leetcode.cn/problems/longest-subarray-of-1s-after-deleting-one-element/) - 标签:数组、动态规划、滑动窗口 - 难度:中等 ## 题目链接 - [1493. 删掉一个元素以后全为 1 的最长子数组 - 力扣](https://leetcode.cn/problems/longest-subarray-of-1s-after-deleting-one-element/) ## 题目大意 **描述**:给定一个二进制数组 $nums$,需要从数组中删掉一个元素。 **要求**:返回最长的且只包含 $1$ 的非空子数组的长度。如果不存在这样的子数组,请返回 $0$。 **说明**: - $1 \le nums.length \le 10^5$。 - $nums[i]$ 要么是 $0$ 要么是 $1$。 **示例**: - 示例 1: ```python 输入:nums = [1,1,0,1] 输出:3 解释:删掉位置 2 的数后,[1,1,1] 包含 3 个 1。 ``` - 示例 2: ```python 输入:nums = [0,1,1,1,0,1,1,0,1] 输出:5 解释:删掉位置 4 的数字后,[0,1,1,1,1,1,0,1] 的最长全 1 子数组为 [1,1,1,1,1]。 ``` ## 解题思路 ### 思路 1:滑动窗口 维护一个元素值为 $0$ 的元素数量少于 $1$ 个的滑动窗口。则答案为滑动窗口长度减去窗口内 $0$ 的个数求最大值。具体做法如下: 设定两个指针:$left$、$right$,分别指向滑动窗口的左右边界,保证窗口 $0$ 的个数小于 $1$ 个。使用 $window\_count$ 记录窗口中 $0$ 的个数,使用 $ans$ 记录删除一个元素后,最长的只包含 $1$ 的非空子数组长度。 - 一开始,$left$、$right$ 都指向 $0$。 - 如果最右侧元素等于 $0$,则 `window_count += 1` 。 - 如果 $window\_count > 1$ ,则不断右移 $left$,缩小滑动窗口长度。并更新当前窗口中 $0$ 的个数,直到 $window\_count \le 1$。 - 更新答案值,然后向右移动 $right$,直到 $right \ge len(nums)$ 结束。 - 输出答案 $ans$。 ### 思路 1:代码 ```python class Solution: def longestSubarray(self, nums: List[int]) -> int: left, right = 0, 0 window_count = 0 ans = 0 while right < len(nums): if nums[right] == 0: window_count += 1 while window_count > 1: if nums[left] == 0: window_count -= 1 left += 1 ans = max(ans, right - left + 1 - window_count) right += 1 if ans == len(nums): return len(nums) - 1 else: return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1400-1499/maximum-number-of-vowels-in-a-substring-of-given-length.md ================================================ # [1456. 定长子串中元音的最大数目](https://leetcode.cn/problems/maximum-number-of-vowels-in-a-substring-of-given-length/) - 标签:字符串、滑动窗口 - 难度:中等 ## 题目链接 - [1456. 定长子串中元音的最大数目 - 力扣](https://leetcode.cn/problems/maximum-number-of-vowels-in-a-substring-of-given-length/) ## 题目大意 **描述**:给定字符串 $s$ 和整数 $k$。 **要求**:返回字符串 $s$ 中长度为 $k$ 的单个子字符串中可能包含的最大元音字母数。 **说明**: - 英文中的元音字母为($a$, $e$, $i$, $o$, $u$)。 - $1 <= s.length <= 10^5$。 - $s$ 由小写英文字母组成。 - $1 <= k <= s.length$。 **示例**: - 示例 1: ```python 输入:s = "abciiidef", k = 3 输出:3 解释:子字符串 "iii" 包含 3 个元音字母。 ``` - 示例 2: ```python 输入:s = "aeiou", k = 2 输出:2 解释:任意长度为 2 的子字符串都包含 2 个元音字母。 ``` ## 解题思路 ### 思路 1:滑动窗口 固定长度的滑动窗口题目。维护一个长度为 $k$ 的窗口,并统计滑动窗口中最大元音字母数。具体做法如下: 1. $ans$ 用来维护长度为 $k$ 的单个字符串中最大元音字母数。$window\_count$ 用来维护窗口中元音字母数。集合 $vowel\_set$ 用来存储元音字母。 2. $left$ 、$right$ 都指向字符串 $s$ 的第一个元素,即:$left = 0$,$right = 0$。 3. 判断 $s[right]$ 是否在元音字母集合中,如果在则用 $window\_count$ 进行计数。 4. 当窗口元素个数为 $k$ 时,即:$right - left + 1 \ge k$ 时,更新 $ans$。然后判断 $s[left]$ 是否为元音字母,如果是则 `window_count -= 1`,并向右移动 $left$,从而缩小窗口长度,即 `left += 1`,使得窗口大小始终保持为 $k$。 5. 重复 $3 \sim 4$ 步,直到 $right$ 到达数组末尾。 6. 最后输出 $ans$。 ### 思路 1:代码 ```python class Solution: def maxVowels(self, s: str, k: int) -> int: left, right = 0, 0 ans = 0 window_count = 0 vowel_set = ('a','e','i','o','u') while right < len(s): if s[right] in vowel_set: window_count += 1 if right - left + 1 >= k: ans = max(ans, window_count) if s[left] in vowel_set: window_count -= 1 left += 1 right += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1400-1499/maximum-points-you-can-obtain-from-cards.md ================================================ # [1423. 可获得的最大点数](https://leetcode.cn/problems/maximum-points-you-can-obtain-from-cards/) - 标签:数组、前缀和、滑动窗口 - 难度:中等 ## 题目链接 - [1423. 可获得的最大点数 - 力扣](https://leetcode.cn/problems/maximum-points-you-can-obtain-from-cards/) ## 题目大意 **描述**:将卡牌排成一行,给定每张卡片的点数数组 $cardPoints$,其中 $cardPoints[i]$ 表示第 $i$ 张卡牌对应点数。 每次行动,可以从行的开头或者末尾拿一张卡牌,最终保证正好拿到了 $k$ 张卡牌。所得点数就是你拿到手中的所有卡牌的点数之和。 现在给定一个整数数组 $cardPoints$ 和整数 $k$。 **要求**:返回可以获得的最大点数。 **说明**: - $1 \le cardPoints.length \le 10^5$。 - $1 \le cardPoints[i] \le 10^4$ - $1 \le k \le cardPoints.length$。 **示例**: - 示例 1: ```python 输入:cardPoints = [1,2,3,4,5,6,1], k = 3 输出:12 解释:第一次行动,不管拿哪张牌,你的点数总是 1 。但是,先拿最右边的卡牌将会最大化你的可获得点数。最优策略是拿右边的三张牌,最终点数为 1 + 6 + 5 = 12。 ``` - 示例 2: ```python 输入:cardPoints = [2,2,2], k = 2 输出:4 解释:无论你拿起哪两张卡牌,可获得的点数总是 4。 ``` ## 解题思路 ### 思路 1:滑动窗口 可以用固定长度的滑动窗口来做。 由于只能从开头或末尾位置拿 $k$ 张牌,则最后剩下的肯定是连续的 $len(cardPoints) - k$ 张牌。要求求出 $k$ 张牌可以获得的最大收益,我们可以反向先求出连续 $len(cardPoints) - k$ 张牌的最小点数。则答案为 $sum(cardPoints) - min\_sum$。维护一个固定长度为 $len(cardPoints) - k$ 的滑动窗口,求最小和。具体做法如下: 1. $window\_sum$ 用来维护窗口内的元素和,初始值为 $0$。$min\_sum$ 用来维护滑动窗口元素的最小和。初始值为 $sum(cardPoints)$。滑动窗口的长度为 $window\_size$,值为 $len(cardPoints) - k$。 2. 使用双指针 $left$、$right$。$left$ 、$right$ 都指向序列的第一个元素,即:`left = 0`,`right = 0`。 3. 向右移动 $right$,先将 $window\_size$ 个元素填入窗口中。 4. 当窗口元素个数为 $window\_size$ 时,即:$right - left + 1 \ge window\_size$ 时,计算窗口内的元素和,并维护子数组最小和 $min\_sum$。 5. 然后向右移动 $left$,从而缩小窗口长度,即 `left += 1`,使得窗口大小始终保持为 $k$。 6. 重复 4 ~ 5 步,直到 $right$ 到达数组末尾。 7. 最后输出 $sum(cardPoints) - min\_sum$ 即为答案。 注意:如果 $window\_size$ 为 $0$ 时需要特殊判断,此时答案为数组和 $sum(cardPoints)$。 ### 思路 1:代码 ```python class Solution: def maxScore(self, cardPoints: List[int], k: int) -> int: window_size = len(cardPoints) - k window_sum = 0 cards_sum = sum(cardPoints) min_sum = cards_sum left, right = 0, 0 if window_size == 0: return cards_sum while right < len(cardPoints): window_sum += cardPoints[right] if right - left + 1 >= window_size: min_sum = min(window_sum, min_sum) window_sum -= cardPoints[left] left += 1 right += 1 return cards_sum - min_sum ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $cardPoints$ 中的元素数量。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1400-1499/maximum-score-after-splitting-a-string.md ================================================ # [1422. 分割字符串的最大得分](https://leetcode.cn/problems/maximum-score-after-splitting-a-string/) - 标签:字符串 - 难度:简单 ## 题目链接 - [1422. 分割字符串的最大得分 - 力扣](https://leetcode.cn/problems/maximum-score-after-splitting-a-string/) ## 题目大意 **描述**:给定一个由若干 $0$ 和 $1$ 组成的字符串。将字符串分割成两个非空子字符串的得分为:左子字符串中 $0$ 的数量 + 右子字符串中 $1$ 的数量。 **要求**:计算并返回该字符串分割成两个非空子字符串(即左子字符串和右子字符串)所能获得的最大得分。 **说明**: - $2 \le s.length \le 500$。 - 字符串 $s$ 仅由字符 $0$ 和 $1$ 组成。 **示例**: - 示例 1: ```python 输入:s = "011101" 输出:5 解释: 将字符串 s 划分为两个非空子字符串的可行方案有: 左子字符串 = "0" 且 右子字符串 = "11101",得分 = 1 + 4 = 5 左子字符串 = "01" 且 右子字符串 = "1101",得分 = 1 + 3 = 4 左子字符串 = "011" 且 右子字符串 = "101",得分 = 1 + 2 = 3 左子字符串 = "0111" 且 右子字符串 = "01",得分 = 1 + 1 = 2 左子字符串 = "01110" 且 右子字符串 = "1",得分 = 2 + 1 = 3 ``` - 示例 2: ```python 输入:s = "00111" 输出:5 解释:当 左子字符串 = "00" 且 右子字符串 = "111" 时,我们得到最大得分 = 2 + 3 = 5 ``` ## 解题思路 ### 思路 1:前缀和 1. 遍历字符串 $s$,使用前缀和数组来记录每个前缀子字符串中 $1$ 的个数。 2. 再次遍历字符串 $s$,枚举每个分割点,利用前缀和数组计算出当前分割出的左子字符串中 $1$ 的个数与右子字符串中 $0$ 的个数,并计算当前得分,然后更新最大得分。 3. 返回最大得分作为答案。 ### 思路 1:代码 ```python class Solution: def maxScore(self, s: str) -> int: size = len(s) one_cnts = [0 for _ in range(size + 1)] for i in range(1, size + 1): if s[i - 1] == '1': one_cnts[i] = one_cnts[i - 1] + 1 else: one_cnts[i] = one_cnts[i - 1] ans = 0 for i in range(1, size): left_score = i - one_cnts[i] right_score = one_cnts[size] - one_cnts[i] ans = max(ans, left_score + right_score) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1400-1499/minimum-number-of-days-to-make-m-bouquets.md ================================================ # [1482. 制作 m 束花所需的最少天数](https://leetcode.cn/problems/minimum-number-of-days-to-make-m-bouquets/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [1482. 制作 m 束花所需的最少天数 - 力扣](https://leetcode.cn/problems/minimum-number-of-days-to-make-m-bouquets/) ## 题目大意 **描述**:给定一个整数数组 $bloomDay$,以及两个整数 $m$ 和 $k$。$bloomDay$ 代表花朵盛开的时间,$bloomDay[i]$ 表示第 $i$ 朵花的盛开时间。盛开后就可以用于一束花中。 现在需要制作 $m$ 束花。制作花束时,需要使用花园中相邻的 $k$ 朵花 。 **要求**:返回从花园中摘 $m$ 束花需要等待的最少的天数。如果不能摘到 $m$ 束花则返回 $-1$。 **说明**: - $bloomDay.length == n$。 - $1 \le n \le 10^5$。 - $1 \le bloomDay[i] \le 10^9$。 - $1 \le m \le 10^6$。 - $1 \le k \le n$。 **示例**: - 示例 1: ```python 输入:bloomDay = [1,10,3,10,2], m = 3, k = 1 输出:3 解释:让我们一起观察这三天的花开过程,x 表示花开,而 _ 表示花还未开。 现在需要制作 3 束花,每束只需要 1 朵。 1 天后:[x, _, _, _, _] // 只能制作 1 束花 2 天后:[x, _, _, _, x] // 只能制作 2 束花 3 天后:[x, _, x, _, x] // 可以制作 3 束花,答案为 3 ``` - 示例 2: ```python 输入:bloomDay = [1,10,3,10,2], m = 3, k = 2 输出:-1 解释:要制作 3 束花,每束需要 2 朵花,也就是一共需要 6 朵花。而花园中只有 5 朵花,无法满足制作要求,返回 -1。 ``` ## 解题思路 ### 思路 1:二分查找算法 这道题跟「[0875. 爱吃香蕉的珂珂](https://leetcode.cn/problems/koko-eating-bananas/)」、「[1011. 在 D 天内送达包裹的能力](https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days/)」有点相似。 根据题目可知: - 制作花束最少使用时间跟花朵开花最短时间有关系,即 $min(bloomDay)$。 - 制作花束最多使用时间跟花朵开花最长时间有关系,即 $max(bloomDay)$。 - 则制作花束所需要的天数就变成了一个区间 $[min(bloomDay), max(bloomDay)]$。 那么,我们就可以根据这个区间,利用二分查找算法找到一个符合题意的最少天数。而判断某个天数下能否摘到 $m$ 束花则可以写个方法判断。具体步骤如下: - 遍历数组 $bloomDay$。 - 如果 $bloomDay[i] \le days$。就将花朵数量加 $1$。 - 当能摘的花朵数等于 $k$ 时,能摘的花束数目加 $1$,花朵数量置为 $0$。 - 如果 $bloomDay[i] > days$。就将花朵数置为 $0$。 - 最后判断能摘的花束数目是否大于等于 $m$。 整个算法的步骤如下: - 如果 $m \times k > len(bloomDay)$,说明无法满足要求,直接返回 $-1$。 - 使用两个指针 $left$、$right$。令 $left$ 指向 $min(bloomDay)$,$right$ 指向 $max(bloomDay)$。代表待查找区间为 $[left, right]$。 - 取两个节点中心位置 $mid$,判断是否能在 $mid$ 天制作 $m$ 束花。 - 如果不能,则将区间 $[left, mid]$ 排除掉,继续在区间 $[mid + 1, right]$ 中查找。 - 如果能,说明天数还可以继续减少,则继续在区间 $[left, mid]$ 中查找。 - 当 $left == right$ 时跳出循环,返回 $left$。 ### 思路 1:代码 ```python class Solution: def canMake(self, bloomDay, days, m, k): count = 0 flower = 0 for i in range(len(bloomDay)): if bloomDay[i] <= days: flower += 1 if flower == k: count += 1 flower = 0 else: flower = 0 return count >= m def minDays(self, bloomDay: List[int], m: int, k: int) -> int: if m > len(bloomDay) / k: return -1 left, right = min(bloomDay), max(bloomDay) while left < right: mid = left + (right - left) // 2 if not self.canMake(bloomDay, mid, m, k): left = mid + 1 else: right = mid return left ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log (max(bloomDay) - min(bloomDay)))$。 - **空间复杂度**:$O(1)$。 ## 参考资料 - 【题解】[【赤小豆】为什么是二分法,思路及模板 python - 制作 m 束花所需的最少天数 - 力扣(LeetCode)](https://leetcode.cn/problems/minimum-number-of-days-to-make-m-bouquets/solution/chi-xiao-dou-python-wei-shi-yao-shi-er-f-24p7/) ================================================ FILE: docs/solutions/1400-1499/number-of-students-doing-homework-at-a-given-time.md ================================================ # [1450. 在既定时间做作业的学生人数](https://leetcode.cn/problems/number-of-students-doing-homework-at-a-given-time/) - 标签:数组 - 难度:简单 ## 题目链接 - [1450. 在既定时间做作业的学生人数 - 力扣](https://leetcode.cn/problems/number-of-students-doing-homework-at-a-given-time/) ## 题目大意 **描述**:给你两个长度相等的整数数组,一个表示开始时间的数组 $startTime$ ,另一个表示结束时间的数组 $endTime$。再给定一个整数 $queryTime$ 作为查询时间。已知第 $i$ 名学生在 $startTime[i]$ 时开始写作业并于 $endTime[i]$ 时完成作业。 **要求**:返回在查询时间 $queryTime$ 时正在做作业的学生人数。即能够使 $queryTime$ 处于区间 $[startTime[i], endTime[i]]$ 的学生人数。 **说明**: - $startTime.length == endTime.length$。 - $1\le startTime.length \le 100$。 - $1 \le startTime[i] \le endTime[i] \le 1000$。 - $1 \le queryTime \le 1000$。 **示例**: - 示例 1: ```python 输入:startTime = [4], endTime = [4], queryTime = 4 输出:1 解释:在查询时间只有一名学生在做作业。 ``` ## 解题思路 ### 思路 1:枚举算法 - 维护一个用于统计在查询时间 $queryTime$ 时正在做作业的学生人数的变量 $cnt$。然后遍历所有学生的开始时间和结束时间。 - 如果 $queryTime$ 在区间 $[startTime[i], endTime[i]]$ 之间,即 $startTime[i] <= queryTime <= endTime[i]$,则令 $cnt$ 加 $1$。 - 遍历完输出统计人数 $cnt$。 ### 思路 1:枚举算法代码 ```python class Solution: def busyStudent(self, startTime: List[int], endTime: List[int], queryTime: int) -> int: cnt = 0 size = len(startTime) for i in range(size): if startTime[i] <= queryTime <= endTime[i]: cnt += 1 return cnt ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组中的元素个数。 - **空间复杂度**:$O(1)$。 ### 思路 2:线段树 - 因为 $1 \le startTime[i] \le endTime[i] \le 1000$,所以我们可以维护一个区间为 $[0, 1000]$ 的线段树,初始化所有区间值都为 $0$。 - 然后遍历所有学生的开始时间和结束时间,并将区间 $[startTime[i], endTime[i]]$ 值加 $1$。 - 在线段树中查询 $queryTime$ 对应的单点区间 $[queryTime, queryTime]$ 的最大值为多少。 ### 思路 2:线段树代码 ```python # 线段树的节点类 class SegTreeNode: def __init__(self, val=0): self.left = -1 # 区间左边界 self.right = -1 # 区间右边界 self.val = val # 节点值(区间值) self.lazy_tag = None # 区间和问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, nums, function): self.size = len(nums) self.tree = [SegTreeNode() for _ in range(4 * self.size)] # 维护 SegTreeNode 数组 self.nums = nums # 原始数据 self.function = function # function 是一个函数,左右区间的聚合方法 if self.size > 0: self.__build(0, 0, self.size - 1) # 单点更新接口:将 nums[i] 更改为 val def update_point(self, i, val): self.nums[i] = val self.__update_point(i, val, 0) # 区间更新接口:将区间为 [q_left, q_right] 上的所有元素值加上 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, 0) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, 0) # 获取 nums 数组接口:返回 nums 数组 def get_nums(self): for i in range(self.size): self.nums[i] = self.query_interval(i, i) return self.nums # 以下为内部实现方法 # 构建线段树实现方法:节点的存储下标为 index,节点的区间为 [left, right] def __build(self, index, left, right): self.tree[index].left = left self.tree[index].right = right if left == right: # 叶子节点,节点值为对应位置的元素值 self.tree[index].val = self.nums[left] return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.__build(left_index, left, mid) # 递归创建左子树 self.__build(right_index, mid + 1, right) # 递归创建右子树 self.__pushup(index) # 向上更新节点的区间值 # 单点更新实现方法:将 nums[i] 更改为 val,节点的存储下标为 index def __update_point(self, i, val, index): left = self.tree[index].left right = self.tree[index].right if left == right: self.tree[index].val = val # 叶子节点,节点值修改为 val return mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if i <= mid: # 在左子树中更新节点值 self.__update_point(i, val, left_index) else: # 在右子树中更新节点值 self.__update_point(i, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间更新实现方法 def __update_interval(self, q_left, q_right, val, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 if self.tree[index].lazy_tag is not None: self.tree[index].lazy_tag += val # 将当前节点的延迟标记增加 val else: self.tree[index].lazy_tag = val # 将当前节点的延迟标记增加 val interval_size = (right - left + 1) # 当前节点所在区间大小 self.tree[index].val += val * interval_size # 当前节点所在区间每个元素值增加 val return if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return self.__pushdown(index) # 向下更新节点的区间值 mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if q_left <= mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, left_index) if q_right > mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, right_index) self.__pushup(index) # 向上更新节点的区间值 # 区间查询实现方法:在线段树中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, index): left = self.tree[index].left right = self.tree[index].right if left >= q_left and right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return self.tree[index].val # 直接返回节点值 if right < q_left or left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(index) mid = left + (right - left) // 2 # 左右节点划分点 left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, left_index) if q_right > mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, right_index) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:更新下标为 index 的节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, index): left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 self.tree[index].val = self.function(self.tree[left_index].val, self.tree[right_index].val) # 向下更新实现方法:更新下标为 index 的节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, index): lazy_tag = self.tree[index].lazy_tag if lazy_tag is None: return left_index = index * 2 + 1 # 左子节点的存储下标 right_index = index * 2 + 2 # 右子节点的存储下标 if self.tree[left_index].lazy_tag is not None: self.tree[left_index].lazy_tag += lazy_tag # 更新左子节点懒惰标记 else: self.tree[left_index].lazy_tag = lazy_tag left_size = (self.tree[left_index].right - self.tree[left_index].left + 1) self.tree[left_index].val += lazy_tag * left_size # 左子节点每个元素值增加 lazy_tag if self.tree[right_index].lazy_tag is not None: self.tree[right_index].lazy_tag += lazy_tag # 更新右子节点懒惰标记 else: self.tree[right_index].lazy_tag = lazy_tag right_size = (self.tree[right_index].right - self.tree[right_index].left + 1) self.tree[right_index].val += lazy_tag * right_size # 右子节点每个元素值增加 lazy_tag self.tree[index].lazy_tag = None # 更新当前节点的懒惰标记 class Solution: def busyStudent(self, startTime: List[int], endTime: List[int], queryTime: int) -> int: nums = [0 for _ in range(1010)] self.STree = SegmentTree(nums, lambda x, y: max(x, y)) size = len(startTime) for i in range(size): self.STree.update_interval(startTime[i], endTime[i], 1) return self.STree.query_interval(queryTime, queryTime) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 为数组元素的个数。 - **空间复杂度**:$O(n)$。 ### 思路 3:树状数组 - 因为 $1 \le startTime[i] \le endTime[i] \le 1000$,所以我们可以维护一个区间为 $[0, 1000]$ 的树状数组。 - 注意: - 树状数组中 $update(self, index, delta):$ 指的是将对应元素 $nums[index] $ 加上 $delta$。 - $query(self, index):$ 指的是 $index$ 位置之前的元素和,即前缀和。 - 然后遍历所有学生的开始时间和结束时间,将树状数组上 $startTime[i]$ 的值增加 $1$,再将树状数组上$endTime[i]$ 的值减少 $1$。 - 则查询 $queryTime$ 位置的前缀和即为答案。 ### 思路 3:树状数组代码 ```python class BinaryIndexTree: def __init__(self, n): self.size = n self.tree = [0 for _ in range(n + 1)] def lowbit(self, index): return index & (-index) def update(self, index, delta): while index <= self.size: self.tree[index] += delta index += self.lowbit(index) def query(self, index): res = 0 while index > 0: res += self.tree[index] index -= self.lowbit(index) return res class Solution: def busyStudent(self, startTime: List[int], endTime: List[int], queryTime: int) -> int: bit = BinaryIndexTree(1010) size = len(startTime) for i in range(size): bit.update(startTime[i], 1) bit.update(endTime[i] + 1, -1) return bit.query(queryTime) ``` ### 思路 3:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 为数组元素的个数。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1400-1499/path-crossing.md ================================================ # [1496. 判断路径是否相交](https://leetcode.cn/problems/path-crossing/) - 标签:哈希表、字符串 - 难度:简单 ## 题目链接 - [1496. 判断路径是否相交 - 力扣](https://leetcode.cn/problems/path-crossing/) ## 题目大意 **描述**:给定一个字符串 $path$,其中 $path[i]$ 的值可以是 `'N'`、`'S'`、`'E'` 或者 `'W'`,分别表示向北、向南、向东、向西移动一个单位。 你从二维平面上的原点 $(0, 0)$ 处开始出发,按 $path$ 所指示的路径行走。 **要求**:如果路径在任何位置上与自身相交,也就是走到之前已经走过的位置,请返回 $True$;否则,返回 $False$。 **说明**: - $1 \le path.length \le 10^4$。 - $path[i]$ 为 `'N'`、`'S'`、`'E'` 或 `'W'`。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/06/28/screen-shot-2020-06-10-at-123929-pm.png) ```python 输入:path = "NES" 输出:false 解释:该路径没有在任何位置相交。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/06/28/screen-shot-2020-06-10-at-123843-pm.png) ```python 输入:path = "NESWW" 输出:true 解释:该路径经过原点两次。 ``` ## 解题思路 ### 思路 1:哈希表 + 模拟 1. 使用哈希表将 `'N'`、`'S'`、`'E'`、`'W'` 对应横纵坐标轴上的改变表示出来。 2. 使用集合 $visited$ 存储走过的坐标元组。 3. 遍历 $path$,按照 $path$ 所指示的路径模拟行走,并将所走过的坐标使用 $visited$ 存储起来。 4. 如果在 $visited$ 遇到已经走过的坐标,则返回 $True$。 5. 如果遍历完仍未发现已经走过的坐标,则返回 $False$。 ### 思路 1:代码 ```Python class Solution: def isPathCrossing(self, path: str) -> bool: directions = { "N" : (-1, 0), "S" : (1, 0), "W" : (0, -1), "E" : (0, 1), } x, y = 0, 0 visited = set() visited.add((x, y)) for ch in path: x += directions[ch][0] y += directions[ch][1] if (x, y) in visited: return True visited.add((x, y)) return False ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $path$ 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1400-1499/rearrange-words-in-a-sentence.md ================================================ # [1451. 重新排列句子中的单词](https://leetcode.cn/problems/rearrange-words-in-a-sentence/) - 标签:字符串、排序 - 难度:中等 ## 题目链接 - [1451. 重新排列句子中的单词 - 力扣](https://leetcode.cn/problems/rearrange-words-in-a-sentence/) ## 题目大意 **描述**:「句子」是一个用空格分隔单词的字符串。给定一个满足下述格式的句子 $text$: - 句子的首字母大写。 - $text$ 中的每个单词都用单个空格分隔。 **要求**:重新排列 $text$ 中的单词,使所有单词按其长度的升序排列。如果两个单词的长度相同,则保留其在原句子中的相对顺序。 请同样按上述格式返回新的句子。 **说明**: - $text$ 以大写字母开头,然后包含若干小写字母以及单词间的单个空格。 - $1 \le text.length \le 10^5$。 **示例**: - 示例 1: ```python 输入:text = "Leetcode is cool" 输出:"Is cool leetcode" 解释:句子中共有 3 个单词,长度为 8 的 "Leetcode" ,长度为 2 的 "is" 以及长度为 4 的 "cool"。 输出需要按单词的长度升序排列,新句子中的第一个单词首字母需要大写。 ``` - 示例 2: ```python 输入:text = "Keep calm and code on" 输出:"On and keep calm code" 解释:输出的排序情况如下: "On" 2 个字母。 "and" 3 个字母。 "keep" 4 个字母,因为存在长度相同的其他单词,所以它们之间需要保留在原句子中的相对顺序。 "calm" 4 个字母。 "code" 4 个字母。 ``` ## 解题思路 ### 思路 1:模拟 1. 将 $text$ 按照 `" "` 进行分割为单词数组 $words$。 2. 将单词数组按照「单词长度」进行升序排序。 3. 将单词数组用 `" "` 连接起来,并将首字母转为大写字母,其他字母转为小写字母,将结果存入答案字符串 $ans$ 中。 4. 返回答案字符串 $ans$。 ### 思路 1:代码 ```Python class Solution: def arrangeWords(self, text: str) -> str: words = text.split(' ') words.sort(key=lambda word:len(word)) ans = " ".join(words).capitalize() return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 为字符串 $text$ 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1400-1499/running-sum-of-1d-array.md ================================================ # [1480. 一维数组的动态和](https://leetcode.cn/problems/running-sum-of-1d-array/) - 标签:数组、前缀和 - 难度:简单 ## 题目链接 - [1480. 一维数组的动态和 - 力扣](https://leetcode.cn/problems/running-sum-of-1d-array/) ## 题目大意 **描述**:给定一个数组 $nums$。 **要求**:返回数组 $nums$ 的动态和。 **说明**: - **动态和**:数组前 $i$ 项元素和构成的数组,计算公式为 $runningSum[i] = \sum_{x = 0}^{x = i}(nums[i])$。 - $1 \le nums.length \le 1000$。 - $-10^6 \le nums[i] \le 10^6$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,4] 输出:[1,3,6,10] 解释:动态和计算过程为 [1, 1+2, 1+2+3, 1+2+3+4]。 ``` - 示例 2: ```python 输入:nums = [1,1,1,1,1] 输出:[1,2,3,4,5] 解释:动态和计算过程为 [1, 1+1, 1+1+1, 1+1+1+1, 1+1+1+1+1]。 ``` ## 解题思路 ### 思路 1:递推 根据动态和的公式 $runningSum[i] = \sum_{x = 0}^{x = i}(nums[i])$,可以推导出: $runningSum = \begin{cases} nums[0], & i = 0 \cr runningSum[i - 1] + nums[i], & i > 0\end{cases}$ 则解决过程如下: 1. 新建一个长度等于 $nums$ 的数组 $res$ 用于存放答案。 2. 初始化 $res[0] = nums[0]$。 3. 从下标 $1$ 开始遍历数组 $nums$,递推更新 $res$,即:`res[i] = res[i - 1] + nums[i]`。 4. 遍历结束,返回 $res$ 作为答案。 ### 思路 1:代码 ```python class Solution: def runningSum(self, nums: List[int]) -> List[int]: size = len(nums) res = [0 for _ in range(size)] for i in range(size): if i == 0: res[i] = nums[i] else: res[i] = res[i - 1] + nums[i] return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。 - **空间复杂度**:$O(n)$。如果算上答案数组的空间占用,则空间复杂度为 $O(n)$。不算上则空间复杂度为 $O(1)$。 ================================================ FILE: docs/solutions/1400-1499/simplified-fractions.md ================================================ # [1447. 最简分数](https://leetcode.cn/problems/simplified-fractions/) - 标签:数学、字符串、数论 - 难度:中等 ## 题目链接 - [1447. 最简分数 - 力扣](https://leetcode.cn/problems/simplified-fractions/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:返回所有 $0$ 到 $1$ 之间(不包括 $0$ 和 $1$)满足分母小于等于 $n$ 的最简分数。分数可以以任意顺序返回。 **说明**: - $1 \le n \le 100$。 **示例**: - 示例 1: ```python 输入:n = 2 输出:["1/2"] 解释:"1/2" 是唯一一个分母小于等于 2 的最简分数。 ``` - 示例 2: ```python 输入:n = 4 输出:["1/2","1/3","1/4","2/3","3/4"] 解释:"2/4" 不是最简分数,因为它可以化简为 "1/2"。 ``` ## 解题思路 ### 思路 1:数学 如果分子和分母的最大公约数为 $1$ 时,则当前分数为最简分数。 而 $n$ 的数据范围为 $(1, 100)$。因此我们可以使用两重遍历,分别枚举分子和分母,然后通过判断分子和分母是否为最大公约数,来确定当前分数是否为最简分数。 ### 思路 1:代码 ```python class Solution: def simplifiedFractions(self, n: int) -> List[str]: res = [] for i in range(1, n): for j in range(i + 1, n + 1): if math.gcd(i, j) == 1: res.append(str(i) + "/" + str(j)) return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \times \log n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1400-1499/string-matching-in-an-array.md ================================================ # [1408. 数组中的字符串匹配](https://leetcode.cn/problems/string-matching-in-an-array/) - 标签:数组、字符串、字符串匹配 - 难度:简单 ## 题目链接 - [1408. 数组中的字符串匹配 - 力扣](https://leetcode.cn/problems/string-matching-in-an-array/) ## 题目大意 **描述**:给定一个字符串数组 `words`,数组中的每个字符串都可以看作是一个单词。如果可以删除 `words[j]` 最左侧和最右侧的若干字符得到 `word[i]`,那么字符串 `words[i]` 就是 `words[j]` 的一个子字符串。 **要求**:按任意顺序返回 `words` 中是其他单词的子字符串的所有单词。 **说明**: - $1 \le words.length \le 100$。 - $1 \le words[i].length \le 30$ - `words[i]` 仅包含小写英文字母。 - 题目数据保证每个 `words[i]` 都是独一无二的。 **示例**: - 示例 1: ```python 输入:words = ["mass","as","hero","superhero"] 输出:["as","hero"] 解释:"as" 是 "mass" 的子字符串,"hero" 是 "superhero" 的子字符串。此外,["hero","as"] 也是有效的答案。 ``` ## 解题思路 ### 思路 1:KMP 算法 1. 先按照字符串长度从小到大排序,使用数组 `res` 保存答案。 2. 使用两重循环遍历,对于 `words[i]` 和 `words[j]`,使用 `KMP` 匹配算法,如果 `wrods[j]` 包含 `words[i]`,则将其加入到答案数组中,并跳出最里层循环。 3. 返回答案数组 `res`。 ### 思路 1:代码 ```python class Solution: # 生成 next 数组 # next[j] 表示下标 j 之前的模式串 p 中,最长相等前后缀的长度 def generateNext(self, p: str): m = len(p) next = [0 for _ in range(m)] # 初始化数组元素全部为 0 left = 0 # left 表示前缀串开始所在的下标位置 for right in range(1, m): # right 表示后缀串开始所在的下标位置 while left > 0 and p[left] != p[right]: # 匹配不成功, left 进行回退, left == 0 时停止回退 left = next[left - 1] # left 进行回退操作 if p[left] == p[right]: # 匹配成功,找到相同的前后缀,先让 left += 1,此时 left 为前缀长度 left += 1 next[right] = left # 记录前缀长度,更新 next[right], 结束本次循环, right += 1 return next # KMP 匹配算法,T 为文本串,p 为模式串 def kmp(self, T: str, p: str) -> int: n, m = len(T), len(p) next = self.generateNext(p) # 生成 next 数组 j = 0 # j 为模式串中当前匹配的位置 for i in range(n): # i 为文本串中当前匹配的位置 while j > 0 and T[i] != p[j]: # 如果模式串前缀匹配不成功, 将模式串进行回退, j == 0 时停止回退 j = next[j - 1] if T[i] == p[j]: # 当前模式串前缀匹配成功,令 j += 1,继续匹配 j += 1 if j == m: # 当前模式串完全匹配成功,返回匹配开始位置 return i - j + 1 return -1 # 匹配失败,返回 -1 def stringMatching(self, words: List[str]) -> List[str]: words.sort(key=lambda x:len(x)) res = [] for i in range(len(words) - 1): for j in range(i + 1, len(words)): if self.kmp(words[j], words[i]) != -1: res.append(words[i]) break return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2 \times m)$,其中字符串数组长度为 $n$,字符串数组中最长字符串长度为 $m$。 - **空间复杂度**:$O(m)$。 ================================================ FILE: docs/solutions/1400-1499/subrectangle-queries.md ================================================ # [1476. 子矩形查询](https://leetcode.cn/problems/subrectangle-queries/) - 标签:设计、数组、矩阵 - 难度:中等 ## 题目链接 - [1476. 子矩形查询 - 力扣](https://leetcode.cn/problems/subrectangle-queries/) ## 题目大意 **要求**:实现一个类 SubrectangleQueries,它的构造函数的参数是一个 $rows \times cols $的矩形(这里用整数矩阵表示),并支持以下两种操作: 1. `updateSubrectangle(int row1, int col1, int row2, int col2, int newValue)`:用 $newValue$ 更新以 $(row1,col1)$ 为左上角且以 $(row2,col2)$ 为右下角的子矩形。 2. `getValue(int row, int col)`:返回矩形中坐标 (row,col) 的当前值。 **说明**: - 最多有 $500$ 次 `updateSubrectangle` 和 `getValue` 操作。 - $1 <= rows, cols <= 100$。 - $rows == rectangle.length$。 - $cols == rectangle[i].length$。 - $0 <= row1 <= row2 < rows$。 - $0 <= col1 <= col2 < cols$。 - $1 <= newValue, rectangle[i][j] <= 10^9$。 - $0 <= row < rows$。 - $0 <= col < cols$。 **示例**: - 示例 1: ```python 输入: ["SubrectangleQueries","getValue","updateSubrectangle","getValue","getValue","updateSubrectangle","getValue","getValue"] [[[[1,2,1],[4,3,4],[3,2,1],[1,1,1]]],[0,2],[0,0,3,2,5],[0,2],[3,1],[3,0,3,2,10],[3,1],[0,2]] 输出: [null,1,null,5,5,null,10,5] 解释: SubrectangleQueries subrectangleQueries = new SubrectangleQueries([[1,2,1],[4,3,4],[3,2,1],[1,1,1]]); // 初始的 (4x3) 矩形如下: // 1 2 1 // 4 3 4 // 3 2 1 // 1 1 1 subrectangleQueries.getValue(0, 2); // 返回 1 subrectangleQueries.updateSubrectangle(0, 0, 3, 2, 5); // 此次更新后矩形变为: // 5 5 5 // 5 5 5 // 5 5 5 // 5 5 5 subrectangleQueries.getValue(0, 2); // 返回 5 subrectangleQueries.getValue(3, 1); // 返回 5 subrectangleQueries.updateSubrectangle(3, 0, 3, 2, 10); // 此次更新后矩形变为: // 5 5 5 // 5 5 5 // 5 5 5 // 10 10 10 subrectangleQueries.getValue(3, 1); // 返回 10 subrectangleQueries.getValue(0, 2); // 返回 5 ``` - 示例 2: ```python 输入: ["SubrectangleQueries","getValue","updateSubrectangle","getValue","getValue","updateSubrectangle","getValue"] [[[[1,1,1],[2,2,2],[3,3,3]]],[0,0],[0,0,2,2,100],[0,0],[2,2],[1,1,2,2,20],[2,2]] 输出: [null,1,null,100,100,null,20] 解释: SubrectangleQueries subrectangleQueries = new SubrectangleQueries([[1,1,1],[2,2,2],[3,3,3]]); subrectangleQueries.getValue(0, 0); // 返回 1 subrectangleQueries.updateSubrectangle(0, 0, 2, 2, 100); subrectangleQueries.getValue(0, 0); // 返回 100 subrectangleQueries.getValue(2, 2); // 返回 100 subrectangleQueries.updateSubrectangle(1, 1, 2, 2, 20); subrectangleQueries.getValue(2, 2); // 返回 20 ``` ## 解题思路 ### 思路 1:暴力 矩形最大为 $row \times col == 100 \times 100$,则每次更新最多需要更新 $10000$ 个值,更新次数最多为 $500$ 次。 用暴力更新的方法最多需要更新 $5000000$ 次,我们可以尝试一下用暴力更新的方法解决本题(提交后发现可以通过)。 ### 思路 1:代码 ```Python class SubrectangleQueries: def __init__(self, rectangle: List[List[int]]): self.rectangle = rectangle def updateSubrectangle(self, row1: int, col1: int, row2: int, col2: int, newValue: int) -> None: for row in range(row1, row2 + 1): for col in range(col1, col2 + 1): self.rectangle[row][col] = newValue def getValue(self, row: int, col: int) -> int: return self.rectangle[row][col] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(row \times col \times 500)$。 - **空间复杂度**:$O(row \times col)$。 ================================================ FILE: docs/solutions/1400-1499/xor-operation-in-an-array.md ================================================ # [1486. 数组异或操作](https://leetcode.cn/problems/xor-operation-in-an-array/) - 标签:位运算、数学 - 难度:简单 ## 题目链接 - [1486. 数组异或操作 - 力扣](https://leetcode.cn/problems/xor-operation-in-an-array/) ## 题目大意 给定两个整数 n、start。数组 nums 定义为:nums[i] = start + 2*i(下标从 0 开始)。n 为数组长度。返回数组 nums 中所有元素按位异或(XOR)后得到的结果。 ## 解题思路 ### 1. 模拟 直接按照题目要求模拟即可。 ### 2. 规律 - $x \oplus x = 0$; - $x \oplus y = y \oplus x$(交换律); - $(x \oplus y) \oplus z = x \oplus (y \oplus z)$(结合律); - $x \oplus y \oplus y = x$(自反性); - $\forall i \in Z$,有 $4i \oplus (4i+1) \oplus (4i+2) \oplus (4i+3) = 0$; - $\forall i \in Z$,有 $2i \oplus (2i+1) = 1$; - $\forall i \in Z$,有 $2i \oplus 1 = 2i+1$。 本题中计算的是 $start \oplus (start + 2) \oplus (start + 4) \oplus (start + 6) \oplus … \oplus (start+(2*(n-1)))$。 可以看出,如果 start 为奇数,则 $start+2, start + 4, …, start + (2 \times(n - 1))$ 都为奇数。如果 start 为偶数,则 $start + 2, start + 4, …, start + (2 \times(n - 1))$ 都为偶数。则它们对应二进制的最低位相同,则我们可以将最低位提取处理单独处理。从而将公式转换一下。 令 $s = \frac{start}{2}$,则等式变为 $(s) \oplus (s+1) \oplus (s+2) \oplus (s+3) \oplus … \oplus (s+(n-1)) * 2 + e$,e 表示运算结果的最低位。 根据自反性,$(s) \oplus (s+1) \oplus (s+2) \oplus (s+3) \oplus … \oplus (s+(n-1)) = \\ (1 \oplus 2 \oplus … \oplus (s-1)) \oplus (1 \oplus 2 \oplus … \oplus (s-1) \oplus (s) \oplus (s+1) \oplus … \oplus (s+(n-1)))$ 例如: $3 \oplus 4 \oplus 5 \oplus 6 \oplus 7 = (1 \oplus 2) \oplus (1 \oplus 2 \oplus 3 \oplus 4 \oplus 5 \oplus 6 \oplus7)$ 就变为了计算前 n 项序列的异或值。假设我们定义一个函数 sumXor(x) 用于计算前 n 项数的异或结果,通过观察可得出: $sumXor(x) = \begin{cases} \begin{array} \ x, & x = 4i, k \in Z \cr (x-1) \oplus x, & x = 4i+1, k \in Z \cr (x-2) \oplus (x-1) \oplus x, & x = 4i+2, k \in Z \cr (x-3) \oplus (x-2) \oplus (x-3) \oplus x, & x = 4i+3, k \in Z \end{array} \end{cases}$ 继续化简得: $sumXor(x) = \begin{cases} \begin{array} \ x, & x = 4i, k \in Z \cr 1, & x = 4i+1, k \in Z \cr x+1, & x = 4i+2, k \in Z \cr 0, & x = 4i+3, k \in Z \end{array} \end{cases}$ 则最终结果为 $sumXor(s-1) \oplus sumXor(s+n-1) * 2 + e$。 下面还有最后一位 e 的计算。 - 如果 start 为偶数,则最后一位 e 为 0。 - 如果 start 为奇数,最后一位 e 跟 n 有关,如果 n 为奇数,则最后一位 e 为 1,如果 n 为偶数,则最后一位 e 为 0。 总结下来就是 `e = start & n & 1`。 ## 代码 1. 模拟 ```python class Solution: def xorOperation(self, n: int, start: int) -> int: ans = 0 for i in range(n): ans ^= (start + i * 2) return ans ``` 2. 规律 ```python class Solution: def sumXor(self, x): if x % 4 == 0: return x if x % 4 == 1: return 1 if x % 4 == 2: return x + 1 return 0 def xorOperation(self, n: int, start: int) -> int: s = start >> 1 e = n & start & 1 ans = self.sumXor(s-1) ^ self.sumXor(s + n - 1) return ans << 1 | e ``` ================================================ FILE: docs/solutions/1500-1599/can-make-arithmetic-progression-from-sequence.md ================================================ # [1502. 判断能否形成等差数列](https://leetcode.cn/problems/can-make-arithmetic-progression-from-sequence/) - 标签:数组、排序 - 难度:简单 ## 题目链接 - [1502. 判断能否形成等差数列 - 力扣](https://leetcode.cn/problems/can-make-arithmetic-progression-from-sequence/) ## 题目大意 **描述**:给定一个数字数组 `arr`。如果一个数列中,任意相邻两项的差总等于同一个常数,那么这个数序就称为等差数列。 **要求**:如果数组 `arr` 通过重新排列可以形成等差数列,则返回 `True`;否则返回 `False`。 **说明**: - $2 \le arr.length \le 1000$ - $-10^6 \le arr[i] \le 10^6$ **示例**: - 示例 1: ```python 输入:arr = [3,5,1] 输出:True 解释:数组重新排序后得到 [1,3,5] 或者 [5,3,1],任意相邻两项的差分别为 2 或 -2 ,可以形成等差数列。 ``` ## 解题思路 ### 思路 1: - 如果数组元素个数小于等于 `2`,则数组肯定可以形成等差数列,直接返回 `True`。 - 对数组进行排序。 - 从下标为 `2` 的元素开始,遍历相邻的 `3` 个元素 `arr[i]` 、`arr[i - 1]`、`arr[i - 2]`。判断 `arr[i] - arr[i - 1]` 是否等于 `arr[i - 1] - arr[i - 2]`。如果不等于,则数组无法形成等差数列,返回 `False`。 - 如果遍历完数组,则说明数组可以形成等差数列,返回 `True`。 ## 代码 ### 思路 1 代码: ```python class Solution: def canMakeArithmeticProgression(self, arr: List[int]) -> bool: size = len(arr) if size <= 2: return True arr.sort() for i in range(2, size): if arr[i] - arr[i - 1] != arr[i - 1] - arr[i - 2]: return False return True ``` ================================================ FILE: docs/solutions/1500-1599/count-good-triplets.md ================================================ # [1534. 统计好三元组](https://leetcode.cn/problems/count-good-triplets/) - 标签:数组、枚举 - 难度:简单 ## 题目链接 - [1534. 统计好三元组 - 力扣](https://leetcode.cn/problems/count-good-triplets/) ## 题目大意 **描述**:给定一个整数数组 $arr$,以及 $a$、$b$、$c$ 三个整数。 **要求**:统计其中好三元组的数量。 **说明**: - **好三元组**:如果三元组($arr[i]$、$arr[j]$、$arr[k]$)满足下列全部条件,则认为它是一个好三元组。 - $0 \le i < j < k < arr.length$。 - $| arr[i] - arr[j] | \le a$。 - $| arr[j] - arr[k] | \le b$。 - $| arr[i] - arr[k] | \le c$。 - $3 \le arr.length \le 100$。 - $0 \le arr[i] \le 1000$。 - $0 \le a, b, c \le 1000$。 **示例**: - 示例 1: ```python 输入:arr = [3,0,1,1,9,7], a = 7, b = 2, c = 3 输出:4 解释:一共有 4 个好三元组:[(3,0,1), (3,0,1), (3,1,1), (0,1,1)]。 ``` - 示例 2: ```python 输入:arr = [1,1,2,2,3], a = 0, b = 0, c = 1 输出:0 解释:不存在满足所有条件的三元组。 ``` ## 解题思路 ### 思路 1:枚举 - 使用三重循环依次枚举所有的 $(i, j, k)$,判断对应 $arr[i]$、$arr[j]$、$arr[k]$ 是否满足条件。 - 然后统计出所有满足条件的三元组的数量。 ### 思路 1:代码 ```python class Solution: def countGoodTriplets(self, arr: List[int], a: int, b: int, c: int) -> int: size = len(arr) ans = 0 for i in range(size): for j in range(i + 1, size): for k in range(j + 1, size): if abs(arr[i] - arr[j]) <= a and abs(arr[j] - arr[k]) <= b and abs(arr[i] - arr[k]) <= c: ans += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^3)$,其中 $n$ 是数组 $arr$ 的长度。 - **空间复杂度**:$O(1)$。 ### 思路 2:枚举优化 + 前缀和 我们可以先通过二重循环遍历二元组 $(j, k)$,找出所有满足 $| arr[j] - arr[k] | \le b$ 的二元组。 然后在 $| arr[j] - arr[k] | \le b$ 的条件下,我们需要找到满足以下要求的 $arr[i]$ 数量: 1. $i < j$。 2. $| arr[i] - arr[j] | \le a$。 3. $| arr[i] - arr[k] | \le c$。 4. $0 \le arr[i] \le 1000$。 其中 $2$、$3$ 去除绝对值之后可变为: 1. $arr[j] - a \le arr[i] \le arr[j] + a$。 2. $arr[k] - c \le arr[i] \le arr[k] + c$。 将这两个条件再结合第 $4$ 个条件综合一下就变为:$max(0, arr[j] - a, arr[k] - c) \le arr[i] \le min(arr[j] + a, arr[k] + c, 1000)$。 假如定义 $left = max(0, arr[j] - a, arr[k] - c)$,$right = min(arr[j] + a, arr[k] + c, 1000)$。 现在问题就转变了如何快速获取在值域区间 $[left, right]$ 中,有多少个 $arr[i]$。 我们可以利用前缀和数组,先计算出 $[0, 1000]$ 范围中,满足 $arr[i] < num$ 的元素个数,即为 $prefix\_cnts[num]$。 然后对于区间 $[left, right]$,通过 $prefix\_cnts[right] - prefix\_cnts[left - 1]$ 即可快速求解出区间 $[left, right]$ 内 $arr[i]$ 的个数。 因为 $i < j < k$,所以我们可以在每次 $j$ 向右移动一位的时候,更新 $arr[j]$ 对应的前缀和数组,保证枚举到 $j$ 时,$prefix\_cnts$ 存储对应元素值的个数足够正确。 ### 思路 2:代码 ```python class Solution: def countGoodTriplets(self, arr: List[int], a: int, b: int, c: int) -> int: size = len(arr) ans = 0 prefix_cnts = [0 for _ in range(1010)] for j in range(size): for k in range(j + 1, size): if abs(arr[j] - arr[k]) <= b: left_j, right_j = arr[j] - a, arr[j] + a left_k, right_k = arr[k] - c, arr[k] + c left, right = max(0, left_j, left_k), min(1000, right_j, right_k) if left <= right: if left == 0: ans += prefix_cnts[right] else: ans += prefix_cnts[right] - prefix_cnts[left - 1] for k in range(arr[j], 1001): prefix_cnts[k] += 1 return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n^2 + n \times S)$,其中 $n$ 是数组 $arr$ 的长度,$S$ 为数组的值域上限。 - **空间复杂度**:$O(S)$。 ================================================ FILE: docs/solutions/1500-1599/count-odd-numbers-in-an-interval-range.md ================================================ # [1523. 在区间范围内统计奇数数目](https://leetcode.cn/problems/count-odd-numbers-in-an-interval-range/) - 标签:数学 - 难度:简单 ## 题目链接 - [1523. 在区间范围内统计奇数数目 - 力扣](https://leetcode.cn/problems/count-odd-numbers-in-an-interval-range/) ## 题目大意 **描述**:给定两个非负整数 `low` 和 `high`。 **要求**:返回 `low` 与 `high` 之间(包括二者)的奇数数目。 **说明**: - $0 \le low \le high \le 10^9$。 **示例**: - 示例 1: ```python 输入:low = 3, high = 7 输出:3 解释:3 到 7 之间奇数数字为 [3,5,7] ``` ## 解题思路 ### 思路 1: 暴力枚举 `[low, high]` 之间的奇数可能会超时。我们可以通过公式直接计算出 `[0, low - 1]` 之间的奇数个数和 `[0, high]` 之间的奇数个数,然后将两者相减即为答案。 计算奇数个数的公式为:$pre(x) = \lfloor \frac{x + 1}{2} \rfloor$。 ## 代码 ### 思路 1 代码: ```python class Solution: def pre(self, val): return (val + 1) >> 1 def countOdds(self, low: int, high: int) -> int: return self.pre(high) - self.pre(low - 1) ``` ================================================ FILE: docs/solutions/1500-1599/index.md ================================================ ## 本章内容 - [1502. 判断能否形成等差数列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/can-make-arithmetic-progression-from-sequence.md) - [1507. 转变日期格式](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/reformat-date.md) - [1523. 在区间范围内统计奇数数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/count-odd-numbers-in-an-interval-range.md) - [1534. 统计好三元组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/count-good-triplets.md) - [1547. 切棍子的最小成本](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-cost-to-cut-a-stick.md) - [1551. 使数组中所有元素相等的最小操作数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-operations-to-make-array-equal.md) - [1556. 千位分隔数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/thousand-separator.md) - [1561. 你可以获得的最大硬币数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/maximum-number-of-coins-you-can-get.md) - [1567. 乘积为正数的最长子数组长度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/maximum-length-of-subarray-with-positive-product.md) - [1582. 二进制矩阵中的特殊位置](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/special-positions-in-a-binary-matrix.md) - [1584. 连接所有点的最小费用](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/min-cost-to-connect-all-points.md) - [1593. 拆分字符串使唯一子字符串的数目最大](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/split-a-string-into-the-max-number-of-unique-substrings.md) - [1595. 连通两组点的最小成本](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/minimum-cost-to-connect-two-groups-of-points.md) ================================================ FILE: docs/solutions/1500-1599/maximum-length-of-subarray-with-positive-product.md ================================================ # [1567. 乘积为正数的最长子数组长度](https://leetcode.cn/problems/maximum-length-of-subarray-with-positive-product/) - 标签:贪心、数组、动态规划 - 难度:中等 ## 题目链接 - [1567. 乘积为正数的最长子数组长度 - 力扣](https://leetcode.cn/problems/maximum-length-of-subarray-with-positive-product/) ## 题目大意 给定一个整数数组 `nums`。 要求:求出乘积为正数的最长子数组的长度。 - 子数组:是由原数组中零个或者更多个连续数字组成的数组。 ## 解题思路 使用动态规划来做。使用数组 `pos` 表示以下标 `i` 结尾的乘积为正数的最长子数组长度。使用数组 `neg` 表示以下标 `i` 结尾的乘积为负数的最长子数组长度。 - 先初始化 `pos[0]`、`neg[0]`。 - 如果 `nums[0] == 0`,则 `pos[0] = 0, neg[0] = 0`。 - 如果 `nums[0] > 0`,则 `pos[0] = 1, neg[0] = 0`。 - 如果 `nums[0] < 0`,则 `pos[0] = 0, neg[0] = 1`。 - 然后从下标 `1` 开始递推遍历数组 `nums`,对于 `nums[i - 1]` 和 `nums[i]`: - 如果 `nums[i - 1] == 0`,显然有 `pos[i] = 0`,`neg[i] = 0`。表示:以`i` 结尾的乘积为正数的最长子数组长度为 `0`,以`i` 结尾的乘积为负数数的最长子数组长度也为 `0`。 - 如果 `nums[i - 1] > 0`,则 `pos[i] = pos[i - 1] + 1`。而 `neg[i]` 需要进行判断,如果 `neg[i - 1] > 0`,则再乘以当前 `nums[i]` 后仍为负数,此时长度 +1,即 `neg[i] = neg[i - 1] + 1 `。而如果 `neg[i - 1] == 0`,则 `neg[i] = 0`。 - 如果 `nums[i - 1] < 0`,则 `pos[i]` 需要进行判断,如果 `neg[i - 1] > 0`,再乘以当前 `nums[i]` 后变为正数,此时长度 +1,即 `pos[i] = neg[i - 1] + 1`。而如果 `neg[i - 1] = 0`,则 `pos[i] = 0`。 - 更新 `ans` 答案为 `pos[i]` 最大值。 - 最后输出答案 `ans`。 ## 代码 ```python class Solution: def getMaxLen(self, nums: List[int]) -> int: size = len(nums) pos = [0 for _ in range(size + 1)] neg = [0 for _ in range(size + 1)] if nums[0] == 0: pos[0], neg[0] = 0, 0 elif nums[0] > 0: pos[0], neg[0] = 1, 0 else: pos[0], neg[0] = 0, 1 ans = pos[0] for i in range(1, size): if nums[i] == 0: pos[i] = 0 neg[i] = 0 elif nums[i] > 0: pos[i] = pos[i - 1] + 1 neg[i] = neg[i - 1] + 1 if neg[i - 1] > 0 else 0 elif nums[i] < 0: pos[i] = neg[i - 1] + 1 if neg[i - 1] > 0 else 0 neg[i] = pos[i - 1] + 1 ans = max(ans, pos[i]) return ans ``` ## 参考资料 - 【题解】[递推就完事了,巨好理解~ - 乘积为正数的最长子数组长度 - 力扣](https://leetcode.cn/problems/maximum-length-of-subarray-with-positive-product/solution/di-tui-jiu-wan-shi-liao-ju-hao-li-jie-by-time-limi/) ================================================ FILE: docs/solutions/1500-1599/maximum-number-of-coins-you-can-get.md ================================================ # [1561. 你可以获得的最大硬币数目](https://leetcode.cn/problems/maximum-number-of-coins-you-can-get/) - 标签:贪心、数组、数学、博弈、排序 - 难度:中等 ## 题目链接 - [1561. 你可以获得的最大硬币数目 - 力扣](https://leetcode.cn/problems/maximum-number-of-coins-you-can-get/) ## 题目大意 有 `3*n` 堆数目不一的硬币,三个人按照下面的规则分硬币: - 每一轮选出任意 3 堆硬币。 - Alice 拿走硬币数量最多的那一堆。 - 我们自己拿走硬币数量第二多的那一堆。 - Bob 拿走最后一堆。 - 重复这个过程,直到没有更多硬币。 现在给定一个整数数组 `piles`,代表 `3*n` 堆硬币,其中 `piles[i]` 表示第 `i` 堆中硬币的数目。 ## 解题思路 每次 `3` 堆,总共取 `n` 次。Bob 每次总是选择最少的一堆,所以最终 Bob 得到 `3*n` 堆中最少的 `n` 堆才能使得另外两个人获得更多。所以先对硬币堆进行排序。Bob 拿走最少的 `n` 堆。我们接着分剩下的 `2*n` 堆。 按照大小顺序,每次都选取硬币数目最多的两堆, Alice 取得较大的一堆,我们取较小的一堆。 然后继续在剩余堆中选取硬币数目最多的两堆,同样 Alice 取得较大的一堆,我们取较小的一堆。 只有这样才能在满足规则的情况下,使我们所获得硬币数最多。 最后统计我们所获取的硬币数,并返回结果。 ## 代码 ```python class Solution: def maxCoins(self, piles: List[int]) -> int: piles.sort() ans = 0 for i in range(len(piles) // 3, len(piles), 2): ans += piles[i] return ans ``` ================================================ FILE: docs/solutions/1500-1599/min-cost-to-connect-all-points.md ================================================ # [1584. 连接所有点的最小费用](https://leetcode.cn/problems/min-cost-to-connect-all-points/) - 标签:并查集、图、数组、最小生成树 - 难度:中等 ## 题目链接 - [1584. 连接所有点的最小费用 - 力扣](https://leetcode.cn/problems/min-cost-to-connect-all-points/) ## 题目大意 **描述**:给定一个 $points$ 数组,表示 2D 平面上的一些点,其中 $points[i] = [x_i, y_i]$。 链接点 $[x_i, y_i]$ 和点 $[x_j, y_j]$ 的费用为它们之间的 **曼哈顿距离**:$|x_i - x_j| + |y_i - y_j|$。其中 $|val|$ 表示 $val$ 的绝对值。 **要求**:返回将所有点连接的最小总费用。 **说明**: - 只有任意两点之间有且仅有一条简单路径时,才认为所有点都已连接。 - $1 \le points.length \le 1000$。 - $-10^6 \le x_i, y_i \le 10^6$。 - 所有点 $(x_i, y_i)$ 两两不同。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2020/08/26/d.png) ![](https://assets.leetcode.com/uploads/2020/08/26/c.png) ```python 输入:points = [[0,0],[2,2],[3,10],[5,2],[7,0]] 输出:20 解释:我们可以按照上图所示连接所有点得到最小总费用,总费用为 20 。 注意到任意两个点之间只有唯一一条路径互相到达。 ``` - 示例 2: ```python 输入:points = [[3,12],[-2,5],[-4,1]] 输出:18 ``` ## 解题思路 将所有点之间的费用看作是边,则所有点和边可以看作是一个无向图。每两个点之间都存在一条无向边,边的权重为两个点之间的曼哈顿距离。将所有点连接的最小总费用,其实就是求无向图的最小生成树。对此我们可以使用 Prim 算法或者 Kruskal 算法。 ### 思路 1:Prim 算法 每次选择最短边来扩展最小生成树,从而保证生成树的总权重最小。算法通过不断扩展小生成树的顶点集合 $MST$,逐步构建出最小生成树。 ### 思路 1:代码 ```Python class Solution: def distance(self, point1, point2): return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) def Prim(self, points, start): size = len(points) vis = set() dis = [float('inf') for _ in range(size)] ans = 0 # 最小生成树的边权值 dis[start] = 0 # 起始位置到起始位置的边权值初始化为 0 for i in range(1, size): dis[i] = self.distance(points[start], points[i]) vis.add(start) for _ in range(size - 1): # 进行 n 轮迭代 min_dis = float('inf') min_dis_i = -1 for i in range(size): if i not in vis and dis[i] < min_dis: min_dis = dis[i] min_dis_i = i if min_dis_i == -1: return -1 ans += min_dis vis.add(min_dis_i) for i in range(size): if i not in vis: dis[i] = min(dis[i], self.distance(points[i], points[min_dis_i])) return ans def minCostConnectPoints(self, points: List[List[int]]) -> int: return self.Prim(points, 0) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(n^2)$。 ### 思路 2:Kruskal 算法 通过依次选择权重最小的边并判断其两个端点是否连接在同一集合中,从而逐步构建最小生成树。这个过程保证了最终生成的树是无环的,并且总权重最小。 ### 思路 2:代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] self.count = n def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.count -= 1 def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def Kruskal(self, edges, size): union_find = UnionFind(size) edges.sort(key=lambda x: x[2]) ans, cnt = 0, 0 for x, y, dist in edges: if union_find.is_connected(x, y): continue ans += dist cnt += 1 union_find.union(x, y) if cnt == size - 1: return ans return ans def minCostConnectPoints(self, points: List[List[int]]) -> int: size = len(points) edges = [] for i in range(size): xi, yi = points[i] for j in range(i + 1, size): xj, yj = points[j] dist = abs(xi - xj) + abs(yi - yj) edges.append([i, j, dist]) ans = self.Kruskal(edges, size) return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(m \times \log(n))$。其中 $m$ 为边数,$n$ 为节点数,本题中 $m = n^2$。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/1500-1599/minimum-cost-to-connect-two-groups-of-points.md ================================================ # [1595. 连通两组点的最小成本](https://leetcode.cn/problems/minimum-cost-to-connect-two-groups-of-points/) - 标签:位运算、数组、动态规划、状态压缩、矩阵 - 难度:困难 ## 题目链接 - [1595. 连通两组点的最小成本 - 力扣](https://leetcode.cn/problems/minimum-cost-to-connect-two-groups-of-points/) ## 题目大意 **描述**:有两组点,其中一组中有 $size_1$ 个点,第二组中有 $size_2$ 个点,且 $size_1 \ge size_2$。现在给定一个大小为 $size_1 \times size_2$ 的二维数组 $cost$ 用于表示两组点任意两点之间的链接成本。其中 $cost[i][j]$ 表示第一组中第 $i$ 个点与第二组中第 $j$ 个点的链接成本。 如果两个组中每个点都与另一个组中的一个或多个点连接,则称这两组点是连通的。 **要求**:返回连通两组点所需的最小成本。 **说明**: - $size_1 == cost.length$。 - $size_2 == cost[i].length$。 - $1 \le size_1, size_2 \le 12$。 - $size_1 \ge size_2$。 - $0 \le cost[i][j] \le 100$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/09/20/ex1.jpg) ```python 输入:cost = [[15, 96], [36, 2]] 输出:17 解释:连通两组点的最佳方法是: 1--A 2--B 总成本为 17。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/09/20/ex2.jpg) ```python 输入:cost = [[1, 3, 5], [4, 1, 1], [1, 5, 3]] 输出:4 解释:连通两组点的最佳方法是: 1--A 2--B 2--C 3--A 最小成本为 4。 请注意,虽然有多个点连接到第一组中的点 2 和第二组中的点 A ,但由于题目并不限制连接点的数目,所以只需要关心最低总成本。 ``` ## 解题思路 ### 思路 1:状压 DP ### 思路 1:代码 ```python class Solution: def connectTwoGroups(self, cost: List[List[int]]) -> int: m, n = len(cost), len(cost[0]) states = 1 << n dp = [[float('inf') for _ in range(states)] for _ in range(m + 1)] dp[0][0] = 0 for i in range(1, m + 1): for state in range(states): for j in range(n): dp[i][state | (1 << j)] = min(dp[i][state | (1 << j)], dp[i - 1][state] + cost[i - 1][j], dp[i][state] + cost[i - 1][j]) return dp[m][states - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**: - **空间复杂度**: ================================================ FILE: docs/solutions/1500-1599/minimum-cost-to-cut-a-stick.md ================================================ # [1547. 切棍子的最小成本](https://leetcode.cn/problems/minimum-cost-to-cut-a-stick/) - 标签:数组、动态规划、排序 - 难度:困难 ## 题目链接 - [1547. 切棍子的最小成本 - 力扣](https://leetcode.cn/problems/minimum-cost-to-cut-a-stick/) ## 题目大意 **描述**:给定一个整数 $n$,代表一根长度为 $n$ 个单位的木根,木棍从 $0 \sim n$ 标记了若干位置。例如,长度为 $6$ 的棍子可以标记如下: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/08/09/statement.jpg) 再给定一个整数数组 $cuts$,其中 $cuts[i]$ 表示需要将棍子切开的位置。 我们可以按照顺序完成切割,也可以根据需要更改切割顺序。 每次切割的成本都是当前要切割的棍子的长度,切棍子的总成本是所有次切割成本的总和。对棍子进行切割将会把一根木棍分成两根较小的木棍(这两根小木棍的长度和就是切割前木棍的长度)。 **要求**:返回切棍子的最小总成本。 **说明**: - $2 \le n \le 10^6$。 - $1 \le cuts.length \le min(n - 1, 100)$。 - $1 \le cuts[i] \le n - 1$。 - $cuts$ 数组中的所有整数都互不相同。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/08/09/e1.jpg) ```python 输入:n = 7, cuts = [1,3,4,5] 输出:16 解释:按 [1, 3, 4, 5] 的顺序切割的情况如下所示。 第一次切割长度为 7 的棍子,成本为 7 。第二次切割长度为 6 的棍子(即第一次切割得到的第二根棍子),第三次切割为长度 4 的棍子,最后切割长度为 3 的棍子。总成本为 7 + 6 + 4 + 3 = 20 。而将切割顺序重新排列为 [3, 5, 1, 4] 后,总成本 = 16(如示例图中 7 + 4 + 3 + 2 = 16)。 ``` ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/08/09/e11.jpg) - 示例 2: ```python 输入:n = 9, cuts = [5,6,1,4,2] 输出:22 解释:如果按给定的顺序切割,则总成本为 25。总成本 <= 25 的切割顺序很多,例如,[4, 6, 5, 2, 1] 的总成本 = 22,是所有可能方案中成本最小的。 ``` ## 解题思路 ### 思路 1:动态规划 我们可以预先在数组 $cuts$ 种添加位置 $0$ 和位置 $n$,然后对数组 $cuts$ 进行排序。这样待切割的木棍就对应了数组中连续元素构成的「区间」。 ###### 1. 阶段划分 按照区间长度进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][j]$ 表示为:切割区间为 $[i, j]$ 上的小木棍的最小成本。 ###### 3. 状态转移方程 假设位置 $i$ 与位置 $j$ 之间最后一个切割的位置为 $k$,则 $dp[i][j]$ 取决与由 $k$ 作为切割点分割出的两个区间 $[i, k]$ 与 $[k, j]$ 上的最小成本 + 切割位置 $k$ 所带来的成本。 而切割位置 $k$ 所带来的成本是这段区间所代表的小木棍的长度,即 $cuts[j] - cuts[i]$。 则状态转移方程为:$dp[i][j] = min \lbrace dp[i][k] + dp[k][j] + cuts[j] - cuts[i] \rbrace, \quad i < k < j$ ###### 4. 初始条件 - 相邻位置之间没有切割点,不需要切割,最小成本为 $0$,即 $dp[i - 1][i] = 0$。 - 其余位置默认为最小成本为一个极大值,即 $dp[i][j] = \infty, \quad i + 1 \ne j$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[i][j]$ 表示为:切割区间为 $[i, j]$ 上的小木棍的最小成本。 所以最终结果为 $dp[0][size - 1]$。 ### 思路 1:代码 ```python class Solution: def minCost(self, n: int, cuts: List[int]) -> int: cuts.append(0) cuts.append(n) cuts.sort() size = len(cuts) dp = [[float('inf') for _ in range(size)] for _ in range(size)] for i in range(1, size): dp[i - 1][i] = 0 for l in range(3, size + 1): # 枚举区间长度 for i in range(size): # 枚举区间起点 j = i + l - 1 # 根据起点和长度得到终点 if j >= size: continue dp[i][j] = float('inf') for k in range(i + 1, j): # 枚举区间分割点 # 状态转移方程,计算合并区间后的最优值 dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + cuts[j] - cuts[i]) return dp[0][size - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m^3)$,其中 $m$ 为数组 $cuts$ 的元素个数。 - **空间复杂度**:$O(m^2)$。 ================================================ FILE: docs/solutions/1500-1599/minimum-operations-to-make-array-equal.md ================================================ # [1551. 使数组中所有元素相等的最小操作数](https://leetcode.cn/problems/minimum-operations-to-make-array-equal/) - 标签:数学 - 难度:中等 ## 题目链接 - [1551. 使数组中所有元素相等的最小操作数 - 力扣](https://leetcode.cn/problems/minimum-operations-to-make-array-equal/) ## 题目大意 **描述**:存在一个长度为 $n$ 的数组 $arr$,其中 $arr[i] = (2 \times i) + 1$,$(0 \le i < n)$。 在一次操作中,我们可以选出两个下标,记作 $x$ 和 $y$($0 \le x, y < n$),并使 $arr[x]$ 减去 $1$,$arr[y]$ 加上 $1$)。最终目标是使数组中所有元素都相等。 现在给定一个整数 $n$,即数组 $arr$ 的长度。 **要求**:返回使数组 $arr$ 中所有元素相等所需要的最小操作数。 **说明**: - 题目测试用例将会保证:在执行若干步操作后,数组中的所有元素最终可以全部相等。 - $1 \le n \le 10^4$。 **示例**: - 示例 1: ```python 输入:n = 3 输出:2 解释:arr = [1, 3, 5] 第一次操作选出 x = 2 和 y = 0,使数组变为 [2, 3, 4] 第二次操作继续选出 x = 2 和 y = 0,数组将会变成 [3, 3, 3] ``` - 示例 2: ```python 输入:n = 6 输出:9 ``` ## 解题思路 ### 思路 1:贪心 通过观察可以发现,数组中所有元素构成了一个等差数列,为了使所有元素相等,在每一次操作中,尽可能让较小值增大,让较大值减小,直到到达平均值为止,这样才能得到最小操作次数。 在一次操作中,我们可以同时让第 $i$ 个元素增大与第 $n - 1 - i$ 个元素减小。这样,我们只需要统计出数组前半部分元素变化幅度即可。 ### 思路 1:代码 ```python class Solution: def minOperations(self, n: int) -> int: ans = 0 for i in range(n // 2): ans += n - 1 - 2 * i return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:贪心 + 优化 数组前半部分元素变化幅度的计算可以看做是一个等差数列求和,所以我们可以直接根据高斯求和公式求出结果。 $\lbrace n - 1 + [n - 1 - 2 * (n \div 2 - 1)]\rbrace \times (n \div 2) \div 2 = n \times n \div 4$ ### 思路 2:代码 ```python class Solution: def minOperations(self, n: int) -> int: return n * n // 4 ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(1)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1500-1599/reformat-date.md ================================================ # [1507. 转变日期格式](https://leetcode.cn/problems/reformat-date/) - 标签:字符串 - 难度:简单 ## 题目链接 - [1507. 转变日期格式 - 力扣](https://leetcode.cn/problems/reformat-date/) ## 题目大意 **描述**:给定一个字符串 $date$,它的格式为 `Day Month Year` ,其中: - $Day$ 是集合 `{"1st", "2nd", "3rd", "4th", ..., "30th", "31st"}` 中的一个元素。 - $Month$ 是集合 `{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}` 中的一个元素。 - $Year$ 的范围在 $[1900, 2100]$ 之间。 **要求**:将字符串转变为 `YYYY-MM-DD` 的格式,其中: - $YYYY$ 表示 $4$ 位的年份。 - $MM$ 表示 $2$ 位的月份。 - $DD$ 表示 $2$ 位的天数。 **说明**: - 给定日期保证是合法的,所以不需要处理异常输入。 **示例**: - 示例 1: ```python 输入:date = "20th Oct 2052" 输出:"2052-10-20" ``` - 示例 2: ```python 输入:date = "6th Jun 1933" 输出:"1933-06-06" ``` ## 解题思路 ### 思路 1:模拟 1. 将字符串分割为三部分,分别按照以下规则得到日、月、年: 1. 日:去掉末尾两位英文字母,将其转为整型数字,并且进行补零操作,使其宽度为 $2$。 2. 月:使用哈希表将其映射为对应两位数字。 3. 年:直接赋值。 2. 将得到的年、月、日使用 `"-"` 进行链接并返回。 ### 思路 1:代码 ```python class Solution: def reformatDate(self, date: str) -> str: months = { "Jan" : "01", "Feb" : "02", "Mar" : "03", "Apr" : "04", "May" : "05", "Jun" : "06", "Jul" : "07", "Aug" : "08", "Sep" : "09", "Oct" : "10", "Nov" : "11", "Dec" : "12" } date_list = date.split(' ') day = "{:0>2d}".format(int(date_list[0][: -2])) month = months[date_list[1]] year = date_list[2] return year + "-" + month + "-" + day ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1500-1599/special-positions-in-a-binary-matrix.md ================================================ # [1582. 二进制矩阵中的特殊位置](https://leetcode.cn/problems/special-positions-in-a-binary-matrix/) - 标签:数组、矩阵 - 难度:简单 ## 题目链接 - [1582. 二进制矩阵中的特殊位置 - 力扣](https://leetcode.cn/problems/special-positions-in-a-binary-matrix/) ## 题目大意 **描述**:给定一个 $m \times n$ 的二进制矩阵 $mat$。 **要求**:返回矩阵 $mat$ 中特殊位置的数量。 **说明**: - **特殊位置**:如果位置 $(i, j)$ 满足 $mat[i][j] == 1$ 并且行 $i$ 与列 $j$ 中的所有其他元素都是 $0$(行和列的下标从 $0$ 开始计数),那么它被称为特殊位置。 - $m == mat.length$。 - $n == mat[i].length$。 - $1 \le m, n \le 100$。 - $mat[i][j]$ 是 $0$ 或 $1$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/12/23/special1.jpg) ```python 输入:mat = [[1,0,0],[0,0,1],[1,0,0]] 输出:1 解释:位置 (1, 2) 是一个特殊位置,因为 mat[1][2] == 1 且第 1 行和第 2 列的其他所有元素都是 0。 ``` - 示例 2: ![img](https://assets.leetcode.com/uploads/2021/12/24/special-grid.jpg) ```python 输入:mat = [[1,0,0],[0,1,0],[0,0,1]] 输出:3 解释:位置 (0, 0),(1, 1) 和 (2, 2) 都是特殊位置。 ``` ## 解题思路 ### 思路 1:模拟 1. 按照行、列遍历二位数组 $mat$。 2. 使用数组 $row\_cnts$、$col\_cnts$ 分别记录每行和每列所含 $1$ 的个数。 3. 再次按照行、列遍历二维数组 $mat$。 4. 统计满足 $mat[row][col] == 1$ 并且 $row\_cnts[row] == col\_cnts[col] == 1$ 的位置个数。 5. 返回答案。 ### 思路 1:代码 ```Python class Solution: def numSpecial(self, mat: List[List[int]]) -> int: rows, cols = len(mat), len(mat[0]) row_cnts = [0 for _ in range(rows)] col_cnts = [0 for _ in range(cols)] for row in range(rows): for col in range(cols): row_cnts[row] += mat[row][col] col_cnts[col] += mat[row][col] ans = 0 for row in range(rows): for col in range(cols): if mat[row][col] == 1 and row_cnts[row] == 1 and col_cnts[col] == 1: ans += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$,其中 $m$、$n$ 分别为数组 $mat$ 的行数和列数。 - **空间复杂度**:$O(m + n)$。 ================================================ FILE: docs/solutions/1500-1599/split-a-string-into-the-max-number-of-unique-substrings.md ================================================ # [1593. 拆分字符串使唯一子字符串的数目最大](https://leetcode.cn/problems/split-a-string-into-the-max-number-of-unique-substrings/) - 标签:哈希表、字符串、回溯 - 难度:中等 ## 题目链接 - [1593. 拆分字符串使唯一子字符串的数目最大 - 力扣](https://leetcode.cn/problems/split-a-string-into-the-max-number-of-unique-substrings/) ## 题目大意 **描述**:给定一个字符串 $s$。将字符串 $s$ 拆分后可以得到若干非空子字符串,这些子字符串连接后应当能够还原为原字符串。但是拆分出来的每个子字符串都必须是唯一的 。 **要求**:拆分该字符串,并返回拆分后唯一子字符串的最大数目。 **说明**: - 子字符串是字符串中的一个连续字符序列。 - $1 \le s.length \le 16$。 - $s$ 仅包含小写英文字母。 **示例**: - 示例 1: ```python 输入:s = "ababccc" 输出:5 解释:一种最大拆分方法为 ['a', 'b', 'ab', 'c', 'cc'] 。像 ['a', 'b', 'a', 'b', 'c', 'cc'] 这样拆分不满足题目要求,因为其中的 'a' 和 'b' 都出现了不止一次。 ``` - 示例 2: ```python 输入:s = "aba" 输出:2 解释:一种最大拆分方法为 ['a', 'ba']。 ``` ## 解题思路 ### 思路 1:回溯算法 维护一个全局变量 $ans$ 用于记录拆分后唯一子字符串的最大数目。并使用集合 $s\_set$ 记录不重复的子串。 - 从下标为 $0$ 开头的子串回溯。 - 对于下标为 $index$ 开头的子串,我们可以在 $index + 1$ 开始到 $len(s) - 1$ 的位置上,分别进行子串拆分,将子串拆分为 $s[index: i + 1]$。 - 如果当前子串不在 $s\_set$ 中,则将其存入 $s\_set$ 中,然后记录当前拆分子串个数,并从 $i + 1$ 的位置进行下一层递归拆分。然后在拆分完,对子串进行回退操作。 - 如果拆到字符串 $s$ 的末尾,则记录并更新 $ans$。 - 在开始位置还可以进行以下剪枝:如果剩余字符个数 + 当前子串个数 <= 当前拆分后子字符串的最大数目,则直接返回。 最后输出 $ans$。 ### 思路 1:代码 ```python class Solution: ans = 0 def backtrack(self, s, index, count, s_set): if len(s) - index + count <= self.ans: return if index >= len(s): self.ans = max(self.ans, count) return for i in range(index, len(s)): sub_s = s[index: i + 1] if sub_s not in s_set: s_set.add(sub_s) self.backtrack(s, i + 1, count + 1, s_set) s_set.remove(sub_s) def maxUniqueSplit(self, s: str) -> int: s_set = set() self.ans = 0 self.backtrack(s, 0, 0, s_set) return self.ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 2^n)$,其中 $n$ 为字符串的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1500-1599/thousand-separator.md ================================================ # [1556. 千位分隔数](https://leetcode.cn/problems/thousand-separator/) - 标签:字符串 - 难度:简单 ## 题目链接 - [1556. 千位分隔数 - 力扣](https://leetcode.cn/problems/thousand-separator/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:每隔三位田间点(即 `"."` 符号)作为千位分隔符,并将结果以字符串格式返回。 **说明**: - $0 \le n \le 2^{31}$。 **示例**: - 示例 1: ```python 输入:n = 987 输出:"987" ``` - 示例 2: ```python 输入:n = 123456789 输出:"123.456.789" ``` ## 解题思路 ### 思路 1:模拟 1. 使用字符串变量 $ans$ 用于存储答案,使用一个计数器 $idx$ 来记录当前位数的个数。 2. 将 $n$ 转为字符串 $s$ 后,从低位向高位遍历。 3. 将当前数字 $s[i]$ 存入 $ans$ 中,计数器加 $1$,当计数器为 $3$ 的整数倍并且当前数字位不是最高位时,将 `"."` 存入 $ans$ 中。 4. 遍历完成后,将 $ans$ 翻转后返回。 ### 思路 1:代码 ```python class Solution: def thousandSeparator(self, n: int) -> str: s = str(n) ans = "" idx = 0 for i in range(len(s) - 1, -1, -1): ans += s[i] idx += 1 if idx % 3 == 0 and i != 0: ans += "." return ''.join(reversed(ans)) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/1600-1699/count-sorted-vowel-strings.md ================================================ # [1641. 统计字典序元音字符串的数目](https://leetcode.cn/problems/count-sorted-vowel-strings/) - 标签:数学、动态规划、组合数学 - 难度:中等 ## 题目链接 - [1641. 统计字典序元音字符串的数目 - 力扣](https://leetcode.cn/problems/count-sorted-vowel-strings/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:返回长度为 $n$、仅由原音($a$、$e$、$i$、$o$、$u$)组成且按字典序排序的字符串数量。 **说明**: - 字符串 $a$ 按字典序排列需要满足:对于所有有效的 $i$,$s[i]$ 在字母表中的位置总是与 $s[i + 1]$ 相同或在 $s[i+1] $之前。 - $1 \le n \le 50$。 **示例**: - 示例 1: ```python 输入:n = 1 输出:5 解释:仅由元音组成的 5 个字典序字符串为 ["a","e","i","o","u"] ``` - 示例 2: ```python 输入:n = 2 输出:15 解释:仅由元音组成的 15 个字典序字符串为 ["aa","ae","ai","ao","au","ee","ei","eo","eu","ii","io","iu","oo","ou","uu"] 注意,"ea" 不是符合题意的字符串,因为 'e' 在字母表中的位置比 'a' 靠后 ``` ## 解题思路 ### 思路 1:组和数学 题目要求按照字典序排列,则如果确定了每个元音的出现次数可以确定一个序列。 对于长度为 $n$ 的序列,$a$、$e$、$i$、$o$、$u$ 出现次数加起来为 $n$ 次,且顺序为 $a…a \rightarrow e…e \rightarrow i…i \rightarrow o…o \rightarrow u…u$。 我们可以看作是将 $n$ 分隔成了 $5$ 份,每一份对应一个原音字母的数量。 我们可以使用「隔板法」的方式,看作有 $n$ 个球,$4$ 个板子,将 $n$ 个球分隔成 $5$ 份。 则一共有 $n + 4$ 个位置可以放板子,总共需要放 $4$ 个板子,则答案为 $C_{n + 4}^4$,其中 $C$ 为组和数。 ### 思路 1:代码 ```Python class Solution: def countVowelStrings(self, n: int) -> int: return comb(n + 4, 4) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(| \sum |)$,其中 $\sum$ 为字符集,本题中 $| \sum | = 5$ 。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1600-1699/count-subtrees-with-max-distance-between-cities.md ================================================ # [1617. 统计子树中城市之间最大距离](https://leetcode.cn/problems/count-subtrees-with-max-distance-between-cities/) - 标签:位运算、树、动态规划、状态压缩、枚举 - 难度:困难 ## 题目链接 - [1617. 统计子树中城市之间最大距离 - 力扣](https://leetcode.cn/problems/count-subtrees-with-max-distance-between-cities/) ## 题目大意 **描述**:给定一个整数 $n$,代表 $n$ 个城市,城市编号为 $1 \sim n$。同时给定一个大小为 $n - 1$ 的数组 $edges$,其中 $edges[i] = [u_i, v_i]$ 表示城市 $u_i$ 和 $v_i$ 之间有一条双向边。题目保证任意城市之间只有唯一的一条路径。换句话说,所有城市形成了一棵树。 **要求**:返回一个大小为 $n - 1$ 的数组,其中第 $i$ 个元素(下标从 $1$ 开始)是城市间距离恰好等于 $i$ 的子树数目。 **说明**: - **两个城市间距离**:定义为它们之间需要经过的边的数目。 - **一棵子树**:城市的一个子集,且子集中任意城市之间可以通过子集中的其他城市和边到达。两个子树被认为不一样的条件是至少有一个城市在其中一棵子树中存在,但在另一棵子树中不存在。 - $2 \le n \le 15$。 - $edges.length == n - 1$。 - $edges[i].length == 2$。 - $1 \le u_i, v_i \le n$。 - 题目保证 $(ui, vi)$ 所表示的边互不相同。 **示例**: - 示例 1: ```python 输入:n = 4, edges = [[1,2],[2,3],[2,4]] 输出:[3,4,0] 解释: 子树 {1,2}, {2,3} 和 {2,4} 最大距离都是 1 。 子树 {1,2,3}, {1,2,4}, {2,3,4} 和 {1,2,3,4} 最大距离都为 2 。 不存在城市间最大距离为 3 的子树。 ``` - 示例 2: ```python 输入:n = 2, edges = [[1,2]] 输出:[1] ``` ## 解题思路 ### 思路 1:树形 DP + 深度优先搜索 因为题目中给定 $n$ 的范围为 $2 \le n \le 15$,范围比较小,我们可以通过类似「[0078. 子集](https://leetcode.cn/problems/subsets/)」中二进制枚举的方式,得到所有子树的子集。 而对于一个确定的子树来说,求子树中两个城市间距离就是在求子树的直径,这就跟 [「1245. 树的直径」](https://leetcode.cn/problems/tree-diameter/) 和 [「2246. 相邻字符不同的最长路径」](https://leetcode.cn/problems/longest-path-with-different-adjacent-characters/) 一样了。 那么这道题的思路就变成了: 1. 通过二进制枚举的方式,得到所有子树。 2. 对于当前子树,通过树形 DP + 深度优先搜索的方式,计算出当前子树的直径。 3. 统计所有子树直径中经过的不同边数个数,将其放入答案数组中。 ### 思路 1:代码 ```python class Solution: def countSubgraphsForEachDiameter(self, n: int, edges: List[List[int]]) -> List[int]: graph = [[] for _ in range(n)] # 建图 for u, v in edges: graph[u - 1].append(v - 1) graph[v - 1].append(u - 1) def dfs(mask, u): nonlocal visited, diameter visited |= 1 << u # 标记 u 访问过 u_len = 0 # u 节点的最大路径长度 for v in graph[u]: # 遍历 u 节点的相邻节点 if (visited >> v) & 1 == 0 and mask >> v & 1: # v 没有访问过,且在子集中 v_len = dfs(mask, v) # 相邻节点的最大路径长度 diameter = max(diameter, u_len + v_len + 1) # 维护最大路径长度 u_len = max(u_len, v_len + 1) # 更新 u 节点的最大路径长度 return u_len ans = [0 for _ in range(n - 1)] for mask in range(3, 1 << n): # 二进制枚举子集 if mask & (mask - 1) == 0: # 子集至少需要两个点 continue visited = 0 diameter = 0 u = mask.bit_length() - 1 dfs(mask, u) # 在子集 mask 中递归求树的直径 if visited == mask: ans[diameter - 1] += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 2^n)$,其中 $n$ 为给定的城市数目。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1600-1699/design-parking-system.md ================================================ # [1603. 设计停车系统](https://leetcode.cn/problems/design-parking-system/) - 标签:设计、计数、模拟 - 难度:简单 ## 题目链接 - [1603. 设计停车系统 - 力扣](https://leetcode.cn/problems/design-parking-system/) ## 题目大意 给一个停车场设计一个停车系统。停车场总共有三种尺寸的车位:大、中、小,每种尺寸的车位分别有固定数目。 现在要求实现 `ParkingSystem` 类: - `ParkingSystem(big, medium, small)`:初始化 ParkingSystem 类,三个参数分别对应三种尺寸车位的数目。 - `addCar(carType) -> bool:`:检测是否有 `carType` 对应的停车位,如果有,则将车停入车位,并返回 `True`,否则返回 `False`。 ## 解题思路 使用不同成员变量存放车位数目。并根据给定操作进行判断。 ## 代码 ```python class ParkingSystem: def __init__(self, big: int, medium: int, small: int): self.park = [0, big, medium, small] def addCar(self, carType: int) -> bool: if self.park[carType] == 0: return False self.park[carType] -= 1 return True ``` ================================================ FILE: docs/solutions/1600-1699/determine-if-two-strings-are-close.md ================================================ # [1657. 确定两个字符串是否接近](https://leetcode.cn/problems/determine-if-two-strings-are-close/) - 标签:哈希表、字符串、排序 - 难度:中等 ## 题目链接 - [1657. 确定两个字符串是否接近 - 力扣](https://leetcode.cn/problems/determine-if-two-strings-are-close/) ## 题目大意 **描述**:如果可以使用以下操作从一个字符串得到另一个字符串,则认为两个字符串 接近 : - 操作 1:交换任意两个现有字符。 - 例如,`abcde` -> `aecdb`。 - 操作 2:将一个 现有 字符的每次出现转换为另一个现有字符,并对另一个字符执行相同的操作。 - 例如,`aacabb` -> `bbcbaa`(所有 `a` 转化为 `b`,而所有的 `b` 转换为 `a` )。 给定两个字符串,$word1$ 和 $word2$。 **要求**:如果 $word1$ 和 $word2$ 接近 ,就返回 $True$;否则,返回 $False$。 **说明**: - $1 \le word1.length, word2.length \le 10^5$。 - $word1$ 和 $word2$ 仅包含小写英文字母。 **示例**: - 示例 1: ```python 输入:word1 = "abc", word2 = "bca" 输出:True 解释:2 次操作从 word1 获得 word2 。 执行操作 1:"abc" -> "acb" 执行操作 1:"acb" -> "bca" ``` - 示例 2: ```python 输入:word1 = "a", word2 = "aa" 输出:False 解释:不管执行多少次操作,都无法从 word1 得到 word2 ,反之亦然。 ``` ## 解题思路 ### 思路 1:模拟 无论是操作 1,还是操作 2,只是对字符位置进行交换,而不会产生或者删除字符。 则我们只需要检查两个字符串的字符种类以及每种字符的个数是否相同即可。 具体步骤如下: 1. 分别使用哈希表 $cnts1$、$cnts2$ 统计每个字符串中的字符种类,每种字符的个数。 2. 判断两者的字符种类是否相等,并且判断每种字符的个数是否相同。 3. 如果字符种类相同,且每种字符的个数完全相同,则返回 $True$,否则,返回 $False$。 ### 思路 1:代码 ```Python class Solution: def closeStrings(self, word1: str, word2: str) -> bool: cnts1 = Counter(word1) cnts2 = Counter(word2) return cnts1.keys() == cnts2.keys() and sorted(cnts1.values()) == sorted(cnts2.values()) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(max(n1, n2) + |\sum| \times \log | \sum |)$,其中 $n1$、$n2$ 分别为字符串 $word1$、$word2$ 的长度,$\sum$ 为字符集,本题中 $| \sum | = 26$。 - **空间复杂度**:$O(| \sum |)$。 ================================================ FILE: docs/solutions/1600-1699/find-valid-matrix-given-row-and-column-sums.md ================================================ # [1605. 给定行和列的和求可行矩阵](https://leetcode.cn/problems/find-valid-matrix-given-row-and-column-sums/) - 标签:贪心、数组、矩阵 - 难度:中等 ## 题目链接 - [1605. 给定行和列的和求可行矩阵 - 力扣](https://leetcode.cn/problems/find-valid-matrix-given-row-and-column-sums/) ## 题目大意 **描述**:给你两个非负整数数组 `rowSum` 和 `colSum` ,其中 `rowSum[i]` 是二维矩阵中第 `i` 行元素的和,`colSum[j]` 是第 `j` 列元素的和。换句话说,我们不知道矩阵里的每个元素,只知道每一行的和,以及每一列的和。 **要求**:找到并返回一个大小为 `rowSum.length * colSum.length` 的任意非负整数矩阵,且该矩阵满足 `rowSum` 和 `colSum` 的要求。 **说明**: - 返回任意一个满足题目要求的二维矩阵即可,题目保证存在至少一个可行矩阵。 - $1 \le rowSum.length, colSum.length \le 500$。 - $0 \le rowSum[i], colSum[i] \le 10^8$。 - $sum(rows) == sum(columns)$。 **示例**: - 示例 1: ```python 输入:rowSum = [3,8], colSum = [4,7] 输出:[[3,0], [1,7]] 解释 第 0 行:3 + 0 = 3 == rowSum[0] 第 1 行:1 + 7 = 8 == rowSum[1] 第 0 列:3 + 1 = 4 == colSum[0] 第 1 列:0 + 7 = 7 == colSum[1] 行和列的和都满足题目要求,且所有矩阵元素都是非负的。 另一个可行的矩阵为 [[1,2], [3,5]] ``` ## 解题思路 ### 思路 1:贪心算法 题目要求找出一个满足要求的非负整数矩阵,矩阵中元素值可以为 `0`。所以我们可以尽可能将大的值填入前面的行和列中,然后剩余位置用 `0` 补齐即可。具体做法如下: 1. 使用二维数组 `board` 来保存答案,初始情况下,`board` 中元素全部赋值为 `0`。 2. 遍历二维数组的每一行,每一列。当前位置下的值为当前行的和与当前列的和的较小值,即 `board[row][col] = min(rowSum[row], colSum[col])`。 3. 更新当前行的和,将当前行的和减去 `board[row][col]`。 4. 更新当前列的和,将当前列的和减去 `board[row][col]`。 5. 遍历完返回二维数组 `board`。 ### 思路 1:贪心算法代码 ```python class Solution: def restoreMatrix(self, rowSum: List[int], colSum: List[int]) -> List[List[int]]: rows, cols = len(rowSum), len(colSum) board = [[0 for _ in range(cols)] for _ in range(rows)] for row in range(rows): for col in range(cols): board[row][col] = min(rowSum[row], colSum[col]) rowSum[row] -= board[row][col] colSum[col] -= board[row][col] return board ``` ================================================ FILE: docs/solutions/1600-1699/get-maximum-in-generated-array.md ================================================ # [1646. 获取生成数组中的最大值](https://leetcode.cn/problems/get-maximum-in-generated-array/) - 标签:数组、动态规划、模拟 - 难度:简单 ## 题目链接 - [1646. 获取生成数组中的最大值 - 力扣](https://leetcode.cn/problems/get-maximum-in-generated-array/) ## 题目大意 **描述**:给定一个整数 $n$,按照下述规则生成一个长度为 $n + 1$ 的数组 $nums$: - $nums[0] = 0$。 - $nums[1] = 1$。 - 当 $2 \le 2 \times i \le n$ 时,$nums[2 \times i] = nums[i]$。 - 当 $2 \le 2 \times i + 1 \le n$ 时,$nums[2 \times i + 1] = nums[i] + nums[i + 1]$。 **要求**:返回生成数组 $nums$ 中的最大值。 **说明**: - $0 \le n \le 100$。 **示例**: - 示例 1: ```python 输入:n = 7 输出:3 解释:根据规则: nums[0] = 0 nums[1] = 1 nums[(1 * 2) = 2] = nums[1] = 1 nums[(1 * 2) + 1 = 3] = nums[1] + nums[2] = 1 + 1 = 2 nums[(2 * 2) = 4] = nums[2] = 1 nums[(2 * 2) + 1 = 5] = nums[2] + nums[3] = 1 + 2 = 3 nums[(3 * 2) = 6] = nums[3] = 2 nums[(3 * 2) + 1 = 7] = nums[3] + nums[4] = 2 + 1 = 3 因此,nums = [0,1,1,2,1,3,2,3],最大值 3 ``` - 示例 2: ```python 输入:n = 2 输出:1 解释:根据规则,nums[0]、nums[1] 和 nums[2] 之中的最大值是 1 ``` ## 解题思路 ### 思路 1:模拟 1. 按照题目要求,定义一个长度为 $n + 1$ 的数组 $nums$。 2. 按照规则模拟生成对应的 $nums$ 数组元素。 3. 求出数组 $nums$ 中最大值,并作为答案返回。 ### 思路 1:代码 ```python class Solution: def getMaximumGenerated(self, n: int) -> int: if n <= 1: return n nums = [0 for _ in range(n + 1)] nums[1] = 1 for i in range(n): if 2 * i <= n: nums[2 * i] = nums[i] if 2 * i + 1 <= n: nums[2 * i + 1] = nums[i] + nums[i + 1] ans = max(nums) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1600-1699/index.md ================================================ ## 本章内容 - [1603. 设计停车系统](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/design-parking-system.md) - [1605. 给定行和列的和求可行矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/find-valid-matrix-given-row-and-column-sums.md) - [1614. 括号的最大嵌套深度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/maximum-nesting-depth-of-the-parentheses.md) - [1617. 统计子树中城市之间最大距离](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/count-subtrees-with-max-distance-between-cities.md) - [1631. 最小体力消耗路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/path-with-minimum-effort.md) - [1641. 统计字典序元音字符串的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/count-sorted-vowel-strings.md) - [1646. 获取生成数组中的最大值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/get-maximum-in-generated-array.md) - [1647. 字符频次唯一的最小删除次数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/minimum-deletions-to-make-character-frequencies-unique.md) - [1657. 确定两个字符串是否接近](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/determine-if-two-strings-are-close.md) - [1658. 将 x 减到 0 的最小操作数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/minimum-operations-to-reduce-x-to-zero.md) - [1672. 最富有客户的资产总量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/richest-customer-wealth.md) - [1695. 删除子数组的最大得分](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/maximum-erasure-value.md) - [1698. 字符串的不同子字符串个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/number-of-distinct-substrings-in-a-string.md) ================================================ FILE: docs/solutions/1600-1699/maximum-erasure-value.md ================================================ # [1695. 删除子数组的最大得分](https://leetcode.cn/problems/maximum-erasure-value/) - 标签:数组、哈希表、滑动窗口 - 难度:中等 ## 题目链接 - [1695. 删除子数组的最大得分 - 力扣](https://leetcode.cn/problems/maximum-erasure-value/) ## 题目大意 **描述**:给定一个正整数数组 $nums$,从中删除一个含有若干不同元素的子数组。删除子数组的「得分」就是子数组各元素之和 。 **要求**:返回只删除一个子数组可获得的最大得分。 **说明**: - **子数组**:如果数组 $b$ 是数组 $a$ 的一个连续子序列,即如果它等于 $a[l],a[l+1],...,a[r]$ ,那么它就是 $a$ 的一个子数组。 - $1 \le nums.length \le 10^5$。 - $1 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [4,2,4,5,6] 输出:17 解释:最优子数组是 [2,4,5,6] ``` - 示例 2: ```python 输入:nums = [5,2,1,2,5,2,1,2,5] 输出:8 解释:最优子数组是 [5,2,1] 或 [1,2,5] ``` ## 解题思路 ### 思路 1:滑动窗口 题目要求的是含有不同元素的连续子数组最大和,我们可以用滑动窗口来做,维护一个不包含重复元素的滑动窗口,计算最大的窗口和。具体方法如下: - 用滑动窗口 $window$ 来记录不重复的元素个数,$window$ 为哈希表类型。用 $window\_sum$ 来记录窗口内子数组元素和,$ans$ 用来维护最大子数组和。设定两个指针:$left$、$right$,分别指向滑动窗口的左右边界,保证窗口中没有重复元素。 - 一开始,$left$、$right$ 都指向 $0$。 - 将最右侧数组元素 $nums[right]$ 加入当前窗口 $window$ 中,记录该元素个数。 - 如果该窗口中该元素的个数多于 $1$ 个,即 $window[s[right]] > 1$,则不断右移 $left$,缩小滑动窗口长度,并更新窗口中对应元素的个数,直到 $window[s[right]] \le 1$。 - 维护更新无重复元素的最大子数组和。然后右移 $right$,直到 $right \ge len(nums)$ 结束。 - 输出无重复元素的最大子数组和。 ### 思路 1:代码 ```python class Solution: def maximumUniqueSubarray(self, nums: List[int]) -> int: window_sum = 0 left, right = 0, 0 window = dict() ans = 0 while right < len(nums): window_sum += nums[right] if nums[right] not in window: window[nums[right]] = 1 else: window[nums[right]] += 1 while window[nums[right]] > 1: window[nums[left]] -= 1 window_sum -= nums[left] left += 1 ans = max(ans, window_sum) right += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1600-1699/maximum-nesting-depth-of-the-parentheses.md ================================================ # [1614. 括号的最大嵌套深度](https://leetcode.cn/problems/maximum-nesting-depth-of-the-parentheses/) - 标签:栈、字符串 - 难度:简单 ## 题目链接 - [1614. 括号的最大嵌套深度 - 力扣](https://leetcode.cn/problems/maximum-nesting-depth-of-the-parentheses/) ## 题目大意 **描述**:给你一个有效括号字符串 $s$。 **要求**:返回该字符串 $s$ 的嵌套深度 。 **说明**: - 如果字符串满足以下条件之一,则可以称之为 有效括号字符串(valid parentheses string,可以简写为 VPS): - 字符串是一个空字符串 `""`,或者是一个不为 `"("` 或 `")"` 的单字符。 - 字符串可以写为 $AB$($A$ 与 B 字符串连接),其中 $A$ 和 $B$ 都是有效括号字符串 。 - 字符串可以写为 ($A$),其中 $A$ 是一个有效括号字符串。 - 类似地,可以定义任何有效括号字符串 $s$ 的 嵌套深度 $depth(s)$: - `depth("") = 0`。 - `depth(C) = 0`,其中 $C$ 是单个字符的字符串,且该字符不是 `"("` 或者 `")"`。 - `depth(A + B) = max(depth(A), depth(B))`,其中 $A$ 和 $B$ 都是 有效括号字符串。 - `depth("(" + A + ")") = 1 + depth(A)`,其中 A 是一个 有效括号字符串。 - $1 \le s.length \le 100$。 - $s$ 由数字 $0 \sim 9$ 和字符 `'+'`、`'-'`、`'*'`、`'/'`、`'('`、`')'` 组成。 - 题目数据保证括号表达式 $s$ 是有效的括号表达式。 **示例**: - 示例 1: ```python 输入:s = "(1+(2*3)+((8)/4))+1" 输出:3 解释:数字 8 在嵌套的 3 层括号中。 ``` - 示例 2: ```python 输入:s = "(1)+((2))+(((3)))" 输出:3 ``` ## 解题思路 ### 思路 1:模拟 我们可以使用栈来进行模拟括号匹配。遍历字符串 $s$,如果遇到左括号,则将其入栈,如果遇到右括号,则弹出栈中的左括号,与当前右括号进行匹配。在整个过程中栈的大小的最大值,就是我们要求的 $s$ 的嵌套深度,其实也是求最大的连续左括号的数量(跳过普通字符,并且与右括号匹配后)。具体步骤如下: 1. 使用 $ans$ 记录最大的连续左括号数量,使用 $cnt$ 记录当前栈中左括号的数量。 2. 遍历字符串 $s$: 1. 如果遇到左括号,则令 $cnt$ 加 $1$。 2. 如果遇到右括号,则令 $cnt$ 减 $1$。 3. 将 $cnt$ 与答案进行比较,更新最大的连续左括号数量。 3. 遍历完字符串 $s$,返回答案 $ans$。 ### 思路 1:代码 ```Python class Solution: def maxDepth(self, s: str) -> int: ans, cnt = 0, 0 for ch in s: if ch == '(': cnt += 1 elif ch == ')': cnt -= 1 ans = max(ans, cnt) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1600-1699/minimum-deletions-to-make-character-frequencies-unique.md ================================================ # [1647. 字符频次唯一的最小删除次数](https://leetcode.cn/problems/minimum-deletions-to-make-character-frequencies-unique/) - 标签:贪心、哈希表、字符串、排序 - 难度:中等 ## 题目链接 - [1647. 字符频次唯一的最小删除次数 - 力扣](https://leetcode.cn/problems/minimum-deletions-to-make-character-frequencies-unique/) ## 题目大意 **描述**:给定一个字符串 $s$。 **要求**:返回使 $s$ 成为优质字符串需要删除的最小字符数。 **说明**: - **频次**:指的是该字符在字符串中的出现次数。例如,在字符串 `"aab"` 中,`'a'` 的频次是 $2$,而 `'b'` 的频次是 $1$。 - **优质字符串**:如果字符串 $s$ 中不存在两个不同字符频次相同的情况,就称 $s$ 是优质字符串。 - $1 \le s.length \le 10^5$。 - $s$ 仅含小写英文字母。 **示例**: - 示例 1: ```python 输入:s = "aab" 输出:0 解释:s 已经是优质字符串。 ``` - 示例 2: ```python 输入:s = "aaabbbcc" 输出:2 解释:可以删除两个 'b' , 得到优质字符串 "aaabcc" 。 另一种方式是删除一个 'b' 和一个 'c' ,得到优质字符串 "aaabbc"。 ``` ## 解题思路 ### 思路 1:贪心算法 + 哈希表 1. 使用哈希表 $cnts$ 统计每字符串中每个字符出现次数。 2. 然后使用集合 $s\_set$ 保存不同的出现次数。 3. 遍历哈希表中所偶出现次数: 1. 如果当前出现次数不在集合 $s\_set$ 中,则将该次数添加到集合 $s\_set$ 中。 2. 如果当前出现次数在集合 $s\_set$ 中,则不断减少该次数,直到该次数不在集合 $s\_set$ 中停止,将次数添加到集合 $s\_set$ 中,同时将减少次数累加到答案 $ans$ 中。 4. 遍历完哈希表后返回答案 $ans$。 ### 思路 1:代码 ```Python class Solution: def minDeletions(self, s: str) -> int: cnts = Counter(s) s_set = set() ans = 0 for key, value in cnts.items(): while value > 0 and value in s_set: value -= 1 ans += 1 s_set.add(value) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1600-1699/minimum-operations-to-reduce-x-to-zero.md ================================================ # [1658. 将 x 减到 0 的最小操作数](https://leetcode.cn/problems/minimum-operations-to-reduce-x-to-zero/) - 标签:数组、哈希表、二分查找、前缀和、滑动窗口 - 难度:中等 ## 题目链接 - [1658. 将 x 减到 0 的最小操作数 - 力扣](https://leetcode.cn/problems/minimum-operations-to-reduce-x-to-zero/) ## 题目大意 **描述**:给定一个整数数组 $nums$ 和一个整数 $x$ 。每一次操作时,你应当移除数组 $nums$ 最左边或最右边的元素,然后从 $x$ 中减去该元素的值。请注意,需要修改数组以供接下来的操作使用。 **要求**:如果可以将 $x$ 恰好减到 $0$,返回最小操作数;否则,返回 $-1$。 **说明**: - $1 \le nums.length \le 10^5$。 - $1 \le nums[i] \le 10^4$。 - $1 \le x \le 10^9$。 **示例**: - 示例 1: ```python 输入:nums = [1,1,4,2,3], x = 5 输出:2 解释:最佳解决方案是移除后两个元素,将 x 减到 0。 ``` - 示例 2: ```python 输入:nums = [3,2,20,1,1,3], x = 10 输出:5 解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0。 ``` ## 解题思路 ### 思路 1:滑动窗口 将 $x$ 减到 $0$ 的最小操作数可以转换为求和等于 $sum(nums) - x$ 的最长连续子数组长度。我们可以维护一个区间和为 $sum(nums) - x$ 的滑动窗口,求出最长的窗口长度。具体做法如下: 令 `target = sum(nums) - x`,使用 $max\_len$ 维护和等于 $target$ 的最长连续子数组长度。然后用滑动窗口 $window\_sum$ 来记录连续子数组的和,设定两个指针:$left$、$right$,分别指向滑动窗口的左右边界,保证窗口中的和刚好等于 $target$。 - 一开始,$left$、$right$ 都指向 $0$。 - 向右移动 $right$,将最右侧元素加入当前窗口和 $window\_sum$ 中。 - 如果 $window\_sum > target$,则不断右移 $left$,缩小滑动窗口长度,并更新窗口和的最小值,直到 $window\_sum \le target$。 - 如果 $window\_sum == target$,则更新最长连续子数组长度。 - 然后继续右移 $right$,直到 $right \ge len(nums)$ 结束。 - 输出 $len(nums) - max\_len$ 作为答案。 - 注意判断题目中的特殊情况。 ### 思路 1:代码 ```python class Solution: def minOperations(self, nums: List[int], x: int) -> int: target = sum(nums) - x size = len(nums) if target < 0: return -1 if target == 0: return size left, right = 0, 0 window_sum = 0 max_len = float('-inf') while right < size: window_sum += nums[right] while window_sum > target: window_sum -= nums[left] left += 1 if window_sum == target: max_len = max(max_len, right - left + 1) right += 1 return len(nums) - max_len if max_len != float('-inf') else -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1600-1699/number-of-distinct-substrings-in-a-string.md ================================================ # [1698. 字符串的不同子字符串个数](https://leetcode.cn/problems/number-of-distinct-substrings-in-a-string/) - 标签:字典树、字符串、后缀数组、哈希函数、滚动哈希 - 难度:中等 ## 题目链接 - [1698. 字符串的不同子字符串个数 - 力扣](https://leetcode.cn/problems/number-of-distinct-substrings-in-a-string/) ## 题目大意 给定一个字符串 `s`。 要求:返回 `s` 的不同子字符串的个数。 注意:字符串的「子字符串」是由原字符串删除开头若干个字符(可能是 0 个)并删除结尾若干个字符(可能是 0 个)形成的字符串。 ## 解题思路 构建一颗字典树。分别将原字符串删除开头若干个字符的子字符串依次插入到字典树中。 每次插入过程中碰到字典树中没有的字符节点时,说明此时插入的字符串可作为新的子字符串。 我们可以通过统计插入过程中新建字符节点的次数的方式来获取不同子字符串的个数。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> int: """ Inserts a word into the trie. """ cur = self cnt = 0 for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cnt += 1 cur = cur.children[ch] cur.isEnd = True return cnt class Solution: def countDistinct(self, s: str) -> int: trie_tree = Trie() cnt = 0 for i in range(len(s)): cnt += trie_tree.insert(s[i:]) return cnt ``` ================================================ FILE: docs/solutions/1600-1699/path-with-minimum-effort.md ================================================ # [1631. 最小体力消耗路径](https://leetcode.cn/problems/path-with-minimum-effort/) - 标签:深度优先搜索、广度优先搜索、并查集、数组、二分查找、矩阵、堆(优先队列) - 难度:中等 ## 题目链接 - [1631. 最小体力消耗路径 - 力扣](https://leetcode.cn/problems/path-with-minimum-effort/) ## 题目大意 **描述**:给定一个 $rows \times cols$ 大小的二维数组 $heights$,其中 $heights[i][j]$ 表示为位置 $(i, j)$ 的高度。 现在要从左上角 $(0, 0)$ 位置出发,经过方格的一些点,到达右下角 $(n - 1, n - 1)$ 位置上。其中所经过路径的花费为「这条路径上所有相邻位置的最大高度差绝对值」。 **要求**:计算从 $(0, 0)$ 位置到 $(n - 1, n - 1)$ 的最优路径的花费。 **说明**: - **最优路径**:路径上「所有相邻位置最大高度差绝对值」最小的那条路径。 - $rows == heights.length$。 - $columns == heights[i].length$。 - $1 \le rows, columns \le 100$。 - $1 \le heights[i][j] \le 10^6$。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/10/25/ex1.png) ```python 输入:heights = [[1,2,2],[3,8,2],[5,3,5]] 输出:2 解释:路径 [1,3,5,3,5] 连续格子的差值绝对值最大为 2 。 这条路径比路径 [1,2,2,2,5] 更优,因为另一条路径差值最大值为 3。 ``` - 示例 2: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/10/25/ex2.png) ```python 输入:heights = [[1,2,3],[3,8,4],[5,3,5]] 输出:1 解释:路径 [1,2,3,4,5] 的相邻格子差值绝对值最大为 1 ,比路径 [1,3,5,3,5] 更优。 ``` ## 解题思路 ### 思路 1:并查集 将整个网络抽象为一个无向图,每个点与相邻的点(上下左右)之间都存在一条无向边,边的权重为两个点之间的高度差绝对值。 我们要找到左上角到右下角的最优路径,可以遍历所有的点,将所有的边存储到数组中,每条边的存储格式为 $[x, y, h]$,意思是编号 $x$ 的点和编号为 $y$ 的点之间的权重为 $h$。 然后按照权重从小到大的顺序,对所有边进行排序。 再按照权重大小遍历所有边,将其依次加入并查集中。并且每次都需要判断 $(0, 0)$ 点和 $(n - 1, n - 1)$ 点是否连通。 如果连通,则该边的权重即为答案。 ### 思路 1:代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] self.count = n def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.count -= 1 def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def minimumEffortPath(self, heights: List[List[int]]) -> int: row_size = len(heights) col_size = len(heights[0]) size = row_size * col_size edges = [] for row in range(row_size): for col in range(col_size): if row < row_size - 1: x = row * col_size + col y = (row + 1) * col_size + col h = abs(heights[row][col] - heights[row + 1][col]) edges.append([x, y, h]) if col < col_size - 1: x = row * col_size + col y = row * col_size + col + 1 h = abs(heights[row][col] - heights[row][col + 1]) edges.append([x, y, h]) edges.sort(key=lambda x: x[2]) union_find = UnionFind(size) for edge in edges: x, y, h = edge[0], edge[1], edge[2] union_find.union(x, y) if union_find.is_connected(0, size - 1): return h return 0 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n \times \log(m \times n))$,其中 $m$ 和 $n$ 分别为矩阵的行数和列数。主要耗时在于对所有边进行排序,排序复杂度为 $O(m \times n \times \log(m \times n))$,并查集的合并与查找操作均摊为常数级。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/1600-1699/richest-customer-wealth.md ================================================ # [1672. 最富有客户的资产总量](https://leetcode.cn/problems/richest-customer-wealth/) - 标签:数组、矩阵 - 难度:简单 ## 题目链接 - [1672. 最富有客户的资产总量 - 力扣](https://leetcode.cn/problems/richest-customer-wealth/) ## 题目大意 **描述**:给定一个 $m \times n$ 的整数网格 $accounts$,其中 $accounts[i][j]$ 是第 $i$ 位客户在第 $j$ 家银行托管的资产数量。 **要求**:返回最富有客户所拥有的资产总量。 **说明**: - 客户的资产总量:指的是他们在各家银行托管的资产数量之和。 - 最富有客户:资产总量最大的客户。 - $m == accounts.length$。 - $n == accounts[i].length$。 - $1 \le m, n \le 50$。 - $1 \le accounts[i][j] \le 100$。 **示例**: - 示例 1: ```python 输入:accounts = [[1,2,3],[3,2,1]] 输出:6 解释: 第 1 位客户的资产总量 = 1 + 2 + 3 = 6 第 2 位客户的资产总量 = 3 + 2 + 1 = 6 两位客户都是最富有的,资产总量都是 6 ,所以返回 6。 ``` - 示例 2: ```python 输入:accounts = [[1,5],[7,3],[3,5]] 输出:10 解释: 第 1 位客户的资产总量 = 6 第 2 位客户的资产总量 = 10 第 3 位客户的资产总量 = 8 第 2 位客户是最富有的,资产总量是 10,随意返回 10。 ``` ## 解题思路 ### 思路 1:直接模拟 1. 使用变量 $max\_ans$ 存储最富有客户所拥有的资产总量。 2. 遍历所有客户,对于当前客户 $accounts[i]$,统计其拥有的资产总量。 3. 将当前客户的资产总量与 $max\_ans$ 进行比较,如果大于 $max\_ans$,则更新 $max\_ans$ 的值。 4. 遍历完所有客户,最终返回 $max\_ans$ 作为结果。 ### 思路 1:代码 ```python class Solution: def maximumWealth(self, accounts: List[List[int]]) -> int: max_ans = 0 for i in range(len(accounts)): total = 0 for j in range(len(accounts[i])): total += accounts[i][j] if total > max_ans: max_ans = total return max_ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。其中 $m$ 和 $n$ 分别为二维数组 $accounts$ 的行数和列数。两重循环遍历的时间复杂度为 $O(m * n)$ 。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1700-1799/calculate-money-in-leetcode-bank.md ================================================ # [1716. 计算力扣银行的钱](https://leetcode.cn/problems/calculate-money-in-leetcode-bank/) - 标签:数学 - 难度:简单 ## 题目链接 - [1716. 计算力扣银行的钱 - 力扣](https://leetcode.cn/problems/calculate-money-in-leetcode-bank/) ## 题目大意 **描述**:Hercy 每天都往力扣银行里存钱。 最开始,他在周一的时候存入 $1$ 块钱。从周二到周日,他每天都比前一天多存入 $1$ 块钱。在接下来的每个周一,他都会比前一个周一多存入 $1$ 块钱。 给定一个整数 $n$。 **要求**:计算在第 $n$ 天结束的时候,Hercy 在力扣银行中总共存了多少块钱。 **说明**: - $1 \le n \le 1000$。 **示例**: - 示例 1: ```python 输入:n = 4 输出:10 解释:第 4 天后,总额为 1 + 2 + 3 + 4 = 10。 ``` - 示例 2: ```python 输入:n = 10 输出:37 解释:第 10 天后,总额为 (1 + 2 + 3 + 4 + 5 + 6 + 7) + (2 + 3 + 4) = 37 。注意到第二个星期一,Hercy 存入 2 块钱。 ``` ## 解题思路 ### 思路 1:暴力模拟 1. 记录当前周 $week$ 和当前周的当前天数 $day$。 2. 按照题目要求,每天增加 $1$ 块钱,每周一比上周一增加 $1$ 块钱。这样,每天存钱数为 $week + day - 1$。 3. 将每天存的钱数累加起来即为答案。 ### 思路 1:代码 ```python class Solution: def totalMoney(self, n: int) -> int: weak, day = 1, 1 ans = 0 for i in range(n): ans += weak + day - 1 day += 1 if day == 8: day = 1 weak += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:等差数列计算优化 每周一比上周一增加 $1$ 块钱,则每周七天存钱总数比上一周多 $7$ 块钱。所以每周存的钱数是一个等差数列。我们可以通过高斯求和公式求出所有整周存的钱数,再计算出剩下天数存的钱数,两者相加即为答案。 ### 思路 2:代码 ```python class Solution: def totalMoney(self, n: int) -> int: week_cnt = n // 7 weak_first_money = (1 + 7) * 7 // 2 weak_last_money = weak_first_money + 7 * (week_cnt - 1) week_ans = (weak_first_money + weak_last_money) * week_cnt // 2 day_cnt = n % 7 day_first_money = 1 + week_cnt day_last_money = day_first_money + day_cnt - 1 day_ans = (day_first_money + day_last_money) * day_cnt // 2 return week_ans + day_ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(1)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1700-1799/check-if-one-string-swap-can-make-strings-equal.md ================================================ # [1790. 仅执行一次字符串交换能否使两个字符串相等](https://leetcode.cn/problems/check-if-one-string-swap-can-make-strings-equal/) - 标签:哈希表、字符串、计数 - 难度:简单 ## 题目链接 - [1790. 仅执行一次字符串交换能否使两个字符串相等 - 力扣](https://leetcode.cn/problems/check-if-one-string-swap-can-make-strings-equal/) ## 题目大意 **描述**:给定两个长度相等的字符串 `s1` 和 `s2`。 已知一次「字符串交换操作」步骤如下:选出某个字符串中的两个下标(不一定要相同),并交换这两个下标所对应的字符。 **要求**:如果对其中一个字符串执行最多一次字符串交换可以使两个字符串相等,则返回 `True`;否则返回 `False`。 **说明**: - $1 \le s1.length, s2.length \le 100$。 - $s1.length == s2.length$。 - `s1` 和 `s2` 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 给定:s1 = "bank", s2 = "kanb" 输出:True 解释:交换 s1 中的第一个和最后一个字符可以得到 "kanb",与 s2 相同 ``` ## 解题思路 ### 思路 1: - 用一个变量 `diff_cnt` 记录两个字符串中对应位置上出现不同字符的次数。用 `c1`、`c2` 记录第一次出现不同字符时两个字符串对应位置上的字符。 - 遍历两个字符串,对于第 `i` 个位置的字符 `s1[i]` 和 `s2[i]`: - 如果 `s1[i] == s2[i]`,继续判断下一个位置。 - 如果 `s1[i] != s2[i]`,则出现不同字符的次数加 `1`。 - 如果出现不同字符的次数等于 `1`,则记录第一次出现不同字符时两个字符串对应位置上的字符。 - 如果出现不同字符的次数等于 `2`,则判断第一次出现不同字符时两个字符串对应位置上的字符与当前位置字符交换之后是否相等。如果不等,则说明交换之后 `s1` 和 `s2` 不相等,返回 `False`。如果相等,则继续判断下一个位置。 - 如果出现不同字符的次数超过 `2`,则不符合最多一次字符串交换的要求,返回 `False`。 - 如果遍历完,出现不同字符的次数为 `0` 或者 `2`,为 `0` 说明无需交换,本身 `s1` 和 `s2` 就是相等的,为 `2` 说明交换一次字符串之后 `s1` 和 `s2` 相等,此时返回 `True`。否则返回 `False`。 ## 代码 ### 思路 1 代码: ```python class Solution: def areAlmostEqual(self, s1: str, s2: str) -> bool: size = len(s1) diff_cnt = 0 c1, c2 = None, None for i in range(size): if s1[i] == s2[i]: continue diff_cnt += 1 if diff_cnt == 1: c1 = s1[i] c2 = s2[i] elif diff_cnt == 2: if c1 != s2[i] or c2 != s1[i]: return False else: return False return diff_cnt == 0 or diff_cnt == 2 ``` ================================================ FILE: docs/solutions/1700-1799/decode-xored-array.md ================================================ # [1720. 解码异或后的数组](https://leetcode.cn/problems/decode-xored-array/) - 标签:位运算、数组 - 难度:简单 ## 题目链接 - [1720. 解码异或后的数组 - 力扣](https://leetcode.cn/problems/decode-xored-array/) ## 题目大意 n 个非负整数构成数组 arr,经过编码后变为长度为 n-1 的整数数组 encoded,其中 `encoded[i] = arr[i] XOR arr[i+1]`。例如 arr = [1, 0, 2, 1] 经过编码后变为 encoded = [1, 2, 3]。 现在给定编码后的数组 encoded 和原数组 arr 的第一个元素 arr[0]。要求返回原数组 arr。 ## 解题思路 首先要了解异或的性质: - 异或运算满足交换律和结合律。 - 交换律:`a^b = b^a` - 结合律:`(a^b)^c = a^(b^c)` - 任何整数和自身做异或运算结果都为 0,即 `x^x = 0`。 - 任何整数和 0 做异或运算结果都为其本身,即 `x^0 = 0`。 已知当 $1 \le i \le n$ 时,有 `encoded[i-1] = arr[i-1] XOR arr[i]`。两边同时「异或」上 arr[i-1]。得: - `encoded[i-1] XOR arr[i-1] = arr[i-1] XOR arr[i] XOR arr[i-1]` - `encoded[i-1] XOR arr[i-1] = arr[i] XOR 0` - `encoded[i-1] XOR arr[i-1] = arr[i]` 所以就可以根据所得结论 `arr[i] = encoded[i-1] XOR arr[i-1]` 模拟得出原数组 arr。 ## 代码 ```python class Solution: def decode(self, encoded: List[int], first: int) -> List[int]: n = len(encoded) + 1 arr = [0] * n arr[0] = first for i in range(1, n): arr[i] = encoded[i-1] ^ arr[i-1] return arr ``` ================================================ FILE: docs/solutions/1700-1799/find-center-of-star-graph.md ================================================ # [1791. 找出星型图的中心节点](https://leetcode.cn/problems/find-center-of-star-graph/) - 标签:图 - 难度:简单 ## 题目链接 - [1791. 找出星型图的中心节点 - 力扣](https://leetcode.cn/problems/find-center-of-star-graph/) ## 题目大意 **描述**:有一个无向的行型图,由 $n$ 个编号 $1 \sim n$ 的节点组成。星型图有一个中心节点,并且恰好有 $n - 1$ 条边将中心节点与其他每个节点连接起来。 给定一个二维整数数组 $edges$,其中 $edges[i] = [u_i, v_i]$ 表示节点 $u_i$ 与节点 $v_i$ 之间存在一条边。 **要求**:找出并返回该星型图的中心节点。 **说明**: - $3 \le n \le 10^5$。 - $edges.length == n - 1$。 - $edges[i].length == 2$。 - $1 \le ui, vi \le n$。 - $ui \ne vi$。 - 题目数据给出的 $edges$ 表示一个有效的星型图。 **示例**: - 示例 1: ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2021/03/14/star_graph.png) ```python 输入:edges = [[1,2],[2,3],[4,2]] 输出:2 解释:如上图所示,节点 2 与其他每个节点都相连,所以节点 2 是中心节点。 ``` - 示例 2: ```python 输入:edges = [[1,2],[5,1],[1,3],[1,4]] 输出:1 ``` ## 解题思路 ### 思路 1:求度数 根据题意可知:中心节点恰好有 $n - 1$ 条边将中心节点与其他每个节点连接起来,那么中心节点的度数一定为 $n - 1$。则我们可以遍历边集数组 $edges$,统计出每个节点 $u$ 的度数 $degrees[u]$。最后返回度数为 $n - 1$ 的节点编号。 ### 思路 1:代码 ```python class Solution: def findCenter(self, edges: List[List[int]]) -> int: n = len(edges) + 1 degrees = collections.Counter() for u, v in edges: degrees[u] += 1 degrees[v] += 1 for i in range(1, n + 1): if degrees[i] == n - 1: return i return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1700-1799/find-nearest-point-that-has-the-same-x-or-y-coordinate.md ================================================ # [1779. 找到最近的有相同 X 或 Y 坐标的点](https://leetcode.cn/problems/find-nearest-point-that-has-the-same-x-or-y-coordinate/) - 标签:数组 - 难度:简单 ## 题目链接 - [1779. 找到最近的有相同 X 或 Y 坐标的点 - 力扣](https://leetcode.cn/problems/find-nearest-point-that-has-the-same-x-or-y-coordinate/) ## 题目大意 **描述**:给定两个整数 `x` 和 `y`,表示笛卡尔坐标系下的 `(x, y)` 点。再给定一个数组 `points`,其中 `points[i] = [ai, bi]`,表示在 `(ai, bi)` 处有一个点。当一个点与 `(x, y)` 拥有相同的 `x` 坐标或者拥有相同的 `y` 坐标时,我们称这个点是有效的。 **要求**:返回数组中距离 `(x, y)` 点出曼哈顿距离最近的有效点在 `points` 中的下标位置。如果有多个最近的有效点,则返回下标最小的一个。如果没有有效点,则返回 `-1`。 **说明**: - **曼哈顿距离**:`(x1, y1)` 和 `(x2, y2)` 之间的曼哈顿距离为 `abs(x1 - x2) + abs(y1 - y2)` 。 - $1 \le points.length \le 10^4$。 - $points[i].length == 2$。 - $1 \le x, y, ai, bi \le 10^4$。 **示例**: - 示例 1: ```python 输入:x = 3, y = 4, points = [[1, 2], [3, 1], [2, 4], [2, 3], [4, 4]] 输出:2 解释:在所有点中 [3, 1]、[2, 4]、[4, 4] 为有效点。其中 [2, 4]、[4, 4] 距离 [3, 4] 曼哈顿距离最近,都为 1。[2, 4] 下标最小,所以返回 2。 ``` ## 解题思路 ### 思路 1: - 使用 `min_dist` 记录下有效点中最近的曼哈顿距离,初始化为 `float('inf')`。使用 `min_index` 记录下符合要求的最小下标。 - 遍历 `points` 数组,遇到有效点之后计算一下当前有效点与 `(x, y)` 的曼哈顿距离,并判断更新一下有效点中最近的曼哈顿距离 `min_dist` 和符合要求的最小下标 `min_index`。 - 遍历完之后,判断一下 `min_dist` 是否等于 `float('inf')`。如果等于,说明没有找到有效点,则返回 `-1`。如果不等于,则返回符合要求的最小下标 `min_index`。 ## 代码 ### 思路 1 代码: ```python class Solution: def nearestValidPoint(self, x: int, y: int, points: List[List[int]]) -> int: min_dist = float('inf') min_index = 0 for i in range(len(points)): if points[i][0] == x or points[i][1] == y: dist = abs(points[i][0] - x) + abs(points[i][1] - y) if dist < min_dist: min_dist = dist min_index = i if min_dist == float('inf'): return -1 return min_index ``` ================================================ FILE: docs/solutions/1700-1799/index.md ================================================ ## 本章内容 - [1710. 卡车上的最大单元数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/maximum-units-on-a-truck.md) - [1716. 计算力扣银行的钱](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/calculate-money-in-leetcode-bank.md) - [1720. 解码异或后的数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/decode-xored-array.md) - [1726. 同积元组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/tuple-with-same-product.md) - [1736. 替换隐藏数字得到的最晚时间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/latest-time-by-replacing-hidden-digits.md) - [1742. 盒子中小球的最大数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/maximum-number-of-balls-in-a-box.md) - [1749. 任意子数组和的绝对值的最大值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/maximum-absolute-sum-of-any-subarray.md) - [1763. 最长的美好子字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/longest-nice-substring.md) - [1779. 找到最近的有相同 X 或 Y 坐标的点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/find-nearest-point-that-has-the-same-x-or-y-coordinate.md) - [1790. 仅执行一次字符串交换能否使两个字符串相等](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/check-if-one-string-swap-can-make-strings-equal.md) - [1791. 找出星型图的中心节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/find-center-of-star-graph.md) ================================================ FILE: docs/solutions/1700-1799/latest-time-by-replacing-hidden-digits.md ================================================ # [1736. 替换隐藏数字得到的最晚时间](https://leetcode.cn/problems/latest-time-by-replacing-hidden-digits/) - 标签:贪心、字符串 - 难度:简单 ## 题目链接 - [1736. 替换隐藏数字得到的最晚时间 - 力扣](https://leetcode.cn/problems/latest-time-by-replacing-hidden-digits/) ## 题目大意 **描述**:给定一个字符串 $time$,格式为 `hh:mm`(小时:分钟),其中某几位数字被隐藏(用 `?` 表示)。 **要求**:替换 $time$ 中隐藏的数字,返回你可以得到的最晚有效时间。 **说明**: - **有效时间**: `00:00` 到 `23:59` 之间的所有时间,包括 `00:00` 和 `23:59`。 - $time$ 的格式为 `hh:mm`。 - 题目数据保证你可以由输入的字符串生成有效的时间。 **示例**: - 示例 1: ```python 输入:time = "2?:?0" 输出:"23:50" 解释:以数字 '2' 开头的最晚一小时是 23 ,以 '0' 结尾的最晚一分钟是 50。 ``` - 示例 2: ```python 输入:time = "0?:3?" 输出:"09:39" ``` ## 解题思路 ### 思路 1:贪心算法 为了使有效时间尽可能晚,我们可以从高位到低位依次枚举所有符号为 `?` 的字符。在保证时间有效的前提下,每一位上取最大值,并进行保存。具体步骤如下: - 如果第 $1$ 位为 `?`: - 如果第 $2$ 位已经确定,并且范围在 $[4, 9]$ 中间,则第 $1$ 位最大为 $1$; - 否则第 $1$ 位最大为 $2$。 - 如果第 $2$ 位为 `?`: - 如果第 $1$ 位上值为 $2$,则第 $2$ 位最大可以为 $3$; - 否则第 $2$ 位最大为 $9$。 - 如果第 $3$ 位为 `?`: - 第 $3$ 位最大可以为 $5$。 - 如果第 $4$ 位为 `?`: - 第 $4$ 位最大可以为 $9$。 ### 思路 1:代码 ```python class Solution: def maximumTime(self, time: str) -> str: time_list = list(time) if time_list[0] == '?': if '4' <= time_list[1] <= '9': time_list[0] = '1' else: time_list[0] = '2' if time_list[1] == '?': if time_list[0] == '2': time_list[1] = '3' else: time_list[1] = '9' if time_list[3] == '?': time_list[3] = '5' if time_list[4] == '?': time_list[4] = '9' return "".join(time_list) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1700-1799/longest-nice-substring.md ================================================ # [1763. 最长的美好子字符串](https://leetcode.cn/problems/longest-nice-substring/) - 标签:位运算、哈希表、字符串、分治、滑动窗口 - 难度:简单 ## 题目链接 - [1763. 最长的美好子字符串 - 力扣](https://leetcode.cn/problems/longest-nice-substring/) ## 题目大意 **描述**: 给定一个字符串 $s$。 **要求**:返回 $s$ 最长的美好子字符串。 **说明**: - **美好字符串**:当一个字符串 $s$ 包含的每一种字母的大写和小写形式同时出现在 $s$ 中,就称这个字符串 $s$ 是美好字符串。 - $1 \le s.length \le 100$。 **示例**: - 示例 1: ```python 输入:s = "YazaAay" 输出:"aAa" 解释:"aAa" 是一个美好字符串,因为这个子串中仅含一种字母,其小写形式 'a' 和大写形式 'A' 也同时出现了。 "aAa" 是最长的美好子字符串。 ``` - 示例 2: ```python 输入:s = "Bb" 输出:"Bb" 解释:"Bb" 是美好字符串,因为 'B' 和 'b' 都出现了。整个字符串也是原字符串的子字符串。 ``` ## 解题思路 ### 思路 1:枚举 字符串 $s$ 的范围为 $[1, 100]$,长度较小,我们可以枚举所有的子串,判断该子串是否为美好字符串。 由于大小写英文字母各有 $26$ 位,则我们可以利用二进制来标记某字符是否在子串中出现过,我们使用 $lower$ 标记子串中出现过的小写字母,使用 $upper$ 标记子串中出现过的大写字母。如果满足 $lower == upper$,则说明该子串为美好字符串。 具体解法步骤如下: 1. 使用二重循环遍历字符串。对于子串 $s[i]…s[j]$,使用 $lower$ 标记子串中出现过的小写字母,使用 $upper$ 标记子串中出现过的大写字母。 2. 如果 $s[j]$ 为小写字母,则 $lower$ 对应位置标记为出现过该小写字母,即:`lower |= 1 << (ord(s[j]) - ord('a'))`。 3. 如果 $s[j]$ 为大写字母,则 $upper$ 对应位置标记为出现过该小写字母,即:`upper |= 1 << (ord(s[j]) - ord('A'))`。 4. 判断当前子串对应 $lower$ 和 $upper$ 是否相等,如果相等,并且子串长度大于记录的最长美好字符串长度,则更新最长美好字符串长度。 5. 遍历完返回记录的最长美好字符串长度。 ### 思路 1:代码 ```Python class Solution: def longestNiceSubstring(self, s: str) -> str: size = len(s) max_pos, max_len = 0, 0 for i in range(size): lower, upper = 0, 0 for j in range(i, size): if s[j].islower(): lower |= 1 << (ord(s[j]) - ord('a')) else: upper |= 1 << (ord(s[j]) - ord('A')) if lower == upper and j - i + 1 > max_len: max_len = j - i + 1 max_pos = i return s[max_pos: max_pos + max_len] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 为字符串 $s$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1700-1799/maximum-absolute-sum-of-any-subarray.md ================================================ # [1749. 任意子数组和的绝对值的最大值](https://leetcode.cn/problems/maximum-absolute-sum-of-any-subarray/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [1749. 任意子数组和的绝对值的最大值 - 力扣](https://leetcode.cn/problems/maximum-absolute-sum-of-any-subarray/) ## 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:找出 $nums$ 中「和的绝对值」最大的任意子数组(可能为空),并返回最大值。 **说明**: - **子数组 $[nums_l, nums_{l+1}, ..., nums_{r-1}, nums_{r}]$ 的和的绝对值**:$abs(nums_l + nums_{l+1} + ... + nums_{r-1} + nums_{r})$。 - $abs(x)$ 定义如下: - 如果 $x$ 是负整数,那么 $abs(x) = -x$。 - 如果 $x$ 是非负整数,那么 $abs(x) = x$。 - $1 \le nums.length \le 10^5$。 - $-10^4 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [1,-3,2,3,-4] 输出:5 解释:子数组 [2,3] 和的绝对值最大,为 abs(2+3) = abs(5) = 5。 ``` - 示例 2: ```python 输入:nums = [2,-5,1,-4,3,-2] 输出:8 解释:子数组 [-5,1,-4] 和的绝对值最大,为 abs(-5+1-4) = abs(-8) = 8。 ``` ## 解题思路 ### 思路 1:动态规划 子数组和的绝对值的最大值,可能来自于「连续子数组的最大和」,也可能来自于「连续子数组的最小和」。 而求解「连续子数组的最大和」,我们可以参考「[0053. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/)」的做法,使用一个变量 $mmax$ 来表示以第 $i$ 个数结尾的连续子数组的最大和。使用另一个变量 $mmin$ 来表示以第 $i$ 个数结尾的连续子数组的最小和。然后取两者绝对值的最大值为答案 $ans$。 具体步骤如下: 1. 遍历数组 $nums$,对于当前元素 $nums[i]$: 1. 如果 $mmax < 0$,则「第 $i - 1$ 个数结尾的连续子数组的最大和」+「第 $i$ 个数的值」<「第 $i$ 个数的值」,所以 $mmax$ 应取「第 $i$ 个数的值」,即:$mmax = nums[i]$。 2. 如果 $mmax \ge 0$ ,则「第 $i - 1$ 个数结尾的连续子数组的最大和」 +「第 $i$ 个数的值」 >= 第 $i$ 个数的值,所以 $mmax$ 应取「第 $i - 1$ 个数结尾的连续子数组的最大和」 +「第 $i$ 个数的值」,即:$mmax = mmax + nums[i]$。 3. 如果 $mmin > 0$,则「第 $i - 1$ 个数结尾的连续子数组的最大和」+「第 $i$ 个数的值」>「第 $i$ 个数的值」,所以 $mmax$ 应取「第 $i$ 个数的值」,即:$mmax = nums[i]$。 4. 如果 $mmin \le 0$ ,则「第 $i - 1$ 个数结尾的连续子数组的最大和」 +「第 $i$ 个数的值」 <= 第 $i$ 个数的值,所以 $mmax$ 应取「第 $i - 1$ 个数结尾的连续子数组的最大和」 +「第 $i$ 个数的值」,即:$mmin = mmin + nums[i]$。 5. 维护答案 $ans$,将 $mmax$ 和 $mmin$ 绝对值的最大值与 $ans$ 进行比较,并更新 $ans$。 2. 遍历完返回答案 $ans$。 ### 思路 1:代码 ```python class Solution: def maxAbsoluteSum(self, nums: List[int]) -> int: ans = 0 mmax, mmin = 0, 0 for num in nums: mmax = max(mmax, 0) + num mmin = min(mmin, 0) + num ans = max(ans, mmax, -mmin) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1700-1799/maximum-number-of-balls-in-a-box.md ================================================ # [1742. 盒子中小球的最大数量](https://leetcode.cn/problems/maximum-number-of-balls-in-a-box/) - 标签:哈希表、数学、计数 - 难度:简单 ## 题目链接 - [1742. 盒子中小球的最大数量 - 力扣](https://leetcode.cn/problems/maximum-number-of-balls-in-a-box/) ## 题目大意 **描述**:给定两个整数 $lowLimit$ 和 $highLimt$,代表 $n$ 个小球的编号(包括 $lowLimit$ 和 $highLimit$,即 $n == highLimit = lowLimit + 1$)。另外有无限个盒子。 现在的工作是将每个小球放入盒子中,其中盒子的编号应当等于小球编号上每位数字的和。例如,编号 $321$ 的小球应当放入编号 $3 + 2 + 1 = 6$ 的盒子,而编号 $10$ 的小球应当放入编号 $1 + 0 = 1$ 的盒子。 **要求**:返回放有最多小球的盒子中的小球数量。如果有多个盒子都满足放有最多小球,只需返回其中任一盒子的小球数量。 **说明**: - $1 \le lowLimit \le highLimit \le 10^5$。 **示例**: - 示例 1: ```python 输入:lowLimit = 1, highLimit = 10 输出:2 解释: 盒子编号:1 2 3 4 5 6 7 8 9 10 11 ... 小球数量:2 1 1 1 1 1 1 1 1 0 0 ... 编号 1 的盒子放有最多小球,小球数量为 2。 ``` - 示例 2: ```python 输入:lowLimit = 5, highLimit = 15 输出:2 解释: 盒子编号:1 2 3 4 5 6 7 8 9 10 11 ... 小球数量:1 1 1 1 2 2 1 1 1 0 0 ... 编号 5 和 6 的盒子放有最多小球,每个盒子中的小球数量都是 2。 ``` ## 解题思路 ### 思路 1:动态规划 + 数位 DP 将 $lowLimit$、$highLimit$ 转为字符串 $s1$、$s2$,并将 $s1$ 补上前导 $0$,令其与 $s2$ 长度一致。定义递归函数 `def dfs(pos, remainTotal, isMaxLimit, isMinLimit):` 表示构造第 $pos$ 位及之后剩余数位和为 $remainTotal$ 的合法方案数。 因为数据范围为 $[1, 10^5]$,对应数位和范围为 $[1, 45]$。因此我们可以枚举所有的数位和,并递归调用 `dfs(i, remainTotal, isMaxLimit, isMinLimit)`,求出不同数位和对应的方案数,并求出最大方案数。 接下来按照如下步骤进行递归。 1. 从 `dfs(0, i, True, True)` 开始递归。 `dfs(0, i, True, True)` 表示: 1. 从位置 $0$ 开始构造。 2. 剩余数位和为 $i$。 3. 开始时当前数位最大值受到最高位数位的约束。 4. 开始时当前数位最小值受到最高位数位的约束。 2. 如果剩余数位和小于 $0$,说明当前方案不符合要求,则返回方案数 $0$。 3. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时: 1. 如果剩余数位和 $remainTotal$ 等于 $0$,说明当前方案符合要求,则返回方案数 $1$。 2. 如果剩余数位和 $remainTotal$ 不等于 $0$,说明当前方案不符合要求,则返回方案数 $0$。 4. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:`ans = 0`。 5. 如果遇到 $isNum == False$,说明之前位数没有填写数字,当前位可以跳过,这种情况下方案数等于 $pos + 1$ 位置上没有受到 $pos$ 位的约束,并且之前没有填写数字时的方案数,即:`ans = dfs(i + 1, state, False, False)`。 6. 根据 $isMaxLimit$ 和 $isMinLimit$ 来决定填当前位数位所能选择的最小数字($minX$)和所能选择的最大数字($maxX$)。 7. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $d$。 8. 方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, remainTotal - d, isMaxLimit and d == maxX, isMinLimit and d == minX)`。 1. `remainTotal - d` 表示当前剩余数位和减去 $d$。 2. `isMaxLimit and d == maxX` 表示 $pos + 1$ 位最大值受到之前 $pos$ 位限制。 3. `isMinLimit and d == maxX` 表示 $pos + 1$ 位最小值受到之前 $pos$ 位限制。 9. 最后返回所有 `dfs(0, i, True, True)` 中最大的方案数即可。 ### 思路 1:代码 ```python class Solution: def countBalls(self, lowLimit: int, highLimit: int) -> int: s1, s2 = str(lowLimit), str(highLimit) m, n = len(s1), len(s2) if m < n: s1 = '0' * (n - m) + s1 @cache # pos: 第 pos 个数位 # remainTotal: 表示剩余数位和 # isMaxLimit: 表示是否受到上限选择限制。如果为真,则第 pos 位填入数字最多为 s2[pos];如果为假,则最大可为 9。 # isMinLimit: 表示是否受到下限选择限制。如果为真,则第 pos 位填入数字最小为 s1[pos];如果为假,则最小可为 0。 def dfs(pos, remainTotal, isMaxLimit, isMinLimit): if remainTotal < 0: return 0 if pos == n: # remainTotal 为 0,则表示当前方案符合要求 return int(remainTotal == 0) ans = 0 # 如果前一位没有填写数字,或受到选择限制,则最小可选择数字为 s1[pos],否则最少为 0(可以含有前导 0)。 minX = int(s1[pos]) if isMinLimit else 0 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s2[pos]) if isMaxLimit else 9 # 枚举可选择的数字 for d in range(minX, maxX + 1): ans += dfs(pos + 1, remainTotal - d, isMaxLimit and d == maxX, isMinLimit and d == minX) return ans ans = 0 for i in range(46): ans = max(ans, dfs(0, i, True, True)) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n \times 45)$。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/1700-1799/maximum-units-on-a-truck.md ================================================ # [1710. 卡车上的最大单元数](https://leetcode.cn/problems/maximum-units-on-a-truck/) - 标签:贪心、数组、排序 - 难度:简单 ## 题目链接 - [1710. 卡车上的最大单元数 - 力扣](https://leetcode.cn/problems/maximum-units-on-a-truck/) ## 题目大意 **描述**:现在需要将一些箱子装在一辆卡车上。给定一个二维数组 $boxTypes$,其中 $boxTypes[i] = [numberOfBoxesi, numberOfUnitsPerBoxi]$。 $numberOfBoxesi$ 是类型 $i$ 的箱子的数量。$numberOfUnitsPerBoxi$ 是类型 $i$ 的每个箱子可以装载的单元数量。 再给定一个整数 $truckSize$ 表示一辆卡车上可以装载箱子的最大数量。只要箱子数量不超过 $truckSize$,你就可以选择任意箱子装到卡车上。 **要求**:返回卡车可以装载的最大单元数量。 **说明**: - $1 \le boxTypes.length \le 1000$。 - $1 \le numberOfBoxesi, numberOfUnitsPerBoxi \le 1000$。 - $1 \le truckSize \le 106$。 **示例**: - 示例 1: ```python 输入:boxTypes = [[1,3],[2,2],[3,1]], truckSize = 4 输出:8 解释 箱子的情况如下: - 1 个第一类的箱子,里面含 3 个单元。 - 2 个第二类的箱子,每个里面含 2 个单元。 - 3 个第三类的箱子,每个里面含 1 个单元。 可以选择第一类和第二类的所有箱子,以及第三类的一个箱子。 单元总数 = (1 * 3) + (2 * 2) + (1 * 1) = 8 ``` - 示例 2: ```python 输入:boxTypes = [[5,10],[2,5],[4,7],[3,9]], truckSize = 10 输出:91 ``` ## 解题思路 ### 思路 1:贪心算法 题目中,一辆卡车上可以装载箱子的最大数量是固定的($truckSize$),那么如果想要使卡车上装载的单元数量最大,就应该优先选取装载单元数量多的箱子。 所以,从贪心算法的角度来考虑,我们应该按照每个箱子可以装载的单元数量对数组 $boxTypes$ 从大到小排序。然后优先选取装载单元数量多的箱子。 下面我们使用贪心算法三步走的方法解决这道题。 1. **转换问题**:将原问题转变为,在 $truckSize$ 的限制下,当选取完装载单元数量最多的箱子 $box$ 之后,再解决剩下箱子($truckSize - box[0]$)的选择问题(子问题)。 2. **贪心选择性质**:对于当前 $truckSize$,优先选取装载单元数量最多的箱子。 3. **最优子结构性质**:在上面的贪心策略下,当前 $truckSize$ 的贪心选择 + 剩下箱子的子问题最优解,就是全局最优解。也就是说在贪心选择的方案下,能够使得卡车可以装载的单元数量达到最大。 使用贪心算法的解决步骤描述如下: 1. 对数组 $boxTypes$ 按照每个箱子可以装载的单元数量从大到小排序。使用变量 $res$ 记录卡车可以装载的最大单元数量。 2. 遍历数组 $boxTypes$,对于当前种类的箱子 $box$: 1. 如果 $truckSize > box[0]$,说明当前种类箱子可以全部装载。则答案数量加上该种箱子的单元总数,即 $box[0] \times box[1]$,并且最大数量 $truckSize$ 减去装载的箱子数。 2. 如果 $truckSize \le box[0]$,说明当前种类箱子只能部分装载。则答案数量加上 $truckSize \times box[1]$,并跳出循环。 3. 最后返回答案 $res$。 ### 思路 1:代码 ```python class Solution: def maximumUnits(self, boxTypes: List[List[int]], truckSize: int) -> int: boxTypes.sort(key=lambda x:x[1], reverse=True) res = 0 for box in boxTypes: if truckSize > box[0]: res += box[0] * box[1] truckSize -= box[0] else: res += truckSize * box[1] break return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 是数组 $boxTypes$ 的长度。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/1700-1799/tuple-with-same-product.md ================================================ # [1726. 同积元组](https://leetcode.cn/problems/tuple-with-same-product/) - 标签:数组、哈希表 - 难度:中等 ## 题目链接 - [1726. 同积元组 - 力扣](https://leetcode.cn/problems/tuple-with-same-product/) ## 题目大意 **描述**:给定一个由不同正整数组成的数组 $nums$。 **要求**:返回满足 $a \times b = c \times d$ 的元组 $(a, b, c, d)$ 的数量。其中 $a$、$b$、$c$ 和 $d$ 都是 $nums$ 中的元素,且 $a \ne b \ne c \ne d$。 **说明**: - $1 \le nums.length \le 1000$。 - $1 \le nums[i] \le 10^4$。 - $nums$ 中的所有元素互不相同。 **示例**: - 示例 1: ```python 输入:nums = [2,3,4,6] 输出:8 解释:存在 8 个满足题意的元组: (2,6,3,4) , (2,6,4,3) , (6,2,3,4) , (6,2,4,3) (3,4,2,6) , (4,3,2,6) , (3,4,6,2) , (4,3,6,2) ``` - 示例 2: ```python 输入:nums = [1,2,4,5,10] 输出:16 解释:存在 16 个满足题意的元组: (1,10,2,5) , (1,10,5,2) , (10,1,2,5) , (10,1,5,2) (2,5,1,10) , (2,5,10,1) , (5,2,1,10) , (5,2,10,1) (2,10,4,5) , (2,10,5,4) , (10,2,4,5) , (10,2,5,4) (4,5,2,10) , (4,5,10,2) , (5,4,2,10) , (5,4,10,2) ``` ## 解题思路 ### 思路 1:哈希表 + 数学 1. 二重循环遍历数组 $nums$,使用哈希表 $cnts$ 记录下所有不同 $nums[i] \times nums[j]$ 的结果。 2. 因为满足 $a \times b = c \times d$ 的元组 $(a, b, c, d)$ 可以按照不同顺序进行组和,所以对于 $x$ 个 $nums[i] \times nums[j]$,就有 $C_x^2$ 种组和方法。 3. 遍历哈希表 $cnts$ 中所有值 $value$,将不同组和的方法数累积到答案 $ans$ 中。 4. 遍历完返回答案 $ans$。 ### 思路 1:代码 ```Python class Solution: def tupleSameProduct(self, nums: List[int]) -> int: cnts = Counter() size = len(nums) for i in range(size): for j in range(i + 1, size): product = nums[i] * nums[j] cnts[product] += 1 ans = 0 for key, value in cnts.items(): ans += value * (value - 1) * 4 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$,其中 $n$ 表示数组 $nums$ 的长度。 - **空间复杂度**:$O(n^2)$。 ================================================ FILE: docs/solutions/1800-1899/check-if-all-the-integers-in-a-range-are-covered.md ================================================ # [1893. 检查是否区域内所有整数都被覆盖](https://leetcode.cn/problems/check-if-all-the-integers-in-a-range-are-covered/) - 标签:数组、哈希表、前缀和 - 难度:简单 ## 题目链接 - [1893. 检查是否区域内所有整数都被覆盖 - 力扣](https://leetcode.cn/problems/check-if-all-the-integers-in-a-range-are-covered/) ## 题目大意 **描述**:给定一个二维整数数组 $ranges$ 和两个整数 $left$ 和 $right$。每个 $ranges[i] = [start_i, end_i]$ 表示一个从 $start_i$ 到 $end_i$ 的 闭区间 。 **要求**:如果闭区间 $[left, right]$ 内每个整数都被 $ranges$ 中至少一个区间覆盖,那么请你返回 $True$ ,否则返回 $False$。 **说明**: - $1 \le ranges.length \le 50$。 - $1 \le start_i \le end_i \le 50$。 - $1 \le left \le right \le 50$。 **示例**: - 示例 1: ```python 输入:ranges = [[1,2],[3,4],[5,6]], left = 2, right = 5 输出:True 解释:2 到 5 的每个整数都被覆盖了: - 2 被第一个区间覆盖。 - 3 和 4 被第二个区间覆盖。 - 5 被第三个区间覆盖。 ``` - 示例 2: ```python 输入:ranges = [[1,10],[10,20]], left = 21, right = 21 输出:False 解释:21 没有被任何一个区间覆盖。 ``` ## 解题思路 ### 思路 1:暴力 区间的范围为 $[1, 50]$,所以我们可以使用一个长度为 $51$ 的标志数组 $flags$ 用于标记区间内的所有整数。 1. 遍历数组 $ranges$ 中的所有区间 $[l, r]$。 2. 对于区间 $[l, r]$ 和区间 $[left, right]$,将两区间相交部分标记为 $True$。 3. 遍历区间 $[left, right]$ 上的所有整数,判断对应标志位是否为 $False$。 4. 如果对应标志位出现 $False$,则返回 $False$。 5. 如果遍历完所有标志位都为 $True$,则返回 $True$。 ### 思路 1:代码 ```Python class Solution: def isCovered(self, ranges: List[List[int]], left: int, right: int) -> bool: flags = [False for _ in range(51)] for l, r in ranges: for i in range(max(l, left), min(r, right) + 1): flags[i] = True for i in range(left, right + 1): if not flags[i]: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(50 \times n)$。 - **空间复杂度**:$O(50)$。 ================================================ FILE: docs/solutions/1800-1899/index.md ================================================ ## 本章内容 - [1822. 数组元素积的符号](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/sign-of-the-product-of-an-array.md) - [1827. 最少操作使数组递增](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/minimum-operations-to-make-the-array-increasing.md) - [1833. 雪糕的最大数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/maximum-ice-cream-bars.md) - [1844. 将所有数字用字符替换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/replace-all-digits-with-characters.md) - [1858. 包含所有前缀的最长单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/longest-word-with-all-prefixes.md) - [1859. 将句子排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/sorting-the-sentence.md) - [1876. 长度为三且各字符不同的子字符串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/substrings-of-size-three-with-distinct-characters.md) - [1877. 数组中最大数对和的最小值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/minimize-maximum-pair-sum-in-array.md) - [1879. 两个数组最小的异或值之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/minimum-xor-sum-of-two-arrays.md) - [1893. 检查是否区域内所有整数都被覆盖](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/check-if-all-the-integers-in-a-range-are-covered.md) - [1897. 重新分配字符使所有字符串都相等](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/redistribute-characters-to-make-all-strings-equal.md) ================================================ FILE: docs/solutions/1800-1899/longest-word-with-all-prefixes.md ================================================ # [1858. 包含所有前缀的最长单词](https://leetcode.cn/problems/longest-word-with-all-prefixes/) - 标签:深度优先搜索、字典树 - 难度:中等 ## 题目链接 - [1858. 包含所有前缀的最长单词 - 力扣](https://leetcode.cn/problems/longest-word-with-all-prefixes/) ## 题目大意 给定一个字符串数组 `words`。 要求:找出 `words` 中所有前缀从都在 `words` 中的最长字符串。如果存在多个符合条件相同长度的字符串,则输出字典序中最小的字符串。如果不存在这样的字符串,返回 `' '`。 - 例如:令 `words = ["a", "app", "ap"]`。字符串 `"app"` 含前缀 `"ap"` 和 `"a"` ,都在 `words` 中。 ## 解题思路 使用字典树存储所有单词,再将字典中单词按照长度从大到小、字典序从小到大排序。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return False cur = cur.children[ch] if not cur.isEnd: return False return True class Solution: def longestWord(self, words: List[str]) -> str: tire_tree = Trie() for word in words: tire_tree.insert(word) words.sort(key=lambda x:(-len(x), x)) for word in words: if tire_tree.search(word): return word return '' ``` ================================================ FILE: docs/solutions/1800-1899/maximum-ice-cream-bars.md ================================================ # [1833. 雪糕的最大数量](https://leetcode.cn/problems/maximum-ice-cream-bars/) - 标签:贪心、数组、排序 - 难度:中等 ## 题目链接 - [1833. 雪糕的最大数量 - 力扣](https://leetcode.cn/problems/maximum-ice-cream-bars/) ## 题目大意 **描述**:给定一个数组 $costs$ 表示不同雪糕的定价,其中 $costs[i]$ 表示第 $i$ 支雪糕的定价。再给定一个整数 $coins$ 表示 Tony 一共有的现金数量。 **要求**:计算并返回 Tony 用 $coins$ 现金能够买到的雪糕的最大数量。 **说明**: - $costs.length == n$。 - $1 \le n \le 10^5$。 - $1 \le costs[i] \le 10^5$。 - $1 \le coins \le 10^8$。 **示例**: - 示例 1: ```python 输入:costs = [1,3,2,4,1], coins = 7 输出:4 解释:Tony 可以买下标为 0、1、2、4 的雪糕,总价为 1 + 3 + 2 + 1 = 7 ``` - 示例 2: ```python 输入:costs = [10,6,8,7,7,8], coins = 5 输出:0 解释:Tony 没有足够的钱买任何一支雪糕。 ``` ## 解题思路 ### 思路 1:排序 + 贪心 贪心思路,如果想尽可能买到多的雪糕,就应该优先选择价格便宜的雪糕。具体步骤如下: 1. 对数组 $costs$ 进行排序。 2. 按照雪糕价格从低到高开始买雪糕,并记录下购买雪糕的数量,知道现有钱买不起雪糕为止。 3. 输出购买雪糕的数量作为答案。 ### 思路 1:代码 ```python class Solution: def maxIceCream(self, costs: List[int], coins: int) -> int: costs.sort() ans = 0 for cost in costs: if coins >= cost: ans += 1 coins -= cost return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log_2n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1800-1899/minimize-maximum-pair-sum-in-array.md ================================================ # [1877. 数组中最大数对和的最小值](https://leetcode.cn/problems/minimize-maximum-pair-sum-in-array/) - 标签:贪心、数组、双指针、排序 - 难度:中等 ## 题目链接 - [1877. 数组中最大数对和的最小值 - 力扣](https://leetcode.cn/problems/minimize-maximum-pair-sum-in-array/) ## 题目大意 **描述**:一个数对 $(a, b)$ 的数对和等于 $a + b$。最大数对和是一个数对数组中最大的数对和。 - 比如,如果我们有数对 $(1, 5)$,$(2, 3)$ 和 $(4, 4)$,最大数对和为 $max(1 + 5, 2 + 3, 4 + 4) = max(6, 5, 8) = 8$。 给定一个长度为偶数 $n$ 的数组 $nums$,现在将 $nums$ 中的元素分为 $n / 2$ 个数对,使得: - $nums$ 中每个元素恰好在一个数对中。 - 最大数对和的值最小。 **要求**:在最优数对划分的方案下,返回最小的最大数对和。 **说明**: - $n == nums.length$。 - $2 \le n \le 10^5$。 - $n$ 是偶数。 - $1 \le nums[i] \le 10^5$。 **示例**: - 示例 1: ```python 输入:nums = [3,5,2,3] 输出:7 解释:数组中的元素可以分为数对 (3,3) 和 (5,2)。 最大数对和为 max(3+3, 5+2) = max(6, 7) = 7。 ``` - 示例 2: ```python 输入:nums = [3,5,4,2,4,6] 输出:8 解释:数组中的元素可以分为数对 (3,5),(4,4) 和 (6,2)。 最大数对和为 max(3+5, 4+4, 6+2) = max(8, 8, 8) = 8。 ``` ## 解题思路 ### 思路 1:排序 + 贪心 为了使最大数对和的值尽可能的小,我们应该尽可能的让数组中最大值与最小值组成一对,次大值与次小值组成一对。而其他任何方案都会使得最大数对和的值更大。 那么,我们可以先将数组进行排序,然后首尾依次进行组对,并计算这种方案下的最大数对和即为答案。 ### 思路 1:代码 ```python class Solution: def minPairSum(self, nums: List[int]) -> int: nums.sort() ans, size = 0, len(nums) for i in range(len(nums) // 2): ans = max(ans, nums[i] + nums[size - 1 - i]) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/1800-1899/minimum-operations-to-make-the-array-increasing.md ================================================ # [1827. 最少操作使数组递增](https://leetcode.cn/problems/minimum-operations-to-make-the-array-increasing/) - 标签:贪心、数组 - 难度:简单 ## 题目链接 - [1827. 最少操作使数组递增 - 力扣](https://leetcode.cn/problems/minimum-operations-to-make-the-array-increasing/) ## 题目大意 **描述**:给定一个整数数组 $nums$(下标从 $0$ 开始)。每一次操作中,你可以选择数组中的一个元素,并将它增加 $1$。 - 比方说,如果 $nums = [1,2,3]$,你可以选择增加 $nums[1]$ 得到 $nums = [1,3,3]$。 **要求**:请你返回使 $nums$ 严格递增的最少操作次数。 **说明**: - 我们称数组 $nums$ 是严格递增的,当它满足对于所有的 $0 \le i < nums.length - 1$ 都有 $nums[i] < nums[i + 1]$。一个长度为 $1$ 的数组是严格递增的一种特殊情况。 - $1 \le nums.length \le 5000$。 - $1 \le nums[i] \le 10^4$。 **示例**: - 示例 1: ```python 输入:nums = [1,1,1] 输出:3 解释:你可以进行如下操作: 1) 增加 nums[2] ,数组变为 [1,1,2]。 2) 增加 nums[1] ,数组变为 [1,2,2]。 3) 增加 nums[2] ,数组变为 [1,2,3]。 ``` - 示例 2: ```python 输入:nums = [1,5,2,4,1] 输出:14 ``` ## 解题思路 ### 思路 1:贪心算法 题目要求使 $nums$ 严格递增的最少操作次数。当遇到 $nums[i - 1] \ge nums[i]$ 时,我们应该在满足要求的同时,尽可能使得操作次数最少,则 $nums[i]$ 应增加到 $nums[i - 1] + 1$ 时,此时操作次数最少,并且满足 $nums[i - 1] < nums[i]$。 具体操作步骤如下: 1. 从左到右依次遍历数组元素。 2. 如果遇到 $nums[i - 1] \ge nums[i]$ 时: 1. 本次增加的最少操作次数为 $nums[i - 1] + 1 - nums[i]$,将其计入答案中。 2. 将 $nums[i]$ 变为 $nums[i - 1] + 1$。 3. 遍历完返回答案 $ans$。 ### 思路 1:代码 ```Python class Solution: def minOperations(self, nums: List[int]) -> int: ans = 0 for i in range(1, len(nums)): if nums[i - 1] >= nums[i]: ans += nums[i - 1] + 1 - nums[i] nums[i] = nums[i - 1] + 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1800-1899/minimum-xor-sum-of-two-arrays.md ================================================ # [1879. 两个数组最小的异或值之和](https://leetcode.cn/problems/minimum-xor-sum-of-two-arrays/) - 标签:位运算、数组、动态规划、状态压缩 - 难度:困难 ## 题目链接 - [1879. 两个数组最小的异或值之和 - 力扣](https://leetcode.cn/problems/minimum-xor-sum-of-two-arrays/) ## 题目大意 **描述**:给定两个整数数组 $nums1$ 和 $nums2$,两个数组长度都为 $n$。 **要求**:将 $nums2$ 中的元素重新排列,使得两个数组的异或值之和最小。并返回重新排列之后的异或值之和。 **说明**: - **两个数组的异或值之和**:$(nums1[0] \oplus nums2[0]) + (nums1[1] \oplus nums2[1]) + ... + (nums1[n - 1] \oplus nums2[n - 1])$(下标从 $0$ 开始)。 - 举个例子,$[1, 2, 3]$ 和 $[3,2,1]$ 的异或值之和 等于 $(1 \oplus 3) + (2 \oplus 2) + (3 \oplus 1) + (3 \oplus 1) = 2 + 0 + 2 = 4$。 - $n == nums1.length$。 - $n == nums2.length$。 - $1 \le n \le 14$。 - $0 \le nums1[i], nums2[i] \le 10^7$。 **示例**: - 示例 1: ```python 输入:nums1 = [1,2], nums2 = [2,3] 输出:2 解释:将 nums2 重新排列得到 [3,2] 。 异或值之和为 (1 XOR 3) + (2 XOR 2) = 2 + 0 = 2。 ``` - 示例 2: ```python 输入:nums1 = [1,0,3], nums2 = [5,3,4] 输出:8 解释:将 nums2 重新排列得到 [5,4,3] 。 异或值之和为 (1 XOR 5) + (0 XOR 4) + (3 XOR 3) = 4 + 4 + 0 = 8。 ``` ## 解题思路 ### 思路 1:状态压缩 DP 由于数组 $nums2$ 可以重新排列,所以我们可以将数组 $nums1$ 中的元素顺序固定,然后将数组 $nums1$ 中第 $i$ 个元素与数组 $nums2$ 中所有还没被选择的元素进行组合,找到异或值之和最小的组合。 同时因为两个数组长度 $n$ 的大小范围只有 $[1, 14]$,所以我们可以采用「状态压缩」的方式来表示 $nums2$ 中当前元素的选择情况。 「状态压缩」指的是使用一个 $n$ 位的二进制数 $state$ 来表示排列中数的选取情况。 如果二进制数 $state$ 的第 $i$ 位为 $1$,说明数组 $nums2$ 第 $i$ 个元素在该状态中被选取。反之,如果该二进制的第 $i$ 位为 $0$,说明数组 $nums2$ 中第 $i$ 个元素在该状态中没有被选取。 举个例子: 1. $nums2 = \lbrace 1, 2, 3, 4 \rbrace$,$state = (1001)_2$,表示选择了第 $1$ 个元素和第 $4$ 个元素,也就是 $1$、$4$。 2. $nums2 = \lbrace 1, 2, 3, 4, 5, 6 \rbrace$,$state = (011010)_2$,表示选择了第 $2$ 个元素、第 $4$ 个元素、第 $5$ 个元素,也就是 $2$、$4$、$5$。 这样,我们就可以通过动态规划的方式来解决这道题。 ###### 1. 阶段划分 按照数组 $nums$ 中元素选择情况进行阶段划分。 ###### 2. 定义状态 定义当前数组 $nums2$ 中元素选择状态为 $state$,$state$ 对应选择的元素个数为 $count(state)$。 则可以定义状态 $dp[state]$ 表示为:当前数组 $nums2$ 中元素选择状态为 $state$,并且选择了 $nums1$ 中前 $count(state)$ 个元素的情况下,可以组成的最小异或值之和。 ###### 3. 状态转移方程 对于当前状态 $dp[state]$,肯定是从比 $state$ 少选一个元素的状态中递推而来。我们可以枚举少选一个元素的状态,找到可以组成的异或值之和最小值,赋值给 $dp[state]$。 举个例子 $nums2 = \lbrace 1, 2, 3, 4 \rbrace$,$state = (1001)_2$,表示选择了第 $1$ 个元素和第 $4$ 个元素,也就是 $1$、$4$。那么 $state$ 只能从 $(1000)_2$ 和 $(0001)_2$ 这两个状态转移而来,我们只需要枚举这两种状态,并求出转移过来的异或值之和最小值。 即状态转移方程为:$dp[state] = min(dp[state], \quad dp[state \oplus (1 \text{ <}\text{< } i)] + (nums1[i] \oplus nums2[one\_cnt - 1]))$,其中 $state$ 第 $i$ 位一定为 $1$,$one\_cnt$ 为 $state$ 中 $1$ 的个数。 ###### 4. 初始条件 - 既然是求最小值,不妨将所有状态初始为最大值。 - 未选择任何数时,异或值之和为 $0$,所以初始化 $dp[0] = 0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[state]$ 表示为:当前数组 $nums2$ 中元素选择状态为 $state$,并且选择了 $nums1$ 中前 $count(state)$ 个元素的情况下,可以组成的最小异或值之和。 所以最终结果为 $dp[states - 1]$,其中 $states = 1 \text{ <}\text{< } n$。 ### 思路 1:代码 ```python class Solution: def minimumXORSum(self, nums1: List[int], nums2: List[int]) -> int: ans = float('inf') size = len(nums1) states = 1 << size dp = [float('inf') for _ in range(states)] dp[0] = 0 for state in range(states): one_cnt = bin(state).count('1') for i in range(size): if (state >> i) & 1: dp[state] = min(dp[state], dp[state ^ (1 << i)] + (nums1[i] ^ nums2[one_cnt - 1])) return dp[states - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^n \times n)$,其中 $n$ 是数组 $nums1$、$nums2$ 的长度。 - **空间复杂度**:$O(2^n)$。 ================================================ FILE: docs/solutions/1800-1899/redistribute-characters-to-make-all-strings-equal.md ================================================ # [1897. 重新分配字符使所有字符串都相等](https://leetcode.cn/problems/redistribute-characters-to-make-all-strings-equal/) - 标签:哈希表、字符串、计数 - 难度:简单 ## 题目链接 - [1897. 重新分配字符使所有字符串都相等 - 力扣](https://leetcode.cn/problems/redistribute-characters-to-make-all-strings-equal/) ## 题目大意 **描述**:给定一个字符串数组 $words$(下标从 $0$ 开始计数)。 在一步操作中,需先选出两个 不同 下标 $i$ 和 $j$,其中 $words[i]$ 是一个非空字符串,接着将 $words[i]$ 中的任一字符移动到 $words[j]$ 中的 任一 位置上。 **要求**:如果执行任意步操作可以使 $words$ 中的每个字符串都相等,返回 $True$;否则,返回 $False$。 **说明**: - $1 <= words.length <= 100$。 - $1 <= words[i].length <= 100$ - $words[i]$ 由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:words = ["abc","aabc","bc"] 输出:true 解释:将 words[1] 中的第一个 'a' 移动到 words[2] 的最前面。 使 words[1] = "abc" 且 words[2] = "abc"。 所有字符串都等于 "abc" ,所以返回 True。 ``` - 示例 2: ```python 输入:words = ["ab","a"] 输出:False 解释:执行操作无法使所有字符串都相等。 ``` ## 解题思路 ### 思路 1:哈希表 如果通过重新分配字符能够使所有字符串都相等,则所有字符串的字符需要满足: 1. 每个字符串中字符种类相同, 2. 每个字符串中各种字符的个数相同。 则我们可以使用哈希表来统计字符串中字符种类及个数。具体步骤如下: 1. 遍历单词数组 $words$ 中的所有单词 $word$。 2. 遍历所有单词 $word$ 中的所有字符 $ch$。 3. 使用哈希表 $cnts$ 统计字符种类及个数。 4. 如果所有字符个数都是单词个数的倍数,则说明通过重新分配字符能够使所有字符串都相等,则返回 $True$。 5. 否则返回 $False$。 ### 思路 1:代码 ```Python class Solution: def makeEqual(self, words: List[str]) -> bool: size = len(words) cnts = Counter() for word in words: for ch in word: cnts[ch] += 1 return all(value % size == 0 for key, value in cnts.items()) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(s + |\sum|)$,其中 $s$ 为数组 $words$ 中所有单词的长度之和,$\sum$ 是字符集,本题中 $|\sum| = 26$。 - **空间复杂度**:$O(|\sum|)$。 ================================================ FILE: docs/solutions/1800-1899/replace-all-digits-with-characters.md ================================================ # [1844. 将所有数字用字符替换](https://leetcode.cn/problems/replace-all-digits-with-characters/) - 标签:字符串 - 难度:简单 ## 题目链接 - [1844. 将所有数字用字符替换 - 力扣](https://leetcode.cn/problems/replace-all-digits-with-characters/) ## 题目大意 **描述**:给定一个下标从 $0$ 开始的字符串 $s$。字符串 $s$ 的偶数下标处为小写英文字母,奇数下标处为数字。 定义一个函数 `shift(c, x)`,其中 $c$ 是一个字符且 $x$ 是一个数字,函数返回字母表中 $c$ 后边第 $x$ 个字符。 - 比如,`shift('a', 5) = 'f'`,`shift('x', 0) = 'x'`。 对于每个奇数下标 $i$,我们需要将数字 $s[i]$ 用 `shift(s[i - 1], s[i])` 替换。 **要求**:替换字符串 $s$ 中所有数字以后,将字符串 $s$ 返回。 **说明**: - 题目保证 `shift(s[i - 1], s[i])` 不会超过 `'z'`。 - $1 \le s.length \le 100$。 - $s$ 只包含小写英文字母和数字。 - 对所有奇数下标处的 $i$,满足 `shift(s[i - 1], s[i]) <= 'z'` 。 **示例**: - 示例 1: ```python 输入:s = "a1c1e1" 输出:"abcdef" 解释:数字被替换结果如下: - s[1] -> shift('a',1) = 'b' - s[3] -> shift('c',1) = 'd' - s[5] -> shift('e',1) = 'f' ``` - 示例 2: ```python 输入:s = "a1b2c3d4e" 输出:"abbdcfdhe" 解释:数字被替换结果如下: - s[1] -> shift('a',1) = 'b' - s[3] -> shift('b',2) = 'd' - s[5] -> shift('c',3) = 'f' - s[7] -> shift('d',4) = 'h' ``` ## 解题思路 ### 思路 1:模拟 1. 先定义一个 `shift(ch, x)` 用于替换 `s[i]`。 2. 将字符串转为字符串列表,定义为 $res$。 3. 以两个字符为一组遍历字符串,对 $res[i]$ 进行修改。 4. 将字符串列表连接起来,作为答案返回。 ### 思路 1:代码 ```python class Solution: def replaceDigits(self, s: str) -> str: def shift(ch, x): return chr(ord(ch) + x) res = list(s) for i in range(1, len(s), 2): res[i] = shift(res[i - 1], int(res[i])) return "".join(res) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1800-1899/sign-of-the-product-of-an-array.md ================================================ # [1822. 数组元素积的符号](https://leetcode.cn/problems/sign-of-the-product-of-an-array/) - 标签:数组、数学 - 难度:简单 ## 题目链接 - [1822. 数组元素积的符号 - 力扣](https://leetcode.cn/problems/sign-of-the-product-of-an-array/) ## 题目大意 **描述**:已知函数 `signFunc(x)` 会根据 `x` 的正负返回特定值: - 如果 `x` 是正数,返回 `1`。 - 如果 `x` 是负数,返回 `-1`。 - 如果 `x` 等于 `0`,返回 `0`。 现在给定一个整数数组 `nums`。令 `product` 为数组 `nums` 中所有元素值的乘积。 **要求**:返回 `signFun(product)` 的值。 **说明**: - $1 \le nums.length \le 1000$。 - $-100 \le nums[i] \le 100$。 **示例**: - 示例 1: ```python 输入 nums = [-1,-2,-3,-4,3,2,1] 输出 1 解释 数组中所有值的乘积是 144,且 signFunc(144) = 1 ``` ## 解题思路 ### 思路 1: 题目要求的是数组所有值乘积的正负性,但是我们没必要将所有数乘起来再判断正负性。只需要统计出数组中负数的个数,再加以判断即可。 - 使用变量 `minus_count` 记录数组中负数个数。 - 然后遍历数组 `nums`,对于当前元素 `num`: - 如果为 `0`,则最终乘积肯定为 `0`,直接返回 `0`。 - 如果小于 `0`,负数个数加 `1`。 - 最终统计出数组中负数的个数为 `minus_count`。 - 如果 `minus_count` 是 `2` 的倍数,则说明最终乘积为正数,返回 `1`。 - 如果 `minus_count` 不是 `2` 的倍数,则说明最终乘积为负数,返回 `-1`。 ## 代码 ### 思路 1 代码: ```python class Solution: def arraySign(self, nums: List[int]) -> int: minus_count = 0 for num in nums: if num < 0: minus_count += 1 elif num == 0: return 0 if minus_count % 2 == 0: return 1 else: return -1 ``` ================================================ FILE: docs/solutions/1800-1899/sorting-the-sentence.md ================================================ # [1859. 将句子排序](https://leetcode.cn/problems/sorting-the-sentence/) - 标签:字符串、排序 - 难度:简单 ## 题目链接 - [1859. 将句子排序 - 力扣](https://leetcode.cn/problems/sorting-the-sentence/) ## 题目大意 **描述**:给定一个句子 $s$,句子中包含的单词不超过 $9$ 个。并且句子 $s$ 中每个单词末尾添加了「从 $1$ 开始的单词位置索引」,并且将句子中所有单词打乱顺序。 举个例子,句子 `"This is a sentence"` 可以被打乱顺序得到 `"sentence4 a3 is2 This1"` 或者 `"is2 sentence4 This1 a3"` 。 **要求**:重新构造并得到原本顺序的句子。 **说明**: - **一个句子**:指的是一个序列的单词用单个空格连接起来,且开头和结尾没有任何空格。每个单词都只包含小写或大写英文字母。 - $2 \le s.length \le 200$。 - $s$ 只包含小写和大写英文字母、空格以及从 $1$ 到 $9$ 的数字。 - $s$ 中单词数目为 $1$ 到 $9$ 个。 - $s$ 中的单词由单个空格分隔。 - $s$ 不包含任何前导或者后缀空格。 **示例**: - 示例 1: ```python 输入:s = "is2 sentence4 This1 a3" 输出:"This is a sentence" 解释:将 s 中的单词按照初始位置排序,得到 "This1 is2 a3 sentence4" ,然后删除数字。 ``` - 示例 2: ```python 输入:s = "Myself2 Me1 I4 and3" 输出:"Me Myself and I" 解释:将 s 中的单词按照初始位置排序,得到 "Me1 Myself2 and3 I4" ,然后删除数字。 ``` ## 解题思路 ### 思路 1:模拟 1. 将句子 $s$ 按照空格分隔成数组 $s\_list$。 2. 遍历数组 $s\_list$ 中的单词: 1. 从单词中分割出对应单词索引 $idx$ 和对应单词 $word$。 2. 将单词 $word$ 存入答案数组 $res$ 对应位置 $idx - 1$ 上,即:$res[int(idx) - 1] = word$。 3. 将答案数组用空格拼接成句子字符串,并返回。 ### 思路 1:代码 ```python class Solution: def sortSentence(self, s: str) -> str: s_list = s.split() size = len(s_list) res = ["" for _ in range(size)] for sub in s_list: idx = "" word = "" for ch in sub: if '1' <= ch <= '9': idx += ch else: word += ch res[int(idx) - 1] = word return " ".join(res) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m)$,其中 $m$ 为给定句子 $s$ 的长度。 - **空间复杂度**:$O(m)$。 ================================================ FILE: docs/solutions/1800-1899/substrings-of-size-three-with-distinct-characters.md ================================================ # [1876. 长度为三且各字符不同的子字符串](https://leetcode.cn/problems/substrings-of-size-three-with-distinct-characters/) - 标签:哈希表、字符串、计数、滑动窗口 - 难度:简单 ## 题目链接 - [1876. 长度为三且各字符不同的子字符串 - 力扣](https://leetcode.cn/problems/substrings-of-size-three-with-distinct-characters/) ## 题目大意 **描述**:给定搞一个字符串 $s$。 **要求**:返回 $s$ 中长度为 $3$ 的好子字符串的数量。如果相同的好子字符串出现多次,则每一次都应该被记入答案之中。 **说明**: - **子字符串**:指的是一个字符串中连续的字符序列。 - **好子字符串**:如果一个字符串中不含有任何重复字符,则称这个字符串为好子字符串。 - $1 \le s.length \le 100$。 - $s$ 只包含小写英文字母。 **示例**: - 示例 1: ```python 输入:s = "xyzzaz" 输出:1 解释:总共有 4 个长度为 3 的子字符串:"xyz","yzz","zza" 和 "zaz" 。 唯一的长度为 3 的好子字符串是 "xyz" 。 ``` - 示例 2: ```python 输入:s = "aababcabc" 输出:4 解释:总共有 7 个长度为 3 的子字符串:"aab","aba","bab","abc","bca","cab" 和 "abc" 。 好子字符串包括 "abc","bca","cab" 和 "abc" 。 ``` ## 解题思路 ### 思路 1:模拟 1. 遍历字符串 $s$ 中长度为 3 的子字符串。 2. 判断子字符串中的字符是否有重复。如果没有重复,则答案进行计数。 3. 遍历完输出答案。 ### 思路 1:代码 ```python class Solution: def countGoodSubstrings(self, s: str) -> int: ans = 0 for i in range(2, len(s)): if s[i - 2] != s[i - 1] and s[i - 1] != s[i] and s[i - 2] != s[i]: ans += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1900-1999/add-minimum-number-of-rungs.md ================================================ # [1936. 新增的最少台阶数](https://leetcode.cn/problems/add-minimum-number-of-rungs/) - 标签:贪心、数组 - 难度:中等 ## 题目链接 - [1936. 新增的最少台阶数 - 力扣](https://leetcode.cn/problems/add-minimum-number-of-rungs/) ## 题目大意 **描述**:给定一个严格递增的整数数组 $rungs$,用于表示梯子上每一台阶的高度。当前你正站在高度为 $0$ 的地板上,并打算爬到最后一个台阶。 另给定一个整数 $dist$。每次移动中,你可以到达下一个距离当前位置(地板或台阶)不超过 $dist$ 高度的台阶。当前,你也可以在任何正整数高度插入尚不存在的新台阶。 **要求**:返回爬到最后一阶时必须添加到梯子上的最少台阶数。 **说明**: - **示例**: - 示例 1: ```python 输入:rungs = [1,3,5,10], dist = 2 输出:2 解释: 现在无法到达最后一阶。 在高度为 7 和 8 的位置增设新的台阶,以爬上梯子。 梯子在高度为 [1,3,5,7,8,10] 的位置上有台阶。 ``` - 示例 2: ```python 输入:rungs = [3,4,6,7], dist = 2 输出:1 解释: 现在无法从地板到达梯子的第一阶。 在高度为 1 的位置增设新的台阶,以爬上梯子。 梯子在高度为 [1,3,4,6,7] 的位置上有台阶。 ``` ## 解题思路 ### 思路 1:贪心算法 + 模拟 1. 遍历梯子的每一层台阶。 2. 计算每一层台阶与上一层台阶之间的差值 $diff$。 3. 每层最少需要新增的台阶数为 $\lfloor \frac{diff - 1}{dist} \rfloor$,将其计入答案 $ans$ 中。 4. 遍历完返回答案。 ### 思路 1:代码 ```Python class Solution: def addRungs(self, rungs: List[int], dist: int) -> int: ans, cur = 0, 0 for h in rungs: diff = h - cur ans += (diff - 1) // dist cur = h return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $rungs$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1900-1999/check-if-all-characters-have-equal-number-of-occurrences.md ================================================ # [1941. 检查是否所有字符出现次数相同](https://leetcode.cn/problems/check-if-all-characters-have-equal-number-of-occurrences/) - 标签:哈希表、字符串、计数 - 难度:简单 ## 题目链接 - [1941. 检查是否所有字符出现次数相同 - 力扣](https://leetcode.cn/problems/check-if-all-characters-have-equal-number-of-occurrences/) ## 题目大意 **描述**:给定一个字符串 $s$。如果 $s$ 中出现过的所有字符的出现次数相同,那么我们称字符串 $s$ 是「好字符串」。 **要求**:如果 $s$ 是一个好字符串,则返回 `True`,否则返回 `False`。 **说明**: - $1 \le s.length \le 1000$。 - $s$ 只包含小写英文字母。 **示例**: - 示例 1: ```python 输入:s = "abacbc" 输出:true 解释:s 中出现过的字符为 'a','b' 和 'c' 。s 中所有字符均出现 2 次。 ``` - 示例 2: ```python 输入:s = "aaabb" 输出:false 解释:s 中出现过的字符为 'a' 和 'b' 。 'a' 出现了 3 次,'b' 出现了 2 次,两者出现次数不同。 ``` ## 解题思路 ### 思路 1:哈希表 1. 使用哈希表记录字符串 $s$ 中每个字符的频数。 2. 然后遍历哈希表中的键值对,检测每个字符的频数是否相等。 3. 如果发现频数不相等,则直接返回 `False`。 4. 如果检查完发现所有频数都相等,则返回 `True`。 ### 思路 1:代码 ```python class Solution: def areOccurrencesEqual(self, s: str) -> bool: counter = Counter(s) flag = -1 for key in counter: if flag == -1: flag = counter[key] else: if flag != counter[key]: return False return True ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1900-1999/concatenation-of-array.md ================================================ # [1929. 数组串联](https://leetcode.cn/problems/concatenation-of-array/) - 标签:数组 - 难度:简单 ## 题目链接 - [1929. 数组串联 - 力扣](https://leetcode.cn/problems/concatenation-of-array/) ## 题目大意 **描述**:给定一个长度为 $n$ 的整数数组 $nums$。 **要求**:构建一个长度为 $2 \times n$ 的答案数组 $ans$,答案数组下标从 $0$ 开始计数 ,对于所有 $0 \le i < n$ 的 $i$ ,满足下述所有要求: - $ans[i] == nums[i]$。 - $ans[i + n] == nums[i]$。 具体而言,$ans$ 由两个 $nums$ 数组「串联」形成。 **说明**: - $n == nums.length$。 - $1 \le n \le 1000$。 - $1 \le nums[i] \le 1000$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,1] 输出:[1,2,1,1,2,1] 解释:数组 ans 按下述方式形成: - ans = [nums[0],nums[1],nums[2],nums[0],nums[1],nums[2]] - ans = [1,2,1,1,2,1] ``` - 示例 2: ```python 输入:nums = [1,3,2,1] 输出:[1,3,2,1,1,3,2,1] 解释:数组 ans 按下述方式形成: - ans = [nums[0],nums[1],nums[2],nums[3],nums[0],nums[1],nums[2],nums[3]] - ans = [1,3,2,1,1,3,2,1] ``` ## 解题思路 ### 思路 1:按要求模拟 1. 定义一个数组变量(列表)$ans$ 作为答案数组。 2. 然后按顺序遍历两次数组 $nums$ 中的元素,并依次添加到 $ans$ 的尾部。最后返回 $ans$。 ### 思路 1:代码 ```python class Solution: def getConcatenation(self, nums: List[int]) -> List[int]: ans = [] for num in nums: ans.append(num) for num in nums: ans.append(num) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(n)$。如果算上答案数组的空间占用,则空间复杂度为 $O(n)$。不算上则空间复杂度为 $O(1)$。 ### 思路 2:利用运算符 Python 中可以直接利用 `+` 号运算符将两个列表快速进行串联。即 `return nums + nums`。 ### 思路 2:代码 ```python class Solution: def getConcatenation(self, nums: List[int]) -> List[int]: return nums + nums ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(n)$。如果算上答案数组的空间占用,则空间复杂度为 $O(n)$。不算上则空间复杂度为 $O(1)$。 ================================================ FILE: docs/solutions/1900-1999/count-square-sum-triples.md ================================================ # [1925. 统计平方和三元组的数目](https://leetcode.cn/problems/count-square-sum-triples/) - 标签:数学、枚举 - 难度:简单 ## 题目链接 - [1925. 统计平方和三元组的数目 - 力扣](https://leetcode.cn/problems/count-square-sum-triples/) ## 题目大意 **描述**:给你一个整数 $n$。 **要求**:请你返回满足 $1 \le a, b, c \le n$ 的平方和三元组的数目。 **说明**: - **平方和三元组**:指的是满足 $a^2 + b^2 = c^2$ 的整数三元组 $(a, b, c)$。 - $1 \le n \le 250$。 **示例**: - 示例 1: ```python 输入 n = 5 输出 2 解释 平方和三元组为 (3,4,5) 和 (4,3,5)。 ``` - 示例 2: ```python 输入:n = 10 输出:4 解释:平方和三元组为 (3,4,5),(4,3,5),(6,8,10) 和 (8,6,10)。 ``` ## 解题思路 ### 思路 1:枚举算法 我们可以在 $[1, n]$ 区间中枚举整数三元组 $(a, b, c)$ 中的 $a$ 和 $b$。然后判断 $a^2 + b^2$ 是否小于等于 $n$,并且是完全平方数。 在遍历枚举的同时,我们维护一个用于统计平方和三元组数目的变量 `cnt`。如果符合要求,则将计数 `cnt` 加 $1$。最终,我们返回该数目作为答案。 利用枚举算法统计平方和三元组数目的时间复杂度为 $O(n^2)$。 - 注意:在计算中,为了防止浮点数造成的误差,并且两个相邻的完全平方正数之间的距离一定大于 $1$,所以我们可以用 $\sqrt{a^2 + b^2 + 1}$ 来代替 $\sqrt{a^2 + b^2}$。 ### 思路 1:代码 ```python class Solution: def countTriples(self, n: int) -> int: cnt = 0 for a in range(1, n + 1): for b in range(1, n + 1): c = int(sqrt(a * a + b * b + 1)) if c <= n and a * a + b * b == c * c: cnt += 1 return cnt ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1900-1999/eliminate-maximum-number-of-monsters.md ================================================ # [1921. 消灭怪物的最大数量](https://leetcode.cn/problems/eliminate-maximum-number-of-monsters/) - 标签:贪心、数组、排序 - 难度:中等 ## 题目链接 - [1921. 消灭怪物的最大数量 - 力扣](https://leetcode.cn/problems/eliminate-maximum-number-of-monsters/) ## 题目大意 **描述**:你正在玩一款电子游戏,在游戏中你需要保护城市免受怪物侵袭。给定一个下标从 $0$ 开始且大小为 $n$ 的整数数组 $dist$,其中 $dist[i]$ 是第 $i$ 个怪物与城市的初始距离(单位:米)。 怪物以恒定的速度走向城市。每个怪物的速度都以一个长度为 $n$ 的整数数组 $speed$ 表示,其中 $speed[i]$ 是第 $i$ 个怪物的速度(单位:千米/分)。 你有一种武器,一旦充满电,就可以消灭 一个 怪物。但是,武器需要 一分钟 才能充电。武器在游戏开始时是充满电的状态,怪物从 第 $0$ 分钟时开始移动。 一旦任一怪物到达城市,你就输掉了这场游戏。如果某个怪物 恰好 在某一分钟开始时到达城市(距离表示为 $0$),这也会被视为输掉 游戏,在你可以使用武器之前,游戏就会结束。 **要求**:返回在你输掉游戏前可以消灭的怪物的最大数量。如果你可以在所有怪物到达城市前将它们全部消灭,返回 $n$。 **说明**: - **示例**: - 示例 1: ```python 输入:dist = [1,3,4], speed = [1,1,1] 输出:3 解释: 第 0 分钟开始时,怪物的距离是 [1,3,4],你消灭了第一个怪物。 第 1 分钟开始时,怪物的距离是 [X,2,3],你消灭了第二个怪物。 第 3 分钟开始时,怪物的距离是 [X,X,2],你消灭了第三个怪物。 所有 3 个怪物都可以被消灭。 ``` - 示例 2: ```python 输入:dist = [1,1,2,3], speed = [1,1,1,1] 输出:1 解释: 第 0 分钟开始时,怪物的距离是 [1,1,2,3],你消灭了第一个怪物。 第 1 分钟开始时,怪物的距离是 [X,0,1,2],所以你输掉了游戏。 你只能消灭 1 个怪物。 ``` ## 解题思路 ### 思路 1:排序 + 贪心算法 对于第 $i$ 个怪物,最晚可被消灭的时间为 $times[i] = \lfloor \frac{dist[i] - 1}{speed[i]} \rfloor$。我们可以根据以上公式,将所有怪物最晚可被消灭时间存入数组 $times$ 中,然后对 $times$ 进行升序排序。 然后遍历数组 $times$,对于第 $i$ 个怪物: 1. 如果 $times[i] < i$,则说明第 $i$ 个怪物无法被消灭,直接返回 $i$ 即可。 2. 如果 $times[i] \ge i$,则说明第 $i$ 个怪物可以被消灭,继续向下遍历。 如果遍历完数组 $times$,则说明所有怪物都可以被消灭,则返回 $n$。 ### 思路 1:代码 ```Python class Solution: def eliminateMaximum(self, dist: List[int], speed: List[int]) -> int: times = [] for d, s in zip(dist, speed): time = (d - 1) // s times.append(time) times.sort() size = len(times) for i in range(size): if times[i] < i: return i return size ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 为数组 $dist$ 的长度。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/1900-1999/find-the-middle-index-in-array.md ================================================ # [1991. 找到数组的中间位置](https://leetcode.cn/problems/find-the-middle-index-in-array/) - 标签:数组、前缀和 - 难度:简单 ## 题目链接 - [1991. 找到数组的中间位置 - 力扣](https://leetcode.cn/problems/find-the-middle-index-in-array/) ## 题目大意 **描述**:给定一个下标从 $0$ 开始的整数数组 $nums$。 **要求**:返回最左边的中间位置 $middleIndex$(也就是所有可能中间位置下标做小的一个)。如果找不到这样的中间位置,则返回 $-1$。 **说明**: - **中间位置 $middleIndex$**:满足 $nums[0] + nums[1] + … + nums[middleIndex - 1] == nums[middleIndex + 1] + nums[middleIndex + 2] + … + nums[nums.length - 1]$ 的数组下标。 - 如果 $middleIndex == 0$,左边部分的和定义为 $0$。类似的,如果 $middleIndex == nums.length - 1$,右边部分的和定义为 $0$。 **示例**: - 示例 1: ```python 输入:nums = [2,3,-1,8,4] 输出:3 解释: 下标 3 之前的数字和为:2 + 3 + -1 = 4 下标 3 之后的数字和为:4 = 4 ``` - 示例 2: ```python 输入:nums = [1,-1,4] 输出:2 解释: 下标 2 之前的数字和为:1 + -1 = 0 下标 2 之后的数字和为:0 ``` ## 解题思路 ### 思路 1:前缀和 1. 先遍历一遍数组,求出数组中全部元素和为 $total$。 2. 再遍历一遍数组,使用变量 $prefix\_sum$ 为前 $i$ 个元素和。 3. 当遍历到第 $i$ 个元素时,其数组左侧元素之和为 $prefix\_sum$,右侧元素和为 $total - prefix\_sum - nums[i]$。 1. 如果左右元素之和相等,即 $prefix\_sum == total - prefix\_sum - nums[i]$($2 \times prefix\_sum + nums[i] == total$) 时,$i$ 为中间位置。此时返回 $i$。 2. 如果不满足,则继续累加当前元素到 $prefix\_sum$ 中,继续向后遍历。 4. 如果找不到符合要求的中间位置,则返回 $-1$。 ### 思路 1:代码 ```python class Solution: def findMiddleIndex(self, nums: List[int]) -> int: total = sum(nums) prefix_sum = 0 for i in range(len(nums)): if 2 * prefix_sum + nums[i] == total: return i prefix_sum += nums[i] return -1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1900-1999/index.md ================================================ ## 本章内容 - [1903. 字符串中的最大奇数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/largest-odd-number-in-string.md) - [1921. 消灭怪物的最大数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/eliminate-maximum-number-of-monsters.md) - [1925. 统计平方和三元组的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/count-square-sum-triples.md) - [1929. 数组串联](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/concatenation-of-array.md) - [1930. 长度为 3 的不同回文子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/unique-length-3-palindromic-subsequences.md) - [1936. 新增的最少台阶数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/add-minimum-number-of-rungs.md) - [1941. 检查是否所有字符出现次数相同](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/check-if-all-characters-have-equal-number-of-occurrences.md) - [1947. 最大兼容性评分和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/maximum-compatibility-score-sum.md) - [1984. 学生分数的最小差值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/minimum-difference-between-highest-and-lowest-of-k-scores.md) - [1986. 完成任务的最少工作时间段](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/minimum-number-of-work-sessions-to-finish-the-tasks.md) - [1991. 找到数组的中间位置](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/find-the-middle-index-in-array.md) - [1994. 好子集的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/the-number-of-good-subsets.md) ================================================ FILE: docs/solutions/1900-1999/largest-odd-number-in-string.md ================================================ # [1903. 字符串中的最大奇数](https://leetcode.cn/problems/largest-odd-number-in-string/) - 标签:贪心、数学、字符串 - 难度:简单 ## 题目链接 - [1903. 字符串中的最大奇数 - 力扣](https://leetcode.cn/problems/largest-odd-number-in-string/) ## 题目大意 **描述**:给定一个字符串 $num$,表示一个大整数。 **要求**:在字符串 $num$ 的所有非空子字符串中找出值最大的奇数,并以字符串形式返回。如果不存在奇数,则返回一个空字符串 `""`。 **说明**: - **子字符串**:指的是字符串中一个连续的字符序列。 - $1 \le num.length \le 10^5$ - $num$ 仅由数字组成且不含前导零。 **示例**: - 示例 1: ```python 输入:num = "52" 输出:"5" 解释:非空子字符串仅有 "5"、"2" 和 "52" 。"5" 是其中唯一的奇数。 ``` - 示例 2: ```python 输入:num = "4206" 输出:"" 解释:在 "4206" 中不存在奇数。 ``` ## 解题思路 ### 思路 1:贪心算法 如果某个数 $x$ 为奇数,则 $x$ 末尾位上的数字一定为奇数。那么我们只需要在末尾为奇数的字符串中考虑最大的奇数即可。显而易见的是,最大的奇数一定是长度最长的那个。所以我们只需要逆序遍历字符串,找到第一个奇数,从整个字符串开始位置到该奇数位置所代表的整数,就是最大的奇数。具体步骤如下: 1. 逆序遍历字符串 $s$。 2. 找到第一个奇数位置 $i$,则 $num[0: i + 1]$ 为最大的奇数,将其作为答案返回。 ### 思路 1:代码 ```python class Solution: def largestOddNumber(self, num: str) -> str: for i in range(len(num) - 1, -1, -1): if int(num[i]) % 2 == 1: return num[0: i + 1] return "" ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1900-1999/maximum-compatibility-score-sum.md ================================================ # [1947. 最大兼容性评分和](https://leetcode.cn/problems/maximum-compatibility-score-sum/) - 标签:位运算、数组、动态规划、回溯、状态压缩 - 难度:中等 ## 题目链接 - [1947. 最大兼容性评分和 - 力扣](https://leetcode.cn/problems/maximum-compatibility-score-sum/) ## 题目大意 **描述**:有一份由 $n$ 个问题组成的调查问卷,每个问题的答案只有 $0$ 或 $1$。将这份调查问卷分发给 $m$ 名学生和 $m$ 名老师,学生和老师的编号都是 $0 \sim m - 1$。现在给定一个二维整数数组 $students$ 表示 $m$ 名学生给出的答案,其中 $studuents[i][j]$ 表示第 $i$ 名学生第 $j$ 个问题给出的答案。再给定一个二维整数数组 $mentors$ 表示 $m$ 名老师给出的答案,其中 $mentors[i][j]$ 表示第 $i$ 名导师第 $j$ 个问题给出的答案。 每个学生要和一名导师互相配对。配对的学生和导师之间的兼容性评分等于学生和导师答案相同的次数。 - 例如,学生答案为 $[1, 0, 1]$,而导师答案为 $[0, 0, 1]$,那么他们的兼容性评分为 $2$,因为只有第 $2$ 个和第 $3$ 个答案相同。 **要求**:找出最优的学生与导师的配对方案,以最大程度上提高所有学生和导师的兼容性评分和。然后返回可以得到的最大兼容性评分和。 **说明**: - $m == students.length == mentors.length$。 - $n == students[i].length == mentors[j].length$。 - $1 \le m, n \le 8$。 - $students[i][k]$ 为 $0$ 或 $1$。 - $mentors[j][k]$ 为 $0$ 或 $1$。 **示例**: - 示例 1: ```python 输入:students = [[1,1,0],[1,0,1],[0,0,1]], mentors = [[1,0,0],[0,0,1],[1,1,0]] 输出:8 解释:按下述方式分配学生和导师: - 学生 0 分配给导师 2 ,兼容性评分为 3。 - 学生 1 分配给导师 0 ,兼容性评分为 2。 - 学生 2 分配给导师 1 ,兼容性评分为 3。 最大兼容性评分和为 3 + 2 + 3 = 8。 ``` - 示例 2: ```python 输入:students = [[0,0],[0,0],[0,0]], mentors = [[1,1],[1,1],[1,1]] 输出:0 解释:任意学生与导师配对的兼容性评分都是 0。 ``` ## 解题思路 ### 思路 1:状压 DP 因为 $m$、$n$ 的范围都是 $[1, 8]$,所以我们可以使用「状态压缩」的方式来表示学生的分配情况。即使用一个 $m$ 位长度的二进制数 $state$ 来表示每一位老师是否被分配了学生。如果 $state$ 的第 $i$ 位为 $1$,表示第 $i$ 位老师被分配了学生,如果 $state$ 的第 $i$ 位为 $0$,则表示第 $i$ 位老师没有分配到学生。 这样,我们就可以通过动态规划的方式来解决这道题。 ###### 1. 阶段划分 按照学生的分配情况进行阶段划分。 ###### 2. 定义状态 定义当前学生的分配情况为 $state$,$state$ 中包含 $count(state)$ 个 $1$,表示有 $count(state)$ 个老师被分配了学生。 则可以定义状态 $dp[state]$ 表示为:当前老师被分配学生的状态为 $state$,其中有 $count(state)$ 个老师被分配了学生的情况下,可以得到的最大兼容性评分和。 ###### 3. 状态转移方程 对于当前状态 $state$,肯定是从比 $state$ 少选一个老师被分配的状态中递推而来。我们可以枚举少选一个元素的状态,找到可以得到的最大兼容性评分和,赋值给 $dp[state]$。 即状态转移方程为:$dp[state] = max(dp[state], \quad dp[state \oplus (1 \text{ <}\text{< } i)] + score[i][one\_cnt - 1])$,其中: 1. $state$ 第 $i$ 位一定为 $1$。 2. $state \oplus (1 \text{ <}\text{< } i)$ 为比 $state$ 少选一个元素的状态。 3. $scores[i][one\_cnt - 1]$ 为第 $i$ 名老师分配到第 $one\_cnt - 1$ 名学生的兼容性评分。 关于每位老师与每位同学之间的兼容性评分,我们可以事先通过一个 $m \times m \times n$ 的三重循环计算得出,并且存入到 $m \times m$ 大小的二维矩阵 $scores$ 中。 ###### 4. 初始条件 - 初始每个老师都没有分配到学生的状态下,可以得到的最兼容性评分和为 $0$,即 $dp[0] = 0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[state]$ 表示为:当前老师被分配学生的状态为 $state$,其中有 $count(state)$ 个老师被分配了学生的情况下,可以得到的最大兼容性评分和。所以最终结果为 $dp[states - 1]$,其中 $states = 1 \text{ <}\text{< } m$。 ### 思路 1:代码 ```python class Solution: def maxCompatibilitySum(self, students: List[List[int]], mentors: List[List[int]]) -> int: m, n = len(students), len(students[0]) scores = [[0 for _ in range(m)] for _ in range(m)] for i in range(m): for j in range(m): for k in range(n): scores[i][j] += (students[i][k] == mentors[j][k]) states = 1 << m dp = [0 for _ in range(states)] for state in range(states): one_cnt = bin(state).count('1') for i in range(m): if (state >> i) & 1: dp[state] = max(dp[state], dp[state ^ (1 << i)] + scores[i][one_cnt - 1]) return dp[states - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m^2 \times n + m \times 2^m)$。 - **空间复杂度**:$O(2^m)$。 ================================================ FILE: docs/solutions/1900-1999/minimum-difference-between-highest-and-lowest-of-k-scores.md ================================================ # [1984. 学生分数的最小差值](https://leetcode.cn/problems/minimum-difference-between-highest-and-lowest-of-k-scores/) - 标签:数组、排序、滑动窗口 - 难度:简单 ## 题目链接 - [1984. 学生分数的最小差值 - 力扣](https://leetcode.cn/problems/minimum-difference-between-highest-and-lowest-of-k-scores/) ## 题目大意 **描述**:给定一个下标从 $0$ 开始的整数数组 $nums$,其中 $nums[i]$ 表示第 $i$ 名学生的分数。另给定一个整数 $k$。 **要求**:从数组中选出任意 $k$ 名学生的分数,使这 $k$ 个分数间最高分和最低分的差值达到最小化。返回可能的最小差值 。 **说明**: - $1 \le k \le nums.length \le 1000$。 - $0 \le nums[i] \le 10^5$。 **示例**: - 示例 1: ```python 输入:nums = [90], k = 1 输出:0 解释:选出 1 名学生的分数,仅有 1 种方法: - [90] 最高分和最低分之间的差值是 90 - 90 = 0 可能的最小差值是 0 ``` - 示例 2: ```python 输入:nums = [9,4,1,7], k = 2 输出:2 解释:选出 2 名学生的分数,有 6 种方法: - [9,4,1,7] 最高分和最低分之间的差值是 9 - 4 = 5 - [9,4,1,7] 最高分和最低分之间的差值是 9 - 1 = 8 - [9,4,1,7] 最高分和最低分之间的差值是 9 - 7 = 2 - [9,4,1,7] 最高分和最低分之间的差值是 4 - 1 = 3 - [9,4,1,7] 最高分和最低分之间的差值是 7 - 4 = 3 - [9,4,1,7] 最高分和最低分之间的差值是 7 - 1 = 6 可能的最小差值是 2 ``` ## 解题思路 ### 思路 1:排序 + 滑动窗口 如果想要最小化选择的 $k$ 名学生中最高分与最低分的差值,我们应该在排序后的数组中连续选择 $k$ 名学生。这是因为如果将连续 $k$ 名学生中的某位学生替换成不连续的学生,其最高分 / 最低分一定会发生变化,并且一定会使最高分变得最高 / 最低分变得最低。从而导致差值增大。 因此,最优方案一定是在排序后的数组中连续选择 $k$ 名学生中的所有情况中的其中一种。 这样,我们可以先对数组 $nums$ 进行升序排序。然后使用一个固定长度为 $k$ 的滑动窗口计算连续选择 $k$ 名学生的最高分与最低分的差值。并记录下最小的差值 $ans$,最后作为答案并返回结果。 ### 思路 1:代码 ```Python class Solution: def minimumDifference(self, nums: List[int], k: int) -> int: nums.sort() ans = float('inf') for i in range(k - 1, len(nums)): ans = min(ans, nums[i] - nums[i - k + 1]) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$,其中 $n$ 为数组 $nums$ 的长度。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/1900-1999/minimum-number-of-work-sessions-to-finish-the-tasks.md ================================================ # [1986. 完成任务的最少工作时间段](https://leetcode.cn/problems/minimum-number-of-work-sessions-to-finish-the-tasks/) - 标签:位运算、数组、动态规划、回溯、状态压缩 - 难度:中等 ## 题目链接 - [1986. 完成任务的最少工作时间段 - 力扣](https://leetcode.cn/problems/minimum-number-of-work-sessions-to-finish-the-tasks/) ## 题目大意 **描述**:给定一个整数数组 $tasks$ 代表需要完成的任务。 其中 $tasks[i]$ 表示第 $i$ 个任务需要花费的时长(单位为小时)。再给定一个整数 $sessionTime$,代表在一个工作时段中,最多可以连续工作的小时数。在连续工作至多 $sessionTime$ 小时后,需要进行休息。 现在需要按照如下条件完成给定任务: 1. 如果你在某一个时间段开始一个任务,你需要在同一个时间段完成它。 2. 完成一个任务后,你可以立马开始一个新的任务。 3. 你可以按任意顺序完成任务。 **要求**:按照上述要求,返回完成所有任务所需要的最少数目的工作时间段。 **说明**: - $n == tasks.length$。 - $1 \le n \le 14$。 - $1 \le tasks[i] \le 10$。 - $max(tasks[i]) \le sessionTime \le 15$。 **示例**: - 示例 1: ```python 输入:tasks = [1,2,3], sessionTime = 3 输出:2 解释:你可以在两个工作时间段内完成所有任务。 - 第一个工作时间段:完成第一和第二个任务,花费 1 + 2 = 3 小时。 - 第二个工作时间段:完成第三个任务,花费 3 小时。 ``` - 示例 2: ```python 输入:tasks = [3,1,3,1,1], sessionTime = 8 输出:2 解释:你可以在两个工作时间段内完成所有任务。 - 第一个工作时间段:完成除了最后一个任务以外的所有任务,花费 3 + 1 + 3 + 1 = 8 小时。 - 第二个工作时间段,完成最后一个任务,花费 1 小时。 ``` ## 解题思路 ### 思路 1:状压 DP ### 思路 1:代码 ```python class Solution: def minSessions(self, tasks: List[int], sessionTime: int) -> int: size = len(tasks) states = 1 << size prefix_sum = [0 for _ in range(states)] for state in range(states): for i in range(size): if (state >> i) & 1: prefix_sum[state] = prefix_sum[state ^ (1 << i)] + tasks[i] break dp = [float('inf') for _ in range(states)] dp[0] = 0 for state in range(states): sub = state while sub > 0: if prefix_sum[sub] <= sessionTime: dp[state] = min(dp[state], dp[state ^ sub] + 1) sub = (sub - 1) & state return dp[states - 1] ``` ### 思路 1:复杂度分析 - **时间复杂度**: - **空间复杂度**: ================================================ FILE: docs/solutions/1900-1999/the-number-of-good-subsets.md ================================================ # [1994. 好子集的数目](https://leetcode.cn/problems/the-number-of-good-subsets/) - 标签:位运算、数组、数学、动态规划、状态压缩 - 难度:困难 ## 题目链接 - [1994. 好子集的数目 - 力扣](https://leetcode.cn/problems/the-number-of-good-subsets/) ## 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:返回 $nums$ 中不同的好子集的数目对 $10^9 + 7$ 取余的结果。 **说明**: - **子集**:通过删除 $nums$ 中一些(可能一个都不删除,也可能全部都删除)元素后剩余元素组成的数组。如果两个子集删除的下标不同,那么它们被视为不同的子集。 - **好子集**:如果 $nums$ 的一个子集中,所有元素的乘积可以表示为一个或多个互不相同的质数的乘积,那么我们称它为好子集。 - 比如,如果 $nums = [1, 2, 3, 4]$: - $[2, 3]$,$[1, 2, 3]$ 和 $[1, 3]$ 是好子集,乘积分别为 $6 = 2 \times 3$ ,$6 = 2 \times 3$ 和 $3 = 3$。 - $[1, 4]$ 和 $[4]$ 不是好子集,因为乘积分别为 $4 = 2 \times 2$ 和 $4 = 2 \times 2$。 - $1 \le nums.length \le 10^5$。 - $1 \le nums[i] \le 30$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,4] 输出:6 解释:好子集为: - [1,2]:乘积为 2,可以表示为质数 2 的乘积。 - [1,2,3]:乘积为 6,可以表示为互不相同的质数 2 和 3 的乘积。 - [1,3]:乘积为 3,可以表示为质数 3 的乘积。 - [2]:乘积为 2,可以表示为质数 2 的乘积。 - [2,3]:乘积为 6,可以表示为互不相同的质数 2 和 3 的乘积。 - [3]:乘积为 3,可以表示为质数 3 的乘积。 ``` - 示例 2: ```python 输入:nums = [4,2,3,15] 输出:5 解释:好子集为: - [2]:乘积为 2,可以表示为质数 2 的乘积。 - [2,3]:乘积为 6,可以表示为互不相同质数 2 和 3 的乘积。 - [2,15]:乘积为 30,可以表示为互不相同质数 2,3 和 5 的乘积。 - [3]:乘积为 3,可以表示为质数 3 的乘积。 - [15]:乘积为 15,可以表示为互不相同质数 3 和 5 的乘积。 ``` ## 解题思路 ### 思路 1:状态压缩 DP 根据题意可以看出: 1. 虽然 $nums$ 的长度是 $[1, 10^5]$,但是其值域范围只有 $[1, 30]$,则我们可以将 $[1, 30]$ 的数分为 $3$ 类: 1. 质数:$[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]$(共 $10$ 个数)。由于好子集的乘积拆解后的质因子只能包含这 $10$ 个,我们可以使用一个数组 $primes$ 记录下这 $10$ 个质数,将好子集的乘积拆解为质因子后,每个 $primes[i]$ 最多出现一次。 2. 非质数:$[4, 6, 8, 9, 10, 12, 14, 16, 18, 20, 21, 22, 24, 25, 26, 27, 28, 30]$。非质数肯定不会出现在好子集的乘积拆解后的质因子中。 3. 特殊的数:$[1]$。对于一个好子集而言,无论向中间添加多少个 $1$,得到的新子集仍是好子集。 2. 分类完成后,由于 $[1, 30]$ 中只有 $10$ 个质数,因此我们可以使用一个长度为 $10$ 的二进制数 $state$ 来表示 $primes$ 中质因数的选择情况。其中,如果 $state$ 第 $i$ 位为 $1$,则说明第 $i$ 个质因数 $primes[i]$ 被使用过;如果 $state$ 第 $i$ 位为 $0$,则说明第 $i$ 个质因数 $primes[i]$ 没有被使用过。 3. 题目规定值相同,但是下标不同的子集视为不同子集,那么我们可先统计出 $nums$ 中每个数 $nums[i]$ 的出现次数,将其存入 $cnts$ 数组中,其中 $cnts[num]$ 表示 $num$ 出现的次数。这样在统计方案时,直接计算出 $num$ 的方案数,再乘以 $cnts[num]$ 即可。 接下来,我们就可以使用「动态规划」的方式来解决这道题目了。 ###### 1. 阶段划分 按照质因数的选择情况进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[state]$ 表示为:当质因数选择的情况为 $state$ 时,好子集的数目。 ###### 3. 状态转移方程 对于 $nums$ 中的每个数 $num$,其对应出现次数为 $cnt$。我们可以通过试除法,将 $num$ 分解为不同的质因数,并使用「状态压缩」的方式,用一个二进制数 $cur\_state$ 来表示当前数 $num$ 中使用了哪些质因数。然后枚举所有状态,找到与 $cur\_state$ 不冲突的状态 $state$(也就是除了 $cur\_state$ 中选择的质因数外,选择的其他质因数情况,比如 $cur\_state$ 选择了 $2$ 和 $5$,则枚举不选择 $2$ 和 $5$ 的状态)。 此时,状态转移方程为:$dp[state | cur\_state] = \sum (dp[state] \times cnt) \mod MOD , \quad state \text{ \& } cur\_state == 0$ ###### 4. 初始条件 - 当 $state == 0$,所选质因数为空时,空集为好子集,则 $dp[0] = 1$。同时,对于一个好子集而言,无论向中间添加多少个 $1$,得到的新子集仍是好子集,所以对于空集来说,可以对应出 $2^{cnts[1]}$ 个方案,则最终 $dp[0] = 2^{cnts[1]}$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[state]$ 表示为:当质因数的选择的情况为 $state$ 时,好子集的数目。 所以最终结果为所有状态下的好子集数目累积和。所以我们可以枚举所有状态,并记录下所有好子集的数目和,就是最终结果。 ### 思路 1:代码 ```python class Solution: def numberOfGoodSubsets(self, nums: List[int]) -> int: MOD = 10 ** 9 + 7 primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] cnts = Counter(nums) dp = [0 for _ in range(1 << len(primes))] dp[0] = pow(2, cnts[1], MOD) # 计算 1 # num 分解质因数 for num, cnt in cnts.items(): # 遍历 nums 中所有数及其频数 if num == 1: # 跳过 1 continue flag = True # 检查 num 的质因数是否都不超过 1 cur_num = num cur_state = 0 for i, prime in enumerate(primes): # 对 num 进行试除 cur_cnt = 0 while cur_num % prime == 0: cur_cnt += 1 cur_state |= 1 << i cur_num //= prime if cur_cnt > 1: # 当前质因数超过 1,则 num 不能添加到子集中,跳过 flag = False break if not flag: continue for state in range(1 << len(primes)): if state & cur_state == 0: # 只有当前选择状态与前一状态不冲突时,才能进行动态转移 dp[state | cur_state] = (dp[state | cur_state] + dp[state] * cnt) % MOD ans = 0 # 统计所有非空集合的方案数 for i in range(1, 1 << len(primes)): ans = (ans + dp[i]) % MOD return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n + m \times 2^p)$,其中 $n$ 为数组 $nums$ 的元素个数,$m$ 为 $nums$ 的最大值,$p$ 为 $[1, 30]$ 中的质数个数。 - **空间复杂度**:$O(2^p)$。 ================================================ FILE: docs/solutions/1900-1999/unique-length-3-palindromic-subsequences.md ================================================ # [1930. 长度为 3 的不同回文子序列](https://leetcode.cn/problems/unique-length-3-palindromic-subsequences/) - 标签:哈希表、字符串、前缀和 - 难度:中等 ## 题目链接 - [1930. 长度为 3 的不同回文子序列 - 力扣](https://leetcode.cn/problems/unique-length-3-palindromic-subsequences/) ## 题目大意 **描述**:给定一个人字符串 $s$。 **要求**:返回 $s$ 中长度为 $s$ 的不同回文子序列的个数。即便存在多种方法来构建相同的子序列,但相同的子序列只计数一次。 **说明**: - **回文**:指正着读和反着读一样的字符串。 - **子序列**:由原字符串删除其中部分字符(也可以不删除)且不改变剩余字符之间相对顺序形成的一个新字符串。 - 例如,`"ace"` 是 `"abcde"` 的一个子序列。 - $3 \le s.length \le 10^5$。 - $s$ 仅由小写英文字母组成。 **示例**: - 示例 1: ```python 输入:s = "aabca" 输出:3 解释:长度为 3 的 3 个回文子序列分别是: - "aba" ("aabca" 的子序列) - "aaa" ("aabca" 的子序列) - "aca" ("aabca" 的子序列) ``` - 示例 2: ```python 输入:s = "bbcbaba" 输出:4 解释:长度为 3 的 4 个回文子序列分别是: - "bbb" ("bbcbaba" 的子序列) - "bcb" ("bbcbaba" 的子序列) - "bab" ("bbcbaba" 的子序列) - "aba" ("bbcbaba" 的子序列) ``` ## 解题思路 ### 思路 1:枚举 + 哈希表 字符集只包含 $26$ 个小写字母,所以我们可以枚举这 $26$ 个小写字母。 对于每个小写字母,使用对撞双指针,找到字符串 $s$ 首尾两侧与小写字母相同的最左位置和最右位置。 如果两个位置不同,则我们可以将两个位置中间不重复的字符当作是长度为 $3$ 的子序列最中间的那个字符。 则我们可以统计出两个位置中间不重复字符的个数,将其累加到答案中。 遍历完,返回答案。 ### 思路 1:代码 ```Python class Solution: def countPalindromicSubsequence(self, s: str) -> int: size = len(s) ans = 0 for i in range(26): left, right = 0, size - 1 while left < size and ord(s[left]) - ord('a') != i: left += 1 while right >= 0 and ord(s[right]) - ord('a') != i: right -= 1 if right - left < 2: continue char_set = set() for j in range(left + 1, right): char_set.add(s[j]) ans += len(char_set) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$n \times | \sum | + | \sum |^2$,其中 $n$ 为字符串 $s$ 的长度,$\sum$ 为字符集,本题中 $| \sum | = 26$。 - **空间复杂度**:$O(| \sum |)$。 ================================================ FILE: docs/solutions/2000-2099/final-value-of-variable-after-performing-operations.md ================================================ # [2011. 执行操作后的变量值](https://leetcode.cn/problems/final-value-of-variable-after-performing-operations/) - 标签:数组、字符串、模拟 - 难度:简单 ## 题目链接 - [2011. 执行操作后的变量值 - 力扣](https://leetcode.cn/problems/final-value-of-variable-after-performing-operations/) ## 题目大意 存在一种支持 `4` 种操作和 `1` 个变量 `X` 的编程语言: - `++X` 和 `x++` 使得变量 `X` 值加 `1`。 - `--X` 和 `X--` 使得变脸 `X ` 值减 `1`。 `X` 的初始值是 `0`。现在给定一个字符串数组 `operations`,这是由操作组成的一个列表。 要求:返回执行所有操作后,`X` 的最终值。 ## 解题思路 思路很简单,初始答案 `res` 赋值为 `0`。 然后遍历操作列表 `operations`,判断每一个操作 `operation` 的符号。如果操作中含有 `+`,则让答案加 `1`,否则,则让答案减 `1`。最后输出答案。 ## 代码 ```python def finalValueAfterOperations(self, operations): """ :type operations: List[str] :rtype: int """ res = 0 for opration in operations: res += 1 if '+' in opration else -1 return res ``` ================================================ FILE: docs/solutions/2000-2099/index.md ================================================ ## 本章内容 - [2011. 执行操作后的变量值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/final-value-of-variable-after-performing-operations.md) - [2023. 连接后等于目标字符串的字符串对](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/number-of-pairs-of-strings-with-concatenation-equal-to-target.md) - [2050. 并行课程 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/parallel-courses-iii.md) ================================================ FILE: docs/solutions/2000-2099/number-of-pairs-of-strings-with-concatenation-equal-to-target.md ================================================ # [2023. 连接后等于目标字符串的字符串对](https://leetcode.cn/problems/number-of-pairs-of-strings-with-concatenation-equal-to-target/) - 标签:数组、字符串 - 难度:中等 ## 题目链接 - [2023. 连接后等于目标字符串的字符串对 - 力扣](https://leetcode.cn/problems/number-of-pairs-of-strings-with-concatenation-equal-to-target/) ## 题目大意 **描述**:给定一个数字字符串数组 `nums` 和一个数字字符串 `target`。 **要求**:返回 `nums[i] + nums[j]` (两个字符串连接,其中 `i != j`)结果等于 `target` 的下标 `(i, j)` 的数目。 **说明**: - $2 \le nums.length \le 100$。 - $1 \le nums[i].length \le 100$。 - $2 \le target.length \le 100$。 - `nums[i]` 和 `target` 只包含数字。 - `nums[i]` 和 `target` 不含有任何前导 $0$。 **示例**: - 示例 1: ```python 输入:nums = ["777","7","77","77"], target = "7777" 输出:4 解释:符合要求的下标对包括: - (0, 1):"777" + "7" - (1, 0):"7" + "777" - (2, 3):"77" + "77" - (3, 2):"77" + "77" ``` - 示例 2: ```python 输入:nums = ["123","4","12","34"], target = "1234" 输出:2 解释:符合要求的下标对包括 - (0, 1):"123" + "4" - (2, 3):"12" + "34" ``` ## 解题思路 ### 思路 1:暴力枚举 1. 双重循环遍历所有的 `i` 和 `j`,满足 `i != j` 并且 `nums[i] + nums[j] == target` 时,记入到答案数目中。 2. 遍历完,返回答案数目。 ### 思路 1:代码 ```python class Solution: def numOfPairs(self, nums: List[str], target: str) -> int: res = 0 for i in range(len(nums)): for j in range(len(nums)): if i != j and nums[i] + nums[j] == target: res += 1 return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:哈希表 1. 使用哈希表记录字符串数组 `nums` 中所有数字字符串的数量。 2. 遍历哈希表中的键 `num`。 3. 将 `target` 根据 `num` 的长度分为前缀 `prefix` 和 `suffix`。 4. 如果 `num` 等于 `prefix`,则判断后缀 `suffix` 是否在哈希表中,如果在哈希表中,则说明 `prefix` 和 `suffix` 能够拼接为 `target`。 1. 如果 `num` 等于 `suffix`,此时 `perfix == suffix`,则答案数目累积为 `table[prefix] * (table[suffix] - 1)`。 2. 如果 `num` 不等于 `suffix`,则答案数目累积为 `table[prefix] * table[suffix]`。 5. 最后输出答案数目。 ### 思路 2:代码 ```python class Solution: def numOfPairs(self, nums: List[str], target: str) -> int: res = 0 table = collections.defaultdict(int) for num in nums: table[num] += 1 for num in table: size = len(num) prefix, suffix = target[ :size], target[size: ] if num == prefix and suffix in table: if num == suffix: res += table[prefix] * (table[suffix] - 1) else: res += table[prefix] * table[suffix] return res ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/2000-2099/parallel-courses-iii.md ================================================ # [2050. 并行课程 III](https://leetcode.cn/problems/parallel-courses-iii/) - 标签:图、拓扑排序、数组、动态规划 - 难度:困难 ## 题目链接 - [2050. 并行课程 III - 力扣](https://leetcode.cn/problems/parallel-courses-iii/) ## 题目大意 **描述**:给定一个整数 $n$,表示有 $n$ 节课,课程编号为 $1 \sim n$。 再给定一个二维整数数组 $relations$,其中 $relations[j] = [prevCourse_j, nextCourse_j]$,表示课程 $prevCourse_j$ 必须在课程 $nextCourse_j$ 之前完成(先修课的关系)。 再给定一个下标从 $0$ 开始的整数数组 $time$,其中 $time[i]$ 表示完成第 $(i + 1)$ 门课程需要花费的月份数。 现在根据以下规则计算完成所有课程所需要的最少月份数: - 如果一门课的所有先修课都已经完成,则可以在任意时间开始这门课程。 - 可以同时上任意门课程。 **要求**:返回完成所有课程所需要的最少月份数。 **说明**: - $1 \le n \le 5 * 10^4$。 - $0 \le relations.length \le min(n * (n - 1) / 2, 5 \times 10^4)$。 - $relations[j].length == 2$。 - $1 \le prevCourse_j, nextCourse_j \le n$。 - $prevCourse_j != nextCourse_j$。 - 所有的先修课程对 $[prevCourse_j, nextCourse_j]$ 都是互不相同的。 - $time.length == n$。 - $1 \le time[i] \le 10^4$。 - 先修课程图是一个有向无环图。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2021/10/07/ex1.png) ```python 输入:n = 3, relations = [[1,3],[2,3]], time = [3,2,5] 输出:8 解释:上图展示了输入数据所表示的先修关系图,以及完成每门课程需要花费的时间。 你可以在月份 0 同时开始课程 1 和 2 。 课程 1 花费 3 个月,课程 2 花费 2 个月。 所以,最早开始课程 3 的时间是月份 3 ,完成所有课程所需时间为 3 + 5 = 8 个月。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2021/10/07/ex2.png) ```python 输入:n = 5, relations = [[1,5],[2,5],[3,5],[3,4],[4,5]], time = [1,2,3,4,5] 输出:12 解释:上图展示了输入数据所表示的先修关系图,以及完成每门课程需要花费的时间。 你可以在月份 0 同时开始课程 1 ,2 和 3 。 在月份 1,2 和 3 分别完成这三门课程。 课程 4 需在课程 3 之后开始,也就是 3 个月后。课程 4 在 3 + 4 = 7 月完成。 课程 5 需在课程 1,2,3 和 4 之后开始,也就是在 max(1,2,3,7) = 7 月开始。 所以完成所有课程所需的最少时间为 7 + 5 = 12 个月。 ``` ## 解题思路 ### 思路 1:拓扑排序 + 动态规划 1. 使用邻接表 $graph$ 存放课程关系图,并统计每门课程节点的入度,存入入度列表 $indegrees$。定义 $dp[i]$ 为完成第 $i$ 门课程所需要的最少月份数。使用 $ans$ 表示完成所有课程所需要的最少月份数。 2. 借助队列 $queue$,将所有入度为 $0$ 的节点入队。 3. 将队列中入度为 $0$ 的节点依次取出。对于取出的每个节点 $u$: 1. 遍历该节点的相邻节点 $v$,更新相邻节点 $v$ 所需要的最少月份数,即:$dp[v] = max(dp[v], dp[u] + time[v - 1])$。 2. 更新完成所有课程所需要的最少月份数 $ans$,即:$ans = max(ans, dp[v])$。 3. 相邻节点 $v$ 的入度减 $1$,如果入度减 $1$ 后的节点入度为 0,则将其加入队列 $queue$。 4. 重复 $3$ 的步骤,直到队列中没有节点。 5. 最后返回 $ans$。 ### 思路 1:代码 ```python class Solution: def minimumTime(self, n: int, relations: List[List[int]], time: List[int]) -> int: graph = [[] for _ in range(n + 1)] indegrees = [0 for _ in range(n + 1)] for u, v in relations: graph[u].append(v) indegrees[v] += 1 queue = collections.deque() dp = [0 for _ in range(n + 1)] ans = 0 for i in range(1, n + 1): if indegrees[i] == 0: queue.append(i) dp[i] = time[i - 1] ans = max(ans, time[i - 1]) while queue: u = queue.popleft() for v in graph[u]: dp[v] = max(dp[v], dp[u] + time[v - 1]) ans = max(ans, dp[v]) indegrees[v] -= 1 if indegrees[v] == 0: queue.append(v) return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n)$,其中 $m$ 为数组 $relations$ 的长度。 - **空间复杂度**:$O(m + n)$。 ================================================ FILE: docs/solutions/2100-2199/find-substring-with-given-hash-value.md ================================================ # [2156. 查找给定哈希值的子串](https://leetcode.cn/problems/find-substring-with-given-hash-value/) - 标签:字符串、滑动窗口、哈希函数、滚动哈希 - 难度:困难 ## 题目链接 - [2156. 查找给定哈希值的子串 - 力扣](https://leetcode.cn/problems/find-substring-with-given-hash-value/) ## 题目大意 **描述**:如果给定整数 `p` 和 `m`,一个长度为 `k` 且下标从 `0` 开始的字符串 `s` 的哈希值按照如下函数计算: - $hash(s, p, m) = (val(s[0]) * p^0 + val(s[1]) * p^1 + ... + val(s[k-1]) * p^{k-1}) mod m$. 其中 `val(s[i])` 表示 `s[i]` 在字母表中的下标,从 `val('a') = 1` 到 `val('z') = 26`。 现在给定一个字符串 `s` 和整数 `power`,`modulo`,`k` 和 `hashValue` 。 **要求**:返回 `s` 中 第一个 长度为 `k` 的 子串 `sub`,满足 `hash(sub, power, modulo) == hashValue`。 **说明**: - 子串:定义为一个字符串中连续非空字符组成的序列。 - $1 \le k \le s.length \le 2 * 10^4$。 - $1 \le power, modulo \le 10^9$。 - $0 \le hashValue < modulo$。 - `s` 只包含小写英文字母。 - 测试数据保证一定存在满足条件的子串。 **示例**: - 示例 1: ```python 输入:s = "leetcode", power = 7, modulo = 20, k = 2, hashValue = 0 输出:"ee" 解释:"ee" 的哈希值为 hash("ee", 7, 20) = (5 * 1 + 5 * 7) mod 20 = 40 mod 20 = 0 。 "ee" 是长度为 2 的第一个哈希值为 0 的子串,所以我们返回 "ee" 。 ``` ## 解题思路 ### 思路 1:Rabin Karp 算法、滚动哈希算法 这道题目的思想和 Rabin Karp 字符串匹配算法中用到的滚动哈希思想是一样的。不过两者计算的公式是相反的。 - 本题目中的子串哈希计算公式:$hash(s, p, m) = (val(s[i]) * p^0 + val(s[i+1]) * p^1 + ... + val(s[i+k-1]) * p^{k-1}) \mod m$. - RK 算法中的子串哈希计算公式:$hash(s, p, m) = (val(s[i]) * p^{k-1} + val(s[i+1]) * p^{k-2} + ... + val(s[i+k-1]) * p^0) \mod m$. 可以看出两者的哈希计算公式是反的。 在 RK 算法中,下一个子串的哈希值计算方式为:$Hash(s_{[i + 1, i + k]}) = \{[Hash(s_{[i, i + k - 1]}) - s_i \times d^{k - 1}] \times d + s_{i + k} \times d^{0} \} \mod m$。其中 $Hash(s_{[i, i + k - 1]}$ 为当前子串的哈希值,$Hash(s_{[i + 1, i + k]})$ 为下一个子串的哈希值。 这个公式也可以用文字表示为:**在计算完当前子串的哈希值后,向右滚动字符串,即移除当前子串中最左侧字符的哈希值($val(s[i]) * p^{k-1}$)之后,再将整体乘以 $p$,再移入最右侧字符的哈希值 $val(s[i+k])$**。 我们可以参考 RK 算法中滚动哈希的计算方式,将其应用到本题中。 因为两者的哈希计算公式相反,所以本题中,我们可以从右侧想左侧逆向遍历字符串,当计算完当前子串的哈希值后,移除当前子串最右侧字符的哈希值($ val(s[i+k-1]) * p^{k-1}$)之后,再整体乘以 $p$,再移入最左侧字符的哈希值 $val(s[i - 1])$。 在本题中,对应的下一个逆向子串的哈希值计算方式为:$Hash(s_{[i - 1, i + k - 2]}) = \{ [Hash(s_{[i, i + k - 1]}) - s_{i + k - 1} \times d^{k - 1}] \times d + s_{i - 1} \times d^{0} \} \mod m$。其中 $Hash(s_{[i, i + k - 1]})$ 为当前子串的哈希值,$Hash(s_{[i - 1, i + k - 2]})$ 是下一个逆向子串的哈希值。 利用取模运算的两个公式: - $(a \times b) \mod m = ((a \mod m) \times (b \mod m)) \mod m$ - $(a + b) \mod m = (a \mod m + b \mod m) \mod m$ 我们可以把上面的式子转变为: $$\begin{aligned} Hash(s_{[i - 1, i + k - 2]}) &= \{[Hash(s_{[i, i + k - 1]}) - s_{i + k - 1} \times d^{k - 1}] \times d + s_{i - 1} \times d^{0} \} \mod m \cr &= \{[Hash(s_{[i, i + k - 1]}) - s_{i + k - 1} \times d^{k - 1}] \times d \mod m + s_{i - 1} \times d^{0} \mod m \} \mod m \cr &= \{[Hash(s_{[i, i + k - 1]}) - s_{i + k - 1} \times d^{k - 1}] \mod m \times d \mod m + s_{i - 1} \times d^{0} \mod m \} \mod m \end{aligned}$$ > 注意:这里之所以用了「反向迭代」而不是「正向迭代」是因为如果使用了正向迭代,那么每次移除的最左侧字符哈希值为 $val(s[i]) * p^0$,之后整体需要除以 $p$,再移入最右侧字符哈希值为($val(s[i+k]) * p^{k-1})$)。 > > 这样就用到了「除法」。而除法是不满足取模运算对应的公式的,所以这里不能用这种方法进行迭代。 > > 而反向迭代,用到的是乘法。在整个过程中是满足取模运算相关的公式。乘法取余不影响最终结果。 ### 思路 1:代码 ```python class Solution: def subStrHash(self, s: str, power: int, modulo: int, k: int, hashValue: int) -> str: hash_t = 0 n = len(s) for i in range(n - 1, n - k - 1, -1): hash_t = (hash_t * power + (ord(s[i]) - ord('a') + 1)) % modulo # 计算最后一个子串的哈希值 h = pow(power, k - 1) % modulo # 计算最高位项,方便后续移除操作 ans = "" if hash_t == hashValue: ans = s[n - k: n] for i in range(n - k - 1, -1, -1): # 反向迭代,滚动计算子串的哈希值 hash_t = (hash_t - h * (ord(s[i + k]) - ord('a') + 1)) % modulo # 移除 s[i + k] 的哈希值 hash_t = (hash_t * power % modulo + (ord(s[i]) - ord('a') + 1) % modulo) % modulo # 添加 s[i] 的哈希值 if hash_t == hashValue: # 如果子串哈希值等于 hashValue,则为答案 ans = s[i: i + k] return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。其中字符串 $s$ 的长度为 $n$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/2100-2199/index.md ================================================ ## 本章内容 - [2156. 查找给定哈希值的子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/find-substring-with-given-hash-value.md) - [2172. 数组的最大与和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/maximum-and-sum-of-array.md) ================================================ FILE: docs/solutions/2100-2199/maximum-and-sum-of-array.md ================================================ # [2172. 数组的最大与和](https://leetcode.cn/problems/maximum-and-sum-of-array/) - 标签:位运算、数组、动态规划、状态压缩 - 难度:困难 ## 题目链接 - [2172. 数组的最大与和 - 力扣](https://leetcode.cn/problems/maximum-and-sum-of-array/) ## 题目大意 **描述**:给定一个长度为 $n$ 的整数数组 $nums$ 和一个整数 $numSlots$ 满足 $2 \times numSlots \ge n$。一共有 $numSlots$ 个篮子,编号为 $1 \sim numSlots$。 现在需要将所有 $n$ 个整数分到这些篮子中,且每个篮子最多有 $2$ 个整数。 **要求**:返回将 $nums$ 中所有数放入 $numSlots$ 个篮子中的最大与和。 **说明**: - **与和**:当前方案中,每个数与它所在篮子编号的按位与运算结果之和。 - 比如,将数字 $[1, 3]$ 放入篮子 $1$ 中,$[4, 6]$ 放入篮子 $2$ 中,这个方案的与和为 $(1 \text{ AND } 1) + (3 \text{ AND } 1) + (4 \text{ AND } 2) + (6 \text{ AND } 2) = 1 + 1 + 0 + 2 = 4$。 - $n == nums.length$。 - $1 \le numSlots \le 9$。 - $1 \le n \le 2 \times numSlots$。 - $1 \le nums[i] \le 15$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,4,5,6], numSlots = 3 输出:9 解释:一个可行的方案是 [1, 4] 放入篮子 1 中,[2, 6] 放入篮子 2 中,[3, 5] 放入篮子 3 中。 最大与和为 (1 AND 1) + (4 AND 1) + (2 AND 2) + (6 AND 2) + (3 AND 3) + (5 AND 3) = 1 + 0 + 2 + 2 + 3 + 1 = 9。 ``` - 示例 2: ```python 输入:nums = [1,3,10,4,7,1], numSlots = 9 输出:24 解释:一个可行的方案是 [1, 1] 放入篮子 1 中,[3] 放入篮子 3 中,[4] 放入篮子 4 中,[7] 放入篮子 7 中,[10] 放入篮子 9 中。 最大与和为 (1 AND 1) + (1 AND 1) + (3 AND 3) + (4 AND 4) + (7 AND 7) + (10 AND 9) = 1 + 1 + 3 + 4 + 7 + 8 = 24 。 注意,篮子 2 ,5 ,6 和 8 是空的,这是允许的。 ``` ## 解题思路 ### 思路 1:状压 DP 每个篮子最多可分 $2$ 个整数,则我们可以将 $1$ 个篮子分成两个篮子,这样总共有 $2 \times numSlots$ 个篮子,每个篮子中最多可以装 $1$ 个整数。 同时因为 $numSlots$ 的范围为 $[1, 9]$,$2 \times numSlots$ 的范围为 $[2, 19]$,范围不是很大,所以我们可以用「状态压缩」的方式来表示每个篮子中的整数放取情况。 即使用一个 $n \times numSlots$ 位的二进制数 $state$ 来表示每个篮子中的整数放取情况。如果 $state$ 的第 $i$ 位为 $1$,表示第 $i$ 个篮子里边放了整数,如果 $state$ 的第 $i$ 位为 $0$,表示第 $i$ 个篮子为空。 这样,我们就可以通过动态规划的方式来解决这道题。 ###### 1. 阶段划分 按照 $2 \times numSlots$ 个篮子中的整数放取情况进行阶段划分。 ###### 2. 定义状态 定义当前每个篮子中的整数放取情况为 $state$,$state$ 对应选择的整数个数为 $count(state)$。 则可以定义状态 $dp[state]$ 表示为:将前 $count(state)$ 个整数放到篮子里,并且每个篮子中的整数放取情况为 $state$ 时,可以获得的最大与和。 ###### 3. 状态转移方程 对于当前状态 $dp[state]$,肯定是从比 $state$ 少选一个元素的状态中递推而来。我们可以枚举少选一个元素的状态,找到可以获得的最大与和,赋值给 $dp[state]$。 即状态转移方程为:$dp[state] = min(dp[state], dp[state \oplus (1 \text{ <}\text{< } i)] + (i // 2 + 1) \text{ \& } nums[one\_cnt - 1])$,其中: 1. $state$ 第 $i$ 位一定为 $1$。 2. $state \oplus (1 \text{ <}\text{< } i)$ 为比 $state$ 少选一个元素的状态。 3. $i // 2 + 1$ 为篮子对应编号 4. $nums[one\_cnt - 1]$ 为当前正在考虑的数组元素。 ###### 4. 初始条件 - 初始每个篮子中都没有放整数的情况下,可以获得的最大与和为 $0$,即 $dp[0] = 0$。 ###### 5. 最终结果 根据我们之前定义的状态,$dp[state]$ 表示为:将前 $count(state)$ 个整数放到篮子里,并且每个篮子中的整数放取情况为 $state$ 时,可以获得的最大与和。所以最终结果为 $max(dp)$。 > 注意:当 $one\_cnt > len(nums)$ 时,无法通过递推得到 $dp[state]$,需要跳过。 ### 思路 1:代码 ```python class Solution: def maximumANDSum(self, nums: List[int], numSlots: int) -> int: states = 1 << (numSlots * 2) dp = [0 for _ in range(states)] for state in range(states): one_cnt = bin(state).count('1') if one_cnt > len(nums): continue for i in range(numSlots * 2): if (state >> i) & 1: dp[state] = max(dp[state], dp[state ^ (1 << i)] + ((i // 2 + 1) & nums[one_cnt - 1])) return max(dp) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(2^m \times m)$,其中 $m = 2 \times numSlots$。 - **空间复杂度**:$O(2^m)$。 ================================================ FILE: docs/solutions/2200-2299/add-two-integers.md ================================================ # [2235. 两整数相加](https://leetcode.cn/problems/add-two-integers/) - 标签:数学 - 难度:简单 ## 题目链接 - [2235. 两整数相加 - 力扣](https://leetcode.cn/problems/add-two-integers/) ## 题目大意 **描述**:给定两个整数 $num1$ 和 $num2$。 **要求**:返回这两个整数的和。 **说明**: - $-100 \le num1, num2 \le 100$。 **示例**: - 示例 1: ```python 示例 1: 输入:num1 = 12, num2 = 5 输出:17 解释:num1 是 12,num2 是 5,它们的和是 12 + 5 = 17,因此返回 17。 ``` - 示例 2: ```python 输入:num1 = -10, num2 = 4 输出:-6 解释:num1 + num2 = -6,因此返回 -6。 ``` ## 解题思路 ### 思路 1:直接计算 1. 直接计算整数 $num1$ 与 $num2$ 的和,返回 $num1 + num2$ 即可。 ### 思路 1:代码 ```python class Solution: def sum(self, num1: int, num2: int) -> int: return num1 + num2 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/2200-2299/count-integers-in-intervals.md ================================================ # [2276. 统计区间中的整数数目](https://leetcode.cn/problems/count-integers-in-intervals/) - 标签:设计、线段树、有序集合 - 难度:困难 ## 题目链接 - [2276. 统计区间中的整数数目 - 力扣](https://leetcode.cn/problems/count-integers-in-intervals/) ## 题目大意 **描述**:给定一个区间的空集。 **要求**:设计并实现满足要求的数据结构: - 新增:添加一个区间到这个区间集合中。 - 统计:计算出现在 至少一个 区间中的整数个数。 实现 CountIntervals 类: - `CountIntervals()` 使用区间的空集初始化对象 - `void add(int left, int right)` 添加区间 `[left, right]` 到区间集合之中。 - `int count()` 返回出现在 至少一个 区间中的整数个数。 **说明**: - 区间 `[left, right]` 表示满足 $left \le x \le right$ 的所有整数 `x`。 - $1 \le left \le right \le 10^9$。 - 最多调用 `add` 和 `count` 方法 **总计** $10^5$ 次。 - 调用 `count` 方法至少一次。 **示例**: - 示例 1: ```python 输入: ["CountIntervals", "add", "add", "count", "add", "count"] [[], [2, 3], [7, 10], [], [5, 8], []] 输出: [null, null, null, 6, null, 8] 解释: CountIntervals countIntervals = new CountIntervals(); // 用一个区间空集初始化对象 countIntervals.add(2, 3); // 将 [2, 3] 添加到区间集合中 countIntervals.add(7, 10); // 将 [7, 10] 添加到区间集合中 countIntervals.count(); // 返回 6 // 整数 2 和 3 出现在区间 [2, 3] 中 // 整数 7、8、9、10 出现在区间 [7, 10] 中 countIntervals.add(5, 8); // 将 [5, 8] 添加到区间集合中 countIntervals.count(); // 返回 8 // 整数 2 和 3 出现在区间 [2, 3] 中 // 整数 5 和 6 出现在区间 [5, 8] 中 // 整数 7 和 8 出现在区间 [5, 8] 和区间 [7, 10] 中 // 整数 9 和 10 出现在区间 [7, 10] 中 ``` ## 解题思路 ### 思路 1:动态开点线段树 这道题可以使用线段树来做。 因为区间的范围是 $[1, 10^9]$,普通数组构成的线段树不满足要求。需要用到动态开点线段树。具体做法如下: - 初始化方法,构建一棵线段树。每个线段树的节点类存储当前区间中保存的元素个数。 - 在 `add` 方法中,将区间 `[left, right]` 上的每个元素值赋值为 `1`,则区间值为 `right - left + 1`。 - 在 `count` 方法中,返回区间 $[0, 10^9]$ 的区间值(即区间内元素个数)。 ### 思路 1:动态开点线段树代码 ```python # 线段树的节点类 class SegTreeNode: def __init__(self, left=-1, right=-1, val=0, lazy_tag=None, leftNode=None, rightNode=None): self.left = left # 区间左边界 self.right = right # 区间右边界 self.mid = left + (right - left) // 2 self.leftNode = leftNode # 区间左节点 self.rightNode = rightNode # 区间右节点 self.val = val # 节点值(区间值) self.lazy_tag = lazy_tag # 区间问题的延迟更新标记 # 线段树类 class SegmentTree: # 初始化线段树接口 def __init__(self, function): self.tree = SegTreeNode(0, int(1e9)) self.function = function # function 是一个函数,左右区间的聚合方法 # 区间更新接口:将区间为 [q_left, q_right] 上的元素值修改为 val def update_interval(self, q_left, q_right, val): self.__update_interval(q_left, q_right, val, self.tree) # 区间查询接口:查询区间为 [q_left, q_right] 的区间值 def query_interval(self, q_left, q_right): return self.__query_interval(q_left, q_right, self.tree) # 以下为内部实现方法 # 区间更新实现方法 def __update_interval(self, q_left, q_right, val, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 node.lazy_tag = val # 将当前节点的延迟标记标记为 val interval_size = (node.right - node.left + 1) # 当前节点所在区间大小 node.val = val * interval_size # 当前节点所在区间每个元素值改为 val return if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 if q_left <= node.mid: # 在左子树中更新区间值 self.__update_interval(q_left, q_right, val, node.leftNode) if q_right > node.mid: # 在右子树中更新区间值 self.__update_interval(q_left, q_right, val, node.rightNode) self.__pushup(node) # 区间查询实现方法:在线段树的 [left, right] 区间范围中搜索区间为 [q_left, q_right] 的区间值 def __query_interval(self, q_left, q_right, node): if node.left >= q_left and node.right <= q_right: # 节点所在区间被 [q_left, q_right] 所覆盖 return node.val # 直接返回节点值 if node.right < q_left or node.left > q_right: # 节点所在区间与 [q_left, q_right] 无关 return 0 self.__pushdown(node) # 向下更新节点所在区间的左右子节点的值和懒惰标记 res_left = 0 # 左子树查询结果 res_right = 0 # 右子树查询结果 if q_left <= node.mid: # 在左子树中查询 res_left = self.__query_interval(q_left, q_right, node.leftNode) if q_right > node.mid: # 在右子树中查询 res_right = self.__query_interval(q_left, q_right, node.rightNode) return self.function(res_left, res_right) # 返回左右子树元素值的聚合计算结果 # 向上更新实现方法:更新 node 节点区间值 等于 该节点左右子节点元素值的聚合计算结果 def __pushup(self, node): if node.leftNode and node.rightNode: node.val = self.function(node.leftNode.val, node.rightNode.val) # 向下更新实现方法:更新 node 节点所在区间的左右子节点的值和懒惰标记 def __pushdown(self, node): if node.leftNode is None: node.leftNode = SegTreeNode(node.left, node.mid) if node.rightNode is None: node.rightNode = SegTreeNode(node.mid + 1, node.right) lazy_tag = node.lazy_tag if node.lazy_tag is None: return node.leftNode.lazy_tag = lazy_tag # 更新左子节点懒惰标记 left_size = (node.leftNode.right - node.leftNode.left + 1) node.leftNode.val = lazy_tag * left_size # 更新左子节点值 node.rightNode.lazy_tag = lazy_tag # 更新右子节点懒惰标记 right_size = (node.rightNode.right - node.rightNode.left + 1) node.rightNode.val = lazy_tag * right_size # 更新右子节点值 node.lazy_tag = None # 更新当前节点的懒惰标记 class CountIntervals: def __init__(self): self.STree = SegmentTree(lambda x, y: x + y) self.left = 10 ** 9 self.right = 0 def add(self, left: int, right: int) -> None: self.STree.update_interval(left, right, 1) def count(self) -> int: return self.STree.query_interval(0, int(1e9)) # Your CountIntervals object will be instantiated and called as such: # obj = CountIntervals() # obj.add(left,right) # param_2 = obj.count() ``` ================================================ FILE: docs/solutions/2200-2299/count-lattice-points-inside-a-circle.md ================================================ # [2249. 统计圆内格点数目](https://leetcode.cn/problems/count-lattice-points-inside-a-circle/) - 标签:几何、数组、哈希表、数学、枚举 - 难度:中等 ## 题目链接 - [2249. 统计圆内格点数目 - 力扣](https://leetcode.cn/problems/count-lattice-points-inside-a-circle/) ## 题目大意 **描述**:给定一个二维整数数组 `circles`。其中 `circles[i] = [xi, yi, ri]` 表示网格上圆心为 `(xi, yi)` 且半径为 `ri` 的第 $i$ 个圆。 **要求**:返回出现在至少一个圆内的格点数目。 **说明**: - **格点**:指的是整数坐标对应的点。 - 圆周上的点也被视为出现在圆内的点。 - $1 \le circles.length \le 200$。 - $circles[i].length == 3$。 - $1 \le xi, yi \le 100$。 - $1 \le ri \le min(xi, yi)$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2022/03/02/exa-11.png) ```python 输入:circles = [[2,2,1]] 输出:5 解释: 给定的圆如上图所示。 出现在圆内的格点为 (1, 2)、(2, 1)、(2, 2)、(2, 3) 和 (3, 2),在图中用绿色标识。 像 (1, 1) 和 (1, 3) 这样用红色标识的点,并未出现在圆内。 因此,出现在至少一个圆内的格点数目是 5。 ``` - 示例 2: ```python 输入:circles = [[2,2,2],[3,4,1]] 输出:16 解释: 给定的圆如上图所示。 共有 16 个格点出现在至少一个圆内。 其中部分点的坐标是 (0, 2)、(2, 0)、(2, 4)、(3, 2) 和 (4, 4)。 ``` ## 解题思路 ### 思路 1:枚举算法 题目要求中 $1 \le xi, yi \le 100$,$1 \le ri \le min(xi, yi)$。则圆中点的范围为 $1 \le x, y \le 200$。 我们可以枚举所有坐标和所有圆,检测该坐标是否在圆中。 为了优化枚举范围,我们可以先遍历一遍所有圆,计算最小、最大的 $x$、$y$ 范围,再枚举所有坐标和所有圆,并进行检测。 ### 思路 1:代码 ```python class Solution: def countLatticePoints(self, circles: List[List[int]]) -> int: min_x, min_y = 200, 200 max_x, max_y = 0, 0 for circle in circles: if circle[0] + circle[2] > max_x: max_x = circle[0] + circle[2] if circle[0] - circle[2] < min_x: min_x = circle[0] - circle[2] if circle[1] + circle[2] > max_y: max_y = circle[1] + circle[2] if circle[1] - circle[2] < min_y: min_y = circle[1] - circle[2] ans = 0 for x in range(min_x, max_x + 1): for y in range(min_y, max_y + 1): for xi, yi, ri in circles: if (xi - x) * (xi - x) + (yi - y) * (yi - y) <= ri * ri: ans += 1 break return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(x \times y)$,其中 $x$、$y$ 分别为横纵坐标的个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/2200-2299/index.md ================================================ ## 本章内容 - [2235. 两整数相加](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/add-two-integers.md) - [2246. 相邻字符不同的最长路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/longest-path-with-different-adjacent-characters.md) - [2249. 统计圆内格点数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/count-lattice-points-inside-a-circle.md) - [2276. 统计区间中的整数数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/count-integers-in-intervals.md) ================================================ FILE: docs/solutions/2200-2299/longest-path-with-different-adjacent-characters.md ================================================ # [2246. 相邻字符不同的最长路径](https://leetcode.cn/problems/longest-path-with-different-adjacent-characters/) - 标签:树、深度优先搜索、图、拓扑排序、数组、字符串 - 难度:困难 ## 题目链接 - [2246. 相邻字符不同的最长路径 - 力扣](https://leetcode.cn/problems/longest-path-with-different-adjacent-characters/) ## 题目大意 **描述**:给定一个长度为 $n$ 的数组 $parent$ 来表示一棵树(即一个连通、无向、无环图)。该树的节点编号为 $0 \sim n - 1$,共 $n$ 个节点,其中根节点的编号为 $0$。其中 $parent[i]$ 表示节点 $i$ 的父节点,由于节点 $0$ 是根节点,所以 $parent[0] == -1$。再给定一个长度为 $n$ 的字符串,其中 $s[i]$ 表示分配给节点 $i$ 的字符。 **要求**:找出路径上任意一对相邻节点都没有分配到相同字符的最长路径,并返回该路径的长度。 **说明**: - $n == parent.length == s.length$。 - $1 \le n \le 10^5$。 - 对所有 $i \ge 1$ ,$0 \le parent[i] \le n - 1$ 均成立。 - $parent[0] == -1$。 - $parent$ 表示一棵有效的树。 - $s$ 仅由小写英文字母组成。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2022/03/25/testingdrawio.png) ```python 输入:parent = [-1,0,0,1,1,2], s = "abacbe" 输出:3 解释:任意一对相邻节点字符都不同的最长路径是:0 -> 1 -> 3 。该路径的长度是 3 ,所以返回 3。 可以证明不存在满足上述条件且比 3 更长的路径。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2022/03/25/graph2drawio.png) ```python 输入:parent = [-1,0,0,0], s = "aabc" 输出:3 解释:任意一对相邻节点字符都不同的最长路径是:2 -> 0 -> 3 。该路径的长度为 3 ,所以返回 3。 ``` ## 解题思路 ### 思路 1:树形 DP + 深度优先搜索 因为题目给定的是表示父子节点的 $parent$ 数组,为了方便递归遍历相邻节点,我们可以根据 $partent$ 数组,建立一个由父节点指向子节点的有向图 $graph$。 如果不考虑相邻节点是否为相同字符这一条件,那么这道题就是在求树的直径(树的最长路径长度)中的节点个数。 对于根节点为 $u$ 的树来说: 1. 如果其最长路径经过根节点 $u$,则 **最长路径长度 = 某子树中的最长路径长度 + 另一子树中的最长路径长度 + 1**。 2. 如果其最长路径不经过根节点 $u$,则 **最长路径长度 = 某个子树中的最长路径长度**。 即:**最长路径长度 = max(某子树中的最长路径长度 + 另一子树中的最长路径长度 + 1,某个子树中的最长路径长度)**。 对此,我们可以使用深度优先搜索递归遍历 $u$ 的所有相邻节点 $v$,并在递归遍历的同时,维护一个全局最大路径和变量 $ans$,以及当前节点 $u$ 的最大路径长度变量 $u\_len$。 1. 先计算出从相邻节点 $v$ 出发的最长路径长度 $v\_len$。 2. 更新维护全局最长路径长度为 $self.ans = max(self.ans, \quad u\_len + v\_len + 1)$。 3. 更新维护当前节点 $u$ 的最长路径长度为 $u\_len = max(u\_len, \quad v\_len + 1)$。 因为题目限定了「相邻节点字符不同」,所以在更新全局最长路径长度和当前节点 $u$ 的最长路径长度时,我们需要判断一下节点 $u$ 与相邻节点 $v$ 的字符是否相同,只有在字符不同的条件下,才能够更新维护。 最后,因为题目要求的是树的直径(树的最长路径长度)中的节点个数,而:**路径的节点 = 路径长度 + 1**,所以最后我们返回 $self.ans + 1$ 作为答案。 ### 思路 1:代码 ```python class Solution: def longestPath(self, parent: List[int], s: str) -> int: size = len(parent) # 根据 parent 数组,建立有向图 graph = [[] for _ in range(size)] for i in range(1, size): graph[parent[i]].append(i) ans = 0 def dfs(u): nonlocal ans u_len = 0 # u 节点的最大路径长度 for v in graph[u]: # 遍历 u 节点的相邻节点 v_len = dfs(v) # 相邻节点的最大路径长度 if s[u] != s[v]: # 相邻节点字符不同 ans = max(ans, u_len + v_len + 1) # 维护最大路径长度 u_len = max(u_len, v_len + 1) # 更新 u 节点的最大路径长度 return u_len # 返回 u 节点的最大路径长度 dfs(0) return ans + 1 ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 是树的节点数目。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/2300-2399/count-special-integers.md ================================================ # [2376. 统计特殊整数](https://leetcode.cn/problems/count-special-integers/) - 标签:数学、动态规划 - 难度:困难 ## 题目链接 - [2376. 统计特殊整数 - 力扣](https://leetcode.cn/problems/count-special-integers/) ## 题目大意 **描述**:给定一个正整数 $n$。 **要求**:求区间 $[1, n]$ 内的所有整数中,特殊整数的数目。 **说明**: - **特殊整数**:如果一个正整数的每一个数位都是互不相同的,则称它是特殊整数。 - $1 \le n \le 2 \times 10^9$。 **示例**: - 示例 1: ```python 输入:n = 20 输出:19 解释:1 到 20 之间所有整数除了 11 以外都是特殊整数。所以总共有 19 个特殊整数。 ``` - 示例 2: ```python 输入:n = 5 输出:5 解释:1 到 5 所有整数都是特殊整数。 ``` ## 解题思路 ### 思路 1:动态规划 + 数位 DP 将 $n$ 转换为字符串 $s$,定义递归函数 `def dfs(pos, state, isLimit, isNum):` 表示构造第 $pos$ 位及之后所有数位的合法方案数。接下来按照如下步骤进行递归。 1. 从 `dfs(0, 0, True, False)` 开始递归。 `dfs(0, 0, True, False)` 表示: 1. 从位置 $0$ 开始构造。 2. 初始没有使用数字(即前一位所选数字集合为 $0$)。 3. 开始时受到数字 $n$ 对应最高位数位的约束。 4. 开始时没有填写数字。 2. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时: 1. 如果 $isNum == True$,说明当前方案符合要求,则返回方案数 $1$。 2. 如果 $isNum == False$,说明当前方案不符合要求,则返回方案数 $0$。 3. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:`ans = 0`。 4. 如果遇到 $isNum == False$,说明之前位数没有填写数字,当前位可以跳过,这种情况下方案数等于 $pos + 1$ 位置上没有受到 $pos$ 位的约束,并且之前没有填写数字时的方案数,即:`ans = dfs(i + 1, state, False, False)`。 5. 如果 $isNum == True$,则当前位必须填写一个数字。此时: 1. 根据 $isNum$ 和 $isLimit$ 来决定填当前位数位所能选择的最小数字($minX$)和所能选择的最大数字($maxX$), 2. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $d$。 3. 如果之前没有选择 $d$,即 $d$ 不在之前选择的数字集合 $state$ 中,则方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, state | (1 << d), isLimit and d == maxX, True)`。 1. `state | (1 << d)` 表示之前选择的数字集合 $state$ 加上 $d$。 2. `isLimit and d == maxX` 表示 $pos + 1$ 位受到之前 $pos$ 位限制。 3. $isNum == True$ 表示 $pos$ 位选择了数字。 6. 最后的方案数为 `dfs(0, 0, True, False)`,将其返回即可。 ### 思路 1:代码 ```python class Solution: def countSpecialNumbers(self, n: int) -> int: # 将 n 转换为字符串 s s = str(n) @cache # pos: 第 pos 个数位 # state: 之前选过的数字集合。 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 # isNum: 表示 pos 前面的数位是否填了数字。 # 如果为真,则当前位不可跳过;如果为假,则当前位可跳过。 def dfs(pos, state, isLimit, isNum): if pos == len(s): # isNum 为 True,则表示当前方案符合要求 return int(isNum) ans = 0 if not isNum: # 如果 isNum 为 False,则可以跳过当前数位 ans = dfs(pos + 1, state, False, False) # 如果前一位没有填写数字,则最小可选择数字为 0,否则最少为 1(不能含有前导 0)。 minX = 0 if isNum else 1 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s[pos]) if isLimit else 9 # 枚举可选择的数字 for d in range(minX, maxX + 1): # d 不在选择的数字集合中,即之前没有选择过 d if (state >> d) & 1 == 0: ans += dfs(pos + 1, state | (1 << d), isLimit and d == maxX, True) return ans return dfs(0, 0, True, False) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n \times 10 \times 2^{10})$,其中 $n$ 为给定整数。 - **空间复杂度**:$O(\log n \times 2^{10})$。 ================================================ FILE: docs/solutions/2300-2399/index.md ================================================ ## 本章内容 - [2376. 统计特殊整数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2300-2399/count-special-integers.md) ================================================ FILE: docs/solutions/2400-2499/index.md ================================================ ## 本章内容 - [2427. 公因子的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2400-2499/number-of-common-factors.md) ================================================ FILE: docs/solutions/2400-2499/number-of-common-factors.md ================================================ # [2427. 公因子的数目](https://leetcode.cn/problems/number-of-common-factors/) - 标签:数学、枚举、数论 - 难度:简单 ## 题目链接 - [2427. 公因子的数目 - 力扣](https://leetcode.cn/problems/number-of-common-factors/) ## 题目大意 **描述**:给定两个正整数 $a$ 和 $b$。 **要求**:返回 $a$ 和 $b$ 的公因子数目。 **说明**: - **公因子**:如果 $x$ 可以同时整除 $a$ 和 $b$,则认为 $x$ 是 $a$ 和 $b$ 的一个公因子。 - $1 \le a, b \le 1000$。 **示例**: - 示例 1: ```python 输入:a = 12, b = 6 输出:4 解释:12 和 6 的公因子是 1、2、3、6。 ``` - 示例 2: ```python 输入:a = 25, b = 30 输出:2 解释:25 和 30 的公因子是 1、5。 ``` ## 解题思路 ### 思路 1:枚举算法 最直接的思路就是枚举所有 $[1, min(a, b)]$ 之间的数,并检查是否能同时整除 $a$ 和 $b$。 当然,因为 $a$ 与 $b$ 的公因子肯定不会超过 $a$ 与 $b$ 的最大公因数,则我们可以直接枚举 $[1, gcd(a, b)]$ 之间的数即可,其中 $gcd(a, b)$ 是 $a$ 与 $b$ 的最大公约数。 ### 思路 1:代码 ```python class Solution: def commonFactors(self, a: int, b: int) -> int: ans = 0 for i in range(1, math.gcd(a, b) + 1): if a % i == 0 and b % i == 0: ans += 1 return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\sqrt{min(a, b)})$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/2500-2599/difference-between-maximum-and-minimum-price-sum.md ================================================ # [2538. 最大价值和与最小价值和的差值](https://leetcode.cn/problems/difference-between-maximum-and-minimum-price-sum/) - 标签:树、深度优先搜索、数组、动态规划 - 难度:困难 ## 题目链接 - [2538. 最大价值和与最小价值和的差值 - 力扣](https://leetcode.cn/problems/difference-between-maximum-and-minimum-price-sum/) ## 题目大意 **描述**:给定一个整数 $n$ 和一个长度为 $n - 1$ 的二维整数数组 $edges$ 用于表示一个 $n$ 个节点的无向无根图,节点编号为 $0 \sim n - 1$。其中 $edges[i] = [ai, bi]$ 表示树中节点 $ai$ 和 $bi$ 之间有一条边。再给定一个整数数组 $price$,其中 $price[i]$ 表示图中节点 $i$ 的价值。 一条路径的价值和是这条路径上所有节点的价值之和。 你可以选择树中任意一个节点作为根节点 $root$。选择 $root$ 为根的开销是以 $root$ 为起点的所有路径中,价值和最大的一条路径与最小的一条路径的差值。 **要求**:返回所有节点作为根节点的选择中,最大的开销为多少。 **说明**: - $1 \le n \le 10^5$。 - $edges.length == n - 1$。 - $0 \le ai, bi \le n - 1$。 - $edges$ 表示一棵符合题面要求的树。 - $price.length == n$。 - $1 \le price[i] \le 10^5$。 **示例**: - 示例 1: ![](https://assets.leetcode.com/uploads/2022/12/01/example14.png) ```python 输入:n = 6, edges = [[0,1],[1,2],[1,3],[3,4],[3,5]], price = [9,8,7,6,10,5] 输出:24 解释:上图展示了以节点 2 为根的树。左图(红色的节点)是最大价值和路径,右图(蓝色的节点)是最小价值和路径。 - 第一条路径节点为 [2,1,3,4]:价值为 [7,8,6,10] ,价值和为 31 。 - 第二条路径节点为 [2] ,价值为 [7] 。 最大路径和与最小路径和的差值为 24 。24 是所有方案中的最大开销。 ``` - 示例 2: ![](https://assets.leetcode.com/uploads/2022/11/24/p1_example2.png) ```python 输入:n = 3, edges = [[0,1],[1,2]], price = [1,1,1] 输出:2 解释:上图展示了以节点 0 为根的树。左图(红色的节点)是最大价值和路径,右图(蓝色的节点)是最小价值和路径。 - 第一条路径包含节点 [0,1,2]:价值为 [1,1,1] ,价值和为 3 。 - 第二条路径节点为 [0] ,价值为 [1] 。 最大路径和与最小路径和的差值为 2 。2 是所有方案中的最大开销。 ``` ## 解题思路 ### 思路 1:树形 DP + 深度优先搜索 1. 因为 $price$ 数组中元素都为正数,所以价值和最小的一条路径一定为「单个节点」,也就是根节点 $root$ 本身。 2. 因为价值和最大的路径是从根节点 $root$ 出发的价值和最大的一条路径,所以「最大的开销」等于「从根节点 $root$ 出发的价值和最大的一条路径」与「路径中一个端点值」 的差值。 3. 价值和最大的路径的两个端点中,一个端点为根节点 $root$,另一个节点为叶子节点。 这样问题就变为了求树中一条路径,使得路径的价值和减去其中一个端点值的权值最大。 对此我们可以使用深度优先搜索递归遍历二叉树,并在递归遍历的同时,维护一个最大开销变量 $ans$。 然后定义函数 ` def dfs(self, u, father):` 计算以节点 $u$ 为根节点的子树中,带端点的最大路径和 $max\_s1$,以及去掉端点的最大路径和 $max\_s2$,其中 $father$ 表示节点 $u$ 的根节点,用于遍历邻接节点的过程中过滤父节点,避免重复遍历。 初始化带端点的最大路径和 $max\_s1$ 为 $price[u]$,表示当前只有一个节点,初始化去掉端点的最大路径和 $max\_s2$ 为 $0$,表示当前没有节点。 然后在遍历节点 $u$ 的相邻节点 $v$ 时,递归调用 $dfs(v, u)$,获取以节点 $v$ 为根节点的子树中,带端点的最大路径和 $s1$,以及去掉端点的最大路径和 $s2$。此时最大开销变量 $self.ans$ 有两种情况: 1. $u$ 的子树中带端点的最大路径和,加上 $v$ 的子树中不带端点的最大路径和,即:$max\_s1 + s2$。 2. $u$ 的子树中去掉端点的最大路径和,加上 $v$ 的子树中带端点的最大路径和,即:$max\_s2 + s1$。 此时我们更新最大开销变量 $self.ans$,即:$self.ans = max(self.ans, \quad max\_s1 + s2, \quad max\_s2 + s1)$。 然后更新 $u$ 的子树中带端点的最大路径和 $max\_s1$,即:$max\_s1= max(max\_s1, \quad s1 + price[u])$。 再更新 $u$ 的子树中去掉端点的最大路径和 $max\_s2$,即:$max\_s2 = max(max\_s2, \quad s2 + price[u])$。 最后返回带端点 $u$ 的最大路径和 $max\_s1$,以及去掉端点 $u$ 的最大路径和 $。 最终,最大开销变量 $self.ans$ 即为答案。 ### 思路 1:代码 ```python class Solution: def __init__(self): self.ans = 0 def dfs(self, graph, price, u, father): max_s1 = price[u] max_s2 = 0 for v in graph[u]: if v == father: # 过滤父节点,避免重复遍历 continue s1, s2 = self.dfs(graph, price, v, u) self.ans = max(self.ans, max_s1 + s2, max_s2 + s1) max_s1 = max(max_s1, s1 + price[u]) max_s2 = max(max_s2, s2 + price[u]) return max_s1, max_s2 def maxOutput(self, n: int, edges: List[List[int]], price: List[int]) -> int: graph = [[] for _ in range(n)] for u, v in edges: graph[u].append(v) graph[v].append(u) self.dfs(graph, price, 0, -1) return self.ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为树中节点个数。 - **空间复杂度**:$O(n)$。 ## 参考链接 - 【题解】[二维差分模板 双指针 树形DP 树的直径【力扣周赛 328】](https://www.bilibili.com/video/BV1QT41127kJ/) - 【题解】[2538. 最大价值和与最小价值和的差值 题解](https://github.com/doocs/leetcode/blob/main/solution/2500-2599/2538.Difference Between Maximum and Minimum Price Sum/README.md) ================================================ FILE: docs/solutions/2500-2599/index.md ================================================ ## 本章内容 - [2538. 最大价值和与最小价值和的差值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2500-2599/difference-between-maximum-and-minimum-price-sum.md) - [2585. 获得分数的方法数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2500-2599/number-of-ways-to-earn-points.md) ================================================ FILE: docs/solutions/2500-2599/number-of-ways-to-earn-points.md ================================================ # [2585. 获得分数的方法数](https://leetcode.cn/problems/number-of-ways-to-earn-points/) - 标签:数组、动态规划 - 难度:困难 ## 题目链接 - [2585. 获得分数的方法数 - 力扣](https://leetcode.cn/problems/number-of-ways-to-earn-points/) ## 题目大意 **描述**:考试中有 $n$ 种类型的题目。给定一个整数 $target$ 和一个下标从 $0$ 开始的二维整数数组 $types$,其中 $types[i] = [count_i, marks_i]$ 表示第 $i$ 种类型的题目有 $count_i$ 道,每道题目对应 $marks_i$ 分。 **要求**:返回你在考试中恰好得到 $target$ 分的方法数。由于答案可能很大,结果需要对 $10^9 + 7$ 取余。 **说明**: - 同类型题目无法区分。比如说,如果有 $3$ 道同类型题目,那么解答第 $1$ 和第 $2$ 道题目与解答第 $1$ 和第 $3$ 道题目或者第 $2$ 和第 $3$ 道题目是相同的。 - $1 \le target \le 1000$。 - $n == types.length$。 - $1 \le n \le 50$。 - $types[i].length == 2$。 - $1 \le counti, marksi \le 50$。 **示例**: - 示例 1: ```python 输入:target = 6, types = [[6,1],[3,2],[2,3]] 输出:7 解释:要获得 6 分,你可以选择以下七种方法之一: - 解决 6 道第 0 种类型的题目:1 + 1 + 1 + 1 + 1 + 1 = 6 - 解决 4 道第 0 种类型的题目和 1 道第 1 种类型的题目:1 + 1 + 1 + 1 + 2 = 6 - 解决 2 道第 0 种类型的题目和 2 道第 1 种类型的题目:1 + 1 + 2 + 2 = 6 - 解决 3 道第 0 种类型的题目和 1 道第 2 种类型的题目:1 + 1 + 1 + 3 = 6 - 解决 1 道第 0 种类型的题目、1 道第 1 种类型的题目和 1 道第 2 种类型的题目:1 + 2 + 3 = 6 - 解决 3 道第 1 种类型的题目:2 + 2 + 2 = 6 - 解决 2 道第 2 种类型的题目:3 + 3 = 6 ``` - 示例 2: ```python 输入:target = 5, types = [[50,1],[50,2],[50,5]] 输出:4 解释:要获得 5 分,你可以选择以下四种方法之一: - 解决 5 道第 0 种类型的题目:1 + 1 + 1 + 1 + 1 = 5 - 解决 3 道第 0 种类型的题目和 1 道第 1 种类型的题目:1 + 1 + 1 + 2 = 5 - 解决 1 道第 0 种类型的题目和 2 道第 1 种类型的题目:1 + 2 + 2 = 5 - 解决 1 道第 2 种类型的题目:5 ``` ## 解题思路 ### 思路 1:动态规划 ###### 1. 阶段划分 按照进行阶段划分。 ###### 2. 定义状态 定义状态 $dp[i][w]$ 表示为:前 $i$ 种题目恰好组成 $w$ 分的方案数。 ###### 3. 状态转移方程 前 $i$ 种题目恰好组成 $w$ 分的方案数,等于前 $i - 1$ 种问题恰好组成 $w - k \times marks_i$ 分的方案数总和,即状态转移方程为:$dp[i][w] = \sum_{k = 0} dp[i - 1][w - k \times marks_i]$。 ###### 4. 初始条件 - 前 $0$ 种题目恰好组成 $0$ 分的方案数为 $1$。 ###### 5. 最终结果 根据我们之前定义的状态, $dp[i][w]$ 表示为:前 $i$ 种题目恰好组成 $w$ 分的方案数。 所以最终结果为 $dp[size][target]$。 ### 思路 1:代码 ```python class Solution: def waysToReachTarget(self, target: int, types: List[List[int]]) -> int: size = len(types) group_count = [types[i][0] for i in range(len(types))] weight = [[(types[i][1] * k) for k in range(types[i][0] + 1)] for i in range(len(types))] mod = 1000000007 dp = [[0 for _ in range(target + 1)] for _ in range(size + 1)] dp[0][0] = 1 # 枚举前 i 组物品 for i in range(1, size + 1): # 枚举背包装载重量 for w in range(target + 1): # 枚举第 i 组物品能取个数 dp[i][w] = dp[i - 1][w] for k in range(1, group_count[i - 1] + 1): if w >= weight[i - 1][k]: dp[i][w] += dp[i - 1][w - weight[i - 1][k]] dp[i][w] %= mod return dp[size][target] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times target \times m)$,其中 $n$ 为题目种类数,$target$ 为目标分数,$m$ 为每种题目的最大分数。 - **空间复杂度**:$O(n \times target)$。 ================================================ FILE: docs/solutions/2700-2799/count-of-integers.md ================================================ # [2719. 统计整数数目](https://leetcode.cn/problems/count-of-integers/) - 标签:数学、字符串、动态规划 - 难度:困难 ## 题目链接 - [2719. 统计整数数目 - 力扣](https://leetcode.cn/problems/count-of-integers/) ## 题目大意 **描述**:给定两个数字字符串 $num1$ 和 $num2$,以及两个整数 $max\_sum$ 和 $min\_sum$。 **要求**:返回好整数的数目。答案可能很大,请返回答案对 $10^9 + 7$ 取余后的结果。 **说明**: - **好整数**:如果一个整数 $x$ 满足一下条件,我们称它是一个好整数: - $num1 \le x \le num2$。 - $num\_sum \le digit\_sum(x) \le max\_sum$。 - $digit\_sum(x)$ 表示 $x$ 各位数字之和。 - $1 \le num1 \le num2 \le 10^{22}$。 - $1 \le min\_sum \le max\_sum \le 400$。 **示例**: - 示例 1: ```python 输入:num1 = "1", num2 = "12", min_num = 1, max_num = 8 输出:11 解释:总共有 11 个整数的数位和在 1 到 8 之间,分别是 1,2,3,4,5,6,7,8,10,11 和 12 。所以我们返回 11。 ``` - 示例 2: ```python 输入:num1 = "1", num2 = "5", min_num = 1, max_num = 5 输出:5 解释:数位和在 1 到 5 之间的 5 个整数分别为 1,2,3,4 和 5 。所以我们返回 5。 ``` ## 解题思路 ### 思路 1:动态规划 + 数位 DP 将 $num1$ 补上前导 $0$,补到和 $num2$ 长度一致,定义递归函数 `def dfs(pos, total, isMaxLimit, isMinLimit):` 表示构造第 $pos$ 位及之后所有数位的合法方案数。接下来按照如下步骤进行递归。 1. 从 `dfs(0, 0, True, True)` 开始递归。 `dfs(0, 0, True, True)` 表示: 1. 从位置 $0$ 开始构造。 2. 初始数位和为 $0$。 3. 开始时当前数位最大值受到最高位数位的约束。 4. 开始时当前数位最小值受到最高位数位的约束。 2. 如果 $total > max\_sum$,说明当前方案不符合要求,则返回方案数 $0$。 3. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时: 1. 如果 $min\_sum \le total \le max\_sum$,说明当前方案符合要求,则返回方案数 $1$。 2. 如果不满足,则当前方案不符合要求,则返回方案数 $0$。 4. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:`ans = 0`。 5. 根据 $isMaxLimit$ 和 $isMinLimit$ 来决定填当前位数位所能选择的最小数字($minX$)和所能选择的最大数字($maxX$)。 6. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $d$。 7. 方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, total + d, isMaxLimit and d == maxX, isMinLimit and d == minX)`。 1. `total + d` 表示当前数位和 $total$ 加上 $d$。 2. `isMaxLimit and d == maxX` 表示 $pos + 1$ 位最大值受到之前 $pos$ 位限制。 3. `isMinLimit and d == maxX` 表示 $pos + 1$ 位最小值受到之前 $pos$ 位限制。 8. 最后的方案数为 `dfs(0, 0, True, True) % MOD`,将其返回即可。 ### 思路 1:代码 ```python class Solution: def count(self, num1: str, num2: str, min_sum: int, max_sum: int) -> int: MOD = 10 ** 9 + 7 # 将 num1 补上前导 0,补到和 num2 长度一致 m, n = len(num1), len(num2) if m < n: num1 = '0' * (n - m) + num1 @cache # pos: 第 pos 个数位 # total: 表示数位和 # isMaxLimit: 表示是否受到上限选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 # isMaxLimit: 表示是否受到下限选择限制。如果为真,则第 pos 位填入数字最小为 s[pos];如果为假,则最小可为 0。 def dfs(pos, total, isMaxLimit, isMinLimit): if total > max_sum: return 0 if pos == n: # 当 min_sum <= total <= max_sum 时,当前方案符合要求 return int(total >= min_sum) ans = 0 # 如果受到选择限制,则最小可选择数字为 num1[pos],否则最大可选择数字为 0。 minX = int(num1[pos]) if isMinLimit else 0 # 如果受到选择限制,则最大可选择数字为 num2[pos],否则最大可选择数字为 9。 maxX = int(num2[pos]) if isMaxLimit else 9 # 枚举可选择的数字 for d in range(minX, maxX + 1): ans += dfs(pos + 1, total + d, isMaxLimit and d == maxX, isMinLimit and d == minX) return ans % MOD return dfs(0, 0, True, True) % MOD ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times 10)$,其中 $n$ 为数组 $nums2$ 的长度。 - **空间复杂度**:$O(n \times max\_sum)$。 ================================================ FILE: docs/solutions/2700-2799/index.md ================================================ ## 本章内容 - [2719. 统计整数数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2700-2799/count-of-integers.md) ================================================ FILE: docs/solutions/LCR/0H97ZC.md ================================================ # [LCR 075. 数组的相对排序](https://leetcode.cn/problems/0H97ZC/) - 标签:数组、哈希表、计数排序、排序 - 难度:简单 ## 题目链接 - [LCR 075. 数组的相对排序 - 力扣](https://leetcode.cn/problems/0H97ZC/) ## 题目大意 给定两个数组,`arr1` 和 `arr2`,其中 `arr2` 中的元素各不相同,`arr2` 中的每个元素都出现在 `arr1` 中。 要求:对 `arr1` 中的元素进行排序,使 `arr1` 中项的相对顺序和 `arr2` 中的相对顺序相同。未在 `arr2` 中出现过的元素需要按照升序放在 `arr1` 的末尾。 注意: - `1 <= arr1.length, arr2.length <= 1000`。 - `0 <= arr1[i], arr2[i] <= 1000`。 ## 解题思路 因为元素值范围在 `[0, 1000]`,所以可以使用计数排序的思路来解题。 使用数组 `count` 统计 `arr1` 各个元素个数。 遍历 `arr2` 数组,将对应元素`num2` 按照个数 `count[num2]` 添加到答案数组 `ans` 中,同时在 `count` 数组中减去对应个数。 然后在处理 `count` 中剩余元素,将 `count` 中大于 `0` 的元素下标依次添加到答案数组 `ans` 中。 ## 代码 ```python class Solution: def relativeSortArray(self, arr1: List[int], arr2: List[int]) -> List[int]: count = [0 for _ in range(1010)] for num1 in arr1: count[num1] += 1 res = [] for num2 in arr2: while count[num2] > 0: res.append(num2) count[num2] -= 1 for num in range(len(count)): while count[num] > 0: res.append(num) count[num] -= 1 return res ``` ================================================ FILE: docs/solutions/LCR/0on3uN.md ================================================ # [LCR 087. 复原 IP 地址](https://leetcode.cn/problems/0on3uN/) - 标签:字符串、回溯 - 难度:中等 ## 题目链接 - [LCR 087. 复原 IP 地址 - 力扣](https://leetcode.cn/problems/0on3uN/) ## 题目大意 给定一个只包含数字的字符串,用来表示一个 IP 地址。 要求:返回所有由 `s` 构成的有效 IP 地址,可以按任何顺序返回答案。 - 有效 IP 地址:正好由四个整数(每个整数由 0~255 的数构成,且不能含有前导 0),整数之间用 `.` 分割。 例如:`0.1.2.201` 和 `192.168.1.1` 是有效 IP 地址,但是 `0.011.255.245`、`192.168.1.312` 和 `192.168@1.1` 是 无效 IP 地址。 ## 解题思路 回溯算法。使用 `res` 存储所有有效 IP 地址。用 `point_num` 表示当前 IP 地址的 `.` 符号个数。 定义回溯方法,从 `start_index` 位置开始遍历字符串。 - 如果字符串中添加的 `.` 符号数量为 `3`,则判断当前字符串是否为有效 IP 地址,如果为有效 IP 地址则加入到 `res` 数组中。直接返回。 - 然后在 `[start_index, len(s) - 1]` 范围循环遍历,判断 `[start_index, i]` 范围所代表的子串是否合法。如果合法: - 则 `point_num += 1`。 - 然后在 i 位置后边增加 `.` 符号,继续回溯遍历。 - 最后 `point_num -= 1` 进行回退。 - 不符合则直接跳出循环。 - 最后返回 `res`。 ## 代码 ```python class Solution: res = [] def backstrack(self, s: str, start_index: int, point_num: int): if point_num == 3: if self.isValid(s, start_index, len(s) - 1): self.res.append(s) return for i in range(start_index, len(s)): if self.isValid(s, start_index, i): point_num += 1 self.backstrack(s[:i + 1] + '.' + s[i + 1:], i + 2, point_num) point_num -= 1 else: break def isValid(self, s: str, start: int, end: int): if start > end: return False if s[start] == '0' and start != end: return False num = 0 for i in range(start, end + 1): if s[i] > '9' or s[i] < '0': return False num = num * 10 + ord(s[i]) - ord('0') if num > 255: return False return True def restoreIpAddresses(self, s: str) -> List[str]: self.res.clear() if len(s) > 12: return self.res self.backstrack(s, 0, 0) return self.res ``` ================================================ FILE: docs/solutions/LCR/0ynMMM.md ================================================ # [LCR 039. 柱状图中最大的矩形](https://leetcode.cn/problems/0ynMMM/) - 标签:栈、数组、单调栈 - 难度:困难 ## 题目链接 - [LCR 039. 柱状图中最大的矩形 - 力扣](https://leetcode.cn/problems/0ynMMM/) ## 题目大意 给定一个非负整数数组 `heights` ,`heights[i]` 用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 要求:计算出在该柱状图中,能够勾勒出来的矩形的最大面积。 ## 解题思路 思路一:枚举「宽度」。一重循环枚举所有柱子,第二重循环遍历柱子右侧的柱子,所得的宽度就是两根柱子形成区间的宽度,高度就是这段区间中的最小高度。然后计算出对应面积,记录并更新最大面积。这样下来,时间复杂度为 $O(n^2)$。 思路二:枚举「高度」。一重循环枚举所有柱子,以柱子高度为当前矩形高度,然后向两侧延伸,遇到小于当前矩形高度的情况就停止。然后计算当前矩形面积,记录并更新最大面积。这样下来,时间复杂度也是 $O(n^2)$。 思路三:利用「单调栈」减少两侧延伸的复杂度。 - 枚举所有柱子。 - 如果当前柱子高度较大,大于等于栈顶柱体的高度,则直接将当前柱体入栈。 - 如果当前柱体高度较小,小于栈顶柱体的高度,则一直出栈,直到当前柱体大于等于栈顶柱体高度。 - 出栈后,说明当前柱体是出栈柱体向右找到的第一个小于当前柱体高度的柱体,那么就可以向右将宽度扩展到当前柱体。 - 出栈后,说明新的栈顶柱体是出栈柱体向左找到的第一个小于新的栈顶柱体高度的柱体,那么就可以向左将宽度扩展到新的栈顶柱体。 - 以新的栈顶柱体为左边界,当前柱体为右边界,以出栈柱体为高度。计算矩形面积,然后记录并更新最大面积。 ## 代码 ```python class Solution: def largestRectangleArea(self, heights: List[int]) -> int: heights.append(0) ans = 0 stack = [] for i in range(len(heights)): while stack and heights[stack[-1]] >= heights[i]: cur = stack.pop(-1) left = stack[-1] + 1 if stack else 0 right = i - 1 ans = max(ans, (right - left + 1) * heights[cur]) stack.append(i) return ans ``` ================================================ FILE: docs/solutions/LCR/1fGaJU.md ================================================ # [LCR 007. 三数之和](https://leetcode.cn/problems/1fGaJU/) - 标签:数组、双指针、排序 - 难度:中等 ## 题目链接 - [LCR 007. 三数之和 - 力扣](https://leetcode.cn/problems/1fGaJU/) ## 题目大意 给定一个包含 `n` 个整数的数组 `nums`,判断 `nums` 中是否存在三个元素 `a`、`b`、`c`,满足 `a + b + c = 0`。 要求:找出所有满足要求的不重复的三元组。 ## 解题思路 直接三重遍历查找 a、b、c 的时间复杂度是:$O(n^3)$。我们可以通过一些操作来降低复杂度。 先将数组进行排序,以保证按顺序查找 a、b、c 时,元素值为升序,从而保证所找到的三个元素是不重复的。同时也方便下一步使用双指针减少一重遍历。时间复杂度为:$O(nlogn)$ 第一重循环遍历 a,对于每个 a 元素,从 a 元素的下一个位置开始,使用双指针 left,right。left 指向 a 元素的下一个位置,right 指向末尾位置。先将 left 右移、right 左移去除重复元素,再进行下边的判断。 - 如果 `nums[a] + nums[left] + nums[right] = 0`,则得到一个解,将其加入答案数组中,并继续将 left 右移,right 左移; - 如果 `nums[a] + nums[left] + nums[right] > 0`,说明 nums[right] 值太大,将 right 向左移; - 如果 `nums[a] + nums[left] + nums[right] < 0`,说明 nums[left] 值太小,将 left 右移。 ## 代码 ```python class Solution: def threeSum(self, nums: List[int]) -> List[List[int]]: n = len(nums) nums.sort() ans = [] for i in range(n): if i > 0 and nums[i] == nums[i - 1]: continue left = i + 1 right = n - 1 while left < right: while left < right and left > i + 1 and nums[left] == nums[left - 1]: left += 1 while left < right and right < n - 1 and nums[right + 1] == nums[right]: right -= 1 if left < right and nums[i] + nums[left] + nums[right] == 0: ans.append([nums[i], nums[left], nums[right]]) left += 1 right -= 1 elif nums[i] + nums[left] + nums[right] > 0: right -= 1 else: left += 1 return ans ``` ================================================ FILE: docs/solutions/LCR/21dk04.md ================================================ # [LCR 097. 不同的子序列](https://leetcode.cn/problems/21dk04/) - 标签:字符串、动态规划 - 难度:困难 ## 题目链接 - [LCR 097. 不同的子序列 - 力扣](https://leetcode.cn/problems/21dk04/) ## 题目大意 给定两个字符串 `s` 和 `t`。 要求:计算在 `s` 的子序列中 `t` 出现的个数。 ## 解题思路 动态规划求解。 定义状态 `dp[i][j]`表示为:以 `i - 1` 为结尾的 `s` 子序列中出现以 `j - 1` 为结尾的 `t` 的个数。 则状态转移方程为: - 如果 `s[i - 1] == t[j - 1]`,则:`dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]`。即 `dp[i][j]` 来源于两部分: - 使用 `s[i - 1]` 匹配 `t[j - 1]`,则 `dp[i][j]` 取源于以 `i - 2` 为结尾的 `s` 子序列中出现以 `j - 2` 为结尾的 `t` 的个数,即 `dp[i - 1][j - 1]`。 - 不使用 `s[i - 1]` 匹配 `t[j - 1]`,则 `dp[i][j]` 取源于以 `i - 2` 为结尾的 `s` 子序列中出现以 `j - 1` 为结尾的 `t` 的个数,即 `dp[i - 1][j]`。 - 如果 `s[i - 1] != t[j - 1]`,那么肯定不能用 `s[i - 1]` 匹配 `t[j - 1]`,则 `dp[i][j]` 取源于 `dp[i - 1][j]`。 下面来看看初始化: - `dp[i][0]` 表示以 `i - 1` 为结尾的 `s` 子序列中出现空字符串的个数。把 `s` 中的元素全删除,出现空字符串的个数就是 `1`,则 `dp[i][0] = 1`。 - `dp[0][j]` 表示空字符串中出现以 `j - 1` 结尾的 `t` 的个数,空字符串无论怎么变都不会变成 `t`,则 `dp[0][j] = 0` - `dp[0][0]` 表示空字符串中出现空字符串的个数,这个应该是 `1`,即 `dp[0][0] = 1`。 然后递推求解,最后输出 `dp[size_s][size_t]`。 ## 代码 ```python class Solution: def numDistinct(self, s: str, t: str) -> int: size_s = len(s) size_t = len(t) dp = [[0 for _ in range(size_t + 1)] for _ in range(size_s + 1)] for i in range(size_s): dp[i][0] = 1 for i in range(1, size_s + 1): for j in range(1, size_t + 1): if s[i - 1] == t[j - 1]: dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] else: dp[i][j] = dp[i - 1][j] return dp[size_s][size_t] ``` ================================================ FILE: docs/solutions/LCR/2AoeFn.md ================================================ # [LCR 098. 不同路径](https://leetcode.cn/problems/2AoeFn/) - 标签:数学、动态规划、组合数学 - 难度:中等 ## 题目链接 - [LCR 098. 不同路径 - 力扣](https://leetcode.cn/problems/2AoeFn/) ## 题目大意 给定一个 `m * n` 的棋盘, 机器人在左上角的位置,机器人每次只能向右、或者向下移动一步。 要求:求出到达棋盘右下角共有多少条不同的路径。 ## 解题思路 可以用动态规划求解,设 `dp[i][j]` 是从 `(0, 0)`到 `(i, j)` 的不同路径数。显然 `dp[i][j] = dp[i-1][j] + dp[i][j-1]`。对于第一行、第一列,因为只能超一个方向走,所以 `dp[i][0] = 1`,`dp[0][j] = 1`。 ## 代码 ```python class Solution: def uniquePaths(self, m: int, n: int) -> int: dp = [1 for _ in range(n)] for i in range(1, m): for j in range(1, n): dp[j] += dp[j - 1] return dp[-1] ``` ================================================ FILE: docs/solutions/LCR/2VG8Kg.md ================================================ # [LCR 008. 长度最小的子数组](https://leetcode.cn/problems/2VG8Kg/) - 标签:数组、二分查找、前缀和、滑动窗口 - 难度:中等 ## 题目链接 - [LCR 008. 长度最小的子数组 - 力扣](https://leetcode.cn/problems/2VG8Kg/) ## 题目大意 给定一个只包含正整数的数组 `nums` 和一个正整数 `target`。 要求:找出数组中满足和大于等于 `target` 的长度最小的「连续子数组」,并返回其长度。 ## 解题思路 最直接的做法是暴力枚举,时间复杂度为 $O(n^2)$。但是我们可以利用滑动窗口的方法,在时间复杂度为 $O(n)$ 的范围内解决问题。 定义两个指针 `start` 和 `end`。`start` 代表滑动窗口开始位置,`end` 代表滑动窗口结束位置。再定义一个变量 `sum` 用来存储滑动窗口中的元素和,一个变量 `ans` 来存储满足提议的最小长度。 先不断移动 `end`,直到 `sum ≥ target`,则更新最小长度值 `ans`。然后再将滑动窗口的起始位置从滑动窗口中移出去,直到 `sum ≤ target`,在移出的期间,同样要更新最小长度值 `ans`。 然后等满足 `sum ≤ target` 时,再移动 `end`,重复上一步,直到遍历到数组末尾。 ## 代码 ```python class Solution: def minSubArrayLen(self, target: int, nums: List[int]) -> int: if not nums: return 0 n = len(nums) start = 0 end = 0 sum = 0 ans = n + 1 while end < n: sum += nums[end] while sum >= target: ans = min(ans, end - start + 1) sum -= nums[start] start += 1 end += 1 if ans == n + 1: return 0 else: return ans ``` ================================================ FILE: docs/solutions/LCR/2bCMpM.md ================================================ # [LCR 107. 01 矩阵](https://leetcode.cn/problems/2bCMpM/) - 标签:广度优先搜索、数组、动态规划、矩阵 - 难度:中等 ## 题目链接 - [LCR 107. 01 矩阵 - 力扣](https://leetcode.cn/problems/2bCMpM/) ## 题目大意 给定一个由 `0` 和 `1` 组成的矩阵,两个相邻元素间的距离为 `1` 。 要求:找出每个元素到最近的 `0` 的距离,并输出为矩阵。 ## 解题思路 题目要求的是每个 `1` 到 `0`的最短曼哈顿距离。换句话也可以求每个 `0` 到 `1` 的最短曼哈顿距离。这样做的好处是,可以从所有值为 `0` 的元素开始进行搜索,可以不断累积距离,直到遇到值为 `1` 的元素时,可以直接将累积距离直接赋值。 具体操作如下:将所有值为 `0` 的元素坐标加入访问集合中,对所有值为`0` 的元素上下左右进行搜索。每进行一次上下左右搜索,更新新位置的距离值,并把新的位置坐标加入队列和访问集合中,直到遇见值为 `1` 的元素停止搜索。 ## 代码 ```python class Solution: def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]: row_count = len(mat) col_count = len(mat[0]) dist_map = [[0 for _ in range(col_count)] for _ in range(row_count)] zeroes_pos = [] for i in range(row_count): for j in range(col_count): if mat[i][j] == 0: zeroes_pos.append((i, j)) directions = {(1, 0), (-1, 0), (0, 1), (0, -1)} queue = collections.deque(zeroes_pos) visited = set(zeroes_pos) while queue: i, j = queue.popleft() for direction in directions: new_i = i + direction[0] new_j = j + direction[1] if 0 <= new_i < row_count and 0 <= new_j < col_count and (new_i, new_j) not in visited: dist_map[new_i][new_j] = dist_map[i][j] + 1 queue.append((new_i, new_j)) visited.add((new_i, new_j)) return dist_map ``` ================================================ FILE: docs/solutions/LCR/3Etpl5.md ================================================ # [LCR 049. 求根节点到叶节点数字之和](https://leetcode.cn/problems/3Etpl5/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [LCR 049. 求根节点到叶节点数字之和 - 力扣](https://leetcode.cn/problems/3Etpl5/) ## 题目大意 给定一个二叉树的根节点 `root`,树中每个节点都存放有一个 `0` 到 `9` 之间的数字。每条从根节点到叶节点的路径都代表一个数字。例如,从根节点到叶节点的路径是 `1` -> `2` -> `3`,表示数字 `123`。 要求:计算从根节点到叶节点生成的所有数字的和。 ## 解题思路 使用深度优先搜索,记录下路径上所有节点构成的数字,使用 `pretotal` 保存下当前路径上构成的数字。如果遇到叶节点直接返回当前数字,否则递归遍历左右子树,并累加对应结果。 ## 代码 ```python class Solution: def dfs(self, root, pretotal): if not root: return 0 total = pretotal * 10 + root.val if not root.left and not root.right: return total return self.dfs(root.left, total) + self.dfs(root.right, total) def sumNumbers(self, root: TreeNode) -> int: return self.dfs(root, 0) ``` ================================================ FILE: docs/solutions/LCR/3u1WK4.md ================================================ # [LCR 023. 相交链表](https://leetcode.cn/problems/3u1WK4/) - 标签:哈希表、链表、双指针 - 难度:简单 ## 题目链接 - [LCR 023. 相交链表 - 力扣](https://leetcode.cn/problems/3u1WK4/) ## 题目大意 给定 `A`、`B` 两个链表。 要求:判断两个链表是否相交,返回相交的起始点。如果不相交,则返回 `None`。 比如:链表 A 为 [4, 1, 8, 4, 5],链表 B 为 [5, 0, 1, 8, 4, 5]。则如下图所示,两个链表相交的起始节点为 8,则输出结果为 8。 ![](https://assets.leetcode.com/uploads/2018/12/13/160_example_1.png) ## 解题思路 如果两个链表相交,那么从相交位置开始,到结束,必有一段等长且相同的节点。假设链表 A 的长度为 m、链表 B 的长度为 n,他们的相交序列有 k 个,则相交情况可以如下如所示: ![](https://qcdn.itcharge.cn/images/20210401113538.png) 现在问题是如何找到 m-k 或者 n-k 的位置。 考虑将链表 A 的末尾拼接上链表 B,链表 B 的末尾拼接上链表 A。 然后使用两个指针 pA 、PB,分别从链表 A、链表 B 的头节点开始遍历,如果走到共同的节点,则返回该节点。 否则走到两个链表末尾,返回 None。 ![](https://qcdn.itcharge.cn/images/20210401114100.png) ## 代码 ```python class Solution: def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: if headA == None or headB == None: return None pA = headA pB = headB while pA != pB: pA = pA.next if pA != None else headB pB = pB.next if pB != None else headA return pA ``` ================================================ FILE: docs/solutions/LCR/4sjJUc.md ================================================ # [LCR 082. 组合总和 II](https://leetcode.cn/problems/4sjJUc/) - 标签:数组、回溯 - 难度:中等 ## 题目链接 - [LCR 082. 组合总和 II - 力扣](https://leetcode.cn/problems/4sjJUc/) ## 题目大意 给定一个数组 `candidates` 和一个目标数 `target`。 要求:找出 `candidates` 中所有可以使数字和为目标数 `target` 的组合。 数组 `candidates` 中的数字在每个组合中只能使用一次,且 `1 ≤ candidates[i] ≤ 50`。 ## 解题思路 本题不能有重复组合,关键步骤在于去重。 在回溯遍历的时候,下一层递归的 `start_index` 要从当前节点的后一位开始遍历,即 `i + 1` 位开始。而且统一递归层不能使用相同的元素,即需要增加一句判断 `if i > start_index and candidates[i] == candidates[i - 1]: continue`。 ## 代码 ```python class Solution: res = [] path = [] def backtrack(self, candidates: List[int], target: int, sum: int, start_index: int): if sum > target: return if sum == target: self.res.append(self.path[:]) return for i in range(start_index, len(candidates)): if sum + candidates[i] > target: break if i > start_index and candidates[i] == candidates[i - 1]: continue sum += candidates[i] self.path.append(candidates[i]) self.backtrack(candidates, target, sum, i + 1) sum -= candidates[i] self.path.pop() def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]: self.res.clear() self.path.clear() candidates.sort() self.backtrack(candidates, target, 0, 0) return self.res ``` ================================================ FILE: docs/solutions/LCR/4ueAj6.md ================================================ # [LCR 029. 循环有序列表的插入](https://leetcode.cn/problems/4ueAj6/) - 标签:链表 - 难度:中等 ## 题目链接 - [LCR 029. 循环有序列表的插入 - 力扣](https://leetcode.cn/problems/4ueAj6/) ## 题目大意 给定循环升序链表中的一个节点 `head` 和一个整数 `insertVal`。 要求:将整数 `insertVal` 插入循环升序链表中,并且满足链表仍为循环升序链表。最终返回原先给定的节点。 ## 解题思路 - 先判断所给节点 `head` 是否为空,为空直接创建一个值为 `insertVal` 的新节点,并指向自己,返回即可。 - 如果 `head` 不为空,把 `head` 赋值给 `node` ,方便最后返回原节点 `head`。 - 然后遍历 `node`,判断插入值 `insertVal` 与 `node.val` 和 `node.next.val` 的关系,找到插入位置,具体判断如下: - 如果新节点值在两个节点值中间, 即 `node.val <= insertVal <= node.next.val`。则说明新节点值在最大值最小值中间,应将新节点插入到当前位置,则应将 `insertVal` 插入到这个位置。 - 如果新节点值比当前节点值和当前节点下一节点值都大,并且当前节点值比当前节点值的下一节点值大,即 `node.next.val < node.val <= insertVal`,则说明 `insertVal` 比链表最大值都大,应插入最大值后边。 - 如果新节点值比当前节点值和当前节点下一节点值都小,并且当前节点值比当前节点值的下一节点值大,即 `insertVal < node.next.val < node.val`,则说明 `insertVal` 比链表中最小值都小,应插入最小值前边。 - 找到插入位置后,跳出循环,在插入位置插入值为 `insertVal` 的新节点。 ## 代码 ```python class Solution: def insert(self, head: 'Node', insertVal: int) -> 'Node': if not head: node = Node(insertVal) node.next = node return node node = head while node.next != head: if node.val <= insertVal <= node.next.val: break elif node.next.val < node.val <= insertVal: break elif insertVal < node.next.val < node.val: break else: node = node.next insert_node = Node(insertVal) insert_node.next = node.next node.next = insert_node return head ``` ================================================ FILE: docs/solutions/LCR/569nqc.md ================================================ # [LCR 035. 最小时间差](https://leetcode.cn/problems/569nqc/) - 标签:数组、数学、字符串、排序 - 难度:中等 ## 题目链接 - [LCR 035. 最小时间差 - 力扣](https://leetcode.cn/problems/569nqc/) ## 题目大意 给定一个 24 小时制形式(小时:分钟 "HH:MM")的时间列表 `timePoints`。 要求:找出列表中任意两个时间的最小时间差并以分钟数表示。 ## 解题思路 - 遍历时间列表 `timePoints`,将每个时间转换为以分钟计算的整数形式,比如时间 `14:20`,将其转换为 `14 * 60 + 20 = 860`,存放到新的时间列表 `times` 中。 - 为了处理最早时间、最晚时间之间的时间间隔,我们将 `times` 中最小时间添加到列表末尾一起进行排序。 - 然后将新的时间列表 `times` 按照升序排列。 - 遍历排好序的事件列表 `times` ,找出相邻两个时间的最小间隔值即可。 ## 代码 ```python class Solution: def changeTime(self, timePoint: str): hours, minutes = timePoint.split(':') return int(hours) * 60 + int(minutes) def findMinDifference(self, timePoints: List[str]) -> int: if not timePoints or len(timePoints) > 24 * 60: return 0 times = sorted(self.changeTime(time) for time in timePoints) times.append(times[0] + 24 * 60) res = times[-1] for i in range(1, len(times)): res = min(res, times[i] - times[i - 1]) return res ``` ================================================ FILE: docs/solutions/LCR/6eUYwP.md ================================================ # [LCR 050. 路径总和 III](https://leetcode.cn/problems/6eUYwP/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [LCR 050. 路径总和 III - 力扣](https://leetcode.cn/problems/6eUYwP/) ## 题目大意 ## 解题思路 ## 代码 ```python class Solution: prefixsum_count = dict() def dfs(self, root, prefixsum_count, target_sum, cur_sum): if not root: return 0 res = 0 cur_sum += root.val res += prefixsum_count.get(cur_sum - target_sum, 0) prefixsum_count[cur_sum] = prefixsum_count.get(cur_sum, 0) + 1 res += self.dfs(root.left, prefixsum_count, target_sum, cur_sum) res += self.dfs(root.right, prefixsum_count, target_sum, cur_sum) prefixsum_count[cur_sum] -= 1 return res def pathSum(self, root: TreeNode, targetSum: int) -> int: if not root: return 0 prefixsum_count = dict() prefixsum_count[0] = 1 return self.dfs(root, prefixsum_count, targetSum, 0) ``` ================================================ FILE: docs/solutions/LCR/7LpjUW.md ================================================ # [LCR 118. 冗余连接](https://leetcode.cn/problems/7LpjUW/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [LCR 118. 冗余连接 - 力扣](https://leetcode.cn/problems/7LpjUW/) ## 题目大意 一个 `n` 个节点的树(节点值为 `1~n`)添加一条边后就形成了图,添加的这条边不属于树中已经存在的边。图的信息记录存储与长度为 `n` 的二维数组 `edges`,`edges[i] = [ai, bi]` 表示图中在 `ai` 和 `bi` 之间存在一条边。 现在给定代表边信息的二维数组 `edges`。 要求:找到一条可以山区的边,使得删除后的剩余部分是一个有着 `n` 个节点的树。如果有多个答案,则返回数组 `edges` 中最后出现的边。 ## 解题思路 树可以看做是无环的图,这道题就是要找出那条添加边之后成环的边。可以考虑用并查集来做。 从前向后遍历每一条边,如果边的两个节点不在同一个集合,就加入到一个集合(链接到同一个根节点)。如果边的节点已经出现在同一个集合里,说明边的两个节点已经连在一起了,再加入这条边一定会出现环,则这条边就是所求答案。 ## 代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) self.parent[root_x] = root_y def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def findRedundantConnection(self, edges: List[List[int]]) -> List[int]: size = len(edges) union_find = UnionFind(size + 1) for edge in edges: if union_find.is_connected(edge[0], edge[1]): return edge union_find.union(edge[0], edge[1]) return None ``` ================================================ FILE: docs/solutions/LCR/7WHec2.md ================================================ # [LCR 077. 排序链表](https://leetcode.cn/problems/7WHec2/) - 标签:链表、双指针、分治、排序、归并排序 - 难度:中等 ## 题目链接 - [LCR 077. 排序链表 - 力扣](https://leetcode.cn/problems/7WHec2/) ## 题目大意 给定链表的头节点 `head`。 要求:按照升序排列并返回排序后的链表。 ## 解题思路 归并排序。 1. 利用快慢指针找到链表的中点,以中点为界限将链表拆分成两个子链表。 2. 然后对两个子链表分别递归排序。 3. 将排序后的子链表进行归并排序,得到完整的排序后的链表。 ## 代码 ```python class Solution: def merge_sort(self, head: ListNode, tail: ListNode) -> ListNode: if not head: return head if head.next == tail: head.next = None return head slow = fast = head while fast != tail: slow = slow.next fast = fast.next if fast != tail: fast = fast.next mid = slow return self.merge(self.merge_sort(head, mid), self.merge_sort(mid, tail)) def merge(self, a: ListNode, b: ListNode) -> ListNode: root = ListNode(-1) cur = root while a and b: if a.val < b.val: cur.next = a a = a.next else: cur.next = b b = b.next cur = cur.next if a: cur.next = a if b: cur.next = b return root.next def sortList(self, head: ListNode) -> ListNode: return self.merge_sort(head, None) ``` ================================================ FILE: docs/solutions/LCR/7WqeDu.md ================================================ # [LCR 057. 存在重复元素 III](https://leetcode.cn/problems/7WqeDu/) - 标签:数组、桶排序、有序集合、排序、滑动窗口 - 难度:中等 ## 题目链接 - [LCR 057. 存在重复元素 III - 力扣](https://leetcode.cn/problems/7WqeDu/) ## 题目大意 给定一个整数数组 `nums`,以及两个整数 `k`、`t`。判断数组中是否存在两个不同下标的 `i` 和 `j`,其对应元素满足 `abs(nums[i] - nums[j]) <= t`,同时满足 `abs(i - j) <= k`。如果满足条件则返回 `True`,不满足条件返回 `False`。 ## 解题思路 对于第 `i` 个元素 `nums[i]`,需要查找的区间为 `[i - t, i + t]`。可以利用桶排序的思想。 桶的大小设置为 `t + 1`。我们将元素按照大小依次放入不同的桶中。 遍历数组 `nums` 中的元素,对于元素 `nums[i]` : - 如果 `nums[i]` 放入桶之前桶里已经有元素了,那么这两个元素必然满足 `abs(nums[i] - nums[j]) <= t`, - 如果之前桶里没有元素,那么就将 `nums[i]` 放入对应桶中。 - 然后再判断左右桶的左右两侧桶中是否有元素满足 `abs(nums[i] - nums[j]) <= t`。 - 然后将 `nums[i - k]` 之前的桶清空,因为这些桶中的元素与 `nums[i]` 已经不满足 `abs(i - j) <= k` 了。 最后上述满足条件的情况就返回 `True`,最终遍历完仍不满足条件就返回 `False`。 ## 代码 ```python class Solution: def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool: bucket_dict = dict() for i in range(len(nums)): # 将 nums[i] 划分到大小为 t + 1 的不同桶中 num = nums[i] // (t + 1) # 桶中已经有元素了 if num in bucket_dict: return True # 把 nums[i] 放入桶中 bucket_dict[num] = nums[i] # 判断左侧桶是否满足条件 if (num - 1) in bucket_dict and abs(bucket_dict[num - 1] - nums[i]) <= t: return True # 判断右侧桶是否满足条件 if (num + 1) in bucket_dict and abs(bucket_dict[num + 1] - nums[i]) <= t: return True # 将 i-k 之前的旧桶清除,因为之前的桶已经不满足条件了 if i >= k: bucket_dict.pop(nums[i - k] // (t + 1)) return False ``` ================================================ FILE: docs/solutions/LCR/7p8L0Z.md ================================================ # [LCR 084. 全排列 II](https://leetcode.cn/problems/7p8L0Z/) - 标签:数组、回溯 - 难度:中等 ## 题目链接 - [LCR 084. 全排列 II - 力扣](https://leetcode.cn/problems/7p8L0Z/) ## 题目大意 给定一个可包含重复数字的序列 `nums` 。 要求:按任意顺序返回所有不重复的全排列。 ## 解题思路 这道题跟「[LCR 083. 全排列](https://leetcode.cn/problems/VvJkup/)」不一样的地方在于增加了序列中的元素可重复这一条件。这就涉及到了去重。先对 `nums` 进行排序,然后使用 visited 数组标记该元素在当前排列中是否被访问过。如果未被访问过则将其加入排列中,并在访问后将该元素变为未访问状态。 然后再递归遍历下一层元素之前,增加一句语句进行判重:`if i > 0 and nums[i] == nums[i - 1] and not visited[i - 1]: continue`。 然后进行回溯遍历。 ## 代码 ```python class Solution: res = [] path = [] def backtrack(self, nums: List[int], visited: List[bool]): if len(self.path) == len(nums): self.res.append(self.path[:]) return for i in range(len(nums)): if i > 0 and nums[i] == nums[i - 1] and not visited[i - 1]: continue if not visited[i]: visited[i] = True self.path.append(nums[i]) self.backtrack(nums, visited) self.path.pop() visited[i] = False def permuteUnique(self, nums: List[int]) -> List[List[int]]: self.res.clear() self.path.clear() nums.sort() visited = [False for _ in range(len(nums))] self.backtrack(nums, visited) return self.res ``` ================================================ FILE: docs/solutions/LCR/8Zf90G.md ================================================ # [LCR 036. 逆波兰表达式求值](https://leetcode.cn/problems/8Zf90G/) - 标签:栈、数组、数学 - 难度:中等 ## 题目链接 - [LCR 036. 逆波兰表达式求值 - 力扣](https://leetcode.cn/problems/8Zf90G/) ## 题目大意 给定一个字符串数组 `tokens`,表示「逆波兰表达式」,求解表达式的值。 ## 解题思路 栈的典型应用。遍历字符串数组。遇到操作字符的时候,取出栈顶两个元素,进行运算之后,再将结果入栈。遇到数字,则直接入栈。 ## 代码 ```python class Solution: def evalRPN(self, tokens: List[str]) -> int: stack = [] for token in tokens: if token == '+': stack.append(stack.pop() + stack.pop()) elif token == '-': stack.append(-stack.pop() + stack.pop()) elif token == '*': stack.append(stack.pop() * stack.pop()) elif token == '/': stack.append(int(1 / stack.pop() * stack.pop())) else: stack.append(int(token)) return stack.pop() ``` ================================================ FILE: docs/solutions/LCR/A1NYOS.md ================================================ # [LCR 011. 连续数组](https://leetcode.cn/problems/A1NYOS/) - 标签:数组、哈希表、前缀和 - 难度:中等 ## 题目链接 - [LCR 011. 连续数组 - 力扣](https://leetcode.cn/problems/A1NYOS/) ## 题目大意 给定一个二进制数组 `nums`。 要求:找到含有相同数量 `0` 和 `1` 的最长连续子数组,并返回该子数组的长度。 ## 解题思路 「`0` 和 `1` 数量相同」等价于「`1` 的数量减去 `0` 的数量等于 `0`」。 我们可以使用一个变量 `pre_diff` 来记录下前 `i` 个数中,`1` 的数量比 `0` 的数量多多少个。我们把这个 `pre_diff`叫做「`1` 和 `0` 数量差」,也可以理解为变种的前缀和。 然后我们再用一个哈希表 `pre_dic` 来记录「`1` 和 `0` 数量差」第一次出现的下标。 那么,如果我们在遍历的时候,发现 `pre_diff` 相同的数量差已经在之前出现过了,则说明:这两段之间相减的 `1` 和 `0` 数量差为 `0`。 什么意思呢? 比如说:`j < i`,前 `j` 个数中第一次出现 `pre_diff == 2` ,然后前 `i` 个数中个第二次又出现了 `pre_diff == 2`。那么这两段形成的子数组 `nums[j + 1: i]` 中 `1` 比 `0` 多 `0` 个,则 `0` 和 `1` 数量相同的子数组长度为 `i - j`。 而第二次之所以又出现 `pre_diff == 2` ,是因为前半段子数组 `nums[0: j]` 贡献了相同的差值。 接下来还有一个小问题,如何计算「`1` 和 `0` 数量差」? 我们可以把数组中的 `1` 记为贡献 `+1`,`0` 记为贡献 `-1`。然后使用一个变量 `count`,只要出现 `1` 就让 `count` 加上 `1`,意思是又多出了 `1` 个 `1`。只要出现 `0`,将让 `count` 减去 `1`,意思是 `0` 和之前累积的 `1` 个 `1` 相互抵消掉了。这样遍历完数组,也就计算出了对应的「`1` 和 `0` 数量差」。 整个思路的具体做法如下: - 创建一个哈希表,键值对关系为「`1` 和 `0` 的数量差:最早出现的下标 `i`」。 - 使用变量 `pre_diff` 来计算「`1` 和 `0` 数量差」,使用变量 `count` 来记录 `0` 和 `1` 数量相同的连续子数组的最长长度,然后遍历整个数组。 - 如果 `nums[i] == 1`,则让 `pre_diff += 1`;如果 `nums[i] == 0`,则让 `pre_diff -= 1`。 - 如果在哈希表中发现了相同的 `pre_diff`,则计算相应的子数组长度,与 `count` 进行比较并更新 `count` 值。 - 如果在哈希表中没有发现相同的 `pre_diff`,则在哈希表中记录下第一次出现 `pre_diff` 的下标 `i`。 - 最后遍历完输出 `count`。 > 注意:初始化哈希表为:`pre_dic = {0: -1}`,意思为空数组时,默认「`1` 和 `0` 数量差」为 `0`,且第一次出现的下标为 `-1`。 > > 之所以这样做,是因为在遍历过程中可能会直接出现 `pre_diff == 0` 的情况,这种情况下说明 `nums[0: i]` 中 `0` 和 `1` 数量相同,如果像上边这样初始化后,就可以直接计算出此时子数组长度为 `i - (-1) = i + 1`。 ## 代码 ```python class Solution: def findMaxLength(self, nums: List[int]) -> int: pre_dic = {0: -1} count = 0 pre_sum = 0 for i in range(len(nums)): if nums[i]: pre_sum += 1 else: pre_sum -= 1 if pre_sum in pre_dic: count = max(count, i - pre_dic[pre_sum]) else: pre_dic[pre_sum] = i return count ``` ================================================ FILE: docs/solutions/LCR/D0F0SV.md ================================================ # [LCR 104. 组合总和 Ⅳ](https://leetcode.cn/problems/D0F0SV/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [LCR 104. 组合总和 Ⅳ - 力扣](https://leetcode.cn/problems/D0F0SV/) ## 题目大意 给定一个由不同整数组成的数组 `nums` 和一个目标整数 `target`。 要求:从 `nums` 中找出并返回总和为 `target` 的元素组合个数。 ## 解题思路 完全背包问题。题目求解的是组合数。 动态规划的状态 `dp[i]` 可以表示为:凑成总和 `i` 的组合数。 动态规划的状态转移方程为:`dp[i] = dp[i] + dp[i - nums[j]]`,意思为凑成总和为 `i` 的组合数 = 「不使用当前 `nums[j]`,只使用之前整数凑成和为 `i` 的组合数」+「使用当前 `nums[j]` 凑成金额 `i - nums[j]` 的方案数」。 最终输出 `dp[target]`。 ## 代码 ```python class Solution: def combinationSum4(self, nums: List[int], target: int) -> int: dp = [0 for _ in range(target + 1)] dp[0] = 1 size = len(nums) for i in range(target + 1): for j in range(size): if i - nums[j] >= 0: dp[i] += dp[i - nums[j]] return dp[target] ``` ================================================ FILE: docs/solutions/LCR/FortPu.md ================================================ # [LCR 030. O(1) 时间插入、删除和获取随机元素](https://leetcode.cn/problems/FortPu/) - 标签:设计、数组、哈希表、数学、随机化 - 难度:中等 ## 题目链接 - [LCR 030. O(1) 时间插入、删除和获取随机元素 - 力扣](https://leetcode.cn/problems/FortPu/) ## 题目大意 设计一个数据结构 ,支持时间复杂度为 $O(1)$ 的以下操作: - `insert(val)`:当元素 val 不存在时,向集合中插入该项。 - `remove(val)`:元素 val 存在时,从集合中移除该项。 - `getRandom`:随机返回现有集合中的一项。每个元素应该有相同的概率被返回。 ## 解题思路 普通动态数组进行访问操作,需要线性时间查找解决。我们可以利用哈希表记录下每个元素的下标,这样在访问时可以做到常数时间内访问元素了。对应的插入、删除、后去随机元素需要做相应的变化。 - 插入操作:将元素直接插入到数组尾部,并用哈希表记录插入元素的下标位置。 - 删除操作:使用哈希表找到待删除元素所在位置,将其与数组末尾位置元素相互交换,更新哈希表中交换后元素的下标值,并将末尾元素删除。 - 获取随机元素:使用` random.choice` 获取。 ## 代码 ```python import random class RandomizedSet: def __init__(self): """ Initialize your data structure here. """ self.dict = dict() self.list = list() def insert(self, val: int) -> bool: """ Inserts a value to the set. Returns true if the set did not already contain the specified element. """ if val in self.dict: return False self.dict[val] = len(self.list) self.list.append(val) return True def remove(self, val: int) -> bool: """ Removes a value from the set. Returns true if the set contained the specified element. """ if val in self.dict: idx = self.dict[val] last = self.list[-1] self.list[idx] = last self.dict[last] = idx self.list.pop() self.dict.pop(val) return True return False def getRandom(self) -> int: """ Get a random element from the set. """ return random.choice(self.list) ``` ================================================ FILE: docs/solutions/LCR/Gu0c2T.md ================================================ # [LCR 089. 打家劫舍](https://leetcode.cn/problems/Gu0c2T/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [LCR 089. 打家劫舍 - 力扣](https://leetcode.cn/problems/Gu0c2T/) ## 题目大意 给定一个数组 `nums`,`num[i]` 代表第 `i` 间房屋存放的金额。相邻的房屋装有防盗系统,假如相邻的两间房屋同时被偷,系统就会报警。假如你是一名专业的小偷。 要求:计算在不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。 ## 解题思路 可以用动态规划来解决问题,关键点在于找到状态转移方程。 先考虑最简单的情况。假如只有一间房,则直接偷这间屋子就能偷到最高金额,即 `dp[0] = nums[i]`。假如只有两间房屋,那么就选择金额最大的那间屋进行偷窃,就可以偷到最高金额,即 `dp[1] = max(nums[0], nums[1])`。 如果房屋大于两间,则偷窃第 `i` 间房屋的时候,就有两种状态: - 偷窃第 `i` 间房屋,那么第 `i - 1` 间房屋就不能偷窃了,偷窃的最高金额为:前 `i - 2` 间房屋的最高总金额 + 第 `i` 间房屋的金额,即 `dp[i] = dp[i-2] + nums[i]`; - 不偷窃第 `i` 间房屋,那么第 `i - 1` 间房屋可以偷窃,偷窃的最高金额为:前 `i - 1` 间房屋的最高总金额,即 `dp[i] = dp[i-1]`。 然后这两种状态取最大值即可,即 `dp[i] = max(dp[i-2] + nums[i], dp[i-1])`。 总结下就是: $dp[i] = \begin{cases} \begin{array} {**lr**} nums[0] & i = 0 \cr max( nums[0], nums[1]) & i = 1 \cr max( dp[i-2] + nums[i], dp[i-1]) & i \ge 2 \end{array} \end{cases}$ ## 代码 ```python class Solution: def rob(self, nums: List[int]) -> int: size = len(nums) dp = [0 for _ in range(size)] for i in range(size): if i == 0: dp[i] = nums[i] elif i == 1: dp[i] = max(nums[i - 1], nums[i]) else: dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]) return dp[size - 1] ``` ================================================ FILE: docs/solutions/LCR/GzCJIP.md ================================================ # [LCR 088. 使用最小花费爬楼梯](https://leetcode.cn/problems/GzCJIP/) - 标签:数组、动态规划 - 难度:简单 ## 题目链接 - [LCR 088. 使用最小花费爬楼梯 - 力扣](https://leetcode.cn/problems/GzCJIP/) ## 题目大意 给定一个数组 `cost` 代表一段楼梯,`cost[i]` 代表爬上第 `i` 阶楼梯醒酒药花费的体力值(下标从 `0` 开始)。 每爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。 要求:找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 `0` 或 `1` 的元素作为初始阶梯。 ## 解题思路 使用动态规划方法。 状态 `dp[i]` 表示为:到达第 `i` 个台阶所花费的最少体⼒。 则状态转移方程为: `dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]`。 表示为:到达第 `i` 个台阶所花费的最少体⼒ = 到达第 `i - 1` 个台阶所花费的最小体力 与 到达第 `i - 2` 个台阶所花费的最小体力中的最小值 + 到达第 `i` 个台阶所需要花费的体力值。 ## 代码 ```python class Solution: def minCostClimbingStairs(self, cost: List[int]) -> int: size = len(cost) dp = [0 for _ in range(size + 1)] for i in range(2, size + 1): dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]) return dp[size] ``` ================================================ FILE: docs/solutions/LCR/H8086Q.md ================================================ # [LCR 042. 最近的请求次数](https://leetcode.cn/problems/H8086Q/) - 标签:设计、队列、数据流 - 难度:简单 ## 题目链接 - [LCR 042. 最近的请求次数 - 力扣](https://leetcode.cn/problems/H8086Q/) ## 题目大意 要求:实现一个用来计算特定时间范围内的最近请求的 `RecentCounter` 类: - `RecentCounter()` 初始化计数器,请求数为 0 。 - `int ping(int t)` 在时间 `t` 时添加一个新请求,其中 `t` 表示以毫秒为单位的某个时间,并返回在 `[t-3000, t]` 内发生的请求数。 ## 解题思路 使用一个队列,用于存储 `[t - 3000, t]` 范围内的请求。 获取请求数时,将队首所有小于 `t - 3000` 时间的请求将其从队列中移除,然后返回队列的长度即可。 ## 代码 ```python class RecentCounter: def __init__(self): self.queue = [] def ping(self, t: int) -> int: self.queue.append(t) while self.queue[0] < t - 3000: self.queue.pop(0) return len(self.queue) ``` ================================================ FILE: docs/solutions/LCR/IDBivT.md ================================================ # [LCR 085. 括号生成](https://leetcode.cn/problems/IDBivT/) - 标签:字符串、动态规划、回溯 - 难度:中等 ## 题目链接 - [LCR 085. 括号生成 - 力扣](https://leetcode.cn/problems/IDBivT/) ## 题目大意 给定一个整数 `n`。 要求:生成所有有可能且有效的括号组合。 ## 解题思路 通过回溯算法生成所有答案。为了生成的括号组合是有效的,回溯的时候,使用一个标记变量 `symbol` 来表示是否当前组合是否成对匹配。 如果在当前组合中增加一个 `(`,则 `symbol += 1`,如果增加一个 `)`,则 `symbol -= 1`。显然只有在 `symbol < n` 的时候,才能增加 `(`,在 `symbol > 0` 的时候,才能增加 `)`。 如果最终生成 `2 * n` 的括号组合,并且 `symbol == 0`,则说明当前组合是有效的,将其加入到最终答案数组中。 最终输出最终答案数组。 ## 代码 ```python class Solution: def generateParenthesis(self, n: int) -> List[str]: def backtrack(parenthesis, symbol, index): if n * 2 == index: if symbol == 0: parentheses.append(parenthesis) else: if symbol < n: backtrack(parenthesis + '(', symbol + 1, index + 1) if symbol > 0: backtrack(parenthesis + ')', symbol - 1, index + 1) parentheses = list() backtrack("", 0, 0) return parentheses ``` ================================================ FILE: docs/solutions/LCR/JFETK5.md ================================================ # [LCR 002. 二进制求和](https://leetcode.cn/problems/JFETK5/) - 标签:位运算、数学、字符串、模拟 - 难度:简单 ## 题目链接 - [LCR 002. 二进制求和 - 力扣](https://leetcode.cn/problems/JFETK5/) ## 题目大意 给定两个二进制数的字符串 `a`、`b`。 要求:计算 `a` 和 `b` 的和,返回结果也用二进制表示。 ## 解题思路 这道题可以直接将 `a`、`b` 转换为十进制数,相加后再转换为二进制数。 也可以利用位运算的一些知识,直接求和。 因为 `a`、`b` 为二进制的字符串,先将其转换为二进制数。 本题用到的位运算知识: - 异或运算 `x ^ y` :可以获得 `x + y` 无进位的加法结果。 - 与运算 `x & y`:对应位置为 `1`,说明 `x`、`y` 该位置上原来都为 `1`,则需要进位。 - 座椅运算 `x << 1`:将 a 对应二进制数左移 `1` 位。 这样,通过 `x ^ y` 运算,我们可以得到相加后无进位结果,再根据 `(x & y) << 1`,计算进位后结果。 进行 `x ^ y` 和 `(x & y) << 1`操作之后判断进位是否为 `0`,如果不为 `0`,则继续上一步操作,直到进位为 `0`。 最后将其结果转为 `2` 进制返回。 ## 代码 ```python class Solution: def addBinary(self, a: str, b: str) -> str: x = int(a, 2) y = int(b, 2) while y: carry = ((x & y) << 1) x ^= y y = carry return bin(x)[2:] ``` ================================================ FILE: docs/solutions/LCR/LGjMqU.md ================================================ # [LCR 026. 重排链表](https://leetcode.cn/problems/LGjMqU/) - 标签:栈、递归、链表、双指针 - 难度:中等 ## 题目链接 - [LCR 026. 重排链表 - 力扣](https://leetcode.cn/problems/LGjMqU/) ## 题目大意 给定一个单链表 `L` 的头节点 `head`,单链表 `L` 表示为:$L_0$ -> $L_1$ -> $L_2$ -> ... -> $L_{n-1}$ -> $L_n$。 要求:将单链表 `L` 重新排列为:$L_0$ -> $L_n$ -> $L_1$ -> $L_{n-1}$ -> $L_2$ -> $L_{n-2}$ -> $L_3$ -> $L_{n-3}$ -> ...。 注意:需要将实际节点进行交换。 ## 解题思路 链表不能像数组那样直接进行随机访问。所以我们可以先将链表转为线性表。然后直接按照提要要求的排列顺序访问对应数据元素,重新建立链表。 ## 代码 ```python class Solution: def reorderList(self, head: ListNode) -> None: """ Do not return anything, modify head in-place instead. """ if not head: return vec = [] node = head while node: vec.append(node) node = node.next left, right = 0, len(vec) - 1 while left < right: vec[left].next = vec[right] left += 1 if left == right: break vec[right].next = vec[left] right -= 1 vec[left].next = None ``` ================================================ FILE: docs/solutions/LCR/LwUNpT.md ================================================ # [LCR 045. 找树左下角的值](https://leetcode.cn/problems/LwUNpT/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [LCR 045. 找树左下角的值 - 力扣](https://leetcode.cn/problems/LwUNpT/) ## 题目大意 给定一个二叉树的根节点 `root`。 要求:找出该二叉树 「最底层」的「最左边」节点的值。 ## 解题思路 这个问题拆开来看,一是如何找到「最底层」,而是在「最底层」如何找到最左边的节点。 通过层序遍历,我们可以直接确定最底层节点。而「最底层」的「最左边」节点可以改变层序遍历的左右节点访问顺序。 每层元素先访问右节点,在访问左节点,则最后一个遍历的元素就是「最底层」的「最左边」节点,即左下角的节点,返回该点对应的值即可。 ## 代码 ```python import collections class Solution: def findBottomLeftValue(self, root: TreeNode) -> int: if not root: return -1 queue = collections.deque() queue.append(root) while queue: cur = queue.popleft() if cur.right: queue.append(cur.right) if cur.left: queue.append(cur.left) return cur.val ``` ================================================ FILE: docs/solutions/LCR/M1oyTv.md ================================================ # [LCR 017. 最小覆盖子串](https://leetcode.cn/problems/M1oyTv/) - 标签:哈希表、字符串、滑动窗口 - 难度:困难 ## 题目链接 - [LCR 017. 最小覆盖子串 - 力扣](https://leetcode.cn/problems/M1oyTv/) ## 题目大意 给定一个字符串 `s`、一个字符串 `t`。 要求:返回 `s` 中涵盖 `t` 所有字符的最小子串。如果 `s` 中不存在涵盖 `t` 所有字符的子串,则返回空字符串 `""`。如果存在多个符合条件的子字符串,返回任意一个。 ## 解题思路 使用滑动窗口求解。 `left`、`right` 表示窗口的边界,一开始都位于下标 `0` 处。`need` 用于记录短字符串需要的字符数。`window` 记录当前窗口内的字符数。 将 `right` 右移,直到出现了 `t` 中全部字符,开始右移 `left`,减少滑动窗口的大小,并记录下最小覆盖子串的长度和起始位置。最后输出结果。 ## 代码 ```python class Solution: def minWindow(self, s: str, t: str) -> str: need = collections.defaultdict(int) window = collections.defaultdict(int) for ch in t: need[ch] += 1 left, right = 0, 0 valid = 0 start = 0 size = len(s) + 1 while right < len(s): insert_ch = s[right] right += 1 if insert_ch in need: window[insert_ch] += 1 if window[insert_ch] == need[insert_ch]: valid += 1 while valid == len(need): if right - left < size: start = left size = right - left remove_ch = s[left] left += 1 if remove_ch in need: if window[remove_ch] == need[remove_ch]: valid -= 1 window[remove_ch] -= 1 if size == len(s) + 1: return '' return s[start:start + size] ``` ================================================ FILE: docs/solutions/LCR/M99OJA.md ================================================ # [LCR 086. 分割回文串](https://leetcode.cn/problems/M99OJA/) - 标签:深度优先搜索、广度优先搜索、图、哈希表 - 难度:中等 ## 题目链接 - [LCR 086. 分割回文串 - 力扣](https://leetcode.cn/problems/M99OJA/) ## 题目大意 给定一个字符串 `s`将 `s` 分割成一些子串,保证每个子串都是「回文串」。 要求:返回 `s` 所有可能的分割方案。 ## 解题思路 回溯算法,建立两个数组 `res`、`path`。`res` 用于存放所有满足题意的组合,`path` 用于存放当前满足题意的一个组合。 在回溯的时候判断当前子串是否为回文串,如果不是则跳过,如果是则继续向下一层遍历。 定义判断是否为回文串的方法和回溯方法,从 `start_index = 0` 的位置开始回溯。 - 如果 `start_index >= len(s)`,则将 `path` 中的元素加入到 `res` 数组中。 - 然后对 `[start_index, len(s) - 1]` 范围内的子串进行遍历取值。 - 如果字符串 `s` 在范围 `[start_index, i]` 所代表的子串是回文串,则将其加入 `path` 数组。 - 递归遍历 `[i + 1, len(s) - 1]` 范围上的子串。 - 然后将遍历的范围 `[start_index, i]` 所代表的子串进行回退。 - 最终返回 `res` 数组。 ## 代码 ```python class Solution: res = [] path = [] def backtrack(self, s: str, start_index: int): if start_index >= len(s): self.res.append(self.path[:]) return for i in range(start_index, len(s)): if self.ispalindrome(s, start_index, i): self.path.append(s[start_index: i + 1]) self.backtrack(s, i + 1) self.path.pop() def ispalindrome(self, s: str, start: int, end: int): i, j = start, end while i < j: if s[i] != s[j]: return False i += 1 j -= 1 return True def partition(self, s: str) -> List[List[str]]: self.res.clear() self.path.clear() self.backtrack(s, 0) return self.res ``` ================================================ FILE: docs/solutions/LCR/N6YdxV.md ================================================ # [LCR 068. 搜索插入位置](https://leetcode.cn/problems/N6YdxV/) - 标签:数组、二分查找 - 难度:简单 ## 题目链接 - [LCR 068. 搜索插入位置 - 力扣](https://leetcode.cn/problems/N6YdxV/) ## 题目大意 给定一个排好序的数组 `nums`,以及一个目标值 `target`。 要求:在数组中找到目标值,并返回下标。如果找不到,则返回目标值按顺序插入数组的位置。 ## 解题思路 二分查找法。利用两个指针 `left` 和 `right`,分别指向数组首尾位置。每次用 `left` 和 `right` 中间位置上的元素值与目标值做比较,如果等于目标值,则返回当前位置。如果小于目标值,则更新 `left` 位置为 `mid + 1`,继续查找。如果大于目标值,则更新 `right` 位置为 `mid - 1`,继续查找。直到查找到目标值,或者 `left > right` 值时停止查找。然后返回 `left` 所在位置,即是代插入数组的位置。 ## 代码 ```python class Solution: def searchInsert(self, nums: List[int], target: int) -> int: n = len(nums) left = 0 right = n - 1 while left <= right: mid = left + (right - left) // 2 if nums[mid] == target: return mid elif nums[mid] < target: left = mid + 1 else: right = mid - 1 return left ``` ================================================ FILE: docs/solutions/LCR/NUPfPr.md ================================================ # [LCR 101. 分割等和子集](https://leetcode.cn/problems/NUPfPr/) - 标签:数学、字符串、模拟 - 难度:简单 ## 题目链接 - [LCR 101. 分割等和子集 - 力扣](https://leetcode.cn/problems/NUPfPr/) ## 题目大意 给定一个只包含正整数的非空数组 `nums`。 要求:判断是否可以将这个数组分成两个子集,使得两个子集的元素和相等。 ## 解题思路 动态规划求解。 如果两个子集和相等,则两个子集元素和刚好等于整个数组元素和的一半。这就相当于 `0-1` 背包问题。 定义 `dp[i][j]` 表示从 `[0, i]` 个数中任意选取一些数,放进容量为 j 的背包中,价值总和最大为多少。则 `dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i])`。 转换为一维 dp 就是:`dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])`。 然后进行递归求解。最后判断 `dp[target]` 和 `target` 是否相等即可。 ## 代码 ```python class Solution: def canPartition(self, nums: List[int]) -> bool: size = 100010 dp = [0 for _ in range(size)] sum_nums = sum(nums) if sum_nums & 1: return False target = sum_nums // 2 for i in range(len(nums)): for j in range(target, nums[i] - 1, -1): dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]) if dp[target] == target: return True return False ``` ================================================ FILE: docs/solutions/LCR/NYBBNL.md ================================================ # [LCR 052. 递增顺序搜索树](https://leetcode.cn/problems/NYBBNL/) - 标签:栈、树、深度优先搜索、二叉搜索树、二叉树 - 难度:简单 ## 题目链接 - [LCR 052. 递增顺序搜索树 - 力扣](https://leetcode.cn/problems/NYBBNL/) ## 题目大意 给定一棵二叉搜索树的根节点 `root`。 要求:按中序遍历顺序将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。 ## 解题思路 可以分为两步: 1. 中序遍历二叉搜索树,将节点先存储到列表中。 2. 将列表中的节点构造成一棵递增顺序搜索树。 中序遍历直接按照 `左 -> 根 -> 右` 的顺序递归遍历,然后将遍历的节点存储到 `res` 中。 构造递增顺序搜索树,则用 `head` 保存头节点位置。遍历列表中的每个节点,将其左右指针先置空,再将其连接在上一个节点的右子节点上。 最后返回 `head.right` 即可。 ## 代码 ```python class Solution: def inOrder(self, root, res): if not root: return self.inOrder(root.left, res) res.append(root) self.inOrder(root.right, res) def increasingBST(self, root: TreeNode) -> TreeNode: res = [] self.inOrder(root, res) if not res: return head = TreeNode(-1) cur = head for node in res: node.left = node.right = None cur.right = node cur = cur.right return head.right ``` ================================================ FILE: docs/solutions/LCR/NaqhDT.md ================================================ # [LCR 043. 完全二叉树插入器](https://leetcode.cn/problems/NaqhDT/) - 标签:树、广度优先搜索、设计、二叉树 - 难度:中等 ## 题目链接 - [LCR 043. 完全二叉树插入器 - 力扣](https://leetcode.cn/problems/NaqhDT/) ## 题目大意 要求:设计一个用完全二叉树初始化的数据结构 `CBTInserter`,并支持以下几种操作: - `CBTInserter(TreeNode root)` 使用根节点为 `root` 的给定树初始化该数据结构; - `CBTInserter.insert(int v)` 向树中插入一个新节点,节点类型为 `TreeNode`,值为 `v`。使树保持完全二叉树的状态,并返回插入的新节点的父节点的值; - `CBTInserter.get_root()` 返回树的根节点。 ## 解题思路 使用数组标记完全二叉树中节点的序号,初始化数组为 `[None]`。完全二叉树中节点的序号从 `1` 开始,对于序号为 `k` 的节点,其左子节点序号为 `2k`,右子节点的序号为 `2k + 1`,其父节点的序号为 `k // 2`。 然后在初始化和插入节点的同时,按顺序向数组中插入节点。 ## 代码 ```python class CBTInserter: def __init__(self, root: TreeNode): self.queue = [root] self.nodelist = [None] while self.queue: node = self.queue.pop(0) self.nodelist.append(node) if node.left: self.queue.append(node.left) if node.right: self.queue.append(node.right) def insert(self, v: int) -> int: self.nodelist.append(TreeNode(v)) index = len(self.nodelist) - 1 father = self.nodelist[index // 2] if index % 2 == 0: father.left = self.nodelist[-1] else: father.right = self.nodelist[-1] return father.val def get_root(self) -> TreeNode: return self.nodelist[1] ``` ================================================ FILE: docs/solutions/LCR/O4NDxx.md ================================================ # [LCR 013. 二维区域和检索 - 矩阵不可变](https://leetcode.cn/problems/O4NDxx/) - 标签:设计、数组、矩阵、前缀和 - 难度:中等 ## 题目链接 - [LCR 013. 二维区域和检索 - 矩阵不可变 - 力扣](https://leetcode.cn/problems/O4NDxx/) ## 题目大意 给定一个二维矩阵 `matrix`。 要求:满足以下多个请求: - ` def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:`计算以 `(row1, col1)` 为左上角、`(row2, col2)` 为右下角的子矩阵中各个元素的和。 - `def __init__(self, matrix: List[List[int]]):` 对二维矩阵 `matrix` 进行初始化操作。 ## 解题思路 在进行初始化的时候做预处理,这样在多次查询时可以减少重复计算,也可以减少时间复杂度。 在进行初始化的时候,使用一个二维数组 `pre_sum` 记录下以 `(0, 0)` 为左上角,以当前 `(row, col)` 为右下角的子数组各个元素和,即 `pre_sum[row + 1][col + 1]`。 则在查询时,以 `(row1, col1)` 为左上角、`(row2, col2)` 为右下角的子矩阵中各个元素的和就等于以 `(0, 0)` 到 `(row2, col2)` 的大子矩阵减去左边 `(0, 0)` 到 `(row2, col1 - 1)`的子矩阵,再减去上边 `(0, 0)` 到 `(row1 - 1, col2)` 的子矩阵,再加上左上角 `(0, 0)` 到 `(row1 - 1, col1 - 1)` 的子矩阵(因为之前重复减了)。即 `pre_sum[row2 + 1][col2 + 1] - self.pre_sum[row2 + 1][col1] - self.pre_sum[row1][col2 + 1] + self.pre_sum[row1][col1]`。 ## 代码 ```python class NumMatrix: def __init__(self, matrix: List[List[int]]): rows = len(matrix) cols = len(matrix[0]) self.pre_sum = [[0 for _ in range(cols + 1)] for _ in range(rows + 1)] for row in range(rows): for col in range(cols): self.pre_sum[row + 1][col + 1] = self.pre_sum[row + 1][col] + self.pre_sum[row][col + 1] - self.pre_sum[row][col] + matrix[row][col] def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int: return self.pre_sum[row2 + 1][col2 + 1] - self.pre_sum[row2 + 1][col1] - self.pre_sum[row1][col2 + 1] + self.pre_sum[row1][col1] ``` ================================================ FILE: docs/solutions/LCR/OrIXps.md ================================================ # [LCR 031. LRU 缓存](https://leetcode.cn/problems/OrIXps/) - 标签:设计、哈希表、链表、双向链表 - 难度:中等 ## 题目链接 - [LCR 031. LRU 缓存 - 力扣](https://leetcode.cn/problems/OrIXps/) ## 题目大意 要求:实现一个 `LRU(最近最少使用)缓存机制`,并且在 `O(1)` 时间复杂度内完成 `get`、`put` 操作。 实现 `LRUCache` 类: - `LRUCache(int capacity)` 以正整数作为容量 `capacity` 初始化 LRU 缓存。 - `int get(int key)` 如果关键字 `key` 存在于缓存中,则返回关键字的值,否则返回 `-1`。 - `void put(int key, int value)` 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。 ## 解题思路 LRU(最近最少使用缓存)是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。LRU 更新和插入新页面都发生在链表首,删除页面都发生在链表尾。 ## 代码 ```python class Node: def __init__(self, key=None, val=None, prev=None, next=None): self.key = key self.val = val self.prev = prev self.next = next class LRUCache: def __init__(self, capacity: int): self.capacity = capacity self.hashmap = dict() self.head = Node() self.tail = Node() self.head.next = self.tail self.tail.prev = self.head def get(self, key: int) -> int: if key not in self.hashmap: return -1 node = self.hashmap[key] self.move_node(node) return node.val def put(self, key: int, value: int) -> None: if key in self.hashmap: node = self.hashmap[key] node.val = value self.move_node(node) return if len(self.hashmap) == self.capacity: self.hashmap.pop(self.head.next.key) self.remove_node(self.head.next) node = Node(key=key, val=value) self.hashmap[key] = node self.add_node(node) def remove_node(self, node): node.prev.next = node.next node.next.prev = node.prev def add_node(self, node): self.tail.prev.next = node node.prev = self.tail.prev node.next = self.tail self.tail.prev = node def move_node(self, node): self.remove_node(node) self.add_node(node) ``` ================================================ FILE: docs/solutions/LCR/P5rCT8.md ================================================ # [LCR 053. 二叉搜索树中的中序后继](https://leetcode.cn/problems/P5rCT8/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [LCR 053. 二叉搜索树中的中序后继 - 力扣](https://leetcode.cn/problems/P5rCT8/) ## 题目大意 给定一棵二叉搜索树的根节点 `root` 和其中一个节点 `p`。 要求:找到该节点在树中的中序后继,即按照中序遍历的顺序节点 `p` 的下一个节点。 ## 解题思路 递归遍历,具体步骤如下: - 如果 `root.val` 小于等于 `p.val`,则直接从 `root` 的右子树递归查找比 `p.val` 大的节点,从而找到中序后继。 - 如果 `root.val` 大于 `p.val`,则 `root` 有可能是中序后继,也有可能是 `root` 的左子树。则从 `root` 的左子树递归查找更接近(更小的)。如果查找的值为 `None`,则当前 `root` 就是中序后继,否则继续递归查找,从而找到中序后继。 ## 代码 ```python class Solution: def inorderSuccessor(self, root: 'TreeNode', p: 'TreeNode') -> 'TreeNode': if not p or not root: return None if root.val <= p.val: node = self.inorderSuccessor(root.right, p) else: node = self.inorderSuccessor(root.left, p) if not node: node = root return node ``` ================================================ FILE: docs/solutions/LCR/PzWKhm.md ================================================ # [LCR 090. 打家劫舍 II](https://leetcode.cn/problems/PzWKhm/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [LCR 090. 打家劫舍 II - 力扣](https://leetcode.cn/problems/PzWKhm/) ## 题目大意 给定一个数组 `nums`,`num[i]` 代表第 `i` 间房屋存放的金额,假设房屋可以围成一圈,首尾相连。相邻的房屋装有防盗系统,假如相邻的两间房屋同时被偷,系统就会报警。假如你是一名专业的小偷。 要求:计算在不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。 ## 解题思路 「[LCR 089. 打家劫舍](https://leetcode.cn/problems/Gu0c2T/)」的升级版。可以用动态规划来解决问题,关键点在于找到状态转移方程。 先来考虑最简单的情况。 假如只有一间房屋,则直接偷这间房屋就能偷到最高金额,即 $dp[0] = nums[i]$。假如有两间房屋,那么就选择金额最大的那间房屋进行偷窃,就可以偷到最高金额,即 $dp[1] = max(nums[0], nums[1])$。 两间屋子以下,最多只能偷窃一间房屋,则不用考虑首尾相连的情况。如果三个屋子以上,偷窃了第一间房屋,则不能偷窃最后一间房屋。同样偷窃了最后一间房屋则不能偷窃第一间房屋。 假设总共房屋数量为 N,这种情况可以转换为分别求解 $[0, N - 2]$ 和 $[1, N - 1]$ 范围下首尾不相连的房屋所能偷窃的最高金额,这就变成了「[LCR 089. 打家劫舍](https://leetcode.cn/problems/Gu0c2T/)」的求解问题。 「[LCR 089. 打家劫舍](https://leetcode.cn/problems/Gu0c2T/)」求解思路如下: 如果房屋大于两间,则偷窃第 `i` 间房屋的时候,就有两种状态: - 偷窃第 `i` 间房屋,那么第 `i - 1` 间房屋就不能偷窃了,偷窃的最高金额为:前 `i - 2` 间房屋的最高总金额 + 第 `i` 间房屋的金额,即 $dp[i] = dp[i-2] + nums[i]$; - 不偷窃第 `i` 间房屋,那么第 `i - 1` 间房屋可以偷窃,偷窃的最高金额为:前 `i - 1` 间房屋的最高总金额,即 $dp[i] = dp[i-1]$。 然后这两种状态取最大值即可,即 $dp[i] = max( dp[i-2] + nums[i], dp[i-1])$。 总结下就是: $dp[i] = \begin{cases} nums[0], & i = 0 \cr max( nums[0], nums[1]) & i = 1 \cr max( dp[i-2] + nums[i], dp[i-1]) & i \ge 2 \end{cases}$ ## 代码 ```python class Solution: def rob(self, nums: List[int]) -> int: def helper(nums): size = len(nums) if size == 1: return nums[0] dp = [0 for _ in range(size)] for i in range(size): if i == 0: dp[i] = nums[0] elif i == 1: dp[i] = max(nums[i - 1], nums[i]) else: dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]) return dp[-1] if len(nums) == 1: return nums[0] else: return max(helper(nums[1:]), helper(nums[:-1])) ``` ================================================ FILE: docs/solutions/LCR/Q91FMA.md ================================================ # [LCR 093. 最长的斐波那契子序列的长度](https://leetcode.cn/problems/Q91FMA/) - 标签:数组、哈希表、动态规划 - 难度:中等 ## 题目链接 - [LCR 093. 最长的斐波那契子序列的长度 - 力扣](https://leetcode.cn/problems/Q91FMA/) ## 题目大意 给定一个严格递增的正整数数组 `arr`。 要求:从 `arr` 中找出最长的斐波那契式的子序列的长度。如果不存斐波那契式的子序列,则返回 `0`。 - 斐波那契式序列:如果序列 $X_1, X_2, ..., X_n$ 满足: - $n \ge 3$; - 对于所有 $i + 2 \le n$,都有 $X_i + X_{i+1} = X_{i+2}$。 则称该序列为斐波那契式序列。 - 斐波那契式子序列:从序列 `arr` 中挑选若干元素组成子序列,并且子序列满足斐波那契式序列,则称该序列为斐波那契式子序列。例如:`arr = [3, 4, 5, 6, 7, 8]`。则 `[3, 5, 8]` 是 `arr` 的一个斐波那契式子序列。 ## 解题思路 我们先从最简单的暴力做法思考。 **1. 暴力做法:** 我们先来考虑暴力做法怎么做。 假设 `arr[i]`、`arr[j]`、`arr[k]` 是序列 `arr` 中的 3 个元素,且满足关系:`arr[i] + arr[j] == arr[k]`,则 `arr[i]`、`arr[j]`、`arr[k]` 就构成了 A 的一个斐波那契式子序列。 通过 `arr[i]`、`arr[j]`,我们可以确定下一个斐波那契式子序列元素的值为 `arr[i] + arr[j]`。 因为给定的数组是严格递增的,所以对于一个斐波那契式子序列,如果确定了 `arr[i]`、`arr[j]`,则可以顺着 `arr` 序列,从第 `j + 1` 的元素开始,查找值为 `arr[i] + arr[j]` 的元素 。找到 `arr[i] + arr[j]` 之后,然后在顺着查找子序列的下一个元素。 简单来说,就是确定了 `arr[i]`、`arr[j]`,就能尽可能的得到一个长的斐波那契式子序列,此时我们记录下子序列长度。然后对于不同的 `arr[i]`、`arr[j]`,统计不同的斐波那契式子序列的长度。将这些长度进行比较,其中最长的长度就是答案。 下面是暴力做法的代码: ```python class Solution: def lenLongestFibSubseq(self, arr: List[int]) -> int: size = len(arr) ans = 0 for i in range(size): for j in range(i + 1, size): temp_ans = 0 temp_i = i temp_j = j k = j + 1 while k < size: if arr[temp_i] + arr[temp_j] == arr[k]: temp_ans += 1 temp_i = temp_j temp_j = k k += 1 if temp_ans > ans: ans = temp_ans if ans > 0: return ans + 2 else: return ans ``` 毫无意外的,超出时间限制了。 那么我们怎么来优化呢? **2. 使用哈希表优化做法:** 我们注意到:对于 `arr[i]`、`arr[j]`,要查找的元素 `arr[i] + arr[j]` 是否在 `arr` 中,我们可以预先建立一个反向的哈希表。键值对关系为 `value : idx`,这样就能在 `O(1)` 的时间复杂度通过 `arr[i] + arr[j]` 的值查找到对应的 `k` 值,而不用像原先一样线性查找 `arr[k]` 了。 使用哈希表优化之后的代码如下: ```python class Solution: def lenLongestFibSubseq(self, arr: List[int]) -> int: size = len(arr) ans = 0 idx_map = dict() for idx, value in enumerate(arr): idx_map[value] = idx for i in range(size): for j in range(i + 1, size): temp_ans = 0 temp_i = i temp_j = j while arr[temp_i] + arr[temp_j] in idx_map: temp_ans += 1 k = idx_map[arr[temp_i] + arr[temp_j]] temp_i = temp_j temp_j = k if temp_ans > ans: ans = temp_ans if ans > 0: return ans + 2 else: return ans ``` 再次提交,通过了。 但是,这道题我们还可以用动态规划来做。 **3. 动态规划做法:** 这道题用动态规划来做,难点在于如何「定义状态」和「定义状态转移方程」。 - 定义状态:`dp[i][j]` 表示以 `arr[i]`、`arr[j]` 为结尾的斐波那契式子序列的最大长度。 - 定义状态转移方程:$dp[j][k] = max_{(arr[i] + arr[j] = arr[k], i < j < k)}(dp[i][j] + 1)$ - 意思为:以 `arr[j]`、`arr[k]` 结尾的斐波那契式子序列的最大长度 = 满足 `arr[i] + arr[j] = arr[k]` 条件下,以 `arr[i]`、`arr[j]` 结尾的斐波那契式子序列的最大长度 + 1。 但是直接这样做其实跟 **1. 暴力解法** 一样仍会超时,所以我们依旧采用哈希表优化的方式来提高效率,降低算法的时间复杂度。 具体代码如下: ## 代码 ```python class Solution: def lenLongestFibSubseq(self, arr: List[int]) -> int: size = len(arr) # 初始化 dp dp = [[0 for _ in range(size)] for _ in range(size)] ans = 0 idx_map = {} # 将 value : idx 映射为哈希表,这样可以快速通过 value 获取到 idx for idx, value in enumerate(arr): idx_map[value] = idx for i in range(size): for j in range(i + 1, size): if arr[i] + arr[j] in idx_map: # 获取 arr[i] + arr[j] 的 idx,即斐波那契式子序列下一项元素 k = idx_map[arr[i] + arr[j]] dp[j][k] = max(dp[j][k], dp[i][j] + 1) ans = max(ans, dp[j][k]) if ans > 0: return ans + 2 else: return ans ``` ================================================ FILE: docs/solutions/LCR/QA2IGt.md ================================================ # [LCR 113. 课程表 II](https://leetcode.cn/problems/QA2IGt/) - 标签:深度优先搜索、广度优先搜索、图、拓扑排序 - 难度:中等 ## 题目链接 - [LCR 113. 课程表 II - 力扣](https://leetcode.cn/problems/QA2IGt/) ## 题目大意 给定一个整数 `numCourses`,代表这学期必须选修的课程数量,课程编号为 `0` 到 `numCourses - 1`。再给定一个数组 `prerequisites` 表示先修课程关系,其中 `prerequisites[i] = [ai, bi]` 表示如果要学习课程 `ai` 则必须要学习课程 `bi`。 要求:返回学完所有课程所安排的学习顺序。如果有多个正确的顺序,只要返回其中一种即可。如果无法完成所有课程,则返回空数组。 ## 解题思路 拓扑排序。这道题是「[0207. 课程表](https://leetcode.cn/problems/course-schedule/)」的升级版,只需要在上一题的基础上增加一个答案数组即可。 1. 使用列表 `edges` 存放课程关系图,并统计每门课程节点的入度,存入入度列表 `indegrees`。 2. 借助队列 `queue`,将所有入度为 `0` 的节点入队。 3. 从队列中选择一个节点,并将其加入到答案数组 `res` 中,再让课程数 -1。 4. 将该顶点以及该顶点为出发点的所有边的另一个节点入度 -1。如果入度 -1 后的节点入度不为 `0`,则将其加入队列 `queue`。 5. 重复 3~4 的步骤,直到队列中没有节点。 6. 最后判断剩余课程数是否为 `0`,如果为 `0`,则返回答案数组 `res`,否则,返回空数组。 ## 代码 ```python class Solution: def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]: indegrees = [0 for _ in range(numCourses)] edges = collections.defaultdict(list) res = [] for x, y in prerequisites: edges[y].append(x) indegrees[x] += 1 queue = collections.deque([]) for i in range(numCourses): if not indegrees[i]: queue.append(i) while queue: y = queue.popleft() res.append(y) numCourses -= 1 for x in edges[y]: indegrees[x] -= 1 if not indegrees[x]: queue.append(x) if not numCourses: return res else: return [] ``` ================================================ FILE: docs/solutions/LCR/QC3q1f.md ================================================ # [LCR 062. 实现 Trie (前缀树)](https://leetcode.cn/problems/QC3q1f/) - 标签:设计、字典树、哈希表、字符串 - 难度:中等 ## 题目链接 - [LCR 062. 实现 Trie (前缀树) - 力扣](https://leetcode.cn/problems/QC3q1f/) ## 题目大意 要求:实现前缀树数据结构的相关类 `Trie` 类。 `Trie` 类: - `Trie()` 初始化前缀树对象。 - `void insert(String word)` 向前缀树中插入字符串 `word`。 - `boolean search(String word)` 如果字符串 `word` 在前缀树中,返回 `True`(即,在检索之前已经插入);否则,返回 `False`。 - `boolean startsWith(String prefix)` 如果之前已经插入的字符串 `word` 的前缀之一为 `prefix`,返回 `True`;否则,返回 `False`。 ## 解题思路 前缀树(字典树)是一棵多叉数,其中每个节点包含指向子节点的指针数组 `children`,以及布尔变量 `isEnd`。`children` 用于存储当前字符节点,一般长度为所含字符种类个数,也可以使用哈希表代替指针数组。`isEnd` 用于判断该节点是否为字符串的结尾。 下面依次讲解插入、查找前缀的具体步骤: 插入字符串: - 从根节点开始插入字符串。对于待插入的字符,有两种情况: - 如果该字符对应的节点存在,则沿着指针移动到子节点,继续处理下一个字符。 - 如果该字符对应的节点不存在,则创建一个新的节点,保存在 `children` 中对应位置上,然后沿着指针移动到子节点,继续处理下一个字符。 - 重复上述步骤,直到最后一个字符,然后将该节点标记为字符串的结尾。 查找前缀: - 从跟姐点开始查找前缀,对于待查找的字符,有两种情况: - 如果该字符对应的节点存在,则沿着指针移动到子节点,继续查找下一个字符。 - 如果该字符对应的节点不存在,则说明字典树中不包含该前缀,直接返回空指针。 - 重复上述步骤,直到最后一个字符搜索完毕,则说明字典树中存在该前缀。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None and cur.isEnd def startsWith(self, prefix: str) -> bool: """ Returns if there is any word in the trie that starts with the given prefix. """ cur = self for ch in prefix: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None ``` ================================================ FILE: docs/solutions/LCR/QTMn0o.md ================================================ # [LCR 010. 和为 K 的子数组](https://leetcode.cn/problems/QTMn0o/) - 标签:数组、哈希表、前缀和 - 难度:中等 ## 题目链接 - [LCR 010. 和为 K 的子数组 - 力扣](https://leetcode.cn/problems/QTMn0o/) ## 题目大意 给定一个整数数组 `nums` 和一个整数 `k`。 要求:找到该数组中和为 `k` 的连续子数组的个数。 ## 解题思路 看到题目的第一想法是通过滑动窗口求解。但是做下来发现有些数据样例无法通过。发现这道题目中的整数不能保证都为正数,则无法通过滑动窗口进行求解。 先考虑暴力做法,外层两重循环,遍历所有连续子数组,然后最内层再计算一下子数组的和。部分代码如下: ```python for i in range(len(nums)): for j in range(i + 1): sum = countSum(i, j) ``` 这样下来时间复杂度就是 $O(n^3)$ 了。下一步是想办法降低时间复杂度。 先用一重循环遍历数组,计算出数组 `nums` 中前 i 个元素的和(前缀和),保存到一维数组 `pre_sum` 中,那么对于任意 `[j..i]` 的子数组 的和为 `pre_sum[i] - pre_sum[j - 1]`。这样计算子数组和的时间复杂度降为了 $O(1)$。总体时间复杂度为 $O(n^3)$。 但是还是超时了。。 由于我们只关心和为 `k` 出现的次数,不关心具体的解,可以使用哈希表来加速运算。 `pre_sum[i]` 的定义是前 `i` 个元素和,则 `pre_sum[i]` 可以由 `pre_sum[i - 1]` 递推而来,即:`pre_sum[i] = pre_sum[i - 1] + sum[i]`。 `[j..i]` 子数组和为 `k` 可以转换为:`pre_sum[i] - pre_sum[j - 1] == k`。 综合一下,可得:`pre_sum[j - 1] == pre_sum[i] - k `。 所以,当我们考虑以 `i` 结尾和为 `k` 的连续子数组个数时,只需要统计有多少个前缀和为 `pre_sum[i] - k` (即 `pre_sum[j - 1]`)的个数即可。具体做法如下: - 使用 `pre_sum` 变量记录前缀和(代表 `pre_sum[i]`)。 - 使用哈希表 `pre_dic` 记录 `pre_sum[i]` 出现的次数。键值对为 `pre_sum[i] : pre_sum_count`。 - 从左到右遍历数组,计算当前前缀和 `pre_sum`。 - 如果 `pre_sum - k` 在哈希表中,则答案个数累加上 `pre_dic[pre_sum - k]`。 - 如果 `pre_sum` 在哈希表中,则前缀和个数累加 1,即 `pre_dic[pre_sum] += 1`。 - 最后输出答案个数。 ## 代码 ```python class Solution: def subarraySum(self, nums: List[int], k: int) -> int: pre_dic = {0: 1} pre_sum = 0 count = 0 for num in nums: pre_sum += num if pre_sum - k in pre_dic: count += pre_dic[pre_sum - k] if pre_sum in pre_dic: pre_dic[pre_sum] += 1 else: pre_dic[pre_sum] = 1 return count ``` ================================================ FILE: docs/solutions/LCR/Qv1Da2.md ================================================ # [LCR 028. 扁平化多级双向链表](https://leetcode.cn/problems/Qv1Da2/) - 标签:深度优先搜索、链表、双向链表 - 难度:中等 ## 题目链接 - [LCR 028. 扁平化多级双向链表 - 力扣](https://leetcode.cn/problems/Qv1Da2/) ## 题目大意 给定一个带子链表指针 `child` 的双向链表。 要求:将 `child` 的子链表进行扁平化处理,使所有节点出现在单级双向链表中。 扁平化处理如下: ``` 原链表: 1---2---3---4---5---6--NULL | 7---8---9---10--NULL | 11--12--NULL 扁平化之后: 1---2---3---7---8---11---12---9---10---4---5---6--NULL ``` ## 解题思路 递归处理多层链表的扁平化。遍历链表,找到 `child` 非空的节点, 将其子链表链接到当前节点的 `next` 位置(自身扁平化处理)。然后继续向后遍历,不断找到 `child` 节点,并进行链接。直到处理到尾部位置。 ## 代码 ```python class Solution: def dfs(self, node: 'Node'): # 找到链表的尾节点或 child 链表不为空的节点 while node.next and not node.child: node = node.next tail = None if node.child: # 如果 child 链表不为空,将 child 链表扁平化 tail = self.dfs(node.child) # 将扁平化的 child 链表链接在该节点之后 temp = node.next node.next = node.child node.next.prev = node node.child = None tail.next = temp if temp: temp.prev = tail # 链接之后,从 child 链表的尾节点继续向后处理链表 return self.dfs(tail) # child 链表为空,则该节点是尾节点,直接返回 return node def flatten(self, head: 'Node') -> 'Node': if not head: return head self.dfs(head) return head ``` ================================================ FILE: docs/solutions/LCR/RQku0D.md ================================================ # [LCR 019. 验证回文串 II](https://leetcode.cn/problems/RQku0D/) - 标签:贪心、双指针、字符串 - 难度:简单 ## 题目链接 - [LCR 019. 验证回文串 II - 力扣](https://leetcode.cn/problems/RQku0D/) ## 题目大意 给定一个非空字符串 `s`。 要求:判断如果最多从字符串中删除一个字符能否得到一个回文字符串。 ## 解题思路 双指针 + 贪心算法。 - 用两个指针 `left`、`right` 分别指向字符串的开始和结束位置。 - 判断 `s[left]` 是否等于 `s[right]`。 - 如果等于,则 `left` 右移、`right`左移。 - 如果不等于,则判断 `s[left: right - 1]` 或 `s[left + 1, right]` 是为回文串。 - 如果是则返回 `True`。 - 如果不是则返回 `False`,然后继续判断。 - 如果 `right >= left`,则说明字符串 `s` 本身就是回文串,返回 `True`。 ## 代码 ```python class Solution: def checkPalindrome(self, s: str, left: int, right: int): i, j = left, right while i < j: if s[i] != s[j]: return False i += 1 j -= 1 return True def validPalindrome(self, s: str) -> bool: left, right = 0, len(s) - 1 while left < right: if s[left] == s[right]: left += 1 right -= 1 else: return self.checkPalindrome(s, left + 1, right) or self.checkPalindrome(s, left, right - 1) return True ``` ================================================ FILE: docs/solutions/LCR/SLwz0R.md ================================================ # [LCR 021. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/SLwz0R/) - 标签:链表、双指针 - 难度:中等 ## 题目链接 - [LCR 021. 删除链表的倒数第 N 个结点 - 力扣](https://leetcode.cn/problems/SLwz0R/) ## 题目大意 给你一个链表的头节点 `head` 和一个整数 `n`。 要求:删除链表的倒数第 `n` 个节点,并且返回链表的头节点。并且要求使用一次遍历实现。 ## 解题思路 常规思路是遍历一遍链表,求出链表长度,再遍历一遍到对应位置,删除该位置上的节点。 如果用一次遍历实现的话,可以使用快慢指针。让快指针先走 n 步,然后快慢指针、慢指针再同时走,每次一步,这样等快指针遍历到链表尾部的时候,慢指针就刚好遍历到了倒数第 n 个节点位置。将该位置上的节点删除即可。 需要注意的是要删除的节点可能包含了头节点。我们可以考虑在遍历之前,新建一个头节点,让其指向原来的头节点。这样,最终如果删除的是头节点,则删除原头节点即可。返回结果的时候,可以直接返回新建头节点的下一位节点。 ## 代码 ```python class Solution: def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode: newHead = ListNode(0, head) fast = head slow = newHead while n: fast = fast.next n -= 1 while fast: fast = fast.next slow = slow.next slow.next = slow.next.next return newHead.next ``` ================================================ FILE: docs/solutions/LCR/SsGoHC.md ================================================ # [LCR 074. 合并区间](https://leetcode.cn/problems/SsGoHC/) - 标签:数组、排序 - 难度:中等 ## 题目链接 - [LCR 074. 合并区间 - 力扣](https://leetcode.cn/problems/SsGoHC/) ## 题目大意 给定一个数组 `intervals` 表示若干个区间的集合,`intervals[i] = [starti, endi]` 表示单个区间。 要求:合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需要恰好覆盖原数组中的所有区间。 ## 解题思路 设定一个数组 `ans` 用于表示最终不重叠的区间数组,然后对原始区间先按照区间左端点大小从小到大进行排序。 遍历所有区间。先将第一个区间加入 `ans` 数组中。然后依次考虑后边的区间,如果第 `i` 个区间左端点在前一个区间右端点右侧,则这两个区间不会重合,直接将该区间加入 `ans` 数组中。否则的话,这两个区间重合,判断一下两个区间的右区间值,更新前一个区间的右区间值为较大值,然后继续考虑下一个区间,以此类推。 ## 代码 ```python class Solution: def merge(self, intervals: List[List[int]]) -> List[List[int]]: intervals.sort(key=lambda x: x[0]) ans = [] for interval in intervals: if not ans or ans[-1][1] < interval[0]: ans.append(interval) else: ans[-1][1] = max(ans[-1][1], interval[1]) return ans ``` ================================================ FILE: docs/solutions/LCR/TVdhkn.md ================================================ # [LCR 079. 子集](https://leetcode.cn/problems/TVdhkn/) - 标签:位运算、数组、回溯 - 难度:中等 ## 题目链接 - [LCR 079. 子集 - 力扣](https://leetcode.cn/problems/TVdhkn/) ## 题目大意 给定一个整数数组 `nums`,数组中的元素互不相同。 要求:返回该数组所有可能的不重复子集。 ## 解题思路 回溯算法,遍历数组 `nums`。为了使得子集不重复,每次遍历从当前位置的下一个位置进行下一层遍历。 ## 代码 ```python class Solution: def subsets(self, nums: List[int]) -> List[List[int]]: def backtrack(size, subset, index): res.append(subset) for i in range(index, size): backtrack(size, subset + [nums[i]], i + 1) size = len(nums) res = list() backtrack(size, [], 0) return res ``` ================================================ FILE: docs/solutions/LCR/UHnkqh.md ================================================ # [LCR 024. 反转链表](https://leetcode.cn/problems/UHnkqh/) - 标签:递归、链表 - 难度:简单 ## 题目链接 - [LCR 024. 反转链表 - 力扣](https://leetcode.cn/problems/UHnkqh/) ## 题目大意 **描述**:给定一个单链表的头节点 `head`。 **要求**:将其进行反转,并返回反转后的链表的头节点。 ## 解题思路 ### 思路 1. 迭代 1. 使用两个指针 `cur` 和 `pre` 进行迭代。`pre` 指向 `cur` 前一个节点位置。初始时,`pre` 指向 `None`,`cur` 指向 `head`。 2. 将 `pre` 和 `cur` 的前后指针进行交换,指针更替顺序为: 1. 使用 `next` 指针保存当前节点 `cur` 的后一个节点,即 `next = cur.next`; 2. 断开当前节点 `cur` 的后一节点链接,将 `cur` 的 `next` 指针指向前一节点 `pre`,即 `cur.next = pre`; 3. `pre` 向前移动一步,移动到 `cur` 位置,即 `pre = cur`; 4. `cur` 向前移动一步,移动到之前 `next` 指针保存的位置,即 `cur = next`。 3. 继续执行第 2 步中的 1、2、3、4。 4. 最后等到 `cur` 遍历到链表末尾,即 `cur == None`,时,`pre` 所在位置就是反转后链表的头节点,返回新的头节点 `pre`。 使用迭代法反转链表的示意图如下所示: ![迭代法反转链表](https://qcdn.itcharge.cn/images/20220111133639.png) ### 思路 2. 递归 具体做法如下: - 首先定义递归函数含义为:将链表反转,并返回反转后的头节点。 - 然后从 `head.next` 的位置开始调用递归函数,即将 `head.next` 为头节点的链表进行反转,并返回该链表的头节点。 - 递归到链表的最后一个节点,将其作为最终的头节点,即为 `new_head`。 - 在每次递归函数返回的过程中,改变 `head` 和 `head.next` 的指向关系。也就是将 `head.next` 的`next` 指针先指向当前节点 `head`,即 `head.next.next = head `。 - 然后让当前节点 `head` 的 `next` 指针指向 `None`,从而实现从链表尾部开始的局部反转。 - 当递归从末尾开始顺着递归栈的退出,从而将整个链表进行反转。 - 最后返回反转后的链表头节点 `new_head`。 使用递归法反转链表的示意图如下所示: ![递归法反转链表](https://qcdn.itcharge.cn/images/20220111134246.png) ## 代码 1. 迭代 ```python class Solution: def reverseList(self, head: ListNode) -> ListNode: pre = None cur = head while cur != None: next = cur.next cur.next = pre pre = cur cur = next return pre ``` 2. 递归 ```python class Solution: def reverseList(self, head: ListNode) -> ListNode: if head == None or head.next == None: return head new_head = self.reverseList(head.next) head.next.next = head head.next = None return new_head ``` ## 参考资料 - 【题解】[反转链表 - 反转链表 - 力扣](https://leetcode.cn/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-by-leetcode-solution-d1k2/) - 【题解】[【反转链表】:双指针,递归,妖魔化的双指针 - 反转链表 - 力扣(LeetCode)](https://leetcode.cn/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-shuang-zhi-zhen-di-gui-yao-mo-/) ================================================ FILE: docs/solutions/LCR/US1pGT.md ================================================ # [LCR 064. 实现一个魔法字典](https://leetcode.cn/problems/US1pGT/) - 标签:设计、字典树、哈希表、字符串 - 难度:中等 ## 题目链接 - [LCR 064. 实现一个魔法字典 - 力扣](https://leetcode.cn/problems/US1pGT/) ## 题目大意 要求:设计一个使用单词表进行初始化的数据结构。单词表中的单词互不相同。如果给出一个单词,要求判定能否将该单词中的一个字母替换成另一个字母,是的所形成的新单词已经在够构建的单词表中。 实现 MagicDictionary 类: - `MagicDictionary()` 初始化对象。 - `void buildDict(String[] dictionary)` 使用字符串数组 `dictionary` 设定该数据结构,`dictionary` 中的字符串互不相同。 - `bool search(String searchWord)` 给定一个字符串 `searchWord`,判定能否只将字符串中一个字母换成另一个字母,使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,返回 `True`;否则,返回 `False`。 ## 解题思路 - 初始化使用字典树结构。 - `buildDict` 方法中将所有单词存入字典树中。 - `search` 方法中替换 `searchWord` 每一个位置上的字符,然后在字典树中查询。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None and cur.isEnd class MagicDictionary: def __init__(self): """ Initialize your data structure here. """ self.trie_tree = Trie() def buildDict(self, dictionary: List[str]) -> None: for word in dictionary: self.trie_tree.insert(word) def search(self, searchWord: str) -> bool: size = len(searchWord) for i in range(size): for j in range(26): new_ch = chr(ord('a') + j) if searchWord[i] != new_ch: new_word = searchWord[:i] + new_ch + searchWord[i + 1:] if self.trie_tree.search(new_word): return True return False ``` ================================================ FILE: docs/solutions/LCR/UhWRSj.md ================================================ # [LCR 063. 单词替换](https://leetcode.cn/problems/UhWRSj/) - 标签:字典树、数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [LCR 063. 单词替换 - 力扣](https://leetcode.cn/problems/UhWRSj/) ## 题目大意 给定一个由许多词根组成的字典列表 `dictionary`,以及一个句子字符串 `sentence`。 要求:将句子中有词根的单词用词根替换掉。如果单词有很多词根,则用最短的词根替换掉他。最后输出替换之后的句子。 ## 解题思路 将所有的词根存入到前缀树(字典树)中。然后在树上查找每个单词的最短词根。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> str: """ Returns if the word is in the trie. """ cur = self index = 0 for ch in word: if ch not in cur.children: return word cur = cur.children[ch] index += 1 if cur.isEnd: break return word[:index] class Solution: def replaceWords(self, dictionary: List[str], sentence: str) -> str: trie_tree = Trie() for word in dictionary: trie_tree.insert(word) words = sentence.split(" ") size = len(words) for i in range(size): word = words[i] words[i] = trie_tree.search(word) return ' '.join(words) ``` ================================================ FILE: docs/solutions/LCR/VvJkup.md ================================================ # [LCR 083. 全排列](https://leetcode.cn/problems/VvJkup/) - 标签:数组、回溯 - 难度:中等 ## 题目链接 - [LCR 083. 全排列 - 力扣](https://leetcode.cn/problems/VvJkup/) ## 题目大意 给定一个不含重复数字的数组 `nums` 。 要求:返回其有可能的全排列,可以按任意顺序返回。 ## 解题思路 回溯算法递归遍历 `nums` 元素。同时使用 `visited` 数组来标记该元素在当前排列中是否被访问过。如果未被访问过则将其加入排列中,并在访问后将该元素变为未访问状态。 ## 代码 ```python class Solution: def permute(self, nums: List[int]) -> List[List[int]]: def backtrack(size, arrange, index): if index == size: res.append(arrange) return for i in range(size): if visited[i] == True: continue visited[i] = True backtrack(size, arrange + [nums[i]], index + 1) visited[i] = False size = len(nums) res = list() visited = [False for _ in range(size)] backtrack(size, [], 0) return res ``` ================================================ FILE: docs/solutions/LCR/WGki4K.md ================================================ # [LCR 004. 只出现一次的数字 II](https://leetcode.cn/problems/WGki4K/) - 标签:位运算、数组 - 难度:中等 ## 题目链接 - [LCR 004. 只出现一次的数字 II - 力扣](https://leetcode.cn/problems/WGki4K/) ## 题目大意 给定一个整数数组 `nums`,除了某个元素仅出现一次外,其余每个元素恰好出现三次。 要求:找到并返回那个只出现了一次的元素。 ## 解题思路 ### 1. 哈希表 朴素解法就是利用哈希表。统计出每个元素的出现次数。再遍历哈希表,找到仅出现一次的元素。 ### 2. 位运算 将出现三次的元素换成二进制形式放在一起,其二进制对应位置上,出现 `1` 的个数一定是 `3` 的倍数(包括 `0`)。此时,如果在放进来只出现一次的元素,则某些二进制位置上出现 `1` 的个数就不是 `3` 的倍数了。 将这些二进制位置上出现 `1` 的个数不是 `3` 的倍数位置值置为 `1`,是 `3` 的倍数则置为 `0`。这样对应下来的二进制就是答案所求。 注意:因为 Python 的整数没有位数限制,所以不能通过最高位确定正负。所以 Python 中负整数的补码会被当做正整数。所以在遍历到最后 `31` 位时进行 `ans -= (1 << 31)` 操作,目的是将负数的补码转换为「负号 + 原码」的形式。这样就可以正常识别二进制下的负数。参考:[Two's Complement Binary in Python? - Stack Overflow](https://stackoverflow.com/questions/12946116/twos-complement-binary-in-python/12946226) ## 代码 1. 哈希表 ```python class Solution: def singleNumber(self, nums: List[int]) -> int: nums_dict = dict() for num in nums: if num in nums_dict: nums_dict[num] += 1 else: nums_dict[num] = 1 for key in nums_dict: value = nums_dict[key] if value == 1: return key return 0 ``` 2. 位运算 ```python class Solution: def singleNumber(self, nums: List[int]) -> int: ans = 0 for i in range(32): count = 0 for j in range(len(nums)): count += (nums[j] >> i) & 1 if count % 3 != 0: if i == 31: ans -= (1 << 31) else: ans = ans | 1 << i return ans ``` ================================================ FILE: docs/solutions/LCR/WNC0Lk.md ================================================ # [LCR 046. 二叉树的右视图](https://leetcode.cn/problems/WNC0Lk/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [LCR 046. 二叉树的右视图 - 力扣](https://leetcode.cn/problems/WNC0Lk/) ## 题目大意 给定一棵二叉树的根节点 `root`。 要求:按照从顶部到底部的顺序,返回从右侧能看到的节点值。 ## 解题思路 二叉树的层次遍历,不过遍历每层节点的时候,只需要将最后一个节点加入结果数组即可。 ## 代码 ```python class Solution: def rightSideView(self, root: TreeNode) -> List[int]: if not root: return [] queue = [root] order = [] while queue: level = [] size = len(queue) for i in range(size): curr = queue.pop(0) level.append(curr.val) if curr.left: queue.append(curr.left) if curr.right: queue.append(curr.right) if i == size - 1: order.append(curr.val) return order ``` ================================================ FILE: docs/solutions/LCR/WhsWhI.md ================================================ # [LCR 119. 最长连续序列](https://leetcode.cn/problems/WhsWhI/) - 标签:并查集、数组、哈希表 - 难度:中等 ## 题目链接 - [LCR 119. 最长连续序列 - 力扣](https://leetcode.cn/problems/WhsWhI/) ## 题目大意 给定一个未排序的整数数组 `nums`。 要求:找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。并且要用时间复杂度为 $O(n)$ 的算法解决此问题。 ## 解题思路 暴力做法有两种思路。第 1 种思路是先排序再依次判断,这种做法时间复杂度最少是 $O(n \log n)$。第 2 种思路是枚举数组中的每个数 `num`,考虑以其为起点,不断尝试匹配 `num + 1`、`num + 2`、`...` 是否存在,最长匹配次数为 `len(nums)`。这样下来时间复杂度为 $O(n^2)$。但是可以使用集合或哈希表优化这个步骤。 - 先将数组存储到集合中进行去重,然后使用 `curr_streak` 维护当前连续序列长度,使用 `ans` 维护最长连续序列长度。 - 遍历集合中的元素,对每个元素进行判断,如果该元素不是序列的开始(即 `num - 1` 在集合中),则跳过。 - 如果 `num - 1` 不在集合中,说明 `num` 是序列的开始,判断 `num + 1` 、`nums + 2`、`...` 是否在哈希表中,并不断更新当前连续序列长度 `curr_streak`。并在遍历结束之后更新最长序列的长度。 - 最后输出最长序列长度。 将数组存储到集合中进行去重的操作的时间复杂度是 $O(n)$。查询每个数是否在集合中的时间复杂度是 $O(1)$ ,并且跳过了所有不是起点的元素。更新当前连续序列长度 `curr_streak` 的时间复杂度是 $O(n)$,所以最终的时间复杂度是 $O(n)$。符合题意要求。 ## 代码 ```python class Solution: def longestConsecutive(self, nums: List[int]) -> int: ans = 0 nums_set = set(nums) for num in nums_set: if num - 1 not in nums_set: curr_num = num curr_streak = 1 while curr_num + 1 in nums_set: curr_num += 1 curr_streak += 1 ans = max(ans, curr_streak) return ans ``` ================================================ FILE: docs/solutions/LCR/XagZNi.md ================================================ # [LCR 037. 行星碰撞](https://leetcode.cn/problems/XagZNi/) - 标签:栈、数组 - 难度:中等 ## 题目链接 - [LCR 037. 行星碰撞 - 力扣](https://leetcode.cn/problems/XagZNi/) ## 题目大意 给定一个整数数组 `asteroids`,表示在同一行的小行星。 数组中的每一个元素,其绝对值表示小行星的大小,正负表示小行星的移动方向(正表示向右移动,负表示向左移动)。每一颗小行星以相同的速度移动。小行星按照下面的规则发生碰撞。 - 碰撞规则:两个行星相互碰撞,较小的行星会爆炸。如果两颗行星大小相同,则两颗行星都会爆炸。两颗移动方向相同的行星,永远不会发生碰撞。 要求:找出碰撞后剩下的所有小行星,将答案存入数组并返回。 ## 解题思路 用栈模拟小行星碰撞,具体步骤如下: - 遍历数组 `asteroids`。 - 如果栈为空或者当前元素 `asteroid` 为正数,将其压入栈。 - 如果当前栈不为空并且当前元素 `asteroid` 为负数: - 与栈中元素发生碰撞,判断当前元素和栈顶元素的大小和方向,如果栈顶元素为正数,并且当前元素的绝对值大于栈顶元素,则将栈顶元素弹出,并继续与栈中元素发生碰撞。 - 碰撞完之后,如果栈为空并且栈顶元素为负数,则将当前元素 `asteroid` 压入栈,表示碰撞完剩下了 `asteroid`。 - 如果栈顶元素恰好与当前元素值大小相等、方向相反,则弹出栈顶元素,表示碰撞完两者都爆炸了。 - 最后返回栈作为答案。 ## 代码 ```python class Solution: def asteroidCollision(self, asteroids: List[int]) -> List[int]: stack = [] for asteroid in asteroids: if not stack or asteroid > 0: stack.append(asteroid) else: while stack and 0 < stack[-1] < -asteroid: stack.pop() if not stack or stack[-1] < 0: stack.append(asteroid) elif stack[-1] == -asteroid: stack.pop() return stack ``` ================================================ FILE: docs/solutions/LCR/XltzEq.md ================================================ # [LCR 018. 验证回文串](https://leetcode.cn/problems/XltzEq/) - 标签:双指针、字符串 - 难度:简单 ## 题目链接 - [LCR 018. 验证回文串 - 力扣](https://leetcode.cn/problems/XltzEq/) ## 题目大意 给定一个字符串 `s`。 要求:判断是否为回文串。(只考虑字符串中的字母和数字字符,并且忽略字母的大小写) ## 解题思路 左右两个指针 `start` 和 `end`,左指针 `start` 指向字符串头部,右指针 `end` 指向字符串尾部。先过滤掉除字母和数字字符以外的字符,在判断 `s[start]` 和 `s[end]` 是否相等。不相等返回 `False`,相等则继续过滤和判断。 ## 代码 ```python class Solution: def isPalindrome(self, s: str) -> bool: n = len(s) start = 0 end = n - 1 while start < end: if not s[start].isalnum(): start += 1 continue if not s[end].isalnum(): end -= 1 continue if s[start].lower() == s[end].lower(): start += 1 end -= 1 else: return False return True ``` ================================================ FILE: docs/solutions/LCR/YaVDxD.md ================================================ # [LCR 102. 目标和](https://leetcode.cn/problems/YaVDxD/) - 标签:数组、动态规划、回溯 - 难度:中等 ## 题目链接 - [LCR 102. 目标和 - 力扣](https://leetcode.cn/problems/YaVDxD/) ## 题目大意 给定一个整数数组 `nums` 和一个整数 `target`。数组长度不超过 `20`。向数组中每个整数前加 `+` 或 `-`。然后串联起来构造成一个表达式。 要求:返回通过上述方法构造的、运算结果等于 `target` 的不同表达式数目。 ## 解题思路 暴力方法就是使用深度优先搜索对每位数字遍历 `+`、`-`,并统计符合要求的表达式数目。但是实际发现超时了。所以采用动态规划的方法来做。 假设数组中所有元素和为 `sum`,数组中所有符号为 `+` 的元素为 `sum_x`,符号为 `-` 的元素和为 `sum_y`。则 `target = sum_x - sum_y`。 而 `sum_x + sum_y = sum`。根据两个式子可以求出 `2 * sum_x = target + sum `,即 `sum_x = (target + sum) / 2`。 那么这道题就变成了,如何在数组中找到一个集合,使集合中元素和为 `(target + sum) / 2`。这就变为了求容量为 `(target + sum) / 2` 的 `01` 背包问题。 动态规划的状态 `dp[i]` 表示为:填满容量为 `i` 的背包,有 `dp[i]` 种方法。 动态规划的状态转移方程为:`dp[i] = dp[i] + dp[i-num]`,意思为填满容量为 `i` 的背包的方法数 = 不使用当前 `num`,只使用之前元素填满容量为 `i` 的背包的方法数 + 填满容量 `i - num` 的包的方法数,再填入 `num` 的方法数。 ## 代码 ```python class Solution: def findTargetSumWays(self, nums: List[int], target: int) -> int: sum_nums = sum(nums) if target > sum_nums or (target + sum_nums) % 2 == 1: return 0 size = (target + sum_nums) // 2 dp = [0 for _ in range(size + 1)] dp[0] = 1 for num in nums: for i in range(size, num - 1, -1): dp[i] = dp[i] + dp[i - num] return dp[size] ``` ================================================ FILE: docs/solutions/LCR/Ygoe9J.md ================================================ # [LCR 081. 组合总和](https://leetcode.cn/problems/Ygoe9J/) - 标签:数组、回溯 - 难度:中等 ## 题目链接 - [LCR 081. 组合总和 - 力扣](https://leetcode.cn/problems/Ygoe9J/) ## 题目大意 给定一个无重复元素的正整数数组 `candidates` 和一个正整数 `target`。 要求:找出 `candidates` 中所有可以使数字和为目标数 `target` 的唯一组合。 注意:数组 `candidates` 中的数字可以无限重复选取,且 `1 ≤ candidates[i] ≤ 200`。 ## 解题思路 回溯算法,因为 `1 ≤ candidates[i] ≤ 200`,所以即便是 `candidates[i]` 值为 `1`,重复选取也会等于或大于 target,从而终止回溯。 建立两个数组 `res`、`path`。`res` 用于存放所有满足题意的组合,`path` 用于存放当前满足题意的一个组合。 定义回溯方法,`start_index = 1` 开始进行回溯。 - 如果 `sum > target`,则直接返回。 - 如果 `sum == target`,则将 `path` 中的元素加入到 `res` 数组中。 - 然后对 `[start_index, n]` 范围内的数进行遍历取值。 - 如果 `sum + candidates[i] > target`,可以直接跳出循环。 - 将和累积,即 `sum += candidates[i]`,然后将当前元素 `i` 加入 `path` 数组。 - 递归遍历 `[start_index, n]` 上的数。 - 加之前的和回退,即 `sum -= candidates[i]`,然后将遍历的 `i` 元素进行回退。 - 最终返回 `res` 数组。 ## 代码 ```python class Solution: res = [] path = [] def backtrack(self, candidates: List[int], target: int, sum: int, start_index: int): if sum > target: return if sum == target: self.res.append(self.path[:]) return for i in range(start_index, len(candidates)): if sum + candidates[i] > target: break sum += candidates[i] self.path.append(candidates[i]) self.backtrack(candidates, target, sum, i) sum -= candidates[i] self.path.pop() def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: self.res.clear() self.path.clear() candidates.sort() self.backtrack(candidates, target, 0, 0) return self.res ``` ================================================ FILE: docs/solutions/LCR/ZL6zAn.md ================================================ # [LCR 105. 岛屿的最大面积](https://leetcode.cn/problems/ZL6zAn/) - 标签:深度优先搜索、广度优先搜索、并查集、数组、矩阵 - 难度:中等 ## 题目链接 - [LCR 105. 岛屿的最大面积 - 力扣](https://leetcode.cn/problems/ZL6zAn/) ## 题目大意 给定一个只包含 `0`、`1` 元素的二维数组,`1` 代表岛屿,`0` 代表水。一座岛的面积就是上下左右相邻相邻的 `1` 所组成的连通块的数目。找到最大的岛屿面积。 ## 解题思路 使用深度优先搜索方法。遍历二维数组的每一个元素,对于每个值为 `1` 的元素,记下其面积。然后将该值置为 `0`(防止二次重复计算),再递归其上下左右四个位置,并将深度优先搜索搜到的值为 `1` 的元素个数,进行累积统计。 ## 代码 ```python class Solution: def dfs(self, grid, i, j): size_n = len(grid) size_m = len(grid[0]) if i < 0 or i >= size_n or j < 0 or j >= size_m or grid[i][j] == 0: return 0 ans = 1 grid[i][j] = 0 ans += self.dfs(grid, i + 1, j) ans += self.dfs(grid, i, j + 1) ans += self.dfs(grid, i - 1, j) ans += self.dfs(grid, i, j - 1) return ans def maxAreaOfIsland(self, grid: List[List[int]]) -> int: ans = 0 for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == 1: ans = max(ans, self.dfs(grid, i, j)) return ans ``` ================================================ FILE: docs/solutions/LCR/ZVAVXX.md ================================================ # [LCR 009. 乘积小于 K 的子数组](https://leetcode.cn/problems/ZVAVXX/) - 标签:数组、滑动窗口 - 难度:中等 ## 题目链接 - [LCR 009. 乘积小于 K 的子数组 - 力扣](https://leetcode.cn/problems/ZVAVXX/) ## 题目大意 给定一个正整数数组 `nums` 和一个整数 `k`。 要求:找出该数组内乘积小于 `k` 的连续子数组的个数。 ## 解题思路 滑动窗口求解。 设定两个指针:`left`、`right`,分别指向滑动窗口的左右边界,保证窗口内所有数的乘积 `product` 都小于 `k`。 - 一开始,`left`、`right` 都指向 `0`。 - 向右移动 `right`,将最右侧元素加入当前子数组乘积 `product` 中。 - 如果 `product >= k` ,则不断右移 `left`,缩小滑动窗口,并更新当前乘积值 `product` 直到 `product < k`。 - 累积答案个数 += 1,继续右移 `right`,直到 `right >= len(nums)` 结束。 - 输出累积答案个数。 ## 代码 ```python class Solution: def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int: if k <= 1: return 0 size = len(nums) left, right = 0, 0 count = 0 product = 1 while right < size: product *= nums[right] right += 1 while product >= k: product /= nums[left] left += 1 count += (right - left) return count ``` ================================================ FILE: docs/solutions/LCR/a7VOhD.md ================================================ # [LCR 020. 回文子串](https://leetcode.cn/problems/a7VOhD/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [LCR 020. 回文子串 - 力扣](https://leetcode.cn/problems/a7VOhD/) ## 题目大意 给定一个字符串 `s`。 要求:计算 `s` 中有多少个回文子串。 ## 解题思路 动态规划求解。 先定义状态 `dp[i][j]` 表示为区间 `[i, j]` 的子串是否为回文子串,如果是,则 `dp[i][j] = True`,如果不是,则 `dp[i][j] = False`。 接下来确定状态转移共识: 如果 `s[i] == s[j]`,分为以下几种情况: - `i == j`,单字符肯定是回文子串,`dp[i][j] == True`。 - `j - i == 1`,比如 `aa` 肯定也是回文子串,`dp[i][j] = True`。 - 如果 `j - i > 1`,则需要看 `[i + 1, j - 1]` 区间是不是回文子串,`dp[i][j] = dp[i + 1][j - 1]`。 如果 `s[i] != s[j]`,那肯定不是回文子串,`dp[i][j] = False`。 下一步确定遍历方向。 由于 `dp[i][j]` 依赖于 `dp[i + 1][j - 1]`,所以我们可以从左下角向右上角遍历。 同时,在递推过程中记录下 `dp[i][j] == True` 的个数,即为最后结果。 ## 代码 ```python class Solution: def countSubstrings(self, s: str) -> int: size = len(s) dp = [[False for _ in range(size)] for _ in range(size)] res = 0 for i in range(size - 1, -1, -1): for j in range(i, size): if s[i] == s[j]: if j - i <= 1: dp[i][j] = True else: dp[i][j] = dp[i + 1][j - 1] else: dp[i][j] = False if dp[i][j]: res += 1 return res ``` ================================================ FILE: docs/solutions/LCR/aMhZSa.md ================================================ # [LCR 027. 回文链表](https://leetcode.cn/problems/aMhZSa/) - 标签:栈、递归、链表、双指针 - 难度:简单 ## 题目链接 - [LCR 027. 回文链表 - 力扣](https://leetcode.cn/problems/aMhZSa/) ## 题目大意 给定一个链表的头节点 `head`。 要求:判断该链表是否为回文链表。 ## 解题思路 利用数组,将链表元素依次存入。然后再使用两个指针,一个指向数组开始位置,一个指向数组结束位置,依次判断首尾对应元素是否相等,如果都相等,则为回文链表。如果不相等,则不是回文链表。 ## 代码 ```python class Solution: def isPalindrome(self, head: ListNode) -> bool: nodes = [] p1 = head while p1 != None: nodes.append(p1.val) p1 = p1.next return nodes == nodes[::-1] ``` ================================================ FILE: docs/solutions/LCR/aseY1I.md ================================================ # [LCR 005. 最大单词长度乘积](https://leetcode.cn/problems/aseY1I/) - 标签:位运算、数组、字符串 - 难度:中等 ## 题目链接 - [LCR 005. 最大单词长度乘积 - 力扣](https://leetcode.cn/problems/aseY1I/) ## 题目大意 给定一个字符串数组 `words`。字符串中只包含英语的小写字母。 要求:计算当两个字符串 `words[i]` 和 `words[j]` 不包含相同字符时,它们长度的乘积的最大值。如果没有不包含相同字符的一对字符串,返回 0。 ## 解题思路 这道题的核心难点是判断任意两个字符串之间是否包含相同字符。最直接的做法是先遍历第一个字符串的每个字符,再遍历第二个字符串查看是否有相同字符。但是这样做的话,时间复杂度过高。考虑怎么样可以优化一下。 题目中说字符串中只包含英语的小写字母,也就是 `26` 种字符。一个 `32` 位的 `int` 整数每一个二进制位都可以表示一种字符的有无,那么我们就可以通过一个整数来表示一个字符串中所拥有的字符种类。延伸一下,我们可以用一个整数数组来表示一个字符串数组中,每个字符串所拥有的字符种类。 接下来事情就简单了,两重循环遍历整数数组,遇到两个字符串不包含相同字符的情况,就计算一下他们长度的乘积,并维护一个乘积最大值。最后输出最大值即可。 ## 代码 ```python class Solution: def maxProduct(self, words: List[str]) -> int: size = len(words) arr = [0 for _ in range(size)] for i in range(size): word = words[i] len_word = len(word) for j in range(len_word): arr[i] |= 1 << (ord(word[j]) - ord('a')) ans = 0 for i in range(size): for j in range(i + 1, size): if arr[i] & arr[j] == 0: k = len(words[i]) * len(words[j]) ans = k if ans < k else ans return ans ``` ================================================ FILE: docs/solutions/LCR/bLyHh0.md ================================================ # [LCR 116. 省份数量](https://leetcode.cn/problems/bLyHh0/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [LCR 116. 省份数量 - 力扣](https://leetcode.cn/problems/bLyHh0/) ## 题目大意 一个班上有 `n` 个同学,其中一些彼此是朋友,另一些不是。如果 `a` 与 `b` 是直接朋友,且 `b` 与 `c` 也是直接朋友,那么 `a` 与 `c` 是间接朋友。 现在定义「朋友圈」是由一组直接或间接朋友组成的集合。 现在给定一个 `n * n` 的矩阵 `isConnected` 表示班上的朋友关系。其中 `isConnected[i][j] = 1` 表示第 `i` 个同学和第 `j` 个同学是直接朋友,`isConnected[i][j] = 0` 表示第 `i` 个同学和第 `j` 个同学不是直接朋友。 要求:根据给定的同学关系,返回「朋友圈」的数量。 ## 解题思路 可以利用并查集来做。具体做法如下: 遍历矩阵 `isConnected`。如果 `isConnected[i][j] = 1`,将 `i` 节点和 `j` 节点相连。然后判断每个同学节点的根节点,然后统计不重复的根节点有多少个,即为「朋友圈」的数量。 ## 代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] self.count = n def find(self, x): while x != self.parent[x]: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.count -= 1 def is_connected(self, x, y): return self.find(x) == self.find(y) class Solution: def findCircleNum(self, isConnected: List[List[int]]) -> int: size = len(isConnected) union_find = UnionFind(size) for i in range(size): for j in range(i + 1, size): if isConnected[i][j] == 1: union_find.union(i, j) return union_find.count ``` ================================================ FILE: docs/solutions/LCR/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof.md ================================================ # [LCR 165. 解密数字](https://leetcode.cn/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [LCR 165. 解密数字 - 力扣](https://leetcode.cn/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/) ## 题目大意 给定一个数字 `num`,按照如下规则将其翻译为字符串:`0` 翻译为 `a`,`1` 翻译为 `b`,…,`11` 翻译为 `l`,…,`25` 翻译为 `z`。 要求:计算出共有多少种可能的翻译方案。 ## 解题思路 可用动态规划来做。 将数字 `nums` 转为字符串 `s`。设 `dp[i]` 表示字符串 `s` 前 `i` 个数字 `s[0: i]` 的翻译方案数。`dp[i]` 的来源有两种情况: 1. 第 `i - 1`、`i - 2` 构成的数字在 `[10, 25]`之间,则 `dp[i]` 来源于: `s[i - 1]` 单独翻译的方案数(即 `dp[i - 1]`) + `s[i - 2]` 和 `s[i - 1]` 连起来进行翻译的方案数(即 `dp[i - 2]`)。 2. 第 `i - 1`、`i - 2` 构成的数字在 `[10, 25]`之外,则 `dp[i]` 来源于:`s[i]` 单独翻译的方案数。 ## 代码 ```python class Solution: def translateNum(self, num: int) -> int: s = str(num) size = len(s) dp = [0 for _ in range(size + 1)] dp[0] = 1 dp[1] = 1 for i in range(2, size + 1): temp = int(s[i-2:i]) if temp >= 10 and temp <= 25: dp[i] = dp[i - 1] + dp[i - 2] else: dp[i] = dp[i - 1] return dp[size] ``` ================================================ FILE: docs/solutions/LCR/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof.md ================================================ # [LCR 164. 破解闯关密码](https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/) - 标签:贪心、字符串、排序 - 难度:中等 ## 题目链接 - [LCR 164. 破解闯关密码 - 力扣](https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/) ## 题目大意 **描述**:给定一个非负整数数组 $nums$。 **要求**:将数组中的数字拼接起来排成一个数,打印能拼接出的所有数字中的最小的一个。 **说明**: - $0 < nums.length \le 100$。 - 输出结果可能非常大,所以你需要返回一个字符串而不是整数。 - 拼接起来的数字可能会有前导 $0$,最后结果不需要去掉前导 $0$。 **示例**: - 示例 1: ```python 输入: [10,2] 输出: "102" ``` - 示例 2: ```python 输入:[3,30,34,5,9] 输出:"3033459" ``` ## 解题思路 ### 思路 1:自定义排序 本质上是给数组进行排序。假设 $x$、$y$ 是数组 $nums$ 中的两个元素。则排序的判断规则如下所示: - 如果拼接字符串 $x + y > y + x$,则 $x$ 大于 $y$,$y$ 应该排在 $x$ 前面,从而使拼接起来的数字尽可能的小。 - 反之,如果拼接字符串 $x + y < y + x$,则 $x$ 小于 $y$,$x$ 应该排在 $y$ 前面,从而使拼接起来的数字尽可能的小。 按照上述规则,对原数组进行排序。这里使用了 `functools.cmp_to_key` 自定义排序函数。 ### 思路 1:自定义排序代码 ```python import functools class Solution: def minNumber(self, nums: List[int]) -> str: def cmp(a, b): if a + b == b + a: return 0 elif a + b > b + a: return 1 else: return -1 nums_s = list(map(str, nums)) nums_s.sort(key=functools.cmp_to_key(cmp)) return ''.join(nums_s) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。排序算法的时间复杂度为 $O(n \times \log n)$。 - **空间复杂度**:$O(1)$。 ## 参考资料 - 【题解】[LCR 164. 破解闯关密码(自定义排序,清晰图解) - 把数组排成最小的数 - 力扣](https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/solution/mian-shi-ti-45-ba-shu-zu-pai-cheng-zui-xiao-de-s-4/) ================================================ FILE: docs/solutions/LCR/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof.md ================================================ # [LCR 192. 把字符串转换成整数 (atoi)](https://leetcode.cn/problems/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof/) - 标签:字符串 - 难度:中等 ## 题目链接 - [LCR 192. 把字符串转换成整数 (atoi) - 力扣](https://leetcode.cn/problems/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof/) ## 题目大意 给定一个字符串 `str`。 要求:使其能换成一个 32 位有符号整数。并且该方法满足以下要求: - 丢弃开头无用的空格字符,直到找到第一个非空格字符为止。 - 当找到的第一个非空字符为正负号时,将该符号与后面尽可能多的连续数组组合起来,作为该整数的正负号。如果第一个非空字符为数字,则直接将其与之后连续的数字字符组合起来,形成整数。 - 该字符串中除了有效的整数部分之后也可能会存在多余字符,可直接将这些字符忽略,不会对函数造成影响。 - 如果第一个非空格字符不是一个有效整数字符、或者字符串为空、字符串仅包含空白字符时,函数不需要进行转换。 - 需要检测有效性,无法读取返回 0。 - 所有整数范围为 $[-2^{31}, 2^{31} - 1]$,超过这个范围,则返回 $2^{31} - 1$ 或者 $-2^{31}$。 ## 解题思路 根据题意直接模拟即可。 1. 先去除前后空格。 2. 检测正负号。 3. 读入数字,并用字符串存储数字结果 4. 将数字字符串转为整数,并根据正负号转换整数结果。 5. 判断整数范围,并返回最终结果。 ## 代码 ```python class Solution: def strToInt(self, str: str) -> int: num_str = "" positive = True start = 0 s = str.lstrip() if not s: return 0 if s[0] == '-': positive = False start = 1 elif s[0] == '+': positive = True start = 1 elif not s[0].isdigit(): return 0 for i in range(start, len(s)): if s[i].isdigit(): num_str += s[i] else: break if not num_str: return 0 num = int(num_str) if not positive: num = -num return max(num, -2 ** 31) else: return min(num, 2 ** 31 - 1) ``` ================================================ FILE: docs/solutions/LCR/bao-han-minhan-shu-de-zhan-lcof.md ================================================ # [LCR 147. 最小栈](https://leetcode.cn/problems/bao-han-minhan-shu-de-zhan-lcof/) - 标签:栈、设计 - 难度:简单 ## 题目链接 - [LCR 147. 最小栈 - 力扣](https://leetcode.cn/problems/bao-han-minhan-shu-de-zhan-lcof/) ## 题目大意 要求:设计一个「栈」,实现 `push` ,`pop` ,`top` ,`min` 操作,并且操作时间复杂度都是 `O(1)`。 ## 解题思路 使用一个栈,栈元素中除了保存当前值之外,再保存一个当前最小值。 - `push` 操作:如果栈不为空,则判断当前值与栈顶元素所保存的最小值,并更新当前最小值,将新元素保存到栈中。 - `pop`操作:正常出栈 - `top` 操作:返回栈顶元素保存的值。 - `min` 操作:返回栈顶元素保存的最小值。 ## 代码 ```python class MinStack: def __init__(self): """ initialize your data structure here. """ self.stack = [] class Node: def __init__(self, x): self.val = x self.min = x def push(self, x: int) -> None: node = self.Node(x) if len(self.stack) == 0: self.stack.append(node) else: topNode = self.stack[-1] if node.min > topNode.min: node.min = topNode.min self.stack.append(node) def pop(self) -> None: self.stack.pop() def top(self) -> int: return self.stack[-1].val def min(self) -> int: return self.stack[-1].min ``` ================================================ FILE: docs/solutions/LCR/bu-ke-pai-zhong-de-shun-zi-lcof.md ================================================ # [LCR 186. 文物朝代判断](https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof/) - 标签:数组、排序 - 难度:简单 ## 题目链接 - [LCR 186. 文物朝代判断 - 力扣](https://leetcode.cn/problems/bu-ke-pai-zhong-de-shun-zi-lcof/) ## 题目大意 给定一个 `5` 位数的数组 `nums` 代表扑克牌中的 `5` 张牌。其中 `2~10` 为数字本身,`A` 用 `1` 表示,`J` 用 `11` 表示,`Q` 用 `12` 表示,`K` 用 `13` 表示,大小王用 `0` 表示,且大小王可以替换任意数字。 要求:判断给定的 `5` 张牌是否是一个顺子,即是否为连续的`5` 个数。 ## 解题思路 先不考虑牌中有大小王,如果 `5` 个数是连续的,则这 `5` 个数中最大值最小值的关系为:`最大值 - 最小值 = 4`。如果牌中有大小王可以替换这 `5` 个数中的任意数字,则除大小王之外剩下数的最大值最小值关系为 `最大值 - 最小值 <= 4`。而且剩余数不能有重复数字。于是可以这样进行判断。 遍历 `5` 张牌: - 如果出现大小王,则跳过。 - 判断 `5` 张牌中是否有重复数,如果有则直接返回 `False`,如果没有则将其加入集合。 - 计算 `5` 张牌的最大值,最小值。 最后判断 `最大值 - 最小值 <= 4` 是否成立。如果成立,返回 `True`,否则返回 `False`。 ## 代码 ```python class Solution: def isStraight(self, nums: List[int]) -> bool: max_num, min_num = 0, 14 repeat = set() for num in nums: if num == 0: continue if num in repeat: return False repeat.add(num) max_num = max(max_num, num) min_num = min(min_num, num) return max_num - min_num <= 4 ``` ================================================ FILE: docs/solutions/LCR/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof.md ================================================ # [LCR 190. 加密运算](https://leetcode.cn/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof/) - 标签:位运算、数学 - 难度:简单 ## 题目链接 - [LCR 190. 加密运算 - 力扣](https://leetcode.cn/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof/) ## 题目大意 给定两个整数 `a`、`b`。 要求:不能使用运算符 `+`、`-`、`*`、`/`,计算两整数 `a` 、`b` 之和。 ## 解题思路 需要用到位运算的一些知识。 - 异或运算 a ^ b :可以获得 a + b 无进位的加法结果。 - 与运算 a & b:对应位置为 1,说明 a、b 该位置上原来都为 1,则需要进位。 - 座椅运算 a << 1:将 a 对应二进制数左移 1 位。 这样,通过 a^b 运算,我们可以得到相加后无进位结果,再根据 (a&b) << 1,计算进位后结果。 进行 a^b 和 (a&b) << 1操作之后判断进位是否为 0,如果不为 0,则继续上一步操作,直到进位为 0。 > 注意: > > Python 的整数类型是无限长整数类型,负数不确定符号位是第几位。所以我们可以将输入的数字手动转为 32 位无符号整数。 > > 通过 a &= 0xFFFFFFFF 即可将 a 转为 32 位无符号整数。最后通过对 a 的范围判断,将其结果映射为有符号整数。 ## 代码 ```python class Solution: def getSum(self, a: int, b: int) -> int: MAX_INT = 0x7FFFFFFF MASK = 0xFFFFFFFF a &= MASK b &= MASK while b: carry = ((a & b) << 1) & MASK a ^= b b = carry if a <= MAX_INT: return a else: return ~(a ^ MASK) ``` ================================================ FILE: docs/solutions/LCR/c32eOV.md ================================================ # [LCR 022. 环形链表 II](https://leetcode.cn/problems/c32eOV/) - 标签:哈希表、链表、双指针 - 难度:中等 ## 题目链接 - [LCR 022. 环形链表 II - 力扣](https://leetcode.cn/problems/c32eOV/) ## 题目大意 给定一个链表的头节点 `head`。 要求:判断链表中是否有环,如果有环则返回入环的第一个节点,无环则返回 `None`。 ## 解题思路 利用两个指针,一个慢指针每次前进一步,快指针每次前进两步(两步或多步效果是等价的)。如果两个指针在链表头节点以外的某一节点相遇(即相等)了,那么说明链表有环,否则,如果(快指针)到达了某个没有后继指针的节点时,那么说明没环。 如果有环,则再定义一个指针,和慢指针一起每次移动一步,两个指针相遇的位置即为入口节点。 这是因为:假设入环位置为 A,快慢指针在在 B 点相遇,则相遇时慢指针走了 a + b 步,快指针走了 $a + n(b+c) + b$ 步。 $2(a + b) = a + n(b + c) + b$。可以推出:$a = c + (n-1)(b + c)$。 我们可以发现:从相遇点到入环点的距离 $c$ 加上 $n-1$ 圈的环长 $b + c$ 刚好等于从链表头部到入环点的距离。 ## 代码 ```python class Solution: def detectCycle(self, head: ListNode) -> ListNode: fast, slow = head, head while True: if not fast or not fast.next: return None fast = fast.next.next slow = slow.next if fast == slow: break ans = head while ans != slow: ans, slow = ans.next, slow.next return ans ``` ================================================ FILE: docs/solutions/LCR/chou-shu-lcof.md ================================================ # [LCR 168. 丑数](https://leetcode.cn/problems/chou-shu-lcof/) - 标签:哈希表、数学、动态规划、堆(优先队列) - 难度:中等 ## 题目链接 - [LCR 168. 丑数 - 力扣](https://leetcode.cn/problems/chou-shu-lcof/) ## 题目大意 给定一个整数 `n`。 要求:找出并返回第 `n` 个丑数。 - 丑数:只包含质因数 `2`、`3`、`5` 的正整数。 ## 解题思路 动态规划求解。 定义状态 `dp[i]` 表示第 `i` 个丑数。 状态转移方程为:`dp[i] = min(dp[p2] * 2, dp[p3] * 3, dp[p5] * 5)` ,其中 `p2`、`p3`、`p5` 分别表示当前 `i` 中 `2`、`3`、`5` 的质因子数量。 ## 代码 ```python class Solution: def nthUglyNumber(self, n: int) -> int: dp = [1 for _ in range(n)] p2, p3, p5 = 0, 0, 0 for i in range(1, n): dp[i] = min(dp[p2] * 2, dp[p3] * 3, dp[p5] * 5) if dp[i] == dp[p2] * 2: p2 += 1 if dp[i] == dp[p3] * 3: p3 += 1 if dp[i] == dp[p5] * 5: p5 += 1 return dp[n - 1] ``` ================================================ FILE: docs/solutions/LCR/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof.md ================================================ # [LCR 150. 彩灯装饰记录 II](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof/) - 标签:树、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [LCR 150. 彩灯装饰记录 II - 力扣](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof/) ## 题目大意 给定一棵二叉树的根节点 `root`。 要求:从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。 ## 解题思路 广度优先搜索,需要增加一些变化。普通广度优先搜索只取一个元素,变化后的广度优先搜索每次取出第 i 层上所有元素。 具体步骤如下: - 根节点入队。 - 当队列不为空时,求出当前队列长度 $s_i$。 - 依次从队列中取出这 $s_i$ 个元素,并将其左右子节点入队,遍历完之后将这层节点数组加入答案数组中,然后继续迭代。 - 当队列为空时,结束。 ## 代码 ```python class Solution: def levelOrder(self, root: TreeNode) -> List[List[int]]: if not root: return [] queue = [root] order = [] while queue: level = [] size = len(queue) for _ in range(size): curr = queue.pop(0) level.append(curr.val) if curr.left: queue.append(curr.left) if curr.right: queue.append(curr.right) if level: order.append(level) return order ``` ================================================ FILE: docs/solutions/LCR/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof.md ================================================ # [LCR 151. 彩灯装饰记录 III](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof/) - 标签:树、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [LCR 151. 彩灯装饰记录 III - 力扣](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof/) ## 题目大意 给定一个二叉树的根节点 `root`。 要求:返回其之字形层序遍历。 - 之字形层序遍历:从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行。 ## 解题思路 广度优先搜索,在二叉树的层序遍历的基础上需要增加一些变化。 普通广度优先搜索只取一个元素,变化后的广度优先搜索每次取出第 i 层上所有元素。 新增一个变量 odd,用于判断当前层数是奇数层,还是偶数层。从而判断元素遍历方向。 存储每层元素的 level 列表改用双端队列,如果是奇数层,则从末尾添加元素。如果是偶数层,则从头部添加元素。 具体步骤如下: - 根节点入队。 - 当队列不为空时,求出当前队列长度 $s_i$,并判断当前层数的奇偶性。 - 依次从队列中取出这 $s_i$ 个元素。 - 如果为奇数层,如果是奇数层,则从 level 末尾添加元素。 - 如果是偶数层,则从 level头部添加元素。 - 然后保存将其左右子节点入队,然后继续迭代。 - 当队列为空时,结束。 ## 代码 ```python import collections class Solution: def levelOrder(self, root: TreeNode) -> List[List[int]]: if not root: return [] queue = [root] order = [] odd = True while queue: level = collections.deque() size = len(queue) for _ in range(size): curr = queue.pop(0) if odd: level.append(curr.val) else: level.appendleft(curr.val) if curr.left: queue.append(curr.left) if curr.right: queue.append(curr.right) if level: order.append(list(level)) odd = not odd return order ``` ================================================ FILE: docs/solutions/LCR/cong-shang-dao-xia-da-yin-er-cha-shu-lcof.md ================================================ # [LCR 149. 彩灯装饰记录 I](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof/) - 标签:树、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [LCR 149. 彩灯装饰记录 I - 力扣](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof/) ## 题目大意 给定一棵二叉树的根节点 `root`。 要求:从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。 ## 解题思路 广度优先搜索。 具体步骤如下: - 根节点入队。 - 当队列不为空时,求出当前队列长度 $s_i$。 - 依次从队列中取出这 $s_i$ 个元素,将其加入答案数组,并将其左右子节点入队,然后继续迭代。 - 当队列为空时,结束。 ## 代码 ```python class Solution: def levelOrder(self, root: TreeNode) -> List[int]: if not root: return [] queue = [root] order = [] while queue: size = len(queue) for _ in range(size): curr = queue.pop(0) order.append(curr.val) if curr.left: queue.append(curr.left) if curr.right: queue.append(curr.right) return order ``` ================================================ FILE: docs/solutions/LCR/cong-wei-dao-tou-da-yin-lian-biao-lcof.md ================================================ # [LCR 123. 图书整理 I](https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/) - 标签:栈、递归、链表、双指针 - 难度:简单 ## 题目链接 - [LCR 123. 图书整理 I - 力扣](https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/) ## 题目大意 给定一个链表的头节点 `head`。 要求:从尾到头反过来返回每个节点的值(用数组返回)。 ## 解题思路 - 定义数组 `res`,从头到尾遍历链表。 - 将每个节点值存入数组中。 - 直接返回倒序数组。 ## 代码 ```python class Solution: def reversePrint(self, head: ListNode) -> List[int]: res = [] while head: res.append(head.val) head = head.next return res[::-1] ``` ================================================ FILE: docs/solutions/LCR/dKk3P7.md ================================================ # [LCR 032. 有效的字母异位词](https://leetcode.cn/problems/dKk3P7/) - 标签:哈希表、字符串、排序 - 难度:简单 ## 题目链接 - [LCR 032. 有效的字母异位词 - 力扣](https://leetcode.cn/problems/dKk3P7/) ## 题目大意 给定两个字符串 `s` 和 `t`。 要求:判断 `t` 和 `s` 是否使用了相同的字符构成(字符出现的种类和数目都相同,字符顺序不完全相同)。 ## 解题思路 1. 先判断字符串 `s` 和 `t` 的长度,不一样直接返回 `False`; 2. 如果 `s` 和 `t` 相等,则直接返回 `False`,因为变位词的字符顺序不完全相同; 3. 分别遍历字符串 `s` 和 `t`。先遍历字符串 `s`,用哈希表存储字符串 `s` 中字符出现的频次; 4. 再遍历字符串 `t`,哈希表中减去对应字符的频次,出现频次小于 `0` 则输出 `False`; 5. 如果没出现频次小于 `0`,则输出 `True`。 ## 代码 ```python class Solution: def isAnagram(self, s: str, t: str) -> bool: if len(s) != len(t) or s == t: return False strDict = dict() for ch in s: if ch in strDict: strDict[ch] += 1 else: strDict[ch] = 1 for ch in t: if ch in strDict: strDict[ch] -= 1 if strDict[ch] < 0: return False else: return False return True ``` ================================================ FILE: docs/solutions/LCR/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof.md ================================================ # [LCR 135. 报数](https://leetcode.cn/problems/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof/) - 标签:数组、数学 - 难度:简单 ## 题目链接 - [LCR 135. 报数 - 力扣](https://leetcode.cn/problems/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof/) ## 题目大意 给定一个数字 `n`。 要求:按顺序打印从 `1` 到最大 `n` 位的十进制数。 ## 解题思路 直接枚举 $1 \sim 10^{n} - 1$,生成列表并返回。 ## 代码 ```python class Solution: def printNumbers(self, n: int) -> List[int]: return [i for i in range(1, 10 ** n)] ``` ================================================ FILE: docs/solutions/LCR/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof.md ================================================ # [LCR 169. 招式拆解 II](https://leetcode.cn/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof/) - 标签:队列、哈希表、字符串、计数 - 难度:简单 ## 题目链接 - [LCR 169. 招式拆解 II - 力扣](https://leetcode.cn/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof/) ## 题目大意 给定一个字符串 `s`。 要求:从字符串 `s` 中找到第一个只出现一次的字符。如果没有,则返回空格 ` `。 ## 解题思路 遍历字符串 `s`,使用哈希表存储每个字符频数。 再次遍历字符串 `s`,返回第一个频数为 `1` 的字符。 ## 代码 ```python class Solution: def firstUniqChar(self, s: str) -> str: dic = dict() for ch in s: if ch in dic: dic[ch] += 1 else: dic[ch] = 1 for ch in s: if ch in dic and dic[ch] == 1: return ch return ' ' ``` ================================================ FILE: docs/solutions/LCR/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof.md ================================================ # [LCR 139. 训练计划 I](https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/) - 标签:数组、双指针、排序 - 难度:简单 ## 题目链接 - [LCR 139. 训练计划 I - 力扣](https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/) ## 题目大意 **描述**:给定一个整数数组 $nums$。 **要求**:将奇数元素位于数组的前半部分,偶数元素位于数组的后半部分。 **说明**: - $0 \le nums.length \le 50000$。 - $0 \le nums[i] \le 10000$。 **示例**: - 示例 1: ```python 输入:nums = [1,2,3,4,5] 输出:[1,3,5,2,4] 解释:为正确答案之一 ``` ## 解题思路 ### 思路 1:快慢指针 定义快慢指针 $slow$、$fast$,开始时都指向 $0$。 - $fast$ 向前搜索奇数位置,$slow$ 指向下一个奇数应当存放的位置。 - $fast$ 不断进行右移,当遇到奇数时,将该奇数与 $slow$ 指向的元素进行交换,并将 $slow$ 进行右移。 - 重复上面操作,直到 $fast$ 指向数组末尾。 ### 思路 1:代码 ```python class Solution: def exchange(self, nums: List[int]) -> List[int]: slow, fast = 0, 0 while fast < len(nums): if nums[fast] % 2 == 1: nums[slow], nums[fast] = nums[fast], nums[slow] slow += 1 fast += 1 return nums ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为数组 $nums$ 中的元素个数。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/LCR/dui-cheng-de-er-cha-shu-lcof.md ================================================ # [LCR 145. 判断对称二叉树](https://leetcode.cn/problems/dui-cheng-de-er-cha-shu-lcof/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [LCR 145. 判断对称二叉树 - 力扣](https://leetcode.cn/problems/dui-cheng-de-er-cha-shu-lcof/) ## 题目大意 给定一个二叉树的根节点 `root`。 要求:检查这课二叉树是否是左右对称的。 ## 解题思路 递归遍历左右子树, 然后判断当前节点的左右子节点。如果可以直接判断的情况,则跳出递归,直接返回结果。如果无法直接判断结果,则递归检测左右子树的外侧节点是否相等,同理再递归检测左右子树的内侧节点是否相等。 ## 代码 ```python class Solution: def isSymmetric(self, root: TreeNode) -> bool: if not root: return True return self.check(root.left, root.right) def check(self, left: TreeNode, right: TreeNode): if not left and not right: return True elif not left and right: return False elif left and not right: return False elif left.val != right.val: return False return self.check(left.left, right.right) and self.check(left.right, right.left) ``` ================================================ FILE: docs/solutions/LCR/dui-lie-de-zui-da-zhi-lcof.md ================================================ # [LCR 184. 设计自助结算系统](https://leetcode.cn/problems/dui-lie-de-zui-da-zhi-lcof/) - 标签:设计、队列、单调队列 - 难度:中等 ## 题目链接 - [LCR 184. 设计自助结算系统 - 力扣](https://leetcode.cn/problems/dui-lie-de-zui-da-zhi-lcof/) ## 题目大意 要求:设计一个「队列」,实现 `max_value` 函数,可通过 `max_value` 得到大年队列的最大值。并且要求 `max_value`、`push_back`、`pop_front` 的均摊时间复杂度都是 `O(1)`。 ## 解题思路 利用空间换时间,使用两个队列。其中一个为原始队列 `queue`,另一个为递减队列 `deque`,`deque` 用来保存队列的最大值,具体做法如下: - `push_back` 操作:如果 `deque` 队尾元素小于即将入队的元素 `value`,则将小于 `value` 的元素全部出队,再将 `valuew` 入队。否则直接将 `value` 直接入队,这样 `deque` 队首元素保存的就是队列的最大值。 - `pop_front` 操作:先判断 `deque`、`queue` 是否为空,如果 `deque` 或者 `queue` 为空,则说明队列为空,直接返回 `-1`。如果都不为空,从 `queue` 中取出一个元素,并跟 `deque` 队首元素进行比较,如果两者相等则需要将 `deque` 队首元素弹出。 - `max_value` 操作:如果 `deque` 不为空,则返回 `deque` 队首元素。否则返回 `-1`。 ## 代码 ```python import collections import queue class MaxQueue: def __init__(self): self.queue = queue.Queue() self.deque = collections.deque() def max_value(self) -> int: if self.deque: return self.deque[0] else: return -1 def push_back(self, value: int) -> None: while self.deque and self.deque[-1] < value: self.deque.pop() self.deque.append(value) self.queue.put(value) def pop_front(self) -> int: if not self.deque or not self.queue: return -1 ans = self.queue.get() if ans == self.deque[0]: self.deque.popleft() return ans ``` ================================================ FILE: docs/solutions/LCR/er-cha-shu-de-jing-xiang-lcof.md ================================================ # [LCR 144. 翻转二叉树](https://leetcode.cn/problems/er-cha-shu-de-jing-xiang-lcof/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [LCR 144. 翻转二叉树 - 力扣](https://leetcode.cn/problems/er-cha-shu-de-jing-xiang-lcof/) ## 题目大意 给定一个二叉树的根节点 `root`。 要求:将其进行左右翻转。 ## 解题思路 从根节点开始遍历,然后从叶子节点向上递归交换左右子树位置。 ## 代码 ```python class Solution: def mirrorTree(self, root: TreeNode) -> TreeNode: if not root: return root left = self.mirrorTree(root.left) right = self.mirrorTree(root.right) root.left = right root.right = left return root ``` ================================================ FILE: docs/solutions/LCR/er-cha-shu-de-shen-du-lcof.md ================================================ # [LCR 175. 计算二叉树的深度](https://leetcode.cn/problems/er-cha-shu-de-shen-du-lcof/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [LCR 175. 计算二叉树的深度 - 力扣](https://leetcode.cn/problems/er-cha-shu-de-shen-du-lcof/) ## 题目大意 给定一个二叉树的根节点 `root`。 要求:找出树的深度。 - 深度:从根节点到叶节点一次经过的节点形成一条路径,最长路径的长度为树的深度。 ## 解题思路 递归遍历,先递归遍历左右子树,返回左右子树的高度,则当前节点的高度为左右子树最大深度 + 1。即 `max(left_height, right_height) + 1`。 ## 代码 ```python class Solution: def maxDepth(self, root: TreeNode) -> int: if root == None: return 0 left_height = self.maxDepth(root.left) right_height = self.maxDepth(root.right) return max(left_height, right_height) + 1 ``` ================================================ FILE: docs/solutions/LCR/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof.md ================================================ # [LCR 194. 二叉树的最近公共祖先](https://leetcode.cn/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/) - 标签:树、深度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [LCR 194. 二叉树的最近公共祖先 - 力扣](https://leetcode.cn/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/) ## 题目大意 给定一个二叉树的根节点 `root`,再给定两个指定节点 `p`、`q`。 要求:找到两个指定节点 `p`、`q` 的最近公共祖先。 - 祖先:如果节点 `p` 在节点 `node` 的左子树或右子树中,或者 `p == node`,则称 `node` 是 `p` 的祖先。 - 最近公共祖先:对于树的两个节点 `p`、`q`,最近公共祖先表示为一个节点 `lca_node`,满足 `lca_node` 是 `p`、`q` 的祖先且 `lca_node` 的深度尽可能大(一个节点也可以是自己的祖先) ## 解题思路 设 `lca_node` 为节点 `p`、`q` 的最近公共祖先。则 `lca_node` 只能是下面几种情况: - `p`、`q` 在 `lca_node` 的子树中,且分别在 `lca_node` 的两侧子树中。 - `p = lca_node`,且 `q` 在 `lca_node` 的左子树或右子树中。 - `q = lca_node`,且 `p` 在 `lca_node` 的左子树或右子树中。 下面递归求解 `lca_node`。递归需要满足以下条件: - 如果 `p`、`q` 都不为空,则返回 `p`、`q` 的公共祖先。 - 如果 `p`、`q` 只有一个存在,则返回存在的一个。 - 如果 `p`、`q` 都不存在,则返回存在的一个。 具体思路为: - 如果当前节点 `node` 为 `None`,则说明 `p`、`q` 不在 `node` 的子树中,不可能为公共祖先,直接返回 `None`。 - 如果当前节点 `node` 等于 `p` 或者 `q`,那么 `node` 就是 `p`、`q` 的最近公共祖先,直接返回 `node` - 递归遍历左子树、右子树,并判断左右子树结果。 - 如果左子树为空,则返回右子树。 - 如果右子树为空,则返回左子树。 - 如果左右子树都不为空,则说明 `p`、`q` 在当前根节点的两侧,当前根节点就是他们的最近公共祖先。 - 如果左右子树都为空,则返回空。 ## 代码 ```python class Solution: def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode: if root == p or root == q: return root if root: node_left = self.lowestCommonAncestor(root.left, p, q) node_right = self.lowestCommonAncestor(root.right, p, q) if node_left and node_right: return root elif not node_left: return node_right else: return node_left return None ``` ================================================ FILE: docs/solutions/LCR/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof.md ================================================ # [LCR 153. 二叉树中和为目标值的路径](https://leetcode.cn/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/) - 标签:树、深度优先搜索、回溯、二叉树 - 难度:中等 ## 题目链接 - [LCR 153. 二叉树中和为目标值的路径 - 力扣](https://leetcode.cn/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/) ## 题目大意 给定一棵二叉树的根节点 `root` 和一个整数 `target`。 要求:打印出二叉树中各节点的值的和为 `target` 的所有路径。从根节点开始往下一直到叶节点所经过的节点形成一条路径。 ## 解题思路 回溯求解。在回溯的同时,记录下当前路径。同时维护 `target`,每遍历到一个节点,就减去该节点值。如果遇到叶子节点,并且 `target == 0` 时,将当前路径加入答案数组中。然后递归遍历左右子树,并回退当前节点,继续遍历。 ## 代码 ```python class Solution: def pathSum(self, root: TreeNode, target: int) -> List[List[int]]: res = [] path = [] def dfs(root: TreeNode, target: int): if not root: return path.append(root.val) target -= root.val if not root.left and not root.right and target == 0: res.append(path[:]) dfs(root.left, target) dfs(root.right, target) path.pop() dfs(root, target) return res ``` ================================================ FILE: docs/solutions/LCR/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof.md ================================================ # [LCR 174. 寻找二叉搜索树中的目标节点](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:简单 ## 题目链接 - [LCR 174. 寻找二叉搜索树中的目标节点 - 力扣](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/) ## 题目大意 **描述**:给定一棵二叉搜索树的根节点 $root$,以及一个整数 $k$。 **要求**:找出二叉搜索树书第 $k$ 大的节点。 **说明**: - **示例**: - 示例 1: ![](https://pic.leetcode.cn/1695101634-kzHKZW-image.png) ```python 输入:root = [7, 3, 9, 1, 5], cnt = 2 7 / \ 3 9 / \ 1 5 输出:7 ``` - 示例 2: ![](https://pic.leetcode.cn/1695101636-ESZtLa-image.png) ```python 输入: root = [10, 5, 15, 2, 7, null, 20, 1, null, 6, 8], cnt = 4 10 / \ 5 15 / \ \ 2 7 20 / / \ 1 6 8 输出: 8 ``` ## 解题思路 ### 思路 1:遍历 已知中序遍历「左 -> 根 -> 右」能得到递增序列。逆中序遍历「右 -> 根 -> 左」可以得到递减序列。 则根据「右 -> 根 -> 左」递归遍历 k 次,找到第 $k$ 个节点位置,并记录答案。 ### 思路 1:代码 ```python class Solution: res = 0 k = 0 def dfs(self, root): if not root: return self.dfs(root.right) if self.k == 0: return self.k -= 1 if self.k == 0: self.res = root.val return self.dfs(root.left) def kthLargest(self, root: TreeNode, k: int) -> int: self.res = 0 self.k = k self.dfs(root) return self.res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$,其中 $n$ 为树中节点数量。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/LCR/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof.md ================================================ # [LCR 152. 验证二叉搜索树的后序遍历序列](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/) - 标签:栈、树、二叉搜索树、递归、二叉树、单调栈 - 难度:中等 ## 题目链接 - [LCR 152. 验证二叉搜索树的后序遍历序列 - 力扣](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/) ## 题目大意 **描述**:给定一个整数数组 $postorder$。数组的任意两个数字都互不相同。 **要求**:判断该数组是不是某二叉搜索树的后序遍历结果。如果是,则返回 `True`,否则返回 `False`。 **说明**: - 数组长度 <= 1000。 - $postorder$ 中无重复数字。 **示例**: - 示例 1: ![](https://pic.leetcode.cn/1694762751-fwHhWX-%E5%89%91%E6%8C%8733%E7%A4%BA%E4%BE%8B1.png) ```python 输入: postorder = [4,9,6,9,8] 输出: false 解释:从上图可以看出这不是一颗二叉搜索树 ``` - 示例 2: ![](https://pic.leetcode.cn/1694762510-vVpTic-%E5%89%91%E6%8C%8733.png) ```python 输入: postorder = [4,6,5,9,8] 输出: true 解释:可构建的二叉搜索树如上图 ``` ## 解题思路 ### 思路 1:递归分治 后序遍历的顺序为:左 -> 右 -> 根。而二叉搜索树的定义是:左子树所有节点值 < 根节点值,右子树所有节点值 > 根节点值。 所以,可以把数组最右侧元素作为二叉搜索树的根节点值。然后判断数组的左右两侧是否符合左侧值都小于该节点值,右侧值都大于该节点值。如果不满足,则说明不是某二叉搜索树的后序遍历结果。 找到左右分界线位置,然后递归左右数组继续查找。 终止条件为数组 开始位置 > 结束位置,此时该树的子节点数目小于等于 $1$,直接返回 `True` 即可。 ### 思路 1:代码 ```python class Solution: def verifyPostorder(self, postorder: List[int]) -> bool: def verify(left, right): if left >= right: return True index = left while postorder[index] < postorder[right]: index += 1 mid = index while postorder[index] > postorder[right]: index += 1 return index == right and verify(left, mid - 1) and verify(mid, right - 1) if len(postorder) <= 2: return True return verify(0, len(postorder) - 1) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n^2)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/LCR/er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof.md ================================================ # [LCR 193. 二叉搜索树的最近公共祖先](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:简单 ## 题目链接 - [LCR 193. 二叉搜索树的最近公共祖先 - 力扣](https://leetcode.cn/problems/er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof/) ## 题目大意 给定一棵二叉搜索树的根节点 `root` 和两个指定节点 `p`、`q`。 要求:找到该树中两个指定节点 `p`、`q` 的最近公共祖先。 - 祖先:如果节点 `p` 在节点 `node` 的左子树或右子树中,或者 `p == node`,则称 `node` 是 `p` 的祖先。 - 最近公共祖先:对于树的两个节点 `p`、`q`,最近公共祖先表示为一个节点 `lca_node`,满足 `lca_node` 是 `p`、`q` 的祖先且 `lca_node` 的深度尽可能大(一个节点也可以是自己的祖先) ## 解题思路 对于节点 `p`、节点 `q`,最近公共祖先就是从根节点分别到它们路径上的分岔点,也是路径中最后一个相同的节点,现在我们的问题就是求这个分岔点。 使用递归遍历查找最近公共祖先。 - 从根节点开始遍历; - 如果当前节点的值大于 `p`、`q` 的值,说明 `p` 和 `q` 应该在当前节点的左子树,因此将当前节点移动到它的左子节点,继续遍历; - 如果当前节点的值小于 `p`、`q` 的值,说明 `p` 和 `q` 应该在当前节点的右子树,因此将当前节点移动到它的右子节点,继续遍历; - 如果当前节点不满足上面两种情况,则说明 `p` 和 `q` 分别在当前节点的左右子树上,则当前节点就是分岔点,直接返回该节点即可。 ## 代码 ```python class Solution: def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': ancestor = root while True: if ancestor.val > p.val and ancestor.val > q.val: ancestor = ancestor.left elif ancestor.val < p.val and ancestor.val < q.val: ancestor = ancestor.right else: break return ancestor ``` ================================================ FILE: docs/solutions/LCR/er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof.md ================================================ # [LCR 155. 将二叉搜索树转化为排序的双向链表](https://leetcode.cn/problems/er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof/) - 标签:栈、树、深度优先搜索、二叉搜索树、链表、二叉树、双向链表 - 难度:中等 ## 题目链接 - [LCR 155. 将二叉搜索树转化为排序的双向链表 - 力扣](https://leetcode.cn/problems/er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof/) ## 题目大意 给定一棵二叉树的根节点 `root`。 要求:将这棵二叉树转换为一个排序的循环双向链表。要求不能创建新的节点,只能调整树中节点指针的指向。 ## 解题思路 通过中序递归遍历可以将二叉树升序排列输出。这道题需要在中序遍历的同时,将节点的左右指向进行改变。使用 `head`、`tail` 存放双向链表的头尾节点,然后从根节点开始,进行中序递归遍历。 具体做法如下: - 如果当前节点为空,直接返回。 - 如果当前节点不为空: - 递归遍历左子树。 - 如果尾节点不为空,则将尾节点与当前节点进行连接。 - 如果尾节点为空,则初始化头节点。 - 将当前节点标记为尾节点。 - 递归遍历右子树。 - 最后将头节点和尾节点进行连接。 ## 代码 ```python class Solution: def treeToDoublyList(self, root: 'Node') -> 'Node': def dfs(node: 'Node'): if not node: return dfs(node.left) if self.tail: self.tail.right = node node.left = self.tail else: self.head = node self.tail = node dfs(node.right) if not root: return None self.head, self.tail = None, None dfs(root) self.head.left = self.tail self.tail.right = self.head return self.head ``` ================================================ FILE: docs/solutions/LCR/er-jin-zhi-zhong-1de-ge-shu-lcof.md ================================================ # [LCR 133. 位 1 的个数](https://leetcode.cn/problems/er-jin-zhi-zhong-1de-ge-shu-lcof/) - 标签:位运算 - 难度:简单 ## 题目链接 - [LCR 133. 位 1 的个数 - 力扣](https://leetcode.cn/problems/er-jin-zhi-zhong-1de-ge-shu-lcof/) ## 题目大意 给定一个无符号整数 `n`。 要求:统计其对应二进制表达式中 `1` 的个数。 ## 解题思路 ### 1. 循环按位计算 对整数 n 的每一位进行按位与运算,并统计结果。 ### 2. 改进位运算 利用 $n \text{ \& } (n-1)$ 。这个运算刚好可以将 n 的二进制中最低位的 $1$ 变为 $0$。 比如 $n = 6$ 时,$6 = (110)_2$,$6 - 1 = (101)_2$,$(110)_2 \text{ \& } (101)_2 = (100)_2$ 。 利用这个位运算,不断的将 $n$ 中最低位的 $1$ 变为 $0$,直到 $n$ 变为 $0$ 即可,其变换次数就是我们要求的结果。 ## 代码 1. 循环按位计算 ```python class Solution: def hammingWeight(self, n: int) -> int: ans = 0 while n: ans += (n & 1) n = n >> 1 return ans ``` 2. 改进位运算 ```python class Solution: def hammingWeight(self, n: int) -> int: ans = 0 while n: n &= n-1 ans += 1 return ans ``` ================================================ FILE: docs/solutions/LCR/er-wei-shu-zu-zhong-de-cha-zhao-lcof.md ================================================ # [LCR 121. 寻找目标值 - 二维数组](https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/) - 标签:数组、二分查找、分治、矩阵 - 难度:中等 ## 题目链接 - [LCR 121. 寻找目标值 - 二维数组 - 力扣](https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/) ## 题目大意 给定一个 `m * n` 大小的有序整数矩阵 `matrix`。每行元素从左到右升序排列,每列元素从上到下升序排列。再给定一个目标值 `target`。 要求:判断矩阵中是否可以找到 `target`,如果找到 `target`,返回 `True`,否则返回 `False`。 ## 解题思路 矩阵是有序的,可以考虑使用二分搜索来进行查找。 迭代对角线元素,假设对角线元素的坐标为 `(row, col)`。把数组元素按对角线分为右上角部分和左下角部分。 则对于当前对角线元素右侧第 `row` 行、对角线元素下侧第 `col` 列进行二分查找。 - 如果找到目标,直接返回 `True`。 - 如果找不到目标,则缩小范围,继续查找。 - 直到所有对角线元素都遍历完,依旧没找到,则返回 `False`。 ## 代码 ```python class Solution: def diagonalBinarySearch(self, matrix, diagonal, target): left = 0 right = diagonal while left < right: mid = left + (right - left) // 2 if matrix[mid][mid] < target: left = mid + 1 else: right = mid return left def rowBinarySearch(self, matrix, begin, cols, target): left = begin right = cols while left < right: mid = left + (right - left) // 2 if matrix[begin][mid] < target: left = mid + 1 elif matrix[begin][mid] > target: right = mid - 1 else: left = mid break return begin <= left <= cols and matrix[begin][left] == target def colBinarySearch(self, matrix, begin, rows, target): left = begin + 1 right = rows while left < right: mid = left + (right - left) // 2 if matrix[mid][begin] < target: left = mid + 1 elif matrix[mid][begin] > target: right = mid - 1 else: left = mid break return begin <= left <= rows and matrix[left][begin] == target def findNumberIn2DArray(self, matrix: List[List[int]], target: int) -> bool: rows = len(matrix) if rows == 0: return False cols = len(matrix[0]) if cols == 0: return False min_val = min(rows, cols) index = self.diagonalBinarySearch(matrix, min_val - 1, target) if matrix[index][index] == target: return True for i in range(index + 1): row_search = self.rowBinarySearch(matrix, i, cols - 1, target) col_search = self.colBinarySearch(matrix, i, rows - 1, target) if row_search or col_search: return True return False ``` ================================================ FILE: docs/solutions/LCR/fan-zhuan-dan-ci-shun-xu-lcof.md ================================================ # [LCR 181. 字符串中的单词反转](https://leetcode.cn/problems/fan-zhuan-dan-ci-shun-xu-lcof/) - 标签:双指针、字符串 - 难度:简单 ## 题目链接 - [LCR 181. 字符串中的单词反转 - 力扣](https://leetcode.cn/problems/fan-zhuan-dan-ci-shun-xu-lcof/) ## 题目大意 给定一个字符串 `s`。 要求:逐个翻转字符串中所有的单词。 说明: - 数组字符串 `s` 可以再前面、后面或者单词间包含多余的空格。 - 翻转后的单词应当只有一个空格分隔。 - 翻转后的字符串不应该包含额外的空格。 ## 解题思路 最简单的就是调用 API 进行切片,翻转。复杂一点的也可以根据 API 的思路写出模拟代码。 ## 代码 ```python class Solution: def reverseWords(self, s: str) -> str: return " ".join(reversed(s.split())) ``` ================================================ FILE: docs/solutions/LCR/fan-zhuan-lian-biao-lcof.md ================================================ # [LCR 141. 训练计划 III](https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/) - 标签:递归、链表 - 难度:简单 ## 题目链接 - [LCR 141. 训练计划 III - 力扣](https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/) ## 题目大意 **描述**:给定一个链表的头节点 `head`。 **要求**:将该链表反转并输出反转后链表的头节点。 ## 解题思路 ### 思路 1. 迭代 1. 使用两个指针 `cur` 和 `pre` 进行迭代。`pre` 指向 `cur` 前一个节点位置。初始时,`pre` 指向 `None`,`cur` 指向 `head`。 2. 将 `pre` 和 `cur` 的前后指针进行交换,指针更替顺序为: 1. 使用 `next` 指针保存当前节点 `cur` 的后一个节点,即 `next = cur.next`; 2. 断开当前节点 `cur` 的后一节点链接,将 `cur` 的 `next` 指针指向前一节点 `pre`,即 `cur.next = pre`; 3. `pre` 向前移动一步,移动到 `cur` 位置,即 `pre = cur`; 4. `cur` 向前移动一步,移动到之前 `next` 指针保存的位置,即 `cur = next`。 3. 继续执行第 2 步中的 1、2、3、4。 4. 最后等到 `cur` 遍历到链表末尾,即 `cur == None`,时,`pre` 所在位置就是反转后链表的头节点,返回新的头节点 `pre`。 使用迭代法反转链表的示意图如下所示: ![迭代法反转链表](https://qcdn.itcharge.cn/images/20220111133639.png) ### 思路 2. 递归 具体做法如下: - 首先定义递归函数含义为:将链表反转,并返回反转后的头节点。 - 然后从 `head.next` 的位置开始调用递归函数,即将 `head.next` 为头节点的链表进行反转,并返回该链表的头节点。 - 递归到链表的最后一个节点,将其作为最终的头节点,即为 `new_head`。 - 在每次递归函数返回的过程中,改变 `head` 和 `head.next` 的指向关系。也就是将 `head.next` 的`next` 指针先指向当前节点 `head`,即 `head.next.next = head `。 - 然后让当前节点 `head` 的 `next` 指针指向 `None`,从而实现从链表尾部开始的局部反转。 - 当递归从末尾开始顺着递归栈的退出,从而将整个链表进行反转。 - 最后返回反转后的链表头节点 `new_head`。 使用递归法反转链表的示意图如下所示: ![递归法反转链表](https://qcdn.itcharge.cn/images/20220111134246.png) ## 代码 1. 迭代 ```python class Solution: def reverseList(self, head: ListNode) -> ListNode: pre = None cur = head while cur != None: next = cur.next cur.next = pre pre = cur cur = next return pre ``` 2. 递归 ```python class Solution: def reverseList(self, head: ListNode) -> ListNode: if head == None or head.next == None: return head new_head = self.reverseList(head.next) head.next.next = head head.next = None return new_head ``` ## 参考资料 - 【题解】[反转链表 - 反转链表 - 力扣](https://leetcode.cn/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-by-leetcode-solution-d1k2/) - 【题解】[【反转链表】:双指针,递归,妖魔化的双指针 - 反转链表 - 力扣(LeetCode)](https://leetcode.cn/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-shuang-zhi-zhen-di-gui-yao-mo-/) ================================================ FILE: docs/solutions/LCR/fei-bo-na-qi-shu-lie-lcof.md ================================================ # [LCR 126. 斐波那契数](https://leetcode.cn/problems/fei-bo-na-qi-shu-lie-lcof/) - 标签:记忆化搜索、数学、动态规划 - 难度:简单 ## 题目链接 - [LCR 126. 斐波那契数 - 力扣](https://leetcode.cn/problems/fei-bo-na-qi-shu-lie-lcof/) ## 题目大意 给定一个整数 `n`。 要求:计算斐波那契数列的第 `n` 项。 注意:答案需对 `1000000007` 进行取余操作。 ## 解题思路 斐波那契的递推公式为:`F(n) = F(n-1) + F(n-2)`。 直接根据递推公式求解即可。注意答案需要取余。 ## 代码 ```python class Solution: def fib(self, n: int) -> int: if n < 2: return n f1 = 0 f2 = 0 f3 = 1 for i in range(2, n + 1): f1, f2 = f2, f3 f3 = (f1 + f2) % 1000000007 return f3 ``` ================================================ FILE: docs/solutions/LCR/fpTFWP.md ================================================ # [LCR 112. 矩阵中的最长递增路径](https://leetcode.cn/problems/fpTFWP/) - 标签:深度优先搜索、广度优先搜索、图、拓扑排序、记忆化搜索、数组、动态规划、矩阵 - 难度:困难 ## 题目链接 - [LCR 112. 矩阵中的最长递增路径 - 力扣](https://leetcode.cn/problems/fpTFWP/) ## 题目大意 给定一个 `m * n` 大小的整数矩阵 `matrix`。要求:找出其中最长递增路径的长度。 对于每个单元格,可以往上、下、左、右四个方向移动,不能向对角线方向移动或移动到边界外。 ## 解题思路 深度优先搜索。使用二维数组 `record` 存储遍历过的单元格最大路径长度,已经遍历过的单元格就不需要再次遍历了。 ## 代码 ```python class Solution: max_len = 0 directions = {(1, 0), (-1, 0), (0, 1), (0, -1)} def longestIncreasingPath(self, matrix: List[List[int]]) -> int: if not matrix: return 0 rows, cols = len(matrix), len(matrix[0]) record = [[0 for _ in range(cols)] for _ in range(rows)] def dfs(i, j): record[i][j] = 1 for direction in self.directions: new_i, new_j = i + direction[0], j + direction[1] if 0 <= new_i < rows and 0 <= new_j < cols and matrix[new_i][new_j] > matrix[i][j]: if record[new_i][new_j] == 0: dfs(new_i, new_j) record[i][j] = max(record[i][j], record[new_i][new_j] + 1) self.max_len = max(self.max_len, record[i][j]) for i in range(rows): for j in range(cols): if record[i][j] == 0: dfs(i, j) return self.max_len ``` ================================================ FILE: docs/solutions/LCR/fu-za-lian-biao-de-fu-zhi-lcof.md ================================================ # [LCR 154. 复杂链表的复制](https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof/) - 标签:哈希表、链表 - 难度:中等 ## 题目链接 - [LCR 154. 复杂链表的复制 - 力扣](https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof/) ## 题目大意 给定一个链表,每个节点除了 `next` 指针之后,还包含一个随机指针 `random`,该指针可以指向链表中的任何节点或者空节点。 要求:将该链表进行深拷贝。 ## 解题思路 遍历链表,利用哈希表,以旧节点:新节点为映射关系,将节点关系存储下来。 再次遍历链表,将新链表的 `next` 和 `random` 指针设置好。 ## 代码 ```python class Solution: def copyRandomList(self, head: 'Node') -> 'Node': if not head: return None node_dict = dict() curr = head while curr: new_node = Node(curr.val, None, None) node_dict[curr] = new_node curr = curr.next curr = head while curr: if curr.next: node_dict[curr].next = node_dict[curr.next] if curr.random: node_dict[curr].random = node_dict[curr.random] curr = curr.next return node_dict[head] ``` ================================================ FILE: docs/solutions/LCR/g5c51o.md ================================================ # [LCR 060. 前 K 个高频元素](https://leetcode.cn/problems/g5c51o/) - 标签:数组、哈希表、分治、桶排序、计数、快速选择、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [LCR 060. 前 K 个高频元素 - 力扣](https://leetcode.cn/problems/g5c51o/) ## 题目大意 给定一个整数数组 `nums` 和一个整数 `k`。 要求:返回出现频率前 `k` 高的元素。可以按任意顺序返回答案。 ## 解题思路 - 使用哈希表记录下数组中各个元素的频数。时间复杂度 $O(n)$,空间复杂度 $O(n)$。 - 然后将哈希表中的元素去重,转换为新数组。时间复杂度 $O(n)$,空间复杂度 $O(n)$。 - 利用建立大顶堆,此时堆顶元素即为频数最高的元素。时间复杂度 $O(n)$,空间复杂度 $O(n)$。 - 将堆顶元素加入到答案数组中,并交换堆顶元素与末尾元素,此时末尾元素已移出堆。继续调整大顶堆。时间复杂度 $O(log{n})$。 - 调整玩大顶堆之后,此时堆顶元素为频数第二高的元素,和上一步一样,将其加入到答案数组中,继续交换堆顶元素与末尾元素,继续调整大顶堆。 - 不断重复上步,直到 k 次结束。调整 k 次的时间复杂度 $O(nlog{n})$。 总体时间复杂度 $O(nlog{n})$。 因为用的是大顶堆,堆的规模是 N 个元素,调整 k 次,所以时间复杂度是 $O(nlog{n})$。 如果用小顶堆,只需维护 k 个元素的小顶堆,不断向堆中替换元素即可,时间复杂度为 $O(nlog{k})$。 ## 代码 ```python class Solution: # 调整为大顶堆 def heapify(self, nums, nums_dict, index, end): left = index * 2 + 1 right = left + 1 while left <= end: # 当前节点为非叶子节点 max_index = index if nums_dict[nums[left]] > nums_dict[nums[max_index]]: max_index = left if right <= end and nums_dict[nums[right]] > nums_dict[nums[max_index]]: max_index = right if index == max_index: # 如果不用交换,则说明已经交换结束 break nums[index], nums[max_index] = nums[max_index], nums[index] # 继续调整子树 index = max_index left = index * 2 + 1 right = left + 1 # 初始化大顶堆 def buildMaxHeap(self, nums, nums_dict): size = len(nums) # (size-2) // 2 是最后一个非叶节点,叶节点不用调整 for i in range((size - 2) // 2, -1, -1): self.heapify(nums, nums_dict, i, size - 1) return nums # 堆排序方法(本题未用到) def maxHeapSort(self, nums, nums_dict): self.buildMaxHeap(nums) size = len(nums) for i in range(size): nums[0], nums[size - i - 1] = nums[size - i - 1], nums[0] self.heapify(nums, nums_dict, 0, size - i - 2) return nums def topKFrequent(self, nums: List[int], k: int) -> List[int]: # 统计元素频数 nums_dict = dict() for num in nums: if num in nums_dict: nums_dict[num] += 1 else: nums_dict[num] = 1 # 使用 set 方法去重,得到新数组 new_nums = list(set(nums)) size = len(new_nums) # 初始化大顶堆 self.buildMaxHeap(new_nums, nums_dict) res = list() for i in range(k): # 堆顶元素为当前堆中频数最高的元素,将其加入答案中 res.append(new_nums[0]) # 交换堆顶和末尾元素,继续调整大顶堆 new_nums[0], new_nums[size - i - 1] = new_nums[size - i - 1], new_nums[0] self.heapify(new_nums, nums_dict, 0, size - i - 2) return res ``` ================================================ FILE: docs/solutions/LCR/gaM7Ch.md ================================================ # [LCR 103. 零钱兑换](https://leetcode.cn/problems/gaM7Ch/) - 标签:广度优先搜索、数组、动态规划 - 难度:中等 ## 题目链接 - [LCR 103. 零钱兑换 - 力扣](https://leetcode.cn/problems/gaM7Ch/) ## 题目大意 给定不同面额的硬币 `coins` 和一个总金额 `amount`。 乔秋:计算出凑成总金额所需的最少的硬币个数。如果无法凑出,则返回 `-1`。 ## 解题思路 完全背包问题。 可以转换为有 `n` 枚不同的硬币,每种硬币可以无限次使用。凑成总金额为 `amount` 的背包,最少需要多少硬币。 动态规划的状态 `dp[i]` 可以表示为:凑成总金额为 `i` 的组合中,至少有 `dp[i]` 枚硬币。 动态规划的状态转移方程为:`dp[i] = min(dp[i], + dp[i-coin] + 1`,意思为凑成总金额为 `i` 最少硬币数量 = 「不使用当前 `coin`,只使用之前硬币凑成金额 `i` 的最少硬币数量」和「凑成金额 `i - num` 的最少硬币数量,再加上当前硬币」两者的较小值。 ## 代码 ```python class Solution: def coinChange(self, coins: List[int], amount: int) -> int: dp = [float('inf') for _ in range(amount + 1)] dp[0] = 0 for coin in coins: for i in range(coin, amount + 1): dp[i] = min(dp[i], dp[i - coin] + 1) if dp[amount] != float('inf'): return dp[amount] else: return -1 ``` ================================================ FILE: docs/solutions/LCR/gou-jian-cheng-ji-shu-zu-lcof.md ================================================ # [LCR 191. 按规则计算统计结果](https://leetcode.cn/problems/gou-jian-cheng-ji-shu-zu-lcof/) - 标签:数组、前缀和 - 难度:中等 ## 题目链接 - [LCR 191. 按规则计算统计结果 - 力扣](https://leetcode.cn/problems/gou-jian-cheng-ji-shu-zu-lcof/) ## 题目大意 给定一个数组 `A`。 要求:构建一个数组 `B`,其中 `B[i]` 为数组 `A` 中除了 `A[i]` 之外的其他所有元素乘积。 要求不能使用除法。 ## 解题思路 构造一个答案数组 `B`,长度和数组 `A` 长度一致。先从左到右遍历一遍 `A` 数组,将 `A[i]` 左侧的元素乘积累积起来,存储到 `B` 数组中。再从右到左遍历一遍,将 `A[i]` 右侧的元素乘积累积起来,再乘以原本 `B[i]` 的值,即为 `A` 中除了 `A[i]` 之外的其他所有元素乘积。 ## 代码 ```python class Solution: def constructArr(self, a: List[int]) -> List[int]: size = len(a) b = [1 for _ in range(size)] left = 1 for i in range(size): b[i] *= left left *= a[i] right = 1 for i in range(size - 1, -1, -1): b[i] *= right right *= a[i] return b ``` ================================================ FILE: docs/solutions/LCR/gu-piao-de-zui-da-li-run-lcof.md ================================================ # [LCR 188. 买卖芯片的最佳时机](https://leetcode.cn/problems/gu-piao-de-zui-da-li-run-lcof/) - 标签:数组、动态规划 - 难度:中等 ## 题目链接 - [LCR 188. 买卖芯片的最佳时机 - 力扣](https://leetcode.cn/problems/gu-piao-de-zui-da-li-run-lcof/) ## 题目大意 给定一个数组 `nums`,`nums[i]` 表示一支给定股票第 `i` 天的价格。选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。求能获取的最大利润。 ## 解题思路 最简单的思路当然是两重循环暴力枚举,寻找不同天数下的最大利润。 但更好的做法是进行一次遍历。设置两个变量 `minprice`(用来记录买入的最小值)、`maxprofit`(用来记录可获取的最大利润)。 进行一次遍历,遇到当前价格比 `minprice` 还要小的,就更新 `minprice`。如果单签价格大于或者等于 `minprice`,则判断一下以当前价格卖出的话能卖多少,如果比 `maxprofit` 还要大,就更新 `maxprofit`。 ## 代码 ```python class Solution: def maxProfit(self, prices: List[int]) -> int: minprice = 10010 maxprofit = 0 for price in prices: if price < minprice: minprice = price elif price - minprice > maxprofit: maxprofit = price - minprice return maxprofit ``` ================================================ FILE: docs/solutions/LCR/h54YBf.md ================================================ # [LCR 048. 二叉树的序列化与反序列化](https://leetcode.cn/problems/h54YBf/) - 标签:树、深度优先搜索、广度优先搜索、设计、字符串、二叉树 - 难度:困难 ## 题目链接 - [LCR 048. 二叉树的序列化与反序列化 - 力扣](https://leetcode.cn/problems/h54YBf/) ## 题目大意 要求:设计一个算法,来实现二叉树的序列化与反序列化。 ## 解题思路 ### 1. 序列化:将二叉树转为字符串数据表示 按照前序递归遍历二叉树,并将根节点跟左右子树的值链接起来(中间用 `,` 隔开)。 注意:如果遇到空节点,则标记为 'None',这样在反序列化时才能唯一确定一棵二叉树。 ### 2. 反序列化:将字符串数据转为二叉树结构 先将字符串按 `,` 分割成数组。然后递归处理每一个元素。 - 从数组左侧取出一个元素。 - 如果当前元素为 'None',则返回 None。 - 如果当前元素不为空,则新建一个二叉树节点作为根节点,保存值为当前元素值。并递归遍历左右子树,不断重复从数组中取出元素,进行判断。 - 最后返回当前根节点。 ## 代码 ```python class Codec: def serialize(self, root): """Encodes a tree to a single string. :type root: TreeNode :rtype: str """ if not root: return 'None' return str(root.val) + ',' + str(self.serialize(root.left)) + ',' + str(self.serialize(root.right)) def deserialize(self, data): """Decodes your encoded data to tree. :type data: str :rtype: TreeNode """ def dfs(datalist): val = datalist.pop(0) if val == 'None': return None root = TreeNode(int(val)) root.left = dfs(datalist) root.right = dfs(datalist) return root datalist = data.split(',') return dfs(datalist) ``` ================================================ FILE: docs/solutions/LCR/hPov7L.md ================================================ # [LCR 044. 在每个树行中找最大值](https://leetcode.cn/problems/hPov7L/) - 标签:树、深度优先搜索、广度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [LCR 044. 在每个树行中找最大值 - 力扣](https://leetcode.cn/problems/hPov7L/) ## 题目大意 给定一棵二叉树的根节点 `root`。 要求:找出二叉树中每一层的最大值。 ## 解题思路 利用队列进行层序遍历,并记录下每一层的最大值,将其存入答案数组中。 ## 代码 ```python class Solution: def largestValues(self, root: TreeNode) -> List[int]: queue = [] res = [] if root: queue.append(root) while queue: max_level = float('-inf') size_level = len(queue) for i in range(size_level): node = queue.pop(0) max_level = max(max_level, node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) res.append(max_level) return res ``` ================================================ FILE: docs/solutions/LCR/he-bing-liang-ge-pai-xu-de-lian-biao-lcof.md ================================================ # [LCR 142. 训练计划 IV](https://leetcode.cn/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof/) - 标签:递归、链表 - 难度:简单 ## 题目链接 - [LCR 142. 训练计划 IV - 力扣](https://leetcode.cn/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof/) ## 题目大意 给定两个升序链表。 要求:将其合并为一个升序链表。 ## 解题思路 利用归并排序的思想。 创建一个新的链表节点作为头节点(记得保存),然后判断 l1和 l2 头节点的值,将较小值的节点添加到新的链表中。 当一个节点添加到新的链表中之后,将对应的 l1 或 l2 链表向后移动一位。 然后继续判断当前 l1 节点和当前 l2 节点的值,继续将较小值的节点添加到新的链表中,然后将对应的链表向后移动一位。 这样,当 l1 或 l2 遍历到最后,最多有一个链表还有节点未遍历,则直接将该节点链接到新的链表尾部即可。 ## 代码 ```python class Solution: def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode: newHead = ListNode(-1) curr = newHead while l1 and l2: if l1.val <= l2.val: curr.next = l1 l1 = l1.next else: curr.next = l2 l2 = l2.next curr = curr.next curr.next = l1 if l1 is not None else l2 return newHead.next ``` ================================================ FILE: docs/solutions/LCR/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof.md ================================================ # [LCR 180. 文件组合](https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/) - 标签:数学、双指针、枚举 - 难度:简单 ## 题目链接 - [LCR 180. 文件组合 - 力扣](https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/) ## 题目大意 **描述**:给定一个正整数 `target`。 **要求**:输出所有和为 `target` 的连续正整数序列(至少含有两个数)。序列中的数字由小到大排列,不同序列按照首个数字从小到大排列。 **说明**: - $1 \le target \le 10^5$。 **示例**: - 示例 1: ```python 输入:target = 9 输出:[[2,3,4],[4,5]] ``` - 示例 2: ```python 输入:target = 15 输出:[[1,2,3,4,5],[4,5,6],[7,8]] ``` ## 解题思路 ### 思路 1:枚举算法 连续正整数序列中元素的最小值大于等于 `1`,而最大值不会超过 `target`。所以我们可以枚举可行的区间,并计算出区间和,将其与 `target` 进行比较,如果相等则将对应的区间元素加入答案数组中,最终返回答案数组。 因为题目要求至少含有两个数,则序列开始元素不会超过 `target` 的一半,所以序列开始元素可以从 `1` 开始,枚举到 `target // 2` 即可。 具体步骤如下: 1. 使用列表变量 `res` 作为答案数组。 2. 使用一重循环 `i`,用于枚举序列开始位置,枚举范围为 `[1, target // 2]`。 3. 使用变量 `cur_sum` 维护当前区间的区间和,`cur_sum` 初始为 `0`。 4. 使用第 `2` 重循环 `j`,用于枚举序列的结束位置,枚举范围为 `[i, target - 1]`,并累积计算当前区间的区间和,即 `cur_sum += j`。 1. 如果当前区间的区间和大于 `target`,则跳出循环。 2. 如果当前区间的区间和等于 `target`,则将区间上的元素保存为列表,并添加到答案数组中,然后跳出第 `2` 重循环。 5. 遍历完返回答案数组。 ### 思路 1:代码 ```python class Solution: def findContinuousSequence(self, target: int) -> List[List[int]]: res = [] for i in range(1, target // 2 + 1): cur_sum = 0 for j in range(i, target): cur_sum += j if cur_sum > target: break if cur_sum == target: cur_res = [] for k in range(i, j + 1): cur_res.append(k) res.append(cur_res) break return res ``` ### 思路 1:复杂度分析 - **时间复杂度**:$target \times \sqrt{target}$。 - **空间复杂度**:$O(1)$。 ### 思路 2:滑动窗口 具体做法如下: - 初始化窗口,令 `left = 1`,`right = 2`。 - 计算 `sum = (left + right) * (right - left + 1) // 2`。 - 如果 `sum == target`,时,将其加入答案数组中。 - 如果 `sum < target` 时,说明需要扩大窗口,则 `right += 1`。 - 如果 `sum > target` 时,说明需要缩小窗口,则 `left += 1`。 - 直到 `left >= right` 时停止,返回答案数组。 ### 思路 2:滑动窗口代码 ```python class Solution: def findContinuousSequence(self, target: int) -> List[List[int]]: left, right = 1, 2 res = [] while left < right: sum = (left + right) * (right - left + 1) // 2 if sum == target: arr = [] for i in range(0, right - left + 1): arr.append(i + left) res.append(arr) left += 1 elif sum < target: right += 1 else: left += 1 return res ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(target)$。 - **空间复杂度**:$O(1)$。 ================================================ FILE: docs/solutions/LCR/he-wei-sde-liang-ge-shu-zi-lcof.md ================================================ # [LCR 179. 查找总价格为目标值的两个商品](https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/) - 标签:数组、双指针、二分查找 - 难度:简单 ## 题目链接 - [LCR 179. 查找总价格为目标值的两个商品 - 力扣](https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/) ## 题目大意 给定一个升序数组 `nums`,以及一个目标整数 `target`。 要求:在数组中查找两个数,使它们的和刚好等于 `target`。 ## 解题思路 因为数组是升序的,可以使用双指针。`left`、`right` 分别指向数组首尾位置。 - 计算 `sum = nums[left] + nums[right]`。 - 如果 `sum > target`,则 `right` 进行左移。 - 如果 `sum < target`,则 `left` 进行右移。 - 如果 `sum == target`,则返回 `[nums[left], nums[right]]`。 ## 代码 ```python class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: left, right = 0, len(nums) - 1 while left < right: sum = nums[left] + nums[right] if sum > target: right -= 1 elif sum < target: left += 1 else: return nums[left], nums[right] return [] ``` ================================================ FILE: docs/solutions/LCR/hua-dong-chuang-kou-de-zui-da-zhi-lcof.md ================================================ # [LCR 183. 望远镜中最高的海拔](https://leetcode.cn/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/) - 标签:队列、滑动窗口、单调队列、堆(优先队列) - 难度:困难 ## 题目链接 - [LCR 183. 望远镜中最高的海拔 - 力扣](https://leetcode.cn/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/) ## 题目大意 给定一个整数数组 `nums` 和滑动窗口的大小 `k`。表示为大小为 `k` 的滑动窗口从数组的最左侧移动到数组的最右侧。我们只能看到滑动窗口内的 `k` 个数字,滑动窗口每次只能向右移动一位。 要求:返回滑动窗口中的最大值。 ## 解题思路 暴力求解的话,二重循环,时间复杂度为 $O(n * k)$。 我们可以使用优先队列,每次窗口移动时想优先队列中增加一个节点,并删除一个节点。将窗口中的最大值加入到答案数组中。 ## 代码 ```python class Solution: def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: size = len(nums) if size == 0: return [] q = [(-nums[i], i) for i in range(k)] heapq.heapify(q) res = [-q[0][0]] for i in range(k, size): heapq.heappush(q, (-nums[i], i)) while q[0][1] <= i - k: heapq.heappop(q) res.append(-q[0][0]) return res ``` ================================================ FILE: docs/solutions/LCR/iIQa4I.md ================================================ # [LCR 038. 每日温度](https://leetcode.cn/problems/iIQa4I/) - 标签:栈、数组、单调栈 - 难度:中等 ## 题目链接 - [LCR 038. 每日温度 - 力扣](https://leetcode.cn/problems/iIQa4I/) ## 题目大意 给定一个列表 `temperatures`,每一个位置对应每天的气温。要求输出一个列表,列表上每个位置代表如果要观测到更高的气温,至少需要等待的天数。如果之后的气温不再升高,则用 `0` 来代替。 ## 解题思路 题目的意思实际上就是给定一个数组,每个位置上有整数值。对于每个位置,在该位置后侧找到第一个比当前值更高的值。求该点与该位置的距离,将所有距离保存为数组返回结果。 很简单的思路是对于每个温度值,向后依次进行搜索,找到比当前温度更高的值。 更好的方式使用「递减栈」。栈中保存元素的下标。 首先,将答案数组全部赋值为 0。然后遍历数组每个位置元素。 - 如果栈为空,则将当前元素的下标入栈。 - 如果栈不为空,且当前数字大于栈顶元素对应数字,则栈顶元素出栈,并计算下标差。 - 此时当前元素就是栈顶元素的下一个更高值,将其下标差存入答案数组中保存起来,判断栈顶元素。 - 直到当前数字小于或等于栈顶元素,则停止出栈,将当前元素下标入栈。 ## 代码 ```python class Solution: def dailyTemperatures(self, temperatures: List[int]) -> List[int]: n = len(temperatures) stack = [] ans = [0 for _ in range(n)] for i in range(n): while stack and temperatures[i] > temperatures[stack[-1]]: index = stack.pop() ans[index] = (i - index) stack.append(i) return ans ``` ================================================ FILE: docs/solutions/LCR/iSwD2y.md ================================================ # [LCR 065. 单词的压缩编码](https://leetcode.cn/problems/iSwD2y/) - 标签:字典树、数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [LCR 065. 单词的压缩编码 - 力扣](https://leetcode.cn/problems/iSwD2y/) ## 题目大意 给定一个单词数组 `words`。要求对 `words` 进行编码成一个助记字符串,用来帮助记忆。`words` 中拥有相同字符后缀的单词可以合并成一个单词,比如`time` 和 `me` 可以合并成 `time`。同时每个不能再合并的单词末尾以 `#` 为结束符,将所有合并后的单词排列起来就是一个助记字符串。 要求:返回对 `words` 进行编码的最小助记字符串 `s` 的长度。 ## 解题思路 构建一个字典树。然后对字符串长度进行从小到大排序。 再依次将去重后的所有单词插入到字典树中。如果出现比当前单词更长的单词,则将短单词的结尾置为 `False`,意为替换掉短单词。 然后再依次在字典树中查询所有单词,「单词长度 + 1」就是当前不能在合并的单词,累加起来就是答案。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = False cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None and cur.isEnd class Solution: def minimumLengthEncoding(self, words: List[str]) -> int: trie_tree = Trie() words = list(set(words)) words.sort(key=lambda i: len(i)) ans = 0 for word in words: trie_tree.insert(word[::-1]) for word in words: if trie_tree.search(word[::-1]): ans += len(word) + 1 return ans ``` ================================================ FILE: docs/solutions/LCR/index.md ================================================ ## 本章内容 - [LCR 001. 两数相除](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/xoh6Oh.md) - [LCR 002. 二进制求和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/JFETK5.md) - [LCR 003. 比特位计数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/w3tCBm.md) - [LCR 004. 只出现一次的数字 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/WGki4K.md) - [LCR 005. 最大单词长度乘积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/aseY1I.md) - [LCR 006. 两数之和 II - 输入有序数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/kLl5u1.md) - [LCR 007. 三数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/1fGaJU.md) - [LCR 008. 长度最小的子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/2VG8Kg.md) - [LCR 009. 乘积小于 K 的子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ZVAVXX.md) - [LCR 010. 和为 K 的子数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/QTMn0o.md) - [LCR 011. 连续数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/A1NYOS.md) - [LCR 012. 寻找数组的中心下标](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/tvdfij.md) - [LCR 013. 二维区域和检索 - 矩阵不可变](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/O4NDxx.md) - [LCR 016. 无重复字符的最长子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/wtcaE1.md) - [LCR 017. 最小覆盖子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/M1oyTv.md) - [LCR 018. 验证回文串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/XltzEq.md) - [LCR 019. 验证回文串 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/RQku0D.md) - [LCR 020. 回文子串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/a7VOhD.md) - [LCR 021. 删除链表的倒数第 N 个结点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/SLwz0R.md) - [LCR 022. 环形链表 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/c32eOV.md) - [LCR 023. 相交链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/3u1WK4.md) - [LCR 024. 反转链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/UHnkqh.md) - [LCR 025. 两数相加 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/lMSNwu.md) - [LCR 026. 重排链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/LGjMqU.md) - [LCR 027. 回文链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/aMhZSa.md) - [LCR 028. 扁平化多级双向链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/Qv1Da2.md) - [LCR 029. 循环有序列表的插入](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/4ueAj6.md) - [LCR 030. O(1) 时间插入、删除和获取随机元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/FortPu.md) - [LCR 031. LRU 缓存](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/OrIXps.md) - [LCR 032. 有效的字母异位词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/dKk3P7.md) - [LCR 033. 字母异位词分组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/sfvd7V.md) - [LCR 034. 验证外星语词典](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/lwyVBB.md) - [LCR 035. 最小时间差](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/569nqc.md) - [LCR 036. 逆波兰表达式求值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/8Zf90G.md) - [LCR 037. 行星碰撞](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/XagZNi.md) - [LCR 038. 每日温度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/iIQa4I.md) - [LCR 039. 柱状图中最大的矩形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/0ynMMM.md) - [LCR 041. 数据流中的移动平均值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/qIsx9U.md) - [LCR 042. 最近的请求次数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/H8086Q.md) - [LCR 043. 完全二叉树插入器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/NaqhDT.md) - [LCR 044. 在每个树行中找最大值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/hPov7L.md) - [LCR 045. 找树左下角的值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/LwUNpT.md) - [LCR 046. 二叉树的右视图](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/WNC0Lk.md) - [LCR 047. 二叉树剪枝](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/pOCWxh.md) - [LCR 048. 二叉树的序列化与反序列化](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/h54YBf.md) - [LCR 049. 求根节点到叶节点数字之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/3Etpl5.md) - [LCR 050. 路径总和 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/6eUYwP.md) - [LCR 051. 二叉树中的最大路径和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/jC7MId.md) - [LCR 052. 递增顺序搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/NYBBNL.md) - [LCR 053. 二叉搜索树中的中序后继](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/P5rCT8.md) - [LCR 054. 把二叉搜索树转换为累加树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/w6cpku.md) - [LCR 055. 二叉搜索树迭代器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/kTOapQ.md) - [LCR 056. 两数之和 IV - 输入二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/opLdQZ.md) - [LCR 057. 存在重复元素 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/7WqeDu.md) - [LCR 059. 数据流中的第 K 大元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/jBjn9C.md) - [LCR 060. 前 K 个高频元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/g5c51o.md) - [LCR 062. 实现 Trie (前缀树)](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/QC3q1f.md) - [LCR 063. 单词替换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/UhWRSj.md) - [LCR 064. 实现一个魔法字典](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/US1pGT.md) - [LCR 065. 单词的压缩编码](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/iSwD2y.md) - [LCR 066. 键值映射](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/z1R5dt.md) - [LCR 067. 数组中两个数的最大异或值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ms70jA.md) - [LCR 068. 搜索插入位置](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/N6YdxV.md) - [LCR 072. x 的平方根](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/jJ0w9p.md) - [LCR 073. 爱吃香蕉的狒狒](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/nZZqjQ.md) - [LCR 074. 合并区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/SsGoHC.md) - [LCR 075. 数组的相对排序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/0H97ZC.md) - [LCR 076. 数组中的第 K 个最大元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/xx4gT2.md) - [LCR 077. 排序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/7WHec2.md) - [LCR 078. 合并 K 个升序链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/vvXgSW.md) - [LCR 079. 子集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/TVdhkn.md) - [LCR 080. 组合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/uUsW3B.md) - [LCR 081. 组合总和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/Ygoe9J.md) - [LCR 082. 组合总和 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/4sjJUc.md) - [LCR 083. 全排列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/VvJkup.md) - [LCR 084. 全排列 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/7p8L0Z.md) - [LCR 085. 括号生成](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/IDBivT.md) - [LCR 086. 分割回文串](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/M99OJA.md) - [LCR 087. 复原 IP 地址](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/0on3uN.md) - [LCR 088. 使用最小花费爬楼梯](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/GzCJIP.md) - [LCR 089. 打家劫舍](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/Gu0c2T.md) - [LCR 090. 打家劫舍 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/PzWKhm.md) - [LCR 093. 最长的斐波那契子序列的长度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/Q91FMA.md) - [LCR 095. 最长公共子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/qJnOS7.md) - [LCR 097. 不同的子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/21dk04.md) - [LCR 098. 不同路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/2AoeFn.md) - [LCR 101. 分割等和子集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/NUPfPr.md) - [LCR 102. 目标和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/YaVDxD.md) - [LCR 103. 零钱兑换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/gaM7Ch.md) - [LCR 104. 组合总和 Ⅳ](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/D0F0SV.md) - [LCR 105. 岛屿的最大面积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ZL6zAn.md) - [LCR 106. 判断二分图](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/vEAB3K.md) - [LCR 107. 01 矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/2bCMpM.md) - [LCR 108. 单词接龙](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/om3reC.md) - [LCR 109. 打开转盘锁](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zlDJc7.md) - [LCR 111. 除法求值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/vlzXQL.md) - [LCR 112. 矩阵中的最长递增路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/fpTFWP.md) - [LCR 113. 课程表 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/QA2IGt.md) - [LCR 116. 省份数量](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/bLyHh0.md) - [LCR 118. 冗余连接](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/7LpjUW.md) - [LCR 119. 最长连续序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/WhsWhI.md) - [LCR 120. 寻找文件副本](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-zhong-fu-de-shu-zi-lcof.md) - [LCR 121. 寻找目标值 - 二维数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-wei-shu-zu-zhong-de-cha-zhao-lcof.md) - [LCR 122. 路径加密](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ti-huan-kong-ge-lcof.md) - [LCR 123. 图书整理 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/cong-wei-dao-tou-da-yin-lian-biao-lcof.md) - [LCR 124. 推理二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zhong-jian-er-cha-shu-lcof.md) - [LCR 125. 图书整理 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/yong-liang-ge-zhan-shi-xian-dui-lie-lcof.md) - [LCR 126. 斐波那契数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/fei-bo-na-qi-shu-lie-lcof.md) - [LCR 127. 跳跃训练](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/qing-wa-tiao-tai-jie-wen-ti-lcof.md) - [LCR 128. 库存管理 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof.md) - [LCR 129. 字母迷宫](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ju-zhen-zhong-de-lu-jing-lcof.md) - [LCR 130. 衣橱整理](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ji-qi-ren-de-yun-dong-fan-wei-lcof.md) - [LCR 131. 砍竹子 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/jian-sheng-zi-lcof.md) - [LCR 133. 位 1 的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-jin-zhi-zhong-1de-ge-shu-lcof.md) - [LCR 134. Pow(x, n)](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zhi-de-zheng-shu-ci-fang-lcof.md) - [LCR 135. 报数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof.md) - [LCR 136. 删除链表的节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shan-chu-lian-biao-de-jie-dian-lcof.md) - [LCR 139. 训练计划 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof.md) - [LCR 140. 训练计划 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof.md) - [LCR 141. 训练计划 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/fan-zhuan-lian-biao-lcof.md) - [LCR 142. 训练计划 IV](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/he-bing-liang-ge-pai-xu-de-lian-biao-lcof.md) - [LCR 143. 子结构判断](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-de-zi-jie-gou-lcof.md) - [LCR 144. 翻转二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-shu-de-jing-xiang-lcof.md) - [LCR 145. 判断对称二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/dui-cheng-de-er-cha-shu-lcof.md) - [LCR 146. 螺旋遍历二维数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shun-shi-zhen-da-yin-ju-zhen-lcof.md) - [LCR 147. 最小栈](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/bao-han-minhan-shu-de-zhan-lcof.md) - [LCR 148. 验证图书取出顺序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zhan-de-ya-ru-dan-chu-xu-lie-lcof.md) - [LCR 149. 彩灯装饰记录 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/cong-shang-dao-xia-da-yin-er-cha-shu-lcof.md) - [LCR 150. 彩灯装饰记录 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof.md) - [LCR 151. 彩灯装饰记录 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof.md) - [LCR 152. 验证二叉搜索树的后序遍历序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof.md) - [LCR 153. 二叉树中和为目标值的路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof.md) - [LCR 154. 复杂链表的复制](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/fu-za-lian-biao-de-fu-zhi-lcof.md) - [LCR 155. 将二叉搜索树转化为排序的双向链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof.md) - [LCR 156. 序列化与反序列化二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/xu-lie-hua-er-cha-shu-lcof.md) - [LCR 157. 套餐内商品的排列顺序](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zi-fu-chuan-de-pai-lie-lcof.md) - [LCR 158. 库存管理 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof.md) - [LCR 159. 库存管理 III](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zui-xiao-de-kge-shu-lcof.md) - [LCR 160. 数据流中的中位数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-ju-liu-zhong-de-zhong-wei-shu-lcof.md) - [LCR 161. 连续天数的最高销售额](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/lian-xu-zi-shu-zu-de-zui-da-he-lcof.md) - [LCR 163. 找到第 k 位数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof.md) - [LCR 164. 破解闯关密码](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof.md) - [LCR 165. 解密数字](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof.md) - [LCR 166. 珠宝的最高价值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/li-wu-de-zui-da-jie-zhi-lcof.md) - [LCR 167. 招式拆解 I](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof.md) - [LCR 168. 丑数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/chou-shu-lcof.md) - [LCR 169. 招式拆解 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof.md) - [LCR 170. 交易逆序对的总数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-de-ni-xu-dui-lcof.md) - [LCR 171. 训练计划 V](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof.md) - [LCR 172. 统计目标成绩的出现次数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof.md) - [LCR 173. 点名](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/que-shi-de-shu-zi-lcof.md) - [LCR 174. 寻找二叉搜索树中的目标节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof.md) - [LCR 175. 计算二叉树的深度](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-shu-de-shen-du-lcof.md) - [LCR 176. 判断是否为平衡二叉树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ping-heng-er-cha-shu-lcof.md) - [LCR 177. 撞色搭配](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof.md) - [LCR 179. 查找总价格为目标值的两个商品](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/he-wei-sde-liang-ge-shu-zi-lcof.md) - [LCR 180. 文件组合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof.md) - [LCR 181. 字符串中的单词反转](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/fan-zhuan-dan-ci-shun-xu-lcof.md) - [LCR 182. 动态口令](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/zuo-xuan-zhuan-zi-fu-chuan-lcof.md) - [LCR 183. 望远镜中最高的海拔](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/hua-dong-chuang-kou-de-zui-da-zhi-lcof.md) - [LCR 184. 设计自助结算系统](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/dui-lie-de-zui-da-zhi-lcof.md) - [LCR 186. 文物朝代判断](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/bu-ke-pai-zhong-de-shun-zi-lcof.md) - [LCR 187. 破冰游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof.md) - [LCR 188. 买卖芯片的最佳时机](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/gu-piao-de-zui-da-li-run-lcof.md) - [LCR 189. 设计机械累加器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/qiu-12n-lcof.md) - [LCR 190. 加密运算](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof.md) - [LCR 191. 按规则计算统计结果](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/gou-jian-cheng-ji-shu-zu-lcof.md) - [LCR 192. 把字符串转换成整数 (atoi)](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof.md) - [LCR 193. 二叉搜索树的最近公共祖先](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-sou-suo-shu-de-zui-jin-gong-gong-zu-xian-lcof.md) - [LCR 194. 二叉树的最近公共祖先](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof.md) ================================================ FILE: docs/solutions/LCR/jBjn9C.md ================================================ # [LCR 059. 数据流中的第 K 大元素](https://leetcode.cn/problems/jBjn9C/) - 标签:树、设计、二叉搜索树、二叉树、数据流、堆(优先队列) - 难度:简单 ## 题目链接 - [LCR 059. 数据流中的第 K 大元素 - 力扣](https://leetcode.cn/problems/jBjn9C/) ## 题目大意 设计一个 ` KthLargest` 类,用于找到数据流中第 `k` 大元素。 - `KthLargest(int k, int[] nums)`:使用整数 `k` 和整数流 `nums` 初始化对象。 - `int add(int val)`:将 `val` 插入数据流 `nums` 后,返回当前数据流中第 `k` 大的元素。 ## 解题思路 - 建立大小为 `k` 的大顶堆,堆中元素保证不超过 k 个。 - 每次 `add` 操作时,将新元素压入堆中,如果堆中元素超出了 `k` 个,则将堆中最小元素(堆顶)移除。 - 此时堆中最小元素(堆顶)就是整个数据流中的第 `k` 大元素。 ## 代码 ```python import heapq class KthLargest: def __init__(self, k: int, nums: List[int]): self.min_heap = [] self.k = k for num in nums: heapq.heappush(self.min_heap, num) if len(self.min_heap) > k: heapq.heappop(self.min_heap) def add(self, val: int) -> int: heapq.heappush(self.min_heap, val) if len(self.min_heap) > self.k: heapq.heappop(self.min_heap) return self.min_heap[0] ``` ================================================ FILE: docs/solutions/LCR/jC7MId.md ================================================ # [LCR 051. 二叉树中的最大路径和](https://leetcode.cn/problems/jC7MId/) - 标签:树、深度优先搜索、动态规划、二叉树 - 难度:困难 ## 题目链接 - [LCR 051. 二叉树中的最大路径和 - 力扣](https://leetcode.cn/problems/jC7MId/) ## 题目大意 给定一个二叉树的根节点 `root`。 要求:返回其最大路径和。 - 路径:从树中的任意节点出发,沿父节点——子节点连接,到达任意节点的序列。同一个节点在一条路径序列中至多出现一次。该路径至少包含一个节点,且不一定经过根节点。 - 路径和:路径中各节点值的总和。 ## 解题思路 深度优先搜索遍历二叉树。递归的同时,维护一个最大路径和变量。定义函数 `dfs(self, root)` 计算二叉树中以该节点为根节点,并且经过该节点的最大贡献值。 计算的结果可能的情况有 2 种: - 经过空节点的最大贡献值等于 `0`。 - 经过非空节点的最大贡献值等于 当前节点值 + 左右子节点的最大贡献值中较大的一个。 在递归时,我们先计算左右子节点的最大贡献值,再更新维护当前最大路径和变量。 最终 `max_sum` 即为答案。 ## 代码 ```python class Solution: def __init__(self): self.max_sum = float('-inf') def dfs(self, root): if not root: return 0 left_max = max(self.dfs(root.left), 0) right_max = max(self.dfs(root.right), 0) self.max_sum = max(self.max_sum, root.val + left_max + right_max) return root.val + max(left_max, right_max) def maxPathSum(self, root: TreeNode) -> int: self.dfs(root) return self.max_sum ``` ================================================ FILE: docs/solutions/LCR/jJ0w9p.md ================================================ # [LCR 072. x 的平方根](https://leetcode.cn/problems/jJ0w9p/) - 标签:数学、二分查找 - 难度:简单 ## 题目链接 - [LCR 072. x 的平方根 - 力扣](https://leetcode.cn/problems/jJ0w9p/) ## 题目大意 要求:实现 `int sqrt(int x)` 函数。计算并返回 `x` 的平方根(只保留整数部分),其中 `x` 是非负整数。 ## 解题思路 因为求解的是 x 开方的整数部分。所以我们可以从 0~x 的范围进行遍历,找到 k^2 <= x 的最大结果。 为了减少时间复杂度,使用二分查找的方式来搜索答案。 ## 代码 ```python class Solution: def mySqrt(self, x: int) -> int: left = 0 right = x ans = -1 while left <= right: mid = (left + right) // 2 if mid * mid <= x: ans = mid left = mid + 1 else: right = mid - 1 return ans ``` ================================================ FILE: docs/solutions/LCR/ji-qi-ren-de-yun-dong-fan-wei-lcof.md ================================================ # [LCR 130. 衣橱整理](https://leetcode.cn/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/) - 标签:深度优先搜索、广度优先搜索、动态规划 - 难度:中等 ## 题目链接 - [LCR 130. 衣橱整理 - 力扣](https://leetcode.cn/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/) ## 题目大意 **描述**:有一个 `m * n` 大小的方格,坐标从 `(0, 0)` 到 `(m - 1, n - 1)`。一个机器人从 `(0, 0)` 处的格子开始移动,每次可以向上、下、左、右移动一格(不能移动到方格外),也不能移动到行坐标和列坐标的数位之和大于 `k` 的格子。现在给定 `3` 个整数 `m`、`n`、`k`。 **要求**:计算并输出该机器人能够达到多少个格子。 **说明**: - $1 \le n, m \le 100$。 - $0 \le k \le 20$。 **示例**: - 示例 1: ```python 输入:m = 2, n = 3, k = 1 输出:3 ``` - 示例 2: ```python 输入:m = 3, n = 1, k = 0 输出:1 ``` ## 解题思路 ### 思路 1:广度优先搜索 先定义一个计算数位和的方法 `digitsum`,该方法输入一个整数,返回该整数各个数位的总和。 然后我们使用广度优先搜索方法,具体步骤如下: - 将 `(0, 0)` 加入队列 `queue` 中。 - 当队列不为空时,每次将队首坐标弹出,加入访问集合 `visited` 中。 - 再将满足行列坐标的数位和不大于 `k` 的格子位置加入到队列中,继续弹出队首位置。 - 直到队列为空时停止。输出访问集合的长度。 ### 思路 1:代码 ```python import collections class Solution: def digitsum(self, n: int): ans = 0 while n: ans += n % 10 n //= 10 return ans def movingCount(self, m: int, n: int, k: int) -> int: queue = collections.deque([(0, 0)]) visited = set() while queue: x, y = queue.popleft() if (x, y) not in visited and self.digitsum(x) + self.digitsum(y) <= k: visited.add((x, y)) for dx, dy in [(1, 0), (0, 1)]: nx = x + dx ny = y + dy if 0 <= nx < m and 0 <= ny < n: queue.append((nx, ny)) return len(visited) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m \times n)$。其中 $m$ 为方格的行数,$n$ 为方格的列数。 - **空间复杂度**:$O(m \times n)$。 ================================================ FILE: docs/solutions/LCR/jian-sheng-zi-lcof.md ================================================ # [LCR 131. 砍竹子 I](https://leetcode.cn/problems/jian-sheng-zi-lcof/) - 标签:数学、动态规划 - 难度:中等 ## 题目链接 - [LCR 131. 砍竹子 I - 力扣](https://leetcode.cn/problems/jian-sheng-zi-lcof/) ## 题目大意 给定一根长度为 `n` 的绳子,将绳子剪成整数长度的 `m` 段,每段绳子长度即为 `k[0]`、`k[1]`、...、`k[m - 1]`。 要求:计算出 `k[0] * k[1] * ... * k[m - 1]` 可能的最大乘积。 ## 解题思路 可以使用动态规划求解。 定义状态 `dp[i]` 为:拆分长度为 `i` 的绳子,可以获得的最大乘积为 `dp[i]`。 将 `j` 从 `1` 遍历到 `i - 1`,通过两种方式得到 `dp[i]`: - `(i - j) * j` ,直接将长度为 `i` 的绳子分割为 `i - j` 和 `j`,获取两者乘积。 - `dp[i - j] * j`,将长度为 `i`的绳子 中的 `i - j` 部分拆分,得到 `dp[i - j]`,和 `j` ,获取乘积。 则 `dp[i]` 取两者中的最大值。遍历 `j`,得到 `dp[i]` 的最大值。 则状态转移方程为:`dp[i] = max(dp[i], (i - j) * j, dp[i - j] * j)`。 最终输出 `dp[n]`。 ## 代码 ```python class Solution: def cuttingRope(self, n: int) -> int: dp = [0 for _ in range(n + 1)] dp[1] = 1 for i in range(2, n + 1): for j in range(1, i): dp[i] = max(dp[i], dp[i - j] * j, (i - j) * j) return dp[n] ``` ================================================ FILE: docs/solutions/LCR/ju-zhen-zhong-de-lu-jing-lcof.md ================================================ # [LCR 129. 字母迷宫](https://leetcode.cn/problems/ju-zhen-zhong-de-lu-jing-lcof/) - 标签:数组、回溯、矩阵 - 难度:中等 ## 题目链接 - [LCR 129. 字母迷宫 - 力扣](https://leetcode.cn/problems/ju-zhen-zhong-de-lu-jing-lcof/) ## 题目大意 给定一个 `m * n` 大小的二维字符矩阵 `board` 和一个字符串单词 `word`。如果 `word` 存在于网格中,返回 `True`,否则返回 `False`。 - 单词必须按照字母顺序通过上下左右相邻的单元格字母构成。且同一个单元格内的字母不允许被重复使用。 ## 解题思路 回溯算法在二维矩阵 `board` 中按照上下左右四个方向递归搜索。设函数 `backtrack(i, j, index)` 表示从 `board[i][j]` 出发,能否搜索到单词字母 `word[index]`,以及 `index` 位置之后的后缀子串。如果能搜索到,则返回 `True`,否则返回 `False`。`backtrack(i, j, index)` 执行步骤如下: - 如果 $board[i][j] = word[index]$,而且 `index` 已经到达 `word` 字符串末尾,则返回 `True`。 - 如果 $board[i][j] = word[index]$,而且 `index` 未到达 `word` 字符串末尾,则遍历当前位置的所有相邻位置。如果从某个相邻位置能搜索到后缀子串,则返回 `True`,否则返回 `False`。 - 如果 $board[i][j] \ne word[index]$,则当前字符不匹配,返回 `False`。 ## 代码 ```python class Solution: def exist(self, board: List[List[str]], word: str) -> bool: directs = [(0, 1), (0, -1), (1, 0), (-1, 0)] rows = len(board) if rows == 0: return False cols = len(board[0]) visited = [[False for _ in range(cols)] for _ in range(rows)] def backtrack(i, j, index): if index == len(word) - 1: return board[i][j] == word[index] if board[i][j] == word[index]: visited[i][j] = True for direct in directs: new_i = i + direct[0] new_j = j + direct[1] if 0 <= new_i < rows and 0 <= new_j < cols and visited[new_i][new_j] == False: if backtrack(new_i, new_j, index + 1): return True visited[i][j] = False return False for i in range(rows): for j in range(cols): if backtrack(i, j, 0): return True return False ``` ================================================ FILE: docs/solutions/LCR/kLl5u1.md ================================================ # [LCR 006. 两数之和 II - 输入有序数组](https://leetcode.cn/problems/kLl5u1/) - 标签:数组、双指针、二分查找 - 难度:简单 ## 题目链接 - [LCR 006. 两数之和 II - 输入有序数组 - 力扣](https://leetcode.cn/problems/kLl5u1/) ## 题目大意 给定一个升序数组:`numbers` 和一个目标值 `target`。 要求:从数组中找出满足相加之和等于 `target` 的两个数,并返回两个数在数组中下的标值。 ## 解题思路 因为数组是有序的,所以我们可以使用两个指针 low,high。low 指向数组开始较小元素位置,high 指向数组较大元素位置。判断两个位置上的元素和,如果和等于目标值,则返回两个元素位置。如果和大于目标值,则 high 左移,继续检测。如果和小于目标值,则 low 右移,继续检测。直到 low 和 high 移动到相同位置停止检测。如果最终仍没找到,则返回 [0, 0]。 ## 代码 ```python class Solution: def twoSum(self, numbers: List[int], target: int) -> List[int]: low = 0 high = len(numbers) - 1 while low < high: total = numbers[low] + numbers[high] if total == target: return [low, high] elif total < target: low += 1 else: high -= 1 return [0, 0] ``` ================================================ FILE: docs/solutions/LCR/kTOapQ.md ================================================ # [LCR 055. 二叉搜索树迭代器](https://leetcode.cn/problems/kTOapQ/) - 标签:栈、树、设计、二叉搜索树、二叉树、迭代器 - 难度:中等 ## 题目链接 - [LCR 055. 二叉搜索树迭代器 - 力扣](https://leetcode.cn/problems/kTOapQ/) ## 题目大意 要求:实现一个二叉搜索树的迭代器 `BSTIterator`。表示一个按中序遍历二叉搜索树(BST)的迭代器: - `def __init__(self, root: TreeNode):`:初始化 `BSTIterator` 类的一个对象,会给出二叉搜索树的根节点。 - `def hasNext(self) -> bool:`:如果向右指针遍历存在数字,则返回 `True`,否则返回 `False`。 - `def next(self) -> int:`:将指针向右移动,返回指针处的数字。 ## 解题思路 中序遍历的顺序是:左、根、右。我们使用一个栈来保存节点,以便于迭代的时候取出对应节点。 - 初始的遍历当前节点的左子树,将其路径上的节点存储到栈中。 - 调用 next 方法的时候,从栈顶取出节点,因为之前已经将路径上的左子树全部存入了栈中,所以此时该节点的左子树为空,这时候取出节点右子树,再将右子树的左子树进行递归遍历,并将其路径上的节点存储到栈中。 - 调用 hasNext 的方法的时候,直接判断栈中是否有值即可。 ## 代码 ```python class BSTIterator: def __init__(self, root: TreeNode): self.stack = [] self.in_order(root) def in_order(self, node): while node: self.stack.append(node) node = node.left def next(self) -> int: node = self.stack.pop() if node.right: self.in_order(node.right) return node.val def hasNext(self) -> bool: return len(self.stack) != 0 ``` ================================================ FILE: docs/solutions/LCR/lMSNwu.md ================================================ # [LCR 025. 两数相加 II](https://leetcode.cn/problems/lMSNwu/) - 标签:栈、链表、数学 - 难度:中等 ## 题目链接 - [LCR 025. 两数相加 II - 力扣](https://leetcode.cn/problems/lMSNwu/) ## 题目大意 给定两个非空链表的头节点 `l1` 和 `l2` 来代表两个非负整数。数字最高位位于链表开始位置。每个节点只储存一位数字。除了数字 `0` 之外,这两个链表代表的数字都不会以 `0` 开头。 要求:将这两个数相加会返回一个新的链表。 ## 解题思路 链表中最高位位于链表开始位置,最低位位于链表结束位置。这与我们做加法的数位顺序是相反的。为了将链表逆序,从而从低位开始处理数位,我们可以借用两个栈:将链表中所有数字分别压入两个栈中,再依次取出相加。 同时,在相加的时候,还要考虑进位问题。具体步骤如下: - 将链表 `l1` 中所有节点值压入 `stack1` 栈中,再将链表 `l2` 中所有节点值压入 `stack2` 栈中。 - 使用 `res` 存储新的结果链表,一开始指向 `None`,`carry` 记录进位。 - 如果 `stack1` 或 `stack2` 不为空,或着进位 `carry` 不为 `0`,则: - 从 `stack1` 中取出栈顶元素 `num1`,如果 `stack1` 为空,则 `num1 = 0`。 - 从 `stack2` 中取出栈顶元素 `num2`,如果 `stack2` 为空,则 `num2 = 0`。 - 计算相加结果,并计算进位。 - 建立新节点,存储进位后余下的值,并令其指向 `res`。 - `res` 指向新节点,继续判断。 - 如果 `stack1`、`stack2` 都为空,并且进位 `carry` 为 `0`,则输出 `res`。 ## 代码 ```python class Solution: def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode: stack1, stack2 = [], [] while l1: stack1.append(l1.val) l1 = l1.next while l2: stack2.append(l2.val) l2 = l2.next res = None carry = 0 while stack1 or stack2 or carry != 0: num1 = stack1.pop() if stack1 else 0 num2 = stack2.pop() if stack2 else 0 cur_sum = num1 + num2 + carry carry = cur_sum // 10 cur_sum %= 10 cur_node = ListNode(cur_sum) cur_node.next = res res = cur_node return res ``` ================================================ FILE: docs/solutions/LCR/li-wu-de-zui-da-jie-zhi-lcof.md ================================================ # [LCR 166. 珠宝的最高价值](https://leetcode.cn/problems/li-wu-de-zui-da-jie-zhi-lcof/) - 标签:数组、动态规划、矩阵 - 难度:中等 ## 题目链接 - [LCR 166. 珠宝的最高价值 - 力扣](https://leetcode.cn/problems/li-wu-de-zui-da-jie-zhi-lcof/) ## 题目大意 给定一个 `m * n` 大小的二维矩阵 `grid` 代表棋盘,棋盘的每一格都放有一个礼物,每个礼物有一定的价值(价值大于 `0`)。`grid[i][j]` 表示棋盘第 `i` 行第 `j` 列的礼物价值。我们可以从左上角的格子开始拿礼物,每次只能向右或者向下移动一格,直到到达棋盘的右下角。 要求:计算出最多能拿多少价值的礼物。 ## 解题思路 可以用动态规划求解,设 `dp[i][j]` 是从 `(0, 0)` 到 `(i - 1, j - 1)` 能得礼物的最大价值。 显然 `dp[i][j] = max(dp[i - 1][j] + dp[i][j - 1]) + grid[i][j]`。 因为是自上而下递推 `dp[i-1][j]` 可以用 `dp[j]` 来表示,所以也可以将二维改为一位。状态转移公式为: `dp[j] = max(dp[j], dp[j - 1]) + grid[i][j]`。 ## 代码 ```python class Solution: def maxValue(self, grid: List[List[int]]) -> int: if not grid: return 0 size_m = len(grid) size_n = len(grid[0]) dp = [0 for _ in range(size_n + 1)] for i in range(size_m): for j in range(size_n): dp[j + 1] = max(dp[j], dp[j + 1]) + grid[i][j] return dp[size_n] ``` ================================================ FILE: docs/solutions/LCR/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof.md ================================================ # [LCR 140. 训练计划 II](https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/) - 标签:链表、双指针 - 难度:简单 ## 题目链接 - [LCR 140. 训练计划 II - 力扣](https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/) ## 题目大意 给定一个链表的头节点 `head`,以及一个整数 `k`。 要求返回链表的倒数第 `k` 个节点。 ## 解题思路 常规思路是遍历一遍链表,求出链表长度,再遍历一遍到对应位置,返回该位置上的节点。 如果用一次遍历实现的话,可以使用快慢指针。让快指针先走 `k` 步,然后快慢指针、慢指针再同时走,每次一步,这样等快指针遍历到链表尾部的时候,慢指针就刚好遍历到了倒数第 `k` 个节点位置。返回该该位置上的节点即可。 ## 代码 ```python class Solution: def getKthFromEnd(self, head: ListNode, k: int) -> ListNode: slow = head fast = head for _ in range(k): if fast == None: return fast fast = fast.next while fast: slow = slow.next fast = fast.next return slow ``` ================================================ FILE: docs/solutions/LCR/lian-xu-zi-shu-zu-de-zui-da-he-lcof.md ================================================ # [LCR 161. 连续天数的最高销售额](https://leetcode.cn/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/) - 标签:数组、分治、动态规划 - 难度:简单 ## 题目链接 - [LCR 161. 连续天数的最高销售额 - 力扣](https://leetcode.cn/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/) ## 题目大意 给定一个整数数组 `nums` 。 要求:找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和,要求时间复杂度为 `O(n)`。 ## 解题思路 动态规划的方法,关键点是要找到状态转移方程。 假设 f(i) 表示第 i 个数结尾的「连续子数组的最大和」,那么 $max_{0 < i \le n-1} {f(i)} = max(f(i-1) + nums[i], nums[i])$ 即将之前累加和加上当前值与当前值做比较。 - 如果将之前累加和加上当前值 > 当前值,那么加上当前值。 - 如果将之前累加和加上当前值 < 当前值,那么 $f(i) = nums[i]$。 ## 代码 ```python class Solution: def maxSubArray(self, nums: List[int]) -> int: max_ans = nums[0] ans = 0 for num in nums: ans = max(ans + num, num) max_ans = max(max_ans, ans) return max_ans ``` ================================================ FILE: docs/solutions/LCR/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof.md ================================================ # [LCR 171. 训练计划 V](https://leetcode.cn/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/) - 标签:哈希表、链表、双指针 - 难度:简单 ## 题目链接 - [LCR 171. 训练计划 V - 力扣](https://leetcode.cn/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/) ## 题目大意 给定 A、B 两个链表,判断两个链表是否相交,返回相交的起始点。如果不相交,则返回 None。 比如:链表 A 为 [4, 1, 8, 4, 5],链表 B 为 [5, 0, 1, 8, 4, 5]。则如下图所示,两个链表相交的起始节点为 8,则输出结果为 8。 ![](https://assets.leetcode.com/uploads/2018/12/13/160_example_1.png) ## 解题思路 如果两个链表相交,那么从相交位置开始,到结束,必有一段等长且相同的节点。假设链表 A 的长度为 m、链表 B 的长度为 n,他们的相交序列有 k 个,则相交情况可以如下如所示: ![](https://qcdn.itcharge.cn/images/20210401113538.png) 现在问题是如何找到 m-k 或者 n-k 的位置。 考虑将链表 A 的末尾拼接上链表 B,链表 B 的末尾拼接上链表 A。 然后使用两个指针 pA 、PB,分别从链表 A、链表 B 的头节点开始遍历,如果走到共同的节点,则返回该节点。 否则走到两个链表末尾,返回 None。 ![](https://qcdn.itcharge.cn/images/20210401114100.png) ## 代码 ```python class Solution: def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: if not headA or not headB: return None pA = headA pB = headB while pA != pB: pA = pA.next if pA else headB pB = pB.next if pB else headA return pA ``` ================================================ FILE: docs/solutions/LCR/lwyVBB.md ================================================ # [LCR 034. 验证外星语词典](https://leetcode.cn/problems/lwyVBB/) - 标签:数组、哈希表、字符串 - 难度:简单 ## 题目链接 - [LCR 034. 验证外星语词典 - 力扣](https://leetcode.cn/problems/lwyVBB/) ## 题目大意 给定一组用外星语书写的单词字符串数组 `words`,以及表示外星字母表的顺序的字符串 `order` 。 要求:判断 `words` 中的单词是否都是按照 `order` 来排序的。如果是,则返回 `True`,否则返回 `False`。 ## 解题思路 如果所有单词是按照 `order` 的规则升序排列,则所有单词都符合规则。而判断所有单词是升序排列,只需要两两比较相邻的单词即可。所以我们可以先用哈希表存储所有字母的顺序,然后对所有相邻单词进行两两比较,如果最终是升序排列,则符合要求。具体步骤如下: - 使用哈希表 `order_map` 存储字母的顺序。 - 遍历单词数组 `words`,比较相邻单词 `word1` 和 `word2` 中所有字母在 `order_map` 中的下标,看是否满足 `word1 <= word2`。 - 如果全部满足,则返回 `True`。如果有不满足的情况,则直接返回 `False`。 ## 代码 ```python class Solution: def isAlienSorted(self, words: List[str], order: str) -> bool: order_map = dict() for i in range(len(order)): order_map[order[i]] = i for i in range(len(words) - 1): word1 = words[i] word2 = words[i + 1] flag = True for j in range(min(len(word1), len(word2))): if word1[j] != word2[j]: if order_map[word1[j]] > order_map[word2[j]]: return False else: flag = False break if flag and len(word1) > len(word2): return False return True ``` ================================================ FILE: docs/solutions/LCR/ms70jA.md ================================================ # [LCR 067. 数组中两个数的最大异或值](https://leetcode.cn/problems/ms70jA/) - 标签:位运算、字典树、数组、哈希表 - 难度:中等 ## 题目链接 - [LCR 067. 数组中两个数的最大异或值 - 力扣](https://leetcode.cn/problems/ms70jA/) ## 题目大意 给定一个整数数组 `nums`。 要求:返回 `num[i] XOR nums[j]` 的最大运算结果。其中 `0 ≤ i ≤ j < n`。 ## 解题思路 最直接的想法暴力求解。两层循环计算两两之间的异或结果,记录并更新最大异或结果。 更好的做法可以减少一重循环。首先,要取得异或结果的最大值,那么从二进制的高位到低位,尽可能的让每一位异或结果都为 `1`。 将数组中所有数字的二进制形式从高位到低位依次存入字典树中。然后是利用异或运算交换律:如果 `a ^ b = max` 成立,那么 `a ^ max = b` 与 `b ^ max = a` 均成立。这样当我们知道 `a` 和 `max` 时,可以通过交换律求出 `b`。`a` 是我们遍历的每一个数,`max` 是我们想要尝试的最大值,从 `111111...` 开始,从高位到低位依次填 `1`。 对于 `a` 和 `max`,如果我们所求的 `b` 也在字典树中,则表示 `max` 是可以通过 `a` 和 `b` 得到的,那么 `max` 就是所求最大的异或。如果 `b` 不在字典树中,则减小 `max` 值继续判断,或者继续查询下一个 `a`。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, num: int, max_bit: int) -> None: """ Inserts a word into the trie. """ cur = self for i in range(max_bit, -1, -1): bit = num >> i & 1 if bit not in cur.children: cur.children[bit] = Trie() cur = cur.children[bit] cur.isEnd = True def search(self, num: int, max_bit: int) -> int: """ Returns if the word is in the trie. """ cur = self res = 0 for i in range(max_bit, -1, -1): bit = num >> i & 1 if 1 - bit not in cur.children: res = res * 2 cur = cur.children[bit] else: res = res * 2 + 1 cur = cur.children[1 - bit] return res class Solution: def findMaximumXOR(self, nums: List[int]) -> int: trie_tree = Trie() max_bit = len(format(max(nums), 'b')) - 1 ans = 0 for num in nums: trie_tree.insert(num, max_bit) ans = max(ans, trie_tree.search(num, max_bit)) return ans ``` ================================================ FILE: docs/solutions/LCR/nZZqjQ.md ================================================ # [LCR 073. 爱吃香蕉的狒狒](https://leetcode.cn/problems/nZZqjQ/) - 标签:数组、二分查找 - 难度:中等 ## 题目链接 - [LCR 073. 爱吃香蕉的狒狒 - 力扣](https://leetcode.cn/problems/nZZqjQ/) ## 题目大意 给定一个数组 `piles` 代表 `n` 堆香蕉。其中 `piles[i]` 表示第 `i` 堆香蕉的个数。再给定一个整数 `h` ,表示最多可以在 `h` 小时内吃完所有香蕉。狒狒决定以速度每小时 `k`(未知)根的速度吃香蕉。每一个小时,她将选择其中一堆香蕉,从中吃掉 `k` 根。如果这堆香蕉少于 `k` 根,狒狒将在这一小时吃掉这堆的所有香蕉,并且这一小时不会再吃其他堆的香蕉。 要求:返回狒狒可以在 `h` 小时内吃掉所有香蕉的最小速度 `k`(`k` 为整数)。 ## 解题思路 先来看 `k` 的取值范围,因为 `k` 是整数,且速度肯定不能为 `0` 吧,为 `0` 的话就永远吃不完了。所以`k` 的最小值可以取 `1`。`k` 的最大值根香蕉中最大堆的香蕉个数有关,因为 `1` 个小时内只能选择一堆吃,不能再吃其他堆的香蕉,则 `k` 的最大值取香蕉堆的最大值即可。即 `k` 的最大值为 `max(piles)`。 我们的目标是求出 `h` 小时内吃掉所有香蕉的最小速度 `k`。现在有了区间「`[1, max(piles)]`」,有了目标「最小速度 `k`」。接下来使用二分查找算法来查找「最小速度 `k`」。至于计算 `h` 小时内能否以 `k` 的速度吃完香蕉,我们可以再写一个方法 `canEat` 用于判断。如果能吃完就返回 `True`,不能吃完则返回 `False`。下面说一下算法的具体步骤。 - 使用两个指针 `left`、`right`。令 `left` 指向 `1`,`right` 指向 `max(piles)`。代表待查找区间为 `[left, right]` - 取两个节点中心位置 `mid`,判断是否能在 `h` 小时内以 `k` 的速度吃完香蕉。 - 如果不能吃完,则将区间 `[left, mid]` 排除掉,继续在区间 `[mid + 1, right]` 中查找。 - 如果能吃完,说明 `k` 还可以继续减小,则继续在区间 `[left, mid]` 中查找。 - 当 `left == right` 时跳出循环,返回 `left`。 ## 代码 ```python class Solution: def canEat(self, piles, hour, speed): time = 0 for pile in piles: time += (pile + speed - 1) // speed return time <= hour def minEatingSpeed(self, piles: List[int], h: int) -> int: left, right = 1, max(piles) while left < right: mid = left + (right - left) // 2 if not self.canEat(piles, h, mid): left = mid + 1 else: right = mid return left ``` ================================================ FILE: docs/solutions/LCR/om3reC.md ================================================ # [LCR 108. 单词接龙](https://leetcode.cn/problems/om3reC/) - 标签:广度优先搜索、哈希表、字符串 - 难度:困难 ## 题目链接 - [LCR 108. 单词接龙 - 力扣](https://leetcode.cn/problems/om3reC/) ## 题目大意 给定两个单词 `beginWord` 和 `endWord`,以及一个字典 `wordList`。找到从 `beginWord` 到 `endWord` 的最短转换序列中的单词数目。如果不存在这样的转换序列,则返回 0。 转换需要遵守的规则如下: - 每次转换只能改变一个字母。 - 转换过程中的中间单词必须为字典中的单词。 ## 解题思路 广度优先搜索。使用队列存储将要遍历的单词和单词数目。 从 `beginWord` 开始变换,把单词的每个字母都用 `a ~ z` 变换一次,变换后的单词是否是 `endWord`,如果是则直接返回。 否则查找变换后的词是否在 `wordList` 中。如果在 `wordList` 中找到就加入队列,找不到就输出 `0`。然后按照广度优先搜索的算法急需要遍历队列中的节点,直到所有单词都出队时结束。 ## 代码 ```python class Solution: def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: if not wordList or endWord not in wordList: return 0 word_set = set(wordList) if beginWord in word_set: word_set.remove(beginWord) queue = collections.deque() queue.append((beginWord, 1)) while queue: word, level = queue.popleft() if word == endWord: return level for i in range(len(word)): for j in range(26): new_word = word[:i] + chr(ord('a') + j) + word[i + 1:] if new_word in word_set: word_set.remove(new_word) queue.append((new_word, level + 1)) return 0 ``` ================================================ FILE: docs/solutions/LCR/opLdQZ.md ================================================ # [LCR 056. 两数之和 IV - 输入二叉搜索树](https://leetcode.cn/problems/opLdQZ/) - 标签:树、深度优先搜索、广度优先搜索、二叉搜索树、哈希表、双指针、二叉树 - 难度:简单 ## 题目链接 - [LCR 056. 两数之和 IV - 输入二叉搜索树 - 力扣](https://leetcode.cn/problems/opLdQZ/) ## 题目大意 给定一个二叉搜索树的根节点 `root` 和一个整数 `k`。 要求:判断该二叉搜索树是否存在两个节点值的和等于 `k`。如果存在,则返回 `True`,不存在则返回 `False`。 ## 解题思路 二叉搜索树中序遍历的结果是从小到大排序,所以我们可以先对二叉搜索树进行中序遍历,将中序遍历结果存储到列表中。再使用左右指针查找节点值和为 `k` 的两个节点。 ## 代码 ```python class Solution: def inOrder(self, root, nums): if not root: return self.inOrder(root.left, nums) nums.append(root.val) self.inOrder(root.right, nums) def findTarget(self, root: TreeNode, k: int) -> bool: nums = [] self.inOrder(root, nums) left, right = 0, len(nums) - 1 while left < right: sum = nums[left] + nums[right] if sum == k: return True elif sum < k: left += 1 else: right -= 1 return False ``` ================================================ FILE: docs/solutions/LCR/pOCWxh.md ================================================ # [LCR 047. 二叉树剪枝](https://leetcode.cn/problems/pOCWxh/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [LCR 047. 二叉树剪枝 - 力扣](https://leetcode.cn/problems/pOCWxh/) ## 题目大意 给定一棵二叉树的根节点 `root`,树的每个节点值要么是 `0`,要么是 `1`。 要求:剪除该二叉树中所有节点值为 `0` 的子树。 - 节点 `node` 的子树为: `node` 本身,以及所有 `node` 的后代。 ## 解题思路 定义辅助方法 `containsOnlyZero(root)` 递归判断以 `root` 为根的子树中是否只包含 `0`。如果子树中只包含 `0`,则返回 `True`。如果子树中含有 `1`,则返回 `False`。当 `root` 为空时,也返回 `True`。 然后递归遍历二叉树,判断当前节点 `root` 是否只包含 `0`。如果只包含 `0`,则将其置空,返回 `None`。否则递归遍历左右子树,并设置对应的左右指针。 最后返回根节点 `root`。 ## 代码 ```python class Solution: def containsOnlyZero(self, root: TreeNode): if not root: return True if root.val == 1: return False return self.containsOnlyZero(root.left) and self.containsOnlyZero(root.right) def pruneTree(self, root: TreeNode) -> TreeNode: if not root: return root if self.containsOnlyZero(root): return None root.left = self.pruneTree(root.left) root.right = self.pruneTree(root.right) return root ``` ================================================ FILE: docs/solutions/LCR/ping-heng-er-cha-shu-lcof.md ================================================ # [LCR 176. 判断是否为平衡二叉树](https://leetcode.cn/problems/ping-heng-er-cha-shu-lcof/) - 标签:树、深度优先搜索、二叉树 - 难度:简单 ## 题目链接 - [LCR 176. 判断是否为平衡二叉树 - 力扣](https://leetcode.cn/problems/ping-heng-er-cha-shu-lcof/) ## 题目大意 给定一棵二叉树的根节点 `root`。 要求:判断该树是不是平衡二叉树。如果是平衡二叉树,返回 `True`,否则,返回 `False`。 - 平衡二叉树:任意节点的左右子树深度不超过 `1`。 ## 解题思路 递归遍历二叉树。先递归遍历左右子树,判断左右子树是否平衡,再判断以当前节点为根节点的左右子树是否平衡。 如果遍历的子树是平衡的,则返回它的高度,否则返回 `-1`。 只要出现不平衡的子树,则该二叉树一定不是平衡二叉树。 ## 代码 ```python class Solution: def isBalanced(self, root: TreeNode) -> bool: def height(root: TreeNode) -> int: if root == None: return False leftHeight = height(root.left) rightHeight = height(root.right) if leftHeight == -1 or rightHeight == -1 or abs(leftHeight - rightHeight) > 1: return -1 else: return max(leftHeight, rightHeight) + 1 return height(root) >= 0 ``` ================================================ FILE: docs/solutions/LCR/qIsx9U.md ================================================ # [LCR 041. 数据流中的移动平均值](https://leetcode.cn/problems/qIsx9U/) - 标签:设计、队列、数组、数据流 - 难度:简单 ## 题目链接 - [LCR 041. 数据流中的移动平均值 - 力扣](https://leetcode.cn/problems/qIsx9U/) ## 题目大意 **描述**:给定一个整数数据流和一个窗口大小 `size`。 **要求**:根据滑动窗口的大小,计算滑动窗口里所有数字的平均值。要求实现 `MovingAverage` 类: - `MovingAverage(int size)`:用窗口大小 `size` 初始化对象。 - `double next(int val)`:成员函数 `next` 每次调用的时候都会往滑动窗口增加一个整数,请计算并返回数据流中最后 `size` 个值的移动平均值,即滑动窗口里所有数字的平均值。 **说明**: - $1 \le size \le 1000$。 - $-10^5 \le val \le 10^5$。 - 最多调用 `next` 方法 $10^4$ 次。 **示例**: - 示例 1: ```python 输入: inputs = ["MovingAverage", "next", "next", "next", "next"] inputs = [[3], [1], [10], [3], [5]] 输出: [null, 1.0, 5.5, 4.66667, 6.0] 解释: MovingAverage movingAverage = new MovingAverage(3); movingAverage.next(1); // 返回 1.0 = 1 / 1 movingAverage.next(10); // 返回 5.5 = (1 + 10) / 2 movingAverage.next(3); // 返回 4.66667 = (1 + 10 + 3) / 3 movingAverage.next(5); // 返回 6.0 = (10 + 3 + 5) / 3 ``` ## 解题思路 ### 思路 1:队列 1. 使用队列保存滑动窗口的元素,并记录对应窗口大小和元素和。 2. 当队列长度小于窗口大小的时候,直接向队列中添加元素,并记录当前窗口中的元素和。 3. 当队列长度等于窗口大小的时候,先将队列头部元素弹出,再添加元素,并记录当前窗口中的元素和。 4. 然后根据元素和和队列中元素个数计算出平均值。 ### 思路 1:代码 ```python class MovingAverage: def __init__(self, size: int): """ Initialize your data structure here. """ self.queue = [] self.size = size self.sum = 0 def next(self, val: int) -> float: if len(self.queue) < self.size: self.queue.append(val) else: if self.queue: self.sum -= self.queue[0] self.queue.pop(0) self.queue.append(val) self.sum += val return self.sum / len(self.queue) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(1)$。初始化方法和每次调用 `next` 方法的时间复杂度都是 $O(1)$。 - **空间复杂度**:$O(size)$。其中 $size$ 就是给定的滑动窗口的大小。 ================================================ FILE: docs/solutions/LCR/qJnOS7.md ================================================ # [LCR 095. 最长公共子序列](https://leetcode.cn/problems/qJnOS7/) - 标签:字符串、动态规划 - 难度:中等 ## 题目链接 - [LCR 095. 最长公共子序列 - 力扣](https://leetcode.cn/problems/qJnOS7/) ## 题目大意 给定两个字符串 `text1` 和 `text2`。 要求:返回两个字符串的最长公共子序列的长度。如果不存在公共子序列,则返回 `0`。 - 子序列:原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 - 公共子序列:两个字符串所共同拥有的子序列。 ## 解题思路 用动态规划来做。 动态规划的状态 `dp[i][j]` 表示为:前 `i` 个字符组成的字符串 `str1` 与前 `j` 个字符组成的字符串 `str2` 的最长公共子序列长度为 `dp[i][j]`。 遍历字符串 `text1` 和 `text2`,则状态转移方程为: - 如果 `text1[i - 1] == text2[j - 1]`,则找到了一个公共元素,则 `dp[i][j] = dp[i - 1][j - 1] + 1`。 - 如果 `text1[i - 1] != text2[j - 1]`,则 `dp[i][j]` 需要考虑两种情况,取其中最大的那种: - `text1` 前 `i - 1` 个字符组成的字符串 `str1` 与 `text2` 前 `j` 个字符组成的 `str2` 的最长公共子序列长度,即 `dp[i - 1][j]`。 - `text1` 前 `i` 个字符组成的字符串 `str1` 与 `text2` 前 `j - 1` 个字符组成的 `str2` 的最长公共子序列长度,即 `dp[i][j - 1]`。 最后输出 `dp[sise1][size2]` 即可,`size1`、`size2` 分别为 `text1`、`text2` 的字符串长度。 ## 代码 ```python class Solution: def longestCommonSubsequence(self, text1: str, text2: str) -> int: size1 = len(text1) size2 = len(text2) dp = [[0 for _ in range(size2 + 1)] for _ in range(size1 + 1)] for i in range(1, size1 + 1): for j in range(1, size2 + 1): if text1[i - 1] == text2[j - 1]: dp[i][j] = dp[i - 1][j - 1] + 1 else: dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) return dp[size1][size2] ``` ================================================ FILE: docs/solutions/LCR/qing-wa-tiao-tai-jie-wen-ti-lcof.md ================================================ # [LCR 127. 跳跃训练](https://leetcode.cn/problems/qing-wa-tiao-tai-jie-wen-ti-lcof/) - 标签:记忆化搜索、数学、动态规划 - 难度:简单 ## 题目链接 - [LCR 127. 跳跃训练 - 力扣](https://leetcode.cn/problems/qing-wa-tiao-tai-jie-wen-ti-lcof/) ## 题目大意 一直青蛙一次可以跳上 `1` 级台阶,也可以跳上 `2` 级台阶。 要求:求该青蛙跳上 `n` 级台阶共有多少中跳法。答案需要对 `1000000007` 取余。 ## 解题思路 先来看一下规律: 第 0 级台阶:1 种方法(比较特殊) 第 1 级台阶:1 种方法(从 0 阶爬 1 阶) 第 2 阶台阶:2 种方法(从 0 阶爬 2 阶,从 1 阶爬 1 阶) 第 i 阶台阶:从第 i-1 阶台阶爬 1 阶,或者从第 i-2 阶台阶爬 2 阶。 则推出递推公式为: - 当 `n = 0` 时,`F(i) = 1`。 - 当 `n > 0` 时,`F(i) = F(i-1) + F(i-2)`。 ## 代码 ```python class Solution: def numWays(self, n: int) -> int: if n == 0: return 1 f1, f2, f3 = 0, 1, 1 for i in range(2, n + 1): f1, f2 = f2, f3 f3 = (f1 + f2) % 1000000007 return f3 ``` ================================================ FILE: docs/solutions/LCR/qiu-12n-lcof.md ================================================ # [LCR 189. 设计机械累加器](https://leetcode.cn/problems/qiu-12n-lcof/) - 标签:位运算、递归、脑筋急转弯 - 难度:中等 ## 题目链接 - [LCR 189. 设计机械累加器 - 力扣](https://leetcode.cn/problems/qiu-12n-lcof/) ## 题目大意 给定一个整数 `n`。 要求:计算 `1 + 2 + ... + n`,并且不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句(A?B:C)。 ## 解题思路 Python 中的逻辑运算最终返回的是最后一个非空值。比如 `3 and 2 and 'a'` 最终返回的是 `'a'`。利用这个特性可以递归求解。 ## 代码 ```python class Solution: def sumNums(self, n: int) -> int: return n and n + self.sumNums(n - 1) ``` ================================================ FILE: docs/solutions/LCR/que-shi-de-shu-zi-lcof.md ================================================ # [LCR 173. 点名](https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/) - 标签:位运算、数组、哈希表、数学、二分查找 - 难度:简单 ## 题目链接 - [LCR 173. 点名 - 力扣](https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/) ## 题目大意 给定一个 `n - 1` 个数的升序数组,数组中元素值都在 `0 ~ n - 1` 之间。 `nums` 中有且只有一个数字不在该数组中。 要求:找出这个缺失的数字。 ## 解题思路 可以用二分查找解决。 对于中间值,判断元素值与索引值是否一致,如果一致,则说明缺失数字在索引的右侧。如果不一致,则可能为当前索引或者索引的左侧。 ## 代码 ```python class Solution: def missingNumber(self, nums: List[int]) -> int: if len(nums) == 0: return 0 left, right = 0, len(nums) - 1 while left < right: mid = left + (right - left) // 2 if mid == nums[mid]: left = mid + 1 else: right = mid if left == nums[left]: return left + 1 else: return left ``` ================================================ FILE: docs/solutions/LCR/sfvd7V.md ================================================ # [LCR 033. 字母异位词分组](https://leetcode.cn/problems/sfvd7V/) - 标签:数组、哈希表、字符串、排序 - 难度:中等 ## 题目链接 - [LCR 033. 字母异位词分组 - 力扣](https://leetcode.cn/problems/sfvd7V/) ## 题目大意 给定一个字符串数组 `strs`。 要求:将包含字母相同的字符串组合在一起,不需要考虑输出顺序。 ## 解题思路 使用哈希表记录字母相同的字符串。对每一个字符串进行排序,按照 排序字符串:字母相同的字符串数组 的键值顺序进行存储。最终将哈希表的值转换为对应数组返回结果。 ## 代码 ```python class Solution: def groupAnagrams(self, strs: List[str]) -> List[List[str]]: str_dict = dict() res = [] for s in strs: sort_s = str(sorted(s)) if sort_s in str_dict: str_dict[sort_s] += [s] else: str_dict[sort_s] = [s] for sort_s in str_dict: res += [str_dict[sort_s]] return res ``` ================================================ FILE: docs/solutions/LCR/shan-chu-lian-biao-de-jie-dian-lcof.md ================================================ # [LCR 136. 删除链表的节点](https://leetcode.cn/problems/shan-chu-lian-biao-de-jie-dian-lcof/) - 标签:链表 - 难度:简单 ## 题目链接 - [LCR 136. 删除链表的节点 - 力扣](https://leetcode.cn/problems/shan-chu-lian-biao-de-jie-dian-lcof/) ## 题目大意 给定一个链表。 要求:删除链表中值为 `val` 的节点,并返回新的链表头节点。 ## 解题思路 用两个指针 `prev` 和 `curr`。`prev` 指向前一节点和当前节点,`curr` 指向当前节点。从前向后遍历链表,遇到值为 `val` 的节点时,将 `prev` 指向当前节点的下一个节点,继续递归遍历。遇不到则更新 `prev` 指针,并继续遍历。 需要注意的是要删除的节点可能包含了头节点。我们可以考虑在遍历之前,新建一个头节点,让其指向原来的头节点。这样,最终如果删除的是头节点,则删除原头节点即可。返回结果的时候,可以直接返回新建头节点的下一位节点。 ## 代码 ```python class Solution: def deleteNode(self, head: ListNode, val: int) -> ListNode: newHead = ListNode(0, head) newHead.next = head prev, curr = newHead, head while curr: if curr.val == val: prev.next = curr.next else: prev = curr curr = curr.next return newHead.next ``` ================================================ FILE: docs/solutions/LCR/shu-de-zi-jie-gou-lcof.md ================================================ # [LCR 143. 子结构判断](https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [LCR 143. 子结构判断 - 力扣](https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/) ## 题目大意 给定两棵二叉树的根节点 `A`、`B`。 要求:判断 `B` 是不是 `A` 的子结构。(空树不是任意一棵树的子结构)。 - `B` 是 `A` 的子结构:`A` 中有出现和 `B` 相同的结构和节点值。 ## 解题思路 深度优先搜索。 - 先判断特例,如果 `A`、`B` 都为空树,则直接返回 `False`。 - 然后递归判断 `A`、`B` 是否相等。 - 如果 `A`、`B` 相等,则返回 `True`。 - 如果 `A`、`B` 不相等,则递归判断 `B` 是否是 `A` 的左子树的子结构,或者 `B` 是否是 `A` 的右子树的子结构,如果有一种满足,则返回 `True`,如果都不满足,则返回 `False`。 递归判断 `A`、`B` 是否相等的具体方法如下: - 如果 `B` 为空树,则直接返回 `False`,因为空树不是任意一棵树的子结构。 - 如果 `A` 为空树或者 `A` 节点的值不等于 `B` 节点的值,则返回 `False`。 - 如果 `A`、`B` 都不为空,且节点值相同,则递归判断 `A` 的左子树和 `B` 的左子树是否相等,判断 `A` 的右子树和 `B` 的右子树是否相等。如果都相等,则返回 `True`,否则返回 `False`。 ## 代码 ```python class Solution: def hasSubStructure(self, A: TreeNode, B: TreeNode) -> bool: if not B: return True if not A or A.val != B.val: return False return self.hasSubStructure(A.left, B.left) and self.hasSubStructure(A.right, B.right) def isSubStructure(self, A: TreeNode, B: TreeNode) -> bool: if not A or not B: return False if self.hasSubStructure(A, B): return True return self.isSubStructure(A.left, B) or self.isSubStructure(A.right, B) ``` ================================================ FILE: docs/solutions/LCR/shu-ju-liu-zhong-de-zhong-wei-shu-lcof.md ================================================ # [LCR 160. 数据流中的中位数](https://leetcode.cn/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof/) - 标签:设计、双指针、数据流、排序、堆(优先队列) - 难度:困难 ## 题目链接 - [LCR 160. 数据流中的中位数 - 力扣](https://leetcode.cn/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof/) ## 题目大意 要求:设计一个支持一下两种操作的数组结构: - `void addNum(int num)`:从数据流中添加一个整数到数据结构中。 - `double findMedian()`:返回目前所有元素的中位数。 ## 解题思路 使用一个大顶堆 `queMax` 记录大于中位数的数,使用一个小顶堆 `queMin` 小于中位数的数。 - 当添加元素数量为偶数: `queMin` 和 `queMax` 中元素数量相同,则中位数为它们队头的平均值。 - 当添加元素数量为奇数:`queMin` 中的数比 `queMax` 多一个,此时中位数为 `queMin` 的队头。 为了满足上述条件,在进行 `addNum` 操作时,我们应当分情况处理: - `num > max{queMin}`:此时 `num` 大于中位数,将该数添加到大顶堆 `queMax` 中。新的中位数将大于原来的中位数,所以可能需要将 `queMax` 中的最小数移动到 `queMin` 中。 - `num ≤ max{queMin}`:此时 `num` 小于中位数,将该数添加到小顶堆 `queMin` 中。新的中位数将小于等于原来的中位数,所以可能需要将 `queMin` 中最大数移动到 `queMax` 中。 ## 代码 ```python import heapq class MedianFinder: def __init__(self): """ initialize your data structure here. """ self.queMin = list() self.queMax = list() def addNum(self, num: int) -> None: if not self.queMin or num < -self.queMin[0]: heapq.heappush(self.queMin, -num) if len(self.queMax) + 1 < len(self.queMin): heapq.heappush(self.queMax, -heapq.heappop(self.queMin)) else: heapq.heappush(self.queMax, num) if len(self.queMax) > len(self.queMin): heapq.heappush(self.queMin, -heapq.heappop(self.queMax)) def findMedian(self) -> float: if len(self.queMin) > len(self.queMax): return -self.queMin[0] return (-self.queMin[0] + self.queMax[0]) / 2 ``` ================================================ FILE: docs/solutions/LCR/shu-zhi-de-zheng-shu-ci-fang-lcof.md ================================================ # [LCR 134. Pow(x, n)](https://leetcode.cn/problems/shu-zhi-de-zheng-shu-ci-fang-lcof/) - 标签:递归、数学 - 难度:中等 ## 题目链接 - [LCR 134. Pow(x, n) - 力扣](https://leetcode.cn/problems/shu-zhi-de-zheng-shu-ci-fang-lcof/) ## 题目大意 给定浮点数 `x` 和整数 `n`。 要求:实现 `pow(x, n)`,即计算 $x^n$,不能使用库函数,不需要考虑大数问题。 ## 解题思路 常规方法是直接将 x 累乘 n 次得出结果,时间复杂度为 $O(n)$。可以利用快速幂来减少时间复杂度。 如果 n 为偶数,$x^n = x^{n/2} * x^{n/2}$。如果 n 为奇数,$x^n = x * x^{(n-1)/2} * x^{(n-1)/2}$。 $x^(n/2)$ 又可以继续向下递归划分。则我们可以利用低纬度的幂计算结果,来得到高纬度的幂计算结果。 这样递归求解,时间复杂度为 $O(logn)$,并且递归也可以转为递推来做。 需要注意如果 n 为负数,可以转换为 $\frac{1}{x} ^{(-n)}$。 ## 代码 ```python class Solution: def myPow(self, x: float, n: int) -> float: if x == 0.0: return 0.0 res = 1 if n < 0: x = 1 / x n = -n while n: if n & 1: res *= x x *= x n >>= 1 return res ``` ================================================ FILE: docs/solutions/LCR/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof.md ================================================ # [LCR 163. 找到第 k 位数字](https://leetcode.cn/problems/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof/) - 标签:数学、二分查找 - 难度:中等 ## 题目链接 - [LCR 163. 找到第 k 位数字 - 力扣](https://leetcode.cn/problems/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof/) ## 题目大意 数字以 `0123456789101112131415…` 的格式序列化到一个字符序列中。在这个序列中,第 `5` 位(从下标 `0` 开始计数)是 `5`,第 `13` 位是 `1`,第 `19` 位是 `4`,等等。 要求:返回任意第 `n` 位对应的数字。 ## 解题思路 根据题意中的字符串,找数学规律: - `123456789`:是 `9` 个 `1` 位数字。 - `10111213...9899`:是 `90` 个 `2` 位数字。 - `100...999`:是 `900` 个 `3` 位数字。 - `1000...9999` 是 `9000` 个 `4` 位数字。 - 我们可以先找到对应的数字对应的位数 `digits`。 - 然后找到该位数 `digits` 的起始数字 `start`。 - 再计算出 `n` 所在的数字 `number`。`number` 等于从起始数字 `start` 开始的第 $\lfloor(n - 1) / digits\rfloor$ 个数字。即 `number = start + (n - 1) // digits`。 - 然后确定 `n` 对应的是数字 `number` 中的哪一位。即 `idx = (n - 1) % digits`。 - 最后返回结果。 ## 代码 ```python class Solution: def findNthDigit(self, n: int) -> int: digits = 1 start = 1 base = 9 while n > base: n -= base digits += 1 start *= 10 base = start * digits * 9 number = start + (n - 1) // digits idx = (n - 1) % digits return int(str(number)[idx]) ``` ## 参考资料 - 【题解】[面试题44. 数字序列中某一位的数字(迭代 + 求整 / 求余,清晰图解) - 数字序列中某一位的数字 - 力扣](https://leetcode.cn/problems/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof/solution/mian-shi-ti-44-shu-zi-xu-lie-zhong-mou-yi-wei-de-6/) ================================================ FILE: docs/solutions/LCR/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof.md ================================================ # [LCR 158. 库存管理 II](https://leetcode.cn/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/) - 标签:数组、哈希表、分治、计数、排序 - 难度:简单 ## 题目链接 - [LCR 158. 库存管理 II - 力扣](https://leetcode.cn/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/) ## 题目大意 给定一个数组 `nums`,其中有一个数字出现次数超过数组长度一半。 要求:找到出现次数超过数组长度一半的数字。 ## 解题思路 可以利用哈希表。遍历一遍数组 `nums`,用哈希表统计每个元素 `num` 出现的次数,再遍历一遍哈希表,找出元素个数最多的元素即可。 ## 代码 ```python class Solution: def majorityElement(self, nums: List[int]) -> int: numDict = dict() for num in nums: if num in numDict: numDict[num] += 1 else: numDict[num] = 1 max = 0 max_index = -1 for num in numDict: if numDict[num] > max: max = numDict[num] max_index = num return max_index ``` ================================================ FILE: docs/solutions/LCR/shu-zu-zhong-de-ni-xu-dui-lcof.md ================================================ # [LCR 170. 交易逆序对的总数](https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/) - 标签:树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 - 难度:困难 ## 题目链接 - [LCR 170. 交易逆序对的总数 - 力扣](https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/) ## 题目大意 **描述**:给定一个数组 $nums$。 **要求**:计算出数组中的逆序对的总数。 **说明**: - **逆序对**:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。 - $0 \le nums.length \le 50000$。 **示例**: - 示例 1: ```python 输入: [7,5,6,4] 输出: 5 ``` ## 解题思路 ### 思路 1:归并排序 归并排序主要分为:「分解过程」和「合并过程」。其中「合并过程」实质上是两个有序数组的合并过程。 ![归并排序](https://qcdn.itcharge.cn/images/20220414204405.png) 每当遇到 左子数组当前元素 > 右子树组当前元素时,意味着「左子数组从当前元素开始,一直到左子数组末尾元素」与「右子树组当前元素」构成了若干个逆序对。 比如上图中的左子数组 $[0, 3, 5, 7]$ 与右子树组 $[1, 4, 6, 8]$,遇到左子数组中元素 $3$ 大于右子树组中元素 $1$。则左子数组从 $3$ 开始,经过 $5$ 一直到 $7$,与右子数组当前元素 $1$ 都构成了逆序对。即 $[3, 1]$、$[5, 1]$、$[7, 1]$ 都构成了逆序对。 因此,我们可以在合并两个有序数组的时候计算逆序对。具体做法如下: 1. 使用全局变量 $cnt$ 来存储逆序对的个数。然后进行归并排序。 2. **分割过程**:先递归地将当前序列平均分成两半,直到子序列长度为 $1$。 1. 找到序列中心位置 $mid$,从中心位置将序列分成左右两个子序列 $left\_arr$、$right\_arr$。 2. 对左右两个子序列 $left\_arr$、$right\_arr$ 分别进行递归分割。 3. 最终将数组分割为 $n$ 个长度均为 $1$ 的有序子序列。 3. **归并过程**:从长度为 $1$ 的有序子序列开始,依次进行两两归并,直到合并成一个长度为 $n$ 的有序序列。 1. 使用数组变量 $arr$ 存放归并后的有序数组。 2. 使用两个指针 $left\_i$、$right\_i$ 分别指向两个有序子序列 $left\_arr$、$right\_arr$ 的开始位置。 3. 比较两个指针指向的元素: 1. 如果 $left\_arr[left\_i] \le right\_arr[right\_i]$,则将 $left\_arr[left\_i]$ 存入到结果数组 $arr$ 中,并将指针移动到下一位置。 2. 如果 $left\_arr[left\_i] > right\_arr[right\_i]$,则 **记录当前左子序列中元素与当前右子序列元素所形成的逆序对的个数,并累加到 $cnt$ 中,即 `self.cnt += len(left_arr) - left_i`**,然后将 $right\_arr[right\_i]$ 存入到结果数组 $arr$ 中,并将指针移动到下一位置。 4. 重复步骤 $3$,直到某一指针到达子序列末尾。 5. 将另一个子序列中的剩余元素存入到结果数组 $arr$ 中。 6. 返回归并后的有序数组 $arr$。 4. 返回数组中的逆序对的总数,即 $self.cnt$。 ### 思路 1:代码 ```python class Solution: cnt = 0 def merge(self, left_arr, right_arr): # 归并过程 arr = [] left_i, right_i = 0, 0 while left_i < len(left_arr) and right_i < len(right_arr): # 将两个有序子序列中较小元素依次插入到结果数组中 if left_arr[left_i] <= right_arr[right_i]: arr.append(left_arr[left_i]) left_i += 1 else: self.cnt += len(left_arr) - left_i arr.append(right_arr[right_i]) right_i += 1 while left_i < len(left_arr): # 如果左子序列有剩余元素,则将其插入到结果数组中 arr.append(left_arr[left_i]) left_i += 1 while right_i < len(right_arr): # 如果右子序列有剩余元素,则将其插入到结果数组中 arr.append(right_arr[right_i]) right_i += 1 return arr # 返回排好序的结果数组 def mergeSort(self, arr): # 分割过程 if len(arr) <= 1: # 数组元素个数小于等于 1 时,直接返回原数组 return arr mid = len(arr) // 2 # 将数组从中间位置分为左右两个数组。 left_arr = self.mergeSort(arr[0: mid]) # 递归将左子序列进行分割和排序 right_arr = self.mergeSort(arr[mid:]) # 递归将右子序列进行分割和排序 return self.merge(left_arr, right_arr) # 把当前序列组中有序子序列逐层向上,进行两两合并。 def reversePairs(self, nums: List[int]) -> int: self.cnt = 0 self.mergeSort(nums) return self.cnt ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(n)$。 ### 思路 2:树状数组 数组 $tree[i]$ 表示数字 $i$ 是否在序列中出现过,如果数字 $i$ 已经存在于序列中,$tree[i] = 1$,否则 $tree[i] = 0$。 1. 按序列从左到右将值为 $nums[i]$ 的元素当作下标为$nums[i]$,赋值为 $1$ 插入树状数组里,这时,比 $nums[i]$ 大的数个数就是 $i + 1 - query(a)$。 2. 将全部结果累加起来就是逆序数了。 ### 思路 2:代码 ```python import bisect class BinaryIndexTree: def __init__(self, n): self.size = n self.tree = [0 for _ in range(n + 1)] def lowbit(self, index): return index & (-index) def update(self, index, delta): while index <= self.size: self.tree[index] += delta index += self.lowbit(index) def query(self, index): res = 0 while index > 0: res += self.tree[index] index -= self.lowbit(index) return res class Solution: def reversePairs(self, nums: List[int]) -> int: size = len(nums) sort_nums = sorted(nums) for i in range(size): nums[i] = bisect.bisect_left(sort_nums, nums[i]) + 1 bit = BinaryIndexTree(size) ans = 0 for i in range(size): bit.update(nums[i], 1) ans += (i + 1 - bit.query(nums[i])) return ans ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n \times \log n)$。 - **空间复杂度**:$O(n)$。 ================================================ FILE: docs/solutions/LCR/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof.md ================================================ # [LCR 177. 撞色搭配](https://leetcode.cn/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof/) - 标签:位运算、数组 - 难度:中等 ## 题目链接 - [LCR 177. 撞色搭配 - 力扣](https://leetcode.cn/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof/) ## 题目大意 给定一个整型数组 `nums` 。`nums` 里除两个数字之外,其他数字都出现了两次。 要求:找出这两个只出现一次的数字。要求时间复杂度是 $O(n)$,空间复杂度是 $O(1)$。 ## 解题思路 - 求解这道题之前,我们先来看看如何求解「一个数组中除了某个元素只出现一次以外,其余每个元素均出现两次。」即「[136. 只出现一次的数字](https://leetcode.cn/problems/single-number/)」问题。我们可以对所有数不断进行异或操作,最终可得到单次出现的元素。 - 如果数组中有两个数字只出现一次,其余每个元素均出现两次。那么经过全部异或运算。我们可以得到只出现一次的两个数字的异或结果。 - 根据异或结果的性质,异或运算中如果某一位上为 `1`,则说明异或的两个数在该位上是不同的。根据这个性质,我们将数字分为两组:一组是和该位为 `0` 的数字,另一组是该位为 `1` 的数字。然后将这两组分别进行异或运算,就可以得到最终要求的两个数字。 ## 代码 ```python class Solution: def singleNumbers(self, nums: List[int]) -> List[int]: all_xor = 0 for num in nums: all_xor ^= num # 获取所有异或中最低位的 1 mask = 1 while all_xor & mask == 0: mask <<= 1 a_xor, b_xor = 0, 0 for num in nums: if num & mask == 0: a_xor ^= num else: b_xor ^= num return a_xor, b_xor ``` ================================================ FILE: docs/solutions/LCR/shu-zu-zhong-zhong-fu-de-shu-zi-lcof.md ================================================ # [LCR 120. 寻找文件副本](https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/) - 标签:数组、哈希表、排序 - 难度:简单 ## 题目链接 - [LCR 120. 寻找文件副本 - 力扣](https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/) ## 题目大意 给定一个包含 `n + 1` 个整数的数组 `nums`,里边包含的值都在 `1 ~ n` 之间。假设 `nums` 中只存在一个重复的整数,要求找出这个重复的数。 ## 解题思路 使用哈希表存储数组每个元素,遇到重复元素则直接返回该元素。 ## 代码 ```python class Solution: def findRepeatNumber(self, nums: List[int]) -> int: nums_dict = dict() for num in nums: if num in nums_dict: return num nums_dict[num] = 1 return -1 ``` ================================================ FILE: docs/solutions/LCR/shun-shi-zhen-da-yin-ju-zhen-lcof.md ================================================ # [LCR 146. 螺旋遍历二维数组](https://leetcode.cn/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/) - 标签:数组、矩阵、模拟 - 难度:简单 ## 题目链接 - [LCR 146. 螺旋遍历二维数组 - 力扣](https://leetcode.cn/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/) ## 题目大意 给定一个 `m * n` 大小的二维矩阵 `matrix`。 要求:按照顺时针旋转的顺序,返回矩阵中的所有元素。 ## 解题思路 按照题意进行模拟。可以实现定义一下上、下、左、右的边界,然后按照逆时针的顺序从边界上依次访问元素。 当访问完当前边界之后,要更新一下边界位置,缩小范围,方便下一轮进行访问。 ## 代码 ```python class Solution: def spiralOrder(self, matrix: List[List[int]]) -> List[int]: size_m = len(matrix) if size_m == 0: return [] size_n = len(matrix[0]) if size_n == 0: return [] up, down, left, right = 0, size_m - 1, 0, size_n - 1 ans = [] while True: for i in range(left, right + 1): ans.append(matrix[up][i]) up += 1 if up > down: break for i in range(up, down + 1): ans.append(matrix[i][right]) right -= 1 if right < left: break for i in range(right, left - 1, -1): ans.append(matrix[down][i]) down -= 1 if down < up: break for i in range(down, up - 1, -1): ans.append(matrix[i][left]) left += 1 if left > right: break return ans ``` ================================================ FILE: docs/solutions/LCR/ti-huan-kong-ge-lcof.md ================================================ # [LCR 122. 路径加密](https://leetcode.cn/problems/ti-huan-kong-ge-lcof/) - 标签:字符串 - 难度:简单 ## 题目链接 - [LCR 122. 路径加密 - 力扣](https://leetcode.cn/problems/ti-huan-kong-ge-lcof/) ## 题目大意 给定一个字符串 `s`。 要求:将字符串 `s` 中的每个空格换成 `%20`。 ## 解题思路 Python 的字符串是不可变类型,所以需要先用数组存储答案,再将其转为字符串返回。具体操作如下。 - 定义数组 `res`,遍历字符串 `s`。 - 如果当前字符 `ch` 为空格,则将 ` %20` 加入到数组中。 - 如果当前字符 `ch` 不为空格,则直接加入到数组中。 - 遍历完之后,通过 `join` 将其转为字符串返回。 ## 代码 ```python class Solution: def replaceSpace(self, s: str) -> str: res = [] for ch in s: if ch == ' ': res.append("%20") else: res.append(ch) return "".join(res) ``` ================================================ FILE: docs/solutions/LCR/tvdfij.md ================================================ # [LCR 012. 寻找数组的中心下标](https://leetcode.cn/problems/tvdfij/) - 标签:数组、前缀和 - 难度:简单 ## 题目链接 - [LCR 012. 寻找数组的中心下标 - 力扣](https://leetcode.cn/problems/tvdfij/) ## 题目大意 给定一个数组 `nums`。 要求:找到「左侧元素和」与「右侧元素和相等」的位置,如果找不到,则返回 `-1`。 ## 解题思路 两次遍历,第一次遍历先求出数组全部元素和。第二次遍历找到左侧元素和恰好为全部元素和一半的位置。 ## 代码 ```python class Solution: def pivotIndex(self, nums: List[int]) -> int: sum = 0 for i in range(len(nums)): sum += nums[i] curr_sum = 0 for i in range(len(nums)): if curr_sum * 2 + nums[i] == sum: return i curr_sum += nums[i] return -1 ``` ================================================ FILE: docs/solutions/LCR/uUsW3B.md ================================================ # [LCR 080. 组合](https://leetcode.cn/problems/uUsW3B/) - 标签:数组、回溯 - 难度:中等 ## 题目链接 - [LCR 080. 组合 - 力扣](https://leetcode.cn/problems/uUsW3B/) ## 题目大意 给定两个整数 `n` 和 `k`。 要求:返回范围 `[1, n]` 中所有可能的 `k` 个数的组合。可以按任何顺序返回答案。 ## 解题思路 组合问题通常可以用回溯算法来解决。定义两个数组 `res`、`path`。`res` 用来存放最终答案,`path` 用来存放当前符合条件的一个结果。再使用一个变量 `start_index` 来表示从哪一个数开始遍历。 定义回溯方法,`start_index = 1` 开始进行回溯。 - 如果 `path` 数组的长度等于 `k`,则将 `path` 中的元素加入到 `res` 数组中。 - 然后对 `[start_index, n]` 范围内的数进行遍历取值。 - 将当前元素 `i` 加入 `path` 数组。 - 递归遍历 `[start_index, n]` 上的数。 - 将遍历的 `i` 元素进行回退。 - 最终返回 `res` 数组。 ## 代码 ```python class Solution: res = [] path = [] def backtrack(self, n: int, k: int, start_index: int): if len(self.path) == k: self.res.append(self.path[:]) return for i in range(start_index, n - (k - len(self.path)) + 2): self.path.append(i) self.backtrack(n, k, i + 1) self.path.pop() def combine(self, n: int, k: int) -> List[List[int]]: self.res.clear() self.path.clear() self.backtrack(n, k, 1) return self.res ``` ================================================ FILE: docs/solutions/LCR/vEAB3K.md ================================================ # [LCR 106. 判断二分图](https://leetcode.cn/problems/vEAB3K/) - 标签:深度优先搜索、广度优先搜索、并查集、图 - 难度:中等 ## 题目链接 - [LCR 106. 判断二分图 - 力扣](https://leetcode.cn/problems/vEAB3K/) ## 题目大意 给定一个代表 n 个节点的无向图的二维数组 `graph`,其中 `graph[u]` 是一个节点数组,由节点 `u` 的邻接节点组成。对于 `graph[u]` 中的每个 `v`,都存在一条位于节点 `u` 和节点 `v` 之间的无向边。 该无向图具有以下属性: - 不存在自环(`graph[u]` 不包含 `u`)。 - 不存在平行边(`graph[u]` 不包含重复值)。 - 如果 `v` 在 `graph[u]` 内,那么 `u` 也应该在 `graph[v]` 内(该图是无向图)。 - 这个图可能不是连通图,也就是说两个节点 `u` 和 `v` 之间可能不存在一条连通彼此的路径。 要求:判断该图是否是二分图,如果是二分图,则返回 `True`;否则返回 `False`。 - 二分图:如果能将一个图的节点集合分割成两个独立的子集 `A` 和 `B`,并使图中的每一条边的两个节点一个来自 `A` 集合,一个来自 `B` 集合,就将这个图称为 二分图 。 ## 解题思路 对于图中的任意节点 `u` 和 `v`,如果 `u` 和 `v` 之间有一条无向边,那么 `u` 和 `v` 必然属于不同的集合。 我们可以通过在深度优先搜索中对邻接点染色标记的方式,来识别该图是否是二分图。具体做法如下: - 找到一个没有染色的节点 `u`,将其染成红色。 - 然后遍历该节点直接相连的节点 `v`,如果该节点没有被染色,则将该节点直接相连的节点染成蓝色,表示两个节点不是同一集合。如果该节点已经被染色并且颜色跟 `u` 一样,则说明该图不是二分图,直接返回 `False`。 - 从上面染成蓝色的节点 `v` 出发,遍历该节点直接相连的节点。。。依次类推的递归下去。 - 如果所有节点都顺利染上色,则说明该图为二分图,返回 `True`。否则,如果在途中不能顺利染色,则返回 `False`。 ## 代码 ```python class Solution: def dfs(self, graph, colors, i, color): colors[i] = color for j in graph[i]: if colors[j] == colors[i]: return False if colors[j] == 0 and not self.dfs(graph, colors, j, -color): return False return True def isBipartite(self, graph: List[List[int]]) -> bool: size = len(graph) colors = [0 for _ in range(size)] for i in range(size): if colors[i] == 0 and not self.dfs(graph, colors, i, 1): return False return True ``` ================================================ FILE: docs/solutions/LCR/vlzXQL.md ================================================ # [LCR 111. 除法求值](https://leetcode.cn/problems/vlzXQL/) - 标签:深度优先搜索、广度优先搜索、并查集、图、数组、最短路 - 难度:中等 ## 题目链接 - [LCR 111. 除法求值 - 力扣](https://leetcode.cn/problems/vlzXQL/) ## 题目大意 给定一个变量对数组 `equations` 和一个实数数组 `values` 作为已知条件,其中 `equations[i] = [Ai, Bi]` 和 `values[i]` 共同表示 `Ai / Bi = values[i]`。每个 `Ai` 或 `Bi` 是一个表示单个变量的字符串。 再给定一个表示多个问题的数组 `queries`,其中 `queries[j] = [Cj, Dj]` 表示第 `j` 个问题,要求:根据已知条件找出 `Cj / Dj = ?` 的结果作为答案。返回所有问题的答案。如果某个答案无法确定,则用 `-1.0` 代替,如果问题中出现了给定的已知条件中没有出现的表示变量的字符串,则也用 `-1.0` 代替这个答案。 ## 解题思路 在「[等式方程的可满足性](https://leetcode.cn/problems/satisfiability-of-equality-equations)」的基础上增加了倍数关系。在「[等式方程的可满足性](https://leetcode.cn/problems/satisfiability-of-equality-equations)」中我们处理传递关系使用了并查集,这道题也是一样,不过在使用并查集的同时还要维护倍数关系。 举例说明: - `a / b = 2.0`:说明 `a = 2b`,`a` 和 `b` 在同一个集合。 - `b / c = 3.0`:说明 `b = 3c`,`b` 和 `c` 在同一个集合。 根据上述两式可得:`a`、`b`、`c` 都在一个集合中,且 `a = 2b = 6c`。 我们可以将同一集合中的变量倍数关系都转换为与根节点变量的倍数关系,比如上述例子中都转变为与 `a` 的倍数关系。 具体操作如下: - 定义并查集结构,并在并查集中定义一个表示倍数关系的 `multiples` 数组。 - 遍历 `equations` 数组、`values` 数组,将每个变量按顺序编号,并使用 `union` 将其并入相同集合。 - 遍历 `queries` 数组,判断两个变量是否在并查集中,并且是否在同一集合。如果找到对应关系,则将计算后的倍数关系存入答案数组,否则则将 `-1` 存入答案数组。 - 最终输出答案数组。 并查集中维护倍数相关方法说明: - `find` 方法: - 递推寻找根节点,并将倍数累乘,然后进行路径压缩,并且更新当前节点的倍数关系。 - `union` 方法: - 如果两个节点属于同一集合,则直接返回。 - 如果两个节点不属于同一个集合,合并之前当前节点的倍数关系更新,然后再进行更新。 - `is_connect` 方法: - 如果两个节点不属于同一集合,返回 `-1`。 - 如果两个节点属于同一集合,则返回倍数关系。 ## 代码 ```python class UnionFind: def __init__(self, n): self.parent = [i for i in range(n)] self.multiples = [1 for _ in range(n)] def find(self, x): multiple = 1.0 origin = x while x != self.parent[x]: multiple *= self.multiples[x] x = self.parent[x] self.parent[origin] = x self.multiples[origin] = multiple return x def union(self, x, y, multiple): root_x = self.find(x) root_y = self.find(y) if root_x == root_y: return self.parent[root_x] = root_y self.multiples[root_x] = multiple * self.multiples[y] / self.multiples[x] return def is_connected(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x != root_y: return -1.0 return self.multiples[x] / self.multiples[y] class Solution: def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]: equations_size = len(equations) hash_map = dict() union_find = UnionFind(2 * equations_size) id = 0 for i in range(equations_size): equation = equations[i] var1, var2 = equation[0], equation[1] if var1 not in hash_map: hash_map[var1] = id id += 1 if var2 not in hash_map: hash_map[var2] = id id += 1 union_find.union(hash_map[var1], hash_map[var2], values[i]) queries_size = len(queries) res = [] for i in range(queries_size): query = queries[i] var1, var2 = query[0], query[1] if var1 not in hash_map or var2 not in hash_map: res.append(-1.0) else: id1 = hash_map[var1] id2 = hash_map[var2] res.append(union_find.is_connected(id1, id2)) return res ``` ================================================ FILE: docs/solutions/LCR/vvXgSW.md ================================================ # [LCR 078. 合并 K 个升序链表](https://leetcode.cn/problems/vvXgSW/) - 标签:链表、分治、堆(优先队列)、归并排序 - 难度:困难 ## 题目链接 - [LCR 078. 合并 K 个升序链表 - 力扣](https://leetcode.cn/problems/vvXgSW/) ## 题目大意 给定一个链表数组 `lists`,每个链表都已经按照升序排列。 要求:将所有链表合并到一个升序链表中,返回合并后的链表。 ## 解题思路 分而治之的思想。将链表数组不断二分,转为规模为二分之一的子问题,然后再进行归并排序。 ## 代码 ```python class Solution: def merge_sort(self, lists: List[ListNode], left: int, right: int) -> ListNode: if left == right: return lists[left] mid = left + (right - left) // 2 node_left = self.merge_sort(lists, left, mid) node_right = self.merge_sort(lists, mid + 1, right) return self.merge(node_left, node_right) def merge(self, a: ListNode, b: ListNode) -> ListNode: root = ListNode(-1) cur = root while a and b: if a.val < b.val: cur.next = a a = a.next else: cur.next = b b = b.next cur = cur.next if a: cur.next = a if b: cur.next = b return root.next def mergeKLists(self, lists: List[ListNode]) -> ListNode: if not lists: return None size = len(lists) return self.merge_sort(lists, 0, size - 1) ``` ================================================ FILE: docs/solutions/LCR/w3tCBm.md ================================================ # [LCR 003. 比特位计数](https://leetcode.cn/problems/w3tCBm/) - 标签:位运算、动态规划 - 难度:简单 ## 题目链接 - [LCR 003. 比特位计数 - 力扣](https://leetcode.cn/problems/w3tCBm/) ## 题目大意 给定一个整数 `n`。 要求:对于 `0 ≤ i ≤ n` 的每一个 `i`,计算其二进制表示中 `1` 的个数,返回一个长度为 `n + 1` 的数组 `ans` 作为答案。 ## 解题思路 可以根据整数的二进制特点将其分为两类: - 奇数:一定比前面相邻的偶数多一个 `1`。 - 偶数:一定和除以 `2` 之后的数一样多。 - 边界 `0`:`1` 的个数为 `0`。 于是可以根据规律,从 `0` 开始到 `n` 进行递推求解。 ## 代码 ```python class Solution: def countBits(self, n: int) -> List[int]: dp = [0 for _ in range(n + 1)] for i in range(1, n + 1): if i % 2 == 1: dp[i] = dp[i - 1] + 1 else: dp[i] = dp[i // 2] return dp ``` ================================================ FILE: docs/solutions/LCR/w6cpku.md ================================================ # [LCR 054. 把二叉搜索树转换为累加树](https://leetcode.cn/problems/w6cpku/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [LCR 054. 把二叉搜索树转换为累加树 - 力扣](https://leetcode.cn/problems/w6cpku/) ## 题目大意 给定一棵二叉搜索树(BST)的根节点 `root`,且二叉搜索树的节点值各不相同。要求将其转化为「累加树」,使其每个节点 `node` 的新值等于原树中大于或等于 `node.val` 的值之和。 二叉搜索树的定义: - 如果左子树不为空,则左子树上所有节点值均小于它的根节点值; - 如果右子树不为空,则右子树上所有节点值均大于它的根节点值; - 任意节点的左、右子树也分别为二叉搜索树。 ## 解题思路 题目要求将每个节点的值修改为原来的节点值加上大于它的节点值之和。已知二叉搜索树的中序遍历可以得到一个升序数组。 题目就可以变为:修改升序数组中每个节点值为末尾元素累加和。由于末尾元素累加和的求和过程和遍历顺序相反,所以我们可以考虑换种思路。 二叉搜索树的中序遍历顺序为:左 -> 根 -> 右,从而可以得到一个升序数组,那么我们将左右反着遍历,即顺序为:右 -> 根 -> 左,就可以得到一个降序数组,这样就可以在遍历的同时求前缀和。 当然我们在计算前缀和的时候,需要用到前一个节点的值,所以需要用变量 `pre` 存储前一节点的值。 ## 代码 ```python class Solution: pre = 0 def createBinaryTree(self, root: TreeNode): if not root: return self.createBinaryTree(root.right) root.val += self.pre self.pre = root.val self.createBinaryTree(root.left) def convertBST(self, root: TreeNode) -> TreeNode: self.pre = 0 self.createBinaryTree(root) return root ``` ================================================ FILE: docs/solutions/LCR/wtcaE1.md ================================================ # [LCR 016. 无重复字符的最长子串](https://leetcode.cn/problems/wtcaE1/) - 标签:哈希表、字符串、滑动窗口 - 难度:中等 ## 题目链接 - [LCR 016. 无重复字符的最长子串 - 力扣](https://leetcode.cn/problems/wtcaE1/) ## 题目大意 给定一个字符串 `s`。 要求:找出其中不含有重复字符的 最长子串 的长度。 ## 解题思路 利用集合来存储不重复的字符。用两个指针分别指向最长子串的左右节点。遍历字符串,右指针不断右移,利用集合来判断有没有重复的字符,如果没有,就持续向右扩大右边界。如果出现重复字符,就缩小左侧边界。每次移动终止,都要计算一下当前不含重复字符的子串长度,并判断一下是否需要更新最大长度。 ## 代码 ```python class Solution: def lengthOfLongestSubstring(self, s: str) -> int: if not s: return 0 letterSet = set() right = 0 ans = 0 for i in range(len(s)): if i != 0: letterSet.remove(s[i - 1]) while right < len(s) and s[right] not in letterSet: letterSet.add(s[right]) right += 1 ans = max(ans, right - i) return ans ``` ================================================ FILE: docs/solutions/LCR/xoh6Oh.md ================================================ # [LCR 001. 两数相除](https://leetcode.cn/problems/xoh6Oh/) - 标签:位运算、数学 - 难度:简单 ## 题目链接 - [LCR 001. 两数相除 - 力扣](https://leetcode.cn/problems/xoh6Oh/) ## 题目大意 给定两个整数,被除数 dividend 和除数 divisor。要求返回两数相除的商,并且不能使用乘法,除法和取余运算。取值范围在 $[-2^{31}, 2^{31}-1]$。如果结果溢出,则返回 $2^{31} - 1$。 ## 解题思路 题目要求不能使用乘法,除法和取余运算。 可以把被除数和除数当做二进制,这样进行运算的时候,就可以通过移位运算来实现二进制的乘除。 - 先将除数不断左移,移位到位数大于或等于被除数。记录其移位次数 count。 - 然后再将除数右移 count 次,模拟二进制除法运算。 - 如果当前被除数大于等于除数,则将 1 左移 count 位,即为当前位的商,并将其累加答案上。再用除数减去被除数,进行下一次运算。 ## 代码 ```python 添加备注 class Solution: def divide(self, a: int, b: int) -> int: MIN_INT, MAX_INT = -2147483648, 2147483647 symbol = True if (a ^ b) < 0 else False if a < 0: a = -a if b < 0: b = -b # 除数不断左移,移位到位数大于或等于被除数 count = 0 while a >= b: count += 1 b <<= 1 # 向右移位,不断模拟二进制除法运算 res = 0 while count > 0: count -= 1 b >>= 1 if a >= b: res += (1 << count) a -= b if symbol: res = -res if MIN_INT <= res <= MAX_INT: return res else: return MAX_INT ``` ================================================ FILE: docs/solutions/LCR/xu-lie-hua-er-cha-shu-lcof.md ================================================ # [LCR 156. 序列化与反序列化二叉树](https://leetcode.cn/problems/xu-lie-hua-er-cha-shu-lcof/) - 标签:树、深度优先搜索、广度优先搜索、设计、字符串、二叉树 - 难度:困难 ## 题目链接 - [LCR 156. 序列化与反序列化二叉树 - 力扣](https://leetcode.cn/problems/xu-lie-hua-er-cha-shu-lcof/) ## 题目大意 给定一棵二叉树的根节点 `root`。 要求:设计一个算法,来实现二叉树的序列化与反序列化。 ## 解题思路 1. 序列化:将二叉树转为字符串数据表示 按照前序递归遍历二叉树,并将根节点跟左右子树的值链接起来(中间用 `,` 隔开)。 注意:如果遇到空节点,则标记为 'None',这样在反序列化时才能唯一确定一棵二叉树。 2. 反序列化:将字符串数据转为二叉树结构 先将字符串按 `,` 分割成数组。然后递归处理每一个元素。 - 从数组左侧取出一个元素。 - 如果当前元素为 'None',则返回 None。 - 如果当前元素不为空,则新建一个二叉树节点作为根节点,保存值为当前元素值。并递归遍历左右子树,不断重复从数组中取出元素,进行判断。 - 最后返回当前根节点。 ## 代码 ```python class Codec: def serialize(self, root): """Encodes a tree to a single string. :type root: TreeNode :rtype: str """ if not root: return 'None' return str(root.val) + ',' + str(self.serialize(root.left)) + ',' + str(self.serialize(root.right)) def deserialize(self, data): """Decodes your encoded data to tree. :type data: str :rtype: TreeNode """ def dfs(datalist): val = datalist.pop(0) if val == 'None': return None root = TreeNode(int(val)) root.left = dfs(datalist) root.right = dfs(datalist) return root datalist = data.split(',') return dfs(datalist) ``` ================================================ FILE: docs/solutions/LCR/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof.md ================================================ # [LCR 128. 库存管理 I](https://leetcode.cn/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/) - 标签:数组、二分查找 - 难度:简单 ## 题目链接 - [LCR 128. 库存管理 I - 力扣](https://leetcode.cn/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/) ## 题目大意 给定一个数组 `numbers`,`numbers` 是有升序数组经过「旋转」得到的。但是旋转次数未知。数组中可能存在重复元素。 要求:找出数组中的最小元素。 - 旋转:将数组整体右移。 ## 解题思路 数组经过「旋转」之后,会有两种情况,第一种就是原先的升序序列,另一种是两段升序的序列。 第一种的最小值在最左边。第二种最小值在第二段升序序列的第一个元素。 ``` * * * * * * ``` ``` * * * * * * ``` 最直接的办法就是遍历一遍,找到最小值。但是还可以有更好的方法。考虑用二分查找来降低算法的时间复杂度。 创建两个指针 left、right,分别指向数组首尾。让后计算出两个指针中间值 mid。将 mid 与右边界进行比较。 1. 如果 `numbers[mid] > numbers[right]`,则最小值不可能在 `mid` 左侧,一定在 `mid` 右侧,则将 `left` 移动到 `mid + 1` 位置,继续查找右侧区间。 2. 如果 `numbers[mid] < numbers[right]`,则最小值一定在 `mid` 左侧,将 `right` 移动到 `mid` 位置上,继续查找左侧区间。 3. 当 `numbers[mid] == numbers[right]`,无法判断在 `mid` 的哪一侧,可以采用 `right = right - 1` 逐步缩小区域。 ## 代码 ```python class Solution: def minArray(self, numbers: List[int]) -> int: left = 0 right = len(numbers) - 1 while left < right: mid = left + (right - left) // 2 if numbers[mid] > numbers[right]: left = mid + 1 elif numbers[mid] < numbers[right]: right = mid else: right = right - 1 return numbers[left] ``` ================================================ FILE: docs/solutions/LCR/xx4gT2.md ================================================ # [LCR 076. 数组中的第 K 个最大元素](https://leetcode.cn/problems/xx4gT2/) - 标签:数组、分治、快速选择、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [LCR 076. 数组中的第 K 个最大元素 - 力扣](https://leetcode.cn/problems/xx4gT2/) ## 题目大意 给定一个未排序的数组 `nums`,从中找到第 `k` 个最大的数字。 ## 解题思路 很不错的一道题,面试常考。 直接可以想到的思路是:排序后输出数组上对应第 k 位大的数。所以问题关键在于排序方法的复杂度。 冒泡排序、选择排序、插入排序时间复杂度 $O(n^2)$ 太高了,解答会超时。 可考虑堆排序、归并排序、快速排序。 这道题的要求是找到第 k 大的元素,使用归并排序只有到最后排序完毕才能返回第 k 大的数。而堆排序每次排序之后,就会确定一个元素的准确排名,同理快速排序也是如此。 ### 1. 堆排序 升序堆排序的思路如下: 1. 先建立大顶堆 2. 让堆顶最大元素与最后一个交换,然后调整第一个元素到倒数第二个元素,这一步获取最大值 3. 再交换堆顶元素与倒数第二个元素,然后调整第一个元素到倒数第三个元素,这一步获取第二大值 4. 以此类推,直到最后一个元素交换之后完毕。 这道题我们只需进行 1 次建立大顶堆, k-1 次调整即可得到第 k 大的数。 时间复杂度:$O(n^2)$ ### 2. 快速排序 快速排序每次调整,都会确定一个元素的最终位置,且以该元素为界限,将数组分成了两个数组,前一个数组元素都比该元素小,后一个元素都比该元素大。 这样,只要某次划分的元素恰好是第 k 个下标就找到了答案。并且我们只需关注 k 元素所在区间的排序情况,与 k 元素无关的区间排序都可以忽略。这样进一步减少了执行步骤。 ### 3. 借用标准库(不建议) 提交代码中的最快代码是调用了 Python 的 heapq 库,或者 sort 方法。 这样的确可以通过,但是不建议这样做。借用标准库实现,只能说对这个库的 API 和相关数据结构的用途相对熟悉,而不代表着掌握了这个数据结构。可以问问自己,如果换一种语言,自己还能不能实现对应的数据结构?刷题的本质目的是为了把算法学会学透,而不仅仅是调 API。 ## 代码 1. 堆排序 ```python class Solution: def findKthLargest(self, nums: List[int], k: int) -> int: # 调整为大顶堆 def heapify(nums, index, end): left = index * 2 + 1 right = left + 1 while left <= end: # 当前节点为非叶子节点 max_index = index if nums[left] > nums[max_index]: max_index = left if right <= end and nums[right] > nums[max_index]: max_index = right if index == max_index: # 如果不用交换,则说明已经交换结束 break nums[index], nums[max_index] = nums[max_index], nums[index] # 继续调整子树 index = max_index left = index * 2 + 1 right = left + 1 # 初始化大顶堆 def buildMaxHeap(nums): size = len(nums) # (size-2) // 2 是最后一个非叶节点,叶节点不用调整 for i in range((size - 2) // 2, -1, -1): heapify(nums, i, size - 1) return nums buildMaxHeap(nums) size = len(nums) for i in range(k-1): nums[0], nums[size-i-1] = nums[size-i-1], nums[0] heapify(nums, 0, size-i-2) return nums[0] ``` 2. 快速排序 ```python import random class Solution: def findKthLargest(self, nums: List[int], k: int) -> int: def randomPartition(nums, low, high): i = random.randint(low, high) nums[i], nums[high] = nums[high], nums[i] return partition(nums, low, high) def partition(nums, low, high): x = nums[high] i = low-1 for j in range(low, high): if nums[j] <= nums[high]: i += 1 nums[i], nums[j] = nums[j], nums[i] nums[i+1], nums[high] = nums[high], nums[i+1] return i+1 def quickSort(nums, low, high, k): n = len(nums) if low < high: pi = randomPartition(nums, low, high) if pi == n-k: return nums[len(nums)-k] if pi > n-k: quickSort(nums, low, pi-1, k) if pi < n-k: quickSort(nums, pi+1, high, k) return nums[len(nums)-k] return quickSort(nums, 0, len(nums)-1, k) ``` 3. 借用标准库 ```python class Solution: def findKthLargest(self, nums: List[int], k: int) -> int: nums.sort() return nums[len(nums)-k] ``` ```python import heapq class Solution: def findKthLargest(self, nums: List[int], k: int) -> int: res = [] for n in nums: if len(res) < k: heapq.heappush(res, n) elif n > res[0]: heapq.heappop(res) heapq.heappush(res, n) return heapq.heappop(res) ``` ================================================ FILE: docs/solutions/LCR/yong-liang-ge-zhan-shi-xian-dui-lie-lcof.md ================================================ # [LCR 125. 图书整理 II](https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/) - 标签:栈、设计、队列 - 难度:简单 ## 题目链接 - [LCR 125. 图书整理 II - 力扣](https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/) ## 题目大意 要求:使用两个栈实现先入先出队列。需要实现对应的两个函数: - `appendTail`:在队列尾部插入整数。 - `deleteHead`:在队列头部删除整数(如果队列中没有元素,`deleteHead` 返回 -1)。 ## 解题思路 使用两个栈,inStack 用于输入,outStack 用于输出。 - `appendTail` 操作:将元素压入 inStack 中 - `deleteHead` 操作: - 先判断 `inStack` 和 `outStack` 是否都为空,如果都为空则说明队列中没有元素,直接返回 `-1`。 - 如果 `outStack` 输出栈为空,将 `inStack` 输入栈元素依次取出,按顺序压入 `outStack` 栈。这样 `outStack` 栈的元素顺序和之前 `inStack` 元素顺序相反,`outStack` 顶层元素就是要取出的队头元素,将其移出,并返回该元素。如果 `outStack` 输出栈不为空,则直接取出顶层元素。 ## 代码 ```python class CQueue: def __init__(self): self.inStack = [] self.outStack = [] def appendTail(self, value: int) -> None: self.inStack.append(value) def deleteHead(self) -> int: if len(self.outStack) == 0 and len(self.inStack) == 0: return -1 if (len(self.outStack) == 0): while (len(self.inStack) != 0): self.outStack.append(self.inStack[-1]) self.inStack.pop() top = self.outStack[-1] self.outStack.pop() return top ``` ================================================ FILE: docs/solutions/LCR/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof.md ================================================ # [LCR 187. 破冰游戏](https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/) - 标签:递归、数学 - 难度:简单 ## 题目链接 - [LCR 187. 破冰游戏 - 力扣](https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/) ## 题目大意 **描述**:$0$、$1$、…、$n - 1$ 这 $n$ 个数字排成一个圆圈,从数字 $0$ 开始,每次从圆圈里删除第 $m$ 个数字。现在给定整数 $n$ 和 $m$。 **要求**:求出这个圆圈中剩下的最后一个数字。 **说明**: - $1 \le num \le 10^5$。 - $1 \le target \le 10^6$。 **示例**: - 示例 1: ```python 输入:num = 7, target = 4 输出:1 ``` - 示例 2: ```python 输入:num = 12, target = 5 输出:0 ``` ## 解题思路 ### 思路 1:枚举 + 模拟 模拟循环删除,需要进行 $n - 1$ 轮,每轮需要对节点进行 $m$ 次访问操作。总体时间复杂度为 $O(n \times m)$。 可以通过找规律来做,以 $n = 5$、$m = 3$ 为例。 - 刚开始为 $0$、$1$、$2$、$3$、$4$。 - 第一次从 $0$ 开始数,数 $3$ 个数,于是 $2$ 出圈,变为 $3$、$4$、$0$、$1$。 - 第二次从 $3$ 开始数,数 $3$ 个数,于是 $0$ 出圈,变为 $1$、$3$、$4$。 - 第三次从 $1$ 开始数,数 $3$ 个数,于是 $4$ 出圈,变为 $1$、$3$。 - 第四次从 $1$ 开始数,数 $3$ 个数,于是 $1$ 出圈,变为 $3$。 - 所以最终为 $3$。 通过上面的流程可以发现:每隔 $m$ 个数就要删除一个数,那么被删除的这个数的下一个数就会成为新的起点。就相当于数组进行左移了 $m$ 位。反过来思考的话,从最后一步向前推,则每一步都向右移动了 $m$ 位(包括胜利者)。 如果用 $f(n, m)$ 表示: $n$ 个数构成环没删除 $m$ 个数后,最终胜利者的位置,则 $f(n, m) = f(n - 1, m) + m$。 即等于 $n - 1$ 个数构成的环没删除 $m$ 个数后最终胜利者的位置,像右移动 $m$ 次。 问题是现在并不是真的进行了右移,因为当前数组右移后超过数组容量的部分应该重新放到数组头部位置。所以公式应为:$f(n, m) = [f(n - 1, m) + m] \mod n$,$n$ 为反过来向前推的时候,每一步剩余的数字个数(比如第二步推回第一步,n $4$),则反过来递推公式为: - $f(1, m) = 0$。 - $f(2, m) = [f(1, m) + m] \mod 2$。 - $f(3, m) = [f(2, m) + m] \mod 3$。 - 。。。。。。 - $f(n, m) = [f(n - 1, m) + m] \mod n $。 接下来就是递推求解了。 ### 思路 1:代码 ```python class Solution: def lastRemaining(self, n: int, m: int) -> int: ans = 0 for i in range(2, n + 1): ans = (m + ans) % i return ans ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n)$。 - **空间复杂度**:$O(1)$。 ## 参考资料: - [字节题库 - #剑62 - 简单 - 圆圈中最后剩下的数字 - 1刷](https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/zi-jie-ti-ku-jian-62-jian-dan-yuan-quan-3hlji/) ================================================ FILE: docs/solutions/LCR/z1R5dt.md ================================================ # [LCR 066. 键值映射](https://leetcode.cn/problems/z1R5dt/) - 标签:设计、字典树、哈希表、字符串 - 难度:中等 ## 题目链接 - [LCR 066. 键值映射 - 力扣](https://leetcode.cn/problems/z1R5dt/) ## 题目大意 要求:实现一个 MapSum 类,支持两个方法,`insert` 和 `sum`: - `MapSum()` 初始化 MapSum 对象。 - `void insert(String key, int val)` 插入 `key-val` 键值对,字符串表示键 `key`,整数表示值 `val`。如果键 `key` 已经存在,那么原来的键值对将被替代成新的键值对。 - `int sum(string prefix)` 返回所有以该前缀 `prefix` 开头的键 `key` 的值的总和。 ## 解题思路 可以构造前缀树(字典树)解题。 - 初始化时,构建一棵前缀树(字典树),并增加 `val` 变量。 - 调用插入方法时,用字典树存储 `key`,并在对应字母节点存储对应的 `val`。 - 在调用查询总和方法时,先查找该前缀 `prefix` 对应的前缀树节点,从该节点开始,递归遍历该节点的子节点,并累积子节点的 `val`,进行求和,并返回求和累加结果。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False self.value = 0 def insert(self, word: str, value: int) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True cur.value = value def search(self, word: str) -> int: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return 0 cur = cur.children[ch] return self.dfs(cur) def dfs(self, root) -> int: if not root: return 0 res = root.value for node in root.children.values(): res += self.dfs(node) return res class MapSum: def __init__(self): """ Initialize your data structure here. """ self.trie_tree = Trie() def insert(self, key: str, val: int) -> None: self.trie_tree.insert(key, val) def sum(self, prefix: str) -> int: return self.trie_tree.search(prefix) ``` ================================================ FILE: docs/solutions/LCR/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof.md ================================================ # [LCR 172. 统计目标成绩的出现次数](https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/) - 标签:数组、二分查找 - 难度:简单 ## 题目链接 - [LCR 172. 统计目标成绩的出现次数 - 力扣](https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/) ## 题目大意 给定一个排序数组 `nums`,以及一个整数 `target`。 要求:统计 `target` 在排序数组 `nums` 中出现的次数。 ## 解题思路 两次二分查找。 - 先查找 `target` 第一次出现的位置(下标):`left`。 - 再查找 `target` 最后一次出现的位置(下标):`right`。 - 最终答案为 `right - left + 1`。 ## 代码 ```python class Solution: def searchLeft(self, nums, target): left, right = 0, len(nums) - 1 while left < right: mid = left + (right - left) // 2 if nums[mid] < target: left = mid + 1 else: right = mid if nums[left] == target: return left else: return -1 def searchRight(self, nums, target): left, right = 0, len(nums) - 1 while left < right: mid = left + (right - left + 1) // 2 if nums[mid] <= target: left = mid else: right = mid - 1 return left def search(self, nums: List[int], target: int) -> int: if len(nums) == 0: return 0 left = self.searchLeft(nums, target) right = self.searchRight(nums, target) if left == -1: return 0 return right - left + 1 ``` ================================================ FILE: docs/solutions/LCR/zhan-de-ya-ru-dan-chu-xu-lie-lcof.md ================================================ # [LCR 148. 验证图书取出顺序](https://leetcode.cn/problems/zhan-de-ya-ru-dan-chu-xu-lie-lcof/) - 标签:栈、数组、模拟 - 难度:中等 ## 题目链接 - [LCR 148. 验证图书取出顺序 - 力扣](https://leetcode.cn/problems/zhan-de-ya-ru-dan-chu-xu-lie-lcof/) ## 题目大意 给定连个整数序列 `pushed` 和 `popped`,其中 `pushed` 表示栈的压入顺序。 要求:判断第二个序列 `popped` 是否为栈的压出序列。 ## 解题思路 借助一个栈来模拟压入、压出的操作。检测最后是否能模拟成功。 ## 代码 ```python class Solution: def validateStackSequences(self, pushed: List[int], popped: List[int]) -> bool: stack = [] index = 0 for item in pushed: stack.append(item) while(stack and stack[-1] == popped[index]): stack.pop() index += 1 return len(stack) == 0 ``` ================================================ FILE: docs/solutions/LCR/zhong-jian-er-cha-shu-lcof.md ================================================ # [LCR 124. 推理二叉树](https://leetcode.cn/problems/zhong-jian-er-cha-shu-lcof/) - 标签:树、数组、哈希表、分治、二叉树 - 难度:中等 ## 题目链接 - [LCR 124. 推理二叉树 - 力扣](https://leetcode.cn/problems/zhong-jian-er-cha-shu-lcof/) ## 题目大意 给定一棵二叉树的前序遍历结果和中序遍历结果。 要求:构建该二叉树,并返回其根节点。假设树中没有重复的元素。 ## 解题思路 前序遍历的顺序是:根 -> 左 -> 右。中序遍历的顺序是:左 -> 根 -> 右。根据前序遍历的顺序,可以找到根节点位置。然后在中序遍历的结果中可以找到对应的根节点位置,就可以从根节点位置将二叉树分割成左子树、右子树。同时能得到左右子树的节点个数。此时构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历进行上述步骤,直到节点为空,具体操作步骤如下: - 从前序遍历顺序中当前根节点的位置在 `postorder[0]`。 - 通过在中序遍历中查找上一步根节点对应的位置 `inorder[k]`,从而将二叉树的左右子树分隔开,并得到左右子树节点的个数。 - 从上一步得到的左右子树个数将前序遍历结果中的左右子树分开。 - 构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。 ## 代码 ```python class Solution: def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: def createTree(preorder, inorder, n): if n == 0: return None k = 0 while preorder[0] != inorder[k]: k += 1 node = TreeNode(inorder[k]) node.left = createTree(preorder[1: k + 1], inorder[0: k], k) node.right = createTree(preorder[k + 1:], inorder[k + 1:], n - k - 1) return node return createTree(preorder, inorder, len(inorder)) ``` ================================================ FILE: docs/solutions/LCR/zi-fu-chuan-de-pai-lie-lcof.md ================================================ # [LCR 157. 套餐内商品的排列顺序](https://leetcode.cn/problems/zi-fu-chuan-de-pai-lie-lcof/) - 标签:字符串、回溯 - 难度:中等 ## 题目链接 - [LCR 157. 套餐内商品的排列顺序 - 力扣](https://leetcode.cn/problems/zi-fu-chuan-de-pai-lie-lcof/) ## 题目大意 给定一个字符串 `s`。 要求:打印出该字符串中字符的所有排列。可以以任意顺序返回这个字符串数组,但里边不能有重复元素。 ## 解题思路 因为原字符串可能含有重复元素,所以在回溯的时候需要进行去重。先将字符串 `s` 转为 `list` 列表,再对列表进行排序,然后使用 `visited` 数组标记该元素在当前排列中是否被访问过。如果未被访问过则将其加入排列中,并在访问后将该元素变为未访问状态。 然后再递归遍历下一层元素之前,增加一句语句进行判重:`if i > 0 and nums[i] == nums[i - 1] and not visited[i - 1]: continue`。 然后进行回溯遍历。 ## 代码 ```python class Solution: res = [] path = [] def backtrack(self, ls, visited): if len(self.path) == len(ls): self.res.append(''.join(self.path)) return for i in range(len(ls)): if i > 0 and ls[i] == ls[i - 1] and not visited[i - 1]: continue if not visited[i]: visited[i] = True self.path.append(ls[i]) self.backtrack(ls, visited) self.path.pop() visited[i] = False def permutation(self, s: str) -> List[str]: self.res.clear() self.path.clear() ls = list(s) ls.sort() visited = [False for _ in range(len(s))] self.backtrack(ls, visited) return self.res ``` ================================================ FILE: docs/solutions/LCR/zlDJc7.md ================================================ # [LCR 109. 打开转盘锁](https://leetcode.cn/problems/zlDJc7/) - 标签:广度优先搜索、数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [LCR 109. 打开转盘锁 - 力扣](https://leetcode.cn/problems/zlDJc7/) ## 题目大意 有一把带有四个数字的密码锁,每个位置上有 0~9 共 10 个数字。每次只能将其中一个位置上的数字转动一下。可以向上转,也可以向下转。比如:1 -> 2、2 -> 1。 密码锁的初始数字为:`0000`。现在给定一组表示死亡数字的字符串数组 `deadends`,和一个带有四位数字的目标字符串 `target`。 如果密码锁转动到 `deadends` 中任一字符串状态,则锁就会永久锁定,无法再次旋转。 要求:求出最小的选择次数,使得锁的状态由 `0000` 转动到 `target`。 ## 解题思路 使用宽度优先搜索遍历,将`0000` 状态入队。 - 将队列中的元素出队,判断是否为死亡字符串 - 如果为死亡字符串,则跳过该状态,否则继续执行。 - 如果为目标字符串,则返回当前路径长度,否则继续执行。 - 枚举当前状态所有位置所能到达的所有状态,并判断是否访问过该状态。 - 如果之前出现过该状态,则继续执行,否则将其存入队列,并标记访问。 ## 代码 ```python class Solution: def openLock(self, deadends: List[str], target: str) -> int: queue = collections.deque(['0000']) visited = set(['0000']) deadset = set(deadends) level = 0 while queue: size = len(queue) for _ in range(size): cur = queue.popleft() if cur in deadset: continue if cur == target: return level for i in range(len(cur)): up = self.upward_adjust(cur, i) if up not in visited: queue.append(up) visited.add(up) down = self.downward_adjust(cur, i) if down not in visited: queue.append(down) visited.add(down) level += 1 return -1 def upward_adjust(self, s, i): s_list = list(s) if s_list[i] == '9': s_list[i] = '0' else: s_list[i] = chr(ord(s_list[i]) + 1) return "".join(s_list) def downward_adjust(self, s, i): s_list = list(s) if s_list[i] == '0': s_list[i] = '9' else: s_list[i] = chr(ord(s_list[i]) - 1) return "".join(s_list) ``` ================================================ FILE: docs/solutions/LCR/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof.md ================================================ # [LCR 167. 招式拆解 I](https://leetcode.cn/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/) - 标签:哈希表、字符串、滑动窗口 - 难度:中等 ## 题目链接 - [LCR 167. 招式拆解 I - 力扣](https://leetcode.cn/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/) ## 题目大意 给定一个字符串 `s`。 要求:找出其中不含有重复字符的最长子串的长度。 ## 解题思路 利用集合来存储不重复的字符。用两个指针分别指向最长子串的左右节点。遍历字符串,右指针不断右移,利用集合来判断有没有重复的字符,如果没有,就持续向右扩大右边界。如果出现重复字符,就缩小左侧边界。每次移动终止,都要计算一下当前不含重复字符的子串长度,并判断一下是否需要更新最大长度。 ## 代码 ```python class Solution: def lengthOfLongestSubstring(self, s: str) -> int: if not s: return 0 letterSet = set() right = 0 ans = 0 for i in range(len(s)): if i != 0: letterSet.remove(s[i - 1]) while right < len(s) and s[right] not in letterSet: letterSet.add(s[right]) right += 1 ans = max(ans, right - i) return ans ``` ================================================ FILE: docs/solutions/LCR/zui-xiao-de-kge-shu-lcof.md ================================================ # [LCR 159. 库存管理 III](https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/) - 标签:数组、分治、快速选择、排序、堆(优先队列) - 难度:简单 ## 题目链接 - [LCR 159. 库存管理 III - 力扣](https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/) ## 题目大意 **描述**:给定整数数组 $arr$,再给定一个整数 $k$。 **要求**:返回数组 $arr$ 中最小的 $k$ 个数。 **说明**: - $0 \le k \le arr.length \le 10000$。 - $0 \le arr[i] \le 10000$。 **示例**: - 示例 1: ```python 输入:arr = [3,2,1], k = 2 输出:[1,2] 或者 [2,1] ``` - 示例 2: ```python 输入:arr = [0,1,2,1], k = 1 输出:[0] ``` ## 解题思路 直接可以想到的思路是:排序后输出数组上对应的最小的 k 个数。所以问题关键在于排序方法的复杂度。 冒泡排序、选择排序、插入排序时间复杂度 $O(n^2)$ 太高了,解答会超时。 可考虑堆排序、归并排序、快速排序。 ### 思路 1:堆排序(基于大顶堆) 具体做法如下: 1. 使用数组前 $k$ 个元素,维护一个大小为 $k$ 的大顶堆。 2. 遍历数组 $[k, size - 1]$ 的元素,判断其与堆顶元素关系,如果遇到比堆顶元素小的元素,则将与堆顶元素进行交换。再将这 $k$ 个元素调整为大顶堆。 3. 最后输出大顶堆的 $k$ 个元素。 ### 思路 1:代码 ```python class Solution: def heapify(self, nums: [int], index: int, end: int): left = index * 2 + 1 right = left + 1 while left <= end: # 当前节点为非叶子节点 max_index = index if nums[left] > nums[max_index]: max_index = left if right <= end and nums[right] > nums[max_index]: max_index = right if index == max_index: # 如果不用交换,则说明已经交换结束 break nums[index], nums[max_index] = nums[max_index], nums[index] # 继续调整子树 index = max_index left = index * 2 + 1 right = left + 1 # 初始化大顶堆 def buildMaxHeap(self, nums: [int], k: int): # (k-2) // 2 是最后一个非叶节点,叶节点不用调整 for i in range((k - 2) // 2, -1, -1): self.heapify(nums, i, k - 1) return nums def getLeastNumbers(self, arr: List[int], k: int) -> List[int]: size = len(arr) if k <= 0 or not arr: return [] if size <= k: return arr self.buildMaxHeap(arr, k) for i in range(k, size): if arr[i] < arr[0]: arr[i], arr[0] = arr[0], arr[i] self.heapify(arr, 0, k - 1) return arr[:k] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(n\log_2k)$。 - **空间复杂度**:$O(1)$。 ### 思路 2:快速排序 使用快速排序在每次调整时,都会确定一个元素的最终位置,且以该元素为界限,将数组分成了左右两个子数组,左子数组中的元素都比该元素小,右子树组中的元素都比该元素大。 这样,只要某次划分的元素恰好是第 $k$ 个元素下标,就找到了数组中最小的 $k$ 个数所对应的区间,即 $[0, k - 1]$。 并且我们只需关注第 $k$ 个最小元素所在区间的排序情况,与第 $k$ 个最小元素无关的区间排序都可以忽略。这样进一步减少了执行步骤。 ### 思路 2:代码 ```python import random class Solution: # 从 arr[low: high + 1] 中随机挑选一个基准数,并进行移动排序 def randomPartition(self, arr: [int], low: int, high: int): # 随机挑选一个基准数 i = random.randint(low, high) # 将基准数与最低位互换 arr[i], arr[low] = arr[low], arr[i] # 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上 return self.partition(arr, low, high) # 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上 def partition(self, arr: [int], low: int, high: int): pivot = arr[low] # 以第 1 为为基准数 i = low + 1 # 从基准数后 1 位开始遍历,保证位置 i 之前的元素都小于基准数 for j in range(i, high + 1): # 发现一个小于基准数的元素 if arr[j] < pivot: # 将小于基准数的元素 arr[j] 与当前 arr[i] 进行换位,保证位置 i 之前的元素都小于基准数 arr[i], arr[j] = arr[j], arr[i] # i 之前的元素都小于基准数,所以 i 向右移动一位 i += 1 # 将基准节点放到正确位置上 arr[i - 1], arr[low] = arr[low], arr[i - 1] # 返回基准数位置 return i - 1 def quickSort(self, arr, low, high, k): size = len(arr) if low < high: # 按照基准数的位置,将序列划分为左右两个子序列 pi = self.randomPartition(arr, low, high) if pi == k: return arr[:k] if pi > k: # 对左子序列进行递归快速排序 self.quickSort(arr, low, pi - 1, k) if pi < k: # 对右子序列进行递归快速排序 self.quickSort(arr, pi + 1, high, k) return arr[:k] def getLeastNumbers(self, arr: List[int], k: int) -> List[int]: size = len(arr) if k >= size: return arr return self.quickSort(arr, 0, size - 1, k) ``` ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。证明过程可参考「算法导论 9.2:期望为线性的选择算法」。 - **空间复杂度**:$O(\log n)$。递归使用栈空间的空间代价期望为 $O(\log n)$。 ================================================ FILE: docs/solutions/LCR/zuo-xuan-zhuan-zi-fu-chuan-lcof.md ================================================ # [LCR 182. 动态口令](https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/) - 标签:数学、双指针、字符串 - 难度:简单 ## 题目链接 - [LCR 182. 动态口令 - 力扣](https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/) ## 题目大意 给定一个字符串 `s` 和一个整数 `n`。 要求:将字符串 `s` 每个字符向左旋转 `n` 位。 - 左旋转:将字符串前面的若干字符转移到字符串的尾部。 ## 解题思路 - 使用数组 `res` 存放答案。 - 先遍历 `[n, len(s) - 1]` 范围的字符,将其存入数组。 - 再遍历 `[0, n - 1]` 范围的字符,将其存入数组。 - 将数组转为字符串返回。 ## 代码 ```python class Solution: def reverseLeftWords(self, s: str, n: int) -> str: res = [] for i in range(n, len(s)): res.append(s[i]) for i in range(n): res.append(s[i]) return "".join(res) ``` ================================================ FILE: docs/solutions/index.md ================================================ ## 本章内容 - [第 1 ~ 99 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0001-0099/) - [第 100 ~ 199 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0100-0199/) - [第 200 ~ 299 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0200-0299/) - [第 300 ~ 399 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/) - [第 400 ~ 499 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0400-0499/) - [第 500 ~ 599 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0500-0599/) - [第 600 ~ 699 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0600-0699/) - [第 700 ~ 799 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0700-0799/) - [第 800 ~ 899 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0800-0899/) - [第 900 ~ 999 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0900-0999/) - [第 1000 ~ 1099 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1000-1099/) - [第 1100 ~ 1199 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1100-1199/) - [第 1200 ~ 1299 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1200-1299/) - [第 1300 ~ 1399 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1300-1399/) - [第 1400 ~ 1499 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1400-1499/) - [第 1500 ~ 1599 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1500-1599/) - [第 1600 ~ 1699 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1600-1699/) - [第 1700 ~ 1799 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1700-1799/) - [第 1800 ~ 1899 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1800-1899/) - [第 1900 ~ 1999 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/1900-1999/) - [第 2000 ~ 2099 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2000-2099/) - [第 2100 ~ 2199 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2100-2199/) - [第 2200 ~ 2299 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2200-2299/) - [第 2300 ~ 2399 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2300-2399/) - [第 2400 ~ 2499 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2400-2499/) - [第 2500 ~ 2599 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2500-2599/) - [第 2700 ~ 2799 题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/2700-2799/) - [LCR 系列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/LCR/) - [面试题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/) ================================================ FILE: docs/solutions/interviews/bracket-lcci.md ================================================ # [面试题 08.09. 括号](https://leetcode.cn/problems/bracket-lcci/) - 标签:字符串、动态规划、回溯 - 难度:中等 ## 题目链接 - [面试题 08.09. 括号 - 力扣](https://leetcode.cn/problems/bracket-lcci/) ## 题目大意 给定一个整数 `n`。 要求:生成所有有可能且有效的括号组合。 ## 解题思路 通过回溯算法生成所有答案。为了生成的括号组合是有效的,回溯的时候,使用一个标记变量 `symbol` 来表示是否当前组合是否成对匹配。 如果在当前组合中增加一个 `(`,则 `symbol += 1`,如果增加一个 `)`,则 `symbol -= 1`。显然只有在 `symbol < n` 的时候,才能增加 `(`,在 `symbol > 0` 的时候,才能增加 `)`。 如果最终生成 `2 * n` 的括号组合,并且 `symbol == 0`,则说明当前组合是有效的,将其加入到最终答案数组中。 最终输出最终答案数组。 ## 代码 ```python class Solution: def generateParenthesis(self, n: int) -> List[str]: def backtrack(parenthesis, symbol, index): if n * 2 == index: if symbol == 0: parentheses.append(parenthesis) else: if symbol < n: backtrack(parenthesis + '(', symbol + 1, index + 1) if symbol > 0: backtrack(parenthesis + ')', symbol - 1, index + 1) parentheses = list() backtrack("", 0, 0) return parentheses ``` ================================================ FILE: docs/solutions/interviews/calculator-lcci.md ================================================ # [面试题 16.26. 计算器](https://leetcode.cn/problems/calculator-lcci/) - 标签:栈、数学、字符串 - 难度:中等 ## 题目链接 - [面试题 16.26. 计算器 - 力扣](https://leetcode.cn/problems/calculator-lcci/) ## 题目大意 给定一个包含正整数、加(`+`)、减(`-`)、乘(`*`)、除(`/`)的算出表达式(括号除外)。表达式仅包含非负整数,`+`、`-`、`*`、`/` 四种运算符和空格 ` `。整数除法仅保留整数部分。 要求:计算其结果。 ## 解题思路 计算表达式中,乘除运算优先于加减运算。我们可以先进行乘除运算,再将进行乘除运算后的整数值放入原表达式中相应位置,再依次计算加减。 可以考虑使用一个栈来保存进行乘除运算后的整数值。正整数直接压入栈中,负整数,则将对应整数取负号,再压入栈中。这样最终计算结果就是栈中所有元素的和。 具体做法: - 遍历字符串 s,使用变量 op 来标记数字之前的运算符,默认为 `+`。 - 如果遇到数字,继续向后遍历,将数字进行累积,得到完整的整数 num。判断当前 op 的符号。 - 如果 op 为 `+`,则将 num 压入栈中。 - 如果 op 为 `-`,则将 -num 压入栈中。 - 如果 op 为 `*`,则将栈顶元素 top 取出,计算 top * num,并将计算结果压入栈中。 - 如果 op 为 `/`,则将栈顶元素 top 取出,计算 int(top / num),并将计算结果压入栈中。 - 如果遇到 `+`、`-`、`*`、`/` 操作符,则更新 op。 - 最后将栈中整数进行累加,并返回结果。 ## 代码 ```python class Solution: def calculate(self, s: str) -> int: size = len(s) stack = [] op = '+' index = 0 while index < size: if s[index] == ' ': index += 1 continue if s[index].isdigit(): num = ord(s[index]) - ord('0') while index + 1 < size and s[index + 1].isdigit(): index += 1 num = 10 * num + ord(s[index]) - ord('0') if op == '+': stack.append(num) elif op == '-': stack.append(-num) elif op == '*': top = stack.pop() stack.append(top * num) elif op == '/': top = stack.pop() stack.append(int(top / num)) elif s[index] in "+-*/": op = s[index] index += 1 return sum(stack) ``` ================================================ FILE: docs/solutions/interviews/color-fill-lcci.md ================================================ # [面试题 08.10. 颜色填充](https://leetcode.cn/problems/color-fill-lcci/) - 标签:深度优先搜索、广度优先搜索、数组、矩阵 - 难度:简单 ## 题目链接 - [面试题 08.10. 颜色填充 - 力扣](https://leetcode.cn/problems/color-fill-lcci/) ## 题目大意 给定一个二维整数矩阵 `image`,其中 `image[i][j]` 表示矩阵第 `i` 行、第 `j` 列上网格块的颜色值。再给定一个起始位置 `(sr, sc)`,以及一个目标颜色 `newColor`。 要求:对起始位置 `(sr, sc)` 所在位置周围区域填充颜色为 `newColor`。并返回填充后的图像 `image`。 - 周围区域:颜色相同且在上、下、左、右四个方向上存在相连情况的若干元素。 ## 解题思路 深度优先搜索。使用二维数组 `visited` 标记访问过的节点。遍历上、下、左、右四个方向上的点。如果下一个点位置越界,或者当前位置与下一个点位置颜色不一样,则对该节点进行染色。 在遍历的过程中注意使用 `visited` 标记访问过的节点,以免重复遍历。 ## 代码 ```python class Solution: directs = [(0, 1), (0, -1), (1, 0), (-1, 0)] def dfs(self, image, i, j, origin_color, color, visited): rows, cols = len(image), len(image[0]) for direct in self.directs: new_i = i + direct[0] new_j = j + direct[1] # 下一个位置越界,则当前点在边界,对其进行着色 if new_i < 0 or new_i >= rows or new_j < 0 or new_j >= cols: image[i][j] = color continue # 如果访问过,则跳过 if visited[new_i][new_j]: continue # 如果下一个位置颜色与当前颜色相同,则继续搜索 if image[new_i][new_j] == origin_color: visited[new_i][new_j] = True self.dfs(image, new_i, new_j, origin_color, color, visited) # 下一个位置颜色与当前颜色不同,则当前位置为连通区域边界,对其进行着色 else: image[i][j] = color def floodFill(self, image: List[List[int]], sr: int, sc: int, newColor: int) -> List[List[int]]: if not image: return image rows, cols = len(image), len(image[0]) visited = [[False for _ in range(cols)] for _ in range(rows)] visited[sr][sc] = True self.dfs(image, sr, sc, image[sr][sc], newColor, visited) return image ``` ================================================ FILE: docs/solutions/interviews/eight-queens-lcci.md ================================================ # [面试题 08.12. 八皇后](https://leetcode.cn/problems/eight-queens-lcci/) - 标签:数组、回溯 - 难度:困难 ## 题目链接 - [面试题 08.12. 八皇后 - 力扣](https://leetcode.cn/problems/eight-queens-lcci/) ## 题目大意 - n 皇后问题:将 n 个皇后放置在 `n * n` 的棋盘上,并且使得皇后彼此之间不能攻击。 - 皇后彼此不能相互攻击:指的是任何两个皇后都不能处于同一条横线、纵线或者斜线上。 现在给定一个整数 `n`,返回所有不同的「n 皇后问题」的解决方案。每一种解法包含一个不同的「n 皇后问题」的棋子放置方案,该方案中的 `Q` 和 `.` 分别代表了皇后和空位。 ## 解题思路 经典的回溯问题。使用 `chessboard` 来表示棋盘,`Q` 代表皇后,`.` 代表空位,初始都为 `.`。然后使用 `res` 存放最终答案。 先定义棋盘合理情况判断方法,判断同一条横线、纵线或者斜线上是否存在两个以上的皇后。 再定义回溯方法,从第一行开始进行遍历。 - 如果当前行 `row` 等于 `n`,则当前棋盘为一个可行方案,将其拼接加入到 `res` 数组中。 - 遍历 `[0, n]` 列元素,先验证棋盘是否可行,如果可行: - 将当前行当前列尝试换为 `Q`。 - 然后继续递归下一行。 - 再将当前行回退为 `.`。 - 最终返回 `res` 数组。 ## 代码 ```python class Solution: res = [] def backtrack(self, n: int, row: int, chessboard: List[List[str]]): if row == n: temp_res = [] for temp in chessboard: temp_str = ''.join(temp) temp_res.append(temp_str) self.res.append(temp_res) return for col in range(n): if self.isValid(n, row, col, chessboard): chessboard[row][col] = 'Q' self.backtrack(n, row + 1, chessboard) chessboard[row][col] = '.' def isValid(self, n: int, row: int, col: int, chessboard: List[List[str]]): for i in range(row): if chessboard[i][col] == 'Q': return False i, j = row - 1, col - 1 while i >= 0 and j >= 0: if chessboard[i][j] == 'Q': return False i -= 1 j -= 1 i, j = row - 1, col + 1 while i >= 0 and j < n: if chessboard[i][j] == 'Q': return False i -= 1 j += 1 return True def solveNQueens(self, n: int) -> List[List[str]]: self.res.clear() chessboard = [['.' for _ in range(n)] for _ in range(n)] self.backtrack(n, 0, chessboard) return self.res ``` ================================================ FILE: docs/solutions/interviews/factorial-zeros-lcci.md ================================================ # [面试题 16.05. 阶乘尾数](https://leetcode.cn/problems/factorial-zeros-lcci/) - 标签:数学 - 难度:简单 ## 题目链接 - [面试题 16.05. 阶乘尾数 - 力扣](https://leetcode.cn/problems/factorial-zeros-lcci/) ## 题目大意 给定一个整数 `n`。 要求:计算 `n` 的阶乘中尾随零的数量。 注意:$0 <= n <= 10^4$。 ## 解题思路 阶乘中,末尾 `0` 的来源只有 `2 * 5`。所以尾随 `0` 的个数为 `2` 的倍数个数和 `5` 的倍数个数的最小值。又因为 `2 < 5`,`2` 的倍数个数肯定小于等于 `5` 的倍数,所以直接统计 `5` 的倍数个数即可。 ## 代码 ```python class Solution: def trailingZeroes(self, n: int) -> int: count = 0 while n > 0: count += n // 5 n = n // 5 return count ``` ================================================ FILE: docs/solutions/interviews/first-common-ancestor-lcci.md ================================================ # [面试题 04.08. 首个共同祖先](https://leetcode.cn/problems/first-common-ancestor-lcci/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [面试题 04.08. 首个共同祖先 - 力扣](https://leetcode.cn/problems/first-common-ancestor-lcci/) ## 题目大意 给定一个二叉树,要求找到该树中指定节点 `p`、`q` 的最近公共祖先: - 祖先:如果节点 `p` 在节点 `node` 的左子树或右子树中,或者 `p = node`,则称 `node` 是 `p` 的祖先。 - 最近公共祖先:对于树的两个节点 `p`、`q`,最近公共祖先表示为一个节点 `lca_node`,满足 `lca_node` 是 `p`、`q` 的祖先且 `lca_node` 的深度尽可能大(一个节点也可以是自己的祖先)。 ## 解题思路 设 `lca_node` 为节点 `p`、`q` 的最近公共祖先。则 `lca_node` 只能是下面几种情况: - `p`、`q` 在 `lca_node` 的子树中,且分别在 `lca_node` 的两侧子树中。 - `p == lca_node`,且 `q` 在 `lca_node` 的左子树或右子树中。 - `q == lca_node`,且 `p` 在 `lca_node` 的左子树或右子树中。 下面递归求解 `lca_node`。递归需要满足以下条件: - 如果 `p`、`q` 都不为空,则返回 `p`、`q` 的公共祖先。 - 如果 `p`、`q` 只有一个存在,则返回存在的一个。 - 如果 `p`、`q` 都不存在,则返回存在的一个。 具体思路为: - 如果当前节点 `node` 为 `None`,则说明 `p`、`q` 不在 `node` 的子树中,不可能为公共祖先,直接返回 `None`。 - 如果当前节点 `node` 等于 `p` 或者 `q`,那么 `node` 就是 `p`、`q` 的最近公共祖先,直接返回 `node`。 - 递归遍历左子树、右子树,并判断左右子树结果。 - 如果左子树为空,则返回右子树。 - 如果右子树为空,则返回左子树。 - 如果左右子树都不为空,则说明 `p`、`q` 在当前根节点的两侧,当前根节点就是他们的最近公共祖先。 - 如果左右子树都为空,则返回空。 ## 代码 ```python class Solution: def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode: if root == p or root == q: return root if root: node_left = self.lowestCommonAncestor(root.left, p, q) node_right = self.lowestCommonAncestor(root.right, p, q) if node_left and node_right: return root elif not node_left: return node_right else: return node_left return None ``` ================================================ FILE: docs/solutions/interviews/group-anagrams-lcci.md ================================================ # [面试题 10.02. 变位词组](https://leetcode.cn/problems/group-anagrams-lcci/) - 标签:数组、哈希表、字符串、排序 - 难度:中等 ## 题目链接 - [面试题 10.02. 变位词组 - 力扣](https://leetcode.cn/problems/group-anagrams-lcci/) ## 题目大意 给定一个字符串数组 `strs`。 要求:将所有变位词组合在一起。不需要考虑输出顺序。 - 变位词:字母相同,但排列不同的字符串。 ## 解题思路 使用哈希表记录变位词。对每一个字符串进行排序,按照 `排序字符串:变位词数组` 的键值顺序进行存储。 最终将哈希表的值转换为对应数组返回结果。 ## 代码 ```python class Solution: def groupAnagrams(self, strs: List[str]) -> List[List[str]]: str_dict = dict() res = [] for s in strs: sort_s = str(sorted(s)) if sort_s in str_dict: str_dict[sort_s] += [s] else: str_dict[sort_s] = [s] for sort_s in str_dict: res += [str_dict[sort_s]] return res ``` ================================================ FILE: docs/solutions/interviews/implement-queue-using-stacks-lcci.md ================================================ # [面试题 03.04. 化栈为队](https://leetcode.cn/problems/implement-queue-using-stacks-lcci/) - 标签:栈、设计、队列 - 难度:简单 ## 题目链接 - [面试题 03.04. 化栈为队 - 力扣](https://leetcode.cn/problems/implement-queue-using-stacks-lcci/) ## 题目大意 要求:实现一个 MyQueue 类,要求仅使用两个栈实现先入先出队列。 ## 解题思路 使用两个栈,`inStack` 用于输入,`outStack` 用于输出。 - `push` 操作:将元素压入 `inStack` 中。 - `pop` 操作:如果 `outStack` 输出栈为空,将 `inStack` 输入栈元素依次取出,按顺序压入 `outStack` 栈。这样 `outStack` 栈的元素顺序和之前 `inStack` 元素顺序相反,`outStack` 顶层元素就是要取出的队头元素,将其移出,并返回该元素。如果 `outStack` 输出栈不为空,则直接取出顶层元素。 - `peek` 操作:和 `pop` 操作类似,只不过最后一步不需要取出顶层元素,直接将其返回即可。 - `empty` 操作:如果 `inStack` 和 `outStack` 都为空,则队列为空,否则队列不为空。 ## 代码 ```python class MyQueue: def __init__(self): """ Initialize your data structure here. """ self.inStack = [] self.outStack = [] def push(self, x: int) -> None: """ Push element x to the back of queue. """ self.inStack.append(x) def pop(self) -> int: """ Removes the element from in front of queue and returns that element. """ if (len(self.outStack) == 0): while (len(self.inStack) != 0): self.outStack.append(self.inStack[-1]) self.inStack.pop() top = self.outStack[-1] self.outStack.pop() return top def peek(self) -> int: """ Get the front element. """ if (len(self.outStack) == 0): while (len(self.inStack) != 0): self.outStack.append(self.inStack[-1]) self.inStack.pop() top = self.outStack[-1] return top def empty(self) -> bool: """ Returns whether the queue is empty. """ return len(self.outStack) == 0 and len(self.inStack) == 0 ``` ================================================ FILE: docs/solutions/interviews/index.md ================================================ ## 本章内容 - [面试题 01.07. 旋转矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/rotate-matrix-lcci.md) - [面试题 01.08. 零矩阵](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/zero-matrix-lcci.md) - [面试题 02.02. 返回倒数第 k 个节点](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/kth-node-from-end-of-list-lcci.md) - [面试题 02.05. 链表求和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/sum-lists-lcci.md) - [面试题 02.06. 回文链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/palindrome-linked-list-lcci.md) - [面试题 02.07. 链表相交](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/intersection-of-two-linked-lists-lcci.md) - [面试题 02.08. 环路检测](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/linked-list-cycle-lcci.md) - [面试题 03.02. 栈的最小值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/min-stack-lcci.md) - [面试题 03.04. 化栈为队](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/implement-queue-using-stacks-lcci.md) - [面试题 04.02. 最小高度树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/minimum-height-tree-lcci.md) - [面试题 04.05. 合法二叉搜索树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/legal-binary-search-tree-lcci.md) - [面试题 04.06. 后继者](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/successor-lcci.md) - [面试题 04.08. 首个共同祖先](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/first-common-ancestor-lcci.md) - [面试题 04.12. 求和路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/paths-with-sum-lcci.md) - [面试题 08.04. 幂集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/power-set-lcci.md) - [面试题 08.07. 无重复字符串的排列组合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/permutation-i-lcci.md) - [面试题 08.08. 有重复字符串的排列组合](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/permutation-ii-lcci.md) - [面试题 08.09. 括号](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/bracket-lcci.md) - [面试题 08.10. 颜色填充](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/color-fill-lcci.md) - [面试题 08.12. 八皇后](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/eight-queens-lcci.md) - [面试题 10.01. 合并排序的数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/sorted-merge-lcci.md) - [面试题 10.02. 变位词组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/group-anagrams-lcci.md) - [面试题 10.09. 排序矩阵查找](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/sorted-matrix-search-lcci.md) - [面试题 16.02. 单词频率](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/words-frequency-lcci.md) - [面试题 16.05. 阶乘尾数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/factorial-zeros-lcci.md) - [面试题 16.26. 计算器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/calculator-lcci.md) - [面试题 17.06. 2出现的次数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/number-of-2s-in-range-lcci.md) - [面试题 17.14. 最小K个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/smallest-k-lcci.md) - [面试题 17.15. 最长单词](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/longest-word-lcci.md) - [面试题 17.17. 多次搜索](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/interviews/multi-search-lcci.md) ================================================ FILE: docs/solutions/interviews/intersection-of-two-linked-lists-lcci.md ================================================ # [面试题 02.07. 链表相交](https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/) - 标签:哈希表、链表、双指针 - 难度:简单 ## 题目链接 - [面试题 02.07. 链表相交 - 力扣](https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/) ## 题目大意 给定两个链表的头节点 `headA`、`headB`。 要求:找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 `None` 。 比如:链表 A 为 `[4, 1, 8, 4, 5]`,链表 B 为 `[5, 0, 1, 8, 4, 5]`。则如下图所示,两个链表相交的起始节点为 `8`,则输出结果为 `8`。 ![](https://assets.leetcode.com/uploads/2018/12/13/160_example_1.png) ## 解题思路 如果两个链表相交,那么从相交位置开始,到结束,必有一段等长且相同的节点。假设链表 `A` 的长度为 `m`、链表 `B` 的长度为 `n`,他们的相交序列有 `k` 个,则相交情况可以如下如所示: ![](https://qcdn.itcharge.cn/images/20210401113538.png) 现在问题是如何找到 `m - k` 或者 `n - k` 的位置。 考虑将链表 `A` 的末尾拼接上链表 `B`,链表 `B` 的末尾拼接上链表 `A`。 然后使用两个指针 `pA` 、`pB`,分别从链表 `A`、链表 `B` 的头节点开始遍历,如果走到共同的节点,则返回该节点。 否则走到两个链表末尾,返回 `None`。 ![](https://qcdn.itcharge.cn/images/20210401114100.png) ## 代码 ```python class Solution: def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: if headA == None or headB == None: return None pA = headA pB = headB while pA != pB : pA = pA.next if pA != None else headB pB = pB.next if pB != None else headA return pA ``` ================================================ FILE: docs/solutions/interviews/kth-node-from-end-of-list-lcci.md ================================================ # [面试题 02.02. 返回倒数第 k 个节点](https://leetcode.cn/problems/kth-node-from-end-of-list-lcci/) - 标签:链表、双指针 - 难度:简单 ## 题目链接 - [面试题 02.02. 返回倒数第 k 个节点 - 力扣](https://leetcode.cn/problems/kth-node-from-end-of-list-lcci/) ## 题目大意 给定一个链表的头节点 `head`,以及一个整数 `k`。 要求:返回链表的倒数第 `k` 个节点的值。 ## 解题思路 常规思路是遍历一遍链表,求出链表长度,再遍历一遍到对应位置,返回该位置上的节点。 如果用一次遍历实现的话,可以使用快慢指针。让快指针先走 `k` 步,然后快慢指针、慢指针再同时走,每次一步,这样等快指针遍历到链表尾部的时候,慢指针就刚好遍历到了倒数第 `k` 个节点位置。返回该该位置上的节点即可。 ## 代码 ```python class Solution: def kthToLast(self, head: ListNode, k: int) -> int: slow = head fast = head for _ in range(k): if fast == None: return fast fast = fast.next while fast: slow = slow.next fast = fast.next return slow.val ``` ================================================ FILE: docs/solutions/interviews/legal-binary-search-tree-lcci.md ================================================ # [面试题 04.05. 合法二叉搜索树](https://leetcode.cn/problems/legal-binary-search-tree-lcci/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [面试题 04.05. 合法二叉搜索树 - 力扣](https://leetcode.cn/problems/legal-binary-search-tree-lcci/) ## 题目大意 给定一个二叉树的根节点 `root`。 要求:检查该二叉树是否为二叉搜索树。 二叉搜索树特征: - 节点的左子树只包含小于当前节点的数。 - 节点的右子树只包含大于当前节点的数。 - 所有左子树和右子树自身必须也是二叉搜索树。 ## 解题思路 根据题意进行递归遍历即可。前序、中序、后序遍历都可以。 以前序遍历为例,递归函数为:`preorderTraversal(root, min_v, max_v)` 前序遍历时,先判断根节点的值是否在 `(min_v, max_v)` 之间。如果不在则直接返回 `False`。在区间内,则继续递归检测左右子树是否满足,都满足才是一棵二叉搜索树。 递归遍历左子树的时候,要将上界 `max_v` 改为左子树的根节点值,因为左子树上所有节点的值均小于根节点的值。同理,遍历右子树的时候,要将下界 `min_v` 改为右子树的根节点值,因为右子树上所有节点的值均大于根节点。 ## 代码 ```python class Solution: def isValidBST(self, root: TreeNode) -> bool: def preorderTraversal(root, min_v, max_v): if root == None: return True if root.val >= max_v or root.val <= min_v: return False return preorderTraversal(root.left, min_v, root.val) and preorderTraversal(root.right, root.val, max_v) return preorderTraversal(root, float('-inf'), float('inf')) ``` ================================================ FILE: docs/solutions/interviews/linked-list-cycle-lcci.md ================================================ # [面试题 02.08. 环路检测](https://leetcode.cn/problems/linked-list-cycle-lcci/) - 标签:哈希表、链表、双指针 - 难度:中等 ## 题目链接 - [面试题 02.08. 环路检测 - 力扣](https://leetcode.cn/problems/linked-list-cycle-lcci/) ## 题目大意 给定一个链表的头节点 `head`。 要求:判断链表中是否有环,如果有环则返回入环的第一个节点,无环则返回 None。 ## 解题思路 利用两个指针,一个慢指针每次前进一步,快指针每次前进两步(两步或多步效果是等价的)。如果两个指针在链表头节点以外的某一节点相遇(即相等)了,那么说明链表有环,否则,如果(快指针)到达了某个没有后继指针的节点时,那么说明没环。 如果有环,则再定义一个指针,和慢指针一起每次移动一步,两个指针相遇的位置即为入口节点。 这是因为:假设入环位置为 A,快慢指针在在 B 点相遇,则相遇时慢指针走了 $a + b$ 步,快指针走了 $a + n(b+c) + b$ 步。 $2(a + b) = a + n(b + c) + b$。可以推出:$a = c + (n-1)(b + c)$。 我们可以发现:从相遇点到入环点的距离 $c$ 加上 $n-1$ 圈的环长 $b + c$ 刚好等于从链表头部到入环点的距离。 ## 代码 ```python class Solution: def detectCycle(self, head: ListNode) -> ListNode: fast, slow = head, head while True: if not fast or not fast.next: return None fast = fast.next.next slow = slow.next if fast == slow: break ans = head while ans != slow: ans, slow = ans.next, slow.next return ans ``` ================================================ FILE: docs/solutions/interviews/longest-word-lcci.md ================================================ # [面试题 17.15. 最长单词](https://leetcode.cn/problems/longest-word-lcci/) - 标签:字典树、数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [面试题 17.15. 最长单词 - 力扣](https://leetcode.cn/problems/longest-word-lcci/) ## 题目大意 给定一组单词 `words`。 要求:找出其中的最长单词,且该单词由这组单词中的其他单词组合而成。如果有多个长度相同的结果,返回其中字典序最小的一项,如果没有符合要求的单词则返回空字符串。 ## 解题思路 先将所有单词按照长度从长到短排序,相同长度的字典序小的排在前面。然后将所有单词存入字典树中。 然后一重循环遍历所有单词 `word`,二重循环遍历单词中所有字符 `word[i]`。 如果当前遍历的字符为单词末尾,递归判断从 `i + 1` 位置开始,剩余部分是否可以切分为其他单词组合,如果可以切分,则返回当前单词 `word`。如果不可以切分,则返回空字符串 `""`。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return False cur = cur.children[ch] return cur is not None and cur.isEnd def splitToWord(self, remain): if not remain or remain == "": return True cur = self for i in range(len(remain)): ch = remain[i] if ch not in cur.children: return False if cur.children[ch].isEnd and self.splitToWord(remain[i + 1:]): return True cur = cur.children[ch] return False def dfs(self, words): for word in words: cur = self size = len(word) for i in range(size): ch = word[i] if i < size - 1 and cur.children[ch].isEnd and self.splitToWord(word[i+1:]): return word cur = cur.children[ch] return "" class Solution: def longestWord(self, words: List[str]) -> str: words.sort(key=lambda x: (-len(x), x)) trie_tree = Trie() for word in words: trie_tree.insert(word) ans = trie_tree.dfs(words) return ans ``` ================================================ FILE: docs/solutions/interviews/min-stack-lcci.md ================================================ # [面试题 03.02. 栈的最小值](https://leetcode.cn/problems/min-stack-lcci/) - 标签:栈、设计 - 难度:简单 ## 题目链接 - [面试题 03.02. 栈的最小值 - 力扣](https://leetcode.cn/problems/min-stack-lcci/) ## 题目大意 设计一个「栈」,要求实现 `push` ,`pop` ,`top` ,`getMin` 操作,其中 `getMin` 要求能在常数时间内实现。 ## 解题思路 使用一个栈,栈元素中除了保存当前值之外,再保存一个当前最小值。 - `push` 操作:如果栈不为空,则判断当前值与栈顶元素所保存的最小值,并更新当前最小值,将新元素保存到栈中。 - `pop`操作:正常出栈 - `top` 操作:返回栈顶元素保存的值。 - `getMin` 操作:返回栈顶元素保存的最小值。 ## 代码 ```python class MinStack: def __init__(self): """ initialize your data structure here. """ self.stack = [] class Node: def __init__(self, x): self.val = x self.min = x def push(self, x: int) -> None: node = self.Node(x) if len(self.stack) == 0: self.stack.append(node) else: topNode = self.stack[-1] if node.min > topNode.min: node.min = topNode.min self.stack.append(node) def pop(self) -> None: self.stack.pop() def top(self) -> int: return self.stack[-1].val def getMin(self) -> int: return self.stack[-1].min ``` ================================================ FILE: docs/solutions/interviews/minimum-height-tree-lcci.md ================================================ # [面试题 04.02. 最小高度树](https://leetcode.cn/problems/minimum-height-tree-lcci/) - 标签:树、二叉搜索树、数组、分治、二叉树 - 难度:简单 ## 题目链接 - [面试题 04.02. 最小高度树 - 力扣](https://leetcode.cn/problems/minimum-height-tree-lcci/) ## 题目大意 给定一个升序的有序数组 `nums`。 要求:创建一棵高度最小的二叉搜索树(高度平衡的二叉搜索树)。 ## 解题思路 直观上,如果把数组的中间元素当做根,那么数组左侧元素都小于根节点,右侧元素都大于根节点,且左右两侧元素个数相同,或最多相差 `1` 个。那么构建的树高度差也不会超过 `1`。所以猜想出:如果左右子树约平均,树就越平衡。这样我们就可以每次取中间元素作为当前的根节点,两侧的元素作为左右子树递归建树,左侧区间 `[L, mid - 1]` 作为左子树,右侧区间 `[mid + 1, R]` 作为右子树。 ## 代码 ```python class Solution: def sortedArrayToBST(self, nums: List[int]) -> TreeNode: size = len(nums) if size == 0: return None mid = size // 2 root = TreeNode(nums[mid]) root.left = Solution.sortedArrayToBST(self, nums[:mid]) root.right = Solution.sortedArrayToBST(self, nums[mid + 1:]) return root ``` ================================================ FILE: docs/solutions/interviews/multi-search-lcci.md ================================================ # [面试题 17.17. 多次搜索](https://leetcode.cn/problems/multi-search-lcci/) - 标签:字典树、数组、哈希表、字符串、字符串匹配、滑动窗口 - 难度:中等 ## 题目链接 - [面试题 17.17. 多次搜索 - 力扣](https://leetcode.cn/problems/multi-search-lcci/) ## 题目大意 给定一个较长字符串 `big` 和一个包含较短字符串的数组 `smalls`。 要求:设计一个方法,根据 `smalls` 中的每一个较短字符串,对 `big` 进行搜索。输出 `smalls` 中的字符串在 `big` 里出现的所有位置 `positions`,其中 `positions[i]` 为 `smalls[i]` 出现的所有位置。 ## 解题思路 构建字典树,将 `smalls` 中所有字符串存入字典树中,并在字典树中记录下插入字符串的顺序下标。 然后一重循环遍历 `big`,表示从第 `i` 位置开始的字符串 `big[i:]`。然后在字符串前缀中搜索对应的单词,将所有符合要求的单词插入顺序位置存入列表中,返回列表。 对于列表中每个单词插入下标顺序 `index` 和 `big[i:]` 来说, `i` 就是 `smalls` 中第 `index` 个字符串所对应在 `big` 中的开始位置,将其存入答案数组并返回即可。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False self.index = -1 def insert(self, word: str, index: int) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True cur.index = index def search(self, text: str) -> list: """ Returns if the word is in the trie. """ cur = self res = [] for i in range(len(text)): ch = text[i] if ch not in cur.children: return res cur = cur.children[ch] if cur.isEnd: res.append(cur.index) return res class Solution: def multiSearch(self, big: str, smalls: List[str]) -> List[List[int]]: trie_tree = Trie() for i in range(len(smalls)): word = smalls[i] trie_tree.insert(word, i) res = [[] for _ in range(len(smalls))] for i in range(len(big)): for index in trie_tree.search(big[i:]): res[index].append(i) return res ``` ================================================ FILE: docs/solutions/interviews/number-of-2s-in-range-lcci.md ================================================ # [面试题 17.06. 2出现的次数](https://leetcode.cn/problems/number-of-2s-in-range-lcci/) - 标签:递归、数学、动态规划 - 难度:困难 ## 题目链接 - [面试题 17.06. 2出现的次数 - 力扣](https://leetcode.cn/problems/number-of-2s-in-range-lcci/) ## 题目大意 **描述**:给定一个整数 $n$。 **要求**:计算从 $0$ 到 $n$ (包含 $n$) 中数字 $2$ 出现的次数。 **说明**: - $n \le 10^9$。 **示例**: - 示例 1: ```python 输入: 25 输出: 9 解释: (2, 12, 20, 21, 22, 23, 24, 25)(注意 22 应该算作两次) ``` ## 解题思路 ### 思路 1:动态规划 + 数位 DP 将 $n$ 转换为字符串 $s$,定义递归函数 `def dfs(pos, cnt, isLimit):` 表示构造第 $pos$ 位及之后所有数位中数字 $2$ 出现的个数。接下来按照如下步骤进行递归。 1. 从 `dfs(0, 0, True)` 开始递归。 `dfs(0, 0, True)` 表示: 1. 从位置 $0$ 开始构造。 2. 初始数字 $2$ 出现的个数为 $0$。 3. 开始时受到数字 $n$ 对应最高位数位的约束。 2. 如果遇到 $pos == len(s)$,表示到达数位末尾,此时:返回数字 $2$ 出现的个数 $cnt$。 3. 如果 $pos \ne len(s)$,则定义方案数 $ans$,令其等于 $0$,即:`ans = 0`。 4. 如果遇到 $isNum == False$,说明之前位数没有填写数字,当前位可以跳过,这种情况下方案数等于 $pos + 1$ 位置上没有受到 $pos$ 位的约束,并且之前没有填写数字时的方案数,即:`ans = dfs(i + 1, state, False, False)`。 5. 如果 $isNum == True$,则当前位必须填写一个数字。此时: 1. 因为不需要考虑前导 $0$ 所以当前位数位所能选择的最小数字($minX$)为 $0$。 2. 根据 $isLimit$ 来决定填当前位数位所能选择的最大数字($maxX$)。 3. 然后根据 $[minX, maxX]$ 来枚举能够填入的数字 $d$。 4. 方案数累加上当前位选择 $d$ 之后的方案数,即:`ans += dfs(pos + 1, cnt + (d == 2), isLimit and d == maxX)`。 1. `cnt + (d == 2)` 表示之前数字 $2$ 出现的个数加上当前位为数字 $2$ 的个数。 2. `isLimit and d == maxX` 表示 $pos + 1$ 位受到之前位 $pos$ 位限制。 6. 最后的方案数为 `dfs(0, 0, True)`,将其返回即可。 ### 思路 1:代码 ```python class Solution: def numberOf2sInRange(self, n: int) -> int: # 将 n 转换为字符串 s s = str(n) @cache # pos: 第 pos 个数位 # cnt: 之前数字 2 出现的个数。 # isLimit: 表示是否受到选择限制。如果为真,则第 pos 位填入数字最多为 s[pos];如果为假,则最大可为 9。 def dfs(pos, cnt, isLimit): if pos == len(s): return cnt ans = 0 # 不需要考虑前导 0,则最小可选择数字为 0 minX = 0 # 如果受到选择限制,则最大可选择数字为 s[pos],否则最大可选择数字为 9。 maxX = int(s[pos]) if isLimit else 9 # 枚举可选择的数字 for d in range(minX, maxX + 1): ans += dfs(pos + 1, cnt + (d == 2), isLimit and d == maxX) return ans return dfs(0, 0, True) ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(\log n)$。 - **空间复杂度**:$O(\log n)$。 ================================================ FILE: docs/solutions/interviews/palindrome-linked-list-lcci.md ================================================ # [面试题 02.06. 回文链表](https://leetcode.cn/problems/palindrome-linked-list-lcci/) - 标签:栈、递归、链表、双指针 - 难度:简单 ## 题目链接 - [面试题 02.06. 回文链表 - 力扣](https://leetcode.cn/problems/palindrome-linked-list-lcci/) ## 题目大意 给定一个链表的头节点 `head`。 要求:判断该链表是否为回文链表。 ## 解题思路 利用数组,将链表元素依次存入。然后再使用两个指针,一个指向数组开始位置,一个指向数组结束位置,依次判断首尾对应元素是否相等,如果都相等,则为回文链表。如果不相等,则不是回文链表。 ## 代码 ```python class Solution: def isPalindrome(self, head: ListNode) -> bool: nodes = [] p1 = head while p1 != None: nodes.append(p1.val) p1 = p1.next return nodes == nodes[::-1] ``` ================================================ FILE: docs/solutions/interviews/paths-with-sum-lcci.md ================================================ # [面试题 04.12. 求和路径](https://leetcode.cn/problems/paths-with-sum-lcci/) - 标签:树、深度优先搜索、二叉树 - 难度:中等 ## 题目链接 - [面试题 04.12. 求和路径 - 力扣](https://leetcode.cn/problems/paths-with-sum-lcci/) ## 题目大意 给定一个二叉树的根节点 `root`,和一个整数 `targetSum`。 要求:求出该二叉树里节点值之和等于 `targetSum` 的路径的数目。 - 路径:不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。 ## 解题思路 直观想法是: 以每一个节点 `node` 为起始节点,向下检测延伸的路径。递归遍历每一个节点所有可能的路径,然后将这些路径数目加起来即为答案。 但是这样会存在许多重复计算。我们可以定义节点的前缀和来减少重复计算。 - 节点的前缀和:从根节点到当前节点路径上所有节点的和。 有了节点的前缀和,我们就可以通过前缀和来计算两节点之间的路劲和。即:`则两节点之间的路径和 = 两节点之间的前缀和之差`。 为了计算符合要求的路径数量,我们用哈希表存储「前缀和的节点数量」。哈希表以「当前节点的前缀和」为键,以「该前缀和的节点数量」为值。这样就能通过哈希表直接计算出符合要求的路径数量,从而累加到答案上。 整个算法的具体步骤如下: - 通过先序遍历方式递归遍历二叉树,计算每一个节点的前缀和 `cur_sum`。 - 从哈希表中取出 `cur_sum - target_sum` 的路径数量(也就是表示存在从前缀和为 `cur_sum - target_sum` 所对应的节点到前缀和为 `cur_sum` 所对应的节点的路径个数)累加到答案 `res` 中。 - 然后以「当前节点的前缀和」为键,以「该前缀和的节点数量」为值,存入哈希表中。 - 递归遍历二叉树,并累加答案值。 - 恢复哈希表「当前前缀和的节点数量」,返回答案。 ## 代码 ```python ``` ================================================ FILE: docs/solutions/interviews/permutation-i-lcci.md ================================================ # [面试题 08.07. 无重复字符串的排列组合](https://leetcode.cn/problems/permutation-i-lcci/) - 标签:字符串、回溯 - 难度:中等 ## 题目链接 - [面试题 08.07. 无重复字符串的排列组合 - 力扣](https://leetcode.cn/problems/permutation-i-lcci/) ## 题目大意 给定一个字符串 `S`。 要求:打印出该字符串中字符的所有排列。可以以任意顺序返回这个字符串数组,但里边不能有重复元素。 ## 解题思路 使用 `visited` 数组标记该元素在当前排列中是否被访问过。如果未被访问过则将其加入排列中,并在访问后将该元素变为未访问状态。然后进行回溯遍历。 ## 代码 ```python class Solution: res = [] path = [] def backtrack(self, S, visited): if len(self.path) == len(S): self.res.append(''.join(self.path)) return for i in range(len(S)): if not visited[i]: visited[i] = True self.path.append(S[i]) self.backtrack(S, visited) self.path.pop() visited[i] = False def permutation(self, S: str) -> List[str]: self.res.clear() self.path.clear() visited = [False for _ in range(len(S))] self.backtrack(S, visited) return self.res ``` ================================================ FILE: docs/solutions/interviews/permutation-ii-lcci.md ================================================ # [面试题 08.08. 有重复字符串的排列组合](https://leetcode.cn/problems/permutation-ii-lcci/) - 标签:字符串、回溯 - 难度:中等 ## 题目链接 - [面试题 08.08. 有重复字符串的排列组合 - 力扣](https://leetcode.cn/problems/permutation-ii-lcci/) ## 题目大意 给定一个字符串 `s`,字符串中包含有重复字符。 要求:打印出该字符串中字符的所有排列。可以以任意顺序返回这个字符串数组。 ## 解题思路 因为原字符串可能含有重复元素,所以在回溯的时候需要进行去重。先将字符串 `s` 转为 `list` 列表,再对列表进行排序,然后使用 `visited` 数组标记该元素在当前排列中是否被访问过。如果未被访问过则将其加入排列中,并在访问后将该元素变为未访问状态。 然后再递归遍历下一层元素之前,增加一句语句进行判重:`if i > 0 and nums[i] == nums[i - 1] and not visited[i - 1]: continue`。 然后进行回溯遍历。 ## 代码 ```python class Solution: res = [] path = [] def backtrack(self, ls, visited): if len(self.path) == len(ls): self.res.append(''.join(self.path)) return for i in range(len(ls)): if i > 0 and ls[i] == ls[i - 1] and not visited[i - 1]: continue if not visited[i]: visited[i] = True self.path.append(ls[i]) self.backtrack(ls, visited) self.path.pop() visited[i] = False def permutation(self, S: str) -> List[str]: self.res.clear() self.path.clear() ls = list(S) ls.sort() visited = [False for _ in range(len(S))] self.backtrack(ls, visited) return self.res ``` ================================================ FILE: docs/solutions/interviews/power-set-lcci.md ================================================ # [面试题 08.04. 幂集](https://leetcode.cn/problems/power-set-lcci/) - 标签:位运算、数组、回溯 - 难度:中等 ## 题目链接 - [面试题 08.04. 幂集 - 力扣](https://leetcode.cn/problems/power-set-lcci/) ## 题目大意 给定一个集合 `nums`,集合中不包含重复元素。 压枪欧秋:返回该集合的所有子集。 ## 解题思路 回溯算法,遍历集合 `nums`。为了使得子集不重复,每次遍历从当前位置的下一个位置进行下一层遍历。 ## 代码 ```python class Solution: def subsets(self, nums: List[int]) -> List[List[int]]: def backtrack(size, subset, index): res.append(subset) for i in range(index, size): backtrack(size, subset + [nums[i]], i + 1) size = len(nums) res = list() backtrack(size, [], 0) return res ``` ================================================ FILE: docs/solutions/interviews/rotate-matrix-lcci.md ================================================ # [面试题 01.07. 旋转矩阵](https://leetcode.cn/problems/rotate-matrix-lcci/) - 标签:数组、数学、矩阵 - 难度:中等 ## 题目链接 - [面试题 01.07. 旋转矩阵 - 力扣](https://leetcode.cn/problems/rotate-matrix-lcci/) ## 题目大意 给定一个 `n * n` 大小的二维矩阵用来表示图像,其中每个像素的大小为 4 字节。 要求:设计一种算法,将图像旋转 90 度。并且要不占用额外内存空间。 ## 解题思路 题目要求不占用额外内存空间,就是要在原二维矩阵上直接进行旋转操作。我们可以用翻转操作代替旋转操作。具体可以分为两步: 1. 上下翻转。 2. 主对角线翻转。 举个例子: ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ``` 上下翻转后变为: ``` 13 14 15 16 9 10 11 12 5 6 7 8 1 2 3 4 ``` 在经过主对角线翻转后变为: ``` 13 9 5 1 14 10 6 2 15 11 7 3 16 12 8 4 ``` ## 代码 ```python class Solution: def rotate(self, matrix: List[List[int]]) -> None: """ Do not return anything, modify matrix in-place instead. """ size = len(matrix) for i in range(size // 2): for j in range(size): matrix[i][j], matrix[size - i - 1][j] = matrix[size - i - 1][j], matrix[i][j] for i in range(size): for j in range(i): matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] ``` ================================================ FILE: docs/solutions/interviews/smallest-k-lcci.md ================================================ # [面试题 17.14. 最小K个数](https://leetcode.cn/problems/smallest-k-lcci/) - 标签:数组、分治、快速选择、排序、堆(优先队列) - 难度:中等 ## 题目链接 - [面试题 17.14. 最小K个数 - 力扣](https://leetcode.cn/problems/smallest-k-lcci/) ## 题目大意 给定整数数组 `arr`,再给定一个整数 `k`。 要求:返回数组 `arr` 中最小的 `k` 个数。 ## 解题思路 直接可以想到的思路是:排序后输出数组上对应的最小的 k 个数。所以问题关键在于排序方法的复杂度。 冒泡排序、选择排序、插入排序时间复杂度 $O(n^2)$ 太高了,解答会超时。 可考虑堆排序、归并排序、快速排序。本题使用堆排序。具体做法如下: 1. 利用数组前 `k` 个元素,建立大小为 `k` 的大顶堆。 2. 遍历数组 `[k, size - 1]` 的元素,判断其与堆顶元素关系,如果比堆顶元素小,则将其赋值给堆顶元素,再对大顶堆进行调整。 3. 最后输出前调整过后的大顶堆的前 `k` 个元素。 ## 代码 ```python class Solution: def heapify(self, nums: [int], index: int, end: int): left = index * 2 + 1 right = left + 1 while left <= end: # 当前节点为非叶子节点 max_index = index if nums[left] > nums[max_index]: max_index = left if right <= end and nums[right] > nums[max_index]: max_index = right if index == max_index: # 如果不用交换,则说明已经交换结束 break nums[index], nums[max_index] = nums[max_index], nums[index] # 继续调整子树 index = max_index left = index * 2 + 1 right = left + 1 # 初始化大顶堆 def buildMaxHeap(self, nums: [int], k: int): # (k-2) // 2 是最后一个非叶节点,叶节点不用调整 for i in range((k - 2) // 2, -1, -1): self.heapify(nums, i, k - 1) return nums def smallestK(self, arr: List[int], k: int) -> List[int]: size = len(arr) if k <= 0 or not arr: return [] if size <= k: return arr self.buildMaxHeap(arr, k) for i in range(k, size): if arr[i] < arr[0]: arr[i], arr[0] = arr[0], arr[i] self.heapify(arr, 0, k - 1) return arr[:k] ``` ================================================ FILE: docs/solutions/interviews/sorted-matrix-search-lcci.md ================================================ # [面试题 10.09. 排序矩阵查找](https://leetcode.cn/problems/sorted-matrix-search-lcci/) - 标签:数组、二分查找、分治、矩阵 - 难度:中等 ## 题目链接 - [面试题 10.09. 排序矩阵查找 - 力扣](https://leetcode.cn/problems/sorted-matrix-search-lcci/) ## 题目大意 给定一个 `m * n` 大小的有序整数矩阵。每一行、每一列都按升序排列。再给定一个目标值 `target`。 要求:判断矩阵中是否可以找到 `target`,如果找到 `target`,返回 `True`,否则返回 `False`。 ## 解题思路 矩阵是有序的,可以考虑使用二分搜索来进行查找。 迭代对角线元素,假设对角线元素的坐标为 `(row, col)`。把数组元素按对角线分为右上角部分和左下角部分。 则对于当前对角线元素右侧第 `row` 行、对角线元素下侧第 `col` 列进行二分查找。 - 如果找到目标,直接返回 `True`。 - 如果找不到目标,则缩小范围,继续查找。 - 直到所有对角线元素都遍历完,依旧没找到,则返回 `False`。 ## 代码 ```python class Solution: def diagonalBinarySearch(self, matrix, diagonal, target): left = 0 right = diagonal while left < right: mid = left + (right - left) // 2 if matrix[mid][mid] < target: left = mid + 1 else: right = mid return left def rowBinarySearch(self, matrix, begin, cols, target): left = begin right = cols while left < right: mid = left + (right - left) // 2 if matrix[begin][mid] < target: left = mid + 1 elif matrix[begin][mid] > target: right = mid - 1 else: left = mid break return begin <= left <= cols and matrix[begin][left] == target def colBinarySearch(self, matrix, begin, rows, target): left = begin + 1 right = rows while left < right: mid = left + (right - left) // 2 if matrix[mid][begin] < target: left = mid + 1 elif matrix[mid][begin] > target: right = mid - 1 else: left = mid break return begin <= left <= rows and matrix[left][begin] == target def searchMatrix(self, matrix: List[List[int]], target: int) -> bool: rows = len(matrix) if rows == 0: return False cols = len(matrix[0]) if cols == 0: return False min_val = min(rows, cols) index = self.diagonalBinarySearch(matrix, min_val - 1, target) if matrix[index][index] == target: return True for i in range(index + 1): row_search = self.rowBinarySearch(matrix, i, cols - 1, target) col_search = self.colBinarySearch(matrix, i, rows - 1, target) if row_search or col_search: return True return False ``` ================================================ FILE: docs/solutions/interviews/sorted-merge-lcci.md ================================================ # [面试题 10.01. 合并排序的数组](https://leetcode.cn/problems/sorted-merge-lcci/) - 标签:数组、双指针、排序 - 难度:简单 ## 题目链接 - [面试题 10.01. 合并排序的数组 - 力扣](https://leetcode.cn/problems/sorted-merge-lcci/) ## 题目大意 **描述**:给定两个排序后的数组 `A` 和 `B`,以及 `A` 的元素数量 `m` 和 `B` 的元素数量 `n`。 `A` 的末端有足够的缓冲空间容纳 `B`。 **要求**:编写一个方法,将 `B` 合并入 `A` 并排序。 **说明**: - $A.length == n + m$。 **示例**: - 示例 1: ```python 输入: A = [1,2,3,0,0,0], m = 3 B = [2,5,6], n = 3 输出: [1,2,2,3,5,6] ``` ## 解题思路 ### 思路 1:归并排序 可以利用归并排序算法的归并步骤思路。 1. 使用两个指针分别表示`A`、`B` 正在处理的元素下标。 2. 对 `A`、`B` 进行归并操作,将结果存入新数组中。归并之后,再将所有元素赋值到数组 `A` 中。 ### 思路 1:代码 ```python class Solution: def merge(self, A: List[int], m: int, B: List[int], n: int) -> None: """ Do not return anything, modify A in-place instead. """ arr = [] index_A, index_B = 0, 0 while index_A < m and index_B < n: if A[index_A] <= B[index_B]: arr.append(A[index_A]) index_A += 1 else: arr.append(B[index_B]) index_B += 1 while index_A < m: arr.append(A[index_A]) index_A += 1 while index_B < n: arr.append(B[index_B]) index_B += 1 for i in range(m + n): A[i] = arr[i] ``` ### 思路 1:复杂度分析 - **时间复杂度**:$O(m + n)$。 - **空间复杂度**:$O(m + n)$。 ================================================ FILE: docs/solutions/interviews/successor-lcci.md ================================================ # [面试题 04.06. 后继者](https://leetcode.cn/problems/successor-lcci/) - 标签:树、深度优先搜索、二叉搜索树、二叉树 - 难度:中等 ## 题目链接 - [面试题 04.06. 后继者 - 力扣](https://leetcode.cn/problems/successor-lcci/) ## 题目大意 给定一棵二叉搜索树的根节点 `root` 和其中一个节点 `p`。 要求:找出该节点在树中的中序后继,即按照中序遍历的顺序节点 `p` 的下一个节点。如果节点 `p` 没有对应的下一个节点,则返回 `None`。 ## 解题思路 递归遍历,具体步骤如下: - 如果 `root.val` 小于等于 `p.val`,则直接从 `root` 的右子树递归查找比 `p.val` 大的节点,从而找到中序后继。 - 如果 `root.val` 大于 `p.val`,则 `root` 有可能是中序后继,也有可能是 `root` 的左子树。则从 `root` 的左子树递归查找更接近(更小的)。如果查找的值为 `None`,则当前 `root` 就是中序后继,否则继续递归查找,从而找到中序后继。 ## 代码 ```python class Solution: def inorderSuccessor(self, root: TreeNode, p: TreeNode) -> TreeNode: if not p or not root: return None if root.val <= p.val: node = self.inorderSuccessor(root.right, p) else: node = self.inorderSuccessor(root.left, p) if not node: node = root return node ``` ================================================ FILE: docs/solutions/interviews/sum-lists-lcci.md ================================================ # [面试题 02.05. 链表求和](https://leetcode.cn/problems/sum-lists-lcci/) - 标签:递归、链表、数学 - 难度:中等 ## 题目链接 - [面试题 02.05. 链表求和 - 力扣](https://leetcode.cn/problems/sum-lists-lcci/) ## 题目大意 给定两个非空的链表 `l1` 和 `l2`,表示两个非负整数,每位数字都是按照逆序的方式存储的,每个节点存储一位数字。 要求:计算两个整数的和,并逆序返回表示和的链表。 ## 解题思路 模拟大数加法,按位相加,将结果添加到新链表上。需要注意进位和对 `10` 取余。 ## 代码 ```python class Solution: def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode: head = curr = ListNode(0) carry = 0 while l1 or l2 or carry: if l1: num1 = l1.val l1 = l1.next else: num1 = 0 if l2: num2 = l2.val l2 = l2.next else: num2 = 0 sum = num1 + num2 + carry carry = sum // 10 curr.next = ListNode(sum % 10) curr = curr.next return head.next ``` ================================================ FILE: docs/solutions/interviews/words-frequency-lcci.md ================================================ # [面试题 16.02. 单词频率](https://leetcode.cn/problems/words-frequency-lcci/) - 标签:设计、字典树、数组、哈希表、字符串 - 难度:中等 ## 题目链接 - [面试题 16.02. 单词频率 - 力扣](https://leetcode.cn/problems/words-frequency-lcci/) ## 题目大意 要求:设计一个方法,找出任意指定单词在一本书中的出现频率。 支持如下操作: - `WordsFrequency(book)` 构造函数,参数为字符串数组构成的一本书。 - `get(word)` 查询指定单词在书中出现的频率。 ## 解题思路 使用字典树统计单词频率。 构造函数时,构建一个字典树,并将所有单词存入字典树中,同时在字典树中记录并维护单词频率。 查询时,调用字典树查询方法,查询单词频率。 ## 代码 ```python class Trie: def __init__(self): """ Initialize your data structure here. """ self.children = dict() self.isEnd = False self.count = 0 def insert(self, word: str) -> None: """ Inserts a word into the trie. """ cur = self for ch in word: if ch not in cur.children: cur.children[ch] = Trie() cur = cur.children[ch] cur.isEnd = True cur.count += 1 def search(self, word: str) -> bool: """ Returns if the word is in the trie. """ cur = self for ch in word: if ch not in cur.children: return 0 cur = cur.children[ch] if cur and cur.isEnd: return cur.count return 0 class WordsFrequency: def __init__(self, book: List[str]): self.tire_tree = Trie() for word in book: self.tire_tree.insert(word) def get(self, word: str) -> int: return self.tire_tree.search(word) ``` ================================================ FILE: docs/solutions/interviews/zero-matrix-lcci.md ================================================ # [面试题 01.08. 零矩阵](https://leetcode.cn/problems/zero-matrix-lcci/) - 标签:数组、哈希表、矩阵 - 难度:中等 ## 题目链接 - [面试题 01.08. 零矩阵 - 力扣](https://leetcode.cn/problems/zero-matrix-lcci/) ## 题目大意 给定一个 `m * n` 大小的二维矩阵 `matrix`。 要求:编写一种算法,如果矩阵中某个元素为 `0`,增将其所在行与列清零。 ## 解题思路 直观上可以使用两个数组或者集合来标记行和列出现 `0` 的情况,但更好的做法是不用开辟新的数组或集合,直接原本二维矩阵 `matrix` 的空间。使用数组原本的元素进行记录出现 0 的情况。 设定两个变量 `flag_row0`、`flag_col0` 来标记第一行、第一列是否出现了 `0`。 接下来我们使用数组第一行、第一列来标记 `0` 的情况。 对数组除第一行、第一列之外的每个元素进行遍历,如果某个元素出现 `0` 了,则使用数组的第一行、第一列对应位置来存储 `0` 的标记。 再对数组除第一行、第一列之外的每个元素进行遍历,通过对第一行、第一列的标记 0 情况,进行置为 `0` 的操作。 最后再根据 `flag_row0`、`flag_col0` 的标记情况,对第一行、第一列进行置为 `0` 的操作。 ## 代码 ```python class Solution: def setZeroes(self, matrix: List[List[int]]) -> None: """ Do not return anything, modify matrix in-place instead. """ rows = len(matrix) cols = len(matrix[0]) flag_col0 = False flag_row0 = False for i in range(rows): if matrix[i][0] == 0: flag_col0 = True break for j in range(cols): if matrix[0][j] == 0: flag_row0 = True break for i in range(1, rows): for j in range(1, cols): if matrix[i][j] == 0: matrix[i][0] = matrix[0][j] = 0 for i in range(1, rows): for j in range(1, cols): if matrix[i][0] == 0 or matrix[0][j] == 0: matrix[i][j] = 0 if flag_col0: for i in range(rows): matrix[i][0] = 0 if flag_row0: for j in range(cols): matrix[0][j] = 0 ```