2024-10-10 模拟赛总结
\(100+100+0+20=220\),部分分还是没有拿满。
比赛链接:http://172.45.35.5/d/HEIGETWO/homework/6707886f6735d3863dc8c0ef
或 http://yl503.yali.edu.cn/d/HEIGETWO/homework/6707886f6735d3863dc8c0ef
A - 植物收集 / collect
题意:
你要收集 \(n\) 个阶段的植物,你可以选择花费 \(a_i\) 的价格买到一个阶段的植物,你还可以花费 \(k\) 的代价使当前所有的植物的阶段加一,若一个植物阶段为 \(n\),那么其阶段会变成 \(1\),求收集到所有 \(n\) 个阶段的植物的最小代价。
思路:
首先可以发现一个显然的性质,我们考虑将所有植物单个使用技能,那么多个植物使用的最少技能次数为单个使用技能次数的最大值。所以我们可以枚举使用了 \(k\) 次技能,那么单个植物可以使用的最多技能次数为 \(k\),那么就是在当前位置的前 \(k\) 个找到 \(a\) 的最小值,这可以用单调队列维护,这样就实现了时间复杂度为 \(O(n^2)\) 的做法。通过打表可以知道最小代价关于使用技能次数的函数是一个开口朝上的单峰函数,可以用二分寻找最小值,时间复杂度 \(O(n\log n)\),可以通过此题。
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1e5 + 5;
int n, k, a[kMaxN * 2], q[kMaxN * 2], h, t, L, R, M;
long long ans = 1e18, f[kMaxN];
long long C(int l, long long ret = 0) {
if (f[l]) return f[l];
h = 1, t = 0;
for (int i = n - l + 1; i <= n; i++) {
for (; h <= t && a[q[t]] >= a[i]; t--) {
}
q[++t] = i;
}
for (int i = n + 1; i <= n + n; i++) {
for (; h <= t && a[q[t]] >= a[i]; t--) {
}
q[++t] = i, ret += a[q[h]];
for (; h <= t && q[h] <= i - l; h++) {
}
}
return f[l] = ret + 1LL * l * k;
}
int main() {
freopen("collect.in", "r", stdin);
freopen("collect.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> k, f[n + 1] = 1e18;
for (int i = 1; i <= n; i++) {
cin >> a[i], a[n + i] = a[i];
}
L = 0, R = n, M = L + R >> 1;
for (; L < R; M = L + R >> 1) {
C(M + 1) >= C(M) ? R = M : L = M + 1;
}
cout << C(L);
return 0;
}
B - 美丽子区间 / interval
题意:
定义一个子区间是美丽的,当且仅当这个子区间的左端点和右端点都不是这个子区间的最大值或最小值,给定一个长度为 \(n\) 的排列 \(p\),求 \(p\) 有多少个子区间是美丽的。
思路:
我们考虑一个数能成为美丽子区间的右端点,这个区间的左端点有什么限制,那么我们就需要找到这个数的从右到左数第一个比它大的和第一个比他小的数,这两个位置取 \(\min\),就是左端点需要在这个数的左边(不包括这个数)。同理,那么对于每一个数它要成为左端点,右端点也有一段区间。我们设 \(l_i\) 表示以编号为 \(i\) 的数为右端点,区间左端点至少要左到哪里,\(r_i\) 表示一边好为 \(i\) 的数为左端点,区间右端点至少要右到哪里,\(l_i,r_i\) 可以用单调栈或者权值树状数组求。
我们枚举以 \(i\) 为右端点,那么只有 \(1\) 到 \(l_i\) 的数可以成为区间的左端点,但是还要保证这些数的 \(r_i\) 小于 \(i\),这就是 P10814 【模板】离线二维数点,这可以将问题离线,然后用权值树状数组处理。(在线的话好像也可以用主席树做)
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 2e5 + 5;
int n, a[kMaxN], c[2][kMaxN], l[kMaxN], r[kMaxN];
long long ans;
vector<int> v[kMaxN];
void modify(int x, int y, int id, int o) { // o : [0 : min / 1 : max / 2 : +]
for (int i = x; i <= n; i += i & -i) {
c[id][i] = o == 2 ? c[id][i] + y : o ? max(c[id][i], y) : min(c[id][i], y);
}
}
int query(int x, int id, int o) {
int r = o == 2 ? 0 : o ? -1e9 : 1e9;
for (int i = x; i; i -= i & -i) {
r = o == 2 ? r + c[id][i] : o ? max(c[id][i], r) : min(c[id][i], r);
}
return r;
}
int main() {
freopen("interval.in", "r", stdin);
freopen("interval.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
memset(c[0], 0xc0, sizeof(c[0])), memset(c[1], 0xc0, sizeof(c[1]));
for (int i = 1; i <= n; i++) {
l[i] = min(query(n + 1 - a[i], 0, 1), query(a[i], 1, 1)) - 1, modify(n + 1 - a[i], i, 0, 1), modify(a[i], i, 1, 1);
}
memset(c[0], 0x3f, sizeof(c[0])), memset(c[1], 0x3f, sizeof(c[1]));
for (int i = n; i; i--) {
r[i] = max(query(n + 1 - a[i], 0, 0), query(a[i], 1, 0)) + 1, modify(n + 1 - a[i], i, 0, 0), modify(a[i], i, 1, 0);
}
for (int i = 1; i <= n; i++) {
if (l[i] > 0) v[l[i]].push_back(i);
}
memset(c[0], 0, sizeof(c[0]));
for (int i = 1; i <= n; i++) {
if (r[i] <= n) modify(r[i], 1, 0, 2);
for (int j : v[i]) {
ans += query(j, 0, 2);
}
}
cout << ans;
return 0;
}
C - 字符序列 / subseq
题意:
定义一个函数 \(f(c,s)=cs_1cs_2\cdots cs_{|s|}c\)(表示拼接),给定一个长度为 \(n\) 的字符串 \(c\),\(s_0\) 为空串,\(s_i=f(c_i,s_{i-1})\),求 \(s_n\) 的本质不同子序列的个数。
思路:
首先我们考虑如何快速求出一个本质不同子序列的个数。考虑 dp。
第一种做法:
我们令 \(f_i\) 为选到第 \(i\) 位的本质不同子序列的个数。
- 若在 \(i\) 之前没有出现过 \(s_i\),那么 \(f_i=2\times f_{i-1}+1\),可以在前 \(i-1\) 的子序列中加上当前字符或者不加所以要乘 \(2\),加一是因为要算自己单个成子序列。
- 若在 \(i\) 之前出现过 \(s_i\),那么 \(f_i=2\times f_{i-1}-f_{last_{s_i}-1}\),\(last_{s_i}\) 表示 \(s_i\) 上一次出现的位置减去的原因是它如果加上当前字符就会算重,不需要加一是因为包括在 \(f_{i-1}\) 中。
这种做法的时间复杂度是 \(O(n)\) 的,这种做法的优点是与字符集大小无关,缺点是不能矩阵乘法。
第二种做法:
我们令 \(f_{i,j}\) 表示选到第 \(i\) 位,结尾为 \(j\) 的本质不同子序列的个数。
- 若 \(j\neq s_i\),那么 \(f_{i,j}=f_{i-1,j}\)。
- 若 \(j= s_i\),那么 \(f_{i,j}=\sum_{k\in V} f_{i-1,k}\),\(V\) 为字符集。
- 若 \(k\neq s_i\),那么可以在最后加上一个字符 \(s_i\)。
- 若 \(k=s_i\),那么可以选用前面的结尾为 \(s_i\) 的子序列,不需要加上 \(s_i\)。
这种做法的时间复杂度是 \(O(n\times |V|)\),这种做法的优点是可以矩阵乘法,缺点是与字符集大小有关。
这道题我们使用第二种做法,因为它可以矩阵乘法。
我们观察这个字符串是如何生成的,例如 \(c=\texttt{abcd}\),那么:
观察 \(s_4\),可以发现它是一个回文串,且回文串的两边也是回文串,所以我们可以分治做,用分治求出转移矩阵的乘积,用矩阵乘法即可。
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 505, kM = 1e9 + 7;
int n;
char c[kMaxN];
long long ans;
struct M {
int n, m;
long long a[30][30];
M(int h = 0, int w = 0) { n = h, m = w, memset(a, 0, sizeof(a)); }
M(char c) {
n = m = 27, memset(a, 0, sizeof(a)), a[26][26] = 1;
for (int j = 0; j < 26; j++) {
if (j != c - 'a') {
a[j][j] = 1;
} else {
for (int i = 0; i < 27; i++) {
a[i][j] = 1;
}
}
}
}
long long* operator[](int x) { return a[x]; }
M operator*(M b) const {
M r(n, b.m);
for (int i = 0; i < n; i++) {
for (int k = 0; k < m; k++) {
for (int j = 0; j < b.m; j++) {
r[i][j] = (r[i][j] + a[i][k] * b[k][j]) % kM;
}
}
}
return r;
}
} F(1, 27), Ft(27, 27);
int main() {
freopen("subseq.in", "r", stdin);
freopen("subseq.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> c + 1, F[0][26] = 1;
for (int i = 0; i < 27; i++) {
Ft[i][i] = 1;
}
for (int i = n; i; i--) {
Ft = Ft * M(c[i]) * Ft;
}
F = F * Ft;
for (int i = 0; i < 26; i++) {
ans = (ans + F[0][i]) % kM;
}
cout << ans;
return 0;
}
D - 网络攻防 / attack
题意:
给定一个 \(n\) 个点 \(m\) 条边的无向连通图,求删除至多 \(k\) 条边使图不连通的方案数。(\(k\in \{1,2\}\))
思路:
首先当 \(k=1\),就是求割边数量,可以用 tarjan 求,也可以用 dfs 树和树上差分求,简单讲一下第二种做法。
首先构建出 dfs 树,由于是无向图,所以只有返祖边,没有被返祖边覆盖的边就是桥,我们可以用树上差分求出每一条边被覆盖几次。
现在考虑 \(k=2\),只需要求删除 \(2\) 条边使图不连通的方案数即可,开始分类讨论。(下面的边的覆盖集,指树边被非树边覆盖的集合)
- 这两条边都是非树边,这显然不能使图不连通。
- 这两条边一条是桥,一条是非树边,显然会使图不连通。
- 这两条边一条是普通树边,一条是非树边,若这条普通树边的覆盖集中有且仅有这条非树边,那么就肯定能使图连通,这种情况我们可以求出有多少条树边只被一条非树边覆盖即可。
- 这两条边一条是桥,一条是桥,显然可以使图不连通。
- 这两条边一条是桥,一条是普通树边,显然也可以使图不连通。
- 这两条边一条是普通树边,另一条也是普通树边,举几个例子,可以发现一个结论就是若这两条边的覆盖集相等,那么可以使图不连通。判断集合是否相等,可以用随机化和哈希判断。
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1e5 + 5, kMaxM = 2e5 + 5;
int n, m, k, cnt1, cnttree, cntnottree, cntbridge;
long long ans, r[kMaxM], d[kMaxN], d2[kMaxN], sum[kMaxN], cnt[kMaxN];
mt19937 mrand(time(0) + 3247);
bitset<kMaxN> visp;
bitset<kMaxM> vise, istree, isbridge;
vector<pair<int, int>> e[kMaxN], g[kMaxN];
map<long long, int> mp;
void dfs(int u) {
visp[u] = 1;
for (pair<int, int> i : e[u]) {
long long w = r[i.first];
int v = i.second;
if (!visp[v]) {
vise[i.first] = 1, istree[i.first] = 1, g[u].push_back({i.first, v}), g[v].push_back({i.first, u}), dfs(v);
} else if (!vise[i.first]) {
vise[i.first] = 1, d2[u]++, d2[v]--, d[u] += w, d[v] -= w;
}
}
}
void dfs1(int u, int fa) {
sum[u] = d[u], cnt[u] = d2[u];
for (pair<int, int> i : g[u]) {
int v = i.second;
if (v != fa) {
dfs1(v, u), sum[u] += sum[v], cnt[u] += cnt[v], cnt1 += cnt[v] == 1, cntbridge += isbridge[i.first] = !sum[v], cnttree += istree[i.first], mp[sum[v]]++;
}
}
}
int main() {
freopen("attack.in", "r", stdin);
freopen("attack.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m >> k;
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v, r[i] = uniform_int_distribution<long long>(1e9, 1e12)(mrand), e[u].push_back({i, v}), e[v].push_back({i, u});
}
dfs(1), dfs1(1, 0), cntnottree = m - cnttree;
if (k == 2) {
ans += 1LL * cntbridge * cntnottree + cnt1 + 1LL * cntbridge * (cnttree - cntbridge) + 1LL * cntbridge * (cntbridge - 1) / 2, mp[0] = 0;
for (pair<long long, int> i : mp) {
ans += 1LL * i.second * (i.second - 1) / 2;
}
}
cout << cntbridge + ans;
return 0;
}