神秘题

Trick

  1. 排列置换题,考虑转化乘环上移动问题。(精灵之环)

  2. 关于异或题目,在二进制上考虑。(New Divide)

  3. \(a_1,a_2,\dots,a_n\) 中选出一个数使得和 \(x\) 按位或/与最大,考虑用高维前缀和维护超集。(New Divide)

  4. 对于往后跳 \(k\in[1,n]\) 步,可以考虑调和级数、bitset、根号分治。([CCPC 2023 北京市赛] 替换)

  5. 图上 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\) 的最大代价,转移就是:

\[f_{u,0}=\min_{(u,v)\in E} f_{v,1}+w(u,v) \\ f_{u,1}=\max_{(u,v)\in E} f_{v,0}+w(u,v) \]

因为有环,所以要放在 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;
}
posted @ 2025-09-16 17:03  咚咚的锵  阅读(11)  评论(0)    收藏  举报