# MDCPP 模擬賽 題目 / 題解 RANK : ![](https://i.imgur.com/9cmbS1T.png) ## 1 Increasing Array ### Author : 91XB41 ### Time Limit : 1000 ms ### Description :::spoiler 題敘(左側三角形點開) --- 給你一個陣列 $A$,你需要透過修改使得這個陣列呈非嚴格遞增,也就是後一個數字要大於等於前一個數字。 每次修改你可以任選一個 $i$,讓 $A_i = A_i + 1$ 。 求最少需要修改幾次才能完成。 --- ::: ### Input Description :::spoiler 輸入說明(左側三角形點開) --- 給你一個數字 $N$ 代表接下來有長為$N$的陣列 接下來一行有$N$個數字$N_i$ $N \leq 10^6$ $A_i \leq 10^{18}$ --- ::: ### Output Description :::spoiler 輸出說明(左側三角形點開) --- 輸出最少修改次數 --- ::: ### Sample :::spoiler 輸出說明(左側三角形點開) --- input 1: ``` 5 3 2 5 1 7 ``` output 1: ``` 5 ``` --- ::: --- 這題既然是第一題解法自然用最基礎的方式去想就好了 不過因為一些技術上的問題,很遺憾的只能讓你們拿到98分 好正式進入題解 這題因為要非嚴格遞增,所以基本上我們只要抓出不符合狀況(後面比前面小的) 然後把他加到跟前面一樣大(非嚴格),就能在改變最少東西的情況下修正 就能夠得出需要花費的最少費用 如果覺得有點疑惑的話,建議還是多想一下吧 因為這題真的滿簡單的,所以我們還是直接上code吧 範例code :::spoiler ```cpp= #include <bits/stdc++.h> #define int long long using namespace std; signed main () { int n , ans = 0; cin >> n; int a[n]; for(int i = 0;i < n;i++){ cin >> a[i]; } for(int i = 1;i < n;i++){ if(a[i] < a[i - 1]){ // 判斷是否有後面比前面小 ans += a[i - 1] - a[i]; // 把他加到跟前面一樣大的花費 a[i] = a[i - 1]; // 讓他跟前面一樣大 } } cout << ans; } ``` ::: ## 2 大富翁 ### Author : oolala ### Time Limit : 1000 ms ### Description :::spoiler 題敘(左側三角形點開) --- 有一位小朋友在玩大富翁,一共有 $N$ 個格子,有些格子已經被佔領,若停在該格上,則需要付過路費 $A_i$。 已知這位小朋友從第零格開始,擲了 $M$ 次骰子,每次前進擲中的結果 $B_i$,請問到終點時 (抵達 or 超過第 $N$ 格) 這位小朋友須付出多少錢 ? --- ::: ### Input Description :::spoiler 輸入說明(左側三角形點開) --- 第一行輸入兩整數 $N, M$ 第二行輸入 $N$ 個整數 $A_i$,代表每隔的過路費 ( 0 代表該格尚未被占領 ) 第三行輸入 $M$ 個整數 $B_i$,代表每次擲中的結果 $M \leq N \leq 10000$ $A_i \leq 1000$, $1 \leq Bi \leq 6$ --- ::: ### Output Description :::spoiler 輸出說明(左側三角形點開) --- 輸出所需的過路費 --- ::: ### Sample :::spoiler 輸出說明(左側三角形點開) --- input 1: ``` 6 3 3 2 1 3 2 1 3 1 4 ``` output 1: ``` 4 ``` input 2: ``` 14 6 0 0 0 359 0 0 0 473 0 867 487 0 0 0 4 1 1 4 3 5 ``` output 2: ``` 1226 ``` --- ::: --- 這題要怎麼解決呢 ? 模擬就好啦 ~ 我們就設立一個變數 s = 0 每次加上走的步數,就會是目前走到的那個步數 當我們知道目前走到的那個步數,就可以知道當格需要花費多少的費用 只要把每格需要花費的費用加在一起,就可以成功得到最終費用了 順利拿到AC~~ 範例code :::spoiler ```cpp= #include <bits/stdc++.h> #define int long long using namespace std; int arr[10050]; signed main(){ int n,m; cin>>n>>m; for(int i=1;i<=n;i++) cin>>arr[i]; int s = 0; // 起始點 int cost = 0; // 總共的花費 int plus; // 增加的步數 for(int i=1;i<=m;i++){ cin>>plus; s += plus; // 從起始點多走了plus步 cost += arr[s]; // 加上當前那格的花費 } cout<<cost<<endl; // 輸出總花費 } ``` ::: ## 3 TC 與9x9立可帶 ### Author : 91XB41 ### Time Limit : 1000 ms ### Description :::spoiler 題敘(左側三角形點開) --- TC 覺得9x9的39元立可帶實在太便宜、太好用了 因此他決定要把口袋裡所有錢通通拿來買39元立可帶 假設他每隔5分鐘就買一個立可帶 請你記錄他買立可帶的過程 --- ::: ### Input Description :::spoiler 輸入說明(左側三角形點開) --- 輸入為一正整數 $money$ ($39 \leq money \leq 1000$) 代表 TC 有多少錢 --- ::: ### Output Description :::spoiler 輸出說明(左側三角形點開) --- 每買一個立可帶就輸出兩行 格式如下方sample output 第一次買立可帶的時間為0:00 --- ::: ### Sample :::spoiler 輸出說明(左側三角形點開) --- input 1: ``` 200 ``` output 1: ``` 0:00 TC bought white-out money : 161 5:00 TC bought white-out money : 122 10:00 TC bought white-out money : 83 15:00 TC bought white-out money : 44 20:00 TC bought white-out money : 5 ``` --- ::: --- 其實不難看出本題是此次模擬賽的送分題,這題最主要就是測驗輸出文字的能力 但是在簡單的題目中其實藏有細節,有些人在輸出"money"時並沒有換行,導致WA 所以這題還是在提醒各位要注意輸出範例格式!!! 範例code :::spoiler ```cpp= #include<iostream> using namespace std; int main() { int money; int minute = 0; cin >> money; while( money >=39) { money = money-39; cout << minute << ":00 Eric bought white-out" << endl; cout << " money : " << money << endl; minute = minute+5; } return 0; } ``` ::: ## 4 TC 的燙手山芋 ### Author : reset007 ### Time Limit : 1000 ms ### Description :::spoiler 題敘(左側三角形點開) --- TC 有 $n-1$ 個好朋友 這天他突然得到一顆不知道哪來的燙手山芋 果不其然 懶惰的 TC 想把他丟給他的朋友們處理 然而他的朋友們也有相同的想法 結果 傳來傳去還是傳回了 TC 手上 假設一開始燙手山芋一定在 TC 手上 每個人都不會傳給自己 傳了 $k$ 次後 燙手山芋仍在 TC 手上的方法數為 $a_k$ 請求出 ($n \times a_k$)除以48763的餘數是多少 為了讓 TC 不那麼可憐 保證 TC 一定有朋友 --- ::: ### Input Description :::spoiler 輸入說明(左側三角形點開) --- 輸入為兩整數 $n, k$ $n$ 代表總共有多少人在傳這顆燙手山芋 ( $2 \leq n \leq 1000$ ) $k$ 代表傳了多少次 ( $1 \leq k \leq 1000$ ) 20%的測資滿足 $n,k \leq 5$ 30%的測資滿足 $n \leq 100, k \leq 5$ 49%無其他限制 (SPECIAL)1%( $n \leq 8763, k \leq 10^9$ ) --- ::: ### Output Description :::spoiler 輸出說明(左側三角形點開) --- 輸出為一個介於[0,48762]之間的整數 代表有多少種方法 --- ::: ### Sample :::spoiler 輸出說明(左側三角形點開) --- input 1: ``` 2 3 ``` output 1: ``` 0 ``` input 2: ``` 5 4 ``` output 2: ``` 260 ``` input 3: ``` 87 87 ``` output 3: ``` 270 ``` --- ::: ### 文本內容 :::spoiler 在這 以下為提供語法班看的額外補充資料 可能會對這題有幫助 也可能不會 #### 遞迴與動態規劃(Recursive & DP) 這是一個「大事化小 小事化無」的解法 具體來說 我們會將較難的問題分解成小一點的 比較好理解或解決的問題 舉個栗子 「總共有n層階梯 你目前站在第0階 你每次可以選擇要走1階或是2階的階梯 請問你走到第n階有幾種方法數?」 思考一下就能發現 「走到第n階」只能透過「從第n-1階走一階上來」和「從第n-2階跨兩階上來」這兩個步驟來完成 若我們記F(n)為走到第n階的方法數 則F(n)=F(n-1)+F(n-2) 而且透過簡單計算可以知道F(1)=1, F(2)=2 這麼一來 我們知道它的規律 就能輕鬆求出所有答案了 他甚至只是個費氏數列 對了 階乘(!)也是遞迴喔 ![](https://i.imgur.com/rjaTzYe.png) 小結: ![](https://i.imgur.com/TY0AXjx.png) --- 然而 遞迴是非常慢的 因為他會不斷的去call那些已經知道的東西 F(5)=F(4)+F(3)=( F(3)+F(2) )+F(3) (F3被叫了兩次) 所以會另外開設一個dp[]陣列 來存放我們已經知道的東西 我們可以寫說 ```cpp= dp[1]=1; dp[2]=2; for(int i=3;i<=n;i++){ dp[i]=dp[i-1]+dp[i-2]; } ``` 這樣只要跑n次就能得到答案了 這就是最基本的DP概念 #### 計數原理-乘法原理 例題 從你家出發有8種公車路線可以到台中車站 並且台中車站有8個車次的火車可以到達整修過還是很醜的彰化車站 $\Rightarrow$你有==8 $\times$ 8=64==種方式可以到彰化車站 像這種一次一次慢慢進行的行動 我們把每次小行動的方法數相乘 通常就能得到最終方法數 再來個栗子 今天有一隻青蛙和10片荷葉 青蛙一開始在其中一片荷葉上 他每次會往其他9片荷葉的其中一片跳 我們要求跳2次後回到一開始那片荷葉的跳法數 $\Rightarrow$第一跳有9種方法 但第二跳只能往原本的葉子跳 $\Rightarrow$總共有==9 $\times$ 1=9==個跳法 --- #### 快速冪 今天你想求$3^8$ 你會慢慢的算$3\times 3\times 3\times 3\times 3\times 3\times 3\times 3$ 還是把$3$平方平方再平方? 這就是快速冪的核心思想 與其一次一次自乘 不如用平方快速解決 那如果今天要求$487^{63}$呢? 我們拆成$487^{32} \times 487^{16} \times 487^8 \times 487^4 \times 487^2 \times 487$ 其中較大的數都能透過小數平方得到 總共只要進行10次就能得到$487^{63}$囉(包括平方自乘與計算答案的相乘) ::: ### 題解 本題身為介於語法班和算法班之間的~~魔王~~過渡題 我分配了一些測資 是光靠基礎排列組合就能求出來的數學部分 以下解釋各配分的解法 :::spoiler 滿分解答code 先解釋一下這題的key point >如果要在第$k$次傳給 TC >那第$k-1$次就不能給 TC >方法數 = 隨便亂傳-第$k-1$次在 TC 手上的方法 >而每次傳遞都有$n-1$種方法 >遞迴式$a_k=(n-1)^{(k-1)}-a_{k-1}$ >而且$a_1=0$(第1次不可能在TC手上) 到這裡你可以選擇開始解DP 但拿不到滿分 所以重點就是把這個遞迴式解成通式啦! code: ```cpp= #include<bits/stdc++.h> using namespace std; const int mod = 48763; long long int fpow(int a, int b){ long long int base = a, power = 1; while(b){ if(b&1)power = (power*base)%mod; base = (base*base)%mod; b/=2; } return power%mod; } int solve(int n,int k){ long long int answer; answer = fpow(n-1, k-1); if(k&1)answer--; else answer++; answer *= (n-1); return answer % 48763; } int main(){ int n,k; cin>>n>>k; cout<<solve(n,k); return 0; } ``` ::: :::spoiler 0~50分解 事實上小題1和2是幾乎一樣的 只是2的底數稍大 如果窮舉$n \leq 5, k \leq 5$的所有答案就能拿20分 如果能稍微把公式推廣到多一點人數的情形 就能再拿30 ::: :::spoiler 99分解 等到$k$變大後 單純套公式做遞迴鐵定會爆炸 所以就是我們dp的使用時機啦 dp陣列開到[1000][1000]是沒問題的喔 ::: ## 5 地鐵 ### Author : explosionnn ### Time Limit : 2000 ms ### Description :::spoiler 提敘(右側三角形點開) --- explosionnn喜歡在別人的minecraft伺服器蓋地鐵,而所有的地鐵路線呈網狀,代表某站與東西南北四站接連通。如今explosionnn從左上角的站點$(1, 1)$出發,欲前往右下角的站點$(n, m)$,一路上只往右或是下走。此外他有一顆終界珍珠,使用它能夠瞬間移動至下一站,而不花費任何時間,但是使用完珍珠即消滅。 現在給你移動到各站的時間,請求出從左上角移動到右下角最少需花費多少時間 --- ::: ### Input Description :::spoiler 輸入說明(右側三角形點開) --- 第一行有兩個整數 $N$ 和 $M$,代表這是此路網大小為$N*M$ 接下來有$N$列,每一列有$M$個整數,第$i$列$j$行的整數$A_{i,j}$代表從左方或是上方的站點到達此站點須花費$A_{i,j}$的時間 --- ::: ### Output Description :::spoiler 輸出說明(右側三角形點開) --- 輸出從左上角移動到右下角最少需花費多少時間 --- ::: ### Sample :::spoiler 輸出說明(右側三角形點開) --- input 1: ``` 3 3 1 7 3 50 8 8 1 8 8 ``` output 1: ``` 18 ``` input 2: ``` 3 4 2 6 5 4 7 6 8 7 4 3 9 8 ``` output 2: ``` 24 ``` --- ::: ### 題解 想必大家剛接觸dp有寫過這種二維dp的經典題吧: 給定一個二維陣列,從左上走到右下,只能往右或是下走,問你怎麼走路上經過的點的和會最大或是最小? 而這題是加強版,多了一個終界珍珠,讓你可以忽視其中一點的值,那麼要怎麼做呢? dp的陣列我們可以再開一個維度,最後一個維度的大小只有二,紀錄走到$A_{i, j}$的時候是不是已經用終界珍珠了,所以轉移式如下: ```cpp= dp[i][j][0] = min(dp[i-1][j][0], dp[i][j-1][0]) + station[i][j]; dp[i][j][1] = min(min(dp[i-1][j][1], dp[i][j-1][1]) + station[i][j], min(dp[i-1][j][0], dp[i][j-1][0])); ``` $dp[i][j][0]$代表走到$A_{i, j}$的時候還沒使用過終界珍珠,這也代表走過來的路上也都沒有使用過。因此只能從$dp[i-1][j][0], dp[i][j-1][0]$這兩個第三維度同為$0$的值轉移過來,並且加上到達$A_{i, j}$的時間$station[i][j]$ $dp[i][j][1]$則代表走到$A_{i, j}$的時候已經使用過終界珍珠,可以分為兩個情況: 第一種情況為早就使用過了,所以從$dp[i-1][j][1]$或是$dp[i][j-1][1]$走到$A_{i, j}$的時候沒有珍珠可以用了,只能用走的。因此要從$dp[i-1][j][1], dp[i][j-1][1]$這兩個第三維度同為$1$的值轉移過來,並且加上到達$A_{i, j}$的時間$station[i][j]$ 第二種情況為走到$A_{i, j}$的時候才用終界珍珠,因此在那之前是沒使用過的,轉移式為$min(dp[i-1][j][0], dp[i][j-1][0])$,最後不需要加上$station[i][j]$,因為是使用珍珠到達$station[i][j]$的 AC code :::spoiler code ```cpp= #include<bits/stdc++.h> #define int long long #define pb push_back #define pii pair<int, int> using namespace std; const int MAXN = 2e3+5; int INF = 1e18; int station[MAXN][MAXN], dp[MAXN][MAXN][2]; int n, m; signed main(){ cin >> n >> m; for(int i = 0; i <= n; i++){ for(int j = 0; j <= m; j++){ dp[i][j][0] = dp[i][j][1] = INF; } } for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++){ cin >> station[i][j]; } } for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++){ if(i == 1 && j == 1){ dp[1][1][0] = dp[1][1][1] = station[i][j]; continue; } dp[i][j][0] = min(dp[i-1][j][0], dp[i][j-1][0]) + station[i][j]; dp[i][j][1] = min(min(dp[i-1][j][1], dp[i][j-1][1]) + station[i][j], min(dp[i-1][j][0], dp[i][j-1][0])); } } cout << dp[n][m][1] << "\n"; } ``` ::: ## 6. TC 出遊去 ### Author : 極速車神大佬 ### Time Limit : 4000 ms ### Description 暑假了 ! 最近風和日麗,而 TC 打算帶他的眾多ㄌㄌ出去玩。 這個國家有 $N$ 個村莊,而由 $M$ 個道路連接村莊 $u$ 和 村莊 $v$,每個道路有一個長度 $W_i$。 他們打算每個村莊都要前往,但是 TC 為了照顧他的ㄌㄌ,所以他要盡量讓每次ㄌㄌ走的路不要太長。換句話說,他要選擇幾條道路,讓選擇的道路的最大值最小,並且能夠走完各個村莊。 註:TC 可以從任意一點開始,且道路是雙向的。 ### Input Description 輸入有多筆測資。 第一行有個整數 $T$,代表測資個數 接下來一行有兩個整數 $N$ 和 $M$,代表有幾個村莊及道路。 接下來有 $M$ 行,每行有三個數字, 分別是 $u_i$、$v_i$、$w_i$,代表連接 $u_i$ 到 $v_i$ 的道路長度是 $w_i$ $1 \leq T \leq 100$ $2 \leq N \leq 10^5$ $1 \leq M \leq 10^6$ $1 \leq u_i, w_i \leq N$ $1 \leq w_i \leq 10^5$ $\ \sum_{i=1}^{T} N_T \leq 10^5$ 部份給分 : 30 % 給的圖是由 $N$ 個村莊,$N-1$ 個邊連接,並且保證能夠互相到達 30 % $w_i \leq 10$ 40 % 無其他限制 ### Output Description 請輸出道路最大值最小,若無論如何都無法到達,請輸出 impossible ### Sample ``` // Sample input 1 1 4 4 1 2 1 2 3 2 1 3 1 1 4 1 // Sample output 1 1 // Sample input 2 2 4 5 1 2 1 2 3 3 1 3 2 2 4 4 3 4 3 6 5 1 2 2 2 3 5 4 6 2 5 6 3 1 3 4 // Sample output 2 3 impossible // Sample input 3 1 2 1 1 2 10 // Sample output 3 10 ``` 這題究竟要怎麼完成呢 ? 對於 第一個 subtask: 我們可以發現為了要走到全部的點,每條邊一定要走到,所以將全部路徑長取 $max$ 即可 對於 第二個 subtask: 我們可以枚舉上限,從 1 ~ 10,依據上限來判斷可不可以走。 對於全部: 透過題目敘述可以發現,題目要你求道路長度的上限最小值。 而這又是一個外掛二分搜的模型:最大值最小。 我們知道如果上限越大的話,就越容易到達,這就構成了二分搜單調性 那要怎麼完成確認函數呢 ? 很簡單,我們就 DFS / BFS 一次就好, 而如果超過上限的邊就不去走他。 複雜度:$O((N+M)*logC)$ $O(N+M)$ 來自 DFS / BFS $O(logC)$ 來自外掛二分搜 ```cpp= #include <bits/stdc++.h> #define pb push_back #define pii pair<int,int> #define mset(a,b) memset(a,b,sizeof(a)) using namespace std; const int maxn = 1e5 + 5 ; vector<pii>g[maxn]; int vis[maxn],cnt,n,m; void dfs(int x,int p,int c) { if(vis[x]++) return ; cnt++; for(auto [v,e]:g[x]) { if(v==p or e>c) continue; dfs(v,x,c); } } bool check(int c) { cnt=0; mset(vis,0); dfs(1,-1,c); if(cnt==n) return true; else return false; } signed main() { ios::sync_with_stdio(0); cin.tie(0); int t; cin>>t; while(t--) { cin>>n>>m; for(int i=1;i<=n;i++) g[i].clear(); while(m--) { int u,v,w; cin>>u>>v>>w; g[u].pb({v,w}); g[v].pb({u,w}); } int l=1,r=1e4,best=-1; while(l<=r) { int mid=(l+r)>>1; if(check(mid)) best=mid,r=mid-1; else l=mid+1; } if(~best) cout<<best<<endl; else cout<<"impossible\n"; } } ``` 而其實這題有另一個更好的方法,就是利用最小生成樹 (MST) ! 這個算是比較進階的內容,有興趣的同學可以看[這裡](https://oi-wiki.org/graph/mst/#kruskal) 複雜度:$O(M*logM)$ 這裡附上程式碼: ```cpp= #include <bits/stdc++.h> #define pb push_back #define tup tuple<int,int,int> using namespace std; const int maxn = 1e5 + 5 ; int pa[maxn],mx,cnt; int findset(int x) { return x==pa[x]?x:pa[x]=findset(pa[x]); } vector<tup>edge; void uni(int u,int v,int w) { u=findset(u),v=findset(v); if(u==v) return ; pa[u]=v; mx=w; cnt++; } bool cmp(tup t1,tup t2) { return get<2>(t1)<get<2>(t2); // tuple 用法 } signed main() { ios::sync_with_stdio(0); cin.tie(0); int t,n,m; cin>>t; while(t--) { cin>>n>>m; for(int i=1;i<=n;i++) pa[i]=i; edge.clear(); while(m--) { int u,v,w; cin>>u>>v>>w; edge.pb({u,v,w}); } sort(edge.begin(),edge.end(),cmp); for(auto [u,v,w]:edge) { uni(u,v,w); } if(cnt!=n-1) cout<<"impossible\n"; else cout<<mx<<endl; } } ``` ## 7 Tim Chen與黑市的白毛ㄌㄌ交易 ### Author : YouZhe ### Time Limit : 600 ms ### Description :::spoiler 提敘(右側三角形點開) --- 眾所皆知Tim Chen是個狂熱的ㄌㄌ愛好者,最近他從黑市中找到了一個攤販,願意以吸引人的價格賣給他最上等的白毛ㄌㄌ 這個攤販花了不少心血才弄到一隻難能一見的白毛ㄌㄌ,因此他對於他的訂價絲毫不肯退讓 並且為了讓他在黑市的生意不要曝光,攤販不願意找任何錢,畢竟錢上是有編號的,找錢相當於讓自己的資訊流出 正好Tim Chen又是一個寧願不吃午餐來省錢的台南血脈客家人(或者他只是想要存錢去吃女僕咖啡廳)。因此他不可能做出給更多錢的舉動 也就是說他們之間的交易**只可能在剛好等於定價時發生,且只能付剛好的錢(不找錢)** 今天正是Tim Chen要買下白毛ㄌㄌ的日子,但黑市攤販跟他說當他來到黑市後才願意釋出價格$C$ 因此他無法準備確切數目的紙鈔,最後他在錢包裡**準備了$N$種面額的紙鈔各一張** 但是在路上時,或許是因為滿腦都是白毛ㄌㄌ,所以**有$M$張鈔票從他的錢包掉了出去** 現在**給出$Q$次詢問,每次詢問給出$M$個數字** 代表Tim Chen**丟掉的鈔票面額**。 試問每次詢問中,Tim Chen有**幾種方法**可以湊到購買白毛ㄌㄌ的價格 若沒有任何湊到白毛ㄌㄌ價格的方法,請用力嘲笑他:"Ha! What a poor guy!" (答案請對10^9+7取模) --- ::: ### Input Description :::spoiler 輸入說明(右側三角形點開) --- 第一行有兩個整數$N$、$C$,代表攜帶的紙鈔數量、要賣出的價格 第二行,有$N$個數字$a_i$代表紙鈔的面額。 接下來有一個整數$Q$詢問的次數 接下來有$Q$組數字: 第一行有一個整數$M$代表遺失的紙鈔數量 第二行,有$M$個數字$b_i$代表遺失紙鈔的面額。 $1 \leq N \leq 2000$ $1 \leq C \leq 2000$ $1 \leq Q \leq 2000$ $1 \leq M \leq 10$ $1 \leq a_i, b_i \leq 2000$ b集合在a集合之中 部份給分 50%$1 \leq N, C, Q, a_i ,b_i \leq 100$ 50% 原測資 --- ::: ### Output Description :::spoiler 輸出說明(右側三角形點開) --- 請輸出湊到白毛ㄌㄌ價格的方法數,若為0,請輸出Ha! What a poor guy! --- ::: ### Sample :::spoiler 輸出說明(右側三角形點開) --- input 1: ``` 5 8 1 4 3 6 8 2 1 6 3 6 8 1 ``` output 1: ``` 2 Ha! What a poor guy! ``` --- ::: ### 題解 這題呢我預估解出來的人也是在3個以內,不過因為其他題目太邪惡了,所以她就被排到了第6題 至於要怎麼解這題呢? 如果還記得背包DP的最後一堂課,那就會發現這題長的就一臉找錢問題對吧。 只不過要把它從無限背包的架構,改成01背包的架構 的確,這題就是那題找錢問題的延伸。不過我猜大多數人根本沒聽懂就是了。 首先這一看,先套進找錢問題的模板就是我們的第一步了 接下來,因為我們可以知道的是,它會有Q次查詢改變原本的物品 所以這裡可以直接看出第一種作法 O(QNC)的做法,我們只要改變Q次物品,並且每次都對它做一次背包DP 每次背包DP的複雜度是O(NC),乘在一起就變成O(QNC)了,這也是前50%的拿法 阿因為不是AC code,所以我就不放範例code了 現在知道了O(QNC),很顯然這在完全測資裡是會爆掉的 所以我們希望能在每次修改之後,不需要都多做一次背包DP 那麼就先觀察掉一種面額的鈔票會導致甚麼結果 很顯然,在背包DP裡,就是透過這種面額加到的方法數會失效嘛 ```cpp= for(int i=1;i<=n;i++) for(int j = c;j>=w[i];j--){ dp[j] = dp[j]+dp[j-w[i]]; } ``` 那麼我們就反過來做,把他所得到的結果反過來扣掉 ```cpp= for(int j=lost;j<=c;j++) //lost代表掉的錢面額 ndp[j] = ndp[j]-ndp[j-lost]; } ``` 至於為甚麼要從小到大順推過去,而不是按照01背包那樣,由大到小逆堆呢 因為我們如果不先把小容量的背包扣掉的話,大容量的背包就會扣掉使用lost這個面額達成的方法數,變成扣了兩次 舉個例 有 1 2 3 4 面額 要湊到價格6 -|1|2|3|4|5|6 -|-|-|-|-|-|- for i=1|1|0|0|0|0|0 for i=2|1|1|1|0|0|0 for i=3|1|1|2|1|1|1 for i=4|1|1|2|2|2|2 現在扣掉3這個面額 如果逆著做 -|1|2|3|4|5|6 -|-|-|-|-|-|- 原本|1|1|2|2|2|2 後來|1|1|1|1|1|0 因為6先扣掉了3的2種方法,所以掛了,本來還有2+4這種,算出來卻變0種 3的兩種方法包刮[1+2],[3],如果6先扣它,就相當於扣了一個0+3+3的情況 但這顯然是不成立的,所以我們要正著做它 變成這樣 -|1|2|3|4|5|6 -|-|-|-|-|-|- 原本|1|1|2|2|2|2 後來|1|1|1|1|1|1 答案就會正確是1 以下附上範例code :::spoiler code ```cpp= #include <bits/stdc++.h> #define int long long #define mod 1000000007 using namespace std; const int MAXN = 3000+50; int dp[MAXN]; int w[MAXN]; signed main(){ int n,c; cin>>n>>c; for(int i=1;i<=n;i++){ cin>>w[i]; } dp[0] = 1; for(int i=1;i<=n;i++) for(int j = c;j>=w[i];j--){ dp[j] = (dp[j]+dp[j-w[i]])%mod; } // 01背包架構的找錢 // O(NC) int q; cin>>q; while(q--){ int ndp[MAXN]; //new dp memcpy(ndp,dp,sizeof(dp));//O(NQ) //這個memcpy()代表把dp陣列複製給ndp陣列 //你們也可以用for迴圈來做 int m; cin>>m; int lost; for(int i=1;i<=m;i++){ cin>>lost; for(int j=lost;j<=c;j++) // 順推 這樣才不會扣到已經用了lost來湊出的方法數,變成扣了兩次 ndp[j] = (ndp[j]-ndp[j-lost]+mod)%mod; //前面即取過模,只取一次 } //O(QMC) if(ndp[c]==0) cout<<"Ha! What a poor guy!"<<endl; else cout<<ndp[c]<<endl; } } ``` :::