前 k 优相关杂题选做

前 k 优相关杂题选做

二分答案判定排名

这个套路挺典的,懒得找题目了。

枚举下一步决策

P3672 小清新签到题

给定 \(n, k, x\) ,求第 \(k\) 小的长度为 \(n\) 的逆序对数为 \(x\) 的排列。

\(n \le 300\)

预处理 \(f_{i, j}\) 表示长度为 \(i\) ,逆序对数量为 \(j\) 的方案数,然后暴力贪心尝试每一位放的数字即可,时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 3e2 + 7;

ll f[N][N * N >> 1], s[N * N];
bool vis[N];

ll k;
int n, m;

signed main() {
    scanf("%d%lld%d", &n, &k, &m);
    fill(s, s + m + 1, f[0][0] = 1);

    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= m; ++j)
            f[i][j] = min(j >= i ? s[j] - s[j - i] : s[j], k + 1);

        s[0] = f[i][0];

        for (int j = 1; j <= m; ++j)
            s[j] = s[j - 1] + f[i][j];
    }

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j) {
            if (vis[j])
                continue;

            int c = j - 1 - count(vis + 1, vis + j, true);

            if (f[n - i][m - c] >= k) {
                printf("%d ", j);
                vis[j] = true, m -= c;
                break;
            }

            k -= f[n - i][m - c];
        }

    return 0;
}

CF1470E Strange Permutation

给定排列 \(a_{1 \sim n}\) ,定义翻转 \([l, r]\) 区间的代价为 \(r - l\) 。定义一次操作为选择若干不交区间依次翻转,代价为各次翻转的代价和。

将所有能通过一次代价 \(\le c\) 的操作得到的排列按字典序排序后去重,\(q\) 次询问,每次询问第 \(x\) 个排列的第 \(y\) 个位置的数。

\(n \le 3 \times 10^4\)\(q \le 3 \times 10^5\)\(c \le 4\)

预处理 \(f_{i, j}\) 表示 \(a_{i \sim n}\) 用不超过 \(j\) 的代价能形成的本质不同排列数,不难 \(O(nc^2)\) 背包 DP 求出。

查询时考虑按顺序遍历,每次尝试决策。记剩余的排名与代价分别为 \(x', c'\) ,翻转了 \(a_{i \sim p}\) 后若 \(x' \leq f_{p + 1, c' - (p - i)}\) 则递归到 \(p + 1\) ,直接做可以做到 \(O(qnc)\)

由于 \(c\) 的范围很小,实际上大部分决策都是 \(p = i\) (不翻转)的,对于每一个不翻转的段,已知左端点时显然可以二分找到右端点,\(O(nc^2)\) 预处理 \(g_{i, j} = \sum_{p - i \leq j \and a_p < a_i} f_{p + 1, j - (p - i)}\)\(s_{i, j} = \sum_{k \leq i} g_{k, j}\) ,对于左端点 \(l\) ,右端点 \(r\) 合法当且仅当 \(0 < x' - (s_{r - 1, c'} - s_{l - 1, c'}) \leq f_{r, c'}\) ,由此不难做到 \(O(qc \log n)\) 查询。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 3e4 + 7, C = 5;

vector<int> nxt[N];

ll f[N][C], s[N][C];
int a[N];

int n, c, q;

int calc(int i, int c, ll k, int x) {
    int l = i, r = n + 1, pos = x;

    while (l <= r) {
        int mid = (l + r) >> 1;
        ll delta = k - (s[mid - 1][c] - s[i - 1][c]);

        if (1 <= delta && delta <= f[mid][c])
            pos = mid, l = mid + 1;
        else
            r = mid - 1;
    }

    if (pos == i) {
        for (int p : nxt[i])
            if (p <= i + c) {
                if (k <= f[p + 1][c - (p - i)])
                    return i <= x && x <= p ? a[i + (p - x)] : calc(p + 1, c - (p - i), k, x);
                
                k -= f[p + 1][c - (p - i)];
            }

        return -1;
    } else
        return i <= x && x < pos ? a[x] : calc(pos, c, k - (s[pos - 1][c] - s[i - 1][c]), x);
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d%d", &n, &c, &q);

        for (int i = 1; i <= n; ++i)
            scanf("%d", a + i);

        for (int i = 1; i <= n; ++i) {
            nxt[i].resize(min(n - i + 1, c + 1));
            iota(nxt[i].begin(), nxt[i].end(), i);
            
            sort(nxt[i].begin(), nxt[i].end(), [](const int &x, const int &y) {
                return a[x] < a[y];
            });
        }

        fill(f[n + 1], f[n + 1] + c + 1, 1);

        for (int i = n; i; --i)
            for (int j = 0; j <= c; ++j) {
                f[i][j] = 0;

                for (int k = 0; k <= min(j, n - i); ++k)
                    f[i][j] += f[i + k + 1][j - k];
            }

        for (int i = 1; i <= n; ++i)
            for (int j = 0; j <= c; ++j) {
                s[i][j] = s[i - 1][j];

                for (int p : nxt[i])
                    if (p <= i + j && a[p] < a[i])
                        s[i][j] += f[p + 1][j - (p - i)];
            }

        while (q--) {
            int x;
            ll k;
            scanf("%d%lld", &x, &k);
            printf("%d\n", k > f[1][c] ? -1 : calc(1, c, k, x));
        }
    }

    return 0;
}

维护候补答案集合

P2048 [NOI2010] 超级钢琴

给定 \(n, k, L, R, a_{1 \sim n}\) ,求所有长度 \(\in [L, R]\) 的区间中区间和前 \(k\) 大的区间和的和。

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

考虑维护候补答案集合:设计一种转移,状态之间构成一棵外向树(到达每个点的路径唯一),且各个状态均不劣于子树内的状态。如此只要维护一个堆,每次找到最优状态,再扩展所有儿子即可。

记三元组 \((x, l, r)\) 表示左端点为 \(x\) ,右端点 \(\in [l, r]\) 的最大区间和,将所有可能的答案扔到堆里面,每次拿出最大者,找到 \([l, r]\) 中的最优点 \(p\) 统计当前答案,并将 \([l, r]\) 分裂为 \([l, p - 1]\)\([p + 1, r]\) 扔入堆中即可。

\(n, k\) 同阶,时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e5 + 7, LOGN = 19;

ll s[N];
int a[N];

int n, k, L, R;

namespace ST {
int f[LOGN][N];

inline int cmp(const int &a, const int &b) {
    return s[a] > s[b] ? a : b;
}

inline void prework(int n) {
    iota(f[0] + 1, f[0] + n + 1, 1);

    for (int j = 1; j <= __lg(n); ++j)
        for (int i = 1; i + (1 << j) - 1 <= n; ++i)
            f[j][i] = cmp(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
}

inline int query(int l, int r) {
    int k = __lg(r - l + 1);
    return cmp(f[k][l], f[k][r - (1 << k) + 1]);
}
} // namespace ST

struct Node {
    int x, l, r;

    inline bool operator < (const Node &rhs) const {
        return s[ST::query(l, r)] - s[x - 1] < s[ST::query(rhs.l, rhs.r)] - s[rhs.x - 1];
    }
};

signed main() {
    scanf("%d%d%d%d", &n, &k, &L, &R);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i), s[i] = s[i - 1] + a[i];

    ST::prework(n);
    priority_queue<Node> q;

    for (int i = 1; i + L - 1 <= n; ++i)
        q.emplace((Node) {i, i + L - 1, min(n, i + R - 1)});

    ll ans = 0;

    while (k && !q.empty()) {
        Node it = q.top();
        q.pop(), --k;
        int p = ST::query(it.l, it.r);
        ans += s[p] - s[it.x - 1];

        if (p != it.l)
            q.emplace((Node) {it.x, it.l, p - 1});

        if (p != it.r)
            q.emplace((Node) {it.x, p + 1, it.r});
    }

    printf("%lld", ans);
    return 0;
}

Tree

给定一棵 \(n\) 个点的树,点带点权,求点权和前 \(k\) 小的包含根连通块的点权和。

\(n, k \le 2 \times 10^5\)\(1 \le a_i \le 10^9\)

不难发现若一个连通块在前 \(k\) 小,则其子连通块显然也在前 \(k\)

考虑维护候补答案集合,一个显然的暴力是一开始加入根作为初始连通块,钦定只能扩展儿子,每次扩展各个儿子后加入堆,时间复杂度 \(O(kn + k \log n)\)

考虑优化,发现一次扩展会加入一堆可能答案。考虑每次都精确扩展,即每次都先扩展最小的邻域点,然后从当前邻域中删去该点扔进堆中,然后再将扩展到的连通块扔进堆中。

此时需要动态维护当前连通块权值和,扩张前的权值和,当前的邻域集合。

当前的邻域集合用可持久化可并堆维护即可,视 \(n, k\) 同阶,时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, M = 1e7 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

struct Node {
    ll bfr, aft;
    int rt;

    inline bool operator < (const Node &rhs) const {
        return aft > rhs.aft;
    }
};

ll val[M];
int a[N], lc[M], rc[M], dist[M], id[M], near[N];

int n, k, tot;

inline int newnode(int k) {
    return ++tot, lc[tot] = rc[tot] = dist[tot] = 0, id[tot] = k, val[tot] = a[k], tot;
}

inline int copy(int x) {
    return lc[++tot] = lc[x], rc[tot] = rc[x], dist[tot] = dist[x], id[tot] = id[x], val[tot] = val[x], tot;
}

int merge(int a, int b) {
    if (!a || !b)
        return a | b;

    if (val[a] > val[b])
        swap(a, b);

    int x = copy(a);
    rc[x] = merge(rc[x], b);

    if (dist[lc[x]] < dist[rc[x]])
        swap(lc[x], rc[x]);

    dist[x] = dist[rc[x]] + 1;
    return x;
}

void dfs(int u, int f) {
    for (int v : G.e[u])
        if (v != f)
            dfs(v, u), near[u] = merge(near[u], newnode(v));
}

signed main() {
    scanf("%d%d", &n, &k);

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    dfs(1, 0);
    priority_queue<Node> q;
    q.emplace((Node){0, a[1], newnode(1)});

    for (int i = 1; i < k; ++i) {
        Node it = q.top();
        q.pop();

        int cur = merge(lc[it.rt], rc[it.rt]);

        if (cur)
            q.emplace((Node){it.bfr, it.bfr + val[cur], cur});

        cur = merge(cur, near[id[it.rt]]);

        if (cur)
            q.emplace((Node){it.aft, it.aft + val[cur], cur});
    }

    printf("%lld ", q.top().aft);
    return 0;
}

P6646 [CCO 2020] Shopping Plans

\(n\) 个物品,第 \(i\) 个物品种类为 \(a_i\) 、价格为 \(c_i\) 。对于第 \(i\) 个种类,至少需要购买 \(x_i\) 个,至多购买 \(y_i\) 个。求总价前 \(k\) 小的方案的总价。

\(n, m, k \le 2 \times 10^5\)

先考虑只有一种商品的情况,将商品按价格升序排序,则任意一个位置右移后总价一定变高,在此扩展方式的基础上考虑设计状态。

对于一种选取方案,可以将其视为从最后一个选取的数开始依次移动至该方案。考虑用四元组 \((x, y, z, w)\) 表示状态,其中 \(x\) 表示倒序考虑到选取的第 \(x\) 个数(即 \(x\) 之后的数字都不动),\(y\) 表示第 \(x\) 个数当前的位置,\(z\) 表示 \(x + 1\) 选取的位置的前一位(\(x\) 能移动到的最远处),\(w\) 表示该状态的总价。转移要么尝试扩展当前的 \(x\) ,要么直接确定 \(x\) 的位置之后扩展 \(x - 1\)

对于多种选取方案,维护方案和单种是类似的,在外层再套一个类似的做法即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 2e5 + 7;

struct Node {
    struct node {
        ll sum;
        int ed, cur, lim;

        inline bool operator < (const node &rhs) const {
            return sum > rhs.sum;
        }
    };

    vector<ll> ans;
    vector<int> vec;
    priority_queue<node> q;

    int l, r;

    inline bool prework() {
        if (l > vec.size())
            return false;

        sort(vec.begin(), vec.end());

        if (!l)
            ans.emplace_back(0);

        ll sum = 0;

        for (int i = 0; i < min((int)vec.size(), r); ++i) {
            sum += vec[i];

            if (i + 1 >= l)
                q.emplace((node){sum, i, i, vec.size() - 1});
        }

        return true;
    }

    inline ll query(int k) {
        while (!q.empty() && k > ans.size()) {
            node x = q.top();
            q.pop(), ans.emplace_back(x.sum);

            if (x.cur < x.lim)
                q.emplace((node){x.sum + vec[x.cur + 1] - vec[x.cur], x.ed, x.cur + 1, x.lim});

            if (x.ed && x.ed < x.cur)
                q.emplace((node){x.sum + vec[x.ed] - vec[x.ed - 1], x.ed - 1, x.ed, x.cur - 1});
        }

        return k > ans.size() ? inf : ans[k - 1];
    }
} nd[N];

struct node {
    ll sum;
    int cur, cnt;

    inline bool operator < (const node &rhs) const {
        return sum > rhs.sum;
    }
};

pair<ll, int> tr[N];

int n, m, k;

signed main() {
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 1; i <= n; ++i) {
        int x, y;
        scanf("%d%d", &x, &y);
        nd[x].vec.emplace_back(y);
    }

    ll sum = 0;

    for (int i = 1; i <= m; ++i) {
        scanf("%d%d", &nd[i].l, &nd[i].r);

        if (!nd[i].prework()) {
            while (k--)
                puts("-1");

            return 0;
        }

        sum += nd[i].query(1), tr[i] = make_pair(nd[i].query(2) - nd[i].query(1), i);
    }

    printf("%lld\n", sum), --k;
    sort(tr + 1, tr + m + 1);
    priority_queue<node> q;
    q.emplace((node){sum + tr[1].first, 1, 2});

    while (k && !q.empty()) {
        node x = q.top();
        q.pop();

        if (x.sum >= inf)
            break;

        printf("%lld\n", x.sum), --k;
        int u = x.cur, id = tr[u].second;
        q.emplace((node){x.sum + nd[id].query(x.cnt + 1) - nd[id].query(x.cnt), u, x.cnt + 1});

        if (u < m) {
            q.emplace((node){x.sum + tr[u + 1].first, u + 1, 2});

            if (x.cnt == 2)
                q.emplace((node){x.sum + tr[u + 1].first - tr[u].first, u + 1, 2});
        }
    }

    while (k--)
        puts("-1");

    return 0;
}

P5283 [十二省联考 2019] 异或粽子

给定 \(a_{1 \sim n}\) ,求区间异或和前 \(k\) 大区间的区间异或和的和。

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

前缀和转化后问题转化为任选两个数异或和最大,固定其中一个数,另一个数依次选更优者即可,使用 Trie 数找更优者即可做到 \(O((n + k) \log n + k \log V)\)

#include <bits/stdc++.h>
typedef unsigned int uint;
typedef long long ll;
using namespace std;
const int N = 5e5 + 7;

uint s[N];

int n, k;

namespace Trie {
const int S = N << 5, B = 31;

int ch[S][2], siz[S];

int tot = 1;

inline void insert(uint k) {
	int u = 1;
	++siz[u];

	for (int i = B; ~i; --i) {
		int c = k >> i & 1;

		if (!ch[u][c])
			ch[u][c] = ++tot;

		++siz[u = ch[u][c]];
	}
}

inline uint querymax(int x, int k) {
	int u = 1;
	uint res = 0;

	for (int i = B; ~i; --i) {
		int c = s[x] >> i & 1;

		if (k <= siz[ch[u][c ^ 1]])
			res |= 1u << i, u = ch[u][c ^ 1];
		else
			k -= siz[ch[u][c ^ 1]], u = ch[u][c];
	}

	return res;
}
} // namespace Trie

signed main() {
	scanf("%d%d", &n, &k), k *= 2;
	Trie::insert(0);

	for (int i = 1; i <= n; ++i)
		scanf("%u", s + i), Trie::insert(s[i] ^= s[i - 1]);

	priority_queue<tuple<uint, int, int> > q;

	for (int i = 0; i <= n; ++i)
		q.emplace(Trie::querymax(i, 1), i, 1);

	ll ans = 0;

	while (k--) {
		auto now = q.top();
		q.pop(), ans += get<0>(now);
		q.emplace(Trie::querymax(get<1>(now), get<2>(now) + 1), get<1>(now), get<2>(now) + 1);
	}

	printf("%lld", ans / 2);
	return 0;
}

其他

[ABC391F] K-th Largest Triplet

给定 \(a_{1 \sim n}, b_{1 \sim n}, c_{1 \sim n}\) ,定义三元组 \((x, y, z)\) 的权值为 \(a_x b_y + b_y c_z + c_z a_x\) ,求第 \(k\) 大权值。

\(n \le 2 \times 10^5\)\(k \le 5 \times 10^5\)

不难发现若 \(i \le x, j \le y, k \le z\) ,则 \((i, j, k)\) 的权值不超过 \((x, y, z)\) 的权值。

考虑将 \(a, b, c\) 排序,然后暴力枚举三元组。显然只有 \(x \times y \times z \le k\) 的三元组 \((x, y, z)\) 可能成为答案。取出这些三元组取第 \(k\) 大即可,时间复杂度 \(O(n \log n + k \ln^2 k)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;

int a[N], b[N], c[N];

int n, m;

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    for (int i = 1; i <= n; ++i)
        scanf("%d", b + i);

    for (int i = 1; i <= n; ++i)
        scanf("%d", c + i);

    sort(a + 1, a + n + 1, greater<int>());
    sort(b + 1, b + n + 1, greater<int>());
    sort(c + 1, c + n + 1, greater<int>());
    vector<ll> vec;

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= min(n, m / i); ++j)
            for (int k = 1; k <= min(n, m / i / j); ++k)
                vec.emplace_back(1ll * a[i] * b[j] + 1ll * b[j] * c[k] + 1ll * c[k] * a[i]);

    nth_element(vec.begin(), vec.begin() + m - 1, vec.end(), greater<ll>());
    printf("%lld", vec[m - 1]);
    return 0;
}
posted @ 2026-02-26 15:32  wshcl  阅读(21)  评论(0)    收藏  举报