【刷题笔记】小DP,你是否有很多优化?

这篇博客用来存储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

猜你喜欢

转载自www.cnblogs.com/zzzuozhe-gjy/p/12741619.html
今日推荐