题目
S 公司的网络管理员琳收到了一通紧急来电。来电告知公司内部设备连接的网络中发现了恶意代码,必须马上采取相应措施。
S 公司有 N 台设备,通过 M 根线路连接,所以如果其中一台设备感染恶意代码,其他设备也会通过连接网络感染上恶意代码。目前,已有 K 台设备感染恶意代码,代码扩散危机迫在眉睫。受感染的设备需要立即处理,但在目前的情况下,最多只能处理三台设备。
保受感染的设备数量尽可能少的情况下,帮助琳确定要处理的设备,同时求出未受感染设备的最大数量。
如上图所示,有 10 台设备连接,编号为 2、4、5、8 的设备感染了恶意代码,处理编号为 2、4、5 的设备,就可以保护 7 台设备,这是能实现的不受感染设备的最大数量。
[限制条件]
1.设备数量N为5以上,100,000以下的整数。
2.线路数量 M 为1以上,300,000以下的整数。
3.受感染的设备数K为3以上,N以下的整数。
4.受感染的设备即使经过处理后,仍然可能会再次被其他受感染的设备感染。
[输入]
首先给出测试用例数量T,随后给出T个测试用例。在每个测试用例的第一行,给定设备数量 N,线路数量 M、感染恶意代码的设备数量 K,以空格分隔。从第二行到第M行,每行给定一条线路两端连接的设备 A 和 B 的编号,以空格分隔。在第 M+2 行,给定 K 台感染恶意代码的设备编号,以空格分隔。
[输出]
每行输出一个测试用例。首先,输出“#x”(其中 x 表示测试用例编号,从 1 开始),加一个空格,最后输出处理三台设备后,恶性代码扩散时,能实现的未感染设备的最大数量。
[输入和输出示例]
(输入)
2
10 9 4
1 2
2 3
2 5
4 5
4 6
5 6
5 7
8 9
9 10
2 4 5 8
10 9 5
1 2
2 3
2 5
4 5
4 6
5 6
5 7
8 9
9 10
2 4 5 6 8
(输出)
#1 7
#2 3
思路
求出每个连通分量的点个数以及坏点个数。
然后用零一背包求满足坏点个数的连通分量点个数和的最大值(0个坏点的,是必须选的)。
代码
import java.io.*;
import java.util.*;
public class Main {
static int N, M, K;
static ArrayList<Integer>[] DATA; // 链表数组,用于记录每个点的直接连接点
static boolean[] VISITED; // 已访问标记数组
static boolean[] IS_BAD; // 坏点记录数组
static ArrayList<int[]> DP_DATA; // 背包使用数组
static long ASW;
static int CNT; // 已访问点的数量
public static void main(String[] args) throws IOException {
System.setIn(new FileInputStream("D:\\SW\\TestCase\\sample_input_20210712.txt"));
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringTokenizer st = new StringTokenizer(br.readLine());
int T = Integer.parseInt(st.nextToken()); // Case数量
for (int t = 1; t <= T; t++) {
st = new StringTokenizer(br.readLine());
N = Integer.parseInt(st.nextToken()); // 点数量
M = Integer.parseInt(st.nextToken()); // 边数量
K = Integer.parseInt(st.nextToken()); // 坏点数量
DATA = new ArrayList[N + 1]; // 初始化链表数组,用于记录每个点的直接连接点
VISITED = new boolean[N + 1]; // 初始化已访问标记数组
IS_BAD = new boolean[N + 1]; // 初始化坏点记录数组
DP_DATA = new ArrayList<int[]>(); // 初始化背包使用数组
ASW = 0; // 初始化最终结果
CNT = 0; // 初始化已访问点的数量
for (int i = 1; i <= N; i++) {
DATA[i] = new ArrayList<Integer>(); // 初始化每个点的连接点
}
Arrays.fill(VISITED, false);
Arrays.fill(IS_BAD, false);
// 读入边(连接)信息,并记录
for (int i = 0; i < M; i++) {
st = new StringTokenizer(br.readLine());
int s = Integer.parseInt(st.nextToken());
int e = Integer.parseInt(st.nextToken());
DATA[s].add(e);
DATA[e].add(s);
}
// 读入坏点信息,并记录
st = new StringTokenizer(br.readLine());
for (int i = 0; i < K; i++) {
int tmp = Integer.parseInt(st.nextToken());
IS_BAD[tmp] = true;
}
// BFS求出每个连通分量节点及其中的坏点个数
for (int i = 1; i <= N; i++) {
if (CNT == N) // 若每个点都已经被访问过,则所有连通分量已经求出
break;
if (VISITED[i]) // 若当前点被访问过,则剪枝
continue;
bfs(i); // 当前点尚未被访问到过,则新发现一个联通分量,BFS求连通分量节点及其中的坏点个数
}
// 方案一:零一背包求最优解
getPackage();
// 方案二:
// 最多只能修复3个坏点,“3”这个数字是固定的
// 那么只有三种情况:
// 3 = 1 + 1 + 1,3 = 1 + 2,3 = 3
// 也就是说,有0个坏点的,是必须计算的(Sum(有0个坏点的点数量最多的3个连通分量))
// 再加上另外的三种情况取最大值;
// 即:
// 情况1(3 = 1 + 1 + 1):Sum(只有一个坏点的点数量最多的3个连通分量)
// 情况2(3 = 1 + 2):Sum(只有一个坏点的点数量最多的1个连通分量,有两个坏点的点数量最多的1个连通分量)
// 情况3(3 = 3):Sum(有三个坏点的点数量最多的1个连通分量)
bw.write("#" + t + " " + ASW + "\n");
bw.flush();
}
bw.close();
}
static void getPackage() {
int[] dp = new int[4];
for (int i = 0; i < DP_DATA.size(); i++) {
int[] tmp = DP_DATA.get(i);
for (int j = 3; j >= tmp[0]; j--) {
dp[j] = Math.max(dp[j - tmp[0]] + tmp[1], dp[j]);
}
}
ASW = dp[3];
}
static void bfs(int s) {
Queue<Integer> pq = new ArrayDeque<Integer>();
pq.add(s);
VISITED[s] = true;
int tmpCnt = 0; // 当前连通分量节点数量
int tmp = 0; // 当前连通分量坏点数量
while (!pq.isEmpty()) {
int curr = pq.poll();
tmpCnt += 1;
CNT++;
if (IS_BAD[curr])
tmp += 1;
for (int i = 0; i < DATA[curr].size(); i++) {
int next = DATA[curr].get(i);
if (VISITED[next])
continue;
VISITED[next] = true;
pq.add(next);
}
}
// 1.如果tmp>3,则此连通图的顶点个数为0
// 2.如果tmp<=3,则最大联通图的顶点为tmp<=3,tmp为0 1 2 3 的加总取最大
if (tmp <= 3) {
DP_DATA.add(new int[] { tmp, tmpCnt });
}
}
}