Codeforces 杂题集 2.0

时间:2022-12-26 11:41:44

 

记录一些没有写在其他随笔中的 Codeforces 杂题, 以 Problemset 题号排序

 

1326D2 - Prefix-Suffix Palindrome (Hard version)

题意: 给出一个串 s, |s| ≤ 1e6, 要求选出一个前缀和一个后缀(不相交, 可以为空), 使得它们连接后是一个回文串. 求最长的回文串.

思路: 马拉车处理半径数组, 用前缀, 后缀, 半径与相同的前后缀相交的情况更新答案.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 1e6 + 5; int t, n;
string s0, res;
char s[2 * maxn];
int d[2 * maxn]; int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> t;
while (t--) {
res = "";
cin >> s0;
n = s0.size();
s[0] = '#';
inc(i, 0, n - 1) {
s[i * 2 + 1] = s0[i];
s[i * 2 + 2] = '#';
}
int l = 0, r = -1;
inc(i, 0, 2 * n) {
if (i <= r)
d[i] = min(d[l + r - i], r - i);
else
d[i] = 0;
int del = 0;
for (;; del++) {
if (i - d[i] - del < 0 || i + d[i] + del > 2 * n) break;
if (s[i - d[i] - del] != s[i + d[i] + del]) break;
}
del--;
d[i] += del;
if (i + d[i] > r) {
r = i + d[i];
l = i - d[i];
}
}
int f1 = 0, f2 = 0;
inc(i, 1, n) {
if (d[i] == i) {
f1 = max(i, f1);
}
}
inc(i, n, 2 * n) {
if (d[i] + i == 2 * n) {
f2 = max(d[i], f2);
}
}
if (f1 > f2)
res = s0.substr(0, f1);
else
res = s0.substr(n - f2);
l = -1, r = n;
while (l + 1 <= r - 1 && s0[l + 1] == s0[r - 1]) l++, r--;
int l1 = -1, r1 = n, num = res.size();
inc(i, 2 * l + 2, 2 * r) {
int l2 = (i - d[i]) / 2, r2 = (i + d[i]) / 2 - 1,
mm = min(l2, n - 1 - r2) * 2 + (r2 - l2 + 1);
if (l2 > r2 || l2 == 0 || r2 == n - 1) continue;
if ((l2 <= l + 1 || r2 >= r - 1) && num < mm) {
l1 = l2, r1 = r2, num = mm;
}
}
if (num > (int)res.size()) {
int mm = min(l1, n - 1 - r1);
string tmp = s0.substr(0, mm) + s0.substr(l1, r1 - l1 + 1) +
s0.substr(n - mm);
res = tmp;
}
cout << res << "\n";
}
}

 

1326E - Bombs

题意: 给出一个 1 - n 的排列 \({p_i}\), 某些位置有"炸弹"(但至少有一个位置没有炸弹), 按顺序扫描一遍 \({p_i}\), 每扫到一个数就把它加入集合, 如果该位置有炸弹, 就移除当前集合中最大的数, 最后输出集合中最大的数作为结果. 有另一个排列 \({q_i}\), 假设当前 i = x, 则位置 \(q_1, q_2, ... , q_{x-1}\) 是有炸弹的. 输出 i 取遍 1 - n 的答案. n ≤ 3e5.

思路: 我们只关心当前最大的数是否会被炸弹带走, 例如初始最大的数是 n, 只要当前所有炸弹都在 n 的前面, 答案就一直是 n; 当 n 被炸没了, 就考虑 n - 1 能否成为答案, 依此类推. 所以我们要快速查询对于一个数是否会被炸弹带走, 假设对于当前的数 now 后面有 x 个炸弹, 而前面有 y 个比 now 大的数没有被炸掉, 当 y ≥ x 时说明 now 没有被炸掉. 考虑这样的一个序列, 当出现一个炸弹时, 对应位置 -1, 而当前已出现的数(从大往小枚举)所在位置 +1, 如果该序列的一个后缀和为正数(并且后缀和的位置一定要包括到 pos[now]), 代表比 now 大的数不比炸弹少(事实上只会小于等于), now 就是答案; 否则 now 自减 1, now 自减后所在的位置 +1, 继续查询它的后缀和是否为正. 用线段树维护这个序列的后缀和即可.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 3e5 + 5; int p[maxn], q[maxn], n, pos[maxn];
int c[maxn]; int f[maxn * 4], mx[maxn * 4];
void down(int k, int l, int r) {
if (f[k]) {
int mid = l + r >> 1;
f[k << 1] += f[k], mx[k << 1] += f[k];
f[k << 1 | 1] += f[k], mx[k << 1 | 1] += f[k];
f[k] = 0;
}
}
void up(int k) { mx[k] = max(mx[k << 1], mx[k << 1 | 1]); }
void add(int k, int l, int r, int a, int b, int val) {
if (a <= l && r <= b) {
f[k] += val;
mx[k] += val;
return;
}
down(k, l, r);
int mid = l + r >> 1;
if (a <= mid) add(k << 1, l, mid, a, b, val);
if (b > mid) add(k << 1 | 1, mid + 1, r, a, b, val);
up(k);
}
int query(int k, int l, int r, int a, int b) {
if (a <= l && r <= b) return mx[k];
down(k, l, r);
int res = 0;
int mid = l + r >> 1;
if (a <= mid) res = max(res, query(k << 1, l, mid, a, b));
if (b > mid) res = max(res, query(k << 1 | 1, mid + 1, r, a, b));
return res;
} int main() {
scanf("%d", &n);
inc(i, 1, n) {
scanf("%d", &p[i]);
pos[p[i]] = i;
}
inc(i, 1, n) scanf("%d", &q[i]);
int now = n;
add(1, 1, n, 1, pos[now], 1);
inc(i, 1, n) {
printf("%d%c", now, i == n ? '\n' : ' ');
if (i == n) break;
add(1, 1, n, 1, q[i], -1);
while (query(1, 1, n, 1, pos[now]) <= 0) {
now--;
add(1, 1, n, 1, pos[now], 1);
}
}
}

 

1325E - Ehab's REAL Number Theory Problem

题意: 给出 n 个数 ai, n ≤ 1e5, 1 ≤ ai ≤ 1e6, ai 最多有 7 个因子. 要选出一个非空的子序列, 使得这些数的积是一个完全平方数. 问子序列的最短长度.

思路: 每个数选取后至多影响到积的 2 个质因子的奇偶性, 因此可以对每一个数建边: 有 2 个次数为奇数的质因子时连接代表这两个数的节点; 只有 1 个次数为奇数的质因子时连接该数与 1; 否则, 该数就是一个完全平方数. 建图后相当于要选出一个最小子图, 每个点的度数是偶数, 问题便转换为找到图上最小的环, 这个图最多有 1e5个点. 考虑朴素的做法: 对每一个节点 bfs, 可以找到该点所在的最小环. 复杂度为 \(O(n^2)\). 注意到大于 \(\sqrt n\) 的两个数间不会建边, 即所有环一定经过了不大于 \(\sqrt n\) 的质数或点 1 (最多 169 个节点). 这样可以只 bfs 这些节点更新答案.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 1e5 + 5; const int maxnum = 1e6;
int prim[maxnum], pvis[maxnum + 5], pcnt, id[maxnum + 5];
void getprim() {
for (int i = 2; i <= maxnum; i++) {
if (!pvis[i]) {
prim[++pcnt] = i;
id[i] = pcnt;
}
for (int j = 1; j <= pcnt && prim[j] * i <= maxnum; j++) {
pvis[prim[j] * i] = 1;
if (i % prim[j] == 0) break;
}
}
} int n, a[maxn], res;
vector<int> g[maxn]; void con(int u, int v) {
g[u].push_back(v);
g[v].push_back(u);
}
int d[maxn];
void bfs(int s) {
memset(d, 127, sizeof(d));
queue<int> que;
que.push(s);
d[s] = 0;
while (que.size()) {
int q = que.front();
que.pop();
for (int i : g[q]) {
if (d[i] == d[q] + 1 || d[i] == d[q]) {
res = min(res, d[i] + d[q] + 1);
return;
} else if (d[i] > d[q] + 1) {
d[i] = d[q] + 1;
que.push(i);
}
}
}
} int main() {
getprim();
res = 10'000'000;
scanf("%d", &n);
inc(i, 0, n - 1) scanf("%d", &a[i]);
inc(i, 0, n - 1) {
vector<int> v;
for (int j = 1; j <= 168; j++) {
if (a[i] % prim[j] == 0) {
int cnt = 0;
while (a[i] % prim[j] == 0) {
cnt++;
a[i] /= prim[j];
}
if (cnt & 1) v.push_back(j);
}
}
if (a[i] > 1) v.push_back(id[a[i]]);
if (v.size() == 2)
con(v[0], v[1]);
else if (v.size() == 1)
con(v[0], 0);
else {
res = 1;
break;
}
}
for (int i = 0; i <= 168; i++) bfs(i);
if (res == 10'000'000) res = -1;
cout << res << "\n";
}

 

1325F - Ehab's Last Theorem

题意: 给出一个 n 个点, m 条边的图, n ≤ 1e5, m ≤ 2e5, 没有重边, 自环. 要求输出它的一个大小为 \(\lceil n \rceil\) 的独立集或者长度至少为 \(\lceil n \rceil\) 的环(自行选择并可证明一定有解).

思路: 官方题解给出了一个很有意义的定理: 图上所有节点的度数都不小于 d 时, 图上一定存在一个长度不小于 d + 1 的环. 证明: 从任意一点 dfs 直至当前 dfs 的点的相邻点都已被标记, 由于它的度数不小于 d, 说明当前 dfs 链上已有至少 d + 1 个点, 令当前点与它的相邻点中最接近 dfs 序的起点的点连接便构成了需要的环. 回到原题, 我们不断地删除图上度数小于 \(\lceil n \rceil - 1\) 的点及其相邻点, 如果能以此法进行 \(\lceil n \rceil\) 次, 便找到了需要的独立集; 如果在某一时刻找不到度数小于 \(\lceil n \rceil\) 的点, 便可以运用前述定理找到需要的环(此时剩余的子图一定不为空).

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 1e5 + 5; int n, m, u, v, sq;
vector<int> g[maxn];
int du[maxn], vis[maxn], vis2[maxn], vis3[maxn];
vector<int> r1, r2;
int cnt; void dfs(int x) {
vis2[x] = 1;
r2.push_back(x);
int fail = 1;
for (int i : g[x]) {
if (!vis[i] && !vis2[i]) {
fail = 0;
dfs(i);
// break;
}
}
if (fail) {
cout << "2\n";
for (int i : g[x]) vis3[i] = 1;
vis3[x] = 1;
int sz = r2.size();
inc(i, 0, sz - 1) if (vis3[r2[i]]) {
cout << sz - i << "\n";
inc(j, i, sz - 1) cout << r2[j] << " ";
break;
}
cout << "\n";
exit(0);
}
} int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++) {
scanf("%d %d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
du[u]++, du[v]++;
}
sq = (int)ceil(sqrt(n));
inc(i, 1, sq) {
inc(j, 1, n) if (!vis[j] && du[j] < sq - 1) {
r1.push_back(j);
vis[j] = 1;
for (int k : g[j]) {
if (!vis[k]) {
vis[k] = 1, du[k]--;
for (int l : g[k]) du[l]--;
}
}
break;
}
if (r1.size() < i) break;
if (i == sq) {
cout << "1\n";
for (int j : r1) cout << j << " ";
cout << "\n";
return 0;
}
}
inc(i, 1, n) {
if (!vis[i]) {
dfs(i);
break;
}
}
}

 

1316E - Team Building

题意: 为了组建排球队, 现在需要 p 个选手和 k 个观众, 给出 n 个人选, 每个人具有去做第 j 位选手和观众贡献的力量值 \(s_{ij}\) 和 \(a_i\). 现在要最大化总力量值. 2 ≤ n ≤ 1e5, 1 ≤ p ≤ 7, 1 ≤ k, p + k ≤ n.

思路: 按 \(a_i\) 降序排序, 定义 dp[i][S] 为使用前 i 个人时的最大力量值(每个人做选手, 观众, 或什么也不做都有可能), S 是位置被占用的状态. 状态转移时, 考虑第 i 个人: 1. 做第 j 位选手; 2. 做观众; 3. 吃瓜. (1) (3) 都是好做的, 而做观众得考虑当前当观众的机会是否已经用完了. 由于已经按 \(a_i\) 降序排序, 所以观众总是贪心地取前面的未做选手的人, 所以对于 dp[i][S] 做观众的人数为 max(i - __builtin_popcount(S), k), 即靠前的人不当选手就得当观众, 这样就可以知道当前第 i 人能否还能当观众.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 1e6 + 5; int n, k, p;
ll dp[2][130];
int s[maxn][10], a[maxn], id[maxn]; bool cmp(int x, int y) { return a[x] > a[y]; } int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> p >> k;
inc(i, 0, n - 1) cin >> a[i];
inc(i, 0, n - 1) inc(j, 0, p - 1) cin >> s[i][j];
inc(i, 0, n - 1) id[i] = i;
sort(id, id + n, cmp);
ll *now = dp[0], *nxt = dp[1];
inc(j, 1, (1 << p) - 1) nxt[j] = -1;
inc(i, 0, n - 1) {
inc(j, 0, (1 << p) - 1) now[j] = nxt[j];
inc(j, 0, (1 << p) - 1) {
if (now[j] == -1) continue;
inc(k, 0, p - 1) if (!((j >> k) & 1)) nxt[j | (1 << k)] =
max(nxt[j | (1 << k)], now[j] + s[id[i]][k]);
if (i - __builtin_popcount(j) < k) {
nxt[j] = max(nxt[j], now[j] + a[id[i]]);
}
}
}
cout << nxt[(1 << p) - 1] << "\n";
}

 

1313C2 - Skyscrapers (hard version)

题意: 给出N个摩天大楼的最大高度, 要求高度数列是山脊型, 问最大的高度和.

思路: 刚开始以为是dp. 但是这题每个数只减不增, 所以在确定某一点为最大时, 其他点的答案是唯一确定的(贪心). 从两边各扫描一遍单调栈处理出\(\sum_{1≤j≤i}h_j\)和\(\sum_{i≤j≤n}h_j\)的最大值.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++)
#define dec(i, l, r) for (int i = l; i >= r; i--) const int maxn = 1e6 + 5; int a[maxn];
ll pre[maxn], suff[maxn];
int n; int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
inc(i, 0, n - 1) cin >> a[i];
stack<int> id;
ll sum = 0;
inc(i, 0, n - 1) {
while (!id.empty() && a[i] < a[id.top()]) {
int j = id.top();
id.pop();
sum -= (ll)a[j] * (j - (id.empty() ? -1 : id.top()));
}
sum += (ll)a[i] * (i - (id.empty() ? -1 : id.top()));
pre[i] = sum;
id.push(i);
}
while (id.size()) id.pop();
sum = 0;
dec(i, n - 1, 0) {
while (!id.empty() && a[i] < a[id.top()]) {
int j = id.top();
id.pop();
sum -= (ll)a[j] * ((id.empty() ? n : id.top()) - j);
}
sum += (ll)a[i] * ((id.empty() ? n : id.top()) - i);
pre[i] += sum;
id.push(i);
}
inc(i, 0, n - 1) pre[i] -= a[i];
int pos = max_element(pre, pre + n) - pre;
dec(i, pos - 1, 0) if (a[i] > a[i + 1]) a[i] = a[i + 1];
inc(i, pos + 1, n - 1) if (a[i] > a[i - 1]) a[i] = a[i - 1];
inc(i, 0, n - 1) cout << a[i] << " ";
cout << "\n";
}

 

1312E - Array Shrinking

题意: 给出一个有 n 个数的数列 \(a_i\), 可以选择两个大小相同位置相邻的数 x, 用一个 x + 1 代替这两个元素. 问数列经任意次合并后的最短长度. n ≤ 100, \(1 ≤ a_i ≤ 1000\).

思路: \(dp[l][r]\) 为区间 [l, r] 能否合并成一个元素, 若不能则返回 -1, 若可以则返回最后合并的结果. 状态转移时, 考虑到如果一个长度大于 1 的区间可以合并成一个元素, 那它一定可以分成两个也可以合并成一个元素的区间(并且大小相同).

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 1e3 + 5; int n, a[maxn], dp[maxn][maxn];
int f[maxn]; int solve(int l, int r) {
if (dp[l][r] != 0) return dp[l][r];
inc(i, l, r - 1) {
int pre = solve(l, i);
if (pre != -1 && pre == solve(i + 1, r)) return dp[l][r] = pre + 1;
}
return dp[l][r] = -1;
} int main() {
cin >> n;
inc(i, 1, n) cin >> a[i];
inc(i, 1, n) dp[i][i] = a[i];
inc(i, 1, n) {
f[i] = n;
inc(j, 1, i) {
if (solve(j, i) > 0) f[i] = min(f[i], f[j - 1] + 1);
}
}
cout << f[n] << "\n";
}

 

1311E - Construct the Binary Tree

题意: 构造一个 n 个点深度和为 d 的二叉树.

思路: 链状的树深度和是最大的, 以此为初始状态, 每次把最下面的节点往上移至某一层填满或者深度和达到d. 这也证明了 n 固定时深度和是在一个区间上连续的. 知道每层有几个点后层序遍历输出顶点.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 3e5 + 5;
const int inf = 0x3f3f3f3f; int l[maxn], r[maxn]; int main() {
int t, n, d;
inc(i, 2, 5000) {
l[i] = inf;
inc(j, 0, i - 1) {
l[i] = min(l[j] + l[i - j - 1] + i - 1, l[i]);
r[i] = max(r[j] + r[i - j - 1] + i - 1, r[i]);
}
}
cin >> t;
while (t--) {
cin >> n >> d;
if (d < l[n] || d > r[n]) {
cout << "NO\n";
continue;
}
cout << "YES\n";
vector<int> v(n, 1);
int tmp = n * (n - 1) / 2, last = 1, top = n - 1;
while (tmp > d) {
if (tmp - (top - last) >= d) {
tmp -= top - last;
if (--v[top] == 0) top--;
if (last < 30 && ++v[last] == 1 << last) last++;
} else {
v[top]--;
v[top - (tmp - d)]++;
break;
}
}
int cnt = 1;
for (int i = 1; i <= n - 1 && v[i]; i++) {
inc(j, 0, v[i] - 1) { cout << cnt + j / 2 << " "; }
cnt += v[i - 1];
}
cout << "\n";
}
}

 

1311F - Moving Points

题意: 给出一维上的若干个点, 每个点具有一个速度. 定义 d(i, j) 为所有时刻点 i 与点 j 的最小距离. 求\(\sum_{1≤i<j≤n}\) d(i, j).

思路: 按坐标排序, BIT维护点的位置分布和距离和.

 

1310D - Tourism

题意: 给出一个N个点的有向图, 要求从点1出发, 走k(偶数)条边后回到点1, 路径可以走已走过的点和边, 但不能有奇环, 问最短路径长度. N≤80, k≤10.

思路: 给所有点随机染色成两种颜色, 这样路径就变成了在二分图上左右横跳, 可以用dp求解最短路径. 每次染色成功的概率为 \(\frac{1}{2^{k-1}}\), 试验\(M×2^{k-1}\)得到正确答案的概率为\(1-e^{-M}\). 这题不用状压而是随机是因为N大而k小, 染色染对的概率比较高, 其他n-k个点染错了也没事. 另外官方题解的第一个解法没有看懂在说什么.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 85;
const int inf = 0x3f3f3f3f; int d[maxn][maxn], dp[maxn], col[maxn];
int n, k; int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
srand(0);
cin >> n >> k;
inc(i, 1, n) inc(j, 1, n) cin >> d[i][j];
int ans = inf, cnt = 5000;
// while (cnt--) {
while ((double)clock() / CLOCKS_PER_SEC < 0.5) {
inc(i, 2, n) col[i] = rand() % 2;
memset(dp, 63, sizeof(dp));
dp[1] = 0;
inc(i, 1, k) {
inc(j, 1, n) if (((i + col[j]) & 1) == 0) dp[j] = inf;
inc(u, 1, n) if ((i + col[u]) & 1)
inc(v, 1, n) if (((i + col[v]) & 1) == 0) dp[v] =
min(dp[v], dp[u] + d[u][v]);
}
ans = min(ans, dp[1]);
}
cout << ans << "\n";
}

 

1307D Cow and Fields

题意: 给出一个N(N \(\leq\) 2e5)个点的无向图, 已知其中有k(2 \(\leq\) k \(\leq\) N)个点是"特殊的"的, 你必须选择其中两个点连边, 使得点1到点N的距离最大化.

思路: 问题转化为最大化 min(d[1][u] + 1 + d[v][n], d[1][v] + 1 + d[u][n]). 此时是希望在固定i/j时可以确定min取哪个. 按d[1][u] - d[u][n]降序排列, 此时在选定当前点 i 后, 另一个点 j (j > i)无论是哪个, 最小值都是d[1][i] + 1 + d[j][n], 利用这一点, 就可以O(1)地查询后缀解决.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++)
#define pii pair<int, int>
#define fi first
#define se second
#define pb push_back int n, m, k; const int maxv = 2e5 + 5;
const int inf = 0x3f3f3f3f;
int V, d[maxv];
vector<int> g[maxv];
pii p[maxv];
int suffix[maxv]; void solve(int s) {
queue<int> que;
que.push(s);
memset(d, 63, sizeof(d));
d[s] = 0;
while (!que.empty()) {
int q = que.front();
que.pop();
for (int i = 0; i < g[q].size(); i++) {
if (d[g[q][i]] == inf) {
d[g[q][i]] = d[q] + 1;
que.push(g[q][i]);
}
}
}
} bool cmp(const pii &p1, const pii &p2) { return p1.fi - p1.se < p2.fi - p2.se; } int sp[maxv], u, v;
int main() {
cin >> n >> m >> k;
inc(i, 0, k - 1) cin >> sp[i];
inc(i, 1, m) {
u--, v--;
cin >> u >> v;
g[u].pb(v);
g[v].pb(u);
}
solve(1);
for (int i = 0; i < k; i++) p[i].fi = d[sp[i]];
solve(n);
for (int i = 0; i < k; i++) p[i].se = d[sp[i]];
sort(p, p + k, cmp);
suffix[k - 1] = p[k - 1].se;
for (int i = k - 2; i >= 0; i--) suffix[i] = max(suffix[i + 1], p[i].se);
int res = 0;
for (int i = 0; i < k - 1; i++) res = max(res, p[i].fi + suffix[i + 1] + 1);
cout << min(d[1], res);
}

 

1307E - Cow and Treats

题意: 给出一列N个草的美味程度, 现在有M只牛, 牛会从左端或右端出发, 见到与自己fi相同的草就吃, 吃满hi单位的草为止, 然后原地倒下, 不让其他牛经过自己. 农夫要选出尽可能多的牛吃草, 而不发生有牛被吃饱了倒下的其他牛挡到的情况. 问最大的牛数量以及方案数(左边出发还是右边出发认为是不同的, 出发的顺序是不考虑的). N≤5000, M≤5000.

思路: 只能想到以dp[i][j]为当前从左出发的牛最远到i, 从右出发到j. 正解是考虑枚举从左边出发的牛最远到哪, 然后就可以考虑吃各种美味程度的牛都会产生怎样的贡献. 注意不要漏掉从左出发到 0 的情况. 另外我自己的方法是确保有牛到达这个最远的地方(也导致讨论的情况变多), 不然可能会重复算方案数. 这题O(n^2)维护的变量是可以优化到 O(nlogn) 的.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++)
#define dec(i, l, r) for (int i = l; i >= r; i--) const int maxn = 5e3 + 5;
const int mod = 1e9 + 7; int n, m, t, f;
int a[maxn], pre[maxn];
int ans1[maxn];
ll ans2[maxn]; int top[maxn];
struct cow {
int f, l, r;
bool operator<(const cow& o) const { return f < o.f; }
};
vector<cow> c[maxn];
int vis[maxn]; int main() {
cin >> n >> m;
inc(i, 1, n) cin >> a[i];
inc(i, 1, m) {
cin >> t >> f;
c[t].push_back({f, n + 1, 0});
}
inc(i, 1, n) sort(c[i].begin(), c[i].end());
inc(i, 1, n) {
int nw = a[i];
pre[nw]++;
if (top[nw] < c[nw].size() && pre[nw] == c[nw][top[nw]].f)
c[nw][top[nw]++].l = i, vis[i] = 1;
}
memset(top, 0, sizeof(top));
memset(pre, 0, sizeof(pre));
dec(i, n, 1) {
int nw = a[i];
pre[nw]++;
if (top[nw] < c[nw].size() && pre[nw] == c[nw][top[nw]].f)
c[nw][top[nw]++].r = i;
}
vis[0] = 1;
inc(lim, 0, n) {
if (!vis[lim]) continue;
ans2[lim] = 1;
inc(i, 1, n) {
if (c[i].size() == 1) {
if (c[i][0].l == lim)
ans1[lim]++;
else {
if (c[i][0].l < lim && c[i][0].r > lim)
ans1[lim]++, ans2[lim] = ans2[lim] * 2 % mod;
else if (c[i][0].l < lim || c[i][0].r > lim)
ans1[lim]++;
}
} else if (c[i].size() > 1) {
int cnt[3] = {0}, fail = 1;
inc(j, 0, c[i].size() - 1) {
if (c[i][j].l == lim)
fail = 0;
else if (c[i][j].l < lim) {
if (c[i][j].r > lim)
cnt[0]++;
else
cnt[1]++;
} else if (c[i][j].r > lim)
cnt[2]++;
}
if (fail) {
if (cnt[0] >= 2 || cnt[0] == 1 && cnt[1] + cnt[2] ||
cnt[1] && cnt[2]) {
ans1[lim] += 2;
ans2[lim] = ans2[lim] *
(cnt[1] * (cnt[0] + cnt[2]) +
cnt[0] * (cnt[0] - 1 + cnt[2])) %
mod;
} else if (cnt[0] + cnt[1] + cnt[2]) {
ans1[lim] += 1;
if (cnt[0])
ans2[lim] = ans2[lim] * 2 % mod;
else
ans2[lim] = ans2[lim] * max(cnt[1], cnt[2]) % mod;
}
} else {
ans1[lim] += 1 + (cnt[0] + cnt[2] > 0);
ans2[lim] = ans2[lim] * max(1, cnt[0] + cnt[2]) % mod;
}
}
}
}
int md = 0;
inc(i, 0, n) md = max(md, ans1[i]);
ll res = 0;
inc(i, 0, n) if (ans1[i] == md) res += ans2[i];
cout << md << " " << res % mod << "\n";
}

 

1305E - Kuroni and the Score Distribution

题意: 构造一个严格递增的数列 {\(a_i\)}, \(a_i ≤ 1e9\), 使得对于所有三元组 (i, j, k), 1 ≤ i < j < k ≤ n, 满足 \(a_i + a_j = a_k\) 的数量恰为 m.

思路: 考虑对于 {1, 2, ... , n} 这样的一个起始序列, 枚举 k, 会发现有 \(\lfloor \frac{k - 1}{2} \rfloor\) 个满足条件的三元组, 从而得到总的三元组数. 因此 m > \(\sum ^ {n} _ {i=1} {\lfloor \frac{i - 1}{2} \rfloor}\) 可以先判定是不可能有解的. 我们找到这样的 k 使得 $\sum ^ {k} _ {i=1} {\lfloor \frac{i - 1}{2} \rfloor} ≤ m < \sum ^ {k+1} _ {i=1} {\lfloor \frac{i - 1}{2} \rfloor} $ 成立, 而剩下的三元组 res = m - \(\sum ^ {k} _ {i=1} {\lfloor \frac{i - 1}{2} \rfloor}\) 全由 $ a_{k+1} = 2 * k + 1 - res $ (这个数是怎么来的?考虑 $a_{k-2×res+1} + a_k = a_{k+1} $)与前面的数组合搞定. 再后面的数取大奇数防止出现合法三元组就可了, 即它们的间距大于 \(a_{k+1}\) 且 \(a_{k+2}+a_{k+3} > a_n\).

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 1e6 + 5; int top, n, m;
int a[maxn]; int main() {
cin >> n >> m;
inc(i, 1, n) a[i] = a[i - 1] + (i - 1) / 2;
int pos = upper_bound(a + 1, a + n + 1, m) - a - 1;
if (a[n] < m) {
cout << "-1\n";
} else {
inc(i, 1, pos) cout << i << " ";
if (pos < n) {
int res = m - a[pos];
cout << 2 * pos + 1 - 2 * res << " ";
inc(i, pos + 2, n) cout << (int)1e6 + 1 + (int)1e4 * (i - pos)
<< " ";
}
cout << "\n";
}
}

 

1305F - Kuroni and the Punishment

题意: 给出 n 个数 \(a_i\), n ≤ 2e5, \(a_i\) ≤ 1e12. 可以每次使某一个数 +1 / -1 (但要保持还是正数). 问要使这些数的最小公因数不为 1 的最少操作数.

思路: 由于可以对每个数进行至多一次操作, 使得所有数都是偶数, 从而满足题意的要求, 说明答案不大于 n, 进而得知变化次数大于 1 的数的个数不大于 \(\lfloor \frac{n}{2} \rfloor\). 随机取出 \(a_i\) 中的一个数, 设为 x, 至少有 1 / 2 的概率 x 被增减次数是小于等于 1 的, 把 x - 1, x, x + 1 质因数分解, 这样随机取出 20 次后, 最小公因数出现在这些质因数的概率为 \(1-2^{-20}\). 这个集合至多有 $ 60 × \log{a_i} $ 个数. 对于这些可能是最小公因数的质数, 可以每个都 O(n) 地更新答案.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 1e6 + 5; int n;
ll a[maxn], res; const int maxnum = 1e6;
int prim[maxnum], pvis[maxnum + 5], pcnt;
void getprim() {
for (int i = 2; i <= maxnum; i++) {
if (!pvis[i]) prim[++pcnt] = i;
for (int j = 1; j <= pcnt && prim[j] * i <= maxnum; j++) {
pvis[prim[j] * i] = 1;
if (i % prim[j] == 0) break;
}
}
} set<ll> s; int main() {
srand(0);
ios::sync_with_stdio(false);
cin.tie(nullptr);
getprim();
cin >> n;
inc(i, 0, n - 1) cin >> a[i];
res = n;
for (int i = 1, j = rand() % n; i <= 20; i++, j = (j + rand()) % n) {
inc(del, -1, 1) {
ll tmp = a[j] + del;
for (int k = 1; k <= pcnt && tmp > 1; k++) {
if (tmp % prim[k] == 0) {
s.insert(prim[k]);
while (tmp % prim[k] == 0) tmp /= prim[k];
}
}
if (tmp > 1) s.insert(tmp);
}
}
for (ll e : s) {
ll tmp = 0;
inc(i, 0, n - 1) tmp +=
a[i] > e ? min(a[i] % e, e - a[i] % e) : e - a[i];
res = min(tmp, res);
}
cout << res << "\n";
}

 

1304E - 1-Trees and Queries

题意: 给出一个N(N \(\leq\) 1e5)个点的树, q(q \(\leq\) 1e5)组询问, 每次询问会连接x, y两点, 查询是否存在长度为k的a, b间路径(可以经过重复的边).

思路: 原本树上路径长度是唯一的, 引入x-y后只需考虑在x-y上经过奇数次的情形, 奇数次又可以等效于经过1次. 讨论三种走法, a-b, a-x-y-b, a-y-x-b, 剩下的路程可以模2处理(在一条边上来回走), 而走x-y是唯一可能影响到奇偶性的走法.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 1e5 + 5;
const int logn = 20; int n;
int par[logn][maxn], dep[maxn], root;
vector<int> g[maxn]; void dfs(int v, int p, int d) {
par[0][v] = p;
dep[v] = d;
for (int i = 0; i < g[v].size(); i++)
if (g[v][i] != p) dfs(g[v][i], v, d + 1);
}
void init() {
dfs(root, -1, 0);
for (int k = 0; k + 1 < logn; k++) {
for (int v = 0; v < n; v++) {
if (par[k][v] < 0)
par[k + 1][v] = -1;
else
par[k + 1][v] = par[k][par[k][v]];
}
}
}
int lca(int u, int v) {
if (dep[u] > dep[v]) swap(u, v);
for (int k = 0; k < logn; k++)
if ((dep[v] - dep[u]) >> k & 1) v = par[k][v];
if (u == v) return u;
for (int k = logn - 1; k >= 0; k--) {
if (par[k][u] != par[k][v]) u = par[k][u], v = par[k][v];
}
return par[0][u];
}
int dis(int u, int v) { return dep[u] + dep[v] - 2 * dep[lca(u, v)]; } int u, v, q, x, y, k; int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
inc(i, 1, n - 1) {
cin >> u >> v;
u--, v--;
g[u].push_back(v);
g[v].push_back(u);
}
init();
cin >> q;
while (q--) {
cin >> x >> y >> u >> v >> k;
x--, y--, u--, v--;
int d1 = dis(u, v);
int d2 = dis(u, x) + 1 + dis(y, v);
int d3 = dis(u, y) + 1 + dis(x, v);
if (d1 <= k && (k - d1) % 2 == 0 || d2 <= k && (k - d2) % 2 == 0 ||
d3 <= k && (k - d3) % 2 == 0)
cout << "yes\n";
else
cout << "no\n";
}
}

 

1304F1 - Animal Observation (easy version)

题意: 摄影师要在 n 天拍摄 m 个区域的动物, 每天可以布置一个相机来观察连续 k 个区域, 每次相机可以工作2天(也就是每天会有2个相机在工作, 但是观察区域重叠时不会重复算), 已知每一天每片区域出现的动物数量, 问最多能观察到多少动物. n≤50, m≤2e4, k≤min(m, 20). 下面是从原题盗来的图.

从原题盗来的图
![](https://img2018.cnblogs.com/blog/1730657/202002/1730657-20200228131414607-1635122897.png)

**思路:** dp[i][j]为在第 i 天布置的相机观察 [j, j + k -1] 区域, 累计最多的观察动物数. 而它会从上一行三种状态转移过来, 即左边不重合, 重合, 右边不重合, 其中不重合就是查询最大的前缀/后缀, 而重合时可以扫描一遍 O(k) 地减去重合部分的影响. 总的时间复杂度为O(nmk).

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++)
#define dec(i, l, r) for (int i = l; i >= r; i--) const int maxn = 55;
const int maxm = 2e4 + 5; int a[maxn][maxm], dp[maxn][maxm], sum[maxn][maxm];
int n, m, k;
int pre[maxn][maxm], suff[maxn][maxm]; int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
inc(i, 0, n - 1) inc(j, 0, m - 1) cin >> a[i][j]; inc(i, 0, n - 1) inc(j, 0, k - 1) sum[i][0] += a[i][j];
inc(i, 0, n - 1) inc(j, 1, m - k) sum[i][j] =
sum[i][j - 1] + a[i][j + k - 1] - a[i][j - 1]; inc(i, 0, m - k) dp[0][i] = sum[0][i] + sum[1][i]; pre[0][0] = dp[0][0];
inc(i, 1, m - k) pre[0][i] = max(pre[0][i - 1], dp[0][i]);
suff[0][m - k] = dp[0][m - k];
dec(i, m - k - 1, 0) suff[0][i] = max(suff[0][i + 1], dp[0][i]); inc(i, 1, n - 1) {
inc(j, 0, m - k) {
if (j - k >= 0) dp[i][j] = max(dp[i][j], pre[i - 1][j - k]);
if (j + k <= m - k) dp[i][j] = max(dp[i][j], suff[i - 1][j + k]);
int tmp = 0;
inc(p, j - k + 1, j) {
tmp += a[i][p + k - 1];
if (p >= 0) dp[i][j] = max(dp[i][j], dp[i - 1][p] - tmp);
}
inc(p, j + 1, j + k - 1) {
tmp -= a[i][p - 1];
if (p <= m - k) dp[i][j] = max(dp[i][j], dp[i - 1][p] - tmp);
}
dp[i][j] += sum[i][j] + sum[i + 1][j];
}
pre[i][0] = dp[i][0];
inc(j, 1, m - k) pre[i][j] = max(pre[i][j - 1], dp[i][j]);
suff[i][m - k] = dp[i][m - k];
dec(j, m - k - 1, 0) suff[i][j] = max(suff[i][j + 1], dp[i][j]);
} cout << pre[n - 1][m - k] << "\n";
}

 

1304F2 - Animal Observation (hard version)

题意: 与 easy version 唯一的区别在于 k 的取值范围由 k ≤ min(m, 20) 变成 k ≤ m.

思路1: 考虑更快地从上一行得到最大值. 对于 dp[i][j], 我们已经知道了上一行的哪些区域会发生重合并且要减去哪些值, 而对于 dp[i][j + 1], 我们发现每个位置要减去的值仅变化了 a[i][j] 或者 a[i][j + k] 或者不变, 所以可以维护这样的线段树: 当前已处理至 dp[i][j], 线段树中每一个节点代表了上一行的答案减去与 dp[i][j] 重合的部分, 而处理 dp[i][j + 1]前, 上一行范围 [j - k + 1, j] 的节点要 +a[i][j](不再与 a[i][j] 重合), 在范围 [j + 1, j + k] 的节点要 -a[i][j + k] (与 a[i][j + k] 发生重合), 这样就可以在 O(nmlogk)的时间里得到答案了.

解法1
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++)
#define dec(i, l, r) for (int i = l; i >= r; i--) const int maxn = 55;
const int maxm = 2e4 + 5; int a[maxn][maxm], dp[maxm], nxt[maxm], sum[maxn][maxm];
int n, m, k; int w[maxm * 4], f[maxm * 4];
void push(int k) {
if (f[k]) {
f[k << 1] += f[k], f[k << 1 | 1] += f[k];
w[k << 1] += f[k], w[k << 1 | 1] += f[k];
f[k] = 0;
}
}
void up(int k) { w[k] = max(w[k << 1], w[k << 1 | 1]); }
void build(int k, int l, int r) {
if (l == r) {
w[k] = dp[l], f[k] = 0;
return;
}
w[k] = f[k] = 0;
int mid = l + r >> 1;
build(k << 1, l, mid);
build(k << 1 | 1, mid + 1, r);
up(k);
}
void change(int k, int l, int r, int val, int a, int b) {
if (a <= l && r <= b) {
w[k] += val, f[k] += val;
return;
}
push(k);
int mid = l + r >> 1;
if (a <= mid) change(k << 1, l, mid, val, a, b);
if (b > mid) change(k << 1 | 1, mid + 1, r, val, a, b);
up(k);
} int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
inc(i, 0, n - 1) inc(j, 0, m - 1) cin >> a[i][j]; inc(i, 0, n - 1) inc(j, 0, k - 1) sum[i][0] += a[i][j];
inc(i, 0, n - 1) inc(j, 1, m - k) sum[i][j] =
sum[i][j - 1] + a[i][j + k - 1] - a[i][j - 1]; inc(i, 0, m - k) nxt[i] = sum[0][i] + sum[1][i]; inc(i, 1, n - 1) {
inc(j, 0, m - k) dp[j] = nxt[j], nxt[j] = 0;
int tmp = 0;
dec(j, k - 1, 0) {
tmp += a[i][j];
dp[j] -= tmp;
} build(1, 0, m - k); inc(j, 0, m - k) {
nxt[j] = w[1] + sum[i][j] + sum[i + 1][j];
change(1, 0, m - k, a[i][j], max(0, j - k + 1), j);
change(1, 0, m - k, -a[i][j + k], j + 1, min(j + k, m - k));
}
} int res = 0;
inc(i, 0, m - k) res = max(res, nxt[i]);
cout << res << "\n";
}

思路2: 考虑用单调队列来维护相交部分的最大值, 复杂度可以优化成 O(nm). 对于上下行不相交的情况, 仍然使用前后缀更新; 而相交的部分是这样处理的: 从左往右扫描, 把 dp[i - 1][j] - sum[i][j] 加入队列并保持单调性, 弹出编号为 j - k 的元素, 查询队列最大值, 队列整体 +a[i][j] (因为不再与 a[i][j] 相交, 参见思路 1); 而弹出操作我是给每个元素加了编号, 以编号检查是否要弹出, 查询队列最大值自然是队尾元素, 队列整体增加并没有真正实施, 而是加在了马甲 tmp 上, 查询的时候加上 tmp 即可, 后面压入一个新的数时得减去 tmp 保证比较大小的正确性, 由于压入的数不是原本的真值, 所以我前面要用编号来确定弹出的数还在不在. 个人感觉知道这题是用单调队列和队列整体增加操作后, 后面的细节最好自己解决.

解法2
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++)
#define dec(i, l, r) for (int i = l; i >= r; i--)
#define pii pair<int, int>
#define fi first
#define se second
#define pb push_back const int maxn = 55;
const int maxm = 2e4 + 5; int a[maxn][maxm], dp[maxm], nxt[maxm], sum[maxn][maxm];
int pre[maxm], suff[maxm];
int n, m, k; int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
inc(i, 0, n - 1) inc(j, 0, m - 1) cin >> a[i][j]; inc(i, 0, n - 1) inc(j, 0, k - 1) sum[i][0] += a[i][j];
inc(i, 0, n - 1) inc(j, 1, m - k) sum[i][j] =
sum[i][j - 1] + a[i][j + k - 1] - a[i][j - 1]; inc(i, 0, m - k) nxt[i] = sum[0][i] + sum[1][i]; inc(i, 1, n - 1) {
inc(j, 0, m - k) dp[j] = nxt[j], nxt[j] = 0; pre[0] = dp[0];
inc(i, 1, m - k) pre[i] = max(pre[i - 1], dp[i]);
suff[m - k] = dp[m - k];
dec(i, m - k - 1, 0) suff[i] = max(suff[i + 1], dp[i]); inc(j, 0, m - k) {
if (j - k >= 0) nxt[j] = max(pre[j - k], nxt[j]);
if (j + k <= m - k) nxt[j] = max(suff[j + k], nxt[j]);
} int tmp = 0;
deque<pii> que;
inc(j, 0, m - k) {
int now = dp[j] - tmp - sum[i][j];
while (que.size() && que.front().fi < now) que.pop_front();
que.push_front(pii(now, j));
if (j - k >= 0 && que.size() && que.back().se == j - k)
que.pop_back();
nxt[j] = max(nxt[j], que.back().fi + tmp);
tmp += a[i][j];
}
tmp = 0;
que.clear();
dec(j, m - k, 0) {
int now = dp[j] - tmp - sum[i][j];
while (que.size() && que.front().fi < now) que.pop_front();
que.push_front(pii(now, j));
if (j + k <= m - k && que.size() && que.back().se == j + k)
que.pop_back();
nxt[j] = max(nxt[j], que.back().fi + tmp);
tmp += a[i][j + k - 1];
} inc(j, 0, m - k) nxt[j] += sum[i][j] + sum[i + 1][j];
} int res = 0;
inc(i, 0, m - k) res = max(res, nxt[i]);
cout << res << "\n";
}

 

1301D - Time to Run

题意: 让一只cow在方格里不走重复边地尽可能运动.

思路: 模拟, 构造. 写代码前一定要先计算一下自己的走法是不是在数量上是不是等于总的边数, 以及 n, m = 1 等极限情况能否适用.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) int n, m, k; struct pii {
int d;
string s;
pii(int d_, string s_) { d = d_, s = s_; }
}; queue<pii> que;
queue<pii> res; int main() {
cin >> n >> m >> k;
if (k > 4 * n * m - 2 * n - 2 * m) {
cout << "NO\n";
return 0;
}
cout << "YES\n";
if (m == 1) {
if (k <= n - 1)
cout << "1\n" << k << " D\n";
else
cout << "2\n" << n - 1 << " D\n" << k - n + 1 << " U\n";
return 0;
}
inc(i, 1, n - 1) {
if (k) que.push(pii(min(k, m - 1), "R")), k -= min(k, m - 1);
pii tmp = pii(-1, "");
if (k % 3 == 1 && k < 3 * (m - 1)) tmp = pii(1, "D"), k--;
if (k % 3 == 2 && k < 3 * (m - 1)) tmp = pii(1, "DU"), k -= 2;
if (k >= 3)
que.push(pii(min(k / 3, m - 1), "DUL")), k -= min(k / 3, m - 1) * 3;
if (tmp.s.size()) que.push(tmp);
if (k) que.push(pii(1, "D")), k--;
}
if (k) que.push(pii(min(k, m - 1), "R")), k -= min(k, m - 1);
if (k) que.push(pii(min(k, m - 1), "L")), k -= min(k, m - 1);
if (k) que.push(pii(min(k, n - 1), "U")), k -= min(k, n - 1);
cout << que.size() << "\n";
while (que.size())
cout << que.front().d << " " << que.front().s << "\n", que.pop();
}

 

1290C - Prefix Enlightenment

题意: 给出一个长度为 n 的 01 串和 k 个集合, 集合里面是 1 - n 的一些数, 任意三个集合的交集是空集. 要求选出部分集合, 对于每个集合中的每个数, 都会使得 01 串中的相应位置翻转. 问要使得串前 m 个数都是 1, 最少需要选出的集合数(保证有解). 输出 m 取遍 1 - n 共 n 个答案.

思路: 每个位置最多只会受 2 个集合的选取与否影响. 要使得某一位置的数为 1, 如果该位置受 2 个集合影响, 可以得到这 2 个集合是否会共同存在, 如果受 1 个集合影响, 可以得到这 1 个集合是否会存在. 用带权并查集维护这样的关系, 根节点存储当前块中与根节点共存的数的数量和不共存的(每个节点初始值为 (1, 0)), 对答案的贡献是这两个权值的最小值, 每个节点到根节点的距离的奇偶性反映是否与根节点共存. 同时建立一个特殊的节点, 权值为 (0, inf), 当一个集合被断言存在或不存在时就与该节点相连, 若存在距离为 0; 不存在距离为1.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++)
#define dec(i, l, r) for (int i = l; i >= r; i--)
#define pii pair<int, int>
#define fi first
#define se second
#define pb push_back const int maxn = 3e5 + 5; int n, k, q, c;
ll res;
string s;
vector<int> v[maxn]; int par[maxn], dis[maxn];
ll val[maxn][2]; int find(int x) {
if (x == par[x]) return x;
int pp = par[x];
par[x] = find(par[x]);
dis[x] += dis[pp];
return par[x];
}
ll query(int x) {
x = find(x);
return min(val[x][0], val[x][1]);
}
void unite(int x, int y, int d) {
int px = find(x), py = find(y);
if (px != py) {
d = (d + dis[x] + dis[y]) & 1;
res -= query(px), res -= query(py);
par[px] = py;
dis[px] += d;
val[py][0] += val[px][0 ^ d];
val[py][1] += val[px][1 ^ d];
res += query(py);
}
} int main() {
cin >> n >> k >> s;
inc(i, 1, k) par[i] = i, val[i][0] = 1;
par[k + 1] = k + 1, val[k + 1][1] = (int)1e7;
inc(i, 1, k) {
scanf("%d", &q);
while (q--) {
scanf("%d", &c);
v[c].push_back(i);
}
}
inc(i, 1, n) {
if (s[i - 1] == '1') {
if (v[i].size() == 2) {
unite(v[i][0], v[i][1], 0);
} else if (v[i].size() == 1) {
unite(v[i][0], k + 1, 1);
}
} else {
if (v[i].size() == 2) {
unite(v[i][0], v[i][1], 1);
} else if (v[i].size() == 1) {
unite(v[i][0], k + 1, 0);
}
}
printf("%d\n", res);
}
}

 

1277D - Let's Play the Words?

题意: 给出若干个互不相同的01串, 要求翻转尽可能少的串, 使得新的串集合可以首尾相同地相连, 且互不相同.

注意: 00和11可以任意插入到有0/1的地方. 01与10较多者翻转成较少者. 字符串判重用tried树或者map.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++) const int maxn = 4e6 + 5; int ch[maxn][2], cnt[maxn], tot; void init(int x) {
tot = 0;
memset(cnt, 0, sizeof(int) * x);
memset(ch, -1, sizeof(int) * x * 2);
}
void insert(string str) {
int p = 0;
for (int i = 0; i < str.size(); i++) {
if (ch[p][str[i] - '0'] == -1) ch[p][str[i] - '0'] = ++tot;
p = ch[p][str[i] - '0'];
}
cnt[p]++;
}
int find(string str) {
int p = 0;
for (int i = 0; i < str.size(); i++) {
if (ch[p][str[i] - '0'] == -1) return 0;
p = ch[p][str[i] - '0'];
}
return cnt[p];
} int t, n;
string s[maxn]; int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> t;
inc(cas, 1, t) {
cin >> n;
int sz = 0, kind[4] = {0};
inc(i, 0, n - 1) {
cin >> s[i];
sz += s[i].size();
kind[((s[i][0] == '1') << 1) + (s[i].back() == '1')]++;
}
init(sz + 1);
inc(i, 0, n - 1) insert(s[i]);
if (!kind[1] && !kind[2] && kind[0] && kind[3])
cout << "-1\n";
else {
int half = kind[1] + kind[2] >> 1;
int r = half - min(kind[1], kind[2]);
cout << r << "\n";
if (kind[1] < half) {
for (int i = 0; i < n && r; i++)
if (((s[i][0] == '1') << 1) + (s[i].back() == '1') == 2) {
reverse(s[i].begin(), s[i].end());
if (find(s[i])) continue;
cout << i + 1 << " ";
r--;
}
}
if (kind[2] < half) {
for (int i = 0; i < n && r; i++)
if (((s[i][0] == '1') << 1) + (s[i].back() == '1') == 1) {
reverse(s[i].begin(), s[i].end());
if (find(s[i])) continue;
cout << i + 1 << " ";
r--;
}
}
cout << "\n";
}
}
}

 

1276C - Beautiful Rectangle

题意: 给出N(N≤1e5)个数, 要选出尽可能多的数, 放入一个矩阵中, 使得矩阵每一行, 每一列的数各不相同.

思路: 不妨令行数≤列数, 显然每个数的频数<=行数, 所以在确定行数后, 可以知道哪些数能填入矩阵. 之后要构造出一种填数方法, 考虑这样的顺序, (0, 0), (1, 1) ... (sz - 1, sz - 1), (1, 0), (2, 1), ... , (sz - 1, sz), 前面先填频数最高的, 一定不同行不同列, 而后面的数任意取小于sz的连续位置, 都是不同行不同列的.

view code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inc(i, l, r) for (int i = l; i <= r; i++)
#define pii pair<int, int>
#define fi first
#define se second
#define pb push_back const int maxn = 1e6 + 5; int a[maxn];
int n, top, m;
pii p[maxn]; int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
inc(i, 0, n - 1) cin >> a[i];
sort(a, a + n);
p[top++] = pii(1, a[0]);
inc(i, 1, n - 1) {
if (a[i] == a[i - 1])
p[top - 1].fi++;
else
p[top++] = pii(1, a[i]);
}
sort(p, p + top, greater<pii>());
int ri = 0, res = 0;
for (int i = 1, j = top - 1, s = 0; i <= n; i++) {
while (j >= 0 && p[j].fi <= i) s += p[j--].fi;
int ss = s + i * (j + 1);
if (ss / i >= i && ss - ss % i > res) {
res = ss - ss % i;
ri = i;
}
}
int rj = res / ri;
cout << res << "\n" << ri << " " << rj << "\n";
vector<vector<int> > mat(ri, vector<int>(res / ri));
for (int i = 0, k = 0, tmp = 0; i < rj; i++) {
for (int j = 0; j < ri; j++) {
mat[j][(i + j) % rj] = p[k].se;
if (++tmp == min(ri, p[k].fi)) k++, tmp = 0;
}
}
inc(i, 0, ri - 1) {
inc(j, 0, rj - 1) cout << mat[i][j] << " ";
cout << "\n";
}
}