【NOIP 2018】Day2 T3 保卫王国
Problem
Description
Z 国有\(n\)座城市,\(n - 1\)条双向道路,每条双向道路连接两座城市,且任意两座城市 都能通过若干条道路相互到达。
Z 国的国防部长小 Z 要在城市中驻扎军队。驻扎军队需要满足如下几个条件:
- 一座城市可以驻扎一支军队,也可以不驻扎军队。
- 由道路直接连接的两座城市中至少要有一座城市驻扎军队。
- 在城市里驻扎军队会产生花费,在编号为\(i\)的城市中驻扎军队的花费是\(p_i\)。
小 Z 很快就规划出了一种驻扎军队的方案,使总花费最小。但是国王又给小 Z 提出 了\(m\)个要求,每个要求规定了其中两座城市是否驻扎军队。小 Z 需要针对每个要求逐一 给出回答。具体而言,如果国王提出的第\(j\)个要求能够满足上述驻扎条件(不需要考虑 第 \(j\) 个要求之外的其它要求),则需要给出在此要求前提下驻扎军队的最小开销。如果 国王提出的第j个要求无法满足,则需要输出\(-1~~(1 ≤ j ≤ m)\)。现在请你来帮助小 Z。
Input Format
第 1 行包含两个正整数\(n,m\)和一个字符串\(type\),分别表示城市数、要求数和数据类型。\(type\)是一个由大写字母 \(A\),\(B\) 或 \(C\) 和一个数字 1,2,3 组成的字符串。它可以帮助你获得部分分。你可能不需要用到这个参数。这个参数的含义在【数据规模与约定】中 有具体的描述。
第 2 行\(n\)个整数\(p_i\),表示编号\(i\)的城市中驻扎军队的花费。
接下来 \(n - 1\) 行,每行两个正整数\(u,v\),表示有一条\(u\)到\(v\)的双向道路。
接下来 \(m\) 行,第\(j\)行四个整数\(a,x,b,y(a ≠ b)\),表示第\(j\)个要求是在城市\(a\)驻扎\(x\)支军队, 在城市\(b\)驻扎\(y\)支军队。其中,\(x\) , \(y\)的取值只有 0 或 1:若 \(x\) 为 0,表示城市 \(a\) 不得驻 扎军队,若 \(x\) 为 1,表示城市 \(a\) 必须驻扎军队;若 \(y\)为 0,表示城市 \(b\)不得驻扎军队, 若 \(y\)为 1,表示城市 \(b\) 必须驻扎军队。
输入文件中每一行相邻的两个数据之间均用一个空格分隔。
Output Format
输出共 \(m\) 行,每行包含 1 个整数,第\(j\)行表示在满足国王第\(j\)个要求时的最小开销, 如果无法满足国王的第\(j\)个要求,则该行输出 -1。
Sample
Input
5 3 C3
2 4 1 3 9
1 5
5 2
5 3
3 4
1 0 3 0
2 1 3 1
1 0 5 0
Output
12
7
-1
Explanation
Explanation for Input
对于第一个要求,在 4 号和 5 号城市驻扎军队时开销最小。
对于第二个要求,在 1 号、2 号、3 号城市驻扎军队时开销最小。
第三个要求是无法满足的,因为在 1 号、5 号城市都不驻扎军队就意味着由道路直接连 接的两座城市中都没有驻扎军队。
Range
对于全部数据,\(n=m\le 10^5,1\le p_i\le 10^5\)。
测试点编号 | $\text{type}$ | $n=$ |
---|---|---|
$1,2$ | A3 | $10$ |
$3,4$ | C3 | |
$5,6$ | A3 | $100$ |
$7$ | C3 | |
$8,9$ | A3 | $2\times 10^3$ |
$10,11$ | C3 | |
$12,13$ | A1 | $10^5$ |
$14\sim 16$ | A2 | |
$17$ | A3 | |
$18,19$ | B1 | |
$20,21$ | C1 | |
$22$ | C2 | |
$23\sim 25$ | C3 |
数据类型的含义:
A:城市 \(i\) 与城市 \(i+1\) 直接相连。
B:任意城市与城市 \(1\) 的距离不超过 \(100\)(距离定义为最短路径上边的数量),即如果这棵树以 \(1\) 号城市为根,深度不超过 \(100\)。
C:在树的形态上无特殊约束。
1:询问时保证 \(a=1,x=1\),即要求在城市 \(1\) 驻军。对 \(b,y\) 没有限制。
2:询问时保证 \(a,b\) 是相邻的(由一条道路直接连通)
3:在询问上无特殊约束。
Algorithm
倍增,树形DP
Mentality
首先看到数据范围为 \(1e5\),那么发现 \(n^2\) 算法是不可能过的,不难想到算法是 \(nlogn\) 的。那么接着看到题面,我们会发现一件事,那就是每个询问只影响两个点,而通过树形\(dp\)的过程我们可以知道一件事情:除了一个询问影响的两个点以外的点的\(dp\)过程其实都是没有变化的,或者说,除去这两个点,当做整棵树里没有它们以及它们的子树,只考虑其他点的最优 \(dp\) 值还是不变!
那么不难想到,我们处理出除了这两个点以外的 \(dp\) 值,再单独处理这两个点不就行了吗?可是不能直接处理,时间空间都会爆炸。那么你想到了什么吗?倍增!联赛每年必考的倍增不见了!那么题目不就清晰明朗起来了吗?
\(f[i][0/1]\)表示选或不选\(i\)结点,选择以\(i\)为根的子树所有结点的最优值。\(g[i][0/1]\)表示的是选或不选\(i\)结点,整棵树除了以\(i\)为根的子树以外全部选择的最优值。然后呢?当然就是倍增!设\(fa\)为\(i\)的\(2^j\)祖先,\(dp[i][j][0/1][0/1]\)表示的是\(i\)结点选或不选,\(i\)的\(2^j\)祖先\(fa\)选或不选,以\(fa\)为根的子树内独不选以\(i\)为根的子树的最优值!换句话来说,也可以说是 \(i\) 结点相对于它的 \(2^j\) 祖先结点为根的树的另一个 \(g\) 数组。
那么处理出了倍增数组之后呢?我们这样做:对于两个询问的点不断向上跳并统计\(dp\)值,如果其中一个是另一个的祖先结点,那么这一个的答案就可以直接输出。否则继续向上统计答案,直到到达\(lca\)处。为什么跳到这种位置就行了呢?因为修改两点的影响范围是有限的,对于范围之外的点它们的\(dp\)值并不会受到影响!
具体怎么跳?对于当前点\(x\)来说,记录它的子树最优值\(tx[1/0]\),代表当前点选或不选的子树最优值。那么对于初始修改点,\(tx\)数组的两个数里只会有一个是有值的,另一个设为\(inf\)即可。另一个点\(y\)自然同理。
至于判断 \(-1\)?\(emm...\)如果修改的两点是相连的并且修改值都为 \(0\) 呗......
Code
这个地方我分开写伐。
f 数组:
void DP(int x)
{
f[x][1]=w[x];
for(int i=head[x];i;i=nx[i])
if(to[i]!=fa[x])
{
int p=to[i];
DP(p);
f[x][1]+=min(f[p][0],f[p][1]);
f[x][0]+=f[p][1];
}
}
g 数组:
void D_P(int x)
{
for(int i=head[x];i;i=nx[i])
if(to[i]!=fa[x])
{
int p=to[i];
g[p][0]=g[x][1]+f[x][1]-min(f[p][0],f[p][1]);
g[p][1]=min(g[x][0]+f[x][0]-f[p][1],g[p][0]);
D_P(p);
}
}
重点- dp 数组:
for (int i = 1; i <= n; i++) {
jump[i][0] = fa[i];
dp[i][0][0][0] = 2e15;
dp[i][0][0][1] =
f[fa[i]][1] -
min(f[i][0],
f[i]
[1]); //这里你可能会疑惑:为什么是减去min?因为这并不是别的,而是减去f数组DP上来的时候贡献的DP值
dp[i][0][1][0] = f[fa[i]][0] - f[i][1];
dp[i][0][1][1] = f[fa[i]][1] - min(f[i][0], f[i][1]);
}
for (int j = 1; j <= 17; j++)
for (int i = 1; i <= n; i++) {
jump[i][j] = jump[jump[i][j - 1]][j - 1];
for (int k = 0; k <= 1; k++)
for (int l = 0; l <= 1; l++) {
dp[i][j][k][l] = 2e15;
for (int q = 0; q <= 1; q++)
dp[i][j][k][l] =
min(dp[i][j][k][l],
dp[i][j - 1][k][q] + dp[jump[i][j - 1]][j - 1][q][l]);
}
}
关键-向上跳:
long long work() {
先跳到同一深度
if (deep[a] < deep[b]) {
swap(a, b);
swap(c, d);
}
tx,ty分别代表a,b跳到当前点的最优值,nx,ny代表下一步转移的值 long long
tx[2] = {inf, inf},
ty[2] = {inf, inf}, nx[2], ny[2];
tx[c] = f[a][c], ty[d] = f[b][d];
for (int i = 17; i >= 0; i--)
if (deep[jump[a][i]] >= deep[b]) {
nx[0] = nx[1] = inf;
for (int j = 0; j <= 1; j++)
for (int k = 0; k <= 1; k++) nx[j] = min(nx[j], tx[k] + dp[a][i][k][j]);
tx[0] = nx[0], tx[1] = nx[1];
a = jump[a][i];
}
if (a == b)
return tx[d] + g[b][d]; //如果b为a的祖先,那么直接返回DP最优值+子树外最优值
a,b一起跳 for (int i = 17; i >= 0; i--) if (jump[a][i] != jump[b][i]) {
nx[0] = nx[1] = inf;
ny[0] = ny[1] = inf;
for (int j = 0; j <= 1; j++)
for (int k = 0; k <= 1; k++) {
nx[j] = min(nx[j], tx[k] + dp[a][i][k][j]);
ny[j] = min(ny[j], ty[k] + dp[b][i][k][j]);
}
tx[0] = nx[0], tx[1] = nx[1];
ty[0] = ny[0], ty[1] = ny[1];
a = jump[a][i];
b = jump[b][i];
}
int lca = fa[a];
判断返回值哪个更优
return min(f[lca][0] - f[a][1] - f[b][1] + tx[1] + ty[1] + g[lca][0],
f[lca][1] - min(f[a][0], f[a][1]) - min(f[b][0], f[b][1]) +
min(tx[0], tx[1]) + min(ty[0], ty[1]) + g[lca][1]);
展开写法:
long long sum1 =
min(f[lca][0] - f[a][1] - f[b][1] + tx[1] + ty[1] +
g[lca][0]); //减去原本DP值并加上跳过来的最优值再加上子树外最优值。
long long sum2 =
min(f[lca][1] - min(f[a][0], f[a][1]) - min(f[b][0], f[b][1]) +
min(tx[0], tx[1]) + min(ty[0], ty[1])) +
g[lca][1]; //减去原本DP值加上当前最优加上子树外最优
}
总代码:
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
int n, m, nx[200001], to[200001], head[100001], a, b, c, d;
int fa[200001], deep[200001], w[100001], jump[100001][18];
long long f[100001][2], g[100001][2], dp[100001][18][2][2], inf = 2e15;
char type[2];
void add(int u, int v, int d) {
nx[d] = head[u];
to[d] = v;
head[u] = d;
}
void build(int x) {
for (int i = head[x]; i; i = nx[i])
if (to[i] != fa[x]) {
fa[to[i]] = x;
deep[to[i]] = deep[x] + 1;
build(to[i]);
}
}
void DP(int x) {
f[x][1] = w[x];
if (x == a) f[x][(c + 1) % 2] = inf;
if (x == b) f[x][(d + 1) % 2] = inf;
for (int i = head[x]; i; i = nx[i])
if (to[i] != fa[x]) {
int p = to[i];
DP(p);
f[x][1] += min(f[p][0], f[p][1]);
f[x][0] += f[p][1];
}
}
void D_P(int x) {
for (int i = head[x]; i; i = nx[i])
if (to[i] != fa[x]) {
int p = to[i];
g[p][0] = g[x][1] + f[x][1] - min(f[p][0], f[p][1]);
g[p][1] = min(g[x][0] + f[x][0] - f[p][1], g[p][0]);
D_P(p);
}
}
long long work() {
if (deep[a] < deep[b]) {
swap(a, b);
swap(c, d);
}
long long tx[2] = {inf, inf}, ty[2] = {inf, inf}, nx[2], ny[2];
tx[c] = f[a][c], ty[d] = f[b][d];
for (int i = 17; i >= 0; i--)
if (deep[jump[a][i]] >= deep[b]) {
nx[0] = nx[1] = inf;
for (int j = 0; j <= 1; j++)
for (int k = 0; k <= 1; k++) nx[j] = min(nx[j], tx[k] + dp[a][i][k][j]);
tx[0] = nx[0], tx[1] = nx[1];
a = jump[a][i];
}
if (a == b) return tx[d] + g[b][d];
for (int i = 17; i >= 0; i--)
if (jump[a][i] != jump[b][i]) {
nx[0] = nx[1] = inf;
ny[0] = ny[1] = inf;
for (int j = 0; j <= 1; j++)
for (int k = 0; k <= 1; k++) {
nx[j] = min(nx[j], tx[k] + dp[a][i][k][j]);
ny[j] = min(ny[j], ty[k] + dp[b][i][k][j]);
}
tx[0] = nx[0], tx[1] = nx[1];
ty[0] = ny[0], ty[1] = ny[1];
a = jump[a][i];
b = jump[b][i];
}
int lca = fa[a];
return min(f[lca][0] - f[a][1] - f[b][1] + tx[1] + ty[1] + g[lca][0],
f[lca][1] - min(f[a][0], f[a][1]) - min(f[b][0], f[b][1]) +
min(tx[0], tx[1]) + min(ty[0], ty[1]) + g[lca][1]);
}
int main() {
cin >> n >> m >> type;
int u, v;
for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
for (int i = 1; i < n; i++) {
scanf("%d%d", &u, &v);
add(u, v, i);
add(v, u, i + n);
}
deep[1] = 1;
build(1);
DP(1);
D_P(1);
for (int i = 1; i <= n; i++) {
jump[i][0] = fa[i];
dp[i][0][0][0] = 2e15;
dp[i][0][0][1] = f[fa[i]][1] - min(f[i][0], f[i][1]);
dp[i][0][1][0] = f[fa[i]][0] - f[i][1];
dp[i][0][1][1] = f[fa[i]][1] - min(f[i][0], f[i][1]);
}
for (int j = 1; j <= 17; j++)
for (int i = 1; i <= n; i++) {
jump[i][j] = jump[jump[i][j - 1]][j - 1];
for (int k = 0; k <= 1; k++)
for (int l = 0; l <= 1; l++) {
dp[i][j][k][l] = 2e15;
for (int q = 0; q <= 1; q++)
dp[i][j][k][l] =
min(dp[i][j][k][l],
dp[i][j - 1][k][q] + dp[jump[i][j - 1]][j - 1][q][l]);
}
}
for (int i = 1; i <= m; i++) {
scanf("%d%d%d%d", &a, &c, &b, &d);
if ((fa[a] == b && c == 0 && d == 0) || (fa[b] == a && c == 0 && d == 0)) {
cout << "-1\n";
continue;
}
cout << work() << endl;
}
}