Dynamic Programming[0]

建中資訊讀書會

  • 225班
  • 電子計算機研習社_學術長
  • 綽號807
  • 資讀講師(第0堂)

這我

這是甚麼?

  • 動態規劃
  • DP
  • 從小問題的答案推得大答案

Dynamic Programming

某貓要走n階樓梯,一次能向上跨一階或二階,有幾種走法?

1

0

5

2

3

4

1

1

2

3

5

8

走法數

階梯

dp[x]=dp[x-1]+dp[x-2]

轉移式

定狀態

定狀態

  • 初始值
  • 小問題的解

定狀態

  • 推得當前答案
  • 可利用已知

轉移式

#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
signed main(){
	int n;
	cin>>n;
	vector<int> dp(100,0);
	dp[0]=dp[1]=1;
	for(int s=2;s<100;s++) dp[s]=dp[s-1]+dp[s-2];
	for(int s,k=0;k<n;k++){
		cin>>s;
		cout<<dp[s]<<"\n";
	}
}
# PRESENTING CODE

你已經會了

通靈轉移式

寫出使狀態

講完了下課

常見dp問題

網格路徑問題

有n條縱向及m條橫向的長方形路網中,求捷徑走法數。

網格路徑問題

dp[n][m]紀錄到i、j格上的走法數

初始 dp[0][0]=1;

轉移式 dp[i][j]=dp[i-1][j]+dp[i][j-1];

事實上可以C n+m取n

O(n \times m)
O(n+m)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=1e9+7;
int pow(int x,int y){
    int ans=1;
    for(int base=x,s=1;s<=y;s<<=1,base=base*base%mod) if(s&y) ans=ans*base%mod;
    return ans; 
}
int divide(int x,int y){
    return x*pow(y,mod-2)%mod;
}
signed main(){
    int n,m;
    cin>>n>>m;
    if(n==1||m==1){
        cout<<1;
        return 0;
    }
    int d[n+m];
    d[0]=1;
    for(int s=1;s<n+m;s++) d[s]=d[s-1]*(s+1)%mod;
    cout<<divide(divide(d[n+m-3],d[n-2]),d[m-2]);
}
# PRESENTING CODE

網格路徑問題

網格路徑問題

背包問題

有一個背包,承重上限為x,現在有n個東西可以選擇是否放進背包,給你每個東西的重量w[i]和價值c[i],輸出你能裝下的最大價值。

背包問題

每種東西只有一個

ISCOJ4509

CSES1158

0/1背包問題

dp[i]紀錄花費時間為i時拯救的最大價值

初始 dp[m+1]皆設為0

每次考慮第s個包包

轉移式 dp[i]=max(dp[i],dp[i-t[s]]+v[s])

i由大到小才不會重複計算

O(m \times n)
#include<bits/stdc++.h>
#define int long long
using namespace std;
signed main(){
    int n,m,sum;
    cin>>n>>m;
    vector<int> v(n),t(n);
    vector<int> dp(m+1,0);
    for(int &i:v) cin>>i,sum+=i;
    for(int &i:t) cin>>i;
    for(int s=0;s<n;s++){
        for(int d=m;d>=t[s];d--){
            dp[d]=max(dp[d],dp[d-t[s]]+v[s]);
        }
    }
    cout<<sum-dp[m];
    return 0;
}
# PRESENTING CODE

每種東西都有無限個

CSES1635

無限背包問題

要改哪裡?

無限背包問題

i由小到大才會重複計算

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
vector<int> copied;
void ms(int l,int r,vector<int> &data){
    if(r-l==1) return;
    int mid=l+r>>1,e=mid-l;
    ms(l,mid,data),ms(mid,r,data);
    if(copied.size()<e) copied.resize(e);
    for(int s=0;s<e;s++) copied[s]=data[s+l];
    for(int s=l,sl=0,sr=mid;sl<e;s++){
        if(sr==r||copied[sl]<=data[sr]) data[s]=copied[sl++];
        else data[s]=data[sr++];
    }
    return;
}
int main(){
    ios_base::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n,x;
    cin>>n>>x;
    vector<int> ways(x+1);
    vector<int> coins(n);
    for(int s=0;s<n;s++) cin>>coins[s];
    ms(0,n,coins);
    ways[0]=1;
    for(int s=1;s<=x;s++){
        for(int d=0;d<n&&s-coins[d]>=0;d++){
            ways[s]+=ways[s-coins[d]];
            ways[s]%=mod;
        }
    }
    cout<<ways[x]<<endl;
}
# PRESENTING CODE
不要問我為甚麼要手刻合併排序

每種東西有c[i]個

有限背包問題

一個一個算  這好像有點蠢

怎麼拆?

1、2、4、8、16、32、...&剩下的

log(c[i])個
log(m \times n \times log(c[i]))

每種書都有無限個

TIOJ1387

ISCOJ4470

有限背包問題

#include<bits/stdc++.h>
using namespace std;
int main(){
        int n,t;
        cin>>n;
        vector<int> w(n),c(n),m(n);
        for(int i=0;i<n;i++) cin>>w[i]>>m[i]>>c[i];
        cin>>t;
        vector<int> dp(t+1,0);
        for(int i=0,mm,mw;i<n;i++){
                for(int a=1;a<c[i];c[i]-=a,a<<=1){
                        mm=m[i]*a,mw=w[i]*a;
                        for(int j=t;j>=mw;j--) dp[j]=max(dp[j],dp[j-mw]+mm);
                }
                mm=m[i]*c[i],mw=w[i]*c[i];
                for(int j=t;j>=mw;j--) dp[j]=max(dp[j],dp[j-mw]+mm);
        }
        cout<<dp[t]<<"\n";
}
# PRESENTING CODE

LIS

Longest Increasing Subsequence

給你n個數,求最長遞增子序列

最常遞增子序列

lis[]就是LIS

每次比較x[i]

比所有LIS中的元素小: lis[0]=x[i]

比所有LIS中的元素大: lis[lislen++]=x[i]

否則: 將LIS中最小大於x[i]者=x[i]

O(n log(n))
#include<bits/stdc++.h>
using namespace std;
 
int main(){
    ios_base::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n;
    cin>>n;
    vector<int> x(n,0);
    vector<int> lis(n,1e9+7);
    int Max=1;
    for(int &i:x) cin>>i;
    lis[0]=x[0];
    for(int &i:x){
        if(i==lis[Max-1]||i==lis[0]) continue;
        else if(i>lis[Max-1]) lis[Max++]=i;
        else if(i<=lis[0]) lis[0]=i;
        else for(int l=0,r=Max,mid=l+r>>1;r;mid=l+r>>1){
            if(r-l==1) lis[r]=i,r=0;
            else if(lis[mid]<i) l=mid;
            else if(lis[mid]>i) r=mid;
            else break;
        }
    }
    cout<<Max<<endl;
}
# PRESENTING CODE

Edit Distance

給你字串A、B,有三種操作

  1. 增加一個字元
  2. 刪除一個字元
  3. 更改一個字元

求最低操作次數使兩字串相同

Edit Distance

dp[i][j]代表到A[i]、B[j]兩字串更改至完全相同所需的最少操作次數

對於每個i、j dp[i][0]=i; dp[0][j]=j;

轉移式

A[i]和B[j]相同:

dp[i][j]=min(dp[i-1][j]+1, dp[i][j-1]+1,dp[i-1][j-1])

不同:

dp[i][j]=min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+1)

O(n^2)
#include<bits/stdc++.h>
using namespace std;
int main(){
    string a,b;
    cin>>a>>b;
    int la=a.size(),lb=b.size();
    //cout<<la<<" "<<lb<<endl;
    a=" "+a;
    b=" "+b;
    vector<vector<int>> dp(la+1,vector<int> (lb+1,1e9+7));
    dp[0][0]=0;
    for (int i = 0; i <= la; ++i) dp[i][0] = i; // 加上這裡
    for (int i = 0; i <= lb; ++i) dp[0][i] = i; // 加上這裡
    
    for(int x=1;x<=la;x++) for(int y=1;y<=lb;y++){
        dp[x][y]=min(dp[x][y],dp[x-1][y]+1);
        dp[x][y]=min(dp[x][y],dp[x][y-1]+1);
        if(a[x]==b[y]) dp[x][y]=min(dp[x][y],dp[x-1][y-1]);
        else dp[x][y]=min(dp[x][y],dp[x-1][y-1]+1);
    }
    cout<<dp[la][lb];
}
# PRESENTING CODE
Made with Slides.com