最後更新日期 2024 / 01 / 01
上下有個有一根木樑
然後中間有 n 條可能會相交的木板
找出最多可以釘上幾條木板並構造最大字典序的解
(細節自己去 TIOJ 看 :<)
因為這題看起來沒人寫題解
於是我就發了這篇文章 ouob
仔細觀察問題
我們可以把問題轉換成 LIS 或 LCS
LCS 的話應該不難理解
因為要釘最多木板其實就是要在上下兩條陣列各選一個共同的子序列
然後把每個數字一一對應釘起來
但是 LCS 需要
所以一定會 TLE
接著是 LIS 的作法
仔細觀察題目的話可以發現
如果木板有"座標"的話
其實我們在做的事情就是"對木板做 LIS"
更精確一點的說
就是我們其實可以把木板在上樑的點跟下樑的點綁成一個 pair 做 LIS
我們說木板 A 的座標小於木板 B 代表 A 的下樑座標跟上樑座標都小於 B
至於座標的部分大概就是這個意思:
以附圖為例
木板 3 的座標就是 (2, 1)
木板 4 的座標就是 (5, 3)
木板 5 的座標就是 (4, 4)
如果用最普通的 DP 方式
就算能構造解也很難求最大字典序
於是我們修改一下計算方式
把轉移式改成 “定義
其實就只是倒著做 LIS 然後 Increasing 改成 Decreasing 就好了
做完 DP 之後
我們要從 dp 值大的開始構造
先找一個 dp 值跟最終 LIS 一樣長而且字典序最大的元素
接著往右找小一點的直到 1 為止
關於上面提到的"座標"
實際上不需要真的把座標算出來
我們只需要把第二個陣列(代表下梁座標的陣列)每一個元素替換成該元素在第一個陣列出現的位置
並對第二個陣列做 LIS 就可以了
原因的話講起來有點麻煩 自己想吧
第一次傳 WA 了一片
然後 debug 的時後測出這樣的測資會出錯
如果你也找不到你的程式坐在哪裡的話
不仿測試看看這筆測資
3
3 1 2
1 2 3
#include <bits/stdc++.h>
using namespace std;
#define all(x) x.begin(), x.end()
#define io ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
const int MAXN = 2e5 + 10;
// map 會超時,所以改 unordered_map
unordered_map<int, int> rpos;
unordered_map<int, int> pos;
vector<int> lens[MAXN];
int v[MAXN];
bool cmp(int a, int b) {
return rpos[v[a]] > rpos[v[b]];
}
signed main() {
io;
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
int tmp;
cin >> tmp;
// 紀錄元素座標
pos[tmp] = i;
}
for (int i = 1; i <= n; i++) {
int tmp;
cin >> tmp;
// 將陣列的值設定為目前元素在上面陣列的出現位置
v[i] = pos[tmp];
// 把坐標在映射回他的值,輸出的時候會用到
rpos[pos[tmp]] = tmp;
}
// LIS 的部分
// 作法類似:https://usaco.guide/gold/lis?lang=cpp
int len = 0;
vector<int> dp;
for (int i = n; i >= 1; i--) {
int ans = 0; // 儲存目前 dp 值:以 i 為開頭能構造出多長的 LIS
if (dp.empty() || dp.back() > v[i]) {
dp.push_back(v[i]);
// 紀錄目前的 dp 值
ans = dp.size();
} else {
auto ite = upper_bound(all(dp), v[i], greater<int>());
*ite = v[i];
// 紀錄目前的 dp 值
ans = ite - dp.begin() + 1;
}
// len 維護最終 LIS 長度
len = max(len, ans);
// 紀錄每個長度的解
lens[ans].push_back(i);
}
// 構造解
int cur = 0;
for (int i = len; i >= 1; i--) {
// 依照字典序做 sort
sort(all(lens[i]), cmp);
for (auto j : lens[i]) {
// 排除在目前選到的元素左邊的元素
if (j <= cur) continue;
// 排除加入後會相交的元素 (用於排除文章中寫的“一個小測資”那段的 Case)
if (v[j] < v[cur]) continue;
cur = j;
// 把 index 映射回去並輸出
cout << rpos[v[j]] << " ";
break;
}
}
}
兩個人到一個 n times m 大的迷宮裡探險 在迷宮裡只能往右和往下走 而入口在 1, 1 出口在 n, m 求離開迷宮最多能帶走多少寶藏 迷宮示意圖如下problem image應該不難聯想到這題 CSES - Grid Paths 只要把狀態開成四維 分別代表第一個人的 xy 座標及第二個人的 xy 坐標 然後可以推出:begin{equation}dpijkl = gridij + text{max} begin{cases} dpi-1jk-1l dpi-1jkl-1 dpij-1k-1l dpij-1kl-1 end{cases}end{equation}不過加上 gridij 的部份有些細節要處理 詳細轉移的部份自己看下面的程式碼在本地測試了一下就順利通過了範測 ouob結果剛剛的作法傳上去之後很快就拿到 TLE 了 於是就想說順便來嘗試看看最近剛學到的壓常小技巧cpp加上去後通過了更多比測資但依然還是 TLE又仔細思考了一下題目 發現 i + j neq k + l 的狀態一定不合法 所以就把不合法的狀態 continue 掉然後就神奇的成功壓常 AC 了 :Dcppusing namespace std;const int MAXN = 1e2 + 3;const int INF = 32000;int mpMAXNMAXN;int dpMAXNMAXNMAXNMAXN;signed main { iosbase::syncwithstdio0; cin.tie0; cout.tie0; int n, m; cin n m; 把邊界都設定為 -INF for int i = 0; i = n + 1; i++ { for int j = 0; j = m + 1; j++ { for int k = 0; k = n + 1; k++ { for int l = 0; l = m + 1; l++ { if i == 0 || i == n+1 || j == 0 || j == m+1 || k == 0 || k == n+1 || l == 0 || l == m+1 { dpijkl = -INF; } } } } } 初始狀態 dp0000 = 0; dp0101 = 0; dp1010 = 0; dp0110 = 0; dp1111 = 0; 輸入整張地圖 for int i = 1; i = n; i++ { for int j = 1; j = m; j++ { cin mpij; } } DP for int i = 1; i = n; i++ { for int j = 1; j = m; j++ { for int k = 1; k = n; k++ { for int l = 1; l = m; l++ { 一點點剪枝 if i + j = k + l continue; if mpij == -1 || mpkl == -1 { dpijkl = -INF; continue; } 轉移 dpijkl = max{ dpi-1jk-1l, dpi-1jkl-1, dpij-1k-1l, dpij-1kl-1, }; 拿到寶藏增加分數 dpijkl += mpij == 1 + mpkl == 1 i = k || j = l; } } } } 輸出答案 cout maxdpnmnm, short0 endl;}
TIOJ這次 APCS 跟 ISSC 撞期 然後我就放掉 APCS 跑去打 ISSC : D 下一次要慢三天才能報了 qwq數線上有一隻老鼠 且有 n 個食物散落在數線上 老鼠可以選擇一直往左或一直往右 求最多可以吃到幾個食物及最後吃食物的位置把問題轉換一下 就變成是在問老鼠左邊還是右邊食物比較多、最左邊或最右邊的食物在哪裡 於是只要簡單維護一下就可以幾個變數就可以解出來了基本語法cppusing namespace std;signed main { int x, n, tmp; cin x n; int cntl = 0, cntr = 0; 左邊與右邊食物數量 int maxv = -1e5, minv = 1e5; 用最大與最小值維護最後停在的食物位置 while n-- { cin tmp; if tmp = x cntr++; 如果食物在左邊 if tmp = x cntl++; 如果食物在右邊 maxv = maxmaxv, tmp; minv = minminv, tmp; } if cntl cntr cout cntl " " minv endl; 往做邊走解更優 else cout cntr " " maxv endl; 否則決定往右邊走}在一個 n times m 大小的表格上每格裡都有一張牌 你可以水平或垂直消除數值為 x 的兩張牌並獲得 x 分,但中間不能有其他牌 求最多能獲得多少分枚舉每張牌,嘗試上或左匹配是否存在相同的牌即可 使用 -1 紀錄已經匹配掉的牌至於為什麼只要匹配左上不用考慮右下 是因為左跟右、還有上跟下是相對關係我比較在意的地方是 本來以為這個動作需要重複多次才能匹配所有牌 結果竟然一次就過了,不知道是不是 ZeroJudge 測資太弱 🤔或者其實有辦法證明這個作法最多只需要一次更新關於這個問題,感謝 MSQRT 電神的補充 確實 Zerojudge 測資有疏失 已加強程式碼!二維陣列、模擬cppusing namespace std;int tb5050; 表格signed main { int n, m; cin n m; 輸入資料 for int i = 0; i n; i++ { for int j = 0; j m; j++ { cin tbij; } } int ans = 0; 重複執行匹配動作至少 nm 次以配對所有卡牌 for int = 0; 1000; ++ { for int i = 0; i n; i++ { for int j = 0; j m; j++ { if tbij == -1 continue; 嘗試往上配對 if i 0 { int k = i-1; while k = 0 tbkj == -1 k--; if tbij == tbkj { 配對成功 ans += tbij; 增加分數 tbij = tbkj = -1; 將兩張牌紀錄乘已匹配 continue; 匹配成功後即可忽略這張牌 } } 嘗試往左配對 if j 0 { int k = j-1; while k = 0 tbik == -1 k--; if tbij == tbik { 配對成功 ans += tbij; 增加分數 tbik = tbij = -1; 將兩張牌紀錄乘已匹配 continue; 匹配成功後即可忽略這張牌 } } } } } cout ans endl;}一個 n times m 的表格上每格有一個水管 水管分成 H、I、F、7、L 等類型 每種水管可能連接到上、下、左、右其中某些方向 求將水管連起來後最大連通塊本題詳細題意建議直接進 ZeroJudge 題目頁面查看簡單來說只要對整張圖做 DFS 並一邊維護連通塊大小即可 比較麻煩的地方就是建圖而已 這份程式碼部份參考了 dreamoon 老師的 code 才得以把它改得這麼乾淨的 時間複雜度 Onm圖論、DFS BFScppusing namespace std; 上 右 下 左mapchar, vectorint mask{ {'X', {1,1,1,1}}, {'I', {1,0,1,0}}, {'H', {0,1,0,1}}, {'L', {1,1,0,0}}, {'7', {0,0,1,1}}, {'F', {0,1,1,0}}, {'J', {1,0,0,1}}, {'0', {0,0,0,0}},};vectorint graph500000; 存圖bool vis500000; 紀錄造訪過的點int ans = 0, cur = 0;int n, m; 對每座標分配一個 idint id int x, int y { return x m + y;} DFS 並維護連通塊大小void dfsint id { cur++; visid = true; for auto u : graphid { if visu continue; visu = true; dfsu; }}signed main { cin n m; vectorstring vn; for int i = 0; i n; i++ cin vi; 建立整張圖 for int i = 0; i n; i++ { for int j = 0; j m; j++ { 上配下 if i + 1 n maskvij2 maskvi + 1j0 { graphidi, j.pushbackidi + 1, j; graphidi + 1, j.pushbackidi, j; } 左配右 if j + 1 m maskvij1 maskvij + 13 { graphidi, j.pushbackidi, j + 1; graphidi, j + 1.pushbackidi, j; } } } 對每一個節點做 DFS for int i = 0; i n; i++ { for int j = 0; j m; j++ { cur = 0; dfsidi, j; ans = maxans, cur; 嘗試更新答案 } } cout ans endl;}題目~~廢話~~劇情就不多寫了 簡單來說就是求最大連續子序列合,但可以跳過 k 個元素在不考慮 k 的情況下 這是一個非常經典的問題 解法的話也非常多元 以現在這題來說 Kadane's Algorithm 應該會是比較好做的那如果把 k 的條件加進去呢? 把測資範圍仔細讀過之後可以注意到 k leq 20 考慮動態規劃的作法 我們定義 dpi 為考慮陣列前 i 個元素的最佳解 然後我們可以往前跳過不超過 k 個 因此 dpi 可以從 dpi-1、dpi-2 cdots dpi-k-1 轉移過來取 max 但我們還需要紀錄總共已經使用了幾個金牌 所以再開一個維度來維護這個資訊綜合以上幾點,我們可以推出這樣的轉移式 定義 dpij 為考慮前 i 個元素且使用 j 個金牌的最佳解 轉移的部份 dpij = max{1 leq l leq k}dpi-l-1j-l + vali最後照著轉移式把程式寫出來就好ㄌ時間複雜度 On times k^2更新感謝 Colten 提出了 更好的作法 dpij 一樣是代表在第 i 天使用 j 個金牌的最佳解 不過轉移的部份只需要對 dpi-1j-1 和 dpi-1j + vali 取 max 就可以 兩個狀態分別代表在第 i 天使用和不使用金牌 時間複雜度直接少了一個 k 降到 On times k 下面程式碼也用這個寫法實現然後根據 吳邦一教授的說法 原本 On times k^2 的作法如果使用 Python 的話時間應該不會過動態規劃cppusing namespace std;int v200000;int dp20000030;signed main { int n, k; cin n k; for int i = 1; i = n; i++ { 第 i 天 cin vi; 因為只會依賴到之前的狀態所以可以邊讀入邊做 for int j = 0; j = k; j++ { 使用 j 個金牌 if j 0 dpij = dpi-1j-1; 使用金牌 dpij = maxdpij, dpi-1j + vi; 不使用金牌 } } int ans = 0; 在所有合法狀態中取得最佳解 for int i = 0; i = k; i++ { for int j = 1; j = n; j++ { ans = max{ans, dpji, 0LL}; } } cout ans endl;}整體來說 這次 APCS 其實不算太難(? 第三題實做比較麻煩一點以外 最後一題 DP 轉移式也還算好推
APCS有點怕遲到很早出門 結果正式開始前半小時就到師大了 不過教室已經開了 就到裡面坐著寫寫題等工作人員準備一下之後 很快就開放參與者報到了 領了名牌還有幾個師大的紀念品 因為下午團體賽要分組所以也抽了組別的籤比賽時常 2.5 小時總共有 8 題 開賽後先打開了第一題 ~~但看到一堆複雜的算式所以就先跳過了~~ 第二題感覺就是排序裸題 ~~但沒看很仔細導致看不懂題目所以也跳過了~~ 第三題是字串模擬題 我決定從這題開始下手 結果因為最近用 Mac 而且家裡 Windowns 鍵盤跟比賽的差很多的關係 打字打的超痛苦 方向鍵位置差很多而且要按 ctrl 一直按到 alt 寫到心態很崩 這種水題傳上去甚至還吃了一個 WA ~~我甚至在想為什麼我要花時間來這裡受苦受難~~ 重新看題目後發現原來完全理解錯意思了 修改一下之後總算 AC 了一題 翻開第四題 看到測資範圍到了 10^5 總算看到一題算法題了 大概花幾分鐘思考就把雙指針寫完 不過還是吃了幾次 WA 加上讀題跟 debug 大概總共花了半小時才 AC 這題第五題 很明顯的水題目 簡單陣列操作而已 大概只花了幾分鐘就寫完這題 AC 第六題 裸的 STL prorityqueue 題 寫起來也還算順手 1 發 AC接下來是第七題 ~~但看起來有點難所以先看第八題~~ 第八題是括號運算式 + 二分搜或枚舉 不過看起來實做有點麻煩 所以先回去看前面的題目好了重新讀懂第二題之後花了一點時間就 AC 了回到第一題 仔細看題目之後發現 原來似乎只是要模擬計算過程而已 不過浮點數感覺很棘手 總之還是先寫在說 結果傳上去馬上吃了一個 WA 花了一點時間 debug 之後獲得 20 分子題分數 不會有小數點精度問題的子題 qwq最剩下半小時我決定來寫最後一題目 把括號運算式弄好了 枚舉也做好了 不過一直吃 WA 比賽就這樣結束了總分:620 打的比上次還慘先上一張超讚的午餐圖片簡單來說就是比薩+飲料 然後系主任還有一個一個同學發巧克力 QQ 球 系主任人超好 然後吃完午餐就是頒獎跟摸彩時間 結果意外的拿了個人賽第二名 獲得了一副藍牙耳機 摸彩還摸到前幾大獎 可惜前三名不能拿摸彩獎品所以重抽了接著是神奇的團康小遊戲(? 他們會發給每個人一張表格 上面有很多格子 每個格子寫著不同內容 例如「我來自雲林」、「我近視不到 200 度」、「我知道 DFS」之類的 要找符合內容的人簽名 最後先連成一條、兩條、三條線的可以獲得獎品 總之我沒成功拿到獎品就是了 QAQ這次的團體賽玩的是 Same Game 簡單來說就是在盤面上找到兩個以上同色球連再一起消除得分 消除後空格會有上面的球掉下來 獲得越多分數的排名越高 不過跟一般的差別就在我們要使用程式操作這個遊戲一剛開始的想法是 有沒有可能這就是某種很酷的 dp 題目 畢竟目前狀態消除不同位置都會讓不同的球往下掉 也就會改變未來的狀態進而影響結果 不過後來想了一下沒什麼頭緒 但看到了第一步可以花 20 秒計算 於是我就想說 不如來試試看 Minimax Algorithm 吧 以前做遊戲的電腦 AI 時用過 想說感覺應該可以用在這邊 其實簡單來說就是暴力枚舉 k 步後的所有狀態在選擇最佳的走而已 花了快一個小時寫完後 結果才跑兩層記憶體就炸了 qwq 嗯 預測一兩步跟沒預測一樣 所以只好果斷放棄這個做法想起剛剛在講解規則時 有提到一些策略 於是我們就先把那些能做的都做出來 主要就是兩個「先固定消一個顏色,消完再消下一個」跟「先把連通塊小的顏色消掉」 於是我們就拿了第三名 ???帶走了一個安妮亞滑鼠墊 : D整體來說還蠻好玩的 午餐很讚而且也帶了不少戰利品 還認識了一些人 ~~準確來說是只認識了一個人 TAT~~
Journal