最後更新日期 2024 / 01 / 01

[題解] TIOJ 2173 收集寶藏 (北市賽 2019 pE)

題意

兩個人到一個 n×m 大的迷宮裡探險
在迷宮裡只能往右和往下走
而入口在 (1,1) 出口在 (n,m)
求離開迷宮最多能帶走多少寶藏
迷宮示意圖如下

problem image

很暴力的 DP

應該不難聯想到這題 CSES - Grid Paths
只要把狀態開成四維
分別代表第一個人的 xy 座標及第二個人的 xy 坐標
然後可以推出:

dp[i][j][k][l]=grid[i][j]+max{dp[i1][j][k1][l]dp[i1][j][k][l1]dp[i][j1][k1][l]dp[i][j1][k][l1]

不過加上 grid[i][j] 的部份有些細節要處理
詳細轉移的部份自己看下面的程式碼

在本地測試了一下就順利通過了範測 ouob

快樂壓常時間

結果剛剛的作法傳上去之後很快就拿到 TLE 了
於是就想說順便來嘗試看看最近剛學到的壓常小技巧

#define int short

加上去後通過了更多比測資但依然還是 TLE

又仔細思考了一下題目
發現 i+jk+l 的狀態一定不合法
所以就把不合法的狀態 continue 掉

然後就神奇的成功壓常 AC 了 :D

程式碼

#include <bits/stdc++.h>
using namespace std;
#define int short
#define endl "\n"

const int MAXN = 1e2 + 3;
const int INF = 32000;

int mp[MAXN][MAXN];
int dp[MAXN][MAXN][MAXN][MAXN];

signed main() {
    ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    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) {
                        dp[i][j][k][l] = -INF;
                    }
                }
            }
        }
    }
    // 初始狀態
    dp[0][0][0][0] = 0;
    dp[0][1][0][1] = 0;
    dp[1][0][1][0] = 0;
    dp[0][1][1][0] = 0;
    dp[1][1][1][1] = 0;
    // 輸入整張地圖
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cin >> mp[i][j];
        }
    }
    // 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 (mp[i][j] == -1 || mp[k][l] == -1) {
                        dp[i][j][k][l] = -INF;
                        continue;
                    }
                    // 轉移
                    dp[i][j][k][l] = max({
                        dp[i-1][j][k-1][l],
                        dp[i-1][j][k][l-1],
                        dp[i][j-1][k-1][l],
                        dp[i][j-1][k][l-1],
                    });
                    // 拿到寶藏增加分數
                    dp[i][j][k][l] += (mp[i][j] == 1) + (mp[k][l] == 1) * (i != k || j != l);
                }
            }
        }
    }
    // 輸出答案
    cout << max(dp[n][m][n][m], (short)0) << endl;
}

推薦文章

[題解] TIOJ 2195 粉刷護欄 (TOI 2021 初選 pC)

上下有個有一根木樑 然後中間有 n 條可能會相交的木板 找出最多可以釘上幾條木板並構造最大字典序的解 細節自己去 TIOJ 看 :因為這題看起來沒人寫題解 於是我就發了這篇文章 ouob仔細觀察問題 我們可以把問題轉換成 LIS 或 LCSLCS 的話應該不難理解 因為要釘最多木板其實就是要在上下兩條陣列各選一個共同的子序列 然後把每個數字一一對應釘起來 但是 LCS 需要 On^2 的時間複雜度才能計算 所以一定會 TLE接著是 LIS 的作法 仔細觀察題目的話可以發現 如果木板有"座標"的話 其實我們在做的事情就是"對木板做 LIS" 更精確一點的說 就是我們其實可以把木板在上樑的點跟下樑的點綁成一個 pair 做 LIS 我們說木板 A 的座標小於木板 B 代表 A 的下樑座標跟上樑座標都小於 B至於座標的部分大概就是這個意思: 以附圖為例 木板 3 的座標就是 2, 1 木板 4 的座標就是 5, 3 木板 5 的座標就是 4, 4如果用最普通的 DP 方式 就算能構造解也很難求最大字典序 於是我們修改一下計算方式 把轉移式改成 "定義 dpi 為把第 i 個元當頭素能構造出的 LIS" 其實就只是倒著做 LIS 然後 Increasing 改成 Decreasing 就好了做完 DP 之後 我們要從 dp 值大的開始構造 先找一個 dp 值跟最終 LIS 一樣長而且字典序最大的元素 接著往右找小一點的直到 1 為止關於上面提到的"座標" 實際上不需要真的把座標算出來 我們只需要把第二個陣列代表下梁座標的陣列每一個元素替換成該元素在第一個陣列出現的位置 並對第二個陣列做 LIS 就可以了 ~~原因的話講起來有點麻煩 自己想吧~~第一次傳 WA 了一片 然後 debug 的時後測出這樣的測資會出錯 如果你也找不到你的程式坐在哪裡的話 不仿測試看看這筆測資33 1 21 2 3cppusing namespace std;const int MAXN = 2e5 + 10; map 會超時,所以改 unorderedmapunorderedmapint, int rpos;unorderedmapint, int pos;vectorint lensMAXN;int vMAXN;bool cmpint a, int b { return rposva rposvb;}signed main { io; int n; cin n; for int i = 1; i = n; i++ { int tmp; cin tmp; 紀錄元素座標 postmp = i; } for int i = 1; i = n; i++ { int tmp; cin tmp; 將陣列的值設定為目前元素在上面陣列的出現位置 vi = postmp; 把坐標在映射回他的值,輸出的時候會用到 rpospostmp = tmp; } LIS 的部分 作法類似: int len = 0; vectorint dp; for int i = n; i = 1; i-- { int ans = 0; 儲存目前 dp 值:以 i 為開頭能構造出多長的 LIS if dp.empty || dp.back vi { dp.pushbackvi; 紀錄目前的 dp 值 ans = dp.size; } else { auto ite = upperboundalldp, vi, greaterint; ite = vi; 紀錄目前的 dp 值 ans = ite - dp.begin + 1; } len 維護最終 LIS 長度 len = maxlen, ans; 紀錄每個長度的解 lensans.pushbacki; } 構造解 int cur = 0; for int i = len; i = 1; i-- { 依照字典序做 sort sortalllensi, cmp; for auto j : lensi { 排除在目前選到的元素左邊的元素 if j = cur continue; 排除加入後會相交的元素 用於排除文章中寫的“一個小測資”那段的 Case if vj vcur continue; cur = j; 把 index 映射回去並輸出 cout rposvj " "; break; } }}

TIOJ

APCS 2023 10 月題解

這次 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

題解 CSES Permutations

給予一個數字 n 你要使用數字 1 sim n 構造一個陣列 使得不存在任意兩個相鄰數字的差為 1把 n 個數字分成 1 sim lfloor frac{n}{2} rfloor 和 lfloor frac{n}{2}+1 rfloor sim n 兩堆 並穿插構造 ans 陣列就可以保證任兩數的差大約為 lfloorfrac{n}{2}rfloor 最後跑過整個陣列驗證是否合法即可cppusing namespace std;signed main { iosbase::syncwithstdiofalse;cin.tie0; int n; cin n; vectorint arrn; dequeint ans; int mid = n2; for int i = 1; i = n2; i ++ { int leftVal = i; int rightVal = mid + i; ans.pushbackrightVal; ans.pushbackleftVal; } if n 1 == 1 { ans.pushfrontmid + n2 +1; } for int i = 0; i n-1; i++ { if absansi+1 - ansi == 1 { cout "NO SOLUTION" endl; return 0 ; } } for int i = 0; i n; i ++ { cout ansi " "; } cout endl;}

CSES