ACGO 排位賽#12 - 題目解析
別問為什麼沒有挑戰賽#11,因為挑戰賽#11被貪心的 Yuilice 吃掉了(不是)。
本次挑戰賽難度相比較前面幾次有所提升。
爆料:小魚現在已經入職了研發部門,排位賽的算分級制將在未來的幾場排位賽中做出重大改變。之後就不會出現大家段位都很低的情況了。對於程度好的同學,稍微打一兩把排位賽段位就會有很大的提升。
第一題 - 50%𝐴𝐼, 50%𝐻𝑢𝑚𝑎𝑛
題目連結跳轉:點選跳轉
STL 大法真好,用自帶的 string.size()
方法就可以快速求出一個字串的長度。具體思路見程式碼,根據題目要求模擬就好了。
本題的 AC 程式碼如下:
#include <iostream>
#include <cmath>
using namespace std;
/*
PS: 信心倍增題目。
寫完這道題會讓你覺得這場比賽特別簡單。但其實不是。
*/
int n, cnt;
string str;
int main(){
cin >> n;
for (int i=1; i<=n; i++){
cin >> str;
if (str.size() > 5) cnt++;
}
int ans = ceil(1.0 * cnt / n * 100);
printf("%d%%AI, %d%%Human\n", ans, 100 - ans);
return 0;
}
第二題 - 統計區間內奇數與偶數的數量
題目連結跳轉:點選跳轉
對於這種區間的問題,可以先考慮一部分。如果要求出從 \([1, L]\) 區間內滿足條件的數字我們應該怎麼辦?假設 \(L\) 是一個偶數,那麼 \([1, L]\) 區間的奇數和偶數就應該是 \(L / 2\)。相同地,假設 \(L\) 是一個奇數,那麼 \([1, L]\) 區間的奇數和偶數就分別是 \(L / 2 + 1\) 和 \(L / 2\)。注意到 \(L / 2 + 1\) 和 \(L / 2\) 兩個公式都可以被簡寫成 \((L + 1) / 2\)。
設 \(odd(L)\) 和 \(even(L)\) 分別為區間 \([1, L]\) 的奇數個數和偶數個數。那麼可以得出結論,如果要求 \([L, R]\) 區間的奇數個數,答案就應該是 \(odd(R) - odd(L-1)\) 和 \(even(R) - even(L-1)\)。
本題的 AC 程式碼如下,程式碼並沒有使用函式,見諒:
#include <iostream>
using namespace std;
/*
思維水題。
稍微想一下找找規律就好了。
*/
int q, l, r, k;
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> q;
while(q--){
cin >> k >> l >> r;
if (k == 1) cout << ((r + 1) >> 1) - ((l) >> 1) << endl;
else cout << (r >> 1) - ((l - 1) >> 1) << endl;
}
return 0;
}
第三題 - 火星揹包 II
題目連結跳轉:點選跳轉
一道反向 \(0/1\) 揹包的模板題目。通常,\(0/1\) 揹包問題的目標是選擇若干物品,使得這些物品的總重量不超過揹包容量,並且這些物品的總價值最大化。而反向 \(0/1\) 揹包問題則是反其道而行之,其目標是選擇若干物品,使得這些物品的總重量不低於給定的最小值,並且這些物品的總價值最小化。
主要就是狀態的定義比較難:設 \(dp[j]\) 表示恰好湊出總重量為 \(j\) 的最小代價。那麼我們可以得到如下的狀態轉移方程:
根據狀態的定義,我們將 \(dp\) 陣列一開始全部初始化為正無窮大,同時另 \(dp_0 = 0\),表示恰好湊出重量為 \(0\) 的物品的最小代價為 \(0\),正無窮大代表暫時還沒有答案,需要在後續的迴圈中更新。
之後就是跑一遍正常的 Knapsack 01 程式碼就好了。在輸出答案的時候,我們需要找到一個滿足條件 \((dp[i] \le m)\) 的最大值。用一個 for
迴圈就可以搞定了。
本題的 AC 程式碼如下:
#include <iostream>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;
/*
反向 01 揹包題目。
問題不大,X03 的同學們在集訓營做過類似的題目(maybe 僅限杭州八月一期)。
*/
int n, m, sum;
int dp[100005], ans;
int w[1005], v[1005];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for (int i=1; i<=n; i++){
cin >> w[i] >> v[i];
sum += v[i];
}
memset(dp, 0x7f, sizeof dp);
dp[0] = 0;
for (int i=1; i<=n; i++){
for (int j=sum; j>=v[i]; j--){
dp[j] = min(dp[j], dp[j - v[i]] + w[i]);
}
}
for (int i=1; i<=sum; i++){
if (dp[i] <= m) ans = i;
}
cout << ans << endl;
return 0;
}
第四題 - 可分數列
題目連結跳轉:點選跳轉
題目很簡單,但需要仔細想一會兒才行。這道題的解決思路就是 找規律!沒錯,就是找一下規律就好了。我們只需要關注如何將這 \(4N + 2\) 個數列在去掉兩個元素後可以被平均分成 \(N\) 個等差數列。
透過手動模擬的方式,注意到我們只需要把這些數字豎著排列就可以解決問題。另每一列有四個元素,但其中有兩列有五個元素(代表 \(4N + 2\) 的 \(+2\) 部分),共有 \(N\) 列。例如數列 \([1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27]\)。豎著排列之後就是這個樣子的:
1 7 13 19 25
3 9 15 21 27
5 11 17 23
可以看到這些元素構成了三個等差數列,且兩個等差數列的長度為 \(5\)。那麼對於長度為 \(4\) 的等差數列我們完全可以不用管,直接輸出就好了。而對於這兩個長度為 \(5\) 的等差數列,我們考慮從這兩個等差數列中各刪除一個數字來構成新的等差數列。綜合來看,我們發現刪除元素 \(3\) 和 \(25\) 後,新的兩個等差數列依舊滿足題目限定的條件。
再多列舉幾個例子,發現規律也是相通的。那麼因此我們可以得出結論,對於任意一個長度為 \(4N + 2\) 的等差數列,只需要刪除這個等差數列的第 \(2\) 項和第 \(4N + 1\) 項,剩下的 \(4N\) 個元素一定可以組成 \(N\) 個長度為 \(4\) 的等差數列。
關於本題的更多資訊,可以閱讀 アイドル 老師的題解作為補充:連結跳轉。
找到規律後程式碼就很好寫了,本題的 AC 程式碼如下:
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
int n, x, d;
signed main(){
cin >> n >> x >> d;
if (n == 1){
cout << -1 << endl;
return 0;
}
cout << 2 << " " << 4 * n + 1 << endl;
for (int i=0; i<n; i++){
int t = x + i * d;
if (i == 1) t += n * d;
for (int j=0; j<4; j++)
cout << t + j * n * d << " ";
cout << endl;
}
return 0;
}
第五題 - 花火大會
題目連結跳轉:點選跳轉
這道題的輸出不是故意為難大家,因為輸出實在太大了,超出了 CF 限定了 64MB 的限制,因此只能將輸出地圖改為了輸出地圖的雜湊值(對於沒有學過雜湊的同學來說,輸出答案也是一個比較困難的事情)。
本道題的難度其實不大,就是一個多源不同權的無權最短路問題。但由於資料量比較大,使用普通的優先佇列維護會超時(別問我是怎麼知道的,我一開始就用了優先佇列,然後在 #13 測試點就 TLE 了),因此我們需要考慮如何找到一個 workaround 來替換優先佇列。
注意到我們只需要另外再使用一個 while
,在每次獲取頭節點的時候判斷某一個煙花是否剛好在該時間點燃放,如果煙花正好燃放,我們就把這個煙花的座標加入到佇列之中。
while(arr[cnt].z <= t.z && cnt <= k){
vis[arr[cnt].x][arr[cnt].y] = 1;
que.push(arr[cnt]); cnt ++;
}
由於每一個煙花每一個單位時間只會影響附近的一個格子,說明在放入煙花的時候佇列中隊頭和隊尾的權重是相同的,這樣就保證佇列內的元素權重一定是單調遞增的。這這種情況下,這道題就轉換成了使用 BFS 來求多源無權最短路的條件。
本題的 AC 程式碼如下:
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int MOD = 998244353;
int n, m, k, cnt = 2;
struct node{
int x, y, z;
} arr[1005];
queue<node> que;
int dis[5005][5005];
int vis[5005][5005];
int dx[] = {0, 1, -1, 0};
int dy[] = {1, 0, 0, -1};
bool cmp(node a, node b){
return a.z < b.z;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m >> k;
for (int i=1, x, y, z; i<=k; i++)
cin >> arr[i].x >> arr[i].y >> arr[i].z;
sort(arr+1, arr+1+k, cmp);
que.push(arr[1]);
vis[arr[1].x][arr[1].y] = 1;
while(!que.empty()){
node t = que.front();
que.pop();
if (dis[t.x][t.y]) continue;
dis[t.x][t.y] = t.z;
while(arr[cnt].z <= t.z && cnt <= k){
vis[arr[cnt].x][arr[cnt].y] = 1;
que.push(arr[cnt]); cnt ++;
}
for (int i=0; i<4; i++){
int cx = dx[i] + t.x;
int cy = dy[i] + t.y;
if (cx < 1 || cy < 1 || cx > n || cy > m) continue;
if (dis[cx][cy]) continue;
if (vis[cx][cy]) continue;
vis[cx][cy] = 1;
que.push((node){cx, cy, t.z+1});
}
}
long long ans = 0, p = 1;
for (int i=1; i<=m; i++)
p = (p * 233) % MOD;
for (int i=1; i<=n; i++){
for (int j=1; j<=m; j++){
p = (p * 233) % MOD;
ans = (ans + (dis[i][j] * p) % MOD) % MOD;
}
}
cout << ans << endl;
return 0;
}
第六題 - 劍之試煉
題目連結跳轉:點選跳轉
這道題超級噁心,我發自內心地對那些在比賽過程中 AC 此題目的同學表示尊敬。
思路上非常好想,由於所有的怪獸都要打,且打每一個怪獸的時間都是固定的,因此我們可以在一開始就先預處理出打完所有怪獸的時間,需要最佳化的就只有趕路的時間了。
先考慮每一層的情況,對於任意一層來說,關鍵點就是找到一個最優的路徑(從某一個起點出發,經過所有的點後再傳送到下一層的最短路徑)即可。學習過狀態壓縮動態規劃的同學可以很容易想到模板題【最短 Hamilton 路徑】(洛谷連結:連結跳轉)。
對於每一層來說,具體的實現方法和函式如下:
-
cover(dist, grid, "#*");
- 將
dist
和grid
中標記為"#"
或"*"
的所有障礙物進行覆蓋處理。這一步的作用是標記出所有不可通行的區域,為後續的距離計算或路徑規劃提供基礎。
- 將
-
getMinDist(dist, grid, "#");
- 計算從當前所在位置到
grid
中標記為"#"
(障礙物)位置的最短距離。這一步的作用是為後續的路徑規劃獲取到障礙物的距離資訊,方便下一步進行路徑選擇。
- 計算從當前所在位置到
-
hamilton(dist, grid);
- 在
dist
和grid
上執行哈密頓路徑(Hamiltonian Path)的計算,目的是找到一條經過所有目標點的路徑。這一步的作用是根據前面的距離資訊,找到一條經過所有必經點的路徑。
- 在
-
cover(dist, grid, "#.");
- 對
dist
和grid
中標記為"#"
(障礙物)和"."
(空地)的部分進行覆蓋處理。這一步是為了確保後續的距離計算排除已標記的障礙物,並識別可通行的路徑。
- 對
-
getMinDist(dist, grid, "#");
- 再次計算從當前所在位置到
grid
中標記為"#"
的最短距離。這一步是為了更新經過路徑覆蓋處理後的最短距離,確保得到最新的距離資訊。
- 再次計算從當前所在位置到
除此之外就是一些小的細節最佳化了,具體請參閱程式碼註釋。
本題的 AC 程式碼如下(本程式碼參考了黃老師的 std,並加以修改(我是不會說我陣列傳來傳去把自己傳死了,討厭指標的一天)),若需要更詳細的解答,可以 參考本文:
#include <iostream>
#include <algorithm>
#include <queue>
#include <cstring>
#include <vector>
using namespace std;
int n, m, k;
string map[30][105];
int dist[105][105];
struct node{
int x, y, w;
bool friend operator < (node a, node b){
return a.w > b.w;
}
};
const int dx[] = {1, 0, -1, 0};
const int dy[] = {0, 1, 0, -1};
/*
PS:本來想用純陣列加指標的方式做的。
後來寫著寫著把自己寫死掉了,還是 vector 方便。
普通的二維陣列傳來傳去簡直要我的命。
::: 雖然我的編譯器見不得 c++11 的標準,每次都給我提 warning。頭大。
*/
// 轉換,將每一個怪獸的血量都轉換成對應的數字。
int calc(char c){
if (c == '.') return 0;
if (c == 'R') return 1;
if (c == 'B') return 3;
if (c == 'D') return 8;
if (c == 'S') return 24;
if (c == 'G') return 36;
return 0x7f7f7f7f;
}
// 最普通的廣度優先搜尋演算法:
// 記錄從 sx, sy 出發,到當前層 grid 的每一個點的最短路徑。
// 其中 block 代表無法走的格子。
vector<vector<int> > bfs(vector<string> &grid, int sx, int sy, string block){
vector<vector<int> > dist(n, vector<int>(m, 0x7f7f7f7f));
dist[sx][sy] = 0;
struct node{
int x, y;
}; queue<node> que;
que.push((node){sx, sy});
while(!que.empty()){
node t = que.front();
que.pop();
for (int i=0; i<4; i++){
int cx = t.x + dx[i];
int cy = t.y + dy[i];
if (cx < 0 || cy < 0 || cx >= n || cy >= m) continue;
// 字串函式,判斷當前格子是否是障礙物,如果是障礙物的話就忽略。
if (block.find(grid[cx][cy]) != string::npos) continue;
if (dist[cx][cy] < 0x7f7f7f7f) continue;
dist[cx][cy] = dist[t.x][t.y] + 1;
que.push((node){cx, cy});
}
}
return dist;
}
// 將 dist 陣列根據 grid 復原。
// 如果當前位置無法被走到,則將 dist 更新為無窮大。
void cover(vector<vector<int> > &dist, vector<string> &grid, string block){
for (int i=0; i<n; i++){
for (int j=0; j<m; j++){
if (block.find(grid[i][j]) != std::string::npos)
dist[i][j] = 0x7f7f7f7f;
}
}
return ;
}
// dijkstra 最短路演算法。
// 主要作用是計算並更新每個節點到其餘節點的最短距離,並透過廣度優先搜尋(BFS)演算法來實現。
// dist[i][j] 是從一開始設定的起點到達 (i, j) 的累積代價,考慮了路徑上經過的每個格子的代價。
void getMinDist(vector<vector<int> > &dist, vector<string> &grid, string block){
priority_queue<node> que;
// 一開始把所有起點都入隊。
for (int i=0; i<n; i++){
for (int j=0; j<m; j++){
que.push((node){i, j, dist[i][j]});
}
}
while(!que.empty()){
node t = que.top();
que.pop();
if (dist[t.x][t.y] < t.w) continue;
for (int i=0; i<4; i++){
int cx = t.x + dx[i];
int cy = t.y + dy[i];
if (cx < 0 || cy < 0 || cx >= n || cy >= m) continue;
if (block.find(grid[cx][cy]) != string::npos) continue;
// 如果已經存在更優解了,就忽略。
if (dist[cx][cy] <= t.w + 1) continue;
dist[cx][cy] = t.w + 1;
que.push((node){cx, cy, t.w + 1});
}
}
}
// 計算漢密爾頓路徑
// 即從起點出發走完所有的點最後再回來的路徑最短路。
void hamilton(vector<vector<int> > &dist, vector<string> &grid){
struct node{
int x, y;
}; vector<node> vec;
for (int i=0; i<n; i++){
for (int j=0; j<m; j++){
if (grid[i][j] == '*')
vec.push_back((node){i, j});
}
}
int k = vec.size();
vector<vector<int> > f(k);
// f 陣列用於計算每一個關鍵節點(怪獸)之間的最短路。
for (int i=0; i<k; i++){
int sx = vec[i].x;
int sy = vec[i].y;
auto toOther = bfs(grid, sx, sy, "#");
for (int j=0; j<k; j++){
f[i].push_back(toOther[vec[j].x][vec[j].y]);
}
}
// 對於 Hamilton 路徑不熟悉的,直接去搜洛谷模板題先看一眼。
// X04-01 的同學應該是學過的(畢竟是我挑的題目)。
vector<vector<int> > dp(1 << k, vector<int>(k, 0x7f7f7f7f));
for (int i=0; i<k; i++){
int sx = vec[i].x;
int sy = vec[i].y;
dp[1 << i][i] = dist[sx][sy];
}
for (int i=0; i<(1<<k); i++){
for (int j=0; j<k; j++){
if (~i >> j & 1) continue;
for (int l=0; l<k; l++){
if (~(i ^ 1 << j) >> l & 1) continue;
dp[i][j] = min(dp[i][j], dp[i ^ 1 << j][l] + f[l][j]);
}
}
}
for (int i=0; i<k; i++){
int sx = vec[i].x;
int sy = vec[i].y;
dist[sx][sy] = dp[(1 << k) - 1][i];
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m >> k;
vector<vector<string> > grids(k, vector<string>(n));
for (auto &grid : grids){
for (auto &row : grid){
cin >> row;
}
}
int cost = k - 1;
for (auto &grid : grids){
for (auto &row : grid){
for (auto &c : row){
if (c != '#' && c != '.'){
cost += calc(c);
c = '*';
}
}
}
}
vector<vector<int> > dist = bfs(grids[0], 0, 0, "#*");
/*
先排除掉某些障礙物和關鍵點,以計算初步的最短路徑。
再使用 hamilton 函式來處理關鍵點之間的路徑規劃,得到所有關鍵點的最優路徑。
最後進一步排除空白區域和障礙物,重新計算網格中各點之間的最短路徑,得到最終結果。
*/
for (auto &grid : grids){
cover(dist, grid, "#*");
getMinDist(dist, grid, "#");
hamilton(dist, grid);
cover(dist, grid, "#.");
getMinDist(dist, grid, "#");
}
// 計算答案。
int ans = 0x7f7f7f7f;
for (int i=0; i<n; i++){
for (int j=0; j<m; j++){
ans = min(ans, dist[i][j]);
}
}
cout << ans + cost << endl;
return 0;
}