P5024 保卫王国
题目描述
Z 国有 \(n\) 座城市,\((n−1)\) 条双向道路,每条双向道路连接两座城市,且任意两座城市都能通过若干条道路相互到达。
Z 国的国防部长小 Z 要在城市中驻扎军队。驻扎军队需要满足如下几个条件:
- 一座城市可以驻扎一支军队,也可以不驻扎军队。
- 由道路直接连接的两座城市中至少要有一座城市驻扎军队。
- 在城市里驻扎军队会产生花费,在编号为 \(i\) 的城市中驻扎军队的花费是 \(p_i\)。
小 Z 很快就规划出了一种驻扎军队的方案,使总花费最小。但是国王又给小 Z 提出了 \(m\) 个要求,每个要求规定了其中两座城市是否驻扎军队。小 Z 需要针对每个要求逐一给出回答。具体而言,如果国王提出的第 \(j\) 个要求能够满足上述驻扎条件(不需要考虑第 \(j\) 个要求之外的其它要求),则需要给出在此要求前提下驻扎军队的最小开销。如果国王提出的第 \(j\) 个要求无法满足,则需要输出 −1。现在请你来帮助小 Z。
输入格式
第一行有两个整数和一个字符串,依次表示城市数 \(n\),要求数 \(m\) 和数据类型 \(type\)。\(type\) 是一个由大写字母 A
,B
或 C
和一个数字 1
,2
,3
组成的字符串。它可以帮助你获得部分分。你可能不需要用到这个参数。这个参数的含义在【数据规模与约定】中 有具体的描述。
第二行有 \(n\) 个整数,第 \(i\) 个整数表示编号 \(i\) 的城市中驻扎军队的花费 \(p_i\)。
接下来 \((n−1)\) 行,每行两个整数 \(u\), \(v\) 表示有一条 \(u\) 到 \(v\) 的双向道路。
接下来 \(m\) 行,每行四个整数 \(a,x,b,y\),表示一个要求是在城市 \(a\) 驻扎 \(x\) 支军队,在城市 \(b\) 驻扎 \(y\) 支军队。其中,\(x,y\) 的取值只有 \(0\) 或 \(1\):
- 若 \(x\) 为 \(0\),表示城市 \(a\) 不得驻扎军队。
- 若 \(x\) 为 \(1\),表示城市 \(a\) 必须驻扎军队。
- 若 \(y\) 为 \(0\),表示城市 \(b\) 不得驻扎军队。
- 若 \(y\) 为 \(1\),表示城市 \(b\) 必须驻扎军队。
输入文件中每一行相邻的两个数据之间均用一个空格分隔。
输出格式
输出共 \(m\) 行,每行包含一个个整数,第 \(j\) 行表示在满足国王第 \(j\) 个要求时的最小开销, 如果无法满足国王的第 \(j\) 个要求,则该行输出 −1。
输入输出样例
输入 #1
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
输出 #1
12
7
-1
说明/提示
样例 1 解释
- 对于第一个要求,在 \(4\) 号和 \(5\) 号城市驻扎军队时开销最小。
- 对于第二个要求,在 \(1\) 号、\(2\) 号、\(3\) 号城市驻扎军队时开销最小。
- 第三个要求是无法满足的,因为在 \(1\) 号、$54 号城市都不驻扎军队就意味着由道路直接连 接的两座城市中都没有驻扎军队。
数据规模与约定
测试点编号 | type | n=m |
---|---|---|
1,2 | A3 |
10 |
3,4 | C3 |
10 |
5,6 | A3 |
100 |
7 | C3 |
100 |
8,9 | A3 |
\(2\times 10^3\) |
10,11 | C3 |
\(2\times 10^3\) |
12,13 | A1 |
\(10^5\) |
14,15,16 | A2 |
\(10^5\) |
17 | A3 |
\(10^5\) |
18,19 | B1 |
\(10^5\) |
20,21 | C1 |
\(10^5\) |
22 | C2 |
\(10^5\) |
23,24,25 | C3 |
\(10^5\) |
数据类型的含义:
A
:城市 \(i\) 与城市 \(i+1\) 直接相连。B
:任意城市与城市 1 的距离不超过 100(距离定义为最短路径上边的数量),即如果这 棵树以 1 号城市为根,深度不超过 100。C
:在树的形态上无特殊约束。1
:询问时保证 \(a=1,x=1\),即要求在城市 1 驻军。对 \(b,y\) 没有限制。2
:询问时保证 \(a,b\) 是相邻的(由一条道路直接连通)3
:在询问上无特殊约束。
对于 \(100%\)的数据,保证\(1 \leq n,m ≤ 10^5\),\(1 ≤ p_i ≤ 10^5\) , \(1 \leq u, v, a, b \leq n\), \(a \neq b\),\(x, y \in \{0, 1\}\)。
题解
什么 \(Noip\) 居然考模板题,哦是 \(动态dp\), 那没事了.
动态dp,板子题了。
首先,我们考虑不带修改的时候,转移方程可以写成这样
\(f[i][0] = \displaystyle \sum_{to \in son[i]} f[to][1]\)
\(f[i][1] = \displaystyle \sum_{to \in son[i]} min(f[to][1],f[to][0]) \ + w[i]\)
还是把轻儿子和重儿子的贡献分开来考虑。
设 \(g[i][0] = \displaystyle \sum_{to \in 轻儿子} f[to][1]\) \(g[i][1] = \displaystyle \sum_{to \in 轻儿子} min(f[to][1],f[to][0]) \ + w[x]\)
解释一下 \(g[i][0]\) 表示必须选他轻儿子的总的价值和, \(g[i][1]\) 表示 他轻儿子选或不选的最大价值加上这个点的权值
转移方程就可以转化为:
\(f[i][0] = g[i][0] + f[son[x]][1]\)
\(f[i][1] = g[i][1] + min(f[to][1],f[to][0]) \ +w[x]\)
这里定义广义矩阵乘法为
矩阵 \(C = A \times B\)
那么 \(c[i][j] = min(a[i][k] + b[k][j])\)
上面的那个转移方程转化为矩阵就可以写成:
然后就可以愉快的套动态dp 的板子了
每个点必须放城市可以通过 \(-\infin\) 来实现,反之不能放就可以通过 \(+ \infin\) 实现。
但最后计算答案时,要必须放城市的点要加上减去的 \(\infin\).
因为每个询问是独立的,所以每次询问不要忘了,输出答案之后,要把之前的点的权值还原回来
无解的情况就是当两个点相邻且都不能被选的情况。
Code(不开o2,过不去系列):
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define int long long
const int inf = 5e9;
const int N = 1e5+10;
char type[2];
int n,m,tot,x,y,num,val,u,v,a,b,w1,w2;
int head[N],top[N],dep[N],siz[N],fa[N],son[N];
int end[N],f[N][2],g[N][2],dfn[N],w[N],ord[N];
struct node
{
int a[2][2];
node() {a[0][0] = a[0][1] = a[1][0] = a[1][1] = inf;}//矩阵初始化为极大值
}tr[N<<2],base[N];
struct bian
{
int to,net;
}e[N<<1];
inline int read()
{
int s = 0,w = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9'){s =s * 10+ch - '0'; ch = getchar();}
return s * w;
}
void add(int x,int y)
{
e[++tot].to = y;
e[tot].net = head[x];
head[x] = tot;
}
node operator * (node x,node y)//广义矩阵乘法
{
node res;
for(int i = 0; i <= 1; i++)
{
for(int j = 0; j <= 1; j++)
{
for(int k = 0; k <= 1; k++)
{
res.a[i][j] = min(res.a[i][j],x.a[i][k] + y.a[k][j]);
}
}
}
return res;
}
void get_tree(int x)//树剖预处理
{
dep[x] = dep[fa[x]] + 1; siz[x] = 1;
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa[x]) continue;
fa[to] = x;
get_tree(to);
siz[x] += siz[to];
if(siz[to] > siz[son[x]]) son[x] = to;
}
}
void dfs(int x,int topp)//处理出每条链的链顶
{
dfn[x] = ++num; top[x] = topp; ord[num] = x;
if(son[x] == -1)
{
end[topp] = num;//预处理每条重链的链末
return;
}
else dfs(son[x],topp);
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa[x] || to == son[x]) continue;
dfs(to,to);
}
}
void dp(int x,int fa)//先预处理出 f,g 值
{
g[x][1] = f[x][1] = w[x];
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa) continue;
dp(to,x);
f[x][0] += f[to][1];
f[x][1] += min(f[to][0],f[to][1]);
if(to != son[x])
{
g[x][0] += f[to][1];
g[x][1] += min(f[to][0],f[to][1]);
}
}
}
void up(int o)
{
tr[o] = tr[o<<1] * tr[o<<1|1];
}
void build(int o,int L,int R)//建树操作
{
if(L == R)
{
int tmp = ord[L];
tr[o].a[0][1] = g[tmp][0];//构建转移矩阵
tr[o].a[1][0] = tr[o].a[1][1] = g[tmp][1];
base[L] = tr[o];
return;
}
int mid = (L + R)>>1;
build(o<<1,L,mid);
build(o<<1|1,mid+1,R);
up(o);
}
void chenge(int o,int L,int R,int x)
{
if(L == R)
{
tr[o] = base[L];
return;
}
int mid = (L + R)>>1;
if(x <= mid) chenge(o<<1,L,mid,x);
if(x > mid) chenge(o<<1|1,mid+1,R,x);
up(o);
}
node query(int o,int l,int r,int L,int R)
{
if(L <= l && R >= r) return tr[o];
int mid = (l + r)>>1;
if(R <= mid) return query(o<<1,l,mid,L,R);
if(L > mid) return query(o<<1|1,mid+1,r,L,R);
return query(o<<1,l,mid,L,R) * query(o<<1|1,mid+1,r,L,R);
}
node get_node(int x)
{
return query(1,1,n,dfn[x],end[top[x]]);
}
void modify(int x,int val)//增量法修改每个点的转移矩阵
{
base[dfn[x]].a[1][0] += val - w[x];
base[dfn[x]].a[1][1] = base[dfn[x]].a[1][0];
w[x] = val;
while(x)
{
node Old = get_node(top[x]);
chenge(1,1,n,dfn[x]);
node New = get_node(top[x]);
int tmp = dfn[fa[top[x]]];
base[tmp].a[0][1] += New.a[1][1] - Old.a[1][1];
base[tmp].a[1][0] += min(New.a[0][1],New.a[1][1]) - min(Old.a[0][1],Old.a[1][1]);
base[tmp].a[1][1] = base[tmp].a[1][0];
x = fa[top[x]];
}
}
signed main()
{
n = read(); m = read();
scanf("%s",type+1);
for(int i = 1; i <= n; i++)
{
w[i] = read();
son[i] = -1;
}
for(int i = 1; i <= n-1; i++)
{
u = read(); v = read();
add(u,v); add(v,u);
}
get_tree(1); dfs(1,1); dp(1,1); build(1,1,n);
for(int i = 1; i <= m; i++)
{
a = read(); w1 = read();
b = read(); w2 = read();
if(!w1 && !w2 &&(fa[a] == b || fa[b] == a))//无解的情况
{
puts("-1");
continue;
}
int v1 = w[a], v2 = w[b];
if(w1 == 0) modify(a,v1+inf);
if(w1 == 1) modify(a,v1-inf);
if(w2 == 0) modify(b,v2+inf);
if(w2 == 1) modify(b,v2-inf);
node ans = get_node(1);
int res = min(ans.a[0][1],ans.a[1][1]);
if(w1 == 1) res += inf;//加回减去的 inf
if(w2 == 1) res += inf;
modify(a,v1);//改回原值
modify(b,v2);
printf("%lld\n",res);
}
return 0;
}
这题实现起来还是比较麻烦的,自己调了一下午才能勉强调出来(一定是自己太菜了)。
另外还有一种倍增的做法,然鹅我并不会。