高质量补题记录集

本文最后更新于:2 年前

2022牛客第一场 J

Serval and Essay

一道不错的图论题

比较有价值的部分是图的合并,类似并查集,合并两点时可以使用其中一个点编号作为合并后的点集编号。

用启发式合并思想,让小集合向大集合合并,合并时将小集合上的连边转移到大集合上。

int n;
int fa[maxn], siz[maxn], cnt[maxn];
set<int> nt[maxn], fm[maxn];

int find(int x){
    if(fa[x] == x) return x;
    return fa[x] = find(fa[x]);
}

void solve() {
    cin >> n;
    for(int i=1;i<=n;i++){
        cnt[i] = 0;
        nt[i].clear(), fm[i].clear();
        fa[i] = i, siz[i] = 1;
    }
    for(int i=1;i<=n;i++){
        int k; cin >> k;
        cnt[i] = k;
        for(int j=1;j<=k;j++){
            int x; cin >> x;
            nt[x].insert(i);
            fm[i].insert(x);
        }
    }
    queue<pair<int, int>> q; //存要合并的两个集合
    for(int i=1;i<=n;i++){
        if(cnt[i] == 1){
            q.emplace(i, *fm[i].begin());
        }
    }
    int ans = 1;
    while(!q.empty()){
        auto [x, y] = q.front();
        q.pop();

        //merge
        x = find(x), y = find(y);
        if(x == y) continue;
        if(siz[x] > siz[y]) swap(x, y);
        fa[x] = y;
        siz[y] += siz[x];
        ans = max(ans, siz[y]);
        for(int v: nt[x]){
            if(nt[y].count(v)){
                cnt[v]--;
                if(cnt[v] == 1) q.emplace(v, y);
            nt[y].insert(v);
        }
    }
    static int cas = 0;
    cout << "Case #" << ++cas << ": " << ans << '\n';
}

2022牛客第三场B

Boss

学到一个可删除堆的奇技淫巧,但这还不是关键

这里的最短路类似二分图和网络流中的“增广路”,路径代表的是一种转移方案,$a-b-c-d$ 这样的一条路径,实际上指将当前的人放到 $a$ 处,$a$ 中选取一个代价最小的人移到 $b$ 处…最后 $d$ 处多出一个人,最短路径即为将 $i$ 塞入的最小代价。

可以用一个set记录每个节点上已经有哪些人,记 $u,v$ 间的转移方案 ${dis,id}$,表示将 $id$ 这个人从 $u$ 转移到 $v$ 的代价为 $dis$,开 $K^2$ 个堆记录任意两点间的所有转移方案,在人员变动时维护。

template<typename T> struct heap{ //奇技淫巧:可删除堆 常数优秀
    priority_queue<T, vector<T>, greater<T> > q1, q2;
    void push(const T& x) {q1.push(x);}
    void erase(const T& x) {q2.push(x);}
    void release(){
        while(!q1.empty() && !q2.empty() && q1.top() == q2.top()){
            q1.pop();
            q2.pop();
        }
    }
    bool empty(){
        release();
        return q1.empty();
    }
    T top(){
        release();
        return q1.top();
    }
};

int n, m;
heap<pair<ll, int>> tr[12][12]; //tr[x][y] 从x到y的所有可行路径,{dis, id}
set<int> nodes[12]; //存每个节点上所有的人

ll vol[maxn];
ll cost[maxn][12]; //基础花费:cost[x][k] x这个人匹配第k个城市的代价
ll move_dis(int u, int x, int y){ //转移代价:u这个人从x城切换到y城的代价
    return cost[u][y] - cost[u][x];
}
pair<ll, int> min_dis(int x, int y){ //获取当前从x转移到y代价最小的路径
    if(tr[x][y].empty()) return {INF, 0};
    return tr[x][y].top();
}
void add(int k, int p){ //在k城添加一个人p,维护
    nodes[k].insert(p);
    for(int i=1;i<=m;i++){
        if(i == k) continue;
        tr[k][i].push({move_dis(p, k, i), p});
    }
}
void remove(int k, int p){ //从k城移除一个人p,维护
    nodes[k].erase(p);
    for(int i=1;i<=m;i++){
        if(i == k) continue;
        tr[k][i].erase({move_dis(p, k, i), p});
    }
}

void solve() {
    cin >> n >> m;
    for(int i=1;i<=m;i++) cin >> vol[i];
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin >> cost[i][j];
        }
    }
    ll ans = 0;
    for(int i=1;i<=n;i++){
        queue<int> q;		//先跑最短路,即最小的将当前i塞进去的代价
        vector<int> d(m+1);
        vector<pair<int, int>> frm(m+1, {0, 0});
        vector<bool> inq(m+1, true);
        for(int j=1;j<=m;j++) q.push(j), d[j] = cost[i][j];
        while(!q.empty()){
            int u = q.front();
            q.pop();
            inq[u] = false;
            for(int v=1;v<=m;v++){
                if(v == u) continue;
                auto [dis, id] = min_dis(u, v);
                if(d[v] > d[u] + dis){
                    d[v] = d[u] + dis;
                    frm[v] = {u, id};
                    if(!inq[v]) q.push(v), inq[v] = true;
                }
            }
        }
        int ed = 0;
        for(int j=1;j<=m;j++){
            if(nodes[j].size() >= vol[j]) continue;
            if(!ed || d[j] < d[ed]) ed = j;
        }
        assert(ed);
        ans += d[ed];	   //记录将i这个人塞进去的转移路径,沿着路径每个节点的状态,同时维护堆
        while(true){
            auto [fa, id] = frm[ed];
            if(!fa){
                add(ed, i);
                break;
            }
            remove(fa, id);
            add(ed, id);
            ed = fa;
        }
    }
    cout << ans << '\n';
}

2022杭电第四场1002

Link with Running

边权有两种:距离和价值,要求一条距离最短前提下的价值最大的路。(距离可以为0)

因为距离为0,一条无距离有价值的路实际上相当于一条负权边,无法用传统dijkstra解决。

解法:先仅按照距离dij跑出到每个点的最短距离,得到最短路径图,在最短路径图上,tarjian缩点去除可能存在的环,变成一个DAG,再在DAG上跑最长路即可。

  • 最短路径图即图上所有最短路径构成的图,可以从终点开始往前逆向跑,对于所有 $dis_v=dis_u+w$ 的路径加入最短路径图,也可以直接顺着跑,得到的点数会更多(包含了到所有点的最短路)。

  • DAG上的最长路可以按拓扑序更新(即一个点需要在前驱都遍历后更新),也可以直接spfa。

思路很简单,但是几层图换来换去代码属实调了很久,最后整出来这么一份非常结构化的代码,但还是很难直接拿去当图论模板..

struct Edge{
    int t, w, val;
};
template<typename E>
class Graph{
public:
    int n;
    vector<vector<E>> vp;
    Graph(int n): n(n), vp(n+1, vector<E>()){};
    void addEdge(int x, const E &e){ vp[x].push_back(e);}
};

vector<ll> dijkstra(Graph<Edge> &g, int st){
    using pii = pair<ll, int>;
    vector<ll> dis(g.n+1, INF);
    priority_queue<pii, vector<pii>, greater<pii>> q;
    q.push({0, st}); dis[st] = 0;
    while(!q.empty()){
        pii uq = q.top();
        ll ud = uq.first, u = uq.second;
        q.pop();
        if(ud > dis[u]) continue;
        for(Edge &e: g.vp[u]){
            int v = e.t;
            if(dis[v] > dis[u] + e.w){
                dis[v] = dis[u] + e.w;
                q.push({dis[v], v});
            }
        }
    }
    return dis;
}

//获取最短路径图
Graph<pair<int, ll>> pathGraph(Graph<Edge> &g, int st){
    auto dis = dijkstra(g, 1); //计算最短路
    Graph<pair<int, ll>> res(g.n);
    for(int i=1;i<=g.n;i++){
        for(Edge &e: g.vp[i]){
            int v = e.t;
            if(dis[v] == dis[i] + e.w){
                res.addEdge(i, {v, e.val});
            }
        }
    }
    cout << dis[g.n] << ' ';
    return res;
}

namespace Connect{
    int scc[maxn], stk[maxn], h, scn;
    int dfn[maxn], low[maxn], dfc;
    void tarjian(Graph<pair<int, ll>> &g,int p){
        dfn[p] = low[p] = ++dfc;
        stk[++h] = p;
        for(auto &e: g.vp[p]){
            int v = e.first;
            if(!dfn[v]) {
                tarjian(g, v);
                low[p] = min(low[p], low[v]);
            } else if(!scc[v]) {
                low[p] = min(low[p], dfn[v]);
            }
        }
        if(low[p] == dfn[p]){
            ++scn;
            while(stk[h] != p) scc[stk[h--]] = scn;
            scc[stk[h--]] = scn;
        }
    }
    Graph<pair<int, ll>> getDag(Graph<pair<int, ll>> &g){
        h = scn = dfc = 0;
        for(int i=1;i<=g.n;i++) dfn[i] = scc[i] = 0;
        for(int i=1;i<=g.n;i++) if(!scc[i]) tarjian(g, i);
        Graph<pair<int, ll>> res(scn);
        for(int i=1;i<=g.n;i++){
            for(auto &e: g.vp[i]){
                int v = e.first;
                if(scc[i] != scc[v]){
                    res.addEdge(scc[i], {scc[v], e.second});
                }
            }
        }
        return res;
    }
}

void solve(){
    int n, m;
    cin >> n >> m;
    Graph<Edge> g(n);
    for(int i=1;i<=m;i++){
        int x, y, e, p;
        cin >> x >> y >> e >> p;
        g.addEdge(x, {y, e, p});
    }
    auto spg = pathGraph(g, 1);
    auto dag = Connect::getDag(spg);

    vector<ll> f(dag.n+1, 0), du(dag.n+1, 0);
    for(int i=1;i<=dag.n;i++){
        for(auto &e: dag.vp[i]){
            du[e.first]++;
        }
    }
    queue<int> q;
    for(int i=1;i<=dag.n;i++) if(du[i] == 0) q.push(i);
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for(auto &e: dag.vp[u]){
            int v = e.first;
            f[v] = max(f[v], f[u] + e.second);
            if(--du[v] <= 0) q.push(v);
        }
    }
    cout << f[Connect::scc[n]] << '\n';
}

2022杭电第四场1005

Link with Level Editor II

矩阵+神奇双指针(对顶栈)

首先这题可以用矩阵维护一段区间的方案数,但矩阵并非一定可逆,故没法用前缀和搞。

比较容易想到的是线段树暴力维护矩阵区间乘,常数很大但是卡卡能过。

另有神奇的无需删除的双指针尺取(题解叫对顶栈):

如图所示,双指针 $l,r$ 即为两个栈顶,右指针移动时将新元素加入右栈中;左指针移动时将栈顶元素弹出,接触到中间点时,将右栈的所有元素一一弹出并加入左栈,将右端点作为新的中点。

左栈中加入元素时记录该元素到中点的后缀和,这样在左栈弹出后,仍可以得知左栈的总和,和右栈和相加得到区间和(此题中为积)。

如此便可以 $O(n)$ 地做不带删除的尺取了!

int lim;
struct matrix{
    static const int N = 21, M = 21;
    ll a[N][M];
    matrix(int x = 0){ //单位矩阵
        memset(a, 0, sizeof(a));
        for(int i=1;i<=N;i++) a[i][i] = x;
    }
    inline matrix operator*(const matrix& y) const{
        matrix r = 0;
        for (int i = 0; i < N; ++i)
            for (int k = 0; k < M; ++k) {
                ll t = a[i][k];
                for (int j = 0; j < M; ++j){
                    r.a[i][j] += t * y.a[k][j];
                    if(r.a[i][j] > lim) r.a[i][j] = lim + 1;
                }
            }
        return r;
    }
};

matrix a[maxn], s[maxn];
void solve(){
    int n, m;
    cin >> n >> m >> lim;
    for(int i=1;i<=n;i++){
        int l; cin >> l;
        a[i] = 1;
        for(int j=1;j<=l;j++){
            int x, y; cin >> x >> y;
            a[i].a[x][y] = 1;
        }
    }
    matrix cur(1);
    auto check = [&](int l, int r){
        ll ans = 0;
        for(int i=1;i<=m;i++){
            ans += s[l].a[1][i] * cur.a[i][m];
            if(ans > lim) return false;
        }
        return true;
    };
    int ans = 0, mid = 0;
    for(int l=0, r=1; r<=n; r++){
        cur = cur * a[r];
        while(l == 0 || !check(l, r)){
            l++;
            if(l > mid){
                s[r] = a[r];
                for(int i=r-1;i>mid;i--) s[i] = a[i] * s[i+1];
                mid = r;
                cur = 1;
            }
        }
        ans = max(ans, r - l + 1);
    }
    cout << ans << '\n';
}

2022杭电第八场1002

Darkmoon Faire

大力数据结构维护题。

从左往右推,设 $f_i$ 为前 $i$ 个位置划分好的方案数,接下来要转移到 $i+1$。

维护两个单调栈,一个递增一个递减,对于递增单调栈中相邻两个位置 $x,y$,可以知道 $(x,y]$ 这段作为起始位置时,最小值都为 $a_y$。那么就可以知道:这段区间中的所有奇数/偶数位置都能满足最小值条件。

对于最大值条件也是同理,可以用两个线段树分别维护奇偶位置上的tag(满足条件数),同时满足两个条件的位置即可进行转移。

线段树中,维护每个区间的最大满足条件的数量(tag),和具有最大条件数量的位置的 $f_i$ 之和。对线段树的操作包括单点添加新的dp值,区间tag加1和减1(在单调栈中元素被pop时减)。

因为我们的操作能保证合理(只对具有tag1区间的减tag1,tag2一样),所以不需要记录区间具有两种条件的哪一种,只需要记录数量即可。

int n, a[maxn];
ll f[maxn];
struct segTree {
    function<int(int)> fp; //自带离散映射的线段树
    ll sum[maxn << 2], mx[maxn << 2];
    int add[maxn << 2];
    void build(int p, int l, int r) {
        add[p] = sum[p] = mx[p] = 0;
        if(l == r) return;
        int mid = (l + r) / 2;
        build(p * 2, l, mid);
        build(p * 2 + 1, mid + 1, r);
    }
    void push_up(int p) {
        sum[p] = 0, mx[p] = max(mx[p * 2], mx[p * 2 + 1]);
        if(mx[p] == mx[p * 2]) (sum[p] += sum[p * 2]) %= mode;
        if(mx[p] == mx[p * 2 + 1]) (sum[p] += sum[p * 2 + 1]) %= mode;
    }
    void push_add(int p, int d) {
        mx[p] += d;
        add[p] += d;
    }
    void push_down(int p) {
        if(add[p]) {
            push_add(p * 2, add[p]);
            push_add(p * 2 + 1, add[p]);
            add[p] = 0;
        }
    }
    void addvalue(int p, int l, int r, int pos, ll val) {
        if(l == r) {
            (sum[p] += val) %= mode;
            return;
        }
        push_down(p);
        int mid = (l + r) / 2;
        if(pos <= fp(mid)) addvalue(p * 2, l, mid, pos, val);
        else addvalue(p * 2 + 1, mid + 1, r, pos, val);
        push_up(p);
    }
    void modify(int p, int l, int r, int L, int R, int d) {
        if(fp(l) > R || L > fp(r)) return;
        if(L <= fp(l) && fp(r) <= R) {
            push_add(p, d);
            return;
        }
        push_down(p);
        int mid = (l + r) / 2;
        modify(p * 2, l, mid, L, R, d);
        modify(p * 2 + 1, mid + 1, r, L, R, d);
        push_up(p);
    }
    ll queryTot() {
        if(mx[1] == 2) return sum[1];
        else return 0;
    }
}st[2];

int stk[2][maxn], h[2];
void solve() {
    cin >> n;
    for(int i = 1;i <= n;i++) cin >> a[i];

    st[0].fp = [](int x) { return 2 * x;};
    st[1].fp = [](int x) { return 2 * x - 1;};
    int hn = (n + 1) / 2;
    st[0].build(1, 1, hn);
    st[1].build(1, 1, hn);
    h[0] = h[1] = 0;
    stk[1][++h[1]] = stk[0][++h[0]] = 0;
    f[0] = 1;
    for(int i = 1;i <= n;i++) {
        st[i & 1].addvalue(1, 1, hn, i, f[i - 1]);

        while(h[0] > 1 && a[stk[0][h[0]]] > a[i]) {
            int f = stk[0][h[0]] & 1;
            st[f ^ 1].modify(1, 1, hn, stk[0][h[0] - 1] + 1, stk[0][h[0]], -1);
            h[0]--;
        }
        st[(i & 1) ^ 1].modify(1, 1, hn, stk[0][h[0]] + 1, i, 1);
        stk[0][++h[0]] = i;

        while(h[1] > 1 && a[stk[1][h[1]]] < a[i]) {
            int f = stk[1][h[1]] & 1;
            st[f].modify(1, 1, hn, stk[1][h[1] - 1] + 1, stk[1][h[1]], -1);
            h[1]--;
        }
        st[i & 1].modify(1, 1, hn, stk[1][h[1]] + 1, i, 1);
        stk[1][++h[1]] = i;

        f[i] = (st[0].queryTot() + st[1].queryTot()) % mode;
    }
    cout << f[n] << '\n';
}

2022杭电第八场1009

Gilneas

感觉挺妙的一个线段树上概率处理+线段树合并

对于每个点 $u$,它连向子树的边中一定只存在一条有颜色。

考虑 $u$ 的这个子树中的每个操作,将它们按照时间排序,一个操作能为 $u$ 的连边作出贡献,当且仅当这个操作成功,且之后的操作都失败。

把操作按时间放到线段树上,区间维护 $sxp$ 表示区间内所有操作的 $1-p$ 乘积(全失败的概率),和 $sum$ 表示 $c_i\times p_i\times\prod_{j>i}(1-p_{j})$ 的总和。即期望贡献。

这两个信息很好push_up。

那么对于每个点 $u$,将其子树的线段树合并起来之后取 $sum$ 即可得到它连儿子边的总贡献,注意还要考虑 $u$ 这个点本身的操作会将边权清空,计算sum时可以先将 $u$ 本身的操作的 $c_i$ 视为0加入线段树,处理贡献后再将 $c_i$ 改回来。

struct segTree {
    ll sum[maxn << 5], sxp[maxn << 5];
    int ls[maxn << 5], rs[maxn << 5];
    int tcnt = 0;
    int newnode() {
        ++tcnt;
        sum[tcnt] = 0;
        sxp[tcnt] = 1;
        ls[tcnt] = rs[tcnt] = 0;
        return tcnt;
    }
    void push_up(int p) {
        sum[p] = (sum[ls[p]] * sxp[rs[p]] % mode + sum[rs[p]]) % mode;
        sxp[p] = sxp[ls[p]] * sxp[rs[p]] % mode;
    }
    int merge(int x, int y, int l, int r) {
        if(!x) return y;
        if(!y) return x;
        assert(l != r);
        int mid = (l + r) / 2;
        ls[x] = merge(ls[x], ls[y], l, mid);
        rs[x] = merge(rs[x], rs[y], mid + 1, r);
        push_up(x);
        return x;
    }
    int modify(int p, int l, int r, int pos, ll pi, ll ci) {
        if(!p) p = newnode();
        if(l == r) {
            sxp[p] = (1ll - pi + mode) % mode;
            sum[p] = pi * ci % mode;
            return p;
        }
        int mid = (l + r) / 2;
        if(pos <= mid) ls[p] = modify(ls[p], l, mid, pos, pi, ci);
        else rs[p] = modify(rs[p], mid + 1, r, pos, pi, ci);
        push_up(p);
        return p;
    }
}st;

int n, m;
vector<int> vp[maxn];
vector<array<int, 3>> opt[maxn];

ll ans = 0;
int dfs(int p) { //返回子树的线段树根节点
    int rt = st.newnode();
    for(auto tup: opt[p]) {
        int id = tup[0], pi = tup[1];
        st.modify(rt, 1, m, id, pi, 0);
    } 
    for(int v : vp[p]) rt = st.merge(rt, dfs(v), 1, m);
    (ans += st.sum[rt]) %= mode;
    for(auto tup : opt[p]) {
        int id = tup[0], pi = tup[1], ci = tup[2];
        st.modify(rt, 1, m, id, pi, ci);
    }
    return rt;
}

void solve() {
    st.tcnt = -1;
    st.newnode(); //初始化0点
    ans = 0;
    cin >> n >> m;
    for(int i = 1;i <= n;i++) {
        vp[i].clear();
        opt[i].clear();
    }
    for(int i = 2;i <= n;i++) {
        int fa; cin >> fa;
        vp[fa].push_back(i);
    }
    for(int i = 1;i <= m;i++) {
        int x, c, p;
        cin >> x >> c >> p;
        opt[x].push_back({i, p, c});
    }
    dfs(1);
    cout << ans << '\n';
}

2022杭电第九场1001

Arithmetic Subsequence

比较难想到是构造题

需要不包含任何等差数列,一个可以切入的思考点是:等差数列要么全是奇数/偶数,要么在奇偶之间来回切换

如果把奇数放到一边,偶数放到另一边,那么必然不存在跨两边的等差数列,就可以分治求解了。

奇数部分可以集体-1,偶数部分可以集体除2,都不影响等差数列的存在性。

int n, a[maxn];

void cdq(int l, int r) {
    if(l + 1 >= r) return;
    int m = l;
    for(int i = l;i <= r;i++) if(a[i] & 1) swap(a[m++], a[i]);
    for(int i = l;i < m;i++) a[i]--;
    cdq(l, m - 1);
    for(int i = m;i <= r;i++) a[i] /= 2;
    cdq(m, r);
    for(int i = l;i < m;i++) a[i]++;
    for(int i = m;i <= r;i++) a[i] *= 2;
}

void solve() {
    cin >> n;
    for(int i = 1;i <= n;i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    for(int i = 3;i <= n;i++) if(a[i] == a[i - 1] && a[i] == a[i - 2]) {
        cout << "NO\n";
        return;
    }
    cdq(1, n);
    cout << "YES\n";
    for(int i = 1;i <= n;i++) cout << a[i] << " \n"[i == n];
}

2022杭电第十场1008

Minimum Diameter

对于静态森林,可以求出各个树的直径,将其他小直径的中点和最大直径的中点相连,答案易得。

所以问题转化为动态维护树上直径,操作只有连边

我们可以单开一个并查集记录每个树的直径以及直径两端点,合并两树时,只需考虑四个端点的两两组合,合并后的直径一定在它们当中。

使用LCT可以快速求出任意两点间距离,且支持森林连边操作。

struct LCT {
    int ch[maxn][2], fa[maxn], siz[maxn], swp[maxn];
    inline int dir(int p) { return ch[fa[p]][1] == p; }
    inline void push_up(int p) { siz[p] = 1 + siz[ch[p][0]] + siz[ch[p][1]]; }//sum[p] = sum[ch[p][0]] ^ sum[ch[p][1]] ^ val[p]; }
    inline bool isroot(int p) { return fa[p] == 0 || (ch[fa[p]][0] != p && ch[fa[p]][1] != p); } //是否为splay的根(不是原树的根!)
    inline void push_swap(int p) {
        if(!p) return;
        swap(ch[p][0], ch[p][1]), swp[p] ^= 1;
        //        push_down(p);//调试用 立即下放所有懒标记
    }
    inline void push_down(int p) {
        if(swp[p]) {
            swp[p] = 0;
            push_swap(ch[p][0]);
            push_swap(ch[p][1]);
        }
    }
    void rotate(int p) {//出bug了必须首先来查rotate 注意变化前后顺序的问题 太麻了
        if(fa[p] == 0) return;
        int f = fa[p], ff = fa[f];
        int d = dir(p), s = ch[p][d ^ 1];
        if(!isroot(f)) ch[ff][dir(f)] = p;//与普通splay不同之处:如果父亲节点已经是根节点,一定不能让ff认儿子
        fa[s] = f;
        fa[p] = ff; ch[p][d ^ 1] = f;
        fa[f] = p; ch[f][d] = s;
        push_up(f);//最后一个节点的push_up在splay那
    }
    void push_all(int p) {
        if(!isroot(p)) push_all(fa[p]);
        push_down(p);
    }
    void splay(int p) {//将p转到其所在splay(实链节点集合)的根节点
        push_all(p);//从顶向下push_down
        while(!isroot(p)) {
            if(!isroot(fa[p])) rotate(dir(p) == dir(fa[p]) ? fa[p] : p);
            rotate(p);//双旋,不管旋了父还是子,第二次旋转都要跟上,不然会假
        }
        push_up(p);
    }
    void access(int p) {//打通p所在真实树的根节点到p的路径
        int lst = 0;
        while(p) {
            splay(p);
            ch[p][1] = lst;
            push_up(p);
            lst = p;
            p = fa[p];
        }
    }
    void makeroot(int p) {//p变为其所在真实树中的根节点(同时也是splay根)
        access(p);
        splay(p);
        push_swap(p);
    }
    int findroot(int p) {//查找所在真实树的根节点,注意这个也会改变p所在splay结构
        access(p);
        splay(p);
        while(ch[p][0]) push_down(p), p = ch[p][0];
        return p;
    }
    void split(int x, int y) {//将x,y之间的路径剖出来作为splay,然后x作为原树根,y作为splay根
        makeroot(x);
        access(y);
        splay(y);
    }
    void link(int x, int y) {//连边(虚边)
        makeroot(x);
        if(x != findroot(y)) fa[x] = y;//注意,连虚边一定要让所在splay的根节点的fa连出去,这里x已经是splay根节点了而y不一定
    }
    void cut(int x, int y) {//删边
        if(findroot(x) != findroot(y)) return;
        split(x, y);//记牢此时x为原树根,y为splay根,原树根仅仅是深度最小的节点,完全可以有fa
        if(fa[x] != y || ch[x][1]) return;//要x,y之间有连边,就要它们在splay的中序遍历上相邻
        ch[y][0] = fa[x] = 0;
        push_up(y);
    }
    int dist(int x, int y) {
        split(x, y);
        return siz[y];
    }
    void init(int n) {
        for(int i = 0;i <= n;i++) ch[i][0] = ch[i][1] = fa[i] = swp[i] = 0;
    }
}lct;

int a[maxn], f[maxn];
struct diameter {
    int x, y, d;
    bool operator <(const diameter& t)const {
        return d < t.d;
    }
}d[maxn];

int find(int x) {
    if(x == f[x]) return x;
    return f[x] = find(f[x]);
}
int cnt[maxn];
void solve() {
    int n, m;
    cin >> n >> m;
    lct.init(n);
    cnt[0] = 0;
    for(int i = 1;i <= n;i++) {
        f[i] = i;
        lct.siz[i] = 1;
        d[i] = {i, i, 0};
        cnt[i] = 0;
        cnt[0]++;
    }
    int mxd = 0;
    for(int i = 1;i <= m;i++) {
        int x, y; cin >> x >> y;
        int fx = find(x), fy = find(y);
        cnt[d[fx].d]--;
        cnt[d[fy].d]--;
        lct.link(x, y);
        diameter tmp = max(d[fx], d[fy]);
        tmp = max(tmp, {d[fx].x, d[fy].x, lct.dist(d[fx].x, d[fy].x) - 1});
        tmp = max(tmp, {d[fx].x, d[fy].y, lct.dist(d[fx].x, d[fy].y) - 1});
        tmp = max(tmp, {d[fx].y, d[fy].x, lct.dist(d[fx].y, d[fy].x) - 1});
        tmp = max(tmp, {d[fx].y, d[fy].y, lct.dist(d[fx].y, d[fy].y) - 1});
        f[fx] = fy;
        d[fy] = tmp;
        cnt[tmp.d]++;
        mxd = max(mxd, tmp.d);

        // for(auto x : alld) cout << x << ' ';
        // cout << '\n';

        int mx[4], h = 0;
        for(int i = 1;h < 3 && i <= cnt[mxd];i++) mx[++h] = mxd;
        if(mxd > 0) for(int i = 1;h < 3 && i <= cnt[mxd - 1];i++) mx[++h] = mxd - 1;
        if(mxd > 1) for(int i = 1;h < 3 && i <= cnt[mxd - 2];i++) mx[++h] = mxd - 2;
        int ans = mx[1];
        if(h > 1) ans = max(ans, (mx[1] + 1) / 2 + 1 + (mx[2] + 1) / 2);
        if(h > 2) ans = max(ans, (mx[2] + 1) / 2 + 2 + (mx[3] + 1) / 2);
        cout << ans << '\n';
    }
}

COMPFEST 14 J. Journey

如果只有一个起点,答案即所有边权x2再减去树上的最长路径(直径)

现在可以取第二个起点,那么答案(要减去的部分)有两种情况:

  • 树上两条有单个点相交的路径
  • 树上两条不相交的路径,再加上两路径之间的最大边x2

对于第一种情况,考虑枚举交点,然后走四条链出去,这个比较好维护

第二种情况,枚举中间边,然后需要知道以这条边作为分割,上下两棵树的直径。

要维护这两个东西比较麻烦,具体如下:

ll dia[maxn];   // p这个子树的直径
ll dup[maxn];   // 除去p子树的直径
ll dp[maxn][4]; // p向下最大的四条链
ll dp2[maxn][2];// p儿子中最大的两个直径
ll up[maxn];    // p向上走的最长链

首先一遍 dfs 求出 dp, dp2, dia

然后再 dfs 第二遍,这一遍从上向下转移

设当前跑到边 $u\rightarrow v$,边权 $w$

up[v] 可以由 up[u] + w,以及 u子树中除了v之外的最长链 + w 转移来

dup[v] 可以由 dup[u] 以及 u子树中除了v之外的最大直径u连出的链中除了v之外的最大两条链之和 转移来

想到了这些就会发现题目可做了,然后就是分类讨论的事了(

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int maxn = 1e6 + 100;
const int mode = 1e9 + 7;
 
vector<pair<int, ll>> vp[maxn];
ll dia[maxn];   // p子树的直径
ll dup[maxn];   // 除去p子树的直径
ll dp[maxn][4]; // p向下最大的四条链
ll dp2[maxn][2];// p儿子中最大的两个直径
ll up[maxn];    // 向上最长链
 
void add1to(int p, ll val){
    for(int i = 0;i < 4;i++) {
        if(dp[p][i] < val) swap(val, dp[p][i]);
    }
}
void add2to(int p, ll val) {
    for(int i = 0;i < 2;i++) {
        if(dp2[p][i] < val) swap(val, dp2[p][i]);
    }
}
 
void dfs(int p, int fa) {
    dia[p] = 0;
    for(auto [v, w] : vp[p]) {
        if(v != fa) {
            dfs(v, p);
            add1to(p, dp[v][0] + w);
            add2to(p, dia[v]);
            dia[p] = max(dia[p], dia[v]);
        }
    }
    dia[p] = max(dia[p], dp[p][0] + dp[p][1]);
}
 
void dfs2(int p, int fa, ll& res) {
    ll tmp;
    add1to(p, up[p]);
    for(auto [v, w] : vp[p]) {
        if(v != fa) {
            up[v] = w + dp[p][dp[p][0] == dp[v][0] + w ? 1 : 0];
            ll mx1 = dp2[p][dp2[p][0] == dia[v] ? 1 : 0];
            ll mx2;
            if(dp[p][0] == dp[v][0] + w) mx2 = dp[p][1] + dp[p][2];
            else if(dp[p][1] == dp[v][0] + w) mx2 = dp[p][0] + dp[p][2];
            else mx2 = dp[p][0] + dp[p][1];
            dup[v] = max({dup[p], mx1, mx2});
 
            res = max(res, dia[v] + dup[v] + 2 * w);
            dfs2(v, p, res);
        }
    }
}
 
void solve() {
    int n;
    cin >> n;
    ll ans = 0;
    for(int i = 1;i < n;i++) {
        int u, v, w; cin >> u >> v >> w;
        vp[u].emplace_back(v, w);
        vp[v].emplace_back(u, w);
        ans += w * 2;
    }
    dfs(1, 0);
    ll res = 0;
    dfs2(1, 0, res);
    for(int i = 1;i <= n;i++) {
        if(vp[i].size() < 4) continue;
        res = max(res, dp[i][0] + dp[i][1] + dp[i][2] + dp[i][3]);
    }
    cout << ans - res << '\n';
 
}

2022 ICPC 网赛2 H

首先分别考虑贡献,因为这题中一个方案的贡献由所有 $u,v$ 间的路径长度( $u,v$ 之间没有其他选中点)组成,故去考虑每一条路径 的贡献。

一条路径计入答案,当且仅当其两端点 $u,v$ 都选中,且路径中其他点都不被选中(仅计算被直接选择时的贡献,作为子路径时不考虑,这样能做到不重复不遗漏),然后我们再去考虑这个路径会被计算多少次。

此时其他点可选可不选,对于任意一点 $i$,它对这个路径产生贡献的次数都为 $2^{n-d-2}$,那么这条路径的总贡献为:$d(n-d+3)2^{n-d-2}$

有了这个式子,只需要枚举图中所有的路径计算和即可,但这显然不现实。

考虑换根dp,对于每个点记录其向上走和向下走的所有路径贡献和,如何转移?

注意到每条路径的式子形式类似 $xy2^y$,对于许多这样的式子和,要进行全体 x+1 等类似操作并维护是很容易的,只需要维护 $x2^y,y2^y,2^y$ 和作为辅助即可。

同理进行 y-1 等操作也不难,剩下的就是一个标准的换根dp了。

ll qpow(ll x, ll p) {
    if(p < 0) return qpow(qpow(x, -p), mode - 2);
    ll r = 1;
    while(p > 0) {
        if(p & 1) r = r * x % mode;
        x = x * x % mode;
        p /= 2;
    }
    return r;
}

ll iv2 = qpow(2, mode - 2);

struct node {
    ll xxy, xy, yy, y;
    node() : xxy(0), xy(0), yy(0), y(0) {};
    node(ll a, ll b) {
        y = qpow(2, b);
        yy = b * y % mode;
        xy = a * y % mode;
        xxy = b * xy % mode;
    }
    node operator +(const node& t) {
        node r = *this;
        (r.xxy += t.xxy) %= mode;
        (r.xy += t.xy) %= mode;
        (r.yy += t.yy) %= mode;
        (r.y += t.y) %= mode;
        return r;
    }
    node operator -(const node& t) {
        node r = *this;
        (r.xxy -= t.xxy) %= mode;
        (r.xy -= t.xy) %= mode;
        (r.yy -= t.yy) %= mode;
        (r.y -= t.y) %= mode;
        return r;
    }
    node add1() {
        node r(0, 0);
        r.xxy = (xxy + yy - xy - y + 2 * mode) % mode * iv2 % mode;
        r.yy = (yy - y + mode) * iv2 % mode;
        r.xy = (xy + y) * iv2 % mode;
        r.y = y * iv2 % mode;
        return r;
    }
};

int n;
vector<int> vp[maxn];
node dw[maxn], up[maxn];

void dfs1(int p, int fa) {
    dw[p] = node(0, n + 3);
    for(int v : vp[p]) {
        if(v != fa) {
            dfs1(v, p);
            dw[p] = dw[p] + dw[v].add1();
        }
    }
}

void dfs2(int p, int fa) {
    for(int v : vp[p]) {
        if(v != fa) {
            up[v] = (up[p] + dw[p] - dw[v].add1()).add1();
            dfs2(v, p);
        }
    }
}

void solve() {
    cin >> n;
    for(int i = 1;i < n;i++) {
        int x, y; cin >> x >> y;
        vp[x].push_back(y);
        vp[y].push_back(x);
    }
    dfs1(1, 0);
    dfs2(1, 0);
    ll ans = 0;
    for(int i = 1;i <= n;i++) ans = (ans + (up[i] + dw[i] - node(0, n + 3)).xxy) % mode;
    cout << ans * qpow(64, mode - 2) % mode << '\n';
}

2022 CCPC 威海 K

题意:要求寻找区间 $[l, r](0<l\le r)$ 的个数,满足给出的 $n$ 个条件,条件形式如下:

  • 1 $k$ $x$:可以在 $[l,r]$ 中找出 $k$ 个不同数,使得他们的和为 $x$
  • 2 $k$ $x$:无法在 $[l,r]$ 中找出 $k$ 个不同数,使得他们的和为 $x$

首先注意到一点:对于任意区间 $[l,r]$,从中取 $k$ 个数的值域是连续的,最小值为 $\frac{(l+l+k-1)\times l}{2}$,最大值同理(式子不写了)

考虑第一种条件,我们可以找到这样一个区间: $[L,L+k-1]$ 总和为 $x$,或者 $[L,L+k]$,前 $k$ 个数和小于 $x$ 且后 $k$ 个数和大于 $x$。

条件一转化为:$[l,r]$ 必须包括这个区间,可以合并所有条件一得到 $l$ 的最大值和 $r$ 的最小值。

同理,条件二转化为 $[l,r]$ 必须不包括这个区间,然而这个东西在线段上比较难处理,可以在 $lOr$ 平面上考虑。

画个图发现就是求个矩形面积并,可以扫描线或者单调栈处理。

void solve() {
    int n; cin >> n;
    ll L = 2e9, R = 0;
    vector<pii> ban;
    for(int i = 1;i <= n;i++) {
        int opt, k, x;
        cin >> opt >> k >> x;

        ll l = 1, r = 2e9;
        while(l < r) {
            ll mid = (l + r) / 2;
            ll val = (mid + mid + k - 1) * k / 2;
            if(val >= x) r = mid;
            else l = mid + 1;
        }
        r = l + k - 1;
        ll val = (l + l + k - 1) * k / 2;
        if(val != x) l--;

        if(opt == 1) {
            L = min(L, l);
            R = max(R, r);
        } else {
            if(l == 0) continue;
            ban.emplace_back(l, r);
        }
    }
    if(L == 0) {
        cout << "0\n";
        return;
    }
    if(ban.empty()) {
        cout << "-1\n";
        return;
    }

    for(auto& [x, y] : ban) {
        if(x > L) x = L;
        if(y < R) y = R;
    }
    sort(ban.begin(), ban.end(), [&](pii& a, pii& b) { return a.second < b.second;});

    stack<pii> stk;
    for(auto& [x, y] : ban) {
        if(stk.empty() || x > stk.top().first) {
            stk.emplace(x, y);
        }
    }

    if(stk.top().first < L) {
        cout << "-1\n";
        return;
    }
    ll ans = 0;
    ll lst = stk.top().second;
    stk.pop();
    while(!stk.empty()) {
        auto [x, y] = stk.top();
        stk.pop();
        ans += (lst - y) * (L - x);
        lst = y;
    }
    ans += (lst - R) * L;
    cout << ans << '\n';
}

2022 CCPC 威海 F

题意:给出一个有向图,每个点有颜色 $c_i$ 和费用 $w_i$,走到一个未打标记的点时,需花费 $w_i$ 并打标记,随后你可以选择任意个颜色不同的节点 $j$,回收上面的标记并返还 $w_j$ 的费用。

问:对于每一对起点终点,初始需要多少钱才能抵达。 $n \le 300$

显然当我们走到一个点时,会选择回收所有不同颜色的节点的费用,

那么对于一条路径,把它拆分为若干颜色段,走到每一段时都会回收前一段的费用,那么这段路径的代价就是MAX(每一段的费用和+下一段第一个点的费用,最后一段的费用)

就变成了一个瓶颈路的问题。

具体做法:把原图划分为若干个同颜色的连通块,在每个连通块内部floyd求出两两间最短路,然后枚举块内的起点($s$)和终点($t$),再枚举终点向块外连的边($t\rightarrow j$),在新图上建一条 $s\rightarrow j$ 的边。

在新图上枚举起点,朴素prim跑最小瓶颈路($n^2$)

这样就将 “每一段的费用和+下一段第一个点的费用” 考虑完了,还需要考虑最后一段的费用,这个不能直接在块内连边,因为我们要求最小瓶颈路,边权不能“合成”。(这个词用的比较抽象,自行理解一下)

只需要在最后输出前,用块内其他点+块内距离松弛一次即可,同样因为边不能“合成”,松弛后的结果不能更新到最短路中,而应当直接输出。

ll dis[303][303];
int col[303], w[303];
bool vis[303];
 
vector<int> vp1[303];
ll dis2[303][303];
 
void dfs(int p, vector<int>& ls) { //染色
    vis[p] = true;
    ls.push_back(p);
    for(int v : vp1[p]) {
        if(!vis[v] && col[v] == col[p]) {
            dfs(v, ls);
        }
    }
}
 
void solve() {
    int n, m;
    cin >> n >> m;
    fill(dis[0], dis[0] + 303 * 303, 1e18);
    fill(dis2[0], dis2[0] + 303 * 303, 1e18);
    fill(vis, vis + n + 1, false);
    for(int i = 1;i <= n;i++) {
        vp1[i].clear();
    }
    for(int i = 1;i <= n;i++) cin >> col[i];
    for(int i = 1;i <= n;i++) cin >> w[i], dis[i][i] = w[i];
    for(int i = 1;i <= m;i++) {
        int x, y; cin >> x >> y;
        vp1[x].push_back(y);
        vp1[y].push_back(x);
        dis[x][y] = dis[y][x] = w[x] + w[y];
    }
    
    for(int i = 1;i <= n;i++) {
        if(!vis[i]) {
            vector<int> ls;
            dfs(i, ls);
            for(int k : ls) {
                for(int i : ls) {
                    for(int j : ls) {
                        if(dis[i][k] + dis[k][j] - w[k] < dis[i][j]) {
                            dis[i][j] = dis[j][i] = dis[i][k] + dis[k][j] - w[k];
                        }
                    }
                }
            }
            for(int i : ls) {
                for(int j : ls) {
                    for(int v : vp1[j]) {
                        if(col[j] != col[v]) {
                            dis2[i][v] = min(dis2[i][v], dis[i][j] + w[v]);
                        }
                    }
                }
            }
        }
    }
 
    vector ans(n + 1, vector<ll>(n + 1, 1e18));
    for(int i = 1;i <= n;i++) {
        ans[i][i] = w[i];
        fill(vis, vis + n + 1, false);
        for(int cc = 1;cc <= n;cc++) { //prim
            int p = 0; ans[i][0] = 1e18;
            for(int j = 1;j <= n;j++) {
                if(!vis[j] && ans[i][j] < ans[i][p]) p = j;
            }
            vis[p] = true;
            for(int v = 1;v <= n;v++) {
                ans[i][v] = min(ans[i][v], max(ans[i][p], dis2[p][v]));
            }
        }
    }
 
    for(int i = 1;i <= n;i++) {
        for(int j = 1;j <= n;j++) {
            ll opt = ans[i][j];
            // 最后一步可以使用dis
            for(int k = 1;k <= n;k++) {
                opt = min(opt, max(ans[i][k], dis[k][j]));
            }
            if(i == j) opt = 0;
            cout << opt << " \n"[j == n];
        }
    }
}

高质量补题记录集
http://www.lxtyin.ac.cn/2022/09/28/2022-09-28-高质量补题记录集/
作者
lx_tyin
发布于
2022年9月28日
许可协议