2020牛客暑期多校训练营(第四场)A题解

话说这题出题人给的题解看了半天看不大懂啊……
还是 看了好多AC代码才搞明白

题目传送门

首先这道题的题面给出了一个easy版本的问题,我们先从这一问题开始。
读完题目后,我们略一思考就可发现,随着x的减少,k是会不断的增加的,这显然是一个单调的函数关系,因此很容易想到二分。在easy版本中k固定的情况下,二分的区间就是1~n,每次check时,我们先找出当前深度最深的点,向上跳x个距离到达某一结点u,将其设为关键点,然后将以u点为根的子树打上标记,表示这颗子树已经被“处理”了。这样每次重复上述过程,直到整棵树都已经被标记,这样我们就可以得到一个关键点的数量k’,将k’与k进行比较即可。当然,根节点必须是关键点。
实际代码实现可能需要一些细节,就不深入了。
接下来进入正题。
这道题将k改为了一个从1~n的值,如果我们依然像上述做法一样固定k,二分x的话,复杂度会比较高,因此我们换一种思路。
我们发现,无论k是多少,x都只会落在1~n的范围内,因此,我们只需要枚举x,然后按上述方法求出每一个x对应的k,用数组将其记录下来。由于一个k可能对应有不同的x,所以我们还需要取一个x的最小值。
不过实现起来还有亿点点细节。
寻找最大值、向上跳x步以及标记树上结点的操作都必须是log级别,不然会T,因此我们通过dfs序将这棵树映射到数组上,并用线段树维护其最深的结点和标记情况,寻找祖先的方法则是倍增。
不了解dfs序和倍增的小伙伴自行百度。
下面上代码

#include 

using namespace std;

const int maxn = 2e5+10;
struct edge{
    int to,nxt;
}e[maxn<<1];

///线段树,col为懒惰标记,0表示未标记,1表示已标记,2表示还原标记
struct tr{
    int mx,mx2,col;///这里存两个最深的结点编号方便还原
}tree[maxn<<3];

int head[maxn];
int parent[maxn][30];
int newIdx[2][maxn];
int ans[maxn];
int dfn[maxn<<1];
int Log2[maxn],depth[maxn];
int cnt,tot,n,m;

void addEdge(int u,int v){
    e[++cnt].to = v;
    e[cnt].nxt = head[u];
    head[u] = cnt;
}

inline void pushUp(int u){
    tree[u].mx = depth[tree[u<<1].mx]>depth[tree[u<<1|1].mx]?tree[u<<1].mx:tree[u<<1|1].mx;
}

void build(int l,int r,int u){
    if(l==r){
        tree[u].mx = dfn[l];
        tree[u].col = 0;
        tree[u].mx2 = tree[u].mx;
        return;
    }
    int mid = (l+r)>>1;
    build(l,mid,u<<1);
    build(mid+1,r,u<<1|1);
    pushUp(u);
    tree[u].mx2 = tree[u].mx;
}

void dfs(int u,int fa){///处理dfs序和倍增
    dfn[++tot] = u;
    newIdx[0][u] = tot;
    depth[u] = depth[fa]+1;
    parent[u][0] = fa;
    for(int i=1;i<=Log2[depth[u]]-1;++i)
        parent[u][i] = parent[parent[u][i-1]][i-1];
    for(int i=head[u];i;i=e[i].nxt){
        int v = e[i].to;
        if(v==fa) continue;
        dfs(v,u);
    }
    dfn[++tot] = u;
    newIdx[1][u] = tot;
}

int find(int u,int x){///倍增寻找祖先
    int a = u;
    if(x==0) return a;
    for(int i=Log2[x]-1;i>=0;--i){
        if((1<<i)<x&&parent[a][i]){
            a = parent[a][i];
            x -= (1<<i);
        }
    }
    return parent[a][0];
}

void pushDown(int u){
    int col = tree[u].col;
    if(!col) return;
    if(col==1){
        tree[u<<1].col = tree[u<<1|1].col = col;
        tree[u<<1].mx = tree[u<<1|1].mx = 0;
        tree[u].col = 0;
    }
    else if(col==2){
        tree[u<<1].col = tree[u<<1|1].col = col;
        tree[u<<1].mx = tree[u<<1].mx2;
        tree[u<<1|1].mx = tree[u<<1|1].mx2;
        tree[u].col = 0;
    }
}

void modify(int L,int R,int l,int r,int u){
    if(L>=l&&R<=r){
        tree[u].col = 1;
        tree[u].mx = 0;
        return;
    }
    pushDown(u);
    int mid = (L+R)>>1;
    if(mid>=l) modify(L,mid,l,r,u<<1);
    if(mid<r) modify(mid+1,R,l,r,u<<1|1);
    pushUp(u);
}

int solve(int x){
    tree[1].mx = tree[1].mx2;
    tree[1].col = 2;
    int start,u,cnt = 0;
    while(1){
        start = tree[1].mx;
        u = find(start,x);
        modify(1,tot,newIdx[0][u],newIdx[1][u],1);
        cnt ++;
        if(u==1) break;
    }
    return cnt;
}

int main()
{
    while(scanf("%d",&n)!=EOF){
        ///记得务必初始化干净
        int res = 0;
        memset(parent,0,sizeof(parent));
        cnt = tot = depth[0] = 0;
        for(int i=1;i<=n;++i){
            head[i] = 0;
            newIdx[0][i] = newIdx[1][i] = 0;
            depth[i] = 0;
            Log2[i] = Log2[i-1]+(1<<Log2[i-1]==i);
        }
        for(int i=1;i<n;++i){
            int u,v,w;
            scanf("%d %d %d",&u,&v,&w);
            addEdge(u,v);
            addEdge(v,u);
        }
        dfs(1,1);
        build(1,tot,1);
        for(int i=1;i<=n;++i){
            ans[i] = n+1;
        }
        ///枚举x,计算k,用x更新k
        ///因为是反向枚举,所以最后得到的一定是最小值
        for(int i=n;i>=0;--i){
            ans[solve(i)] = i;
        }
        ///由于枚举x得到的k是最小的,有些k会没有被更新到,所以要再判一次
        for(int i=2;i<=n;++i){
            if(ans[i]==n+1) ans[i] = ans[i-1];
        }
        for(int i=1;i<=n;++i){
            res += ans[i];
        }
        printf("%d\n",res);
    }
    return 0;
}

下面还剩下最后一个问题,关于这个算法的复杂度。
通过以上代码我们可以看出,对于每一个枚举的x,我们都要进行k次循环,而每次循环内部进行的更改都是logn的,所以最后算法的复杂度为
∑ k k l o g n \sum_{k}klogn kklogn
假设一条链的深度为d,我们通过画图可以发现:
x = d时,k = 1
x = d-1 ~ d/2时,k = 2
x = d/2 - 1 ~ d/3时,k = 3
……
总结一下就是:
k = ⌊ d x ⌋ ( + 1 ) k=\lfloor \frac{d}{x} \rfloor (+1) k=xd(+1)
后面的+1根据情况,能整除时不用,不能整除时就要+1
于是,算法的复杂度就变成了
∑ i = 1 n n i l o g n \sum_{i=1}^n \frac{n}{i}logn i=1ninlogn
中间出现了调和级数
查百度可以知道,调和级数的近似值为ln(x+1)
于是算法的复杂度近似为n(logn)^2

如果题解中有什么错误欢迎指出

你可能感兴趣的:(2020牛客暑期多校训练营(第四场)A题解)