树分治练习题题解

一、题意

 

给定一棵树,节点个数$N \le 20000$,每条边上有一个代价$c$和收益$b$,
找到一条路径$p$使得该路径上的代价和不超过$C$且收益和最大,输出最大收益。

 

二、思路&知识点

具体思路如下,这儿只说说启发式合并,同时简单证明一下时间复杂度。

启发式合并,听起来很神奇,其实只是名字好听而已。

启发式:其实就是利用了一些组合优化的方法来优化运算量,在这里,用的就是排序。

合并:说白了就是利用归并排序的思想合并两个子数组。

在下面的思路中,我们在预处理重心$G$的所有子节点的子树信息后,对子树的大小做了从小到大的排序处理,这样,后面每次合并$dis$数组和$tdis$数组时,所合并的信息都是有效的,而且是效率最高的,并不会“拖泥带水”。

在此题中,因为题目的特殊性,$dis$数组并没有去重,所以启发式合并的优势体现地不是特别明显。但是,我接下来将给出最坏的情况,并简单证明这题的时间复杂度。

最坏的情况:重心$G$有$m$棵子树,它们的大小依次为:$1,2,3,4,\cdots,x$,那么,由方程$\frac{(1+x)x}{2} \le 20000$可以解出,$x \le \sqrt{40000}$,即$x\le \sqrt{N}$。每一次合并所需要的计算量为:$\sum\limits_{k=1}^{i}size_k$,即$C_{k+1}^2$,那么,$x$次加起来的计算量就是$C_{x+2}^3\frac{(x+2)(x+1)x}{6}=\frac{N*x}{6}$,即单次处理重心的总的计算量是$N \cdot \sqrt{N}$。

当然,如果每次可去重,或者记录前$i$棵子树信息的数组元素不会增加,那单次处理重心的时间复杂度是$N \cdot logN$。

三、代码

 

/*
题意:给定一棵树,节点个数<=20000,每条边上有一个代价c和收益b,
找到一条路径p使得该路径上的代价和不超过C且收益和最大,输出最大收益。

总体思路:
对于这种树上找两点的,很显然可以用树分治做。
假设当前找出来的重心是G,然后再算出其他点到G的代价和c以及相应的收益和b。
然后,朴素的方法是,在上面dfs的时候对每个点打个标记,标记它是哪个子树上的。
这样,对所有点的代价和从小到大排序,枚举每个点p,再二分找出代价和不超过C-p.c的数组下标idx,
然后扫描1->idx之间的所有点q,如果点p和点q不同组,那么,更新答案。
但是,这样的方法,最坏的情况,还是会退化成O(N^2)。
解决办法就是,采用启发式合并(博客正文中已经详细说明)。
使用两个数组,dis和tdis。
dis数组维护的是前i-1的子树所有节点的代价和以及在不超过该代价下最大的收益。
要注意看,看仔细,是不超过该代价下的最大的收益,也就是说,在保证dis按照代价从小到大有序之后,
还要对dis数组的收益b做一遍前缀最大值。
tdis数组维护的是第i棵子树所有节点的代价和以及相应的收益。
注意看仔细,tdis数组是第i棵子树中的信息,而且并没有刷过前缀最大值。
有了以上数组以后,每次计算出tdis数组以后,枚举tdis数组的每个元素x,
由于dis数组已经刷过前缀最大值,所以,只需要二分找出代价和不超过C-e.c的元素y,
然后用x.b+y.b更新答案即可。
另外,需要注意的是,如果答案只是一条从重心到子树中某个节点的链,还需要用tdis更新一遍答案。
最后,使用归并排序的思想归并dis数组和tdis数组即可,同时记得重新对dis数组刷一遍前缀最大值。

=================================================================================
在此题中,应用此方法的时间复杂度是:O(N*sqrt(N)*logN)
sqrt(N)的原因说简单点就是这种不去重方式的启发式合并,计算量是O(N*sqrt(N))的,。
=================================================================================
**/
#pragma comment(linker,"/STACK:102400000,102400000")
#include<bits/stdc++.h>
using namespace std;
#define MAXN 20010
struct edge {
    int to, next;
    int c, b;
    edge(int to = 0, int next = 0, int c = 0, int b = 0): to(to), next(next), c(c), b(b) {}
} es[MAXN * 2];
int head[MAXN], ecnt;
int n, C;

void add(int from, int to, int c, int d) {
    es[++ecnt] = edge(to, head[from], c, d), head[from] = ecnt;
}
void init() {
    memset(head, 0, sizeof(head[0]) * (n + 5));
    ecnt = 0;

}
int siz[MAXN], maxsonsiz[MAXN], G, subn;
bool vis[MAXN];
int ans;
/*存重心子节点的编号,重心到子节点的边的编号,以子节点为根的子树的大小*/
struct node {
    int id, eid, siz;
    node(int id = 0, int eid = 0, int siz = 0): id(id), eid(eid), siz(siz) {}
    bool operator <(node nd)const {
        return siz < nd.siz;
    }
} pro[MAXN];
/*存从重心往下搜索路径上消耗的代价和收益*/
struct record {
    int c, b;
    record(int c = 0, int b = 0): c(c), b(b) {}
} dis[MAXN], tdis[MAXN], tmp[MAXN];
int pcnt, dis_siz, tdis_siz, tmp_siz;
/*下面main函数中一定要记得调用这个函数初始化*/
void init2() {
    G = 0, subn = n, maxsonsiz[G] = subn;
    memset(vis, 0, sizeof(vis[0]) * (n + 3));
    ans = 0;
    dis_siz = tdis_siz = 0;
}
/*模板:找重心函数*/
void find_center(int root, int par) {
    siz[root] = 1, maxsonsiz[root] = 0;
    for(int i = head[root]; i; i = es[i].next) {
        int to = es[i].to;
        if(to != par && !vis[to]) {
            find_center(to, root);
            siz[root] += siz[to];
            maxsonsiz[root] = max(maxsonsiz[root], siz[to]);
        }
    }
    maxsonsiz[root] = max(maxsonsiz[root], subn - siz[root]);
    if(maxsonsiz[root] < maxsonsiz[G])G = root;
}
/*计算以root为根的子树的大小*/
int getsiz(int root, int par) {
    int res = 1;
    for(int i = head[root]; i; i = es[i].next) {
        int to = es[i].to;
        if(to != par && !vis[to]) {
            res += getsiz(to, root);
        }
    }
    return res;
}
/*
第一句,如果写了,相当剪了枝,那么之前算出了的子树的siz就会不对,
那么下面的find_ans函数中的tdis数组的长度就是tdis_siz,
否则,tdis_siz=pro[i].siz,随便用哪个都可以。
因此,总的来说,还是写下剪枝好,下面find_ans函数中统一用tdis_siz。
*/
void getdis(int root, int par, record r) {
    if(r.c>C)return;
    tdis[++tdis_siz] = r;
    for(int i = head[root]; i; i = es[i].next) {
        int to = es[i].to;
        if(to != par && !vis[to]) {
            getdis(to, root, record(r.c + es[i].c, r.b + es[i].b));
        }
    }
}

bool cmp(record a, record b) {
    if(a.c == b.c)return a.b < b.b;
    else return a.c < b.c;
}

int bs(int l, int r, int v) {
    int low = l - 1, high = r + 1, mid;
    while(low < high - 1) {
        mid = (low + high) / 2;
        if(dis[mid].c <= v)low = mid;
        else high = mid;
    }
    return low;
}

void find_ans(int center, int par) {
    pcnt = 0;
    /*预处理每个儿子节点,然后再启发式合并。*/
    for(int i = head[center]; i; i = es[i].next) {
        int to = es[i].to;
        if(to != par && !vis[to]) {
            pro[++pcnt] = node(to, i, getsiz(to, center));
        }
    }
    sort(pro + 1, pro + pcnt + 1);
    for(int i = 1; i <= pcnt; ++i) {
        tdis_siz = 0;
        getdis(pro[i].id, center, record(es[pro[i].eid].c, es[pro[i].eid].b));
        if(i > 1) {
            /*对于当前子树的所有tdis来说,二分查找出前i-1棵子树的dis*/
            for(int j = 1; j <= tdis_siz; ++j) {
                int pos = bs(1, dis_siz, C - tdis[j].c);
                if(pos != 0)ans = max(ans, tdis[j].b + dis[pos].b);
            }
        }
        /*这里要非常注意,如果答案是从重心到子树中某个节点的链,就用这条语句来更新答案。*/
        for(int j = 1; j <= tdis_siz; ++j)if(tdis[j].c <= C)ans = max(ans, tdis[j].b);
        /*这里一定要记得排序,因为后面要做启发式合并。*/
        sort(tdis + 1, tdis + tdis_siz + 1, cmp);
        tmp_siz = 0;/*一下是归并排序思想做启发式合并,官方库的merge还是不太方便,自己实现最靠谱。*/
        for(int p1 = 1, p2 = 1; p1 <= dis_siz || p2 <= tdis_siz;) {
            if(p1 <= dis_siz && p2 <= tdis_siz) {
                if(dis[p1].c < tdis[p2].c)tmp[++tmp_siz] = dis[p1++];
                else if(dis[p1].c == tdis[p2].c) {
                    if(dis[p1].b < tdis[p2].b)tmp[++tmp_siz] = dis[p1++];
                    else tmp[++tmp_siz] = tdis[p2++];
                }
                else tmp[++tmp_siz] = tdis[p2++];
            }
            else if(p1 <= dis_siz)tmp[++tmp_siz] = dis[p1++];
            else tmp[++tmp_siz] = tdis[p2++];
        }
        /*这里要非常注意,dis数组收益b需要维护前i棵树的前缀和*/
        for(int j = 1; j <= tmp_siz; ++j)dis[j].c = tmp[j].c, dis[j].b = max(dis[j - 1].b, tmp[j].b);
        dis_siz = tmp_siz;
    }
    dis_siz = 0;
}
/*模板:树分治*/
void solve(int root, int par) {
    find_center(root, par);
    assert(G != 0);
    vis[G] = 1;
    find_ans(G, 0);
    for(int i = head[G]; i; i = es[i].next) {
        int to = es[i].to;
        if(!vis[to]) {
            G = 0, subn = siz[to], maxsonsiz[G] = subn;
            solve(to, G);
        }
    }
}

int main() {
//    freopen("h.in", "r", stdin);
    int T;
    for(scanf("%d", &T); T--;) {
        scanf("%d", &n);
        init();
        for(int i = 1, a, b, c, d; i < n; ++i) {
            scanf("%d%d%d%d", &a, &b, &c, &d);
            add(a, b, c, d), add(b, a, c, d);
        }
        scanf("%d", &C);
        init2();
        solve(1, 0);
        printf("%d\n", ans);
    }
    return 0;
}

 

posted @ 2018-10-30 01:20 fuzhihong0917 阅读(...) 评论(...) 编辑 收藏