算法学习笔记

学习来源:Leetcode,CSDN等

1 排序算法

内部排序算法(直接在内存中排序)有8种常用算法:

img

性能比较

平均情况 最好情况 最坏情况 空间复杂度 稳定性 一趟排序能确定元素的位置与否
直接插入排序 $O(N^2)$ $O(N)$ $O(N^2)$ $O(1)$ 稳定 不能
希尔排序 $O(N^{1.3})$ $O(N)$ $O(N^2)$ $O(1)$ 不稳定 不能
简单选择排序 $O(N^2)$ $O(N^2)$ $O(N^2)$ $O(1)$ 不稳定
堆排序 $O(N\log{N})$ $O(N\log{N})$ $O(N\log{N})$ $O(1)$ 不稳定
冒泡排序 $O(N^2)$ $O(N)$ $O(N^2)$ $O(1)$ 稳定
快速排序 $O(N\log{N})$ $O(N\log{N})$ $O(N^2)$ $O(\log{N})$ 不稳定
归并排序 $O(N\log{N})$ $O(N\log{N})$ $O(N\log{N})$ $O(N)$ 稳定

稳定性

稳定排序:排序前后两个相等的数相对位置不变,则算法稳定

非稳定排序:排序前后两个相等的数相对位置发生了变化,则算法不稳定

复杂度比较

  • 常数阶$O(1)$
  • 对数阶$O(\log{N})$
  • 线性阶$O(N)$
  • 线性对数阶$O(N\log{N})$
  • 平方阶$O(N^2)$
  • 立方阶$O(N^3)$
  • K方阶$O(N^k)$
  • 指数阶$O(2^N)$

从上到下效率越来越低。

1.1 交换类排序

1.1.1 冒泡排序

① 分析

算法步骤:

  1. 将序列当中的左右元素,依次比较,保证右边的元素始终大于左边的元素;( 第一轮结束后,序列最后一个元素一定是当前序列的最大值;)
  2. 对序列当中剩下的n-1个元素再次执行步骤1。
  3. 对于长度为n的序列,一共需要执行n-1轮比较(如果在一轮中没有发生比较,可以直接结束循环)

最好的情况: 如果待排序数据序列为正序,则一趟冒泡就可完成排序(没有发生排序),排序码的比较次数为n-1次,且没有移动,时间复杂度为O(n)。

最坏的情况: 如果待排序数据序列为逆序,则冒泡排序需要n-1次趟起泡,每趟进行n-i次排序码的比较和移动,即比较和移动次数均达到最大值:

排序情况分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
3, 44, 38, 5, 15

总共5个数,最坏情况下需要进行4轮排序

第一轮:对n(5)个数进行排序
3, 38, 44, 5, 15
3, 38, 5, 44, 15
3, 38, 5, 15, 44

第二轮:对n-1(4)个数进行排序
3, 5, 38, 15, 44
3, 5, 15, 38, 44 (排序完成,3,4轮可以不用进行,通过flag控制)

第三轮:对n-2(3)个数进行排序
3, 5, 15, 38, 44

第四轮:对n-4(2)个数进行排序
3, 5, 15, 38, 44

结束
② 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class BubbleSortTest {
public static void main(String[] args) {
int[] arr = {12,3,33,11,32,9,8,5};
System.out.println("排序前:" + Arrays.toString(arr));

// 控制轮数
for (int i = 0; i < arr.length - 1; i++) {
// 是否发生交换
boolean isSwap = false;
// 对arr.length-i个数进行排序
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j+1]) {
// 进行交换
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
isSwap = true;
}
}
System.out.println("第" + i + "轮:" + Arrays.toString(arr));
if (!isSwap) {
// 如果没有发生交换,则说明已经是有序的了,不用再进行额外的排序
break;
}
}
}
}

1.1.2 快速排序

① 分析

思想:分治法

算法步骤:

  1. 从待排序的n个记录中任意选取一个记录(通常选取第一个记录)为分区标准;
  2. 把所有小于该排序列的记录移动到左边,把所有大于该排序码的记录移动到右边,中间放所选记录,称之为第一趟排序;
  3. 然后对前后两个子序列分别重复上述过程,直到所有记录都排好序。

最好的情况: 是每趟排序结束后,每次划分使两个子文件的长度大致相等。

最坏的情况: 是待排序记录已经排好序。

排序情况分析:

1
66,13,51,76,81,26,57,69,23 pivot=66

将两个指针ij分别指向序列的起始和最后的位置。

反复操作以下两步:

  1. j逐渐减小,并逐次比较j指向的元素和目标元素(pivot)的大小,若arr(j)<pivot则交换位置。
  2. i逐渐增大,并逐次比较i指向的元素和目标元素的大小,若arr(i)>pivot则交换位置。
  3. 直到ij指向同一个值,这一轮就结束。

第一轮结束后为:

1
23 13 51 66 81 26 57 69 76

然后以66为界,对两个子序列进行排序:

1
2
81 26 57 69 76 pivot=81
23 13 51 pivot=23
② 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// a:待排序数组,low:最低位的下标,high:最高位的下标
void quickSort(int arr[],int low, int high) {
if(low >= high) {
return;
}
int left = low;
int right = high;
/*用数组的第一个记录作为分区元素*/
int pivot = arr[left];
while(left != right) {
/*从右向左扫描,找第一个码值小于key的记录,并交换到key*/
while(left < right && arr[right] >= pivot)
--right;
arr[left] = arr[right];
/*从左向右扫描,找第一个码值大于key的记录,并交换到右边*/
while(left < right && arr[left] <= pivot)
++left;
arr[right] = arr[left];
}
/*分区元素放到正确位置*/
arr[left] = pivot;
// 递归
quickSort(arr, low, left - 1);
quickSort(arr, left + 1, high);
}

1.2 插入类排序

1.2.1 直接插入排序

① 分析

image-20230722200924617

image-20230722200941985

算法步骤

从待排序的n个记录中的第二个记录(第一个记录看成有序)开始,依次与前面的记录比较并寻找插入的位置,每次外循环结束后,将当前的数插入到合适的位置。

  1. 第一层循环:遍历待比较的所有数组元素
  2. 第二层循环:将本轮选择的元素(selected)与已经排好序的元素(ordered)相比较。如果:selected > ordered,那么将二者交换

最好情况:当待排序记录已经有序。

最坏情况:如果待排序记录为逆序。

② 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String args[]) {
int[] arr = new int[]{17,62,39,52,8,24};
for(int i = 1; i < arr.length; i++) { // 从第二个元素开始比较
int temp = arr[i]; // 记录当前元素
for(int j = i - 1; j >= 0; j--) { // 从有序区的最后一个元素开始比较
if(arr[j] > temp) { // 如果比当前元素大
arr[j + 1] = arr[j]; // 从该处往后所有元素向后移动一位
arr[j] = temp; // 将当前元素插入到arr[j]中
}
}
}
}
}

1.2.2 希尔排序

① 分析

希尔排序是将数据分组,将每一组进行插入排序。每一组排成有序后,最后整体就变有序了。

算法步骤:

  1. 将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;
  2. 每次将gap折半减小,循环上述操作;
  3. 当gap=1时,利用直接插入,完成排序。

gap的选取:

一般选取待排序数组长度的三分之一或二分之一

图示

image-20230802154056142

image-20230802154113822

image-20230802154126235

此时分为了1组,再排序一次(此时就是直接插入排序)后变成下面情况:

image-20230802154139151

② 实现

三层循环:最外层控制gap,里面两层和直接插入排序差别不大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void shellSort(int[] array) {
int gap = array.length; //为数组的长度的1/2
while (gap > 1) {
gap /= 2; // 每次gap除以2
shell(array, gap); //调用直接插入排序方法
}
}

//实现直接插入排序方法
public static void shell(int[] array, int gap) {
// i下标从第一组的第二个数据开始(无序区的第一个元素)
for (int i = gap; i < array.length ; i++) {
int tmp = array[i]; // tmp存放i下标的值
int j = i - gap; // j下标为i-gap个位置(有序区的最后一个元素)
// j每次-gap个位置
for (; j >= 0; j -= gap) {
if (array[j] > tmp) {
// j下标的值大,将j下标的值放到j变量加上一个gap的位置上
array[j + gap] = array[j];
}else {
// j下标的值较小,j下标的值要直接放到j变量加上一个gap的位置上
break;
}
}
// 此时j下标的值是负数了,将tmp的值放到j变量加上一个gap的位置上
array[j + gap] = tmp;
}
}

1.3 选择类排序

1.3.1 简单选择排序

① 分析

算法步骤

  1. 首先在未排序序列中找到最小元素,存放到排序序列的起始位置。
  2. 再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

img

② 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SelectionSort implements IArraySort {

@Override
public int[] sort(int[] sourceArray) throws Exception {
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
// 总共要经过 N-1 轮比较
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
// 每轮需要比较的次数 N-i
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
// 记录目前能找到的最小值元素的下标
min = j;
}
}
// 将找到的最小值和i位置所在的值进行交换
if (i != min) {
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
}
return arr;
}
}

1.3.2 堆排序

① 分析

堆是具有以下性质的完全二叉树

  • 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆
  • 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆

例如:

image-20230727195248100

映射到数组为:

image-20230727195312964


节点编号的关系:(节点编号从0开始算)

  • 节点 i 的「左子节点」为2i+1,「右子节点」为2i+2,其父节点是 (i-1)/2 (0节点除外)
  • 对于一棵有n个节点的完全二叉树,最后一个非叶子节点的编号为(n-2)/2或者n/2 - 1

此时大根堆和小根堆,对于每个节点i满足以下关系:

  • 大根堆:nums[i] >= nums[2i+1] && nums[i] >= nums[2i+2]
  • 小根堆:nums[i] <= nums[2i+1] && nums[i] <= nums[2i+2]

算法步骤:

  1. 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点,位于数组的第一个位置。
  2. 将最大元素与末尾元素进行交换,此时末尾就为最大值。
  3. 然后将剩余n-1个元素重新构造成一个大顶堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

细节:

  • 从最后一个「非叶子节点」为根节点的子树出发,从右往左、从下往上进行调整操作。
  • 对于以某个非叶子节点的子树而言,其基本的调整操作包括:
    • 如果该节点大于等于其左右两个子节点,则无需再对该节点进行调整,因为它的子树已经是堆有序的;
    • 如果该节点小于其左右两个子节点中的较大者,则将该节点与子节点中的较大者进行交换,并从刚刚较大者的位置出发继续进行调整操作,直至堆有序。

图示

给定待排序数组:nums=[5,2,1,9,6,8],最后一个非叶子节点编号为6/2-1=2

第一步:

image-20230802163215576

第二步:

image-20230802163304407

第三步:

image-20230802163321830

第四步:

image-20230802163408012

至此,建立了一个大根堆。

注意到,在每一次建好大根堆后,堆顶元素即为当前范围内的最大值,因此要得到数组中的第 K 个最大元素需要:建堆(大根堆) K 次,此时堆顶元素即为第 K 个最大元素。

② 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class HeapSort {
@Test
public void test() {
int[] arr = {5, 2, 1, 9, 6, 8};
heapSort(arr);
}

public void heapSort(int[] arr) {
// 从最后一个非叶子节点进行调整
int startIndex = (arr.length - 2) / 2;
// 每次减去1,代表从右到左,从下到上调整,i=0即为根节点
for (int i = startIndex; i >= 0; i--) {
toMaxHeap(arr, arr.length, i);
}
//已经变成大根堆,交换堆顶和最后一个元素,循环
for (int i = arr.length - 1; i > 0; i--) {
swap(arr, i, 0);
// size=i,一直在减小
// 从根节点向下调整,所以为0
toMaxHeap(arr, i, 0);
}
System.out.println(Arrays.toString(arr));
}

//构造大根堆
public void toMaxHeap(int[] arr, int size, int index) {
int leftChildIndex = 2 * index + 1;
int rightChildIndex = 2 * index + 2;
int maxIndex = index;
//找到最大的元素
if (leftChildIndex < size && arr[leftChildIndex] > arr[maxIndex]) {
maxIndex = leftChildIndex;
}
if (rightChildIndex < size && arr[rightChildIndex] > arr[maxIndex]) {
maxIndex = rightChildIndex;
}
//如果是子节点比父节点大,则进行交换
if (maxIndex != index) {
swap(arr, index, maxIndex);
//重新调整大根堆顺序
toMaxHeap(arr, size, maxIndex);
}
}

public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

1.4 归并排序

1.4.1 分析

归并排序属于比较类非线性时间排序,号称比较类排序中性能最佳者,在数据中应用中较广。归并排序是java语言底层的Arrays.sort()的实现。

归并排序是分治法(Divide and Conquer)的一个典型的应用。

若将两个有序集合并成一个有序表,称为2-路归并,与之对应的还有多路归并。

img

算法步骤:先分解,再合并

1.4.2 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class MergeSort {

public static void mergeSort(int[] a, int lo, int hi) {
int[] temp = new int[a.length];
mergeSort(a, temp, lo, hi);
}

public static void mergeSort(int[] a) {
mergeSort(a, 0, a.length - 1);
}

private static void mergeSort(int[] a, int[] temp, int lo, int hi) {
int mid = (lo + hi) / 2;
if (hi - lo > 1) { // 子数组长度大于1时,进行分解
mergeSort(a, temp, lo, mid);
mergeSort(a, temp, mid + 1, hi);
}
// 合并
merge(a, temp, lo, mid, hi);
}

private static void merge(int[] a, int[] temp, int lo, int mid, int hi) {
int arr1Begin = lo;
int arr1End = mid;

int arr2Begin = mid + 1;
int arr2End = hi;
int k = lo;

while (arr1Begin <= arr1End || arr2Begin <= arr2End) {
if (arr1Begin > arr1End) { // 说明1号数组中已经无值
temp[k++] = a[arr2Begin++];
} else if (arr2Begin > arr2End) { // 说明二号数组中已经无值
temp[k++] = a[arr1Begin++];
} else {// 两个数组都没有结束,比大小添加。
if (a[arr1Begin] < a[arr2Begin]) { // 比大小添加
temp[k++] = a[arr1Begin++];
} else {
temp[k++] = a[arr2Begin++];
}
}
}
for (int i = lo; i <= hi; i++) {
a[i] = temp[i];
}
}

public static void main(String[] args) {
int a[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
mergeSort(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
}

1.5 基数排序

基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。

具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

例如:

image-20230802175523705

1.6 例题

题1:将5个不同的数据进行排序,至多需要比较()次比较。

计算方法:$C_{n}^{2}$,n为待排序个数

组合数公式:
$$
C_{n}^{m}=\frac{n!}{m!(n-m)!}
$$
解答:考虑采用最暴力的做法,每两个数之间都比较一次,一共需要比较$C_{5}^{2}$次(10次)。


题2:现有N条词以及对应的拼音串,对其排序,排序规则:首先按拼音串的字母序排序,如果拼音串相同,则按当前词所在的顺序排序,下列哪些排序算法符合条件?( )

A.插入排序

B.快速排序

C.堆排序

D.冒泡排序

解答:AD,本质分为两次排序,后一次排序不能破坏前一次排序的结果,因此只能采用稳定排序。


题3:如果开发一款打扑克的游戏,现在要对选手拿到的牌进行排序,请问一下哪种排序方式最合适?

A.冒泡排序

B.希尔排序

C.选择排序

D.插入排序

解答:D,牌是一张一张发的,每发一张就要插入到手牌中,因此选用插入排序最合适。


题4:有序列(8,5,2,12,7,1,6,10,9,3,4,11),新序列(4,5,2,3,1,6,7,10,9,12,8,11)是下列( )排序算法一趟扫描结果。

A.堆排序

B.快速排序

C.希尔排序

D.冒泡排序

解答:B,注意pivot不一定是第一个元素,这里的pivot选择了7,左小右大。


题5:将一个从大到小的数组,用以下排序方法排序成从小到大的,()最快。

A.插入排序

B.冒泡排序

C.快速排序

D.堆排序

解答:D,逆序排列(最坏情况),ABC均为平方阶,堆排序最坏情况为线性对数阶。


题6:

2 回溯法

思想

回溯法,又叫试探法,是一种寻找最优解的暴力搜寻法。主要思想是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

基本思想:

  • 针对具体问题,定义问题的解空间
  • 确定易于搜索的解空间结构(数据结构的选择)。
  • 一般以DFS(深度优先搜索)的方式搜索解空间。
  • 在搜索过程中,可以使用剪枝函数等来优化算法。

站在回溯的某个节点上,我们需要思考三个问题:

  1. **路径:**就是我们当前做出的选择
  2. 选择列表:就是我们当前可以做出的选择
  3. 结束条件:也就是到达决策树底部,无法再做出选择

通用代码框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
result=[]; 

def backtrack(路径,选择列表)
if 满足结束条件:
result.add(路径);
return;

for 选择 in 选择列表

做选择 // 关键

backtrack(路径,选择列表) // 继续搜索

撤销选择 // 回溯

Leetcode例题

1 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

1
2
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

1
2
输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

1
2
输入:nums = [1]
输出:[[1]]

多叉树的生成过程:以[1, 2, 3]为例

image.png

对于这道题而言,当树的深度等于数组大小时,就结束搜索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/*
* @lc app=leetcode.cn id=46 lang=cpp
*
* [46] 全排列
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
int len = nums.size(); // 数组长度
vector<vector<int>> res; // 最终结果
if(len == 0) {
return res;
}
int depth = 0; // 递归深度
vector<bool> used(false, 0); // 是否使用过
vector<int> path; // 中间结果
dfs(nums, len, depth, used, path, res);
return res;
}

void dfs(vector<int>& nums, int len, int depth,
vector<int>& used, vector<int>& path, vector<vector<int>>& res) {
// 递归终止条件:递归深度等于数组长度,说明数字已经使用完
if(depth == len) {
res.push_back(path);
return;
}
// 没有终止(没有使用完数字)
// 遍历查看哪一个数字还没有使用过
for(int i = 0; i < len; i++) {
if(!used[i]) { // 没有使用过
used[i] = true;
path.push_back(nums[i]);
// 继续搜索
dfs(nums, len, depth + 1, used, path, res);
// 搜索完毕后进行回溯(原路返回)
used[i] = false;
path.pop_back();
}
}
}
};
// @lc code=end

总结:

做题的时候,建议 先画树形图 ,画图能帮助我们想清楚递归结构,想清楚如何剪枝。拿题目中的示例,想一想人是怎么做的,一般这样下来,这棵递归树都不难画出。

在画图的过程中思考清楚:

  • 分支如何产生;
  • 题目需要的解在哪里?是在叶子结点、还是在非叶子结点、还是在从跟结点到叶子结点的路径?
  • 哪些搜索会产生不需要的解的?例如:产生重复是什么原因,如果在浅层就知道这个分支不能产生需要的结果,应该提前剪枝,剪枝的条件是什么,代码怎么写?

2 全排列②

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

示例 1:

1
2
3
4
5
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]

示例 2:

1
2
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

这道题和上道题不同的地方在于,需要对遍历生成的树进行剪枝,即去掉不符合题意的叶子结点。

如果要比较两个列表是否一样,一个容易想到的办法是对列表分别排序,然后逐个比对。既然要排序,我们就可以在搜索之前就对候选数组排序,一旦发现某个分支搜索下去可能搜索到重复的元素就停止搜索,这样结果集中不会包含重复列表。

重点想象深度优先遍历在这棵树上执行的过程,哪些地方遍历下去一定会产生重复,这些地方的状态的特点是什么?

对比图中标注 ① 和 ② 的地方。相同点是:这一次搜索的起点和上一次搜索的起点一样。不同点是:

  • 标注 ① 的地方上一次搜索的相同的数刚刚被撤销;
  • 标注 ② 的地方上一次搜索的相同的数刚刚被使用。

image.png

如何判断当前分支是否是重复的分支: 因为之前已经排好序了,所以当前选择的元素nums[i]如果和前一个选择的元素相同 ,即nums[i] == nums[i-1]就说明该分支有可能是重复的。 但是这个相等条件有两种可能,一种是,1 1‘ 2,也就是选择完1之后再选择第二个1,两个元素虽然重复,但是第二个元素是前一个元素的下一层,这时是没有问题的。 另一种是之前的同层分支已经有 1 1‘ 2了,这次的选择是 1‘ 1 2 。两个元素重复,且重的是同层路径。那就说明是重复分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
* @lc app=leetcode.cn id=47 lang=cpp
*
* [47] 全排列 II
*/

// @lc code=start
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
// 排序
sort(nums.begin(), nums.end());
int len = nums.size(); // 数组长度
vector<vector<int>> res; // 最终结果
if(len == 0) {
return res;
}
vector<bool> used(len, false); // 是否使用过
vector<int> path; // 中间结果
dfs(nums, len, 0, used, path, res);
return res;
}
void dfs(vector<int>& nums, int len, int depth,
vector<bool>& used, vector<int>& path, vector<vector<int>>& res) {
// 递归终止条件:递归深度等于数组长度,说明数字已经使用完
if(depth == len) {
res.push_back(path);
return;
}
// 没有终止(没有使用完数字)
// 遍历查看哪一个数字还没有使用过
for(int i = 0; i < len; i++) {
if(used[i]) {
continue;
}
// 当前元素和上个元素相同(已经排好序),并且上一个元素未被使用,则是重复分支
// 原因:上一个元素肯定先于当前元素被选中,因此已经存在于当前分支的左侧的同层。
if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) {
continue;
}
used[i] = true;
path.push_back(nums[i]);
// 继续搜索
dfs(nums, len, depth + 1, used, path, res);
// 搜索完毕后进行回溯(原路返回)
used[i] = false;
path.pop_back();
}
}
};
// @lc code=end

3 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

1
2
3
4
5
6
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

示例 2:

1
2
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

1
2
输入: candidates = [2], target = 1
输出: []

首先画出一棵树来分析,以candidates = [2, 3, 6, 7]target = 7 为例:

image-20230319173432585

可以看出递归返回的条件为target=0,剪枝条件是target<0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
* @lc app=leetcode.cn id=39 lang=cpp
*
* [39] 组合总和
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
int len = candidates.size();
vector<vector<int>> res;
vector<int> path;
// 因为元素可以反复的取可能不止一次,因此不需要设置used
dfs(candidates, path, 0, res, len, target);
return res;
}

void dfs(vector<int>& candidates, vector<int>& path, int index, vector<vector<int>>& res, int len, int target) {
int sum = 0;
for(int& x : path) {
sum+=x;
}
if(sum == target) { // 递归返回
res.push_back(path);
return;
}
if(sum > target) { // 递归返回
return;
}
for(int i = index; i < len; i++) {
path.push_back(candidates[i]);
// 由于每一个元素可以重复使用,下一轮搜索的起点依然是 i
dfs(candidates, path, i, res, len, target);
path.pop_back();
}
}
};
// @lc code=end

4 组合总数②

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次

注意:解集不能包含重复的组合。

示例 1:

1
2
3
4
5
6
7
8
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

1
2
3
4
5
6
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

这道题和1.2.2类似,都是数组中的元素有重复的,因此需要排序,利用1.2.2的方法解答。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*
* @lc app=leetcode.cn id=40 lang=cpp
*
* [40] 组合总和 II
*/

// @lc code=start
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
int len = candidates.size();
vector<vector<int>> res;
vector<int> path;
sort(candidates.begin(), candidates.end());
dfs(candidates, path, 0, res, len, target);
return res;
}
void dfs(vector<int>& candidates, vector<int>& path, int index, vector<vector<int>>& res, int len, int target) {
int sum = 0;
for(int& x : path) {
sum+=x;
}
if(sum == target) { // 结束条件1
res.push_back(path);
return;
}
if(sum > target) { // 结束条件2
return;
}
for(int i = index; i < len; i++) {
// 当前选择的元素和上一个的元素相同,则是重复分支
// 原因:上一个元素被选时肯定先于当前元素的选择,因此分支位于当前分支的左侧的同层
if (i > index && candidates[i] == candidates[i - 1]) {
continue;
}
path.push_back(candidates[i]);
// 从i+1开始取
dfs(candidates, path, i+1, res, len, target);
path.pop_back(); // 回溯
}
}
};
// @lc code=end

5 组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

1
2
3
4
5
6
7
8
9
10
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

示例 2:

1
2
输入:n = 1, k = 1
输出:[[1]]
  • 1 <= n <= 20
  • 1 <= k <= n

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
* @lc app=leetcode.cn id=77 lang=cpp
*
* [77] 组合
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> res;
vector<int> path;
// 构造待选数组
vector<int> nums(n, 0);
int count = 1;
for(vector<int>::iterator iter = nums.begin(); iter != nums.end(); iter++) {
*iter += count;
count++;
}
dfs(nums, nums.size(), 0, k, path, res);
return res;
}

void dfs(vector<int>& nums, int len, int begin, int k, vector<int>& path, vector<vector<int>>& res) {
if(path.size() == k) {
res.push_back(path);
return;
}
for(int i = begin; i < len; i++) {
path.push_back(nums[i]);
// 从i+1开始选择
dfs(nums, nums.size(), i+1, k, path, res);
path.pop_back();
}
}
};
// @lc code=end

6 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

1
2
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

1
2
输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

这道题的特殊之处在于,每个结点都是符合条件的解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* @lc app=leetcode.cn id=78 lang=cpp
*
* [78] 子集
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
int len = nums.size();
vector<vector<int>> res;
vector<int> temp;
dfs(nums, 0, len, temp, res);
// 必包含空集
res.push_back({});
return res;
}

void dfs(vector<int>& nums, int begin, int len,
vector<int>& temp, vector<vector<int>>& res) {
if(begin == len) { // 结束条件:当起始位置为数组长度时
return;
}
for(int i = begin; i < len; i++) {
temp.push_back(nums[i]);
// 每个结点都是符合题意的解
res.push_back(temp);
// 从i+1开始取元素,不取前面出现过的元素
dfs(nums, i+1, len, temp, res);
temp.pop_back(); // 回溯
}
}
};
// @lc code=end

7 子集②

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

1
2
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

1
2
输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10

和上一题的不同在于,包含重复元素,思想同1.2.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/*
* @lc app=leetcode.cn id=90 lang=cpp
*
* [90] 子集 II
*/

// @lc code=start
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
// 排序
sort(nums.begin(), nums.end());
int len = nums.size();
vector<vector<int>> res;
vector<int> temp;
vector<bool> used(len, false);
dfs(nums, 0, len, temp, res, used);
// 必包含空集
res.push_back({});
return res;
}

void dfs(vector<int>& nums, int begin, int len,
vector<int>& temp, vector<vector<int>>& res, vector<bool>& used) {
if(begin == len) { // 结束条件:当起始位置为数组长度时
return;
}
for(int i = begin; i < len; i++) {
// 关键
if(i > 0 && nums[i] == nums[i-1] && !used[i-1]) {
continue;
}
temp.push_back(nums[i]);
used[i] = true;
// 每个结点都是符合题意的解
res.push_back(temp);
// 从i+1开始取元素,不取前面出现过的元素
dfs(nums, i+1, len, temp, res, used);
temp.pop_back(); // 回溯
used[i] = false;
}
}
};
// @lc code=end

8 排列序列

给出集合 [1,2,3,...,n],其所有元素共有 n! 种排列。

按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:

  1. "123"
  2. "132"
  3. "213"
  4. "231"
  5. "312"
  6. "321"

给定 nk,返回第 k 个排列。

示例 1:

1
2
输入:n = 3, k = 3
输出:"213"

示例 2:

1
2
输入:n = 4, k = 9
输出:"2314"

示例 3:

1
2
输入:n = 3, k = 1
输出:"123"

提示:

  • 1 <= n <= 9
  • 1 <= k <= n!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/*
* @lc app=leetcode.cn id=60 lang=cpp
*
* [60] 排列序列
*/

// @lc code=start
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
string getPermutation(int n, int k) {
vector<int> nums(n, 0);
int count = 1;
for(int& num : nums) {
num = count;
count++;
}
vector<int> path;
vector<vector<int>> res;
count = 0; // 统计到了第几个
vector<bool> used(n, false);
dfs(nums, path, n, 0, k, count, res, used);
vector<int> tmp = res[0];
string resStr;
for(int& s : tmp) {
// 将整型转换成string
resStr += to_string(s);
}
return resStr;
}

void dfs(vector<int>& nums, vector<int>& path,
int len, int depth, int k, int& count, vector<vector<int>>& res, vector<bool>& used) {
if(depth == len) {
count+=1;
if(count == k) {
res.push_back(path);
}
return;
}

for(int i = 0; i < len; i++) {
if(count == k) { // 已经找到了第k个,后面的就不用遍历
break;
}
if(!used[i]) {
path.push_back(nums[i]);
used[i] = true;
dfs(nums, path, len, depth+1, k, count, res, used);
used[i] = false;
path.pop_back();
}
}
}
};
// @lc code=end

上面的代码通过回溯法,从第1个叶子结点开始寻找,直到找到第k个叶子结点,该方法可以优化。

所求排列 一定在叶子结点处得到,进入每一个分支,可以根据已经选定的数的个数,进而计算还未选定的数的个数,然后计算阶乘,就知道这一个分支的 叶子结点 的个数:

  • 如果 k 大于这一个分支将要产生的叶子结点数,直接跳过这个分支,这个操作叫「剪枝」;
  • 如果 k 小于等于这一个分支将要产生的叶子结点数,那说明所求的全排列一定在这一个分支将要产生的叶子结点里,需要递归求解。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
public:
// 方法1:使用回溯法的思路思考,则每当选择一个数加入排列时,可以计算出剩下的数还有多少种排列的可能,
// 即可以计算出当前被选择的数的排列总数,设用 remain_fac 表示, remain_fac = 剩下的待选择的数的阶乘。
// 然后将 remain_fac 与 k 进行大小比较,若小于 k ,则说明所要求的第 k 个排列不在以 当前选定的数 为开头的所有排列中,直接跳过
// 一次递归到底就能找到 第 k 个排列
string getPermutation(int n, int k) {
vector<bool> isUsed(n + 1, false); // 用于标记每个数是否用过
string res = ""; // 用于返回的结果排列序列

dfs(isUsed, res, n, k);
return res;
}
// 递归实现
void dfs(vector<bool> &isUsed, string &res, int n, int k){
int res_len = res.size();
if(res_len == n){ // 结束条件,当前排列已经结束
return;
}
int remain_fac = factorial(n - res_len - 1); // 剩下的数的全排列的个数
for(int i = 1; i <= n; ++i){ // 遍历 [1, n]
if(isUsed[i]){ // 跳过已使用的数
continue;
}
if(remain_fac > 0 && remain_fac < k){ // 剩下的数的全排列个数小于当前 k ,说明第 k 个排列肯定不在当前的递归子树中,直接跳过该递归
k -= remain_fac; // 剪枝
continue;
}
res = res + static_cast<char>('0' + i);
isUsed[i] = true;
dfs(isUsed, res, n, k);
// 因为是一次递归直接到叶子,所以不需要还原状态
}
}
// 求 n 的阶乘 或者直接使用数组:factorial = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880}
int factorial(int n){
int res = 1;
while(n > 0){
res *= n;
n--;
}
return res;
}
};

9 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

1
2
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

1
2
输入:n = 1
输出:["()"]

提示:

  • 1 <= n <= 8

image-20230316200541699

例如n=3时,可生成的左括号数和右括号数分别为3;

  • 当前左右括号都有大于 0 个可以使用的时候,才产生分支;
  • 产生左分支的时候,只看当前是否还有左括号(left>0)可以使用;
  • 产生右分支的时候,还受到左分支的限制,右边剩余可以使用的括号数量一定得在严格大于左边剩余的数量(left<right)的时候,才可以产生分支;
  • 在左边和右边剩余的括号数都等于 0 (left=right=0)的时候结算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* @lc app=leetcode.cn id=22 lang=cpp
*
* [22] 括号生成
*/

// @lc code=start
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
vector<string> generateParenthesis(int n) {
int left = n;
int right = n;
vector<string> res;
string str("");
dfs(str, left, right, res);
return res;
}

void dfs(string str, int left, int right, vector<string>& res) {
if(left == 0 && right == 0) { // 叶子结点
res.push_back(str);
return;
}
if(left > right) { // 不可行的状态
return;
}
if(left > 0) { // 添加左括号
dfs(str + "(", left-1, right, res);
}
if(left < right) { // 添加右括号
dfs(str + ")", left, right-1, res);
}
}
};
// @lc code=end

10 复原IP地址

有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:"0.1.2.201" "192.168.1.1"有效 IP 地址,但是 "0.011.255.245""192.168.1.312""192.168@1.1"无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

示例 1:

1
2
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:

1
2
输入:s = "0000"
输出:["0.0.0.0"]

示例 3:

1
2
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

提示:

  • 1 <= s.length <= 20
  • s 仅由数字组成

11 图像渲染

有一幅以 m x n 的二维整数数组表示的图画 image ,其中 image[i][j] 表示该图画的像素值大小。

你也被给予三个整数 sr , scnewColor 。你应该从像素 image[sr][sc] 开始对图像进行 上色填充

为了完成 上色工作 ,从初始像素开始,记录初始坐标的 上下左右四个方向上 像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应 四个方向上 像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为 newColor

最后返回 经过上色渲染后的图像

img

示例 1:

1
2
3
4
输入: image = [[1,1,1],[1,1,0],[1,0,1]],sr = 1, sc = 1, newColor = 2
输出: [[2,2,2],[2,2,0],[2,0,1]]
解析: 在图像的正中间,(坐标(sr,sc)=(1,1)),在路径上所有符合条件的像素点的颜色都被更改成2。
注意,右下角的像素没有更改为2,因为它不是在上下左右四个方向上与初始点相连的像素点。

示例 2:

1
2
输入: image = [[0,0,0],[0,0,0]], sr = 0, sc = 0, newColor = 2
输出: [[2,2,2],[2,2,2]]

提示:

  • m == image.length
  • n == image[i].length
  • 1 <= m, n <= 50
  • 0 <= image[i][j], newColor < 216
  • 0 <= sr < m
  • 0 <= sc < n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
* @lc app=leetcode.cn id=733 lang=cpp
*
* [733] 图像渲染
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
const int dx[4] = {1, 0, 0, -1}; // x偏移量
const int dy[4] = {0, 1, -1, 0}; // y偏移量
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {
int m = image.size();
int n = image[0].size();
vector<vector<int>> res(image); // 最终结果
vector<vector<int>> visited; // 是否已经访问
for(int i = 0; i < m; i++) {
vector<int> temp(n, 0);
visited.push_back(temp);
}
dfs(image, res, sr, sc, color, visited);
res[sr][sc] = color;
return res;
}

void dfs(vector<vector<int>>& image, vector<vector<int>>& res, int sr, int sc, int color, vector<vector<int>>& visited) {
int cur = image[sr][sc];
visited[sr][sc] = 1; // 标记访问
for(int i = 0; i < 4; i++) { // 下右左上
int x = sr + dx[i];
int y = sc + dy[i];
if(x >= 0 && x < image.size() && y >= 0 && y < image[0].size() && image[x][y] == cur) {
if(visited[x][y] == 1) { // 已经访问则跳过
continue;
}
res[x][y] = color;
dfs(image, res, x, y, color, visited);
}
}
}
};
// @lc code=end

12 岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

1
2
3
4
5
6
7
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1

示例 2:

1
2
3
4
5
6
7
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 300
  • grid[i][j] 的值为 '0''1'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*
* @lc app=leetcode.cn id=200 lang=cpp
*
* [200] 岛屿数量
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
const int dx[4] = {1, 0, 0, -1}; // x偏移量
const int dy[4] = {0, 1, -1, 0}; // y偏移量
int numIslands(vector<vector<char>>& grid) {
int m = grid.size();
int n = grid[0].size();
int res = 0; // 结果
vector<vector<int>> visited; // 是否已经访问
for(int i = 0; i < m; i++) {
vector<int> temp(n, 0);
visited.push_back(temp);
}
for(int i = 0; i < m; i++) { // 行
for(int j = 0; j < n; j++) { // 列
if(visited[i][j] == 0 && grid[i][j] == '1') {
dfs(grid, visited, i, j);
res+=1;
}
}
}
return res;
}

void dfs(vector<vector<char>>& grid, vector<vector<int>>& visited, int x, int y) {
visited[x][y] = 1; // 标记访问
for(int i = 0; i < 4; i++) { // 下右左上
int _x = x + dx[i];
int _y = y + dy[i];
// 在合理范围内 && 是陆地 && 没有访问过
if(_x >= 0 && _x < grid.size() && _y >= 0 && _y < grid[0].size() && grid[_x][_y] == '1' && visited[_x][_y] == 0) {
dfs(grid, visited, _x, _y);
}
}
}
};
// @lc code=end

13 被围绕的区域

给你一个 m x n 的矩阵 board ,由若干字符 'X''O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O''X' 填充。

示例 1:

img

1
2
3
输入: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:

1
2
输入:board = [["X"]]
输出:[["X"]]

提示:

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 200
  • board[i][j]'X''O'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/*
* @lc app=leetcode.cn id=130 lang=cpp
*
* [130] 被围绕的区域
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
const int dx[4] = {1, 0, 0, -1};
const int dy[4] = {0, 1, -1, 0};
bool flag = false;
void solve(vector<vector<char>>& board) {
int m = board.size();
int n = board[0].size();
vector<vector<int>> visited; // 是否已经访问
vector<vector<int>> path;
for(int i = 0; i < m; i++) {
vector<int> temp(n, 0);
visited.push_back(temp);
}
vector<vector<int>> ovisited(visited);
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(board[i][j] == 'O') {
flag = false;
if(i == 0 || i == board.size() - 1 || j == 0 || j == board[0].size() - 1) {
flag = true;
}
path.push_back({i, j});
dfs(board, path, visited, i, j);
if(!flag) { // 被包围
for(vector<int> v : path) {
board[v[0]][v[1]] = 'X';
}
}
path.clear();
visited = ovisited;
}
}
}
}

void dfs(vector<vector<char>>& board, vector<vector<int>>& path, vector<vector<int>>& visited, int x, int y) {
visited[x][y] = 1;
for(int i = 0; i < 4; i++) {
int _x = x + dx[i];
int _y = y + dy[i];
if(_x >= 0 && _x < board.size() && _y >= 0 && _y < board[0].size() && visited[_x][_y] == 0) {
if(board[_x][_y] == 'O') {
if(_x == 0 || _x == board.size() - 1 || _y == 0 || _y == board[0].size() - 1) {
flag = true; // 没有被包围
}
path.push_back({_x, _y});
dfs(board, path, visited, _x, _y);
// visited[_x][_y] == 0; // 回溯
}
}
}
}
};
// @lc code=end

14 单词搜索

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:

img

1
2
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

示例 2:

img

1
2
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true

示例 3:

img

1
2
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false

提示:

  • m == board.length
  • n = board[i].length
  • 1 <= m, n <= 6
  • 1 <= word.length <= 15
  • boardword 仅由大小写英文字母组成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/*
* @lc app=leetcode.cn id=79 lang=cpp
*
* [79] 单词搜索
*/

// @lc code=start
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
const int dx[4] = {1, 0, 0, -1};
const int dy[4] = {0, 1, -1, 0};
bool flag = false;
bool exist(vector<vector<char>>& board, string word) {
int m = board.size();
int n = board[0].size();
vector<vector<int>> visited; // 是否已经访问
for(int i = 0; i < m; i++) {
vector<int> temp(n, 0);
visited.push_back(temp);
}
vector<vector<int>> ovisited(visited); // 保存原有的访问数组
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(board[i][j] == word[0]) {
dfs(board, visited, i, j, word, 1);
}
visited = ovisited;
}
}
return flag;
}

void dfs(vector<vector<char>>& board, vector<vector<int>>& visited, int x, int y, string& word, int seq) {
visited[x][y] = true;
if(seq == word.size()) {
flag = true;
return;
}
for(int i = 0; i < 4; i++) {
if(flag) { // 剪枝
break;
}
int _x = x + dx[i];
int _y = y + dy[i];
if(_x >= 0 && _x < board.size() && _y >= 0 && _y < board[0].size() && visited[_x][_y] == 0) {
if(board[_x][_y] == word[seq]) {
dfs(board, visited, _x, _y, word, seq+1);
visited[_x][_y] = false; // 回溯
}
}
}
}
};
// @lc code=end

15 路径总和②

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

示例 1:

1
2
输入: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:

1
2
输入:root = [1,2,3], targetSum = 5
输出:[]

示例 3:

1
2
输入:root = [1,2], targetSum = 0
输出:[]

提示:

  • 树中节点总数在范围 [0, 5000]
  • -1000 <= Node.val <= 1000
  • -1000 <= targetSum <= 1000

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
* @lc app=leetcode.cn id=113 lang=cpp
*
* [113] 路径总和 II
*/

// @lc code=start

#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
vector<vector<int>> res;
vector<int> path;
if(root == nullptr) {
return res;
}
dfs(root, path, res, targetSum);
return res;
}

void dfs(TreeNode* node, vector<int>& path, vector<vector<int>>& res, int remain) {
path.push_back(node->val);
// 到达叶子结点
if(node->right == nullptr && node->left == nullptr) {
if(remain - node->val == 0) {
res.push_back(path);
return;
}
}
// 向左查找
if(node->left) {
dfs(node->left, path, res, remain - node->val);
// 回溯
path.pop_back();
}


// 向右查找
if(node->right) {
dfs(node->right, path, res, remain - node->val);
// 回溯
path.pop_back();
}
}
};
// @lc code=end

16 叶子相似的树

请考虑一棵二叉树上所有的叶子,这些叶子的值按从左到右的顺序排列形成一个 叶值序列

image-20230426150717477

举个例子,如上图所示,给定一棵叶值序列为 (6, 7, 4, 9, 8) 的树。

如果有两棵二叉树的叶值序列是相同,那么我们就认为它们是 叶相似 的。

如果给定的两个根结点分别为 root1root2 的树是叶相似的,则返回 true;否则返回 false

示例 1:

image-20230426150732361

1
2
输入:root1 = [3,5,1,6,2,9,8,null,null,7,4], root2 = [3,5,1,6,7,4,2,null,null,null,null,null,null,9,8]
输出:true

提示:

  • 给定的两棵树结点数在 [1, 200] 范围内
  • 给定的两棵树上的值在 [0, 200] 范围内

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <vector>
using namespace std;
class Solution {
public:
bool leafSimilar(TreeNode* root1, TreeNode* root2) {
vector<int> leafNodes1;
vector<int> leafNodes2;

dfs(root1, leafNodes1);
dfs(root2, leafNodes2);
if(leafNodes1.size() != leafNodes2.size()) {
return false;
}
for(int i = 0; i < leafNodes1.size(); i++) {
if(leafNodes1[i] != leafNodes2[i])
return false;
}
return true;
}


void dfs(TreeNode* node, vector<int>& path) {
// 返回条件:到达叶子结点
if(node->left == nullptr && node->right == nullptr) {
path.push_back(node->val);
return;
}
if(node->left)
dfs(node->left, path);
if(node->right)
dfs(node->right, path);
}
};

3 贪心算法

思想

贪心算法(greedy algorithm ,又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择。

利用贪心法求解的问题应具备如下2个特征:

  1. 贪心选择性质:一个问题的整体最优解可通过一系列局部的最优解的选择达到,并且每次的选择可以依赖以前作出的选择,但不依赖于后面要作出的选择。这就是贪心选择性质。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
  2. 最优子结构性质:当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心法求解的关键所在。在实际应用中,至于什么问题具有什么样的贪心选择性质是不确定的,需要具体问题具体分析。

Leetcode例题

分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

示例 1:

1
2
3
4
5
6
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。

示例 2:

1
2
3
4
5
6
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.

提示:

  • 1 <= g.length <= 3 * 104
  • 0 <= s.length <= 3 * 104
  • 1 <= g[i], s[j] <= 231 - 1

首先将两个数组排序,即用小饼干满足胃口最小的小孩,这样能得到满足的小孩最多。外层循环为分发的饼干,内层为小孩的胃口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
* @lc app=leetcode.cn id=455 lang=cpp
*
* [455] 分发饼干
*/

// @lc code=start
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int cur;
int begin = 0;
int res = 0;
for(int i = 0; i < s.size(); i++) {
cur = s[i]; // 当前饼干
for(int j = begin; j < g.size(); j++) {
if(cur >= g[j]) { // 满足
res++;
begin = j + 1; // 下标加1,即下一个小孩
break;
} else { // 没有满足,则后面的不可能满足,跳出循环,换一个更大的饼干
break;
}
}
}
return res;
}
};
// @lc code=end

摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

  • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
  • 相反,[1, 4, 7, 2, 5][1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度

示例 1:

1
2
3
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。

示例 2:

1
2
3
4
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。

示例 3:

1
2
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2

提示:

  • 1 <= nums.length <= 1000
  • 0 <= nums[i] <= 1000

进阶:你能否用 O(n) 时间复杂度完成此题?

题解

柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false

示例 1:

1
2
3
4
5
6
7
输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。

示例 2:

1
2
3
4
5
6
7
输入:bills = [5,5,10,10,20]
输出:false
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 false。

提示:

  • 1 <= bills.length <= 105
  • bills[i] 不是 5 就是 10 或是 20

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*
* @lc app=leetcode.cn id=860 lang=cpp
*
* [860] 柠檬水找零
*/

// @lc code=start
#include <vector>
#include <queue>
using namespace std;
class Solution {
queue<int> q1;
queue<int> q2;
bool flag;
public:
bool lemonadeChange(vector<int>& bills) {
if(bills[0] != 5) {
return false;
}
for(int i = 0; i < bills.size(); i++) {
flag = false;
int cash = bills[i];
switch(cash) {
case 5:
q1.push(cash);
flag = true;
break;
case 10:
flag = change(5);
break;
case 20:
flag = change(15);
break;
}
if(!flag) {
return false;
}
}
return true;
}

bool change(int bill) {
if(bill == 5) {
if(q1.size() == 0) {
return false;
} else {
q1.pop();
q2.push(10);
return true;
}
} else { // 15
if(q1.size() >= 1 && q2.size() >= 1) {
q1.pop();
q2.pop();
return true;
} else {
if(q1.size() >= 3) {
for(int i = 0;i < 3; i++) {
q1.pop();
}
return true;
} else {
return false;
}
}
}
return false;
}
};
// @lc code=end

4 动态规划

思想

暂略

背包问题理论基础

416.分割等和子集1

对于面试的话,其实掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包。

01背包

n件物品和一个最多能背重量为w的背包。第i件物品的重量是weight[i],得到的价值是value[i]每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

暴力解法:每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是$o(2^n)$,这里的n表示物品数量。

所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!

例子:

背包最大重量为4

物品为:

重量 价值
物品0 1 15
物品1 3 20
物品2 4 30

问背包能背的物品最大价值是多少?

题解

  • 确定dp数组以及下标的含义

对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

动态规划-背包问题1

  • 确定递推公式

不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)

放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值。

所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

  • 初始值

首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图:

动态规划-背包问题2

状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。

代码初始化如下:

1
2
3
4
5
6
7
for (int j = 0 ; j < weight[0]; j++) {  // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。
dp[0][j] = 0;
}
// 正序遍历
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}

此时dp数组初始化情况如图所示:

动态规划-背包问题7

1
2
3
4
5
// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}

可以看出dp[i][j] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。

image-20230326185953786

  • 确定遍历顺序

可以看出,有两个遍历的维度:物品与背包重量

那么问题来了,先遍历 物品还是先遍历背包重量呢?

其实都可以!! 但是先遍历物品更好理解

那么我先给出先遍历物品,然后遍历背包重量的代码。

1
2
3
4
5
6
7
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void test_2_wei_bag_problem1() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagweight = 4;

// 二维数组
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));

// 初始化
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}

// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

}
}

cout << dp[weight.size() - 1][bagweight] << endl;
}

int main() {
test_2_wei_bag_problem1();
}

动态规划-背包问题4

Leetcode例题

爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

1
2
3
4
5
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

1
2
3
4
5
6
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

提示:

  • 1 <= n <= 45

爬第n阶楼梯的方法数量,等于 2 部分之和:

  1. 爬上 n−1 阶楼梯的方法数量,因为再爬1阶就能到第n阶
  2. 爬上 n−2 阶楼梯的方法数量,因为再爬2阶就能到第n阶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
* @lc app=leetcode.cn id=70 lang=cpp
*
* [70] 爬楼梯
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
int climbStairs(int n) {
if(n <= 2) {
return n;
}
vector<int> dp(n + 1, 0);
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i < n + 1; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
// @lc code=end

优化:滚动数组。我们不难通过转移方程和边界条件给出一个时间复杂度和空间复杂度都是 O(n) 的实现,但是这里的$f(x)$之和$f(x-1)$与$f(x-2)$有关。

fig1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* @lc app=leetcode.cn id=70 lang=cpp
*
* [70] 爬楼梯
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
int climbStairs(int n) {
if(n <= 2) {
return n;
}
int f_n_1 = 1, f_n_2 = 0, f_n = 2;
for(int i = 3; i <= n; i++) {
f_n_2 = f_n_1; // 前进
f_n_1 = f_n; // 前进
f_n = f_n_1 + f_n_2; // 递推
}
return f_n;
}
};
// @lc code=end

使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1:

1
2
3
4
5
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。

示例 2:

1
2
3
4
5
6
7
8
9
10
输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。

提示:

  • 2 <= cost.length <= 1000
  • 0 <= cost[i] <= 999
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
* @lc app=leetcode.cn id=746 lang=cpp
*
* [746] 使用最小花费爬楼梯
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int len = cost.size();
vector<int> dp1(len + 1, 0); // 从第一个阶梯开始
vector<int> dp2(len, 0); // 从第二个阶梯开始
dp1[0] = 0;
dp1[1] = cost[0];
dp2[0] = 0;
dp2[1] = cost[1];
for(int i = 2; i <= len; i++) {
dp1[i] = min(dp1[i - 1] + cost[i - 1], dp1[i - 2] + cost[i - 2]);
}
for(int i = 2; i < len; i++) {
dp2[i] = min(dp2[i - 1] + cost[i], dp2[i - 2] + cost[i - 1]);
}
return dp1[len] < dp2[len - 1] ? dp1[len] : dp2[len - 1];
}
};
// @lc code=end

或者将两种情况考虑在一起,此时$dp[0]=dp[1]=0$:

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
vector<int> dp(n + 1);
dp[0] = dp[1] = 0;
for (int i = 2; i <= n; i++) {
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[n];
}
};

斐波那契数列

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 01 开始,后面的每一项数字都是前面两项数字的和。也就是:

1
2
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n)

示例 1:

1
2
3
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

1
2
3
输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3:

1
2
3
输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

提示:

  • 0 <= n <= 30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* @lc app=leetcode.cn id=509 lang=cpp
*
* [509] 斐波那契数
*/

// @lc code=start
class Solution {
public:
int fib(int n) {
if(n <= 1) {
return n;
}
int *dp = new int[n+1];
dp[0] = 0;
dp[1] = 1;
for(int i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
// @lc code=end

滚动数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int fib(int n) {
if(n <= 1) {
return n;
}
int q = 0, p = 1, r = 1;
for(int i = 2; i < n; i++) {
q = p;
p = r;
r = p + q;
}
return r;
}
};

不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

image-20230321174213065

问总共有多少条不同的路径?

示例 1:

1
2
输入:m = 3, n = 7
输出:28

示例 2:

1
2
3
4
5
6
7
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

1
2
输入:m = 7, n = 3
输出:28

示例 4:

1
2
输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109

可以使用回溯法,但是时间超时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
* @lc app=leetcode.cn id=62 lang=cpp
*
* [62] 不同路径
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
const int dx[2] = {1, 0};
const int dy[2] = {0, 1};
int uniquePaths(int m, int n) {
int res = 0;
vector<vector<int>> board;
for(int i = 0; i < m; i++) {
vector<int> temp(n, 0);
board.push_back(temp);
}
board[m - 1][n - 1] = 1;
dfs(board, 0, 0, res);
return res;
}

void dfs(vector<vector<int>>& board, int x, int y, int& res) {
if(board[x][y] == 1) {
res++;
return;
}
for(int i = 0; i < 2; i++) { // 先下后右
int _x = x + dx[i];
int _y = y + dy[i];
if(_x < board.size() && _y < board[0].size()) {
dfs(board, _x, _y, res);
}
}
}
};
// @lc code=end

使用动态规划:

image-20230321180418377

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
* @lc app=leetcode.cn id=62 lang=cpp
*
* [62] 不同路径
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 0));
int i, j;
for(i = 0; i < m; i++) {
dp[i][0] = 1;
}
for(i = 0; i < n; i++) {
dp[0][i] = 1;
}
for(i = 1; i < m; i++) {
for(j = 1; j < n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
// @lc code=end

不同路径②

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 10 来表示。

示例 1:

img

1
2
3
4
5
6
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

1
2
输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

  • m == obstacleGrid.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j]01

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*
* @lc app=leetcode.cn id=63 lang=cpp
*
* [63] 不同路径 II
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int i, j;
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
// 特例
if(obstacleGrid[m-1][n-1] == 1 || obstacleGrid[0][0] == 1) {
return 0;
}
vector<vector<int>> dp(m, vector<int>(n, 0));
for(i = 0; i < m; i++) {
if(obstacleGrid[i][0] == 1) { // 途中有障碍,则后面的都是0
break;
} else {
dp[i][0] = 1; // 否则初值赋值为1
}
}
for(i = 0; i < n; i++) {
if(obstacleGrid[0][i] == 1) {
break;
} else {
dp[0][i] = 1;
}
}
for(i = 1; i < m; i++) {
for(j = 1; j < n; j++) {
dp[i][j] = 0;
if(obstacleGrid[i][j] == 1) { // 该点有障碍,则设置为0,不可达
dp[i][j] = 0;
continue;
}
if(dp[i-1][j] != 0) { // 不为0,则经过该条路可达i,j点
dp[i][j] += dp[i-1][j];
}
if(dp[i][j-1] != 0) {
dp[i][j] += dp[i][j-1];
}
}
}
return dp[m-1][n-1];
}
};
// @lc code=end

整数拆分

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积

示例 1:

1
2
3
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

1
2
3
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

提示:

  • 2 <= n <= 58

题解

image-20230321210932404

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
* @lc app=leetcode.cn id=343 lang=cpp
*
* [343] 整数拆分
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1, 0);
dp[0] = 0;
dp[1] = 1;
// dp[i]的含义:将i分成多个部分后,各个部分相乘得到的最大值
for(int i = 2; i <= n; i++) {
int curMax = 0; // 记录j在变化时,dp[i]的最大值
for(int j = 1; j < i; j++) { // j是整数i分出来的一部分,范围:1 <= j < i
curMax = max(curMax, max(j * (i - j), j * dp[i - j]));
}
dp[i] = curMax;
}
return dp[n];
}
};
// @lc code=end

不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例 1:

img

1
2
输入:n = 3
输出:5

示例 2:

1
2
输入:n = 1
输出:1

提示:

  • 1 <= n <= 19

题解

image-20230322153050578

image-20230322153114200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
* @lc app=leetcode.cn id=96 lang=cpp
*
* [96] 不同的二叉搜索树
*/

// @lc code=start
#include <vector>
using namespace std;
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n+1, 0);
dp[0] = 1; // 空树也是一棵树
dp[1] = 1;
for(int i = 2; i <= n; i++) {
dp[i] = 0;
for(int j = 0; j < i; j++) {
// 左子树的组合数*右子树的组合数
dp[i] = dp[i] + dp[j] * dp[i-j-1];
}
}
return dp[n];
}
};
// @lc code=end

分割等和子集*

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

1
2
3
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

1
2
3
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

题解

打家劫舍③*

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额

示例 1:

image-20230329211025081

1
2
3
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

image-20230329211035761

1
2
3
输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

提示:

  • 树的节点数在 [1, 104] 范围内
  • 0 <= Node.val <= 104

题解

image-20230329212757233

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
* @lc app=leetcode.cn id=337 lang=cpp
*
* [337] 打家劫舍 III
*/

// @lc code=start
#include <unordered_map>
using namespace std;
class Solution {
unordered_map<TreeNode*, int> f;
unordered_map<TreeNode*, int> g;
public:
int rob(TreeNode* root) {
postorder(root);
return max(f[root], g[root]);
}
// 后序遍历
void postorder(TreeNode* node) {
if(node == nullptr) {
return ;
}
postorder(node->left);
postorder(node->right);
f[node] = node->val + g[node->left] + g[node->right];
g[node] = max(f[node->left], g[node->left]) + max(f[node->right], g[node->right]);
}
};
// @lc code=end

买卖股票的最佳时机②

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

示例 1:

1
2
3
4
5
输入: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:

1
2
3
4
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。

示例 3:

1
2
3
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。

提示:

  • 1 <= prices.length <= 3 * 104
  • 0 <= prices[i] <= 104

题解