标签: 树形DP

[CSP-S2 2019]括号树 题解

[CSP-S2 2019]括号树 题解

题目地址:洛谷:P5658 括号树 – 洛谷 | 计算机科学教育新生态 题目背 

[ZJOI2007]时态同步 题解

[ZJOI2007]时态同步 题解

题目地址:洛谷:【P1131】[ZJOI2007]时态同步 – 洛谷、BZOJ 

[SDOI2011]消耗战 题解

[SDOI2011]消耗战 题解

题目地址:洛谷:【P2495】[SDOI2011]消耗战 – 洛谷、BZOJ:Problem 2286. — [Sdoi2011]消耗战

题目描述

在一场战争中,战场由n个岛屿和n-1个桥梁组成,保证每两个岛屿间有且仅有一条路径可达。现在,我军已经侦查到敌军的总部在编号为1的岛屿,而且他们已经没有足够多的能源维系战斗,我军胜利在望。已知在其他k个岛屿上有丰富能源,为了防止敌军获取能源,我军的任务是炸毁一些桥梁,使得敌军不能到达任何能源丰富的岛屿。由于不同桥梁的材质和结构不同,所以炸毁不同的桥梁有不同的代价,我军希望在满足目标的同时使得总代价最小。
侦查部门还发现,敌军有一台神秘机器。即使我军切断所有能源之后,他们也可以用那台机器。机器产生的效果不仅仅会修复所有我军炸毁的桥梁,而且会重新随机资源分布(但可以保证的是,资源不会分布到1号岛屿上)。不过侦查部门还发现了这台机器只能够使用m次,所以我们只需要把每次任务完成即可。

题意简述

给你一棵包含$n$个点的带边权的树,有$m$次询问,每次给你一个大小为$k_i$的点集,你可以花费边权代价切断树上的一些边,使得这个点集中的点与树根$1$不连通,求满足上述条件的最小切边代价。

输入输出格式

输入格式:
第一行一个整数n,代表岛屿数量。
接下来n-1行,每行三个整数u,v,w,代表u号岛屿和v号岛屿由一条代价为c的桥梁直接相连,保证1<=u,v<=n且1<=c<=100000。
第n+1行,一个整数m,代表敌方机器能使用的次数。
接下来m行,每行一个整数ki,代表第i次后,有ki个岛屿资源丰富,接下来k个整数h1,h2,…hk,表示资源丰富岛屿的编号。

输出格式:
输出有m行,分别代表每次任务的最小代价。

输入输出样例

输入样例#1:

10
1 5 13
1 9 6
2 1 19
2 4 8
2 3 91
5 6 8
7 5 4
7 8 31
10 7 9
3
2 10 6
4 5 7 8 3
3 9 4 6

输出样例#1:

12
32
22

说明

【数据规模和约定】
对于10%的数据,2<=n<=10,1<=m<=5,1<=ki<=n-1
对于20%的数据,2<=n<=100,1<=m<=100,1<=ki<=min(10,n-1)
对于40%的数据,2<=n<=1000,m>=1,sigma(ki)<=500000,1<=ki<=min(15,n-1)
对于100%的数据,2<=n<=250000,m>=1,sigma(ki)<=500000,1<=ki<=n-1

题解

如果只有一次询问,我们可以进行一次DFS处理出$mn[u]$代表$u$点到$1$点链上的最小边,显然,断开它是切断$u$和$1$连接的最优方案。我们用$dp[u]$表示切断$u$子树中所有点与$1$的连接的最小代价,则转移为
$$ dp[u] = \sum_{v \in \mathrm{son}(u)} \min \{ dp[v], mn[v] \} $$
这个转移表示要么切断$v$到$u$的这条边,要么切断其子树内的边。DP初值设置为对于每一个点集里的点,$dp[u]$初始为$mn[u]$,这样在DP求和的时候,如果一个父亲自己也是点集中的点,权值也会被计算进去。显然,如果两个点到$1$路径上的最优边是同一条,那么一定可以在这条最优边处向上转移时只计算一次权值,这是由于$\min$的存在。
然而这个算法是$O(n)$的,也就是说,总复杂度为$O(mn)$,并不能通过本题。
我们考虑建立虚树解决这个问题。虚树是指一棵只包含指定点即它们两两间LCA的辅助用树,这样做可以减少树上点的数量,降低DP的复杂度至$O(k_i \log k_i)$。
具体而言,如果我们处理出树的DFS序,那么对DFS序相邻的两点求LCA,一定可以得到所有点两两的LCA,这是由于,在不同子树内的点的LCA可以在处理到子树DFS序区间端点之间的LCA的时候被求出来。但是,LCA和儿子的边的关系并不明确,直接建边似乎是不明智的选择。我们考虑一种特殊的树的遍历序:欧拉序,它是指DFS遍历时,进入DFS栈的时候将该点加入序列,出栈时也加入一次。如果对于上面的所有点及其LCA的集合,分别插入出入栈点用欧拉序排序,就可以利用这一顺序来模拟DFS了,省去了建边的过程。
模拟DFS的时候,只需要在出栈的时候进行操作,这是因为,此时出栈点$u$子树的DP已经计算完毕,只需要去更新它父亲的DP值即可,而出栈后的栈顶元素就是它的父亲,直接按照上面的方程去转移即可。
使用欧拉序的虚树算法的复杂度是单次$O(n \log n)$的,达到这个复杂度的操作是查询LCA和排序。这里的$n$指的是虚树的节点数,它的规模是$O(k_i \log k_i)$的。
如此,总复杂度终于变得科学了不少。不过本题有点卡常,稍微卡一卡也可以卡进去。

代码

// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>

#include <algorithm>
#include <vector>

typedef long long LL;

inline LL min(LL a, LL b) {
    return a < b ? a : b;
}

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
        ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1; register char c = fgc();
    for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
    for(; isdigit(c); c = fgc()) res = res * 10 + c - '0';
    return res * neg;
}

const int MAXN = 500005;

int n, m;

struct Edge {
    int to, w;
};

std::vector<Edge> gra[MAXN];

int anc[MAXN][20], mn[MAXN], dep[MAXN], dfn[MAXN], clk;

void dfs(int u, int fa) {
    dfn[u] = ++clk;
    for(int i = 0; i < gra[u].size(); i++) {
        int v = gra[u][i].to;
        if(v == fa) continue;
        anc[v][0] = u;
        dep[v] = dep[u] + 1;
        mn[v] = std::min(mn[u], gra[u][i].w);
        dfs(v, u);
    }
    dfn[u + n] = ++clk;
}

inline void calanc() {
    for(int i = 1; i <= 19; i++) {
        for(int j = 1; j <= n; j++) {
            anc[j][i] = anc[anc[j][i - 1]][i - 1];
        }
    }
}

inline int querylca(int u, int v) {
    if(dep[u] > dep[v]) std::swap(u, v);
    int del = dep[v] - dep[u];
    for(int i = 19; i >= 0; i--) {
        if(del & (1 << i)) v = anc[v][i];
    }
    if(u == v) return u;
    for(int i = 19; i >= 0; i--) {
        if(anc[u][i] != anc[v][i]) {
            u = anc[u][i];
            v = anc[v][i];
        }
    }
    return anc[u][0];
}

inline bool cmp(int a, int b) {
    return dfn[a] < dfn[b];
}

LL dp[MAXN];
bool vis[MAXN];

std::vector<int> vec;
int sta[MAXN], stop;

int main() {
    n = readint();
    for(int i = 1, u, v, w; i < n; i++) {
        u = readint(); v = readint(); w = readint();
        gra[u].push_back(Edge {v, w});
        gra[v].push_back(Edge {u, w});
    }
    mn[1] = 1e9;
    dfs(1, 0);
    calanc();
    m = readint();
    while(m--) {
        vec.clear();
        int k = readint();
        for(int i = 1; i <= k; i++) {
            int t = readint();
            vis[t] = true; dp[t] = mn[t]; vec.push_back(t);
        }
        std::sort(vec.begin(), vec.end(), cmp);
        for(int i = 1; i < k; i++) {
            int l = querylca(vec[i - 1], vec[i]);
            if(!vis[l]) {
                vis[l] = true;
                vec.push_back(l);
            }
        }
        if(!vis[1]) vec.push_back(1);
        for(int i = 0; vec[i] <= n; i++) {
            vec.push_back(vec[i] + n);
        }
        std::sort(vec.begin(), vec.end(), cmp);
        for(int i = 0; i < vec.size(); i++) {
            if(vec[i] <= n) {
                sta[stop++] = vec[i];
            } else {
                int u = sta[--stop];
                if(u != 1) {
                    int fa = sta[stop - 1]; dp[fa] += min(mn[u], dp[u]);
                } else {
                    printf("%lld\n", dp[u]);
                }
                dp[u] = 0; vis[u] = false;
            }
        }
    }
    return 0;
}
[ZJOI2008]骑士 题解

[ZJOI2008]骑士 题解

题目地址:洛谷:【P2607】[ZJOI2008]骑士 – 洛谷、BZOJ:P 

[APIO2014]连珠线 题解

[APIO2014]连珠线 题解

题目地址:洛谷:【P3647】[APIO2014]连珠线 – 洛谷、BZOJ: 

[HAOI2010]软件安装 题解

[HAOI2010]软件安装 题解

题目地址:洛谷:【P2515】[HAOI2010]软件安装 – 洛谷、BZOJ:Problem 2427. — [HAOI2010]软件安装

题目描述

现在我们的手头有N个软件,对于一个软件i,它要占用Wi的磁盘空间,它的价值为Vi。我们希望从中选择一些软件安装到一台磁盘容量为M计算机上,使得这些软件的价值尽可能大(即Vi的和最大)。
但是现在有个问题:软件之间存在依赖关系,即软件i只有在安装了软件j(包括软件j的直接或间接依赖)的情况下才能正确工作(软件i依赖软件j)。幸运的是,一个软件最多依赖另外一个软件。如果一个软件不能正常工作,那么它能够发挥的作用为0。
我们现在知道了软件之间的依赖关系:软件i依赖软件Di。现在请你设计出一种方案,安装价值尽量大的软件。一个软件只能被安装一次,如果一个软件没有依赖则Di=0,这时只要这个软件安装了,它就能正常工作。

输入输出格式

输入格式:
第1行:N, M (0<=N<=100, 0<=M<=500)
第2行:W1, W2, … Wi, …, Wn (0<=Wi<=M )
第3行:V1, V2, …, Vi, …, Vn (0<=Vi<=1000 )
第4行:D1, D2, …, Di, …, Dn (0<=Di<=N, Di≠i )

输出格式:
一个整数,代表最大价值

输入输出样例

输入样例#1:

3 10
5 5 6
2 3 4
0 1 1

输出样例#1:

5

题解

i依赖Di可以表示成图上的有向边(Di, i),而由于强连通分量内部两两互达,一个安装了其他的一定都要被安装,因此可以缩点考虑。缩完点以后的图是一个有向树,考虑找到所有入度为0的点,从虚拟树根0向它连边,DFS跑一遍树形背包,答案就是dp[0][m]。
该树形背包是该点选择才能够继续选择子树,所以默认该点一定被选择,对于点u所有的dp[u][i](i≥wu)都要被初始化成vu。树形背包的转移方程如下
\displaystyle dp[i][j+w_u] = \max_{(u, v) \in E}\{ dp[u][j+w_u-k] + dp[v][k] \} \ (k \leq j)
枚举从子树选取多少体积的东西转移而来,比较容易理解。

代码

// Code by KSkun, 2018/5
#include <cstdio>
#include <cctype>

#include <algorithm>
#include <vector>
#include <set>
#include <stack>
#include <utility>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    register char c = fgc();
    while(!isdigit(c)) {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(isdigit(c)) {
        res = (res << 1) + (res << 3) + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 105;

int n, m;

int w[MAXN], v[MAXN], wn[MAXN], vn[MAXN], degn[MAXN];
std::vector<int> gra[MAXN], gran[MAXN];
std::set<std::pair<int, int> > edges;

int dfn[MAXN], low[MAXN], sno[MAXN], clk, scc;
std::stack<int> sta;
bool insta[MAXN];

inline void tarjan(int u) {
    dfn[u] = low[u] = ++clk;
    insta[u] = true; sta.push(u);
    for(int i = 0; i < gra[u].size(); i++) {
        int v = gra[u][i];
        if(!dfn[v]) {
            tarjan(v);
            low[u] = std::min(low[u], low[v]);
        } else if(insta[v]) {
            low[u] = std::min(low[u], dfn[v]);
        }
    }
    if(low[u] == dfn[u]) {
        scc++;
        int p;
        do {
            p = sta.top(); sta.pop(); insta[p] = false;
            sno[p] = scc;
            wn[scc] += w[p];
            vn[scc] += v[p];
        } while(p != u);
    }
}

int dp[MAXN][505];

inline void dfs(int u) {
    for(int i = wn[u]; i <= m; i++) {
        dp[u][i] = vn[u];
    }
    for(int i = 0; i < gran[u].size(); i++) {
        int v = gran[u][i];
        dfs(v);
        for(int j = m - wn[u]; j >= 0; j--) {
            for(int k = 0; k <= j; k++) {
                dp[u][j + wn[u]] = std::max(dp[u][j + wn[u]], dp[u][j + wn[u] - k] + dp[v][k]);
            }
        }
    }
}

int di;

int main() {
    n = readint(); m = readint();
    for(int i = 1; i <= n; i++) {
        w[i] = readint();
    }
    for(int i = 1; i <= n; i++) {
        v[i] = readint();
    }
    for(int i = 1; i <= n; i++) {
        di = readint(); if(!di) continue;
        gra[di].push_back(i);
    }
    for(int i = 1; i <= n; i++) {
        if(!dfn[i]) tarjan(i);
    }
    for(int u = 1; u <= n; u++) {
        for(int i = 0; i < gra[u].size(); i++) {
            int v = gra[u][i];
            if(sno[u] != sno[v] && !edges.count(std::make_pair(sno[u], sno[v]))) {
                gran[sno[u]].push_back(sno[v]); degn[sno[v]]++;
                edges.insert(std::make_pair(sno[u], sno[v]));
            }
        }
    } 
    for(int i = 1; i <= scc; i++) {
        if(!degn[i]) gran[0].push_back(i);
    }
    dfs(0);
    printf("%d", dp[0][m]);
    return 0;
}
[CQOI2009]叶子的染色 题解

[CQOI2009]叶子的染色 题解

题目地址:洛谷:【P3155】[CQOI2009]叶子的染色 – 洛谷、BZO 

[SCOI2016]背单词 题解

[SCOI2016]背单词 题解

题目地址:洛谷:【P3294】[SCOI2016]背单词 – 洛谷、BZOJ: 

[洛谷1273]有线电视网 题解

[洛谷1273]有线电视网 题解

题目地址:【P1273】有线电视网 – 洛谷

题目描述

某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。
从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。
现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。
写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。

输入输出格式

输入格式:
输入文件的第一行包含两个用空格隔开的整数N和M,其中2 \leq N \leq 30001 \leq M \leq N-1,N为整个有线电视网的结点总数,M为用户终端的数量。
第一个转播站即树的根结点编号为1,其他的转播站编号为2到N-M,用户终端编号为N-M+1到N。
接下来的N-M行每行表示—个转播站的数据,第i+1行表示第i个转播站的数据,其格式如下:

K A1 C1 A2 C2 … Ak Ck

K表示该转播站下接K个结点(转播站或用户),每个结点对应一对整数A与C,A表示结点编号,C表示从当前转播站传输信号到结点A的费用。最后一行依次表示所有用户为观看比赛而准备支付的钱数。

输出格式:
输出文件仅一行,包含一个整数,表示上述问题所要求的最大用户数。

输入输出样例

输入样例#1:

5 3
2 2 2 5 3
2 3 2 4 3
3 4 2

输出样例#1:

2

说明

样例解释
pic1
如图所示,共有五个结点。结点①为根结点,即现场直播站,②为一个中转站,③④⑤为用户端,共M个,编号从N-M+1到N,他们为观看比赛分别准备的钱数为3、4、2,从结点①可以传送信号到结点②,费用为2,也可以传送信号到结点⑤,费用为3(第二行数据所示),从结点②可以传输信号到结点③,费用为2。也可传输信号到结点④,费用为3(第三行数据所示),如果要让所有用户(③④⑤)都能看上比赛,则信号传输的总费用为:2+3+2+3=10,大于用户愿意支付的总费用3+4+2=9,有线电视网就亏本了,而只让③④两个用户看比赛就不亏本了。

解题思路

显然是一个树形DP,但是每个子树怎么处理也就成了问题。对于每一个终端用户,决策是选或不选,可以看成一个树上的01背包,因此设计状态为dp[u][i]表示节点u给i个用户提供服务的利润,转移方程如下
dp[u][i+k] = max\{dp[u][i+k], dp[u][i] + dp[v][k] - w[u][v]\}
其中,v是u的子节点,k是一个枚举的量,表示从子节点选择多少用户提供服务。依照01背包的顺序枚举即可。最后的答案在dp[1][i]中,取dp值非负的i最大值即可。

代码

// Code by KSkun, 2017/12
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>

struct io {
    char buf[1 << 26], *s;

    io() {
        fread(s = buf, 1, 1 << 26, stdin);
    }

    inline int read() {
        register int res = 0;
        while(*s < '0' || *s > '9') s++;
        while(*s >= '0' && *s <= '9') res = res * 10 + *s++ - '0';
        return res;
    }
};

io ip;

#define read ip.read 

struct Edge {
    int to, w;
    Edge(int to, int w): to(to), w(w) {}
};

std::vector<Edge> vec[3005];

inline void addedge(int u, int v, int w) {
    vec[u].push_back(Edge(v, w));
    vec[v].push_back(Edge(u, w));
} 

int n, m, w[3005], kt, at, ct, fa[3005], siz[3005], dp[3005][3005];

inline void dfs(int u) {
    dp[u][0] = 0;
    if(vec[u].size() == 1) {
        siz[u] = 1;
        dp[u][1] = w[u];
        return;
    }
    for(int i = 0; i < vec[u].size(); i++) {
        int v = vec[u][i].to;
        if(v == fa[u]) continue;
        fa[v] = u;
        dfs(v);
        for(int j = siz[u]; j >= 0; j--) {
            for(int k = siz[v]; k >= 0; k--) {
                dp[u][j + k] = std::max(dp[u][j + k], dp[u][j] + dp[v][k] - vec[u][i].w);
            }
        }
        siz[u] += siz[v];
    }
}

int main() {
    memset(dp, 0xc0, sizeof dp);
    n = read();
    m = read();
    for(int i = 1; i <= n - m; i++) {
        kt = read();
        for(int j = 1; j <= kt; j++) {
            at = read();
            ct = read();
            addedge(i, at, ct);
        }
    }
    for(int i = n - m + 1; i <= n; i++) {
        w[i] = read();
    }
    dfs(1);
    for(int i = m; i >= 0; i--) {
        if(dp[1][i] >= 0) {
            printf("%d", i);
            return 0;
        }
    }
    return 0;
}