会的人说是水题,不会的是神题,这就是状态dp的魅力。
hdu4804
轮廓线dp水题,所谓轮廓线dp,核心思想就是,以轮廓线作为状态,然后进行dp,不是按行,不是按列,是轮廓线,必须把握这一精髓。
对于这个题目,只需记录当前轮廓线上哪些点已经被覆盖,哪些点没有被覆盖,以及这种轮廓线状态下的结果。然后,不断地由当前轮廓线递推出将当前轮廓右下角纳入轮廓线之后的状态。而由当前轮廓线状态推理下一轮廓线状态时,实际上状态中所有点仅当前处理位置上部的节点和左部的节点对下一轮廓线状态有关,其他均可直接从上以轮廓线状态推得,不清楚的可以在纸上划一下。然后就是分情况讨论了。详细看代码。
代码:
#include
#include
#include
#include
#include
using namespace std;
int n,m,c,d;
char map[110][11];
long long int dp[2][25][1<<10+1]; //第一维状态压缩,记录处理到第几个轮廓线,第二维记录用多少个1*1,第三维记录状态,值为当前状态结果
const int MOD=1000000000+7;
int now;
void DP()
{
memset(dp,0,sizeof(dp));
dp[0][0][(1<0&&(j&y)) //放1*1,上面必须为1,且至少占用一个1*1的方块,左边与轮廓线相接,空或不空均可
dp[now][k][j]=(dp[now][k][j]+dp[pre][k-1][j])%MOD;
if ( t>0 && !(j&x) && (j&y) ) //横放1*2,左边必须为空且不是第一格,上面必须为1,否则此点铺完后上面不与轮廓线相接,空出一个
dp[now][k][j|x]=(dp[now][k][j|x]+dp[pre][k][j])%MOD;
if (!(j&y)) //竖放2*1,上面必须为零,左边与轮廓线相接,无所谓
dp[now][k][j|y]=(dp[now][k][j|y]+dp[pre][k][j])%MOD;
if (j&y) //先空着,上面必须为1,左边与轮廓线相接,无所谓
dp[now][k][j-y]=(dp[now][k][j-y]+dp[pre][k][j])%MOD;
}
else
{
if (j&y) //上面为1,左边无所谓,当前格假设用1*1铺好但不占用1*1的数量
dp[now][k][j]=(dp[now][k][j]+dp[pre][k][j])%MOD;
}
}
}
}
}
}
int main()
{
while (cin>>n>>m>>c>>d)
{
for (int i=0;i
hdu4856
题目给定一些管道和管道的起点和终点,可以从任意管道出发,问遍历所有管道一次仅且一次至少需要走多少步。
很容易想到,首先要求出管道两两之间的最短距离,然后得出最少消耗。无论BFS还是什么的构造出最短距离的新的图,然后就是状态压缩dp了。一开始想的搜索,超时了一下午,后来用的状态压缩dp。设dp[i][j]表示当前状态最后一个走的是i,状态为j的情况,用一个长度为n的二进制数j表示n条管道是否走过,然后将所有只走一条管道的状态标记为0,其余标记为无穷大,表示无法到达。然后将状态按从小到大的顺序进行dp,至于为什么按这个顺序,是因为dp的时候当前状态只与当前状态没有走i的时候有关,当前状态没有走i表示的数肯定小于当前状态表示的数,这样才能形成递推关系,且保证了每个当前状态最优(所有能推出当前状态的结果都被检测了一遍去的最小值)。详细看代码。
代码:
#include
#include
#include
#include
using namespace std;
int map[20][20];
int dis[20][20]; //最短路
int d[20][20]; //i的终点到j的起点的距离
int sx[20];
int sy[20];
int ex[20];
int ey[20];
int gx[4]={1,-1,0,0};
int gy[4]={0,0,-1,1};
int n,m;
int ans;
int dp[20][1<<15+1]; //当前状态最后走i,状态为j的情况数
int INF=999999;
int shortpath(int x1,int y1,int x2,int y2)
{
queue mx;
queue my;
memset(dis,-1,sizeof(dis));
dis[x1][y1]=0;
mx.push(x1);
my.push(y1);
int curx,cury,curs;
while (!mx.empty())
{
curx=mx.front();
cury=my.front();
mx.pop();
my.pop();
for (int i=0;i<=3;i++)
{
if (map[curx+gx[i]][cury+gy[i]])
{
int nx=curx+gx[i];
int ny=cury+gy[i];
if (nx<1||nx>n||ny<1||ny>n)
continue ;
if (dis[nx][ny]==-1||dis[curx][cury]+1=dis[x2][y2])
continue ;
mx.push(nx);
my.push(ny);
}
}
}
}
return dis[x2][y2];
}
int min(int a,int b)
{
return a>b?b:a;
}
void DP()
{
int mm=1<>n>>m)
{
for (int i=1;i<=n;i++)
{
for (int t=1;t<=n;t++)
{
char a;
cin>>a;
if (a=='.')
map[i][t]=1;
else
map[i][t]=0;
}
}
for (int i=1;i<=m;i++)
scanf("%d%d%d%d",&sx[i],&sy[i],&ex[i],&ey[i]);
for (int i=1;i<=m;i++)
{
for (int t=1;t<=m;t++)
{
d[i-1][t-1]=shortpath(ex[i],ey[i],sx[t],sy[t]);
if (d[i-1][t-1]==-1)
d[i-1][t-1]=INF;
}
}
ans=INF;
DP();
for (int i=0;i=INF)
ans=-1;
cout<
hdu3681
花了大半天时间才改对,代码能力真是渣渣。
这题目思路也是常规思路,求最短路,重新构图,然后状态dp跑一遍即可。
对于能量池,如果用的话,就是当前节点经过能量池到达另一节点,不用的话,就是两个节点直接到达,如果在两个节点最短路径上存在能量池,则用能量池肯定是一种比不用能量池更优的策略,求max的时候会自动考虑这种情况,将能量池也看做节点,建图之后二分结果跑状态dp,直至找到最小的能跑完的值便是答案,注意细节,输出结果即可,详细见代码。
附代码:
#include
#include
#include
#include
using namespace std;
char map[20][20];
int x[20];
int y[20];
int dis[20][20];
int d[20][20]; //新图最短路
int dp[16][1<<15+1]; //老规矩,当前停留节点为i,状态为t时剩余的能量
int n,m,cnt,sum,f,maxs;
int gx[4]={0,0,-1,1};
int gy[4]={1,-1,0,0};
const int INF=999999;
int max(int a,int b)
{
return a>b?a:b;
}
void shortpath(int x1,int y1) //bfs跑最短路
{
queue mx;
queue my;
for (int i=0;i=n||ny<0||ny>=m)
continue ;
if (dis[nx][ny]>dis[curx][cury]+1)
{
dis[nx][ny]=dis[curx][cury]+1;
mx.push(nx);
my.push(ny);
}
}
}
}
bool ok(int a) //检查是否能跑完
{
for (int i=0;i=0&&t>=cnt) //如果能到达新状态,且新走到的节点是能量池,则充能
dp[t][i|p]=a;
}
}
}
return false;
}
int main()
{
while (scanf("%d%d",&n,&m)&&n+m)
{
getchar();
for (int i=0;i>1;
if (ok(mid))
h=mid-1;
else
l=mid+1;
}
if (l>300)
l=-1;
printf("%d\n",l);
}
}
poj3311
状态压缩水题,确定一下终点待的位置就行了,再确定一下起点就保证了起点出发终点截止,然后中间更新是注意可以重复访问就行
附代码:
#include
#include
#include
using namespace std;
int mp[15][15];
int d[15][15];
int dp[1<<12][15];
int n;
const int INF=0x3f3f3f3f;
int min(int a,int b)
{
return a>b?b:a;
}
void solve()
{
int maxs=1<<(n+1);
for (int i=0;i>n&&n)
{
for (int i=0;i<=n;i++)
for (int t=0;t<=n;t++)
cin>>d[i][t];
for (int i=0;i<=n;i++)
for (int t=0;t<=n;t++)
for (int k=0;k<=n;k++)
if (d[t][k]>d[t][i]+d[i][k])
d[t][k]=d[t][i]+d[i][k];
solve();
}
}
依旧是状态压缩,只是这个题目要求每个点最多走两次,不是一次,而且,最大的坑点是,这个题目有重边!!!坑了多少单纯的骚年。
至于每个点走两次,模拟一个三进制,预处理记录下结果就行,输出时判断状态是否全覆盖了,其实也可以预处理,懒得写了。
附代码:
#include
#include
#include
using namespace std;
int mp[15][15];
int n,m;
int p[15];
int mem[60000][15];
int dp[60000][15];
const int INF=0x3f3f3f3f;
void init() //预处理一个三进制数和各状态的每位情况
{
memset(mem,0,sizeof(mem));
memset(p,0,sizeof(p));
p[0]=1;
for (int i=1;i<15;i++)
p[i]=p[i-1]*3;
for (int i=0;i<60000;i++)
{
int cur=i;
for (int t=0;t<11;t++)
{
mem[i][t]=cur%3;
cur=cur/3;
}
}
}
int min(int a,int b)
{
return a>b?b:a;
}
void solve()
{
int maxs=p[n];
for (int i=0;i=INF)
ans=-1;
cout<>n>>m)
{
int a,b,c;
for (int i=0;i<=n;i++)
for (int t=0;t<=n;t++)
mp[i][t]=INF;
for (int i=1;i<=m;i++)
{
cin>>a>>b>>c;
mp[a-1][b-1]=mp[b-1][a-1]=min(mp[a-1][b-1],c); //重边!!!
}
solve();
}
}
CF453B
要求数字两两互质,那么对于每一个质因子,在这个数的序列中肯定最多出现一次。由于1可以无限取,那么大于等于59的数取了肯定没有取1更优,因此取得数的上限是58。预处理出58以内的所有质因子,共16个,用这16个数去状态dp,以当前取了多少个数处于什么状态作为状态划分,依次向后推理并记录路径,输出答案即可。
附代码:
#include
#include
#include
#include
using namespace std;
int p[20]={2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53};
int v[65];
int cnt=16;
int dp[101][1<<18]; //当前状态为s,构造到第t个数
int mark[101][1<<16];
const int INF=0x3f3f3f3f;
void init()
{
memset(v,0,sizeof(v));
for (int i=1;i<=60;i++)
{
for (int t=0;t0?a:-a;
}
int main()
{
int n;
init();
while (cin>>n)
{
int a[110];
for (int i=1;i<=n;i++)
cin>>a[i];
memset(dp,-1,sizeof(dp));
dp[0][0]=0;
for (int i=1;i<=n;i++) //数是逐步向后推理的,状态是不定的,因此以数向后推作为第一条件
{
for (int t=1;t<59;t++) //枚举当前位用哪个数字
{
int s=(~v[t])&((1<<16)-1);
for (int j=s;;j=(j-1)&s) //枚举没有用这个数字的状态
{
if (dp[i-1][j]==-1) //如果不用这个数字的状态不能达到,跳过
continue ;
if (dp[i][j|v[t]]==-1||dp[i][j|v[t]]>dp[i-1][j]+abs(a[i]-t)) //判断是否更优
{
dp[i][j|v[t]]=dp[i-1][j]+abs(a[i]-t);
mark[i][j|v[t]]=t; //记录路径
}
if (j==0) //处理结束,跳出
break;
}
}
}
stack ms;
int mm=0;
for (int i=0;i<(1<<16);i++)
if (dp[n][i]!=-1&&dp[n][i]0)
{
int x=mark[cur][mm];
ms.push(x);
mm^=v[x];
cur--;
}
cout<
hdu4906
背包+状态压缩的题目
不得不吐槽理解题意是一件很困难的事情,题意理解了之后,看下数据量就该向状态压缩方向想了,然后再想到完全背包,两者一结合,题目便解出来了
具体题解看代码
附代码:
#include
#include
#include
using namespace std;
#define ll long long int
const int mod=1000000000+7;
ll dp[(1<<20)+100]; //dp是一个完全背包的过程,i表示背包背到现在的能表示这么多数的状态的总个数,如5=101,表示当前状态能表示1
//和3(第一位和第三位),dp[5]表示背包至今用了这么多数且只能能表示1和5两只的数列个数。
int main()
{
int T;
cin>>T;
while (T--)
{
int n,k,l;
cin>>n>>k>>l;
int tt=0;
if (l>k) //大于k的部分的数肯定是无用的数,不能导致新的可用状态出现
{
tt=l-k;
l=k;
}
int w=(1<=0;i--)
{
if (dp[i]==0) //剪枝
continue ;
ll temp=dp[i]*tt%mod; //当前状态加上一个大于k的无用的数之后得到的新的状态
ll now=dp[i];
for (int t=l;t>=1;t--) //当前状态加上一个能导致新的状态的数,需要更新操作
{
int s=i|(1<<(t-1))|((i<
这题比赛的时候感觉可能是状态dp,可惜还是没能力写出来。
网上的题解都用二维数组,但我觉得这题用一维数组肯定能表示出来,改了许久bug终于搞定。
具体就是,维护一个递减的序列作为当前选取某些值之后形成的序列的状态,因为如果递增,那么前面的数无论如何也不可能被后面的数合并掉,然后,依次对每个数进行两种判断,选或者不选,不选的话不用更新,选的话,遍历状态,如果新加入的值的小于等于状态的lowbit,那么可以加入形成一个新的序列,更新一下序列值,且这样更新的新状态的二进制表示肯定大于之前状态的二进制表示。如果大于状态的lowbit,那么一旦选用,该状态的所有部分就可以丢掉了,因为不可能合并了,因此找最大价值的即可。最后遍历末状态求最值即可。
具体看代码:
#include
#include
#include
using namespace std;
int dp[1<<13];
int n;
int v[510];
int lowbit(int a)
{
return a&(-a);
}
int max(int a,int b)
{
return a>b?a:b;
}
void solve()
{
memset(dp,-1,sizeof(dp));
dp[0]=0;
int m=(1<<13)-1;
for (int i=1;i<=n;i++)
{
int mm=0;
for (int t=m;t>=0;t--)
{
//不选用就不用更新,已经存在dp中,只需要更新选用之后能否造成更优解即可
if (dp[t]==-1)
continue ;
if (lowbit(t)>=v[i]) //形成新的递减序列
{
int k=(t+v[i])^t; //新获得的价值
dp[t+v[i]]=max(dp[t+v[i]],dp[t]+k);
}else //该状态已不能合并,舍弃该状态,取所有这样状态的最大值
mm=max(dp[t],mm);
}
dp[v[i]]=max(mm+v[i],dp[v[i]]); //能否将仅以当前数形成的序列的状态更新更优
}
}
int main()
{
int T;
cin>>T;
while (T--)
{
cin>>n;
for (int i=1;i<=n;i++)
cin>>v[i];
solve();
int ans=0;
for (int i=1;i<(1<<13);i++)
ans=max(ans,dp[i]);
cout<