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;
}