1. 程式人生 > 其它 >Codeforces 1368H - Breadboard Capacity(最小割+線段樹維護矩陣乘法)

Codeforces 1368H - Breadboard Capacity(最小割+線段樹維護矩陣乘法)

從一個 trivial 的網路流入手,找性質分析最小割形態+線段樹維護矩陣乘法,hot tea

Easy version:Codeforces 題面傳送門 & 洛谷題面傳送門

Hard version:Codeforces 題面傳送門 & 洛谷題面傳送門

首先看到這種從某一種顏色的點連向另一種顏色的點,要求經過的邊不重複的問題,可以很自然地想到網路流,具體來說咱們建立源 \(S\) 和匯 \(T\),從源點 \(S\) 向所有紅色點連容量為 \(1\) 的邊,從所有藍色點向匯點 \(T\) 連容量為 \(1\) 的邊,然後將網路內部所有邊都改為容量為 \(1\) 的雙向邊,然後跑最大流就是答案。

但是就算 H1 資料範圍也高達 \(10^5\),更別說 H2 還帶修改呢,因此直接最大流顯然不可行。因此考慮模擬最大流(瞎起名字*1),即考慮原圖的最小割。但是問題又來了,且不談時間複雜度的問題,如果按照以往模擬最大流的套路:使用以最小割的情況為狀態設計 \(dp\)

,或者對於每個割掉的邊集重新建圖跑最大流,套用到此題上顯然不可行。怎麼辦呢?這時候就要用到我們善於發現的眼睛了,對於這種看似無從下手的題目,我們不妨來分析一下該圖的最小割有什麼性質,我們考慮將與源點連通的點標為紅色,與匯點連通的點標為藍色,那麼可以發現以下性質:

Observation \(1\). 如果割掉的邊內部出現環,並且這些環上的邊都連在內部的 \(n\times m\) 個點中(即不與邊界上的 \(2(n+m)\) 個點連邊),我們顯然可以改變環內部點的顏色使答案不會變得更劣。

證明:顯然如果我們改變環內部的顏色,使其與外界一致,原來連線環內部和環外部的割邊都會消失並且不會新增新的割邊,因此割的大小減少。

Observation \(2\). 如果出現一條割掉的邊形成的路徑連線了同一個或相鄰的邊界,那麼我們可以改變點的顏色使這樣的路徑消失並且答案不會變得更劣。

也就是對於下圖中左邊的情形,我們可以將其改成右邊的情形(注:這裡借用了官方題解的圖):

證明:如果我們將這個割掉的邊形成的路徑包圍的部分中,屬於內部的 \(nm\) 個點的部分(也就是上圖中改變顏色的五個點)的顏色改為與路徑另一側的點一直,那麼顯然這條路徑上的割邊都會消失,此同時有可能會增加一些割邊,這些增加的割邊都來自於邊界上的點與該路徑包圍的區域中的點連成的邊(也就是上圖中增加的三條割邊),但是很顯然的一件事是,增加的割邊的個數不會超過這條路徑與該矩形區域邊界的公共部分的長度,又由小學奧數,這塊區域與邊界公共部分的長度不會超過這塊區域在矩形內部的輪廓的長度,因此減少的割邊條數終究還是 \(\ge\)

增加的割邊條數,割邊條數終究還是會減少。

Observation \(3\). 如果連線相對邊界上的路徑出現了折線,那麼我們完全可以將它捋直並且答案不會變得更劣

也就是對於下圖中左邊的情形,我們可以將其改成右邊的情形(注:這裡再次借用了官方題解的圖):

證明:我們不妨假設這條折線是由上邊界連向下邊界的,並且起點是上邊界上第 \(i\) 個點與第 \(i+1\) 個點之間的邊,終點是下邊界上第 \(j\) 個點與第 \(j+1\) 個點之間的邊,那麼這條路徑的長度,也就是這條路徑上的割邊數量顯然會 \(\ge n+|i-j|\),而如果我們把它捋直,連線著內部 \(nm\) 個點的割邊恰有 \(n\) 個,而最多還會多出 \(|i-j|\) 條割邊,因此割邊數量不會增加。

不難發現,經過這三次調整之後,我們的割邊要麼出現在邊界上的點與內部點之間,要麼是一列上完整的 \(n\) 條割邊,或者一行上完整的 \(m\) 條割邊,因此考慮以此為狀態設計 \(dp\)。行和列顯然是對稱的,因此考慮一種情況即可映象地求出另一種情況,這裡以行為例。設 \(dp_{i,j}\) 表示當前考慮了前 \(i\) 行,第 \(i\) 行上中間 \(m\) 個點的顏色是 \(j\) 最少割邊條數,其中 \(0\) 表示紅色,\(1\) 表示藍色,那麼顯然有轉移:

  • \(dp_{i,0}=\min(dp_{i-1,0},dp_{i-1,1}+m)+[L_i=0]+[R_i=0]\)
  • \(dp_{i,1}=\min(dp_{i-1,0}+m,dp_{i-1,1})+[L_i=1]+[R_i=1]\)

然後對行和列的最優答案取個 \(\min\) 即可,時間複雜度線性,可以通過 H1

const int MAXN=1e5;
int n,m;char L[MAXN+5],R[MAXN+5],U[MAXN+5],D[MAXN+5];
int dp[MAXN+5][2],ans=0x3f3f3f3f;
int main(){
	scanf("%d%d%*d%s%s%s%s",&n,&m,L+1,R+1,U+1,D+1);
	memset(dp,63,sizeof(dp));dp[0][0]=dp[0][1]=0;
	for(int i=1;i<=m;i++) dp[0][0]+=(U[i]=='B'),dp[0][1]+=(U[i]=='R');
	for(int i=1;i<=n;i++){
		dp[i][0]=min(dp[i-1][0],dp[i-1][1]+m)+(L[i]=='B')+(R[i]=='B');
		dp[i][1]=min(dp[i-1][1],dp[i-1][0]+m)+(L[i]=='R')+(R[i]=='R');
	}
	for(int i=1;i<=m;i++) dp[n][0]+=(D[i]=='B'),dp[n][1]+=(D[i]=='R');
	chkmin(ans,dp[n][0]);chkmin(ans,dp[n][1]);
	memset(dp,63,sizeof(dp));dp[0][0]=dp[0][1]=0;
	for(int i=1;i<=n;i++) dp[0][0]+=(L[i]=='B'),dp[0][1]+=(L[i]=='R');
	for(int i=1;i<=m;i++){
		dp[i][0]=min(dp[i-1][0],dp[i-1][1]+n)+(U[i]=='B')+(D[i]=='B');
		dp[i][1]=min(dp[i-1][1],dp[i-1][0]+n)+(U[i]=='R')+(D[i]=='R');
	}
	for(int i=1;i<=n;i++) dp[m][0]+=(R[i]=='B'),dp[m][1]+=(R[i]=='R');
	chkmin(ans,dp[m][0]);chkmin(ans,dp[m][1]);
	printf("%d\n",ans);
	return 0;
}

關於 H2,由於帶上修改,並且 \(dp\) 狀態的第二維較小,因此可以想到線段樹維護矩陣乘法,具體來說我們對行和列分別建一棵線段樹,線段樹上每個節點 \([L,R]\) 維護四個 \(2\times 2\) 的矩陣,分別表示 \(L_i,R_i\) 顏色均不改變/\(L_i\) 顏色改變 \(R_i\) 不變/\(L_i\) 顏色不變 \(R_i\) 改變/\(L_i,R_i\) 顏色均改變情況下的轉移矩陣,這樣每次區間翻轉只用交換兩對矩陣即可,時間複雜度 \(n\log n\),常數略有點大,但還是莽過去了(

const int MAXN=1e5;
int n,m,qu;
struct mat{
	int a[2][2];
	mat(){memset(a,63,sizeof(a));}
	mat operator *(const mat &rhs){
		mat res;
		for(int i=0;i<2;i++) for(int j=0;j<2;j++) for(int k=0;k<2;k++)
			chkmin(res.a[i][j],a[i][k]+rhs.a[k][j]);
		return res;
	}
};
struct solver{
	int n,m;char s1[MAXN+5],s2[MAXN+5];
	mat get(int x,int y){
		mat res;
		res.a[0][0]=x+y;res.a[0][1]=m+2-x-y;
		res.a[1][0]=m+x+y;res.a[1][1]=2-x-y;
		return res;
	}
	struct node{int l,r,s1,s2,tg1,tg2;mat v[2][2];} s[MAXN*4+5];
	void pushup(int k){
		for(int a=0;a<2;a++) for(int b=0;b<2;b++)
			s[k].v[a][b]=s[k<<1].v[a][b]*s[k<<1|1].v[a][b];
		s[k].s1=s[k<<1].s1+s[k<<1|1].s1;
		s[k].s2=s[k<<1].s2+s[k<<1|1].s2;
	}
	void build(int k,int l,int r){
		s[k].l=l;s[k].r=r;
		if(l==r){
			for(int a=0;a<2;a++) for(int b=0;b<2;b++)
				s[k].v[a][b]=get((s1[l]=='B')^a,(s2[l]=='B')^b);
			s[k].s1=(s1[l]=='B');s[k].s2=(s2[l]=='B');
			return;
		} int mid=l+r>>1;build(k<<1,l,mid);build(k<<1|1,mid+1,r);
		pushup(k);
	}
	void tag1(int k){
		swap(s[k].v[0][0],s[k].v[1][0]);
		swap(s[k].v[0][1],s[k].v[1][1]);
		s[k].s1=s[k].r-s[k].l+1-s[k].s1;
		s[k].tg1^=1;
	}
	void tag2(int k){
		swap(s[k].v[0][0],s[k].v[0][1]);
		swap(s[k].v[1][0],s[k].v[1][1]);
		s[k].s2=s[k].r-s[k].l+1-s[k].s2;
		s[k].tg2^=1;
	}
	void pushdown(int k){
		if(s[k].tg1){tag1(k<<1);tag1(k<<1|1);s[k].tg1=0;}
		if(s[k].tg2){tag2(k<<1);tag2(k<<1|1);s[k].tg2=0;}
	}
	void modify(int k,int l,int r,int t){
		if(l<=s[k].l&&s[k].r<=r) return ((t==1)?tag1(k):tag2(k)),void();
		int mid=(pushdown(k),s[k].l+s[k].r>>1);
		if(r<=mid) modify(k<<1,l,r,t);
		else if(l>mid) modify(k<<1|1,l,r,t);
		else modify(k<<1,l,mid,t),modify(k<<1|1,mid+1,r,t);
		pushup(k);
	}
	void init(){build(1,1,n);}
	mat query(){return s[1].v[0][0];}
	int ask1(){return s[1].s1;}
	int ask2(){return s[1].s2;}
} H,V;
int calc(){
	mat m1=H.query(),m2=V.query();
	return min(min(
	min(m1.a[0][0]+V.ask1()+V.ask2(),m1.a[0][1]+V.ask1()+m-V.ask2()),
	min(m1.a[1][0]+m-V.ask1()+V.ask2(),m1.a[1][1]+m-V.ask1()+m-V.ask2())),min(
	min(m2.a[0][0]+H.ask1()+H.ask2(),m2.a[0][1]+H.ask1()+n-H.ask2()),
	min(m2.a[1][0]+n-H.ask1()+H.ask2(),m2.a[1][1]+n-H.ask1()+n-H.ask2())));
}
int main(){
	scanf("%d%d%d",&n,&m,&qu);V.m=H.n=n;V.n=H.m=m;
	scanf("%s%s%s%s",H.s1+1,H.s2+1,V.s1+1,V.s2+1);
	H.init();V.init();printf("%d\n",calc());
	while(qu--){
		char c;int l,r;cin>>c>>l>>r;
		switch(c){
			case 'L':{H.modify(1,l,r,1);break;}
			case 'R':{H.modify(1,l,r,2);break;}
			case 'U':{V.modify(1,l,r,1);break;}
			case 'D':{V.modify(1,l,r,2);break;}
		} printf("%d\n",calc());
	}
	return 0;
}