CF1689E & CF1685C & CF1658F & CF1659E

介绍一些神奇的性质题

CF1689E:

给定一张 \(n\) 个点构成的图,每个点有一个非负整数的权值 \(a_1,a_2,\cdots,a_n\)
\(i,j\) 有边当且仅当 \(a_i\&a_j\gt0\)
每次可以进行以下两种操作之一:

  • 选择一个元素 \(a_i\) 并将它加 \(1\)
  • 选择一个元素 \(a_i\) 并将它减 \(1\)(只有 \(a_i\gt0\) 时才可执行此操作)

可以证明存在一个有穷的操作序列使得这张图连通,求出最少的操作次数。

显然先将所有的 \(0\) 变成 \(1\)

然后发现剩下的操作次数不大于 \(2\),一种具体的方式如下:

  • 考虑所有 \(\text{lowbit}\) 最高的数
  • 如果只有一个,将它减一即可
  • 否则随便找一个加一,再随便找一个减一

于是枚举所有一步操作的情况一一检验即可,时间复杂度为 \(\mathcal O(n^2\log n)\)

Code:

#include <bits/stdc++.h>
using namespace std;
const int N = 2005;
int T;
int n, a[N], ans;
int x;
int tot;
bool vis[N]; int fa[N];

int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }

bool check() {
	for (int i = 1; i <= n; ++i) fa[i] = i;
	for (int k = 0; k < 30; ++k) {
		int p = n;
		for (int i = 1; p == n && i < n; ++i) if (a[i] >> k & 1) p = i;
		int Fa = find(p);
		for (int i = p + 1; i <= n; ++i) if (a[i] >> k & 1) fa[find(i)] = Fa;
	}
	for (int i = 2; i <= n; ++i) if (find(i) != find(1)) return false;
	return true;
}

void dfs(int u) {
	vis[u] = 1, ++tot;
	for (int v = 1; v <= n; ++v)
		if (!vis[v] && (a[u] & a[v])) dfs(v);
}

void solve() {
	scanf("%d", &n), ans = tot = x = 0; memset(vis + 1, 0, sizeof (bool) * n);
	for (int i = 1; i <= n; ++i) {
		scanf("%d", &a[i]);
		if (!a[i]) a[i] = 1, ++ans;
		x = max(x, a[i] & -a[i]);
	}
	dfs(1);
	if (tot == n) {
		printf("%d\n", ans);
		for (int i = 1; i <= n; ++i) printf("%d ", a[i]);
		printf("\n");
		return;
	}
	bool flag = 0;
	for (int i = 1; i <= n; ++i) {
		--a[i];
		if (check()) { flag = 1; break; }
		a[i] += 2;
		if (check()) { flag = 1; break; }
		--a[i];
	}
	if (flag) {
		printf("%d\n", ans + 1);
		for (int i = 1; i <= n; ++i) printf("%d ", a[i]);
		printf("\n");
		return;
	}
	flag = 0;
	for (int i = 1; i <= n; ++i) if ((a[i] & -a[i]) == x) {
		if (!flag) {
			--a[i], flag = 1;
		}
		else {
			++a[i]; break;
		}
	}
	printf("%d\n", ans + 2);
	for (int i = 1; i <= n; ++i) printf("%d ", a[i]);
	printf("\n"); 
}

int main() {
	scanf("%d", &T);
	while (T--) solve();
	return 0;
}

CF1658C:

给定一个由 \(n\)(\(n\)) 组成的括号串,求出最少翻转多少个区间可以使得整个括号串合法,并构造一组方案。\(1\le n\le10^5\)

令左括号为 \(+1\),右括号为 \(-1\),需要使得最终序列前缀和都非负。
结论:任意括号串只需要翻转不超过 \(2\) 个区间。
证明:令 \(a_i\) 表示将括号看成 \(+1,-1\) 之后的 \([1,i]\) 的前缀和,则找到 \(a_x\) 值最大的任意一个 \(x\),翻转 \([1,x],[x+1,2n]\) 显然合法。这个画个图自己看看就明白了。
不需要翻转的情况判一下就好了,问题转到了是否存在只翻转一个区间的方案。
\(l,r\) 是最前和最后一个前缀和 \(\lt0\) 的位置,那么最终翻转的区间 \([L,R]\) 一定要满足 \(L\le l,R\gt r\),最终 \([1,L),(R,2n]\) 的前缀和没有改变,考虑 \(i\in[L,R]\) 的前缀和的改变,记 \(b_i\) 是新的前缀和,\(b_i=a_{L-1}+a_R-a_{i^{′}}\),其中 \(i^{′}\)\(i\) 翻转前的位置。
那么贪心的选择 \(L\le l,R\gt r\)\(a_{L-1},a_R\) 最大的 \([L,R]\) 检验即可。

Code:

#include <bits/stdc++.h>
using namespace std;
const int N = 200005;
int T;
int n; char s[N], t[N];
int a[N], b[N];

void solve() {
	scanf("%d%s", &n, s + 1), n *= 2;
	for (int i = 1; i <= n; ++i) a[i] = a[i - 1] + (s[i] == '(' ? 1 : -1);
	int L = 1, R = n;
	while (L <= n && a[L] >= 0) ++L; if (L == n + 1) return printf("%d\n", 0), void();
	while (R > 0 && a[R] >= 0) --R;
	for (int i = L; i; --i) a[i] > a[L - 1] && (L = i + 1);
	for (int i = R; i <= n; ++i) a[i] > a[R] && (R = i);
	for (int i = 1; i <= n; ++i) t[i] = s[i];
	reverse(t + L, t + R + 1);
	int p = 1;
	for (int i = 2; i <= n; ++i) a[i] > a[p] && (p = i);
	for (int i = 1; i <= n; ++i) b[i] = b[i - 1] + (t[i] == '(' ? 1 : -1);
	for (int i = 1; i <= n; ++i) if (b[i] < 0) { return printf("%d\n%d %d\n%d %d\n", 2, 1, p, p + 1, n), void(); }
	printf("%d\n%d %d\n", 1, L, R);
}

int main() {
	scanf("%d", &T);
	while (T--) solve();
	return 0;
}

CF1658F:

定义一个 01 串的可爱度为 \(\frac{串中1的个数}{串长度}\)
现在有一个长度为 \(n\) 的 01 串 \(s\),要从中选取若干个不相交的子串,要求:

  • 所有子串长度加起来为 \(m\)
  • 所有子串拼起来后,可爱度和 \(s\) 的可爱度一样。

现在要求你给出一个合法选取方案,并保证方案的子串个数最少。输出你的方案的子串个数,并输出每个子串的起始结束位置。

记字符串有 \(A\)\(\texttt{0}\)\(B\)\(\texttt{1}\),那么将一个 \(\texttt{0}\) 的价值记为 \(-B\)\(\texttt{1}\) 的价值记为 \(A\)。那么价值和为 \(0\) 的子串的可爱度与整个串的可爱度是一样的。
结论:把最后一个字符和第一个字符连起来,形成一个环,环上一定存在一个长度为 \(m\) 的区间的价值和为 \(0\)
证明:如果每个区间价值和都 \(\gt0\) 或每个区间价值和都 \(\lt0\),整个串的价值和不可能为 \(0\)。如果存在一个 \(\gt0\) 的区间和 \(\lt0\) 的区间,因为相邻区间中字符 \(\texttt{1}\) 的数量的变化量不超过 \(1\),所以中间总存在一个价值和为 \(0\) 的区间。
于是答案不超过 \(2\),检查是否能为 \(1\) 即可。
别忘了特判串里要有小数个 \(1\) 的情况,这一定不合法。

Code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 200005;
int T;
int n, m, cnt;
int a[N*2];
char s[N];

void solve() {
	scanf("%d%d", &n, &m);
	scanf("%s", s + 1);
	cnt = 0;
	for (int i = 1; i <= n; ++i) cnt += (s[i] == '1');
	if ((1ll * m * cnt) % n) return printf("%d\n", -1), void();
	for (int i = 1; i <= n; ++i) a[i + n] = a[i] = s[i] - '0';
	for (int i = 1; i <= n * 2; ++i) a[i] = a[i] * n - cnt;
	for (int i = 1; i <= n * 2; ++i) a[i] += a[i - 1];
	int l = 0, r = 0;
	for (int i = m; i <= n * 2; ++i) {
		if (a[i - m] == a[i]) {
			l = i - m + 1, r = i;
			break;
		}
	}
	if (r <= n) printf("%d\n%d %d\n", 1, l, r);
	else printf("%d\n%d %d\n%d %d\n", 2, 1, r - n, l, n);
}

int main() {
	scanf("%d", &T);
	while (T--) solve();
	return 0;
}

CF1659E:

给定一个 \(n\) 个节点 \(m\) 条边的无向简单连通图,边有边权。
我们定义一条途径(即可以重复经过同一个节点或同一条边的路径)的权值如下:

  • 设该途径按顺序经过的边的权值为 \(w_1,w_2,w_3,\cdots\)
    则该途径的权值为 \(\text{mex}(\{w_1,w_1\&w_2,w_1\&w_2\&w_3,\cdots\})\)
    其中 \(\&\) 表示按位与运算,\(\text{mex}(S)\) 表示 \(S\) 中未出现过的最小自然数。

给定 \(q\) 次询问,每次给定两个不同的整数 \(u,v\),求所有从节点 \(u\) 开始到节点 \(v\) 结束的途径中,途径权值的最小值。

观察样例发现答案只有 \(0,1,2\),那么是否是这样呢?
的确如此
证明:假设答案 \(\gt2\),则说明至少出现了 \(0,1,2\),因为权值是 \(\&\),即单调不增,那么肯定出现 \(2\),但是发现 \(2\) 怎么 \(\&\) 都不会变成 \(1\)。假设不成立。
判断答案能否为 \(0\)
对每一位开一个并查集,维护图的连通性,判断两点是否在其中一张图中连通即可。
判断答案能否为 \(1\)
则此时一定存在一个位置满足前面的权值都 \(\gt1\),且后面的权值都是 \(0\)
换句话说,只要在走某条边之前的与和大于 \(1\),走之后与和为 \(0\) 就可以了,然后接下来怎么走都可以,所以这样答案与终点无关。
那么怎么得到这样一条路径呢?
显然我们需要选定一位 \(i\)(不能是二进制下最低的一位),然后从出发点走遍所有边权二进制这一位为 \(1\) 的边,然后我们只需要找是否存在一条边能够让与和变成 \(0\)
具体实现的话需要利用前面建立的并查集,并且记录每一个点所有的出边的边权的与和 \(f_i\),然后算出在同一个联通块里面的点 \(f_i\) 的与和,如果是 \(0\) 代表这一个联通块内的点作为出发点可以做到答案为 \(1\)
如果不是 \(0\) 也不是 \(1\),那就是 \(2\)

Code:

#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, m;
int f[N], g[N];
int fa[30][N];
bool flag[N];

int find(int x, int k) {
	if (x == fa[k][x]) return x;
	return fa[k][x] = find(fa[k][x], k);
}

void merge(int u, int v, int k) {
	int fu = find(u, k), fv = find(v, k);
	if (fu == fv) return;
	fa[k][fu] = fv;
}

bool check(int u, int v) {
	for (int i = 0; i < 30; ++i) if (find(u, i) == find(v, i)) return true;
	return false;
}

int main() {
	ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
	cin >> n >> m; for (int i = 1; i <= n; ++i) f[i] = (1 << 30) - 1; for (int i = 0; i < 30; ++i) for (int j = 1; j <= n; ++j) fa[i][j] = j;
	for (int i = 1, u, v, w; i <= m; ++i) {
		cin >> u >> v >> w;
		f[u] &= w, f[v] &= w;
		for (int j = 0; j < 30; ++j) if (w >> j & 1) {
			merge(u, v, j);
		}
	}
	for (int k = 1; k < 30; ++k) {
		for (int i = 1; i <= n; ++i) g[i] = (1 << 30) - 1;
		for (int i = 1; i <= n; ++i) g[find(i, k)] &= f[i];
		for (int i = 1; i <= n; ++i) if (g[find(i, k)] == 0) flag[i] = 1;
	}
	int Q; cin >> Q;
	while (Q--) {
		int u, v; cin >> u >> v;
		if (check(u, v)) cout << 0 << '\n';
		else if (flag[u]) cout << 1 << '\n';
		else cout << 2 << '\n';
	}
	return 0;
}
posted @ 2022-10-02 06:53  Kobe303  阅读(46)  评论(0编辑  收藏  举报