该文将从客观方面总结一期前半期所学知识,并分析一定的例题
DAY 1
第一天的内容以复习为主。主要复习了最短路/负环、生成树、拓扑排序/关键路径、LCA、权值线段树、欧拉回路。
下面讲简要讲解知识点的内容和一道例题。
NUM.1(最短路/负环)
最短路可分为单源最短路和多源最短路。对于前者一般用Dijkstra或者SPFA进行求解,后者可用floyd解决。
Dijkstra
本质为贪心,令 d i s i dis_i disi为源点到 i i i的最短距离,则我们在所有已处理好的点中选出最小的一点 i n d e x index index,再由 i n d e x index index进一步处理它所连接的所有点。
K e y c o d e s Key \ codes Key codes:
for(int i = head[index]; i; i = edge[i].next) {
int to = edge[i].to;
if(dis[to] > dis[index] + edge[i].w) dis[to] = dis[index] + edge[i].w;
}
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2),还不够优秀,考虑优化。
我们发现,后面的更新无法继续优化,所以重心放在寻找
i
n
d
e
x
index
index上。
自然想到优先队列,将更新的点丢进去,按照
d
i
s
dis
dis的大小排序,直接取队头达到优化的目的。
其实用堆就行啦
Dijkstra模板:
void Dijkstra(int s) { //s为源点
for(int i = 1; i <= tot; i++) { //初始化
dis[i] = 0x3f3f3f3f;
vis[i] = 0;
}
dis[s] = 0;
vis[s] = 1;
priority_queue<node> q;
q.push((node){s, 0}); //丢源点
while(!q.empty()) {
node tmp = q.top();
q.pop();
vis[tmp.id] = 1;
for(int i = head[tmp.id]; i; i = edge[i].next) { //更新
int to = edge[i].to;
if(dis[to] > dis[tmp.id] + edge[i].w) {
dis[to] = dis[tmp.id] + edge[i].w;
if(!vis[to]) q.push((node){to, dis[to]});
}
}
}
}
时间复杂度: O ( n log n ) O(n \log n) O(nlogn)
SPFA
使用队列优化的bellon-ford,与优先队列优化后的Dijkstra的差别在于一个点可以反复入队。当一个点出队后:
v
i
s
[
i
]
=
0
vis[i]=0
vis[i]=0,进队时再修改即可。
好处在于:它可以处理负权环,当一个点入队次数大于n时,存在负权环。
它用普通队列维护,改动很少,不给代码。
期望时间复杂度:
O
(
n
k
)
O(nk)
O(nk),
k
k
k为一个常数,容易被卡。
floyd
最纯粹最好理解的暴力最短路。令 d p [ i ] [ j ] dp[i][j] dp[i][j]表示从 i i i到 j j j的最短路,再对其进行松弛操作处理即可。
松弛
由于直接从 i i i到 j j j可能不是最优解,考虑再引入一个点 k k k。如果先从 i i i到 k k k,再从 k k k到 j j j更优,则更新 d p dp dp数组。
K e y c o d e s Key \ codes Key codes:
void floyd() {
for(int k = 1; k <= n; k++) { //顺序不能随意改动
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
}
}
}
}
时间复杂度: O ( n 3 ) O(n^3) O(n3)
例题.1
源自:HDU 6805
题意简述:
给定一张无向图。有一个人送蛋糕,从
s
s
s号点送到
t
t
t号点,当他经过第
i
i
i号点时,如果该点为
l
e
f
t
left
left点,就必须用左手拿蛋糕,如果该点为
r
i
g
h
t
right
right点,就必须用右手拿蛋糕,如果该点为
m
i
d
d
l
e
middle
middle点,那么左右手都行。给出每次换手的时间,求从
s
s
s号点到
t
t
t号点的最快时间。
换手时将无法行动。
输入:
第一行一个
t
t
t,表示数据总数。
每个数据的第一行
5
5
5个数,表示点的总数,路径的总数,起点,终点,以及换手的时间
输出:
共
t
t
t行,分别表示第
i
i
i号数据的答案
S a m p l e i n p u t Sample \ input Sample input:
1
3 3 1 3 100
LRM
1 2 10
2 3 10
1 3 100
S a m p l e o u t p u t Sample \ output Sample output:
100
数据范围:
1
≤
n
≤
1
0
5
1 \leq n \leq 10^5
1≤n≤105,
1
≤
m
≤
2
×
1
0
5
1 \leq m \leq 2 ×10^5
1≤m≤2×105
换手的时间和路径的长度均不大于
1
0
9
10^9
109
解析
拿到这道题,应该可以迅速想到用
D
i
j
k
s
t
r
a
Dijkstra
Dijkstra进行求解,关键在于左右手的限制。
我们可以将每个点拆成
2
2
2个点,分别表示能用哪只手到该点。
for(int i = 1; i <= n; i++) {
if(c[i] == 'L') {
tix[i].push_back(++tot);
tix[i].push_back(-1);
}
else if(c[i] == 'R') {
tix[i].push_back(-1);
tix[i].push_back(++tot);
}
else {
tix[i].push_back(++tot);
tix[i].push_back(++tot);
}
}
在建边时,我们利用此 t i x tix tix数组,可以直接求出从 i i i号点到第 j j j号点是否需要换手,再将权值进行修改。
for(int i = 1; i <= m; i++) {
int u, v, w;
SF("%lld%lld%lld", &u, &v, &w);
for(int j = 0; j < 2; j++) {
if(tix[u][j] == -1) continue;
for(int k = 0; k < 2; k++) {
if(tix[v][k] == -1) continue;
add(tix[u][j], tix[v][k], w + p * (j != k)); //j!=k说明手不一样,需要加上换手的费用
add(tix[v][k], tix[u][j], w + p * (j != k));
}
}
}
最后建一个虚点连接起点被拆成的那两个点,而答案在终点被拆成的那两个点里面。取
m
i
n
min
min即可。
想到拆点这题就很傻。
K
e
y
c
o
d
e
s
Key \ codes
Key codes已给出,不给完整代码。
例题.2
源自:没找到
题意简述:
给定一张无向图。一个人要询问
q
q
q次
i
i
i到
j
j
j的最短路,输出答案。
注意:
此题的路径为小数,需要一定的精度。当答案没有小数部分时,输出它的整数部分,否则保留一位小数。
如果
i
i
i到
j
j
j有多条直接连通的路径,他会选择最长的那条。
输入:
第一行3个整数,分别表示点的数量,边的数量和询问的次数
输出:
共
q
q
q行,分别表示第
i
i
i号询问的答案
S a m p l e i n p u t Sample \ input Sample input:
5 7 3
1 2 6
2 3 10
3 4 5
4 5 4
5 1 2
2 5 8
1 4 7
1 4
1 3
3 5
S a m p l e o u t p u t Sample \ output Sample output:
6
11
9
数据范围:
1
≤
n
≤
20
1 \leq n \leq 20
1≤n≤20
解析
拿到这道题,看到 n n n的范围如此友好,自然想到 f l o y d floyd floyd,之后除了一些细节问题,代码难度并不大,一道模板题。不给代码。
例题.3
源自:Vijos P1053
题意简述:
给定一张有向图。判断其是否存在负权环。
注意:
当这张图有负权环时,只能输出一个
−
1
-1
−1,否则输出源点
s
s
s到其它所有点的最短路。不连通输出
N
o
P
a
t
h
NoPath
NoPath。
输入:
第一行3个整数,分别表示点的数量,边的数量和源点
输出:
如“注意”所示。
S a m p l e i n p u t Sample \ input Sample input:
6 8 1
1 3 4
1 2 6
3 4 -7
6 4 2
2 4 5
3 6 3
4 5 1
3 5 4
S a m p l e o u t p u t Sample \ output Sample output:
0
6
4
-3
-2
7
数据范围:
2
≤
n
≤
1000
2 \leq n \leq 1000
2≤n≤1000,
1
≤
m
≤
1
0
5
1 \leq m \leq 10^5
1≤m≤105,
1
≤
∣
1 \leq |
1≤∣距离
∣
≤
1
0
6
| \leq 10^6
∣≤106
解析
首先解决如何判断负权环,可以用SPFA利用队列来进行判断,但这张图有可能不止一张,所以放弃SPFA转而使用dfs提前解决掉这种情况。后面操作很傻。
K e y c o d e s Key \ codes Key codes:
void dfs(int k) {
t[k] = 1; //是否搜索过
for(int i = head[k]; i; i = edge[i].next) {
int to = edge[i].to;
if(dis[to] > dis[k] + edge[i].w) {
dis[to] = dis[k] + edge[i].w;
if(vis[to]) { //重复进队,有负权环
PF("-1");
exit(0);
}
vis[to] = 1;
dfs(to);
vis[to] = 0;
}
}
}
NUM.2(生成树)
生成树一般求最小生成树,次小生成树可以用LCA进一步求出,但这里只讨论最小生成树
Prim
以点为基础单位。写法类似于Dijkstra,区别在于:最小生成树的本质也是一棵树,所以其边的数量等于点的数量
−
1
-1
−1,因此,可以定义一个
M
S
T
MST
MST变量,再每次取出
i
n
d
e
x
index
index后加上它对应的权值即可。
改动不大,不给代码,
Kruskal
以边为基础单位。本质为贪心,先对每条边的权值从小到大排序,然后一条一条地加入进最小生成树。由于树一定没有环,所以需要一个并查集来维护已加入最小生成树的点的父亲,当一条边想要加入的时候,判断一下它连接的两个点是否都已在最小生成树中,如果是,那就一定不能再加入。
K e y c o d e s Key \ codes Key codes:
void kruskal() {
for(int i = 1; i <= 2 * m; i++) {
int fu = find(s[i].u);
int fv = find(s[i].v);
if(fu != fv) {
fa[fu] = fv;
sum++;
ans += s[i].w;
if(sum == n - 1) return;
}
}
}
例题.1
源自:loj #10067
题意简述:
给定一棵树,你将有机会将其扩展成为完全图(即所有的点两两都有连边),请问你扩展成完全图后,能够取得的最小边权和。
输入:
第一行一个数表示数的点数,接下来
n
−
1
n-1
n−1行描述这棵树。
输出:
如题,最小边权和的值。
S a m p l e i n p u t Sample \ input Sample input:
4
1 2 1
1 3 1
1 4 2
S a m p l e o u t p u t Sample \ output Sample output:
12
数据范围:
1
≤
n
≤
1
0
5
1 \leq n \leq 10^5
1≤n≤105,答案不超过
l
o
n
g
l
o
n
g
long \ long
long long范围。
解析
拿到这道题,初看可能看不出来和生成树有什么关系。“蛤?这个题不都把树给我了吗”,但我们可以将每条边的两个点取出来,找到它们的父亲,如果要将其构造成完全图,显然需要增加
s
i
z
e
[
f
a
[
u
]
]
∗
s
i
z
e
[
f
a
[
v
]
]
−
1
size[fa[u]] * size[fa[v]] - 1
size[fa[u]]∗size[fa[v]]−1条边,而要使边权最小且不能小到比给出的最小生成树还小,显然权值设置成
s
[
i
]
.
w
+
1
s[i].w + 1
s[i].w+1最优。最小生成树满足无环,所以
s
i
z
e
[
f
a
[
u
]
]
size[fa[u]]
size[fa[u]]和
s
i
z
e
[
f
a
[
v
]
]
size[fa[v]]
size[fa[v]]一定不会互相包含。可以证明此答案无误。
对模板改动不大,不给代码。
NUM.3(拓扑排序/关键路径)
拓扑排序是在一个有向无环图
(
D
A
G
)
(DAG)
(DAG)中进行的,主要用于有限制的路径里面。比如要从
2
2
2到
3
3
3,就必须先从
1
1
1到
2
2
2。而拓扑排序解决的就是这样的一个问题,求出依次所经过的所有点的编号。
拓扑排序常用的模板有
2
2
2个,可以用
d
f
s
dfs
dfs或者
b
f
s
bfs
bfs求解,只给一份。
b
f
s
K
e
y
c
o
d
e
s
bfs \ Key \ codes
bfs Key codes:
void toposort() {
for(int i = 1; i <= n; i++) {
if(d[i] == 0) q.push(i); // 入度为0,先压入队列
}
while(!q.empty()) {
int tmp = q.front();
q.pop();
res[++len] = tmp; // 取出答案
for(int i = head[tmp]; i; i = edge[i].next) {
int V = edge[i].to;
if(--d[V] == 0) q.push(V); // 每次将入度-1,到0时压入队列
}
}
}
例题.1
源自:没找到
题意简述:
有台电脑被病毒入侵后,所有的字母被替换成了另一个字母。现在给出
n
n
n个按照字典序排序的字符串被修改后的顺序,请你帮助他还原一些字母。如果条件不足或有误,输出
−
1
-1
−1。
输入:
第一行一个整数
n
n
n,接下来
n
n
n行,表示被修改后的字符串(原本按照字典序排序),最后一行是你需要去还原的字母。
输出:
如果可能的话,输出还原后的答案,否则输出
−
1
-1
−1。
S a m p l e i n p u t Sample \ input Sample input:
6
cebdbac
cac
ecd
dca
aba
bac
cedab
S a m p l e o u t p u t Sample \ output Sample output:
abcde
数据范围:
1
≤
n
≤
50000
1 \leq n \leq 50000
1≤n≤50000,所有字母均为小写,每个字符串的长度不超过
100
100
100
解析
这道题由于字符串是由字典序排序的,所以当两个串之间第一个字符不一样时,我们就可能在它们之间连接一条有向边,如果第一个字符一样,就依次向后比较连边,最后跑一边拓扑排序就解决了此问题。当拓扑排序中的点比需要还原的字符的数量还少的话,自然输出
−
1
-1
−1。
细节较多,留给读者自己解决,不给代码。
NUM.4(LCA)
LCA解决的问题是求树上任意两点的最近公共祖先,利用倍增的思想,每次向上跳
2
j
2 ^ j
2j步,可以大大减少时间复杂度。当然还可以用其它方法求解LCA。
那么我们现在要考虑的是如何求出向上跳
2
j
2 ^ j
2j步后到的点。利用
b
f
s
bfs
bfs,先将树转化成无向的,然后考虑
d
p
dp
dp式。设
f
a
[
i
]
[
j
]
fa[i][j]
fa[i][j]为从
i
i
i号点向上跳
2
j
2 ^ j
2j步后所到的点。那么我们取出队头元素后,考虑修改它的儿子的
f
a
fa
fa值。设它的儿子为
t
o
to
to点,自己为
t
m
p
tmp
tmp点。显而易见的是当
t
o
to
to向上跳
1
1
1步时,刚好能跳到
t
m
p
tmp
tmp点。即:
f
a
[
t
o
]
[
0
]
=
t
m
p
fa[to][0] = tmp
fa[to][0]=tmp。然后,
t
o
to
to想要向上跳
2
j
2 ^ j
2j步,无异于先跳
2
j
−
1
2 ^ {j - 1}
2j−1步,然后再跳
2
j
−
1
2 ^ {j - 1}
2j−1步。那么方程就出来了。即:
f
a
[
t
o
]
[
j
]
=
f
a
[
f
a
[
t
o
]
[
j
−
1
]
]
[
j
−
1
]
fa[to][j] = fa[fa[to][j-1]][j-1]
fa[to][j]=fa[fa[to][j−1]][j−1]。至此,方程解决。
之后考虑如何求出LCA,我们可以在 b f s bfs bfs中求出每个点的深度 d [ i ] d[i] d[i],假设我们现在需要求出 x x x和 y y y的LCA,那么我们令 d [ y ] > = d [ x ] d[y] >= d[x] d[y]>=d[x],即 y y y比 x x x更深。然后由 y y y开始,一步步向上跳,跳到比 x x x低但不是太低的位置。接着 x x x和 y y y再一起跳。最后跳到一样的位置,再返回即可。
K e y c o d e s Key \ codes Key codes:
void bfs(int rt) {
queue<int> q;
q.push(rt);
d[rt] = 1;
while(!q.empty()) {
int tmp = q.front();
q.pop();
for(int i = head[tmp]; i; i = edge[i].next) {
int to = edge[i].to;
if(d[to]) continue;
d[to] = d[tmp] + 1;
fa[to][0] = tmp;
for(int j = 1; j <= 20; j++) fa[to][j] = fa[fa[to][j - 1]][j - 1];
q.push(to);
}
}
}
int lca(int u, int v) {
if(d[u] > d[v]) swap(u, v);
for(int i = 20; i >= 0; i--) {
if(d[fa[v][i]] >= d[u]) v = fa[v][i];
}
if(u == v) return u;
for(int i = 20; i >= 0; i--) {
if(fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
}
return fa[u][0];
}
例题.1
源自:没找到
题意简述:
在一棵树上有两个点,求它们两个的距离。
输入:
给定一棵有
n
n
n个点的树,有
q
q
q次询问,每次询问你需要回答输入的两个点的距离
输出:
共
q
q
q行,分别表示第
i
i
i次询问的答案。
S a m p l e i n p u t Sample \ input Sample input:
6
1 2
1 3
2 4
2 5
3 6
2
2 6
5 6
S a m p l e o u t p u t Sample \ output Sample output:
3
4
数据范围:
1 ≤ n ≤ 1 0 5 1 \leq n \leq10^5 1≤n≤105
解析
看到 n n n的范围,果断放弃 O ( n 2 ) O(n ^ 2) O(n2)的暴力,考虑倍增LCA。上面的模板已给出,那么我们求出LCA后,怎么进一步求出距离呢?画个图,一切都明白了: a n s = d [ x ] + d [ y ] − 2 ∗ d [ l c a ( x , y ) ] ans = d[x] + d[y] - 2 * d[lca(x, y)] ans=d[x]+d[y]−2∗d[lca(x,y)]
例题.2
源自:AHOI 2008
题意简述:
q
q
q次询问,每次询问有三个点,现在你需要再找一个点,使得这三个点到该点的距离最小。
输入:
先描述一棵树,然后有
q
q
q次询问,你需要进行处理。
输出:
共
q
q
q行,每行
2
2
2个整数,分别表示选择的点以及三个点到该点的距离和。
S a m p l e i n p u t Sample \ input Sample input:
6 4
1 2
2 3
2 4
4 5
5 6
4 5 6
6 3 1
2 4 4
6 6 6
S a m p l e o u t p u t Sample \ output Sample output:
5 2
2 5
4 1
6 0
数据范围:
1 ≤ n ≤ 5 × 1 0 5 1 \leq n \leq5 × 10^5 1≤n≤5×105
解析
此题显然用LCA求解最优。我们可以先求出这 3 3 3个点两两之间的LCA,显然的是:求出来的 3 3 3个LCA一定有 2 2 2个相同(在纸上画图),那么答案的那个点一定就在另一个不同的点上。对于此,读者可以自行证明,并不困难。
NUM.5(权值线段树)
普通的线段树维护的是区间的最值,那么权值线段树也与其相似。只不过权值线段树可以看做是一个桶,所维护的是一段区间的数的数量。
举个栗子:
10
1 1 2 3 3 4 4 4 4 5
其所对应的权值线段树为:
[1-10]: sum = 10
[1-5]: sum = 10
[1-3]: sum = 5
[1-1]: sum = 2
[2-3]: sum = 3
[2-2]: sum = 1
[3-3]: sum = 2
[4-5]: sum = 5
[4-4]: sum = 4
[5-5]: sum = 1
[6-10]: sum = 0
(懂力吧)
再说说一个小优化。
动态开点
当我们的 n n n值很大的时候,我们发现,题目所限制的空间已经不足普通线段树所需要的空间。为了优化,我们选择在需要使用节点时再开出此节点所需的空间。这就是所谓的动态开点。可以将空间优化到 O ( q log q ) O(q \log q) O(qlogq), q q q是询问的次数。
K e y c o d e s Key \ codes Key codes:
if(tree[p].lson == 0) tree[p].lson = ++tot;
add(tree[p].lson, id, l, mid);
if(tree[p].rson == 0) tree[p].rson = ++tot;
add(tree[p].rson, id, mid + 1, r);
例题.1
源自:没找到
题意简述:
给出
3
3
3种操作,分别如下:
- 1 x 1 \ x 1 x,将 x x x号位置的数 + 1 +1 +1。
- 2 x y 2 \ x \ y 2 x y,将 x x x到 y y y号位置的数清空。
- 3 x y k 3 \ x \ y \ k 3 x y k,输出在 x x x到 y y y号位置间的第 k k k大的数字的位置。
注意:这里的第
k
k
k大是允许重复的。
比如有这样一些数:
1 2 3 3 3
其第 1 1 1大,第 2 2 2大,第 3 3 3大的数我们都认为是 3 3 3,但位置可能不同。
输入:
给定
q
q
q次如上述所示的输入。
输出:
对于每一次的第
3
3
3种输入,输出一行表示其答案。
S a m p l e i n p u t Sample \ input Sample input:
20
1 3
1 2
1 1
1 1
1 2
1 3
1 2
1 3
3 1 3 1
3 1 3 2
3 1 3 3
3 1 3 4
3 1 3 5
3 1 3 6
3 1 3 7
3 1 3 8
3 1 2 1
3 1 2 2
3 1 2 3
3 1 2 4
S a m p l e o u t p u t Sample \ output Sample output:
3
3
3
2
2
2
1
1
2
2
2
1
数据范围:
1 ≤ n ≤ 1 0 9 1 \leq n \leq10^9 1≤n≤109, 1 ≤ q ≤ 1 0 5 1 \leq q \leq 10^5 1≤q≤105
放个图便于理解:
解析
看到 n n n的范围,显然需要动态开点。发现题目要我们解决的是第 k k k大的数,想到权值线段树。下面放出带注释的代码,读者可以自行阅读。
在此,笔者只写了与权值线段树和动态开点有关的注释,如果您对于线段树的掌握还不够熟练,建议再复习一遍线段树,有助于阅读下文。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<climits>
using namespace std;
#define SF scanf
#define PF printf
struct Tree {
int l, r, sum, lson, rson; //因为动态开点的缘故,需要保存左右子树的下标
bool lz;
}tree[2000005];
int tot = 1; //当前开了几个点
void lay_down(int p) {
if(tree[p].lz) {
tree[tree[p].lson].lz = tree[tree[p].rson].lz = 1;
tree[tree[p].lson].sum = tree[tree[p].rson].sum = tree[p].lz = 0;
}
}
void add(int p, int id, int l, int r) {
tree[p].l = l, tree[p].r = r;
if(l == r) { //权值线段树,以下标为单位
tree[p].sum++;
tree[p].lz = 0;
return;
}
lay_down(p);
int mid = l + r >> 1;
if(id <= mid) {
if(tree[p].lson == 0) tree[p].lson = ++tot;
add(tree[p].lson, id, l, mid);
}
else {
if(tree[p].rson == 0) tree[p].rson = ++tot;
add(tree[p].rson, id, mid + 1, r);
}
tree[p].sum = tree[tree[p].lson].sum + tree[tree[p].rson].sum;
}
void empty(int p, int l, int r) {
if(p == 0 || tree[p].lz == 1 || tree[p].sum == 0) return;
if(l <= tree[p].l && tree[p].r <= r) {
tree[p].sum = 0;
tree[p].lz = 1;
return;
}
lay_down(p);
if(l <= tree[tree[p].lson].r) empty(tree[p].lson, l, r);
if(tree[tree[p].rson].l <= r) empty(tree[p].rson, l, r);
tree[p].sum = tree[tree[p].lson].sum + tree[tree[p].rson].sum;
}
int querysum(int p, int l, int r) { //求出l到r的数的总和
if(p == 0 || tree[p].lz == 1 || tree[p].sum == 0) return 0;
if(l <= tree[p].l && tree[p].r <= r) return tree[p].sum;
int ans = 0;
lay_down(p);
if(l <= tree[tree[p].lson].r) ans += querysum(tree[p].lson, l, r);
if(tree[tree[p].rson].l <= r) ans += querysum(tree[p].rson, l, r);
return ans;
}
int query(int p, int l, int r, int x) {
if(p == 0 || tree[p].lz == 1 || tree[p].sum == 0) return -1;
if(tree[p].l == tree[p].r) {
if(tree[p].sum >= x) return tree[p].l; //当当前根节点的数的总数比查询的x大,那么x的位置一定在这里面
return -1;
}
int cnt = 0, mid = tree[p].l + tree[p].r >> 1;
lay_down(p);
if(mid < r) {
cnt = querysum(tree[p].rson, l, r);
if(cnt >= x) return query(tree[p].rson, l, r, x); //如果右子树的数的总数比x大,那么x的位置一定在右子树里面
}
if(l <= mid) return query(tree[p].lson, l, r, x - cnt); //否则在左子树中查询第x-cnt大的数
return -1;
}
int main() {
int q;
SF("%d", &q);
while(q--) {
int f, l, r, x;
SF("%d", &f);
if(f == 1) {
SF("%d", &x);
add(1, x, 1, 1000000000); //权值线段树的l和r坐标有点不同哦~
}
else if(f == 2) {
SF("%d%d", &l, &r);
empty(1, l, r);
}
else {
SF("%d%d%d", &l, &r, &x);
int ans = query(1, l, r, x);
PF("%d\n", ans);
}
}
return 0;
}
NUM.6(欧拉回路)
- 欧拉回路是指从一个点出发经过所有点而不重复经过任何一个点后又回到了出发点的一条路径。
- 欧拉路径是指从一个点出发经过所有点而不重复经过任何一个点的一条路径。
注意区分它俩。
显然,区别在于一个要回到起点,而另一个可以在任意一点结束。所以求法不同。
欧拉回路
我们知道,一张图肯定可以分为有向图和无向图。所以我们要进行分类讨论。
有向图
结论:如果所有点的入度都等于该点的出度,那么这张图存在欧拉回路。
无向图
结论:如果所有点的度均为偶数,那么这张图存在欧拉回路。
欧拉路径
当然,也要分为有向图和无向图分别进行讨论。
有向图
结论:如果有且仅有两个点入度和出度不同,并且其中一个点入度比出度大1,另一个点出度比入度大1
,那么这张图存在欧拉路径。无向图
结论:如果有且仅有两个点度数不为偶数,那么这张图存在欧拉路径。
需要补充的是:如果一张图存在欧拉回路,那一定存在欧拉路径。
对于这个知识点而言,需要熟练掌握的只有定义和结论,题目的话只要会一些图的基本操作,加上上述的判断,问题应该不会很大。
未完待续ing…(肝不动啦啊啊啊)