神秘题

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. 异或有关的题目,考虑在二进制上找性质。(绝望)

  6. \(O(2^nX)\) 的二进制题目过不去,考虑折半优化成 \(O(2^{\frac{n}{2}}X)\)。(高维前缀和)

  7. 优化 dp,可以考虑 dp 的本质(比如放在网格图上考虑之类的)。(双扩展序列)

  8. 与在序列上跳 \(k\) 步有关的东西,可以考虑倍增。(跳越)

  9. 某些贪心题,可以优先考虑满足更紧的限制。(CF1251E2 Voting (Hard Version))

  10. 若每次查寻的位置固定,那么可以值维护会被用到的位置,减少复杂度。(#P1356. 食物)

题目

精灵之环

中文题面:

假设知道排列 \(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;
}

绝望

\(high(x)\) 表示 \(x\) 二进制上最高的 \(1\) 所在位。

首先,要让 \(x\oplus y\gt x\),那么要满足 \(x\) 的第 \(high(y)\) 位是 \(0\)

所以可以把那些不在任何一个 \(high(a_i)\) 的位给扔掉,不考虑它们。

接着分析,又可以发现在最优情况下要使得 \(x\oplus y\gt x\)\(y\) 满足除了 \(high(y)\),其它位上的 \(1\)\(x\) 这一位也必须位 \(1\)

考虑证明,如果 \(x\) 有一位是 \(0\),那么可以找到一个 \(z\) 满足 \(high(z)\) 在这一位。(一定可以找到,因为已经把没用的位给去掉了)

然后就可以这样操作:\(x\to (x\oplus z)\to (x\oplus z \oplus y)\to (x\oplus z\ oplus y\oplus z)\),最后也就等价于 \(x\oplus y\),但是多做了几次操作。

证毕。

得到这个后,就可以转化问题了。

每次操作一个 \(a_i\),就相当于把此时的数 \(now\) 的第 \(high(a_i)\) 位的 \(0\) 变成 \(1\),前面的一些 \(1\) 变成 \(0\)

考虑 dp,记 \(f_k\) 表示最多操作多少次使得第 \(k\) 位的 \(0\) 变成 \(1\)

转移就是:

\[f_k\max_{high(a_i)=k}(1+\sum_{k'\lt k,a_i 的第 k' 位为 1}f_{k'}) \]

复杂度 \(O(n\log^2 V)\)

代码
#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 = 1e9, mod = 998244353;

int n;
int a[N], hi[N], f[N];

int vis[N];

void solve() {
    cin >> n;
    for ( int k = 1; k <= 62; k ++) vis[k] = 0, f[k] = 0;

    for ( int i = 1; i <= n; i ++) {
        cin >> a[i], hi[i] = 0;
        int x = a[i];

        while (x) x >>= 1, hi[i] ++;
        vis[hi[i]] = 1;
    }

    int ans = 0;

    for ( int k = 1; k <= 62; k ++) {
        if (! vis[k]) continue ;

        for ( int i = 1; i <= n; i ++) {
            if (hi[i] != k) continue ;
            int res = 0;

            for ( int j = hi[i] - 1; j >= 1; j --)
                if ((a[i] >> (j - 1)) & 1) res += f[j];

            f[k] = max(f[k], 1 + res);
        }

        ans += f[k];
    }

    cout << ans << '\n';
}

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    int T; cin >> T;
    while (T --) solve();

    return 0;
}

高维前缀和

题目纯诈骗。

直接暴力做复杂度 \(O(2^nk)\),感觉优化成 \(O(2^{\frac{n}{2}}k)\) 就可以过。

那么考虑如何折半,首先对于两个数 \(x\)\(y\),那么 \(x\) 的二进制前半段异或上 \(y\) 的二进制前半段的 \(\rm popcount\) 再加上 \(x\) 的二进制后半段异或上 \(y\) 的二进制后半段的 \(\rm popcount\) 肯定是等于 \(x\) 异或 \(y\)\(\rm popcount\) 的。

所以枚举每一个操作,然后枚举 \(0\sim 2^{\frac{n}{2}}-1\) 的所有数 \(x\),。

\(f_{x,c,t}\) 表示满足当前二进制前半段是 \(x\)\(C\) 的后半段是 \(c\)\(D-{\rm popcount}(x\oplus C 的前半段)=t\) 的操作的 \(X\) 的乘积。

这就相当于开了一个桶,在处理答案时可以直接从这个桶得到信息。

对一个数 \(x\) 计算答案时,枚举所有 \(C\) 的后半段,然后把对应的 \(f\) 乘起来即可。

复杂度就是 \(O(2^{\frac{n}{2}}k)\)

代码
#include <bits/stdc++.h>

void Freopen() {
    freopen("popcount.in", "r", stdin);
    freopen("popcount.out", "w", stdout);
}

using namespace std;
const int N = 5e5 + 10, M = 2e5 + 10, inf = 1e9;

int n, mod, k;

int f[1 << 9][1 << 9][20];

signed main() {
    Freopen();

    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n >> mod >> k;
    int hn = n / 2;
    int lim = ((1 << hn) - 1);

    for ( int S1 = 0; S1 <= lim; S1 ++)
        for ( int S2 = 0; S2 <= lim; S2 ++)
            for ( int i = 0; i < 20; i ++)
                f[S1][S2][i] = 1;

    while (k --) {
        int c, d, x;
        cin >> c >> d >> x;

        for ( int S = 0; S <= lim; S ++) {
            int cnt = __builtin_popcount((c & lim) ^ S);
            int T = (c >> hn);
            if (cnt <= d && d - cnt <= n - hn) f[S][T][d - cnt] = 1ll * f[S][T][d - cnt] * x % mod;
        }
    }

    for ( int S = 0; S < (1 << n); S ++) {
        int res = 1;

        for ( int s = 0; s <= lim; s ++)
            res = 1ll * res * f[S & lim][s][__builtin_popcount(s ^ (S >> hn))] % mod;

        cout << res << ' ';
    }

    return 0;
}

[NOIP2023] 双序列拓展

神题!

题目可以转化成能否使得所有 \(f_i\gt g_i\) 或者 \(f_i\lt g_i\),只考虑处理 \(f_i\lt g_i\) 的情况,\(f_i\gt g_i\) 的情况交换 \(X\)\(Y\) 即可。

首先有一个很简单的 \(O(qnm)\) 的 dp,记 \(dp_{i,j}\) 表示 \(X\) 的前 \(i\) 个数和 \(Y\) 的前 \(j\) 个数是否可以匹配,转移有:

\[dp_{i,j}\leftarrow dp_{i-1,j}\cup dp_{i,j-1}\cup dp_{i-1,j-1},X_i\lt Y_j \]

考虑这个 dp 的本质,实际上是有一个 \(n\times m\) 的网格图,\(A_{i,j}=[X_i\lt Y_j]\)

每次可以走右方的一个 \(1\)、下方的一个 \(1\)、右下方的一个 \(1\),问是否能够走到 \(n,m\)

考虑特殊性质,首先如果满足 \(X_{min}\ge Y_{min}\) 或者 \(X_{max}\ge Y_{max}\),那么是一定无法满足的,在网格图上说也就是有一行或一列全是 \(0\)

排除这种情况后,特殊情况就等价于第 \(n\) 行、第 \(m\) 列都是 \(1\),因为 \(Y\) 中所有数都比 \(X_{min}\) 大,\(X\) 中所有数都比 \(Y_{max}\) 小。

那么只用考虑是否能走到第 \(n-1\) 行或者第 \(m-1\) 列,不难发现这把问题的规模缩小了!

这启发可以继续递归下去,具体就是在剩下的 \(n-1\) 个数找到 \(X\) 的最小、最大,剩下 \(m-1\) 个数中找到 \(Y\) 的最小、最大,然后判断,继续递归即可。

特殊性质对正解的启发是很大的。

对于全部数据,可以考虑找出 \(X_{min}\) 的位置与 \(Y_{max}\) 的位置,它们所占据的两条垂直直线把网格图划分成了四个区域,需要判断的就是能否从左上角走到这两条直线、再从这两条直线走到右下角。

照搬特殊性质的做法,只需要对左上区域递归、对右下区域递归即可。

复杂度 \(O(q(n+m))\)

代码
#include <bits/stdc++.h>

void Freopen() {
    freopen("", "r", stdin);
    freopen("", "w", stdout);
}

using namespace std;
const int N = 5e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;

int c, n, m, q;
int a[N], b[N], pa[N], pb[N];

int prex[2][N], prem[2][N], sufx[2][N], sufm[2][N];

void get() {
    int mi = inf, mx = -inf, idi = 0, idx = 0;
    for ( int i = 1; i <= n; i ++) {
        if (mi > a[i]) mi = a[i], idi = i;
        if (mx < a[i]) mx = a[i], idx = i;
        prem[0][i] = idi, prex[0][i] = idx;
    }

    mi = inf, mx = -inf, idi = 0, idx = 0;
    for ( int i = 1; i <= m; i ++) {
        if (mi > b[i]) mi = b[i], idi = i;
        if (mx < b[i]) mx = b[i], idx = i;
        prem[1][i] = idi, prex[1][i] = idx;
    }

    mi = inf, mx = -inf, idi = 0, idx = 0;
    for ( int i = n; i; i --) {
        if (mi > a[i]) mi = a[i], idi = i;
        if (mx < a[i]) mx = a[i], idx = i;
        sufm[0][i] = idi, sufx[0][i] = idx;
    }

    mi = inf, mx = -inf, idi = 0, idx = 0;
    for ( int i = m; i; i --) {
        if (mi > b[i]) mi = b[i], idi = i;
        if (mx < b[i]) mx = b[i], idx = i;
        sufm[1][i] = idi, sufx[1][i] = idx;
    }
}

int chk1( int x, int y) {
    if (x == 1 || y == 1) return 1;

    if (a[prem[0][x - 1]] < b[prem[1][y - 1]]) return chk1(prem[0][x - 1], y);
    if (a[prex[0][x - 1]] < b[prex[1][y - 1]]) return chk1(x, prex[1][y - 1]);

    return 0;
}

int chk2( int x, int y) {
    if (x == n || y == m) return 1;

    if (a[sufm[0][x + 1]] < b[sufm[1][y + 1]]) return chk2(sufm[0][x + 1], y);
    if (a[sufx[0][x + 1]] < b[sufx[1][y + 1]]) return chk2(x, sufx[1][y + 1]);

    return 0;
}

int solve() {
    int F1 = 1, F2 = 1;
    get();
    if (a[1] >= b[1] || a[n] >= b[m]) F1 = 0;
    if (a[prem[0][n]] >= b[prem[1][m]]) F1 = 0;
    if (a[prex[0][n]] >= b[prex[1][m]]) F1 = 0;

    F1 &= (chk1(prem[0][n], prex[1][m]) && chk2(prem[0][n], prex[1][m]));

    swap(a, b), swap(n, m);
    get();
    if (a[1] >= b[1] || a[n] >= b[m]) F2 = 0;
    if (a[prem[0][n]] >= b[prem[1][m]]) F2 = 0;
    if (a[prex[0][n]] >= b[prex[1][m]]) F2 = 0;

    F2 &= (chk1(prem[0][n], prex[1][m]) && chk2(prem[0][n], prex[1][m]));

    swap(a, b), swap(n, m);    
    return (F1 | F2);
}

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> c >> n >> m >> q;

    for ( int i = 1; i <= n; i ++) cin >> a[i], pa[i] = a[i];
    for ( int i = 1; i <= m; i ++) cin >> b[i], pb[i] = b[i];

    cout << solve();

    for ( int o = 1; o <= q; o ++) {
        int kx, ky;
        cin >> kx >> ky;

        while (kx --) {
            int x, v; cin >> x >> v;
            a[x] = v; 
        }

        while (ky --) {
            int x, v; cin >> x >> v;
            b[x] = v;
        }

        cout << solve();

        for ( int i = 1; i <= n; i ++) a[i] = pa[i];
        for ( int i = 1; i <= m; i ++) b[i] = pb[i];
    }

    return 0;
}

跳越

给一个长度为 \(n\) 的 01 串 \(S\),给定每次可以跳的距离 \(k\)(可以从 \(i\) 跳到 \(\max(1,i-k)\)\(\min (n,i+k)\))。

\(q\) 次询问,每次问从 \(a\) 跳到 \(b\) 踩到 \(0\) 的最小值,以及在满足踩到 \(0\) 最少的情况下,跳跃的最少次数。保证 \(S_a=S_b=1\)

\(n,q\le 5\times 10^5\)

首先最优的情况下一定不会往会跳,也就是说只会往一个方向跳,那么从 \(a\) 跳到 \(b\) 就等价于从 \(b\) 跳到 \(a\)

所以只考虑 \(a\lt b\) 的情况。

首先有一个容易想到的贪心,即每次跳到能够跳到的最远的那个 \(1\),如果后面有大于 \(k\)\(0\),就直接跳 \(k\) 步即可。

这个贪心对于第一个问题是正确的,但第二个问题就不对了。

这是很容易被 Hack 的,就比如 \(k=2\) 时对于 11001111,从 \(1\) 跳到 \(7\) 需要踩 \(1\)\(0\),最少跳 \(3\) 次,但用上述贪心就会发现求出来的跳越次数是 \(4\) 次。

这是为什么?对于一个长度大于 \(k\) 的极长 \(0\) 段,如果\(i\) 跳到离它最近的一个 \(1\),再跳出极长 \(0\) 段所踩的 \(0\) 的个数等于\(i\) 直接跳进极长 \(0\) 段,再跳出去所踩的 \(0\) 的个数相等,肯定是选择后者更优,因为踩的 \(0\) 个数相同,且步数更少。

现在就可以得道一个倍增做法了,记 \(f_{i,j}\) 表示从 \(i\) 开始跳 \(2^j\) 步时最优决策下的位置,\(g_{i,j}\) 表示从 \(i\) 开始跳 \(2^j\) 步时最优决策下的位置所踩到的 \(0\) 的个数。

只需要保证 \(f_{i,0}\) 的处理正确即可。

代码
#include <bits/stdc++.h>

using namespace std;
const int N = 5e5 + 5;

int Test, n, q, k, t;
char s[N];

int to[N], f[N][20], g[N][20];

vector< int> vec;

signed main() {
    ios :: sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    cin >> Test >> n >> q >> k >> t;
    cin >> (s + 1);

    for ( int i = 1; i <= n; i ++) if (s[i] == '1') vec.push_back(i);

    for ( int i = 1; i <= n; i ++) {
        int j = upper_bound(vec.begin(), vec.end(), i + k) - vec.begin() - 1;

        if (vec[j] <= i) to[i] = min(n, i + k);
        else if (j == (int)vec.size() - 1) to[i] = min(n, i + k);
        else {
            int t1 = (vec[j + 1] - vec[j] + k - 1) / k;
            int t2 = (vec[j + 1] - i + k - 1) / k;
            to[i] = (t1 < t2 ? vec[j] : min(n, i + k));
        }
    }

    for ( int i = 1; i <= n; i ++)
        f[i][0] = to[i], g[i][0] = (s[to[i]] == '0');

    for ( int i = n; i >= 1; i --)
        for ( int j = 1; j < 20; j ++) {
            f[i][j] = f[f[i][j - 1]][j - 1];
            g[i][j] = g[i][j - 1] + g[f[i][j - 1]][j - 1];
        }

    while (q --) {
        int l, r, ans1 = 0, ans2 = 0;
        
        cin >> l >> r;
        if (l > r) swap(l, r);

        for ( int i = 19; i >= 0; i --)
            if (f[l][i] < r)
                ans1 += g[l][i], ans2 += (1 << i), l = f[l][i];

        if (! t) cout << ans1 << '\n';
        else cout << ans1 << ' ' << ans2 + 1 << '\n';
    }

    return 0;
}

CF1251E2 Voting (Hard Version)

考虑从大的 \(m_i\) 开始考虑。

设当前 \(m_i=x\),记 \(m_i\lt x\) 的个数为 \(tot\),记在 \(\ge x\) 的人中买了 \(cnt\) 人。

那么如果满足 \(x\le cnt+tot\),就可以不用管了,不然就应该在所有 \(m_i\ge x\) 的人中继续买,直到满足这个条件。

肯定每次买都买最小的,用一个优先队列存一下就好。

正确性是很对的,因为已经默认了 \(m_i\lt x\) 的人全部都能被满足,所以 \(m_i=x\) 的人不能被满足,肯定只能从 \(m_i\ge x\) 的人中买。

代码
#include <bits/stdc++.h>
#define int long long

using namespace std;
const int N = 2e5 + 10;

int n;
vector< int> vec[N];

void solve() {
    cin >> n;

    for ( int i = 0; i < n; i ++) vec[i].clear();

    for ( int i = 1; i <= n; i ++) {
        int m, p; cin >> m >> p;
        vec[m].push_back(p);
    }

    priority_queue< int> q;
    int cnt = 0, sum = n, ans = 0;

    for ( int i = n - 1; i >= 0; i --) {
        if (vec[i].empty()) continue ;
        for ( auto v : vec[i]) q.push(-v);
        sum -= vec[i].size();

        int need = i - sum;

        while (cnt < need) {
            ans += -q.top();
            q.pop();
            cnt ++;
        }
    }

    cout << ans << '\n';
}

signed main() {
    ios :: sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int T; cin >> T;
    while (T --) solve();

    return 0;
}

#P1356. 食物

如果直接维护整除的,需要枚举所有倍数,但是显然可以卡掉,所以考虑维护不能被整除的个数。

\(f_i\) 表示 \(1\sim i\) 中没有被集合中的质数整除,答案就是 \(n-f_n\)

当加入一个数 \(p\) 时,\(f_i\) 该如何变化?要在 \(f_i\) 中剔除所有整除 \(p\) 的数,其实也就是 \(f_{\lfloor\dfrac{i}{p}\rfloor}\) 的值。

那么从大到小枚举 \(i\),更新即可。

删除一个数同理,给 \(f_i\) 加上 \(f_{\lfloor\dfrac{i}{p}\rfloor}\),从小到大更新。

复杂度是 \(O(nq)\),如何优化?

因为每次只询问 \(f_n\) 的值,不难发现或者打表可以得到只用维护 \(f_{\lfloor\dfrac{n}{i}\rfloor}\) 的值即可。

那么优化到 \(O(\sqrt{n}q)\)

代码
#include <bits/stdc++.h>
#define all(x) (x).begin(), (x).end()

void Freopen() {
    freopen("food.in", "r", stdin);
    freopen("food.out", "w", stdout);
}

using namespace std;
const int N = 1e6 + 10;

int n, q;
int f[N];
int vis[N];

vector< int> vec1, vec2;

void add( int x) {
    for ( int v : vec2) f[v] -= f[v / x];
}

void del( int x) {
    for ( auto v : vec1) f[v] += f[v / x];
}

signed main() {
    Freopen();

    ios :: sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    cin >> n >> q;

    iota(f + 1, f + n + 1, 1);

    for ( int i = 1; i <= n; i ++)
        if (n / (i + 1) != n / i) vec1.push_back(i);

    vec2 = vec1;
    reverse(all(vec2));

    while (q --) {
        int x; cin >> x;

        if (! vis[x]) {
            add(x);
            vis[x] = 1;
        } else {
            del(x);
            vis[x] = 0;
        }

        cout << n - f[n] << '\n';
    }

    return 0;
}
posted @ 2025-09-16 17:03  咚咚的锵  阅读(19)  评论(0)    收藏  举报