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

分类 笔记 下的文章

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. 赢钱 (money)

人类智慧题, 好像是写成二进制小数, 每次用 Least Significant Bit 去赌? 不是很能理解...

B. 排列 (per)

贡献可以分成五类: 环 (长度 $> 1$); 自环; 链 (长度 $> 2$); 链 (长度 $= 2$); 点. 记问题的答案为 $ans$, 一类贡献即将 $ans$ 乘 $2$, 二类贡献对 $ans$ 没有影响, 这两类都可以最后来计算, 先不管. 分别记三 / 四 / 五类贡献的数量为 $a$ / $b$ / $c$, 接下来进行分析.

这总共 $a+b+c$ 个元素对答案贡献是: 任意分组, 将组内元素拼成环的方案数之和之积. 但注意, 三类贡献由于自身可以反转, 因此要 $\times 2$, 这部分也放到最后来考虑, 而五类贡献不需要, 四类贡献最特殊, 它在自成一组的情况下不需要 $\times 2$, 否则需要. 考虑二项式反演, 设 $g_i$ 为至少有 $i$ 个四类元素自成一组的答案 (不考虑四类贡献的 $\times 2$ 1), $f_i$ 为恰好 (考虑四类贡献的 $\times 2$). 则由二项式反演, 有:

$$ f_i = 2^{b-i}\sum_{j=i}^b (-1)^{j-i}\binom ji g_j $$

考虑 $g_i$ 的求法, 有:

$$ g_i = \binom bi (a+b+c-i)! $$

为什么后面那部分是阶乘? 考虑已经加入了 $1 \sim i-1$, 现在加入 $i$, 它只能接在某个元素的后面, 或者自成一组, 一共 $i$ 种方案, 乘起来就是阶乘.

则:

$$ \begin{aligned} &\sum f_i \\ =&~2^{b-i}\sum_{i=0}^b\sum_{j=i}^b(-1)^{j-i}\binom ji g_j \\ =&~2^b\sum_{j=0}^bg_j\sum_{i=0}^j\left(\frac 12\right)^i(-1)^{j-i}\binom ji \\ =&~2^b\sum_{j=0}^bg_j \left(-\frac 12\right)^j \end{aligned} $$

直接计算即可. 说起来比较麻烦, 其实还是比较简单的, 应该是真正的签到题.

C. 箱子 (box)

贪心策略就是选择极长区间操作, 证明可以考虑贡献形式是求和, 因此操作区间不会相交, 只会包含或无交. 于是用线段树维护左右端点的信息, 区间的答案, 区间如果全部同色的答案 (为了区间覆盖操作) 即可.

D. 排列 (permutation)

学了下高一同学的模拟退火.


/ When thy inconsiderate hand /

/ Flings ope this casement, with my trembling name, /

/ To look on one, whose wit or land, /

/ New battery to thy heart may frame, /

/ Then think this name alive, and that thou thus /

/ In it offend’st my Genius./

—— [John Donne - A Valediction of My Name in the Window]()


  1. 为什么不考虑? 如果在 $g_i$ 的阶段就计算了, 后面就无法套用二项式定理优化复杂度了.

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

Desc.

Link.

有 $h$ 条水平道路和 $w$ 条竖直道路, 在其上行走一个单位分别花费 $a_i$ 和 $b_j$, 问从 $(1, 1)$ 到 $(h, w)$ 的最小花费.

Sol.

把步数拆到恰好拐一个弯的形状, 设从 $(i, j)$ 走到 $(i', j')$. 有两种方式, 其中一种是 $(i, j) \rightarrow (i', j) \rightarrow (i', j')$. 整理得到一个斜率形式. 合并两者凸包. (赶着回家, 写得比较草率...)

int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    int n, m; cin >> n >> m;
    vll a(n), b(m); rds(a), rds(b);
#define fi first
#define se second
    using pii = pair<ll, ll>;
    const auto cross = [&](const pii& a, const pii& b) {
        return a.fi*b.se-a.se*b.fi;
    };
    static int s1[100100], s2[100100], top1, top2;
    for (int i=0;i<n;++i) {
        while (top1 > 1 && cross({s1[top1]-s1[top1-1], a[s1[top1]]-a[s1[top1-1]]}, {i-s1[top1], a[i]-a[s1[top1]]}) < 0)
            top1--;
        s1[++top1] = i;
    }
    for (int i=0;i<m;++i) {
        while (top2 > 1 && cross({s2[top2]-s2[top2-1], b[s2[top2]]-b[s2[top2-1]]}, {i-s2[top2], b[i]-b[s2[top2]]}) < 0)
            top2--;
        s2[++top2] = i;
    }
    int i = 1, j = 1;
    ll ans = 0;
    while (i < top1 || j < top2) {
        if (i < top1 && (j == top2 || cross({s2[j+1]-s2[j], b[s2[j+1]]-b[s2[j]]}, {s1[i+1]-s1[i], a[s1[i+1]]-a[s1[i]]}) < 0)) {
            ans += (s1[i+1]-s1[i])*b[s2[j]];
            i++;
        } else {
            ans += (s2[j+1]-s2[j])*a[s1[i]];
            j++;
        }
    }
    cout << ans << "\n";
}

Desc.

Link.

求出满足如下条件的 $1$ 到 $n$ 的排列数量 (记排列为 $P = (p_1, \dots, p_n))$:

  • $\forall i \in [1, n]$, 有 $\mid p_i-i\mid \geqslant X$.

$1 \leqslant n \leqslant 100$, $1 \leqslant X \leqslant 5$.

Sol.

$X$ 很小, 从这里入手. 注意到 $\geqslant X$ 的状态可能有很多, 但 $< X$ 的状态比较少, 因此计算补集. 考虑容斥, 设 $g_i$ 表示有 $i$ 个数不合法, 剩余数随意放置的方案数, $f_i$ 为恰好 $i$ 个不合法, 剩余数均合法的方案数. 有:

$$ g_i = \sum_{j=i}^n f_j \times \binom ji $$

由二项式反演:

$$ f_i = \sum_{j=i}^n (-1)^{j-i} \times \binom ji \times g_j $$

答案即为 $\displaystyle f_0 = \sum_{i=0}^n (-1)^i \times g_i$. 考虑怎么求 $g_i$. 设 $f_{i, j, s}$ 表示前 $i$ 个位置有 $j$ 个不合法, 其余位置不纳入考虑1, 且第 $i$ 个数的不合法区间当前的被占用情况为 $s$ 的方案数. 转移分当前数是否加入不合法讨论即可, 具体可以看代码.

int main() {
    ios::sync_with_stdio(0);
    cin.tie(nullptr);
    const int MOD = 998244353;
    using mint = modint<MOD>;
    int n, X; cin >> n >> X;
    const int LIM = 1<<(2*X-1), U = LIM-1;
    vector f(n+1, vector(n+1, vector<mint>(LIM)));
    f[0][0][0] = 1;
    for (int i=0;i<n;++i) {
        for (int j=0;j<=i;++j) {
            for (int s=0;s<LIM;++s) {
                f[i+1][j][s>>1] += f[i][j][s];
                for (int p=0;p<2*X-1;++p)
                    if (!((s>>1)&(1<<p)) && i+2-X+p >= 1 && i+2-X+p <= n)
                        f[i+1][j+1][(s>>1)|(1<<p)] += f[i][j][s];
            }
        }
    }
    vector<mint> g(n+1), facs(n+1);
    facs[0] = 1;
    for (int i=1;i<=n;++i)
        facs[i] = facs[i-1]*i;
    for (int i=0;i<=n;++i)
        g[i] = accumulate(allu(f[n][i]), mint(0));
    mint ans = 0;
    for (int i=0;i<=n;++i)
        ans += (i&1?MOD-1:1)*g[i]*facs[n-i];
    cout << ans << "\n";
}

/ 来不及拒绝 /

/ 黑与白的纯洁 /

/ 视而不见 /

/ 假装不见 /

/ 凝望成永别 /

—— Zeno - D!SCOLOЯ ft. 星尘


  1. 为什么其余位置不纳入考虑? 因为随意放置的方案数可以通过乘阶乘计算, 放入 DP 的计算里会很麻烦!