CF 杂题
这是一道交互题。有一个包含 \(n\) 个节点的 DAG,保证:
- 所有边都是从编号大的点连向编号小的点;
- 图的最小链覆盖不超过 \(2\);
你初始只知道图的点数 \(n\)。你需要向交互器提出不超过 \(2n\) 个询问,每次你可以询问图上 \(i,j\) 两个点,交互器会告诉你 \(j\) 能否到达 \(i\)。然后,你需要给出图的一种黑白染色方案,满足:颜色相同的点分布在同一条链上。
显然 DAG 形如两条链中间任意连了一些边。我开始 naive 了,以为只询问 \((i,i+1)\) 然后二分图染色就行。其实中间的边完全可以扰乱这个过程,使得你染出来的某一侧不能形成链。
考虑扫描 \(1\sim n\) 的点然后动态维护两条链的前缀。考虑加入一个新的点 \(i+1\)。如果加入的点始终只向其中一条链的链尾有连边,那么显然是容易的(上面那个算法也能过)。因为此时你可以保证两条链的链尾之间没有连边;根据题目限制,新加的点一定向两个链尾连至少一条边。
考虑如何处理同时向两个链尾连边的情况。发现如果不考虑后面的点是没法判断当前点应该加到哪条链后面的(第一种算法就假在这里)。因此考虑开第三条链维护这些不确定的点。
此时,再新加一个点,如果它向第三条链的链尾有连边,那么它的情况等价于同时向两个链尾连边,因此追加到第三条链末尾。否则,无论如何它不能合并到第三条链末尾,因此将它追加到原来的两条链之一,然后将第三条链直接拼接在另一条链后面。发现此时仍然满足两条链的末尾之间没有边相连。
发现可以将加入一个点的询问次数压缩在 \(2\) 次以内。
代码
| #include<iostream>
using namespace std;
const int N = 110;
int T, n;
int ans[N];
int s2[N];
int t0, t1, t2;
bool query(int i, int j) {
cout << "? " << i << ' ' << j << endl;
cout.flush();
string tmp;
cin >> tmp;
return tmp == "YES";
}
int main() {
cin >> T;
while(T--) {
cin >> n;
t0 = t1 = t2 = 0;
t0 = 1;
ans[1] = 0;
for(int i = 2; i <= n; i++) {
if(t2 && query(s2[t2], i)) s2[++t2] = i;
else if(t0 && query(t0, i)) {
if(!t2 && t1 && query(t1, i)) {
s2[++t2] = i;
continue;
}
t0 = i;
ans[i] = 0;
if(t2) t1 = s2[t2];
while(t2) ans[s2[t2--]] = 1;
} else {
t1 = i;
ans[i] = 1;
if(t2) t0 = s2[t2];
while(t2) ans[s2[t2--]] = 0;
}
}
while(t2) { ans[s2[t2--]] = 0; }
cout << "! ";
for(int i = 1; i <= n; i++) cout << ans[i] << ' ';
cout << endl;
cout.flush();
}
return 0;
}
|
给定一个长为 \(n\) 的序列 \(a_{1\sim n}\),求 \(\operatorname{mex}\bigl(\{\operatorname{lcm}_{i=l}^{r}a_i\mid 1\le l\le r\le n\}\big)\)。
\(n\le 3\times 10^5\)
注意到固定右端点之后,区间 \(\operatorname{lcm}\) 只有本质不同的 \(\log V\) 个,因此集合大小不超过 \(n\log V\)。考虑如何求出这个集合。
考虑动态维护每个前缀的后缀 \(\operatorname{lcm}\) 集合。发现在加入一个数字之后,只需把原先集合中的数字都和当前数字取 \(\operatorname{lcm}\),再把太大的部分砍掉,时间复杂度就是对的。用 set
维护即可。
代码
| #include<iostream>
#include<set>
#include<algorithm>
using namespace std;
const int N = 3e5 + 10;
const int LIM = 1e7;
int T, n;
int a[N];
set<int> ans, tmp;
long long sta[N], top;
inline long long lcm(int a, int b) {
return (long long)a / __gcd(a, b) * b;
}
int main() {
cin >> T;
while(T--) {
cin >> n;
ans.clear(); tmp.clear();
for(int i = 1; i <= n; i++) cin >> a[i];
tmp.insert(a[1]); ans.insert(a[1]);
for(int i = 2; i <= n; i++) {
for(int j : tmp) sta[++top] = lcm(j, a[i]);
tmp.clear();
while(top) {
if(sta[top] <= LIM) { tmp.insert(sta[top]); ans.insert(sta[top]); }
--top;
}
tmp.insert(a[i]); ans.insert(a[i]);
}
for(int i = 1; i <= LIM; i++) {
if(!ans.count(i)) { cout << i << '\n'; break; }
}
}
return 0;
}
|
给定一个正整数集合 \(A\),你每次可以选择集合中一个数字 \(k\),将它分解为 \(k=xy\),然后将 \(k\) 删掉,\(x,y\) 加入集合。问经过若干次操作,集合的极差最小是多少。
\(n\le 10^6,~a_i\le 5\times 10^6\)
考虑双指针进行枚举,外层枚举集合的下界,然后不断缩紧集合的上界(外层枚举集合的上界显然是没有前途的)。设集合当前的范围为 \([l+1,r]\),考虑 \(l+1\) 转移到 \(l\) 时的情况,那么一些数字可以分解成 \(l\) 和一些别的,情况增加,因此更优。
设 \(f_x\) 表示在当前下界 \(l\) 下,将数字 \(x\) 分解成若干数字,最大值最小是多少。在加入一个 \(l\) 时,我们枚举 \(x=kl,~(k\ge l)\),并进行转移:
\[
f_{kl}=\min(f_{kl},f_{k})
\]
记得更新边界 \(f_l=l\)。这样复杂度形如一个调和级数。过程中动态维护 \(f(a_i)\) 的桶,更新边界之后,根据桶来缩紧上界即可。
代码
| #include<iostream>
#define int long long
using namespace std;
const int N = 1e6 + 10;
const int V = 5e6 + 10;
const int INF = 0x3f3f3f3f;
int T;
int n, m, k;
int a[V], f[V], cnt[V];
int ans;
void clear() {
ans = INF;
for(int i = 0; i <= m + 5; i++) a[i] = cnt[i] = 0;
for(int i = 0; i <= m + 5; i++) f[i] = INF;
st.clear();
k = 0;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> T;
while(T--) {
cin >> n >> m;
clear();
for(int i = 1; i <= n; i++) {
int x;
cin >> x;
if(!a[x]) ++k;
a[x] = 1;
}
for(int i = m, r = m, c = 0; i >= 1; i--) {
f[i] = i;
if(a[i]) ++cnt[i], ++c;
for(int j = i; j * i <= m; j++) {
if(f[j] < f[j * i]) {
if(a[j * i]) --cnt[f[j * i]];
f[j * i] = f[j];
if(a[j * i]) ++cnt[f[j * i]];
}
}
while(r > i && !cnt[r]) --r;
if(c == k) ans = min(ans, r - i);
}
cout << ans << '\n';
}
return 0;
}
|
给定一个长为 \(n\) 的有序二元组序列 \((l_i,r_i),~(i\in [1,n],~l_i\le r_i)\)。考虑一个序列 \(a_{1\sim n}\) 满足 \(a_i\in [l_i,r_i]\)。现在有 \(q\) 次询问,每次询问给定 \([x,y]\),询问 \(\operatorname{OR}_{i=x}^{y}a_i\)(按位或)的最大值。
\(n,q\le 2\times 10^5\)
考察 \(a_i\) 和 \(r_i\) 的最长公共前缀,记其长度为 \(k\)。那么 \(a_i\) 从高往低数的第 \(k+1\) 位为 \(0\),\(r_i\) 从高往低数的第 \(k+1\) 位是 \(1\)。然后从第 \(k+2\) 位往后就都可以取 \(1\) 了。
因此预处理 \(f_{i,j,k}\) 表示区间 \([i,j]\) 中 \(r_i\) 的第 \(k\) 位有多少个 \(1\);\(g_{i,j,k}\) 表示区间 \([i,j]\) 中 \(r_i\) 的第 \(k\) 位有多少个 \(1\) 能变成 \(0\)(也就是在 \(l_i,r_i\) 的公共前缀后面并且值为 \(1\),变成 \(0\) 之后可以把后面全填上 \(1\))。对于每次询问,我们从高位往低位处理,考虑第 \(k\) 位,
- 若 \(f_k=0\),那么该位只可能是 \(0\);
- 若 \(f_k>0\),那么必须优先保证当前位为 \(1\),再去考虑后面的位;
- 若 \(f_k\ge 2\wedge g_k\ge 1\),那么选择一个 \(1\) 变成 \(0\),后面的位都填 \(1\) 就行了,结束贪心;
\(f\) 和 \(g\) 容易在 \(O(n\log V)\) 的时间内预处理出来。因此总时间复杂度为 \(O\big((n+q)\log V\big)\)。
代码
| #include<iostream>
using namespace std;
const int N = 2e5 + 10;
inline int lg(int x) {
int res = -1;
while(x) { ++res; x >>= 1; }
return res;
}
int T, n, q;
int a[N], b[N];
int f[N][30], g[N][30];
void clear() {
for(int i = 0; i <= n + 5; i++) {
for(int j = 0; j < 30; j++) f[i][j] = g[i][j] = 0;
}
}
int main() {
cin >> T;
while(T--) {
cin >> n;
clear();
for(int i = 1; i <= n; i++) cin >> a[i] >> b[i];
for(int i = 1; i <= n; i++) {
int tmp = lg(a[i] ^ b[i]);
for(int j = 0; j < 30; j++) {
f[i][j] = f[i - 1][j] + ((b[i] >> j) & 1);
g[i][j] = g[i - 1][j] + (((b[i] >> j) & 1) && (j <= tmp));
}
}
cin >> q;
while(q--) {
int l, r, ans = 0;
cin >> l >> r;
for(int i = 29; i >= 0; i--) {
if(f[r][i] - f[l - 1][i] == 0) continue;
ans |= (1 << i);
if(f[r][i] - f[l - 1][i] == 1) continue;
if(g[r][i] - g[l - 1][i]) { ans += (1 << i) - 1; break; }
}
cout << ans << ' ';
}
cout << '\n';
}
return 0;
}
|
给定一个长为 \(n\) 的排列,你等概率选择一段区间 \([l,r]\) 然后等概率重排这段区间内的数。问得到的新序列的逆序对的期望个数。
\[
n\le 10^5
\]
注意到横跨区间内外的逆序对不会被影响。并且等概率随机重排区间 \([l,r]\) 后,\([l,r]\) 内的期望逆序对数量就是长为 \(r-l+1\) 的随机排列的期望逆序对数。因此问题转化为选择一段区间 \([l,r]\),\([l,r]\) 内的逆序对数量的期望是多少。期望可以转换为求和。
考虑从大到小扫描值域,发现一个数字 \(a_i\) 的贡献等于它左边已经被加进来的点的下标之和,乘以 \(n-i+1\)。于是直接做做完了。
代码
| #include<iostream>
#include<iomanip>
#define int long long
#define ld long double
using namespace std;
const int N = 1e5 + 10;
int n;
ld ans;
int a[N], ia[N];
namespace BIT {
int sum[N];
inline void clear() {
for(int i = 0; i <= n + 5; i++) sum[i] = 0;
}
inline void add(int p, int v) {
for(int i = p; i <= n; i += i & -i) sum[i] += v;
}
inline int query(int p) {
int res = 0;
for(int i = p; i > 0; i -= i & -i) res += sum[i];
return res;
}
}
signed main() {
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++) ia[a[i]] = i;
for(int i = 1; i <= n; i++) {
ans += (ld)(i * (i - 1) / 2) * (n - i + 1) / 2;
}
for(int i = n; i >= 1; i--) {
int p = ia[i];
ans -= (ld)BIT::query(p) * (n - p + 1);
BIT::add(p, p);
}
ans /= n * (n + 1) / 2;
BIT::clear();
for(int i = n; i >= 1; i--) {
ans += BIT::query(a[i]);
BIT::add(a[i], 1);
}
cout << fixed << setprecision(12) << ans << '\n';
return 0;
}
|
bitset
高维偏序板子。用扫描线可以做到 \(O(nm+\frac{n^2m}{\omega})\)。