# dp經典題目
主要說一些dp的經典問題
=_=
## 最大子段和(Maximum Subarray)
例題
https://www.luogu.com.cn/problem/P1115
給你一個序列
當中有正有負
找出一段連續的子序列
其中這個子序列中的和為所有子序列中最大的
### 講解
#### <font color="##FF0000">**方法一:**</font>暴力解
用一個類似雙指針的方法
先用第一個當左指針
右指針一直向右移動直到碰到右界
左界右移
一直下去
每次掃完比較是不是最大值
這樣是可以的
只是很醜
而且超慢O(n^3)
#### <font color="#FF0000">**方法二:**</font>O(n)解
建立一個dp陣列跟一個arr陣列
arr[i]用來記錄第i個值
dp[i]中的值代表在<font color="##FF0000">結尾為i的時候的最大值</font>
這樣說太抽象
拿一個例子說明
<font color="#FFFFFF">_____</font>0. 1. 2. 3. 4. 5. 6.
arr=[2 -4 3 -1 2 -4 3]
**第一步:先設定結尾為index=0**
此時最大值為2
開頭結尾都是自己
<font color="#46A3FF">那紀錄dp[0]=2</font>
繼續下一步
**第二步:設定結尾為index=1**
此時可能出現以下情況:
<font color="#FF8040">**狀況一**:</font>2當頭-4當尾-->為-2
<font color="#FF8040">**狀況二**:</font>-4當頭而且-4當尾-->為-4
要選最大的
所以選狀況一的答案-2
<font color="#46A3FF">那紀錄dp[1]=-2</font>
好像感覺不出什麼東西
繼續看
**第三步:設定結尾為index=2**
此時可能出現以下情況:
<font color="#FF8040">**狀況一**:</font>2當頭3當尾-->為1
<font color="#FF8040">**狀況二**:</font>-4當頭3當尾-->為-1
<font color="#FF8040">**狀況三**:</font>3當頭而且3當尾-->為3
一樣選最大的3
<font color="#46A3FF">那紀錄dp[2]=3</font>
還是感覺不出什麼東西
**第四步:設定結尾為index=3**
<font color="#FF8040">**狀況一**:</font>2當頭-1當尾-->為0
<font color="#FF8040">**狀況二**:</font>-4當頭-1當尾-->為-2
<font color="#FF8040">**狀況三**:</font>3當頭-1當尾-->為2
<font color="#FF8040">**狀況四**:</font>-1當頭而且-1當尾-->為-1
一樣選最大的2
<font color="#46A3FF">那紀錄dp[3]=2</font>
繼續下去
最後dp會變:
dp[2 -2 3 2 4 0 3]
答案為4
你可能沒看出來
其實轉移式就是
>$$dp[i]=max(arr[i],arr[i]+dp[i-1])$$
白話就是結尾為i的最大值可能就是自己一個人
或者是自己加上**以自己前一個為結尾所得到的最大值**
用上面的例子:
```
dp[0]=max(2,2) ->2
dp[1]=max(-4,-4+dp[0]) ->-2
dp[2]=max(3,3+dp[1]) ->3
dp[3]=max(-1,-1+dp[2]) ->2
dp[4]=max(2,2+dp[3]) ->4(answer)
dp[5]=max(-4,-4+dp[4]) ->0
dp[6]=max(3,3+dp[5]) ->3
```
看不懂的話可以看這個
有字幕
https://www.youtube.com/watch?v=2MmGzdiKR9Y&ab_channel=BackToBackSWE
### 範例練習
洛谷:P1115 最大子段和
https://www.luogu.com.cn/problem/P1115
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
#define inf 2e18
#define maxn 200005
#define mod 1000000007
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
int dp[maxn]; //紀錄在結尾為i的時候的最大值
int arr[maxn]; //紀錄陣列中的值
int max_value=-2147483647; //不要設0
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>arr[i];
}
for(int i=1;i<=n;i++){
dp[i]=max(arr[i],arr[i]+dp[i-1]);//紀錄在結尾為i的時候的最大值
max_value=max(max_value,dp[i]);
}
cout<<max_value;
}
```
## 01背包問題
例題
https://zerojudge.tw/ShowProblem?problemid=b184
01背包問題-每種物品個數有只有1個
每個物品都有重量
每個只能取一次
問:在背包容量有限制時能拿走最大的價值
還有其他變化題
可以看:https://hackmd.io/1nEXxuhLTUCaTuYhYMTFhg
### 講解
先把接著要用到的變數規定好
value | 價值
--- | ---
**weight**|**重量**
只考慮第1到i項物品,背包體積為n時的最大價值,紀錄為:
$$dp[i][n]$$
假設固定體積n的背包,且有k種物品考慮
可寫成(偽程式碼):
```cpp=
for(int i=0;i<k;i++){
for(int j=0;j<n;j++){
if(n>=weight[i]){
dp[i][n]=max(dp[i-1][n],dp[i-1][n-weight[i]]+value[i]);
}
else{
dp[i][n]=dp[i-1][n];
}
}
}
```
不過有這樣空間複雜度很大
要減少空間複雜度可以用滾動dp
我們可以把轉移式寫成:
>$$dp[j]=max(dp[j],dp[j-weight[i]+value[i])$$
這樣不只空間複雜度變小很多
轉移式看起來也更乾淨易懂了
以下模板為偽程式碼
```cpp=
for(int i=0;i<物品數量;i++){
for(j=背包最大承重;j>0;j--){
if(j-weight[i]>=0){ //講白話一點:背包還能裝東西就更新
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]); //dp[j]表示在背包最大承重為j時所能有的最大價值
}
}
}
```
### 範例練習
zerojudge:b184: 5. 裝貨櫃問題
https://zerojudge.tw/ShowProblem?problemid=b184
AC(2ms, 336KB)
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
#define inf 1e9
#define maxn 10000005
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
int n;
int w=100;
while(cin>>n){
int weight[105];
int value[105];
int dp[105]={0};
for(int i=1;i<=n;i++){
cin>>weight[i]>>value[i];
}
for(int i=1;i<=n;i++){
for(int j=100;j>0;j--){
if(j-weight[i]>=0){
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
}
}
}
cout<<dp[100]<<endl;
}
}
//b184
```
UVa:10130 - SuperSale
https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=1071
zerojudge:f440: 10130 - SuperSale
https://zerojudge.tw/ShowProblem?problemid=f440
(700ms)
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
#define inf 1e12
int value[1005];
int weight[1005];
int dp[1005];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
int k;
cin>>k;
for(int i=1;i<=k;i++){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>value[i]>>weight[i];
}
int m;
cin>>m;
int cnt=0;
while(m--){
int v;
cin>>v;
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++){
for(int j=v;j>0;j--){
if(j-weight[i]>=0){
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
}
}
}
cnt+=dp[v];
}
cout<<cnt<<endl;
}
}
```
## 有權重的工作排程問題(interval scheduling problem)
例題
https://www.spoj.com/problems/RENT/
給一些工作的起始跟持續時間
但工作獲得的收益不一定相同
問最大獲得的收益為多少
### 講解
跟這個有點類似
https://hackmd.io/Ie1re-PuQiOGxqyxCRDgPg?view#%E6%8E%92%E7%A8%8B%E5%95%8F%E9%A1%8C%E6%B4%BB%E5%8B%95%E9%81%B8%E6%93%87%E5%95%8F%E9%A1%8C%EF%BC%88activity-selection-problem%EF%BC%89
不過每個工作的權重不再是一樣了
不過有個類似的想法就是**最早結束就排前面**
可是要找最優解要怎麼找
對就是dp
用目前的最優解往下推之後的最優解
可以這樣想
時間盡量不要浪費
盡可能銜接起來
所以建立一個陣列紀錄
看不懂就看這
https://www.youtube.com/watch?v=YMdnGChsvvo&t=885s&ab_channel=VivianNTUMiuLab
### 範例練習
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
#define inf 2e18
#define maxn 100005
#define mod 1000000007
struct order{
int start,finish,value,index;
}orderlist[maxn];
bool cmp(order a,order b){
return a.finish<b.finish;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
int t;
cin>>t;
int dp[maxn];
dp[0]=0;
int prev[maxn];
while(t--){
memset(dp,0,sizeof(dp));
memset(prev,0,sizeof(prev));
int n;
cin>>n;
for(int i=1;i<=n;i++){
int duration;
cin>>orderlist[i].start>>duration>>orderlist[i].value;
orderlist[i].finish=orderlist[i].start+duration;
}
sort(orderlist+1,orderlist+n+1,cmp);
for(int i=1;i<=n;i++){
orderlist[i].index=i;
}
for(int i=n;i>0;i--){
for(int j=n-1;j>0;j--){
if(orderlist[j].finish<=orderlist[i].start){
prev[i]=orderlist[j].index;
break;
}
}
}
for(int i=1;i<=n;i++){
dp[i]=max(dp[i-1],dp[prev[i]]+orderlist[i].value);
}
cout<<dp[n]<<endl;
}
}
```