神秘题
Trick
-
排列置换题,考虑转化乘环上移动问题。(精灵之环)
-
关于异或题目,在二进制上考虑。(New Divide)
-
在 \(a_1,a_2,\dots,a_n\) 中选出一个数使得和 \(x\) 按位或/与最大,考虑用高维前缀和维护超集。(New Divide)
-
对于往后跳 \(k\in[1,n]\) 步,可以考虑调和级数、
bitset
、根号分治。([CCPC 2023 北京市赛] 替换) -
图上 dp,考虑用最短路去转移。(Game on Graph)
题目
精灵之环
中文题面:
假设知道排列 \(p\)。
那么把这个排列 \(p\) 的环连出来,环上点的编号是排列的下标,点的值是编号对应的值。
就比如排列 4 1 2 3
的环为:
val: 4 1 2 3 4
id : 1->2->3->4->1...
可以发现把这些环上的值移动一次会使得环上点的编号和值相等,此时就对应排列 \(1,2,3,4,\dots,n\),也就是 \(p_0\)。
然后考虑把 \(p_k\) 的环给连出来,现在对于 \(p_k\) 的一个长度为 \(len\) 的环,如果有 \(\gcd(len,k)=1\),那么就可以把这个环上的值移动 \(k\) 次变成编号与值对应相等的环。
如果不满足 \(\gcd(len,k)=1\),那么可以考虑把若干个长度为 \(len\) 的环拼起来,使得 \(\gcd(len\times t,k)=t\)(\(t\) 为环的个数),这样也可以使得把这个环上的值移动 \(k\) 次变成编号与值对应相等的环。
所以对于每种长度的环,找到一个最小的 \(t\) 使得 \(\gcd(len\times t,k)=t\),然后把这些环每 \(t\) 个拼一起即可,因为题目满足有解,所以一定能够找到这个 \(t\)。
知道了所有环,就可以求出 \(p\) 了。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
}
using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n, k;
int q[N], p[N];
vector< int> G[N];
vector< int> cle[N], ans[N];
vector< vector< int> > vec[N], V;
int vis[N], mp[N], cnt;
void dfs( int u, int id) {
if (vis[u]) return ;
vis[u] = 1, cle[id].push_back(u);
for ( auto v : G[u]) dfs(v, id);
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> k;
for ( int i = 1; i <= n; i ++)
cin >> p[i];
for ( int i = 1; i <= n; i ++) G[p[i]].push_back(p[p[i]]);
for ( int i = 1; i <= n; i ++)
if (! vis[i]) dfs(i, ++ cnt);
for ( int i = 1; i <= cnt; i ++) {
int len = (int)cle[i].size();
vec[len].push_back(cle[i]);
mp[len] = 1;
}
int tot = 0;
for ( int len = 1; len <= n; len ++) {
if (! mp[len]) continue ;
for ( auto v : vec[len]) {
V.push_back(v);
int siz = (int)V.size() * len;
if (__gcd(k, siz) == (int)V.size()) { // 此时 V.size() 就是 t
++ tot;
ans[tot].resize(siz);
for ( int i = 0; i < siz; i ++)
vis[i] = 0;
// 构造环
for ( auto u : V) {
int id = 0;
for ( int i = 0; i < siz; i ++)
if (! vis[i]) {
id = i;
break ;
}
for ( auto c : u)
ans[tot][id] = c, vis[id] = 1,
id = (id + k) % siz;
}
cnt = 0, V.clear();
}
}
}
// 最后在环上面移动一次就是 p(这里默认了环上的点编号与值相等,也就是 p_0 对应的环)
for ( int i = 1; i <= tot; i ++) {
int len = (int)ans[i].size();
for ( int j = 0; j < len - 1; j ++)
q[ans[i][j]] = ans[i][j + 1];
q[ans[i][len - 1]] = ans[i][0];
}
for ( int i = 1; i <= n; i ++) cout << q[i] << ' ';
cout << '\n';
return 0;
}
New Divide
中文题面:
设 \(pre_i\) 表示 \([1,i]\) 的异或和。
那么转化一下就是对每个前缀 \([1,i]\) 找一个 \(k\),使得 \(pre_k+(pre_i \oplus pre_k)\) 的值最大。
直接求是困难的,考虑放在二进制下观察。
pre_i: 1 1 0 0 ...
pre_k: 1 0 1 0 ...
sum : 1 1 2 0 ...
可以发现,如果 \(pre_i\) 二进制下某一位为 \(1\),那么不论 \(pre_k\) 这一位是什么,贡献都是 \(1\);如果 \(pre_i\) 某一位为 \(0\),那么 \(pre_k\) 这一位为 \(1\),贡献为 \(2\),否则贡献为 \(0\)。
所以只用最大化 \(pre_i\) 为 \(0\),\(pre_k\) 为 \(1\) 的位数。
还是不好求,但是发现如果把 \(pre_i\) 按位取反,就可以转化成 \(pre_i\) 与 \(pre_k\) 的按位与最大。(其实也可以不取反,不取反就是求按位或最大,这两个是等价的。)
现在问题变成了,给定一个 \(pre_i\) 记它取反后为 \(val\),要求在 \(pre_1,pre_2,\dots,pre_{i}\) 中选出一个数 \(x\),使得 \(x\vee val\) 最大。
这看上去可以用 Trie 求,但其实不行,因为当 \(val\) 的某一位为 \(0\) 时,走 \(0\) 或 \(1\) 的出边是不确定的。
还是考虑从高到低枚举 \(val\) 的每一位去贪,如果这一位为 \(1\),那么肯定是想在 \(pre_1,pre_2,\dots,pre_{i}\) 选一个这位同样为 \(1\) 的出来,这启发可以维护一个变量 \(now\),表示当前 \(x\) 的值是多少。
具体来说,如果 \(val\) 第 \(i\) 位为 \(1\),那么就看 \(pre_1,pre_2,\dots,pre_{i}\) 中是否有 \(now+2^i\) 的超集,若有就可以让 \(x\) 的第 \(i\) 位取 \(1\)。
这里就可以用高维前缀和了,\(mi_S\) 表示 \(S\) 的超集中编号最小的一个,初始 \(mi_{pre_i}=\min i\)。
只要满足 \(mi_{now+2^i}\le\) 当前前缀的编号,就可以让 \(x\) 第 \(i\) 位取 \(1\)。
最后算出答案即可。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = (1 << 20), M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n;
int a[N];
int mi[N];
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
memset(mi, 127, sizeof mi);
mi[0] = 0;
cin >> n;
for ( int i = 1; i <= n; i ++) cin >> a[i], a[i] ^= a[i - 1], mi[a[i]] = min(mi[a[i]], i);
for ( int i = 0; i < 20; i ++)
for ( int S = 0; S < N; S ++)
if (! (S & (1 << i))) mi[S] = min(mi[S], mi[S | (1 << i)]);
for ( int i = 1; i <= n; i ++) {
int val = (N - 1) ^ a[i], now = 0;
for ( int j = 19; j >= 0; j --)
if ((val & (1 << j)) && mi[now | (1 << j)] <= i) now |= (1 << j);
int res = ((val & a[mi[now]]) << 1) + a[i];
cout << res << ' ';
}
return 0;
}
[CCPC 2023 北京市赛] 替换
这个操作,看起来可以用 bitset
优化。
那么用三个 bitset
分别维护:初始 \(1\) 的位置、初始 \(?\) 的位置、最终是 \(1\) 的位置。
这样做,复杂度 \(O(\frac{n^2\ln n}{\omega})\)。
可以发现,在 \(k\) 很小的时候,用 bitset
做是很亏的,还不如直接暴力。
所以考虑根号分治,在 \(k\le \sqrt n\) 的时候,可以直接跑暴力,在 \(\gt \sqrt n\) 时,直接用 bitset
。
复杂度是 \(O(n\sqrt n+\frac{n^2\ln{\sqrt n}}{\omega})\),吸个氧直接过。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n;
char s[N];
bitset< N> ans, sum, cnt;
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
cin >> (s + 1);
int blk = sqrt(n);
for ( int i = 1; i <= n; i ++)
if (s[i] == '1') sum[i] = 1;
else if (s[i] == '?') cnt[i] = 1;
for ( int k = 1; k <= n; k ++) {
ans = sum;
if (k <= blk) {
for ( int i = k + 1; i <= n; i ++)
if (s[i] == '?' && ans[i - k]) ans[i] = 1;
} else {
for ( int i = k; i <= n; i += k)
ans |= ((ans << k) & cnt);
}
cout << ans.count() << '\n';
}
return 0;
}
Game on Graph
考虑倒着做,考虑从出度为 \(0\) 的所有点到起始点。
那么建反向边,就成了从入度为 \(0\) 的点到起始点。
设 \(f_{i,0}\) 表示先手到点 \(i\) 的最小代价,\(f_{i,1}\) 表示后手到点 \(i\) 的最大代价,转移就是:
因为有环,所以要放在 dij 上转移。
考虑把 \(f_{u,0}\) 的值当作主元进行转移,这样每次从堆里取出的 \(f_{u,0}\) 都是最小值,就可以直接拿去更新 \(f_{u,1}\)。
但 \(f_{u,1}\) 是辅助元,为了让 \(f_{u,1}\) 取到最大值,就必须让它入边的值全部把它更新后,才能入堆。
后续做到这种题要注意一下 dij 转移时的主元与辅助元之分。
代码
#include <bits/stdc++.h>
#define int long long
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e18, mod = 998244353;
int n, m, V;
priority_queue< tuple< int, int, int> > q;
vector< pair< int, int> > G[N];
int in[N];
int f[N][2];
int vis[N][2];
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m >> V;
for ( int i = 1; i <= m; i ++) {
int u, v, w; cin >> u >> v >> w;
G[v].push_back({u, w});
in[u] ++;
}
for ( int i = 1; i <= n; i ++) {
if (! in[i]) q.push({0, i, 0}), q.push({0, i, 1});
else f[i][0] = inf;
}
while (q.size()) {
int u = get<1>(q.top()), op = get<2>(q.top()); q.pop();
if (vis[u][op]) continue ;
vis[u][op] = 1;
for ( auto [v, w] : G[u]) {
if (op) {
if (f[v][0] > f[u][1] + w)
f[v][0] = f[u][1] + w, q.push({-f[v][0], v, 0});
} else {
f[v][1] = max(f[v][1], f[u][0] + w);
if (! -- in[v]) q.push({-f[v][1], v, 1});
}
}
}
if (f[V][0] == inf) cout << "INFINITY\n";
else cout << f[V][0] << '\n';
return 0;
}