模拟赛 R18
R18 - A 子集计数
题目描述
给定一长度为 \(n\) 的序列 \(a_1,a_2,\cdots,a_n\),再额外给定一常数 \(m\),对每个 \(k=0,1,2,\cdots,n\),请你求出有多少个 \(S\subset \{1,2,3,\cdots,n\}\),满足存在 \(T\subset S\) 且:
- \(|T|=|S|-k\)
- \(\sum\limits_{i\in T}a_i\ge m\)。
答案对 \(998244353\) 取模。
Solution
签到题。题意就是让我们找有多少个子集,丢掉其中 \(k\) 的元素之后,剩下的权值仍然 \(\ge m\)。那么我们显然是丢掉最小的 \(k\) 个。那么我们就按 \(a_i\) 从小到大排序。考虑枚举分界点 \(i\) 表示我们在 \([i + 1, n]\) 中选一些数使得其 \(\ge m\),在 \([1, i]\) 中选 \(k\) 个数丢掉。这样就能保证我们总是丢掉最小的一些。\([i + 1, n]\) 中选一个子集使其和 \(\ge m\) 用 0-1 背包即可。后半部分的答案显然就是 \(i \choose k\)。注意枚举分界点时总是要钦定 \(i + 1\) 必须要选,要不然会算重。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 3e3 + 5, mod = 998244353;
int a[N], f[N][N], g[N][N], n, m, c[N][N];
void add(int &x, int y){ (x += y) %= mod; }
signed main(){
freopen("subset.in", "r", stdin);
freopen("subset.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; ++i) cin >> a[i];
sort(a + 1, a + 1 + n);
g[n + 1][0] = 1;
for(int i = n; i >= 1; --i){
for(int j = 0; j <= m; ++j){
int nxt = min(m, a[i] + j);
add(f[i][nxt], g[i + 1][j]);
add(g[i][nxt], g[i + 1][j]);
add(g[i][j], g[i + 1][j]);
}
}
c[0][0] = 1;
for(int i = 1; i <= n; ++i){
c[i][0] = 1;
for(int j = 1; j <= i; ++j){
c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
}
}
for(int k = 0; k <= n; ++k){
int ans = 0;
for(int i = k; i <= n; ++i){
add(ans, c[i][k] * f[i + 1][m]);
}
cout << ans << ' ';
}
return 0;
}
R18 - B 括号序列
题目描述
给定一个包含左右括号的序列 \(s\),你可以进行若干次以下两种操作之一:
- 在任意位置插入一个左括号或者右括号;
- 将序列最后的括号移到最前。
现在你希望通过进行这两种操作将括号序列变为合法括号序列,并且 \(1\) 操作的次数尽可能少。在此基础上,你希望最终括号序列的字典序尽可能小。
一个括号序列被称为合法括号序列,当且仅当可以在其中插入 1 和 + 使其成为一个合法的表达式,例如 ()(),((()))() 均为合法括号序列,但 (,)(,())(((())()) 则不是。
Solution
首先,我们得想到先把左括号和右括号的数量补齐成相同的。要不然无论如何都不可能合法。但如果你先考虑旋转,再去补的话,就不太可做,因为这样很难注意到一个关键但是显然的性质,每次你添加的左括号/右括号个数是相同的,并且第一问的答案跟这个性质有很大关系。
进一步地,把数量补齐之后,我们发现总是可以通过旋转使其变成一个合法括号序列。用折线法说明即可。第一问首先要求我们添加的字符最少,这显然是我们补齐的数量 \(x\)。断环为链,把旋转操作变成看一段 \([i, i + n - 1]\) 的区间。一个很常见的判括号序列合不合法的 trick,考虑前缀和,如果最小值 \(\ge 0\) 就是合法的(注意用这个得先判断括号个数能配对上)。如果发现这个合法的话,再比较哈希比较字典序即可,这样做完了。
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int N = 4e5 + 5;
string s;
int sum[N], a[N], st[25][N], n;
ull f[N], P = 131, pw[N];
ull hsh(int l, int r){
if(l > r) return 0;
return f[r] - f[l - 1] * pw[r - l + 1];
}
bool cmp(int x, int y){
int l = 0, r = n;
while(l < r){
int mid = (l + r + 1) >> 1;
if(hsh(x, x + mid - 1) == hsh(y, y + mid - 1)) l = mid;
else r = mid - 1;
}
if(l == n) return 1;
return a[x + l] > a[y + l];
}
int getmn(int l, int r){
int d = log2(r - l + 1);
return min(st[d][l], st[d][r - (1 << d) + 1]);
}
void solve(){
cin >> s;
n = s.size();
pw[0] = 1;
for(int i = 1; i <= 2 * n; ++i){
pw[i] = pw[i - 1] * P;
if(i <= n) a[i] = (s[i - 1] == '(' ? 1 : -1);
else a[i] = a[i - n];
sum[i] = sum[i - 1] + a[i];
int tmp = (a[i] == 1 ? 1 : 0);
f[i] = f[i - 1] * P + tmp;
st[0][i] = sum[i];
}
int c = -sum[n];
for(int i = 1; (1 << i) <= 2 * n; ++i){
for(int j = 1; j + (1 << i) <= 2 * n; ++j){
st[i][j] = min(st[i - 1][j], st[i - 1][j + (1 << i - 1)]);
}
}
if(c >= 0){
for(int i = 1; i <= c; ++i) cout << '(';
int ans = 0;
for(int i = 1; i <= n; ++i){
int x = getmn(i, i + n - 1);
if(x - sum[i - 1] + c >= 0){
if(!ans) ans = i;
else if(cmp(i, ans)) ans = i;
}
}
for(int i = ans; i <= ans + n - 1; ++i) cout << (a[i] == 1 ? '(' : ')');
}
if(c < 0){
int ans = 0;
for(int i = 1; i <= n; ++i){
int x = getmn(i, i + n - 1);
if(x - sum[i - 1] >= 0){
// cout << i << '\n';
if(!ans) ans = i;
else if(cmp(i, ans)) ans = i;
}
}
for(int i = ans; i <= ans + n - 1; ++i) cout << (a[i] == 1 ? '(' : ')');
for(int i = 1; i <= -c; ++i) cout << ')';
}
cout << '\n';
}
int main(){
cin.tie(0)->sync_with_stdio(0);
int T;
cin >> T;
while(T--) solve();
return 0;
}
R18 - C 字典树
题目描述
给定 \(n\) 个由小写字母组成的字符串 \(s_1,s_2,\cdots,s_n\),每个字符串的长度都是 \(m\)。现在我们随机打乱每个字符串内部的字符之间的顺序得到新的字符串 \(t_1,t_2,\cdots,t_n\),并对新的 \(n\) 个字符串建字典树,求字典树结点个数的期望值。
Solution
容易发现,字典树节点个数就是所有字符串不相同的前缀个数 + 1。
先不看 + 1,在思考一下,对于每次重排过后,我们把第 \(i\) 个字符串前缀字符串组成的集合叫做 \(S_i\) 的话,那么字典树节点个数就是 \(|\bigcup_{i = 1}^n S_i|\)。根据期望的线性性,期望也是可以容斥的。那么就是 \(E(|\bigcup_{i = 1}^n S_i|) = \sum_{T} (-1)^{|T| - 1} E(|\bigcap_{x \in T} S_x|)\)。
考虑如何计算一个给定子集的 \(|\bigcap_{x \in T} S_x|\)。这个就等于这些字符串的公共前缀长度 \(len\)。接下来我们认为相同的字符之间也是互不相同的。一个经典技巧,公共前缀长度的期望 = 存在长度为 1 的公共前缀的概率 + 存在长度为 2 的公共前缀的概率 + \(\cdots\) + 存在长度为 \(m\) 的公共前缀的概率。首先我们随意重排所有字符串的方案数固定的 \((m!)^{|T|}\),一个长度为 \(len\) 的公共前缀的贡献恰好会在 \(1\cdots len\) 每次都被贡献 1 次,这也恰好是它对期望的贡献。如果还是不懂把总方案的分母,看成计数问题理解一些应该就懂了。
计算概率就是计算方案数,考虑如何计算存在长度为 \(l\) 的公共前缀的方案数。发现只跟选了那些字符充当公共前缀有关(因为我们把他们随意重排就组成了所有公共前缀),要求就只有得选出 \(l\) 个。那这个对 26 种字符做背包就好。
注意由于我们认为字符之间都是不相同的,所以计数上有一些细节。比如说,从每个字符串中选了一些位置的字符当前缀之后,还得考虑前缀字符之间对应的关系,别少算了。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 15, M = 105, mod = 1e9 + 7;
int n, m, f[27][M], g[27][M], cnt[N][27], fac[M], mn[27], c[M][M];
string s[N];
void chmin(int &x, int y){ x = min(x, y); }
void add(int &x, int y){ (x += y) %= mod; }
int qpow(int a, int b){
int ret = 1;
for(; b; b >>= 1, a = (a * a) % mod)
if(b & 1) ret = (ret * a) % mod;
return ret;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
fac[0] = 1;
for(int i = 1; i <= m; ++i) fac[i] = (fac[i - 1] * i) % mod;
c[0][0] = 1;
for(int i = 1; i <= m; ++i){
c[i][0] = 1;
for(int j = 1; j <= i; ++j){
c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}
}
for(int i = 1; i <= n; ++i){
cin >> s[i];
for(int j = 0; j < m; ++j)
cnt[i][s[i][j] - 'a' + 1]++;
}
int ans = 0, k = qpow(fac[m], n);
for(int S = 1; S < (1 << n); ++S){
vector<int> tmp;
int num = 1;
for(int i = 1; i <= n; ++i){
if(S & (1 << (i - 1))){
tmp.push_back(i);
}
else num = (num * fac[m]) % mod;
}
int siz = tmp.size();
// if(siz == 1){ add(ans, k * m % mod); continue; }
memset(mn, 0x3f, sizeof(mn));
for(int i : tmp){
for(int j = 1; j <= 26; ++j){
chmin(mn[j], cnt[i][j]);
}
}
for(int j = 1; j <= 26; ++j){
for(int i = 0; i <= mn[j]; ++i){
g[j][i] = 1;
for(int x : tmp) g[j][i] = (g[j][i] * c[cnt[x][j]][i]) % mod;
}
}
memset(f, 0, sizeof(f));
f[0][0] = 1;
for(int i = 1; i <= 26; ++i){
for(int j = 0; j <= m; ++j){
for(int k = 0; k <= min(mn[i], j); ++k){
add(f[i][j], f[i - 1][j - k] * g[i][k] % mod * qpow(fac[k], siz - 1) % mod);
}
}
}
int sum = 0;
for(int l = 1; l <= m; ++l){
// cout << l << ' ' << f[26][l] << '\n';
add(sum, f[26][l] * fac[l] % mod * qpow(fac[m - l], siz) % mod);
}
// cout << S << ' ' << sum * num % mod * qpow(k, mod - 2) % mod << '\n';
add(ans, (siz & 1 ? 1 : mod - 1) * sum % mod * num % mod);
}
cout << ((ans * qpow(k, mod - 2) + 1)) % mod;
return 0;
}
R18 - D 量筒注水
题目描述
水平桌面上有 \(n\) 个量筒,每个量筒均为底面积为 \(1\) 平方米且无限高的圆柱。它们之间由 \(m\) 条管道连接,第 \(i\) 条管道连接量筒 \(u_i\) 和 \(v_i\),高度为 \(h_i\) 米(桌面的高度为 \(0\),保证所有 \(h_i\) 互不相同)。初始每个量筒中均没有水。
\(q\) 次询问,每次询问给定一个点 \(a\) 和一个常数 \(t\),问,如果从初始状态开始持续往 \(1\) 号量筒中注入 \(a\) 立方米的水,那么 \(t\) 号量筒中水的高度会变成多少,答案下取整到整数。询问之间互相独立。
部分测试点强制在线。
Solution
把 \(h_i\) 看成边权,对原图先跑一个最小生成树。我们发现只考虑最小生成树上的边和原问题等价。因为 \(h_i\) 互不相同,当有水经过一条非最小生成树上的边时,所有管道已经形成一个连通器了。这时候去掉这些边显然没有影响。
那么就变成了一个树上问题,首先把这颗最小生成树看成 1 为根。考虑每次二分答案 \(mid\)。找到 \(t\) 能只通过 \(< mid\) 的边到达的离 \(1\) 最近的点(这个点显然是 \(t\) 的某个树上的祖先 \(p\),当然 \(p\) 有可能等于 \(t\))。也就是说,从 1 的水注入这个 \(p\) 开始,他会先流向子树只经过 \(<mid\) 的边能到的点(这个就是 \(t\) 能只通过 \(<mid\) 的边到达的点的集合 \(S\)),然后这些点变成了一个连通器,并且他们会一起上升到高度 \(mid\)。而且我们发现,由于注水的过程类似一种 dfs,所以从 1 的水注入这个 \(p\) 开始,所有注入 1 的水都会流入这个连通器内。那么所需要的水量就是 有 1 的水注入 \(p\) 所需的水量 + \(|S| \times mid\)。
考虑 dp 求有水开始注入 \(p\) 所需的水量 \(f_p\)。首先 \(f_1 = 0\)。然后对于一个点 \(u\) 和他树上的父亲 \(fa_{u}\),假设他们之间的边权是 \(w\)。那么类似上面,还是找到 \(fa_u\) 只经过 \(< w\) 的边能到达的点的集合 \(S\) 和离 1 最近的点 \(v\)。与上面同理,有 \(f_u \gets f_v + |S| \times w\)。
kruskal 重构树求一个点只经过 \(<x\) 的边能到达的点,并同时维护离 1 最近的点(深度最浅的点)即可。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
typedef tuple <int, int, int> tpi;
typedef pair<int, int> pii;
const int N = 2e5 + 5, inf = 1e18;
vector<pii> tr[N];
vector<int> kt[N];
int a[N], fa[N], n, m, q, V, typ, tot, siz[N], dep[N], st[N][21], f[N];
tpi e[N];
pii p[N];
int getf(int u){ return u == fa[u] ? u : fa[u] = getf(fa[u]); }
void init(int u, int fa){
dep[u] = dep[fa] + 1;
for(auto [v, w] : tr[u]){
if(v != fa) init(v, u);
}
}
void dfs(int u, int fa){
st[u][0] = fa;
for(int i = 1; i <= 19; ++i) st[u][i] = st[st[u][i - 1]][i - 1];
if(u <= n) p[u] = pii{dep[u], u}, siz[u] = 1;
else p[u] = pii{inf, 0}, siz[u] = 0;
for(auto v : kt[u]){
if(v != fa){
dfs(v, u);
p[u] = min(p[u], p[v]);
siz[u] += siz[v];
}
}
}
int get(int u, int w){
for(int i = 19; i >= 0; --i){
if(a[st[u][i]] < w) u = st[u][i];
}
return u;
}
void dp(int u, int fa){
for(auto [v, w] : tr[u]){
if(v == fa) continue;
int x = get(u, w); // on kt
f[v] = f[p[x].second] + siz[x] * w;
dp(v, u);
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> q >> V >> typ;
a[0] = inf;
tot = n;
for(int i = 1; i <= 2 * n; ++i) fa[i] = i;
for(int i = 1; i <= m; ++i){
auto &[w, u, v] = e[i];
cin >> u >> v >> w;
}
sort(e + 1, e + 1 + m);
for(int i = 1; i <= m; ++i){
auto [w, u, v] = e[i];
int x = getf(u), y = getf(v);
if(x == y) continue;
tr[u].emplace_back(v, w);
tr[v].emplace_back(u, w);
++tot;
kt[tot].emplace_back(x);
kt[tot].emplace_back(y);
a[tot] = w;
fa[x] = fa[y] = tot;
}
init(1, 0);
dfs(tot, 0);
dp(1, 0);
int lst = 0;
while(q--){
int v, t; cin >> v >> t;
v = (v - 1 + typ * lst) % V + 1;
t = (t - 1 + typ * lst) % n + 1;
int l = 0, r = v;
while(l < r){
int mid = (l + r + 1) >> 1;
int x = get(t, mid), ver = p[x].second;
if(f[ver] + siz[x] * mid <= v) l = mid;
else r = mid - 1;
}
cout << (lst = l) << '\n';
}
return 0;
}
Summary
T2 这种题还是要冷静一点......

浙公网安备 33010602011771号