L6-省选模拟1 A & B 题解

A. 商店购物(DP,组合数学)

题目描述

在 Byteland 一共开着 \(n\) 家商店,编号依次为 1 到 \(n\),其中编号为 1 到 \(m\) 的商店有日消费量上限,第 \(i\) 家商店的日消费量上限为 \(w_i\)

Byteasar 每次购物的过程是这样的:依次经过每家商店,然后购买非负整数价格的商品,并在结账的时候在账本上写上在这家商店消费了多少钱。当然,他在这家商店也可以什么都不买,然后在账本上写上一个 0。

这一天, Byteasar 日常完成了一次购物,但是他不慎遗失了他的账本。他只记得自己这一天一共消费了多少钱,请写一个程序,帮助 Byteasar 计算有多少种可能的账单。

题意

一个人去 \(n\) 个商店购物,其中 \(m\) 家商店有消费上限,第 \(i\)\(1\le i\le m\))家商店的消费上限为 \(w_i\)

已知总花费 \(k\),求消费方案数。答案对 \(10^9+7\) 取余。

对于 \(20\%\) 的数据,\(n=m\)\(1\le n,m,w_i\le100\)\(1\le k\le1000\)

对于 \(100\%\) 的数据,\(1\le m\le n\)\(1\le m\le300\)\(1\le n,k\le5\times10^6\)\(0\le w_i\le300\)

思路

对于前 \(m\) 家商店我们选择 DP 处理,对于后 \(n-m\) 家商店我们利用组合数学求解。

\(W_i=\sum_{j=1}^iw_j\)。设 \(f(i,j)\) 表示在前 \(i\) 个商店总消费为 \(j\) 的方案数。

初值 \(f(0,0)=1\),状态转移方程为

\[f(i,j)=\sum_{k=0}^{\min\{w_i,j\}}f(i-1,j-k), \]

其中 \(1\le i\le m\)\(0\le j\le W_i\)

但是如果这样直接枚举,时间复杂度为 \(O(mw_iW_n)\),毫无疑问会喜提一个大 TLE 超时。

我们观察到,这个式子是一个连续和的形式,也就是说我们可以处理出 前缀和 加快计算。

具体来说,设 \(g(i,j)=\sum_{k=0}^jf(i,k)\)。那么上面的式子可以化为

\[f(i,j)=g(i-1,j)-g(i-1,j-w_i-1)\qquad(\mathrm{if\ }j-w_i-1\ge0). \]

求出所有 \(f(i,j)\) 后再求出所有前缀和 \(g(i,j)\)。这样就可以做到 \(O(mW_n)\),可以通过。

如果进一步优化,数组空间上还可以滚动掉一维,并且 \(f\)\(g\) 可以用同一个数组。具体见代码。

下面考虑后面 \(n-m\) 个商店。发现就是一个球盒问题,钱数看作球,商店看作盒子,球同盒不同,且盒允许空。

设有 \(a\) 个球,\(b\) 个盒子。如果盒不允许空,那么相当于把 \(a\) 个球分成 \(b\) 组。采用隔板法,在 \(a-1\) 个球的间隙中放 \(b-1\) 个隔板,结果为 \(\dbinom{a-1}{b-1}\)

如果盒允许空,那么相当于 先多放 \(b\) 个球,分完组再从每组取出来 \(1\) 个,可以发现这样不影响方案数。于是结果为 \(\dbinom{a+b-1}{b-1}\)

在本题中,枚举 \(j\in[0,W_n]\),则 \(a=k-j\)\(b=n-m\)。于是最后的答案为

\[\sum_{j=0}^{W_n}f(j)\times\binom{k-j+n-m-1}{n-m-1}. \]

另外,组合数需要线性求阶乘的逆元,这样时间复杂度为 \(O(n+k)\) 才能通过。

\(ifac(i)=(i!)^{-1}\)。由于 \((i!)\times ifac(i)\equiv1\equiv(i-1)!\times ifac(i-1)\times i\times i^{-1}\),所以 \(ifac(i)\equiv ifac(i-1)\times i^{-1}\)

而线性求逆元式子为 \(i^{-1}\equiv(p-\left\lfloor p/i\right\rfloor)\times(p\bmod i)^{-1}\)。这样就可以线性求出阶乘的逆元。

证明:设 \(p=ki+r\),即 \(k=\left\lfloor p/i\right\rfloor\)\(r=p\bmod i\),则 \(ki+r\equiv0\pmod p\)

两边乘以 \(i^{-1}r^{-1}\)\(kr^{-1}+i^{-1}\equiv0\pmod p\)

所以 \(i^{-1}\equiv-kr^{-1}\),即 \(i^{-1}\equiv(p-\left\lfloor p/i\right\rfloor)\times(p\bmod i)^{-1}\)

注意特判 \(n=m\) 的情况!!!!!不然会挂 20 pts = =

代码

#include <cstdio>
#include <iostream>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
#define FILENAME "shopping"
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int M = 3e2 + 10, N = 1e7 + 10;
const int MOD = 1e9 + 7;
int n, m, k, w[M], fac[N], ifac[N], inv[N], f[M * M], sum, ans;

inline int Mul(int const &a, int const &b) { return 1ll * a * b % MOD; }
inline int &AddEq(int &a, int const &b) { return (a += b) >= MOD ? (a -= MOD) : a; }
inline int &SubEq(int &a, int const &b) { return (a -= b) < 0 ? (a += MOD) : a; }

void get_fac() {
    fac[0] = ifac[0] = 1;
    inv[1] = 1;
    f(i, 2, n + k) inv[i] = Mul(MOD - MOD / i, inv[MOD % i]);
    f(i, 1, n + k) fac[i] = Mul(fac[i - 1], i), ifac[i] = Mul(ifac[i - 1], inv[i]);
    return;
}

inline int C(int const &a, int const &b) {
    if (a < b) return 0;
    return Mul(fac[a], Mul(ifac[b], ifac[a - b]));
}

signed main() {
    freopen(FILENAME".in", "r", stdin);
    freopen(FILENAME".out", "w", stdout);
    
    scanf("%d%d%d", &n, &m, &k);
    get_fac();
    f(i, 1, m) scanf("%d", &w[i]);
    f(j, 0, w[1]) f[j] = 1;
    f(i, 1, m) {
        sum += w[i];
        g(j, sum, 0) SubEq(f[j], (j - w[i] - 1 < 0) ? 0 : f[j - w[i] - 1]);
        f(j, 1, sum + w[i + 1]) AddEq(f[j], f[j - 1]);
    }
    if (n == m) return cout << SubEq(f[k], f[k - 1]) << '\n', 0;
    g(j, sum, 1) {
        SubEq(f[j], f[j - 1]);
        AddEq(ans, Mul(f[j], C(k - j + n - m - 1, n - m - 1)));
    }
    AddEq(ans, Mul(1, C(k + n - m - 1, n - m - 1))); //j = 0
    cout << ans << '\n';
    
    return 0;
}

B. 公路建设(线段树 + 最小生成树)

题目描述

在 Byteland 一共有 \(n\) 个城市,编号依次为 \(1\)\(n\),它们之间计划修建 \(m\) 条双向道路,其中修建第 \(i\) 条道路的费用为 \(c_i\)

Byteasar 作为 Byteland 公路建设项目的总工程师,他决定选定一个区间 \([l, r]\),仅使用编号在该区间内的道路。他希望选择一些道路去修建,使得连通块的个数尽量少,同时,他不喜欢修建多余的道路,因此每个连通块都可以看成一棵树的结构。

为了选出最佳的区间,Byteasar 会不断选择 \(q\) 个区间,请写一个程序,帮助 Byteasar 计算每个区间内修建公路的最小总费用。

题意

给定 \(n\) 个点 \(m\) 条边的无向图 \(G = (V,E)\),边的编号按照输入顺序为 \(1\)\(m\),第 \(i\) 条边 \((u_i,v_i)\) 的权值为 \(c_i\)

\(q\) 次询问,每次询问给定 \(l_i,r_i\),求 \(G' = (V,E')\) 的最小生成森林,其中 \(E'\subseteq E\) 为所有编号在 \([l_i,r_i]\) 范围内的边的集合。

对于 \(100\%\) 的数据,\(1\le n\le100\)\(1\le m\le 10^5\)\(1\le q\le15000\)\(1\le c_i\le10^6\)

\(1\le u_i,v_i\le n\)\(u_i\ne v_i\)\(1\le l_i\le r_i\le m\)

思路

如果给定一个区间 \([l,r]\),即给定一个边集,那么我们可以很容易地利用 Kruskal 算法求出最小生成树。

注意到 \(n\) 的范围很小,并且最小生成树中最多只有 \(n-1\) 条边。我们考虑如何维护一个区间的最小生成树(MST)上的边。

我们猜想,一个边编号区间 \([l,r]\) 的 MST 一定是 \([l,i]\) 的 MST 和 \([i+1,r]\) 的 MST 的并集的子集(\(l\le i<r\))。

感性理解:由于 \([l,r]\) 的可选范围比 \([l,i]\)\([i+1,r]\) 更大,那么和一条边竞争的其他边也就更多,也就是说,如果一条边连子区间的 MST 都没有入选,那么更不会入选父区间的 MST。

因此,如果用线段树维护一个区间的最小生成树(的边集),那么就可以合并了。

具体来说,就是先将两个子区间的 MST 的边集合并,然后再用 Kruskal 求 MST 即可。见代码中的 merge 函数。

由于 MST 的边数不超过 \(n\),每次合并的时间复杂度也是 \(O(n\alpha(n))\) 的。

总时间复杂度 \(O(qn\alpha(n)\log m)\)

代码

#pragma GCC optimize(2)
#include <cstdio>
#include <vector>
#include <iostream>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define FILENAME "highway"
#define lson (u << 1)
#define rson (u << 1 | 1)
using namespace std;
typedef vector<int> vi;
const int N = 110, M = 1e5 + 10;
int n, m, q;
vi ans, tmp;

struct Edge {
    int u, v, w;
    inline bool operator<(Edge const &o) const { return w < o.w; }
} edge[M];

struct Node {
    int l, r;
    vi mst;
} tr[M << 2];

int fa[N];
int getfa(int x) { return x == fa[x] ? x : fa[x] = getfa(fa[x]); }

inline void work(vi &u, int idx) {
    int fu = getfa(edge[idx].u), fv = getfa(edge[idx].v);
    if (fu ^ fv) {
        fa[fu] = fv;
        u.push_back(idx);
    }
    return;
}

void merge(vi &u, vi a, vi b) {
    f(i, 1, n) fa[i] = i;
    int sa = a.size(), sb = b.size();
    u.clear();
    int i = 0, j = 0;
    while (i < sa && j < sb)
        if (edge[a[i]].w < edge[b[j]].w) work(u, a[i]), ++i;
        else work(u, b[j]), ++j;
    while (i < sa) work(u, a[i]), ++i;
    while (j < sb) work(u, b[j]), ++j;
    return;
}

void build(int u, int l, int r) {
    tr[u].l = l, tr[u].r = r;
    if (l == r) return tr[u].mst.push_back(l);
    int mid = l + r >> 1;
    build(lson, l, mid);
    build(rson, mid + 1, r);
    merge(tr[u].mst, tr[lson].mst, tr[rson].mst);
    return;
}

void query(int u, int l, int r) {
    if (l <= tr[u].l && tr[u].r <= r) {
        merge(tmp, ans, tr[u].mst);
        ans = tmp;
        return;
    }
    int mid = tr[u].l + tr[u].r >> 1;
    if (l <= mid) query(lson, l, r);
    if (r > mid) query(rson, l, r);
    return;
}

int solve(int l, int r) {
    ans.clear();
    query(1, l, r);
    int res = 0;
    f(i, 1, n) fa[i] = i;
    for (int i: ans) {
        int fu = getfa(edge[i].u), fv = getfa(edge[i].v);
        if (fu ^ fv) {
            fa[fu] = fv;
            res += edge[i].w;
        }
    }
    return res;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    freopen(FILENAME".in", "r", stdin);
    freopen(FILENAME".out", "w", stdout);
    
    cin >> n >> m >> q;
    f(i, 1, m) cin >> edge[i].u >> edge[i].v >> edge[i].w;
    build(1, 1, m);
    while (q--) {
        int l, r; cin >> l >> r;
        cout << solve(l, r) << '\n';
    }
    
    return 0;
}
posted @ 2023-03-28 08:11  f2021ljh  阅读(62)  评论(0)    收藏  举报