本文为该书的笔记:刘汝佳. 算法竞赛入门经典.第 2 版[M]. 清华大学出版社, 2014.
在图论中,树是一种无向图( undirected graph ),其中任意两个顶点间存在唯一一条路径。或者说,只要没有回路的连通图就是树。
树的定义:
如果一个无向简单图 G 满足以下相互等价的条件之一,那么 G 是一棵树:
- G 是没有回路的连通图。
- G 没有回路,但是在 G 内添加任意一条边,就会形成一个回路。
- G 是连通的,但是如果去掉任意一条边,就不再连通。
- G 是连通的,并且 3 顶点的完全图
不是 G 的子图。
- G 内的任意两个顶点能被唯一路径所连通。
如果无向简单图 G 有有限个顶点(设为 n 个顶点),那么 G 是一棵树还等价于:
- G 是连通的,有 n − 1 条边,并且 G 没有简单回路。 如果一个无向简单图 G 中没有简单回路,那么 G 是森林。
对于无根树,选定一个结点作为根,则树的层次就确定下来了。
树的最大独立集
对于一棵 n 个结点的无根树,选出尽量多的结点,使得任何两个结点 均不相邻(称为最大独立集),然后输入 n-1 条无向边,输出一个最大独立集(如果有多 解,则任意输出一组)。
状态: 表示以
为根结点的子树的最大独立集大小。
状态转移方程:
其中, 表示
是
的孙子,
表示
是
的儿子。
树的重心(质心)
对于一棵 n 个结点的无根树,找到一个点,使得把树变成以该点为 根的有根树时,最大子树的结点数最小。换句话说,删除这个点后最大连通块(一定是树) 的结点数最小。
状态: 表示以
为根结点的子树的结点个数。
状态转移方程:
删除结点 后,最大的连通块的结点个数:
结点
的子树中最大的有
个结点。结点
的上方子树中有
个结点。
树的最长路径(最远点对)
对于一棵 n 个结点的无根树,找到一条最长路径。换句话 说,要找到两个点,使得它们的距离最远。
先把无根树变成有根树,对于任意结点 i,经过结点 的最长路就是连接结点
的两棵不同子树
的最深叶子的路径。
状态: 表示结点
的子树中根到叶子的最长距离。
状态转移方程:
最终结果:
工人的请愿书(UVa12186)
简略描述:
某公司里有一个老板和 n ( n ≤ 105 )个员工组成树状结构,除了老板之外每个员工都有唯 一的直属上司。老板的编号为 0 ,员工编号为 1 ~ n 。工人们(即没有直接下属的员工)打算 签署一项请愿书递给老板,但是不能跨级递,只能递给直属上司。当一个中级员工(不是工 人的员工)的直属下属中不小于 T%的人签字时,他也会签字并且递给他的直属上司。问: 要让公司老板收到请愿书,至少需要多少个工人签字?
样例:
Sample Input
3 100
0 0 0
3 50
0 0 0
14 60
0 0 1 1 2 2 2 5 7 5 7 5 7 5
0 0
Sample Output
3
2
5
状态: 表示让
给上级发信最少需要多少个工人
状态转移:
加入 有
个子结点,则至少需要
,但是计算
的时候会向下取整,所以可能就会取少了一个。所以个人认为应该是
,但是尚未明白书上为什么写
。
完整程序:
#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring>
#include <string>
#include <math.h>
#include <vector>
using namespace std;
const int maxn = 100000 + 3;
vector<int> sons[maxn];
int n, T;
int tt;
int dp(int u)
{
int ans = 0;
int qq;
int k = sons[u].size();
if (k == 0)
return 1;
int c = k - (100 - T) * k / 100;
vector<int> d;
for (int i = 0; i < k; i++)
{
qq = dp(sons[u][i]);
d.push_back(qq);
}
sort(d.begin(), d.end());
for (int i = 0; i < c; i++)
{
ans += d[i];
}
return ans;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif // LOCAL
while (cin >> n >> T && n && T)
{
for (int i = 0; i <= n; i++)
{
sons[i].clear();
}
for (int i = 1; i <= n; i++)
{
cin >> tt;
sons[tt].push_back(i);
}
cout << dp(0) << endl;
}
return 0;
}
注:该解法中选择不定长数组 vector 来存储树。
Hali-Bula 的晚会(UVa1220)
简略描述:
公司里有 n ( n ≤ 200 )个人形成一个树状结构,即除了老板之外每个员工都有唯一的直属 上司。要求选尽量多的人,但不能同时选择一个人和他的直属上司。问:最多能选多少人, 以及在人数最多的前提下方案是否唯一。
该题是树的最大独立集问题。
状态: 、
分别表示在以
为根结点的子树时不用
时的最大独立集和是否有唯一解, 1 表示是, 0 表示否。
、
分别表示在以
为根结点的子树时使用
时的最大独立集和是否有唯一解, 1 表示是, 0 表示否。
状态转移方程:
d(u,1)=1+sum{d(v,0)|v 是 u 的子结点}
当选择了 u , u 的子结点必须都不能选。当且仅当所有f(v,0)=1
时f(u,1)=1
d(u,0)=0+sum{max(d(v,0),d(v,1))|v 是 u 的子结点}
当 u 没有选, u 的子结点可以被选择,也可以不被选择。什么情况下方案唯一?(1)如果某个 d(v,0)和 d(v,1)相等,则不唯一。(2)如果 max 取到的那个值对应的 f=0 ,方案也不唯一。 完整程序:
#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring>
#include <string>
#include <math.h>
#include <vector>
#include <map>
using namespace std;
const int maxn = 200 + 3;
int d[maxn][2], f[maxn][2];
vector<int> sons[maxn];
int n;
int cnt;
map<string, int> dict; ////使用 map 查看该结点是否被录入,以确定该结点是否已经设置 ID
//根据姓名确定结点编号
int ID(const string &s)
{
//如果该名字之前没有被编号
if (!dict.count(s))
dict[s] = cnt++;
return dict[s];
}
/** \brief 以 u 为根结点的子树时选或者不选 u 时的最大独立集
*
* \param u 子树根结点编号
* \param b 0 表示不选 u 点, 1 表示选 u 点
* \return 该子树该情况下的最大独立集
*
*/
int dp(int u, int b)
{
int &dd = d[u][b];
dd = b;
int &ff = f[u][b];
ff = 1;
//u 子结点的个数
int k = sons[u].size();
if (k == 0)
{
dd = b;
ff = 1;
return dd;
}
if (b)
{
for (int i = 0; i < k; i++)
{
dd += dp(sons[u][i], 0);
ff = ff && f[sons[u][i]][0];
}
}
else
{
for (int i = 0; i < k; i++)
{
dd += max(dp(sons[u][i], 0), dp(sons[u][i], 1));
if (d[sons[u][i]][0] > d[sons[u][i]][1] && f[sons[u][i]][0] == 0)
{
ff = 0;
}
else if (d[sons[u][i]][0] < d[sons[u][i]][1] && f[sons[u][i]][1] == 0)
{
ff = 0;
}
else if (d[sons[u][i]][0] == d[sons[u][i]][1])
{
ff = 0;
}
}
}
//帮助调试
// cout << u << " " << b << endl;
// cout << " " << dd << " " << ff << endl;
return dd;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif // LOCAL
string s, s2;
while (cin >> n && n)
{
int uniq = 1;
cnt = 0;
cin >> s;
//清空变量
dict.clear();
for (int i = 0; i <= n; i++)
{
sons[i].clear();
}
ID(s);
for (int i = 1; i < n; i++)
{
cin >> s >> s2;
sons[ID(s2)].push_back(ID(s));
}
int ans1 = max(dp(0, 0), dp(0, 1));
cout << ans1 << " ";
if (d[0][0] > d[0][1] && f[0][0] == 0)
{
uniq = 0;
}
else if (d[0][0] < d[0][1] && f[0][1] == 0)
{
uniq = 0;
}
else if (d[0][0] == d[0][1])
{
uniq = 0;
}
if (uniq)
{
cout << "Yes" << endl;
}
else
{
cout << "No" << endl;
}
}
return 0;
}
完美的服务(UVa1218)
简略描述:
有 n ( n ≤ 10000 )台机器形成树状结构。要求在其中一些机器上安装服务器,使得每台不 是服务器的计算机恰好和一台服务器计算机相邻。求服务器的最少数量。
状态:
一共有三种状态,但是之前固定思维,只考虑了 0 和 1 。 是以
为根结点的子树的最少服务器数量
:
是服务器,则每个子结点可以是服务器,也可以不是。
:
不是服务器,但是
的父亲是服务器,则每个子结点都必须不是服务器。
:
不是服务器,
的父亲也不是服务器,则其子结点有且仅有一个服务器。
状态转移方程:
完整程序:
#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring>
#include <string>
#include <math.h>
#include <vector>
#include <map>
using namespace std;
const int maxn = 10000 + 3;
const int INF = 1 << 30;
int d[maxn][3];
vector<int> G[maxn], sons[maxn];
int n;
/** \brief 从图整理为树
*
* \param a 结点编号
* \param b a 结点的父亲
* \return
*
*/
void G2tree(int a, int b)
{
int s = G[a].size();
for (int i = 0; i < s; i++)
{
if (b != G[a][i])
{
sons[a].push_back(G[a][i]);
G2tree(G[a][i], a);
}
}
}
// 防止加法溢出
int plus1(int a, int b)
{
if (a == INF || b == INF)
{
return INF;
}
return a + b;
}
//防止减法溢出
int sub1(int a, int b)
{
if (a == INF || b == INF)
{
return INF;
}
return a - b;
}
int dp(int u, int c)
{
int &ans = d[u][c];
if (ans != -1)
{
return ans;
}
int ans1 = 0;
int s = sons[u].size();
switch (c)
{
case 0:
ans = 1;
for (int i = 0; i < s; i++)
{
ans = plus1(ans, min(dp(sons[u][i], 0), dp(sons[u][i], 1)));
}
break;
case 1:
ans = 0;
for (int i = 0; i < s; i++)
{
ans = plus1(ans, dp(sons[u][i], 2));
}
break;
case 2:
ans = INF;
for (int i = 0; i < s; i++)
{
// 在 d[i][2]溢出的情况下使用其他算法
if (dp(sons[u][i], 2) == INF)
{
ans1 = dp(sons[u][i], 0);
for (int j = 0; j < s; j++)
{
if (j == i)
continue;
ans1 = plus1(ans1, dp(sons[u][j], 2));
}
ans = min(ans, ans1);
continue;
}
ans = min(ans,
plus1(
sub1(dp(u, 1), dp(sons[u][i], 2)), dp(sons[u][i], 0)));
}
break;
}
// cout << u << " " << c << " " << ans << endl;
return ans;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif // LOCAL
int a, b;
while (cin >> n && n > 0)
{
memset(d, -1, sizeof(d));
for (int i = 0; i <= n; i++)
{
sons[i].clear();
G[i].clear();
}
while (cin >> a && a > 0)
{
cin >> b;
a--;
b--;
G[a].push_back(b);
G[b].push_back(a);
}
G2tree(0, -1);
// for (int i = 0; i <= n; i++)
// {
// for (int j = 0; j < sons[i].size(); j++)
// {
// cout << sons[i][j] << " ";
// }
// cout << endl;
// }
// cout << "------------" << endl;
cout << min(dp(0, 0), dp(0, 2)) << endl;
}
return 0;
}
该程序通过改变加减法方式来防止溢出。
也可以通过设置适当的 INF 值,然后判断执行加减法之后是否大于 INF 来防止溢出。
该方式需要保证 INF*2
不会溢出。
if(d[u][0] > INF) d[u][0] = INF; // avoid overflow!
if(d[u][1] > INF) d[u][1] = INF;
以上程序也可以看做是树的深度优先搜索算法,所以所以可以先按照 DFS (深度优先搜索)方法将树存储在一个不定长数组里面,然后从后往前递推。