[SDOI2019] 世界地图 题解
其实这题在 2024 年就应该写了,一直拖到现在,要不是因为模拟赛因为没做过这个题导致 T1 坠机了我可能都忘记补了。
题解
每次询问就是让你求一个点集的最小生成树。
相当于求把一个前缀 \([1,l-1]\) 的最小生成树和后缀 \([r+1,m]\) 的最小生成树,通过 \((i,m)-(i,1)\) 的边连接起来合并之后的新的最小生成树。
而一个前缀(后缀同理)\([1,j]\) 的最小生成树也可以认为是前缀 \([1,j-1]\) 的最小生成树和第 \(j\) 列的最小生成树用 \((i,j-1)-(i,j)\) 的边合并之后的最小生成树。
所以我们只需要知道怎么合并两棵最小生成树就可以了。
首先你得知道最小生成树有一个用 LCT 维护的求法,就是每次加进来一条边 \((u,v,w)\) 时如果 \(u,v\) 不连通直接选,否则看一下目前 \(u,v\) 路径上的边权最大的边 \(l\) 和 \(w\) 哪个大,保留更小的那个。(知道思想即可,毕竟最后还是用 Kruskal 求的)。
以求前缀的最小生成树为例,因为合并前缀 \([1,j-1]\) 和第 \(j\) 列时,新加的那 \(n\) 条边只会影响第 \(j-1\) 和 \(j\) 列的点,我们称这两列的点为关键点。那么有且仅有两个关键点之间的路径上的最大边可能会被替换,其他最小生成树上的边在之后都不会变化了,所以我们可以对最小生成树上的那些关键点建立虚树,边权就设为最小生成树上两点之间的路径上的最大边权。那么原图的最小生成树的边权和就可以看成是虚树上的边权和 \(+\) 不在虚树上的边权和。这里主要讲前者怎么维护,后者比较简单直接根据代码理解即可。
合并时把两棵最小生成树对应的两棵虚树上的点和边拿出来,加上新加的那 \(n\) 条边一起跑一个 Kruskal,得到一个新的最小生成树 \(T\)。然后我们找出新图的那些关键点,并在 \(T\) 上对他们建立虚树就得到了新图的虚树,边权仍然是路径上的最大边。
Tip:因为 \(T\) 的点数很少,和关键点是同一个数量级的,所以直接跑两遍 dfs 建虚树即可,没有必要二次排序,后者常数反而更大。
需要注意的是关键点既可能是最左侧一列的点,也可能是最右侧一列的点,但是我们没有必要对最左侧和最右侧的点分别维护虚树,因为众所周知不管往虚树里多加多少点都不影响正确性,所以直接把最左侧和最右侧的点全设为关键点然后一起维护虚树即可。
容易发现任何时刻虚树的规模都是 \(O(n)\) 级别的(最大差不多是 \(8n\)),所以暴力跑 Kruskal 求最小生成树的复杂度是 \(O(n\log n)\),总复杂度是 \(O(n(m+q)\log n)\)。
code
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=1e2+5,M=1e4+5,N2=N*8;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m,T,a[M][N],b[M][N];
unsigned int SA,SB,SC;int lim;
int getweight() {
SA ^= SA << 16;
SA ^= SA >> 5;
SA ^= SA << 1;
unsigned int t = SA;
SA = SB;
SB = SC;
SC^= t ^ SA;
return SC % lim + 1;
}
struct Edge{ int u,v,w; };
struct MST{
int num; //点数,维护的时候需要保证编号最小的 n 个点是最左侧一列的点,编号最大的 n 个点是最右侧一列的点
LL sum; //存储之后不可能再被修改的边的边权和
vector<Edge> E;
void Init(int w[]){
num=n,sum=0;
for(int i=1;i<n;i++) E.push_back({i,i+1,w[i]});
}
LL ask(){
LL ans=sum;
for(Edge e:E) ans+=e.w;
return ans;
}
}pre[M],suf[M];
int fa[N2],id[N2];
LL ans;
int get(int x){return (x==fa[x])?x:(fa[x]=get(fa[x]));}
int tot,head[N2],to[N2<<1],Next[N2<<1],val[N2<<1];
void add(int u,int v,int w){
to[++tot]=v,Next[tot]=head[u],head[u]=tot,val[tot]=w;
}
bool dfs1(int u,int fa){ //dfs 建虚树
int c=0;
for(int i=head[u];i;i=Next[i]){
int v=to[i];
if(v==fa) continue;
c+=dfs1(v,u);
}
if(c>=2) id[u]=1;
return id[u]||c;
}
vector<Edge> newE;
void dfs2(int u,int fa,int lst,int maxn){
if(id[u]){
if(lst){
newE.push_back({lst,id[u],maxn});
ans-=maxn;
/*
只有一条链上的最大边之后才有可能被删掉,剩余的最小生成树上的边之后都不会被删,
ans 维护的就是那些最小生成树上不会被删的边
*/
}
lst=id[u],maxn=0;
}
for(int i=head[u];i;i=Next[i]){
int v=to[i];
if(v==fa) continue;
dfs2(v,u,lst,max(maxn,val[i]));
}
}
MST Merge(MST T1,MST T2,int w[]){
int num=T1.num+T2.num;
vector<Edge> E=T1.E;
for(Edge e:T2.E) E.push_back({e.u+T1.num,e.v+T1.num,e.w});
for(int i=1;i<=n;i++) E.push_back({T1.num-n+i,T1.num+i,w[i]});
sort(E.begin(),E.end(),[&](Edge e1,Edge e2){return e1.w<e2.w;});
ans=0,tot=0;
for(int i=1;i<=num;i++) fa[i]=i,head[i]=0,id[i]=((i<=n)||(i>=num-n+1)); //前 n 个和后 n 个点是关键点
for(Edge e:E){
int u=e.u,v=e.v,w=e.w;
if(get(u)==get(v)) continue;
ans+=w,fa[get(u)]=get(v);
add(u,v,w),add(v,u,w);
}
dfs1(1,0);
int cnt=0;
for(int i=1;i<=num;i++) if(id[i]) id[i]=++cnt; //给所有虚树上的点重标号
newE.clear();
dfs2(1,0,0,0);
MST T3;
T3.num=cnt,T3.sum=T1.sum+T2.sum+ans,T3.E=newE;
return T3;
}
void Init(){
for(int i=1;i<=m;i++) pre[i].Init(b[i]),suf[i].Init(b[i]);
for(int i=2;i<=m;i++) pre[i]=Merge(pre[i-1],pre[i],a[i-1]);
for(int i=m-1;i>=1;i--) suf[i]=Merge(suf[i],suf[i+1],a[i]);
}
signed main(){
scanf("%d%d%u%u%u%d",&n,&m,&SA,&SB,&SC,&lim);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
a[j][i]=getweight();
}
}
for(int i=1;i<n;i++){
for(int j=1;j<=m;j++){
b[j][i]=getweight();
}
}
Init();
scanf("%d",&T);
while(T--){
int l,r; scanf("%d%d",&l,&r);
printf("%lld\n",Merge(suf[r+1],pre[l-1],a[m]).ask());
}
return 0;
}

浙公网安备 33010602011771号