基本算法问题

NP-Hard和NP-Complete的区别:
对NP-Hard问题和NP-Complete问题的一个直观的理解就是指那些很难(很可能是不可能)找到多项式时间算法的问题. 因此一般初学算法的人都会问这样一个问题: NP-Hard和NP-Complete有什么不同? 简单的回答是根据定义, 如果所有NP问题都可以多项式归约到问题A, 那么问题A就是NP-Hard; 如果问题A既是NP-Hard又是NP, 那么它就是NP-Complete. 从定义我们很容易看出, NP-Hard问题类包含了NP-Complete类. 但进一步的我们会问, 是否有属于NP-Hard但不属于NP-Complete的问题呢? 答案是肯定的. 例如停机问题, 也即给出一个程序和输入, 判定它的运行是否会终止. 停机问题是不可判的, 那它当然也不是NP问题. 但对于SAT这样的NP-Complete问题, 却可以多项式归约到停机问题. 因为我们可以构造程序A, 该程序对输入的公式穷举其变量的所有赋值, 如果存在赋值使其为真, 则停机, 否则进入无限循环. 这样, 判断公式是否可满足便转化为判断以公式为输入的程序A是否停机. 所以, 停机问题是NP-Hard而不是NP-Complete.
NP:全称nondeterministicpolynomialtime,指可以在多项式时间内可验证问题。经常有人误把NP理解为non-polynomialtime。
NP 是 Non-deterministic Polynomial 的缩写,NP 问题通俗来说是其解的正确性能够被很容易检查的问题, 这里”很容易检查”指的是存在一个多项式检查算法.
例如, 著名的推销员旅行问题(Travel Saleman Problem or TSP):假设一个推销员需要从香港出发, 经过广州, 北京, 上海,….., 等 n 个城市, 最后返回香港。 任意两个城市之间都有飞机直达, 但票价不等。现在假设公司只给报销 C,使 C ?
推销员旅行问题显然是 NP 的. 因为如果你任意给出一个行程安排, 可以很容易算出旅行总开销. 但是, 要想知道一条总路费小于 $C 的行程是否存在, 在最坏情况下, 必须检查所有可能的旅行安排! 这将是个天文数字.
NP-complete 问题是所有 NP 问题中最难的问题. 它的定义是, 如果你可以找到一个解决某个 NP-complete 问题的多项式算法, 那么所有的 NP 问题都将可以很容易地解决.
通常证明一个问题 A 是 NP-complete 需要两步, 第一先证明 A 是 NP 的, 即满足容易被检查这个性质; 第二步是构造一个从某个已知的 NP-complete 问题 B 到 A 的多项式变换, 使得如果 B 能够被容易地求解, A 也能被容易地 解决. 这样一来, 我们至少需要知道一个 NP-complete 问题.
第一个 NP complete 问题是 SAT 问题, 由 COOK 在 1971 年证明. SAT 问题指的是, 给定一个包含 n 个布尔变量(只能为真或假) X1, X2, .., Xn 的逻辑析取范式, 是否存在它们的一个取值组合, 使得该析取范式被满足? 可以用一个具体例子来说明这一问题, 假设你要安排一个 1000 人的晚宴, 每桌 10 人, 共 100 桌. 主人给了你一张纸, 上面写明其中哪些 人因为 江湖恩怨不能坐在同一张桌子上, 问是否存在一个满足所有这些约束条件的晚宴安排? 这个问题显然是 NP 的, 因为如果有人建议一个安排方式, 你可以很容易检查它是否满足所有约束. COOK 证明了这个问题是 NP-complete 的, 即如果你有一个好的方法能解决晚宴安排问题, 那你就能解决所有的 NP 问题.
这听起来很困难, 因为你必须面对所有的 NP 问题, 而且现在你并不知道任何的 NP-complete 问题可以利用.COOK 用非确定性图灵机( Non-deterministic Turing Machine ) 巧妙地解决了这一问题.
正式地, NP 问题是用非确定性图灵机来定义的, 即所有可以被非确定性图灵机在多项式时间内解决的问题. 非确定性图灵机是一个特殊的图灵机, 它的定义抓住了”解容易被检查” 这一特性. 非确定性图灵机有一个”具有魔力的”猜想部件, 只要问题有一个解, 它一定可以猜中. 例如, 只要存在哪怕一个满足约束的晚宴安排方式, 或是一个满足旅行预算的行程安排, 都无法逃过它的法眼, 它可以在瞬间猜中. 在猜出这个解以后,检查确认部分和一台普通的确定性图灵机完全相同,也即是等价于任何一个实际的计算机程序.
COOK 证明了,任意一个非确定性图灵机的计算过程,即先猜想再验证的过程, 都可以被描述成一个 SAT 问题,这个 SAT 问题实际上总结了该非确定性图灵机在计算过程中必须满足的所有约束条件的总和(包括状态转移, 数据读写的方式等等), 这样, 如果你有一个能解决该 SAT 问题的好的算法, 你就可以解决相应的那个非确定性图灵机计算问题, 因为每个 NP 问题都不过是一个非确定性图灵机计算问题, 所以, 如果你可以解决 SAT , 你就可以解决所有 NP 问题. 因此, SAT 是一个 NP-complete 问题.
有了一个 NP-complete 问题, 剩下的就好办了, 我们不用每次都要和非确定性图灵机打交道, 而可以用前面介绍的两步走的方法证明其它的 NP-complete 问题. 迄今为止, 人们已经发现了成千上万的NP-complete 问题, 它们都具有容易被检查的性质, 包括前面介绍的推销员旅行问题. 当然更重要的是, 它们是否也容易被求解, 这就是著名的 P vs NP 的问题.

贪心法
 贪心法(Greedy algorithm)是一种在每一步选择中都采取在当前状态下最好/优的选择,从而希望导致结果是最好/优的算法。比如在旅行推销员问题中,如果旅行员每次都选择最近的城市, 那这就是一种贪心算法。
  贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。
  贪心算法与动态规划的不同在于它每对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
  贪心法可以解决一些最优性问题,如:求图中的最小生成树、求哈夫曼编码……对于其他问题,贪心法一般不能得到我们所要求的答案。一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。
  贪心法解题特点
  贪心法有一个共同的点就是在最优求解的过程中都采用一种局部最优策略,把问题范围和规模缩小最后把每一步的结果合并起来得到一个全局最优解。
  贪心法解题的一般步骤
  (1)从问题的某个初始解出发;
  (2)采用循环语句,当可以向求解目标前进一部时,就根据局部最优策略,得到一个部分解,缩小问题的范围和规模;
  (3)将所有部分解综合起来,得到问题最终解。
  该算法存在问题:
  1. 不能保证求得的最后解是最佳的;
  2. 不能用来求最大或最小解问题;
  3. 只能求满足某些约束条件的可行解的范围。

穷举法

  穷举法,或称为暴力破解法,是一种针对于密码的破译方法,即将密码进行逐个推算直到找出真正的密码为止。例如一个已知是四位并且全部由数字组成的密码,其可能共有10000种组合,因此最多尝试10000次就能找到正确的密码。理论上利用这种方法可以破解任何一种密码,问题只在于如何缩短试误时间。因此有些人运用计算机来增加效率,有些人辅以字典来缩小密码组合的范围。
破译方法
  穷举法是一种针对于密码的破译方法。这种方法很像数学上的“完全归纳法”并在密码破译方面得到了广泛的应用。简单来说就是将密码进行逐个推算直到找出真正的密码为止。比如一个四位并且全部由数字组成其密码共有10000种组合,也就是说最多我们会尝试10000次才能找到真正的密码。利用这种方法我们可以运用计算机来进行逐个推算,也就是说用我们破解任何一个密码也都只是一个时间问题。
  当然如果破译一个有8位而且有可能拥有大小写数字、字母、以及符号的密码用普通的家用电脑可能会用掉几个月甚至更多的时间去计算,其组合方法可能有几千万亿重种组合。这样长的时间显然是不能接受的。其解决办法就是运用字典,所谓“字典”就是给密码锁定某个范围,比如英文单词以及生日的数字组合等,所有的英文单词不过10万个左右这样可以大大缩小密码范围,很大程度上缩短了破译时间
字母A、B、C、…Z等(26个)
  3.小写字母a、b、c、…z等(26个)
  4.特殊字符~、$、#、@、&、*等(33个)一般较少用
  5.用户自定义字符。
  如果一个多位数并且有可能包含以上所有字符的密码的组合方法一定多的惊人,相对来讲破译的时间也会长的没法接受,有时可能会长达数年之久。
字典
当然如果破译一个有8位而且有可能拥有大小写数字、字母、以及符号的密码用普通的家用电脑可能会用掉几个月甚至更多的时间去计算,其组合方法可能有几千万亿重种组合。这样长的时间显然是不能接受的。其解决办法就是运用字典,所谓“字典”就是给密码锁定某个范围,比如英文单词以及生日的数字组合等,所有的英文单词不过10万个左右这样可以大大缩小密码范围,很大程度上缩短了破译时间。
超级计算机也不在少数,例如IBM为美国军方制造的“飓风”就是很有代表性的一个。

迭代法
  迭代法也称辗转法,是一种不断用变量的旧值递推新值的过程,跟迭代法相对应的是直接法(或者称为一次解法),即一次性解决问题。迭代法又分为精确迭代和近似迭代。“二分法”和“牛顿迭代法”属于近似迭代法。
  迭代算法是用计算机解决问题的一种基本方法。它利用计算机运算速度快、适合做重复性操作的特点,让计算机对一组指令(或一定步骤)进行重复执行,在每次执行这组指令(或这些步骤)时,都从变量的原值推出它的一个新值。
  利用迭代算法解决问题,需要做好以下三个方面的工作:

  一、确定迭代变量。在可以用迭代算法解决的问题中,至少存在一个直接或间接地不断由旧值递推出新值的变量,这个变量就是迭代变量。

  二、建立迭代关系式。所谓迭代关系式,指如何从变量的前一个值推出其下一个值的公式(或关系)。迭代关系式的建立是解决迭代问题的关键,通常可以使用递推或倒推的方法来完成。

  三、对迭代过程进行控制。在什么时候结束迭代过程?这是编写迭代程序必须考虑的问题。不能让迭代过程无休止地重复执行下去。迭代过程的控制通常可分为两种情况:一种是所需的迭代次数是个确定的值,可以计算出来;另一种是所需的迭代次数无法确定。对于前一种情况,可以构建一个固定次数的循环来实现对迭代过程的控制;对于后一种情况,需要进一步分析出用来结束迭代过程的条件。
  例: 验证谷角猜想。日本数学家谷角静夫在研究自然数时发现了一个奇怪现象:对于任意一个自然数 n ,若 n 为偶数,则将其除以 2 ;若 n 为奇数,则将其乘以 3 ,然后再加 1 。如此经过有限次运算后,总可以得到自然数 1 。人们把谷角静夫的这一发现叫做“谷角猜想”。

  要求:编写一个程序,由键盘输入一个自然数 n ,把 n 经过有限次运算后,最终变成自然数 1 的全过程打印出来。
  分析: 定义迭代变量为 n ,按照谷角猜想的内容,可以得到两种情况下的迭代关系式:当 n 为偶数时, n=n/2 ;当 n 为奇数时, n=n*3+1 。用 QBASIC 语言把它描述出来就是:

  if n 为偶数 then

  n=n/2

  else

  n=n*3+1

  end if

  这就是需要计算机重复执行的迭代过程。这个迭代过程需要重复执行多少次,才能使迭代变量 n 最终变成自然数 1 ,这是我们无法计算出来的。因此,还需进一步确定用来结束迭代过程的条件。仔细分析题目要求,不难看出,对任意给定的一个自然数 n ,只要经过有限次运算后,能够得到自然数 1 ,就已经完成了验证工作。因此,用来结束迭代过程的条件可以定义为: n=1 。参考程序如下:

  cls

  input "Please input n=";n

  do until n=1

  if n mod 2=0 then

  rem 如果 n 为偶数,则调用迭代公式 n=n/2

  n=n/2

  print "—";n;

  else

  n=n*3+1

  print "—";n;

  end if

  loop

  end

一般用途:
1、迭代法:代法是用于求方程或方程组近似根的一种常用的算法设计方法。
2、递归
  递归是设计和描述算法的一种有力的工具,由于它在复杂算法的描述中被经常采用,为此在进一步介绍其他算法设计方法之前先讨论它。能采用递归描述的算法通常有这样的特征:为求解规模为N的问题,设法将它分解成规模较小的问题,然后从这些小问题的解方便地构造出大问题的解,并且这些规模较小的问题也能采用同样的分解和综合方法,分解成规模更小的问题,并从这些更小问题的解构造出规模较大问题的解。特别地,当规模N=1时,能直接得解。
  【问题】 编写计算斐波那契(Fibonacci)数列的第n项函数fib(n)。

  斐波那契数列为:0、1、1、2、3、……,即:

  fib(0)=0;

  fib(1)=1;

  fib(n)=fib(n-1)+fib(n-2) (当n>1时)。

  写成递归函数有:

int fib(int n)

  { if (n==0) return 0;

  if (n==1) return 1;

  if (n>1) return fib(n-1)+fib(n-2);

  }

3、组合问题
4、 背包问题

  问题描述:有不同价值、不同重量的物品n件,求从这n件物品中选取一部分物品的选择方案,使选中物品的总重量不超过指定的限制重量,但选中物品的价值之和最大。

  设n 件物品的重量分别为w0、w1、…、wn-1,物品的价值分别为v0、v1、…、vn-1。采用递归寻找物品的选择方案。设前面已有了多种选择的方案,并保留了其中总价值最大的方案于数组option[ ],该方案的总价值存于变量maxv。当前正在考察新方案,其物品选择情况保存于数组cop[ ]。假定当前方案已考虑了前i-1件物品,现在要考虑第i件物品;当前方案已包含的物品的重量之和为tw;至此,若其余物品都选择是可能的话,本方案能达到的总价值的期望值为tv。算法引入tv是当一旦当前方案的总价值的期望值也小于前面方案的总价值maxv时,继续考察当前方案变成无意义的工作,应终止当前方案,立即去考察下一个方案。因为当方案的总价值不比maxv大时,该方案不会被再考察,这同时保证函数后找到的方案一定会比前面的方案更好。
  对于第i件物品的选择考虑有两种可能:
  (1) 考虑物品i被选择,这种可能性仅当包含它不会超过方案总重量限制时才是可行的。选中后,继续递归去考虑其余物品的选择。
  (2) 考虑物品i不被选择,这种可能性仅当不包含物品i也有可能会找到价值更大的方案的情况。
  按以上思想写出递归算法如下:
 

 try(物品i,当前选择已达到的重量和,本方案可能达到的总价值tv
  {
  if(包含物品i是可以接受的)
  { 将物品i包含在当前方案中;
  if (i
  try(i+1,tw+物品i的重量,tv);
  else
  以当前方案作为临时最佳方案保存;
  恢复物品i不包含状态;
  }

  if (不包含物品i仅是可男考虑的)

  if (i

  try(i+1,tw,tv-物品i的价值);

  else
  以当前方案作为临时最佳方案保存;

  }

  为了理解上述算法,特举以下实例。设有4件物品,它们的重量和价值见表:

  物品 0 1 2 3

  重量 5 3 2 1

  价值 4 4 3 1

  并设限制重量为7。则按以上算法,下图表示找解过程。由图知,一旦找到一个解,算法就进一步找更好的佳。如能判定某个查找分支不会找到更好的解,算法不会在该分支继续查找,而是立即终止该分支,并去考察下一个分支。

  按上述算法编写函数和程序如下:

  【程序】

 # include

  # define N 100

  double limitW,totV,maxV;

  int option[N],cop[N];

  struct { double weight;

  double value;

  }a[N];

  int n;

  void find(int i,double tw,double tv)

  { int k;

  

  if (tw+a.weight<=limitW)

  { cop=1;

  if (i

  else

  { for (k=0;k

  option[k]=cop[k];

  maxv=tv;

  }

  cop=0;

  }

  

  if (tv-a.value>maxV)

  if (i

  else

  { for (k=0;k

  option[k]=cop[k];

  maxv=tv-a.value;

  }

  }

  void main()

  { int k;

  double w,v;

  printf(“输入物品种数\n”);

  scanf((“%d”,&n);

  printf(“输入各物品的重量和价值\n”);

  for (totv=0.0,k=0;k

  { scanf(“”,&w,&v);

  a[k].weight=w;

  a[k].value=v;

  totV+=V;

  }

  printf(“输入限制重量\n”);

  scanf(“”,&limitV);

  maxv=0.0;

  for (k=0;k find(0,0.0,totV);

  for (k=0;k

  if (option[k]) printf(“M”,k+1);

  printf(“\n总价值为%.2f\n”,maxv);

  } 

  递归的基本概念和特点
  程序调用自身的编程技巧称为递归( recursion)。
  一个过程或函数在其定义或说明中又直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。用递归思想写出的程序往往十分简洁易懂。
  一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
  注意:
  (1) 递归就是在过程或函数里调用自身;
  (2) 在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。
  
动态规划

  动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。

  动态规划问世以来,在经济管理、生产调度、工程技术和最优控制等方面得到了广泛的应用。例如最短路线、库存管理、资源分配、设备更新、排序、装载等问题,用动态规划方法比用其它方法求解更为方便。
应用举例
1、纳什均衡

 2、囚徒困境

  在博弈论中,含有占优战略均衡的一个著名例子是由塔克给出的“囚徒困境”(prisoners’ dilemma)博弈模型。该模型用一种特别的方式为我们讲述了一个警察与小偷的故事。假设有两个小偷A和B联合犯事、私入民宅被警察抓住。警方将两人分别置于不同的两个房间内进行审讯,对每一个犯罪嫌疑人,警方给出的政策是:如果两个犯罪嫌疑人都坦白了罪行,交出了赃物,于是证据确凿,两人都被判有罪,各被判刑8年;如果只有一个犯罪嫌疑人坦白,另一个人没有坦白而是抵赖,则以妨碍公务罪(因已有证据表明其有罪)再加刑2年,而坦白者有功被减刑8年,立即释放。如果两人都抵赖,则警方因证据不足不能判两人的偷窃罪,但可以私入民宅的罪名将两人各判入狱1年。表2.2给出了这个博弈的支付矩阵。
  表2.2 囚徒困境博弈 [Prisoner’s dilemma]
  虽然动态规划主要用于求解以时间划分阶段的动态过程的优化问题,但是一些与时间无关的静态规划(如线性规划、非线性规划),只要人为地引进时间因素,把它视为多阶段决策过程,也可以用动态规划方法方便地求解。
  动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。
博弈论
 博弈论 Game Theory
约翰·冯·诺依曼
  博弈论亦名“对策论”、“赛局理论”,属应用数学的一个分支, 目前在生物学,经济学,国际关系,计算机科学, 政治学,军事战略和其他很多学科都有广泛的应用。主要研究公式化了的激励结构间的相互作用。是研究具有斗争或竞争性质现象的数学理论和方法。也是运筹学的一个重要学科。 博弈论考虑游戏中的个体的预测行为和实际行为,并研究它们的优化策略。 表面上不同的相互作用可能表现出相似的激励结构(incentive structure),所以他们是同一个游戏的特例。其中一个有名有趣的应用例子是囚徒困境(Prisoner’s dilemma)。
  具有竞争或对抗性质的行为称为博弈行为。在这类行为中,参加斗争或竞争的各方各自具有不同的目标或利益。为了达到各自的目标和利益,各方必须考虑对手的各种可能的行动方案,并力图选取对自己最为有利或最为合理的方案。比如日常生活中的下棋,打牌等。博弈论就是研究博弈行为中斗争各方是否存在着最合理的行为方案,以及如何找到这个合理的行为方案的数学理论和方法。

A╲B 坦白 抵赖
坦白 -8,-8 0,-10
抵赖 -10,0 -1,-1

  我们来看看这个博弈可预测的均衡是什么。对A来说,尽管他不知道B作何选择,但他知道无论B选择什么,他选择“坦白”总是最优的。显然,根据对称性,B也会选择“坦白”,结果是两人都被判刑8年。但是,倘若他们都选择“抵赖”,每人只被判刑1年。在表2.2中的四种行动选择组合中,(抵赖、抵赖)是帕累托最优的,因为偏离这个行动选择组合的任何其他行动选择组合都至少会使一个人的境况变差。不难看出,“坦白”是任一犯罪嫌疑人的占优战略,而(坦白,坦白)是一个占优战略均衡。
3、价格战博弈
  现在我们经常会遇到各种各样的家电价格大战,彩电大战、冰箱大战、空调大战、微波炉大战……这些大战的受益者首先是消费者。每当看到一种家电产品的价格大战,百姓都会“没事儿偷着乐”。在这里,我们可以解释厂家价格大战的结局也是一个“纳什均衡”,而且价格战的结果是谁都没钱赚。因为博弈双方的利润正好是零。竞争的结果是稳定的,即是一个“纳什均衡”。这个结果可能对消费者是有利的,但对厂商而言是灾难性的。所以,价格战对厂商而言意味着自杀。

  4、污染博弈

回溯法

  回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
  1、回溯法的一般描述
  可用回溯法求解的问题P,通常要能表达为:对于已知的由n元组(x1,x2,…,xn)组成的一个状态空间E={(x1,x2,…,xn)∣xi∈Si ,i=1,2,…,n},给定关于n元组中的一个分量的一个约束集D,要求E中满足D的全部约束条件的所有n元组。其中Si是分量xi的定义域,且 |Si| 有限,i=1,2,…,n。我们称E中满足D的全部约束条件的任一n元组为问题P的一个解。
  解问题P的最朴素的方法就是枚举法,即对E中的所有n元组逐一地检测其是否满足D的全部约束,若满足,则为问题P的一个解。但显然,其计算量是相当大的。
  我们发现,对于许多问题,所给定的约束集D具有完备性,即i元组(x1,x2,…,xi)满足D中仅涉及到x1,x2,…,xi的所有约束意味着j(jj。因此,对于约束集D具有完备性的问题P,一旦检测断定某个j元组(x1,x2,…,xj)违反D中仅涉及x1,x2,…,xj的一个约束,就可以肯定,以(x1,x2,…,xj)为前缀的任何n元组(x1,x2,…,xj,xj+1,…,xn)都不会是问题P的解,因而就不必去搜索它们、检测它们。回溯法正是针对这类问题,利用这类问题的上述性质而提出来的比枚举法效率更高的算法。
  回溯法首先将问题P的n元组的状态空间E表示成一棵高为n的带权有序树T,把在E中求问题P的所有解转化为在T中搜索问题P的所有解。树T类似于检索树,它可以这样构造:
  设Si中的元素可排成xi(1) ,xi(2) ,…,xi(mi-1) ,|Si| =mi,i=1,2,…,n。从根开始,让T的第I层的每一个结点都有mi个儿子。这mi个儿子到它们的双亲的边,按从左到右的次序,分别带权xi+1(1) ,xi+1(2) ,…,xi+1(mi) ,i=0,1,2,…,n-1。照这种构造方式,E中的一个n元组(x1,x2,…,xn)对应于T中的一个叶子结点,T的根到这个叶子结点的路径上依次的n条边的权分别为x1,x2,…,xn,反之亦然。另外,对于任意的0≤i≤n-1,E中n元组(x1,x2,…,xn)的一个前缀I元组(x1,x2,…,xi)对应于T中的一个非叶子结点,T的根到这个非叶子结点的路径上依次的I条边的权分别为x1,x2,…,xi,反之亦然。特别,E中的任意一个n元组的空前缀(),对应于T的根。
  因而,在E中寻找问题P的一个解等价于在T中搜索一个叶子结点,要求从T的根到该叶子结点的路径上依次的n条边相应带的n个权x1,x2,…,xn满足约束集D的全部约束。在T中搜索所要求的叶子结点,很自然的一种方式是从根出发,按深度优先的策略逐步深入,即依次搜索满足约束条件的前缀1元组(x1i)、前缀2元组(x1,x2)、…,前缀I元组(x1,x2,…,xi),…,直到i=n为止。
  在回溯法中,上述引入的树被称为问题P的状态空间树;树T上任意一个结点被称为问题P的状态结点;树T上的任意一个叶子结点被称为问题P的一个解状态结点;树T上满足约束集D的全部约束的任意一个叶子结点被称为问题P的一个回答状态结点,它对应于问题P的一个解
  用回溯法解题的一般步骤:
  (1)针对所给问题,定义问题的解空间;
  (2)确定易于搜索的解空间结构;
  (3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
  回溯法C语言举例
  八皇后问题是能用回溯法解决的一个经典问题。
  八皇后问题是一个古老而著名的问题。该问题是十九世纪著名的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
  下面是用回溯法解决八皇后问题的C语言程序
  

#include
  #include
  int col[9]={0},a[9];
  int b[17],c[17];
  main()
  {
  int m,good;
  int i,j,k;
  char q;
  for(i=0;i<17;i++)
  {if(i<9) a[i]=1;
  b[i]=1;c[i]=1;
  }
  good=1;
  col[1]=1;
  m=1;
  while(col[0]!=1)
  {
  if(good)
  if(m==8)
  {
  for(i=1;i<9;i++)
  printf("col[%d] %d\n",i,col[i]);
  printf("input 'q' to quit\n");
  scanf("%c",&q);
  getchar();
  if(q=='q'||q=='Q') exit(0);
  while(col[m]==8)
  { m--;
  a[col[m]]=b[m+col[m]]=c[8+m-col[m]]=1;
  }
  a[col[m]]=b[m+col[m]]=c[8+m-col[m]]=1;
  col[m]++;
  }
  else
  {
  a[col[m]]=b[m+col[m]]=c[8+m-col[m]]=0;
  m++;
  col[m]=1;
  }
  else
  {
  while(col[m]==8)
  {
  m--;
  a[col[m]]=b[m+col[m]]=c[8+m-col[m]]=1;
  }
  col[m]++;
  }
  good=a[col[m]]&&b[m+col[m]]&&c[8+m-col[m]];
  }
  }

你可能感兴趣的:(算法常识)