树上的动态规划--算法竞赛入门经典笔记

918 阅读9分钟

本文为该书的笔记:刘汝佳. 算法竞赛入门经典.第 2 版[M]. 清华大学出版社, 2014.

在图论中,树是一种无向图( undirected graph ),其中任意两个顶点间存在唯一一条路径。或者说,只要没有回路的连通图就是树。

树的定义:

如果一个无向简单图 G 满足以下相互等价的条件之一,那么 G 是一棵树:

  • G 是没有回路的连通图。
  • G 没有回路,但是在 G 内添加任意一条边,就会形成一个回路。
  • G 是连通的,但是如果去掉任意一条边,就不再连通。
  • G 是连通的,并且 3 顶点的完全图 K_{3} 不是 G 的子图。
  • G 内的任意两个顶点能被唯一路径所连通。

如果无向简单图 G 有有限个顶点(设为 n 个顶点),那么 G 是一棵树还等价于:

  • G 是连通的,有 n − 1 条边,并且 G 没有简单回路。 如果一个无向简单图 G 中没有简单回路,那么 G 是森林。

对于无根树,选定一个结点作为根,则树的层次就确定下来了。

树的最大独立集

对于一棵 n 个结点的无根树,选出尽量多的结点,使得任何两个结点 均不相邻(称为最大独立集),然后输入 n-1 条无向边,输出一个最大独立集(如果有多 解,则任意输出一组)。

状态: d(i) 表示以 i 为根结点的子树的最大独立集大小。 状态转移方程:

d(i)=max \left \{ 1+\sum_{j \in gs(i)}d(j),\sum_{j \in s(i)} d(j)\right \}

其中, j \in gs(i) 表示 ji 的孙子, j \in s(i) 表示 ji 的儿子。

树的重心(质心)

对于一棵 n 个结点的无根树,找到一个点,使得把树变成以该点为 根的有根树时,最大子树的结点数最小。换句话说,删除这个点后最大连通块(一定是树) 的结点数最小。

状态: d(i) 表示以 i 为根结点的子树的结点个数。 状态转移方程:

d(i)=\sum_{j \in s(i)} d(j)+1

删除结点 i 后,最大的连通块的结点个数: 结点 i 的子树中最大的有 max \left \{ d(j) \right \} 个结点。结点 i 的上方子树中有 n-d(i) 个结点。

树的最长路径(最远点对)

对于一棵 n 个结点的无根树,找到一条最长路径。换句话 说,要找到两个点,使得它们的距离最远。

先把无根树变成有根树,对于任意结点 i,经过结点 i 的最长路就是连接结点 i 的两棵不同子树 u v 的最深叶子的路径。
状态: d(i) 表示结点 i 的子树中根到叶子的最长距离。
状态转移方程:

d(i)=max \left \{ d(j) \right \}+1

最终结果: d(u)+d(v)+2

工人的请愿书(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

状态: d(u) 表示让 u 给上级发信最少需要多少个工人
状态转移: 加入 uk 个子结点,则至少需要 c=kT/100 ,但是计算 c=kT/100 的时候会向下取整,所以可能就会取少了一个。所以个人认为应该是 c=k-(100-T)*k/100,但是尚未明白书上为什么写 c=(kT-1)/100+1
完整程序:

#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 )个人形成一个树状结构,即除了老板之外每个员工都有唯一的直属 上司。要求选尽量多的人,但不能同时选择一个人和他的直属上司。问:最多能选多少人, 以及在人数最多的前提下方案是否唯一。

该题是树的最大独立集问题。
状态: d(u,0)f(u,0) 分别表示在以 u 为根结点的子树时不用 u 时的最大独立集和是否有唯一解, 1 表示是, 0 表示否。 d(u,1)f(u,1) 分别表示在以 u 为根结点的子树时使用 u 时的最大独立集和是否有唯一解, 1 表示是, 0 表示否。 状态转移方程:

  • d(u,1)=1+sum{d(v,0)|v 是 u 的子结点} 当选择了 u , u 的子结点必须都不能选。当且仅当所有 f(v,0)=1f(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 。 d(u,i) 是以 u 为根结点的子树的最少服务器数量

  1. d(u,0)u 是服务器,则每个子结点可以是服务器,也可以不是。
  2. d(u,1)u 不是服务器,但是 u 的父亲是服务器,则每个子结点都必须不是服务器。
  3. d(u,2)u 不是服务器, u 的父亲也不是服务器,则其子结点有且仅有一个服务器。

状态转移方程:

d(u,0)=1+\sum_{v \in s(u)} min \left \{ d(v,0),d(v,1) \right \}
d(u,1)=\sum_{v \in s(u)}  d(v,2)
d(u,2)=min \left \{ \sum_{w \in s(u)}^{w \not = v} d(w,2) +d(v,0)\right \}
=min \left\{ d(u,1)-d(v,2)+d(v,0) \right \}
=d(u,1)+min \left\{ -d(v,2)+d(v,0) \right \}

完整程序:

#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 (深度优先搜索)方法将树存储在一个不定长数组里面,然后从后往前递推。