In mathematics you don't understand things, you just get used to them.

标签 dp 下的文章

我说怎么 T2 被暴切,原来大家都做过这道题,这下是 🤡 了。

Desc.

Link.

有 $m$ 次事件,和一个初始为空的双端队列,格式如下:

  • IF w v:$\text{PUSH-FRONT}(w, v)$;
  • IG w v:$\text{PUSH-BACK}(w, v)$;
  • DF:$\text{POP-FRONT}$;
  • DG:$\text{POP-BACK}$;
  • QU l r:询问在当前队列中选取若干二元组 $S$,使得 $\sum w \in S \bmod p \in [l, r]$,且 $\sum v \in S$ 最大。

$m \leqslant 5 \times 10^4$,$p \leqslant 500$。

Sol.

朴素 DP 很容易,设 $f_{i, s}$ 表示前 $i$ 个元素中,$w$ 和模 $p$ 的余数为 $s$ 的最大 $v$ 和。关键是如何维护双端队列的操作。

如果维护的数据结构是栈,那么这个题就变得很容易了,恰好,我们有用栈实现双端队列的方法,以下是 tly 的解说:

至少支持:双端插入删除(删除时非空),维护半群运算(只有结合律),做到均摊 $\mathcal O(n)$ 次运算。

比如双端插入删除元素,求矩阵乘或者 min 之类的不可减信息。

……

具体做法是维护两个栈,分别用于前端插入删除/后端插入删除。这个时候就是「插入同端删除,也即可撤回」的情况了。

如果一个栈空了,这个时候就不能直接把另一个栈弄过来。

考虑将另一个栈的元素按中点划分开,分别装入两个栈,这样复杂度是均摊 $\mathcal O(n)$。
39
具体原因考虑势能函数 $\Phi = |size_1 - size_2|$,每次 $\Phi$ 至多增加 $1$,重构则清零(或变成 $1$),因此复杂度均摊下来线性。

(有删改)

代码很简洁。

int __tmp, m, p, w[2][50100], v[2][50100], top[2], q[600];
ll dp[2][50100][600];
string op;
void insert(int a, int b, int f) {
    w[f][++top[f]] = a, v[f][top[f]] = b;
    for (int i=0;i<p;++i)
        dp[f][top[f]][(i+a)%p] = max(dp[f][top[f]-1][i]+b, dp[f][top[f]-1][(i+a)%p]);
}
void remove(int f) {
    if (top[f] == 0) {
        int tmp = top[f^1];
        top[0] = top[1] = 0;
        for (int i=(tmp+1)/2;i>=1;--i) insert(w[f^1][i], v[f^1][i], f);
        for (int i=(tmp+1)/2+1;i<=tmp;++i) insert(w[f^1][i], v[f^1][i], f^1);
    }
    top[f]--;
}
ll ask(int l, int r) {
    ll res = -1, *f = dp[0][top[0]], *g = dp[1][top[1]];
    int h = 1, t = 0;
    for (int i=r;i>=l;--i) {
        while (h <= t && g[i] >= g[q[t]]) t--;
        q[++t] = i;
    }
    for (int i=0;i<p;++i) {
        while (h <= t && ((i+q[h])%p < l || (i+q[h])%p > r)) h++;
        while (h <= t && g[(l+p-i)%p] >= g[q[t]]) t--;
        q[++t] = (l+p-i)%p;
        cmax(res, f[i]+g[q[h]]);
    }
    return res;
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    for (int i=0;i<2;++i)
        memset(dp[i][0]+1, 0xcf, sizeof dp[i][0]);
    cin >> __tmp >> m >> p;
    for (int a, b; m--;) {
        cin >> op;
        if (op[0] == 'I' || op[0] == 'Q') cin >> a >> b;
        if (op == "IF") insert(a%p, b, 0);
        else if (op == "IG") insert(a%p, b, 1);
        else if (op == "DF") remove(0);
        else if (op == "DG") remove(1);
        else cout << ask(a, b) << "\n";
    }
}

我既没有愁苦到足以成为诗人,又没有冷漠到像个哲学家。但我清醒到足以成为一个废人。

—— E·M·齐奥朗 - 眼泪与圣徒

Desc.

Link.

给出 $n$ 个最高位不超过 $m$ 二进制数, 对于每个 $i \in [1, n]$, 找到一个 $j \neq i$, 使得删去 $i$ 和 $j$ 后, 满足票数超过 $\lfloor \frac n2 \rfloor$ 的数位数最大. 某个二进制数的某一位为 $1$ 就相当于给这一位投票.

$3 \leqslant n \leqslant 3\times 10^5$, $1 \leqslant m \leqslant 20$.

Sol.

这篇题解写得很玄学, 不好理解, 建议不看.

考虑如下暴力:

int n, m, s[300100], cnt[30];
int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    cin >> n >> m;
    for (int i=0,x;i<n;++i)
        for (int j=0;j<m;++j) {
            cin >> x;
            s[i] |= x<<j;
            cnt[j] += x;
        }
    for (int i=0;i<n;++i) { // enumerating chairman
        for (int j=0;j<m;++j) cnt[j] -= s[i]>>j&1;
        int ans = 0;
        for (int j=0;j<m;++j) ans += cnt[j] >= n/2;
        int sw = 0; // set waiting to be determined
        for (int j=0;j<m;++j)
            sw |= (cnt[j] == n/2)<<j;
        int ext = 233;
        for (int j=0;j<n;++j)
            if (j != i) chkmin(ext, __builtin_popcount(s[j]&sw));
        cout << ans-ext << "\n";
        for (int j=0;j<m;++j) cnt[j] += s[i]>>j&1;
    }
}

那么问题转化为, 对于 $S$, 求 $\min\{\mid S \cup T_i \mid\}$, 将 $T_i$ 取补集, 则求 $\max\{\mid S\cup T_i\mid\}$. 令 $g_S$ 表示与 $S$ 的交集最大的 $T_i$ 的 $i$. 由于交集一定是 $S$ 的子集, 因此可以从子集转移过来. 很难说, 只能意会, DJ 给我讲了半天才懂.

大概是, 首先求出 $S$ 的某个超集的编号, 这个可以直接用超集和. 然后以这个为初值, 求解上述的 DP. 当然, 正确的思考顺序是为了上面的 DP 构造初值, 这才是我们的 Motivation.

int n, m, s[300100], cnt[30];
int f[1<<20], g[1<<20];
int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    cin >> n >> m;
    memset(f, -1, sizeof f);
    memset(g, -1, sizeof g);
    for (int i=0;i<n;++i) {
        for (int j=0,x;j<m;++j) {
            cin >> x;
            cnt[j] += x;
            s[i] |= (x^1)<<j;
        }
        if (f[s[i]] == -1) f[s[i]] = i;
        else g[s[i]] = i;
    }
    // == computing f[i], such that @i \in @s[f[i]]
    for (int i=(1<<m)-1;i>=0;--i) {
        for (int j=0;j<m;++j) {
            if (!(i&(1<<j))) {
                int ni = i|(1<<j);
                if (f[i] == -1) {
                    f[i] = f[ni];
                    if (g[ni] != f[i] && g[i] == -1) g[i] = g[ni];
                } else {
                    if (f[i] != f[ni] && g[i] == -1) g[i] = f[ni];
                    else if (f[i] != g[ni] && g[i] == -1) g[i] = g[ni];
                }
            }
        }
    }
    // == end computing f[i]
    // == computing f[i], such that |i&s[f[i]]| is max
    for (int i=0;i<1<<m;++i) {
        for (int j=0;j<m;++j) {
            if (i&(1<<j)) {
                int ni = i^(1<<j);
                vi v;
                for (int x : {f[i], g[i], f[ni], g[ni]})
                    if (x > -1) v.pb(x);
                if (v.empty()) continue;
                sort(v.begin(), v.end(), [&](int x, int y) {
                    return __builtin_popcount(s[x]&i) > __builtin_popcount(s[y]&i);
                });
                v.erase(unique(v.begin(), v.end()), v.end());
                f[i] = v[0];
                if ((int) v.size() > 1) g[i] = v[1];
            }
        }
    }
    // == end computing f[i]
    for (int i=0;i<n;++i) {
        for (int j=0;j<m;++j) cnt[j] -= !(s[i]>>j&1);
        int ans = 0, cur = 0;
        for (int j=0;j<m;++j) ans += cnt[j] > n/2;
        for (int j=0;j<m;++j) cur |= (cnt[j] == n/2)<<j;
        if (i == f[cur]) ans += __builtin_popcount(cur&s[g[cur]]);
        else ans += __builtin_popcount(cur&s[f[cur]]);
        cout << ans << "\n";
        for (int j=0;j<m;++j) cnt[j] += !(s[i]>>j&1);
    }
}

/ 板斜尿流急,坑深屎落迟。/

—— [梁实秋 - 忆清华 [上]](https://s.bailushuyuan.org/novel/traditional/chapters/123805)

Desc.

Link.

给定 $n$ 个区间 $[l_i, r_i)$, 问是否能选出恰好 $k$ 区间, 使得两两无交, 并构造一组字典序最小的解.

$1\leqslant n \leqslant 10^5$.

Sol.

一道套着信息学奥赛外壳的 HOMO 题, 撅了好久, やりますね.

考虑如何最小化字典序. 已知前 $i-1$ 个数的选择情况 $S \in 2^{\{1, \dots i-1\}}$, 现在对 $i$ 进行决策. 只需要判断后 $n-i$ 个元素中是否能选出 $k - |S| - 1$ 个无交元素即可, 若能则将 $i$ 加入. 维护 $f(L, R)$ 表示在数轴 $[L, R]$ 上能选出的最大不相交区间数, 于是加入 $i$ 造成的变化就是 $f(L, l-1)+f(r+1, R)-f(l, r)+1$.

于是问题转化成了维护 $f(L, R)$, 由于区间无交, 我们贪心地选择当前能加入的区间中右端点最小的, 于是可以倍增. 令 $f[i][j]$ 为从数轴上 $j$ 出发, 选择 $2^i$ 个无交区间, 能到达地最小右端点. 直接转移即可.

注意 $f$ 的初值, 由于同一个 $l_i$ 可以对应多个 $r_j$, 因此要取 $\min$.

struct Interval {
    int l, r;
    Interval(int _l, int _r) : l(_l), r(_r) {}
    bool operator<(const Interval& rhs) const { return r < rhs.l; }
};
int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    int n, k;
    cin >> n >> k;
    vi l(n), r(n), uni;
    for (int i=0;i<n;++i) {
        cin >> l[i] >> r[i];
        uni.pb(l[i]), uni.pb(r[i]);
    }
    sort(allu(uni));
    uni.erase(unique(allu(uni)), end(uni));
    for (int i=0;i<n;++i) {
        l[i] = lower_bound(allu(uni), l[i])-begin(uni);
        r[i] = lower_bound(allu(uni), r[i])-begin(uni)-1;
    }
    const int m = uni.size();
    const int t = __lg(k)+1;
    vvi f(t, vi(m, m));
    for (int i=0;i<n;++i) chkmin(f[0][l[i]], r[i]);
    for (int i=m-2;i>=0;--i) chkmin(f[0][i], f[0][i+1]);
    for (int i=1;i<t;++i)
        for (int j=m-1;j>=0;--j) {
            if (j+1 < m) f[i][j] = f[i][j+1];
            if (f[i-1][j]+1 < m) chkmin(f[i][j], f[i-1][f[i-1][j]+1]);
        }
    /// @returns: the maximum number of disjoint segments in [@cl, @cr]
    const auto calc = [&](int cl, int cr) {
        if (cl > cr) return 0;
        int res = 0;
        for (int i=t-1;i>=0;--i)
            if (f[i][cl] <= cr) cl = f[i][cl]+1, res += 1<<i;
        return res;
    };
    int tot = calc(0, m-1), cnt = 0;
    if (tot < k) {
        cout << "-1\n";
        return 0;
    }
    set<Interval> s;
    s.emplace(0, m-1);
    for (int i=0;i<n;++i) {
        if (cnt == k) break;
        auto it = s.lower_bound(Interval(l[i], r[i]));
        if (it == s.end()) continue;
        const auto [cl, cr] = *it;
        if (cl > l[i] || cr < r[i]) continue;
        int dlt = calc(cl, l[i]-1)+calc(r[i]+1, cr)-calc(cl, cr)+1;
        if (tot+dlt >= k) {
            cnt++;
            tot += dlt;
            s.erase(it);
            if (cl < l[i]) s.emplace(cl, l[i]-1);
            if (cr > r[i]) s.emplace(r[i]+1, cr);
            cout << i+1 << "\n";
        }
    }
}

/ 寒星坠地 白鸟飞去 /

/ 千宵灯火转眼便览尽 /

—— COP - 光与影的对白 ft. 洛天依

Desc.

Link.

有 $N$ 个商品, 每个商品有个种类 $a_i$, 有个价格 $c_i$.

对于第 $j$ 个种类, 必须购买个数位于 $[x_j,y_j]$ 的商品, 即最少购买 $x_j$ 个, 最多购买 $y_j$ 个该种类的商品.

您需要求出前 $K$ 种便宜的方案所需的价钱, 如果没有这样的方案, 请输出 -1.

Sol.

学习了 command_block 做法.

让我们用 $S$ 来表示一个方案, $\mathrm {trans} (S)$ 表示 $S$ 的后继 (不优于 $S$ 的方案, 具体有哪些需据情况而定). 如果 $\mathrm {trans} (S)$ 包含所有的后继显然是不优的. 对于这类题目, 我们要做的就是优化 $\mathrm {trans} (S)$ 的定义 (或构造方式) 使得其大小被时空限制所接受.

本题可以被分为两个部分, 分别是「$m = 1$」和「$l = r$」. 因为如果解决了后者, 我们就可以在此基础上用前者来求出答案了. 具体的优化过程就略过不述了, 仅作为方法在此记录.

const int N = 2e5;
const ll INF = 1.01e18;
struct Getter {
    struct Tuple {
        int x, y, z;
        ll w;
        Tuple(int _x, int _y, int _z, ll _w)
            : x(_x), y(_y), z(_z), w(_w) {}
        bool operator<(const Tuple& rhs) const { return w > rhs.w; }
    };
    vll cseq;
    vi seq;
    priority_queue<Tuple> decisions;
    int _n;
    int lower, upper;
    void init() {
        _n = seq.size();
        sort(allu(seq));
        if (lower > _n) return void(cseq = vll{INF, INF});
        chkmin(upper, _n);
        decisions.emplace(lower-2, lower-1, _n-1,
                accumulate(&seq[0], &seq[lower], 0ll));
        next(0), next(1);
    }
    void next(int k) {
        if (k < (int)cseq.size()) return;
        if (decisions.empty()) return void(cseq.pb(INF));
        const auto [x, y, z, w] = decisions.top();
        decisions.pop();
        cseq.pb(w);
        if (z == _n-1 && x+1 == y && y+1 < upper)
            decisions.emplace(x+1,y+1, z, w+seq[y+1]);
        if (y >= 0 && y+1 <= z)
            decisions.emplace(x, y+1, z, w-seq[y]+seq[y+1]);
        if (x >= 0 && x+1 < y)
            decisions.emplace(x-1, x+1, y-1, w-seq[x]+seq[x+1]);
    }
} total[N+5];
struct Decision {
    int p, idx;
    ll w;
    Decision(int _p, int _i, ll _w)
        : p(_p), idx(_i), w(_w) {}
    bool operator<(const Decision& rhs) const { return w > rhs.w; }
};
int n, m, k;
int main() {
    ignore = freopen("plan.in", "r", stdin);
    ignore = freopen("plan.out", "w", stdout);
    ios::sync_with_stdio(0);
    IN.tie(nullptr);
    IN > n > m > k;
    for (int i=0,fu,ck;i<n;++i)
        IN > fu > ck, total[fu].seq.pb(ck);
    for (int i=1;i<=m;++i)
        IN > total[i].lower > total[i].upper;
    for (int i=1;i<=m;++i)
        total[i].init();
    sort(total+1, total+m+1, [&](const Getter& lhs, const Getter& rhs) {
        return lhs.seq[1]-lhs.seq[0] < rhs.seq[1]-rhs.seq[0];
    });
    priority_queue<Decision> heap; {
        ll s0 = 0;
        for (int i=1;i<=m;++i)
            s0 += total[i].cseq[0], chkmin(s0, INF);
        heap.emplace(0, 0, s0);
    }
    int fuck = 0;
    while (fuck < k && !heap.empty()) {
        const auto [p, idx, w] = heap.top();
        heap.pop();
        if (w >= INF) break;
        OUT < w < "\n"; ++fuck;
        if (p < m && idx == 1)
            heap.emplace(p+1, 1, w-total[p].cseq[1]+total[p].cseq[0]-total[p+1].cseq[0]+total[p+1].cseq[1]);
        if (p < m)
            heap.emplace(p+1, 1, w-total[p+1].cseq[0]+total[p+1].cseq[1]);
        if (p >= 1) {
            total[p].next(idx+1);
            heap.emplace(p, idx+1, w-total[p].cseq[idx]+total[p].cseq[idx+1]);
        }
    }
    for (;fuck<k;++fuck) OUT < "-1\n";
}

/ When that I was young and a little tiny boy, /

/  With hey, ho, the wind and the rain, /

/ A foolish thing was but a toy, /

/  For the rain it raineth every day. /

—— William Shakespeare - In Twelfth Night

Link.

非常无语. 😅

A. 道路 (road)

总共四种情况, 可分为拐一次弯和拐两次弯, 注意特判同行同列即可.

B. 烟花 (firework)

转化后的题意就是, 每次选择一条边, 将边连接的两点缩成一点, 求在该点子树中选择不超过 $m$ 个儿子, 儿子们的子树大小之积之和. 朴素做法就是枚举断边, 令 $f_{i, j}$ 表示在前 $i$ 个儿子中选择了 $j$ 个的积之和, 转移即:

$$ f_{i, j} \rightarrowtail f_{i+1,j} \\ f_{i, j} \times a_i \rightarrowtail f_{i+1,j+1} $$

其中 $a_i$ 表示第 $i+1$ 个儿子的子树大小 (0-indexed). 第一维可以滚掉. 发现这个贡献形式是可撤销的, 于是遍历到每条边时, 将端点之间的贡献消除即可.

D. 树上的数 (tree)

一步一步翻译题意.

  1. 存在相邻边编号更小: 由于第一步加入的编号是 $1$, 就意味着我们一定按从小到大的顺序加载编号, 否则就会出现中间断开的情况;
  2. 极差之和最小: 由 (1) 得, 点 $u$ 编号最小的连边一定是父边, 那么编号最大的连边就是子树中最后被加载的儿子与 $u$ 连边的编号. 由于贡献是极差, 只与差值相关, 因此可以直接应用贪心, 按 BFS 序从子树大小最小的儿子加到重儿子, 这样就能获得最小的极差. 那么最小的极差和即为 $\displaystyle \sum_u size_u-size_{son_u}$.
  3. 断边的影响: 看了一多小时还不会, 先坑了. 🕳

/ 路在拥挤的行人中落寞孤独,因为它得不到行人的爱慕。 /

/ The road is lonely in its crowd for it is not loved. /

—— Rabindranath Tagore - Stray Birds