【CodeForces】671 D. Roads in Yusland

【题目】D. Roads in Yusland

【题意】给定n个点的树,m条从下往上的链,每条链代价ci,求最少代价使得链覆盖所有边。n,m<=3*10^5,ci<=10^9,time=4s。

【算法】树形DP+线段树||可并堆

【题解】从每条边都需要一条链来覆盖的角度出发,令f[i]表示覆盖子树 i 以及 i到fa[i]的边(i->fa[i])的最小代价,整个过程通过dfs从下往上做。

由于f[son[i]]已知,所以f[i]的转移实际上是考虑覆盖i->fa[i]的链,定义这条链为主链。那么f[i]=min(c+Σf[k]),c是主链代价,k是主链上在i子树内的所有点的子节点(不含主链上点),所有起点在子树i内终点在i的祖先的链都可以作为主链,取最小值

自然地,可以在递归的过程中将Σf[k]并入c中。具体而言,对于每个点x:

1.删。将终点在x的链删除。

2.加。记sum=Σf[son[i]],son[i]子树内所有的链c+=sum-f[son[i]](就是把Σf[k]并入c中),特别地,起点在i的链c+=sum。

3.取。f[i]是子树i中所有的链c的最小值。

现在需要快速支持子树加值和子树求最小值的操作,可以用线段树按dfs序维护所有链实现(把链按起点的dfs序作为线段树下标)。

复杂度O(n log n)。

#include<cstdio>
#include<cctype>
#include<vector>
#include<algorithm>
#define ll long long
using namespace std;
int read(){
    char c;int s=0,t=1;
    while(!isdigit(c=getchar()))if(c=='-')t=-1;
    do{s=s*10+c-'0';}while(isdigit(c=getchar()));
    return s*t;
}
const int maxn=300010;
const ll inf=1e15;
struct tree{int l,r;ll delta,mins;}t[maxn*4];
struct edge{int v,from;}e[maxn*2];
vector<int>v[maxn];
int n,m,ku[maxn],kv[maxn],kw[maxn],kp[maxn],tot=0,dfsnum=0,first[maxn],be[maxn],ed[maxn];
ll a[maxn],f[maxn];
void ins(int u,int v){tot++;e[tot].v=v;e[tot].from=first[u];first[u]=tot;}
void dfs_order(int x,int fa){
    be[x]=dfsnum+1;
    for(int i=0;i<(int)v[x].size();i++){
        kp[v[x][i]]=++dfsnum;
        a[dfsnum]=kw[v[x][i]];
    }
    for(int i=first[x];i;i=e[i].from)if(e[i].v!=fa){
        dfs_order(e[i].v,x);
    }
    ed[x]=dfsnum;
    if(be[x]>ed[x]){printf("-1");exit(0);}
}
void modify(int k,ll x){t[k].mins+=x;t[k].delta+=x;}
void up(int k){t[k].mins=min(t[k<<1].mins,t[k<<1|1].mins);}
void down(int k){
    if(t[k].delta){
        modify(k<<1,t[k].delta);
        modify(k<<1|1,t[k].delta);
        t[k].delta=0;
    }
}
void build(int k,int l,int r){
    t[k].l=l;t[k].r=r;t[k].delta=0;
    if(l==r){t[k].mins=a[l];}else{
        int mid=(l+r)>>1;
        build(k<<1,l,mid);
        build(k<<1|1,mid+1,r);
        up(k);
    }
}
void add(int k,int l,int r,ll x){
    if(l<=t[k].l&&t[k].r<=r){modify(k,x);return;}
    down(k);
    int mid=(t[k].l+t[k].r)>>1;
    if(l<=mid)add(k<<1,l,r,x);
    if(r>mid)add(k<<1|1,l,r,x);
    up(k);
}
ll ask(int k,int l,int r){
    if(l<=t[k].l&&t[k].r<=r){return t[k].mins;}
    down(k);
    int mid=(t[k].l+t[k].r)>>1;
    ll ans=inf;
    if(l<=mid)ans=ask(k<<1,l,r);
    if(r>mid)ans=min(ans,ask(k<<1|1,l,r));
    return ans;
}
ll dp(int x,int fa){
    f[x]=0;ll sum=0;
    for(int i=first[x];i;i=e[i].from)if(e[i].v!=fa)sum+=dp(e[i].v,x);
    for(int i=0;i<(int)v[x].size();i++)add(1,v[x][i],v[x][i],inf);
    add(1,be[x],ed[x],sum);
    for(int i=first[x];i;i=e[i].from)if(e[i].v!=fa){
        add(1,be[e[i].v],ed[e[i].v],-f[e[i].v]);
    }
    f[x]=ask(1,be[x],ed[x]);
    if(x!=1&&f[x]>=inf){printf("-1");exit(0);}
    return f[x];
}
int main(){
    n=read();m=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read();
        ins(u,v);ins(v,u);
    }
    for(int i=1;i<=m;i++){
        ku[i]=read(),kv[i]=read(),kw[i]=read();
        v[ku[i]].push_back(i);
    }
    dfsnum=0;
    dfs_order(1,0);
    build(1,1,dfsnum);
    for(int i=1;i<=m;i++)v[ku[i]].clear();
    for(int i=1;i<=m;i++)v[kv[i]].push_back(kp[i]);
    dp(1,0);
    ll ans=0;
    for(int i=first[1];i;i=e[i].from)ans+=f[e[i].v];
    printf("%lld",ans);
    return 0;
}
View Code

 

可并堆写法:

核心思想仍是——每条边都需要一条链来覆盖。

整个过程通过dfs从下往上做,对于每个点x,维护一个堆包含所有起点在子树x内终点为x的祖先的链(按价值从小到大)。

维护的过程只需要将所有儿子的堆合并过来,然后删除终点在x的链。(堆的删除不需要真的删除,只需要在调用堆顶是判断是否已被删除)

接下来考虑选用哪些链,考虑点x时,子树x内所有边都已经被覆盖,所以实际上是在考虑x->fa[x]这条边的覆盖,那么此时堆x中的链都可以随意选用,但是选用哪条对未来更优当前并不知道。

采用反悔的思想,先选用代价w最小的链,并将堆整体标记-w,之后考虑选用其它边实际上就是“更换”的操作了,当然选用的代价w的链不移除(在堆中代价为0)将一直发挥作用直至其终点。

这样做的正确性就在于,在标记-w时,堆中所有的链可以随意换用,因为都会影响x->fa[x]而子树x已经完全覆盖了无须考虑。

总结起来,对于点x:

1.合并所有son[x]。

2.找到堆顶w加入答案。(不需要特别做删除)

3.整体标记-w。

复杂度O(n log n),常数优势明显。

#include<cstdio>
#include<cctype>
#include<cctype>
#include<algorithm>
#define ll long long
using namespace std;
int read(){
    char c;int s=0,t=1;
    while(!isdigit(c=getchar()))if(c=='-')t=-1;
    do{s=s*10+c-'0';}while(isdigit(c=getchar()));
    return s*t;
}
const int maxn=300010;
struct edge{int v,from;}e[maxn*2];
int tot,first[maxn],l[maxn],r[maxn],d[maxn],root[maxn],n,m,top[maxn];
ll delta[maxn],w[maxn],ans=0;
bool vis[maxn];

void insert(int u,int v){tot++;e[tot].v=v;e[tot].from=first[u];first[u]=tot;}
void modify(int k,int x){delta[k]+=x;w[k]+=x;}
void down(int x){
    if(delta[x]){
        if(l[x])modify(l[x],delta[x]);
        if(r[x])modify(r[x],delta[x]);//make 0 no influence!
        delta[x]=0;
    }
}
int merge(int x,int y){
    if(!x||!y)return x^y;
    if(w[x]>w[y])swap(x,y);
    down(x);r[x]=merge(r[x],y);
    if(d[l[x]]<d[r[x]])swap(l[x],r[x]);
    d[x]=d[r[x]]+1;
    return x;
}
void dfs(int x,int fa){
    for(int i=first[x];i;i=e[i].from)if(e[i].v!=fa)dfs(e[i].v,x),root[x]=merge(root[x],root[e[i].v]);
    vis[x]=1;
    if(x==1)return;
    while(vis[top[root[x]]])root[x]=merge(l[root[x]],r[root[x]]);
    if(!root[x]){printf("-1");exit(0);}
    ans+=w[root[x]];modify(root[x],-w[root[x]]);
}
int main(){
    n=read();m=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read();
        insert(u,v);insert(v,u);
    }
    for(int i=1;i<=m;i++){
        int u=read();top[i]=read();w[i]=read();
        root[u]=merge(root[u],i);
    }
    ans=0;
    dfs(1,0);
    printf("%lld",ans);
    return 0;
}
View Code

 

posted @ 2017-12-22 11:44  ONION_CYC  阅读(660)  评论(0编辑  收藏  举报