回溯部分的回溯问题

时间:2018-11-26 18:01:07

标签: c recursion

我正在尝试编写一种递归算法,当给定整数n时,它将找到到0的最短路径,其中可以进行2个步骤:

  1. 找到a和b使得a * b = n,然后取max(a,b)
  2. 减少1

到目前为止,我设法做到的是:

int maxDiv(int n,int i);
int toZero(int num);
int toZeroHelper(int num);


int main(){
    toZero(150);

}

int maxDiv(int n,int i){
    if(n==1)
        return 1;
    if(n%i==0)
        return n/i;
    else{
        return maxDiv(n,i+1);
    }
}

int toZero(int num){
    if (num==1)
        return 1;
    else
        return toZeroHelper(num);
}

int toZeroHelper(int num){
    if(num==1)
        return num;
    else{
        printf("%d\n",toZeroHelper(maxDiv(num,2)));
        printf("%d\n",toZeroHelper(num-1));
        return num;
    }
}

我能得到的是:

a。如何使用回溯,我需要检查下一步1是否返回小于2的数字或相反的数字?

b。如何到达最短路径?

任何参考/链接/等。将会有用

1 个答案:

答案 0 :(得分:3)

我应该知道得更多,但是我无法抗拒对此感到困惑。为了使此教学法有价值,我将作广泛解释:

首先简短回顾一下Backtracking (from Wikipedia)

  

回溯是一种通用算法,用于查找某些计算问题(尤其是约束满足问题)的所有(或某些)解决方案,该算法逐步为该解决方案构建候选对象,并在确定该候选对象后立即放弃候选对象(“回溯”)。候选人可能无法完成有效的解决方案。

有两个先决条件:

  1. 有一种递归搜索算法。

  2. 在每个递归步骤中,都有可能要比较的替代方法。

回溯是指当另一选择的递归发现结果导致更好的结果时,丢弃该选择的递归发现结果。

免责声明:递归算法不一定需要使用递归函数来实现。或者,也可以使用数据堆栈的迭代。但是,递归函数是恕我直言,是实现递归算法的最直观的方法。

我不确定我是否正确理解了OP的尝试,但是我相信OP监督了一个事实,即对于一个OP可能有多个候选( a b )满足 a·b = n n 。这些多个候选项可能提供不同的解决方案,因此,在这一点上可以应用回溯来确定有关已定义的最小标准的最佳解决方案(即通往0的最短路径)。

找到所有候选对象( a b )的一种简单(可能是幼稚的)方法是从下往下计数 a n 1 ,并通过 b = n / a 计算 b 。对于int数字,这将生成一个对( a b ),其中 a·b≤n 。在条件为 a·b == n 的情况下,可以进行检查以解决此问题:

for (int a = n; a >= 1; --a) {
  int b = n / a;
  if (a * b != n) continue;
  // sufficient pair (a, b) found
}

还要求仅考虑 max(a,b)。幸运的是,int的乘法是可交换的,即如果( a b )是一个解,则( b a )也是如此。因此,只要 a = b 就停止就足够了,因为以下解决方案( a b )已经被检查为( b a )。因此, a 将始终根据需要满足 max(a,b)

for (int a = n; a >= 1; --a) {
  int b = n / a;
  if (a < b) break; // symmetric solutions which are already tested
  if (a * b != n) continue;
  // sufficient pair (a, b) found
}

对于每个sufficient pair (a, b),必须应用第2步(向下一次),并且必须输入递归:

find(int n)
{
  for (int a = n; a >= 1; --a) {
    int b = n / a;
    if (a < b) break; // symmetric solutions which are already tested
    if (a * b != n) continue;
    // sufficient pair (a, b) found
    find(a - 1);
  }
}

递归仍未终止。因此,应该添加针对n == 0的测试:

find(int n)
{
  if (n == 0) return; // start backtracking
  for (int a = n; a >= 1; --a) {
    int b = n / a;
    if (a < b) break; // symmetric solutions which are already tested
    if (a * b != n) continue;
    // sufficient pair (a, b) found
    find(a - 1);
  }
}

回溯仍然缺失。要应用回溯,先决条件是必须根据标准对解决方案进行量化。对于最短路径,一种足够的方法应该是计算递归调用的数量(递归深度)。为此,可以使用函数的返回值(尚未定义):

int find(int n)

对于终止条件(n == 0),返回0(假设从0到达0的路径的长度根据定义为0)。否则,它比找到的最短路径大1。为了确定最短路径,将比较在一个循环中找到的所有路径长度以及最小获胜次数。

int find(int n)
{
  if (n == 0) return 0; // start backtracking with path length 0
  int lenMin;
  for (int a = n; a >= 1; --a) {
    int b = n / a;
    if (a < b) break; // symmetric solutions which are already tested
    if (a * b != n) continue;
    // sufficient pair (a, b) found
    int len = find(a - 1);
    // override lenMin if a shorter path was found
    if (lenMin > len) lenMin = len;
  }
  return lenMin + 1;
}

lenMin的初始化仍然丢失。一种简单的方法是引入一个附加标志(例如int lenMinSet),以确保将lenMin分配给第一个发现的结果,并仅将其比较。但是,这样做可以做得更好:lenMin必须使用足够大的值进行初始化,这肯定会被首次调用的结果(甚至可能是最终结果)击败。我的第一个念头是INT_MAX。三思而后行,我意识到最长的路径不能长于n。我通过假设每个 a = n (和 b = 1 )都有一条路径来提出这个想法。因此,路径应为( n n-1 n-2 ,..., 1 )。路径中的每个节点,其中 a 使结果更短。 (如果我在数学方面足够聪明,我也许可以通过induction来证明这一点,但是因为你必须相信我。)

因此,这是确定通往0的最短路径的最终功能:

int find(int n)
{
  if (n == 0) return 0; // start backtracking with path length 0
  int lenMin = n;
  for (int a = n; a >= 1; --a) {
    int b = n / a;
    if (a < b) break; // symmetric solutions which are already tested
    if (a * b != n) continue;
    // sufficient pair (a, b) found
    int len = find(a - 1);
    // override lenMin if a shorter path was found
    if (lenMin > len) lenMin = len;
  }
  return lenMin + 1;
}

Live demo on ideone

这看起来很不错,但是当路径本身公开时,这将更加令人信服。问题是:很难在递归函数find()中打印路径–以后可能会因回溯而将其丢弃。因此,必须在确定总的最短路径后,以某种方式记录该路径并最终打印出来。

要实现此目的,必须将一个附加参数添加到find()中以为路径提供存储。对于必要的存储大小,已经确定的长度上限也很好。可惜的是,存储的大小(即上限)取决于n。 Variable-length arrays将是一个不错的解决方案。不幸的是,尽管VLA从C99开始就已标准化,但可以选择按标准进行。 (我最近在SO中读到,即使VS2017似乎仍然不支持它们。)因此,对于可移植的解决方案,必须将其与malloc()free()弄混:

void printShortestPath(int n)
{
  if (n <= 0) {
    fprintf(stderr, "n should be > 0!\n");
    return;
  }
  int *path = malloc(n * sizeof n);
  int len = find(n, path);
  printf("Length of shortest path to %d: %d\n", n, len);
  printf("Path:");
  for (int i = 0; i < len; ++i) printf(" %d", path[i]);
  putchar('\n');
  free(path);
}

顺便说一句,malloc()free()比VLA更友好的堆栈(因为是在堆上分配),我意识到该社区中的许多人对于广泛使用堆栈非常关键。

find()必须进行相应的修改:

int find(int n, int *path)

这会引起另一个问题:递归调用find()时,它会填充路径,以后可能会丢弃也可能不会丢弃。因此,它应将提供的路径存储在本地副本中,直到确定它是最终解决方案的一部分为止。

int find(int n, int *pathMin)
{
  if (n == 0) return 0; // start backtracking with path length 0
  pathMin[0] = n;
  if (n == 1) return 1; // trivial result
  int lenMin = n;
  int *path = malloc(n * sizeof n);
  for (int a = n; a > 1; --a) {
    int b = n / a;
    if (a < b) break; // symmetric solutions which are already tested
    if (a * b != n) continue;
    // sufficient pair (a, b) found
    int len = find(a - 1, path);
    // override lenMin if a shorter path was found
    if (lenMin > len) {
      lenMin = len;
      // store current shortest path (it could be final result)
      memcpy(pathMin + 1, path, len * sizeof *path);
    }
  }
  free(path);
  return lenMin + 1;
}

将所有内容放在一起(通过一个小的蛮力测试):

#include <stdio.h>
#include <stdlib.h>

int find(int n, int *pathMin)
{
  if (n == 0) return 0; // start backtracking with path length 0
  pathMin[0] = n;
  if (n == 1) return 1; // trivial result
  int lenMin = n;
  int *path = malloc(n * sizeof n);
  for (int a = n; a > 1; --a) {
    int b = n / a;
    if (a < b) break; // symmetric solutions which are already tested
    if (a * b != n) continue;
    // sufficient pair (a, b) found
    int len = find(a - 1, path);
    // override lenMin if a shorter path was found
    if (lenMin > len) {
      lenMin = len;
      // store current shortest path (it could be final result)
      memcpy(pathMin + 1, path, len * sizeof *path);
    }
  }
  free(path);
  return lenMin + 1;
}

void printShortestPath(int n)
{
  if (n <= 0) {
    fprintf(stderr, "n should be > 0!\n");
    return;
  }
  int *path = malloc(n * sizeof n);
  int len = find(n, path);
  printf("Length of shortest path to %d: %d\n", n, len);
  printf("Path:");
  for (int i = 0; i < len; ++i) printf(" %d", path[i]);
  putchar('\n');
  free(path);
}

int main(void)
{
  // a brute-force test for a range of numbers:
  for (int n = 1; n <= 20; ++n) {
    printShortestPath(n);
  }
  // done
  return 0;
}

输出:

Length of shortest path to 1: 1
Path: 1
Length of shortest path to 2: 2
Path: 2 1
Length of shortest path to 3: 3
Path: 3 2 1
Length of shortest path to 4: 2
Path: 4 1
Length of shortest path to 5: 3
Path: 5 4 1
Length of shortest path to 6: 3
Path: 6 2 1
Length of shortest path to 7: 4
Path: 7 6 2 1
Length of shortest path to 8: 4
Path: 8 3 2 1
Length of shortest path to 9: 3
Path: 9 2 1
Length of shortest path to 10: 3
Path: 10 4 1
Length of shortest path to 11: 4
Path: 11 10 4 1
Length of shortest path to 12: 4
Path: 12 5 4 1
Length of shortest path to 13: 5
Path: 13 12 5 4 1
Length of shortest path to 14: 4
Path: 14 6 2 1
Length of shortest path to 15: 3
Path: 15 4 1
Length of shortest path to 16: 4
Path: 16 15 4 1
Length of shortest path to 17: 5
Path: 17 16 15 4 1
Length of shortest path to 18: 4
Path: 18 5 4 1
Length of shortest path to 19: 5
Path: 19 18 5 4 1
Length of shortest path to 20: 3
Path: 20 4 1

Live Demo on ideone