2025.8.21模拟赛
T1
给定 \(n\) 个整数 \(a_i\), 中间用加减号隔开. 你可以在里面任意添加括号, 求能得到的最大结果是多少.
\(1\le n\le 10^5\)。
如果套三层括号,那么一定可以消成两层及以下的括号。
然后只有在减号的情况下加左括号是有意义的,所以减号时可以加左或右括号,加号就只能加右括号(并且这样会少好多讨论)。
赛时代码
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, a[N], f[N][3];
char c[N];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i < n; ++i)
cin >> a[i] >> c[i + 1];
cin >> a[n];
for (int i = 1; i <= n; ++i)
f[i][0] = f[i][1] = f[i][2] = -2e18;
f[1][0] = a[1];
for (int i = 2; i <= n; ++i) {
if (c[i] == '-') {
f[i][0] = max(f[i - 1][0] - a[i], f[i - 1][1] + a[i]);
f[i][1] = max(f[i - 1][0] - a[i], max(f[i - 1][1] + a[i], f[i - 1][2] + a[i]));
f[i][2] = max(f[i - 1][1] + a[i], f[i - 1][2] - a[i]);
} else {
f[i][0] = max(f[i - 1][0] + a[i], f[i - 1][1] + a[i]);
f[i][1] = max(f[i - 1][1] - a[i], f[i - 1][2] + a[i]);
f[i][2] = f[i - 1][2] + a[i];
}
}
cout << f[n][0] << '\n';
return 0;
}
T2
给定一个 n 个点 m 条边的无向图 G,求 G 上 1 到其他点的最短路的长度。
假设一条路径上的边权依次为 \(w1,w_2,\dots,w_k\) ,于是定义 \(S(w)=\min w_i−\max w_i +\sum w_i\) 为路径长度,求在这个定义下的最短路。
如果不联通,请输出1000000000000000000。
\(1\le n,m\le 2\times 10^5\)。
等价的转化是在一条路径上删去掉一条边权,加上一条边权,求最短路。在每个点放 \(4\) 个状态存是否加上,是否去掉的最短路。
如果去掉的不是最大值,那么最终答案一定不优。最小值同理。因此在求最优解的情况下是正确的。
赛时代码
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e5 + 5;
int n, m, head[N], tot;
ll dis[N][4];
bool vis[N][4];
struct node {
ll dis;
int id, t;
};
bool operator<(node x, node y) { return x.dis > y.dis; }
priority_queue<node> q;
struct edge {
int v, w, nxt;
} e[N << 1];
void add(int u, int v, int w) {
e[++tot] = (edge){v, w, head[u]}, head[u] = tot;
e[++tot] = (edge){u, w, head[v]}, head[v] = tot;
}
void dij() {
for (int i = 1; i <= n; ++i)
dis[i][0] = dis[i][1] = dis[i][2] = dis[i][3] = 1e18;
dis[1][0] = 0;
q.push((node){0, 1, 0});
while (q.size()) {
int u = q.top().id, t = q.top().t;
q.pop();
if (vis[u][t])
continue;
vis[u][t] = 1;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v, w = e[i].w;
if (dis[u][t] + w < dis[v][t])
q.push((node){dis[v][t] = dis[u][t] + w, v, t});
if (!(t & 1) && dis[u][t] + w + w < dis[v][t | 1])
q.push((node){dis[v][t | 1] = dis[u][t] + w + w, v, (t | 1)});
if (!(t & 2) && dis[u][t] < dis[v][t | 2])
q.push((node){dis[v][t | 2] = dis[u][t], v, (t | 2)});
if (t == 0 && dis[u][t] + w < dis[v][3])
q.push((node){dis[v][3] = dis[u][t] + w, v, 3});
}
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1, u, v, w; i <= m; ++i)
cin >> u >> v >> w, add(u, v, w);
dij();
for (int i = 2; i <= n; ++i)
cout << dis[i][3] << ' ';
return 0;
}
T3
有 \(n\) 个人,要分成若干组. 对于第 \(i\) 个人,它所在的组的人数不能超过 \(a_i\),求分组的方案数.组与组之间是不可区分的,每组的人也是无序的,但人与人之间是可以区分的。
50pts: \(1\le n\le 300\)。
100pts:\(1\le n\le 3000\)。
考虑先按 \(a_i\) 从大到小排序。\(f_{i,j}\) 表示前 \(i\) 个人中,有 \(j\) 个人已经分组的方案数。计算每组贡献时将其挂在最后一个人上,既方便直接用最后一个人的 \(a_i\),又不会算重。
即考虑第 \(i\) 个人是否为最后一个人,以及他所在组的大小,然后算一个组合数,做一个类似背包的东西。
50pts 代码
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
const int N = 305, P = 998244353;
int n, a[N];
int c[N][N], f[N], g[N];
void add(int &x, int y) { x = (x + y >= P ? x + y - P : x + y); }
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
sort(a + 1, a + n + 1);
reverse(a + 1, a + n + 1);
for (int i = c[0][0] = 1; i <= n; ++i)
for (int j = c[i][0] = 1; j <= i; ++j)
c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % P;
f[0] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= i; ++j) {
g[j] = f[j];
for (int k = 1; k <= a[i] && k <= j; ++k)
add(g[j], 1ll * f[j - k] * c[i - j + k - 1][k - 1] % P);
}
for (int j = 0; j <= i; ++j)
f[j] = g[j], g[j] = 0;
}
cout << f[n] << '\n';
return 0;
}
考虑一次计算多一点的东西。可以从大到小枚举组的大小,将每个人挂在组的大小上计算。首先将 \(a_i\) 等于组的大小的加进来,枚举一层背包,在枚举一层组数,这样总复杂度是 \(O(n\sum_i \frac{n}{i})\) 即 \(O(n^2\log n)\) 的。
一个问题就是组合系数在赛时困扰了我一会儿。
相当于求在 \(n\) 个人中选 \(k\) 组,每组 \(i\) 人。
第一组有 \(\binom{n}{i}\) 种,第二组有 \(\binom{n-i}{i}\) 种。。。然后还要将先后顺序去掉,所以除以一个 \(k!\)
所以是 \(\prod_{l=1}^k\frac {\binom{n-i(l-1)}{i}}{l}\)。直接递推是简单的。
赛时代码
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3005, P = 998244353;
int n, a[N], inv[N];
int c[N][N], f[N], g[N];
int qp(int x, int y) {
int z = 1;
for (; y; y >>= 1, x = 1ll * x * x % P)
if (y & 1)
z = 1ll * z * x % P;
return z;
}
void add(int &x, int y) { x = (x + y >= P ? x + y - P : x + y); }
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
inv[0] = 0;
for (int i = 1, x; i <= n; ++i)
cin >> x, ++a[x], inv[i] = qp(i, P - 2);
for (int i = c[0][0] = 1; i <= n; ++i)
for (int j = c[i][0] = 1; j <= i; ++j)
c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % P;
f[0] = 1;
for (int i = n, t = 0; i >= 1; --i) {
for (int j = 0; j <= t; ++j) {
add(g[j], f[j]);
int l = t - j + a[i], C = 1;
for (int k = 1; k * i <= l; ++k) {
C = 1ll * C * c[l - (k - 1) * i][i] % P * inv[k] % P;
add(g[i * k + j], 1ll * f[j] * C % P);
}
}
t += a[i];
for (int j = 0; j <= t; ++j)
f[j] = g[j], g[j] = 0;
}
cout << f[n] << '\n';
return 0;
}
T4
有一个 \(nm\) 个点的有向分层图, 共有 \(n\) 层, 每层 \(m\) 个点, 每条边一定是从第 \(i\) 层连向第 \(i+1\) 层。
定义 \(f(i,j)\) 表示选择若干条路径, 每条路径从第 \(i\) 层出发, 在第 \(j\) 层结束, 且每条路径在顶点和边上都不交的情况下, 最多选择的路径数.
求 \(\sum_{i=1}^{n-1}\sum_{j=i+1}^nf(i,j)\)。
50pts:\(n1\le n\le 1000\)。
100pts:\(1\le n\le 4\times 10^4,1\le m\le 9\)。
对于单个求解可以使用网络流。对于每个点拆成进入和走出两个点,中间连一个容量为 \(1\) 的边,其它边容量为 \(1\) 不变。
然后肯定不能真的用网络流去做,考虑最大流等于最小割,而对于这种图一定是割拆点形成的边最优。
所以考虑枚举起点,\(f_{i,s}\) 表示第 \(i\) 层的点集合 \(s\) 的点可以到达起点。
从大到小枚举二进制数 \(s\),再枚举删某个点,复杂度 \(O(n^22^mm)\),赛时就只做到这了。
赛时50pts代码
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
const int N = 1003, M = 10, K = 1 << 9;
int n, m;
int a[N][M], f[N][K], ans;
void Min(int &x, int y) { x = min(x, y); }
char ch;
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1, x; i < n; ++i) {
for (int j = 0; j < m; ++j)
for (int k = 0; k < m; ++k)
cin >> ch, a[i][j] |= ((ch - '0') << k);
}
for (int i = 1; i < n; ++i) {
for (int j = i; j <= n; ++j)
for (int s = 0; s < (1 << m); ++s)
f[j][s] = 1e9;
f[i][(1 << m) - 1] = 0;
for (int j = i; j <= n; ++j) {
for (int s = (1 << m) - 1; s >= 0; --s) {
if (f[j][s] == 1e9)
continue;
for (int k = 0; k < m; ++k)
if ((s >> k) & 1)
Min(f[j][s ^ (1 << k)], f[j][s] + 1);
}
if (j > i)
ans += f[j][0];
if (j < n) {
for (int s = 0; s < (1 << m); ++s) {
if (f[j][s] == 1e9)
continue;
int t = 0;
for (int k = 0; k < m; ++k)
if ((s >> k) & 1)
t |= a[j][k];
Min(f[j + 1][t], f[j][s]);
}
}
}
}
cout << ans << '\n';
return 0;
}
考虑对于同一起点,越往后割起来越容易
然后考虑 \(m\) 只有 \(9\),答案只有 \(9\) 种,考虑 \(f_{i,s,j}\) 表示在第 \(i\) 个位置可达状态为 \(s\),花费 \(>j\) 的位置最远在哪。会发现这玩意可以倒着推,且 \(\sum_{j=0}^m max(f_{i,2^m-1,j}-i,0)\) 就是以 \(i\) 为开头的总答案。
转移的时候一种从后面转移来,一种从子集转移来,复杂度 \(O(nm^22^m)\)。
赛后100pts代码
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
const int N = 40003, M = 10, K = 1 << 9;
char ch;
int n, m;
int a[N][K], f[N][K][M];
long long ans;
void Min(int &x, int y) { x = min(x, y); }
int popc(int x, int res = 0) {
while (x)
++res, x -= x & -x;
return res;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1, x; i < n; ++i) {
for (int j = 0; j < m; ++j)
for (int k = 0; k < m; ++k)
cin >> ch, a[i][1 << j] |= ((ch - '0') << k);
for (int s = 1; s < (1 << m); ++s)
a[i][s] = a[i][s & -s] | a[i][s ^ (s & -s)];
}
memset(f, 0x3f, sizeof(f));
for (int s = 0; s < (1 << m); ++s)
for (int j = 0; j <= m; ++j)
f[n][s][j] = n - (popc(s) <= j);
for (int i = n - 1; i >= 1; --i) {
for (int j = 0; j <= m; ++j)
f[i][0][j] = i - 1;
for (int s = 0; s < (1 << m); ++s)
for (int j = 0; j <= m; ++j) {
Min(f[i][s][j], f[i + 1][a[i][s]][j]);
if (j == m)
continue;
for (int t = s; t; t ^= (t & -t))
Min(f[i][s][j + 1], f[i][s ^ (t & -t)][j]);
}
for (int j = 0; j <= m; ++j)
ans += max(0, f[i][(1 << m) - 1][j] - i), cout << f[i][(1 << m) - 1][j] << ' ';
cout << '\n';
}
cout << ans << '\n';
return 0;
}