CF Round 769 Div2 题解
A题 ABC (构造)
给定一个长度为 \(n\) 的 01 串,问这个 01 串中是否存在一个长度超过 1 的回文串?
\(n\leq 100\)
-
\(n=1\) 是显然不存在
-
\(n=2\) 时,如果两个字符相同,那么这个串本身就是回文的,反之则不是
-
\(n\geq 3\) 时,我们假设第一个字符为 0,那么第二个字符则必须为 1(反之他俩会构成一个回文串),那么对于第三个字符:
- 若第三个字符为 0,那么前三个字符构成一个回文串(010)
- 若第三个字符为 1,那么第二第三个字符构成一个回文串
第一个字符为 1 时同理,所以 \(n\geq 3\) 时必然存在回文串
#include<bits/stdc++.h>
using namespace std;
int n;
string s;
bool solve() {
cin >> n >> s;
if (n == 2) return s[0] != s[1];
return n == 1;
}
int main()
{
int T;
cin >> T;
while (T--) puts(solve() ? "YES" : "NO");
return 0;
}
B题 Roof Construction (构造)
给定从 0 到 \(n-1\) 这 \(n\) 个数,尝试找到一个排列方法,使得 \(\max\limits_{1\leq i < n}p_i\oplus p_{i+1}\) 最小,并输出方案。
\(2\leq n\leq 2*10^5\)
我们尝试找出最大的 \(x\),使得 \(2^x < n\),便发现可以使得这个结果不超过 \(2^x\):将所有大于等于 \(2^x\) 的数放在 0 的右侧(\(2^x\) 紧贴着 0 放),其余数放在左侧,我们会发现 0 右侧的数的异或值均小于 \(2^x\)(因为他们的最高位都为 1,所以相同为 0,依照异或的运算法则被消去了),而右边也一样(他们是干脆那一位都是 0,所以没得消),仅剩下 0 和 \(2^x\) 的异或值为 \(2^x\)。
我们尝试再压一压,发现依据上面的任意流程,任何违反上述过程的操作都只会使得结果变大或者不变(0 和 大于 \(2^x\) 的数相连自不必说,而 0 和小于 \(2^x\) 的数相邻的话,则会使得至少一个不小于 \(2^x\) 的数和另一个小于 \(2^x\) 的数相邻,结果不会变小)
综上,我们得到构造策略:\([1,2^x),[0],[2^x,n)\) 。
#include<bits/stdc++.h>
using namespace std;
void solve() {
int n;
cin >> n;
//这用的是lowbit的技巧
int x = n - 1, p = 0;
while (x) x -= (p = x & -x);
for (int i = 1; i < p; ++i) printf("%d ", i);
printf("0");
for (int i = p; i < n; ++i) printf(" %d", i);
puts("");
}
int main()
{
int T;
cin >> T;
while (T--) solve();
return 0;
}
C题 Strange Test(数学)
给定正整数 \(a,b\),保证初始状态下 \(a<b\)。
现在,我们可以进行若干次不同类型的操作:让 a 自加 1、让 b 自加 1、或让 \(a=a|b\)。
问至少需要多少次操作,可以使得 \(a=b\) ?
\(1\leq a < b \leq 10^6\)
第三个操作不会减少 a,而且会使得 \(a\geq b\),所以考虑只使用一次或者不用,并且用了之后仅进行操作 2。
如果不进行操作三,那么需要 \(b-a\) 次操作。
进行的话,假设在操作前我们使得 a 变成 x,b 变成 y,之后进行操作三之后再进行操作二,那么有总操作次数
后者是常量,所以我们看看前者怎么搞:
我们直接枚举 \(x\) 的值(有了 \(b-a\) 这个操作上限,所以不用担心枚举范围),然后找到最适合的,使得 \(x|y\) 值最小的 y。
我们在二进制上对 \(x\) 和 \(b\) 从高位到低位逐位匹配,找到第一个 \(x\) 上为 1,\(b\) 上为 0 的地方,这意味着我们要让 \(b\) 加到这一位为 1,后面的位均为 0,也就是我们所得到的 \(y\)。
#include<bits/stdc++.h>
using namespace std;
inline int d(int x, int k) { return (x >> k) & 1; }
int calc(int a, int b) {
int res = 0;
for (int k = 30; k >= 0; k--)
if (d(b, k)) res += 1 << k;
else if (d(a, k)) {
res += 1 << k;
break;
}
return res;
}
int solve() {
int a, b;
cin >> a >> b;
int res = b - a;
for (int x = a; x < b; ++x)
res = min(res, x + (x | calc(x, b)) + (1 - a - b));
return res;
}
int main()
{
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}
D题 New Year Concert(数据结构,数学,二分)
对于一个长度为 \(k\) 的数列 \(\{b_n\}\),倘若存在 \(1\leq l\leq r\leq k\) 且 \(\gcd(b_l,b_{l+1},\cdots,b_r)=r-l+1\),那么这个数列就是不完美的。
我们可以进行操作,每次选择数列的某一位,将其修改为另一个正整数,最后使得这个数列变得完美,操作次数被称为修改数列的代价。
现在给定一个长度为 \(n\) 的数组 \(\{a_n\}\),问对于这 \(n\) 个前缀数组(\([a_1],[a_1,a_2],\cdots,[a_1,a_2,\cdots,a_n]\)),他们的代价分别是多少?
\(n\leq 2*10^5,1\leq a_i\leq 10^9\)
区间 gcd,我们可以用线段树来搞定(只用区间查询,连修改都不用)
想一想,如果我们选择修改某个数,我们肯定会将其改为某个大质数(例如 \(10^9+7\)),因为这样可以使得包含这个数的区间不可能满足上面的等式(区间长度为 1 时,gcd 为 \(10^9+7\),反之为 1)。
基于此,我们想到了一个类似双指针的策略:起初让 \(L=1\),随后尽量向右移动 \(R\),当存在 \(L\leq l\leq R\) 且 \(gcd([l,R])=R-l+1\) 时给位置 \(R\) 打上 vis 标记,然后让 \(L=R+1\),周而复始。如果 \(l\) 是暴力枚举的,那么总复杂度为 \(O(n^2\log n)\)。
我们研究一下 \(gcd([l,R])=R-l+1\) 这个等式,发现一个性质:当 R 固定,l 不断左移的时候,等式右边不断增加(这个是显然的),而等式左边则不会增加、或者变小(对于 gcd 操作,加入新的数不可能使得结果变大,只会变小或者不变)。
这个性质在第一眼似乎没啥用处,不过如果我们换个说法,转换一下,那这个性质就比较显然了:若 \(gcd([x,R])=R-x+1\),那么对于任意 \(L\leq i< x\),有 \(gcd([i,R])<R-i+1\);\(x< i\leq R\) 时,则有 \(gcd([i,R])>R-i+1\)。
我们在 \([L,R]\) 上二分第一个符合 \(\gcd([p,R])\leq R-p+1\) 的位置 \(p\),倘若找不到或者 \(\gcd([p,R])< R-p+1\),则说明没有 l 满足这个等式;相反,则给这个位置打上标记即可。
双指针的整体框架为 \(O(n)\),线段树和二分的复杂度均为 \(O(\log n)\),所以总复杂度为 \(O(n\log^2n)\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int gcd(int a, int b) {
if (!b) return a;
return gcd(b, a % b);
}
int n, a[N];
namespace SegmentTree {
struct Node {
int l, r, val;
} a[N << 2];
int mp[N];
inline int ls(int x) { return x << 1; }
inline int rs(int x) { return x << 1 | 1; }
inline void pushup(int x) {
a[x].val = gcd(a[ls(x)].val, a[rs(x)].val);
}
void build(int x, int l, int r, int *arr) {
a[x].l = l, a[x].r = r;
if (l == r) {
a[x].val = arr[l];
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid, arr);
build(rs(x), mid + 1, r, arr);
pushup(x);
}
int query(int x, int l, int r) {
if (l <= a[x].l && a[x].r <= r)
return a[x].val;
int mid = (a[x].l + a[x].r) >> 1;
if (l <= mid && mid < r)
return gcd(query(ls(x), l, r), query(rs(x), l, r));
if (l <= mid) return query(ls(x), l, r);
if (r > mid) return query(rs(x), l, r);
return -1;
}
}
bool BinartSearch(int L, int R) {
int l = L - 1, r = R;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (SegmentTree::query(1, mid, R) <= R - mid + 1) l = mid;
else r = mid - 1;
}
return (l == L - 1 || SegmentTree::query(1, l, R) != R - l + 1);
}
int vis[N];
int main()
{
//read
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
//solve
SegmentTree::build(1, 1, n, a);
int L = 1, R;
while (L <= n) {
for (R = L; R <= n; ++R)
if (!BinartSearch(L, R)) {
vis[R] = 1;
break;
}
L = R + 1;
}
int res = 0;
for (int i = 1; i <= n; ++i)
printf("%d ", res += vis[i]);
return 0;
}