Codeforces Round #789 (Div. 2)

赛后 vp 的,最后只写到 \(\rm C\),大概原因是 \(\rm D\) 思路乱了,没有观察到行列分别计算这一点,只是单纯讨论它们对答案的贡献总合。

A. Tokitsukaze and All Zero Sequence

\(t\) 组数据。给出一个长为 \(n\) 的序列 \(a\),可以不断进行以下操作:

  • 选择两个数 \(a_i,a_j\)
  • 如果 \(a_i=a_j\),则把它们其中一个置为 \(0\),否则全部置为 \(\min(a_i,a_j)\)

求把 \(a\) 全变为 \(0\) 需要的最小操作次数。(\(1\le t\le 10^3,2\le n\le 100,0\le a_i\le 100\))

分讨一下就可以了。注意到如果 \(a\) 中出现 \(0\) 了,则只需要 \(n-m\) 次即可,其中 \(m\) 表示 \(0\) 的个数,只需要把剩下的数都和 \(0\) 做一次操作即可。然后对于不出现 \(0\) 的情况,我们尽量让它出现 \(0\),如果存在 \(a_i=a_j\),则可以\(1\) 次操作出 \(1\)\(0\),这样就是 \(n-1+1=n\) 次操作。如果不存在 \(a_i=a_j\),可以 \(1\) 次操作出一个相等的数对,这样就是 \(n+1\) 次操作。时间复杂度 \(\mathcal{O}(tn\log n)\)

#include <cstdio>
#include <algorithm>
const int N = 110; int a[N];
int main()
{
    int qwq; scanf("%d", &qwq);
    while (qwq--)
    {
        int n, cnt0 = 0; scanf("%d", &n);
        for (int i = 1; i <= n; ++i) scanf("%d", a + i), cnt0 += !a[i];
        if (cnt0) { printf("%d\n", n - cnt0); continue; }
        std::sort(a + 1, a + n + 1); int flg = 0;
        for (int i = 2; i <= n; ++i) flg |= (a[i] == a[i - 1]);
        printf("%d\n", n + !flg);
    }
    return 0;
}

B1. Tokitsukaze and Good 01-String (easy version)

\(t\) 组数据。给出一个长度 \(n\)\(n\) 为偶数的 \(\tt01\) 串,问最少反转几位能使得所有极长的连续相同字符段长度均为偶数。(\(1\le t\le 10^4,2\le n,\sum n\le 2\times 10^5\))

有一个比较好想的贪心做法,也能为 \(\rm B2\) 提供一点思路参考。考虑对于最左端的极长连续段,如果它长度为偶数,则直接删去,如果长度为奇数,则就在它右边的极长连续段的最左边反转一个字符,使它变为偶数长度,然后删去,之后再处理剩下的字符串即可,到最后一个的时候,因为 \(n\) 为偶数,所以最后一段一定是偶数。这样处理,能把所有需要操作的都操作到,并最小化它们对其他段的影响。时间复杂度 \(\mathcal{O}(tn)\)

#include <cstdio>
#include <algorithm>
const int N = 2e5 + 10; char s[N]; int cnt[N];
int main()
{
    int qwq; scanf("%d", &qwq);
    while (qwq--)
    {
        int n, tn = 0; scanf("%d%s", &n, s + 1);
        for (int i = 1; i <= n; ++i) cnt[i] = 0;
        for (int i = 1; i <= n; ++i) tn += (s[i] != s[i - 1]), ++cnt[tn];
        int ans = 0;
        for (int i = 1; i <= tn; ++i) if (cnt[i] & 1) --cnt[i + 1], ++ans;
        printf("%d\n", ans);
    } 
    return 0;
}

B2. Tokitsukaze and Good 01-String (hard version)

在上题的基础上额外询问:

  • 在使所有极长连续段变为偶数,且操作最少的基础上,最小化极长连续段的数量,求出这个数量。

数据范围和多测不变。

观察简单版的构造思路,事实上,这个思路稍加改动即可得到最少的连续段。注意到我们相当于在交界处把 \(\tt01,10\) 改为了 \(\tt00,11\)。更进一步,如果我们把字符串分为形如 \((1,2),(3,4),\cdots\) 这样的二元组,则一旦一个二元组是形如 \(\tt10,01\) 的形式,我们就一定要把它改掉。证明可以考虑归纳,假设这个二元组之前所有的不合法二元组都已经改掉并且符合题意,不妨设 \((i,i+1)=(\mathtt{1},\mathtt{0})\),则 \(i\) 对应的 \(\tt1\) 连续段长度一定为奇数。因为 \(i\) 为奇数,且前面的连续段长度之和为偶数。改成 \(\tt00\)\(\tt11\) 后,就符合题意了,且是最小的操作次数。如果我们从这个角度理解上道题的构造思路,则相当于最后决定变为还是 \(\tt 00,11\) 的是 \(i\) 所在的连续段。而我们只需要把它改为 \(i-1\) 所在的连续段,即可得到最小的连续段数量了,因为这样在受限的修改次数内,每次都缩小连续段数量。因为 \(\tt 01,10\) 总能接上,所以它们对最终的连续段不做贡献。而 \(\tt 00,11\) 它们不能修改,所以在不与 \(i-1\) 对应的连续段相同时,会贡献一个连续段。定义第 \(0\) 位不属于 \(\tt 0,1\) 中的任何一个即可获得边界,时间复杂度 \(\mathcal{O}(tn)\)。对了,至少要有一个连续段,如果全是 \(\tt 01\) 要特判掉,一开始就有 \(\tt 01\) 可以看做修改后和第 \(0\) 位属于同一个连续段,这样再出现 \(\tt 00,11\) 时能正确计算。

#include <cstdio>
#include <algorithm>
const int N = 2e5 + 10; char s[N]; int cnt[N];
int main()
{
    int qwq; scanf("%d", &qwq);
    while (qwq--)
    {
		// 代码里用 c 记录 i-1 的连续段颜色
        int n, a = 0, b = 0, c = -1; scanf("%d%s", &n, s + 1);
        for (int i = 1; i <= n; i += 2)
            if (s[i] != s[i + 1]) ++a; // 增加修改次数,但不改变连续段颜色
            else if (c != s[i]) ++b, c = s[i]; // 增加连续段,且变色
        printf("%d %d\n", a, std::max(b, 1));
    } 
    return 0;
}

C. Tokitsukaze and Strange Inequality

\(t\) 组数据。给出一个长为 \(n\) 的排列 \(p\),求满足以下条件的四元组 \((a,b,c,d)(1\le a<b<c<d\le n)\)

  • \(p_a<p_c\),且 \(p_b>p_d\)

两个四元组不同,当且仅当其中一个数不一样。(\(1\le t\le 10^3,4\le n,\sum n\le 5\times 10^3\))

注意到 \(n\) 不大,所以这支持我们枚举其中两个数,统计另外两个。如果枚举 \((a,b),(c,d)\) 等这种不在同一个不等式里的,则会导致剩下两个既有和枚举的两个的大小关系,还有相互之间相对位置的关系,非常不好处理。所以还是考虑枚举在同一不等式里的 \((a,c)\)\((b,d)\)。代码和接下来的讲解以 \((a,c)\) 为例。

枚举完 \((a,c)\) 后剩下的问题是,统计一个区间内的数的价值和,其中一个数的价值定义为另一个区间里比它小的数个数。其中第一个区间为 \([a+1,c-1]\),第二个区间为 \([c+1,n]\),容易发现分别就是 \(b,d\) 的取值范围。这个信息就非常好预处理了。考虑设 \(f_{i,j}\) 表示前 \(i\) 个数内,有多少比 \(j\) 小的,可以用前缀和轻松处理。然后再设 \(g_{i,j}\)\(i\) 个数,对于区间 \([1,j]\) 的价值和,其中价值的定义同上。这个对 \(f_{i,j}\) 做前缀和也能得到。然后最终 \((a,c)\) 的贡献即为 \((g_{c-1,n}-g_{a,n})-(g_{c-1,c}-g_{a,c})\)。时间复杂度 \(\mathcal{O}(n^2)\)

不想预处理用 DS 加速也能过,多个 \(\log\)

#include <cstdio>
#include <algorithm>
const int N = 5e3 + 10; int p[N], pre[N][N], d[N][N];
int main()
{
    int qwq; scanf("%d", &qwq);
    while (qwq--)
    {
        int n; scanf("%d", &n);
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j) pre[i][j] = 0;
        for (int i = 1; i <= n; ++i) scanf("%d", p + i), ++pre[i][p[i]];
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j) pre[i][j] += pre[i][j - 1] + pre[i - 1][j] - pre[i - 1][j - 1];
        // pre_{i,j} 表示前 i 个数有多少比 j 小的,是排列,所以不用担心 <=
        for (int i = 1; i <= n; ++i) 
            for (int j = 1; j <= n; ++j) d[i][j] = d[i - 1][j] + pre[j][p[i]];
        // d_{i,j} 表示前 i 个数,在前 j 个数内比它们小的数个数和
        long long ans = 0;
        for (int a = 1; a <= n; ++a)
            for (int c = a + 2; c <= n; ++c)
            {
                if (p[a] >= p[c]) continue;
                ans += (d[c - 1][n] - d[a][n]) - (d[c - 1][c] - d[a][c]);
            }
        printf("%lld\n", ans);
    } 
    return 0;
}

D. Tokitsukaze and Meeting

\(t\) 组数据。给出一个长为 \(nm\)\(\tt 01\)\(s\),对于每个前缀,求把它逆序加入 \(n\times m\) 的网格后,有多少行,列至少有一个 \(\tt 1\)。定义把一个串逆序加入网格为,从串的末尾开始数,第 \(i\) 个字符在第 \(\lceil\frac{i}{m}\rceil\) 行,第 \((i-1)\bmod{m}+1\) 列。(\(1\le t\le 10^4,1\le n,m,nm,\sum nm\le 10^6\))

注意到,从 \(s_{1,i}\)\(s_{1,i+1}\) 的过程中,形成的网格形态对于行和列是有截然不同的变化的。对于行来说,是一种奇怪的整体位移,并不是很好看出规律,而对于列,是非常有规律的平移,除了有一列新增加了一个数之外,其他的没有任何变化。

我们先来考虑看起来比较简单的列,更进一步观察可以发现,除了 \(i+1\) 所在列,其余列没有任何变化,而 \(i+1\) 新增加到所在列里,对答案只有一种情况会做贡献,那就是这一列本来没有 \(\tt 1\),而 \(s_{i+1}=\tt 1\)。用一个桶记录哪些列没有值即可。(可以直接以模 \(m\) 的结果作为关键字)

然后对于行,它不好从 \(i\) 对应到 \(i+1\)。不过可以注意到,对于 \(s_{1,i}(1\le i\le m)\) 的情况都是平凡的,只有一行,直接判断即可。而对于 \(s_{1,i+m}\),它相当于在 \(s_{1,i}\) 的基础上,从第一行额外插入了一行,我们只需要在 \(s_{1,i}\) 的答案基础上统计这一行的贡献即可!而这一行的贡献也是非常好统计的,考虑记录 \(s_{1,i}\) 中,最靠近 \(i\)\(\tt 1\) 的位置,如果位于第一行,或者说在区间 \((i-m,i]\) 内,则这一行就能造成 \(1\) 的贡献。

最后的答案就是把两部分加起来。时间复杂度 \(\mathcal{O}(tnm)\)

#include <cstdio>
#include <algorithm>
const int N = 1e6 + 10; char s[N]; int sta[N], ans[N];
int main()
{
    int qwq; scanf("%d", &qwq);
    while (qwq--)
    {   
        int n, m, k = 0, las = -1e9; scanf("%d%d%s", &n, &m, s + 1);
        for (int i = 1; i <= n * m; ++i)
        {
            if (s[i] == '1') las = i;
            if (s[i] == '1' && !sta[i % m]) sta[i % m] = 1, ++k;
            if (i <= m && las != -1e9) ans[i] = 1;
            if (i > m) ans[i] = ans[i - m] + (i - las < m);
            printf("%d ", ans[i] + k);
        }
        for (int i = 0; i <= n * m; ++i) ans[i] = 0, sta[i] = 0;
        puts("");
    } 
    return 0;
}

E. Tokitsukaze and Two Colorful Tapes

\(t\) 组数据。给出两个 \(1\sim n\) 的排列 \(p_1,p_2\),数 \(i\) 有权值 \(q_i\),其中 \(q\) 也是 \(1\sim n\) 的排列。对于一种权值 \(q\),定义它的价值为:

\[\sum_{i=1}^n|q_{p_{1,i}}-q_{p_{2,i}}| \]

给出 \(p_1,p_2\),求出价值的最大值。(\(1\le t\le 10^4,1\le n\le 10^5,\sum n\le 2\times 10^5\))

首先比较容易能想到的一个转化是,在 \(p_{1,i},p_{2,i}\) 之间连边。因为它们都是排列,所以一定能得到一个若干环的集合,每个点的度数均为 \(2\),类似置换环。(如果把自环视为该点度数为 \(2\) 的话)则答案的统计就变为,一条边上两个点权值差的绝对值之和。

我们来考虑一下一个点 \(i\) 做出的贡献。它有两条相邻的边,则贡献即为:

\[|q_i-q_{i-1}|+|q_i-q_{i+1}| \]

中与 \(q_i\) 有关的项。这里加法减法都是在环意义下做的。(即 \(1-1=c,c+1=1\),其中 \(c\) 表示环的大小)分讨一下可以发现共有三种情况:

\[\begin{cases}2q_i&q_i>q_{i-1},q_i>q_{i+1}\\-2q_i&q_i<q_{i-1},q_i<q_{i+1}\\0&\tt otherwise\end{cases} \]

考虑根据这个结论来给每个环上的点分配权值。首先,一个环长如果为偶数,则它可以做到全部的点都满足前两种情况,如果为奇数,则可以只有一个点不满足。这里要尽可能让填满足情况的点更多,感性理解(理性证明真不会 /kk)可以发现是更优的。

那这样,一个大小为 \(c\) 的环就会产生 \(2\lfloor\frac{c}{2}\rfloor\) 个特殊点,其中可以做到一半产生 \(2q_i\) 的贡献,另一半产生 \(-2q_i\),具体来讲,只需要大小交替填即可。显然,我们从按照 \(n\sim 1\) 的顺序填产生正贡献的权值,按照 \(1\sim n\) 的顺序填产生负贡献的权值。剩下的数填到不做贡献的空位即可。

这样,设 \(C\) 为所有环的 \(\lfloor\frac{c}{2}\rfloor\) 之和,最终填进去,且做贡献的数为:

\[\begin{cases}+:n,n-1,n-2,\cdots,n-C+1\\-:1,2,3,\cdots,C\end{cases} \]

根据等差数列求和,答案即为:

\[2\dfrac{(n+n-C+1)C}{2}-2\dfrac{(1+C)C}{2}=2C(n-C) \]

\(\rm dsu\) 找出所有环大小后直接套公式即可。时间复杂度 \(\mathcal{O}(tn\alpha(n))\)

#include <cstdio>
const int N = 1e5 + 10; int p[N];
struct DSU
{
    int f[N], size[N], n;
    void init(int N) { n = N; for (int i = 1; i <= n; ++i) f[i] = i, size[i] = 1; }
    int getf(int x) { return x == f[x] ? x : f[x] = getf(f[x]); }
    void merge(int x, int y) { if (getf(x) != getf(y)) size[getf(y)] += size[getf(x)], f[getf(x)] = getf(y); }
}dsu;
int main()
{
    int qwq; scanf("%d", &qwq);
    while (qwq--)
    {
        int n, c = 0; scanf("%d", &n); dsu.init(n);
        for (int i = 1; i <= n; ++i) scanf("%d", p + i);
        for (int i = 1, x; i <= n; ++i) scanf("%d", &x), dsu.merge(x, p[i]);
        for (int i = 1; i <= n; ++i) if (dsu.getf(i) == i) c += (dsu.size[i] / 2);
        printf("%lld\n", 2ll * c * (n - c));
    }
    return 0;
}

F. Tokitsukaze and Permutations

\(t\) 组数据。有一个 \(1\sim n\) 的排列 \(p\),对它执行 \(k\) 轮冒泡排序后得到排列 \(a\)。定义一个序列 \(v\),其中 \(v_i=\sum\limits_{j=1}^{i-1}[a_j>a_i]\)。给出 \(v\),其中一些位是通配位,求出有多少种可能的 \(p\) 能最终得到 \(v\),答案对 \(998,244,353\) 取模。(\(1\le t\le 10^3,1\le n,\sum n\le 10^6,0\le k<n,-1\le v_i<i\)\(v_i=-1\) 表示这位是通配的)

下文定义 \(v'\) 是不包含通配位的 \(v\)。首先可以发现,知道 \(v'\) 后,\(a\) 是唯一确定的。考虑从 \(a_n\) 依次确定到 \(a_1\)。对于 \(a_n\),通过 \(v'_n\) 我们就能知道它的排名,从而得到它的值。对于 \(a_i\),我们能知道它在前 \(i\) 个数里的排名,由于 \([i+1,n]\) 内的数已知,所以也能知道它的值。

这样,既然 \(v'\) 和它能对应到的排列是唯一的,那我们只需要计数 \(p\) 对应的 \(v'\) 有几种可能即可,这样我们得到的信息也与需要计数的内容统一了。

我们来观察一下一轮冒泡排序会对 \(v'\) 造成怎样的影响。

\[\begin{array}{|c|c|c|}\mathrm{id}&p&v'\\0&\left<3,2,5,1,4\right>&\left<0,1,0,3,1\right>\\1&\left<2,3,1,4,5\right>&\left<0,0,2,0,0\right>\\2&\left<2,1,3,4,5\right>&\left<0,1,0,0,0\right>\\3&\left<1,2,3,4,5\right>&\left<0,0,0,0,0\right>\end{array} \]

好像能发现规律,首先 \(v'\) 是一直在左移的,不仅如此,在移动的时候还在一直 \(-1\),并和 \(0\)\(\max\)。且移动时空出来的位置直接置为 \(0\)\(v_1\) 直接被覆盖。

首先,空出来的位置是本轮排序把正确的数放到该放的地方,的位置,所以它前面一定没有比它大的,置为 \(0\) 即可。而手玩一下可以发现,一个数前面如果有比它大的,则一定恰有一个会被运送到它后面,这样它也要往前平移一格。

好,知道这个规律之后,我们就来看看什么样的 \(v'\) 在经过 \(k\) 轮后最终能和 \(v\) 匹配上。首先,前 \(k\) 个数已经被覆盖掉了,第 \(i\) 位可以是 \([0,i)\) 中的任何一个,所以共有 \(k!\) 种情况。而对于剩下的,\(v'_{i+k}\) 对应的是 \(v_i\)。如果 \(v_i=-1\),则 \(v'_{i+k}\) 可以任意取 \(i+k\) 种值的任何一个;如果 \(v_i>0\),则 \(v'_{i+k}\) 唯一固定为 \(v_i+k\);如果 \(v_i=0\),则 \(v'_{i+k}\) 满足 \(v'_{i+k}-k\le 0\),能取 \(k+1\) 种。最后,因为 \(k\) 轮下来,最后 \(k\) 个数一定是在对的位置的,所以如果 \(\exist i(n-k+1\le i\le n)\),满足 \(v_i\ne 0\)\(v_i\ne -1\),则无解。

时间复杂度 \(\mathcal{O}(tn)\)

#include <cstdio>
const int N = 1e6 + 10, mod = 998244353; typedef long long ll; int v[N];
int main()
{
    int qwq; scanf("%d", &qwq);
    while (qwq--)
    {
        int n, k, ans = 1; scanf("%d%d", &n, &k);
        for (int i = 1; i <= n; ++i) scanf("%d", v + i);
        int flg = 1;
        for (int i = n - k + 1; i <= n && flg; ++i) flg &= (v[i] == 0 || v[i] == -1);
        if (!flg) { puts("0"); continue; }
        for (int i = 1; i <= k; ++i) ans = (ll)ans * i % mod;
        for (int i = k + 1; i <= n; ++i)
            if (v[i - k] == 0) ans = (ll)ans * (k + 1) % mod;
            else if (v[i - k] == -1) ans = (ll)ans * i % mod;
        printf("%d\n", ans);
    }
    return 0;
}

话说 \(\rm F\) 的 key observation 和 P6186 [NOI Online #1 提高组] 冒泡排序 几乎是一样的。然而我做过那道题,没想到 \(\rm F\)\(v,a\) 之间的一一对应,令人感慨。

posted @ 2022-05-16 10:46  zhiyangfan  阅读(65)  评论(0编辑  收藏  举报