代码随想录算法训练营第二十六天 | 39. 组合总和,40.组合总和II, 131.分割回文串[回溯篇]

文章讲述了LeetCode中组合总和、组合总和II和分割回文串的回溯算法实现及其思路,包括剪枝和去重技巧。


LeetCode 39. 组合总和

题目链接:39. 组合总和
文章讲解:代码随想录#39. 组合总和
视频讲解:带你学透回溯算法-组合总和(对应「leetcode」力扣题目:39.组合总和)| 回溯法精讲!

题目描述

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

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

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

示例1

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

示例2

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

示例3

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

提示

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • candidates 的所有元素 互不相同
  • 1 <= target <= 40

思路

同一个数字可以无限制重复使用,这和之前 77.组合 不太一样,所以每次递归遍历时,索引不需要+1。
target作为递归函数的终止条件。

也可以做一些剪枝的操作,比如前sum之和大于target时直接返回。

参考代码

/**
 * Return an array of arrays of size *returnSize.
 * The sizes of the arrays are returned as *returnColumnSizes array.
 * Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
 */

int **res;
int cnt;
int sum;
typedef struct {
    int index;
    int nums[30];
}Data;
Data data = {0};

void backtracking(int* candidates, int candidatesSize, int target, int** returnColumnSizes, int idx)
{
    if (sum == target) {
        res[cnt] = (int *)malloc(data.index * sizeof(int));
        for (int i = 0; i < data.index; i++) {
           res[cnt][i] = data.nums[i]; 
        }
        (*returnColumnSizes)[cnt] = data.index;
        cnt++;
        return;
    }

    for (int i = idx; i < candidatesSize; i++) {
        if (sum + candidates[i] > target) continue; // 剪枝,跳过当前循环
        data.nums[data.index++] = candidates[i];
        sum += candidates[i];
        backtracking(candidates, candidatesSize, target, returnColumnSizes, i);
        sum -= candidates[i]; // 回溯
        data.index--;
        data.nums[data.index] = 0;
    }
}

int** combinationSum(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes) {
    res = (int**)malloc(1000 * sizeof(int));
    *returnColumnSizes = (int*)malloc(sizeof(int) * 200);
    cnt = 0;
    sum = 0;
    backtracking(candidates, candidatesSize, target, returnColumnSizes, 0);
    *returnSize = cnt;
    
    return res;
}

总结

  1. 对于返回值以及returnColumnSizes的初始化时,空间大小不好选择,我每次都是试出来了,这个不太好。

LeetCode 40.组合总和II

题目链接:40.组合总和II
文章讲解:代码随想录#40.组合总和II
视频讲解:回溯算法中的去重,树层去重树枝去重,你弄清楚了没?| LeetCode:40.组合总和II

题目描述

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

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

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

示例1

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

示例2

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

提示

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30

思路

需要注意几个点如下:

  • candidates 数组中的每个数字在每个组合中只能使用一次。
  • candidates 数组中的元素会有重复的。

这道题我们需要考虑去重的情况了。
所谓去重,其实就是使用过的元素不能重复使用。
那应该如何去重呢?
①首先对candidates 数组进行从小到大的排序,相同的数值的元素就连在一起了。
②构造一个与candidates 数组大小相同的used数组,用来表示元素是否使用过。
具体思路可以查看代码随想录

我盗用两张图说明一下重复的情况。
在这里插入图片描述
在这里插入图片描述
在candidates[i] == candidates[i - 1]相同的情况下:

  • used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
  • used[i - 1] == false,说明同一树层candidates[i - 1]使用过

参考代码

int cnt;
int sum;
int* used;
int** ret;
typedef struct {
    int index;
    int nums[100];
} Data;
Data data = {0};

int cmp(const void* p1, const void* p2) { return *(int*)p1 - *(int*)p2; }

void backtracking(int* candidates, int candidatesSize, int target, int** returnColumnSizes, int idx) {
    if (sum == target) {
        ret[cnt] = malloc(sizeof(int) * data.index);
        for (int i = 0; i < data.index; i++)
            ret[cnt][i] = data.nums[i];
        (*returnColumnSizes)[cnt] = data.index;
        cnt++;
        return;
    }

    for (int i = idx; i < candidatesSize; i++) {
        if (sum + candidates[i] > target) // 剪枝操作
            break;
        // 当前执行到i,如果used[i - 1] == 0,说明candidates[i - 1]已经使用过了,直接跳过
        // 如果看不懂,可以多看看随想录的思路,然后自已在本地推导几遍
        if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == 0) 
        	continue;
      
        sum += candidates[i];
        data.nums[data.index++] = candidates[i];
        used[i] = 1;
        // 必须是i+1,下一层的循环跳过当前层的元素,指向下一个元素
        backtracking(candidates, candidatesSize, target, returnColumnSizes, i + 1);
        sum -= candidates[i]; // 回溯
        data.index--;
        data.nums[data.index] = 0;
        used[i] = 0;
    }
}

int** combinationSum2(int* candidates, int candidatesSize, int target,
                      int* returnSize, int** returnColumnSizes) {
    ret = (int**)malloc(10000 * sizeof(int*));
    used = (int*)malloc(100 * sizeof(int));
    *returnColumnSizes = (int*)malloc(sizeof(int) * 100);
    
    for (int i = 0; i < 100; i++) // 清空used数组
        used[i] = 0;
    cnt = 0;
    sum = 0;

    // 排序,将相同值的元素挨在一起
    qsort(candidates, candidatesSize, sizeof(int), cmp);
    backtracking(candidates, candidatesSize, target, returnColumnSizes, 0);
    
    *returnSize = cnt;
    return ret;
}

LeetCode 131.分割回文串

题目链接:[131.分割回文串 (https://leetcode.cn/problems/palindrome-partitioning/)
文章讲解:代码随想录#131.分割回文串
视频讲解:带你学透回溯算法-分割回文串(对应力扣题目:131.分割回文串)| 回溯法精讲!

题目描述

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是正着读和反着读都一样的字符串。

示例1

输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]

示例2

输入:s = “a”
输出:[[“a”]]

提示

  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成

思路

看完题目描述后没有一点儿思路,还是得看一遍视频。
本题有两个关键点:
①字符串切割,可以使用回溯法进行不同方式的切割。
②对切割好的子串进行回文判断,如果是回文,则添加到返回的变量中。

切割问题

其实切割问题,可以采用回溯法进行切割,终止条件就是切割到了字符串的最后一个字符。
递归函数的返回值为void,参数有两个,一个是原字符串,另一个当前层遍历的起始位置idx。

在单层搜索的逻辑中会有一个for循环,相当于对树进行横向遍历,i的起始值为idx,i的终止值为strlen(s)。
首先对以idx/i为起始的字符串进行处理(第一层),
如果[idx, i]的子串为回文,则将子串添加到res变量中,
调用递归函数,对以i+1为起始的字符串进行处理(第二层)。。。
递归函数处理完后,进行回溯操作,然后对i+1继续下一次遍历。
如果不是回文,在同一层i+1继续遍历。

借用一张图说明一下树状关系:
在这里插入图片描述

回文判断

可以使用双指针法,一个从头向尾,一个从尾向前开始遍历,如果前后指针指向的元素相等,则说明是回文字符串。

参考代码

char*** partition(char* s, int* returnSize, int** returnColumnSizes) {
    // 待实现
}

总结

  1. 这道题的复杂之处就是要对收集的结果赋值给一个三维指针,时间有限,我暂时还没有想清楚,后面补上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值