最後更新日期 2024 / 01 / 01

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

題意

上下有個有一根木樑
然後中間有 n 條可能會相交的木板
找出最多可以釘上幾條木板並構造最大字典序的解
(細節自己去 TIOJ 看 :<)

前言的部分

因為這題看起來沒人寫題解
於是我就發了這篇文章 ouob

轉化問題

仔細觀察問題
我們可以把問題轉換成 LIS 或 LCS

LCS 的話應該不難理解
因為要釘最多木板其實就是要在上下兩條陣列各選一個共同的子序列
然後把每個數字一一對應釘起來
但是 LCS 需要 O(n2) 的時間複雜度才能計算
所以一定會 TLE

接著是 LIS 的作法
仔細觀察題目的話可以發現
如果木板有"座標"的話
其實我們在做的事情就是"對木板做 LIS"
更精確一點的說
就是我們其實可以把木板在上樑的點跟下樑的點綁成一個 pair 做 LIS
我們說木板 A 的座標小於木板 B 代表 A 的下樑座標跟上樑座標都小於 B

至於座標的部分大概就是這個意思:
以附圖為例
木板 3 的座標就是 (2, 1)
木板 4 的座標就是 (5, 3)
木板 5 的座標就是 (4, 4)

如何構造解

如果用最普通的 DP 方式
就算能構造解也很難求最大字典序
於是我們修改一下計算方式
把轉移式改成 “定義 dp[i] 為把第 i 個元當頭素能構造出的 LIS”
其實就只是倒著做 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;
        }
    }
}

推薦文章

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

兩個人到一個 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

Dcard 實習計畫 2024

這次去 SITCON 玩大地遊戲的時候,剛好在 Dcard 攤位看到正在招募 2024 暑期實習計畫的實習生,於是我就過去了跟他們聊了一下,沒想到他們竟然說高中生只要有時間也可以來報名看看,身為自學生的我怎麼能錯過這樣的機會呢!按照實習計畫的說明,在投遞履歷的同時,還需要附上「作業」,而這次的作業內容大概就是要建立一個可登入、發文然後串 Github API 的 blog 網頁。alt text我在開始製作這個作業時,其實對於 React 的了解其實幾乎趨近於零,沒有抱持的太大的期待就開始做下去了,想說就透過這個作業邊寫邊學 React 和 Next.js。過程中其實遇到不少困難,起初就是 useEffect 用得太差,導致出現一堆問題,網路上查了許多資料都沒辦法完美的解決我遇到的困難,直到發現了 A Complete Guide to useEffect 這篇文章,對英文不是很好的我來說,這超長的文章大概也花了我兩天的時間才讀完。幸運的是,這篇文章寫得非常詳細而且解決了我遇到的問題,感覺我就是基礎很不扎實吧。alt text除了 useEffect 以外,還遇到另外一個更棘手的就是 Next.js 的快取問題,每當我發布 blog 文章之後,無論怎麼重新載入我的文章都不會顯示出來。翻了 Next.js 的文件才發現快取系統怎麼這麼複雜,總之後來又花了一些時間把 Next.js 的文件都好好讀一一讀,最終稍微解決了這個問題,但解法似乎不是非常優雅。繳交期限最後一週,我參考了之前 某個被錄取的實習生的作業 寫好了 README.md 文件就交上去了。如果有興趣的話,你可以在 這裏 看到我當時的作業,總共 320 個 commits,我花了整整一個月的時間在上面。作業繳交上去幾天後,很順利的收到了面試通知,那時候看到這個超開心!alt text距離面試時間,還有兩週多,首先我上網查了一些資訊,看看之前的 Dcard 面試到底都做了什麼。因為某篇文章說建議去讀 React 底層的東西,於是我就找了 React 技術揭密 這本電子書來讀,裡面真的提到了非常多很底層的東西,像是什麼 fiber node 之類的,不過最後還是看不太懂qwq,因此放棄了這本書。或許對我來說 React 底層太難了,但我至少可以繼續增進我對 React 的了解,根據前面 Next.js 的學習經驗,我決定要把 React 的文件全部讀過一遍!還在 Obsidian 整理了蠻多的筆記,感覺有機會可以公開出來。alt text大約一週的時間,我把 React 讀得差不多了,過程中其實發現了不少作業中可以優化的地方也改上去了。然後又再回去看一下之前面試的考古題,發現我對 Javascript 的了解似乎也不夠深,尤其時原型練之類的東西。最後根據 某 Javascript 大師 的推薦,讀了 這個系列 的文章,真的挺優質的,很多其他地方看不到的內容裡面都有。這部分我比較多的時間可能都還是在研究原型鏈。alt text接下來也花了一些時間學 git 更進階的指令,這個網站 真的挺有意思的,到後面感覺很像在玩解謎遊戲。alt textgit 的部分也整理了一些筆記,不過沒 React 那麼多就是了。alt text面試前兩三天,繼續查了一下考古題,看看 HR 面試的關卡會做什麼,但好像也沒查到什麼太有用的資訊。結果反而有點不知道要做什麼,於是就去處理了一些雜事,把之前 side project 想加的東西加上去、把還沒發佈的 chrome extension 給 publish 上去有的沒的。大概提前兩個多小時左右就先到了現場,看一下面試地點大概在什麼地方,然後去吃個午餐休息一下,本來想再複習一下結果好像來不及了。進公司之後,他們先跟我介紹了一下 Dcard 的歷史,然後參觀一下工作環境(看起來真的很舒服)。接著迎來的第一關就是 HR 面試。其實氛圍還算滿輕鬆的,不過我還是有點緊張。剛開始有跟他們聊了一下關於我現在自學生的身份和接下來的升學打算,後來還有提到問我是如何學習一項新的技術、以及程式學習的歷程,然後問我短中長期規劃之類的。因為我個人網站上面寫了很多比賽的經歷,他們也根據這個詢問了我一些相關問題,比如為什麼想參加這些比賽、跟隊友賽中的策略怎麼樣等等,在小組合作過程當中的協作模式、有沒有遇過衝突及如何化解有的沒的。這邊其實我覺得很多問題我都沒有回答得很好,或者是思考停頓了太久。alt text下一個階段是 Frontend Team 的面試,他們問了很多關於 React 的問題,包含 useMemo、useCallback、SSR、CSR、await async 跟 promise 差在哪、SSG、server component 跟 client component 的差別等等。比較令我印象深刻的是有一題他們問 server component 跟 server side rendering SSR 差在哪裡,第一瞬間幾乎沒什麼靈感,想說不就是 server component 才會 server rendering 這樣嗎,思考一小段時間之後突然有一點想法回想起來之前在看 Next build 結果的時候就連 client component 也被標示成 SSR,於是我就有點免強的回答說:「client component 也會用到 SSR... 對吧?」,似乎應該是正確答案,不過感覺回答的很不完整。另外一個也讓我思考了蠻久的問題就是「Suspense 在 server component 的運作原理是什麼?它是如何做到載入好後把 fallback 替換成取得到的資料的?」,這題我就真的幾乎完全沒頭緒了,雖然大概給了一點零散地回答剛好提到了 streaming 這個詞,他們就說算是有擦到一點邊,對於這個沒回答出來的問題他們也好心的跟我解釋了 Suspense 到底是怎麼是怎麼運作的。alt text剩下一些沒有回答的很好的問題還有,「有沒有自調整過 webpack 的 config」,我回答沒有,大多數情況下都是用 framework 的 default config。「web vitals 的評分項目」,雖然用過,但我幾乎忘光了。作業的部分他們也提到了我的 blog 頁面的 error handling 沒有做得很好,某個 promise 的 .catch 後面沒判斷就直接接了 notFound。然後 blog 頁面的 cache miss 掉以及我用 Map 實作了奇怪的小 cache 也被抓到,快取的部分真的被問得挺慘的ww還有因為之前的專案都是 Vue 寫的,所以他們也稍微問了一些關於 Vue 的問題,比如說你覺得 Vue 跟 React 差在哪、更喜歡哪個之類的,但其實我現在對 Vue 的了解度應該比 React 還低,這部分也挺慘的。面試最後我問他們我還有什麼地方是可以加強的,他們第一句話就是說我非常優秀,三年能學到這樣已經比很多 Junior 強了(這真的讓我很感動),鼓勵的部分結束後,他們給我的建議是應該再多了解 React、Next 的底層在做什麼,可以常常觀察 Devtools 裡面的 Network Tab 到底傳了什麼東西過來。面試結束後的下一週,他們就寄了審核結果的資訊到我的信箱。alt text打開來看到我沒有錄取,其實當初還蠻失望的,不過仔細這樣回過頭來看看這段時間發生的事,沒有錄取好像也挺合情合理的。至少在這個過程當中我真的學習到了非常多東西,了解到自己許多不足的地方,認識了所謂的「面試」到底是什麼,甚至還讓我從 0 學會了 React 跟 Next XD,再次感謝 Dcard 願意給我這次嘗試的機會 如果沒有 Dcard,我很難想像我可以在兩個月之內成長這麼多!統整一下這段時間發生的事情,首先我認為最明顯的缺點應該就是我對 React 和 Next 技術及經驗還不夠充足,尤其是關於偏底層的部分。再來就是技術面試以外的部分我幾乎不知道怎麼準備,感覺 HR 那關我幾乎都是在亂答,像是他們問說有沒有在合作過程當中遇到衝突然後怎麼處理,我回答幾乎沒有,可能藉由這個答案被判斷成我沒什麼合作經驗(雖然這確實),還有短中長期計畫那題也是想了蠻久的。無論如何,接下來這一年我希望可以:- 寫更多前端技術文章- 更深入了解 React Next 底層技術- 學習 webpack- 學習 redux- 累積更多合作開發的經驗希望明年就可以錄取 Dcard!!!

Journal

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