[BZOJ4326][NOIP2015]运输计划(二分+dfs序+树上差分)

题目描述

传送门

题解

首先,要求最大值最小,先想到二分。可以预处理出来所有路径所需要的时间。
关键是怎么check呢?

算法一
每次二分出mid了之后,将所有时间>=mid的路径上的边打标记(链剖+bit即可),然后枚举每一条边,如果这条边打的标记的数量=所有需要打标记的路径的数量,并且这条边是所有满足条件里的最大的,那么将这条边砍断。然后再判断这个最解是否可行就行了。
然而这个时间复杂度是 O(logTmlogn) 的,会被卡掉。

算法二
我们考虑在算法一的基础上进行优化。算法一的瓶颈主要是check的时候是 O(mlogn) 的,查询的时候套了一个bit的log。那么能不能进行 O(m) 的check呢?
当然可以!方法就是——差分!这里用了一个非常常用的树上差分的思路:对于点x,y设r=lca(x,y)。在x+1,y+1,r-2,然后统计后缀和(x->f[x])。这样就可以把每一条路径都打上标记,然后查询就可以了。

虽然Po的代码在BZ上没快多少,但是在UOJ上可以发现算法二是明显更优的。
(比如愚蠢的ATP写了愚蠢的算法一结果被卡T辣!——ATP▽)

代码

#include
#include
#include
using namespace std;
#define N 300005
#define sz 19

int n,m,x,y,z,Max,dfs_clock,ans;
int tot,point[N],nxt[N*2],v[N*2],c[N*2];
int h[N],dis[N],val[N],num[N],tmp[N],f[N][sz+5];
struct hp{int x,y,lca,dis;}edge[N];

void addedge(int x,int y,int z)
{
    ++tot; nxt[tot]=point[x]; point[x]=tot; v[tot]=y; c[tot]=z;
    ++tot; nxt[tot]=point[y]; point[y]=tot; v[tot]=x; c[tot]=z;
}
void build(int x,int fa)
{
    num[++dfs_clock]=x;
    for (int i=1;iif ((h[x]-(1<1) break;
        f[x][i]=f[f[x][i-1]][i-1];
    }
    for (int i=point[x];i;i=nxt[i])
        if (v[i]!=fa)
        {
            f[v[i]][0]=x;
            h[v[i]]=h[x]+1;dis[v[i]]=dis[x]+c[i];val[v[i]]=c[i];
            build(v[i],x);
        }
}
int lca(int x,int y)
{
    if (h[x]int k=h[x]-h[y];
    for (int i=0;iif ((1<if (x==y) return x;
    for (int i=sz-1;i>=0;--i)
        if (f[x][i]!=f[y][i])
            x=f[x][i],y=f[y][i];
    return f[x][0];
}
bool check(int mid)
{
    int cnt=0,limit=0;memset(tmp,0,sizeof(tmp));
    for (int i=1;i<=m;++i)
        if (edge[i].dis>mid)
        {
            ++tmp[edge[i].x];++tmp[edge[i].y];tmp[edge[i].lca]-=2;
            limit=max(limit,edge[i].dis-mid);
            cnt++;
        }
    if (!cnt) return true;
    for (int i=n;i>1;--i) tmp[f[num[i]][0]]+=tmp[num[i]];
    for (int i=2;i<=n;++i)
        if (val[i]>=limit&&tmp[i]==cnt) return true;
    return false;
}
int find()
{
    int l=0,r=Max,mid,ans;
    while (l<=r)
    {
        mid=(l+r)>>1;
        if (check(mid)) ans=mid,r=mid-1;
        else l=mid+1;
    }
    return ans;
}
int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1;iscanf("%d%d%d",&x,&y,&z);
        addedge(x,y,z);Max+=z;
    }
    build(1,0);
    for (int i=1;i<=m;++i)
    {
        scanf("%d%d",&edge[i].x,&edge[i].y);
        edge[i].lca=lca(edge[i].x,edge[i].y);
        edge[i].dis=dis[edge[i].x]+dis[edge[i].y]-dis[edge[i].lca]*2;
    }
    ans=find();
    printf("%d\n",ans);
}

总结

树上差分的常见思路:
①利用dfs序的时间戳,一个点拆成两个点,每次在in+1,在out-1,然后bit统计前缀和,资瓷动态修改和查询子树访问次数。用于子树打标记。
②对于点x,y设r=lca(x,y)。在x+1,y+1,r-2,然后统计后缀和(x->f[x])。用于树链打标记,资瓷查询某条边的访问次数。
③对于点x,y设r=lca(x,y)。在x+1,y+1,r-1,father[r]-1,然后统计后缀和(x->f[x])。用于树链打标记,资瓷查询某个点的访问次数。

你可能感兴趣的:(题解,NOIP,dfs序,二分,差分)