题目

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

树,n个点,点上1~n

给定k

对于点r, 树的一个k个点的子点集S, 令f(r,S) = 根为r, 且包含所有S中点的原树的子树最少的点的个数

你需要计算 所有 r和S的组合 求 所有f(r,S)的和

答案mod 1e9+7

范围

n 2e5

3s

512MB

题解

我的思路

看数据范围n方都不行, 既然又是求和

那么估计又是算贡献一类

从树的结构讲, 与其算贡献不如算不被贡献

一个点u,连接了很多点v0,v1,v2,v3

如果要u不被贡献, 那么r和S的所有点一定在某个vi的联通块内,不会是不同vi的联通块内

而选择来讲, 对于S, 就是C(联通块大小,k), 对于r就是联通块大小

注意联通块 < k时 方案为0


感觉就过了???不想写代码

代码

总结

感觉给时间, 就这样随便搞一搞就完了?

参考

前情提要

在之前做Project Euler 216 的时候

学了一下 如何利用别人的答案,在log n时间内判断n是否是一个 64位以内的质数的 Miller-Rabin 判别法

但如果这个数不是质数, 如何能拆解还没有解决

方法

方法1

回到最初的起点, for一遍 那也是$\sqrt(n)$

而众所周知, $\sqrt {2^{64}} = 2^{32} = $ 4.2e9

单独算一次的时间复杂度都接受不了

方法2

相信随机的力量, 先特判是否质数

在不停的随一个判断是否gcd != 1 来找因数


问题是, 生日悖论(一个房间里有23个人,则他们中有两人生日相同的概率超过一半)

换句话说, 反复生成随机数,有很高几率生成了不少一样的

Pollard 的伪随机数

问题变成 我们希望它概率上看起来随机,值上有不重复得不那么随机

$x_{n+1} = f(x_n) = (x_n^2 + c) mod N$

但也不一定如期望, 例如 x = 0, c= 24,N = 9400, 很有规律, 因为这个递推式说白了就是下一项由上一项决定,肯定有循环,只是循环的早晚

低空间,低时间判断环? 那不是经典面试题双指针吗?(floyd判环算法)

初始$x_1 = y_1$

$x_{n+1} = f(x_n)$

$y_{n+1} = f(f(y_n))$

每次判断$gcd(|x_n - y_n|,N) > 1$, 和是否到达环, 成环则换一个c来跑


性质 |i-j| 是p的倍数,则 |f(i)-f(j)| 也是p的倍数

如果看作环上指针, 也就意味这两个指针距离相等时,其它距离和这个距离相等的两个指针之差,都是p的倍数,

而快慢指针每次会让距离+1, 而对于环上的视角, 其实追上慢指针相当于逐渐减少

实现和细节

据说$O(n^{\frac{1}{4}}\log n)$

$N = 4,N = 1$ 需要特判

素数平方提前判断

让x=0

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
#include <bits/stdc++.h>
#include <atcoder/modint>
using mint = atcoder::modint998244353;
using namespace std;

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

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

typedef __int128_t lll;
ll quick_p(lll b, ll p,ll mod){
lll r = 1;
while(p){
if(p%2)(r*=b)%=mod;
(b*=b)%=mod;
p/=2;
}
return r % mod;
}

bool miller_robin(ll v, ll base, ll startpwr){
lll r = quick_p(base,startpwr,v);
for(ll p = startpwr; p < v-1; p *=2){
if(r == v-1) return true; // -1 开始的序列
if(r == 1) return p == startpwr; // 全1序列
(r*=r)%=v;
}
return false;
}

bool is_prime_64(ll v){
if(v == 2)return true;
if(v < 2 || v % 2 == 0)return false;
ll p = v-1;
while(p % 2 == 0) p /= 2;
for(auto base:{2, 325, 9375, 28178, 450775, 9780504, 1795265022}){
if(base % v == 0) continue; // don't break may cause 4033 bug
// 需要所有都能找到-1开始, 或奇数次开始 全1
if(!miller_robin(v,base,p)) return false;
}
return true;
}

ll my_sqrt(ll v){
if(v <= 1) return v;
ll l = 1; // 左ok
ll r = v; // 右not ok
while(r - l > 1){
ll m = (l+r)/2;
(v/m < m ? r : l) = m; // 防止 overflow
}
return l;
}
unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
mt19937 rand_num(seed);

ll randll(ll low,ll hi){
return low + (rand_num() % static_cast<ll>(hi - low + 1));
}

ll Pollard_Rho(ll N) { // 返回一个> 1的因数
assert(N > 1);
if (N == 4) return 2;
lll ret = my_sqrt(N); // 质数平方 效率低 提前判断
if(ret * ret == N) return ret;
while(true) {
ll c = randll(1, N - 1); // 生成随机的c
auto f = [=](lll x) { return ((lll)x * x + c) % N; }; // ll 表示__int128,防溢出
ll t = 0, r = 0; // 初始两个相同
do{
t = f(t); // 1倍速度
r = f(f(r)); // 2倍速度
ll d = gcd(abs(t - r), N);
if (d > 1 && d < N) return d;
}while (t != r);
}
}

// 分解x为质因数, sorted
vector<ll> fenjie(ll x) {
vector<ll> res = {};
deque <ll> arr = {x};
while(arr.size()){
ll v = arr.front();
arr.pop_front();
if(v == 1) continue;
if(is_prime_64(v)) {
res.pb(v);
continue;
}
ll divisor = Pollard_Rho(v);
arr.push_back(divisor);
arr.push_back(v/divisor);
}
sort(res.begin(),res.end());
return res;
}

固定128距离

减少求gcd的次数, 128次 或者 即将乘起来是N的倍数

大概是$O(n^{\frac{1}{4}})$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ll Pollard_Rho(ll N) {
assert(N!=1);
if (N == 4) return 2;
if (is_prime(N)) return N;
if (is_prime_square(N)) return prime_square(N);
while(true){
ll c = randint(1, N - 1);
auto f = [=](ll x) { return ((lll)x * x + c) % N; };
ll t = 0, r = 0, p = 1, q;
do {
for (int i = 0; i < 128; ++i) { // 令固定距离C=128
t = f(t), r = f(f(r));
if (t == r || (q = (lll)p * abs(t - r) % N) == 0) break; // 如果发现环,或者积即将为0,退出
p = q;
}
ll d = gcd(p, N);
if (d > 1) return d;
} while (t != r);
}
}

C(math, 括号对)D(math,集合,数论,动归)E(并查集)

C

题目

https://atcoder.jp/contests/arc141/tasks/arc141_c

他给两个排列p和q, 长度2n

构造 长2n的括号字符串,含有n个左括号,n个右括号

使得p是所有 让 s[p[i]] 为合法括号序列中的字典序最小的

同时q是所有 让 s[q[i]] 为合法括号序列中的字典序最大的

范围

n<=2e5

2s

1024mb

题解

我的思路

显然开始和最后的位置分别是 左右括号

对于p, 当左右括号都可以选时,一定会选没有被选坐标最小的

当前缀完成匹配时, 只能选左括号, 这时选左括号坐标最小的

于是, 如果当前坐标以前的没有选完,那么说明当前位置是左括号,且没有被选的是右括号

对于q,类似的, 先选最大的, 左括号也是先选最大的

这样分别确定的右括号不少于 左括号个数


但是对于剩余没有填的位置怎么做,我没有思路了,因为它不只需要保证一个排列合法,它需要保证p和q都合法

官方

前面还是一样的, 但这里强调了是 奇数处出现, 因为 要前面匹配完,说明前面用了偶数个

而且不像我那样 需要 前缀未填完, 而只是 奇小于下一个偶, P[2i-1] < P[2i]

但说是 如果还是有多个候选的,那么就是没有方案

如果只有一个候选S,就看是否同时满足p和q

简单的来讲如果S或它的逆序是一个合法的括号序列, (一般情况也可以类似证明, 因为一般的S 可以表示成合法和 和 逆序合法的连接

令 L1 < L2 < L3 … < LN 分别是左括号的下标

R1 < R2 < R3 … < RN 分别是右括号的下标

既然S是合法的,那么有 Li < Ri

因此字典序最大的排列是$L_N,R_N,L_{N-1},R_{N-1},…,L_1,R_1$

因此 S是唯一的

并且确定的过程和上述描述的也是一致的


如果S的逆序是合法的

那么有

令 L1 < L2 < L3 … < LN 分别是左括号的下标

R1 < R2 < R3 … < RN 分别是右括号的下标

既然S的逆序列是合法的,那么有 Li > Ri

所以字典序最小的是$L_1,R_1,L_2,R_2,…,L_N,R_N$

并且确定的过程和上述描述的也是一致的


再对于一般序列来讲

又回到 括号序列的常用技巧,左括号+1,右括号-1

那么其实就是 一些在正平面的一些曲线和负平面的一些曲线,

显然由和0点隔开的 顺序上也相互独立(见官方youtube上画的图

这样 对于每一段来说,是正平面则由最大序列唯一确定, 是负平面则由最小序列

所以 整体都是唯一的

这样就是官方提接说的一般序列的 拼接类似

综上得证

代码

https://atcoder.jp/contests/arc141/submissions/32155305

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
#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for (int i=a;i<(int)n;i++)
int read(){int r;scanf("%d",&r);return r;} // read
int p[400010];
int q[400010];
char s[400010];
char sets(int i,char ch){
return (s[i] && s[i] != ch) ? 0 : (s[i] = ch);
}
bool work(){
int n = read() * 2; // 2e5 * 2
rep(i,0,n) p[i] = read() - 1;
rep(i,0,n) q[i] = read() - 1;
for(auto [arr, cmp]:vector<pair<int *,int> > {{p, 1},{q,-1}}){
rep(i,0,n-1) {
if((arr[i] - arr[i+1]) * cmp <= 0) continue; // 出现反序列
if(!sets(arr[i],'(') || !sets(arr[i+1],')')) return false;
}
}
rep(i,0,n) if(!s[i]) return false; // 不唯一
// check 可能一个合法 另一个不合法
for(auto [arr,st,d]:vector<tuple<int*,int,int> >{{p,0,1},{q,n-1,-1}}){
// 双指针
int i0 = st; // 找所有值
int i1 = st; // 只找左括号
int cnt = 0;
vector<bool> vis(n,false);
rep(i,0,n){
int pos ; // 选取的位置
if(cnt == 0){
while(vis[i1] || s[i1] != '(') i1+=d;
pos = i1;
}else{ // cnt > 0
while(vis[i0])i0+=d;
pos = i0;
}
if(arr[i] != pos) return false; // 和提供的不一致
vis[pos] = true;
cnt += s[pos] == '('?1:-1;
}
if(cnt) return false;
}
printf("%s\n",s);
return true;
}
int main(){
if(!work()) printf("-1\n");
return 0;
}

D

题目

https://atcoder.jp/contests/arc141/tasks/arc141_d

对于一个集合S, 任意两个元素不成倍数关系,那么认为是一个好集合

给一个n个元素,元素值范围在[1,2m]之间的集合, 元素值不重复, 给值时从小到大

对于每个元素,判断是否存在一个S的子集,包含该元素且是好集合

范围

M<=N<2M

M 3e5

题解

我的想法

既然给值就是从小到大, 那么省去了排序

既然一定要a[i], 那么它的倍数和约数一定不可行,而约数是log级别的个数,

这里虽然问是否能恰好m个, 但如果>=m 合法,删掉多于m的依然合法

所以变成能否有不少于m个

对于即不是ai倍数,也不是ai约数的, 考虑最多能取多少个

于是集合被化分成(ai,ai约数,ai倍数) (其它), 那么包含ai的最大的个数是 1+max(其它)


首先 值的倍数从均摊上讲 也是 log级别的, 因为 1/2+1/3+1/4… 在小的时候是 常数倍

但 剩下的如何尽可能多的取, 以及如果只是暴力去尝试的话, 显然 会达到至少 n平方


另一个就是互斥关系, 如果建立互斥关系的2-sat图, 跑tarjan ,能一次计算

注意到互斥关系不会变, 所以2-sat不会变, 但是怎么选一个而不选其它

官方

其实是个卡着边界的问题

考虑所有奇数的2的幂次倍

(1,2,4,8,16...),(3,6,12,24...),(5,10,20...)

注意到的是 ,一共有m组,且每组内部两两是倍数关系, 因此我们选的答案,不会同时出现在一组中, 所以 至多选m个


这个对答案也有帮助, 如果题目给的S, 在上述的2的幂次倍中 有的组不存在,那么显然达不到m

现在问题是跨组会不会有 倍数关系

假设 $x_1 < x_2$ 都是奇数, 选了 $x_1 2^{p_1},x_22^{p_2}$

那么如果成倍数一定是 $p_2 \ge p_1$ 且 $x_2$是$x_1$的倍数

换句话说, 要想合法, 那么一个数的约数对应的2的幂次要比它本身大


考虑 每个奇数的2的幂次的上下界, [Li,Ri]

直接动归转移方程

对于R[value] = min(R[value 的因子]) - 1 且存在于S

对于L[value] = max(L[value的倍数]) + 1 且存在于S

R1,R2,R3,...R,被选的值,L,...,L 将是合法解, [Li <= 被选的幂次 <= Ri]

因为前面的尽可能大,后面尽可能小且 被选值在范围中


综上 因为因子数和均摊倍数个数都是log级别,所以总的均摊代价就是log级别

代码

https://atcoder.jp/contests/arc141/submissions/32169715

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
#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;)

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

bool exist[600010];

vector<int> ys[600010]; // 所有真奇数约数

int L[600010];
int R[600010];
int a[600010];

int n,m;
bool w(){
n = read();
m = read()*2;
rep(i,0,n) {
exist[a[i] = read()] = 1;
}
rep(i,1,m/2+1){
if(i%2 == 0)continue;
rep(t,3,m/2+1){
if(t%2 == 0)continue;
if(i*t > m)break;
ys[i*t].push_back(i);
}
}
// 先检查是否所有组都有
rep(i,1,m+1){
if(i%2==0)continue;
bool found = false;
int itr = i;
while(itr <= m){
if(exist[itr]){
found = true;
break;
}
itr*=2;
}
if(!found)return false;
}
// 计算R
rep(i,1,m){
if(i%2 == 0) continue;
int pwr = 20;
// 计算它因数对它的限制
for(auto item:ys[i]){
pwr = min(pwr,R[item]-1);
}
// 找一个范围内且存在的
bool found = false;
while(pwr >= 0){
if(i * (1<<pwr) <= m){ // 小心 out bound
if(exist[i * (1<<pwr)]){
R[i] = pwr;
found = true;
break;
}
}
pwr--;
}
// printf("L %lld => %d\n",i,pwr);
if(!found) return false; // 不存在合法范围的值
}
// 计算L
per(i,1,m){
if(i%2 == 0) continue;
int pwr = 0;
// 计算它倍数对它的限制
rep(k,3,m+1){
if(k%2==0)continue;
if(i*k > m)break;
pwr = max(pwr,L[i*k]+1);
}
// 找一个范围内且存在的
bool found = false;
while( i*(1<<pwr) <= m){
if(exist[i * (1<<pwr)]){
L[i] = pwr;
found = true;
break;
}
pwr++;
}
if(!found) return false; // 不存在合法范围的值
if(L[i] > R[i]) return false;
}
// 计算答案
rep(i,0,n){
int v = a[i];
int pwr = 0;
while(v%2 == 0){
pwr++;
v/=2;
}
printf("%s\n", L[v] <= pwr && pwr <= R[v]?"Yes":"No");
}
return true;
}
int main(){
if(!w()) rep(i,0,n) printf("No\n");
return 0;
}

E

题目

https://atcoder.jp/contests/arc141/tasks/arc141_e

n方个点, (1..n,1..n)

q 个询问

每个询问 a,b,c,d

会把 点((a+k)%n,(b+k)%n) 和 点((c+k)%n,(d+k)%n) 相连, 其中k取 0 到 n-1

询问之间是影响的, 是在上一次结果上继续连

每次回答一个询问操作后,剩下的联通块数

范围

n 2e5

q 2e5

题解

我的思路

首先, 其实让a,b,c,d 变成 a1=(a+n-a)%n,b1=(b+n-a)%n,c1=(c+n-a)%n,d1=(d+n-a)%n, 因为k取[0..n-1], 所以等价

变成 k,(b1+k)%n,(c1+k)%n,(d1+k)%n

画图, 会发现 (k,(b1+k)%n) 在一条45度角的一条斜线上,((c1+k)%n,(d1+k)%n) 也在一条45度角的一条斜线上

  1. 如果共线, 那么 如果原来不是一个联通块,则 减少了 n-1个联通块, 如果原来是多个联通, 那么对结果影响 = -(个数-1)

有了这个思路, 我们问题通过图像可以变一变

沿着+1,+1的45度, 形成n组点,每组点有个属性内部是否相连

考虑两组之间的关系,

1次连接, 那么这两组形成的是n个连通块, 且内部联通关系,一旦有一个联通则连通

而这个值其实 = gcd(偏移量间隔)

所以未连接和 连接一次的偏移量间隔为 n

而对两个组的影响是相同的

所以变成

哪些组属于一个并查集合, 它们自身内部的偏移量等价(一个值) 它们与根的偏移量等价


似乎就可以做了

但感觉在合并并查集时更新需要注意


然后 真的 我就AC了???? atcoder真的是数学场吗

代码

https://atcoder.jp/contests/arc141/submissions/32185372

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
#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 gcd(int a,int b){
while(b!=0){
a=a%b;
swap(a,b);
}
return a;
}

int fa[200010];
int a[4];
int inner[200010]; // 组内部最小间隔, 一定是n的因子
int tofa[200010]; // 跨组偏移距离
ll n;

int getfa(int i){
if(i == fa[i]) return i;
int newfa = getfa(fa[i]);
tofa[i] = ((tofa[i] + tofa[fa[i]])%n)%inner[newfa];
return fa[i] = newfa;
}

// new root u
// u and v is old root
void link(int u,int v,int off){
fa[v] = u;
inner[u] = gcd(inner[u],inner[v]);
tofa[v] = off % inner[u];
}

int main(){
n = read();
ll q = read();
ll ans = n*n;
iota(fa,fa+n,0);
fill(inner,inner+n,n);
rep(i,0,q){
rep(j,0,4) a[j] = read();
per(j,0,4) (a[j] += n-a[0])%=n;
int g1 = a[1] - a[0];
int g2 = (a[3]-a[2]+n)%n;
int off = a[2] - a[0];
int f1 = getfa(g1);
int f2 = getfa(g2);
// 同组更新内部间隔
if(f1 == f2){
// ans -= inner[f1] - inner[f1];
// printf("SAME %d[%d] => ",f1,inner[f1]);
ans -= inner[f1];
inner[f1] = gcd(inner[f1], 2*n + tofa[g1] - tofa[g2] + off); // 不是off
ans -= -inner[f1];
// printf("%d \n",inner[f1]);
}else{ // 不同组 合并组
// printf("DIFF %d[%d] + %d[%d] => ",f1,inner[f1],f2,inner[f2]);
// g1->f1, g2->f2
// f2->f1?
// f1 - f2 = (f1 - g1) - (f2 - g2) + (g2 - g1)
// ans -= inner[f1] + inner[f2] - inner[f1];
ans -= inner[f1] + inner[f2];
link(f1,f2, (2*n + tofa[g1] - tofa[g2] + off)%n);
ans -= - inner[f1];
// printf("%d \n",inner[f1]);
}
printf("%lld\n",ans);
}

return 0;
}

总结

C

这个基本的能想到, 但是没有尝试更多数据, 去考虑它的唯一性, 还在想怎么填中间的

这方面要培养,有点像反过来想题目, 如果题目本身设计上有唯一性只是需要证明, 这个思路方向, 因为毕竟是确定有答案的题目而不是开放性问题

另外就是括号序列还是不熟悉, 如果熟悉常见+1,-1套路,画图去思考也会简单不少

虽然从逻辑上 我的 当前前面未填完,则当前(,未填都是), 数学上好像 更多信息, 但这里反而成了干扰

据说能用DP做?

D

这数学性好强啊, 知识点是属于集合论的和数论的,甚至有点抽屉原理,

能想到奇数与它的2的幂次倍数的分组是这个题的核心一点

这一想出后面就自然很多了

E

我这赛后没有看题解,竟然AC了 ??????

参考

官方题解 https://atcoder.jp/contests/arc141/editorial/

官方youtube

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

0%