Codeforces Round #787 (Div. 3) F, G題題解
阿新 • • 發佈:2022-05-07
Codeforces Round #787 (Div. 3)
F. Vlad and Unfinished Business ( \(\color{#AAF}{1800}\) )
題意
給了點數為 \(n\) 的樹,和 \(k\) 個必須到達的點,出發的起點 \(x\) 和 到達終點 \(y\),邊權為 \(1\) ,詢問從起點經過指定的
\(k\) 個點,最後到達 \(y\) 的最短路徑花費, \(k\) 個點的訪問順序可以任意。
資料範圍
\(1\leq k \leq n \leq 2 * 10^5\)
思路
- 貪心地想,對於所有任務點必須到,然後要返回,那麼起點到這些點的路徑和至少為2倍到達這些點的花費。
- 問題轉化為,經過 \(k\) 個點的"最小生成樹",當然這裡不需要最小生成樹演算法,只需要知道我們不需要經過的點數即可,剩餘點數為 \(x\),路徑長度為 \(x-1\) 。
- 那麼現在訪問了所有點,要更貪心的想,從 \(x->y\) 我們是不是隻用走一次就行了,先把 \(y\) 以外的點搜完,最後來搜 \(y\) 的子樹即可,那麼 \(path:x->y\) 只需要經過一次。
- 所以最後答案為 \(2*(必經路徑上點數-1) - x->y的深度(距離)\) ,用 dfs 求子樹標記的方式來解,詳細見程式碼,時間複雜度 \(O(n)\)
Solution
#include<bits/stdc++.h> typedef long long ll; typedef std::pair<int, int> PII; typedef std::pair<ll, ll> PLL; typedef unsigned long long ull; #define x first #define y second #define pb push_back #define mkp make_pair #define endl "\n" using namespace std; const int N = 2e5 + 10; int n, k, x, y, ans; vector<int> edge[N]; int dep[N]; bool st[N]; int dfs(int u, int p){ int tot = 0; for(auto v: edge[u]){ if(v == p) continue; dep[v] = dep[u] + 1; tot += dfs(v, u); } if(!tot && !st[u]) ans--; return tot + (st[u] == true); } int main(){ ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; cin >> T; while(T--){ cin >> n >> k; cin >> x >> y; ans = n; vector<int> a(k + 1, 0); for(int i = 1; i <= k; i++){ cin >> a[i]; st[a[i]] = true; } st[x] = true, st[y] = true; for(int i = 1; i < n; i++){ int u, v; cin >> u >> v; edge[u].pb(v); edge[v].pb(u); } dfs(x, -1); cout << 2 * (ans - 1) - dep[y] << endl; for(int i = 1; i <= n; i++){ edge[i].clear(); st[i] = false; dep[i] = 0; } } return 0; }
G. Sorting Pancakes ( \(\color{#FB5}{2300}\) )
題意
給了一個長度 \(n\) 的陣列 \(a\) ,和保證為 \(m\), 每次只能將 \(a_i\) 減 1, \(a_{i-1}\; or\; a_{i+1}\) 加 1,詢問最小的操作次數使 \(a\) 非嚴格遞減。
資料範圍
\(1\leq n,m \leq 250\)
思路
- 毫無疑問,一道dp題,根據資料範圍,大概可以容忍 \(O(n^3)\) 級別的時空複雜度。
- 在設計狀態前,務必需要一個結論:
- 對於這樣的相鄰移動元素的操作而言,設操作後的結果為 \(b\)
- 對於這樣的相鄰移動元素的操作而言,設操作後的結果為 \(b\)
- 首先dp的階段是長度 \(i\),由於陣列和固定,考慮加入一維當前陣列的和 \(j\) ,由於要滿足遞增或者遞減,加入一維記錄最後一個數 \(k\) ,由於需要對比大小,這個數其實是一個最值。
- 那麼狀態已經設計出來了:
dp[i][j][k]
表示陣列前 \(i\) 位,和為 \(j\) ,最後一位最值為 \(k\) 的最小操作次數。 - 考慮如何轉移方便,如果按照題意原本描述來看,轉移方程是遍歷前一個數 \(x,k\leq x\)
dp[i][j][k] = min(dp[i][j][k], dp[i - 1][j - k][x] + abs(j - s[i]))
,觀察發現可以使用最小字尾來優化 \(x\) 的遍歷,此時預處理最小字尾複雜度為 \(O(n^3)\) - 而考慮反著做,即將陣列翻轉後,考慮將陣列構造成非嚴格遞增。那麼 \(k\) 代表的是最後一位的最大值。從小到大列舉\(k\) 時,需要的是最小字首,可以很方便的記錄, 如下程式碼所示
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
int mi = 0x3f3f3f3f;
for(int k = 0; k <= m - j; k++) {
mi = min(mi, dp[i - 1][j][k]); // 轉移 k 時,mi 記錄了上一位數值為 [0, k] 的最小值,因此是合法的
f[i][j + k][k] = min(f[i][j + k][k], mi + abs(s[i] - abs(j + k)));
}
}
}
- 時間複雜度:將 \(n\) 和 \(m\) 同級,\(O(n^3)\) ,翻轉陣列的方法還沒有想到如何優化成 \(O(n^2logn)\) 。
- 如果嘗試採用記錄字尾最小來優化,發現對於下標 \(i\) 上每個數不能超過
m / i
,否則它一定比前面的某個數大(平均值),加入迴圈條件中可以做到 \(O(n^2logn)\)。
- 如果嘗試採用記錄字尾最小來優化,發現對於下標 \(i\) 上每個數不能超過
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 260;
int n, m;
int a[N], s[N];
int dp[N][N][N];
// 將陣列翻轉
// dp[i][j][k] 前i個數,和為j,最後一個數最大值為k 滿足遞增序列最小操作
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = n; i >= 1; i--)
cin >> a[i];
for(int i = 1; i <= n; i++)
s[i] = s[i - 1] + a[i];
memset(dp, 0x3f, sizeof dp);
dp[0][0][0] = 0; // 前0個數,和為0,數最大值為0,操作次數為0
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
int mi = 0x3f3f3f3f;
for(int k = 0; k <= m - j; k++){
mi = min(dp[i - 1][j][k], mi);
dp[i][j + k][k] = min(dp[i][j + k][k], mi + abs((j + k) - s[i]));
}
}
}
int ans = 0x3f3f3f3f;
for(int i = 0; i <= m; i++)
ans = min(ans, dp[n][m][i]);
cout << ans << endl;
return 0;
}