NOIP 2015 Senior 6

来源:互联网 发布:网络暴力 论文 编辑:程序博客网 时间:2024/06/10 17:20

传送门(洛谷)

①求最小值/最大值 => 二分答案进行判断

在 NOIP 中,经常会出现求最小时间的问题。当直接解决走不通时,可以考虑二分一个答案,然后判断一下当前答案是否可行。对于这道题而言,可以通过二分答案来解决。

②一般思路

由于最后要求的是航线最大值的最小值,所以要对答案有贡献,边权被置为0的边必须为某些航线的交集,使对这条边置0后所有航线的最大路程至多为之前的最大值减去改造成虫洞的边权。直接解决问题的难点就在于如何确定哪些航线应该属于被优化的航线,哪些不属于;而二分答案后就很明显了:如果总时间小于等于答案,就不需要属于被优化的航线,否则一定属于被优化的航线。这也是想到二分的突破口。

知道哪些航线需要被优化后,我们就可以求交集了。最后的选择就是在所有需要被优化的航线的交集中选一条边权最大的边。如果所有航线的最大值(当然也是被优化的航线的最大值)减去被选择的边的边权小于等于答案,那么答案可行。

③求交集

一个正常的思路就是每走过一条边,就给这条边的标记+1。走完所有航线后,如果某一条边的标记次数为走过的航线次数,那么那一条边就属于交集。

然而,直接打标记显然不靠谱,所以这里要用更高级的方法。打树剖从原理上来说可以,但联赛不考树剖,而且树剖的常数也是比较大的,所以在这里不讨论树剖。

可以借助数组前缀和的思想。对于数组ai,它的前缀和是ij=1aj。如果要给区间加上一个相同的数并且只在所有操作做完后才查询,就可以通过打差分标记的方法来实现。对于树,也可以用类似的方法。设ai为结点i的标记值,si为结点i及其子树的标记值之和。要使路径上的每一个结点(每一个结点的标记值代表该结点到其父结点的边的标记值)加上1,则可以让起点+1,终点+1,LCA-2。查询时,查询某一个结点的si就可以了。

求前缀和的过程可以直接DFS。

④通过预处理拓扑序加速计算前缀和

相同数量级的操作用DFS的速度肯定不及从头到尾扫过一次数组的速度。事实上,这个题直接用DFS最后一个点容易被卡掉,但是用拓扑排序预处理后就能侥幸通过。其原理是用拓扑排序处理后保证使用到某一个结点的值时其值一定被它的子结点更新完了。这个思想很重要,应用也很广泛。

参考代码

#include <cstdio>#include <cstdlib>#include <cmath>#include <cstring>#include <iostream>#include <algorithm>#include <vector>#include <string>#include <stack>#include <queue>#include <deque>#include <map>#include <set>#include <bitset>using std::cin;using std::cout;using std::endl;typedef int INT;inline INT readIn(){    INT a = 0;    bool minus = false;    char ch = getchar();    while (!(ch == '-' || ch >= '0' && ch <= '9')) ch = getchar();    if (ch == '-')    {        minus = true;        ch = getchar();    }    while (ch >= '0' && ch <= '9')    {        a *= 10;        a += ch;        a -= '0';        ch = getchar();    }    if (minus) a = -a;    return a;}const INT maxn = 300005;INT n, m;struct Edge{    INT to;    INT cost;    INT next;} edges[maxn * 2];INT head[maxn];void addEdge(INT from, INT to, INT cost){    static INT count_ = 0;    count_++;    edges[count_].to = to;    edges[count_].cost = cost;    edges[count_].next = head[from];    head[from] = count_;}INT topo[maxn];INT size;INT depth[maxn];INT next[20][maxn];INT sum[20][maxn];void dfs(INT node = 1){    for(int i = head[node]; i; i = edges[i].next)    {        INT to = edges[i].to;        if(to == next[0][node]) continue;        INT cost = edges[i].cost;        depth[to] = depth[node] + 1;        next[0][to] = node;        sum[0][to] = cost;        dfs(to);    }    topo[++topo[0]] = node;}void goDouble() //这道题的路径长和LCA可以用倍增来处理{    while(1 << size < n) size++;    for(int i = 1; i <= size; i++)    {        for(int j = 1; j <= n; j++)        {            next[i][j] = next[i - 1][next[i - 1][j]];            sum[i][j] = sum[i - 1][j] + sum[i - 1][next[i - 1][j]];        }    }}struct Path{    INT from;    INT to;    INT lca;    INT t;    Path() {}    Path(INT u, INT v) : from(u), to(v), lca(0), t(0)    {        if(depth[u] < depth[v]) std::swap(u, v);        for(int i = size; ~i; i--)        {            if(depth[next[i][u]] < depth[v]) continue;            t += sum[i][u];            u = next[i][u];        }        if(u != v)        {            for(int i = size; ~i; i--)            {                if(next[i][u] == next[i][v]) continue;                t += sum[i][u] + sum[i][v];                u = next[i][u];                v = next[i][v];            }            t += sum[0][u] + sum[0][v];            u = next[0][u];        }        lca = u;    }} paths[maxn];INT maxPath;INT tree[maxn];INT goSum(){    for(int i = 1; i <= n; i++)    {        tree[next[0][topo[i]]] += tree[topo[i]]; //使用预处理好的拓扑序加速    }}bool check(INT s){    memset(tree, 0, sizeof(tree));    INT total = 0;    for(int i = 1; i <= m; i++)    {        if(paths[i].t <= s) continue;        total++;        tree[paths[i].from]++;        tree[paths[i].to]++;        tree[paths[i].lca] -= 2;    }    goSum();    INT maxRemain = 0;    if(total)    {        for(int i = 1; i <= n; i++)        {            if(tree[i] == total)            {                maxRemain = std::max(maxRemain, sum[0][i]);            }        }    }    return maxPath - maxRemain <= s;}void run(){    n = readIn();    m = readIn();    for(int i = 1; i < n; i++)    {        INT from = readIn();        INT to = readIn();        INT cost = readIn();        addEdge(from, to, cost);        addEdge(to, from, cost);    }    depth[1] = 1;    dfs();    goDouble();    for(int i = 1; i <= m; i++)    {        INT from = readIn();        INT to = readIn();        new(paths + i) Path(from, to);        maxPath = std::max(maxPath, paths[i].t);    }    INT l = -1, r = maxPath;    while(r - l > 1)    {        INT mid = l + ((r - l) >> 1);        if(check(mid))        {            r = mid;        }        else        {            l = mid;        }    }    cout << r << endl;}int main(){#ifndef JUDGE    freopen("transport.in", "r", stdin);    freopen("transport.out", "w", stdout);#endif    run();    return 0;}

总结:

这道题虽然说思维难度不大,但其实是很多经典的算法(不如说想法)合并而成的。他们有:

①极值问题通过二分答案转换为判定问题

②通过倍增求树上的路径的长度及LCA(这个也算一个吧)

③前缀和思想在树上的应用,以及差分标记

④通过预处理拓扑序来加速DAG(Tree)上的DP(递推)

原创粉丝点击