Codeforces Round #791 (Div. 2)

第一次没看题解写完全部题目祭(虽然问其他人要提示了吧qwq)。罚时 \(+9\) 寄。海底捞月 \(\rm F\) 祭。

A. AvtoBus

\(t\) 组数据。给出 \(n\),求在满足方程 \(4x+6y=n\) 的所有非负整数解 \(x,y\) 中,\(x+y\) 的最小值和最大值,或报告无解。(\(1\le t\le 10^3,1\le n\le 10^{18}\))

首先无解的情况,有两种情况是显然无解的,分别是 \(n\) 为奇数,和 \(n\le 3\),对于剩下的情况,我们可以把原方程等价看为:

\[2x+3y=\dfrac{n}{2} \]

发现左边可以构造出所有 \(\ge 2\) 的正整数,如果是偶数只需令 \(y=0,x=\frac{n}{4}\),否则只需令 \(y=1,x=\lfloor\frac{n}{4}\rfloor-1\)。所以剩下的情况一定有解。

对于 \(x+y\) 的极值,容易发现最小值是尽量让 \(y\) 最大,最大值是尽量让 \(x\) 最大。刚刚证明一定有解时构造的方案就是尽量用 \(2\) 来凑出 \(\frac{n}{2}\),所以一定是最大的,两种情况综合一下,最大值即为 \(\lfloor\frac{n}{4}\rfloor\)。对于最小值,我们可以用类似的思路构造,只不过这次分讨是关于 \(3\) 的剩余系。当 \(\frac{n}{2}\equiv 0\pmod{3}\) 时,\(x=0,y=\frac{n}{6}\) 即可;当 \(\frac{n}{2}\equiv 1\pmod{3}\) 时,\(x=2,y=\lfloor\frac{n}{6}\rfloor-1\) 即可;当 \(\frac{n}{2}\equiv 2\pmod{3}\) 时,\(x=1,y=\lfloor\frac{n}{6}\rfloor\) 即可。综上,如果 \(\frac{n}{2}\)\(3\) 的倍数,则答案为 \(\frac{n}{6}\),否则为 \(\lfloor\frac{n}{6}\rfloor+1\)。时间复杂度 \(\mathcal{O}(t)\)

#include <cstdio>
typedef long long ll;
int main()
{
	int qwq; scanf("%d", &qwq);
	while (qwq--)
	{
		ll n; scanf("%lld", &n); ll mx, mn;
		if (n <= 3) { puts("-1"); continue; }
        if (n & 1) { puts("-1"); continue; }
        n /= 2; mx = n / 2;
        if (n % 3 == 0) mn = n / 3;
        if (n % 3 == 1) mn = n / 3 + 1;
        if (n % 3 == 2) mn = n / 3 + 1;
        printf("%lld %lld\n", mn, mx);
	}
	return 0;
}

B. Stone Age Problem

维护一个长为 \(n\) 的序列 \(a\),支持以下两种操作,共 \(q\) 次:

  • \(a_p\) 赋值为 \(x\)
  • 将整个数列均赋值为 \(x\)

每次操作后输出当前序列中所有元素的和。(\(1\le n,q\le 2\times 10^5,1\le a_i,x\le 10^9\))

如果你手速比较快可以试着冲个线段树。

注意到两种操作对于序列整体和的改变都很好求出,但问题在于,在操作 \(1\) 时,我们并不知道 \(a_p\) 在最近一次的操作 \(2\) 后是否又被修改了,或根本没有进行过操作 \(2\),从而就不知道原始值是记录的整体 \(\rm tag\) 还是 \(a_p\) 内存的值。一个想法是给每个位置打上 \(\rm vis\) 标记,但问题是这样每次操作 \(2\) 还要清空,依然爆炸。一个常见的想法是,\(\rm vis\) 数组不记录 \(0/1\),而记录它最近一次被修改,是在哪次操作 \(2\) 后,或没有被修改过。这样,只需要给每个操作 \(2\) 分配一个编号即可,时间复杂度 \(\mathcal{O}(q)\)

#include <cstdio>
const int N = 2e5 + 10; int a[N], vis[N]; typedef long long ll;
int main()
{
	int n, q, tn = 0; scanf("%d%d", &n, &q); long long sum = 0; int now = -1;
	for (int i = 1; i <= n; ++i) scanf("%d", a + i), sum += a[i];
	for (int i = 1, op, p, x; i <= q; ++i)
	{
		scanf("%d", &op);
		if (op == 1)
		{
			scanf("%d%d", &p, &x); 
			if (vis[p] != tn) sum += (x - now);
			else sum += (x - a[p]);
			vis[p] = tn; a[p] = x;
			printf("%lld\n", sum);
		}
		else scanf("%d", &x), now = x, sum = (ll)n * x, printf("%lld\n", sum), ++tn;
	}
	return 0;
}

C. Rooks Defenders

给出一个 \(n\times n\) 的棋盘,和 \(q\) 次操作,每次操作形如以下三种形式之一:

  • \((x,y)\) 放一个车。
  • 拿走 \((x,y)\) 中的一个车。(保证这里至少有一个车)
  • 询问子矩阵 \((x_1,y_1)-(x_2,y_2)\) 中所有格子,都是否能被至少一个车攻击到。

车的攻击范围是同行同列。(\(1\le n\le 10^5,1\le q\le 2\times 10^5\))

注意到一个子矩阵能被车的攻击范围覆盖,当且仅当行 \(x_1-x_2\) 中均有车,或列 \(y_1-y_2\) 中均有车。发现问题变为了询问行或列一段区间是否全都有车。考虑放一个车相当于在某一行和某一列增加一个车,删去是类似的。所以对于分别对所有行和所有列维护一个支持单点 \(+1/-1\),区间询问是否全部非 \(0\) 的 DS。线段树或树状数组均可。时间复杂度 \(\mathcal{O}(q\log n)\)

#include <cstdio>
#define ls(k) (k << 1)
#define rs(k) (k << 1 | 1)
const int N = 1e5 + 10;
struct SegTree
{
	struct node{ int l, r, flg, v; }h[N << 2];
	void pushup(int k) { h[k].flg = h[ls(k)].flg | h[rs(k)].flg; }
	void build(int k, int l, int r)
	{
		h[k].l = l; h[k].r = r;
		if (l == r) return h[k].flg = 1, void();
		int mid = (l + r) >> 1;
		build(ls(k), l, mid); build(rs(k), mid + 1, r);
		pushup(k);
	}
	void change(int k, int p, int v)
	{
		if (h[k].l == h[k].r) return h[k].v += v, h[k].flg = !h[k].v, void();
		int mid = (h[k].l + h[k].r) >> 1;
		if (p <= mid) change(ls(k), p, v);
		else change(rs(k), p, v);
		pushup(k);
	}
	int query(int k, int x, int y)
	{
		if (x <= h[k].l && h[k].r <= y) return h[k].flg;
		int mid = (h[k].l + h[k].r) >> 1, ret = 0;
		if (x <= mid) ret |= query(ls(k), x, y);
		if (mid < y) ret |= query(rs(k), x, y);
		return ret;
	}
}sgt[2];
int main()
{
	int n, q; scanf("%d%d", &n, &q); sgt[0].build(1, 1, n); sgt[1].build(1, 1, n);
	for (int i = 1, t, x1, y1, x2, y2; i <= q; ++i)
	{
		scanf("%d%d%d", &t, &x1, &y1);
		if (t == 3)
		{
			scanf("%d%d", &x2, &y2);
			if (!sgt[0].query(1, x1, x2) || !sgt[1].query(1, y1, y2)) puts("Yes"); else puts("No");
		}
		else if (t == 2) sgt[0].change(1, x1, -1), sgt[1].change(1, y1, -1);
		else sgt[0].change(1, x1, 1), sgt[1].change(1, y1, 1);
		
	}
	return 0;
}

D. Toss a Coin to Your Graph...

给出一张 \(n\) 个点 \(m\) 条边的有向图,点 \(i\) 有点权 \(a_i\),从任意点出发经过 \(k-1\) 条边后停下(边和点可以重复经过),最小化经过点点权的最大值,求出这个最小值。(\(1\le n\le 2\times 10^5,0\le m\le 2\times 10^5,1\le k\le 10^{18},1\le a_i\le 10^9\),图无重边自环)

最大值最小直接考虑二分,转化成判断性问题即,给出一张有向图,不能经过点权大于 \(x\) 的点,能否走出长为 \(k-1\) 的路径。这就比较简单了,考虑在有向图上拓扑排序,如果找到环,则一定满足条件。否则需要做 \(\rm dp\),找到图中最长链,判断是否有 \(k-1\) 步即可。时间复杂度 \(\mathcal{O}(n\log a_i)\)

#include <queue>
#include <cstdio>
#include <vector>
const int N = 2e5 + 10; typedef long long ll; 
std::vector<int> G[N]; int a[N], vis[N], in[N], f[N]; int n, m; ll k; 
struct edge{ int x, y; }E[N];
bool check(int x)
{
	std::queue<int> q;
	for (int i = 1; i <= n; ++i) vis[i] = (a[i] > x), in[i] = 0, f[i] = 0, G[i].clear();
	for (int i = 1; i <= m; ++i)
		if (!vis[E[i].x] && !vis[E[i].y]) G[E[i].x].push_back(E[i].y), ++in[E[i].y];
	for (int i = 1; i <= n; ++i) if (!in[i] && !vis[i]) q.push(i);
	while (!q.empty())
	{
		int u = q.front(); q.pop();
		for (auto v : G[u])
		{
			f[v] = std::max(f[v], f[u] + 1);
			if (!--in[v]) q.push(v);
		}
	}
	for (int i = 1; i <= n; ++i) if (in[i] && !vis[i]) return true;
	for (int i = 1; i <= n; ++i)
		if (f[i] >= k - 1 && !vis[i]) return true;
	return false;
}
int main()
{
	scanf("%d%d%lld", &n, &m, &k);
	for (int i = 1; i <= n; ++i) scanf("%d", a + i);
	for (int i = 1; i <= m; ++i) scanf("%d%d", &E[i].x, &E[i].y);
	int l = 1, r = 1e9, mid, ans = -1;
	while (l <= r)
	{
		mid = (l + r) >> 1;
		if (check(mid)) ans = mid, r = mid - 1;
		else l = mid + 1;
	}
	printf("%d\n", ans); return 0;
}

E. Typical Party in Dorm

给出一个长为 \(n\) 的字符串 \(s\),定义一个字符串的权值为满足子串 \(s_{l,r}\) 是回文串的二元组 \((l,r)(l\le r)\) 的个数。\(s\) 中有些字符是 \(\tt ?\),共有 \(q\) 次询问,每次询问给出字符集 \(\Sigma\),求用 \(c\in\Sigma\) 替换掉 \(s\) 中所有的 \(\tt ?\) 能得到的所有 \(s\) 的权值之和,答案对 \(998,244,353\) 取模。(\(1\le n\le 10^3,1\le q\le 2\times 10^5\)\(s\) 中的字符和所有询问的字符集的并集 \(\Sigma=\{\tt a,b,c,\cdots,p,q\}\),共 \(17\) 个)

首先,虽然替换 \(\tt ?\) 能得到的字符串很多,但它们的权值无非就是由 \(\mathcal{O}(n^2)\) 种子串构成的,而后者比前者的数量少得多。所以考虑处理每个子串能对多少个可能的整串做出贡献。

当然,即使 \(\mathcal{O}(n^2)\) 再小,它也没有小到支持每次询问都枚举一遍的程度,但可能的字符集只有 \(2^{17}-1\) 种,所以我们可以考虑预处理出来。但单纯枚举子串再一个个检查是 \(\mathcal{O}(n^3)\) 的并不能通过,不过注意到这个对于中心相同的子串,重复枚举了很多次,所以考虑枚举中心,并往两边拓展。

枚举到 \(i,j\) 的时候,可能出现四种情况。

  1. \(s_i\ne \mathtt{?},s_j\ne \mathtt{?}\),此时如果 \(s_i\ne s_j\),则再继续拓展也不可能产生回文串,停止枚举即可。
  2. \(s_i\ne \mathtt{?},s_j=\mathtt{?}\),此时 \(s_j\) 必须替换为 \(s_i\)。该回文串需要的字符集为 \([i+1,j-1]\) 需要的字符集并上 \(s_i\)
  3. \(s_i=\mathtt{?},s_j\ne \mathtt{?}\),与上一种情况类似。
  4. \(s_i=\mathtt{?},s_j=\mathtt{?}\),先把 \([i,j]\) 中这种对数的个数记为 \(cnt\),下文说有啥用。

发现对于一个子串如果单独看做一个字符串,则设它需要的字符集为 \(S\),它会对所有 \(S\subseteq\Sigma\) 的询问,做 \(|\Sigma|^{cnt}\) 的贡献。当然,这个子串外面还会有 \(\tt ?\),设它的数量为 \(tot\),这还会额外乘上 \(|\Sigma|^{tot}\) 的贡献。

现在有两个问题,首先是在预处理时,我们只能把答案记录在 \(=S\) 的位置,而不能记录到所有 \(S\) 的超集的位置,不过这个问题可以用高维前缀和解决掉。但下一个问题就是我们在做高维前缀和和时,\(\tt a\)\(\tt ab\)\(\tt aab\) 做的贡献是不一样的,因为最终的答案是关于询问的字符集大小而不是需要的字符集大小。但这个问题也比较好解决,注意到 \(S\) 对字符集大小相同的询问做的贡献是相同的,所以可以对每种字符集大小分别记录答案,分别做高维前缀和。

最终询问的时候只需要用预处理好的信息即可。时间复杂度 \(\mathcal{O}((n^2+|\Sigma|2^{|\Sigma|}+q)|\Sigma|)\)

#include <cstdio>
#include <cstring>
const int N = 1e3 + 10, B = (1 << 17) + 10, mod = 998244353; char s[N]; int f[20][B];
inline int ksm(int a, int b)
{
	int ret = 1;
	while (b)
	{
		if (b & 1) ret = 1ll * ret * a % mod;
		a = 1ll * a * a % mod; b >>= 1;
	}
	return ret;
}
int main()
{
	int n, tot = 0; scanf("%d%s", &n, s + 1);
	for (int i = 1; i <= n; ++i) tot += (s[i] == '?');
	for (int i = 1, k, S, ret, now; i <= n; ++i)
	{
		S = 0; ret = 0; now = 0;
		for (int j = i; j <= n; ++j)
		{
			k = i + i - j; if (k < 1 || (s[k] != '?' && s[j] != '?' && s[k] != s[j])) break;
			now += s[k] == '?'; if (k != j) now += s[j] == '?';
			if (s[k] != '?' && s[j] == '?') S |= (1 << (s[k] - 'a'));
			else if (s[j] != '?' && s[k] == '?') S |= (1 << (s[j] - 'a'));
			if (s[j] == '?' && s[k] == '?') ++ret;
			for (int l = 1; l <= 17; ++l) (f[l][S] += ksm(l, ret + tot - now)) %= mod;
		}
	}
	for (int i = 1, k, S, ret, now; i < n; ++i)
	{
		S = 0; ret = 0; now = 0;
		for (int j = i + 1; j <= n; ++j)
		{
			k = i + i - j + 1; if (k < 1 || (s[k] != '?' && s[j] != '?' && s[k] != s[j])) break;
			now += s[k] == '?'; if (k != j) now += s[j] == '?';
			if (s[k] != '?' && s[j] == '?') S |= (1 << (s[k] - 'a'));
			else if (s[j] != '?' && s[k] == '?') S |= (1 << (s[j] - 'a'));
			if (s[j] == '?' && s[k] == '?') ++ret;
			for (int l = 1; l <= 17; ++l) (f[l][S] += ksm(l, ret + tot - now)) %= mod;
		}
	}
	int all = (1 << 17) - 1;
	for (int p = 1; p <= 17; ++p)	
		for (int i = 0; i < 17; ++i)
			for (int S = 0; S <= all; ++S)
				if (S & (1 << i)) (f[p][S] += f[p][S ^ (1 << i)]) %= mod;
	int q; scanf("%d", &q);
	for (int i = 1, S, p; i <= q; ++i)
	{
		scanf("%s", s + 1); S = 0; p = strlen(s + 1);
		for (int j = 1; j <= p; ++j) S |= (1 << (s[j] - 'a'));
		printf("%d\n", f[__builtin_popcount(S)][S]);
	}
	return 0;
}

F. Formalism for Formalism

对于所有在 \([0,10^n-1]\) 的整数,我们认为它们都有前导 \(0\),使得所有数都有 \(n\) 个数位。给出 \(m\) 对二元组 \((u_i,v_i)(0\le u_i<v_i\le 9)\),定义两个数位 \(i,j\) 可以交换,当且仅当:

  • 存在一个 \(j\),使得 \(d_{i}=u_j,d_{i+1}=v_j\)\(d_i=v_j,d_{i+1}=u_j\)。其中 \(d_i\) 表示第 \(i\) 个数位的值。

定义两个数 \(x,y\) 是等价的,当且仅当它们可以通过交换数位变得相等。求一个最大的 \(k\),使得存在一个长度为 \(k\) 的集合,里面的元素互不等价,答案对 \(998,244,353\) 取模。(\(1\le n\le 5\times 10^4,0\le m\le 45\))

首先,求最大的互不等价的集合,相当于求有多少等价类。而对于一个等价类,我们选出一个代表用来计数,本做法选用的是字典序最大的数。

考虑设 \(f_{i,S}\) 表示考虑了前 \(i\) 位,第 \(i+1\) 位不能选 \(S\) 中数字,能组成等价类的个数。最终答案即为 \(\sum\limits_{S}f_{n,S}\)。转移是考虑下一位填哪些。

\(f_{i-1,S}\) 转移到 \(i\) 的时候,选中的数不能在 \(S\) 中,但下一个问题是,假如选了 \(j\)\(i+1\) 位有哪些不能选?直观的想法是,假如存在二元组 \((j,k)\)\((k,j)\),则第 \(i+1\) 位不能选 \(k\),但这个是有问题的。这里我们要考虑一下,所谓的能不能放,到底是怎么回事。因为我们要统计的是字典序最大的,所以一个数字前面 不能存在比它小,且能被它越过 的数字。所以对于二元组 \((j,k)\),因为 \(j<k\),所以放了 \(j\) 之后一定是不能放 \(k\) 的。而对于二元组 \((k,j)\),放了 \(j\) 后,我们需要考虑的是 \(k\) 前面到底存不存在比它小的数。因为 \(j\) 比它大,所以 \(j,k\) 交换并没有问题。但 \(k\) 交换到 \(j\) 的位置后,就要受 \(S\) 的限制了。如果 \(k\in S\),则说明 \(i\) 前面是存在比 \(k\) 小且能被 \(k\) 越过的数的,就不能放了。否则不存在,这样就可以放 \(k\)

把上面的想法用集合表示一下就是,如果设 \((j,k)\)\(k\) 组成的集合为 \(\mathbb{A}\),设 \((k,j)\)\(k\) 组成的集合为 \(\mathbb{B}\),则选 \(j\) 后,\(i+1\) 位置不能放的集合为 \((S\cap \mathbb{B})\cup \mathbb{A}\)。因为一共就 \(10\) 种数码,状压一下即可表示集合,时间复杂度 \(\mathcal{O}(nc2^{c})\),其中 \(c=10\)

#include <cstdio>
const int N = 5e4 + 10, B = (1 << 10) + 10, mod = 998244353; 
int f[N][B], a[B], b[B]; typedef long long ll;
int main()
{
	int n, m; scanf("%d%d", &n, &m);
	for (int i = 1, x, y; i <= m; ++i)
		scanf("%d%d", &x, &y), a[x] |= (1 << y), b[y] |= (1 << x);
	f[0][0] = 1; int all = (1 << 10) - 1;
	for (int i = 1; i <= n; ++i)
		for (int S = 0; S <= all; ++S)
		{
			if (!f[i - 1][S]) continue;
			for (int j = 0; j < 10; ++j)
			{
				if (S & (1 << j)) continue;
				(f[i][(S & b[j]) | a[j]] += f[i - 1][S]) %= mod;
			}
		}
	int ans = 0;
	for (int S = 0; S <= all; ++S) (ans += f[n][S]) %= mod;
	printf("%d\n", ans); return 0;
}
posted @ 2022-05-16 10:47  zhiyangfan  阅读(128)  评论(0编辑  收藏  举报