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\)

则:

\[\begin{aligned} x^2 - y & = z^2 \\ (x+z)(x-z) & = y \end{aligned} \]

\(p = x+z, q = x-z\),则有

\[\begin{aligned} pq = y \end{aligned} \]

\(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;
}
posted @ 2022-04-27 22:33  Suzt_ilymtics  阅读(54)  评论(0编辑  收藏  举报