AtCoder Regular Contest 125 补题记录
为什么说是补题记录?因为赛时只过了一题/hanx
A - Dial Up
给你一个长度为 \(n\) 的 \(01\) 序列 \(a\) 和一个长度为 \(m\) 的 \(01\) 序列 \(b\)。
每次操作可以执行下列操作:
- 把 \(a_1\) 放到 \(a_n\) 之后
- 把 \(a_n\) 放到 \(a_1\) 之前
- 把 \(a_1\) 的值加进序列 \(c\) 的末尾
问能否让序列 \(c\) 变成 \(b\)。
首先无解情况是 \(b\) 有某个数但是 \(a\) 没有。
发现只有第一次出现 \(a_1 \not = b_i \and b_i \not = b_{i-1}\) 时,才会从 \(a_1\) 出发找一个 \(=b_i\) 的位置,其他出现 \(b_{i} \not = b_{i-1}\) 的情况时,都可以在两个相邻的位置左右移动一下就好。
然后根据这个去计算即可。
B - Squares
给你一个数 \(n\),求有多少数满足下列条件:
- \(1 \le x,y \le n\),\(x^2 - y\) 是一个平方数。
对 \(998244353\) 取模。
设平方数为 \(z^2\),
则:
设 \(p = x+z, q = x-z\),则有
故 \(1 \le pq \le y\),又因为 \(x = \frac{p+q}{2}\)。
所以 \(1 \le \frac{p+q}{2} \le n\) 。
设 \(p \ge q\)
考虑枚举 \(q\)
那么 \(q \le p \le \frac{n}{q}\) ,并且 \(p \equiv q \pmod 2\) 。
所以 \(q\) 只需要枚举到 \(\sqrt n\) 级别就好,有多少合法的 \(p\) 可以 \(\mathcal O(1)\) 来计算。
int Calc(int k) { return (n + k * k) / 2 * k; }
signed main() {
n = read();
int lim = sqrt(n) + 1;
for(int p = 1; p <= lim; ++p) {
if(n / p < p) break;
int len = n / p - p + 1;
ans += (len + 1) / 2;
ans %= mod;
}
cout << ans << "\n";
return 0;
}
C - LIS to Original Sequence
给你一个长度为 \(K\) 的序列 \(a\) 和一个数 \(n\)。
其中 \(a\) 序列满足 \(1 \le a_1 < a_2 < ... < a_k \le n\) 。
你要找一个长度为 \(n\) 的序列 \(p\),使得这个 \(p\) 的最长上升子序列之一是 \(a\) 序列且字典序最小。
分情况讨论一下。
如果 \(k=1\),那么把整个序列降序输出即可。
如果 \(a_1 = 1\),那么第一个肯定放 \(1\),剩下的就是一个子问题。
如果 \(a_1 > 1\),那么最优的放法是先放一个 \(a_1\),再放一个 \(1\)。然后剩下的问题是,找一个 \(2,3,...,a_1-1,a_1+1,a_1+2,...,n\) 的排列 \(p'\),使得这个 \(p'\) 的最长上升子序列之一是 \(a_2,a_3,...a_k\) 这个序列。发现这也是一个子问题。
然后递归处理一下就好啦,复杂度 \(\mathcal O(n)\)。
int n, K, fir = 1, now = 1;
int a[MAXN], stc[MAXN], sc = 0;
bool vis[MAXN];
queue<int> q;
void Solve(int pos) {
if(pos > n) return ;
if(now == K) {
for(int i = n; i >= fir; --i) if(!vis[i]) cout << i << " ";
return ;
}
if(pos == n || (now <= K && fir == a[now])) {
cout << fir << " ";
vis[fir] = true;
now ++, fir ++;
while(vis[fir]) fir ++;
Solve(pos + 1);
} else {
cout << a[now] << " " << fir << " ";
vis[a[now]] = true, vis[fir] = true;
now ++, fir ++;
while(vis[fir]) ++ fir;
Solve(pos + 2);
}
}
signed main() {
n = read(), K = read();
for(int i = 1; i <= K; ++i) a[i] = read();
Solve(1);
return 0;
}
D - Unique Subsequence
给你一个长度为 \(n\) 的序列 \(a\)。
求满足一下条件的 \(a\) 的非空子序列 \(s\) 的个数,对 \(998244353\) 取模。
- \(s\) 作为 \(a\) 的子序列在 \(a\) 中只出现一次。
(或者说,对于 \(a\) 的所有非空子序列,输出只出现过一次的子序列 \(s\) 的个数)
(出现一次的子序列计数)
我们考虑从前向后计算答案,设 \(f_i\) 表示以 \(a_i\) 结尾的合法子序列的个数。
设有一个 \(j\) 满足 \(a_j = a_i\)。
考虑在 \([j,i)\) 之间的位置 \(k\),显然 \(a_i\) 是可以接在任意一个 \(a_k\) 后面的。(就算是 \(a_j\) 也可以,相当于连续放了两个 \(a_j\))
考虑在 \([1,j)\) 之间的位置 \(k\),如果把 \(a_i\) 接在 \(a_k\) 后面,和把 \(a_j\) 接在 \(a_k\) 后面是等价的,也就是说出现了相同的子序列,所以不能统计,而且还要把 \(j\) 处的答案清空。
发现只有简单的区间求和区间修改,上一个树状数组就做完了。
最后的答案就是所有 \(f_i\) 的和。
复杂度 \(\mathcal O(n \log n)\)。
int Mod(int x) { return (x % mod + mod) % mod; }
namespace Bit {
int sum[MAXN];
int lb(int x) { return x & -x; }
void Modify(int x, int k) { for(; x <= n; x += lb(x)) sum[x] = Mod(sum[x] + k); }
int Query(int x) { int res = 0; for(; x; x -= lb(x)) res = Mod(res + sum[x]); return res; }
}
signed main() {
n = read();
for(int i = 1; i <= n; ++i) a[i] = read();
for(int i = 1; i <= n; ++i) {
if(!pre[a[i]]) {
int res = Bit::Query(i);
Bit::Modify(i, Mod(1 + res));
pre[a[i]] = i;
} else {
int res = Bit::Query(i) - Bit::Query(pre[a[i]] - 1);
int tmp = Bit::Query(pre[a[i]]) - Bit::Query(pre[a[i]] - 1);
Bit::Modify(pre[a[i]], Mod(- tmp));
Bit::Modify(i, Mod(res));
pre[a[i]] = i;
}
}
int ans = 0;
ans = Bit::Query(n);
cout << ans << "\n";
return 0;
}
E - Snack
有 \(n\) 种零食,\(m\) 个孩子。
第 \(i\) 种零食有 \(a_i\) 块。
把零食分发给孩子们,条件为:
第 \(i\) 个孩子,每种零食最多只能拿 \(b_i\) 块,所有零食一共最多只能拿 \(c_i\) 块。
求出在这些条件下可以分发给孩子们的零食的最大总数。
\(n,m \le 2 \times 10^5\)
先不看数据范围看看怎么做。
建一个源点 \(S\) 和汇点 \(T\),把零食设为 \(L_i\),孩子看作 \(R_i\)。
按照如下方式建图:
- \(S\) 向 \(L_i\) 连边,流量为 \(a_i\);
- \(L_i\) 向 \(R_j\) 连边,流量为 \(b_j\)。
- \(R_i\) 向 \(T\) 连边,流量为 \(c_i\)。
然后最大流就是这个问题的答案。但是 \(n^2\) 的边数会让我们连图都建不完。
因为最大流 \(=\) 最小割,所以我们看看能不能快速求最小割。
我们考虑对 \(L_i\) 进行一手划分。让里面的一部分点与 \(S\) 连通,设这些点的集合为 \(X\),剩下一部分与 \(T\) 连通,设这些点的集合为 \(Y\)。
考虑算一下这种情况下最小割掉多少,首先 \(Y\) 中的点与 \(S\) 相连的边肯定要割掉,每条边的贡献为 \(a_i, i \in Y\)。
然后考虑 \(X\) 中的点,他们与 \(R_i\) 相连,我们考虑每一个 \(R_i\) ,它可以选择割掉与 \(T\) 相连的边,花费为 \(c_i\),也可以割掉所有与 \(X\) 中的点相连的边,花费为 \(|X| b_i\)。我们对于每一个 \(R_i\) 在这两种情况中取一个 \(\min\) 即可。
不难想到,若 \(X\) 的大小相同,选最大的 \(|X|\) 个 \(a_i\) 所对应的 \(i\) 划分到 \(X\) 中是最优的。(因为弄掉 \(X\) 中的花费是 \(\le a_i\) 的,弄掉 \(Y\) 中点的花费是 \(=a_i\) 的)
然后我们可以排个序,以此将每个点 \(L_i\) 加入 \(X\) 中算一下贡献,对所有情况的贡献取个 \(\min\) 就是整张图的最小割,也就是最大流,也就是答案。
对于这个 \(\min \{c_i, |X|b_i\}\),当 \(|X| \le \frac{c_i}{b_i}\) 的时候,取后者更优,否则去前者,因此我们可以对此查分处理一手,然后不断将点加入 \(X\) 的时候顺便维护一个前缀和就可以 \(\mathcal O(1)\) 计算这些答案了。
当然不断将点加入 \(Y\) 也可以,一个道理。我写的是这个。
int n, m;
int a[MAXN], b[MAXN], c[MAXN];
int sumb[MAXN], sumc[MAXN];
int ans = INF, cutS = 0, sb = 0, sc = 0;
signed main() {
n = read(), m = read();
for(int i = 1; i <= n; ++i) a[i] = read();
sort(a + 1, a + n + 1); // 从小到大不断割给 T 是最优的(?)
for(int i = 1; i <= m; ++i) b[i] = read();
for(int i = 1; i <= m; ++i) c[i] = read();
for(int i = 1; i <= m; ++i) {
int lim = c[i] / b[i]; // 去找一下选 c_i 还是 b_i*X 的 min,当 X <= c_i / b_i 的时候选后者,否则选前者
if(lim <= n) {
sumb[n - lim] += b[i];
sumc[n - lim] -= c[i];
} else {
sumb[0] += b[i];
sumc[0] -= c[i];
}
sumc[0] += c[i];
}
for(int i = 0; i <= n; ++i) { // 枚举多少割给了 T
cutS += a[i];
sb += sumb[i];
sc += sumc[i];
ans = min(ans, cutS + sc + sb * (n - i));
}
printf("%lld\n", ans);
return 0;
}
F - Tree Degree Subset Sum
给你一棵 \(n\) 个的树。
求有多少对不同的 \((x,y)\),其中 \(0 \le x \le n\),一对 \((x,y)\) 的含义为选 \(x\) 个点,点的度数和为 \(y\)。
\(2 \le n \le 2 \times 10^5\)。
设 \(d_i\) 表示结点 \(i\) 的度数 \(-1\)。
设 \(f(s)\) 表示度数(\(d_i\))和为 \(s\) 所需的最小的点数,\(g(s)\) 表示度数(\(d_i\))和为 \(s\) 所需的最大点数。
先给出一个引理,
对于所有 \(f(s) \le c \le g(s)\) 的情况都是合法的。
如果我们能证明这个,那么我们只需要 \(dp\) 出 \(f(s)\) 和 \(g(s)\) 就好了。
因为不同的 \(d_i\) 最多只会有 \(\sqrt n\) 个,然后就是一个使用单调队列优化多重背包的形式(如果忘了可以来看 何老师写的笔记。
复杂度是 \(\mathcal O(n \sqrt n)\) 的。
考虑怎么证明这个。
设 \(z\) 表示 \(d\) 中 \(0\) 的个数。如果我们能证明 \(g(s) - f(s) \le 2z\),那么引理就能被证明。那这个为啥对呢,因为 \(f(s)\) 肯定有最少的 \(d_i=0\) 的个数,那么放上 \(0 \sim z\) 个 \(d_i=0\) 的点,\([f(s),f(s)+z]\) 这一段区间就都能得到。而 \(g(s)\) 肯定有最多的 \(d_i=0\) 的个数,那么减掉 \(0 \sim z\) 个 \(d_i=0\) 的点,\([g(s)-z,g(s)]\) 这一段区间就都能得到。
设 \(s\) 表示度数和 \(c\) 表示选的点的数量。
可以发现 \(-z \le s - c \le z - 2\)。左边界的时候是选了 \(z\) 个度数为 \(0\) 的点。
那右边界呢,考虑选所有数,值为 \(\sum (d - 1) = -2\),然后 \(z\) 个度数为 \(0\) 的点不选,就出现了 \(+z\)。
所以说 \(-z \le s - g(s) \le s - f(s) \le z - 2\)。
所以 \(g(s) - f(s) \le 2z\)。
证毕。
#include<bits/stdc++.h>
#define LL long long
//#define int long long
#define orz cout << "tyy YYDS!!!\n"
using namespace std;
const int MAXN = 3e5 + 10;
const int INF = 1e9 + 7;
const int mod = 998244353;
int n, cnt = 0;
LL ans = 0;
int d[MAXN], c[MAXN];
int mx[MAXN], mn[MAXN]; // mx[i] 表示搞出 i 来最多用几个数,mn[i] 表示搞出 i 来最少用几个数
int f[MAXN];
int q[MAXN];
int read() {
int s = 0, f = 0; char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
signed main() {
n = read();
for(int i = 1, u, v; i < n; ++i) {
u = read(), v = read();
d[u] ++, d[v] ++;
}
for(int i = 1; i <= n; ++i) {
d[i] --, c[d[i]] ++;
mx[i] = - INF, mn[i] = INF;
if(!d[i]) cnt ++;
}
mx[0] = mn[0] = 0;
for(int i = 1; i < n; ++i) { // 枚举物品
int x = c[i];
if(!x) continue;
for(int j = 0; j <= n; ++j) f[j] = mx[j];
for(int j = 0; j < i; ++j) { // 枚举余数
int head = 1, tail = 0;
for(int k = 0, p = j; p <= n; ++k, p += i) { // p 是容量
while(head <= tail && q[head] < p - i * x) ++ head;
if(head <= tail) mx[p] = max(mx[p], f[q[head]] + (p - q[head]) / i);
while(head <= tail && f[q[tail]] - (p - q[tail]) / i <= f[p]) tail --;
// if(head <= tail) mx[p] = max(mx[p], f[q[head]] + k - q[head] / i);
// while(head <= tail && f[q[tail]] - q[tail] / i <= f[p] - p / i) tail --;
q[++tail] = p;
}
}
}
for(int i = 1; i < n; ++i) {
int x = c[i];
if(!x) continue;
for(int j = 0; j <= n; ++j) f[j] = mn[j];
for(int j = 0; j < i; ++j) {
int head = 1, tail = 0;
for(int k = 0, p = j; p <= n; ++k, p += i) {
while(head <= tail && q[head] < p - i * x) ++ head;
if(head <= tail) mn[p] = min(mn[p], f[q[head]] + (p - q[head]) / i);
while(head <= tail && f[q[tail]] - (p - q[tail]) / i >= f[p]) tail --;
// if(head <= tail) mn[p] = min(mn[p], f[q[head]] + k - q[head] / i);
// while(head <= tail && f[q[tail]] - q[tail] / i >= f[p] - p / i) tail --;
q[++tail] = p;
}
}
}
for(int i = 0; i <= n; ++i) {
if(mn[i] <= mx[i]) {
ans += (mx[i] - mn[i] + 1 + cnt); // 因为背包的时候没有把 0 当作一个物品
}
}
cout << ans << "\n";
return 0;
}