城市规划 解题报告
分析题意
首先我们需要理解 “联通” 这个概念。有两种情况被称为“联通”:
- 对于同一列的相邻的两个格子,它们都是 “.”,“|”或“O”三者之一
- 对于同一行的相邻的两个格子,它们都是 “.”,“-”或“O”三者之一
然后我们看到本题需要维护联通块,因此可以考虑使用并查集维护联通块。
其次,让我们关注询问区间 \((l,1)(r,M)\),这等价于询问第 \(l\) 行到第 \(r\) 行,因此我们可以考虑到把每一行看做一个整体进行处理。
最后,让我们看到数据范围:\(N \leq 1e5\) 且 \(M \leq 6\)。是熟悉的 \(1e5\),再结合区间查询,让我们想到线段树。
算法分析
在思考怎么使用线段树时,我们会遇到一个问题:怎么合并子节点信息?
结合我们之前学过的“用线段树求最大子段和”,我们可以想到我们只需要处理两个儿子相邻的两行就可以了。
对于这两行(第 \(i\) 行和第 \(i+1\) 行),如果 \((i,j)\) 和 \((i+1,j)\) 是联通的,那么我们就要用并查集合并这两个联通块,同时维护权值(如果被合并的两个联通块都有建筑,那么合并后总权值应该减一,因为这两个联通块在各自节点都被统计过)。
这里有一个问题:在合并后,由部分并查集的根节点并不在最上和最小这两行。我们可以将并查集的根节点转移到这两行上的点来解决问题(详见代码)。
其余的查询和修改与正常的线段树一样,不再赘述。
“talk is cheap,show me the code”
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ls (i<<1)
#define rs (i<<1|1)
#define inf 0x3f3f3f3f
using namespace std;
typedef long long ll;
inline int read(){
register int x=0;
char c=getchar();
while(c<'0' || '9'<c) c=getchar();
while('0'<=c && c<='9') x=(x<<1)+(x<<3)+c-'0',c=getchar();
return x;
}
const int N=1e5+1000,M=8;
int n,m,q,p[N][M];
//连通性判断
bool able_x(int val){return val==0 || val==1 || val==3;}
bool able_y(int val){return val==0 || val==1 || val==2;}
struct Data{
int l,r,f[M<<2],cnt;
bool have[M<<2];
int find(int x){return f[x]=(f[x]==x)?x:find(f[x]);}
bool Union(int x,int y){//合并联通块,返回值为真当且仅当 合并成功且合并的两个联通块都有建筑
int fx=find(x),fy=find(y);
if(fx==fy) return false;
bool res=(have[fx] && have[fy]);
f[fx]=fy;
have[fy]|=have[fx];
return res;
}
Data(int x=0){
l=r=x,cnt=0;
for(int i=1;i<=2*m;i++) f[i]=i,have[i]=0;
for(int i=1;i<=m;i++)
have[i]=(!p[x][i]),Union(i+m,i);
for(int i=1;i<m;i++)
if(able_x(p[x][i]) && able_x(p[x][i+1]))
Union(i,i+1);
for(int i=1;i<=2*m;i++)
if(f[i]==i && have[i])
cnt++;
}
};
int rk[M<<2],nwf[M<<2],nwhave[M<<2];
Data Merge(Data a,Data b){
Data ans;
ans.cnt=a.cnt+b.cnt;
ans.l=a.l,ans.r=b.r;
for(int i=1;i<=m;i++){//继承两个儿子的所有并查集 和 每个并查集是否有建筑
//[1,m] 左儿子左侧 [m+1,2m] 右儿子右侧 [2m+1,3m] 左儿子右侧 [3m+1,4m] 右儿子左侧
ans.f[i] = a.f[i] <=m ? a.f[i] : a.f[i]+m;
ans.f[i+m] = b.f[i+m]<=m ? b.f[i+m]+3*m : b.f[i+m];
ans.f[i+2*m] = a.f[i+m]<=m ? a.f[i+m] : a.f[i+m]+m;
ans.f[i+3*m] = b.f[i] <=m ? b.f[i]+3*m : b.f[i];
ans.have[i] = a.have[i];
ans.have[i+m] = b.have[i+m];
ans.have[i+2*m] = a.have[i+m];
ans.have[i+3*m] = b.have[i];
}
for(int i=1;i<=m;i++)
if(able_y(p[a.r][i]) && able_y(p[b.l][i]))
ans.cnt+= ans.Union(i+2*m,i+3*m) ? -1 : 0;
memset(rk,0,sizeof(rk));
for(int i=1,u;i<=2*m;i++)//找到每一个并查集的根节点,并准备将根节点转移到 在该并查集的 且位于两侧的任意一个节点(rk[u])
u=ans.find(i),rk[u]=i;
for(int i=1;i<=2*m;i++)//这个地方不能直接修改并查集!!! 因为我们仍需要利用旧的并查集去查找原根节点
nwf[i]=rk[ans.find(i)],nwhave[i]=0;
for(int i=1;i<=4*m;i++)//这个地方就可以直接修改,因为之后的操作就和原并查集无关了
if(rk[i])
ans.have[rk[i]]=ans.have[i];
for(int i=1;i<=2*m;i++)
ans.f[i]=nwf[i];
return ans;
}
struct Tree{int l,r;Data val;}t[N<<2];
void push_up(int i){t[i].val=Merge(t[ls].val,t[rs].val);}
void build(int i,int l,int r){
t[i].l=l,t[i].r=r;
if(l==r){
t[i].val=Data(l);
return;
}
int mid=l+r>>1;
build(ls,l,mid);
build(rs,mid+1,r);
push_up(i);
}
Data query(int i,int l,int r){
if(l<=t[i].l && t[i].r<=r)
return t[i].val;
int mid=t[i].l+t[i].r>>1;
if(l>mid) return query(rs,l,r);
if(mid>=r) return query(ls,l,r);
return Merge(query(ls,l,r),query(rs,l,r));
}
void update(int i,int x){
if(t[i].l==t[i].r){
t[i].val=Data(x);
return;
}
int mid=t[i].l+t[i].r>>1;
if(x<=mid) update(ls,x);
else update(rs,x);
push_up(i);
}
char s[10],c;
int main(){
n=read(),m=read();
memset(p,inf,sizeof(p));
for(int i=1;i<=n;i++){
scanf("%s",s+1);
for(int j=1;j<=m;j++){
c=s[j];
if(c=='O') p[i][j]=0;
if(c=='+') p[i][j]=1;
if(c=='|') p[i][j]=2;
if(c=='-') p[i][j]=3;
if(c=='.') p[i][j]=4;
}
}
build(1,1,n);
q=read();
char opt[3];
for(int i=1,l,r;i<=q;i++){
scanf("%s",opt),l=read(),r=read();
if(opt[0]=='Q')
printf("%d\n",query(1,l,r).cnt);
else{
scanf("%s",opt);
if(opt[0]=='O') p[l][r]=0;
if(opt[0]=='+') p[l][r]=1;
if(opt[0]=='|') p[l][r]=2;
if(opt[0]=='-') p[l][r]=3;
if(opt[0]=='.') p[l][r]=4;
update(1,l);
}
}
return 0;
}

浙公网安备 33010602011771号