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知道 到根最近的同样的值在哪, 而联通块的信息直接表示在根上

参考

官方

E

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

长度n,前17个小写字母组成的字符串s,其中有?表示t中任意字符

q次询问,每次给17个字母中的一个子集,问把?所有填子集的填法中,原串的回文子串个数和, 答案mod 998244353

范围

n 1e3

t 17

q 2e5

2s

256MB

我的思路

首先看到q 2e5, 和 t 17, 因为2^17 == 131072,其实可以变成求所有方案的答案

统计所有回文子串 的效率最好就是选取中心,再枚举长度

那么这个题也是这样做, 枚举中心向两边搜索

其中注意到的是,当选定中心后,如果对称位置上有一个问号一个确定字符,那么这个问号必定填这个字符

如果两个都是问号,那么自由度+1, 它

如果对称都填了但是不一样,那么到此结束

也就是n方可以统计 cnt[必要字符bitmask][自由度] = 次数

那么 每次询问bitmask 是 M 的话

ans[M] = sum { cnt[m 是M的子集][i = 0..] * size(M)^i }

那么问题来了, 朴素计算我必定超时, 但是我不知道如何dp, 看Codeforces的群里有人说sosdp

SOSDP 子集和DP

$F[mask] =\sum_{i \in mask}A[i]$

暴力枚举 $O(4^n)$

1
2
3
4
5
rep(mask,0,1 << N){
rep(i,0,1 << N){ // 枚举 mask, 检测是否是它的子集
if((i&mask) == i) F[mask] += A[i];
}
}

枚举子集和$O(3^n)$

子集遍历

也可以dfs,当然dfs会多函数调用和堆栈 比如mask = 111, i依次为 111,110,101,100,011,010,001,000, 注意到的是mask中间穿插0对应的就是i对应位置穿插0,看起来就像每次-1

1
2
3
4
5
6
rep(mask,0,1 << N){
F[mask] = A[0]; // 所有集合包含空集 空集合也作为停止条件
for(int i = mask;i;i = (i-1)&mask){ // 这就是传说中的二进制枚举子集 ,
F[mask] += A[i];
}
}

SOSdp $O(n2^n)$

dp[mask][i] 表示 高位和mask一致, 低[0..i]位所有方案的和

dp[10110][2] = A[10000]+A[10010]+A[10100]+A[10110]

状态转移

第i位为0时,dp[mask][i] = dp[mask][i-1]

第i位为1时,dp[mask][i] = dp[mask][i-1] + dp[mask xor (1 << i)][i-1]

这样变成递推 或者记忆化搜索,可以 $O(n2^n)$ 完成

上面合并一下变成,dp[mask][i] = dp[mask][i-1] + (mask & (1 << i)?dp[mask xor (1 << i)][i-1]:0)

注意到i依赖于i-1还可以滚动数组降低空间

1
2
3
4
5
6
7
rep(mask,0,1<<N) f[mask] = A[mask];
rep(i,0,N){
// 这里不需要从大到小, 因为dp[mask]已经初始化了,只会更新1 << i上为1的,而更新的来源是1 << i上不是1的
rep(mask,0,1 << N){
if(mask & (1 << i)) f[mask]+=f[mask ^ (1 << i)];
}
}

题解

我的暴力的代码

1
2
3
4
5
6
7
8
ans[mask] = 0;
sz = bitcount(mask)
rep(qmark,0,501){ // 自由的问号个数
ti = pow(sz,qmark)
for(int i = mask;i;i = (i-1)&mask){
ans[mask] += cnt[i][qmark] * ti
}
}

这是$O(n 3^{|t|})$ 的复杂度 肯定过不了

通过sosdp降低到$O(n t 2^{|t|})$ 虽然降低了不少,但是依然过不了

这里dp 改一个方式设计, 变成贡献统计

先交换一下循环层级

1
2
3
4
5
6
7
ans[mask] = 0;
sz = bitcount(mask)
for(int i = mask;i;i = (i-1)&mask){
rep(qmark,0,501){
ans[mask] += cnt[i][qmark] * pow(sz,qmark)
}
}

因为{i,qmark}中i是mask的子集,而{i,qmark}对mask 的贡献来讲 只与bitcount(mask) 有关,与mask 具体无关

1
2
3
4
5
6
7
8
9
10
11
12
13
rep(i,0,(1 << N)){
rep(sz,1,17+1){
rep(qmark,0,501){
cost[i][sz] += cnt[i][qmark] * pow(sz,qmark);
}
}
}
ans[mask] = 0;
sz = bitcount(mask)
// sosdp 优化掉
for(int i = mask;i;i = (i-1)&mask){
ans[mask] += cost[i][sz];
}

下面得到优化了, 但上面看起来复杂度并没有变好

但既然都说道贡献统计了,就去看贡献来源

在我们初始找回文时[i...j] 如果是一个回文,它的必须字符集为mask,自由度为qmark

那么

1
2
3
rep(sz,1,17+1){
cost[mask][sz] += pow(sz,qmark);
}

这样 初始化就变成$O(|t| n^2)$


综上 $O(初始化幂次 |t|n + 初始化贡献 |t| n^2 + 初始化答案 |t|^2 2^{|t|} + 查询 |t|q )$

注意到 题目要统计 所有字符串个数,也就是说 同一个位置的回文串 在不同字符串中出现,要多次统计

所以

1
2
3
rep(sz,1,17+1){
cost[mask][sz] += pow(sz,qmark) * pow(sz,total qmark - used qmark);
}

代码

https://codeforces.com/contest/1679/submission/158244316

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
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define MOD 998244353
#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

const int t = 17;
char s[1010]; // 读入
char s2[2010]; // 插入井号简化搜索
int n;

ll p[t+10][1010]; // 幂次预处理
ll cost[(1<<t)+10][t+10]; // 具体的值
ll ans[(1<<t)+10][t+10]; // 由具体值的子集和的答案
char query[t+10];

int main(){
rep(i,1,t+1){
p[i][0] = 1;
rep(pwr,1,1000+1){
p[i][pwr] = p[i][pwr-1]*i%MOD;
}
}
scanf("%d",&n);
scanf("%s",s);
int qtotal = 0;
rep(i,0,n) qtotal += s[i] == '?';
// 字符两两间插入井号方便处理奇偶
s2[0] = s[0];
rep(i,1,n){
s2[i*2-1] = '#';
s2[i*2] = s[i];
}
// 中心
rep(c,0,n*2-1){
int mask = 0;
int qmark = 0;
int qcnt = 0;
rep(l,0,n*2-1){ // 长度
int il = c-l;
int ir = c+l;
if(il < 0 || ir >= n*2-1)break;
qcnt += s2[il] == '?';
qcnt += il != ir && s2[ir] == '?';
if(s2[il] == '#')continue; // 不贡献的
if(s2[il] == s2[ir]){
if(s2[il] == '?'){
qmark++;
}
}else{ // 不等
if(s2[il] == '?'){
mask |= (1 << (s2[ir] - 'a'));
}else if(s2[ir] == '?'){
mask |= (1 << (s2[il] - 'a'));
}else{ // 不同的字符
break;
}
}
// 贡献统计
rep(sz,1,17+1){
// 不同字符串 同一个位置回文串的贡献要多次统计 所以要乘上 其余位置是问号的所有放入方案 让此处的贡献倍数 sz**(qtotal - qcnt)
(cost[mask][sz] += p[sz][qmark] * p[sz][qtotal - qcnt] % MOD )%=MOD;
}
}
}
// sosdp
rep(sz,1,t+1){
rep(mask,0,1 << t){
ans[mask][sz] = cost[mask][sz];
}
rep(i,0,t){
rep(mask,0,1 << t){
if(mask & (1 << i)) (ans[mask][sz] += ans[mask ^ (1 << i)][sz])%=MOD;
}
}
}
// query
int q; // 2e5
scanf("%d",&q);
while(q--){
// string to mask
scanf("%s",query);
int sz = strlen(query);
int mask = 0;
rep(i,0,sz){
mask |= (1<<(query[i]-'a'));
}
printf("%lld\n", ans[mask][sz]);
}
return 0;
}

总结

回文的奇偶处理,可以用两两间插入不会出现的字符,如井号,方便枚举中心

学了一手sosdp

看ssr的twitter说有快速zeta变换, 搜下来看转移方程似乎就是sosdp, https://zhuanlan.zhihu.com/p/33328788?ivk_sa=1024320u

顺便这里的dp转化过程中 通过贡献和来优化效率

参考

官方

F

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

$[0..10^n)$的所有n位整数,不足的补前导零

给m个 (ui,vi) 数对, (ui不等于vi)

x 表示成十进制的数字数组 [d1,d2,…,dn]

一次操作可以交换相邻的d, 但需要满足 这两个数的 (di,di+1)或(di+1,di) 出现在 (ui,vi)中

如果一个数x能够通过上述转换变成y,那么认为它们是相等的, x和x自身相等

问题$[0..10^n)$ 有多少个不同的数, 答案mod 998244353

范围

n 5e4

m 45

u,v [0,9]

3s

256MB

题解

我的思路

先提取一下有用没用的信息,首先m没啥用因为实际就是所有数对的上限

如果x 中有两个数字 c0,c1 但是这两个数字没有在d中出现过, 那么这两个数字的相对前后关系不会改变

换句话说,如果两个相等的 它们互相可以转化,那么它们一定属于某个集合,集合里两两可以转化, 而有限集合一定有最小的, 我们用每个集合中数值最小的来表示一整个集合

于是 这个最小值值可以表示成 [单调递增] [单调递增] [单调递增] , 每两个单调递增之间 的值是不在数对里的

所以如果我们可以得到[起始,结束,长度]的方案数就可以考虑转移方程了

f[i][j][len1] = sum{ f[i][k][len0] * inc[ < k][j][len1-len0] } + inc[i][j][len]

看似复杂度没法搞,而实际上连逻辑也不一定对 201, 它允许 (0,1),(2,1) 那么显然120和它相等且更小

题解

思路是类似的, 也是集合的代表元, 但是并不是靠单调递增划分

而是说[序列长度l] 在后面加上mask中的数,它不会被移动到前面

那么,d会移动到前面的条件就是,序列的尾部的一串数都大于d,且都可以和d交换

[x,d1,d2,d3,...,ds,y]

其中 x > y, 且 y 可以和x,d1,...,ds交换

dp[suff][mask] 表示, suff个digits, 且只有mask中的digit可以被移到最左


考虑长度为s 的一个具体的串 等价最小串 X=[d0,d1,d2,.....,ds]

最长的前缀[d0,d1,d2,....dt] 包含的digits 两两可换, 我们把这样的digits变成mask, X 可以表示成贡献到 dp[s][mask]

那么现在如果左边放一个d, 变成X1 = [d,d0,d1,d2,...,ds]

一旦d和 mask 中某个值可换, 记为e, 且d > e

显然,因为mask中两两可换,所以X1 可以变成 [d,e,d0,d1,d2,...,ds], 然后交换e,d 得到 [e,d,d0,d1,d2,...,ds]

因为e < d ,所以 这个值比X1

即是X前面不能插入d, 如果 mask 中存在比d小,且和d相连的任何一个e

这样可选的d的范围就出来了, 这部分可以预处理

从X而非mask的角度来看,就是说插入了d以后,得到的值依然是 集合表示的最小值(代表元)


mask的变化

如果d可以放,那么X1 = [d,d0,d1,d2,...,ds], 显然,它的 最长两两可换前缀中有d, 且剩余的部分从mask 中取, 因为mask本身就是两两可换,所以只用考虑和d是否有边

所以新mask = d | mask 中和d有边的点


注意到mask 的意义是 mask 中的值两两可换, 又是X的最大前缀

所以这里其实会计算不少无效的mask, 但因为算次数,这些次数一定是0, 不影响答案

代码

https://codeforces.com/contest/1679/submission/158638333

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
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define MOD 998244353
#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

namespace X{
ll r(){ll r;scanf("%lld",&r);return r;} // read
void add(ll &v0,ll &v1){(v0+=v1%MOD)%=MOD;} // add with MOD
};
using namespace X;

const int S = 10;

// mask 意义, mask中两两可换, 是X的最大前缀
// camp[mask0] = mask1, mask1任意一个bit 和 mask0 中的比它小的bit都没有链接
// 在 mask1中的digit 才能加入到 mask0
vector<int> camp(1<<S,(1<<S)-1);
bool conn[S][S]; // 连接状态
ll dp[2][1 << S];
int trans[1 << S][S]; // [mask][digit] = newmask,

int main() {
int n = r();
int m = r();
rep(i,0,m) {
int u = r();
int v = r();
conn[u][v] = conn[v][u] = 1;
}
// 计算 camp
// O(S^2 * 2^S)
rep(mask,0,1 << S){
rep(c,0,S){ // c 在 mask 中
if (!(mask & (1 << c))) continue;
rep(j,c+1,S){ // j > c
if (conn[c][j]) {
camp[mask] &= ~(1 << j);
}
}
}
}
// 计算trans
// O(S^2 * 2^S)
rep(mask,0,1 << S) {
rep(c,0,S){
trans[mask][c] = 1 << c;
rep(j,0,S){
// j 出现在mask 中, (c,j) 可以交换
if ((mask & (1 << j)) && conn[c][j]) {// 和 mask 中存在相连
trans[mask][c] |= 1 << j;
}
}
}
}
// 滚动数组
int cur = 0;
dp[0][0] = 1;
// O(n * S * 2^S)
rep(i,0,n){
// clear
fill(dp[cur^1],dp[cur^1] + (1<<S),0);
rep(mask,0,1<<S){
if (dp[cur][mask] == 0) continue;
rep(c,0,S){
// 在camp[mask] 中的才能加
if (!(camp[mask] & (1 << c))) continue;
add(dp[cur ^ 1][trans[mask][c]] , dp[cur][mask]);
}
}
cur ^= 1;
}

ll ans = 0;
rep(mask,1,1<<S) add(ans,dp[cur][mask]);
printf("%lld\n", ans);
}

总结

我的思路里关于 排序,代表元都有了, 这是好事,但是

其实这里一个核心 在于怎么把 最小值元素X,抽象的表示到一个dp的state中

这里给出的state的设计方案是最长两两可换的前缀的bitmask, 和长度来表示

换句话说如果有人告诉我怎么设计state,那么转移方程还是随便写的

参考

官方

0%