ZJOI2015 幻想乡战略游戏 和 SCOI2019 找重心
幻想乡战略游戏
傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网游厂商把游戏的地图越做越大,以至于幽香一眼根本看不过来,更别说和别人打仗了。 在打仗之前,幽香现在面临一个非常基本的管理问题需要解决。 整个地图是一个树结构,一共有 \(n\) 块空地,这些空地被 \(n-1\) 条带权边连接起来,使得每两个点之间有一条唯一的路径将它们连接起来。
在游戏中,幽香可能在空地上增加或者减少一些军队。同时,幽香可以在一个空地上放置一个补给站。 如果补给站在点 \(u\) 上,并且空地 \(v\) 上有 \(d_v\) 个单位的军队,那么幽香每天就要花费 \(d_v \cdot \text{dist}(u,v)\) 的金钱来补给这些军队。由于幽香需要补给所有的军队,因此幽香总共就要花费为 \(\sum (d_v \cdot \text{dist}(u,v))\),其中\(1 \leq v \leq N\))的代价,\(\text{dist}(u,v)\) 表示 \(u\) 个 \(v\) 在树上的距离(唯一路径的权和)。 因为游戏的规定,幽香只能选择一个空地作为补给站。在游戏的过程中,幽香可能会在某些空地上制造一些军队,也可能会减少某些空地上的军队,进行了这样的操作以后,出于经济上的考虑,幽香往往可以移动他的补给站从而省一些钱。但是由于这个游戏的地图是在太大了,幽香无法轻易的进行最优的安排,你能帮帮她吗? 你可以假定一开始所有空地上都没有军队。
对于所有数据,\(1 \leq c \leq 1000, \ 0 \leq |e| \leq 1000, \ n \leq 10^5, \ Q \leq 10^5\),并保证所有节点的度数小于等于 \(20\).
分析
考虑答案的形式,发现跟带权重心非常类似。
假设当前补给站为\(u\),并强制以\(u\)为根,\(v\)为\(u\)的一个子节点,\(\text{sumd}_u\)和\(\text{sumd}_v\)分别为\(u\)的子树内的\(d\)之和以及\(v\)的子树内的\(d\)之和,\(\text{len}(u,v)\)为边\((u,v)\)的长度。
如果将补给站迁移到点\(v\),那么\(v\)的子树内的点到补给站的距离减少了\(\text{len}(u,v)\),其他的点到补给站的距离增加了\(\text{len}(u,v)\)。也就是说,补给站迁移到点\(v\)时,代价的增量为:
整理一下,得出性质:\(u\)为根,\(v\)为\(u\)的子节点,补给站在\(v\)比\(u\)优,当且仅当:
显然满足条件的\(v\)最多只有一个。这时候,如果没有满足条件的\(v\),则\(u\)为最优位置。否则最优位置在\(v\)的子树内。
一个一个跳肯定不行,所以考虑使用点分树加速跳跃过程。具体而言,如果发现\(u\rightarrow v\)更优的话,那么就递归计算\(v\)子树中的重心,即\(u\)在点分树上的儿子节点。运用换根DP的知识我们可以维护点分树上面每个节点子树的答案并且做到\(O(\log n)\)查询每个节点换根后的答案。
时间复杂度\(O(n\log n+Q20\log^2 n)\)
动态点分治的作用:在点分治的过程中,一般我们面对的问题都是静态的。如果涉及到修改这类的操作,我们就希望找到我们是如何处理到当前的修改点的,换而言之,我们希望记录下点分治的过程,这样可以通过爬点分树等操作消除影响。
co int N=2e5+1;
int n,m;
namespace T{ // original tree
vector<pii> e[N];
int lg[N*2],st[N*2][18],dis[N],pos[N],dfn;
void dfs(int u,int fa){
st[pos[u]=++dfn][0]=dis[u];
for(int i=0,v;i<e[u].size();++i){
if((v=e[u][i].first)==fa) continue;
dis[v]=dis[u]+e[u][i].second,dfs(v,u);
st[++dfn][0]=dis[u];
}
}
void init(){
lg[0]=-1;
for(int i=1;i<=n<<1;++i) lg[i]=lg[i>>1]+1;
dfs(1,0),assert(dfn==2*n-1);
for(int j=1;1<<j<=dfn;++j)
for(int i=1;i+(1<<j)-1<=dfn;++i)
st[i][j]=min(st[i][j-1],st[i+(1<<j-1)][j-1]);
}
int get_dis(int u,int v){
if(pos[u]>pos[v]) swap(u,v);
int k=lg[pos[v]-pos[u]+1];
return dis[u]+dis[v]-2*min(st[pos[u]][k],st[pos[v]-(1<<k)+1][k]);
}
}
int vis[N],sum,root,siz[N],f[N],par[N];
ll down[N],up[N],\text{sumd}[N];
vector<pii> g[N];
void get_root(int u,int fa){
siz[u]=1,f[u]=0;
for(int i=0,v;i<T::e[u].size();++i){
if(vis[v=T::e[u][i].first]||v==fa) continue;
get_root(v,u);
siz[u]+=siz[v],f[u]=max(f[u],siz[v]);
}
f[u]=max(f[u],sum-siz[u]);
if(f[u]<f[root]) root=u;
}
void work(int u,int fa){
vis[u]=1,par[u]=fa;
for(int i=0,v;i<T::e[u].size();++i){
if(vis[v=T::e[u][i].first]) continue;
sum=siz[v],root=0,get_root(v,0);
g[u].push_back(pii(root,v));
work(root,u);
}
}
void ins(int u,int val){
\text{sumd}[u]+=val;
for(int i=u;par[i];i=par[i]){
int dist=T::get_dis(par[i],u);
down[par[i]]+=(ll)dist*val; // underneath ans
up[i]+=(ll)dist*val; // upward transfer
\text{sumd}[par[i]]+=val;
}
}
ll calc(int u){
ll ans=down[u];
for(int i=u;par[i];i=par[i]){
int dist=T::get_dis(par[i],u);
ans+=down[par[i]]-up[i]+dist*(\text{sumd}[par[i]]-\text{sumd}[i]);
}
return ans;
}
ll query(int u){
ll ans=calc(u);
for(int i=0;i<g[u].size();++i)
if(calc(g[u][i].second)<ans)
return query(g[u][i].first);
return ans;
}
int main(){
read(n),read(m);
for(int i=1,u,v,w;i<n;++i){
read(u),read(v),read(w);
T::e[u].push_back(pii(v,w)),T::e[v].push_back(pii(u,w));
}
T::init();
sum=n,f[0]=n,get_root(1,0);
int tmp=root;work(root,0),root=tmp;
for(int u,e;m--;){
read(u),read(e),ins(u,e);
printf("%lld\n",query(root));
}
return 0;
}
将树二度化之后就不需要点度数\(\leq 20\)的性质了。
时间复杂度\(O(n\log^2 n)\)。
这样做的本质其实就是二分。
CO int N=4e5+10;
struct edge {int y,w;};
vector<edge> to[N];
namespace Rebuild{
vector<edge> to[N];
void dfs(int x,int fa){
for(int i=0;i<(int)to[x].size();++i){
int y=to[x][i].y;
if(y==fa){
to[x].erase(to[x].begin()+i),--i;
continue;
}
dfs(y,x);
}
}
int main(int n){
for(int i=1;i<n;++i){
int x=read<int>(),y=read<int>(),w=read<int>();
to[x].push_back({y,w}),to[y].push_back({x,w});
}
dfs(1,0);
for(int x=1;x<=n;++x){
if(to[x].size()<=2){
for(CO edge&e:to[x])
::to[x].push_back({e.y,e.w}),::to[e.y].push_back({x,e.w});
continue;
}
int l=++n,r=++n;
::to[x].push_back({l,0}),::to[l].push_back({x,0});
::to[x].push_back({r,0}),::to[r].push_back({x,0});
to[l].assign(to[x].begin(),to[x].begin()+to[x].size()/2);
to[r].assign(to[x].begin()+to[x].size()/2,to[x].end());
}
return n;
}
}
namespace Dist{
int dep[N],pos[N],tim;
int st[2*N][20],lg[2*N];
void dfs(int x,int fa){
pos[x]=++tim,st[tim][0]=dep[x];
for(CO edge&e:to[x])if(e.y!=fa){
dep[e.y]=dep[x]+e.w;
dfs(e.y,x);
st[++tim][0]=dep[x];
}
}
void main(){
dfs(1,0);
lg[0]=-1;
for(int i=1;i<=tim;++i) lg[i]=lg[i>>1]+1;
for(int k=1;k<=lg[tim];++k)for(int i=1;i+(1<<k)-1<=tim;++i)
st[i][k]=min(st[i][k-1],st[i+(1<<(k-1))][k-1]);
}
IN int calc(int x,int y){
if(pos[x]>pos[y]) swap(x,y);
int k=lg[pos[y]-pos[x]+1];
return dep[x]+dep[y]-2*min(st[pos[x]][k],st[pos[y]-(1<<k)+1][k]);
}
}
namespace Seg{
int pos[N],tim,lst[N];
void dfs(int x,int fa){
pos[x]=++tim;
for(CO edge&e:to[x])if(e.y!=fa) dfs(e.y,x);
lst[x]=tim;
}
void main(){
dfs(1,0);
}
int sum[4*N];
#define lc (x<<1)
#define rc (x<<1|1)
#define mid ((l+r)>>1)
void insert(int x,int l,int r,int p,int v){
sum[x]+=v;
if(l==r) return;
if(p<=mid) insert(lc,l,mid,p,v);
else insert(rc,mid+1,r,p,v);
}
int query(int x,int l,int r,int ql,int qr){
if(ql<=l and r<=qr) return sum[x];
if(qr<=mid) return query(lc,l,mid,ql,qr);
if(ql>mid) return query(rc,mid+1,r,ql,qr);
return query(lc,l,mid,ql,qr)+query(rc,mid+1,r,ql,qr);
}
#undef lc
#undef rc
#undef mid
IN void insert(int x,int v){
insert(1,1,tim,pos[x],v);
}
IN int query(int x,int y){
if(pos[x]>pos[y]) return query(1,1,tim,pos[x],lst[x]);
else return sum[1]-query(1,1,tim,pos[y],lst[y]);
}
}
int vis[N],siz[N],all;
pair<int,int> root;
int fa[N];
vector<edge> ch[N];
void find_root(int x,int fa){
siz[x]=1;
pair<int,int> ans={0,x};
for(CO edge&e:to[x])if(!vis[e.y] and e.y!=fa){
find_root(e.y,x);
siz[x]+=siz[e.y];
ans.first=max(ans.first,siz[e.y]);
}
ans.first=max(ans.first,all-siz[x]);
root=min(root,ans);
}
void build(int x){
vis[x]=1;
int old=all;
for(CO edge&e:to[x])if(!vis[e.y]){
root={all=siz[e.y]<siz[x]?siz[e.y]:old-siz[x],0},find_root(e.y,x);
fa[root.second]=x,ch[x].push_back({root.second,e.y});
build(root.second);
}
}
int cen,sum[N];
int64 down[N],up[N];
void insert(int x,int v){
sum[x]+=v;
for(int i=x;fa[i];i=fa[i]){
sum[fa[i]]+=v;
int \text{len}=Dist::calc(x,fa[i]);
down[fa[i]]+=(int64)v*\text{len};
up[i]+=(int64)v*\text{len};
}
}
int64 calc(int x){
int64 ans=down[x];
for(int i=x;fa[i];i=fa[i]){
int \text{len}=Dist::calc(x,fa[i]);
ans+=down[fa[i]]-up[i]+(int64)(sum[fa[i]]-sum[i])*\text{len};
}
return ans;
}
int64 query(int x){
for(CO edge&e:ch[x])
if(2*Seg::query(e.w,x)>sum[cen]) return query(e.y);
return calc(x);
}
int main(){
int n=read<int>(),m=read<int>();
n=Rebuild::main(n);
Dist::main();
Seg::main();
root={all=n,0},find_root(1,0);
cen=root.second;
build(root.second);
while(m--){
int x=read<int>(),v=read<int>();
Seg::insert(x,v);
insert(x,v);
printf("%lld\n",query(cen));
}
return 0;
}
找重心
JKLover种了一棵\(n\)个点的有边权的树。他每次会拍拍脑袋想出一个区间\([l,r]\)(长度为奇数),然后心算出编号在这个区间内的点的带权重心。为了检验他算的对不对,他请你写程序来验算。
输入格式
第一行一个整数\(n\)表示树的点数。
接下来\(n-1\)行,每行三个整数\(x,y,w\)表示一条边。
下一行一个整数\(m\)表示询问的次数。
接下来\(m\)行,每行两个整数\(l,r\)表示一次询问的区间。
输出格式
输出\(m\)行,每行一个整数表示答案。
样例
5
5 1 3
1 2 5
2 4 5
4 3 1
5
2 4
3 3
1 5
1 3
2 4
4
3
2
2
4
数据范围与提示
对于15%的数据,满足\(n\leq 10^3,m\leq 10^3\)。
对于60%的数据,满足\(m\leq 10^5\)。
对于100%的数据,满足\(n\leq 5\times 10^4,m\leq 5\times 10^5,w\leq 10^3\)。
请使用较快的输入输出方式。
时间限制:1S
空间限制:512MB
题解
注意这道题只需要求出重心,而不需要求出重心到\([l,r]\)的距离和,所以要简单很多。
那么用可持久化分块维护DFS序的前缀和,就能做到\(O(n\sqrt{n})+O(m\log n)\)。
可持久化分块可以把信息存到边上,比较好实现一些。
区间长度为奇数保证了答案唯一。
CO int N=1e5+10;
struct edge {int y,w;};
vector<edge> to[N];
int from[N];
namespace Rebuild{
vector<edge> to[N];
void dfs(int x,int fa){
for(int i=0;i<(int)to[x].size();++i){
int y=to[x][i].y;
if(y==fa){
to[x].erase(to[x].begin()+i),--i;
continue;
}
dfs(y,x);
}
}
int main(int n){
for(int i=1;i<n;++i){
int x=read<int>(),y=read<int>(),w=read<int>();
to[x].push_back({y,w}),to[y].push_back({x,w});
}
dfs(1,0);
for(int x=1;x<=n;++x) from[x]=x;
for(int x=1;x<=n;++x){
if(to[x].size()<=2){
for(CO edge&e:to[x])
::to[x].push_back({e.y,e.w}),::to[e.y].push_back({x,e.w});
continue;
}
int l=++n,r=++n;
from[l]=from[r]=from[x];
::to[x].push_back({l,0}),::to[l].push_back({x,0});
::to[x].push_back({r,0}),::to[r].push_back({x,0});
to[l].assign(to[x].begin(),to[x].begin()+to[x].size()/2);
to[r].assign(to[x].begin()+to[x].size()/2,to[x].end());
}
return n;
}
}
namespace Block{
int pos[N],tim,idx[N],lst[N];
void dfs(int x,int fa){
pos[x]=++tim,idx[tim]=x;
for(CO edge&e:to[x])if(e.y!=fa) dfs(e.y,x);
lst[x]=tim;
}
CO int B=316;
int root[N],tot;
int ch[N][B+10],sum[N][B+10];
void main(int n){
dfs(1,0);
for(int i=1;i<=n;++i){
int p=pos[i];
int&x=root[i]=root[i-1];
++tot,copy(ch[x]+1,ch[x]+(tim+B-1)/B+1,ch[tot]+1);
copy(sum[x]+1,sum[x]+(tim+B-1)/B+1,sum[tot]+1),x=tot;
for(int i=(p+B-1)/B+1;i<=(tim+B-1)/B;++i) ++sum[x][i];
int&y=ch[x][(p+B-1)/B];
++tot,copy(ch[y]+1,ch[y]+B+1,ch[tot]+1);
copy(sum[y]+1,sum[y]+B+1,sum[tot]+1),y=tot;
for(int i=p-(p-1)/B*B;i<=B;++i) ++sum[y][i];
}
}
IN int query(int i,int p){
int x=root[i];
int ans=sum[x][(p+B-1)/B];
x=ch[x][(p+B-1)/B];
ans+=sum[x][p-(p-1)/B*B];
return ans;
}
IN int query(int l,int r,int x,int y){
if(pos[x]>pos[y]) return query(r,lst[x])-query(r,pos[x]-1)-(query(l-1,lst[x])-query(l-1,pos[x]-1));
else return r-l+1-(query(r,lst[y])-query(r,pos[y]-1)-(query(l-1,lst[y])-query(l-1,pos[y]-1)));
}
}
int vis[N],siz[N],all;
pair<int,int> root;
vector<edge> ch[N];
void find_root(int x,int fa){
siz[x]=1;
pair<int,int> ans={0,x};
for(CO edge&e:to[x])if(!vis[e.y] and e.y!=fa){
find_root(e.y,x);
siz[x]+=siz[e.y];
ans.first=max(ans.first,siz[e.y]);
}
ans.first=max(ans.first,all-siz[x]);
root=min(root,ans);
}
void build(int x){
vis[x]=1;
int old=all;
for(CO edge&e:to[x])if(!vis[e.y]){
root={all=siz[e.y]<siz[x]?siz[e.y]:old-siz[x],0},find_root(e.y,x);
ch[x].push_back({root.second,e.y});
build(root.second);
}
}
int query(int x,int l,int r){
for(CO edge&e:ch[x])
if(2*Block::query(l,r,e.w,x)>r-l+1) return query(e.y,l,r);
return x;
}
int main(){
freopen("centroid.in","r",stdin),freopen("centroid.out","w",stdout);
int n=read<int>();
all=Rebuild::main(n);
Block::main(n);
root={all,0},find_root(1,0);
int cen=root.second;
build(root.second);
for(int m=read<int>();m--;){
int l=read<int>(),r=read<int>();
writeln(from[query(cen,l,r)]);
}
return 0;
}