为了规避不确定性,我们将需要枚举的东西放入状态。当不确定性太多的时候,我们就需要将它们压进较少的维数内。
常见的状态:
1. 状态跟某一个信息集合内的每一条都有关。(如 dp 套 dp)
2. 若干条精简而相互独立的信息压在一起处理。 (如每个数字是否出现)
在使用状压 dp 的题目当中,往往能一眼看到一些小数据范围的量,切人点明确。而有些题,这样的量并不明显,需要更深人地分析题目性质才能找到。
二进制数 S 从低(0-based)到高第 i 位的值:(S>>i)&1
二进制数 S 从低(0-based)到高第 i 位的值变为 1:S|(1<<i)
二进制数 S 从低(0-based)到高第 i 位的值变为 0:S&(~(1<<i)) 或 ~((~S)|(1<<i))
二进制数 S 从低(0-based)到高第 i 位的值取反:S^(1<<i)
for(int i=S;i!=0;i=(i-1)&S)
复杂度:
(2n-1) 的子集:2n
(2n-1) 的子集的子集和:3n
(2n-1) 的子集的子集的子集和:4n
……
题目大意:一个 n 个点的带权的有向图,求一条路径,使得这条路径经过每个点恰好一次,并且路径上边的权值和最小。n≤16。
Solution:
设 dp[i][S] 表示目前走到节点 i,已经经过节点的集合为 S 的最小边权。
枚举所有的边 (i,j)∈E,令 w(i,j) 表示它的边权。
if j∉S(因为要满足经过每个点恰好一次,即不能重复经过,所以要满足 j 没有被经过),dp[i][S]+w(i,j)→dp[j][S∪{j}]
若 S&(1<
初始化一个节点的路径 dp[i][1<<i]=0(节点编号为 0~n-1)或 dp[i][1<<(i-1)]=0(节点编号为 1~n),其他的设为 ∞。
把 j 加入到 S:S +/|/^ (1<<j)。因为已经确认了 S 的这一位是 0,则 +,|,^ 都是可以的。
时间复杂度:O(2n·n2)
题目大意:有一个 N*M 的网格图,已知若干个格子不能选、不能有相邻的格子被选。在不限制所选格子总数的前提下,求方案数。N,M≤12。
Solution:
由于限制在相邻的格子上,若按行或按列 dp,就不需要维护之前已经决策过的格子的状态,而是维护一行或一列的格子的状态。
令 dp[i][j] 表示目前 dp 到第 i 行,这一行的状态是 j,所有 1~i 行内的格子被选的方案数。
枚举 i+1 行的状态 k,判断 k 与 j 是否有冲突(是否出现某个格子的上面一个格子被选了的情况,即是否有相邻的格子被选),只需判断 j&k 是否为 1(j 的二进制数与 k 的二进制数是否有在相同的位置同时等于 1)。
dp[i][j]→dp[i+1][k]
预处理出没有相邻的两列同时被选的状态。在一行 12 个格子的网格图中,没有相邻的两列同时被选的合法的状态大概有 377 个。
int ans=0;
for(int i=0;i<(1<<12);i++){
bool flag=1;
for(int j=1;j<=11;j++)
if(((i>>(j-1))&1)&&((i>>j)&1)) flag=0;
if(flag) ++ans;
}
printf("%lld\n",ans);
//Output:377
所以通过预处理,可以通过此题。
Code:
#include
#define int long long
using namespace std;
const int N=13,M=(1<<12)+5,mod=1e9;
int n,m,a[N][N],p[N],f[N][M],ans;
bool v[M];
signed main(){
scanf("%lld%lld",&m,&n),f[0][0]=1;
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++){
scanf("%lld",&a[i][j]);
p[i]=(p[i]<<1)+a[i][j]; //记录第 i 行土地的状态
}
for(int i=0;i<(1<
} //记录状态 i 是否合法(预处理出没有相邻的两列同时被选的状态)
for(int i=1;i<=m;i++) //枚举行
for(int j=0;j<(1<<n);j++) //枚举这行的状态
if(v[j]&&(j&p[i])==j) //判断状态 j 是否合法以及是否在肥沃的土地上
for(int k=0;k<(1<<n);k++) //枚举上一行的状态
if(!(k&j)) f[i][j]=(f[i][j]+f[i-1][k])%mod;
for(int i=0;i<(1<<n);i++)
ans=(ans+f[m][i])%mod; //前 m 行的方案数累加
printf("%lld\n",ans);
return 0;
}
题目大意:给出一张 N 个点 M 条边的无向图。每个点有一个颜色。一条路径是合法的当且仅当它按顺序经过的节点的颜色序列写下来,同种颜色出现的位置是连续的。计算合法的经过每个节点恰好一次的路径条数。N≤100,Color≤10(颜色数),对于任意的 i,Cnt(v|Colorv=i)≤10(每种颜色的节点数)。
Solution:
既然同种颜色出现的位置是连续的。我们不妨将问题划分成两个小问题考虑:
第一个问题:将同种颜色内的串起来。
要求:(1)每个点恰好出现一次(2)相邻两点之间有边相连
令 dp[i][j][S] 表示从 i 号点出发,在 j 号点结束,经过的节点集合为 S 的路径条数。
for(int i=0;i
dp[i][p][S|(1<<p)]+=dp[i][j][S];
注:这里所有的节点编号,为了方便体现,直接写成了 i 之类的。事实上,需要开一个数组维护,把全局当中的颜色为当前颜色的节点转化到 0~k 的范围内。
第二个问题:将每种颜色串起来。
要求:(1)每种颜色恰好出现一次(则需记录颜色状态 2C)(2)相邻颜色的首尾节点间有边相连(则需记录终点 N)
对于一种颜色 c,我们使用 f[c][i][j] 来代表之前的 dp[全局中编号为 i 的点][全局中编号为 j 的点][(1<<k)-1]。
令 dp[i][S] 表示以节点 i 结尾,已经经过的颜色集合为 S 的总方案数。
for(int c=0;c
for(int j=0;j<k[c];j++) //枚举下一个颜色的起点
if(G[i][id[j]]) //当前的终点与下一个起点有边可以到达
for(int q=0;q<k[c];q++) //枚举下一个颜色的终点
dp[id[q]][S|(1<<c)]+=dp[i][S]*f[c][j][q];
题目大意:N 个人打淘汰赛。N 是 2 的幂次。给出两两单挑的胜负情况。一共有 N! 种赛程安排方案,对于每个人,你需要输出有多少种方案使他最终获胜。n≤16。
Solution:
令 dp[i][S] 表示编号为 i 的选手是选手集合 S 中的优胜者的方案数。
那么最后每个点的答案就是 dp[i][(1<<N)-1]
枚举 i 能够战胜的对手 j,然后将集合 S 等分为两个一个包含 i 一个包含 j 的集合。
for(int i=0;i
for(int j=0;j<N;j++) //枚举 i 所击败的对手 j
if(i!=j&&G[i][j]) //i 和 j 不是同一个人并且 i 能够击败 j
for(int k=(S-1)&S;k!=0;k=(k-1)&S) //枚举子集
dp[i][S]+=2LL*dp[i][k]*dp[j][S^k];
复杂度为 O(3n·n2),偏高,需要优化。
题目大意:
一个有 N 个元素且每个元素的取值都在 1~10 范围内的序列被称为好的当且仅当存在下标 x,y,z,w(0≤x<y<z<w≤N)
计数。并对 109+7 取模。3≤N≤40,1≤X≤5,1≤Y≤7,1≤Z≤5。
Solution:
做法 1:当你不知道如何优雅地设计状态/不知道状态的规模有多大时,不妨暴力一点。
因为 X≤5,Y≤7,Z≤5,所以 X+Y+Z≤5+7+5,即 X+Y+Z≤17,ax+ax+1+…+ay-1+ay+ay+1+…+az-1+az+az+1+…+aw-1≤17。
只需维护取值 1~10,和≤17 的序列。
#include
#define int long long
using namespace std;
int cnt;
void dfs(int x){
if(x>17) return ;
++cnt;
for(int i=1;i<=10;i++) dfs(x+i);
}
signed main(){
dfs(0),printf("%lld\n",cnt);
return 0;
}
//Output:130624
大约 105 个状态,做法就显而易见了。
一个 dp 序列当中的元素,每次只保留最靠后的值不超过 17 的元素队列。
dp[i][S][0/1] 表示当前 dp 到序列中第 i 个元素,队列的状态为 S,是否出现过题中所述条件。
一些优化:
不难观察得到,算法的复杂度瓶颈主要是由状态的 vector 形态带来的。
做法 2:思路和前面类似,只不过考虑换个形式表示“和不超过 17”这个状态。
如果我们将这“X+Y+Z”点贡献展开看成一条长度为 X+Y+Z 的格子,一个数可以占领 1~10 个格子。那么我们关心的即为位置在 X、X+Y、X+Y+Z 的三个格子是否是一个数所占领格子的末尾。我们考虑将一个数末尾标为 1,其他格子标为 0。
例子:X=Y=Z=2,后缀状态:
2+2+2
010101
1+1+2+2
110101
2+1+1+2
011101
1+2+2+1
101011
Code:
//开始的格子为 1 的写法
#include
#define int long long
using namespace std;
const int N=50,M=(1<<17)+5,mod=1e9+7;
int n,x,y,z,st,dp[N][M],sum=1,k,ans;
signed main(){
scanf("%lld%lld%lld%lld",&n,&x,&y,&z);
st=((1<<(x+y+z-1)))|(1<<(y+z-1))|(1<<(z-1)); //目标从后往前数的第 X+Y+Z、Y+Z、Z 的位置为 1
dp[0][0]=1;
for(int i=1;i<=n;i++){
sum=sum*10%mod; //计算总方案数(n 位数字,就有 10^n 种方案)
for(int S=0;S<(1<<(x+y+z));S++) //枚举状态
if(dp[i-1][S]&&(S&st)!=st)
for(int c=1;c<=10;c++){ //枚举第 i 位添加的数字
k=((S<<c)|(1<<(c-1)))&((1<<(x+y+z))-1);
//(S<<c)|(1<<(c-1)): 比如 4 的二进制数为 1000,4 后面加上 2 为 4 2,用 100010 表示,即将 4 的二进制向前两位再加上 10。
//…&((1<<(x+y+z))-1: 防止溢出
dp[i][k]=(dp[i][k]+dp[i-1][S])%mod;
}
}
for(int S=0;S<(1<<(x+y+z));S++)
if((S&st)!=st) ans=(ans+dp[n][S])%mod;
printf("%lld\n",(sum-ans)%mod); //总方案数减不符合条件的方案
return 0;
}
题目大意:给出一棵 N 个节点的树,每条树边有一个边权。.每次操作你可以将一条路径上的边权异或上某个值。问最少进行多少次操作可以达到边权全 0。N≤105,0≤边权≤15。
Solution:
首先定义一个点的点权为与其相邻的所有边的边权异或和。
容易证明边权全为 0 等价于点权全为0。
每次操作就可以看成是将任意两个点异或上同一个数。
思考一下要不要考虑同时异或一个不为 a 也不为 b 的数 x。
因为一共只有 16 种取值,我们先将重复的直接消去。剩下的信息只有 0~15 每个数字是否出现过,可以压成一个 216 的状态。
每次枚举一下要操作哪两个数。
题目大意:有 n 条鱼,刚开始它们都自由地生活在水里。接下来每一时刻会有两条鱼相遇。所有可能的“鱼对”之间都是等概率出现这一事件的。鱼 i 和鱼 j 相遇后,i 吃掉 j 的概率为 a[i][j]。 反之 j 吃掉 i 的概率为 1-a[i][j]。求每条鱼 i 活到最后的概率。N≤19。
Solution:
令 dp[S] 表示剩下集合中的鱼状态为 S 的概率。
初始化:dp[2n-1]=1(刚开始全部的鱼都存活)
对于任意 1≤i≤n,dp[2i] 为鱼 i 活到最后的概率。
按从大到小枚举状态 S(按时刻进行),设 c 为 S 中 1 的个数,则有 C(c,2) 个可能的“鱼对”。枚举第一条被选的鱼 i 以及第二条被选的鱼 j,这两条鱼相遇的概率为 1/C(c,2)。
最后输出所有的 dp[2i] 即可。
Code:C(c,2)=c*(c-1)/2
#include
#define int long long
using namespace std;
const int N=20,M=(1<<18)+5;
int n,x,cnt;
double a[N][N],dp[M];
signed main(){
scanf("%lld",&n),dp[(1<
cnt=0,x=S;
while(x) cnt+=x&1,x>>=1; //计算 S 中 1 的个数
for(int i=0;i<n;i++) if(S&(1<<i)) //枚举第一条鱼
for(int j=i+1;j<n;j++) if(S&(1<<j)){ //枚举第二条鱼
dp[S-(1<<j)]+=dp[S]*a[i][j]/(1.0*cnt*(cnt-1)/2); //i 吃掉 j
dp[S-(1<<i)]+=dp[S]*a[j][i]/(1.0*cnt*(cnt-1)/2); //j 吃掉 i
}
}
for(int i=0;i<n;i++)
printf("%.6lf%c",dp[1<<i],i==n-1?'\n':' ');
return 0;
}
题目大意:给出一个长度为 N 的序列,我们称两个数 x,y 是兼容的,当且仅当 x&y=0。对于序列中的每个数,你需要求出任意一个序列中与它兼容的数,或是声明不存在。N≤106,0<a[i]≤4·106。
Solution:
暴力做法:由于数值大小可以存下。令 M 为 >=max{a(i)} 的某个 2k-1。与序列中的某个数 x 兼容的所有数可以表示为 M xor x 的子集。做一下子集枚举,我们得到了一个 O(3k) 的做法。(本题中,k≤22), 这个做法要跑 3e10。
一点改进:令 f(S) 表示 S 的子集中是否有数出现过。F(x) 表示 x 是否在序列中出现过。
那么有:f(S)=F(S)|⋁i∈S f(S\i)
从小到大枚举 S, 每次计算 f 的值时就不需要枚举子集,只用枚举 S 中的元素 v 即可。还能剪枝,当 f(S) 为真时就不需要再枚举下去了。
题目大意:给出一个长度为 N 个序列,序列中的元素取值为 1~8。求一个最长的子序列满足:
题目大意:有一棵 N 个点的树, 1 号点为根节点。现在你知道 M 条信息:
现在你需要数满足条件的树形态数。N≤13,M≤100。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章