问题描述
给你一个n个点m条边的无向无环图,在尽量少的节点上放灯,使得所有灯都被照亮。每盏灯将照亮以它为一个端点的所有边。在灯的总数最小的前提下,被两盏灯同时照亮的边数应尽量大。
输入格式
输入的第一行为测试数据组数T(T≤30)。每组数据第一行为两个整数n和m(m<n≤1000),即点数(所有点编号为0~n-1)和边数;以下m行每行为两个不同的整数a和b,表示有一条边连接a和b(0≤a,b≤n)。
输出格式
对于每组数据,输出3个整数,即灯的总数,被2个灯照亮的边数和只被一个灯照亮的边数。
分析
因为是无向无环图(即森林),所以这道题是树上的动态规划。首先,要先知道一点:对于2个变量a,b,如果要在a最小的前提下,求b的最小值,可以求x=M*a+b的最小值,那么最后答案即为min(a)=x/M,min(b)=x%M,其中M值要大于a的理论最大值与b的理论最小值之差。道理很简单,当M足够大的时候,a决定整个式子的值,而b的值对式子的值不会造成多大的影响,只有当a值不变时,b才能对式子的值造成影响。
那么,对于这道题来说,有2个待优化的条件:1、灯的总数a最小;2、在1的前提下,被2个灯照亮的边数b最大。一个最大,一个最小,显然不方便优化,所以在上面结论的提示下,考虑将第二个条件等价为被1个灯照亮的边数c最小(一条边只能同时被一盏或者2盏灯照亮),这时候就可以直接套用上面的结论了。设x=M*a+c,这里M可以取2000,或者更大,但是要注意不能太大,否则在运算时可能会造成数据溢出。
分析到这里,动态规划的状态方程也不难想了,因为对于每个节点i来说,只有2种方案,放灯或者不放灯,但是节点i放灯与否与它的父节点也有关系,所以设d(i,j)为节点i的父节点j是否放灯(j值为1是放灯,0是不放)的最小x值。下面分别对2种方案进行分析:
1、节点i不放灯。那么i的父节点必须放灯(j=1)或者i本身是根节点。此时d(i,j)=sum{d(k,0)|k取遍i的所有子节点},如果i不是根,还要加上1,因为节点i与其父节点这条边上只有1盏灯
2、节点i放灯。此时d(i,j)=sum{d(k,1)|k取遍i的所有子节点}+M,同样的,如果j=0,且i不是根,还得加上1,因为节点i与其父节点这条边上只有1盏灯。
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> adj[1010];
int vis[1010][2], d[1010][2], n, m;
int dp(int i, int j, int f) {
if (vis[i][j])
return d[i][j];
vis[i][j] = 1;
int& ans = d[i][j];
ans = 2000;
for (int k = 0; k < adj[i].size(); k++) {
if (adj[i][k] != f) {
ans += dp(adj[i][k], 1, i);
}
}
if (!j&&f >= 0)
ans++;
if (j || f < 0) {
int sum = 0;
for (int k = 0; k < adj[i].size(); k++)
if (adj[i][k] != f)
sum += dp(adj[i][k], 0, i);
if (f >= 0)
sum++;
ans = min(ans, sum);
}
return ans;
}
int main() {
int T, a, b;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) {
adj[i].clear();
}
for (int i = 0; i < m; i++) {
scanf("%d%d", &a, &b);
adj[a].push_back(b);
adj[b].push_back(a);
}
memset(vis, 0, sizeof(vis));
int ans = 0;
for (int i = 0; i < n; i++) {
if (!vis[i][0])
ans += dp(i, 0, -1);
}
printf("%d %d %d\n", ans / 2000, (m - ans) % 2000, ans % 2000);
}
return 0;
}