「刷题笔记」DP优化-状压

因为篇幅太长翻着麻烦,计划把DP拆成几个小专题,这里原文只留下状压,其他请至后续博文。

状态压缩优化

所谓状态压缩,就是将原本需要很多很多维来描述,甚至暴力根本描述不清的状态压缩成一维来描述。
时间复杂度一般为(O(2^ncdot n^2))的形式
ZZ并不太会算复杂度,如果博客中复杂度有错误,请指出并尽情嘲讽我,谢谢!)
眼界极窄的ZZ之前只是听说过这个名字……先感谢Lrefrain学长把这个东西介绍给我orz
使用状态压缩优化的常见情景:

  • 这个数据范围怎么有一维出奇的小啊?

互不侵犯

应该是最经典的一道状压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;
} 

Disease Manangement 疾病管理

感性理解一下:就是把所有选中的牛得的病全部叠起来,然后统计(1)不超过(K)的数量
这题可以只用一维的,不知为何写二维反而没过……
问题不大,代码略,重头戏都在后面了……

旅游景点 Tourist Attractions

噩 梦 的 开 端(对于我来说
其实做完想想这题并不很难……然而由于我的(sb)行为(dijkstra)写挂了???导致颓了题解才幡然醒悟,我真是优秀啊。。。
然后就是我的程序自带大常数……所以以后遇到要重复计算的最好压到函数里吧。
吐槽完了,理一下思路吧,首先题中在某个城市停留是需要经过某个特定城市的,因为(k)的范围很小,可以考虑把停留过城市的情况压成一维。
那么再把停留时的限制压缩成一个数组(lim_{1dots k}),判定是否合法时我们把上一步的状态和这个(lim)取一下&,如果结果仍然等于上一步的状态就意味着这个(lim)已经包含在上一步的状态中,也就是已经被满足了。
然后我们跑最短路,对每一个要停留的点跑一边(dijkstra),得到他们之间的最短路(注意,这里需要单独存一下每个点到(n)的最短路)
然后就是一波状压DP正常流程
最后统计答案,取一个((dp_{(1<<k)-1,i}+xdis_{i,0})_{min})(这里(dp_{(1<<k)-1,i}) 指最后一步到达(i)状态为所有都已经停留的最短路,(xdis_{i,0})指每个点到(n)的距离。
放一段核心的吧……

        dp[0][1]=0;
	for(reg int i=0;i<=all;i++)
	{
		for(reg int j=1;j<=k+1;j++)
		{
			if(dp[i][j]!=inf)
			{
				for(reg int l=2;l<=k+1;l++)
				{
					if((i&lim[l])==lim[l])
					{
						dp[i|(1<<(l-2))][l]=min(dp[i|(1<<(l-2))][l],dp[i][j]+xdis[j][l]);
					}
				}
			}
		}
	}
	ans=inf;
	for(reg int i=1;i<=k+1;i++)ans=min(ans,dp[(1<<k)-1][i]+xdis[i][0]);
	cout<<ans;

补一个(dijkstra)板子防降智(qaq)

priority_queue<node> h;
inline void dijkstra(ll s)
{
	memset(vis,0,sizeof(vis));
	memset(dis,0x3f,sizeof(dis));
	dis[s]=0;
	h.push((node){s,0});
	while(!h.empty())
	{
		node ta=h.top();
		ll t=ta.no;
		h.pop();
		if(vis[t])continue;
		vis[t]=1;
		for(reg int i=head[t];i;i=next[i])
		{
			if(dis[e[i].v]>dis[t]+e[i].w)
			{
				dis[e[i].v]=dis[t]+e[i].w;
				h.push((node){e[i].v,dis[e[i].v]});
			}
		}
	}
}

总之永远不要放弃啊,你还会面对更多的挑战呢

愤怒的小鸟

强行解个解析式,然后直接状压就完事了。
首先三点是可以确定一条二次函数曲线的,所以对于每两头猪,如果他们的(x)不等,一定可以有一只从原点发射的鸟能够同时击杀,因此我们枚举每一对猪,求出击杀他们的二次函数曲线解析式(也就是求(a,b)
简单写下过程:
设猪的坐标为((x_1,y_1),(x_2,y_2)),则有
(egin{cases} ax_1^2+bx_1=y_1\ ax_2^2+bx_2=y_2 end{cases})
初中生感到亲切
这里的(x_1,y_1,x_2,y_2)都是已知的,暴力求解,得到
(egin{cases} a=frac{x_2y_1-x_1y_2}{x_1^2x_2-x_2^2x_1}\ b=frac{y_1x_2^2-y_2x_1^2}{x_1x_2^2-x_2x_1^2} end{cases} )
得到许多解析式以后,因为可能存在一条曲线过多只猪,所以我们要记得把所有满足条件的猪都加入到这条曲线的状态中,另外还要考虑只射一只猪的情况,为其他鸟留下表演空间。
还有一个很重要的优化,就是显然每个状态的二进制表示中没射过的最后一只猪早晚都是要射的,所以不如直接枚举包含最后一只猪的状态,因为考虑过只射一只,所以这样没有后效性,我们预处理一下每种状态最后一只没被射的猪的编号就行
(code:)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ld double
const double eps=1e-6;
#define N 20

void cal(ld &a,ld &b,ld x1,ld y1,ld x2,ld y2)
{
	b=(y2*x1*x1-y1*x2*x2)/(x2*x1*x1-x1*x2*x2);
	a=(y1-x1*b)/(x1*x1);
} 

inline ld func(ld a,ld b,ld x)
{
	return a*x*x+b*x;
}

ll lowzer[1<<N]={0};

inline void getlow()
{
	for(int i=0;i<(1<<N);i++)
	{
		ll j=0;
		while(i&(1<<j)&&j<=18)j++;
		lowzer[i]=j+1;
	}
}

ll bird[N][N]={0};
ld pos[N][3]={0};
ll dp[1<<N]={0};
ll t,n,m;
ld a,b;

void clean()
{
	memset(bird,0,sizeof(bird));
	memset(pos,0,sizeof(pos));
	memset(dp,0x3f3f3f3f,sizeof(dp));
}

inline bool dif(ld a,ld b)
{
	//cout<<"		"<<a<<' '<<b<<' '<<eps<<endl;
	return !(fabs(a-b)<=eps);
}

int main()
{
	cin>>t;
	getlow();
	while(t--)
	{
		clean();
		cin>>n>>m;
		for(int i=1;i<=n;i++)
		{
			cin>>pos[i][1]>>pos[i][2];
		}
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++)
			{
				if(!dif(pos[i][1],pos[j][1]))continue;
				cal(a,b,pos[i][1],pos[i][2],pos[j][1],pos[j][2]);
				if(a>-eps)continue;
				//cout<<'	'<<a<<' '<<b<<endl;
				for(int k=1;k<=n;k++)
				{
					if(!dif(func(a,b,pos[k][1]),pos[k][2]))
					{
						bird[i][j]|=(1<<(k-1));
						//cout<<bird[i][j]<<endl;
					}
				}
			}
		}
		dp[0]=0;
		for(int i=0;i<(1<<n);i++)//before
		{
			ll j=lowzer[i];
			for(int k=1;k<=n;k++)//after
			{
				dp[i|bird[j][k]]=min(dp[i|bird[j][k]],dp[i]+1);
			}
			dp[i|(1<<(j-1))]=min(dp[i|(1<<(j-1))],dp[i]+1);
		}
		cout<<dp[(1<<n)-1]<<endl;
	}
	return 0;
}

(教训:一定要看清楚自己写的函数的传参顺序)

动物园

看到数据范围,似乎并没有小的数?
哦他只能看五格……那就存五格的状态好了
(dp[i][S])为到前(i)格,现在看到状态为(S)时开心小朋友的最大值
在转移的时候先取当前状态的后四位,左移一格再讨论之前的一格是(0)(1),转移就很好写了:

dp[j][s]=max(dp[j-1][(s&15)<<1],dp[j-1][(s&15)<<1|1])+cnt[j][s];

其中,(cnt[j][S])这个数组维护的是站在第(j)格状态为(S)能开心的小朋友总数,这个数组我们读入以后预处理一下就行
另外因为是一个环所以我们先钦定一下最后一项的状态,然后把(dp[0][S_0])设成(0),其他都设(-inf),最后(dp[n][S_0])就是答案
不多说了,重在理解。
(code:)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define N 10005
#define C 50005

ll n,c;
ll e,f,l,tmp;
ll lik[C],hte[C];
ll cnt[N][32];
ll dp[N][32];

inline ll r()
{
	ll s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-')w=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		s=s*10+ch-'0';
		ch=getchar();
	}
	return s*w;
} 

int main()
{
	cin>>n>>c;
	for(int i=1;i<=c;i++)
	{
		e=r();f=r();l=r();
		for(int j=1;j<=f;j++)
		{
			tmp=r();
			hte[i]|=(1<<((tmp-e+n)%n));
		}
		for(int j=1;j<=l;j++)
		{
			tmp=r();
			lik[i]|=(1<<((tmp-e+n)%n));
		}
		
		for(int j=0;j<32;j++)
		{ 
			cnt[e][j]+=(bool)((j&hte[i])||(~j&lik[i]));
		}
	}
	ll ans=0;
	for(int i=0;i<32;++i)
    {
        memset(dp[0],128,sizeof(dp[0]));
		dp[0][i]=0;
        for(int j=1;j<=n;++j)
		{
			for(int s=0;s<32;++s) 
			{
				dp[j][s]=max(dp[j-1][(s&15)<<1],dp[j-1][(s&15)<<1|1])+cnt[j][s];
			}
        } 
        ans=max(ans,dp[n][i]);
    }
	cout<<ans;
	return 0;
}

排列perm

因为长度比较小,所以设状态为当前是否选了某位,另一维设成当前的余数。
(dp[i][S])为当前组成余数为(i),状态为(S)的方案总数,用数组记录某位置的数是否选过(用于去重),统计一下就好,答案是(dp[0][(1<<len)-1])
主要部分:

                for(r int i=0;i<(1<<len)-1;i++)
		{
			for(r int j=0;j<d;j++)
			{
				memset(used,0,sizeof(used));
				for(r int k=0;k<len;k++)
				{
					if((i|(1<<k))!=i&&!used[num[k]])
					{
						used[num[k]]=1;
						dp[i|(1<<k)][(j*10+num[k])%d]+=dp[i][j];
					}
				}
			}
		}

集合选数

刚开始试图推结论……然而并没推出来
然后突然发现其实以每一个(i%2||i%3)的数为左上角建立一个矩阵,使得
(egin{cases} a_{i,j}=a_{i,j-1}*3\ a_{i,j}=a_{i-1,j}*2 end{cases} )
那么一个数肯定不能和他上下左右的数被同时取到,于是就转化成状压的经典题型,设(dp[i][S])为第(i)行状态为(S)的答案总数,其他就是基本操作了
不过要注意一点,就是虽然注意了要清空,但是不要清空太多次……毕竟(memset())似乎也是(O(n))的,你要是每次都清空直接给自己平方可还行。所以每次建矩阵前清空下就成
这题最精华的部分应该就在于想到构造矩阵了吧,其他思路过去以后就不难了,而且我码风太丑所以就不放代码了

Bill的挑战

字符串数量不多,所以我们按列考虑,然后用每列的匹配状态转移。
因为他要(k)个字符串,所以预处理一下每个状态里(1)的个数
(dp[i][S])为匹配到第(i)位已匹配状态为(S)的方案数
然后枚举用于匹配字符串的每一位和上一位,因为有一位不行就无法匹配,所以转移时要用&
感觉自己讲的好乱……请看代码辅助理解吧(也不知道有没有人会看)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MOD 1000003
#define reg register

ll t;
ll n,k;
char ch[17][55];
ll len;
ll pos[55][1<<16];
ll dp[55][1<<16];
ll cnt[1<<16];

inline void clean()
{
	memset(dp,0,sizeof(dp));
	memset(pos,0,sizeof(pos));
	memset(ch,0,sizeof(ch));
	dp[0][(1<<n)-1]=1;
}

inline void pre()
{
	for(int i=1;i<(1<<16);i++)cnt[i]=cnt[i>>1]+(i&1);
}

int main()
{
	scanf("%lld",&t);
	pre();
	while(t--)
	{
		scanf("%lld%lld",&n,&k);
		clean();
		for(int i=0;i<n;i++)scanf("%s",ch[i]+1);
		len=strlen(ch[0]+1);
		for(reg int i=0;i<n;i++)
		{
			for(reg int j=1;j<=len;j++)
			{
				for(reg int k=0;k<26;k++)
				{
					if(ch[i][j]-'a'==k||ch[i][j]=='?')
					{
						pos[j-1][k]|=(1<<i);
					}
				}
			}
		}
		for(reg int j=1;j<=len;j++)//before
		{
			for(reg int i=0;i<(1<<n);i++)
			{
				if(dp[j-1][i])for(reg int k=0;k<26;k++)
				{
					dp[j][i&pos[j-1][k]]=(dp[j][i&pos[j-1][k]]+dp[j-1][i])%MOD;
				}
			}
		}
		ll ans=0;
		for(reg int i=0;i<(1<<n);i++)
		{
			if(cnt[i]==k)ans=(ans+dp[len][i])%MOD;
		}
		printf("%lld
",ans);
	}
	return 0;
}

小星星

给神题留坑。

原文地址:https://www.cnblogs.com/zzzuozhe-gjy/p/12741619.html