- 寫在前面
- A 簽到
- B 列舉,結論
- C 列舉,搜尋
- D1 DP,單調性
- D2 DP,單調性,計數
- 寫在最後
寫在前面
比賽地址: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:決策單調性。
然後是日常的夾帶私貨,媽的從實裝之後群裡每天發一萬張偶像瑪麗看得我這幾天是從睜眼嗯到閉眼啊爽死了。