CF1689E & CF1685C & CF1658F & CF1659E
介绍一些神奇的性质题
CF1689E:
给定一张 n 个点构成的图,每个点有一个非负整数的权值 a1,a2,⋯,an。
i,j 有边当且仅当 ai&aj>0。
每次可以进行以下两种操作之一:
- 选择一个元素 ai 并将它加 1
- 选择一个元素 ai 并将它减 1(只有 ai>0 时才可执行此操作)
可以证明存在一个有穷的操作序列使得这张图连通,求出最少的操作次数。
显然先将所有的 0 变成 1。
然后发现剩下的操作次数不大于 2,一种具体的方式如下:
- 考虑所有 lowbit 最高的数
- 如果只有一个,将它减一即可
- 否则随便找一个加一,再随便找一个减一
于是枚举所有一步操作的情况一一检验即可,时间复杂度为 O(n2logn)。
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≤n≤105
令左括号为 +1,右括号为 −1,需要使得最终序列前缀和都非负。
结论:任意括号串只需要翻转不超过 2 个区间。
证明:令 ai 表示将括号看成 +1,−1 之后的 [1,i] 的前缀和,则找到 ax 值最大的任意一个 x,翻转 [1,x],[x+1,2n] 显然合法。这个画个图自己看看就明白了。
不需要翻转的情况判一下就好了,问题转到了是否存在只翻转一个区间的方案。
令 l,r 是最前和最后一个前缀和 <0 的位置,那么最终翻转的区间 [L,R] 一定要满足 L≤l,R>r,最终 [1,L),(R,2n] 的前缀和没有改变,考虑 i∈[L,R] 的前缀和的改变,记 bi 是新的前缀和,bi=aL−1+aR−ai′,其中 i′ 是 i 翻转前的位置。
那么贪心的选择 L≤l,R>r 且 aL−1,aR 最大的 [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 串的可爱度为 串中1的个数串长度。
现在有一个长度为 n 的 01 串 s,要从中选取若干个不相交的子串,要求:
- 所有子串长度加起来为 m。
- 所有子串拼起来后,可爱度和 s 的可爱度一样。
现在要求你给出一个合法选取方案,并保证方案的子串个数最少。输出你的方案的子串个数,并输出每个子串的起始结束位置。
记字符串有 A 个 0 和 B 个 1,那么将一个 0 的价值记为 −B,1 的价值记为 A。那么价值和为 0 的子串的可爱度与整个串的可爱度是一样的。
结论:把最后一个字符和第一个字符连起来,形成一个环,环上一定存在一个长度为 m 的区间的价值和为 0。
证明:如果每个区间价值和都 >0 或每个区间价值和都 <0,整个串的价值和不可能为 0。如果存在一个 >0 的区间和 <0 的区间,因为相邻区间中字符 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 条边的无向简单连通图,边有边权。
我们定义一条途径(即可以重复经过同一个节点或同一条边的路径)的权值如下:
- 设该途径按顺序经过的边的权值为 w1,w2,w3,⋯。
则该途径的权值为 mex({w1,w1&w2,w1&w2&w3,⋯})。
其中 & 表示按位与运算,mex(S) 表示 S 中未出现过的最小自然数。
给定 q 次询问,每次给定两个不同的整数 u,v,求所有从节点 u 开始到节点 v 结束的途径中,途径权值的最小值。
观察样例发现答案只有 0,1,2,那么是否是这样呢?
的确如此
证明:假设答案 >2,则说明至少出现了 0,1,2,因为权值是 &,即单调不增,那么肯定出现 2,但是发现 2 怎么 & 都不会变成 1。假设不成立。
判断答案能否为 0:
对每一位开一个并查集,维护图的连通性,判断两点是否在其中一张图中连通即可。
判断答案能否为 1:
则此时一定存在一个位置满足前面的权值都 >1,且后面的权值都是 0。
换句话说,只要在走某条边之前的与和大于 1,走之后与和为 0 就可以了,然后接下来怎么走都可以,所以这样答案与终点无关。
那么怎么得到这样一条路径呢?
显然我们需要选定一位 i(不能是二进制下最低的一位),然后从出发点走遍所有边权二进制这一位为 1 的边,然后我们只需要找是否存在一条边能够让与和变成 0。
具体实现的话需要利用前面建立的并查集,并且记录每一个点所有的出边的边权的与和 fi,然后算出在同一个联通块里面的点 fi 的与和,如果是 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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通