[HNOI2003]消防局的设立

13 篇文章 0 订阅
9 篇文章 0 订阅

题目描述

2020 年,人类在火星上建立了一个庞大的基地群,总共有 nn 个基地。起初为了节约材料,人类只修建了 n-1n−1 条道路来连接这些基地,并且每两个基地都能够通过道路到达,所以所有的基地形成了一个巨大的树状结构。如果基地 AA 到基地 BB 至少要经过 dd 条道路的话,我们称基地A到基地B的距离为 dd。

由于火星上非常干燥,经常引发火灾,人类决定在火星上修建若干个消防局。消防局只能修建在基地里,每个消防局有能力扑灭与它距离不超过 22 的基地的火灾。

你的任务是计算至少要修建多少个消防局才能够确保火星上所有的基地在发生火灾时,消防队有能力及时扑灭火灾。

输入格式

输入文件的第一行为 nn(1 \leq n \leq 10001≤n≤1000),表示火星上基地的数目。接下来的 n-1n−1 行每行有一个正整数,其中文件第 ii 行的正整数为 a_iai​,表示从编号为 ii 的基地到编号为 a_iai​ 的基地之间有一条道路,为了更加简洁的描述树状结构的基地群,有 a_i\lt iai​<i。

输出格式

仅有一个正整数,表示至少要设立多少个消防局才有能力及时扑灭任何基地发生的火灾。

输入输出样例

输入 #1

6
1
2
3
4
5

输出 #1

2

1.贪心+STL+dfs

考虑当前深度最大的叶子结点,你肯定要有一个消防局去覆盖它,

那么既然他是叶子结点,所以与他距离小于等于2的节点有这么几种:

  1. 他的父亲 2. 他的兄弟 3. 他的爷爷

容易看出,在前两项能够覆盖到的节点,在爷爷那里设立一定也能覆盖到。

所以每次贪心取出深度最大的节点,在他的爷爷哪里放一个消防站

用STL的priority_queue,时间复杂度O(nlogn)

#include<bits/stdc++.h>
#define maxn 1010
#define maxm 2010
using namespace std;

int head[maxn],point[maxm],nxt[maxm],dep[maxn],fa[maxn],tot=0;
bool vis[maxn];
struct cmp{
    bool operator () (int &a,int &b){
        return dep[a]<dep[b];
    } 
};
priority_queue<int,vector<int>,cmp> q;
void add(int x,int y){
    point[++tot]=y;
    nxt[tot]=head[x];
    head[x]=tot;
}
void dfs(int temp,int father,int depth){
    fa[temp]=father;
    dep[temp]=depth;
    for(int j=head[temp];j;j=nxt[j]){
        if(point[j]==father)
            continue;
        dfs(point[j],temp,depth+1);
    }
}
void dfs2(int temp,int depth){
    if(depth>2)
        return;
    vis[temp]=true;
    for(int j=head[temp];j;j=nxt[j])
        dfs2(point[j],depth+1);
}
int main(){
    int n,cnt,x,y,ans=0;
    scanf("%d",&n),cnt=n;
    for(int i=1;i<=n-1;i++)
        scanf("%d",&x),add(i+1,x),add(x,i+1);
    dfs(1,0,1);
    for(int i=1;i<=n;i++)
        q.push(i);
    while(q.size()){
        while(q.size()&&vis[x=q.top()])
            q.pop();
        if(!q.size())
            break;
        if(fa[fa[x]])
            dfs2(fa[fa[x]],0);
        else
            dfs2(1,0);
        ans++; 
    }
    printf("%d\n",ans);
    return 0;
} 

2.dp+链表

状态表示

每一个点xx都有五个状态:

f[x][0]:覆盖到x的爷爷和x整棵子树(向上2层),最少个数

f[x][1]:覆盖到x的父亲和x整棵子树(向上1层),最少个数

f[x][2]:覆盖x整棵子树(向上0层),最少个数

f[x][3]:覆盖所有x的儿子及其子树(向上-1层),最少个数

f[x][4]:覆盖所有x的孙子及其子树(向上-2层),最少个数


  • f[x][0]:覆盖到x的爷爷和x整棵子树(向上2层),最少个数

  • f[x][1]:覆盖到x的父亲和x整棵子树(向上1层),最少个数

  • f[x][2]:覆盖x整棵子树(向上0层),最少个数

  • f[x][3]:覆盖所有x的儿子及其子树(向上-1层),最少个数

  • f[x][4]:覆盖所有x的孙子及其子树(向上-2层),最少个数

转移方程

y,z是x的儿子

f[x][0] = 1 + \sum{f[y][4]}f[x][0]=1+∑f[y][4] f[x][1] = min(f[y][0] + \sum{(y!=z)f[z][3]})f[x][1]=min(f[y][0]+∑(y!=z)f[z][3]) f[x][2] = min(f[y][1] + \sum{(y!=z)f[z][2]})f[x][2]=min(f[y][1]+∑(y!=z)f[z][2]) f[x][3] = \sum{f[y][2]}f[x][3]=∑f[y][2] f[x][4] = \sum{f[y][3]}f[x][4]=∑f[y][3]

显然f[x][i]一定包含f[x][i+1]

易得f[x][0] >= f[x][1] >= f[x][2] >= f[x][3] >= f[x][4]

所以转移时保证满足条件的前提下尽量选最低层的状态


  • f[x][0] = 1 + \sum{f[y][4]}f[x][0]=1+∑f[y][4]

要覆盖到爷爷的话必须选x,并贪心地选y的第五种状态

  • f[x][1] = min(f[y][0] + \sum{(y!=z)f[z][3]})f[x][1]=min(f[y][0]+∑(y!=z)f[z][3])

x的儿子中有一个一定覆盖的爷爷,同时覆盖到兄弟(因为y一定是选了),其他的儿子只需要覆盖的自己的儿子即可

  • f[x][2] = min(f[y][1] + \sum{(y!=z)f[z][2]})f[x][2]=min(f[y][1]+∑(y!=z)f[z][2])

同理,有一个儿子覆盖到父亲,但无法覆盖到y的兄弟,所以其他儿子要覆盖到自己

  • f[x][3] = \sum{f[y][2]}f[x][3]=∑f[y][2]

让每个儿子覆盖到自己即可

  • f[x][4] = \sum{f[y][3]}f[x][4]=∑f[y][3]

让每个儿子覆盖到自己的儿子


注意:

f[x][i]包含f[x][i+1],若f[x][i]f[x][i+1]更优,f[x][i+1]应更新

代码

#include <cstdio>
#include <cctype>
#include <algorithm>

using namespace std;

template<typename T> inline void read(T &x) {
	x = 0; T k = 1; char in = getchar();
	while (!isdigit(in)) { if (in == '-') k = -1; in = getchar(); }
	while (isdigit(in)) x = x * 10 + in - '0', in = getchar();
	x *= k;
}

const int N = 1005;

struct Edge {
    int nxt, to;
};

int n, cnt;
int head[N], f[N][5];
Edge e[N<<1];

inline void add(int u, int v) {
    e[++cnt] = (Edge){head[u], v}, head[u] = cnt;
}

void dfs(int x, int fa) {
    // 记录f[y][2], f[y][3]的总和,后面容斥即可
    int sum2 = 0, sum3 = 0, y, tot;
    for (int i = head[x]; i; i = e[i].nxt)
        if (e[i].to != fa) {
            y = e[i].to;
            dfs(y, x);
            sum2 += f[y][2], sum3 += f[y][3];
            tot++;
        }
    // 没有儿子特判
    if (!tot) {
        f[x][0] = f[x][1] = f[x][2] = 1;
        return;
    }
    f[x][0] = 1, f[x][1] = f[x][2] = N;
    for (int i = head[x]; i; i = e[i].nxt)
        if (e[i].to != fa) {
            y = e[i].to;
            f[x][0] += f[y][4];
            f[x][1] = min(f[x][1], f[y][0] + sum3 - f[y][3]);
            f[x][2] = min(f[x][2], f[y][1] + sum2 - f[y][2]);
            f[x][3] += f[y][2];
            f[x][4] += f[y][3];
        }
    // 检查最小值
    for (int i = 1; i < 5; ++i)
        f[x][i] = min(f[x][i], f[x][i-1]);
}

int main() {
    read(n);
    for (int i = 2, x; i <= n; ++i)
        read(x), add(x, i), add(i, x);
    dfs(1, 0);
    printf("%d\n", f[1][2]);
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值