Codeforces Round 794

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