回溯的概念
回溯法有“通用解题法”之美称,是一种比枚举“聪明”的效率更高的搜索技术。
回溯法是一种试探求解的方法:通过对问题的归纳分析,找出求解问题的一个线索,沿着这一线索往前试探,若试探成功,即可得到解;若试探失败,就逐步往回退,换其他路线再往前试探。
回溯法可以形象地概括为“向前走,碰壁回头”,若再往前走不可能得到解,就回溯,退一步另找线路,这样可以省去大量的无效操作,提高搜索效率。
回溯法分类
1. 迭代回溯,该回溯是非递归回溯,通过迭代式实施回溯,框架描述:

2. 递归回溯,递归也能实现回溯。递归回溯通过递归尝试遍历问题的各个可能解的通路。当发现此路不能时,回溯到上一步,框架描述:

示例
1. 二组均分问题。
参加拔河比赛的16个同学的体重分别为:24 37 29 45 40 34 30 34 33 48 50 45 38 47 39 23,请把这16个同学分为两组,每组8人。为使比赛公平,要求计算每组8人的体重之和,使两组的体重之和相等。
请设计解决以上二组均分问题,并求出共有多少种不同的分组法。
一般地,对已知的2n个整数,确定把这些数分成两组,每组n个数,且每组数据的和相等。我们可以采用回溯法逐步实施调整。
对于已有的存储在b数组中的2n个数,求出总和s与其和的一半s1(若这2n个数的和s为奇数,显然无法分组)。为方便分组调整,设置数组a存储b数组的下表值,即a(i):1-2n。
考察b(1)所在的组,只要另从b(2)-b(n)中选取n-1个数。即定下a(1)=1。其余的a(i)(i=2,...,n)在2-2n中选取不重复的数。因组合与顺序无关,不防设:
![]()
从a(2)取2开始,以后a(i)从a(i-1)+1开始递增1取值直至n+i为止,这样可以避免重复。当a(n)已取值,计算s=b(1)+b(a(2))+...+b(a(n)),对和s进行判别:
若s=s1,满足要求,实现平方。
若s!=s1,则a(n)继续增1再试。如果a(n)已经增至2n,则回溯前一个a(n-1)增1再试。如果a(n-1)已增至2n-1,继续回溯。直至a(2)增至n+2时,结束。
若2n个整数之和s为奇数,二组均分问题肯定无解,即使s为偶数,二组均分问题也不一定有解。有解时,找到并输出所有解。没有解时,显示相关提示信息"无法实现平分"。
以下是上述问题的python代码实现:
b = list(map(int,input().split(" "))) #保存2n个元素
a = [] #保存2n个元素的下标
if(sum(b)%2==0):
print("上面整数的总和为{}".format(sum(b)))
s1 = sum(b)/2
else:
print("上面的整数和为奇数,无法平分")
a.append(0)
a.append(1)
i=1
m=0
while(1):
if(i==(len(b)//2-1)):
s = 0
for j in range(0,(len(b)//2)):
s+=b[a[j]]
if(s==s1):
m+=1
for j in range(0,(len(b)//2)):
print(b[a[j]],end=' ')
print()
else:
i+=1
a.append(a[i-1]+1)
continue
while(a[i]==(len(b)//2-1+i)):
i-=1
if(i>0):
a[i]+=1
else:
break
if(m>0):
print("共有{}种分法".format(m))
else:
print("无法实现二组均分")
2. 伯努利装错信封问题,在写信时将n封信装到n个不同的信封里,有多少种全部装错信封的情况?这个问题推广一下,就是错排问题,是组合数学中的问题之一。考虑一个有n个元素的排列,若一个排列中所有的元素都不在自己原来的位置上,那么这样的排列就称为原排列的一个错排。 n个元素的错排数记为D(n)。 研究一个排列错排个数的问题,叫做错排问题或称为更列问题。
装错信封问题就是求解n个元素全排列中"所有元素都错位"的子集。
求“所有元素都错位”子集,可在实现排列算法中加上"限制取位"的条件。排列组合算法在之前的博文中有。
设置一维数组a,a(i)在1-n中取值,当出现a(i)在自然位或者出现数字相同a(j)=a(i)时返回(j=1,2,...,n-1)。
当i<n时,还未取n个数,i增1后a(i)=1继续;
当i=n且最后一个元素不在自然位a(n)!=n时,输出一个全错位排列,并设置变量s统计错位排列的个数。
当a(i)<n时,a(i)增1继续
当a(i)=n时回溯或调整,知道i=1且a(1)=n时结束。
2.1 以下为上述问题的python代码实现(非递归):
n = int(input()) #输出信的封数
a = [0 for i in range(0,n+1)]
i = 1
a[i] = 2
s = 0
while(1):
t = 1
if(a[i]!=i):
for j in range(1,i):
if(a[j]==a[i]): #出现相同的元素返回
t=0
break
else:
t=0 #元素在自然位时t=0 表示信装对了的时候
if(t and i==n): #已经装完了n封,输出一个解
s+=1
for j in range(1,n+1):
print(a[j],end=" ")
print()
if(s%10==0):
print()
if(t and i<n):
i+=1
a[i]=1
continue
while(a[i]==n and i>0): #调整或者回溯
i-=1
if(i>0):
a[i]+=1
else:
break
print("全部装错的可能为{}种".format(s))
2.2 以下仍然为伯努利装错信封的python代码实现(递归),在排列的基础代码上的修改:
def put(k,n,a):
global s
if(k<=n):
for i in range(1,n+1):
a[k]=i #探索第k个数赋值i 也就是第k个信封放第i封信
if(a[k]!=k):
u=0
for j in range(1,k):
if(a[k]==a[j]): #出现了重复数字的情况
u=1 #第k个信封不能放第i封信 则u=1
else:
continue #a[i]在自然位时返回进行下一轮探索
if(u==0): # 第k个信封可以放第i封信 则检查是否所有的信都放到了信封里面
if(k==n): #如果所有的信都放完了 则输出解
s+=1
for j in range(1,n+1):
print(a[j],end=" ")
print()
if(s%10==0):
print()
else:
put(k+1,n,a) #如果信还没有放完,则探索下一个数
return s
if __name__=="__main__":
n = int(input()) #输入信的数量
s = 0 #记录完全装错的可能性
a = [0 for i in range(0, n + 1)]
put(1,n,a)
print("完全装错的可能有{}种".format(s))
回溯法是一种高效的搜索技术,被誉为通用解题方法。它通过试探前进并适时回溯来寻找问题的解决方案。文章介绍了回溯法的分类,包括迭代回溯和递归回溯,并提供了两个具体的应用实例:二组均分问题和伯努利装错信封问题的python代码实现。
682

被折叠的 条评论
为什么被折叠?



