NOIP2012 疫情控制

题目描述

H 国有 n 个城市,这 n 个城市用 n1 条双向道路相互连通构成一棵树, 1 号城市是首都,也是树中的根节点。

H 国的首都爆发了一种危害性极高的传染病。当局为了控制疫情,不让疫情扩散到边境城市(叶子节点所表示的城市),决定动用军队在一些城市建立检查点,使得从首都到边境城市的每一条路径上都至少有一个检查点,边境城市也可以建立检查点。但特别要注意的是,首都是不能建立检查点的。

现在,在 H 国的一些城市中已经驻扎有军队,且一个城市可以驻扎多个军队。一支军队可以在有道路连接的城市间移动,并在除首都以外的任意一个城市建立检查点,且只能在一个城市建立检查点。一支军队经过一条道路从一个城市移动到另一个城市所需要的时间等于道路的长度(单位:小时)。

请问最少需要多少个小时才能控制疫情。注意:不同的军队可以同时移动。

输入输出格式

输入格式:

第一行一个整数 n ,表示城市个数。

接下来的 n-1 行,每行 3 个整数, u,v,w ,每两个整数之间用一个空格隔开,表示从城市 u 到城市 v 有一条长为 w 的道路。数据保证输入的是一棵树,且根节点编号为 1 。

接下来一行一个整数 m ,表示军队个数。

接下来一行 m 个整数,每两个整数之间用一个空格隔开,分别表示这 m 个军队所驻扎的城市的编号。

输出格式:

一个整数,表示控制疫情所需要的最少时间。如果无法控制疫情则输出 -1 。 

输入输出样例

输入样例#1: 
4 
1 2 1 
1 3 2 
3 4 3 
2 
2 2
输出样例#1: 
3

说明

【输入输出样例说明】

第一支军队在 2 号点设立检查点,第二支军队从 2 号点移动到 3号点设立检查点,所需时间为 3 个小时。

【数据范围】

保证军队不会驻扎在首都。

对于 20%的数据, 2≤ n≤ 10

对于 40%的数据, 2 ≤n≤50,0<w <10^5

对于 60%的数据, 2 ≤ n≤1000,0<w <10^6;

对于 80%的数据, 2 ≤ n≤10,000

对于 100%的数据, 2≤m≤n≤50,000,0<w <10^9 。

NOIP 2012 提高组 第二天 第三题

由于最近刚学了倍增就来做这道题。没想到这道题竟然更偏向于码力检测……如果初学倍增的话思路不好想,而且更重要的是这题细节很多,很不好写。

先分析一下这道题吧!题目相当于在一棵树上,用给定的军队去把这棵树的所有子树全部占领。求最大移动距离的最小值。(哦这显然是二分答案)

首先每只军队一定是向上走的,如果在任意一棵子树上向下走,那不仅自己控制的节点数变少了,而且还会导致移动距离增加。这种情况下向上走一定是比向下走更优的。

只有一种情况军队是需要向下走的,那就是在一开始的时候,有些子树上是没有军队的,这时我们就必须把其他字数上的军队先调到根节点上,再向下走一步以控制子树。注意这里一定是向下只走一步,走多了绝对是亏的。(也就是说,只有根节点的儿子是需要军队驻扎的,如果这棵子树原来没军队)

既然如此,那我们的思路就比较明确了,先二分答案,之后把所有军队都向上调动,如果到不了根节点就让它停在那里并且将该点设为已占领,否则就先把所有的军队全部堆在根。之后我们先dfs一遍已确定根节点有几个儿子是需要重新派军驻扎的,之后依次用军队去覆盖儿子。注意这里的军队调动,我们可以把军队剩余可移动距离和儿子的距离都从大到小排序,之后理想的方法就是用移动距离远的军队覆盖距离最远的儿子,不过还有更好的方法,就是派本来就是从这棵子树上上来的军队去覆盖。这样的话,我们在军队上移的时候记录一下对于每棵子树,从这棵子树上到根节点的军队之中,剩余距离最小的一支军队。这样如果这只军队当时是可用的那就直接使用,否则就用移动距离最远的军队去覆盖,不能覆盖就返回。

这个事基本就差不多了,然后就模拟军队上移的过程,不过直接模拟会超时,所以要预处理一下之后使用倍增模拟。(看到模拟,弱小的码力又开始打怵了……)

具体的思路都说好了,直接上代码看一下,有详细的注释(不然我觉得很难看懂因为细节过多)

(以后写题一定要想好咋处理再写……debug的痛苦……)

#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring> 
#include<algorithm>
#include<cctype>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')
using namespace std;
typedef long long ll;
const ll INF = 5e5 + 5;
const int M = 1e5 + 5;
ll read()
{
    ll ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-') op = -1;
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        ans *= 10;
        ans += ch - '0';
        ch = getchar();
    }
    return ans * op;
}
struct mil
{
    ll id,res;
    bool operator < (const mil &other)const//重载运算符从大到小排序
    {
        return res > other.res;
    }
}a[M],b[M];
struct node
{
    ll next,to,v;
}e[M<<1];
ll d,n,m,x,y,z,ecnt,army[M],l,r,mid,dep[M],f[M],fa[M][25],dis[M][25],head[M],s;
ll rest1[M],rest2[M],tot1,sp[M],tot2,restbj[M],restmin[M],now,ans = -1,used[M];
void add(int x,int y,int z)
{
    e[++ecnt].to = y;
    e[ecnt].v = z;
    e[ecnt].next = head[x];
    head[x] = ecnt;
}
void dfs(ll x,ll y)//第一次dfs,确定每个节点的直接父亲和到直接父亲的距离
{
    for(int i = head[x];i;i = e[i].next)
    {
        ll p = e[i].to;
        if(p == y) continue;//如果是父亲就继续
        fa[p][0] = x;
        dis[p][0] = e[i].v;
        dfs(p,x);
    }
}
void init()
{
    rep(j,1,17)
    {
        rep(i,1,n)//预处理从某个点向上移动2^j个节点所到的位置和走过的距离
        {
            fa[i][j] = fa[fa[i][j-1]][j-1];
            dis[i][j] = dis[i][j-1] + dis[fa[i][j-1]][j-1]; 
        }
    }
}
bool dfs1(int x,int y)
{
    bool lea = 1,pd = 1; 
    if(sp[x]) return 1;//若已被封锁直接返回 
    for(int i = head[x];i;i = e[i].next)
    {
        int to = e[i].to;
        if(to == y) continue;//如果是父亲就返回 
        lea = 0;//说明此节点非叶节点 (叶节点只有父亲能走到就回去了)
        if(!dfs1(to,x))
        {
            pd = 0;//说明存在未封锁的点 
            if(x == 1)//只有在根节点的儿子我们才有必要设防 
            {
                b[++tot2].res = e[i].v;
                b[tot2].id = e[i].to;
            }
            else return 0; //否则直接返回
        }
    }
    if(lea) return 0;//如果是叶节点且没被占就说明没被占 
    else return pd; //看此点是否被封锁
}
bool check(int x)
{
    memset(restbj,0,sizeof(restbj));
    memset(used,0,sizeof(used));
    memset(sp,0,sizeof(sp));
    tot1 = tot2 = 0;
    rep(i,1,m)//全军上提 
    {    
        s = army[i],d = x; 
        per(j,17,0)
        {
            if(fa[s][j] > 1 && dis[s][j] <= d)
            {
                d -= dis[s][j];
                s = fa[s][j];
            }
        }
        if(fa[s][0] == 1 && dis[s][0] <= d)//如果父亲是根而且距离够用
        {
            a[++tot1].res = d - dis[s][0];//这里要另外记录因为军队下一次还要用 
            a[tot1].id = i;//记录是哪只军队 
            if(!restbj[s] || a[tot1].res < restmin[s])
            {
                restmin[s] = a[tot1].res;//更新最小距离以进行下一次比较
                restbj[s] = i;//记录离这棵子树距离最小的军队 
            }
        } 
        else sp[s] = 1;//这个节点视为已被军队占领 
    }
    if(dfs1(1,0)) return 1;//如果全被封锁了直接返回
    sort(a+1,a+1+tot1);sort(b+1,b+1+tot2);//按剩余距离从大到小排序 
    now = 1;used[0] = 1;
    rep(i,1,tot2)
    {
        if(!used[restbj[b[i].id]])//如果离这棵子树距离最小的军队没被占用 
        {
            used[restbj[b[i].id]] = 1;//视为它已被占用,继续 
            continue;
        }
        while(now <= tot1&&(used[a[now].id] || a[now].res < b[i].res))now++;//否则使用剩余距离最大的军队来覆盖它
        //前提是还有剩余的军队而且此军队没被使用,距离足够 
        if(now > tot1) return 0;//如果军队不够则失败 
        used[a[now].id] = 1;//把本次用于覆盖的军队设为已用过 
    }
    return 1;
}
int main()
{
    n = read();
    rep(i,1,n-1)
    {
        x = read(),y = read(),z = read();
        add(x,y,z);
        add(y,x,z);
    }
    dfs(1,0);init();
    m = read();rep(i,1,m) army[i] = read();
    l = 0,r = INF;
    while(l < r)
    {
        mid = l+r >> 1;//二分答案判定即可
        if(check(mid)) r = mid,ans = mid;
        else l = mid+1;
//        printf("$%lld\n",ans);
    }
    printf("%lld\n",ans);
    return 0;
}
/*
10
2 1 3
2 3 4
1 4 7
5 1 9
6 1 2
4 7 9
7 8 8
9 8 8
1 10 2
5
2 8 5 4 2 
*/

 

posted @ 2018-07-27 00:17  CaptainLi  阅读(893)  评论(0编辑  收藏  举报