C

题目

https://codeforces.com/contest/1685/problem/C

给你一个 n个左括号 n个右括号 的序列

最小次数, 翻转子区间,让整个括号合法

范围

n 1e5

1s

256MB

题解

我的思路

括号嘛, 认知还在任意前缀1的个数大于等于0的个数

想的是先转换成0/1,两个指针包裹 两头贪心

没了, 没思路了, 感觉贪心都不对的

写了果然wa了

官方

别统计1和0, 直接 左括号1贡献,右括号-1贡献 XD

同样的也就是前缀和 大于等于0

一个数学结论, 至多两次就能让结果合法

如果 前缀i是 所有前缀中最大的,那么翻转 [1..i] [i+1,2n]

因为 对于 j<=i,新的前缀 newpre[j] = pre[i] - pre[j] >= 0

因为 对于 j> i,新的前缀 newpre[j] = pre[2n] - pre[j] + pre[i] = pre[i] - pre[j] >= 0


那么问题变成有没有办法一次翻转, 因为0次是直接合法,很容易判断,2次有上述方案

对于一次反转, 如果是[L,R], 那么必然有 L <= 首个负前缀l, R>= 最后一个负前缀r

再数学一点 对于 $i \in [L,R] $, newpre = pre[R] - pre[i-1] + pre[L] >= 0

pre[i-1] <= pre[L] + pre[R] 也就是 区间里所有的都不大于两头的和

pre[i] 的可选值是 [L..l-1][l..r][r+1..R]

注意到[l..r] 始终被选, 而两头的随着LR变化

如果L[0..l-1]中最大

如果R[r+1..2n]中最大

那么对于两头的来说, 一定成立,而对[l..r] 来说 它们是能选到的最大的,如果这个还不满足,则没有办法了

如果这个满足则是答案

代码

https://codeforces.com/contest/1685/submission/159099077

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define MOD 1000000007
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
#define per(i,a,n) for (ll i=n;i-->(ll)a;)
#define pb push_back

ll read(){ll r;scanf("%lld",&r);return r;} // read
// n对括号
// reverse substring

char s[200010];
int n;

int pre[200010];

void calc(int st){
int last = -1;
per(i,0,n){
if(pre[i+1] < 0){
last = i;
break;
}
}
// printf("[%d %d]\n",st,last);
int ml = 0;
int mr = 0;
rep(i,0,st){
ml = max(ml,pre[i+1]);
}
rep(i,last,n){
mr = max(mr,pre[i+1]);
}
rep(i,st,last+1){
if(pre[i+1] > ml+mr){
// rev2
printf("2\n");
int maxi = 0;
rep(i,0,n){
if(pre[i+1] > pre[maxi]){
maxi = i+1;
}
}
printf("1 %d\n",maxi);
printf("%d %d\n",maxi+1,n);
return ;
}
}
printf("1\n");
int maxl = 0;
rep(i,0,st){
if(pre[i+1] > pre[maxl]) maxl = i+1;
}
int maxr = n-1;
rep(i,last,n){
if(pre[i+1] > pre[maxr]) maxr = i+1;
}
printf("%d %d\n",maxl+1,maxr);
}


void w(){
n = read();
n*=2;
scanf("%s",s);
rep(i,0,n) pre[i+1] = pre[i] + (s[i] =='('?1:-1);
rep(i,0,n){
if(pre[i+1] < 0){
calc(i);
return ;
}
}
printf("0\n");
}


int main(){
int t = read();
while(t--)w();
return 0;
}

D

题目

https://codeforces.com/contest/1685/problem/D1

https://codeforces.com/contest/1685/problem/D2

给一个1到n的排列p

定义一个排列p的代价为

$\sum {q_i - p_{q_{i+1}}}$

找最小代价的排列q

D2: Hard version: 最小代价排列中字典序最小的

范围

t 100 组测试

n 200, $\sum{n} \le 400$

1s

256mb

题解

我的思路

注意到上面的求和表达式,也就是每一项和它的后一项的差的绝对值,

那么如果一个排列q合法,那么 对它循环的旋转也合法

再来看期望最小值, 如果能够成 |1-1|+..+|v-v| ,全部是相同相减, 那么最小就是0, 而这种需要所有的跳转关系构成一个大环, 而这样解法也就唯一(对于循环的最小来说)

以样例中的2 1, => |1 - (P2=1)| + |2 - (P1=2)| = 0

对于不够成大环的, 必定跳转关系是多个小环

以样例中的2 3 1 4, 这样 是 1->3->2 构成环 ,4 单独一个环, 那么如果让环内代价为0, 那剩下的就是两头的链接代价,

|1 - (P3=1)| + |3 - (P2=3)| + |2 - (P4=4)| + |4 - (P1=2)| = 2+2

|3 - (P2=3)| + |2 - (P1=2)| + |1 - (P4=4)| + |4 - (P3=1)| = 3+3

|2 - (P1=2)| + |1 - (P3=1)| + |3 - (P4=4)| + |4 - (P2=3)| = 1+1

其实是环中选出一个值 和 其它环作拼接, (这里保证环内最小 不知道细节怎么证,但感觉看起来这样贪没啥问题

再比如样例 5 4 3 2 1, 环分别是 1->5, 2->4, 3

分别拿出来1,2,3

(5->1) (3) (4->2)

代价就是 |1-3| + |3-2| + |2-1|

这里也很清晰的是, 这样如果确定了拿出来的值,那么最小代价 = 2|max - min|


综上所述

  1. 找环

  2. 每个环拿出一个值来连接, 让所有拿出来的值 最大减最小尽量小, 这样D1 做完了

  3. 需要在这样的方法中, 1. 正负顺序, 2, 循环平移到1开始 的字典序列最小


问题来了

找环很简单, 但是如何让每个环拿出来一个值,差尽量小?

这里我想到的是染色+滑动窗口, 记录最小的滑动窗口和位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define MOD 1000000007
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
#define per(i,a,n) for (ll i=n;i-->(ll)a;)
#define pb push_back

ll read(){ll r;scanf("%lld",&r);return r;} // read

int p[210];
int p2i[210];
int c[210]; // 染色
void w(){
int n = read();
fill(c+1,c+n+1,-1);
rep(i,1,n+1) {
p[i] = read();
p2i[p[i]] = i;
}
int color = 0;
rep(i,1,n+1) {
if(~c[i])continue;
int itr = i;
do{
c[itr] = color;
itr = p2i[itr];
}while(itr != i);
color++;
}
// 单个环唯一
if(color == 1){
int itr = 1;
do{
printf("%d ",itr);
itr = p2i[itr];
}while(itr != 1);
printf("\n");
return ;
}
vector<int>cnt(color,0); // 当前滑窗各种颜色个数
// 答案 起始位置
int ansst = 0;
int anssz = n+1;
// 滑窗
int l = 1;
int r = 0;
int cur = 0; // 滑窗内颜色种数
while(cur < color && r+1 < n+1) if(++cnt[c[++r]] == 1) cur ++;
// [l..r] 包含所有颜色
while(l <= r){
if( r-l+1 <= anssz){
anssz = r-l+1;
ansst = l;
// printf("[%d %d]\n",l,r);
}
if( -- cnt[c[l++]] == 0 ) cur--;
while(cur < color && r+1 < n+1){
++r;
cnt[c[r]] ++;
if(cnt[c[r]] == 1) cur ++;
}
if(cur < color)break;
}
// [ansst..ansst+anssz-1]
fill(c+1,c+n+1,-1);
rep(i,ansst,ansst+anssz - 1 + 1){
if(~c[i])continue;
int itr = i;
do{
printf("%d ",p2i[itr]);
c[itr] = 1;
itr = p2i[itr];
}while(itr != i);
}
printf("\n");
}
int main(){
int t = read();
while(t--)w();
return 0;
}

然而实现以后wa2的第11个样例了

1
2
4
1 3 2 4

如果按照我上面所说的, (2,3) (1) (4) 这样的三个环, 那么 最大最小差是|4-1| = 3, 所以答案是6

然而, 给了一个拆掉环还更小的方法

q = 1 3 4 2

|1 - P3| + |3 - P4| + |4 - P2| + |2 - P1| = |1 - 2| + |3 - 4| + |4 - 3| + |2 - 1| = 4

emmmmmmm

所以我的思路的细节并卜行

官方 D1

也是先考虑什么时候可以得到零

也是按照 跳转构成的环 来看, 假设有k个环

跨环 的链接 至少是1, 所以下界是 2(k-1)


给出一种构造方法

初始化 p1 = p

for x = 1..n-1 如果 对当前p1 来说 x和 x+1在不同的环中, 则交换他们

显然根据学过的 排列的环的性质来讲, 每次交换两个环里的值 相当于把两个环合并

那么 也就是k-1次操作就可以全部合并成一个环


最后 $q_i = p1_{q_{i+1}}$ 了, 显然这就是一个环, 这个答案对于p1来说,就是0

但我们求的是对于p

$|q_i - p1_{q_{i+1}}| = |p1_{q_{i+1}} - p_{q_{i+1}}|$ 了, 反过来看操作毕竟交换是对称的, 考虑从p1变到p, 每一次交换至多会让结果+2, 因为交换的是两个相邻的值, 所以 答案不大于2(k-1)

综上 从下界看 不小于2(k-1), 从操作上看不大于2(k-1), 所以这个方案就是2(k-1)


至此 并查集随便搞一搞 D1 就做了

D2 现场只有4个人AC了 XD

pi向i连一条有向边

问题变成, 添加一些 i->i-1 和 i-1 -> i 变成 存在欧拉回路

其实和上面 等价的, 这里的环和上面的边对应, 而成欧拉回路, 就是和变成新的环

代码

D1

https://codeforces.com/contest/1685/submission/159201728

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define MOD 1000000007
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
#define per(i,a,n) for (ll i=n;i-->(ll)a;)
#define pb push_back

ll read(){ll r;scanf("%lld",&r);return r;} // read

int p[210];
int p2i[210];
int vis[210]; // 染色
int fa[210];

int getfa(int i){ return i==fa[i]?i:(fa[i] = getfa(fa[i]));}

void link(int u,int v){
int fu = getfa(u);
int fv = getfa(v);
if(fu == fv)return;
fa[fu] = fv;
}

void w(){
int n = read();
fill(vis+1,vis+n+1,false);
iota(fa,fa+n+1,0);
rep(i,1,n+1) {
p[i] = read();
p2i[p[i]] = i;
}
rep(i,1,n+1){
if(vis[i])continue;
int itr = i;
do{
vis[itr] = true;
link(itr,i);
itr = p2i[itr];
}while(itr != i);
}
rep(v,1,n){
if(getfa(v) == getfa(v+1))continue;
swap(p[p2i[v]],p[p2i[v+1]]);
swap(p2i[v],p2i[v+1]);
link(v,v+1);
}
int itr = 1;
do{
printf("%d ",itr);
itr = p2i[itr];
}while(itr != 1);
printf("\n");
}
int main(){
int t = read();
while(t--)w();
return 0;
}

总结

C

括号匹配还是不熟, 1,-1贡献 比1和0统计好很多

这最大值翻转只需要两次也是妙啊

后面的切割和最值

完全就是math,math,math

D1

想到环 和 环之间是好的

但是我构造能力实在是太菜了

而且下界估计想法也有问题,错误的下界估计也会影响思路

感觉这个题还是属于排列的环相关的知识点

然后有上下界相等, 和操作与逆向操作对结果的影响

参考

官方

https://www.cnblogs.com/QQQ0000/p/16321569.html

E

题目

https://codeforces.com/contest/1682/problem/E

给一个不含重复数字的1~n的排列数组a

然后有人通过m次交换,让数组有序, 每次交换记录了被交换数字的坐标(i,j)

其中交换次数是最小次数

现在把交换的坐标对的顺序打乱了给你, 问怎么把交换数组重排序让它恢复, 保证合法

范围

n 2e5

m <= n-1

ai <= n

2s

256MB

题解

我的思路

先考虑自己会怎么交换,能够最小的次数

如果给你的数组, 有多个坐标和值构成环 , 那么最小次数 = (这些环长-1)的和

而每次交换一定让一个位置跑到合法的位置上,并且跑到合法以后不会再动这个位置

因此两个位置只会出现一次不会重复

或者从环的角度看,一次操作,就是 让环的一条边没了连接环的两端

所以考虑类似做拓扑排序, 每次选择一个 交换后有合法产生且能让 目标不再被操作的进行处理

问题在比赛时重复添加的bug没考虑清楚, 但即使修复了重复添加 依然wa5

官方

还好wa5数据小(当然比赛时看不了数据

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

果然我想的交换 虽然次数上表述是对的,但是操作上不一定是删了环上的边,

而是可以交换环上任意两点, 这样的话, 如果是环边,就是环-1

如果不是环边实际上是把环切割成两个小环,而总代价不会减少

而如果是这样,上面的实现当然也不对了,不再是交换后不会再交换了


举一个例子来说

a->b->c->d->e->f->a: 意思是位置a的值应该要去位置b, 位置b的值应该要去位置c …

那么如果交换了位置a位置e

那么新的来说 位置e的值需要去位置b

也就是说当发生(位置i,位置j) 交换以后

i和j就不再属于同一个环了, 并且它们分别属于它们的来源的环

再去掉无关的抽象一次 x0->x1->....->y0->y1->..., 如果(x1,y1)交换, 则得到这样两个环 x0->x1) (....->y0->y1) (...


这样于是就有了 假设x和多个y换,如(x,y0),(x,y1)

x->....->y0->...->y1->...,

那么对于x来说,它和这些y的顺序一定是按箭头方向从近到远的

因为 如果先换了y1,就会变成x) (...->y0->...->y1) (..., 这样x都和y0不在同一个环上,再交换会合并环而不是拆环了

那么对于有x的所有交换就有了拓扑关系, 因为交换的对称性, 所有的交换序列都有了拓扑关系, 然后建立个拓扑图, 随便拓扑排序一下就好了


实现要素

找环, vis数组 + dfs 随便搞

把交换和环一起处理, vector<int> [idx] = 发生了的交换

环上可以做的是值 -> 下标

建立拓扑, 再从环中提取这些交换 并建立拓扑, 判断前后就是 (下标差 + 环长)% 环长 就知道前后了

拓扑排序, 维护入度计数 和 入度为0的点的数组

代码

无(鸽子)

F

题目

https://codeforces.com/contest/1682/problem/F

长n 数组a, 非严格递增排序

长n 数组b, bi != 0

一共q组询问,每次询问l,r, 保证sum(b[l..r]) == 0

b[l..r] 中 小于0的点作为左侧点, 大于0的点作为右侧点, 建立二分图

左侧点i 向 右侧点j 有一条 无限容量,每单位flow代价 为 abs(ai - aj) 的边

源点S, 汇点T

S->左侧i, cost = 0, 容量|bi|

右侧j->T, cost = 0, 容量|bj|

问你, 图的最大流的最小cost为多少, 答案 mod 1e9+7

范围

n 2e5

q 2e5

ai [0,1e9]

bi [-1e9,1e9], bi != 0

3s

256mb

题解

我的思路

而如果和为零,其实也就是说 负数和 = 正数和

那么建立的图,左右侧点连接的边都是无限容量, 而和源点汇点的边容量为 |bi|

所以其实最大流显然是 abs|负数和/或正数和|

换句话说 不需要S和T

就是每个左侧点发出|bi|的流量,每个右侧点接受|bi|的流量, 然后 左侧i到右侧j, 的无线容量的边,每单位流量 = |ai-aj|的代价

问最小代价


如果单次, 贪心

是不是左侧按照ai大小排序,右侧也按照ai大小排序

然后每次最小未输的左侧和右侧点进行1单位flow

证明, 如果有交差(左小到右大,右小到左大)那么必然交换1单位结果更小,而唯一不存在交叉的就是都按照ai排序

代价 = ai排序后 对应输送

或者看成所有的 |bi|个ai 进行排序分别得到l数组和r数组

然后答案 = sum{abs(r[i] - l[i])}

这样如果是单次查询就没啥东西


题目条件中说了ai非严格单调递增

因此不需要自己去排序了

但我并不知道怎么维护,能进行多次查询

官方

上面我的思路的结论是没问题的,但是在计算代价时实际上可以变成 不是去排序

初始化, 大于零和小于零bi绝对值和都为0, 分别记作 S+, S-,

然后遍历i从l到r, 每次遍历后更新S+,S-

如果 当前bi > 0 且 S+ >= S-, 那么说明 这一部分的ai在计算绝对值时全部取负号,因为它要和比它大的ai配

所以贡献为 -ai * |bi|

如果 当前bi > 0 且 S+ < S-, 那么说明 这一部分的ai在计算绝对值时, 有min((S-) - (S+), |bi|)个取负号, 剩下的取负号,因为它一部分和前面配对,一部分和后面配对

所以贡献为 ai * min((S-) - (S+),|bi|) - ai * (|bi| - min((S-)-(S+),|bi|)) = ai * (2min((S-)-(S+),|bi|) - |bi|)

如果 当前bi < 0 且 S+ <= S-, 那么说明 这一部分的ai在计算绝对值时全部取负号,因为它要和比它大的ai配

所以贡献为 -ai * |bi|,和上面同理

bi<0, S+ > S- 也是一样的

综上 都需要ai乘, 那么变化的是ai的贡献的次数, 而这个次数相关的就是 [b[l]..b[i-1]] 的正负绝对值和的差, 再和bi的大小关系

显然 这样的思考方式比我排序依次减和绝对值求和的效率高,因为对于每个i是O(1)的,总代价就是O(r-l), 而我的那样需要O(sum(|b[l..r]|))

而上面的 (S+)-(S-) 其实 等于 sum{b[l..i-1]}


后缀 变形(也可以前缀变形,同理, 计算[0..i]

如果按上述的方法,计算了 [i..n] 的结果, 记录为 res[i]

那么对于查询[l..r], 且 sum{b[l..r]} == 0, 那么答案就是 res[l] - res[r+1], 因为 [l..r]为0了, 所以从r+1开始向后运算时, 一定是正负绝对值差是0

当然这个直接暴力计算res的代价是$O(n^2)$

反过来从贡献角度考虑

a[i] 要 贡献给 res[j], j<=i

与 ai,bi, sum{b[j..i-1]} = pre[i-1] - pre[j-1] 有关

而对于具体的i, ai,bi,pre[i-1]是常量, pre[j-1]随着j变化

pre[i-1]-pre[j-1] 根据上面的推论, 有两头段会让a[i] 常数贡献, 中间一段与pre[j-1]线性关系

考虑 {pre[j-1],j } 二元组排序, 注意 j<=i 的范围限制

然后就变成 区间贡献统计, 区间线性贡献统计, 上树状数组或者线段树?


具体一点

前缀和$p_i = \sum_{k=1}^{i} b_k$

$j \le i$


$b_i > 0$ 时

若 $p_{i-1} - p_{j-1} \ge 0$, 有 $res_j += a_i * -b_i $

若 $p_{i-1} - p_{j-1} \le -b_i$, 有 $res_j += a_i * b_i $

若 $-b_i < p_{i-1} - p_{j-1} < 0$, 有 $res_j += a_i * ( 2p_{j-1} - 2p_{i-1} - b_i ) $


$b_i < 0$ 时

若 $p_{i-1} - p_{j-1} \le 0$, 有 $res_j += a_i * -b_i $

若 $p_{i-1} - p_{j-1} \ge - b_i$, 有 $res_j += a_i * b_i $

若 $ 0 < p_{i-1} - p_{j-1} < - b_i $, 有 $res_j += a_i * ( 2p_{j-1} - 2p_{i-1} - b_i ) $


问题是, 不只是需要 满足 大小关系, 还需要范围, 而且p的排序后下标就不再连续

先不考虑$j \le i$

建立个 下标数组, 按照$p_i$ 大小排序

那么 对于i 来说, 它对三个连续范围内每个贡献 常数($a_i \cdot -b_i$ 或 $ a_i \cdot b_i $ 或 $ a_i \cdot (-2p_{i-1} - b_i) $) / 线性函数的系数 $2a_i$

这样 当你要求具体 查一个位置的时候, 就树状数组 求点值

而这个操作 可以通过 差分数组+树状数组完成, 范围增加 = 范围起始点差值+, 范围结束点差值-, 单点值 = 前缀和


剩下的问题是如何控制$j \le i$

考虑扫描指针,先让所有点都贡献,然后 随着扫描指针从1到n,增加它的反贡献 相当于去掉它的贡献

这样的话我们就能算出每个res[i] = 单点常数 + 单点线性系数$\cdot p_{i-1}$

最后所有询问 直接 res[l] - res[r+1]

jiangly

似乎jiangly的比官方题解更简单, 他做了a数组的差分, 直接用差分和前缀b算的, 没有再用原始的b和a了

在白板上画了一下jiangly老哥的代码

发现jiangly老哥的想法其实 有点牛顿积分->勒贝格积分的味道

$i \in [l,r]$

我们以a[i]-a[i-1] 这段间隔贡献的长度来看

发现, 假设以j开头,那么这段的贡献的长度为$|p_{i-1} - p_{j-1}|$

鹅妹子嘤!!!!!

这直接和单个bi没关系,也不用大于零小于零分类和范围分类讨论了, 只和b前缀和 与 a差分相关了

而且简洁到 对ans[l..r]贡献就是$(a[i] - a[i-1]) * |p_{i-1} - p_{l-1}|$

注意这里和题解也不一样, 不需要先后缀数组res, 再去求差, 直接算每个位置对答案的贡献


剩下的就一样了

为了解决绝对值的问题, 对$p_i$排序

因为对于一个具体的查询来说 j是给定值,所以 你需要的是$(a[i] - a[i-1]) * p_{i-1}$的和 与 $a[i] - a[i-1]$ 的和

对于 $p_{i-1} > p_{j-1}$ 的 正贡献,而 $p_{i-1} < p_{j-1}$ 的负贡献

所以计算答案时, 从$p_i$ 从小到达算, 并且根据$p_i$的指针更新 每个位置i 贡献的正负

代码

https://codeforces.com/contest/1682/submission/158828392

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#include <bits/stdc++.h>
// jiangly
// https://codeforces.com/contest/1682/submission/158055817
// power 和 norm 和std有冲突
using namespace std;
typedef long long ll;
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
#define per(i,a,n) for (ll i=n;i-->(ll)a;)

ll read(){ll r;scanf("%lld",&r);return r;} // read

// 基于 mod P 的整数, structZ
constexpr int P = 1000000007;
// assume -P <= x < 2P
int mynorm(int x) {
if (x < 0) {
x += P;
}
if (x >= P) {
x -= P;
}
return x;
}
template<class T>
T mypow(T a, ll b) {
T res = 1;
for (; b; b /= 2, a *= a) {
if (b % 2) {
res *= a;
}
}
return res;
}
struct Z {
int x;
Z(int x = 0) : x(mynorm(x)) {}
Z(ll x) : x(mynorm(x % P)) {}
int val() const {
return x;
}
Z operator-() const {
return Z(mynorm(P - x));
}
Z inv() const {
assert(x != 0);
return mypow(*this, P - 2);
}
Z &operator*=(const Z &rhs) {
x = ll(x) * rhs.x % P;
return *this;
}
Z &operator+=(const Z &rhs) {
x = mynorm(x + rhs.x);
return *this;
}
Z &operator-=(const Z &rhs) {
x = mynorm(x - rhs.x);
return *this;
}
Z &operator/=(const Z &rhs) {
return *this *= rhs.inv();
}
friend Z operator*(const Z &lhs, const Z &rhs) {
Z res = lhs;
res *= rhs;
return res;
}
friend Z operator+(const Z &lhs, const Z &rhs) {
Z res = lhs;
res += rhs;
return res;
}
friend Z operator-(const Z &lhs, const Z &rhs) {
Z res = lhs;
res -= rhs;
return res;
}
friend Z operator/(const Z &lhs, const Z &rhs) {
Z res = lhs;
res /= rhs;
return res;
}
friend istream &operator>>(istream &is, Z &a) {
ll v;
is >> v;
a = Z(v);
return is;
}
friend ostream &operator<<(ostream &os, const Z &a) {
return os << a.val();
}
};

// 树状数组 0-index
template <typename T>
struct Fenwick {
const int n;
vector<T> a;
Fenwick(int n) : n(n){
a.resize(n);
}
// [x] += v
void add(int x, T v) {
for (int i = x + 1; i <= n; i += i & -i) {
a[i - 1] += v;
}
}
// [0..x)
T sum(int x) {
T ans = 0;
for (int i = x; i > 0; i -= i & -i) {
ans += a[i - 1];
}
return ans;
}
// [l,r)
T rangeSum(int l, int r) {
return sum(r) - sum(l);
}
};

int main() {

int n = read(), q = read();

vector<int> a(n);
vector<ll> b(n + 1);
rep(i,0,n) a[i] = read();
// 倒序做差分
per(i,1,n) a[i] -= a[i - 1];
rep(i,1,n+1) {
b[i] = read();
// 前缀和
b[i] += b[i - 1];
}
// 离线
vector<array<ll, 4>> qry(q); // 查询 按照 {b[l-1],l-1,r,qidx} 排序
rep(i,0,q) {
int l = read()-1, r = read();
qry[i] = {b[l], l, r, i};
}
sort(qry.begin(), qry.end());

// https://en.cppreference.com/w/cpp/algorithm/ranges/iota
// https://www.cplusplus.com/reference/numeric/iota/
// 按照bi 前缀和 大小排序下标
vector<int> p(n);
iota(p.begin(), p.end(), 0);
sort(p.begin(), p.end(), [&](int i, int j) { return b[i] < b[j]; });

Fenwick<Z> s(n), c(n);

rep(i,0,n) {
// 先全部正贡献
s.add(i, Z(b[i]) * a[i]);
c.add(i, a[i]);
}
vector<Z> ans(q);

int j = 0;
for (auto [v, l, r, i] : qry) {
while (j < n && b[p[j]] <= v) { // 根据 bi 前缀大小 决定贡献正负
int k = p[j++];
// 树状数组不支持修改, 只支持增量 ,实际是改成负贡献
s.add(k, -2*Z(b[k]) * a[k]);
c.add(k, -2*a[k]);
}

ans[i] = s.rangeSum(l, r) - c.rangeSum(l, r) * v;
}

for (auto x : ans) {
printf("%d\n",x.val());
}
return 0;
}

总结

E 的关键在于

不只有相邻的可以换, 不相邻的同环上也可以换(Wa5, 这点还是应该枚举稍微大一点的, 其实wa5 的点数才4

交换同环 = 拆环, 交换异环 = 合环

而交换两点,** 这两点分别属于它们前个点的环** 从而推得 同点和其它点多次交换时的先后顺序

有了先后顺序的逻辑,后面拓扑就无脑做了

F

简化的部分做了

但是 在排序对应 相减 取 绝对值 求和的部分, 没有想到怎么转换成 正负号标记, 还是说明绝对值相关知识点不够熟练

而即使看题解时 知道了这样转化, 也没有变成后缀来求的思路, 还在想分治

而且后缀的思路也是提供一个叫 无效答案同规则的差是可以得到有效答案的, 就像函数补充中间点一样


看jiangly的代码, 一个是 基于mod的 struct,可以让逻辑代码里完全不需要写mod 也不用担心写掉, 减少心智负担

第二个是 没有 using namespace std 减少碰撞

iota 可以填数组, sort+lambda 简化排序

另外就是 using namespace std; 是个坏习惯, 比如这里norm和power就有冲突

参考

官方

jiangly

F

题目

https://codeforces.com/contest/1684/problem/F

长n数组a

m个区间[l,r]

自己任选一个范围(与上面的区间无关),修改区间中所有值成任意值, 让上面区间每个区间都没有重复的数

问 你任选的区间最短长度

范围

n 2e5

m 2e5

ai 1e9

2s

256MB

题解

我的思路

ai和n不成比例,与具体值无关, 无脑离散化一下,把ai范围降低到n

对于一个区间[l,r], 如果覆盖了一侧,如[l0,r],那么其实很好求l0的最大值(因为要区间尽量小

只需要通过v2idx[v] = last index, 跳一跳就行了

那么其实可以得到 这些l0 中最小的l0, 记为 L, 同样可以得到最大的R

那么 答案一定是包含了[L,R]

那么问题变成了, 如果就是给你一个区间,但是是部分覆盖如何做到最短, [l,r] 你要找 [l...[L..R]...r]

其中 [l..L-1][R+1..r] 不存在重复的数,还要[L,R]最短

方法

如果答案是[L,R] 也就是 任何给的线段[l,r]中,不存在相同值都不属于[L,R],

先让L = 1, 那么找r 跟上面说的一样, 找到max(min(ri))

然后,如果L左移1,R会如何变化

如果[L+1,R] 满足则就是R否则R只能增大, 甚至 L+1就无法合法了

注意到 如果有同样的l ,那么只用考虑r更大的即可


[L..R] => [L+1..?]

首先 如果 [lastpos[v[L]]...L] 被包含在某个区间中, 那么必定不可行, 之后更大的L也不可行了break掉

如果 大于R的 value = v[L]的 位置在p

[L...p]在某个区间中, 那么必定[L+1..R]不合法

[L+1...p] 则是新的合法的


上面两个都需要的是查询 左端点在[0...pos] 中的给定线段, 右侧端点最大值

这个注意到是一次赋值,多次查询没有更改的,直接前缀最大值

代码

https://codeforces.com/contest/1684/submission/158651275

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
#define per(i,a,n) for (ll i=n;i-->(ll)a;)
#define pb emplace_back
#define all(x) (x).begin(), (x).end()
#define pii pair<int, int>

ll read(){ll r;scanf("%lld",&r);return r;} // read

const int N = 200000;

int f[N+10]; // 前缀最大值[前缀左端点] = 最大右侧端点
ll a[N+10];
vector<int> gist[N+10]; // gist[值] = vector<int> 下标
pii seg[N+10]; // 题目给的线段
ll mnl[N+10]; // mnl[ir] = 最小合法il => [il..ir]没有重复的数
bool s[N+10]; // 存在过的数
int n,m;

void solve() {
// clear
fill(f,f+n,-1);
fill(gist,gist+n,vector<int>());

n = read();
m = read();
rep(i,0,n) a[i] = read();
// 离散一下
vector<pii> sa ;
rep(i,0,n) sa.push_back({a[i],i});
sort(all(sa));
rep(i,0,n) {
auto [v,j] = sa[i];
if(i == 0) a[j] = 0;
else if(v == sa[i-1].first) a[j] = a[sa[i-1].second];
else a[j] = a[sa[i-1].second] + 1;
}

rep(i,0,n) gist[a[i]].pb(i);

rep(i,0,m){
int l = read();
int r = read();
seg[i] = {--l,--r};
f[l] = max(f[l], r);
}
rep(i,1,n) f[i] = max(f[i-1],f[i]);
// 双指针 [il...ir] 没有重复的数
// mnl[ir] = 合法的最小il
int il = n;
per(ir,0,n){
while (il && !s[a[il - 1]]) s[a[--il]] = true;
mnl[ir] = il;
s[a[ir]] = false;
}

// mnr 为L = 1 时 R的最小值 , [R+1..n] 要么就是 本身合法线段要么就是 [R+1..r] 合法
ll mnr = -1;
rep(i,0,m){
auto [l,r] = seg[i];
if (mnl[r] <= l) continue; // 本身就合法 直接忽略
mnr = max(mnr, mnl[r] - 1);
}
if (mnr == -1) {
printf("0\n");
return;
}
ll ans = mnr + 1;
// L 每次 +1
// [l..mnr] => [l+1..?]
rep(l,0,n-1){
// l 不是 a[l] 首次出现的位置
if (gist[a[l]][0] != l) {
// 上一个同样值的位置
int pr = *(--lower_bound(all(gist[a[l]]), l));
// 左端点小于等于 pr, 的最大右端点, 如果删除了 就会有区间包含[pr...l] 有两个a[l]
// 再移动 就不可能了, 所以直接break
if (f[pr] >= l) break;
}
// 下一个 为a[l] 的 且在某个区间中
if (gist[a[l]].back() > mnr ) {
int nxt = *upper_bound(all(gist[a[l]]), mnr);
if (f[l] >= nxt) mnr = nxt;
}
assert(mnr > l);
ans = min(ans, mnr - l);
}
printf("%lld\n",ans);
}

int main() {
int t = read();
while (t--) solve();
return 0;
}

G

题目

https://codeforces.com/contest/1684/problem/G

t 是一个数组

考虑Euclid求gcd

1
2
3
4
5
6
7
8
9
10
11
12
function Euclid(a, b):
if a < b:
swap(a, b)

if b == 0:
return a

r = reminder from dividing a by b
if r > 0:
append r to the back of t

return Euclid(b, r)

p 是一个包含不超过m的正数对的数组

t 初始为空

然后 对p的所有 数对 运行上述算法

t然后被打乱了给你

你需要找一个 数组, len <= 2e4, 能产生 t, 或者判断它不可能存在

范围

len(t) 1e3

m 1e9

1 <= ti <= m

1s

256mb

题解

我的思路

其实就是 辗转相除(a,b), a>b 中间的所有非零余数

b > v, a >= b + v > 2v

所以如果有值不满足m > 2v 则直接不可能输出-1

否则直接无脑2v+1,v+1?, 但注意到2v+1,v+1,v,1 还会产生1, 也就是不行的

另外3v,2v,v 不会有额外产生,如果有多余的3v <= m 可以这样处理掉

所以只用考虑3v > m > 2v 的v值m/2 > v > m/3 (非整除)

a = 2v+i,b = v+i,v,i,... (i <= m-2v < v)

但怎么选i, 以及处理之后的余数,并没有任何想法

v的选择可以从大到小, 这样也可能消耗掉 一部分 m/2 > v > m/3

题解

一样 先把v的范围限定在了3v > m > 2v, 范围以外的和我想的一样的处理

m >= a=2v+i,b=v+i,v,i,...

也就是考虑到 2v + gcd(v,i) <= 2v+i <= m

也就是对于每个 v > m/3, 一定存在一个是它因数的x,且2v + x <= 2m

于是建立二分图

左边 > m/3, 右边 <= m/3

做二分图匹配即可


我的问题,在i 和 m/3大小判断错了, 其实 2v+i = a <= mv > m/3 就可以得到i < m/3

这样的话,v必然是靠小于等于m/3的消耗掉的,不如直接贪约数

有一说一,写起来其实会比F简单?

代码

https://codeforces.com/contest/1684/submission/158654516

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
#define per(i,a,n) for (ll i=n;i-->(ll)a;)
#define pb push_back

ll read(){ll r;scanf("%lld",&r);return r;} // read
const int N = 1000;
vector<int> g[N+10]; // g[二分图左端点] = vector<>右端点
int with[N+10]; // with[右端点] = 链接的来源左端点
int vis[N+10]; // 二分图每轮选左侧起始点时,是否访问到
ll a[N+10];

bool dfs(int v) {
if (vis[v]) return false;
vis[v] = 1;
// 直接就有可选终点
for (auto to : g[v]) {
if (with[to] == -1) {
with[to] = v;
return true;
}
}
// 递归走 v -> to0 -> with[to0] -> t1 -> with[t1] - ... -> took
for (auto to : g[v]) {
if (dfs(with[to])) {
with[to] = v; // 更新指向
return true;
}
}
return false;
}

int main() {
int n = read();
int A = read();
// 二分图
vector<ll> l;
vector<ll> r;

rep(i,0,n) {
a[i] = read();
(3 * a[i] > A ? l : r).pb(a[i]);
}
// 建立边
rep(i,0,l.size()) {
rep(j,0,r.size()) {
if (l[i] % r[j])continue;
if(2 * l[i] + r[j] > A) continue;
g[i].pb(j);
}
}
// 二分图匹配
fill(with,with+r.size(),-1);
rep(i,0,l.size()) {
fill(vis,vis+l.size(),0);
if(!dfs(i)){
// 未消耗掉所有 > m/3
printf("-1\n");
return 0;
}
}
vector<pair<ll,ll>> ans;
rep(j,0,r.size()) {
if (with[j] == -1) {
ans.pb({3 * r[j], 2 * r[j]}); // <= m/3 的 直接 `3v,2v => v`
} else { // 2v+i,v+i => v,i
ans.pb({2 * l[with[j]] + r[j], l[with[j]] + r[j]});
}
}
printf("%d\n",(int)ans.size());
for (auto [a,b]: ans) printf("%lld %lld\n",a,b);
return 0;
}

H

题目

https://codeforces.com/contest/1684/problem/H

给0/1串s, 切分成任意多个不相交子串, 然后让这些子串表示的二进制值的和是2的幂次

范围

|s| 1e6

2s

256mb

题解

我的思路

如果1的个数是2的幂次, 那么直接全部拆碎就完了

不妨设一共有$w$个1,且 $2^k < w < 2^{k+1}$

除了最后一位1, 任何一个1 都可变成2的贡献,不论是通过 11还是10, 它对w的贡献就是+1

但是如果连续的两个1,至多有一个可以贡献2,

同样除了最后两位1, 任何一个1 都可变成4的贡献,通过1XX, 对w贡献是+3

但是如果连续的三个中出现的1,至多有一个可以贡献4,

所以 (w-2)/3 个1 可以变成贡献4, 于是可以多贡献 (w-2)

但值得注意的是, 之所以 (w-2)/3 一个是因为尾部, 一个是因为 连续的3个中出现1, 才会不能让所有的1贡献4, 下限是(w-2)/3

这样的话,也就是说 有部分的贡献的是2, 总可以补全到$>= 2^{k+1}$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
100 = 4 (+3)
10,0 = 2 (+1)
1,0,0 = 1

101 = 5 (+3)
10,1 = 3 (+1)
1,0,1 = 2

110 = 6 (+4)
1,10 = 3 (+1)
11,0 = 3 (+1)
1,1,0 = 2

111 = 7 (+4)
11,1 = 4 (+1)
1,1,1 = 3

所以对于所有1开头的3位, 有的贡献可以+1,+3, 有的贡献可以+1,+4

2^{k+1}-1 >= w >= 2^k+1

所以 通过+3,+4 让w和 2^{k+1}的距离 在某个3元组能达到[1,3]之间, 剩下的[1,3]就靠+1补满

且, 注意到如果是靠不少+3达到的,那么剩余的长3的组一定还不少, 不会耗尽所有

所以w足够大时,必定可行

需要考虑w小的时候, 但多么小呢?

w = 1,2,4,8直接可行

w = 3 时, dst = 4. +1 必定可行

w = 5 时, dst = 8, 需要+3, (111)(110) 就是个不能用上面切割达到的

w = 6,7 时, dst = 8, 需要+2/+1, 必定两/一次+1可以达到

w = 9 时, dst = 16, 需要+7, (111)(111)(111) ,也是不能用上面切割达到

w = …

这块细节就不知道怎么搞, 也不知道大于多少以后w一定可以

方法

1,2,4,等2的幂次, 直接切碎

k = 3 可以用上面我说的方法

k = 5

如果1连续, 1111+1 = 10000

否则1之间存在0, 存在一个0,则101存在,多个0,则100存在. 101+1+1+1=1000, 100+1+1+1+1 = 1000

k > 5

solve(l,r,k,n) 函数表示把有k个1的[l..r]段切来和为n

这里目标n也是放在 2的log(2,k)向上取整幂次,

足够大的n, 考虑是按照k 来切开, 让它分别为 k/2向下取整和 k/2向上取整, 并且 让它们的和都是 n/2

solve(l,r,k,n) = solve(l,pos,k/2,n/2) and solve(pos+1,r,k-k/2,n/2)

然后界限来就是说 k = 6..11 时 如何搞


这里其实可以自己随便乱搞了,毕竟范围有限,目标明确

官方英文题解里有具体方法


这里方法上有一个要注意的是, 当 1的个数是 (2的幂次)+1 时, 它期望切割出来的2的(幂次-1)那一部分还是需要到

比如 17 = 16+1个1, 期望的结果是 2**5 = 32

17 = 8+9, 9的期望结果是16没问题, 但是8 也需要16 而不是得到8

但注意到这个问题集中在2的幂次上

所以再向下考虑4个1要得到8

考虑最左1开头的数:

111: 111+1=1000

110: 110+1+1 = 1000

101: 后面一个1贡献2,1个1贡献1, 101+10+1 = 1000

100: 后面一个1贡献2,两个1贡献1, 100+10+1+1 = 1000

都可以得到8

代码

鸽, 构造+枚举小值 是我太菜了

总结

F:

一个是 包含两个相同值,转化成包含两个相邻相同值,因为同样的值出现多次, 只用考虑相邻 v…v…v, 只用考虑[v…v]…v 或v…[v…v]

看起来没离散2e5个<1e9的map效率还行啊, 仅仅是查询的话

双指针 + 滑窗还是写不熟啊

G:

这里主要在i的范围判断错了,如果 i > m/3 那么 2v+i >= 2m/3 + m/3 = m, 我数学太菜了

H:

从小特例开始考虑算是有一定思路是对的, 但是要敢于分情况讨论

但是这个分治的确没想到,就算想到一半, 可能没想到让k/2向下和向上取整,都去等于 n/2

或者整体来说, 没想到 和可以拆成 和的一半 对应 1个数的一半

特别是这里敢于 2的幂次+1这种数也这样拆

参考

官方

比赛总结

https://ac.nowcoder.com/acm/contest/34330/E

考虑存在一个点 其它点仅和它断边

注意sum n很大, 清理按m清理

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define rep(i,a,n) for (ll i=a;i<n;i++)

// n(完全图), n-1, <= n-2?
int p2[1000010];

ll n ;
ll m ;
vector<int>uv;

void w(){
scanf("%lld %lld",&n,&m);
rep(i,0,m){
int u,v;
scanf("%d %d",&u,&v);
p2[u] ++ ;
p2[v] ++ ;
uv.push_back(u);
uv.push_back(v);
}
// 完全图
if(m == n*(n-1)/2){
printf("0\n");
return ;
}
if(m == n*(n-1)/2 - 1){
printf("-1\n");
return ;
}
// 连了4个点 -2
// 连了3个点 -1
// sum n 很大
if(n*(n-1)/2 - m > n){
printf("-2\n");
return ;
}
rep(i,1,n+1){
if(p2[i] == n-1 - (n*(n-1)/2 - m)){
printf("-1\n");
return ;
}
}
printf("-2\n");
}


int main(){
int t;
scanf("%d",&t);
while(t--){
w();
for(auto u:uv)p2[u] = 0;
uv = {};
}
return 0;
}

E - Labyrinth Adventures

题目

https://codeforces.com/contest/1681/problem/E

n行n列的迷宫

1
2
3
4
5
55555
44445
33345
22345
12345

左下角第一层,然后它八临的第二层,再8临第三层

不同层之间有墙隔着,部分墙的位置有门, 每两层之间有恰好两个门, 一个是上下方向,一个是左右方向, 门是双向通的

q个询问两点之间最少的移动步数

范围

n 1e5

q 2e5

6s

512MB

题解

n很大显然无法建立图

但我们可以考虑如果是图会怎么做

首先如果没有任何阻挡,那么两点之间的曼哈顿距离为 走个直角, 因此如果两个同色,那么必然答案就是曼哈顿距离

那么就是考虑不同颜色

显然也不会 一个色不连续的走到,否则这段路径直接同色最短, 综上 如果 a < b , 那么a->b 的距离 = a->某个门-> a+1->某个门->a+2 ->某个门 -> ... -> b

再改改

位置pos -> 门(a,0) -> 门(b-1,0) -> 位置dst

位置pos -> 门(a,0) -> 门(b-1,1) -> 位置dst

位置pos -> 门(a,1) -> 门(b-1,0) -> 位置dst

位置pos -> 门(a,1) -> 门(b-1,1) -> 位置dst

如果我们能快速求两个门之间的最短距离就好了

直接考虑rmq的办法, 记录 (位置i,0/1号门, 2的幂次j距离, 0/1号门) 的最小距离

这样复杂度为$O(n \cdot log(n) )$ 的初始化和 $O( log(n))$ 的单次查询

代码

会的, 不想写了

F

题目

https://codeforces.com/contest/1681/problem/F

给到一个n个点的树, 边上有整数

记f(u,v) = 点u到点v简单路径上 只出现了一次的数字的个数

求对于所有u < v 的点对, f(u,v)的总和

边的值[1..n] 不需要自己离散化了

范围

n 5e5

6s

1024mb

题解

显然假设一条边上写的是 x

那么从贡献角度来讲, 它对答案的贡献是,左侧端点不再经过值为x边的点数 乘上 右侧端点不再经过值为x边的点数

问题呢, 如果我们通过叶子统计汇总,那么每个叶子上需要记录O(n)个值的出现次数, 那显然就n 方了

那么我们考虑对于一个d, 在dfs时,不要影响到其它的块的办法

任意选一个点作为根,先考虑 dfs过程中经过了两个边为d

分别是(u0,v0,d) 和 (u1,v1,d)

那么 (u1,v1) 的贡献 = 以v0为根的d断开的联通块大小 乘上 以v1为根的d断开的联通块大小

而 以v为根的d断开的联通块大小 = v为根的子树大小 - sum{最近的d断开的 以v1/v2/v3/v4… 为根的树的大小}

这样 辅助数组 记录深搜过程中 上一个同边对应的点即可, 空间就是O(n),


还有一个问题, 如果是 dfs中首次出现的呢

这里的方法是对于每一个d 假装树的根的父节点连出去的就是d,即可

代码

https://codeforces.com/contest/1681/submission/158413431

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define rep(i,a,n) for (ll i=a;i<n;i++)
#define per(i,a,n) for (ll i=n;i-->(ll)a;)
#define pb push_back
const int N = 5e5;
int idx;
tuple<int,int,int> e[N*2 + 10]; // {u,v,d}
vector<pair<int,int> > u2vi[N+10]; // [u] = vector<{v,i}>
int loc[N*2+10]; // [d] = 深搜过程中 当前深搜链上 边为d的叶向端点
int lst[N*2+10]; // lst[v] = v作为叶节点父边为d , 通向根的链上,最近一个边为d的叶向节点 //上一个v'
int sz[N+10]; // 子树大小, 纯的树统计,不考虑边值
ll f[N*2+10]; // 考虑边值有效的树的大小
int n;
void dfs(int u, int fa) { // 点, 父节点
sz[u] = 1; // 纯的子树大小
for(auto [v,ei]: u2vi[u]){
int d = get<2>(e[ei]);
if (v == fa) continue;
lst[v] = loc[d]; // 即帮助深搜恢复loc[d], 也自己记录了它到根的简单路径上最近的 边为d的叶向节点
loc[d] = v; // 当前的深搜的链上 边为d的叶向端点
dfs(v, u);
loc[d] = lst[v]; // 恢复上一次的 loc[d]
sz[u] += sz[v];
}
// 原理就很简单了 对于 u->v, 边为d 来说, 以v为根的连通块大小 = 它的子树大小 - sum{它子树中紧邻的边d的根为v' 的子树大小}
f[u] += sz[u];
f[lst[u]] -= sz[u];
}

int main() {
cin >> n;
rep(i,1,n){
int u, v, d;
scanf("%d %d %d",&u,&v,&d);
e[i] = {u,v,d};
u2vi[u].push_back({v,i});
u2vi[v].push_back({u,i});
}
// 初始化f 和 loc
rep(i,1,n+1){
f[i + n] = n;
loc[i] = i + n; // 初始化为每个值 对应虚拟节点为 value+n
}
dfs(1, -1);
ll ans = 0;
rep(i,2,n+1){
ans += (f[i] * f[lst[i]]);
}
printf("%lld\n",ans);
return 0;
}

总结

其实相当于线性数组的变性, 靠值来记录上一个同样值的位置, 不同的是线性可以直接坐标差得到中间的联通块, 而 树状可以dfs知道 到根最近的同样的值在哪, 而联通块的信息直接表示在根上

参考

官方

0%