Order Capital Round 1 (Codeforces Round 1038, Div. 1 + Div. 2)
A. Greedy Grid
题意:一个\(n\times m\)的矩阵,每个位置有价值,从\((1, 1)\)出发,每次贪婪的往下面和右边种最大的格子走,一直到\((n, m)\)。求有没有一个矩阵使得这个方法不是走的最大价值路径。
模拟一下,如果\(n = 1\)或\(m=1\),只有一条路走,显然不行。如果\(n = 2\)且\(m=2\),也无法构造。如果是其它情况,我们可以让所有位置价值都是\(0\),然后构造沿着边缘构造一条权值都是\(1\)的路径,然后在路径不相邻的一个点给一个极大的值就行。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
void solve() {
int n, m;
std::cin >> n >> m;
if (n == 1 || m == 1 || (n == 2 && m == 2)) {
std::cout << "NO\n";
} else {
std::cout << "YES\n";
}
}
int main() {
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
int t = 1;
std::cin >> t;
while (t -- ) {
solve();
}
return 0;
}
B. Pile Shuffling
题意:有\(n\)个塔,每个塔顶部有\(a_i\)个\(0\),底部是\(b_i\)个\(1\),你每次从一个塔顶部拿出一个元素插入到任意塔的任意位置。求每个塔变成\(c_i, d_i\)状态的最小操作数。
如果\(a_i \leq c_i\),我们不需要对\(0\)进行多余操作,否则就把多余的拿走。如果\(b_i > d_i\),我们需要拿走\(b_i - d_i\)个\(1\),那么把上面的\(0\)先拿走直到满足\(a_i \leq c_i\),然后把剩下的\(0\)插到\(b_i - d_i\)个\(1\)的下面,再把这些\(1\)拿走就行,总共是\(\min(a_i, c_i) + b_i - d_i\)个操作。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
void solve() {
int n;
std::cin >> n;
std::vector<i64> a(n), b(n), c(n), d(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i] >> b[i] >> c[i] >> d[i];
}
i64 ans = 0;
for (int i = 0; i < n; ++ i) {
ans += std::max(0ll, a[i] - c[i]);
if (b[i] > d[i]) {
b[i] -= d[i];
i64 cnt = std::min(a[i], c[i]) + b[i];
ans += cnt;
}
}
std::cout << ans << "\n";
}
int main() {
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
int t = 1;
std::cin >> t;
while (t -- ) {
solve();
}
return 0;
}
C. Manhattan Pairs
题意:\(n\)个点两两配对,使得它们的曼哈顿距离只和最大。
如果只有一维,考虑一对的贡献是\(|x_i - x_j|\),可以让小的数前面是负号,大的数前面是正号,那么排序后从两边最大最小的开始匹配,能得到最优解。
现在考虑二维,我们也可以先按\(x\)排序,然后两边分别按\(y\)排序,这样左边的\(x\)一定是被减,右边的一定是被加,同时有因为按\(y\)排序了,也可以保证让\(y_i - y_j\)对是小的被减,大的被加。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
void solve() {
int n;
std::cin >> n;
std::vector<std::tuple<int, int, int>> a(n);
for (int i = 0; i < n; ++ i) {
int x, y;
std::cin >> x >> y;
a[i] = {x, y, i};
}
std::ranges::sort(a);
std::ranges::sort(a.begin(), a.begin() + n / 2, [&](std::tuple<int, int, int> & a, std::tuple<int, int, int> & b) {
return std::get<1>(a) < std::get<1>(b);
});
std::ranges::sort(a.begin() + n / 2, a.end(), [&](std::tuple<int, int, int> & a, std::tuple<int, int, int> & b) {
return std::get<1>(a) < std::get<1>(b);
});
for (int l = 0, r = n - 1; l < r; ++ l, -- r) {
std::cout << std::get<2>(a[l]) + 1 << " " << std::get<2>(a[r]) + 1 << "\n";
}
}
int main() {
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
int t = 1;
std::cin >> t;
while (t -- ) {
solve();
}
return 0;
}
D. Traffic Lights
题意:从\(1\)号点\(0\)时刻出发,每次在第\(u\)个点的第\(t\)时刻,可以选择让\(t\)加一,代价加一;或者走到第\(t \% deg(u) + 1\)条边对应的点。求\(1\)到\(n\)的最短时间及最短时间的最小代价。
赛时写了一下,一开始决定最多走\(n\)条边,然后发现自己想的太简单了,思考了一会也没想到到底要走几条边,于是限制最多走\(nlogn\)条边,结果\(mle\),然后把\(nlogn\)和\(\frac{1e7}{n}\)取\(min\),\(re\)了。就没写了。
赛后得知原来就是\(bfs\),最多走\(2n\)步。我绷不住了😀
可以记\(dist[i][j]\)表示第\(j\)时刻到点\(i\)的最小代价,那么要么走下一条边,要么原地等待。走下一步不加代价,原地等待加一的代价,是个\(01bfs\)。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
void solve() {
int n, m;
std::cin >> n >> m;
std::vector<std::vector<int>> adj(n);
for (int i = 0; i < m; ++ i) {
int u, v;
std::cin >> u >> v;
-- u, -- v;
adj[u].push_back(v);
adj[v].push_back(u);
}
const int inf = 1e9;
int N = 2 * n;
std::vector dist(n, std::vector<int>(N + 1, inf));
dist[0][0] = 0;
std::deque<std::pair<int, int>> q;
q.emplace_back(0, 0);
int ans = inf;
while (q.size()) {
auto [d, u] = q.front(); q.pop_front();
if (d >= ans) {
continue;
}
if (u == n - 1) {
ans = d;
}
if (d > N) {
continue;
}
int ne = d % (int)adj[u].size();
int v = adj[u][ne];
if (dist[v][d + 1] > dist[u][d]) {
dist[v][d + 1] = dist[u][d];
q.emplace_front(d + 1, v);
}
if (dist[u][d + 1] > dist[u][d] + 1) {
dist[u][d + 1] = dist[u][d] + 1;
q.emplace_back(d + 1, u);
}
}
std::cout << ans << " " << dist[n - 1][ans] << "\n";
}
int main() {
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
int t = 1;
std::cin >> t;
while (t -- ) {
solve();
}
return 0;
}
E. Greedy Grid Counting
题意:一个\(2\times n\)的数组,值都在\([1, k]\)之间,有些地方没填。如果从左上角到右下角每次贪心走右边和下面最大的可以得到最大路径,那么这个数组是好的。求有多少填法使得每个子数组都是好的。
因为只能向右或者向下,那么我们的路径是先走一段第一行的前缀,然后走一段第二行的后缀。我们可以看作一开始走\(a[1][1]\),然后直接到\(a[2][1]\)一直往右到终点,如果我们想改为从\(a[1][2]\)往下,那么路径的值增加\(a[1][2] - a[2][1]\),同理,如果在\(j\)出开始往下,价值变化是\(\sum_{j=2}^{i} a[1][j] - a[2][j - 1]\)。考虑贪心的走法什么时候是对的,显然如果\(a[1][j] - a[2][j - 1] \geq 0\),它会一直往右走,直到\(a[1][j] - a[2][j - 1] < 0\),就往下走,想要这个路径是最大的,那么对于任意的\(j' > j\),都有\(\sum_{x=j + 1}^{j'} a[1][x] - a[2][x - 1] \leq 0\)。题目要求每个子数组都要满足这个条件,那么就是任意一个\((a[1][j] - a[2][j - 1] < 0)\)的位置\(j\),都要有\(j' > j, \sum_{x=j + 1}^{j'} a[1][x] - a[2][x - 1] \leq 0\)。
那么可以维护后缀连续正数的和,记\(f[i][j]\)表示填完了\([i, n)\),且后面跟着的连续正数的和为\(j\),那么可以选择当前位置填一个正数接上去,或者选择填一个负数使得加上这些正数的和小于等于\(0\)。之所以只关心后面连续的正数和,是因为后面填的都是合法的,那么对于后面最近的一个负数,我们只需要满足当前位置到它前面的和都是小于等于\(0\)的,就可以保证整个后缀每个位置都是合法的。然后因为最小值最多为\(-k+1\),所以第二维最多是\(k\)。
当前\(a[1][i] - a[2][i - 1]\)不同取值的个数是可以预处理出来的。然后是记录答案,对于\(i\),我们记录了后缀合法的方案数,然后前缀是有全部\(a[1][j] - a[2][j - 1] \geq 0\)的,整个前缀的合法方案也可以预处理出来,记为\(pre\),那么\(ans += pre[i - 1] \times f[i + 1][x] \times cnt[y]\)。其中\(x, y\)分别代表后面一个选\(x\),当前数选\(y\),\(f[i + 1][x]\)就是后面选\(x\)的方案,\(cnt[y]\)就是当前位置凑出\(y\)的方案。
最后一个是,\(a[1][1], a[2][n]\)都是一定会走到的,所以它们的取值不影响合法方案。所以它们有一个\(-1\)答案就有乘一个\(k\)。
代码省略取模类。
点击查看代码
constexpr int P = 998244353;
using Z = MInt<P>;
void solve() {
int n, k;
std::cin >> n >> k;
std::vector a(2, std::vector<int>(n));
for (int i = 0; i < 2; ++ i) {
for (int j = 0; j < n; ++ j) {
std::cin >> a[i][j];
}
}
std::vector cnt(n, std::vector<Z>(2 * k + 1));
for (int i = n - 1; i > 0; -- i) {
if (a[0][i] == -1 && a[1][i - 1] == -1) {
for (int j = 1; j <= k; ++ j) {
for (int x = 1; x <= k; ++ x) {
cnt[i][j - x + k] += 1;
}
}
} else if (a[0][i] == -1) {
for (int j = 1; j <= k; ++ j) {
cnt[i][j - a[1][i - 1] + k] += 1;
}
} else if (a[1][i - 1] == -1) {
for (int j = 1; j <= k; ++ j) {
cnt[i][a[0][i] - j + k] += 1;
}
} else {
cnt[i][a[0][i] - a[1][i - 1] + k] = 1;
}
}
std::vector<Z> pre(n);
pre[0] = 1;
for (int i = 1; i < n; ++ i) {
Z sum = 0;
for (int j = 0; j <= k; ++ j) {
sum += cnt[i][j + k];
}
pre[i] = pre[i - 1] * sum;
}
std::vector<Z> f(k + 1);
f[0] = 1;
Z ans = pre[n - 1];
for (int i = n - 1; i > 0; -- i) {
std::vector<Z> g(k + 1);
for (int j = 0; j <= k; ++ j) {
for (int x = 0; x + j <= k; ++ x) {
g[j + x] += f[x] * cnt[i][j + k];
}
}
for (int j = -k; j < 0; ++ j) {
for (int x = 0; x + j <= 0; ++ x) {
g[0] += f[x] * cnt[i][j + k];
ans += pre[i - 1] * f[x] * cnt[i][j + k];
}
}
f = g;
}
if (a[0][0] == -1) {
ans *= k;
}
if (a[1][n - 1] == -1) {
ans *= k;
}
std::cout << ans << "\n";
}
int main() {
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
int t = 1;
std::cin >> t;
while (t -- ) {
solve();
}
return 0;
}