不得不说…noip的趋势越来越像是day1基础+思路稳定分数,基本确定你能在哪个获奖区,day2难度提高,给满分dalao拼技术和给day1二等的人一个拿一等的机会,给三等的摸一个二等的机会…
总感觉这个day2数据放大就是一套省选…
T1 旅行
题目描述
小 Y 是一个爱好旅行的 OIer。她来到 X 国,打算将各个城市都玩一遍。
小Y了解到, X国的 n n n 个城市之间有 mm 条双向道路。每条双向道路连接两个城市。 不存在两条连接同一对城市的道路,也不存在一条连接一个城市和它本身的道路。并且, 从任意一个城市出发,通过这些道路都可以到达任意一个其他城市。小 Y 只能通过这些 道路从一个城市前往另一个城市。
小 Y 的旅行方案是这样的:任意选定一个城市作为起点,然后从起点开始,每次可 以选择一条与当前城市相连的道路,走向一个没有去过的城市,或者沿着第一次访问该 城市时经过的道路后退到上一个城市。当小 Y 回到起点时,她可以选择结束这次旅行或 继续旅行。需要注意的是,小 Y 要求在旅行方案中,每个城市都被访问到。
为了让自己的旅行更有意义,小 Y 决定在每到达一个新的城市(包括起点)时,将 它的编号记录下来。她知道这样会形成一个长度为 n n n 的序列。她希望这个序列的字典序 最小,你能帮帮她吗? 对于两个长度均为 n n n 的序列 A A A 和 B B B,当且仅当存在一个正整数 x x x,满足以下条件时, 我们说序列 A A A 的字典序小于 B B B。
对于任意正整数
1
≤
i
<
x
1 ≤ i < x
1≤i<x,序列
A
A
A 的第
i
i
i 个元素
A
i
A_i
Ai 和序列
B
B
B 的第
i
i
i 个元素
B
i
B_i
Bi 相同。
序列
A
A
A 的第
x
x
x 个元素的值小于序列
B
B
B 的第
x
x
x 个元素的值。
输入输出格式
输入格式:
输入文件共
m
+
1
m + 1
m+1 行。第一行包含两个整数
n
,
m
(
m
≤
n
)
n,m(m ≤ n)
n,m(m≤n),中间用一个空格分隔。
接下来 m 行,每行包含两个整数 u , v ( 1 ≤ u , v ≤ n ) u,v (1 ≤ u,v ≤ n) u,v(1≤u,v≤n) ,表示编号为 u u u 和 v v v 的城市之 间有一条道路,两个整数之间用一个空格分隔。
输出格式:
输出文件包含一行,
n
n
n 个整数,表示字典序最小的序列。相邻两个整数之间用一个 空格分隔。
输入输出样例
输入样例#1:
6 5
1 3
2 3
2 5
3 4
4 6
输出样例#1:
1 3 2 5 4 6
输入样例#2:
6 6
1 3
2 3
2 5
3 4
4 5
4 6
输出样例#2:
1 3 2 4 5 6
T1 分析
先分析数据范围
1.
m
=
n
−
1
m = n-1
m=n−1(即普通树), 60分
普通树的话就很简单了,因为当到达当前节点
n
o
w
now
now的时候,必须将这个点的子树全部遍历过才能返回,并且要求字典序最小,所以暴力dfs贪心就可以了,对当前节点
n
o
w
now
now选择最小的子节点
d
f
s
(
m
i
n
(
s
o
n
[
n
o
w
]
)
)
dfs( min(son[now]) )
dfs(min(son[now]))就可以了。
2.
m
=
n
m = n
m=n (即基环树),40分
<基环树>是指一个有 m m m 条边的连通图,即可以看成是一棵树中多了一条边,也就是所谓的<环套树>,那么在环中删除一条边这个图就是一棵树了。
对于这部分,首先要注意一点,对于到达过的点,只能回退到这个点,不能从它的父亲节点再次到达这个节点。所以直接贪心用优先队列维护当前能到达的所有点,选择最小的到达,并把这个点能到达的点加入队列,这个做法是肯定错的。
当然虽然题目本身是存在难度的,但是如果仔细分析的话并没有那么复杂,因为
n
≤
5000
n≤5000
n≤5000 (毕竟还是T1…不会太夸张)
手写几组数据随便画一画就可以发现,虽然这张图存在
m
m
m 条边,但是实际遍历的时候,环上一定有一条边不会被经过,这个随便画一画想清楚就好了。也就是说依旧只会经过
m
−
1
m-1
m−1 条边。
那么就比较简单了,暴力遍历一遍环上的边,选择这条边不经过,那么对于剩下的
m
−
1
m-1
m−1条边,就是第一种情况了,直接dfs即可。求个最大的答案就可以了。
当然这里还可以发现一件事,因为数据只有
n
=
5000
n = 5000
n=5000 ,所以其实不用求边,暴力删除
5000
5000
5000条边的复杂度也就是
O
(
n
2
)
O(n^2)
O(n2) ,不会
T
L
E
TLE
TLE 的。
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <set>
#include <vector>
using namespace std;
vector<int>f[5010];
int n,m,del_a,del_b,len;
int edge[5010][2];
int ans[5010];
bool vis[5010];
int tmp[5010];
void dfs(int now){
tmp[len++] = now;
vis[now] = true;
int l = f[now].size();
for (int i = 0 ; i < l ; ++i){
int go = f[now][i];
if (!vis[go] && !((now == del_a && go == del_b) || (now == del_b && go == del_a)))
dfs(go);
}
return;
}
void check(){
if (len != n) return;
for (int i = 0 ; i < n ; ++i)
if (tmp[i] != ans[i]){
if (tmp[i] > ans[i]) return;
break;
}
for (int i = 0 ; i < n ; ++i)
ans[i] = tmp[i];
}
int main()
{
scanf("%d%d",&n,&m);
for (int i = 0 ; i < m ; ++i){
int a,b;scanf("%d%d",&a,&b);
f[a].push_back(b);
f[b].push_back(a);
edge[i][0] = a;
edge[i][1] = b;
}
memset(ans,0x3f,sizeof(ans));
for (int i = 1 ; i <= n ; ++i)
sort(f[i].begin(),f[i].end());
if (n == m){
for (int i = 0; i < m ; ++i)
{
len = 0;
memset(vis,0,sizeof(vis));
del_a = edge[i][0];
del_b = edge[i][1];
dfs(1);
check();
}
}else{
del_a = del_b = -1;
dfs(1);
check();
}
for (int i = 0 ; i < n ; ++i)
printf("%d%c",ans[i],i!=n-1?' ':'\n');
return 0;
}
T2 填数游戏
题目点击→洛谷 P5023 填数游戏
小 D 特别喜欢玩游戏。这一天,他在玩一款填数游戏。
这个填数游戏的棋盘是一个
n
×
m
n \times m
n×m的矩形表格。玩家需要在表格的每个格子中填入一个数字(数字
0
0
0 或者数字
1
1
1),填数时需要满足一些限制。
下面我们来具体描述这些限制。
为了方便描述,我们先给出一些定义:
- 我们用每个格子的行列坐标来表示一个格子,即(行坐标,列坐标)。(注意: 行列坐标均从 0 0 0 开始编号) 合法路径
- P P P:一条路径是合法的当且仅当:
- 这条路径从矩形表格的左上角的格子 ( 0 , 0 ) (0,0) (0,0)出发,到矩形的右下角格子 ( n − 1 , m − 1 ) (n - 1,m - 1) (n−1,m−1)结束;
- 在这条路径中,每次只能从当前的格子移动到右边与它相邻的格子,或者 从当前格子移动到下面与它相邻的格子。
例如:在下面这个矩形中,只有两条路径是合法的,它们分别是 P 1 P_1 P1: ( 0 , 0 ) (0,0) (0,0) → ( 0 , 1 ) (0,1) (0,1) → ( 1 , 1 ) (1,1) (1,1)和 P 2 P_2 P2: ( 0 , 0 ) (0,0) (0,0) → ( 1 , 0 ) (1,0) (1,0) → ( 1 , 1 ) (1,1) (1,1)。
对于一条合法的路径 P P P,我们可以用一个字符串 w ( P ) w(P) w(P)来表示,该字符串的长度为 n + m − 2 n + m - 2 n+m−2,其中只包含字符“ R R R”或者字符“ D D D”, 第 i i i 个字符记录了路径 P P P 中第 i i i 步的移动方法,“ R R R”表示移动到当前格子右边与它相邻的格子,“ D D D”表示移动到当前格子下面 与它相邻的格子。例如,上图中对于路径 P 1 P_1 P1,有 w ( P 1 ) = w(P_1) = w(P1)=" R D RD RD";而对于另一条路径 P 2 P_2 P2, 有 w ( P 2 ) = w(P_2) = w(P2)=" D R DR DR"。
同时,将每条合法路径 P P P 经过的每个格子上填入的数字依次连接后,会得到一个长 度为 n + m − 1 n + m - 1 n+m−1的 01 01 01 字符串,记为 s ( P ) s(P) s(P)。例如,如果我们在格子 ( 0 , 0 ) (0,0) (0,0)和 ( 1 , 0 ) (1,0) (1,0)上填入数字 0 0 0,在格子 ( 0 , 1 ) (0,1) (0,1)和 ( 1 , 1 ) (1,1) (1,1)上填入数字 1 1 1(见上图红色数字)。那么对于路径 P 1 P_1 P1,我们可以得 到 s ( P 1 ) = s(P_1) = s(P1)=" 011 011 011",对于路径 P 2 P_2 P2,有 s ( P 2 ) = s(P_2) = s(P2)=" 001 001 001"。
游戏要求小
D
D
D 找到一种填数字
0
0
0、
1
1
1 的方法,使得对于两条路径
P
1
P_1
P1,
P
2
P_2
P2,如果
w
(
P
1
)
>
w
(
P
2
)
w(P_1) > w(P_2)
w(P1)>w(P2),那么必须
s
(
P
1
)
≤
s
(
P
2
)
s(P_1) ≤ s(P_2)
s(P1)≤s(P2)。
我们说字符串
a
a
a 比字符串
b
b
b 小,当且仅当字符串
a
a
a 的字典序小于字符串
b
b
b 的字典序,字典序的定义详见第一题。但是仅仅是找一种方法无法满 足小
D
D
D 的好奇心,小
D
D
D 更想知道这个游戏有多少种玩法,也就是说,有多少种填数字 的方法满足游戏的要求?
小 D D D 能力有限,希望你帮助他解决这个问题,即有多少种填 0 0 0、 1 1 1 的方法能满足题目要求。由于答案可能很大,你需要输出答案对 1 0 9 + 7 10^9 + 7 109+7取模的结果。
输入输出格式
输入格式:
输入文件共一行,包含两个正整数 n,mn,m,由一个空格分隔,表示矩形的大小。其 中 nn 表示矩形表格的行数,mm 表示矩形表格的列数。
输出格式:
输出共一行,包含一个正整数,表示有多少种填
0
0
0、
1
1
1 的方法能满足游戏的要求。 注意:输出答案对
1
0
9
+
7
10^9+7
109+7取模的结果。
输入输出样例
输入样例#1:
2 2
输出样例#1:
12
输入样例#2:
3 3
输出样例#2:
112
输入样例#3:
5 5
输出样例#3:
7136
T2 分析
oi一大经典类型题…暴力打表找规律。
先化简一下题意:
给定一个
n
∗
m
n*m
n∗m 网格棋盘,要求用
0
/
1
0/1
0/1填充每一个格子.
使得任意两条从点
(
0
,
0
)
(0,0)
(0,0) 到
(
n
−
1
,
m
−
1
)
(n−1,m−1)
(n−1,m−1) 的不完全相交路径,先向右(字典序大)的路径的
01
01
01 路径字典序相对小.
首先确定一件事,对于任意一种填法,棋盘中填的数字一定满足
a
[
i
]
[
j
]
≥
a
[
i
−
1
,
j
+
1
]
a[i][j]≥a[i-1,j+1]
a[i][j]≥a[i−1,j+1],即从左下到右上的任意对角线中的数字一定是非递增序列。
当然这种题目…一看数据那么小,而且是固定答案的,遇事不决先暴力 …只要把
8
∗
8
8*8
8∗8的表打出来基本就能过了…
当然对于dalao来说,一看
n
≤
8
n≤8
n≤8,第一反应应该是状压才对…不应该是暴力…
/***********************************************/
比赛时的做法可能是这样的:
当然比赛的时候写个暴力找规律没有任何问题…
O
(
2
n
m
)
O(2^{nm})
O(2nm)直接上呗…
这时候会发现表很难打,打到
n
=
7
n=7
n=7 以后就非常慢了,然后
n
=
8
n=8
n=8基本上短时间内只能出一个
(
8
,
8
)
(8,8)
(8,8) 的答案
当然这里不管是打表还是画图,都可以发现第二件事,那就是对于
(
n
,
m
)
(n,m)
(n,m) 的答案和
(
m
,
n
)
(m,n)
(m,n) 是一样的,对称一下就行了。
这时候打一个以下的表…猜一下规律,会发现相邻的两项基本上都满足
(
i
,
j
)
=
(
i
,
j
−
1
)
∗
3
(i,j)=(i,j-1)*3
(i,j)=(i,j−1)∗3 的规律
- n = 1 n=1 n=1时, A N S = 2 m ANS=2^m ANS=2m
- n = 2 n = 2 n=2时, ( 2 , 2 ) = 12 , A N S = 4 ∗ 3 m − 1 (2,2)=12,ANS =4*3^{m-1} (2,2)=12,ANS=4∗3m−1
- n = 3 n=3 n=3时, ( 3 , 3 ) = 112 , A N S = 112 ∗ 3 m − 3 (3,3)=112,ANS = 112*3^{m-3} (3,3)=112,ANS=112∗3m−3
- n = 4 n=4 n=4时, ( 4 , 4 ) = 912 (4,4)=912 (4,4)=912发现 ( 4 , 5 ) = ( 4 , 4 ) ∗ 3 − 3 ∗ 2 4 (4,5) =(4,4)*3-3 *2^4 (4,5)=(4,4)∗3−3∗24,之后的答案依旧满足上述的规律,即 A N S = 2688 ∗ 3 m − 5 ANS = 2688*3^{m-5} ANS=2688∗3m−5
- n = 5 n=5 n=5时, ( 5 , 5 ) = 7136 (5,5)=7136 (5,5)=7136发现 ( 5 , 6 ) = ( 5 , 5 ) ∗ 3 − 3 ∗ 2 5 (5,6)=(5,5)*3-3*2^5 (5,6)=(5,5)∗3−3∗25,之后的答案依旧满足上述规律,即 A N S = 21312 ∗ 3 m − 6 ANS=21312*3^{m-6} ANS=21312∗3m−6
-
n
=
6
n=6
n=6时,
(
6
,
6
)
=
56768
(6,6)=56768
(6,6)=56768发现
(
6
,
7
)
=
(
6
,
6
)
∗
3
−
3
∗
2
6
(6,7)=(6,6)*3-3*2^6
(6,7)=(6,6)∗3−3∗26,之后的答案依旧满足上述规律,即
A
N
S
=
170112
∗
3
m
−
7
ANS=170112*3^{m-7}
ANS=170112∗3m−7
其实这时候就算 n = 7 , 8 n=7,8 n=7,8后面的答案你的代码很难跑出来…猜也猜要猜一个规律上去…
可以发现对于不满足常规规律是从 n = 4 n=4 n=4开始的,那么从 n = 4 n=4 n=4开始找规律,
可以发现 ( 5 , 5 ) = ( 4 , 4 ) ∗ 8 − 5 ∗ 2 5 , ( 6 , 6 ) = ( 5 , 5 ) ∗ 8 − 5 ∗ 2 6 (5,5) = (4,4) * 8 -5*2^5,(6,6)=(5,5)*8-5*2^6 (5,5)=(4,4)∗8−5∗25,(6,6)=(5,5)∗8−5∗26
那么可以猜测一下 ( 7 , 7 ) = ( 6 , 6 ) ∗ 8 − 5 ∗ 2 7 = 453504 , ( 8 , 8 ) = ( 7 , 7 ) ∗ 8 − 5 ∗ 2 8 = 3626752 (7,7)=(6,6)*8-5*2^7=453504,(8,8)=(7,7)*8-5*2^8=3626752 (7,7)=(6,6)∗8−5∗27=453504,(8,8)=(7,7)∗8−5∗28=3626752 - n = 7 n=7 n=7时, ( 7 , 8 ) = ( 7 , 7 ) ∗ 3 − 3 ∗ 2 7 = 1360128 (7,8)=(7,7)*3-3*2^7=1360128 (7,8)=(7,7)∗3−3∗27=1360128,之后的答案即 A N S = 1360128 ∗ 2 m − 8 ANS=1360128*2^{m-8} ANS=1360128∗2m−8
-
n
=
8
n=8
n=8时,
(
8
,
9
)
=
(
8
,
8
)
∗
3
−
3
∗
2
8
=
10879488
(8,9)=(8,8)*3-3*2^8=10879488
(8,9)=(8,8)∗3−3∗28=10879488,之后的答案即
A
N
S
=
10879488
∗
2
m
−
9
ANS=10879488*2^{m-9}
ANS=10879488∗2m−9
这样的话就做完了…并且可以用自己的暴力程序,开着代码一直跑,看看能不能满足自己找的规律,来告诉自己到底能估到几分,当然这个做法就是100分的…但是比赛时起码能保证自己可以获得65分,即 n ≤ 3 n≤3 n≤3的部分
这个表大概跑了半个小时左右吧, n = 8 n=8 n=8的情况暴力跑太慢了。
/***********************************************/
如果想要证明规律的话,需要发现以下条件
- ( n , m ) = ( m , n ) (n,m)=(m,n) (n,m)=(m,n);
- 棋盘中填的数字一定满足 a [ i ] [ j ] ≥ a [ i − 1 , j + 1 ] a[i][j]≥a[i-1,j+1] a[i][j]≥a[i−1,j+1],即从左下到右上的任意对角线中的数字一定是非递增序列;
- 如果(i-1,j)和(i,j-1)的填数相同,那么以(i,j)为左上角、以(n,m)为右下角的子矩阵内所有对角线内的填数各自相等。
那么现在就可以分类讨论了:
若 ( 0 , 1 ) , ( 1 , 0 ) (0,1),(1,0) (0,1),(1,0) 两格相同:
- 这种情况下,只需要使得 ( 1 , 1 ) → ( n , m ) (1,1)→(n,m) (1,1)→(n,m)的所有对角线上数字相同.
- 同时第一列和第一行会受其影响,画一画图,加上推一波式子可以发现,答案形如 4 x ∗ 3 y ∗ 2 z 4^x∗3^y∗2^z 4x∗3y∗2z
若 ( 0 , 1 ) , ( 1 , 0 ) (0,1),(1,0) (0,1),(1,0) 两格不同:
-
(
0
,
2
)
,
(
1
,
1
)
,
(
2
,
0
)
(0,2),(1,1),(2,0)
(0,2),(1,1),(2,0) 相同:
这种情况类似第一种情况 -
(
0
,
2
)
,
(
1
,
1
)
,
(
2
,
0
)
(0,2),(1,1),(2,0)
(0,2),(1,1),(2,0)其中一对相同:
设 f [ i ] f[i] f[i] 表示到第 i i i 条对角线上时,在前两行的第 i − 1 i−1 i−1 条斜线上及以前有过匹配的方案数。
即在两行中,第二行的某一格与其斜上方的格相同,一旦出现这种情况接下来第二行的填数就会有限制.
当第一行和第二行不存在这种情况的时候,当前的一条对角线实际上有 5 5 5 种填法,而当第 i i i 条对角线上出现这种情况的时候,则只有 4 4 4 种填法.
那么考虑一下转移就可以写出:
f [ i ] = f [ i − 1 ] ∗ 4 + 4 ∗ 5 f[i]=f[i−1]∗4+4∗5 f[i]=f[i−1]∗4+4∗5
即
(在 i − 2 i−2 i−2 条斜线上以前就匹配成功的方案 f [ i − 1 ] f[i−1] f[i−1]) * (当前第 i i i条对角线的 4 4 4 种填法) + ( i − 1 i−1 i−1 条斜线第一次匹配成功的 4 4 4 种填法) * (当前第 i i i 条对角线因不受任何影响所以有 5 5 5 种填法.)
PC画图太麻烦了,这里给出
(
3
,
3
)
(3,3)
(3,3) 的图
按照同样的方法可以画出接下去的几个图
(
3
,
3
)
=
4
1
∗
3
1
∗
2
2
+
4
1
∗
3
0
∗
2
4
(3,3)=4^1*3^1*2^2 + 4^1*3^0*2^4
(3,3)=41∗31∗22+41∗30∗24
(
4
,
4
)
=
5
1
∗
4
0
∗
3
0
∗
2
5
+
5
1
∗
4
0
∗
3
1
∗
2
4
+
5
0
∗
4
2
∗
3
0
∗
2
5
(4,4)=5^1 * 4^0 * 3^0 * 2^5 + 5^1*4^0*3^1*2^4 + 5^0 * 4^2*3^0*2^5
(4,4)=51∗40∗30∗25+51∗40∗31∗24+50∗42∗30∗25
(
5
,
5
)
=
5
1
∗
4
1
∗
3
0
∗
2
7
+
5
1
∗
4
0
∗
3
1
∗
2
5
+
5
0
∗
4
3
∗
3
0
∗
2
6
(5,5)=5^1 * 4^1 * 3^0 * 2^7 + 5^1*4^0*3^1*2^5 + 5^0 * 4^3*3^0*2^6
(5,5)=51∗41∗30∗27+51∗40∗31∗25+50∗43∗30∗26
(
6
,
6
)
=
5
1
∗
4
1
∗
3
2
∗
2
7
+
5
1
∗
4
0
∗
3
1
∗
2
6
+
5
0
∗
4
4
∗
3
0
∗
2
7
(6,6)=5^1 * 4^1 * 3^2 * 2^7 + 5^1*4^0*3^1*2^6 + 5^0 * 4^4*3^0*2^7
(6,6)=51∗41∗32∗27+51∗40∗31∗26+50∗44∗30∗27
这样就可以计算出当
n
=
m
n=m
n=m的答案了.
当
m
=
n
+
1
m=n+1
m=n+1 时,前两种情况是很容易推下去的。
按照第三种情况,根据最后一条对角线是否匹配成功进行讨论.
不难推出另外一种的答案实质上是
A
N
S
=
(
f
[
n
]
∗
3
+
4
∗
5
)
∗
2
n
ANS=(f[n]∗3+4∗5)∗2^n
ANS=(f[n]∗3+4∗5)∗2n
当推过几次式子之后可以发现,也满足上面打表所说的规律,
m
m
m 每次
+
1
+1
+1 会让答案
∗
3
*3
∗3
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <set>
#include <vector>
using namespace std;
#define LL long long
const LL mod=1e9+7;
int n,m;
LL qpow(LL a,int n){
LL res=1;
while (n){
if(n&1) res=res*a%mod;
a = a * a % mod;
n>>=1;
}
return res%mod;
}
int main(){
scanf("%d%d",&n,&m);
if(n>m)swap(n,m);
if(n==1){
cout<<qpow(2,m)<<endl;
}
if(n==2){
cout<<4*qpow(3,m-1)%mod<<endl;
}
if(n==3){
cout<<112*qpow(3,m-3)%mod<<endl;
}
if(n==4){
if(m==4)puts("912");
else cout<<2688*qpow(3,m-5)%mod<<endl;
}
if(n==5){
if(m==5)puts("7136");
else cout<<21312*qpow(3,m-6)%mod<<endl;
}
if(n==6){
if(m==6)puts("56768");
else cout<<170112*qpow(3,m-7)%mod<<endl;
}
if(n==7){
if(m==7)puts("453504");
else cout<<1360128*qpow(3,m-8)%mod<<endl;
}
if(n==8){
if(m==8)puts("3626752");
else cout<<10879488*qpow(3,m-9)%mod<<endl;
}
}
T3 保卫王国
题目描述
Z 国有
n
n
n座城市,
n
−
1
n - 1
n−1条双向道路,每条双向道路连接两座城市,且任意两座城市 都能通过若干条道路相互到达。
Z 国的国防部长小 Z 要在城市中驻扎军队。驻扎军队需要满足如下几个条件:
一座城市可以驻扎一支军队,也可以不驻扎军队。
由道路直接连接的两座城市中至少要有一座城市驻扎军队。
在城市里驻扎军队会产生花费,在编号为i的城市中驻扎军队的花费是
p
i
p_i
pi。
小 Z 很快就规划出了一种驻扎军队的方案,使总花费最小。但是国王又给小 Z 提出 了mm个要求,每个要求规定了其中两座城市是否驻扎军队。小 Z 需要针对每个要求逐一 给出回答。具体而言,如果国王提出的第
j
j
j个要求能够满足上述驻扎条件(不需要考虑 第
j
j
j 个要求之外的其它要求),则需要给出在此要求前提下驻扎军队的最小开销。如果 国王提出的第
j
j
j个要求无法满足,则需要输出
−
1
(
1
≤
j
≤
m
)
-1 (1 ≤ j ≤ m)
−1(1≤j≤m)。现在请你来帮助小 Z。
输入输出格式
输入格式:
第
1
1
1 行包含两个正整数
n
,
m
n,m
n,m和一个字符串
t
y
p
e
type
type,分别表示城市数、要求数和数据类型。
t
y
p
e
type
type是一个由大写字母
A
A
A,
B
B
B 或
C
C
C 和一个数字
1
1
1,
2
2
2,
3
3
3 组成的字符串。它可以帮助你获得部分分。你可能不需要用到这个参数。这个参数的含义在【数据规模与约定】中 有具体的描述。
第
2
2
2 行
n
n
n个整数
p
i
p_i
pi,表示编号
i
i
i的城市中驻扎军队的花费。
接下来
n
−
1
n - 1
n−1 行,每行两个正整数
u
,
v
u,v
u,v,表示有一条
u
u
u到
v
v
v的双向道路。
接下来
m
m
m 行,第jj行四个整数
a
,
x
,
b
,
y
(
a
≠
b
)
a,x,b,y(a ≠ b)
a,x,b,y(a̸=b),表示第jj个要求是在城市
a
a
a驻扎
x
x
x支军队, 在城市
b
b
b驻扎yy支军队。其中,
x
x
x 、
y
y
y 的取值只有
0
0
0 或
1
1
1:若
x
x
x 为
0
0
0,表示城市
a
a
a 不得驻 扎军队,若
x
x
x 为
1
1
1,表示城市
a
a
a 必须驻扎军队;若
y
y
y 为
0
0
0,表示城市
b
b
b不得驻扎军队, 若
y
y
y 为
1
1
1,表示城市
b
b
b 必须驻扎军队。
输入文件中每一行相邻的两个数据之间均用一个空格分隔。
输出格式:
输出共
m
m
m 行,每行包含
1
1
1 个整数,第
j
j
j行表示在满足国王第
j
j
j个要求时的最小开销, 如果无法满足国王的第
j
j
j个要求,则该行输出
−
1
-1
−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
T3 分析
黑科技题目…反正我不会… 动态dp模板题,甚至还能实现一下传说中的“全局平衡二叉树”…
简述一下题意:对一颗
n
n
n个点的树进行染色,对第
i
i
i个点染色的花费是
p
i
p_i
pi,要求相邻的两个点之间必须有一个点被染色,然后给出
m
m
m组询问,每次询问强制对两个点固定状态(染色或不染色)
先考虑比赛时的做法:乍一看不会做怎么办…我们暴力一发O(nm)再说…
对于每个点可以选或不选,所以我们用:
{
d
p
[
i
]
[
1
]
表
示
以
i
为
根
的
子
树
,
在
i
节
点
选
择
时
的
最
小
费
用
d
p
[
i
]
[
0
]
表
示
以
i
为
根
的
子
树
,
在
i
节
点
不
选
时
的
最
小
费
用
\begin{cases} dp[i][1]表示以 i 为根的子树,在 i 节点选择时的最小费用\\ dp[i][0]表示以 i 为根的子树,在 i 节点不选时的最小费用 \end{cases}
{dp[i][1]表示以i为根的子树,在i节点选择时的最小费用dp[i][0]表示以i为根的子树,在i节点不选时的最小费用
同时,还要求相邻的两个节点至少有一个要选,所以如果
i
i
i 不选,则
i
i
i 的所有叶节点必须都选,如果
i
i
i 选择,则叶节点没有限制。
以此得到状态转移(
j
j
j 为
i
i
i 的叶节点,
a
[
i
]
a[i]
a[i] 为
i
i
i 节点的权值):
{
d
p
[
i
]
[
1
]
=
∑
m
i
n
(
d
p
[
j
,
1
]
,
d
p
[
j
,
0
]
)
+
a
[
i
]
d
p
[
i
]
[
0
]
=
∑
d
p
[
j
,
1
]
\begin{cases} dp[i][1]=\sum min(dp[j,1],dp[j,0])+a[i]\\ dp[i][0]=\sum dp[j,1] \end{cases}
{dp[i][1]=∑min(dp[j,1],dp[j,0])+a[i]dp[i][0]=∑dp[j,1]
对于每个询问,若必选则将权值修改为
0
0
0,若不选则将权值修改为
I
N
F
INF
INF,最后输出
m
i
n
(
d
p
[
1
,
1
]
,
d
p
[
1
,
0
]
)
min(dp[1,1],dp[1,0])
min(dp[1,1],dp[1,0])
时间复杂度
O
(
n
m
)
O(nm)
O(nm),44分。
显然对于整个代码,进行dp是必须的,那么上面的代码慢在哪里呢?显然是对于每次询问都对整棵树重新做一次dp了,显然对于只修改两个点,可能对于dp值只需要修改几个关键点就可以了,很多点的值是不需要修改的,但是我们依旧做了一次dp,所以可以猜想优化应该在这里。
那么我们考虑修改两个值对整个dp数组会有多少影响。
假设修改x,y两个点,观察一下对整棵树的影响
1
1
1表示根节点,
F
F
F表示
x
,
y
x,y
x,y的最近公共祖先,
A
A
A表示
F
F
F不包含
x
,
y
x,y
x,y的子树,
B
B
B表示根节点
1
1
1不包含
x
,
y
x,y
x,y的子树,
C
,
D
C,D
C,D分别表示
x
,
y
x,y
x,y的子树
我们可以发现,对于x,y修改,对整棵树的影响只有
x
→
1
和
y
→
1
x→1和y→1
x→1和y→1这两条路径上的值。其他点的值都是不会修改的。
所以其实我们对于每个询问需要做的就是修改
x
,
y
x,y
x,y后,将
x
,
y
x,y
x,y的值重新更新到根节点
1
1
1。
因为在结果上我们需要更新的路径可以分为三段:
x
→
L
,
y
→
L
,
L
→
1
x→L,y→L,L→1
x→L,y→L,L→1,即
x
,
y
x,y
x,y到达
L
C
A
(
x
,
y
)
LCA(x,y)
LCA(x,y),再从
L
C
A
(
x
,
y
)
LCA(x,y)
LCA(x,y)到达根节点
1
1
1
所以考虑维护一个数组
f
[
x
]
[
0
/
1
]
[
y
]
[
0
/
1
]
f[x][0/1][y][0/1]
f[x][0/1][y][0/1]表示x不选/选,y不选/选时,x到y这条路径上能得到的最大的dp值.
接下去就可以对询问处理答案了,但是这里需要的空间是
(
n
2
)
(n^2)
(n2)所以我们考虑修改一下记录方式。因为这个数组维护的目的其实也是使得
x
,
y
x,y
x,y到达
L
C
A
(
x
,
y
)
LCA(x,y)
LCA(x,y),所以我们用倍增的方式来优化这个数组。
即将数组维护成
f
[
x
]
[
0
/
1
]
[
i
]
[
0
/
1
]
f[x][0/1][i][0/1]
f[x][0/1][i][0/1]表示
x
x
x 不选/选,从
x
x
x 往上跳
2
i
2^i
2i 步的节点不选/选时,这条路上能得到的最大dp值。
接下去考虑一件事情,因为
x
→
L
x→L
x→L这条路径中,可能还存在一些点,这些点也存在它们的子树,而这个方程是没有办法考虑到这件事情的,所以我们需要重新定义方程:
设
f
[
u
]
[
0
/
1
]
[
i
]
[
0
/
1
]
f[u][0/1][i][0/1]
f[u][0/1][i][0/1]表示
x
x
x 不选/选,从
x
x
x 往上跳
2
i
2^i
2i 步的节点不选/选时,这条路上能得到的最大dp值 且计入其他子树的最终dp值。即在整个祖先节点的子树中减去
x
x
x 的子树的影响。
然后我们得到转移方程:
{
f
[
u
]
[
0
]
[
i
]
[
0
]
=
m
i
n
(
f
[
u
]
[
0
]
[
i
−
1
]
[
0
]
+
f
[
F
]
[
0
]
[
i
−
1
]
[
0
]
,
f
[
u
]
[
0
]
[
i
−
1
]
[
1
]
+
f
[
F
]
[
1
]
[
i
−
1
]
[
0
]
)
;
f
[
u
]
[
0
]
[
i
]
[
1
]
=
m
i
n
(
f
[
u
]
[
0
]
[
i
−
1
]
[
0
]
+
f
[
F
]
[
0
]
[
i
−
1
]
[
1
]
,
f
[
u
]
[
0
]
[
i
−
1
]
[
1
]
+
f
[
F
]
[
1
]
[
i
−
1
]
[
1
]
)
;
f
[
u
]
[
1
]
[
i
]
[
0
]
=
m
i
n
(
f
[
u
]
[
1
]
[
i
−
1
]
[
0
]
+
f
[
F
]
[
0
]
[
i
−
1
]
[
0
]
,
f
[
u
]
[
1
]
[
i
−
1
]
[
1
]
+
f
[
F
]
[
1
]
[
i
−
1
]
[
0
]
)
;
f
[
u
]
[
1
]
[
i
]
[
1
]
=
m
i
n
(
f
[
u
]
[
1
]
[
i
−
1
]
[
0
]
+
f
[
F
]
[
0
]
[
i
−
1
]
[
1
]
,
f
[
u
]
[
1
]
[
i
−
1
]
[
1
]
+
f
[
F
]
[
1
]
[
i
−
1
]
[
1
]
)
;
\begin{cases} f[u][0][i][0] = min(f[u][0][i-1][0] + f[F][0][i-1][0],f[u][0][i-1][1] + f[F][1][i-1][0]);\\ f[u][0][i][1] = min(f[u][0][i-1][0] + f[F][0][i-1][1],f[u][0][i-1][1] + f[F][1][i-1][1]);\\ f[u][1][i][0] = min(f[u][1][i-1][0] + f[F][0][i-1][0],f[u][1][i-1][1] + f[F][1][i-1][0]);\\ f[u][1][i][1] = min(f[u][1][i-1][0] + f[F][0][i-1][1],f[u][1][i-1][1] + f[F][1][i-1][1]); \end{cases}
⎩⎪⎪⎪⎨⎪⎪⎪⎧f[u][0][i][0]=min(f[u][0][i−1][0]+f[F][0][i−1][0],f[u][0][i−1][1]+f[F][1][i−1][0]);f[u][0][i][1]=min(f[u][0][i−1][0]+f[F][0][i−1][1],f[u][0][i−1][1]+f[F][1][i−1][1]);f[u][1][i][0]=min(f[u][1][i−1][0]+f[F][0][i−1][0],f[u][1][i−1][1]+f[F][1][i−1][0]);f[u][1][i][1]=min(f[u][1][i−1][0]+f[F][0][i−1][1],f[u][1][i−1][1]+f[F][1][i−1][1]);
样我们可以直接把
x
,
y
x,y
x,y到
L
L
L 的路径上的 f 数组加起来,并加上
x
,
y
x,y
x,y 的子树的贡献,再加上从
L
L
L 开始到根节点
1
1
1的贡献,再加上子树
A
和
B
A和B
A和B的贡献,就是最终的答案了。
当然这里还存在一些小问题,比如我们要用新的
x
x
x的
d
p
dp
dp值更新出新的
x
x
x的父亲
f
a
t
h
e
r
father
father的
d
p
dp
dp值时,要计算
f
a
t
h
e
r
father
father别的子树的贡献,它保存在
d
p
[
0
/
1
]
[
f
a
]
dp[0/1][fa]
dp[0/1][fa]中,但是
d
p
[
0
/
1
]
[
u
]
dp[0/1][u]
dp[0/1][u]即我们当前要替换的旧的
d
p
dp
dp值也算在里面了
设新的
x
x
x的
d
p
dp
dp值为
N
e
w
New
New,新的
f
a
t
h
e
r
father
father的
d
p
dp
dp值即为
{
l
0
=
d
p
[
0
]
[
L
]
−
d
p
[
1
]
[
u
]
−
d
p
[
1
]
[
v
]
+
u
1
+
v
1
;
l
1
=
d
p
[
1
]
[
L
]
−
m
i
n
(
d
p
[
0
]
[
u
]
,
d
p
[
1
]
[
u
]
)
−
m
i
n
(
d
p
[
0
]
[
v
]
,
d
p
[
1
]
[
v
]
)
+
m
i
n
(
u
0
,
u
1
)
+
m
i
n
(
v
0
,
v
1
)
;
\begin{cases} l0 = dp[0][L] - dp[1][u] - dp[1][v] + u1 + v1;\\ l1 = dp[1][L] - min(dp[0][u],dp[1][u]) - min(dp[0][v],dp[1][v]) + min(u0,u1) + min(v0,v1); \end{cases}
{l0=dp[0][L]−dp[1][u]−dp[1][v]+u1+v1;l1=dp[1][L]−min(dp[0][u],dp[1][u])−min(dp[0][v],dp[1][v])+min(u0,u1)+min(v0,v1);
然后两个特殊情况:
- 当 x , y x,y x,y在一条链上时,倍增 x x x之后已经在 L L L上了,此时直接倍增 L L L即可;
- 当 L C A ( x , y ) LCA(x,y) LCA(x,y)为根节点时,显然不需要倍增 L L L,此时直接算出总答案。
还有个小问题… I N F INF INF不要用 0 x 3 f 3 f 3 f 3 f 3 f 3 f 3 f 3 f 0x3f3f3f3f3f3f3f3f 0x3f3f3f3f3f3f3f3f…要用 100000 ∗ 100000 再 多 补 个 0 100000*100000再多补个0 100000∗100000再多补个0
#include <iostream>
#include <cstdio>
#include <cctype>
#define LL long long
#define INF 100000000000
using namespace std;
const int step[]={1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536,131072};
int cnt = 0,n,m;
long long p[100050];
int father[100050][20],head[100050];
int dep[100050];
int log[100050];
LL dp[100050][3];
LL f[100050][3][20][3];
struct XX{
int to,nxt;
}edg[201000];
void dfs(int u){
dep[u] = dep[father[u][0]] + 1;
dp[u][1] = p[u];
f[u][0][0][0] = INF;
for(int i = 1 ; step[i] < dep[u] ; ++i)
father[u][i] = father[father[u][i-1]][i-1];
for(int i = head[u] ; i ; i = edg[i].nxt){
int v = edg[i].to;
if(v != father[u][0]) {
father[v][0] = u;
dfs(v);
dp[u][0] += dp[v][1];
dp[u][1] += min(dp[v][0],dp[v][1]);
}
}
}
void ddfs(int u){
f[u][1][0][0] = dp[father[u][0]][0] - dp[u][1];
f[u][0][0][1] = f[u][1][0][1] = dp[father[u][0]][1] - min(dp[u][0],dp[u][1]);
for(int i = 1 ; step[i] < dep[u] ; ++i){
int F = father[u][i-1];
f[u][0][i][0] = min(f[u][0][i-1][0] + f[F][0][i-1][0],f[u][0][i-1][1] + f[F][1][i-1][0]);
f[u][0][i][1] = min(f[u][0][i-1][0] + f[F][0][i-1][1],f[u][0][i-1][1] + f[F][1][i-1][1]);
f[u][1][i][0] = min(f[u][1][i-1][0] + f[F][0][i-1][0],f[u][1][i-1][1] + f[F][1][i-1][0]);
f[u][1][i][1] = min(f[u][1][i-1][0] + f[F][0][i-1][1],f[u][1][i-1][1] + f[F][1][i-1][1]);
}
for(int i = head[u] ; i ; i = edg[i].nxt)
if(edg[i].to != father[u][0])
ddfs(edg[i].to);
}
void work(){
int u,x,v,y;
scanf("%d%d%d%d",&u,&x,&v,&y);
if(dep[u] < dep[v]){
swap(u,v);
swap(x,y);
}
LL L,x0,x1,y0,y1,l0,l1,ans;
x0 = x1 = y0 = y1 = l0 = l1 = INF;
if (x) x1 = dp[u][1];
else x0 = dp[u][0];
if (y) y1 = dp[v][1];
else y0 = dp[v][0];
for(int i = log[dep[u] - dep[v]] ; i >= 0 ; --i)
if(dep[u] - step[i] >= dep[v]){
LL t0 = x0;
LL t1 = x1;
x0 = min(t0 + f[u][0][i][0],t1 + f[u][1][i][0]),
x1 = min(t0 + f[u][0][i][1],t1 + f[u][1][i][1]),
u = father[u][i];
} //u往上跳
if(u==v) {
L = u;
if (y) l1 = x1;
else l0 = x0;
}
else{
for(int i = log[dep[u]-1] ; i >= 0 ; --i)
if(father[u][i] != father[v][i]){
LL t0 = x0;
LL t1 = x1;
LL p0 = y0;
LL p1 = y1;
x0 = min(t0 + f[u][0][i][0],t1 + f[u][1][i][0]);
x1 = min(t0 + f[u][0][i][1],t1 + f[u][1][i][1]);
y0 = min(p0 + f[v][0][i][0],p1 + f[v][1][i][0]);
y1 = min(p0 + f[v][0][i][1],p1 + f[v][1][i][1]);
u = father[u][i];
v = father[v][i];
}
L = father[u][0];
l0 = dp[L][0] - dp[u][1] - dp[v][1] + x1 + y1;
l1 = dp[L][1] - min(dp[u][0],dp[u][1]) - min(dp[v][0],dp[v][1]) + min(x0,x1) + min(y0,y1);
}
if(L == 1) ans=min(l0,l1);
else{
for(int i = log[dep[L]-2]; i >= 0 ; --i)
if(dep[L] - step[i] > 1){
LL t0 = l0;
LL t1 = l1;
l0 = min(t0 + f[L][0][i][0],t1 + f[L][1][i][0]);
l1 = min(t0 + f[L][0][i][1],t1 + f[L][1][i][1]);
L = father[L][i];
}
ans=min(dp[1][0] - dp[L][1] + l1 , dp[1][1] - min(dp[L][0],dp[L][1]) + min(l0,l1));
}
if (ans < INF) printf("%lld\n",ans);
else puts("-1");
}
void init(){
for (int i = 2 ; i <= n ; ++i) log[i] = log[i >> 1] + 1;
dfs(1);
ddfs(1);
}
void add(int u,int v){
edg[++cnt] = XX{v,head[u]};
head[u] = cnt;
}
int main(){
scanf("%d%d%*s",&n,&m);
for (int i = 1 ; i <= n ; ++i) scanf("%lld",&p[i]);
for (int i = 0 ; i < n - 1 ; ++i){
int u,v;scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
init();
while(m--) work();
return 0;
}