蓝桥杯 回路计算(状压DP)
-
问题网址https://www.lanqiao.cn/problems/1462/learning/?contest_id=73
-
不了解状压DP是什么的小伙伴可以先去B站看看这个视频
https://www.bilibili.com/video/BV1Z4411x7Kw/?spm_id_from=333.337.search-card.all.click -
楼栋是从0开始编号的。
-
先讲一个最简单的例子:二维数组F [i] [j],i 可以看做二进制,表示状态,而 j 表示 i 状态下的一种情况。
假设有5个点,i表示01101,经过了点3、2、0的一种状态,但是顺序是咋样的呢,现在停留在哪个点呢?不知道。那我们再来看j,假设j=2,说明当前停留在2这个点上,而F[13][2]=2,表示什么?就表示01101路径状态下,最后到达点2的情况有2种,不管起点是啥,情况就是2种。这样更不可能存在重复的情况,为什么呢?因为虽然都经过了3、2、0但是它们最终的落脚点不一样,排列的问题,所以肯定不存在重复。 -
接着我们再来讲一讲状态转移方程的问题。首先要明白一个问题,i状态是递增的,每次新的i的状态的各个情况都是未知的,都是新的状态,需要根据先前的状态来推敲出来。假设i=01101,那我们是不是要求它的各种落脚点情况?答案是显然的。也就是要求F[01101][01000]、F[01101][00100]、F[01101][00001]三个值的大小。(注意实际代码中存的都是10进制,从左到右为F[13][3]、F[13][2]、F[13][0],这边写成二进制好理解一些)《这三个值肯定先前不知道,因为i状态是新的》。怎么求呢?那肯定是利用先前的状态。比如既然要知道求F[01101][01000]这个,是不是可以求F[00101]这个状态下各种情况F[00101][00100]、F[00101][00001]这两种情况的落脚点是否能到点01000,一旦能到达是不是就状态就从00101变成了01101了?而且能到达的情况数不就等于F[00101][00001],也就是落脚点00001与01000存在边或者00100与01000存在边嘛。
-
总结一下就是来了一个新状态i,得到这个状态i各个可能落脚点的情况,然后就要求逐个分析各个落脚点情况的总数了,确定好一个落脚点j,把i状态中的这个落脚点j去除得到状态(i-j),这个状态中也有多个落脚点情况,只要多个落脚点中与j有边,那是不是状态i落脚点j的情况数是不是就等于状态(i-j) 落脚点x(与j有边) 的情况总数和。
直接看代码吧,讲的有点绕,大家可以先去B站看看有关状压DP的讲解,再结合代码看这道题。注释我尽量写的详细了一点。一起加油。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 21;
int n = 21;
LL ans;
// 存储两栋楼是否有通道的数组。
int st[N][N];
// 0到21个点全1,总共有2^21次方个数,1左移21位就等于2^21次方,刚刚好。
LL f[1 << N][N];
int gcd(int x, int y)
{
if (x % y == 0)
{
return y;
}
int res = gcd(y, x % y);
return res;
}
int main()
{
// 找到各个楼栋之间的通道
for (int i = 0; i < N; i++)
{
for (int j = 0; j < N; j++)
{
if (gcd(j + 1, i + 1) == 1)
{
st[i][j] = st[j][i] = 1;
}
}
// 同楼层之间通道设为0,其实这个也用不到,下面代码不会判断自己到自己是否存在边。
st[i][i] = 0;
}
// 状态1(表示经过了0号楼)落脚点为0,认为是有一种情况,不然全为0,DP怎么弄都是0。
f[1][0] = 1;
// 0到21个点全1,总共有2^21次方个数,1左移21位就等于2^21次方,刚刚好。
for (int i = 0; i < (1 << N); i++)
// i访问状态下。找它的各个落脚点j情况。
for (int j = 0; j < N; j++)
{
// 既然是状态i的落脚点,那肯定得在i里面有出现,所以i右移j位为1与上1为1说明存在。
// 那么经过状态 i 到达 j 的方案数等于从起点不经过点 j(也就是状态i中没有j) 到达全部与 j 点可达的点 k 方案数
if (i >> j & 1)
{
// 找到了其中一个落脚点j,那我就从i里面把落脚点j删去,从删去后的状态(假设状态为i-j)找这个状态是否有落脚点与j相连。
for (int k = 0; k < N; k++)
{
// 假设从i删去落脚点j,那么就不会再找j这个点(前面st[j][j]=0),这种情况直接pass了。
// 那就从删去的状态i-j中找这个状态的所有落脚点(只有 i >> k & 1 = 1 才是落脚点) 是否与j有边。
// 有边就好办了,直接i-j状态有边的落脚点k这个情况总数 先加到 i状态落脚点j这个情况里面
// 再找其他的i-j状态下与j有边的落脚点。
if (i >> k & 1 && st[j][k])
{
// 所有这边为加,因为i-j状态下与j有边的落脚点可能不止一个
// i - (1 << j) 表示的就是从i里面把落脚点j删去,也就是状态i-j,再在这个状态找到落脚点k与j有边,那就直接加进去。
f[i][j] += f[i - (1 << j)][k];
}
}
}
}
// 因为每个点都能通向0号大楼(从0开始)
// 那么我们可以认为全部点都经过了,也就是21个位置全为1,就等于(1<<N)-1,这个情况的每个落脚点总计20个落脚点,除了0号楼本身,
// 代表的意思就是从任意点走完全部21个点到达这20落脚点的总数和就等于回路的总数,因为每个点都能通向0号大楼(从0开始)。
for (int i = 1; i < N;i++)
ans += f[(1 << N) - 1][i];
// 有一个有意思的情况,为什么不直接输出f[(1<<N)-1][0]呢?因为它的起始点是0,不能自己又走到0,DP求得是非回路的。
// 正因为起始点是1,那么必须会经过1,所有不经过1的状态,那么它的各种情况都是0,这与初值的设定有关。
cout << f[5][0] << f[14][2] << f[14][14] << endl;
cout << ans << endl;
// cout << f[(1 << N) - 1][0] << endl;
return 0;
}