由NP完全问题引出动态规划——状态压缩DP

“ 所有部分都应当在非强制的情况下组合回一起。要记住,你重组的那部分原来就是你拆解的。因此,如果你不能让它们组合回来的话,那一定是有原因的。要想尽一切办法,除了用锤头。”
– IBM手册, 1925

Part 1

千禧年难题 P = N P P = NP P=NP

要想自然的理解什么是状态压缩DP,我们不妨先回到一切的起点—— N P NP NP完全问题。

N P NP NP完全问题作为计算机领域与数学领域里的一个超级难题, P P P 是否等于 N P NP NP。早在 2000 年 5 月的时候,Clay Institute 将这个问题列为了数学里的七大千禧问题之一,如果有人能证明出 P = N P P = NP P=NP P ≠ N P P ≠ NP P=NP,就会获得该机构整整 100 100 100 万美元的奖金。并且一旦证明出 P = N P P = NP P=NP 将会改变现有人类所有的知识体系。

千禧年七大数学难题

  1. NP完全问题
  2. 霍奇(Hodge)猜想
  3. 庞加莱(Poincare)猜想
  4. 黎曼(Riemann)猜想
  5. 杨-米尔斯(Yang-Mills)存在性和质量缺口
  6. 纳维叶-斯托克斯(Navier-Stokes)方程的存在性与光滑性
  7. 贝赫(Birch)和斯维讷通-戴尔(Swinnerton-Dyer)猜想

那么什么是NP完全问题呢?

首先我们需要明白两个简单的概念,所谓 P P P问题就是指可以在多项式时间内解决的问题,而 N P NP NP问题则是指可以在多项式时间内验证的问题

举个简单的例子吧。

2017年5月27日,在中国乌镇围棋峰会上,世界排名第一,号称人类之光的中国棋手柯洁与谷歌人工智能模型AlphaGo在全人类的瞩目下,进行了一场人类与AI的世纪大战,最终AlphaGo以3比0的总比分获胜,标志着AlphaGo的棋力已经超过人类职业围棋顶尖水平。

很多人由此恐慌,惊叹于AlphaGo对于每一步棋子的超强预测和计算能力。

难道它可以仅仅靠计算就可以推断出整个棋局的胜负状况?

答案是否定的,我们知道围棋是由181枚黑子和180枚白子组成,棋盘由纵横19道线形成的361个交叉点组成。每一个点都可能出现下黑子、下白子或空着不摆子三种情况。那么,361个交叉点,就有 2 361 2^{361} 2361变化的可能,即围棋的着数变化是 1 0 172 10^{172} 10172。这可是一个大得惊人的天文数字。

假设计算机 1 s 1s 1s可以计算 1 亿次 1亿次 1亿次,那么计算出围棋所有的变化情况就需要 1 0 172 / 1 0 8 s = 1 0 164 s ≈ 1 0 159 天 ≈ 2.7 × 1 0 156 年 10^{172}/10^8s=10^{164}s≈10^{159}天≈2.7\times10^{156}年 10172/108s=10164s101592.7×10156这甚至远远大于宇宙大爆炸到现在的时间!

那么AlphaGo是如何做到的?

实际上,AlphaGo每天都会自我博弈上千局,不断学习分析其他围棋高手的棋局,它并不是无敌的,也不是通过所谓的数学计算出整个棋局的情况,这也是当今AI发展的一个方向——通过深度学习的方式,让AI模型学习大量的数据集,从而实现预测的目的,但预测率正确率永远不可能达到 100 % 100\% 100%

说了这么多,你可能已经忘记我们其实是在讨论什么是 N P NP NP完全问题了,在这个例子中,我们发现如果要预测出围棋的所有可能棋局实际上是一件很难的事情,这便是是否可以在多项式时间内解决的含义,但是如果我们已经得到一个确定的棋局,去判断输赢却是一件很简单的事情。(例如解方程很难,但是把解带入方程验证解的正确性却很简单)对于验证围棋这件事,很明显它是一个 N P NP NP问题,既可以在多项式时间内验证的问题,但是预测围棋,它是不是一个 P P P问题呢?在我们的认知中它可能并不是一个 P P P问题,既不可以在多项式时间内解决的问题,因为强行预测的时间复杂度是极其夸张的!

话虽如此,如果数学家真的发现一种方法,可以很快的预测所有围棋的胜负状况,那么这便是所谓的NP完全问题的一种表现,既 P = N P P = NP P=NP

如果真的存在这种方法,既证明了 P = N P P = NP P=NP,那么人类的密码学大厦将会崩塌,因为破解密码是一个 P P P问题,而验证密码则是一个 N P NP NP问题。同样的,扑克,麻将,象棋,围棋,桥牌等等一切我们过去认为变化无穷无法轻易破解的难题都将会存在一个通解,这是一件很可怕的事情,会颠覆我们每一个人的认知,是对人类社会的一场大变革,每个人将不再有任何”隐私“可言,这便是为什么 N P NP NP完全问题位于数学七大千禧问题之首。

Part 2

著名的旅行商问题(TSP)

旅行商问题(TravelingSalesmanProblem,TSP)是一个经典的组合优化问题。经典的TSP可以描述为:一个商品推销员要去若干个城市推销商品,每两个城市之间都存在一条路线,该推销员从一个城市出发,需要经过所有城市后,回到出发地。应如何选择行进路线,以使总的行程最短。

在这个问题中假设一共由 n 个城市 n个城市 n个城市,那么路径的条数就是 n ⋅ ( n − 1 ) n\cdot(n-1) n(n1),所有的路径组合一共有 ( n − 1 ) ! (n-1)! (n1)!种情况,这实际上是一个很大的数, 100 ! = 9.332621544 × 1 0 157 100!=9.332621544 \times 10^{157} 100!9.332621544×10157,如果要枚举每一种情况,那么所花费的时间同样比宇宙大爆炸到现在的时间还要长。(实际上这只是走过去的情况,在旅行商问题中我们还需要返回起点,因此这个数值还要乘以二,实际时间比我们估计的还要恐怖)

有了上面的分析,我们知道实际上旅行商问题也是一个 N P NP NP问题,甚至比 N P NP NP问题所花费的时间还要多,因此我们称之为 N P − H a r d NP-Hard NPHard问题。

可以由下面的图像直观感受一下

在这里插入图片描述

缩放之后

在这里插入图片描述

可以看到y随着x的增大的变化是爆炸式的!

为了简化我们的计算,我们不妨将旅行商问题简化一下。

将经过所有城市后,回到出发地这个条件改为 ⇒ \Rightarrow 经过所有城市,到达最后一个城市即可

也就是说我们现在只需考虑走过去的情况,而不需要考虑走回来的情况。

至此,简化后的旅行商问题可以抽象等价于最短Hamilton路径问题(Acwing 91.)

既给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。

Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。

例图如下

在这里插入图片描述

蓝色五边形中的数字代表当前节点的编号,每条路径上的数字代表当前节点到下一个节点的权重,也就是路径长度。

我们的目标是求出从第0个节点走到第n-1个节点并且不重不漏的恰好经过每一个节点的最短路径长度(可以形象的理解为一笔画)

那么如何求解呢?

由上面的分析我们知道,这个问题实际上是一个 N P − H a r d NP-Hard NPHard问题,因此我们只能在 n n n比较小的情况下讨论一个速度相对较快的解法。(注意,此解法并非真正解决了此问题,只不过是很小维度上的一种特解)。

动态规划(Dynamic Programming)

动态规划作为求解决策过程(decision process)最优化的数学方法,简称DP,它可以使得很多决策问题得到一定程度上的简化。实际上在前人对于旅行商问题的研究中,提出了许许多多的算法,每种算法所适用的情况是不同的,它们都只能在一定的维度解决问题( n n n很小的情况下),我们在此讨论一种相对容易理解并且通用的算法,既动态规划的一个分支——状态压缩DP。

状态压缩DP的特点

状态压缩DP最显著的一个特点就是二进制表示状态,在上述问题中,我们可以将每个节点都看成二进制数的一个位。

在这里插入图片描述

( 00110 ) 2 (00110)_2 (00110)2表示第零个节点没有访问,第一个节点已经访问,第二个节点已经访问,第三个节点没有访问,第四个节点没有访问。

由动态规划的知识我们可以知道,我们最终要求解的状态实际上是由类似于上图的很多的中间状态转移而来。

至于为什么叫状态压缩,在这个例子中,实际上就是用二进制数表示每一个点的选择情况,最后将这个状态的集合压缩成一个二进制数进而再转化为十进制数。

简化版旅行商问题(最短Hamilton路径问题)的小维度解法

有了上面对于状态压缩DP的了解,我们终于可以开始正式求解困惑已久的难题了。一般动态规划问题的第一步就是如何定义我们的状态,在这里我们可以将 f ( i . j ) f(i.j) f(i.j)定义为当前已经走到了第 j j j个点,并且状态压缩之后为 i i i的情况下的最短路径长度。

在这里插入图片描述

状态转移方程为 f ( i , j ) = m i n ( f ( i , j ) , f ( ( i − ( 1 < < j ) , k ) + w ( k , j ) ) ) f(i,j) = min(f(i,j),f((i-(1<<j),k)+w(k,j))) f(i,j)=min(f(i,j),f((i(1<<j),k)+w(k,j)))其中 i − ( 1 < < j ) i-(1<<j) i(1<<j)表示从i这种状态中去除 j j j这个点,既让 j j j的二进制位由 1 1 1变成 0 0 0 w ( k , j ) w(k,j) w(k,j)表示 k k k j j j的权重(路径长度)。

在动态规划问题当中,时间复杂度等于状态数乘上决策数,状态数是 2 n ⋅ n 2^n \cdot n 2nn,决策数就是 n n n,所以总体的时间复杂度是 O ( n 2 ⋅ 2 n ) O(n^2 \cdot 2^n) O(n22n)。虽然这个数字看起来仍然大得夸张,但是仍然要比 n ! n! n!小很多。

举个简单的例子,如果 n = 10 n=10 n=10 n ! = 3628800 n!=3628800 n!=3628800 n 2 ⋅ 2 n = 102400 n^2 \cdot 2^n=102400 n22n=102400,两者相差了三十多倍。随着n的增大,两者的差距还会更大。

因此状态压缩DP对于此问题的简化是巨大的。

具体代码 时间复杂度 O ( n 2 ⋅ 2 n ) O(n^2 \cdot 2^n) O(n22n)

#include <bits/stdc++.h>
using namespace std;

const int N = 21,M = 1 << N;
int f[M][N],w[N][N],n;

int main()
{
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n;
    
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++) cin>>w[i][j];
    
    memset(f,0x3f,sizeof f); //初始化位INF
    f[1][0] = 0; //从第零个点开始走到第零个点 距离为0
    int U = 1 << n;
    for(int i=0;i<U;i++) //必须先枚举状态
        for(int j=0;j<n;j++)
            if((i>>j)&1) //保证f(i,j)合法 既i状态中存在j这个点
                for(int k=0;k<n;k++) //枚举中间点
                    if((i>>k)&1) //保证f(i,k)合法 既i状态中存在k这个点
                        f[i][j] = min(f[i][j],f[i-(1<<j)][k] + w[k][j]); //状态转移方程
    
    cout<<f[(1<<n)-1][n-1]<<endl;

    return 0;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xKazimierzx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值