8、回溯算法相关

上一节:7、二叉树相关

下一节:9、贪心算法相关

回溯算法

1、理论

定义

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。

效率

因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

解决的问题

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

组合与排列

组合是不强调元素顺序的,排列是强调元素顺序。
例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了。

如何理解回溯法

回溯法解决的问题都可以抽象为树形结构,是的,我指的是所有回溯法的问题都可以抽象为树形结构!

因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。

回溯法模板

搜索的遍历过程

在这里插入图片描述

模板

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

2、组合问题

leetcode 77. 组合(相同集合)

leetcode 77. 组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

示例:

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

使用模板进行回溯:
参考 代码随想录 的讲解

/**
 * 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().
 */
#define MAX_SIZE 10001

void backtracking(int n, int k, int start, int* path, int pathSize, int** res, int* returnSize, int** returnColumnSizes) {
    if (pathSize == k) {
        res[*returnSize] = (int*)malloc(sizeof(int) * k);
        memcpy(res[*returnSize], path, sizeof(int) * k);
        (*returnColumnSizes)[(*returnSize)++] = k;
        return;
    }
    /* 就是解法1的剪纸操作,若n = 4,k = 3时,当前路径长度为1的话,那么从3开始就不需要遍历了;
     * 若n = 4,k = 4时,当前路径长度为0的话,那么从2开始就不需要遍历了;
     * 即每一层的start最多可以遍历到 n - (k - pathSize) + 1的位置(加1保证start的起始位置)
     * 
     * 还有这里i从start开始遍历防止1、2;2、1这种重复的集合出现
     */
    for (int i = start; i <= n - (k - pathSize) + 1; i++) {
        path[pathSize] = i;  
        backtracking(n, k, i + 1, path, pathSize + 1, res, returnSize, returnColumnSizes);
    }
}

int** combine(int n, int k, int* returnSize, int** returnColumnSizes){
    *returnSize = 0;
    int** res = (int**)malloc(sizeof(int*) * MAX_SIZE);
    if (n == 0) {
        return res;
    }
    *returnColumnSizes = (int*)calloc(MAX_SIZE, sizeof(int));
    int* path = (int*)calloc(k, sizeof(int));
    backtracking(n, k, 1, path, 0, res, returnSize, returnColumnSizes);
    return res;
}

leetcode 216. 组合总和 III(相同集合)

leetcode 216. 组合总和 III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。

示例 1:

  • 输入: k = 3, n = 7
  • 输出: [[1,2,4]]

此题比较77题就是多了一个路径中元素总和的限制,代码如下:

/**
 * 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().
 */
#define MAX_SZIE 10001

void backtracking(int k, int target, int start, int sum, int* path, int pathSize, int** res, int* returnSize, int** returnColumnSizes) {
    if (sum > target) {
        return;
    }
    if (pathSize == k) {
        if (sum == target) {
            res[*returnSize] = (int*)malloc(sizeof(int) * k);
            memcpy(res[*returnSize], path, sizeof(int) * k);
            (*returnColumnSizes)[(*returnSize)++] = k;
        }
        return;
    }
    for (int i = start; i <= 9 - (k - pathSize) + 1; i++) {
        path[pathSize] = i;
        /* i + 1表示起始节点回溯
         * pathSize + 1表示当前路径回溯
         */
        backtracking(k, target, i + 1, sum + i, path, pathSize + 1, res, returnSize, returnColumnSizes);
    }
}

int** combinationSum3(int k, int n, int* returnSize, int** returnColumnSizes) {
    *returnSize = 0;
    *returnColumnSizes = (int*)malloc(sizeof(int) * MAX_SZIE);
    int** res = (int**)malloc(sizeof(int*) * MAX_SZIE);
    if (n == 0) {
        return res;
    }
    int* path = (int*)malloc(sizeof(int) * k);
    backtracking(k, n, 1, 0, path, 0, res, returnSize, returnColumnSizes);
    return res;
}

leetcode 17. 电话号码的字母组合(不同集合)

leetcode 17. 电话号码的字母组合
在这里插入图片描述
此题解决以下几个问题:

  • 数字和字符进行映射
  • 在不同集合中进行组合,单层遍历的逻辑
  • 输入1 * # 等异常情况

解决:

  • 使用二维数组进行数字和字符的映射
  • 在不同集合中,单层逻辑是 i 每次从0开始
  • 输入异常时,直接返回(但是leetcode的测试集中没有异常输入,就没有处理)

代码如下:

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
char phoneMap[9][5] = {"", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; // 1 不对应任何字母

void backtracking(char* digits, int digitsSize, int index, int pathSize, char* path, char** res, int* returnSize) {
    if (index == digitsSize) {  /* 终止条件,当前路径字符个数达到要求 */
        /* 返回数组分配空间 */
        res[*returnSize] = (char*)malloc(sizeof(char) * (pathSize + 1));
        /* 将当前这一条路径,copy到返回数组中 */
        strcpy(res[*returnSize], path);
        /* 返回数组的行数加一 */
        (*returnSize)++;
    } else {    /* 未达到终止条件 */
        /* 将输入数组的当前字符所代表的字符串取出、并记录其长度lettersSize */
        char* letters = phoneMap[digits[index] - '1'];
        int lettersSize = strlen(letters);
        /* 在当前层进行选择,挨个进行回溯递归遍历 */
        for (int i = 0; i < lettersSize; i++) {
            path[pathSize++] = letters[i];
            path[pathSize] = '\0';
            /* index + 1直接在参数中回溯了
             * pathSize在backtracking函数前后进行回溯
             */
            backtracking(digits, digitsSize, index + 1, pathSize, path, res, returnSize);
            path[--pathSize] = '\0';
        }
    }
}

char ** letterCombinations(char * digits, int* returnSize) {
    /* 特判、输入字符串为空,返回NULL */
    int digitsSize = strlen(digits);
    *returnSize = 0;
    if (digitsSize == 0) {
        return NULL;
    }
    /* 每一位按键最多4个字符,所以返回数组最多4^digitsSize行、每行4个字符 */
    int maxResSize = (int)pow(4, digitsSize);   /* 4 ^ digitsSize */
    char** res = (char**)malloc(sizeof(char*) * maxResSize);
    /* 记录当前路径、每条路径digitsSize个字符加一个'\0' */
    char* path = (char*)malloc(sizeof(char) * (digitsSize + 1));
    backtracking(digits, digitsSize, 0, 0, path, res, returnSize);
    return res;
}

leetcode 39. 组合总和(同一集合,元素不重复,元素任意取多次)

leetcode 39. 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。

说明:

  • 所有数字(包括 target)都是正整数。
    解集不能包含重复的组合。

示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:

[
  [7],
  [2,2,3]
]

此题是在一个集合中组合、集合中的元素可以取任意次。
那么递归终止条件为:sum == target
单层逻辑,i 从 index开始,而且进入下一层递归index不需要 i + 1,传入 i 即可。

代码如下:

/**
 * 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 cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}
void backtracking(int* candidates, int candidatesSize, int sum, int target, int index, int* path, int pathSize, 
                  int** res, int* returnSize, int** returnColumnSizes) {
    if (sum == target) {
        res[*returnSize] = (int*)malloc(sizeof(int) * pathSize);
        memcpy(res[*returnSize], path, sizeof(int) * pathSize);
        (*returnColumnSizes)[*returnSize] = pathSize;
        (*returnSize)++;
    } else {
        for (int i = index; i < candidatesSize && sum + candidates[i] <= target; i++) {
            path[pathSize] = candidates[i];
            backtracking(candidates, candidatesSize, sum + candidates[i], target, i,  
                         path, pathSize + 1, res, returnSize, returnColumnSizes);
        }
    }
}

int** combinationSum(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes) {
    int** res = (int**)malloc(sizeof(int*) * 10001);
    int* path = (int*)malloc(sizeof(int) * 10001);
    *returnColumnSizes = (int*)malloc(sizeof(int) * 10001);
    *returnSize = 0;
    qsort(candidates, candidatesSize, sizeof(int), cmp);
    backtracking(candidates, candidatesSize, 0, target, 0, path, 0, res, returnSize, returnColumnSizes);
    return res;
}

leetcode 40. 组合总和 II(同一集合,元素重复,每个组合中每个元素只使用一次,组合不重复)

在这里插入图片描述
要去重的是“同一树层上的使用过”,如果判断同一树层上元素(相同的元素)是否使用过了呢。

如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。

此时for循环里就应该做continue的操作。

如图:
在这里插入图片描述

我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:

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

在同一树枝下,若candidates[i] == candidates[i - 1]时;used[i - 1] == true 说明 candidates[i] 之前使用过,如 1、2(1)、2(2)、3中组合为:1、2(1)、2(2)、3

在同一数层下,若candidates[i] == candidates[i - 1]时;used[i - 1] == false 说明 candidates[i] 之前使用过,如 1、2(1)、2(2)、3中组合为:1、2(1);1、2(2)

代码如下:

/**
 * 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 cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

void backtracking(int* candidates, int candidatesSize, int sum, int target, int startIndex, int* path, int pathSize, 
                  int** res, int* returnSize, int** returnColumnSizes, bool* used) {
    if (sum == target) {
        res[*returnSize] = (int*)malloc(sizeof(int) * pathSize);
        memcpy(res[*returnSize], path, sizeof(int) * pathSize);
        (*returnColumnSizes)[*returnSize] = pathSize;
        (*returnSize)++;
    } else {
        for (int i = startIndex; i < candidatesSize && sum + candidates[i] <= target; i++) {
            if (i > 0 && candidates[i] == candidates[ i - 1] && used[i - 1] == false) {
                continue;
            }
            used[i] = true;
            path[pathSize] = candidates[i];
            backtracking(candidates, candidatesSize, sum + candidates[i], target, i + 1,  
                         path, pathSize + 1, res, returnSize, returnColumnSizes, used);
            used[i] = false;
        }
    }
}

int** combinationSum2(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes) {
    int** res = (int**)malloc(sizeof(int*) * 10001);
    int* path = (int*)malloc(sizeof(int) * 10001);
    bool* used = (bool*)calloc(10001, sizeof(bool));
    *returnColumnSizes = (int*)malloc(sizeof(int) * 10001);
    *returnSize = 0;
    qsort(candidates, candidatesSize, sizeof(int), cmp);
    backtracking(candidates, candidatesSize, 0, target, 0, path, 0, res, returnSize, returnColumnSizes,used);
    return res;
}

3、切割问题

leetcode 131. 分割回文串(三级指针表示返回值,判断回文串)

leetcode 131. 分割回文串
在这里插入图片描述

  • 切割完之后要存储之前个字串,使用二维path数组存储
  • 判断回文串,使用双指针左右向中间遍历的方法
  • 当startIndex走到sLen时表示切割完成,记录一个结果
  • 还有C语言中,返回的是一个三级指针,需要正确对其内存分配

代码如下:

/**
 * 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().
 */
bool isPalindrome(char* s, int left, int right) {
    for (int i = left, j = right; i < j; i++, j--) {
        if (s[i] != s[j]) {
            return false;
        }
    }
    return true;
}

void backtracking(char* s, int sLen, char*** res, int* returnSize, int** returnColumnSizes, 
                  char path[][100], int pathSize, int startIndex) {
    if (startIndex == sLen) {
        res[*returnSize] = (char**)malloc(sizeof(char*) * pathSize);
        (*returnColumnSizes)[*returnSize] = pathSize;
        for (int i = 0; i < pathSize; i++) {
            res[*returnSize][i] = (char*)malloc(sizeof(char) * (strlen(path[i]) + 1));
            //strncpy(res[*returnSize][i] , path[i], sizeof(char) * (strlen(path[i]) + 1));
            memcpy(res[*returnSize][i], path[i], sizeof(char) * (strlen(path[i]) + 1));
        }
        (*returnSize)++;
    } else {
        for (int i = startIndex; i < sLen; i++) {
            if (!isPalindrome(s, startIndex, i)) {
                continue;
            } else {
            	memcpy(path[pathSize++], s + startIndex, sizeof(char) * (i - startIndex + 1));
                //strncpy(path[pathSize++], s + startIndex, i - startIndex + 1);
                backtracking(s, sLen, res, returnSize, returnColumnSizes, path, pathSize, i + 1);
                memset(path[--pathSize], 0, 100);
            }
        }
    }
}

char*** partition(char* s, int* returnSize, int** returnColumnSizes) {
    *returnSize = 0;
    *returnColumnSizes = (int*)malloc(sizeof(int) * 100001);
    char*** res = (char***)malloc(sizeof(char**) * 100001);
    char path[1000][100];
    int sLen = strlen(s);
    backtracking(s, sLen, res, returnSize, returnColumnSizes, path, 0, 0);
    return res;
}

leetcode 93. 复原 IP 地址(131的加强版)

leetcode 93. 复原 IP 地址
在这里插入图片描述

class Solution {
private:
    /* 返回数组 */
    vector<string> res;
    void backtracking(string& s, int startIndex, int pointNum) {
        /* 当前点数位3,需要进一步判断第四部分的地址值 */
        if (pointNum == 3) {
            /* 判断第四部分是否合法 */
            if (isValid(s, startIndex, s.size() - 1)) {
                /* 将当前路径加入返回数组 */
                res.push_back(s);
            }
            return;
        }
        /* 遍历当前层 */
        for (int i = startIndex; i < s.size(); i++) {
            /* 判断当前部分是否合法 */
            if (isValid(s, startIndex, i)) {
                /* 将分割点插入s中 */
                s.insert(s.begin() + i + 1, '.');
                /* 开始下一层递归 */
                backtracking(s, i + 2, pointNum + 1);
                /* 回溯,将当前加入的点删除 */
                s.erase(s.begin() + i + 1);
            } else {
                break;
            }
        }
    }
    bool isValid(const string& s, int start, int end) {
        /* 越界特判 */
        if (start > end) {
            return false;
        }
        if (s[start] == '0' && start != end) { // 0开头的数字不合法
                return false;
        }
        int num = 0;
        for (int i = start; i <= end; i++) {
            if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法
                return false;
            }
            num = num * 10 + (s[i] - '0');
            if (num > 255) { // 如果大于255了不合法
                return false;
            }
        }
        return true;
    }
public:
    vector<string> restoreIpAddresses(string s) {
        res.clear();
        if (s.size() > 12) {
            return res;
        }
        backtracking(s, 0, 0);
        return res;
    }
};

4、子集问题

子集问题是取所有节点,其他问题是取叶子节点

如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点,既遍历整棵树!

其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。

那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!

leetcode 78. 子集(无重复元素)

leetcode 78. 子集
在这里插入图片描述
元素不重复,每一个集合中每个元素只能使用一次,和排列很像;
代码如下:

/**
 * 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().
 */
void backtracking(int* nums, int numsSize, int** res, int* returnSize, int** returnColumnSizes, 
                  int* path, int pathSize, int startIndex) {
    res[*returnSize] = (int*)malloc(sizeof(int) * pathSize);
    memcpy(res[*returnSize], path, sizeof(int) * pathSize);
    (*returnColumnSizes)[*returnSize] = pathSize;
    (*returnSize)++;
    for (int i = startIndex; i < numsSize; i++) {
        path[pathSize] = nums[i];
        backtracking(nums, numsSize, res, returnSize, returnColumnSizes, path, pathSize + 1, i + 1);
    }
}

int** subsets(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
    *returnSize = 0;
    *returnColumnSizes = (int*)malloc(sizeof(int) * 10001);
    int** res = (int**)malloc(sizeof(int*) * 10001);
    int* path = (int*)malloc(sizeof(int) * numsSize);
    backtracking(nums, numsSize, res, returnSize, returnColumnSizes, path, 0, 0);
    return res;
}

leetcode 90. 子集 II(有重复元素)

leetcode 90. 子集 II
在这里插入图片描述
代码如下:

/**
 * 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 cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}
void backtracking(int* nums, int numsSize, int** res, int* returnSize, int** returnColumnSizes, 
                  int* path, int pathSize, int startIndex, bool* used) {
    res[*returnSize] = (int*)malloc(sizeof(int) * pathSize);
    memcpy(res[*returnSize], path, sizeof(int) * pathSize);
    (*returnColumnSizes)[*returnSize] = pathSize;
    (*returnSize)++;
    for (int i = startIndex; i < numsSize; i++) {
        if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
            continue;
        }
        path[pathSize] = nums[i];
        used[i] = true;
        backtracking(nums, numsSize, res, returnSize, returnColumnSizes, path, pathSize + 1, i + 1, used);
        used[i] = false;
    }
}

int** subsetsWithDup(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
    *returnSize = 0;
    *returnColumnSizes = (int*)malloc(sizeof(int) * 10001);
    int** res = (int**)malloc(sizeof(int*) * 10001);
    int* path = (int*)malloc(sizeof(int) * numsSize);
    bool* used = (bool*)malloc(sizeof(bool) * numsSize);
    qsort(nums, numsSize, sizeof(int), cmp);
    backtracking(nums, numsSize, res, returnSize, returnColumnSizes, path, 0, 0, used);
    return res;
}

leetcode 491. 递增子序列(元素重复、无序)

leetcode 491. 递增子序列
在这里插入图片描述
C语言代码如下:
因为此题输入元素在-100到100之间,使用加100这个操作,使得used中的下表落在0到200之间,这样就可以记录当前层元素的使用情况。

/**
 * 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().
 */
void backtracking(int* nums, int numsSize, int** res, int* returnSize, int** returnColumnSizes, 
                  int* path, int pathSize, int startIndex) {
    /* 终止条件,元素个数大于等于2个 */
    if (pathSize >= 2) {
        /* 为当前返回值分配空间 */
        res[*returnSize] = (int*)malloc(sizeof(int) * pathSize);
        /* 赋值 */
        memcpy(res[*returnSize], path, sizeof(int) * pathSize);
        /* 当前返回值的列数 */
        (*returnColumnSizes)[*returnSize] = pathSize;
        /* 返回行数加一 */
        (*returnSize)++;
    }
    /* 当前层使用过的元素,每一层都有会有一个这样的used数组 */
    int* used = (int*)calloc(201, sizeof(int));
    for (int i = startIndex; i < numsSize; i++) {
        /* 当前路径中的当前元素小于上一个元素、或者当前中当前元素之前以及使用过 */
        if ((pathSize > 0 && nums[i] < path[pathSize - 1]) || used[nums[i] + 100] == 1) {
            continue;
        }
        /* 记录当路径数组中 */
        path[pathSize] = nums[i];
        /* 因为此题输入元素在-100到100之间,使用加100这个操作,
         * 使得used中的下表落在0到200之间,这样就可以记录当前层元素的使用情况
         */
        used[nums[i] + 100] = 1;
        backtracking(nums, numsSize, res, returnSize, returnColumnSizes, path, pathSize + 1, i + 1);
    }
    free(used);
    used = NULL;
}

int** findSubsequences(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
    /* 返回数组行数 */
    *returnSize = 0;
    /* 特判,递增子序列最少需要两个元素 */
    if (numsSize < 2) {
        return NULL;
    }
    /* 返回数组中每一行的列数 */
    *returnColumnSizes = (int*)malloc(sizeof(int) * 100001);
    /* 返回数组 */
    int** res = (int**)malloc(sizeof(int*) * 100001);
    /* 当前路径数组 */
    int* path = (int*)malloc(sizeof(int) * numsSize);
    backtracking(nums, numsSize, res, returnSize, returnColumnSizes, path, 0, 0);
    free(path);
    path = NULL;
    return res;
}

C++代码如下:参考 代码随想录 的讲解
使用set定义used数组能够处理更多数据。

// 版本一
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex) {
        if (path.size() > 1) {
            result.push_back(path);
            // 注意这里不要加return,要取树上的节点
        }
        unordered_set<int> uset; // 使用set对本层元素进行去重
        for (int i = startIndex; i < nums.size(); i++) {
            if ((!path.empty() && nums[i] < path.back())
                    || uset.find(nums[i]) != uset.end()) {
                    continue;
            }
            uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }
};

5、排列问题

leetcode 46. 全排列(无重复元素)

leetcode 46. 全排列

  • 全排列单层搜素逻辑是 i 从0开始,但是也需要使用used数组进行去重;
  • 排列中1、2和2、1是两个集合,但是每一层搜索时前面的元素使用过了,就不能在使用了;
  • 比如1,2:排列为:1、2和2、1,但是不去重的话,就会是
    1、1;
    1、2;
    2、1;
    2、2

代码如下:

/**
 * 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().
 */

void backtracking(int* nums, int numsSize, int** res, int* returnSize, 
                  int** returnColumnSizes, int* path, int pathSize, int* used) {
    if (pathSize == numsSize) {
        res[*returnSize] = (int*)malloc(sizeof(int) * pathSize);
        memcpy(res[*returnSize], path, sizeof(int) * pathSize);
        (*returnColumnSizes)[*returnSize] = numsSize;
        (*returnSize)++;
        return;
    }
    for (int i = 0; i < numsSize; i++) {
        if (used[i] == 1) {
            continue;
        }
        path[pathSize] = nums[i];
        used[i] = 1;
        backtracking(nums, numsSize, res, returnSize, returnColumnSizes, path, pathSize + 1, used);
        used[i] = 0;
    }
}

int** permute(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
    *returnSize = 0;
    *returnColumnSizes = (int*)malloc(sizeof(int) * 100001);
    int**res = (int**)malloc(sizeof(int*) * 100001);
    int* path = (int*)malloc(sizeof(int) * numsSize);
    int* used = (int*)malloc(sizeof(int) * numsSize);
    backtracking(nums, numsSize, res, returnSize, returnColumnSizes, path, 0, used);
    return res;
}

leetcode 47. 全排列 II(有重复元素)

leetcode 47. 全排列 II
在这里插入图片描述

去重代码:
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
    continue;
}
如果改成 used[i - 1] == true, 也是正确的!,去重代码如下:
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
    continue;
}
如果要对树层中前一位去重,就用used[i - 1] == false,如果要对树枝前一位去重用used[i - 1] == true。
对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!

输入: [1,1,1] 来举一个例子。
树层上去重(used[i - 1] == false),的树形结构如下:
在这里插入图片描述
树枝上去重(used[i - 1] == true)的树型结构如下:
在这里插入图片描述
树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重最后可以得到答案,但做了很多无用搜索

/**
 * 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 cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}
void backtracking(int* nums, int numsSize, int** res, int* returnSize, 
                  int** returnColumnSizes, int* path, int pathSize, int* used) {
    if (pathSize == numsSize) {
        res[*returnSize] = (int*)malloc(sizeof(int) * pathSize);
        memcpy(res[*returnSize], path, sizeof(int) * pathSize);
        (*returnColumnSizes)[*returnSize] = numsSize;
        (*returnSize)++;
        return;
    }
    for (int i = 0; i < numsSize; i++) {
        if (i > 0 && nums[i - 1] == nums[i] && used[i - 1] == 0) {
            continue;
        }
        if (used[i] == 0) {
            path[pathSize] = nums[i];
            used[i] = 1;
            backtracking(nums, numsSize, res, returnSize, returnColumnSizes, path, pathSize + 1, used);
            used[i] = 0;
        }
    }
}

int** permuteUnique(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
    *returnSize = 0;
    *returnColumnSizes = (int*)malloc(sizeof(int) * 100001);
    int**res = (int**)malloc(sizeof(int*) * 100001);
    int* path = (int*)malloc(sizeof(int) * numsSize);
    int* used = (int*)calloc(numsSize, sizeof(int));
    qsort(nums, numsSize, sizeof(int), cmp);
    backtracking(nums, numsSize, res, returnSize, returnColumnSizes, path, 0, used);
    return res;
}

leetcode 332. 重新安排行程

leetcode 332. 重新安排行程
在这里插入图片描述
参考 代码随想录 的讲解
这道题目有几个难点:

  • 一个行程中,如果航班处理不好容易变成一个圈,成为死循环
  • 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?
  • 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?
  • 搜索的过程中,如何遍历一个机场所对应的所有机场。

代码如下:

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
char** res;
bool* used;
int g_isFound;

int cmp(const void* str1, const void* str2) {
    const char** tmp1 = *(char**)str1;
    const char** tmp2 = *(char**)str2;
    int res = strcmp(tmp1[0], tmp2[0]);
    if (res == 0) {
        return strcmp(tmp1[1], tmp2[1]);
    }
    return res;
}

void  backtracking(char*** tickets, int ticketsSize, char* start, char** res, int* returnSize, bool* used) {
    if (*returnSize == ticketsSize + 1) {
        g_isFound = 1;
        return;
    }
    for (int i = 0; i < ticketsSize; i++) {
    	/* 这里的used还是在树枝上去重 */
        if (used[i] == false && (strcmp(start, tickets[i][0]) == 0)) {
            res[*returnSize] = (char*)malloc(sizeof(char) * 4);
            memcpy(res[*returnSize], tickets[i][1], sizeof(char) * 4);
            (*returnSize)++;
            used[i] = true;
            backtracking(tickets, ticketsSize, tickets[i][1], res, returnSize, used);
            if (g_isFound) {
                return;
            }
            used[i] = false;
            //(*returnSize)--; //返回数组长度不需要回溯,因为肯定可以找到一个结果,并且找到后就返回了
        }
    }
}

char** findItinerary(char*** tickets, int ticketsSize, int* ticketsColSize, int* returnSize) {
    *returnSize = 0;
    if (!tickets || ticketsSize == 0) {
        return NULL;
    }

    used = (bool*)malloc(sizeof(bool) * ticketsSize);
    memset(used, false, sizeof(bool) * ticketsSize);

    res = (char**)malloc(sizeof(char*) * (ticketsSize + 1));    /* 返回数组中机场的数量就是机票数量加一 */
    res[(*returnSize)++] = (char*)malloc(sizeof(char) * 4);
    memcpy(res[0], "JFK", sizeof(char) * 4);

    g_isFound = 0;
    qsort(tickets, ticketsSize, sizeof(tickets[0]), cmp);   /* 有相同的出发机场,按照字符大小升序排序 */
    backtracking(tickets, ticketsSize, "JFK", res, returnSize, used);
    return res;
}

6、棋盘问题

leetcode 第51题. N皇后

leetcode 第51题. N皇后
在这里插入图片描述
具体看代码:

/**
 * 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().
 */
bool isValid(int n, int* path, int row, int col) {
    for (int i = 0; i < row; i++) { /* 同一列 */
        if (path[i] == col) {
            return false;
        }
    }
    for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {    /* 左上角 */
        if (path[i] == j) {
            return false;
        }
    }
    for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { /* 右上角 */
        if (path[i] == j) {
            return false;
        }
    }
    return true;
}

void backtracking(int n, int row, char*** res, int* returnSize, int** returnColumnSizes, int* path) {
    if (row == n) {
        res[*returnSize] = (char**)malloc(sizeof(char*) * n);
        for (int i = 0; i < n; i++) {
            res[*returnSize][i] = (char*)calloc(n + 1, sizeof(char));
            memset(res[*returnSize][i], '.', n);
            res[*returnSize][i][path[i]] = 'Q';
        }
        (*returnColumnSizes)[*returnSize] = n;
        (*returnSize)++;
        return;
    }
    for (int j = 0; j < n; j++) {
        if (!isValid(n, path, row, j)) {
            continue;
        }
        path[row] = j;
        backtracking(n, row + 1, res, returnSize, returnColumnSizes, path);
    }
}

char*** solveNQueens(int n, int* returnSize, int** returnColumnSizes) {
    *returnSize = 0;
    *returnColumnSizes = (int*)malloc(sizeof(int) * n * n * n);
    char*** res = (char***)malloc(sizeof(char**) * n * n * n);
    int* path = (int*)calloc(n, sizeof(int));
    backtracking(n, 0, res, returnSize, returnColumnSizes, path);
    return res;
}

leetcode 37. 解数独

leetcode 37. 解数独
A
在这里插入图片描述
此题单层逻辑更像排列,每一次都是从头开始遍历,具体看代码:

bool isValid(char** board, int row, int col, int k) {
    /* 判断当前行是否有重复元素 */
    for (int i = 0; i < 9; i++) {
        if (board[i][col] == k) {
            return false;
        }
    }
    /* 判断当前列是否有重复元素 */
    for (int j = 0; j < 9; j++) {
        if (board[row][j] == k) {
            return false;
        }
    }
    /* 计算当前9宫格左上角的位置 */
    int startRow = (row / 3) * 3;
    int startCol = (col / 3) * 3;
    /* 判断当前元素所在九宫格是否有重复元素 */
    for (int i = startRow; i < startRow + 3; i++) {
        for (int j = startCol; j < startCol + 3; j++) {
            if (board[i][j] == k) {
                return false;
            }
        }
    }
    /* 满足条件,返回true */
    return true;
}

bool backtracking(char** board, int boardSize, int* boardColSize) {
    /* 从上到下、从左到右依次遍历输入数组 */
    for (int i = 0; i < boardSize; i++) {
        for (int j = 0; j < *boardColSize; j++) {
            /* 遇到数字跳过 */
            if (board[i][j] != '.') {
                continue;
            }
            /* 依次将数组1到9填入当前位置 */
            for (int k = '1'; k <= '9'; k++) {
                /* 判断当前位置是否与满足条件,是则进入下一层 */
                if (isValid(board, i, j, k)) {
                    board[i][j] = k;
                    /* 判断下一层递归之后是否找到一种解法,是则返回true */
                    if (backtracking(board, boardSize, boardColSize)) {
                        return true;
                    }
                    /* 回溯,将当前位置清零 */
                    board[i][j] = '.';
                }
            }
            /* 若填入的9个数均不满足条件,返回false,说明此解法无效 */
            return false;
        }
    }
    /* 遍历完所有的棋盘,没有返回false,说明找到了解法,返回true */
    return true;
}

void solveSudoku(char** board, int boardSize, int* boardColSize) {
    bool res = backtracking(board, boardSize, boardColSize);
}

上一节:7、二叉树相关

下一节:9、贪心算法相关

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

「已注销」

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值