1. 程式人生 > 其它 >Go語言 - 檔案操作

Go語言 - 檔案操作

解決與連通性有關 DP 問題的有力武器

upd on 2021.7.29:

終於在我生日這一天來補這篇部落格了,tzc 老鴿子(

前置知識:輪廓線 \(dp\)

對於有些涉及到相鄰格子之間的限制的狀壓 \(dp\),如果我們按行轉移要列舉 \(2^m\) 個狀態,複雜度過高,但由於我們只需要關心與未轉移格子所相鄰的格子的狀態,因此我們可以考慮逐格轉移,那麼顯然轉移的點與未轉移的點之間存在一條分界線,我們就稱這條線為輪廓線,那麼我們只用狀壓輪廓線以上部分是否選擇即可,這樣複雜度即可降到 \(2^nnm\)

例題:LOJ 2372 -「CEOI2002」臭蟲積體電路公司

插頭 dp

插頭 \(dp\) 能夠用來求解與連通性有關的問題,包括但不限於迴路計數、路徑問題、連通塊最大權值、廣義路徑問題等,一般對於資料範圍極小,並且涉及到與連通性有關的詞彙時,可以想到插頭 \(dp\)

那麼就讓我們從模板題 P5056 【模板】插頭dp 入手來講解這一神奇的演算法(

首先我們需要明白“插頭 \(dp\)”中“插頭”的含義是什麼,在上面前置知識:輪廓線 \(dp\) 中我們定義了輪廓線對吧,那麼對於一個迴路而言,顯然這條輪廓線會穿過迴路中的某些點,那麼我們就把這條輪廓線與迴路產生的這些交點稱作“插頭”,譬如對於下圖而言,橙色的輪廓線上方就有四個插頭:

那麼我們怎樣記錄一個插頭的狀態呢?首先由於輪廓線長度為 \(m+1\),因此需用一個長度為 \(m+1\) 的陣列來儲存這個輪廓線的狀態,具體來說假設我們當前遍歷到第 \(i\) 行第 \(j\) 列,那麼我們輪廓線第 \(x(x<j)\)

位儲存第 \(x\) 列分界線上下插頭的狀態,第 \(j+1\) 位儲存第 \(i\) 行第 \(j\) 列與第 \(i\) 行第 \(j+1\) 列分界線之間插頭的狀態,第 \(y(y>j+1)\) 位儲存第 \(y-1\) 列分界線上下插頭的狀態。但是記錄插頭時候,僅僅簡單地記錄一個點是否存在插頭是不可行的,因為對於上面例子中的圖和下圖,它們輪廓線上有無插頭的狀態一致,但轉移的情況卻不同:

因此我們不僅要記錄每個點是否存在插頭,還要記錄哪些插頭在同一個連通塊中,此時就有兩種思路:

  1. 最小表示法,也就是將所有聯通塊看作一個不同的數,並且對於每個連通塊,記其編號為第一個屬於這個連通塊的點前面不同連通塊的個數加一,即序列 \((2,4,3,3,5,2,1)\)

    按照這種方式可以被重新標號為 \((1,2,3,3,4,1,5)\),上面例子中的第一幅(靠上的圖)輪廓線的狀態可以被標號為 \((1,2,0,0,2,1)\),下面一幅圖輪廓線的狀態可以被標號為 \((1,1,0,0,2,2)\)

  2. 括號表示法,注意到對於迴路而言,顯然不會存在交叉的情況,因此對於任意連通塊,必須恰好有兩個點屬於這個連通塊,並且如果將這兩個點視作一個區間,那麼這些連通塊表示的區間只存在包含、外離,不存在相交的情況,此時我們的維護方法就呼之欲出:括號序列!具體來說,對於所有連通塊第一次出現的位置,我們將其看作左括號,第二次出現的位置也就自然看成右括號,那麼此時得到的括號序列顯然是合法的括號序列。那麼有人就問了,沒有插頭怎麼辦呢?對於沒有插頭的地方我們直接給它補上 0 即可,譬如上面的情況用括號序列表示就是 \(((00))\),下面的情況用括號序列表示就是 \(()00()\)

    注意:只有迴路、路徑才能用括號表示法,對於連通塊,由於不存在上述性質,因此只能用最小表示法!

那麼怎麼運用插頭 \(dp\) 解決上述問題呢?考慮狀壓 \(dp\)\(dp_{i,j,S}\) 表示考了到了第 \(i\) 行第 \(j\) 列,當前輪廓線上狀態為 \(S\) 的方案數。這裡咱們採用括號表示法,即將左括號視作 \(1\),右括號視作 \(2\),看成一個四進位制數(為什麼是四進位制呢,因為可以用位運算減少常數),轉移就分情況討論:

  • \((i,j)\) 上有障礙,此時該點上顯然不能連線插頭,因此只有 \(j\)\(j+1\) 處都沒有插頭的情況才可以轉移,並且狀態不變。
  • \((i,j)\) 上沒有障礙,那麼記 \(b1\)\(S\)\(j\) 位的值,\(b2\)\(S\)\(j+1\) 位的值。
    1. 如果 \(b1=0\)\(b2=0\),那麼只能在 \((i,j)\) 處新建兩個插頭,即將 \(S\) 的第 \(j\) 位改為 \(1\),第 \(j+1\) 位改為 \(2\)
    2. 如果 \(b1\ne 0\)\(b2=0\),那麼這個插頭可以直行,也可以右拐,直行的情況就是將插頭從第 \(j\) 位移至第 \(j+1\) 位,右拐的情況就是插頭位置保持不變,分別轉移一下即可。
    3. 如果 \(b1=0\)\(b2\ne 0\),同上
    4. 如果 \(b1=1\)\(b2=1\),那麼我們顯然只能將這兩個插頭合併起來並去掉它們,但是直接去掉相當於在括號序列中去掉兩個左括號,會導致括號序列的不合法,因此我們需要向右找到與第二個左括號匹配的右括號,將其改為左括號。
    5. 如果 \(b1=2\)\(b2=2\),與第四種情況類似,不過這次我們要找到與第一個右括號匹配的左括號並將其改為右括號。
    6. 如果 \(b1=2\)\(b2=1\),那直接把這個插頭刪除即可。
    7. 如果 \(b1=1\)\(b2=2\),那麼閉合這個插頭就會形成閉合迴路,對於此題而言,由於只能形成一個閉合迴路,因此該情況只能在 \(i=ex\)\(j=ey\) 的情況下轉移,其中 \((ex,ey)\) 為最右下角的空格子。

還有一個細節,就是我們 DP 的時候四進位制數的值域可能很大,高達 \(2^{24}\),直接列舉會爆,不過注意到可以成為合法的括號序列的狀態數量並不算多,因此考慮剪枝。考慮對 DP 建一個雜湊表,具體來說,由於輪廓線 DP 是可以用滾動陣列優化的,因此考慮從 \(dp_{i,j,S}\) 轉移到 \(dp_{i,j+1,T}\) 時就將 \(T\) 插入雜湊表,如果發現雜湊表中這個值已經存在了就將貢獻累加上去,查詢所有有用的 DP 就遍歷一遍雜湊表即可,具體實現細節可見程式碼。

冷知識:\(10^5\) 級別的雜湊表模數:\(65537,123457,333337,333667,786433\)

const int MAXN=12;
const int MOD=123457;
const int MAX=3e6;
int n,m,pw4[MAXN+3],ex,ey;char s[MAXN+3][MAXN+3];
int hd[MOD+3],nxt[MAX+5],key[2][MAX+5],cnt[2],cur=0,pre=1;
ll val[2][MAX+5],ans=0;
void insert(int x,ll v){
	for(int e=hd[x%MOD];e;e=nxt[e])
		if(key[cur][e]==x) return val[cur][e]+=v,void();
	nxt[++cnt[cur]]=hd[x%MOD];hd[x%MOD]=cnt[cur];
	key[cur][cnt[cur]]=x;val[cur][cnt[cur]]=v;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%s",s[i]+1);
	for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
		if(s[i][j]=='.') ex=i,ey=j;
	for(int i=(pw4[0]=1);i<=MAXN;i++) pw4[i]=pw4[i-1]<<2;
	cnt[cur]=1;val[cur][1]=1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=cnt[cur];j++) key[cur][j]<<=2;
		for(int j=1;j<=m;j++){
			memset(hd,0,sizeof(hd));cur^=pre^=cur^=pre;cnt[cur]=0;
			for(int k=1;k<=cnt[pre];k++){
				int msk=key[pre][k];ll cnt=val[pre][k];
//				printf("%d %d %d %lld\n",i,j,msk,cnt);
				int b1=msk>>(j-1<<1)&3,b2=msk>>(j<<1)&3;
				if(s[i][j]=='*'){
					if(!b1&&!b2) insert(msk,cnt);
				} else{
					if(!b1&&!b2){
						if(s[i][j+1]=='.'&&s[i+1][j]=='.')
							insert(msk+pw4[j-1]+2*pw4[j],cnt);
					} else if(!b1&&b2){
						if(s[i][j+1]=='.') insert(msk,cnt);
						if(s[i+1][j]=='.') insert(msk-pw4[j]*b2+pw4[j-1]*b2,cnt);
					} else if(b1&&!b2){
						if(s[i+1][j]=='.') insert(msk,cnt);
						if(s[i][j+1]=='.') insert(msk-pw4[j-1]*b1+pw4[j]*b1,cnt);
					} else if(b1==1&&b2==1){
						int c=1;
						for(int l=j+1;l<=m;l++){
							if(msk>>(l<<1)&1) ++c;
							if(msk>>(l<<1|1)&1) --c;
							if(!c){
								insert(msk-pw4[j-1]-pw4[j]-pw4[l],cnt);
								break;
							}
						}
					} else if(b1==2&&b2==2){
						int c=1;
						for(int l=j-2;~l;l--){
							if(msk>>(l<<1)&1) --c;
							if(msk>>(l<<1|1)&1) ++c;
							if(!c){
								insert(msk-2*pw4[j-1]-2*pw4[j]+pw4[l],cnt);
								break;
							}
						}
					} else if(b1==2&&b2==1){
						insert(msk-2*pw4[j-1]-pw4[j],cnt);
					} else {
						if(i==ex&&j==ey) ans+=cnt;
					}
				}
			}
		}
	} printf("%lld\n",ans);
	return 0;
}

例題就暫(yong)時(yuan)咕掉了。