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\),状态转移方程为
其中 \(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,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\)。于是最后的答案为
另外,组合数需要线性求阶乘的逆元,这样时间复杂度为 \(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;
}