深搜算法之石子划分问题

#!/usr/bin/python3
#文件:深搜算法之石子划分问题
#作者:巧若拙
#日期:2019年1月4日
'''
给出n堆石子,以及每堆石子数。请将它们分为两堆,使得这两堆的总石子数差最小。
输入n,以及每堆石子数,输出分为两堆后的最小差值。
比如,n=4,四堆石子分别有13,6,8,14颗,则可以分为13+8和14+6的两堆,它们的最小差为1。
分堆算法为
(1)求得所有石子数total,以及它的一半half;
(2)在所有石子堆中作适当选择,对每种选择方案,求不超过half的己选中堆中的石子总数的最大值mx。
所求即为(total-max)-max。
(3)以a(j)表示第j堆石子数;以b(j)表示第j堆石子是否被选中,
如果b(j)=1,表示第j堆被选中,如果b(j)=0表示第j堆没有被选中。
(4)各种方案的表达及次序如下:
以00…00(均不选中),00…01(只选中第n堆石子),00…10(只选中第n-1堆石子),
00…11(选中第n-1堆和第n堆石子),00…100(选中第n-2堆石子),
00…101(选中第n-2堆和第n堆石子),11…11(选中所有n堆石子)。
算法分析:
本题算法思想与“广搜算法之翻转棋子游戏”如出一辙。
初看本题,最容易想到的是穷举法,用包含n个元素的列表b分别表示堆石子的选择状态,b[i]=1表示选择第i堆石子,b[i]=0表示不选择。
可以穷举从b=[0,0,...0,0]到b=[1,1,...1,1],即从都不选择到都选择。
若把列表b的值对应整数t的n位二进制数,则相当于穷举从t=0到t=2^n-1。
比较直白的算法是循环遍历每一个t,将t的n位二进制数存储到列表b;然后遍历b,若b[i]=1则选择第i堆石子;
完成本选择模式后,判断是否有解,若本选择模式能获得更好的解,则更新最优解。
上述算法思想简单明了,但把整数t转化成二进制数并存储到列表b的操作比较耗费时间。
我们可以使用位运算来处理整数t的二进制数,这样就无需引入列表b,可以直接操作整数t了,效率有所提升。
穷举法是容易想到的算法,但是效率实在太低,我们可以使用深度优先搜索(回溯加剪枝)来实现同样的功能。
这样我们就分别用穷举和深搜两种算法实现了所需功能。
回到最初的想法,我们使用列表b来表示整数t的二进制数的每一位,是很直观的想法,只不过每次都需要把整数转化成二进制数比较耗费时间;
其实我们可以直接修改列表b的值来模拟整数t递增的过程,这样就不需要引入整数t及其位运算了,数据结构更清晰。
我也用这种数据结构实现了穷举和深搜两种算法。
进一步思考:当石子堆数量n太大时,对应的n位二进制数太大了,用穷举法和回溯法都不适合;
因为我们对每个石子堆的操作都是选择或者不选择,所以本题可以当做一个0-1背包问题来解决(当然石子的总数不能太大,否则需要太大的空间),
我们可以把背包的容量用石子总量的一半half来代替,物品的质量和价值都用每堆石子的数量来代替,最大价值就是不超过half的石子数量,
这样就可以套用0-1背包问题的基本模型来解决本题。
'''

#将整数t的n位二进制数存储到列表d
def binary_number(t, n): 
    d =[0] * n #高位补零,凑足n位
    i = n -1
    while t > 0:
        d[i] = t & 1         #相当于t % 2
        i, t = i - 1, t >> 1 #相当于t // 2
    return d

#穷举法解石子划分问题:最直观的想法,把整数t的n位二进制数存储到列表b,并根据列表b的值来确定选择模式
def divide_stones_0(a):
    n, max_s = len(a), 0
    for t in range(1<<n): #遍历从[0,1<<len(a)]的所有选择模式
        b = binary_number(t, n) #将整数t的n位二进制数存储到列表b
        s = 0
        for i in range(n):
            s += a[i] * b[i] #累计被选中的石子堆的数量
        if s > max_s and s <= half: #更新最优解
            max_s = s
            d = b.copy()  
    s1, s2 = 0, 0
    for i in range(len(d)):
        if d[i] == 0:
            s1 += a[i]
        else:
            s2 += a[i]
    print(s1, s2, d)
    return total - max_s * 2 #返回最小差值

#穷举法解石子划分问题:直接用位运算操作整数t的各个二进制数位,效率更高
def divide_stones_1(a):
    n, max_s = len(a), 0
    lib = tuple(map(lambda x: 1 << x, range(n-1,-1,-1)))#从高到低标记每个二进制位的1
    for t in range(1<<n): #遍历从[0,1<<len(a)]的所有选择模式
        s = 0
        for i in range(n):
            if t & lib[i] > 0: #t的二进制数第i位是1,则选择该堆石子 
                s += a[i]      #累计被选中的石子堆的数量
        if s > max_s and s <= half: #更新最优解
            max_s = s
            ans = t
    fun = lambda i: int(ans & lib[i] > 0)
    d = list(map(fun, range(n)))
    s1, s2 = 0, 0
    for i in range(len(d)):
        if d[i] == 0:
            s1 += a[i]
        else:
            s2 += a[i]
    print(s1, s2, d)
    return total - max_s * 2 #返回最小差值

#穷举法解石子划分问题:直接修改列表b的值来模拟整数t递增的过程,无需进行位运算,更好理解
def divide_stones_2(a):
    n, max_s = len(a), 0
    b =[0] * n #初始化所有的位都是0
    i = n - 1  #先选择第n堆
    while i >= 0: #遍历从[0,1<<len(a)]的所有选择模式
        s, b[i] = 0, 1       #将b[i]右侧的1都改成0,b[i]改成1,相当于二进制数递增1
        for i in range(len(b)):
            s += a[i] * b[i] #累计被选中的石子堆的数量
        if s > max_s and s <= half: #更新最优解
            max_s = s
            d = b.copy()
        i = n - 1
        while i >= 0 and b[i] == 1: #修改列表b的值来模拟整数t递增的过程,每次递增1
            b[i] = 0         
            i -= 1        #当b[i]==0时跳出循环,并将b[i]改成1,相当于二进制数递增1
    s1, s2 = 0, 0
    for i in range(len(d)):
        if d[i] == 0:
            s1 += a[i]
        else:
            s2 += a[i]
    print(s1, s2, d)
    return total - max_s * 2 #返回最小差值


#深搜法解石子划分问题:直接用位运算操作整数t的各个二进制数位,每层递归函数的选择模式都继承自上一层函数,每层只选择1堆石子
#参数介绍:a——列表,存储各堆石子数量;t——正整数,其二进制数代表当前选择模式;s——正整数,表示已经选择的石子数量。
def dfs_1(a, t, s):
    global max_s
    global ans
    if s > max_s: #更新最优解
        max_s, ans = s, t
    for i in range(len(lib)):
        if t & lib[i] == 0:     #只在第一个值为1的二进制位左侧设置1,以避免重复
            if s+a[i] <= half:  #已经超出半数就无需再选择新的石子堆了
                t |= lib[i]     #将整数t的第i个二进制位设置成1
                dfs_1(a, t, s+a[i])
                t &= ~lib[i]    #将整数t的第i个二进制位恢复成0
        else:
            break

#深搜法解石子划分问题:使用列表b存储当前翻转模式,每层递归函数的选择模式都继承自上一层函数,每层只选择1堆石子
#参数介绍:a——列表,存储各堆石子数量;b——列表,存储当前选择模式;s——正整数,表示已经选择的石子数量。
def dfs_2(a, b, s):
    global max_s
    global ans
    if s > max_s: #更新最优解
        max_s, ans = s, b.copy()
    for i in range(len(b)):
        if b[i] == 0:     #只在第一个值为1的二进制位左侧设置1,以避免重复
            if s+a[i] <= half:  #已经超出半数就无需再选择新的石子堆了
                b[i] = 1    #将整数t的第i个二进制位设置成1
                dfs_2(a, b, s+a[i])
                b[i] = 0     #将整数t的第i个二进制位恢复成0
        else:
            break


#记忆化搜索(备忘录算法)求0-1背包问题,b[n][c]初始化为0 
def dfs_3(n, c):
    global b
    if b[n][c] != 0:
        return b[n][c]

    max_s = 0
    if n == 1: #处理只给定了1堆石子的情形 
        if c >=  a[n-1]:
            max_s = a[n-1]
    else:
        if c < a[n-1]: #若装不下,则不装第n堆石子
            max_s = dfs_3(n-1, c)
        else:  #如果装得下,从装和不装两者中取最大值 
            max_s = max(dfs_3(n-1, c), dfs_3(n-1, c-a[n-1]) + a[n-1])
    b[n][c] = max_s
    return b[n][c]


def show(b, n, c): #利用列表b,输出选择石子堆情况
    d = [0] * n
    for i in range(n, 0, -1):
        if b[i][c] == b[i-1][c-a[i-1]] + a[i-1]: #装载了第i堆石子
            d[i-1] = 1
            c -= a[i-1]
    s1, s2 = 0, 0
    for i in range(len(d)):
        if d[i] == 0:
            s1 += a[i]
        else:
            s2 += a[i]
    print(s1, s2, d)

#动态规划:二维列表存储记录,b[i][j]初始化为0 
def dp_1(n, c):
    b = [[0 for i in range(total+1)] for i in range(n+1)]
    for i in range(1, n+1): #记录前i(1<=i<=n)堆石子装入容量为1-c的背包的最大价值(石子数量)
        for j in range(1, a[i-1]): #背包容量不够,不能装下第i堆石子
            b[i][j] = b[i-1][j]
        for j in range(a[i-1], c+1): #背包容量足够,可以选择装或不装第i堆石子
            b[i][j] = max(b[i-1][j], b[i-1][j-a[i-1]] + a[i-1])

    show(b, n, half)
    return total - b[n][c] * 2 #返回最小差值

#动态规划:优化的动态规划算法,使用2个一维列表代替二维列表,pre[j]和cur[j]均初始化为0 
def dp_2(n, c):
    #pre[j]相当于dp_1()中的b[i-1][j],cur[j]相当于b[i][j] 
    pre = [0] * (total + 1)
    cur = [0] * (total + 1)
    for i in range(1, n+1): #记录前i(1<=i<=n)堆石子装入容量为1-c的背包的最大价值(石子数量)
        for j in range(1, c+1):  #背包容量不够或不装更好,不选择第i堆石子
            if j < a[i-1] or pre[j] > pre[j-a[i-1]] + a[i-1]:
                cur[j] = pre[j]
            else: #背包容量足够且选择第i堆石子有更优解
                cur[j] = pre[j-a[i-1]] + a[i-1]
        for j in range(1, c+1): #复制上一行的数据到当前行 
            pre[j] = cur[j]

    return total - cur[c] * 2 #返回最小差值

#动态规划:优化的动态规划算法,一维列表存储记录,f[j]初始化为0  
def dp_3(n, c):
    f = [0] * (total + 1)
    for i in range(1, n+1): #记录前i(1<=i<=n)堆石子装入容量为1-c的背包的最大价值(石子数量)
        for j in range(c, a[i-1]-1, -1):  #须先求出列坐标j较大的F[j],故让循环变量j的值从大到小递减
            if f[j] < f[j-a[i-1]] + a[i-1]: #当(j < a[i-1] or f[j] >= f[j-a[i-1]] + a[i-1])时,f[j]的值不变
                f[j] = f[j-a[i-1]] + a[i-1]

    return total - f[c] * 2 #返回最小差值


with open('szhf.txt', 'r') as fin:
    for line in fin.readlines():
        print(line.strip())#依次读取每行
        a = list(map(int, line.strip().split(",")))
        total = sum(a)
        half = total // 2

        print(divide_stones_0(a))
        print(divide_stones_1(a))
        print(divide_stones_2(a))

        max_s, ans = 0, 0
        lib = tuple(map(lambda x: 1 << x, range(len(a)-1,-1,-1)))#从高到低标记每个二进制位的1
        dfs_1(a, 0, 0)
        fun = lambda i: int(ans & lib[i] > 0)
        d = list(map(fun, range(len(a))))
        print(d)
        print(total - max_s * 2)

        b =[0] * len(a) #初始化所有的位都是0
        max_s, ans = 0, b.copy()
        dfs_2(a, b, 0)
        print(ans)
        print(total - max_s * 2)

      
        b = [[0 for i in range(total+1)] for i in range(len(a)+1)]
        max_s = dfs_3(len(a), half)
        show(b, len(a), half)
        print(total - max_s * 2)

        print(dp_1(len(a), half))
        print(dp_2(len(a), half))
        print(dp_3(len(a), half))
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值