回溯法

回溯法是一种高效的搜索技术,被誉为通用解题方法。它通过试探前进并适时回溯来寻找问题的解决方案。文章介绍了回溯法的分类,包括迭代回溯和递归回溯,并提供了两个具体的应用实例:二组均分问题和伯努利装错信封问题的python代码实现。

回溯的概念

回溯法有“通用解题法”之美称,是一种比枚举“聪明”的效率更高的搜索技术。

回溯法是一种试探求解的方法:通过对问题的归纳分析,找出求解问题的一个线索,沿着这一线索往前试探,若试探成功,即可得到解;若试探失败,就逐步往回退,换其他路线再往前试探。

回溯法可以形象地概括为“向前走,碰壁回头”,若再往前走不可能得到解,就回溯,退一步另找线路,这样可以省去大量的无效操作,提高搜索效率。

回溯法分类

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))

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值