P1041 [NOIP2003 提高组] 传染病控制

题目背景

本题是错题,后来被证明没有靠谱的多项式复杂度的做法。测试数据非常的水,各种玄学做法都可以通过,不代表算法正确。因此本题题目和数据仅供参考。


近来,一种新的传染病肆虐全球。蓬莱国也发现了零星感染者,为防止该病在蓬莱国大范围流行,该国政府决定不惜一切代价控制传染病的蔓延。不幸的是,由于人们尚未完全认识这种传染病,难以准确判别病毒携带者,更没有研制出疫苗以保护易感人群。于是,蓬莱国的疾病控制中心决定采取切断传播途径的方法控制疾病传播。经过 WHO(世界卫生组织)以及全球各国科研部门的努力,这种新兴传染病的传播途径和控制方法已经研究清楚,剩下的任务就是由你协助蓬莱国疾控中心制定一个有效的控制办法。

题目描述

研究表明,这种传染病的传播具有两种很特殊的性质;

第一是它的传播途径是树型的,一个人 �X 只可能被某个特定的人 �Y 感染,只要 �Y 不得病,或者是 ��XY 之间的传播途径被切断,则 �X 就不会得病。

第二是,这种疾病的传播有周期性,在一个疾病传播周期之内,传染病将只会感染一代患者,而不会再传播给下一代。

这些性质大大减轻了蓬莱国疾病防控的压力,并且他们已经得到了国内部分易感人群的潜在传播途径图(一棵树)。但是,麻烦还没有结束。由于蓬莱国疾控中心人手不够,同时也缺乏强大的技术,以致他们在一个疾病传播周期内,只能设法切断一条传播途径,而没有被控制的传播途径就会引起更多的易感人群被感染(也就是与当前已经被感染的人有传播途径相连,且连接途径没有被切断的人群)。当不可能有健康人被感染时,疾病就中止传播。所以,蓬莱国疾控中心要制定出一个切断传播途径的顺序,以使尽量少的人被感染。

你的程序要针对给定的树,找出合适的切断顺序。

输入格式

输入格式:
第一行是两个整数 �n 和 �p。
接下来 �p 行,每一行有 22 个整数 �i 和 �j,表示节点 �i 和 �j 间有边相连。(意即,第 �i 人和第 �j 人之间有传播途径相连)。其中节点 11 是已经被感染的患者。

输出格式

11 行,总共被感染的人数。

输入输出样例

输入 #1复制

7 6
1 2
1 3
2 4
2 5
3 6
3 7

输出 #1复制

3

说明/提示

对于 100%100% 的数据,1≤�≤3001≤n≤300。

【题目来源】

NOIP 2003 提高组第四题

如果你初学搜索,如何一步一步无伤A掉搜索真题?

让我们以初学者的角度走进改题;

警告:本篇题解面向初学者非最优解或非常规解法,神犇请绕道

做一道题首先需要的是逐步分析:

  1. 题目给定了一棵树, 树的节点 n <= 300 ,同理的变数p = n - 1; 或许是搜索?

  2. 从节点1开始传染,所谓切断一条路径, 不难转换为标记其中一颗子树

  3. 每个传染阶段为每一个已被标记“得病”的节点向下传染,传染次数在最坏情况下刚好为叶节点到1节点的距离; 或许是拓扑?

要寻找正确的解题方法总要进行不断的思考

首先先思考拓扑相关, 从末节点倒推是否是一种可行的方法?

事实证明这种方法在题目限制下是几乎不可行的;

(事实证明可以用来进行一定量的预处理)

那么对我来说就只有搜索一条路可以走;

然后是要求输出当传染人数最少的情况下的人数;

同理为未被传染人数最多时传染人数;

那么首先我们要建立相关的代码框架

1.先从输入开始:

值得一提的是输入的边并没有说明是父节点指向子节点或子节点指向父节点

应该想方法处理

2.dfs函数

不难得到,一个节点若要被传染,那么传染到该节点的第x个传染阶段一定是该节点到1节点的距离

接下来是我个人的想法:

显然以节点下标作为dfs传入参数来进行相关处理不太可行;

那么不如我们以距离1节点的距离x为传入参数

然后对所有距离1节点距离为x的节点进行处理

即为选择一颗子树进行切除, 然后进行下一层dfs

切除该子树要进行的操作为标记所有该子树上的节点并统计节点数量

同时要保证该子树的父节点为在前几层的递归中未被切除;

当无子树可以切除时dfs函数变走到尽头

然后进行思考一下回溯, 这个是较简单的,同理与标记

int clean(int i){
    bol[i] = true;
    int num = 1;
    int p = f[i].size();
    for (int j = 0; j < p; ++j){
        num += clean(f[i][j]);
    }
    return num;
}
void reclean(int i){
    bol[i] = false;
    int p = f[i].size();
    for (int j = 0; j < p; ++j){
        reclean(f[i][j]);
    }
}

每次进行一次切除就要便利一遍全部子节点显然缺乏效率;

但对于本题的数据范围来说还是可以接受的;

上文代码中用到了一个vector, 里面存的是该节点的子节点

那么回到题目头, 我们该如何处理保证f数组里面都为该节点的子节点呢?

用另外一个数组存入输入的所有边

简单套用一个最短路模板统计距离

然后将所有合法的边push入f即可;

为了要保证可以正常处理距离节点1距离为x的全部节点

也要进行简单的统计

void resolve(int i, int cen){
    b[cen][cnt[cen]] = i;
    ++cnt[cen];
    int p = k[i].size();
    for (int j = 0; j < p; ++j){
        if (dis[k[i][j]] == dis[i]+1){
            resolve(k[i][j], cen+1);
            f[i].push_back(k[i][j]);
        }
    }
}

最后统计得未被传染人数最多时的人数

输出节点总数减未被传染人数获得正解;

具体细节详见代码

本人AC代码:(327ms)

#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
#define LL long long
using namespace std;
int n, p, t1, t2, b[305][305], cnt[305], maxx, dis[305];
bool bol[305], vis[305];
vector <int> k[305], f[305];
struct node{
    int x, quan;
    node (int a, int b) : x(a), quan(b){
    }
    friend bool operator < (node a, node b){
        return a.quan > b.quan;
    }
};
int clean(int i){
    bol[i] = true;
    int num = 1;
    int p = f[i].size();
    for (int j = 0; j < p; ++j){
        num += clean(f[i][j]);
    }
    return num;
} //标记部分
void reclean(int i){
    bol[i] = false;
    int p = f[i].size();
    for (int j = 0; j < p; ++j){
        reclean(f[i][j]);
    }
} //回溯部分
void dfs(int cen, int tot){
    maxx = max(maxx, tot);
    for (int i = 0; i < cnt[cen]; ++i){
        if (!bol[b[cen][i]]){
            int num = clean(b[cen][i]);
            tot += num;
            dfs(cen+1, tot);
            reclean(b[cen][i]);
            tot -= num;
        }
    }
} //dfs核心函数
void resolve(int i, int cen){
    b[cen][cnt[cen]] = i;
    ++cnt[cen];
    int p = k[i].size();
    for (int j = 0; j < p; ++j){
        if (dis[k[i][j]] == dis[i]+1){
            resolve(k[i][j], cen+1);
            f[i].push_back(k[i][j]);
        }
    }
} //预处理第二部分
void solve(){
    priority_queue <node> que;
    for (int i = 0; i <= n; ++i) dis[i] = 999;
    dis[1] = 0;
    que.push(node(1, 0));
    while (!que.empty()){
        node temp = que.top();
        que.pop();
        int x = temp.x;
        int p = k[x].size();
        for (int j = 0; j < p; ++j){
            if (dis[k[x][j]] > dis[x]+1){
                dis[k[x][j]] = dis[x]+1;
                que.push(node(k[x][j], dis[k[x][j]]));
            }
        }
    }
    resolve(1, 0);
} //最短路算法进行预处理
//实际上以节点0开始进行拓扑排序效率更高
int main(){
    scanf("%d %d", &n, &p);
    for (int i = 0; i < p; ++i){
        scanf("%d %d", &t1, &t2);
        k[t1].push_back(t2);
        k[t2].push_back(t1);
    }
    solve();
    dfs(1, 0);
    printf("%d", n-maxx);
    //本人代码量命名较随意见谅pu~
}

那么,这道题就用最暴力却细腻的处理解决了;

代码亮点在哪里?要怎么写?

  1. 分模块处理,即使只是简单的处理或回溯也不妨单独多出来一个函数方便进行相关的调试

  2. 理解题意并尽快找到dfs函数所需要传入的参数(例如本题就要尽快从常规方法将节点序号作为传入参数中脱离出来寻找新的做法

  3. 如果无法找到相关的关系,不妨进行一定量的预处理(例如本题中输入边未指明是父节点连向子节点或子节点连向父节点,而我只需要父节点连向子节点的相关边,就需要进行预处理即为单源最短路

  4. 提高思维的深度, 拓宽思维的宽度

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值