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

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;
}
0%