[题解][Codeforces]Codeforces Round #602 (Div. 1) 简要题解
- orz djq_cpp lgm
A
题意
-
给定一个分别含有 \(\frac n2\) 个左括号和右括号的括号序列
-
每次可以将序列的一个区间翻转
-
求一个不超过 \(n\) 次的操作方案,使得操作完之后的序列是合法括号序列,并且恰好有 \(k\) 个前缀是合法括号序列
-
多组数据,所有数据的 \(n\) 之和不超过 \(2000\)
做法:构造
-
随便生成一个合法的目标序列,如 \(k-1\) 个
()
相接再接上 \(\frac n2-k+1\) 个(
和 \(\frac n2-k+1\) 个)
-
依次从左往右检查,对于位置 \(i\) 如果和目标序列不一样则在右边找一个 \(j\) 使得当前序列第 \(j\) 个元素和目标序列第 \(i\) 个元素相等,并翻转区间 \([i,j]\)
-
\(O(n^2)\)
代码
#include <bits/stdc++.h>
template <class T>
inline void read(T &res)
{
res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
if (bo) res = ~res + 1;
}
const int N = 2005;
int n, k, m, l[N], r[N];
char s[N], tar[N];
void work()
{
read(n); read(k);
scanf("%s", s + 1);
for (int i = 1; i < k; i++)
tar[i * 2 - 1] = '(', tar[i * 2] = ')';
for (int i = 1; i <= n / 2 - k + 1; i++)
tar[(k - 1) * 2 + i] = '(', tar[(k - 1) * 2 + n / 2 - k + 1 + i] = ')';
m = 0;
for (int i = 1; i <= n; i++)
{
if (s[i] == tar[i]) continue;
int p;
for (int j = i; j <= n; j++)
if (s[j] == tar[i]) p = j;
l[++m] = i; r[m] = p;
for (int j = i, k = p; j < k; j++, k--) std::swap(s[j], s[k]);
}
printf("%d\n", m);
for (int i = 1; i <= m; i++) printf("%d %d\n", l[i], r[i]);
}
int main()
{
int T; read(T);
while (T--) work();
return 0;
}
B1 & B2
题意
-
给定一个长度为 \(n\) 的序列 \(a\)
-
定义「长度为 \(k\) 的最优子序列」表示长度为 \(k\) 的元素和最大的子序列,如果元素和最大的有多个则取字典序最小者
-
\(m\) 组询问,每次给出 \(k\) 和 \(pos\) 表示询问长度为 \(k\) 的最优子序列的第 \(pos\) 个数
-
\(1\le a_i\le 10^9\) ,\(1\le pos\le k\le n\)
-
Easy Version:\(1\le n,m\le 100\)
-
Hard Version:\(1\le n,m\le 2\times10^5\)
做法:贪心 + 树状数组上二分
-
显然最优策略是每次选最大数,最大数有多个则取最小下标
-
对于 Easy Version,可以每次把子序列求出来之后回答询问
-
而对于 Hard Version,只需将询问离线按 \(k\) 排序之后不断把下标加入集合,每次查询当前集合的第 \(pos\) 小值即可,可以在树状数组上二分实现
代码 (Hard Version)
#include <bits/stdc++.h>
template <class T>
inline void read(T &res)
{
res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
if (bo) res = ~res + 1;
}
const int N = 2e5 + 5;
int n, m, a[N], p[N], A[N], ans[N];
struct query
{
int th, id;
};
std::vector<query> que[N];
inline bool comp(const int &x, const int &y)
{
return a[x] > a[y] || (a[x] == a[y] && x < y);
}
void change(int x, int v)
{
for (; x <= n; x += x & -x)
A[x] += v;
}
int kth(int k)
{
int res = 0, x = 0;
for (int i = 19; i >= 0; i--)
if (x + (1 << i) <= n && res + A[x + (1 << i)] < k)
x += 1 << i, res += A[x];
return x + 1;
}
int main()
{
int x, y;
read(n);
for (int i = 1; i <= n; i++) read(a[i]), p[i] = i;
std::sort(p + 1, p + n + 1, comp);
read(m);
for (int i = 1; i <= m; i++)
{
read(x); read(y);
que[x].push_back((query) {y, i});
}
for (int i = 1; i <= n; i++)
{
change(p[i], 1);
for (int j = 0; j < que[i].size(); j++)
ans[que[i][j].id] = a[kth(que[i][j].th)];
}
for (int i = 1; i <= m; i++) printf("%d\n", ans[i]);
return puts(""), 0;
}
C
题意
-
给定一个由
.
和X
构成的 \(n\times m\) 矩阵 -
求一个最大的 \(T\) 使得存在一个由
.
和X
构成的矩阵 \(A\) -
重复 \(T\) 次,每次对于 \(A\) 上所有的
X
,将以其为中心的 \(3\times3\) 矩阵内的所有元素都变成X
-
这样 \(T\) 次操作结束之后能得到原矩阵,并且在任意一次操作之前没有任意一个
X
在矩阵边界上(即假设这个矩阵是无穷大的,且给定的 \(n\times m\) 区域之外全部都为.
,则操作完 \(T\) 次之后给定的 \(n\times m\) 区域之外也必须全部为.
) -
输出这个 \(T\) 和一个合法的 \(A\) 矩阵
-
\(1\le nm\le 10^6\)
做法:二分 + 贪心 + 二维前缀和
-
考虑二分这个 \(T\)
-
一个位置能放
X
的条件是原矩阵以该位置为中心的边长为 \(2T+1\) 的正方形内全部都是X
-
并且一个位置显然能放就放
-
按照这个原则生成一个 \(A\) 矩阵之后,检查原矩阵每个
X
是否都被覆盖即可 -
判断以某个位置为中心的边长为 \(2T+1\) 的正方形内全部都是
X
,以及判断原矩阵的一个X
是否被覆盖,都可以使用二维前缀和来实现 -
数组要用
vector
存 -
\(O(nm\log\min(n,m))\)
代码
#include <bits/stdc++.h>
template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}
template <class T>
inline T Max(const T &a, const T &b) {return a > b ? a : b;}
const int N = 1e6 + 5;
int n, m;
std::string s[N];
std::vector<int> sum[N], s2[N];
std::vector<char> ans[N];
int sum1(int l, int r, int x, int y)
{
return sum[r][y] - sum[l - 1][y] - sum[r][x - 1] + sum[l - 1][x - 1];
}
int sum2(int l, int r, int x, int y)
{
return s2[r][y] - s2[l - 1][y] - s2[r][x - 1] + s2[l - 1][x - 1];
}
bool check(int mid)
{
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
s2[i][j] = sum1(Max(1, i - mid), Min(n, i + mid),
Max(1, j - mid), Min(m, j + mid)) == 1ll * (mid * 2 + 1)
* (mid * 2 + 1);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
ans[i][j] = s2[i][j] ? 'X' : '.', s2[i][j] += s2[i][j - 1];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
s2[i][j] += s2[i - 1][j];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if ((sum2(Max(1, i - mid), Min(n, i + mid),
Max(1, j - mid), Min(m, j + mid)) > 0) ^ (s[i - 1][j - 1] == 'X'))
return 0;
return 1;
}
int main()
{
std::cin >> n >> m;
for (int i = 0; i < n; i++) std::cin >> s[i];
for (int i = 0; i <= m; i++) sum[0].push_back(0);
for (int i = 1; i <= n; i++)
{
sum[i].push_back(0);
for (int j = 1; j <= m; j++)
sum[i].push_back(sum[i][j - 1] + (s[i - 1][j - 1] == 'X'));
}
for (int j = 1; j <= m; j++)
for (int i = 1; i <= n; i++)
sum[i][j] += sum[i - 1][j];
for (int i = 0; i <= n; i++)
for (int j = 0; j <= m; j++)
s2[i].push_back(0), ans[i].push_back(0);
int l = 0, r = n * m;
while (l <= r)
{
int mid = l + r >> 1;
if (check(mid)) l = mid + 1;
else r = mid - 1;
}
std::cout << r << std::endl;
check(r);
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++) putchar(ans[i][j]);
puts("");
}
return 0;
}
D1 & D2
题意
-
给定一个长度为 \(n\) 的序列 \(h\) ,每个数都是 \([1,k]\) 内的整数
-
求有多少个长度为 \(n\) 的,每个数都为 \([1,k]\) 内整数的序列 \(a\) ,使得:
-
\[\sum_{i=1}^n[a_i=h_i]<\sum_{i=1}^n[a_i=h_{i\bmod n+1}] \]
-
答案对 \(998244353\) 取模
-
\(1\le k\le 10^9\)
-
Easy Version:\(1\le n\le 2000\)
-
Hard Version:\(1\le n\le 2\times10^5\)
做法:组合数学
-
先把式子换一种写法
-
\[\sum_{i=1}^n([a_i=h_{i\bmod n+1}]-[a_i=h_i])>0 \]
-
易得对于每个 \(i\) ,如果 \(h_i=h_{i\bmod n+1}\) 则 \([a_i=h_{i\bmod n+1}]-[a_i=h_i]\) 总是等于 \(0\)
-
故我们只需考虑满足 \(h_i\neq h_{i\bmod n+1}\) 的 \(i\) ,设这样的 \(i\) 有 \(m\) 个,最后答案乘上 \(k^{n-m}\) 即可
-
易得,如果 \(h_i\neq h_{i\bmod n+1}\) ,那么 \([a_i=h_{i\bmod n+1}]-[a_i=h_i]\) 有 \(1\) 种方式取得 \(1\) ,\(1\) 种方式取得 \(-1\) ,\(k-2\) 种方式取得 \(0\)
-
对于 Easy Version 可以直接 \(O(n^2)\) DP
-
对于 Hard Vers考虑枚举这 \(m\) 个式子中有多少个式子不为 \(0\) (设为 \(s\) 个),问题转化成有 \(s\) 个数,每个数可以取 \(1\) 或 \(-1\) ,求和大于 \(0\) 的方案数,最后乘上 \((k-2)^{m-s}\)
-
易得对于这 \(s\) 个数,和大于 \(0\) 的方案和和小于 \(0\) 的方案是对称的,故用 \(2^s\) 减掉和等于 \(0\) 的方案数之后除以 \(2\) 即可,故答案为:
-
\[k^{n-m}\sum_{i=1}^m\frac{2^i-[i\bmod 2=0]\binom{i}{\frac i2}}2(k-2)^{m-i} \]
-
\(O(n)\)
代码
#include <bits/stdc++.h>
template <class T>
inline void read(T &res)
{
res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
if (bo) res = ~res + 1;
}
const int N = 2e5 + 5, rqy = 998244353;
int n, k, h[N], pw[N], p2[N], fac[N], inv[N], dif, ans;
int C(int n, int m)
{
return 1ll * fac[n] * inv[m] % rqy * inv[n - m] % rqy;
}
int main()
{
int pw2 = 1;
read(n); read(k);
pw[0] = p2[0] = fac[0] = inv[0] = inv[1] = 1;
for (int i = 1; i <= n; i++)
{
read(h[i]);
pw[i] = 1ll * pw[i - 1] * k % rqy;
p2[i] = 1ll * p2[i - 1] * (k - 2) % rqy;
fac[i] = 1ll * fac[i - 1] * i % rqy;
}
for (int i = 2; i <= n; i++)
inv[i] = 1ll * (rqy - rqy / i) * inv[rqy % i] % rqy;
for (int i = 2; i <= n; i++) inv[i] = 1ll * inv[i] * inv[i - 1] % rqy;
for (int i = 1; i <= n; i++)
if (h[i] != h[i % n + 1]) dif++;
for (int i = 1; i <= dif; i++)
{
pw2 = (pw2 + pw2) % rqy;
int delta = pw2;
if (!(i & 1)) delta = (delta - C(i, i >> 1) + rqy) % rqy;
delta = 499122177ll * delta % rqy;
ans = (1ll * delta * C(dif, i) % rqy * p2[dif - i] % rqy
* pw[n - dif] + ans) % rqy;
}
return std::cout << ans << std::endl, 0;
}
E
题意
-
给定一个 \(n\) 个元素的数组 \(a\) ,每个数都是 \([1,n]\) 内的整数
-
每次操作可以选择该数组的一部分元素将它们减掉 \(1\)
-
要求每次操作选定的下标集合互不相同
-
求一个不超过 \(n+1\) 次操作的方案,可以证明一定存在
-
\(1\le n\le 10^3\)
做法:构造
-
可以从输出格式中得到启发(摘自官方题解) -
我们大胆猜想存在一个恰好 \(n+1\) 次操作的方案
-
考虑依次确定每个数在哪些操作中被减掉 \(1\)
-
考虑把所有 \(n+1\) 次操作分成一些集合,每个集合内的操作下标集合相同,一开始所有的操作都在一个集合内
-
要求对所有 \(n\) 个数处理完之后,所有的操作各自一个集合
-
于是我们不难想到每处理完一个数 \(a_i\) ,就分裂至少一个大于 \(1\) 的集合
-
而分裂集合实际上是在 \([1,n+1]\) 中选出 \(a_i\) 个染黑,其余染白,然后对于原来的每一个集合,如果这个集合内同时包含两种颜色,就按照颜色来分裂集合
-
于是我们需要保证:如果存在大于 \(1\) 的集合,则必须存在一个集合内同时包含两种颜色
-
随便取一个大于 \(1\) (设其大小为 \(s\) )的集合,枚举这个集合中多少个元素被染黑(设为 \(j\) 从 \(1\) 到 \(\min(a_i,s-1)\)),如果 \(a_i-j\le n+1-s\) 则合法
-
可以证明不管选取的大于 \(1\) 的集合是哪个,只要 \(a_i\in[1,n]\) ,合法方案一定是存在的
-
\(O(n^2\log n)\) (如果实现得和我一样辣鸡) 或 \(O(n^2)\)
Code
#include <bits/stdc++.h>
template <class T>
inline void read(T &res)
{
res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
if (bo) res = ~res + 1;
}
const int N = 1005, rqy = 1e9 + 7;
int n, a[N], m, bel[N], sze[N], ans[N][N], T, has[N], tmp[N];
inline bool comp(int a, int b)
{
return bel[a] < bel[b] || (bel[a] == bel[b] && ans[a][T] < ans[b][T]);
}
int main()
{
read(n);
for (int i = 1; i <= n; i++) read(a[i]);
std::cout << n + 1 << std::endl;
for (int i = 1; i <= n + 1; i++) bel[i] = 1;
m = 1;
for (int i = 1; i <= n; i++)
{
memset(sze, 0, sizeof(sze)); T = i;
if (m == n + 1)
{
for (int j = 1; j <= a[i]; j++) ans[j][i] = 1;
continue;
}
int p, cnt, tot = 0;
for (int j = 1; j <= n + 1; j++) sze[bel[j]]++;
for (int j = 1; j <= m; j++) if (sze[j] > 1) p = j;
for (int j = 1; j < sze[p] && j <= a[i]; j++)
if (a[i] - j <= n + 1 - sze[p])
{cnt = j; break;}
for (int j = 1; j <= n + 1; j++)
if (bel[j] == p && (++tot) <= cnt) ans[j][i] = 1;
tot = m = 0;
for (int j = 1; j <= n + 1; j++)
if (bel[j] != p && (++tot) <= a[i] - cnt) ans[j][i] = 1;
for (int j = 1; j <= n + 1; j++) has[j] = j;
std::sort(has + 1, has + n + 2, comp);
for (int j = 1; j <= n + 1; j++)
{
int u = has[j], v = has[j - 1];
if (j == 1 || bel[u] != bel[v] || ans[u][i] != ans[v][i]) m++;
tmp[u] = m;
}
for (int j = 1; j <= n + 1; j++) bel[j] = tmp[j];
}
for (int i = 1; i <= n + 1; i++)
{
for (int j = 1; j <= n; j++) printf("%d", ans[i][j]);
puts("");
}
return 0;
}
F
题意
-
给定两个集合 \(A\) 和 \(B\)
-
\(A\) 可以表示成 \(n_A\) 个区间的并集
-
\(B\) 可以表示成 \(n_B\) 个区间的并集
-
求 \(C=\{x|x=a\bigoplus b,a\in A,b\in B\}\) 内的所有数之和对 \(998244353\) 取模后的结果
-
\(\bigoplus\) 为按位异或运算
-
\(1\le n_A,n_B\le 100\) ,区间端点是 \(10^{18}\) 范围内的正整数
做法:线段树
-
好题
-
一个显然的想法是对集合 \(A\) 和 \(B\) 分别建立一棵值域为 \([0,2^{60}-1]\) 的线段树,把所有的区间插入到线段树上
-
然后枚举一个集合 \(A\) 拆出的区间和一个集合 \(B\) 拆出的区间,易得这样的区间可以表示成「前 \(k\) 位固定,后 \(60-k\) 位任意」的形式,这样的两个区间的异或集合可以很容易地用一个同样形式的区间表示出来
-
此外这些区间必然在线段树上对应了一个节点,如果某个节点存在一个祖先出现过则这个节点的影响不要计入答案
-
所以把所有的区间按照一定的方式(可以定义为在线段树上的 DFS 序)排序后,使得每个区间的祖先一定在其之前出现,就能实现判定每个节点是否对答案有贡献,对于有贡献的区间可以将该区间内所有整数的和计入答案
-
设 \(L=60\) ,则复杂度为 \(O(n^2L^2\log(n^2L^2))\) ,由于线段树的巨大常数,时间和空间都过不了
-
考虑如何优化。我们注意到,如果一个区间可以表示成「前 \(k\) 位固定,后 \(60-k\) 位任意」的形式,另一个区间可以表示成「前 \(x\) 位固定,后 \(60-x\) 位任意」的形式,那么这两个区间的异或集合区间的前 \(\min(k,x)\) 位是固定的,且只和两个区间内数的前 \(\min(k,x)\) 位有关
-
于是考虑 \(A\) 上的一个节点 \(p\) 和 \(B\) 上的一个节点 \(q\) ,设 \(p\) 的深度小于 \(q\) ,可以证明如果把 \(q\) 变为其父亲节点,那么 \(p\) 与 \(q\) 的异或集合是不变的
-
而线段树提取区间时每层遍历的点数最多 \(4\) 个,所以这两棵动态开点线段树上,某一层的有效点数为 \(O(n)\) 级别
-
我们再定义对于集合 \(A\) 或 \(B\) ,输入给出的区间在线段树上拆成的子区间对应的节点为黑点,线段树上的其余节点为白点
-
根据上面得到的性质,我们可以考虑枚举两棵线段树上深度相同的一对点(需要保证至少一个黑点),计算这两个点对应区间的异或区间
-
易得这样得到的最终异或集合是和原来等价的
-
这时复杂度为 \(O(n^2L\log(n^2L)\) ,可以通过此题
代码
#include <bits/stdc++.h>
template <class T>
inline void read(T &res)
{
res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
if (bo) res = ~res + 1;
}
typedef long long ll;
const int N = 105, M = 5e4 + 5, L = 4e6 + 5, rqy = 998244353, I2 = 499122177;
const ll ET = (1ll << 60) - 1;
int n, na, nb, ans;
struct seg
{
int rt, lc[M], rc[M], dep[M], ToT;
bool mark[M];
ll num[M];
void orznc(int T, ll l, ll r, ll s, ll e, ll x, int &p)
{
if (e < l || s > r) return;
if (!p) dep[p = ++ToT] = T, num[p] = x;
if (s <= l && r <= e) return (void) (mark[p] = 1);
ll mid = l + r >> 1;
orznc(T - 1, l, mid, s, e, x, lc[p]);
orznc(T - 1, mid + 1, r, s, e, x | (1ll << T - 1), rc[p]);
}
} A, B;
struct elem
{
int dep; ll num;
} a[L];
inline bool comp(elem a, elem b)
{
return a.num < b.num || (a.num == b.num && a.dep > b.dep);
}
int main()
{
ll l, r, lst = -1; int d;
read(na);
while (na--) read(l), read(r), A.orznc(60, 0, ET - 1, l, r, 0, A.rt);
read(nb);
while (nb--) read(l), read(r), B.orznc(60, 0, ET - 1, l, r, 0, B.rt);
for (int p = 1; p <= A.ToT; p++)
for (int q = 1; q <= B.ToT; q++)
if (A.dep[p] == B.dep[q] && (A.mark[p] || B.mark[q]))
{
int T = A.dep[p];
a[++n] = (elem) {T, A.num[p] ^ B.num[q]};
}
std::sort(a + 1, a + n + 1, comp);
for (int i = 1; i <= n; i++)
{
if (lst != -1 && d >= a[i].dep && (lst >> d) == (a[i].num >> d))
continue;
d = a[i].dep; lst = a[i].num;
ans = (lst % rqy * ((1ll << d) % rqy) + ans) % rqy;
ans = (((1ll << d) % rqy) * (((1ll << d) - 1) % rqy)
% rqy * I2 + ans) % rqy;
}
return std::cout << ans << std::endl, 0;
}