2023秋训练三 补充题解
题目
B - Playlist
题目描述
一张有 \(N\) 个歌曲的播放列表,第 \(i\) 首歌曲的长度为 \(T_i\) 秒。好渴鹅要在时刻 \(0\) 开始播放歌曲,每次以相等的概率随机播放任何一个歌曲。请问 \((X+0.5)\) 秒时正在播放第一首歌曲的概率,对 \(998244353\) 取模。
输入格式
\(N\) \(X\)
\(T_1\) \(T_2\) \(\dots\) \(T_N\)
输出格式
假如概率为 \(\cfrac{a}{b}\),那么你需要找到一个整数 \(c\) \((c\in 998244353)\),使得 \(az\equiv y\pmod{998244353}\),你需要输出这个 \(c\)。
数据范围
- \(2\le N\le 10^3\);
- \(0\le X\le 10^4\);
- \(1\le T_i\le 10^4\)。
E - Product Development
渴鹅公司计划开发有 \(K\) 个参数的产品,最开始参数都是 \(0\),目标是所有的参数都要大于等于 \(P\)。现在有 \(N\) 个开发计划,第 \(i\) 个开发计划需要 \(C_i\) 的成本,会让第 \(j\) \((1\le j\le K)\) 个参数的值加上 \(A_{(i,j)}\),你需要确定渴鹅公司是否能够达到目标,并输出最小成本。不可以输出 \(-1\)。
输入格式
\(N\) \(K\) \(P\)
\(C_1\) \(A_{(1,1)}\) \(A_{(1,2)}\) \(\dots\) \(A_{(1,K)}\)
\(C_1\) \(A_{(2,1)}\) \(A_{(2,2)}\) \(\dots\) \(A_{(2,K)}\)
\(\dots\)
\(C_1\) \(A_{(N,1)}\) \(A_{(N,2)}\) \(\dots\) \(A_{(N,K)}\)
数据范围
- \(1\le N\le 100\);
- \(1\le K,P\le 5\);
- \(0\le A_{(i,j)}\le P\) \((1\le i\le N,1\le j\le K)\);
- \(1\le C_i\le 10^9\) \((1\le i\le N)\)。
解法
B
首先是输出格式,其实就是求逆元,我们可以通过快速幂的方式求出逆元。假设 \(f(a,b)=a^b\),那么我们设 \(inv\gets f(n,998244353)\),那么每次就乘上 \(inv\) 进行转移就行了。
然后是 dp。我们设 \(dp_i\) 表示为当前歌曲以时刻 \(i\) 结尾的概率,那么对于任意一个 \(T_j\) \((1\le j\le n)\),如果 \(i-T_j\ge 0\) 的话,我们就可以把 \(dp_{(j-T_j)}\) 乘上 \(inv\)(也就是乘上概率)累加到 \(dp_i\)。因此可以推出式子:
最后我们就需要累加答案。因为这里是以时刻 \(i\) 结尾作为状态,而题目中是正在播放的概率,因此我们需要求出 \(\sum\limits_{i=\max(0,x-T_1+1)}^{x}dp_i\),并在输出时摸上 \(998244353\)。注意:这题需要开 long long,并且需要边运算、边取模,不然会炸。
#include <iostream>
#include <vector>
using namespace std;
using ll = long long;
const ll kMaxN = 1e4 + 5, kMod = 998244353;
ll t[kMaxN], dp[kMaxN * 2] = {1}, n, x, inv, ans;
ll ksm(ll a, ll b) {
ll ret = 1;
for (; b; b >>= 1) {
(b & 1) && ((ret *= a) %= kMod);
(a *= a) %= kMod;
}
return ret;
}
int main() {
cin >> n >> x;
inv = ksm(n, kMod - 2);
for (ll i = 1; i <= n; i++) {
cin >> t[i];
}
for (ll i = 0; i <= x; i++) {
for (ll j = 1; j <= n; j++) {
if (i >= t[j]) {
(dp[i] += dp[i - t[j]] * inv) %= kMod;
}
}
}
for (ll i = max(0LL, x - t[1] + 1); i <= x; i++) {
(ans += dp[i]) %= kMod;
}
cout << (ans * inv) % kMod << '\n';
return 0;
}
E
首看到这道题,你或许就会想到 01 背包 dp。由于 \(1\le K,P\le 5\),那我们几乎可以说是乱搞都能过。首先我们先来考虑 \(K=1\) 的简单情况。由于状态就只剩下第一个也是唯一一个参数的值了,那么我们就设 \(dp_i\) 为达到当前第一个参数的和所能获得的最小代价。\(K\) 等于其他的情况也是同理。那么我们就可以获得如下的状态转移方程矩阵:
但是,这样子也太麻烦了吧?其实我们可以把状态开成一个长度为 \(K\) 的向量,那么我们的操作就简单了许多。我们设计一个函数 \(f(v)\) 用来更改状态,那么在这个函数里面,对于任意的 \(j\) \((1\le j\le K)\),\(v_j\gets \min(P,v_j+A_{(i,j)})\),也就是在对应位置上更改。
那么你也许可能会问:为什么要跟 \(P\) 取 \(\min\) 呢?其实这是一种偷懒行为,因为当一个参数已经大于等于 \(P\) 了,那么就没有意义了;并且所有数都大于等于 \(P\) 的数列有很多,而加上了这个操作我们最后的状态就一定是一个全部都是 \(P\) 的向量。那么我们的操作就简单了许多。
注:C++ 的向量为 vector,映射为 map。而我们可能会发现如果直接压数组那么同层状态就会就会重复遍历,所以我们使用滚动数大法——用一个 \(f\) 来记录上一层状态。(注意最开始的初始状态 \(dp\) 和 \(f\) 数组都需要加进去,好渴鹅就是因为这里 WA 了两发。)
#include <iostream>
#include <vector>
#include <map>
using namespace std;
using ll = long long;
ll n, k, p;
int main() {
cin >> n >> k >> p;
vector<ll> c(n + 1);
vector<vector<ll>> a(n + 1, vector<ll>(k + 1));
for (ll i = 1; i <= n; i++) {
cin >> c[i];
for (ll j = 1; j <= k; j++) {
cin >> a[i][j];
}
}
map<vector<ll>, ll> dp, f;
f.insert({vector<ll>(k, 0), 0});
dp.insert({vector<ll>(k, 0), 0});
for (ll i = 1; i <= n; i++) {
for (auto j : f) {
vector<ll> v = j.first;
for (ll l = 1; l <= k; l++) {
v[l - 1] = min(p, v[l - 1] + a[i][l]);
}
if (dp.count(v)) {
dp[v] = min(dp[v], j.second + c[i]);
} else {
dp.insert({v, j.second + c[i]});
}
}
f = dp;
}
if (dp.count(vector<ll>(k, p))) {
cout << dp[vector<ll>(k, p)] << '\n';
} else {
cout << "-1\n";
}
return 0;
}

浙公网安备 33010602011771号