回溯算法

组合

77. 组合 - 力扣(LeetCode)

基本回溯

要把问题转换成树的形式,参考代码随想录 (programmercarl.com)

image-20230109091445585

  • 相当于对树进行深度搜索的过程

  • 用path记录到叶节点的路径,所谓叶节点也就是满足k个数的情况,亦即递归函数出口。

    path相当于一个栈

  • 当到达叶节点时,path中存放的就是一个组合,需要把这个组合保存到结果集合中

    然后进行回溯,将栈顶节点退出,继续查看下一个节点

  • 当前遍历需要遍历从开始节点s到n的所有数

    使用开始节点s是为了防止重复

实现代码如下

//直接定义全局变量,方便递归操作
int **ans;	//组合集合
int *path;	//当前组合
int top;	//path栈指针
int cnt;	//组合个数

void fun(int n, int k, int s){
    int i;
    if(top == k){	//递归出口,组合中的数已经达到k个
        int *temp = (int *) malloc(sizeof(int) * k);
        for(i = 0; i < top; ++i){
            temp[i] = path[i];
        }
        ans[cnt++] = temp;	//将这个组合保存在结果集中
    }
    else{
        for(i = s; i <= n; ++i){
            path[top++] = i;
            fun(n, k, i + 1);
            top--;  //回溯
        }
    }
}

int** combine(int n, int k, int* returnSize, int** returnColumnSizes){
    int i;
    ans = (int **) malloc(sizeof(int *) * 10000);
    path = (int *)malloc(sizeof(int) * 22);
    cnt = 0;
    top = 0;
    fun(n, k, 1);
    *returnSize = cnt;
    
    *returnColumnSizes = (int *) malloc(sizeof(int) * cnt);
    for(i = 0; i < cnt; ++i){
        (*returnColumnSizes)[i] = k;
    }
    return ans;
}

优化(剪枝)

避免进入根本不可能的情况的递归

  1. 已经选择的元素个数:path.size();

  2. 所需需要的元素个数为: k - path.size();

  3. 列表中剩余元素(n-i+1) >= 所需需要的元素个数(k - path.size())

    不满足这个条件,则不有足够的需要的组合数

  4. 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历

优化代码

for(i = s; i <= n-(k-top)+1; ++i){
    path[top++] = i;
    fun(n, k, i + 1);
    top--;  //回溯
}

216. 组合总和 III - 力扣(LeetCode)

同一种类型的题目,只不过在选择的时候多了一个限制条件,差别在

  • 回溯时,有几个限制条件就要处理几个回溯,不能漏(⭐重要)
  • 剪枝可以考虑多个条件(减枝之前先处理回溯)

实现代码

int **ans;
int *path;
int top;
int cnt;
int sum;	//新增限制条件

void fun(int k, int n, int start){
    int i;
    if(top == k){
        if(sum == n){	//新增限制条件
            int *temp = (int *) malloc(sizeof(int) * top);
            for(i = 0; i < top; ++i){
                temp[i] = path[i];
            }
            ans[cnt++] = temp;
        }
    }
    else{
        for(i = start; i <= 9; ++i){
            path[top++] = i;
            sum += i;
            fun(k, n, i + 1);
            sum -= i;   //回溯,别忘了回溯sum要减去i
            top--;      //回溯
        }
    }
}

int** combinationSum3(int k, int n, int* returnSize, int** returnColumnSizes){
    int i;
    ans = (int **) malloc(sizeof(int *) * 10000);
    path = (int *) malloc(sizeof(int) * 10);
    *returnColumnSizes = (int *) malloc(sizeof(int) * 10000);
    cnt = 0;
    top = 0;
    sum = 0;
    fun(k, n, 1);
    *returnSize = cnt;
    for(i = 0; i < cnt; ++i){
        (*returnColumnSizes)[i] = k;
    }
    return ans;
}

优化(减枝)代码

for(i = start; i <= 9-(k-top)+1; ++i){  //剪枝
    path[top++] = i;
    sum += i;
    if(sum > n){    //剪枝,sum已经>k,之后的数都不用考虑了
        sum -= i;   //剪枝之前先处理回溯
        top--;      //剪枝之前先处理回溯
        break;
    }
    fun(k, n, i + 1);
    sum -= i;   //回溯,别忘了回溯sum要减去i
    top--;      //回溯
}

电话号码的字母组合

17. 电话号码的字母组合 - 力扣(LeetCode)

组合问题先画出树形结构再分析

image-20230110101430112

给定数字串digits,可以看到如果遍历到树的节点高度为digits的长度,就是需要的组合,将其存放如结果集合

前两道题是同一个集合中元素的组合,而本题是不同集合元素的组合,因此控制方式不能再是定义开始位置start,而因该是每次从头开始。

注意:要提前处理digits为空的情况

实现代码如下

char **ans;	//结果集合
char *path;	//当前组合(栈)
//定义电话号码键盘
char *s[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
int cnt;	//组合个数
int top;	//path栈指针

void fun(char *digits, int index){
    int i;
    if(index == strlen(digits)){
        char *temp = (char*)malloc(sizeof(char) * (index + 1));
        for(i = 0; i < top; ++i){
            temp[i] = path[i];
        }
        temp[i] = 0;
        ans[cnt++] = temp;
        return ;
    }

    int d = digits[index] - '0';
    char *t = s[d];

    for(i = 0; i < strlen(t); ++i){
        path[top++] = t[i];
        fun(digits, index + 1);
        top--;	//回溯
    }
}

char ** letterCombinations(char * digits, int* returnSize){
    ans = (char **)malloc(sizeof(char *) * 10000);
    //要提前处理digits为空的情况,否则调用fun,结果集合中会有一个空组合
    if(strlen(digits) == 0){
        *returnSize = 0;
        return ans;
    }
    path = (char *)malloc(sizeof(char) * (strlen(digits) + 1));
    cnt = 0;
    top = 0;
    fun(digits, 0);
    *returnSize = cnt;
    return ans;
}

组合总和

39. 组合总和 - 力扣(LeetCode)

基本组合问题是要求,指定组合中元素的个数要求,且不能重复

而组合总和没有元素个数的要求,要求元素总和为target,且同一个元素可以取多次,组合不能重复

要在基本组合问题的基础上解决两个问题

  • 如何使得组合不重复(即[2,2,3]和[2,3,2]为同一个组合)

    这个问题已经在基本组合中解决了,就是使用start变量控制不能取之前的元素

  • 如何使得同一个元素可以重复取

    基本组合中,取完一个元素i,下一层递归参数是i+1,实现了组合中元素不重复

    为了使得组合中元素可以重复取,取完一个元素i,下一层递归参数变为i即可

该问题的树形结构如下所示

image-20230110105641557

实现代码如下

int **ans;	//结果集合
int *path;	//当前组合(相当于一个栈,在此栈上进行回溯操作)
int *colSize;	//当前组合中元素个数
int cnt;	//符合要求的组合数
int top;	//栈顶指针
int sum;	//当前元素和

void fun(int *nums, int n, int target, int start){
    int i;
    if(sum == target){	//符合要求,保存在结果集合中
        int *temp = (int *)malloc(sizeof(int) * 1000);
        for(i = 0; i < top; ++i){
            temp[i] = path[i];
        }
        ans[cnt] = temp;
        colSize[cnt++] = top;
    }
    else{
        for(i = start; i < n; ++i){	//从start开始遍历,避免组合重复
            path[top++] = nums[i];
            sum += nums[i];
            if(sum > target){	//剪枝
                sum -= nums[i];
                top--;
                continue;
            }
            fun(nums, n, target, i);	//注意参数为i,可以重复取同一个元素
            sum -= nums[i];		//回溯
            top--;				//回溯
        }
    }
}

int** combinationSum(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes){
    ans = (int **)malloc(sizeof(int *) * 200);
    path = (int *)malloc(sizeof(int) * 10000);
    (*returnColumnSizes) = (int *)malloc(sizeof(int) * 200);
    cnt = 0;
    top = 0;
    sum = 0;
    colSize = (int *) malloc(sizeof(int) * 200);

    fun(candidates, candidatesSize, target, 0);
    *returnSize = cnt;
    int i;
    for(i = 0; i < cnt; ++i){
        (*returnColumnSizes)[i] = colSize[i];
    }
    return ans;
}

(难)组合总和Ⅱ

40. 组合总和 II - 力扣(LeetCode)

解题参考:

回溯算法中的去重,树层去重树枝去重,你弄清楚了没?| LeetCode:40.组合总和II_哔哩哔哩_bilibili

代码随想录 (programmercarl.com)

与上题的不同之处

  • 给定的数组元素可能有重复

  • 要求一个元素只能使用一次

    但组合中可能出现重复元素,因为给定的数组中该元素可能是重复的

同层值相同的元素不能重复使用,因为可能出现组合重复的情况,纵深值相同的元素可以重复出现,因为虽然值相同,但不是同一个元素(给定数组中可能存在值相同的多个元素)

实现代码如下

int **ans;	//结果组合集合
int *path;	//当前组合
int *colSize;	//每个组合中元素的个数
int top;	//栈顶指针
int cnt;	//符合条件的组合数
int sum;	//当前栈中元素和
int used[110];	//标记数组

void fun(int *candidates, int candidatesSize, int target, int startIndex){
    int i;
    if(sum == target){	//符合条件 保存到结果集合
        int *temp = (int *)malloc(sizeof(int) * 200);
        for(i = 0; i < top; ++i){
            temp[i] = path[i];
        }
        ans[cnt] = temp;
        colSize[cnt++] = top;
    }
    else{
        for(i = startIndex; i < candidatesSize; ++i){
            //同层元素值相同,可能出现组合重复
            if(i > 0 && candidates[i] == candidates[i-1] && used[i-1] == 0){
                continue;
            }
            path[top++] = candidates[i];
            sum += candidates[i];
            used[i] = 1;
            //递归出口,由于提前排序,之后的必定大于target,直接break
            if(sum > target){	
                sum -= candidates[i];
                top--;
                used[i] = 0;
                break;
            }
            //i+1不允许同一个元素重复使用
            fun(candidates, candidatesSize, target, i+1);
            sum -= candidates[i];	//回溯
            top--;					//回溯
            used[i] = 0;			//回溯
        }
    }
}

//归并排序
void merge(int a[], int low, int mid, int high){
    int k = 0;
    int *b = (int *)malloc(sizeof(int) * (high-low+1));
    int i = low, j = mid + 1;
    while(i <= mid && j <= high){
        if(a[i] < a[j]){
            b[k++] = a[i++];
        }
        else {
            b[k++] = a[j++];
        }
    }
    while(i <= mid){
        b[k++] = a[i++];
    }
    while(j <= high){
        b[k++] = a[j++];
    }   

    for(i = 0; i < k; ++i){
        a[low+i] = b[i];
    }
}

void mergeSort(int a[], int low, int high){
    if(low < high){
        int mid = (low + high) / 2;
        mergeSort(a, low, mid);
        mergeSort(a, mid+1, high);
        merge(a, low, mid, high);
    }
}

int** combinationSum2(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes){
    ans = (int **) malloc(sizeof(int *) * 10000);
    path = (int *)malloc(sizeof(int) * 200);
    colSize = (int *)malloc(sizeof(int) * 200);
    (*returnColumnSizes) = (int *)malloc(sizeof(int) * 200);
    int i;
    top = 0;
    sum = 0;
    cnt = 0;
    for(i = 1; i < 110; ++i){	//used初始化
        used[i] = 0;
    }
    mergeSort(candidates, 0, candidatesSize-1);	//先对数组排序小->大
    fun(candidates, candidatesSize, target, 0);
    *returnSize = cnt;
    for(i = 0; i < cnt; ++i){
        (*returnColumnSizes)[i] = colSize[i];
    }
    return ans;
}

全排列

46. 全排列 - 力扣(LeetCode)

排列与组合不同之处:组合无序,排列有序

使用used数组标记元素是否已经被取过

未命名文件

具体代码如下

int **ans;	//结果集合
int *path;	//当前排列
int top;	//栈指针
int cnt;	//当前排列个数
int used[10];	//标记数组

void fun(int *nums, int n){
    int i;
    if(top == n){	//递归出口,也就是树中叶子节点
        int *temp = (int *)malloc(sizeof(int) * 10);
        for(i = 0; i < top; ++i){
            temp[i] = path[i];
        }
        ans[cnt++] = temp;	//将当前排列存放到结果集合
    }
    else{
        for(i = 0; i < n; ++i){	//排列有序,可以取前面的元素
            if(used[i] == 1){	//已经取过不再取
                continue;
            }
            path[top++] = nums[i];	//处理nums[i]
            used[i] = 1;			//处理nums[i]
            fun(nums, n);
            used[i] = 0;	//回溯
            top--;			//回溯
        }
    }
}


int** permute(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
    ans = (int **)malloc(sizeof(int *) * 4000);
    path = (int *)malloc(sizeof(int) * 10);
    (*returnColumnSizes) = (int *)malloc(sizeof(int) * 4000);
    //初始化
    top = 0;
    cnt = 0;
    int i;
    for(i = 0; i < 10; ++i){
        used[i] = 0;
    }

    fun(nums, numsSize);

    for(i = 0; i < cnt; ++i){
        (*returnColumnSizes)[i] = numsSize;
    }
    *returnSize = cnt;
    return ans;
}

全排列Ⅱ

在全排列的基础上使用组合总和Ⅱ的去重方法即可:先排序,同层不能重复出现相同的key,纵深可以出现

//同层不能取key相同,纵深可以取key相同
void fun(int *nums, int n){
    int i;
    if(top == n){
        int *temp = (int *)malloc(sizeof(int) * top);
        for(i = 0; i < top; ++i){
            temp[i] = path[i];
        }
        ans[cnt++] = temp;
    }
    else{
        for(int i = 0; i < n; ++i){
            if(used1[i] == 1){
                continue;
            }
            if(i > 0 && nums[i-1] == nums[i] && used2[i-1] == 0){
                continue;
            }
            path[top++] = nums[i];
            used1[i] = 1;
            used2[i] = 1;
            fun(nums, n);
            used1[i] = 0;
            used2[i] = 0;
            top--;
        }
    }
}

子集

78. 子集 - 力扣(LeetCode)

与组合问题的不同之处在于,要收集树中有所节点,不能重复,只能向后看,要添加startIndex

实现代码如下

int **ans;
int *cur;
int top;
int cnt;

void fun(int *nums, int n, int startIndex, int **returnColumnSizes){
    int i;
    int *temp = (int *)malloc(sizeof(int) * top);
    for(i = 0; i < top; ++i){
        temp[i] = cur[i];
    }
    ans[cnt] = temp;
    (*returnColumnSizes)[cnt++] = top;

    for(i = startIndex; i < n; ++i){
        cur[top++] = nums[i];
        fun(nums, n, i+1, returnColumnSizes);
        top--;  //回溯
    }
}

int** subsets(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
    ans = (int **)malloc(sizeof(int *) * 10000);
    cur = (int *)malloc(sizeof(int) * 12);
    (*returnColumnSizes) = (int *)malloc(sizeof(int) * 10000);
    top = cnt = 0;

    fun(nums, numsSize, 0, returnColumnSizes);

    *returnSize = cnt;
    return ans;
}

子集Ⅱ

90. 子集 II - 力扣(LeetCode)

在子集的基础上添加去重操作,同层不能重复取key相同的元素,纵深可以,添加标记数组实现

实现代码如下

void fun(int *nums, int n, int startIndex, int **returnColumnSizes){
    int i;
    int *temp = (int *)malloc(sizeof(int) * top);
    for(i = 0; i < top; ++i){
        temp[i] = cur[i];
    }
    ans[cnt] = temp;
    (*returnColumnSizes)[cnt++] = top;

    for(i = startIndex; i < n; ++i){
        if(i > 0 && nums[i-1] == nums[i] && used[i-1] == 0){	//⭐关键代码
            continue;
        }
        cur[top++] = nums[i];
        used[i] = 1;
        fun(nums, n, i+1, returnColumnSizes);
        top--;          //回溯
        used[i] = 0;    //回溯
    }
}

(难)递增子序列

491. 递增子序列 - 力扣(LeetCode)

和子集Ⅱ类似,但是要注意

  1. 求的是子序列,不能改变原始序列的顺序,不能进行排序
  2. 主要要求递增子序列,和元素个数大于2,要有剪枝操作
  3. 要有去重操作,本题的去重不能进行排序,要额外增加一个访问数组,存放本层已经访问的过的key

实现代码如下

int **ans;
int *cur;
int top;
int cnt;

int find(int *a, int n, int key){	//检查当前key是否已经出现过
    int i;
    for(i = 0; i < n; ++i){
        if(a[i] == key)
            return 1;
    }
    return 0;
}

void fun(int *nums, int n, int startIndex, int **returnColumnSizes){
    int i;
    int *used = (int *)malloc(sizeof(int) * 20); //记录本层已经出现过的key
    int useSize = 0;
    if(top >= 2){	//元素个数大于2的保存在结果集合中
        int *temp = (int*)malloc(sizeof(int) * top);
        for(i = 0; i < top; ++i){
            temp[i] = cur[i];
        }
        ans[cnt] = temp;
        (*returnColumnSizes)[cnt++] = top;
    }

    for(i = startIndex; i < n; ++i){
        if(top > 0 && nums[i] < cur[top-1]){   //出现了递减数列 跳过
            continue;
        }
        if(find(used, useSize, nums[i]) == 1){  //本层已经出现过相同的key 跳过
            continue;
        }
        cur[top++] = nums[i];
        used[useSize++] = nums[i];	//保存已经出现过的key
        fun(nums, n, i+1, returnColumnSizes);
        top--;	//回溯
    }
}

int** findSubsequences(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
    ans = (int **)malloc(sizeof(int *) * 40000);
    cur = (int *)malloc(sizeof(int) * 20);
    (*returnColumnSizes) = (int *)malloc(sizeof(int) * 40000);
    top = cnt = 0;
    int i;

    fun(nums, numsSize, 0, returnColumnSizes);

    *returnSize = cnt;
    return ans;   
}

总结

  1. 回溯算法可以用于求解:组合、排列、子集等问题,回溯问题均可以通过分析转换成树的形式表达。

  2. 注意组合与排列的区别,组合集合内的元素是无序的,即{1,2}与{2,1}是同一个组合,而对于排列,集合内的元素是有序的,{1,2}与{2,1}是两个不同的排列。

  3. 组合与排列在代码实现上的不同是,组合取完一个元素的取值之后,要考虑此元素之后的元素,而排列取完一个元素之后要从头考虑所有的元素,原因就是组合无序,而排列有序。

  4. 子集也可看做是组合问题,不同的是,组合考虑的树中的叶子节点,而子集考虑的是树中的所有节点。

  5. 在解题的过程中要注意题目给定的数组是否有重复的元素,若有重复的元素还涉及到去重的操作,整体思路是每一层递归不能重复使用关键字相同的元素,原因是会产生重复,而递归纵深方向可以使用关键字相同的元素,原因是虽然纵深方向关键字相同,但实际上是不同的元素。

posted @ 2023-01-14 14:20  dctwan  阅读(33)  评论(0编辑  收藏  举报