#9 CF671D & CF671E & CF643D
Roads in Yusland
题目描述
解法
只能说一看就是经典题,然后反应出线段树合并做法和 \(\tt set\) 维护差分标记做法,但是发现还有一种时空复杂度以及实现难度都十分优秀的左偏树做法,所以来记录一下。
定义子树 \(u\) 内的合法方案为,覆盖完子树 \(u\) 内所有边的路径选取方案。发现我们只需要关心合法方案的权值以及向上延伸长度,而延伸长度可以用某条具体的路径来描述。
我们用小根堆来维护子树 \(u\) 内的所有合法方案,考虑如何合并儿子子树 \(v\),对于 \(v\) 中可以延伸到 \(u\) 的方案我们保留,其它的方案扔掉,那么 \(v\) 中的方案还要添加一些权值才能成为 \(u\) 中的合法方案,设 \(f(u)\) 表示覆盖 \(u\) 及其父边的最小代价,那么添加的权值就是 \(\sum_{x\in son(u)} f(x)-f(v)\),\(f(u)\) 可以在回溯的时候轻松获得。
那么我们直接上左偏树复杂度就对了,在左偏树上打标记也不难,时间复杂度 \(O(n\log n)\)
#include <cstdio>
#include <vector>
#include <cstdlib>
#include <iostream>
using namespace std;
const int M = 300005;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans,rt[M],dp[M],dep[M];vector<int> g[M];
int id[M],dis[M],ls[M],rs[M],val[M],fl[M];
void add(int x,int y)
{
if(x) val[x]+=y,fl[x]+=y;
}
void down(int x)
{
if(fl[x]) add(ls[x],fl[x]),add(rs[x],fl[x]),fl[x]=0;
}
int merge(int x,int y)
{
if(!x || !y) return x+y;
if(val[x]>val[y]) swap(x,y);
down(x);rs[x]=merge(rs[x],y);
if(dis[ls[x]]<dis[rs[x]]) swap(ls[x],rs[x]);
dis[x]=dis[rs[x]]+1;
return x;
}
void dfs(int u,int fa)
{
int sum=0;
dep[u]=dep[fa]+1;
for(int v:g[u]) if(v^fa)
{
dfs(v,u);sum+=dp[v];
add(rt[v],-dp[v]);
rt[u]=merge(rt[u],rt[v]);
}
add(rt[u],sum);
while(rt[u] && dep[id[rt[u]]]>=dep[u])
down(rt[u]),rt[u]=merge(ls[rt[u]],rs[rt[u]]);
if(!rt[u]) {puts("-1");exit(0);}
dp[u]=val[rt[u]];
}
signed main()
{
n=read();m=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read();
g[u].push_back(v);
g[v].push_back(u);
}
for(int i=1;i<=m;i++)
{
int u=read(),v=read(),c=read();
dis[i]=1;val[i]=c;id[i]=v;
rt[u]=merge(rt[u],i);
}
dep[1]=1;
for(int v:g[1]) dfs(v,1),ans+=dp[v];
printf("%lld\n",ans);
}
Organizing a Race
题目描述
解法
很容易地把 \([l,r]\) 可以互达的判据写成前缀和判据,设 \(a\) 为 \(w\) 的前缀和,\(b\) 为 \(g\) 的前缀和。构造 \(c_i=a_{i-1}-b_{i-1},d_i=b_i-a_{i-1}\),那么区间 \([l,r]\) 合法的充要条件是:
我们分别解决这两个条件,首先我们把 \(l\) 从后往前扫描,然后维护关于 \(c_i\) 的单调栈。考虑操作 \(g_i\leftarrow g_i+1\) 的影响是 \(c[i+1...n]\) 全体减 \(1\),\(d[i...n]\) 全体加 \(1\)
贪心地看,我们设 \(x\) 满足 \(c_x>c_l\) 的第一个位置,那么我们肯定一直操作 \(x-1\) 直到合法,因为修改放在越后面对于满足 \(d_r\) 是最大值的限制是最有利的,所以可以用单调栈加线段树的组合技维护操作次数,得到数组 \(\{d_i'\}\)(即经过操作之后的 \(d\) 数组)
然后考虑找出最远的右端点,首先可以单调栈二分求出右端点大致的范围 \([i,p]\)(要满足修改次数 \(\leq k\)),然后在这个范围中我们考虑线段树上二分求解右端点。那么如何判断答案在右子树呢?考虑判据应该是:\(\max\{d[mid+1...r]\}+k\geq\max\{d'[1...mid]\}\),因为如果修改作用在了左边同时也会作用在右边,而剩下的修改我们应该是全部作用在右端点上的,所以便有了这个式子。
但是注意 \(\max\{d[mid+1...r]\}\) 应该限定在 \([i,p]\) 的范围内,所以还要套一个线段树询问去找,一开始我们把 \(d'\) 全部设置为负无穷,在扫描的时候逐步激活即可,时间复杂度 \(O(n\log^2 n)\)
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 100005;
#define int long long
const int inf = 1e18;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,ans,a[M],b[M],c[M],d[M],s[M];
int mx[M<<2],tr[M<<2],fl[M<<2];
void add(int i,int c)
{
tr[i]+=c;fl[i]+=c;
}
void down(int i)
{
if(fl[i])
add(i<<1,fl[i]),add(i<<1|1,fl[i]),fl[i]=0;
}
void up(int i)
{
mx[i]=max(mx[i<<1],mx[i<<1|1]);
tr[i]=max(tr[i<<1],tr[i<<1|1]);
}
void build(int i,int l,int r)
{
if(l==r) {mx[i]=d[l];tr[i]=-inf;return ;}
int mid=(l+r)>>1;
build(i<<1,l,mid);
build(i<<1|1,mid+1,r);
up(i);
}
void upd(int i,int l,int r,int L,int R,int c)
{
if(L>r || l>R) return ;
if(L<=l && r<=R) {add(i,c);return ;}
int mid=(l+r)>>1;down(i);
upd(i<<1,l,mid,L,R,c);
upd(i<<1|1,mid+1,r,L,R,c);
up(i);
}
int qry(int i,int l,int r,int L,int R)
{
if(L>r || l>R) return -inf;
if(L<=l && r<=R) return mx[i];
int mid=(l+r)>>1;down(i);
return max(qry(i<<1,l,mid,L,R),
qry(i<<1|1,mid+1,r,L,R));
}
int get(int i,int l,int r,int L,int R,int x)
{
if(l==r) return (mx[i]+k>=x)?l:0;
int mid=(l+r)>>1;down(i);
if(mid<L) return get(i<<1|1,mid+1,r,L,R,x);
if(R<=mid) return get(i<<1,l,mid,L,R,x);
if(qry(i<<1|1,mid+1,r,L,R)+k>=max(x,tr[i<<1]))
return get(i<<1|1,mid+1,r,L,R,max(x,tr[i<<1]));
return get(i<<1,l,mid,L,R,x);
}
signed main()
{
n=read();k=read();
for(int i=1;i<n;i++) a[i]=a[i-1]+read();
for(int i=1;i<=n;i++) b[i]=b[i-1]+read();
for(int i=1;i<=n;i++) c[i]=a[i-1]-b[i-1];
for(int i=1;i<=n;i++) d[i]=b[i]-a[i-1];
build(1,1,n);
for(int i=n;i>=1;i--)
{
while(m && c[s[m]]<=c[i])
{
if(m>1) upd(1,1,n,s[m-1]-1,n,c[s[m]]-c[s[m-1]]);
m--;
}
if(m) upd(1,1,n,s[m]-1,n,c[s[m]]-c[i]);
s[++m]=i;upd(1,1,n,i,i,inf+d[i]);
int l=1,r=m,p=1;
while(l<=r)
{
int mid=(l+r)>>1;
if(c[s[mid]]<=c[i]+k) p=mid,r=mid-1;
else l=mid+1;
}
p=(p==1)?n:s[p-1]-1;
ans=max(ans,get(1,1,n,i,p,-inf)-i+1);
}
printf("%lld\n",ans);
}
Bearish Fanpages
题目描述
解法
只有一个 \(O(n\sqrt n\log n)\) 的想法,我还是太辣鸡了,根本没有观察性质的能力。
考虑本题的关键是每个点有且仅有一个后继,我们称这个后继关系为"父亲"。那么一个点对其父亲的贡献是便于计算的,但是父亲对于儿子的贡献却是难以维护的(因为儿子数目多且离散)
那么我们就不维护父亲对儿子的贡献,只维护儿子对父亲的贡献,在取某个点的值的时候再算上父亲的贡献即可。那么我们轻易地解决了第二问,对于第三问,每个点用 multiset
维护其所有儿子的值(不算父亲的贡献),然后算上父亲的贡献插入到一个大的 multiset
中即可。
对于修改影响到的点只有 i,f[i],f[f[i]],f[f[f[i]]],j,f[j],f[f[j]]
,所以修改就是一个大模拟,注意某个点不要反复插入,这可以用打标记的方法解决,时间复杂度 \(O(n\log n)\)
总结
考虑适当地不维护某些贡献也是重要的思维方式。这些贡献的特点是:变化很大,但是单次计算很快。对于树\(/\)基环树问题我们可以考虑不维护父亲的贡献。
#include <cstdio>
#include <iostream>
#include <set>
using namespace std;
const int M = 100005;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,f[M],d[M],a[M],t[M],v[M];multiset<int> s[M];
void ins(int x,int y) {s[x].insert(y);}
void rem(int x,int y) {s[x].erase(s[x].find(y));}
void add(int x)
{
if(v[x] || !s[x].size()) return;
v[x]=1;
ins(0,*s[x].begin()+t[x]/(d[x]+1));
ins(0,*(--s[x].end())+t[x]/(d[x]+1));
}
void del(int x)
{
if(!v[x] || !s[x].size()) return;
v[x]=0;
rem(0,*s[x].begin()+t[x]/(d[x]+1));
rem(0,*(--s[x].end())+t[x]/(d[x]+1));
}
void work(int i,int j)
{
//delete in s[0]
del(f[i]);del(f[f[i]]);del(f[f[f[i]]]);
del(j);del(f[j]);del(f[f[j]]);
//remove a[i] in f[i]
rem(f[i],a[i]);
//update f[i] in f[f[i]]
rem(f[f[i]],a[f[i]]);
a[f[i]]-=t[i]/(d[i]+1);
a[f[i]]-=t[f[i]]-d[f[i]]*(t[f[i]]/(d[f[i]]+1));
a[f[i]]+=t[f[i]]-(d[f[i]]-1)*(t[f[i]]/d[f[i]]);
ins(f[f[i]],a[f[i]]);
//update f[f[i]] in f[f[f[i]]]
rem(f[f[f[i]]],a[f[f[i]]]);
a[f[f[i]]]-=t[f[i]]/(d[f[i]]+1);
a[f[f[i]]]+=t[f[i]]/d[f[i]];
ins(f[f[f[i]]],a[f[f[i]]]);
//update the deg
d[f[i]]--;d[j]++;
//add a[i] in j
ins(j,a[i]);
//update j in f[j]
rem(f[j],a[j]);
a[j]+=t[i]/(d[i]+1);
a[j]-=t[j]-(d[j]-1)*(t[j]/d[j]);
a[j]+=t[j]-d[j]*(t[j]/(d[j]+1));
ins(f[j],a[j]);
//update j in f[f[j]]
rem(f[f[j]],a[f[j]]);
a[f[j]]-=t[j]/d[j];
a[f[j]]+=t[j]/(d[j]+1);
ins(f[f[j]],a[f[j]]);
//add in s[0]
add(f[i]);add(f[f[i]]);add(f[f[f[i]]]);
add(j);add(f[j]);add(f[f[j]]);
f[i]=j;
}
signed main()
{
n=read();m=read();
for(int i=1;i<=n;i++) t[i]=read();
for(int i=1;i<=n;i++)
f[i]=read(),d[i]++,d[f[i]]++;
for(int i=1;i<=n;i++)
{
a[i]+=t[i]-d[i]*(t[i]/(d[i]+1));
a[f[i]]+=t[i]/(d[i]+1);
}
for(int i=1;i<=n;i++) s[f[i]].insert(a[i]);
for(int i=1;i<=n;i++) add(i);
while(m--)
{
int op=read(),x=0,y=0;
if(op==1)
x=read(),y=read(),work(x,y);
if(op==2)
x=read(),printf("%lld\n",a[x]+t[f[x]]/(d[f[x]]+1));
if(op==3)
printf("%lld %lld\n",*s[0].begin(),*(--s[0].end()));
}
}