zoukankan      html  css  js  c++  java
  • 插头$DP$学习小结

    插头(DP)学习小结

    这种辣鸡毒瘤东西也能叫算法。。。

    很优秀的一个算法。

    最基本的适用范围主要是数据范围极小的网格图路径计数问题。

    如果是像(Noi2018)那种的话建议考生在其他两道题难度超过普及组的情况下放弃这题。

    其实大佬想做也可以去刚一下

    切记如果在考场上看到这种题目,千万不要觉得你看出正解就是切了此题。

    请一定将插头(DP)题当做一道毒瘤大模拟看待。


    要点

    这种东西细节挺多的,如果是比较灵活的题目那些转移一定都要好好考虑清楚,尽量做到一次过,否则调试时间可能会爆炸。

    目前只做了一道题就是清华集训的简单回路。

    用自己的写法加借鉴(Zsy)以及萝卜的实现思路,我完成了这道题。

    下面是细节注意点:

    (1.) 关于状态的设计。首先像这道题可以使用括号序列来优化状态,其他题目也可能有相应的方法来优化状态。那么,以这题打比方,我们采用括号序列来优化状态,比较常见的做法有几种状态表示。

    一种是三进制写法,将每一个格子的状态压成三进制数(左括号,右括号,无括号)来进行转移,无用的状态在转移的过程中处理掉从而不影响复杂度,这样的实现比较直接,在转移时的做法在后面会提到。

    第二种是四进制写法,与三进制类似,它比三进制优秀的一点是可以使用系统的位运算从而达到看起来更加优秀的常数以及更加顺手的实现。

    以上两种对于空间都有所浪费,当然也可以通过(Hash)或者(Map)把有效状态压到一起,这点就根据题目的空间要求以及考场上时间的充裕程度而定了。

    我用的方法和以上都有不一样的地方,我将有用的状态直接搜了出来存在(vector)里面,并加以编码,需要回映射的时候使用(map)来查询标号,因为对于(map)的调用都在预处理里面所以并不会对总复杂度产生影响,这样调值的时候就是直接访问数组了,在我看来这样比较方便。

    而上面三种写法的状态都是从左到右记录轮廓线的状态,另外还有一种写法是把竖着的那一条轮廓线记在一个固定位置,其他的位置与网格图一一对应。

    当然这些都只是个人习惯问题,在灵活处理的情况下是不影响代码正确性的。


    (2.) 关于一般转移。一般是进行分类讨论,对当前格子的两条轮廓线是否有插头以及它是左括号还是右括号进行讨论,根据题目来画图辅助理解转移即可。实现方式可以预处理类似建图一样将有效转移连一条边,也可以是直接写一个转移函数在转移的过程中大力讨论,这里建议前一种,后一种可能导致转移的常数过大甚至影响到复杂度。

    另外,对于每道题的转移都会有各自的一些要点,比如简单回路这题,我们需要的是单一的一条回路,所以对于两条轮廓线的状态分别是(1,2)(左括号,右括号)的时候,是不能转移到这两个括号合并的状态去的,因为这样的两个括号是一对括号,如果闭合就会在当前状态中形成一个独立的回路,从而产生不合法的贡献。

    上面前三种写法在一般转移的时候是没有不同之处的,第四种可能会麻烦一点,具体可以问问萝卜第四种的写法。


    (3.) 关于障碍物。对于障碍物的转移,我们需要额外新增一种转移,每到障碍物格子的时候,将该两条轮廓线上没插头的状态复制出去,其他转移都是不合法的,这里所有的写法应该都大同小异。


    (4.) 关于边界时。当你(DP)到一行的最后一个需要跳到下一行时,你需要新增一种转移,并且在每次换行时调用。对于前三种实现方式,显然有效的状态是最后一条轮廓线无插头的状态,然后将竖的轮廓线(最后一条)移到第一位,其他整体向后移动一位。对于前两种转移运用位运算(三进制自己写)来比较方便地完成,而我的写法则利用(vector)的自带函数(pop\_back)以及(insert)来完成,也比较方便,同时因为是预处理所以并不影响复杂度((insert)(O(n))的)。而对于第四种,我们发现它并不需要任何额外的转移,正常做即可,因为他的竖轮廓线不受位置的影响。


    (5.) 关于两个括号序列是否可以合并。部分题目(比如这题)就需要判断两个括号序列是否可以合并而形成一个完整的回路(或者多个环之类的)。关于上面的问题有两种做法。

    第一种是模拟走环,就是模拟在这个环上的行走过程,某一时刻从一个括号走出去了或者走到起点时有括号没走到则是不合法。

    第二种用并查集。首先检查括号个数是否相同,然后将同一个括号的两个括号并起来,再上下依次相并,最后检查所有括号是否都被并到了一起。我用的是第二种,但是这种方法扩展性并不高,也许需要一定的改良,建议理解第一种。


    (6.) 关于常数以及复杂度。再提一下,插头(DP)的效率很大程度上可以从预处理中优化,即可以通过预处理来对代码效率进行优化,有时可以直接降低复杂度,如果你被卡常了,尝试多预处理一些东西,特别是在复杂度最高的那些地方。


    大概就是这一些吧,有什么后面再补充。这里贴一下简单回路的代码。(至少刚交的时候是(UOJ)(RK1)

    #include<cstdio>
    #include<map>
    #include<vector>
    
    using namespace std;
    
    #define N 1010
    #define ll long long
    #define RG register
    #define MOD 1000000007
    
    inline int read(){
        RG int x=0,t=1;RG char ch=getchar();
        while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
        if(ch=='-')t=-1,ch=getchar();
        while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
        return x*t;
    }
    
    int n,m,K,Q,Map[N][10];
    
    vector<int> V[150]; int cnt; map<vector<int>,int> M;
    inline int Plu(const int &x,const int &y) { return x+y>=MOD?x+y-MOD:x+y; }
    inline void Dfs(int k,int L,vector<int> now){
    	if(k==m+2){	if(!L) V[++cnt]=now,M[now]=cnt; return ; }
    	now.push_back(0),Dfs(k+1,L,now),now.pop_back();
    	now.push_back(1),Dfs(k+1,L+1,now),now.pop_back();
    	if(L) now.push_back(2),Dfs(k+1,L-1,now),now.pop_back();
    }
    
    int f[2][150],F[N][150],G[N][150],Ans[N][10];
    struct mona { int nxt,en; } ;
    
    struct Trans{
    	int first[150],top; mona s[150*150];
    	inline void Insert(int x,int y) { s[++top]=(mona) { first[x],y },first[x]=top; }
    	inline void To1(int j){
    		vector<int> now;
    		for(RG int i=1;i<=cnt;++i){
    			int A=V[i][j-1],B=V[i][j];
    			if(A==0&&B==0) now=V[i],now[j-1]=1,now[j]=2,Insert(i,M[now]),Insert(i,i);
    			else if(A!=0&&B==0) now=V[i],now[j]=A,now[j-1]=0,Insert(i,M[now]),Insert(i,i);
    			else if(A==0&&B!=0) now=V[i],now[j-1]=B,now[j]=0,Insert(i,M[now]),Insert(i,i);
    			else if(A==2&&B==1) now=V[i],now[j-1]=now[j]=0,Insert(i,M[now]);
    			else if(A==1&&B==1){ int t=1,k;
    				for(k=j+1;t;++k)
    					if(V[i][k]==1) ++t;
    					else if(V[i][k]) --t;
    				now=V[i],now[k-1]=1,now[j-1]=now[j]=0,Insert(i,M[now]);
    			}	else if(A==2&&B==2){ int t=1,k;
    				for(k=j-2;t;--k)
    					if(V[i][k]==1) --t;
    					else if(V[i][k]) ++t;
    				now=V[i],now[k+1]=2,now[j-1]=now[j]=0,Insert(i,M[now]);
    			}
    		}
    	}
    	inline void To2(){
    		for(RG int i=1;i<=cnt;++i){
    			if(V[i][m]) continue ; vector<int> now=V[i];
    			now.pop_back(),now.insert(now.begin(),0);
    			Insert(i,M[now]);
    		}   return ;
    	}
    	inline void To3(int j){
    		for(RG int i=1;i<=cnt;++i)
    			if(!V[i][j-1]&&!V[i][j]) Insert(i,i);
    	}
    	inline void Gone(int *F,int *G){
    		for(RG int i=1;i<=cnt;++i)
    			for(RG int j=first[i];j;j=s[j].nxt)
    				G[s[j].en]=Plu(G[s[j].en],F[i]);
    	}
    }   A[10],C[10],D;
    
    struct Answer { int a,b; } st[150*150]; int tot,fa[20],tmp1[10],tmp2[10];
    inline int Find(int x) { if(fa[x]!=x) fa[x]=Find(fa[x]); return fa[x]; }
    inline void Merge(int x,int y) { int fx=Find(x),fy=Find(y); if(fx^fy) fa[fx]=fy; }
    inline bool Check(int A,int B){
    	int tt1=0,tt2=0,cp=0; if(V[A][m]) return 0;
    	for(RG int i=1;i<=m;++i){
    		if((V[A][i]&&!V[B][i])||(!V[A][i]&&V[B][i])) return 0;
    		fa[i]=i,fa[i+m]=i+m;
    	}
    	for(RG int i=0;i^m;++i){
    		if(V[A][i]==1) tmp1[++tt1]=i+1;   if(V[A][i]==2) Merge(tmp1[tt1--],i+1);
    		if(V[B][i]==1) tmp2[++tt2]=i+1+m; if(V[B][i]==2) Merge(tmp2[tt2--],i+1+m);
    	}	for(RG int i=0;i^m;++i) if(V[A][i]&&V[B][i]) Merge(i+1,i+1+m);
    	for(RG int i=0;i^m;++i){
    		if(!V[A][i]) continue ;	if(!cp) cp=Find(i+1);
    		if(Find(i+1)^cp)   return 0;
    	}
    	for(RG int i=0;i^m;++i){
    		if(!V[B][i]) continue ;	if(!cp) cp=Find(i+1+m);
    		if(Find(i+1+m)^cp) return 0;
    	}   return 1;
    }
    inline void Pre(){
    	for(RG int i=1;i<=cnt;++i)
    		for(RG int j=1;j<=cnt;++j)
    			if(Check(i,j)) st[++tot]=(Answer) { i,j };
    }
    
    int main(){
    	freopen("travel.in","r",stdin);
    	freopen("travel.out","w",stdout);
    	n=read(),m=read(),K=read(),Dfs(1,0,V[0]);
    	for(RG int i=1;i<=K;++i) Map[read()][read()]=1;
    	for(RG int i=1;i<=m;++i) A[i].To1(i); D.To2(),Pre();
    	for(RG int i=1;i<=m;++i) C[i].To3(i);
    	int now=1,lst=0; f[lst][1]=1;
    	for(RG int i=1;i<=n;++i){
    		for(RG int k=1;k<=cnt;++k) f[now][k]=0;
    		D.Gone(f[lst],f[now]),now^=lst^=now^=lst;
    		for(RG int j=1;j<=m;++j,now^=lst^=now^=lst){
    			for(RG int k=1;k<=cnt;++k) f[now][k]=0;
    			if(!Map[i][j]) A[j].Gone(f[lst],f[now]);
    			else C[j].Gone(f[lst],f[now]);
    		}   for(RG int k=1;k<=cnt;++k) F[i][k]=f[lst][k];
    	}   for(RG int k=1;k<=cnt;++k) f[lst][k]=0; f[lst][1]=1;
    	for(RG int i=n;i;--i){
    		for(RG int k=1;k<=cnt;++k) f[now][k]=0;
    		D.Gone(f[lst],f[now]),now^=lst^=now^=lst;
    		for(RG int j=1;j<=m;++j,now^=lst^=now^=lst){
    			for(RG int k=1;k<=cnt;++k) f[now][k]=0;
    			if(!Map[i][j]) A[j].Gone(f[lst],f[now]);
    			else C[j].Gone(f[lst],f[now]);
    		}   for(RG int k=1;k<=cnt;++k) G[i][k]=f[lst][k];
    	}
    	for(RG int i=1;i^n;++i)
    		for(RG int j=1;j<=m;++j)
    			for(RG int k=1;k<=tot;++k){
    				int p=st[k].a,q=st[k].b;
    					if(V[p][j-1]&&V[q][j-1])
    						Ans[i][j]=(1LL*F[i][p]*G[i+1][q]+Ans[i][j])%MOD;
    			}	Q=read(); while(Q--) printf("%d
    ",Ans[read()][read()]);
    }
    
    
  • 相关阅读:
    艾伟_转载:你知道吗?——ASP.NET的Session会导致的性能问题 狼人:
    艾伟_转载:一次挂死(hang)的处理过程及经验 狼人:
    艾伟也谈项目管理,微型项目实践感悟 狼人:
    艾伟_转载:[原创]再谈IIS与ASP.NET管道 狼人:
    艾伟_转载:企业库缓存依赖的实现基于文件依赖 狼人:
    艾伟也谈项目管理,我也发软件开发团队的思考(侧重点是人员) 狼人:
    MYSQL用户名:root
    map 和 unordered_map以char * 为key
    设计模式单例模式(singleton)
    Android允许其他应用程序启动你的Activity
  • 原文地址:https://www.cnblogs.com/Lovemona/p/10114805.html
Copyright © 2011-2022 走看看