Codeforces 1368H - Breadboard Capacity(最小割+线段树维护矩阵乘法)

时间:2022-12-03 22:42:27

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\). 如果出现一条割掉的边形成的路径连接了同一个或相邻的边界,那么我们可以改变点的颜色使这样的路径消失并且答案不会变得更劣。

也就是对于下图中左边的情形,我们可以将其改成右边的情形(注:这里借用了官方题解的图):

Codeforces 1368H - Breadboard Capacity(最小割+线段树维护矩阵乘法)

证明:如果我们将这个割掉的边形成的路径包围的部分中,属于内部的 \(nm\) 个点的部分(也就是上图中改变颜色的五个点)的颜色改为与路径另一侧的点一直,那么显然这条路径上的割边都会消失,此同时有可能会增加一些割边,这些增加的割边都来自于边界上的点与该路径包围的区域中的点连成的边(也就是上图中增加的三条割边),但是很显然的一件事是,增加的割边的个数不会超过这条路径与该矩形区域边界的公共部分的长度,又由小学奥数,这块区域与边界公共部分的长度不会超过这块区域在矩形内部的轮廓的长度,因此减少的割边条数终究还是 \(\ge\) 增加的割边条数,割边条数终究还是会减少。

Observation \(3\). 如果连接相对边界上的路径出现了折线,那么我们完全可以将它捋直并且答案不会变得更劣

也就是对于下图中左边的情形,我们可以将其改成右边的情形(注:这里再次借用了官方题解的图):

Codeforces 1368H - Breadboard Capacity(最小割+线段树维护矩阵乘法)

证明:我们不妨假设这条折线是由上边界连向下边界的,并且起点是上边界上第 \(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;
}