Codeforces Round #777 (Div. 2)

A 和 B 比较简单就不写了。

C. Madoka and Childish Pranks

一个初始是全白的矩阵,每次可以选一个子矩阵染成棋盘(左上角是白色)

构造一个不超过 \(n*m\) 次的方法把矩阵染成目标样子,或输出无解。

Key :每次染一个 \(1\ast 2\) 的,可以把右侧的变黑,\(2\ast 1\) 的可以把下侧的变黑。

因此对于每一行,我又可以从右往左依次染 \(1\ast 2\) ,除第一列任何位置都可以染黑。

对于第一列从下往上依次染 \(2\ast 1\) ,除 \((1,1)\) 位置外都可以染黑。

所以只要 \((1,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
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

inline int rd() {
int x = 0;
bool f = 0;
char c = getchar();
for (; !isdigit(c); c = getchar()) f |= (c == '-');
for (; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return f ? -x : x;
}

inline int gn() {
char c = getchar();
for (; !isdigit(c); c = getchar());
return c - '0';
}

#define N 107
#define fr first
#define sc second
#define mp make_pair
#define pb push_back
#define pii pair<int, int>

int a[N][N];

vector<pair<pii,pii>> s;

inline void work() {
s.clear();
int n = rd(), m = rd();
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) a[i][j] = gn();
if (a[1][1]) {puts("-1"); return;}
for (int i = 1; i <= n; ++i)
for (int j = m; j > 1; --j)
if (a[i][j]) s.pb(mp(mp(i,j - 1), mp(i, j)));
for (int i = n; i > 1; --i)
if (a[i][1]) s.pb(mp(mp(i - 1, 1), mp(i, 1)));
printf("%d\n", (int)s.size());
for (auto x : s)
printf("%d %d %d %d\n", x.fr.fr, x.fr.sc, x.sc.fr, x.sc.sc);
}

int main() {
for (int t = rd(); t; --t) work();
return 0;
}

D. Madoka and the Best School in Russia

定义一个数字 \(x\) 是好的,当且仅当 \(x\)\(d\) 的倍数。

定义一个数字 \(x\) 是漂亮的,当且仅当他不能被拆分成两个好的数的乘积(也就是只含有一个 \(d\)

给你一个好的数 \(x\) ,问你是否有至少两种不同的方法,把 \(x\) 拆成若干个漂亮的数的乘积。

方法不同即拆分得到的数集不同。

因为每个漂亮数有且仅有一个 \(d\) ,因此 \(x\) 里有几个 \(d\) ,就至多要拆成几个漂亮数。

先考虑把 \(x\) 里的 \(d\) 都去掉,剩下的数是 \(y\) ,我们至少得到一种方案是 \(d,d,\dots,d,d\ast y\)

  • 如果 \(y\) 可以拆分(不是素数),那么就肯定有解;

  • 如果 \(y\) 不可拆分:

    • 如果 \(d\) 不可拆分,肯定无解(没有可拆的了)
    • 如果 \(x\) 里只有两个 \(d\) ,肯定无解(没有可拆的了)
    • 如果 \(x\) 里有超过三个 \(d\) ,肯定有解(把 \(y\)\(d\) 拆分得到的三个数,分配给另外三个 \(d\)
    • 如果 \(x\) 里正好有三个 \(d\) ,需要检验一下把 \(d\) 拆出来的两部分某一部分分给 \(y\) 会不会形成新的 \(d\)
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
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

inline int rd() {
int x = 0;
bool f = 0;
char c = getchar();
for (; !isdigit(c); c = getchar()) f |= (c == '-');
for (; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return f ? -x : x;
}

#define N 107

inline void work() {
int x = rd(), d = rd();
if (x % d != 0) {puts("NO"); return;}
if (x / d % d != 0) {puts("NO"); return;}
int cnt = 0;
for (; x % d == 0; x /= d, ++cnt);
int lim = sqrt(x);
for (int i = 2; i <= lim; ++i)
if (x % i == 0) {puts("YES"); return;}
if (cnt == 2) {puts("NO"); return;}
lim = sqrt(d);
for (int i = 2; i <= lim; ++i)
if (d % i == 0) {
if (cnt > 3) {puts("YES"); return;}
if (1ll * i * x % d != 0 || 1ll * d / i * x % d != 0) {puts("YES"); return;}
}
puts("NO");
}

int main() {
for (int t = rd(); t; --t) work();
return 0;
}

E. Madoka and the Sixth-graders

题意太复杂,我简单说一下,不清楚的看原题。

给定一个共 \(n\) 个点的内向基环树森林,开始每个点上有一个数(是 \(n\) 的排列)。

每一个时刻所有点按照所在边移动一次,如果某一时刻某个点上有很多数,只保留最小的。

如果某一时刻某个点上没数了(叶子),那么按照节点编号从小到大依次往里面塞 \(n+1,n+2,\dots\)

现在给出森林的形态,和经过若干时刻后每个点上的数字 \(a_1,\dots,a_n\)

请你还原出来一个字典序最小的初始状态,保证有解。

Key 1 :假设已知经过的时间是 \(t\) ,那么每个点按照边移动 \(t\) 步以后的位置上的数字一定是不超过 \(n\) 的。

证明:如果是树的部分,那么 \(n\) 以后的数字永远追不上;如果是环的部分,因为 \(n\) 以后的数字比 \(n\) 大,所以都会被舍弃。

Key 2 :两个点如果在某一步之后同时移动到了同一个点,那么后面的路径都相同(因为每个点只有一个出边)。

找出经过的时间 \(t\) :假设叶子个数是 \(cnt\) ,序列里最大是 \(mx\) ,那么 \(t=\frac{mx - n}{cnt}\) (每一次移动会引进 \(cnt\) 个数)。


每个位置可能会放哪个数?

根据前面提到的 Key,假设 \(u\)\(t\) 步之后到达的点是 \(v\) ,那么 \(u\) 上开始的数字要么是 \(a_v\) ,要么比 \(a_v\) 大,在移动的过程中某一步被挤掉了。

也就是说,如果一个点集 \(S\) 里所有点走 \(t\) 步以后到达的点都是 \(v\) ,那么这些点初始状态里有一个必定是 \(a_v\)其他都比 \(a_v\)

怎么找 \(v\) :因为每个点都只有一条出边,因此可以直接倍增找


怎么贪心?

我们先令结果序列 \(b_u=a_v\) ,也就是假设每个点的初始状态就是走 \(t\) 步以后的位置上的值。

\(S_x=\\{u|b_u=x\\}\) ,考虑从小到大放数字 \(x\)

  • 如果 \(S_x\ne \emptyset\) ,那么就把 \(x\) 放到 \(S_x\) 中位置最靠前的
  • 如果 \(S_x=\emptyset\) ,那么就把 \(x\) 放到 \(S_1\cup S_2\cup\dots\cup S_{x-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
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

inline int rd() {
int x = 0;
bool f = 0;
char c = getchar();
for (; !isdigit(c); c = getchar()) f |= (c == '-');
for (; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return f ? -x : x;
}

#define N 100007

int f[N][40], a[N], res[N];

bool vis[N];

queue<int> s[N];

set<int> S;

inline void work() {

int n = rd();
for (int i = 1; i <= n; ++i) {
f[i][0] = rd(); vis[f[i][0]] = 1;
}
for (int j = 1; j < 40; ++j)
for (int i = 1; i <= n; ++i)
f[i][j] = f[f[i][j - 1]][j - 1];

int mx = 0, cnt = 0;
for (int i = 1; i <= n; ++i) {
if (!vis[i]) ++cnt;
a[i] = rd(); mx = max(mx, a[i]);
}
cnt = (mx - n) / cnt;

memset(vis, 0, sizeof(vis));

for (int i = 1; i <= n; ++i) {
int u = i;
for (int k = 30; k >= 0; --k)
if (cnt & (1 << k)) u = f[u][k];
res[i] = a[u]; s[res[i]].push(i);
vis[res[i]] = 1; //Sx不空
}

for (int i = 1; i <= n; ++i)
if (vis[i]) s[i].pop(); //把i放到Si最靠前的位置

int nw = 0;
for (int i = 1; i <= n; ++i)
if (!vis[i]) {
while (nw <= i) {
while (s[nw].size()) {
S.insert(s[nw].front());
s[nw].pop();
}
++nw;
}
res[*S.begin()] = i; S.erase(S.begin());
}
for (int i = 1; i <= n; ++i) printf("%d ", res[i]);
}

int main() {
for (int t = 1; t; --t) work();
return 0;
}

F. Madoka and Laziness

给定一个没有重复数值的数列 \(\{a_i\}\) ,问有多少种方式将数列划分为两个峰序列(严格单增再单减)。

两种划分不同,当且仅当至少某一个峰序列的峰值不同(认为 <a,b><b,a> 相同)。

Key :最大值一定是其中一个峰的峰值。

这个观察有什么用呢?因为一个峰值固定,我们可以得到答案是 O(n) 的。

也就是说,我们只需要去检验,其他的每一个值是否有可能成为峰值即可。

怎么检验呢?只需要判断是否存在一种拆分方式,使得到每个位置之前是升的,过了这个位置之后是降的,并且没给这个序列的元素扔给最大值所在的序列都合法。


我们可以假设另一个峰值(峰值B)在最大值(峰值A)的右侧,然后把序列翻过来再做一遍即可。

这样就有三个阶段:

  • 到峰值A前:两个序列都上升

这一阶段序列A里只要单调递增,放多大的都可以。

我们为了后面序列B还要增的考虑,应该尽量减少序列B在这一部分的最大值。

\(f_i\) 为考虑前缀 \(i\) ,则 \(a_i\) 必定为某一个序列结尾,则另一个序列结尾最小值是多少。

转移很简单,接在 \(a_{i-1}\) 后,或接在 \(f_{i-1}\) 后。

1
2
if (a[i] > a[i - 1]) f[i] = min(f[i], f[i - 1]);
if (a[i] > f[i - 1]) f[i] = min(f[i], a[i - 1]);
  • 峰值A后,峰值B前:序列A下降,序列B上升

这一阶段因为两个序列单调性不同,所以贪心策略也不同。

\(g_{i,0}\) 表示第 \(i\) 个元素放到序列A里,另一个序列(正在上升的序列B)末尾最小是多少。

\(g_{i,1}\) 表示第 \(i\) 个元素放到序列B里,另一个序列(正在下降的序列A)末尾最大是多少。

转移也很简单,讨论一下接在谁后面就好了。

1
2
3
4
if (a[i] < a[i - 1]) g[i][0] = min(g[i][0], g[i - 1][0]);
if (a[i] < g[i - 1][1]) g[i][0] = min(g[i][0], a[i - 1]);
if (a[i] > a[i - 1]) g[i][1] = max(g[i][1], g[i - 1][1]);
if (a[i] > g[i - 1][0]) g[i][1] = max(g[i][1], a[i - 1]);
  • 峰值B后:两个序列都下降

倒着考虑,变成单增的,那么在保证峰值B跟着的序列合法的前提下,另一个序列(留给峰值A的)的最大值要尽可能小。

因此处理方法同第一个阶段(设为 \(h\) 数组)。


那么怎么判断是否合法嘞?把 \(a_i\) 放到序列B里,另一个序列合法( 也就是满足 g[i][1] > h[i] ) 就可以啦~

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

inline int rd() {
int x = 0;
bool f = 0;
char c = getchar();
for (; !isdigit(c); c = getchar()) f |= (c == '-');
for (; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return f ? -x : x;
}

#define N 500007
#define inf 1e9 + 7

int n, a[N], f[N], g[N][2];

inline int calc() {

int p = 0; //maxpos
for (int i = 1; i <= n; ++i) {
f[i] = g[i][0] = inf;
g[i][1] = -1;
if (a[i] > a[p]) p = i;
}

for (int i = 1; i <= p; ++i) {
if (a[i] > a[i - 1]) f[i] = min(f[i], f[i - 1]);
if (a[i] > f[i - 1]) f[i] = min(f[i], a[i - 1]);
}

g[p][0] = f[p]; //承接第一阶段的最宽松约束
for (int i = p + 1; i <= n; ++i) {
if (a[i] < a[i - 1]) g[i][0] = min(g[i][0], g[i - 1][0]);
if (a[i] < g[i - 1][1]) g[i][0] = min(g[i][0], a[i - 1]);
if (a[i] > a[i - 1]) g[i][1] = max(g[i][1], g[i - 1][1]);
if (a[i] > g[i - 1][0]) g[i][1] = max(g[i][1], a[i - 1]);
}

int ans = 0;
for (int i = n; i > p; --i) {
if (a[i] > a[i + 1]) f[i] = min(f[i], f[i + 1]);
if (a[i] > f[i + 1]) f[i] = min(f[i], a[i + 1]);
if (g[i][1] > f[i]) ++ans;
}

return ans;
}

int main() {
n = rd();
for (int i = 1; i <= n; ++i) a[i] = rd();
int ans = calc();
reverse(a + 1, a + 1 + n);
printf("%d\n", ans + calc());
return 0;
}