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

棋盘

需要注意的几点:

  • 题面编号都是从0开始的,所以第1行实际指的是中间那行
  • (2^{32})取模,其实就是(unsigned int),直接自然溢出啥事没有
  • 棋子攻击范围不会旋转

首先,我们得找出所有满足条件的同一行的状态,在此之前,我们要先处理出状态(S)下,第(k)行能被棋子攻击到的格子(at[k][S])
先钦定自己不能攻击自己
再枚举每一个状态中每一个1,然后对攻击范围模板进行左移/右移操作,把这一行这种状态能攻击到的格子或上他
接下来,枚举每一个状态,如果(at[1][i] and i==0),则记录下这种合法的情况。
这个时候我们就可以枚举每一行的状态来求解了,但是这样时间复杂度是(O(ncdot 2^{2m}))的,而在(n)给到(1e6)的情况下无疑会炸掉,需要考虑进一步的优化
(颓了一下题解)
其实,每一种状态都是由之前的某种状态转移来的,且可以重复,那就说明如果两个状态可以排在一起,他们就可以多次排在一起
考虑矩阵快速幂优化
设初始矩阵中(A[i][j]=1)表示(j)可以排在(i)的后面
则有这样的柿子:(f[i][j]=f[i][k]cdot f[k][j])
这样的情况下,我们求(A^n),因为第一种情况是为空,所以(sumlimits_{i=1}^{cnt}A^n[1][i])即为答案,意为开始为空,经过(n)行后最后一行为状态(i)的总数

code:

code:

#include<bits/stdc++.h>
using namespace std;
#define ll unsigned int
#define ull unsigned long long
#define ZZ_zuozhe
#define M 6
#define N 80

ll n,m,p,k;
ll lim[4];
ll at[4][1<<M];
ll sit[N],cnt=0;

struct mat
{
	ll a[N][N];
	mat(){memset(a,0,sizeof a);}
};

mat operator*(mat a,mat b)
{
	mat c;
	for(int i=1;i<=cnt;i++)for(int j=1;j<=cnt;j++)for(int k=1;k<=cnt;k++)
	{
		c.a[i][j]=c.a[i][j]+a.a[i][k]*b.a[k][j];
	}
	return c;
}

mat ksm(mat a,ll b)
{
	mat res=a;
	b--;
	while(b)
	{
		if(b&1)res=res*a;
		a=a*a;
		b>>=1;
	}
	return res;
}

mat a,ans;
ll t;

int main()
{
	scanf("%u%u%u%u",&n,&m,&p,&k);
	for(int i=0;i<3;i++)
	{
		for(int j=0;j<p;j++)
		{
			scanf("%u",&t);
			if(t)lim[i]|=(1<<j);
		}
		
	}
	lim[1]-=(1<<k);
	ll all=(1<<m)-1;
	for(int i=0;i<=all;i++)
	{
		at[0][i]=at[1][i]=at[2][i]=0;
		for(int j=0,p=i;p;j++,p>>=1)
		{
			if((p&1)==0)continue;
			at[0][i]|=(j<k)?lim[0]>>(k-j):lim[0]<<(j-k);
			at[1][i]|=(j<k)?lim[1]>>(k-j):lim[1]<<(j-k);
			at[2][i]|=(j<k)?lim[2]>>(k-j):lim[2]<<(j-k);//处理每种状态每行能够攻击到的位置集合
		}
	}
	for(int i=0;i<=all;i++)
	{
		if((i&at[1][i])==0)sit[++cnt]=i;
	}
	for(int i=1;i<=cnt;i++)
	{
		for(int j=1;j<=cnt;j++)
		{
			if(((sit[i]&at[0][sit[j]])==0)&&((sit[j]&at[2][sit[i]])==0))a.a[i][j]++;
                        //处理初始矩阵,如果j放在i后面不冲突就++
		}
	}
	a.a[1][1]=1;
	ans=ksm(a,n);
	ll aa=0;
	for(int i=1;i<=cnt;i++)
	{
		aa+=ans.a[1][i];
	}
	printf("%u
",aa);
	return 0;
}

最大前缀和

开始的思路

其实求最大前缀和似乎就是每个负数前判一次……
这几个数总排列方式是(n!),答案要乘他就说明直接拿可能值(cdot)这种情况的次数求和就行……
(n)的范围看起来挺合适拿来状压的
口胡一下:(f[i][S])为选(i)个当前状态为(S)的期望和
如果新取一个负数答案应该是直接加,取正数就得更新,但是好像也不一定会更新……
是不是得记录下当前值和更新答案的临界值什么的……
或者用状压记录负数位置……?
(实际证明没一点挨上边)

正解

状压还能这么出的吗……
深挖一下最大前缀和的性质:

[max(sumlimits_{j=1}^{i} a_j),iin [1,n] ]

设数列(A)的最大前缀和为(pre_j),即(sumlimits_{i=1}^{j} a_i)
那么由此可以推出:

  1. 对于任意(1leq x leq j),有(sumlimits_{i=x}^{j} A_i)定不小于0(否则可以把这部分减去)
  2. 对于任意(j+1leq x leq n),有(sumlimits_{i=j+1}^{x} A_i)定不大于0(否则可以把这部分加上)

然后,我们发现,原数列可以分成两部分,分别满足不同的条件,既然如此,我们把满足左边条件的排列个数与满足右边条件的排列个数相乘,就可以得到总方案个数。
但是,题目中要求我们求最大前缀和的期望(cdot n!),也就是用每种可能值乘上能得到它的方案个数,所以,结合状压,我们可以将每种状态满足左右的排列个数相乘,再把每种状态的满足条件方案个数乘上每种状态选的数的总和,即为所求
对于右边,我们在一个所有前缀和为负的数列后面加一个数,若新数列总和仍然为负,则新数列所有前缀和为负
对于左边,我们在一个所有后缀和为正的数列前面加一个数,若新数列总和仍然为非负,则新数列所有后缀和为非负
基于这样的过程,我们可以枚举子集,则有

[g[i]=sum g[i-j](jin i,sum[i]< 0),f[i+j]+=f[i](j otin i,sum[i]geq 0) ]

答案为

[sumlimits_{i=0}^{all} sum[i]*f[i]*g[all xor i] ]

另外注意:答案要求非负整数,所以最后要模成正的

code:

code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define N 21
#define mod 998244353
#define S (1<<20)

ll n,all;
ll a[N];
ll sum[S];
ll f[S],g[S];
ll ans=0;

int main()
{
	scanf("%lld",&n);
	all=(1<<n)-1;
	for(int i=0;i<n;i++)scanf("%lld",&sum[1<<i]);
	for(int i=0;i<=all;i++)sum[i]=(sum[i&(-i)]+sum[i^(i&-i)])%mod;
	g[0]=1;
	for(int i=0;i<=all;i++)
	{
		if(sum[i]<0)
		{
			for(int j=0;j<n;j++)
			{
				if(i&(1<<j))
				{
					g[i]=(g[i]+g[i^(1<<j)])%mod;
				}	
			}
		}
	}
	f[0]=1;
	for(int i=0;i<=all;i++)
	{
		if(sum[i]>=0)
		{
			for(int j=0;j<n;j++)
			{
				if((i&(1<<j))==0)
				{
					f[i|(1<<j)]=(f[i|(1<<j)]+f[i])%mod;
				}
			}
		}
	}
	for(int i=0;i<=all;i++)
	{
		//cout<<sum[i]<<' '<<f[i]<<' '<<g[all^i]<<endl;
		ans=(ans+sum[i]*f[i]%mod*g[all^i]%mod+mod)%mod;
	}
	printf("%lld",ans);
	return 0;
}

小星星

事实证明确实不是很擅长 (dp) 这块,必须得多看了……
这种神仙思路怎么想到啊……
先是一个 (O(ncdot 3^n)) 的暴力:
(f[u][u'][S])(u) 在原图上对应 (u') ,其在新图子树中的点在原图中对应情况为 (S) 的方案数
那么 (u) 在树上,可以树形 (dp) 求出来,具体的方法是一边合并子树一边计算答案,详细见代码注释

code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define N 18
#define E 36
#define S (1<<18)
#define pb push_back

ll n,m,t1,t2;
vector<ll> e[N];
ll to[E],nx[E],h[N],tot=0;
inline void add(ll a,ll b){ 
	to[++tot]=b;
	nx[tot]=h[a];
	h[a]=tot;
}

vector<ll> s[N];
ll siz[N],f[N][N][S];
inline void dfs(ll u,ll fa){
	siz[u]=1;
	for(int i=1;i<=n;i++)f[u][i][1<<i-1]=1;//初始化:u在原图对应i
	for(int i=h[u];i;i=nx[i])if(to[i]!=fa){ ll v=to[i];
		dfs(v,u);
		//将子树合并过来
		for(int uu=1;uu<=n;uu++){//新图上映射点
			for(int j=0;j<s[siz[u]].size();j++){//找到符合条件的当前映射子树集合
				ll su=s[siz[u]][j];
				if(!(su&(1<<uu-1)))continue;//uu的子树必须有uu
				for(int tv=0;tv<e[uu].size();tv++){ ll vv=e[uu][tv];//找一条从uu连出去的vv,用来对应v
					if(su&(1<<vv-1))continue; //vv是要合并进来的,之前不能有
					for(int k=0;k<s[siz[v]].size();k++){//找到vv的子树集合
						ll sv=s[siz[v]][k];
						if((su&sv)||!(sv&(1<<vv-1)))continue;//vv子树必须有vv,而且不能和uu子树相交
						f[u][uu][su|sv]+=f[u][uu][su]*f[v][vv][sv];//合并起来
					}
				}
			}
		}
		siz[u]+=siz[v];//更新子树大小
	}
}
ll ans=0;

inline ll read(){
	ll f=0,s=0; char c=getchar();
	while(c>'9'||c<'0')f=(c=='-'),c=getchar();
	while(c>='0'&&c<='9')s=(s<<3)+(s<<1)+(c^'0'),c=getchar();
	return f?-s:s;
}

int main(){
	n=read(); m=read();
	for(int i=1;i<=m;i++){
		t1=read(),t2=read();
		e[t1].pb(t2); e[t2].pb(t1);
	}
	for(int i=1;i<n;i++){
		t1=read(); t2=read();
		add(t1,t2); add(t2,t1);
	}
	for(int i=1,x=0;i<(1<<n);i++,x=0){
		for(int j=1;j<=n;j++)if(i&(1<<j-1))x++;
		s[x].pb(i);//处理出每种大小的集合,用来枚举子树映射集合
	}	
	dfs(1,0);
	for(int i=1;i<=n;i++)ans+=f[1][i][(1<<n)-1];
	printf("%lld",ans);
	return 0;
}

正解:
似乎优化子集dp的常见方法是容斥
之前的算法中,我们规定每个点被映射有且仅有一次,如果不规定,单次dfs复杂度 (O(ncdot 3^n) o O(n^3)),但是这样会造成答案偏大,因为一些点被重复映射,一些点没有被映射,要去掉这些与题意不符的答案,就用到了容斥。
我们的目标是求所有点都被映射有且仅有一次的方案数,那么首先减去有一个点未被映射的方案,这时就多减了有两个点未被映射的方案,同理需要再加上三个点的方案,减去四个点的方案……最后结果是所有不符合题意的答案都被除去

code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int long long
#define N 18
#define E 36
#define S (1<<18)
#define pb push_back

ll n,m,t1,t2;
vector<ll> e[N];
ll to[E],nx[E],h[N],tot=0;
inline void add(ll a,ll b){ 
	to[++tot]=b;
	nx[tot]=h[a];
	h[a]=tot;
}

vector<ll> s[N];
ll del[N];
ll siz[N],f[N][N];
inline void dfs(ll u,ll fa){ ll sum=0;
	for(int i=1;i<=n;i++)f[u][i]=1;//初始化:u在原图对应i
	for(int i=h[u];i;i=nx[i])if(to[i]!=fa){ ll v=to[i];
		dfs(v,u);
		//将子树合并过来
		for(int uu=1;uu<=n;uu++){//新图上映射点
			sum=0;
			for(int tv=0;tv<e[uu].size();tv++){ ll vv=e[uu][tv];
				sum+=f[v][vv]*(del[uu]&del[vv]);
			}
			f[u][uu]*=sum;//乘法原理,已经得到的方案和新方案里选一对
		}
	}
}
ll ans=0;

inline ll read(){
	ll f=0,s=0; char c=getchar();
	while(c>'9'||c<'0')f=(c=='-'),c=getchar();
	while(c>='0'&&c<='9')s=(s<<3)+(s<<1)+(c^'0'),c=getchar();
	return f?-s:s;
}

signed main(){
	n=read(); m=read();
	for(int i=1;i<=m;i++){
		t1=read(),t2=read();
		e[t1].pb(t2); e[t2].pb(t1);
	}
	for(int i=1;i<n;i++){
		t1=read(); t2=read();
		add(t1,t2); add(t2,t1);
	}
	for(int i=0;i<(1<<n);i++){ ll siz=n,res=0; 
		for(int j=1;j<=n;j++)del[j]=0;
		for(int j=1;j<=n;j++)if((1<<j-1)&i)del[j]=1,siz--; //del[i]表示留下i
		dfs(1,0);
		for(int j=1;j<=n;j++)res+=f[1][j]; //sum(f[1][j])即为这种情况下的答案
		if(siz%2)ans-=res;
		else ans+=res;
	}
	printf("%lld",ans);
	return 0;
}
原文地址:https://www.cnblogs.com/zzzuozhe-gjy/p/13418135.html