当前位置:   article > 正文

回溯算法(四)排列问题_回溯法排列问题

回溯法排列问题

目录

一.全排列(一)

1)问题描述

2)思路

 3)回溯三部曲

 4)代码

 二.全排列(二)

 1)问题描述

2)思路

3)代码

4)拓展

三.重新安排行程(难)

1)题目描述

2)思路 

 3)回溯三部曲

4)代码


一.全排列(一)

1)问题描述

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

  • 输入: [1,2,3]
  • 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

2)思路

暴力搜索

我以[1,2,3]为例,抽象成树形结构如下:

 3)回溯三部曲

  • 递归函数参数

首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方

可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。

但排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示:

 

代码如下:

  1. vector<vector<int>> result;
  2. vector<int> path;
  3. void backtracking (vector<int>& nums, vector<bool>& used)
  • 递归终止条件

 

 

代码如下:

  1. vector<vector<int>> result;
  2. vector<int> path;
  3. void backtracking (vector<int>& nums, vector<bool>& used)
  • 递归终止条件

 

可以看出叶子节点,就是收割结果的地方。

那么什么时候,算是到达叶子节点呢?

当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。

代码如下:

  1. // 此时说明找到了一组
  2. if (path.size() == nums.size()) {
  3. result.push_back(path);
  4. return;
  5. }
  • 单层搜索的逻辑

这里和前面问题不同的是, for循环里不用startIndex了

 因为排列问题,每次都要从头开始搜索,例如元素1在[1,2]中已经使用过了,但是在[2,1]中还要再使用一次1。

而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次

代码如下:

  1. for (int i = 0; i < nums.size(); i++) {
  2. if (used[i] == true) continue; // path里已经收录的元素,直接跳过
  3. used[i] = true;
  4. path.push_back(nums[i]);
  5. backtracking(nums, used);
  6. path.pop_back();
  7. used[i] = false;
  8. }

 4)代码

  1. int* path;
  2. int pathTop;
  3. int** ans;
  4. int ansTop;
  5. //将used中元素都设置为0
  6. void initialize(int* used, int usedLength) {
  7. int i;
  8. for(i = 0; i < usedLength; i++) {
  9. used[i] = 0;
  10. }
  11. }
  12. //将path中元素拷贝到ans中
  13. void copy() {
  14. int* tempPath = (int*)malloc(sizeof(int) * pathTop);
  15. int i;
  16. for(i = 0; i < pathTop; i++) {
  17. tempPath[i] = path[i];
  18. }
  19. ans[ansTop++] = tempPath;
  20. }
  21. void backTracking(int* nums, int numsSize, int* used) {
  22. //若path中元素个数等于nums元素个数,将nums放入ans中
  23. if(pathTop == numsSize) {
  24. copy();
  25. return;
  26. }
  27. int i;
  28. for(i = 0; i < numsSize; i++) {
  29. //若当前下标中元素已使用过,则跳过当前元素
  30. if(used[i])
  31. continue;
  32. used[i] = 1;
  33. path[pathTop++] = nums[i];
  34. backTracking(nums, numsSize, used);
  35. //回溯
  36. pathTop--;
  37. used[i] = 0;
  38. }
  39. }
  40. int** permute(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
  41. //初始化辅助变量
  42. path = (int*)malloc(sizeof(int) * numsSize);
  43. ans = (int**)malloc(sizeof(int*) * 1000);
  44. int* used = (int*)malloc(sizeof(int) * numsSize);
  45. //将used数组中元素都置0
  46. initialize(used, numsSize);
  47. ansTop = pathTop = 0;
  48. backTracking(nums, numsSize, used);
  49. //设置path和ans数组的长度
  50. *returnSize = ansTop;
  51. *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop);
  52. int i;
  53. for(i = 0; i < ansTop; i++) {
  54. (*returnColumnSizes)[i] = numsSize;
  55. }
  56. return ans;
  57. }

 二.全排列(二)

 1)问题描述

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

示例 1:

  • 输入:nums = [1,1,2]
  • 输出: [[1,1,2], [1,2,1], [2,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]]

提示:

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

2)思路

这题与全排列(一)的区别是给定一个可包含重复数字的序列,要返回所有不重复的全排列

这里又涉及到去重了

还要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了

我以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图:

 

图中我们对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重。

一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果

3)代码

  1. //临时数组
  2. int *path;
  3. //返回数组
  4. int **ans;
  5. int *used;
  6. int pathTop, ansTop;
  7. //拷贝path到ans中
  8. void copyPath() {
  9. int *tempPath = (int*)malloc(sizeof(int) * pathTop);
  10. int i;
  11. for(i = 0; i < pathTop; ++i) {
  12. tempPath[i] = path[i];
  13. }
  14. ans[ansTop++] = tempPath;
  15. }
  16. void backTracking(int* used, int *nums, int numsSize) {
  17. //若path中元素个数等于numsSize,将path拷贝入ans数组中
  18. if(pathTop == numsSize)
  19. copyPath();
  20. int i;
  21. for(i = 0; i < numsSize; i++) {
  22. //若当前元素已被使用
  23. //或前一位元素与当前元素值相同但并未被使用
  24. //则跳过此分支
  25. if(used[i] || (i != 0 && nums[i] == nums[i-1] && used[i-1] == 0))
  26. continue;
  27. //将当前元素的使用情况设为True
  28. used[i] = 1;
  29. path[pathTop++] = nums[i];
  30. backTracking(used, nums, numsSize);
  31. used[i] = 0;
  32. --pathTop;
  33. }
  34. }
  35. int cmp(void* elem1, void* elem2) {
  36. return *((int*)elem1) - *((int*)elem2);
  37. }
  38. int** permuteUnique(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
  39. //排序数组
  40. qsort(nums, numsSize, sizeof(int), cmp);
  41. //初始化辅助变量
  42. pathTop = ansTop = 0;
  43. path = (int*)malloc(sizeof(int) * numsSize);
  44. ans = (int**)malloc(sizeof(int*) * 1000);
  45. //初始化used辅助数组
  46. used = (int*)malloc(sizeof(int) * numsSize);
  47. int i;
  48. for(i = 0; i < numsSize; i++) {
  49. used[i] = 0;
  50. }
  51. backTracking(used, nums, numsSize);
  52. //设置返回的数组的长度
  53. *returnSize = ansTop;
  54. *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop);
  55. int z;
  56. for(z = 0; z < ansTop; z++) {
  57. (*returnColumnSizes)[z] = numsSize;
  58. }
  59. return ans;
  60. }

4)拓展

去重最关键的代码:

 if(used[i] || (i != 0 && nums[i] == nums[i-1] && used[i-1] == 0))

 如果改成 used[i - 1] == 1, 也是正确的!

 if(used[i] || (i != 0 && nums[i] == nums[i-1] && used[i-1] == 1))

这是为什么呢,就是上面我刚说的,如果要对树层中前一位去重,就用used[i - 1] == false,如果要对树枝前一位去重用used[i - 1] == true。 

对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!

用输入: [1,1,1] 来举一个例子。

树层上去重(used[i - 1] == false),的树形结构如下:

 树枝上去重(used[i - 1] == true)的树型结构如下:

 大家应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。

三.重新安排行程(难)

1)题目描述

给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。

提示:

  • 如果存在多种有效的行程,请你按字符自然排序返回最小的行程组合。例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前
  • 所有的机场都用三个大写字母表示(机场代码)。
  • 假定所有机票至少存在一种合理的行程。
  • 所有的机票必须都用一次 且 只能用一次。

示例 1:

  • 输入:[["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
  • 输出:["JFK", "MUC", "LHR", "SFO", "SJC"]

示例 2:

  • 输入:[["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
  • 输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
  • 解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。

2)思路 

直觉上来看 这道题和回溯法没有什么关系,更像是图论中的深度优先搜索。

实际上确实是深搜,但这是深搜中使用了回溯的例子,在查找路径的时候,如果不回溯,怎么能查到目标路径呢。

这道题目有几个难点:

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

 

1)如何理解死循环

对于死循环,我来举一个有重复机场的例子:

 出发机场和到达机场也会重复的,如果在解题的过程中没有对集合元素处理好,就会死循环。

 2)该记录映射关系

有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?

一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset。

如果对map 和 set 的实现机制不太了解,也不清楚为什么 map、multimap就是有序的同学,可以看看哈希表

这样存放映射关系可以定义为 unordered_map<string, multiset<string>> targets 或者 unordered_map<string, map<string, int>> targets

含义如下:

unordered_map<string, multiset> targets:unordered_map<出发机场, 到达机场的集合> targets

unordered_map<string, map<string, int>> targets:unordered_map<出发机场, map<到达机场, 航班次数>> targets

这两个结构,我选择了后者,因为如果使用unordered_map<string, multiset<string>> targets 遍历multiset的时候,不能删除元素,一旦删除元素,迭代器就失效了。

再说一下为什么一定要增删元素呢,正如开篇我给出的图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环。

所以搜索的过程中就是要不断的删multiset里的元素,那么推荐使用unordered_map<string, map<string, int>> targets

在遍历 unordered_map<出发机场, map<到达机场, 航班次数>> targets的过程中,可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。

如果“航班次数”大于零,说明目的地还可以飞,如果如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。

相当于说我不删,我就做一个标记!

本题以输入:[["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]为例,抽象为树形结构如下:

 3)回溯三部曲

  • 递归函数参数

在讲解映射关系的时候,已经讲过了,使用unordered_map<string, map<string, int>> targets; 来记录航班的映射关系,我定义为全局变量。

当然把参数放进函数里传进去也是可以的,我是尽量控制函数里参数的长度。

参数里还需要ticketNum,表示有多少个航班(终止条件会用上)。

代码如下:

  1. // unordered_map<出发机场, map<到达机场, 航班次数>> targets
  2. unordered_map<string, map<string, int>> targets;
  3. bool backtracking(int ticketNum, vector<string>& result) {

注意函数返回值我用的是bool!

我们之前讲解回溯算法的时候,一般函数返回值都是void,这次为什么是bool呢?

因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线,如图:

 

 所以找到了这个叶子节点了直接返回

当然本题的targets和result都需要初始化,代码如下:

  1. for (const vector<string>& vec : tickets) {
  2. targets[vec[0]][vec[1]]++; // 记录映射关系
  3. }
  4. result.push_back("JFK"); // 起始机场

 

  • 递归终止条件

拿题目中的示例为例,输入: [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] ,这是有4个航班,那么只要找出一种行程,行程里的机场个数是5就可以了。

所以终止条件是:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了。

代码如下:

  1. if (result.size() == ticketNum + 1) {
  2. return true;
  3. }

已经看习惯回溯法代码的同学,到叶子节点了习惯性的想要收集结果,但发现并不需要,本题的result相当于组合求和中的path,也就是本题的result就是记录路径的(就一条),在如下单层搜索的逻辑中result就添加元素了。

  • 单层搜索的逻辑

回溯的过程中,如何遍历一个机场所对应的所有机场呢?

这里刚刚说过,在选择映射函数的时候,不能选择unordered_map<string, multiset<string>> targets, 因为一旦有元素增删multiset的迭代器就会失效,当然可能有牛逼的容器删除元素迭代器不会失效,这里就不在讨论了。

可以说本题既要找到一个对数据进行排序的容器,而且还要容易增删元素,迭代器还不能失效

所以我选择了unordered_map<string, map<string, int>> targets 来做机场之间的映射。

遍历过程如下:

  1. for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
  2. if (target.second > 0 ) { // 记录到达机场是否飞过了
  3. result.push_back(target.first);
  4. target.second--;
  5. if (backtracking(ticketNum, result)) return true;
  6. result.pop_back();
  7. target.second++;
  8. }
  9. }

可以看出 通过unordered_map<string, map<string, int>> targets里的int字段来判断 这个集合里的机场是否使用过,这样避免了直接去删元素。

4)代码

  1. char **result;
  2. bool *used;
  3. int g_found;
  4. int cmp(const void *str1, const void *str2)
  5. {
  6. const char **tmp1 = *(char**)str1;
  7. const char **tmp2 = *(char**)str2;
  8. int ret = strcmp(tmp1[0], tmp2[0]);
  9. if (ret == 0) {
  10. return strcmp(tmp1[1], tmp2[1]);
  11. }
  12. return ret;
  13. }
  14. void backtracting(char *** tickets, int ticketsSize, int* returnSize, char *start, char **result, bool *used)
  15. {
  16. if (*returnSize == ticketsSize + 1) {
  17. g_found = 1;
  18. return;
  19. }
  20. for (int i = 0; i < ticketsSize; i++) {
  21. if ((used[i] == false) && (strcmp(start, tickets[i][0]) == 0)) {
  22. result[*returnSize] = (char*)malloc(sizeof(char) * 4);
  23. memcpy(result[*returnSize], tickets[i][1], sizeof(char) * 4);
  24. (*returnSize)++;
  25. used[i] = true;
  26. /*if ((*returnSize) == ticketsSize + 1) {
  27. return;
  28. }*/
  29. backtracting(tickets, ticketsSize, returnSize, tickets[i][1], result, used);
  30. if (g_found) {
  31. return;
  32. }
  33. (*returnSize)--;
  34. used[i] = false;
  35. }
  36. }
  37. return;
  38. }
  39. char ** findItinerary(char *** tickets, int ticketsSize, int* ticketsColSize, int* returnSize){
  40. if (tickets == NULL || ticketsSize <= 0) {
  41. return NULL;
  42. }
  43. result = malloc(sizeof(char*) * (ticketsSize + 1));
  44. used = malloc(sizeof(bool) * ticketsSize);
  45. memset(used, false, sizeof(bool) * ticketsSize);
  46. result[0] = malloc(sizeof(char) * 4);
  47. memcpy(result[0], "JFK", sizeof(char) * 4);
  48. g_found = 0;
  49. *returnSize = 1;
  50. qsort(tickets, ticketsSize, sizeof(tickets[0]), cmp);
  51. backtracting(tickets, ticketsSize, returnSize, "JFK", result, used);
  52. *returnSize = ticketsSize + 1;
  53. return result;
  54. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/474556
推荐阅读
相关标签
  

闽ICP备14008679号