Codeforces Round 982 (Div. 2)
写在前面
比赛地址:https://codeforces.com/contest/2027。
前期犯唐手慢了呃呃还好把 D1D2 秒了救回来了。
厌学中。
A 签到
考虑小学数学求图形面积经典套路——平移法!不会的小朋友请看:巧求不规则图形周长,三年级学生请收藏,平移法竟能如此好用。
发现一种最优的构造类似样例 1,考虑平移法答案即 \(2\times (\max w + \max h)\)。
你上过小学吗?我觉得我上过。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 110;
//=============================================================
//=============================================================
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
int n; std::cin >> n;
int maxx = 0, maxy = 0;
for (int i = 1; i <= n; ++ i) {
int x, y; std::cin >> x >> y;
maxx = std::max(maxx, x);
maxy = std::max(maxy, y);
}
std::cout << 2 * (maxx + maxy) << "\n";
}
return 0;
}
B 枚举,结论
为什么叫斯大林排序啊我草,有点地狱了我草。
发现若某个数列是合法的,即进行若干次排序后不增,则可以再进行若干次排序,使得该数列长度为 1,即仅保留最大值。
考虑枚举删完后剩余数列的第一个数的位置 \(i\),该数之前的数都需要被删除;易知该数一定为排序后数列的最大值,则在其之后的比他大的数都需要被删除。
则最少删除次数即为:
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2010;
//=============================================================
int n, a[kN];
//=============================================================
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n;
for (int i = 1; i <= n; ++ i) std::cin >> a[i];
int ans = n;
for (int i = 1; i <= n; ++ i) {
int sum = i - 1;
for (int j = i + 1; j <= n; ++ j) sum += (a[j] > a[i]);
ans = std::min(ans, sum);
}
std::cout << ans << "\n";
}
return 0;
}
C 枚举,搜索
发现会产生贡献的只有原数组的位置,且每个位置至多贡献一次,且产生贡献前的数组长度肯定为 \(a_i + i - 1\),产生贡献后的长度肯定为 \(a_i + i - 1 + (i - 1)\)。
考虑将上述 \(O(n)\) 种数组的长度看做节点,进行一次操作对数组长度的影响看做一条有向边的转移,发现产生贡献的顺序实际上构成一个 DAG 的形态,每个节点都对应操作后数组的一种可能长度。在 DAG 上搜索求能转移到的最远的点即可。
注意要钦定每个点只能经过一次,不然会 TLE on 5。
虽然我实现上显式地建了图写了 BFS,但实际上并无必要,直接 DFS 并隐式地转移即可。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 3e5 + 10;
//=============================================================
LL n, nodenum;
LL a[kN], b[kN], len[kN << 1];
std::map <LL, std::vector<LL> > node;
std::map <LL, LL> id;
std::vector<LL> edge[kN << 1];
bool vis[kN << 1];
LL ans;
//=============================================================
void topsort() {
ans = n + 1;
for (int i = 1; i <= nodenum; ++ i) vis[i] = 0;
std::queue<LL> q;
if (len[n + 1] == n + 1) q.push(n + 1);
while (!q.empty()) {
LL u = q.front(); q.pop();
if (vis[u]) continue;
vis[u] = 1;
if (u <= n) ans = std::max(ans, len[u] + u - 1);
for (auto v: edge[u]) {
if (!vis[v]) q.push(v);
}
}
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n;
for (int i = 1; i <= 2 * n; ++ i) edge[i].clear();
node.clear(), id.clear();
nodenum = n;
for (int i = 1; i <= n; ++ i) {
std::cin >> a[i], b[i] = a[i] + i, node[b[i]].push_back(i);
len[i] = b[i];
}
for (auto [x, vec]: node) {
if (x < n + 1) continue;
id[x] = ++ nodenum;
len[nodenum] = x;
}
for (auto [x, vec]: node) {
if (x < n + 1) continue;
for (auto p: vec) {
edge[id[x]].push_back(p);
if (id.count(x + p - 1)) edge[p].push_back(id[x + p - 1]);
}
}
topsort();
std::cout << ans - 1 << "\n";
}
return 0;
}
/*
1
5
2 4 6 2 5
*/
D1 DP,单调性
特别指出了 \(n\cdot m\le 3\times 10^5\),看来是非常有用的条件。
发现最优的操作序列一定是交替进行多次操作 1 和操作 2,于是考虑 DP,设使用操作 \(1\sim i\),恰好删除了前缀 \(1\sim j\) 的最小代价,初始化 \(f_{0, 0} = 0\) 则显然有转移:
答案即为 \(f_{m, n}\),朴素实现时间复杂度 \(O(n^2m)\) 级别,空间复杂度 \(O(nm)\) 级别,时空双爆炸啊有点呃呃。
考虑优化,发现第一维可以滚动数组优化,空间复杂度变为 \(O(m)\) 级别;观察易知对于某个确定的 \(i\),当 \(j\) 递增时 \(f_{i, j}\) 肯定单调不降,则第二种转移的最优决策有单调性,一定会选择在 \(b_i \ge \sum_{j-k\le l\le j} a_l\) 的最大的 \(k\) 处进行转移,套路地考虑双指针优化即可。
总时间复杂度变为 \(O(nm)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 3e5 + 10;
const LL kInf = 1e18;
//=============================================================
int n, m, a[kN], b[kN];
LL sum[kN], f[2][kN];
//=============================================================
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n >> m;
for (int i = 1; i <= n; ++ i) std::cin >> a[i], sum[i] = sum[i - 1] + a[i];
for (int i = 1; i <= m; ++ i) std::cin >> b[i];
int now = 1;
for (int i = 1; i <= n; ++ i) f[0][i] = kInf;
for (int i = 1; i <= m; ++ i, now ^= 1) {
for (int r = 1, l = 1; r <= n; ++ r) {
f[now][r] = f[now ^ 1][r];
while (l <= r && sum[r] - sum[l - 1] > b[i]) ++ l;
if (l <= r) f[now][r] = std::min(f[now][r], f[now][l - 1] + m - i);
}
}
std::cout << (f[now ^ 1][n] < kInf ? f[now ^ 1][n] : -1) << "\n";
}
return 0;
}
D2 DP,单调性,计数
套用 D1 的状态,发现最优决策计数同样仅需考虑当前使用了哪些操作与删除的前缀长度,于是记 \(g_{i, j}\) 表示使用操作 \(1\sim i\),恰好删除了前缀 \(1\sim j\),代价最小时的方案数,初始化 \(g_{0, 0} = 1\)。则有显然的转移:
答案即为 \(g_{m, n}\)。
同样考虑单调性优化,设最优的对 \(f\) 的决策为 \(f_{i, j - k}\),由上式可知,产生贡献的 \(g_{i, j - k}\) 一定是以 \(j-k\) 为左端点的一段区间 \([j-k, j - k']\),该区间内所有状态 \(f_{i, j-k}\sim f_{i, j - k'}\) 的值均相等,且当 \(j\) 增加时该区间的右端点一定是单调不降的。
于是考虑双指针优化 DP 时再维护一个指针表示对 \(g\) 产生贡献的区间,使用前缀和优化转移即可。
总时间复杂度仍为 \(O(nm)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 3e5 + 10;
const LL kInf = 1e18;
const LL p = 1e9 + 7;
//=============================================================
int n, m, a[kN], b[kN];
LL sum[kN], f[2][kN], g[2][kN], pre[kN];
//=============================================================
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n >> m;
for (int i = 1; i <= n; ++ i) std::cin >> a[i], sum[i] = sum[i - 1] + a[i];
for (int i = 1; i <= m; ++ i) std::cin >> b[i];
int now = 1;
f[0][0] = f[1][0] = 0, g[0][0] = g[1][0] = 1;
for (int i = 1; i <= n; ++ i) f[0][i] = kInf, g[0][i] = 0;
for (int i = 1; i <= m; ++ i, now ^= 1) {
for (int r = 1, l = 1, r1 = 0; r <= n; ++ r) {
f[now][r] = f[now ^ 1][r], g[now][r] = g[now ^ 1][r];
while (l <= r && sum[r] - sum[l - 1] > b[i]) ++ l;
if (l <= r) {
r1 = std::max(r1, l - 1);
while (r1 < r - 1 && f[now][r1 + 1] == f[now][l - 1]) ++ r1;
if (f[now][r] > f[now][l - 1] + m - i) {
f[now][r] = f[now][l - 1] + m - i;
g[now][r] = (pre[r1] - pre[l - 1] + g[now][l - 1] + p) % p;
} else if (f[now][r] == f[now][l - 1] + m - i) {
(g[now][r] += (pre[r1] - pre[l - 1] + g[now][l - 1] + p) % p) %= p;
}
}
pre[r] = (pre[r - 1] + g[now][r]) % p;
}
}
if (f[now ^ 1][n] >= kInf) {
std::cout << -1 << "\n";
} else {
std::cout << f[now ^ 1][n] << " " << g[now ^ 1][n] << "\n";
}
}
return 0;
}
写在最后
学到了什么:
- C:图不是很复杂且要在图上运行的算法很简单,隐式建图,好。
- D:决策单调性。
然后是日常的夹带私货,妈的从实装之后群里每天发一万张偶像玛丽看得我这几天是从睁眼嗯到闭眼啊爽死了。

浙公网安备 33010602011771号