Complete Search (a.k.a Brute Force)

概念

搜索,也就是对状态空间进行枚举,通过穷尽所有的可能来找到最优解,或者统计合法解的个数。Complete search,我们在学习递归的时候,就了解到了。递归枚举子集,递归枚举组合,递归枚举排列,是三个入门的问题。

在讲到搜索的时候,会有几个概念,<深度优先搜索、深度优先遍历>,<宽度优先搜索、宽度优先遍历>(也叫广度优先搜索)。遍历呢,一般是指图上的搜索。我们在学习搜索的时候,主要是指学习深度优先搜索。

下面这段话,用来理解和区分这几个常用的概念问题。

  • depth first search(dfs), 按照深度优先的顺序对“问题状态空间”进行搜索的算法(理解理解搜索树,是不是深度优先?)。
  • “深搜”,是一种包括遍历形式、状态记录与检索、剪枝优化等算法整体设计的统称。提前学习建图方法后,掌握在图上进行遍历,进一步把“问题空间”类比为一张图。
  • 研究dfs算法之前,要定义该过程产生的 “搜索树” 结构,整个深搜算法就是基于该搜索树完成的(用递归实现的指数型枚举、排列性枚举、组合型枚举,其实就是深搜的三种最简单的形式)
  • dfs,常常指利用递归函数实现暴力枚举的算法。
  • 递归搜索,该类搜索算法的特点在于,将要搜索的目标分成若干“层”,每层基于前几层的状态进行决策,直到达到目标状态。

不要过多纠结于名词概念,能够inplementation才是王道。

例题:Complete search

Complete search is a general method that can be used to solve almost any algorithm problem. The idea is to generate all possible solutions to the problem using brute force, and then select the best solution or count the number of solutions, depending on the problem.

Complete search is a good technique if there is enough time to go through all the solutions, because the search is usually easy to implement and it always gives the correct answer. If complete search is too slow, other techniques, such as greedy algorithms or dynamic programming, may be needed.

Generating subsets生成子集问题

这个问题我们在学习递归的时候已经学习,复习一下。

递归实现指数型枚举

  1. //子集枚举

递归实现组合型枚举

  1. //在子集枚举的基础上,进行剪枝

Generating permutations生成排列问题

这个问题我们在学习递归的时候已经学习,复习一下。

递归实现排列型枚举

  1. //排列枚举

例题:将正整数拆分问题

  1. //问题1:
  2. 将正整数 n 分解成3个正整数,如 6 = 1 + 2 + 3
  3. 排在后面的数必须大于等于前面的数,输出所有方案。
  4. -- 上机操作:循环实现
  5. -- 上机操作:递归实现
  6. //问题2:
  7. 将正整数 n 分解成 小于等于m 个正整数之和,
  8. 且排在后面的数必须 大于等于 前面的数,并输出所有方案。
  9. 输入:
  10. 4 3
  11. 输出:
  12. 1 1 2
  13. 1 3
  14. 2 2
  15. 4

对于问题1,我们可以使用循环处理,using 3 loops

  1. for (int i = 1; i <= n; ++i)
  2. for (int j = i; j <= n; ++j)
  3. for (int k = j; k <= n; ++k)
  4. if (i + j + k == n) printf("%d=%d+%d+%d\n", n, i, j, k);

如果问题变成“将正整数n分解成4个正整数”,此时就需要4重循环。如果问题变成“分解成小于等于m个整数”,那么,就没办法使用loops去弄了,需要使用递归搜索。

下面,我们考虑问题2。(用搜索树,来表示)

我们将问题分层,第 i 层决定 ai。则为了进行第 i 层决策,我们需要记录三个状态变量:

  • complete search and backtracking搜索与回溯 - 图1,表示后面所有正整数的和
  • complete search and backtracking搜索与回溯 - 图2,表示前一层的正整数,以确保正整数递增
  • complete search and backtracking搜索与回溯 - 图3 ,确保我们最多输出 m 个正整数
  1. // arr[]记录方案
  2. void dfs(int n, int i, int a) {
  3. if (n == 0) {
  4. //输出方案
  5. }
  6. if (i <= m) {
  7. for (int j = a; j <= n; j++) {
  8. arr[i] = j;
  9. dfs(n - j, i + 1, j); //剩余n-j, 分解了i+1个数,前一个数用的是j
  10. }
  11. }
  12. }
  13. dfs(n, 1, 1);

backtracking回溯

回溯法是一种经常被用在深度深度优先搜索(DFS)和广度优先搜索(BFS)的技巧。其本质是:走不通就回头。关键词:恢复现场

例题:八皇后问题

As an example, consider the problem of calculating the number of ways n queens can be placed on an n × n chessboard so that no two queens attack each other. For example, when n = 4, there are two possible solutions:
image.png
The problem can be solved using backtracking by placing queens to the board row by row. More precisely, exactly one queen will be placed on each row so that no queen attacks any of the queens placed before. A solution has been found when all n queens have been placed on the board.

For example, when n = 4, some partial solutions generated by the backtracking algorithm are as follows:
image.png
At the bottom level, the three first configurations are illegal, because the queens attack each other. However, the fourth configuration is valid and it can be extended to a complete solution by placing two more queens to the board. There is only one way to place the two remaining queens.

  1. void search(int y) {
  2. if (y == n) {
  3. count++;
  4. return;
  5. }
  6. for (int x = 0; x < n; x++) {
  7. if (column[x] || diag1[x+y] || diag2[x-y+n-1]) continue;
  8. column[x] = diag1[x+y] = diag2[x-y+n-1] = 1;
  9. search(y+1);
  10. column[x] = diag1[x+y] = diag2[x-y+n-1] = 0;
  11. }
  12. }
  13. search(0); //调用

Let complete search and backtracking搜索与回溯 - 图6 denote the number of ways to place n queens on an n × n chessboard. The above backtracking algorithm tells us that, for example, complete search and backtracking搜索与回溯 - 图7。(记住:八皇后问题有92种方案)

  1. // 按行枚举
  2. //(1)反对角线 y=x+b, 截距 b=y−x,因为我们要把 b 当做数组下标,所以 b 不能是负的
  3. // 所以我们 +n,保证是结果是正的
  4. //(2)而对角线 y=−x+b, 截距是 b=y+x,这里截距一定是正的,所以不需要加偏移量
  5. // 反对角线,左上到右下
  6. // 对角线,左下到右上
  7. // 反对角线udg[n−u+i],对角线 dg[u+i]中的下标
  8. // n−u+i,u+i 表示的是截距
  9. #include <iostream>
  10. using namespace std;
  11. const int N = 20;
  12. // bool数组用来判断搜索的下一个位置是否可行
  13. // col列,dg对角线,udg反对角线
  14. // g[N][N]用来存路径
  15. int n;
  16. char g[N][N];
  17. bool col[N], dg[N], udg[N];
  18. void dfs(int u)
  19. {
  20. // u == n 表示已经搜了n行,故输出这条路径
  21. if (u == n)
  22. {
  23. for (int i = 0; i < n; i ++ ) puts(g[i]); // 等价于cout << g[i] << endl;
  24. puts(""); // 换行
  25. return;
  26. }
  27. //对n个位置按行搜索
  28. for (int i = 0; i < n; i ++ )
  29. //剪枝(对于不满足要求的点,不再继续往下搜索)
  30. //udg[n - u + i],+n是为了保证大于0
  31. if (!col[i] && !dg[u + i] && !udg[n - u + i])
  32. {
  33. g[u][i] = 'Q';
  34. col[i] = dg[u + i] = udg[n - u + i] = true;
  35. dfs(u + 1);
  36. // 恢复现场 这步很关键
  37. col[i] = dg[u + i] = udg[n - u + i] = false;
  38. g[u][i] = '.';
  39. }
  40. }
  41. int main()
  42. {
  43. cin >> n;
  44. for (int i = 0; i < n; i ++ )
  45. for (int j = 0; j < n; j ++ )
  46. g[i][j] = '.';
  47. dfs(0);
  48. return 0;
  49. }
  1. // 按每个元素进行枚举
  2. #include <iostream>
  3. using namespace std;
  4. const int N = 20;
  5. // 因为是一个个搜索,所以加了row
  6. int n;
  7. char g[N][N];
  8. bool row[N], col[N], dg[N], udg[N];
  9. // s表示已经放上去的皇后个数
  10. void dfs(int x, int y, int s)
  11. {
  12. // 处理超出边界的情况
  13. if (y == n) y = 0, x ++ ;
  14. // 说明已经放好了n个皇后,表示枚举完 n^2 个了
  15. if (x == n)
  16. {
  17. if (s == n)
  18. {
  19. for (int i = 0; i < n; i ++ ) puts(g[i]);
  20. puts("");
  21. }
  22. return;
  23. }
  24. // 不放皇后 就往下搜下一个位置
  25. dfs(x, y + 1, s);
  26. // 放皇后
  27. if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n])
  28. {
  29. g[x][y] = 'Q';
  30. row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;
  31. dfs(x, y + 1, s + 1);
  32. row[x] = col[y] = dg[x + y] = udg[x - y + n] = false;
  33. g[x][y] = '.';
  34. }
  35. }
  36. int main()
  37. {
  38. cin >> n;
  39. for (int i = 0; i < n; i ++ )
  40. for (int j = 0; j < n; j ++ )
  41. g[i][j] = '.';
  42. dfs(0, 0, 0);
  43. return 0;
  44. }

参考https://www.acwing.com/solution/content/2820/

代码框架

  1. // version 1
  2. // Typical backtracking procedure
  3. void recursion(state s)
  4. {
  5. if( base(s) )
  6. return ;
  7. for each substate ss
  8. mark ss
  9. recursion (ss)
  10. unmark ss
  11. }
  12. // version 2
  13. int ans = 最坏情况, now; // now为当前答案
  14. void dfs(传入数值) {
  15. if (到达目的地) ans = 从当前解与已有解中选最优;
  16. for (遍历所有可能性)
  17. if (可行) {
  18. 进行操作;
  19. dfs(缩小规模);
  20. 撤回操作;
  21. }
  22. }
  23. // version 3
  24. * Backtracking(state)
  25. * if invalid(state) //e.g. we reached a # or the location is already visited
  26. * return
  27. *
  28. * if found_solution(state)
  29. * mark_solution_found
  30. * return
  31. *
  32. * for each child_state of state
  33. * state' = do changes in state
  34. *
  35. * Backtracking(state')
  36. *
  37. * state = undo changes in state'

递归调用次数的实验

此处需要上机实验

  1. // Could we do recursion calls as we want? NO, you have stack limit size.
  2. // Say your function call reserve 1 integer (4 bytes) and a call need 2 pointers (8 bytes) for saving some return addresses
  3. // Say you have 1.5MB stack size, then you could do maximum 1.5MB / 12 bytes ~= 144617 call
  4. int dep = 0;
  5. void stackTest1()
  6. {
  7. cout<<dep++<<"\n"<<flush;
  8. stackTest1();
  9. }
  10. void stackTest2(int a = 1)
  11. {
  12. cout<<dep++<<"\n"<<flush;
  13. stackTest2(a);
  14. }
  15. void stackTest3()
  16. {
  17. cout<<dep++<<"\n"<<flush;
  18. int arr[50]; // be careful from your created items. E.g. Integrs, Strings, Structs.
  19. stackTest3();
  20. }
  21. void stackTest4()
  22. {
  23. cout<<dep++<<"\n"<<flush;
  24. int arr[100]; // be careful from your created items. E.g. Integrs, Strings, Structs.
  25. stackTest4();
  26. }
  27. void stackTest5()
  28. {
  29. cout<<dep++<<"\n"<<flush;
  30. int arr[50];
  31. vector<int> v(50); // Its internal array on heap not on stack
  32. stackTest5();
  33. }
  34. void stackTest6()
  35. {
  36. cout<<dep++<<"\n"<<flush;
  37. int arr[50];
  38. vector<int> v(1000); // Its internal array on heap not on stack. Then watch from heap too
  39. stackTest6();
  40. }
  41. void stackTest7()
  42. {
  43. cout<<dep++<<"\n"<<flush;
  44. int arr[1000];
  45. stackTest7();
  46. }
  47. void stackTest8()
  48. {
  49. cout<<dep++<<"\n"<<flush;
  50. int arr[1000000]; // So big for my MACHINE stack limit
  51. stackTest8();
  52. }
  53. void stackTest9(int a[]) // this is array reference. it behaves as if it is just an integer
  54. {
  55. cout<<dep++<<"\n"<<flush;
  56. stackTest9(a);
  57. }
  58. int arr[1000000];
  59. int main()
  60. {
  61. stackTest9(arr);
  62. return 0;
  63. }

更进一步的拓展(*)

数字三角形模型

  1. // Given grid of positive numbers, Start from 0, 0 and end at n, n. Move only to right and down - find path with sum of numbers is maximum.
  2. /*
  3. 15
  4. 24
  5. 512
  6. 678
  7. 189
  8. */
  9. int grid[MAX][MAX];
  10. // Think in function F(i, j) that find solution from (i, j) to (n, n)
  11. int maxPathSum(int r, int c)
  12. {
  13. if( !valid(r, c))
  14. return 0;
  15. if (r == n-1 && c == n-1)
  16. return grid[r][c]; // base
  17. int path1 = maxPathSum(r, c+1); // right
  18. int path2 = maxPathSum(r+1, c); // down
  19. return grid[r][c] + max(path1, path2);
  20. }

迷宫是否可以走出

  1. /*
  2. .SX..
  3. ..X.E
  4. ....X
  5. X.XX.
  6. */
  7. char maze[MAX][MAX]; // filled with S (for start), E (for end), . (could pass) and X (block can't path)
  8. bool findEnd(int r, int c) // Recursion State: r, c
  9. {
  10. if( !valid(r, c) || maze[r][c] == 'X')
  11. return false; // invalid position or block position
  12. if( maze[r][c] =='E')
  13. return true; // we found End
  14. // Try the 4 neighbor cells
  15. if(findEnd(r, c-1)) return true; // search up
  16. if(findEnd(r, c+1)) return true; // search down
  17. if(findEnd(r-1, c)) return true; // search left
  18. if(findEnd(r+1, c)) return true; // search right
  19. // Can't find a way for it!
  20. return false;
  21. }
  22. // This code will go to infinity! ... we need to avoid cycles. Mark visited cells
  1. bool vis[MAX][MAX];
  2. bool findEnd2(int r, int c) // Recursion State: r, c and FULL visted array
  3. {
  4. if( !valid(r, c) || maze[r][c] == 'X' || vis[r][c] == 1)
  5. return false; // invalid position or block position
  6. vis[r][c] = 1; // we just visited it, don't allow any one bacl to it
  7. if( maze[r][c] =='E')
  8. return true; // we found End
  9. // Try the 4 neighbor cells
  10. if(findEnd2(r, c-1)) return true; // search up
  11. if(findEnd2(r, c+1)) return true; // search down
  12. if(findEnd2(r-1, c)) return true; // search left
  13. if(findEnd2(r+1, c)) return true; // search right
  14. vis[r][c] = 0; // undo marking, other paths allowed to use it now
  15. // Can't find a way for it!
  16. return false;
  17. }

Flood Fill

  1. // Flood Fill...given maze where cells are . or X. You start at 0, 0..how many cells you could reach?
  2. /*
  3. ..X.
  4. .X.X
  5. ..X.
  6. ...x.
  7. ..x..
  8. .x...
  9. x....
  10. ....X...
  11. ....XXXX
  12. ..X.....
  13. .X....XX
  14. ..X.X.X.
  15. ..X...X.
  16. ...X..XX
  17. */
  18. // A reachable block is called connected components. Each set of positions reachable together are connected component.
  19. int cnt = 0;
  20. void cntReachalbleCells(int r, int c)
  21. {
  22. if( !valid(r, c) || maze[r][c] == 'X' || vis[r][c] == 1)
  23. return; // invalid position or block position
  24. vis[r][c] = 1; // we just visited it, don't allow any one back to it
  25. cnt++;
  26. // Try the 4 neighbor cells
  27. cntReachalbleCells(r, c-1);
  28. cntReachalbleCells(r, c+1);
  29. cntReachalbleCells(r-1, c);
  30. cntReachalbleCells(r+1, c);
  31. }
  32. // What about finding number of connected components?
  33. void cntComponents(int R, int C)
  34. {
  35. int comps = 0;
  36. for(int i = 0; i < R; ++i)
  37. {
  38. for(int j = 0; j < C; ++j)
  39. {
  40. cnt = 0;
  41. cntReachalbleCells(i, j);
  42. if(cnt > 0) comps++;
  43. }
  44. }
  45. }

generate all sequences of given length, of zeros and ones

  1. //
  2. // E.g. for len = 3: 000, 001, 010, 011, 100, 101, 110, 111
  3. // "" Count=1
  4. // 0 1 Count=2
  5. // 00 01 10 11 Count=4
  6. // 000 001 010 011 100 101 110 11 Count=8
  7. //
  8. void generateBinary(int len, string cur = "") // recursion state: integer, string
  9. {
  10. if(len == 0)
  11. {
  12. cout<<cur<<"\n";
  13. return;
  14. }
  15. // At each level, we branch twice...draw this tree
  16. generateBinary(len-1, cur + "0");
  17. generateBinary(len-1, cur + "1");
  18. }

generate all sequences of given length, of zeros, ones and two2

  1. // generate all sequences of given length, of zeros, ones and two2
  2. // E.g. for len = 2: 00, 01, 02, 10, 11, 12, 20, 21, 22
  3. void generateTernary(int len, string cur = "")
  4. {
  5. if(len == 0)
  6. {
  7. cout<<cur<<"\n";
  8. return;
  9. }
  10. // At each level, we branch three times...draw this tree
  11. generateTernary(len-1, cur + "0");
  12. generateTernary(len-1, cur + "1");
  13. generateTernary(len-1, cur + "2");
  14. }

排列枚举

  1. // Print all possible permutations of numbers 0, 1, 2, ...n-1
  2. void perm(int i, int n, int vis[], int cur[])
  3. {
  4. if(i == n)
  5. {
  6. for (int j = 0; j < n; ++j) cout<<cur[j];
  7. cout<<"\n";
  8. return;
  9. }
  10. for (int j = 0; j < n; ++j) if(!vis[j])
  11. {
  12. vis[j] = 1;
  13. cur[i] = j;
  14. perm(i+1, n, vis, cur);
  15. vis[j] = 0;
  16. }
  17. }

组合枚举

  1. // print N choose M combinations
  2. void comb(int i, int n, int m, int cur[], int curLen)
  3. {
  4. if(curLen == m)
  5. {
  6. for (int j = 0; j < m; ++j)
  7. cout<<cur[j];
  8. cout<<"\n";
  9. return;
  10. }
  11. if(i == n) // we reached end, and have current combination is not correct
  12. return;
  13. // take it
  14. cur[curLen] = i;
  15. comb(i+1, n, m, cur, curLen+1);
  16. // don't take it
  17. comb(i+1, n, m, cur, curLen);
  18. }

How to avoid stack problems(记忆化出场)

1- If must do it in recursion, then avoid any unnecessary local data. Move to global as possible
2- Move to iterative procedure
3- Implement your own stack calls!

  1. // Fibonacci Series: Fib(n) = fib(n-1) + fib(n-2). Fib(0) = fib(1) = 1
  2. int fib(int n)
  3. {
  4. if(n <= 1)
  5. return 1;
  6. return fib(n-2) + fib(n-1);
  7. }
  8. // what is search space? n
  9. // what is number of recursive calls? We are branching every time to 2 levels that differs in 1
  10. // fib(5)
  11. // fib(4) fib(3)
  12. // fib(3) fib(2) fib(2) fib(1)
  13. // fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
  14. // fib(1) fib(1)
  15. //
  16. // We almost have 2^N calls
  17. // Wait, a space of N, is called 2^N times!
  18. // A fib of 50 do around ~1125899906842624 call!!!!!!
  19. //
  20. // Then, we must call a state more than once? and it too call other states, that already called!
  21. //
  22. // Check tree above, fib(3) called twice. Fib(2) called three times!
  23. //
  24. // Let's SAVE the answer, and let space of N is called 2N times!
  25. int savedAnswers[MAX]; /// Initialized to -1, means no answer
  26. int fibSave(int n)
  27. {
  28. if(n <= 1)
  29. return 1;
  30. if(savedAnswers[n] != -1)
  31. return savedAnswers[n];
  32. return savedAnswers[n] = fib(n-2) + fib(n-1);
  33. }
  34. // fib(5)=8
  35. // fib(4)=5 fib(3)=3
  36. // fib(3)=3 fib(2)=2
  37. // fib(2)=2 fib(1)=1
  38. // fib(1)=1 fib(1)=1

memoization记忆化

记忆化搜索,是在学习动态规划的时候,用到的。用记忆化搜索的方式,实现dp。在这个章节,练习题目时,也请感受一下记忆化操作。

  1. // 记忆化模板
  2. int g[MAXN]; // 定义记忆化数组
  3. int ans = 最坏情况, now;
  4. void dfs f(传入数值) {
  5. if (g[规模] != 无效数值) return; // 或记录解,视情况而定
  6. if (到达目的地) ans = 从当前解与已有解中选最优; // 输出解,视情况而定
  7. for (遍历所有可能性)
  8. if (可行) {
  9. 进行操作;
  10. dfs(缩小规模);
  11. 撤回操作;
  12. }
  13. }
  14. int main() {
  15. memset(g, 无效数值, sizeof(g)); // 初始化记忆化数组
  16. }

《一本通》题目

【例5.2】组合的输出
  1. //递归实现组合枚举

【例5.3】自然数的拆分
  1. //拆分成若干个小于n的自然数之和

LETTERS
  1. //棋盘上dfs
  2. //向上下左右四个方向移动,并且不能移向曾经经过的字母。问最多可以经过几个字母

八皇后问题
  1. //92种方案

八皇后
  1. //输出第x种方案

迷宫
  1. //棋盘上dfs,判断是否可达。注意是不用回溯

红与黑
  1. //总共能到达多少块黑色瓷砖

棋盘问题
  1. //方法一
  2. //棋盘形状是不规则的,只能在 # 位置上摆放。两个棋子不能同行同列,摆放k个棋子,有多少种方案。设计一个带参数的dfs函数
  3. void dfs(int u, int num) 枚举到第u行,已经摆放了num个棋子
  4. for (int i = u; i < n; i++)
  5. for (int j = 0; j < n; j++){
  6. if (!col[j] && g[i][j] == '#'){
  7. col[j] = true;
  8. dfs(i + 1, num + 1);
  9. col[j] = false;
  10. }
  11. }
  12. dfs(0, 0);
  13. //方法二
  14. // 这种就回归到类似八皇后的代码结构上,但道理是相通的
  15. // 这一行,枚举有没有能选的
  16. // 这一行,压根不选,直奔下一行
  17. void dfs(int u, int num)
  18. {
  19. if (u == n){
  20. if (num == k){
  21. res++;
  22. }
  23. return ;
  24. }
  25. for (int j = 0; j < n; j++){
  26. if (s[u][j] == '#' && !col[j]){
  27. col[j] = true;
  28. dfs(u + 1, num + 1);
  29. col[j] = false;
  30. }
  31. }
  32. dfs(u + 1, num);
  33. }
  34. dfs(0, 0);

取石子游戏
  1. //从较多的那堆里取,取较少那堆的整数倍,最后把一堆棋子取空,就是赢家。多组数据,问先手是不是赢家。
  2. //a / b >= 2, 先手必胜。
  3. //a / b < 2, 先手只有一种取法。
  4. //a / b >= 2,先手必胜。神奇的性质。

马走日
  1. //给出起始位置,问有多少方案可以遍历到棋盘上的所有点。
  2. //num是已经遍历点的个数
  3. void dfs(int x, int y, int num)
  4. //用数组控制8个方向的写法,判断点合法性的方法

单词接龙【难】
  1. //要拼成的单词长度尽可能的长,两个单词重合的部分就会尽可能的少。
  2. if (a.substr(a.size() - k, k) == b.substr(0, k))
  3. g[i][j] = k 维护两个单词之间重叠关系
  4. void dfs(string dragon, int last){
  5. //...
  6. vis[last]++;
  7. for (int i = 0; i < n; i++)
  8. if (g[last][i] && vis[i] < 2)
  9. dfs(dragon + s[i].substr(g[last][i]), i);
  10. vis[last]--;
  11. //...
  12. }
  13. //调用
  14. dfs(s[i], i);

分成互质组【难】
  1. //方法一:拿着鸡蛋去往篮子里放,有合适的放或者不放,没有合适的就先开一个篮子
  2. //爆搜,每一个位置有两种操作
  3. //对已有的每一个组进行枚举,看能不能放进去
  4. //新建一个组,放进去
  5. //方法二:鸡蛋一排摆在地上,拿着篮子去挑鸡蛋
  6. void dfs(int g, int gc, int tc, int start) //group count total count
  7. {
  8. if (g >= res) return ;
  9. if (tc == n) res = min(res, g);
  10. bool flag = true;
  11. for (int i = start; i < n; i++)
  12. if (!st[i] && check(group[g], gc, i)){ //传进去的是一个一维数组
  13. st[i] = true;
  14. group[g][gc] = i;
  15. dfs(g, gc + 1, tc + 1, i + 1);
  16. st[i] = false;
  17. flag = false;
  18. }
  19. if (flag) dfs(g + 1, 0, tc, 0); //从0号下标开始搜
  20. }
  21. //调用
  22. dfs(1, 0, 0, 0);

放苹果
  1. //苹果数量大于盘子数量,第n个盘子不放dfs(m, n - 1),每个盘子都放一个dfs(m - n, n)
  2. //苹果数量小于盘子数量,问题等价于dfs(m, m)
  3. //问题边界:0个苹果,1种放法。1个盘子,1种放法。

例题程序

  1. //将正整数n分解成 小于等于m个 正整数之和,且排在后面的数必须大于等于前面的数,并输出所有方案
  2. //问题2的递归实现
  3. #include <bits/stdc++.h>
  4. using namespace std;
  5. int n, m;
  6. int a[110];
  7. void dfs(int left, int u, int prev)
  8. {
  9. if (left == 0){
  10. for (int i = 1; i < u; i++) printf("%d ", a[i]);
  11. puts("");
  12. return ;
  13. }
  14. if (u > m) return ;
  15. for (int i = prev; i <= left; i++){
  16. a[u] = i;
  17. dfs(left - i, u + 1, i);
  18. }
  19. }
  20. int main()
  21. {
  22. cin >> n >> m;
  23. dfs(n, 1, 1); //剩余n可分配,当前第1位,可以从1开始用
  24. return 0;
  25. }
  1. //把n恰好分成m个正整数的方案,问题1的递归实现
  2. #include <bits/stdc++.h>
  3. using namespace std;
  4. int n, m;
  5. int a[110];
  6. void dfs(int left, int u, int prev)
  7. {
  8. if (left == 0){
  9. if (u == m + 1){
  10. for (int i = 1; i < u; i++) printf("%d ", a[i]);
  11. puts("");
  12. }
  13. return ;
  14. }
  15. if (u > m) return ;
  16. for (int i = prev; i <= left; i++){
  17. a[u] = i;
  18. dfs(left - i, u + 1, i);
  19. }
  20. }
  21. int main()
  22. {
  23. cin >> n >> m;
  24. dfs(n, 1, 1); //剩余n可分配,当前第1位,可以从1开始用
  25. return 0;
  26. }
  1. //重新设计一个dfs函数,这回是带用了多少作为参数
  2. //把n恰好分成m个正整数的方案,问题1的递归实现
  3. #include <bits/stdc++.h>
  4. using namespace std;
  5. int n, m;
  6. int A[110];
  7. void dfs(int u, int sum, int a) //当前第u个,已经分配了sum,可以从a开始用
  8. {
  9. if (u == m + 1)
  10. {
  11. if (sum == n){
  12. for (int i = 1; i <= m; i++) printf("%d ", A[i]);
  13. puts("");
  14. }
  15. return ;
  16. }
  17. for (int j = a; j <= n; j++){
  18. A[u] = j;
  19. dfs(u + 1, sum + j, j);
  20. }
  21. }
  22. int main()
  23. {
  24. cin >> n >> m;
  25. dfs(1, 0, 1);
  26. return 0;
  27. }