【浮*光】#状态压缩# 状压DPの相关练习题
分类:
IT文章
•
2025-01-21 13:14:13
- 重难点:【p3160】局部最小值
-
重难点:【p3736】字符合并
状压DPの一般习题
T1:【p2704】炮兵阵地
- 一个N*M的地图,每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示)。
- 在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队)。
- 每个部队能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。
- 两支炮兵部队之间不能互相攻击,即任何炮兵部队都不在其他部队的攻击范围内。
- 在整个地图区域内最多能够摆放多少我军的炮兵部队。
这题确实是状压dp的经典啊...确实细节也算不太好处理...
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
//用每个m位二进制数表示一行的状态:
//每个二进制数的第k位为1表示在第k列上放置部队。
/*【p2704】炮兵阵地
一个N*M的地图,每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示)。
在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队)。
每个部队能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。
两支炮兵部队之间不能互相攻击,即任何炮兵部队都不在其他部队的攻击范围内。
在整个地图区域内最多能够摆放多少我军的炮兵部队。*/
//预处理出集合S,储存“相邻两个1的距离不小于3”的所有M位二进制数。
int sums[(1<<10)]; //sums[x]表示x的二进制表示中1的个数//int ff[109][1<<11][1<<11]; //2048*2048*109??? ---> 所以要用滚动数组啊qwq
//f[i][j][k]表示第i-1行的状态是j,第i行状态是k时,前i行最多放置的炮兵数。
int f[3][(1<<11)][(1<<11)]={0}; //【滚动数组】记录前两行的状态即可,即(%3=)0,1,2 。
int n,m,a[109],anss=0; //a[109]记录每行的初始可行状态(是二进制数转化为十进制的数)
void reads(int &x){ int fx=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=fx; }
int getsum(int S){ //当前状态 S 里面包含几个 1
int tot=0; while(S){if(S&1) tot++; S>>=1;} return tot;
} //【坑点!!】这个一定要写成函数,要不然会TLE...
int main(){
reads(n); reads(m); char ss;
for(int i=0;i<n;i++) //注意,状压DP中最好都用0开始
for(int j=0;j<m;j++) //用a[i]记录每行的初始可行状态
cin>>ss,a[i]<<=1,a[i]+=(ss=='H'?1:0);
//二进制数a[i]每次向前移一位,记录第i行中障碍的位置
memset(sums,0,sizeof(sums)); //记录每种行状态要放置的部队数
for(int i=0;i<(1<<m);i++) sums[i]=getsum(i);
for(int i=0;i<(1<<m);i++) //【预处理】初始化第一行(行数编号是0)
if(!(i&a[0] || (i&(i<<1)) || (i&(i<<2)))) //判断‘行放置情况’有没有冲突
f[0][i][0]=sums[i]; //第一行要特殊判定,相当于赋初始值
for(int j=0;j<(1<<m);j++) //【预处理】初始化第二行(行数编号是1)
for(int k=0;k<(1<<m);k++) //j是上一行,k是这一行
if(!(j&k || j&a[0] || k&a[1] || (j&(j<<1)) || (j&(j<<2))
|| (k&(k<<1)) || (k&(k<<2)) ) )
f[1][j][k]=sums[k]+sums[j]; //第二行要特殊判定(因为没有2-2=0行)
for(int i=2;i<n;i++) //枚举行数
for(int j=0;j<(1<<m);j++){ //【预处理】“相邻两个1的距离不小于3”的所有M位二进制数
if(j&a[i-1] || (j&(j<<1)) || (j&(j<<2))) continue; //‘行放置状态’冲突
for(int k=0;k<(1<<m);k++){ //j是上一行(i-1),k是这一行(i)
if(k&a[i] || j&k || (k&(k<<1)) || (k&(k<<2))) continue; //特判
for(int FL=0;FL<(1<<m);FL++){ //FL是上上一行
if(FL&j || FL&k || FL&a[i-2] || (FL&(FL<<1)) || (FL&(FL<<2))) continue;
f[i%3][j][k]=max(f[i%3][j][k],f[(i-1)%3][FL][j]+sums[k]); //转移:max放置数
}
}
}
for(int j=0;j<(1<<m);j++) for(int k=0;k<(1<<m);k++) anss=max(anss,f[(n-1)%3][j][k]);
cout<<anss<<endl; return 0; //倒数两行可以是任意状态(可行才会有值),求出最终的max放置个数
}
后来自己重新打了一遍,果然还是把循环里的 j++ 写成了 i++ ...
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
//用每个m位二进制数表示一行的状态:
//每个二进制数的第k位为1表示在第k列上放置部队。
/*【p2704】炮兵阵地
一个N*M的地图,每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示)。
在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队)。
每个部队能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。
两支炮兵部队之间不能互相攻击,即任何炮兵部队都不在其他部队的攻击范围内。
在整个地图区域内最多能够摆放多少我军的炮兵部队。*/
int sums[(1<<10)]; //sums[x]表示x的二进制表示中1的个数
int f[3][(1<<11)][(1<<11)]={0}; //【滚动数组】记录前两行的状态即可,即(%3=)0,1,2 。
int n,m,a[109],anss=0; //a[109]记录每行的初始可行状态(是二进制数转化为十进制的数)
void reads(int &x){ int fx=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=fx; }
int get_cnt(int S){ //当前状态 S 里面包含几个 1
int tot=0; while(S){if(S&1) tot++; S>>=1;} return tot;
} //【坑点!!】这个一定要写成函数,要不然会TLE...
int main(){
reads(n); reads(m); char ss;
for(int i=0;i<n;i++) for(int j=0;j<m;j++) cin>>ss,a[i]<<=1,a[i]+=(ss=='H'?1:0);
for(int i=0;i<(1<<m);i++) sums[i]=get_cnt(i); //先处理出sums
for(int i=0;i<(1<<m);i++) for(int j=0;j<(1<<m);j++)
if( ! ( (i&a[1]) || (j&a[0]) || (i&j) || (i&(i<<1)) //只需预处理第二行
|| (i&(i<<2)) || (j&(j<<1)) || (j&(j<<2)) ) ) f[1][i][j]=sums[j]+sums[i];
for(int i=2;i<n;i++) //注意这里要从i=2开始
for(int j=0;j<(1<<m);j++){ //上一行
if(j&a[i-1]||(j&(j<<1))||(j&(j<<2))) continue;
for(int k=0;k<(1<<m);k++){ if(j&k) continue;
if(k&a[i]||(k&(k<<1))||(k&(k<<2))) continue;
for(int LS=0;LS<(1<<m);LS++){ if((j&LS)||(k&LS)) continue;
if(LS&a[i-2]||(LS&(LS<<1))||(LS&(LS<<2))) continue;
f[i%3][k][j]=max(f[i%3][k][j],f[(i-1)%3][j][LS]+sums[k]); }
}
}
for(int j=0;j<(1<<m);j++) for(int k=0;k<(1<<m);k++) anss=max(anss,f[(n-1)%3][j][k]);
cout<<anss<<endl; return 0; //倒数两行可以是任意状态(可行才会有值),求出最终的max放置个数
}
【p2704】炮兵阵地 //重构之后的代码
T2:【p1896】互不侵犯
- 国王能攻击到它上下左右、以及左上左下右上右下八个方向上一个格子。
- 在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。
f[i][j][s]表示考虑到了第i行,已经放置了j个国王,上一行的放置情况为s。
枚举这一行的放置情况t。要满足:t是合法的,并且可以与s作相邻行。
转移方程:f[i][j+count(t)][t]+=f[i-1][j][s]; //【方案数DP】+【状压DP】
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【洛谷p1896】互不侵犯
国王能攻击到它上下左右、以及左上左下右上右下八个方向上一个格子。
在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。*/
//【方案数DP】+【状压DP】
//f[i][j][s]表示考虑到了第i行,已经放置了j个国王,上一行的放置情况为s。
//枚举这一行的放置情况t。要满足:t是合法的,并且可以与s作相邻行。
//转移方程:f[i][j+count(t)][t]+=f[i-1][j][s];
//count(t)表示t的二进制表示下1的数量,就是这一行的国王数。
void reads(int &x){ int fx=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=fx; }
int n,m,cnt,MAX; ll f[20][400][1000];
int can[1000],sums[2000]; //行能到达的状态,每个状态的国王数
int get_cnt(int x){ int ret=0; while(x) ret+=(x&1),x>>=1; return sums[cnt]=ret; }
int main(){
int l,x,y; ll ans=0; reads(n),reads(m); MAX=(1<<n)-1; //二进制状态总数
for(int i=0;i<=MAX;i++) //预处理可行的‘行状态’,初始化dp数组的第一行
if(!(i&(i<<1))) can[++cnt]=i,f[1][get_cnt(i)][cnt]=1;
for(int i=2;i<=n;i++) for(int j=1;j<=cnt;j++){ x=can[j]; //此行的状态
for(int k=1;k<=cnt;k++){ y=can[k]; //下一行的状态
if((x&y)||(x&(y<<1))||(x&(y>>1))) continue; //不能相邻
for(int l=0;l<=m;l++) f[i][l+sums[j]][j]+=f[i-1][l][k]; //枚举之前已经放的个数
} //转移:f[i][count(t)+上行总国王数][状态]+=f[i-1][上行总国王数][上行状态];
} for(int i=1;i<=cnt;i++) ans+=f[n][m][i]; printf("%lld
",ans);
}
【p1896】互不侵犯
T3:【p3052】摩天大楼的奶牛
- 给出n个物品,体积分别为w[i],现把其分成若干组,
- 要求每组总体积<=W,问最小分组。(n<=18)。
f[i][j]表示已经分了i组,n个物品的选择状态是j时,当前组中的min体积。
如果存在d[i][j]这种方案,枚举不属于状态j的物品k,转移到d[i/i+1][j&(1<<k)]。
从前往后搜索,得到第一个存在j=2^n-1的方案即可。
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;
/*【p3052】摩天大楼的奶牛
给出n个物品,体积分别为w[i],现把其分成若干组,
要求每组总体积<=W,问最小分组。(n<=18)。 */
/*【分析】状压DP
f[i][j]表示已经分了i组,n个物品的选择状态是j时,当前组中的min体积。
如果存在d[i][j]这种方案,枚举不属于状态j的物品k,转移到d[i/i+1][j&(1<<k)]。
从前往后搜索,得到第一个存在j=2^n-1的方案即可。 */
void reads(int &x){ int fx=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=fx; }
int f[20][(1<<20)]; int n,m,w[20];
int main(){
reads(n); reads(m); for(int i=0;i<n;i++) reads(w[i]);
for(int i=0;i<=n;i++) //分组数
for(int j=0;j<=(1<<n)-1;j++) //每个状态
f[i][j]=0x3f3f3f3f; //初始化为+∞
for(int i=0;i<=n;i++) f[1][1<<i]=w[i]; //边界:第1组放任意一个物品
for(int i=0;i<=n;i++) //组数从0~n
for(int j=0;j<=(1<<n)-1;j++)
if(f[i][j]!=0x3f3f3f3f) //如果该状态存在【存在性】
for(int k=0;k<n;k++){ //枚举不属于状态j的物品k
if((j&(1<<k))!=0) continue; //k属于j,舍去 ↓↓↓判断是否更新f[i][j|(1<<k)]
if(f[i][j]+w[k]<=m) f[i][j|(1<<k)]=min(f[i][j|(1<<k)],f[i][j]+w[k]);
else f[i+1][j|1<<k]=w[k]; //上一组存不下,要多存一组
}
for(int i=0;i<=n;i++) //组数从小到大
if(f[i][(1<<n)-1]!=0x3f3f3f3f) //只要有满足全选的状态的方案
{ printf("%d
",i); break; } //输出组数并退出
}
【p3052】摩天大楼的奶牛
T4:【p3622】动物园
- 每个小朋友站在大围栏圈的外面,可以看到连续的 5 个围栏。
- 你得到了所有小朋友喜欢和害怕的动物信息。
- 当下面两处情况之一发生时,小朋友就会高兴:
- 1.至少有一个他害怕的动物被移走
- 2.至少有一个他喜欢的动物没被移走
- 输出一个数,表示最多可以让多少个小朋友高兴。
考虑到不同的小朋友看见的围栏范围可能相同,要预处理num[pos][s]:
表示从第pos个开始的五个围栏移走状态为s(全满则为15=2^4-1)时,满意的人数。
f[i][s]表示‘枚举到第i个围栏,且[i,i+5]的围栏移走状态为s’时的最多满意人数。
则f[i][s]可以由第i-1个围栏移走和不移走两种状态转移得来:
- f[i][s]=max(f[i−1][(s&15)<<1],f[i−1][(s&15)<<1∣1])+num[i][s];
注意:在dp之前先枚举前五个的状态state。避免环形问题。
那么此时必须满足s=state才是有效状态,更新答案。//【状压DP】【环形问题】
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;
/*【p3622】动物园
每个小朋友站在大围栏圈的外面,可以看到连续的 5 个围栏。
你得到了所有小朋友喜欢和害怕的动物信息。
当下面两处情况之一发生时,小朋友就会高兴:
1.至少有一个他害怕的动物被移走
2.至少有一个他喜欢的动物没被移走
输出一个数,表示最多可以让多少个小朋友高兴。 */
/* 考虑到不同的小朋友看见的围栏范围可能相同,要预处理num[pos][s]:
表示从第pos个开始的五个围栏移走状态为s(全满则为15=2^4-1)时,满意的人数。
f[i][s]表示‘枚举到第i个围栏,且[i,i+5]的围栏移走状态为s’时的最多满意人数。
则f[i][s]可以由第i-1个围栏移走和不移走两种状态转移得来:
f[i][s]=max(f[i−1][(s&15)<<1],f[i−1][(s&15)<<1∣1])+num[i][s];
注意:在dp之前先枚举前五个的状态state。避免环形问题。
那么此时必须满足s=state才是有效状态,更新答案。//【状压DP】【环形问题】 */
void reads(int &x){ int fx=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=fx; }
const int N=50019; int n,m,ans,f[N][40],num[N][40];
int main(){
reads(n),reads(m);
for(int i=1,E,a,b,t;i<=m;i++){
reads(E),reads(a),reads(b);
int l=0,d=0; //分别记录不喜欢的和喜欢的对应的状态
for(int j=1;j<=a;j++) //不喜欢的,相应位置上为1
reads(t),t=(t-E+n)%n,l|=1<<t;
for(int j=1;j<=b;j++) //喜欢的,相应位置上为1
reads(t),t=(t-E+n)%n,d|=1<<t;
for(int j=0;j<32;j++) //num表示‘从E开始的5个围栏移走状态为s时,满意的人数’
if((j&l)||(~j&d)) num[E][j]++; //j为要选择移走的状态
}
for(int i=0;i<32;i++){
memset(f[0],128,sizeof(f[0])),f[0][i]=0; //初始-inf,设置dp起点
for(int j=1;j<=n;j++) //枚举围栏数
for(int s=0;s<32;s++) //枚举移走状态,从上一位置移走和不移走两种状态转移
f[j][s]=max(f[j-1][(s&15)<<1],f[j-1][(s&15)<<1|1])+num[j][s];
if(ans<f[n][i]) ans=f[n][i]; //更新ans
}
printf("%d
",ans); return 0; //输出ans:最多的满意人数
}
【p3622】动物园
T5:【p1523】旅行商
- 现在笛卡尔平面上有n(n<=1000)个点,每个点的坐标为(x,y)(为整数)。
- 任意两点之间到达的代价为这两点的欧几里德距离,求最短bitonic tour(双调旅程)。
- 即从最左点开始,严格地从左到右直至最右点,然后严格地从右到左直至出发点。
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
#include <set>
using namespace std;
typedef long long ll;
/*【洛谷p1523】旅行商(NPC难题简化版)
现在笛卡尔平面上有n(n<=1000)个点,每个点的坐标为(x,y)(为整数)。
任意两点之间相互到达的代价为这两点的欧几里德距离,求最短bitonic tour(双调旅程)。
即从最左点开始,严格地从左到右直至最右点,然后严格地从右到左直至出发点。 */
void reads(int &x){ int fx=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} x*=fx; }
/*
int dist[50][50],n,dp[1<<12][20];
int main(){
while(scanf("%d",&n)!=EOF&&n!=0){
n++; for(int i=0;i<n;i++) for(int j=0;j<n;j++) scanf("%d",&dist[i][j]);
for(int k=0;k<n;k++) for(int i=0;i<n;i++) for(int j=0;j<n;j++)
if(dist[i][j]>dist[i][k]+dist[k][j]) dist[i][j]=dist[i][k]+dist[k][j];
for(int i=0;i<(1<<12);i++) for(int j=0;j<20;j++) dp[i][j]=2e9; dp[0][0]=0;
for(int i=1;i<(1<<n);i++) for(int j=0;j<n;j++) //枚举状态j作为终点的情况
if(((1<<j)&i)) for(int k=0;k<n;k++) //枚举上一次的位置k
if( ! ( (i-(1<<j)) || ((1<<k)&(i-(1<<j))) ) ) // k -> j
dp[i][j]=min(dp[i][j],dp[i-(1<<j)][k]+dist[k][j]);
cout<<dp[(1<<n)-1][0]<<endl; //限定终点0
}
} */
//以上是点少的时候的状压解法,洛谷的题目是不能过的
//旅行商问题,不过要求了只能单向走,所以可以简化。
//双向的问题转化为:假设有两个人一起从西往东走,走过的点不能重复。
//f[i][j]表示第一个人走到i,第二个人走到j的最短路径。
//能够发现,第j+1个点不是被第一个人走,就是被第二个人走。
//方程1:f[i][j+1]=min{f[i][j]+d[j][j+1]}
//方程2:f[j][j+1]=min{f[i][j]+d[i][j+1]}
struct node{ double x,y; }a[1019];
double d[1019][1019],f[1019][1019];
bool cmp(node a,node b){ return a.x<b.x; }
double dist(node a,node b)
{ return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); }
int main(){
int n; reads(n); for(int i=0;i<n;i++)
scanf("%lf%lf",&a[i].x,&a[i].y); sort(a,a+n,cmp);
for(int i=0;i<n;i++) for(int j=i+1;j<n;j++)
d[i][j]=dist(a[i],a[j]),f[i][j]=1e30; //预处理+初始化
f[0][1]=d[0][1]; //dp起点
for(int i=0;i<n;i++) for(int j=i+1;j<n;j++)
//j+1点不是被第一个人走,就是被第二个人走
f[i][j+1]=min(f[i][j+1],f[i][j]+d[j][j+1]), //被j走
f[j][j+1]=min(f[j][j+1],f[i][j]+d[i][j+1]); //被i走
double ans=1e30; //当一个人走到终点时,
for(int j=0;j<n-1;j++) //枚举另一个人的最后一步
ans=min(ans,f[j][n-1]+d[j][n-1]); printf("%.2lf
",ans);
}
【洛谷p1523】旅行商(NPC难题简化版)
T6:【UVa1252】20个问题
- 有n个物体,m(m≤11)个特征。每个物体用一个m位01串表示特征具备与否。
- 我在心里想一个物体(一定是这n个物体之一),你来猜,每次可以询问一个特征。
- 然后我会告诉你是否具备,如果你采用最优策略,最少需要询问几次能保证猜到?
用集合s表示已经询问的特征集,a表示已确认具备的特征集,s中除去a的特征是不具备的。
设f[s][a]表示已经询问了特征集s,其中W所具备的特征集为a时,还需要询问的最小次数。
枚举下一次提问的对象是特征k,则询问次数为:【每次取考虑所有的k得到的最小值】
f[s+{k}][a+{k}] = max{ f[s+{k}][a+{k}] , f[s+{k}][a] }+1 //a具备或不具备,每次询问次数+1。
先把满足每个s和a条件的{物体个数}统计出来,记录在cnt[s][a] //预处理
- 其中 s:我们已经问过的特性集; a:具备的特征集。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<set>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
void reads(int &x){
int fx=1;x=0;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')fx=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
x=x*fx; //正负号
}
/*【UVa1252】20个问题
有n(n≤128)个物体,m(m≤11)个特征。每个物体用一个m位01串表示特征具备与否
我在心里想一个物体(一定是这n个物体之一),你来猜,每次可以询问一个特征
然后我会告诉你是否具备,如果你采用最优策略,最少需要询问几次能保证猜到? */
/*【分析】用集合s表示已经询问的特征集,a表示已确认具备的特征集,s中除去a的特征是不具备的。
设f[s][a]表示已经询问了特征集s,其中W所具备的特征集为a时,还需要询问的最小次数。
枚举下一次提问的对象是特征k,则询问次数为:【每次取考虑所有的k得到的最小值】
f[s+{k}][a+{k}] = max{ f[s+{k}][a+{k}] , f[s+{k}][a] }+1 //a具备或不具备,每次询问次数+1。*/
/* 先把满足每个s和a条件的{物体个数}统计出来,记录在cnt[s][a] //预处理 */
// s:我们已经问过的特性集; a:具备的特征集
const int N=11;//物体数
int kase=0,n,m; char objects[129][129];
int vis[1<<N][1<<N],f[1<<N][1<<N]; //f[s][a]表示满足s、a时,还需要询问的最小次数
int cnt[1<<N][1<<N]; //满足s和a条件的{物体个数}
int dp(int s,int a){
if(cnt[s][a]<=1) return 0; //找到了,不用问了
if(cnt[s][a]==2) return 1; //二选一,还需要一次
int& ans=f[s][a]; //记录f[s][a]到ans中,便于计算
if(vis[s][a]==kase) return ans; //记忆化搜索
vis[s][a]=kase; //巧妙:对于每一组,不用清空vis
ans=m; for(int k=0;k<m;k++) if(!(s&(1<<k))){ //枚举特征,找s中没包含的一个特征
int s2=s|(1<<k),a2=a|(1<<k); //计算特征k对s、a的改变
if(cnt[s2][a2]>=1&&cnt[s2][a]>=1) //还需要特殊判断
ans=min(ans,max(dp(s2,a2),dp(s2,a))+1); } // k为真,为假的两种情况
return ans; //满足集合的最少询问次数
}
int main(){
while(scanf("%d%d",&m,&n)==2&&n&&m){
kase++; //↓输入每个物品的状态
for(int i=0;i<n;i++) scanf("%s",objects[i]);
for(int s=0;s<(1<<m);s++){ //枚举所有状态
for(int a=s;a;a=(a-1)&s) cnt[s][a]=0; cnt[s][0]=0; //清空
} for(int i=0;i<n;i++){ int features=0; //每个物品
for(int f=0;f<m;f++) //对于某个特征
if(objects[i][f]=='1') features|=(1<<f); //记录它的所有1
for(int s=0;s<(1<<m);s++) cnt[s][s&features]++; //求交集,得到物品i含有的状态a
} printf("%d
",dp(0,0)); //cnt记录相同特征的物品出现的次数
}
}
【UVa1252】20个问题
T7:【p1278】单词游戏
f[S][c] 即选择状态为S、且末位字符为c时是否可行(bool)
因为单词只和首尾有关,所以转移的时候可以巧妙地用到 a[last].r=a[i].l;
#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<set>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
/*【p1278】单词游戏
词典中单词接龙,问能接到的最大长度。*/
//f[S][c]即选择状态为S、且末位字符为c时是否可行(bool)
//因为单词之和首尾有关,所以转移的时候可以巧妙地用到a[last].r=a[i].l;
void reads(int &x){
int fx=1;x=0;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')fx=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
x=x*fx; //正负号
}
char ss[519]; int n,f[(1<<17)][10],ans=0;
struct node{ char l,r; int c; }a[22];
int id(char ch){ if(ch=='I') return 0; if(ch=='O') return 1;
if(ch=='U') return 2; if(ch=='E') return 3; if(ch=='A') return 4; }
int main(){
reads(n); for(int i=0;i<n;i++){
scanf("%s",ss); int len=strlen(ss);
a[i].l=id(ss[0]),a[i].r=id(ss[len-1]),a[i].c=len;
} for(int i=0;i<=(1<<n)-1;i++)
for(int j=0;j<n;j++) if(i&(1<<j)) //枚举此时新加上的单词j,如果i状态包含这个单词
if(i==(1<<j)||f[i-(1<<j)][a[j].l]) //如果是状态中唯一单词||去掉它的情况出现过
f[i][a[j].r]=f[i-(1<<j)][a[j].l]+a[j].c,ans=max(ans,f[i][a[j].r]);
printf("%d
",ans); return 0; //输出ans:接龙的最大长度
}
【p1278】单词游戏
T8:【p3070】岛游记
- r*c的地图,有’S’:浅水(游泳),’X’:陆地(行走),’.’:深水三种地形。
- 刚开始在任意陆地上,陆地和浅水之间可以相互通行。但无论如何都不能走到深水。
- 要求把所有的岛屿(陆地的连通块)都经过一次。Q:最少要经过几个浅水区?
求所有‘X’所在连通块的编号:flag[i][j],用num记录每个连通块的大小,进行spfa得出连通块之间的最短路。
然后dp:f[S][i] 即经过岛屿(连通块)的状态为S、最后岛屿为i时最少经过的浅水数。
- 方程:f[S][i]=min{f[S^i][k]+d[k,i]}。 // 其中 d[][] 记录两个连通块之间的最短路。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<set>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
void reads(int &x){
int fx=1;x=0;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')fx=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
x=x*fx; //正负号
}
/*【p3070】岛游记
r*c的地图,有’S’:浅水(游泳),’X’:陆地(行走),’.’:深水三种地形。
刚开始在任意陆地上,陆地和浅水之间可以相互通行。但无论如何都不能走到深水。
要求把所有的岛屿(陆地的连通块)都经过一次。Q:最少要经过几个浅水区? */
//求所有‘X’所在连通块的编号:flag[i][j],用num记录每个连通块的大小,进行spfa得出连通块之间的最短路。
//然后dp:f[S][i]即经过岛屿(连通块)的状态为S、最后岛屿为i时最少经过的浅水数。
//方程:f[S][i]=min{f[S^i][k]+d[k,i]}。 //d[][]记录两个连通块之间的最短路
struct node{ int x,y; }maps[16][16]; //maps[][]记录每个连通块的每个格子
queue<node>q; //广搜队列,用于广搜确定连通块 和 SPFA
int n,m,ans=1e9,tot=0,flag[59][59],num[59],d[59][59],dist[59][59];
char ss[59][59]; bool vis[59][59]; int f[(1<<16)][16];
const int dx[]={1,-1,0,0},dy[]={0,0,1,-1};
bool in_(int x,int y){ return x>=1&&x<=n&&y>=1&&y<=m; }
void bfs(int sx,int sy){ //记录第一个连通块的位置
node now1; now1.x=sx,now1.y=sy,q.push(now1);
vis[sx][sy]=1,flag[sx][sy]=tot,num[tot]++;
maps[tot][num[tot]]=now1;
while(!q.empty()){
node now=q.front(),now1;q.pop();
for(int i=0;i<4;i++){
int xx=now.x+dx[i],yy=now.y+dy[i];
if(!in_(xx,yy)||vis[xx][yy]||ss[xx][yy]!='X') continue;
now1.x=xx,now1.y=yy,q.push(now1),vis[xx][yy]=1,flag[xx][yy]=tot;
num[tot]++,maps[tot][num[tot]]=now1; //进队并记录信息
}
}
}
void spfa(int s){ //以连通块s作为起点
memset(vis,0,sizeof(vis));
memset(dist,127/3,sizeof(dist));
for(int i=1;i<=num[s];i++){ //属于该连通块的所有点
vis[maps[s][i].x][maps[s][i].y]=1;
dist[maps[s][i].x][maps[s][i].y]=0,q.push(maps[s][i]);
} node now,now1; //便于取出队列中的当前点
while(!q.empty()){ //进行SPFA
now=q.front(),q.pop(),vis[now.x][now.y]=0;
for(int i=0;i<4;i++){
int xx=now.x+dx[i],yy=now.y+dy[i];
if(!in_(xx,yy)||ss[xx][yy]=='.')continue;
if(ss[xx][yy]=='X'){ //↓↓到达一个新的岛屿(连通块)
if(dist[xx][yy]>dist[now.x][now.y]){
dist[xx][yy]=dist[now.x][now.y];
if(!vis[xx][yy]) vis[xx][yy]=1,now1.x=xx,now1.y=yy,q.push(now1);
} d[flag[xx][yy]][s]=d[s][flag[xx][yy]]= //更新连通块间的最短路
min(d[s][flag[xx][yy]],min(d[flag[xx][yy]][s],dist[xx][yy]));
} if(ss[xx][yy]=='S'){ //↓↓到达浅水区
if(dist[xx][yy]>dist[now.x][now.y]+1){
dist[xx][yy]=dist[now.x][now.y]+1; //更新点间最短路
if(!vis[xx][yy]) vis[xx][yy]=1,now1.x=xx,now1.y=yy,q.push(now1);
}
}
}
}
}
void DP(){ //状压dp
memset(f,127/3,sizeof(f));
for(int i=1;i<=tot;i++) f[1<<(i-1)][i]=0; //初始化每个连通块
for(int S=1;S<=(1<<tot)-1;S++){
for(int i=1;i<=tot;i++){ //枚举当前状态能经过的最后节点
if(!(S&(1<<(i-1)))) continue;
for(int j=1;j<=tot;j++){ //枚举上次经过的最后节点
if(j==i||!(S&(1<<(j-1)))) continue;
f[S][i]=min(f[S][i],f[S^(1<<(i-1))][j]+d[j][i]);
}
}
}
}
int main(){ reads(n),reads(m);
for(int i=1;i<=n;i++) scanf("%s",ss[i]+1);
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) //↓↓遍历整个连通块
if(!vis[i][j]&&ss[i][j]=='X') tot++,bfs(i,j);
memset(d,63,sizeof(d)); //d[][]记录两个连通块之间的最短路
for(int i=1;i<=tot;i++) d[i][i]=0,spfa(i); //寻找两两连通块间的最短路
DP(); for(int j=1;j<=tot;j++) ans=min(ans,f[(1<<tot)-1][j]);
cout<<ans<<endl; return 0; //↑↑枚举终点,得到最优解
}
【p3070】岛游记
T9:【p2915】奶牛混合起来
- 给出n个编号,问排序成混乱的队伍(相邻牛编号均超过k)的方案数。
状压dp。f[s][j] 表示:当前状态s,当前末尾为j的方案数。转移累加即可。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<set>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
void reads(int &x){
int fx=1;x=0;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')fx=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
x=x*fx; //正负号
}
/*【p2915】奶牛混合起来
给出n个编号,问排序成混乱的队伍(相邻牛编号均超过k)的方案数。*/
int a[20]; ll f[(1<<16)][20]; //f[s][j]即:当前状态s,当前末尾为j的方案数
int main(){
int n,k; reads(n),reads(k);
for(int i=1;i<=n;i++) reads(a[i]);
for(int i=1;i<=n;i++) f[1<<(i-1)][i]=1;
for(int s=0;s<=(1<<n)-1;s++)
for(int i=1;i<=n;i++) if(s&(1<<(i-1)))
for(int j=1;j<=n;j++) //if(i!=j&&(s&(1<<(j-1))))
if(abs(a[i]-a[j])>k) f[s][i]+=f[s^(1<<(i-1))][j];
ll ans=0; for(int i=1;i<=n;i++) ans+=f[(1<<n)-1][i];
printf("%lld
",ans); return 0;
}
【p2915】奶牛混合起来
状压DPの简单变形
T10:【p2622】关灯问题
- n盏灯,m个按钮。每个按钮可以同时控制这n盏灯。
- 初始全开,给出不同开关的控制效果,最少按几次按钮才能全关?
#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<set>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
void reads(int &x){
int fx=1;x=0;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')fx=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
x=x*fx; //正负号
}
/*【p2622】关灯问题 // 状压 + 广搜
n盏灯,m个按钮。每个按钮可以同时控制这n盏灯。
现在这些灯都是开的,给出所有开关对所有灯的控制效果,
问最少要按几下按钮才能全部关掉。*/
int n,m,a[109][1019]; bool vis[1000019];
struct node{ int s,step; }; // s:当前状态,step:所需min步数
int spfa(){
queue<node> q; vis[(1<<n)-1]=true;
q.push( (node) {(1<<n)-1,0} ); //以全开为初始状态
while(!q.empty()){ node u=q.front(); q.pop();
if(u.s==0) return u.step; //若状态为0(全关),返回当前步数
for(int i=1;i<=m;i++){ int ss=u.s; //选择一种操作,进行修改,将新状态入队
for(int j=1;j<=n;j++){ //遍历所有灯,按不同操作进行修改
if( a[i][j]==1 && (ss&(1<<j-1)) ) ss^=(1<<j-1);
if( a[i][j]==-1 && !(ss&(1<<j-1)) ) ss|=(1<<j-1); }
if(!vis[ss]) q.push((node){ss,u.step+1}),vis[ss]=true;
} } return -1; //若遍历完所有状态都没有0,则输出-1
}
int main(){ reads(n),reads(m); //灯数n,操作种数m
for(int i=1;i<=m;i++) for(int j=1;j<=n;j++) reads(a[i][j]);
cout<<spfa()<<endl; return 0; } //bfs求达到目标状态的min次数
T11:【p1171】售货员的难题
- 从1号村庄去所有村庄再回来,选一条路线使总路程最短。
- f[s][j]:当前节点状态为s,到达j点的最短路径。
保证每条路线一定要经过1号村庄,所以枚举状态时每次要 s+=2 。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<set>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
void reads(int &x){
int fx=1;x=0;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')fx=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
x=x*fx; //正负号
}
/*【p1171】售货员的难题 //限定性的状压dp
从1号村庄去所有村庄再回来,选一条路线使总路程最短。*/
// f[s][j]:当前节点状态为s,到达j点的最短路径。
//保证每条路线一定要经过1号村庄,所以枚举状态时每次要 s+=2 。
int ans=2e9,g[22][22],f[(1<<20)][22];
int main(){ int n; reads(n);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++) reads(g[i][j]);
memset(f,0x3f,sizeof(f)); f[1][1]=0;
for(int s=1;s<=(1<<n)-1;s+=2) //保证状态中包含节点1
for(int i=1;i<=n;i++) if(f[s][i]<(int)3e5) //剪枝
for(int j=1;j<=n;j++) if(!(s&(1<<(j-1)))) //[注意]括号一定要打全
f[s|(1<<(j-1))][j]=min(f[s|(1<<(j-1))][j],f[s][i]+g[i][j]);
for(int i=1;i<=n;i++) ans=min(ans,f[(1<<n)-1][i]+g[i][1]);
cout<<ans<<endl; return 0;
}
【p1171】售货员的难题 //限定性的状压dp
T12:【p3694】邦邦的合唱团
- N个偶像排成一列,来自M个不同的乐队。
- 要求让部分偶像出队,再站回自己应该站的位置。
- 注意:站回的位置一定是原来有人站、
- 而现在为空的位置。最少让多少偶像出列?
预处理前缀和:sum[i][j]即第i个乐队,在位置j前方,有多少人。
求区间的团队i人数直接可以用sum[i][r]-sum[i][l-1];
注意:每次放置一个团,一定会占据'p~p+num[i](i团队人数)-1'的位置。
按照选择集合的顺序把每个团顺着放(很重要的思想),这样不会有遗漏。
- f[s]=min(f[s],f[s^(1<<i)]+num[i]-sum[i][p+num[i]-1]+sum[i][p-1]);
表示把第i个乐队放在p~(p+num[i]-1),达到状态s,需要增加的最小代价。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<set>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
void reads(int &x){
int fx=1;x=0;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')fx=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
x=x*fx; //正负号
}
/*【p3694】邦邦的合唱团 // 状压 + 前缀和
N个偶像排成一列,来自M个不同的乐队。要求让部分偶像出队,再站回自己应该站的位置。
注意:站回的位置一定是原来有人站、而现在为空的位置。最少让多少偶像出列?*/
//预处理前缀和:sum[i][j]即第i个乐队,在位置j前方,有多少人。
//求区间的团队i人数直接可以用sum[i][r]-sum[i][l-1];
//注意:每次放置一个团,一定会占据'p~p+num[i](i团队人数)-1'的位置。
//按照选择集合的顺序把每个团顺着放(很重要的思想),这样不会有遗漏。
int f[(1<<22)]; int n,m,a[100019],num[22],sum[22][100019];
//f[s]=min(f[s],f[s^(1<<i)]+num[i]-sum[i][p+num[i]-1]+sum[i][p-1]);
//↑↑表示把第i个乐队放在p~(p+num[i]-1),达到状态s,需要增加的最小代价。
int main(){
reads(n),reads(m); for(int i=1;i<=n;i++){ reads(a[i]),num[a[i]]++;
for(int j=1;j<=m;j++) sum[j][i]=sum[j][i-1]; sum[a[i]][i]++; }
for(int s=0;s<(1<<m);s++) f[s]=(int)2e9; f[0]=0; //注意初始化&设置起点
for(int s=0;s<(1<<m);s++){ //每种状态(状态中所有数都放在前面 且 放完了)
int p=1; for(int i=1;i<=m;i++) if(s&(1<<(i-1))) p+=num[i]; //顺序放置
for(int i=1;i<=m;i++) if((s&(1<<(i-1)))==0) //注意状压中的位数对应
f[s|(1<<(i-1))]=min(f[s|(1<<(i-1))],f[s]+num[i]-sum[i][p+num[i]-1]+sum[i][p-1]);
} cout<<f[(1<<m)-1]<<endl; return 0; //注意:(s&(1<<(i-1)))==0这个地方,括号不能打掉
}
【p3694】邦邦的合唱团 // 状压 + 前缀和
T13:【p3160】局部最小值
- n*m的棋盘。局部极小值:数值比所有相邻格子(八连通)都小。
- 给出所有局部极小值的位置,判断有多少个可能的矩阵。
f[i][j] 表示 填入前 i 个数,局部最小值的填入情况为 j 的方案数。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;
/*【p3160】局部最小值 // 状压dp + bfs + 特判
n*m的棋盘。局部极小值:数值比所有相邻格子(八连通)都小。
给出所有局部极小值的位置,判断有多少个可能的矩阵。*/
//【分析】f[i][j] 表示 填入前 i 个数,局部最小值的填入情况为 j 的方案数。
void reads(int &x){ //读入优化(正负整数)
int fx=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
x*=fx; //正负号
}
const int dx[9]={0,1,1,1,0,-1,-1,-1,0};
const int dy[9]={0,1,0,-1,-1,-1,0,1,1},mod=12345678;
char ss[59]; int n,m,px[59],py[59],p,vis[10][10],min_[10][10],v[519],f[59][519];
int solve(){
memset(v,0,sizeof(v));
for(int i=0;i<(1<<p);i++){ //v[i]:每种集合状态i下的可填格子数目
memset(vis,0,sizeof(vis));
for(int j=0;j<p;j++) if(!(i&(1<<j)))
for(int k=0;k<9;k++) vis[px[j]+dx[k]][py[j]+dy[k]]=1;
for(int j=1;j<=n;j++) for(int k=1;k<=m;k++) v[i]+=!vis[j][k];
} memset(f,0,sizeof(f)); f[0][0]=1;
for(int i=1;i<=n*m;i++)
for(int j=0;j<(1<<p);j++){
if(v[j]>=i) f[i][j]=f[i-1][j]*(v[j]-i+1)%mod;
for(int k=0;k<p;k++) if(j&(1<<k))
f[i][j]=(f[i][j]+f[i-1][j^(1<<k)])%mod;
} return f[n*m][(1<<p)-1];
}
int dfs(int x,int y){
if(y>m) x++,y=1; if(x>n) return solve(); //注意dfs的位置转化
//求满足‘给出的位置是局部最小值’总方案数
int i,ans=dfs(x,y+1); //深搜求此位置的总方案
//因为必须满足要‘非给出的位置不是局部最小值’,所以需要容斥,
//即:总方案数-此点(x,y)为‘非给出的局部最小值’的方案数。
for(i=0;i<9;i++) if(min_[x+dx[i]][y+dy[i]]) break; //找出‘局部最小值’的点
//↓↓自己和八方向都不是局部最小值,填入之后就一定会变成局部最小值,与题目矛盾
if(i==9){ px[p]=x,py[p++]=y,min_[x][y]=1; //↓↓利用‘容斥原理’减掉
ans=(ans-dfs(x,y+1)+mod)%mod,p--,min_[x][y]=0; } return ans;
}
int main(){
reads(n),reads(m); for(int i=1;i<=n;i++){
scanf("%s",ss+1); for(int j=1;j<=m;j++)
if(ss[j]=='X') px[p]=i,py[p++]=j; //局部最小值的点
} for(int i=0;i<p;i++){ //状压一般用0开始,所以p也从0开始
for(int j=0;j<i;j++) //【特判】局部最小值冲突的情况
if(abs(px[i]-px[j])<=1&&abs(py[i]-py[j])<=1)
{ puts("0"); return 0; } //不满足‘最小’的性质
min_[px[i]][py[i]]=1; //将局部最小值的点标记为1
} printf("%d
",dfs(1,1)); return 0;
}
【p3160】局部最小值 // 状压dp + bfs + 特判
T14:【p3736】字符合并
- 每次将长度为n的01串中相邻的k个字符合并,得到新字符并获得分数。
- 得到的新字符和分数由这k个字符确定。你需要求出你能获得的最大分数。
- 输入ci和wi,ci表示长度为k的01串连成二进制后、
- 按从小到大顺序得到的第i种合并方案得到的新字符,
- wi表示第i种方案对应获得的分数。
用 f[i][j][s] 表示‘从i到j之间进行替换后成为s的状态’所能得到的最大值。
第一层循环枚举区间长度,用小区间维护大区间,第二层枚举左端点,第三层枚举中间值。
由于还有状压,第四层就是枚举状态了,由于我们每次合并都是以k为长度,右端点已经确定。
如果len=k-1,说明该串可以被合并,就去找每个合法状态中转化为0或1中最大的两个。
#pragma GCC optimize(2)
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【p3736】字符合并 // 区间dp + 状压dp
每次将长度为n的01串中相邻的k个字符合并,得到新字符并获得分数。
得到的新字符和分数由这k个字符确定。你需要求出你能获得的最大分数。
输入ci和wi,ci表示长度为k的01串连成二进制后、
按从小到大顺序得到的第i种合并方案得到的新字符,
wi表示第i种方案对应获得的分数。*/
/*【分析】f[i][j][s]表示‘从i到j之间进行替换后成为s的状态’所能得到的最大值。
第一层循环枚举区间长度,用小区间维护大区间,第二层枚举左端点,第三层枚举中间值。
由于还有状压,第四层就是枚举状态了,由于我们每次合并都是以k为长度,右端点已经确定。
如果len=k-1,说明该串可以被合并,就去找每个合法状态中转化为0或1中最大的两个。*/
int n,k,b[1<<8]; ll c[1<<8],f[301][301][1<<7]; char ss[301];
const ll inf=-1e9;
int main(){
scanf("%d%d%s",&n,&k,ss+1); memset(f,-0x3f,sizeof(f));
for(int i=1;i<=n;i++) f[i][i][ss[i]-'0']=0;
for(int s=0;s<(1<<k);s++) scanf("%d%lld",&b[s],&c[s]);
for(int l=2;l<=n;l++) //【1】枚举dp区间长度
for(int i=1;i<=n-l+1;i++){ //【2】枚举dp区间左端点
int j=i+l-1,len=j-i; //dp区间右端点j
while(len>k-1) len-=k-1; //此dp区间需要合并的次数
for(int t=j;t>0;t-=k-1) //挨个合并(倒序)
for(int s=0;s<(1<<len);s++){ //新状态由小区间扩展而来
if(f[i][t-1][s]>inf&&f[t][j][0]>inf)
f[i][j][s<<1]=max(f[i][j][s<<1],f[i][t-1][s]+f[t][j][0]);
if(f[i][t-1][s]>inf&&f[t][j][1]>inf)
f[i][j][s<<1|1]=max(f[i][j][s<<1|1],f[i][t-1][s]+f[t][j][1]);
} if(len==k-1){ ll g[2]; g[0]=g[1]=inf;//过渡值g[0/1]
for(int s=0;s<(1<<k);s++) if(f[i][j][s]>inf)
g[b[s]]=max(g[b[s]],f[i][j][s]+c[s]); f[i][j][1]=g[1],f[i][j][0]=g[0]; }
} ll ans=inf; for(int i=0;i<(1<<k);i++) ans=max(ans,f[1][n][i]); printf("%lld
",ans);
}
【p3736】字符合并 // 区间dp + 状压dp
一开始真心不知道为什么wa...(调了好久...)好吧发现是输入的锅...
T15:【p4163】排列
- 给一个数字串s和正整数d, 统计s有多少种不同的排列能被d整除(可以有前导0)。
#pragma GCC optimize(2)
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【p4163】排列
给一个数字串s和正整数d, 统计s有多少种不同的排列能被d整除(可以有前导0)。*/
// https://www.luogu.org/blog/MonsterQ/solution-p4163
int T,d,a[11],cnt,dp[1<<11][1001]; bool vis[11]; char ss[11];
int main(){
scanf("%d",&T); int len;
while(T--){ memset(dp,0,sizeof(dp));
scanf("%s%d",ss+1,&d); len=strlen(ss+1),cnt=0;
for(int i=1;i<=len;i++) a[i]=ss[i]-'0'; dp[0][0]=1; //dp赋初值
for(int S=0;S<(1<<len)-1;S++){ //S表示当前所选的状态集合
memset(vis,0,sizeof(vis)); //注意清零
for(int j=1;j<=len;j++) if(!(S&(1<<(j-1)))&&!vis[a[j]]){
//如果a[j]已经转移过就不能继续转移了,j表示遍历s中的各位数字
vis[a[j]]=1; for(int k=0;k<d;k++) //k表示此数对d取余后的数
dp[S|(1<<(j-1))][(k*10+a[j])%d]+=dp[S][k]; }
} printf("%d
",dp[(1<<len)-1][0]);
}
}
【p4163】排列 // 状压数位 标记mod状态
T16:【p4363】一双木棋
#pragma GCC optimize(2)
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【p4363】一双木棋 */
/*【分析】把每个格子的价值记成aij+bij。初始a,b二人得分为sum_ai,sum_bi。
如果i,j被a选了,sum_b中包含的bij抵消,此格子的相关得分变成两倍aij。最后差值/2即可。
所以每一步都选择此步能走的(aij+bij)max的? //肯定是错的.. */ //艰难的30分(特判的30分...)
/*【正解】单调递增的矩阵取数:轮廓线 + 状压 + 记忆化搜索
先放置m个0,再向其中插入n个1,1代表行,每个1右边有几个0就代表了这行选了几个数。
可参考 https://anoxiacxy.blog.luogu.org/solution-p4363 。*/
void reads(int &x){ int fx_=1;x=0;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')fx_=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} }
int n,m,a[11][11],b[11][11],ans=0,vis[(1<<22)],f[(1<<22)];
int dfs(int s,int op){
if(vis[s]) return f[s]; int tmp=op?-2e9:2e9; vis[s]=1;
for(int i=0,j=n+1,k=1;i<n+m;i++){
if((s|(1<<i))!=s) k++; else j--;
if(i==n+m-1||((s>>i)&3)!=1) continue;
int s_=(s^(3<<i)); //翻转一个‘01’或‘10’
if(op) tmp=max(tmp,dfs(s_,op^1)+a[j][k]);
else tmp=min(tmp,dfs(s_,op^1)-b[j][k]);
} return f[s]=tmp; //因为有后效性,所以不能dp,必须要搜索
}
int main(){
reads(n),reads(m);
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) reads(a[i][j]);
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) reads(b[i][j]);
f[((1<<n)-1)<<m]=0; vis[((1<<n)-1)<<m]=1;
//↑↑轮廓线为长度为n+m的01串,初始为11...100...0
printf("%d
",dfs((1<<n)-1,1)); return 0; //进行dfs
}
【p4363】一双木棋 // 轮廓线 + 状压 + 记忆化搜索
——时间划过风的轨迹,那个少年,还在等你