Codeforces Round #788 (Div. 2)

VP 的,写到 \(\rm D\)。有 \(\rm 1h\) 的时间写 \(\rm E\) 最后还是遗憾离场了。想到答案是 \(2^p\),但一直不会构造方案。

A. Prof. Slim

\(t\) 组数据。给出一个长为 \(n\) 的序列 \(a(a_i\ne 0)\),问能否通过若干次以下操作使得序列不降:

  • 选择两个正整数 \(i,j\) 使得 \(a_i,a_j\) 异号。
  • \(a_i\leftarrow -a_i,a_j\leftarrow -a_j\)

(\(1\le t\lt 10^4,1\le n,\sum n\le 10^5,0<|a_i|<10^9\))

注意到如果可行,那一定存在一个分界点,使得它左边的全部 \(<0\),右边的全部 \(>0\)。那可以考虑枚举这个分界点,则这个分界点可行,当且仅当它左边的序列单调不升,它右边的序列单调不降,且左边的正数个数等于右边的负数个数。可以通过预处理出前缀序列是否单调不升,后缀序列是否单调不降,和负数个数的前缀和来 \(\mathcal{O}(1)\) 判断。如果所有的分界点都不可行则整个序列不可行,否则可行。时间复杂度 \(\mathcal{O}(tn)\)

#include <cstdio>
#include <cstring>
#include <algorithm>
const int N = 1e5 + 10; int a[N], pre[N], suf[N], cnt[N];
int main()
{
	int qwq; scanf("%d", &qwq);
	while (qwq--)
	{
		int n, flg = 0; scanf("%d", &n);
		memset(pre, 0, sizeof (pre)); memset(suf, 0, sizeof (suf)); memset(cnt, 0, sizeof (cnt));
		for (int i = 1; i <= n; ++i) scanf("%d", a + i), cnt[i] = cnt[i - 1] + (a[i] < 0);
		pre[0] = pre[1] = 1;
		for (int i = 2; i <= n; ++i) pre[i] = pre[i - 1] & (std::abs(a[i]) <= std::abs(a[i - 1]));
		suf[n + 1] = suf[n] = 1;
		for (int i = n - 1; i; --i) suf[i] = suf[i + 1] & (std::abs(a[i]) <= std::abs(a[i + 1]));
		for (int i = 0; i <= n; ++i)
			if (suf[i + 1] && pre[i] && (i - cnt[i]) == (cnt[n] - cnt[i])) { flg = 1; puts("YES"); break; }
		if (!flg) puts("NO");
	}
	return 0;
}

B. Dorms War

\(t\) 组数据。给出一个长为 \(n\) 的字符串 \(s\),和 \(k\) 个特殊字符 \(c\),对它执行以下操作:

  • 找到所有的 \(k\),满足 \(s_{k+1}\) 是特殊字符。如果不存在这样的 \(k\),则退出操作。
  • \(s\) 中删去所有符合上述条件的 \(s_k\),并回到第一个操作继续找。

求操作会执行几次。(\(1\le t\le 10^5,2\le n\le 10^5,1\le k\le 26,\sum n\le 2\times 10^5\))

考虑对于每个特殊字符所在的位置删几次会停止,则答案即为所有位置的 \(\max\)。一个位置停止有两种可能,要么是前面没字符了,要么是它被后面的特殊字符删掉了,这两种情况需要的轮数取个 \(\min\) 即为所求。对于前面的字符,它能删去的是总字符数减去前面的特殊字符删去的字符数,这个可以通过维护一个前缀和求出。对于后面的特殊字符,如果设当前位置为 \(i\),后面第一个特殊字符为 \(j\),则 \(i\) 将会在 \(j-i\) 轮后被删除。注意到,即使 \(j\) 被删掉了,删掉它的那个特殊字符会代替 \(j\) 的位置,且与 \(i\) 的相对位置不变。只需要从小到大处理特殊位置即可,时间复杂度 \(\mathcal{O}(tn\log n)\)

#include <cstdio>
#include <vector>
#include <algorithm>
const int N = 1e5 + 10; char s[N], c[N][2]; std::vector<int> v, p[26];
int main()
{
	int qwq; scanf("%d", &qwq);
	while (qwq--)
	{
		int n, ans = 0; scanf("%d%s", &n, s + 1); v.clear();
		int k; scanf("%d", &k); for (int i = 1; i <= k; ++i) scanf("%s", c[i]);
		for (int i = 0; i < 26; ++i) p[i].clear();
		for (int i = 1; i <= n; ++i) p[s[i] - 'a'].push_back(i);
		for (int i = 1; i <= k; ++i)
			for (auto q : p[c[i][0] - 'a']) v.push_back(q);	
		if (v.empty()) { puts("0"); continue; } 
		std::sort(v.begin(), v.end()); int pre = 0;
		for (int i = 0, d; i < (int)v.size(); ++i)
		{
			d = v[i] - 1 - pre;
			if (i != (int)v.size() - 1) d = std::min(d, v[i + 1] - v[i]);
			pre += d; ans = std::max(ans, d);
		}
		printf("%d\n", ans);
	}
	return 0;
}

C. Where is the Pizza?

\(t\) 组数据。给出三个 \(1\sim n\) 的排列 \(a,b,d\),其中 \(d_i=a_i\)\(d_i=b_i\),或 \(d_i=0\)。问有多少种 \(c\),满足:

  • \(c\) 是一个 \(1\sim n\) 的排列。
  • 如果 \(d_i\ne 0\),则 \(c_i=d_i\),否则 \(c_i=a_i\)\(c_i=b_i\)

数据保证有解。答案对 \(10^9+7\) 取模。(\(1\le t,n\le 10^5,\sum n\le 5\times 10^5\))

注意到一个位置如果确定为 \(a_i\)\(b_i\)(不管是由于 \(d_i\) 还是自己选的),则另一处地方也会确定。如果设 \(pa_{a_i}=i,pb_{b_i}=i\),则确定为 \(a_i\) 会导致 \(pb_{a_i}\) 的位置只能选 \(a\),同理确定为 \(b_i\) 会导致 \(pa_{b_i}\) 的位置只能选 \(b\)。考虑用一个有向图表示这个关系,则一个位置确定后,它能到达的所有位置都会确定。

遍历一遍建图后,从小到大依次考虑每个位置 \(i\)。定义确定一个点为,从它出发开始打标记,直到无法继续走,或遇到一个打过标记的点。如果 \(d_i\ne 0\),则可以确定 \(a_i\)\(b_i\)。(确定选或者不选都是一种确定)然后再进行一次遍历,如果 \(i\) 满足 \(a_i\ne b_i\)\(a_i,b_i\) 都没有打过标记,就给答案乘上 \(2\)(这里选 \(a,b\) 都可以),并确定 \(a_i,b_i\)。(注意 \(a_i=b_i\) 的时候,是这两个连成一个环,所以可以不用管)时间复杂度 \(\mathcal{O}(tn)\)

#include <cstdio>
#include <vector>
#include <algorithm>
#define id(x, d) (x + d * n)
const int N = 2e5 + 10, mod = 1e9 + 7; 
int a[N], b[N], d[N], pa[N], pb[N], to[N], vis[N];
void work(int x)
{
	if (vis[x]) return ;
	vis[x] = 1; work(to[x]);
}
int main()
{
	int qwq; scanf("%d", &qwq);
	while (qwq--)
	{
		int n, ans = 1; scanf("%d", &n);
		for (int i = 1; i <= n; ++i) scanf("%d", a + i), pa[a[i]] = i;
		for (int i = 1; i <= n; ++i) scanf("%d", b + i), pb[b[i]] = i;
		for (int i = 1; i <= n; ++i) scanf("%d", d + i);
		for (int i = 1; i <= n + n; ++i) vis[i] = 0;
		for (int i = 1; i <= n; ++i)
			to[id(i, 0)] = id(pb[a[i]], 0), to[id(i, 1)] = id(pa[b[i]], 1);
		for (int i = 1; i <= n; ++i)
			if (d[i]) work(id(i, 0)), work(id(i, 1));
		for (int i = 1; i <= n; ++i)
			if (!vis[id(i, 1)] && !vis[id(i, 0)] && a[i] != b[i]) 
				ans = 2 * ans % mod, work(id(i, 1)), work(id(i, 0));
		printf("%d\n", ans);
	}
	return 0;
}

D. Very Suspicious

\(t\) 组数据。有一个无限大的,由正六边形组成类似蜂巢的平面。求最少画几条与正六边形某条边平行的直线能得到 \(n\) 个正三角形,注意得到的正三角形内部不能再有直线。(\(1\le n\le 10^5,1\le n\le 10^9\))

考虑一个转化,注意到根据题目里的限制条件,一个正六边形产生正三角形的形式是固定的,只能产生 \(0,4,6\) 个,分别由 \(1,2,3\) 条直线在正六边形中心相交得到。从直线考虑还是不太直观,注意到直线的交点只能在正六边形的中心,且它比较好计数,所以我们考虑数有多少个交点。具体来讲,正三角形的个数即为交点个数 \(\times 2\)

注意到正六边形的边长一共有三种斜率,所以想要交点更多,感性理解一下可以发现,我们可以交替使用这三种斜率,这样每次画的时候,都可以交到前面尽可能多的直线。

考虑找规律,注意到我们能做到每次画,都和前面不平行的直线有交点,所以对于每条直线能产生的交点贡献,只需要计算前面有几个不平行的直线即可:

\[\begin{array}{c|c|c|c|c|c|c|c|c}\mathbf{1}&\mathbf{2}&\mathbf{3}&\mathbf{4}&\mathbf{5}&\mathbf{6}&\mathbf{7}&\mathbf{8}&\cdots\\0&1&2&2&3&4&4&5&\cdots\end{array} \]

这样,如果我们能求它的任意前缀和,则可以通过二分直线数量的方式求得答案。

对于它的求和,我们可以三三分组(注意到加直线也是按三三分组的):

\[\{0,1,2\},\{2,3,4\},\{4,5,6\},\cdots \]

每个都是等差数列,这样,第 \(i\) 组的和即为: \(\frac{(2(i-1)+2(i-1)+2)3}{2}\),又可以发现,这个式子本身就是关于 \(i\) 的一次方程,所以它构成的数列也是等差数列,这样,前 \(i\) 组的和为:

\[\dfrac{\left(3+\frac{(2(i-1)+2(i-1)+2)3}{2}\right)i}{2}=3i^2 \]

这样,数列中前 \(n\) 项的和可以被分为完整的 \(\lfloor\frac{n}{3}\rfloor\) 组,和散的 \(n\bmod 3\) 个数,分别计算即可。剩下的就是一个二分了,时间复杂度 \(\mathcal{O}(t\log a)\),其中 \(a\) 是自己选的二分上界。

#include <cstdio>
#include <vector>
#include <algorithm>
typedef long long ll;
ll f(int x) 
{ 
	ll ret = 3ll * (x / 3) * (x / 3); int res = x % 3;
	for (int i = (x / 3) * 2, j = 1; j <= res; ++j, ++i) ret += i;
	return ret * 2;
}
int main()
{
	int qwq; scanf("%d", &qwq);
	while (qwq--)
	{
		int n; scanf("%d", &n);
		int l = 1, r = 1e9, mid, ans = -1;
		while (l <= r)
		{
			mid = (l + r) >> 1;
			if (f(mid) >= n) ans = mid, r = mid - 1;
			else l = mid + 1;
		}
		printf("%d\n", ans);
	}
	return 0;
}

E. Hemose on the Tree

\(t\) 组数据。定义一个有根,边带权,点带权树的代价为,根到所有点和所有边的路径权值最大值,定义一个路径的权值为路径上所有点和所有边的权值异或和。给出一个 \(2^p\) 个点的无根树,构造出一种选根,和给每个点,每条边赋值为 \([1,2^{p+1}-1]\) 中独特的一个整数作为权值的方案,使得树的代价最小。(\(1\le t\le 5\times 10^4,1\le p\le 17,\sum 2^p\le 3\times 10^5\))

先不管树,我们把这个问题扔到序列上看看,即使得序列的前缀和数组最大值最小。注意到,答案一定 \(\ge 2^p\),考虑最小的位置 \(i\),满足 \(a_i\operatorname{and}2^p=2^p\),其中 \(\operatorname{and}\) 表示按位或,\(a_i\) 是序列的第 \(i\) 项,则一定有 \(\left(\bigoplus\limits_{j=1}^i a_j\right)\operatorname{and} 2^p=2^p\),即这个前缀和一定 \(\ge 2^p\),从而答案一定 \(\ge 2^p\)

对于树,情况是类似的,考虑最浅的 \(\operatorname{and} 2^p=2^p\) 的点或边即可。根据构造题的套路(瞎猜可得),答案只要证明出 \(\ge 2^p\),最后一定能构造出 \(=2^p\) 的方案。考虑任意钦定一个根,然后把 \([1,2^{p+1}-1]\) 中的整数分成 \(2^p-1\) 个二元组,每个二元组形如 \((x,x+2^p)(x<2^p)\),剩下一个 \(2^p\) 直接赋值给根。则当处理到结点 \(u\) 时,如果它的父亲到根的异或和 \(\operatorname{and}2^p=2^p\),则我们需要在 \((u,fa)\) 这条边就把 \(2^p\) 给消掉(否则就会出现 \(>2^p\) 的情况),所以给 \((u,fa)\) 赋值为 \(x+2^p\),给 \(u\) 赋值为 \(x\)。如果异或和 \(\operatorname{and}2^p=0\),则就不能在边出出现 \(2^p\),所以给 \((u,fa)\) 赋值为 \(x\),给 \(x\) 赋值为 \(x+2^p\)。检查可以发现,这样赋值,所有从根出发的路径,权值要么是 \(2^p\),要么是 \(0\),符合条件。时间复杂度 \(\mathcal{O}(t2^p)\)

#include <cstdio>
#include <vector>
const int N = 2e5 + 10; int tn;
std::vector<std::pair<int, int>> T[N]; int v1[N], v2[N], val[N], p;
void dfs(int u, int fa)
{
	int v, w;
	for (auto x : T[u])
	{
		v = x.first; w = x.second;
		if (v == fa) continue;
		if (!(val[u] & (1 << p))) v2[w] = ++tn, v1[v] = (1 << p) + tn;
		else v2[w] = (1 << p) + (++tn), v1[v] = tn;
		val[v] = val[u] ^ v2[w] ^ v1[v]; dfs(v, u);
	}
}
int main()
{
	int qwq; scanf("%d", &qwq);
	while (qwq--)
	{
		int n; scanf("%d", &p); n = 1 << p; tn = 0;
		for (int i = 1; i <= n; ++i) T[i].clear();
		for (int i = 1, x, y; i < n; ++i)
			scanf("%d%d", &x, &y), T[x].emplace_back(y, i), T[y].emplace_back(x, i);
		v1[1] = val[1] = 1 << p; dfs(1, 0); puts("1");
		for (int i = 1; i <= n; ++i) printf("%d ", v1[i]);
		puts("");
		for (int i = 1; i < n; ++i) printf("%d ", v2[i]);
		puts("");
	}
	return 0;
}

F. Jee, You See?

给出四个整数 \(n,l,r,z\),求满足以下条件的长度为 \(n\) 的序列 \(a\)

  • \(l\le \sum\limits_{i=1}^n a_i\le r\)
  • \(\bigoplus\limits_{i=1}^n a_i=z\)

答案对 \(10^9+7\) 取模。(\(1\le n\le 10^3,1\le l\le r\le 10^{18},1\le z\le 10^{18}\))

这个异或就暗示我们要从二进制位的角度考虑,但注意到,只从异或的角度切入,不仅不好计数,而且对于和也不好满足。所以我们考虑从和切入,但是计算方式是逐位确定二进制位。

考虑一个类似数位 \(\rm dp\) 的思路,转化为前缀和,变为 \(\le x\) 的数个数。从高到低确定 \(n\) 个数的第 \(i\) 位二进制位。注意到不管是异或还是和,我们都只关心这一位放了多少 \(1\),而不是 \(1\) 的位置,更进一步,这里 \(1\) 还会出现进位的问题,所以即使超过了当前可以放的个数(\(x\) 的限制),依然可以用前面的空位继续放。所以我们需要知道的信息是前面还有多少个能放的空位,考虑设 \(f_{i,j}\) 表示从高位开始算的前 \(i\) 位,前 \(i\) 位有 \(j\) 个空位可以用。(注意这里的前是指从最高位到第 \(i\) 位,反映在下标上其实是后缀)则转移是考虑这一位放多少个,能放的上界是 \(2j+[x\operatorname{and}2^i=2^i]\)。所以可以得到 \(\rm dp\) 转移:

\[f_{i+1,j}\times\dbinom{n}{k}\rightarrow f_{i,\min(c-k,n)}(0\le k\le \min(n,c)),c=2j+[x\operatorname{and}2^i=2^i] \]

这里,其实就是枚举下一位放多少,注意放了 \(k\) 个数,会导致还能放 \(\min(c-k,n)\) 个点,跟 \(n\)\(\min\) 是因为每次最多就能放 \(n\) 个点,所以我们就少开点,控制第二维不要爆炸。最后答案即为 \(\sum\limits_{i=0}^n f_{0,i}\)

对于异或的限制,只要在枚举 \(k\) 的时候,控制它为奇数或者偶数即可。时间复杂度 \(\mathcal{O}(n^2\log z)\)

#include <cstdio>
#include <cstring>
#include <algorithm>
const int N = 1e3 + 10, mod = 1e9 + 7; typedef long long ll; 
int f[70][N], C[N][N], n; ll l, r, x; 
inline int solve(ll X)
{
	memset(f, 0, sizeof (f)); f[61][0] = 1;
	for (int i = 60; ~i; --i)
		for (int j = 0; j <= n; ++j)
		{
			int lim = 2 * j + ((X >> i) & 1);
			for (int k = ((x >> i) & 1); k <= std::min(lim, n); k += 2)
				(f[i][std::min(lim - k, n)] += (ll)f[i + 1][j] * C[n][k] % mod) %= mod;
		}
	int ans = 0;
	for (int i = 0; i <= n; ++i) (ans += f[0][i]) %= mod;
	return ans;
}
int main()
{
	scanf("%d%lld%lld%lld", &n, &l, &r, &x); C[0][0] = 1;
	for (int i = 1; i <= n; ++i)
	{
		C[i][0] = 1;
		for (int j = 1; j <= i; ++j) 
			C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
	}
	printf("%d\n", (solve(r) - solve(l - 1) + mod) % mod); return 0;
}
posted @ 2022-05-16 10:47  zhiyangfan  阅读(104)  评论(0编辑  收藏  举报