这篇博客用来存储DP各种优化的题目,因为ZZ
还在学习中,所以这篇博客会随着做题进度不断更新。
状态压缩优化
所谓状态压缩,就是将原本需要很多很多维来描述,甚至暴力根本描述不清的状态压缩成一维来描述。
时间复杂度一般为O(2^n\cdot n^2)$的形式
(ZZ
并不太会算复杂度,如果博客中复杂度有错误,请对我指出并尽情嘲讽我,谢谢!)
眼界极窄的ZZ
之前只是听说过这个名字……先感谢Lrefrain学长把这个东西介绍给我%%%
使用状态压缩优化的常见情景:
这个数据范围怎么有一维出奇的小啊?
互不侵犯
应该是最经典的一道状压dp了,看到这极具特色的数据范围就会了大半
可以对每一行可能出现的所有状态进行压缩,因为每一个位置不是放就是不放,所以我们把放标成1
,不放标成0
,那么对于一行来说,每种状态都可以用一个二进制串来表示,好妙啊!!!
更妙的是,既然用了二进制,那么就可以使用位运算的<<
,>>
和&
运算符,直接判定相邻两行的状态合不合法!
这个真的需要好好体会,越体会越妙!
放一下代码吧,但更重要的是领会精神!
#include<bits/stdc++.h>
using namespace std;
#define MA 1005
#define ll long long
ll sit[MA]={0},ku[MA]={0};
ll cnt=0;
ll n,k;
ll dp[15][MA][100]={0};
int main()
{
scanf("%lld%lld",&n,&k);
for(int i=0;i<(1<<n);i++)
{
if(i&(i<<1))continue;
sit[++cnt]=i;
for(int j=0;j<n;j++)
{
if(i&(1<<j))ku[cnt]++;
}
}
dp[0][1][0]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=cnt;j++)
{
for(int p=ku[j];p<=k;p++)
{
for(int q=1;q<=cnt;q++)
{
if(sit[j]&sit[q])continue;
if(sit[j]&(sit[q]>>1))continue;
if(sit[j]&(sit[q]<<1))continue;
dp[i][j][p]+=dp[i-1][q][p-ku[j]];
}
}
}
}
ll ans=0;
for(int i=1;i<=cnt;i++)
{
ans+=dp[n][i][k];
}
printf("%lld",ans);
return 0;
}
一个来自学长的小技巧
用\(lowbit\)或者枚举算\(01\)串中\(1\)的个数时,如果要算的串很多,可能导致此处算法复杂度爆炸而被卡
那么这个时候,就可以用预处理的方式,先算出所有状态的\(1\)个数,用到的时候直接\(O(1)\)查询就行~
炮兵阵地
发现还要考虑上下,这怎么办呢?
首先,其实考虑下面就相当于下面的考虑上面,所以就不用考虑下面了(有点绕)
然后,因为上面只需要伸两格,所以直接用两维表示\(i\),\(i-1\)行的状态,每次由\(i-1\),\(i-2\)转移得到就可以了!
题里还需要考虑一个平原的问题,这里我们把每行的地形也压缩一下,在进行dp的时候注意判定状态是否合法就行啦(≧▽≦)/
dp部分(其实写得有些麻烦了,不过看起来很整齐)
for(int i=1;i<=cnt;i++)
{
if(sit[i]&pa[1])continue;
dp[1][i][0]=max(dp[1][i][0],mu[i]);
}
for(int i=1;i<=cnt;i++)
{
if(sit[i]&pa[1])continue;
for(int j=1;j<=cnt;j++)
{
if(sit[j]&pa[2])continue;
if(sit[j]&sit[i])continue;
dp[2][j][i]=max(dp[2][j][i],mu[i]+mu[j]);
}
}
for(int i=3;i<=n;i++)
{
for(int j=1;j<=cnt;j++)
{
if(sit[j]&pa[i])continue;
for(int k=1;k<=cnt;k++)
{
dp[i%3][j][k]=0;
if(sit[k]&pa[i-1])continue;
if(sit[j]&sit[k])continue;
for(int l=1;l<=cnt;l++)
{
if(sit[l]&pa[i-2])continue;
if(sit[j]&sit[l])continue;
if(sit[k]&sit[l])continue;
dp[i%3][j][k]=max(dp[i%3][j][k],dp[(i-1)%3][k][l]+mu[j]);
}
}
}
}
特殊方格棋盘
在有前两道题的基础后,这道题应该是不难的一道题,放在后面是因为做的时候发现有一种解法(自我感觉)比较优美
观察整个题,发现每行能且只能放一个车,这就说明不可能在不同的两行上出现相同的状态,也就是在棋盘上每行每一种状态都是不同的
所以我想,既然全都不同,能不能不维护行数,把空间降一维呢?经过思考,这是可以实现的。
我们用\(sit[p]\)来表示某行的一种状态,那么这种状态中随便删去一个1
,就能得到上一行的一个状态,我们只要枚举每一个1
,然后用删去他得到的上一行某个状态的答案更新这一行的答案即可。这里的删去是可以用异或运算轻松实现的,真的是妙!
通过算1
的个数,我们得到行号,我们枚举到的1
的位置+1便可以得到列号,如果这个坐标合法,我们就更新答案。(注意这个+1!)
当时的代码:(可能一些细节和以上说的略有不同,但是思路是一样的)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll n,m;
ll dp[1<<21]={0};
ll a[25][25]={0};
ll x,y;
ll z[1<<21]={0};
int main()
{
for(int i=1;i<=(1<<21);i++)
{
z[i]=z[i>>1]+(i&1);
}
scanf("%lld%lld",&n,&m);
while(m--)
{
scanf("%lld%lld",&x,&y);
a[x][y-1]=1;
}
dp[0]=1;
for(int i=1;i<(1<<n);i++)
{
for(int j=0;j<n;j++)
{
if((i&(1<<j))&&!a[z[i]][j])
{
dp[i]+=dp[i^(1<<j)];
}
}
}
printf("%lld",dp[(1<<n)-1]);
return 0;
}
单调队列优化
太晚了先咕着,要不明早爬不起来了QAQ