• 「刷题笔记」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;
    }
    
  • 相关阅读:
    [bzoj] 2565 最长双回文串
    [codeforces] 17E Palisection
    luogu P3267 [JLOI2016/SHOI2016] 侦查守卫
    181020-181021 模拟 题解
    luogu P2571 [SCOI2010]传送带
    poj1064 Cable master
    poj1422 Air Raid
    luogu P2512 [HAOI2008]糖果传递
    CF549H Degenerate Matrix
    左偏树基础教学
  • 原文地址:https://www.cnblogs.com/zzzuozhe-gjy/p/13418135.html
Copyright © 2020-2023  润新知