[TOC]
# [Dice Combinations](https://cses.fi/problemset/task/1633)
:::spoiler 解法:
令$dp[i]$為$n=i$時的答案(特別定義$dp[0]$=1),考慮每一種組合方法的最後一個數字,例如$dp[x]$中最後一個數字為$4$的組合方法數恰為$dp[x-4]$種,則:
$$
dp[i]=\sum_{j=\max (0,i-6)}^{i-1} dp[j]
$$
複雜度$\text{O} (n)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,mod=1e9+7;
cin>>n;
vector<int> dp(n+1);
dp[0]=1;
for(int i=1;i<=n;i++){
for(int j=max(0,i-6);j<=i-1;j++){
dp[i]+=dp[j];
dp[i]%=mod;
}
}
cout<<dp[n]<<endl;
}
```
:::
# [Minimizing Coins](https://cses.fi/problemset/task/1634)
:::spoiler 解法:
定義$dp[i]$為$x=i$時的答案,集合$S$為硬幣的面額,窮舉最後一枚硬幣的面額,則:
$$
dp[i]=\mathop{\min}_{j\in S,i-j\geq 0} (dp[i-j]+1)
$$
複雜度$\text{O} (nx)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,x;
cin>>n>>x;
vector<int> S(n),dp(x+1,1e7);
dp[0]=0;
for(int i=0;i<n;i++){
cin>>S[i];
}
for(int i=1;i<=x;i++){
for(int j:S){
if(i-j<0) continue;
dp[i]=min(dp[i],dp[i-j]+1);
}
}
if(dp[x]==1e7) cout<<-1<<endl;
else cout<<dp[x]<<endl;
return 0;
}
```
:::
# [Coin Combinations I](https://cses.fi/problemset/task/1635)
:::spoiler 解法:
令$dp[i]$為$n=i$時的答案(特別定義$dp[0]$=1),且集合$S$為硬幣的面額,考慮每一種組合方法的最後一個數字,例如$dp[x]$中最後一個數字為$4$的組合方法數恰為$dp[x-4]$種,則:
$$
dp[i]=\sum_{j\in S,i-j\geq 0} dp[i-j]
$$
複雜度$\text{O} (nx)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,x,mod=1e9+7;
cin>>n>>x;
vector<int> S(n),dp(x+1);
dp[0]=1;
for(int i=0;i<n;i++){
cin>>S[i];
}
for(int i=1;i<=x;i++){
for(int j:S){
if(i-j<0) continue;
dp[i]+=dp[i-j];
dp[i]%=mod;
}
}
cout<<dp[x]<<endl;
return 0;
}
```
:::
# [Coin Combinations II](https://cses.fi/problemset/task/1636)
:::spoiler 解法:
令$dp[i][j]$為只使用前$j$種數字,組合出$i$的方法數。先枚舉$j$,注意到新加入的數字可能用很多次,不過只要由小到大枚舉$i$,就可以包含到所有情形。以範測為例,$dp[(6+3)]$能從$dp[6]$轉移,而因為先枚舉$6$才枚舉$10$,所以已經有包含到(3+3)的情況了。如果不太能理解可以試著開二維的dp陣列或者直接看code。
複雜度$\text{O}(nx)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,x,mod=1e9+7;
cin>>n>>x;
vector<int> S(n),dp(x+1);
for(int i=0;i<n;i++){
cin>>S[i];
}
dp[0]=1;
for(int j:S){
for(int i=j;i<=x;i++){
dp[i]+=dp[i-j];
dp[i]%=mod;
}
}
cout<<dp[x]<<endl;
}
```
:::
# [Removing Digits](https://cses.fi/problemset/task/1637)
:::spoiler 解法:
令$dp[i]$為$n=i$時的答案,集合$S_i$為$i$各位數的數字。則:
$$
dp[i]=\mathop{min}_{j\in S_i} (dp[i-j]+1)
$$
複雜度$\text{O}(n\log_{10} n)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int n;
cin>>n;
vector<int> dp(n+1,1e6+1);
queue<int> q;
for(int i=1;i<n+1;i++){
int temp=i;
while(temp){
if(temp%10!=0) q.push(temp%10);
temp/=10;
}while(q.size()!=0){
dp[i]=min(dp[i],dp[i-q.front()]+1);
q.pop();
}
}
cout<<dp[n]<<endl;
return 0;
}
```
:::
# [Grid Paths](https://cses.fi/problemset/task/1638/)
:::spoiler 解法:
$$
dp[i][j]=dp[i-1][j]+dp[i][j-1]
$$
複雜度$\text{O}(n^2)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,mod=1e9+7;
cin>>n;
vector<vector<int>> v(n,vector<int>(n)),dp(n,vector<int>(n));
for(int i=0;i<n;i++){
string s;
cin>>s;
for(int j=0;j<n;j++){
if(s[j]=='.') v[i][j]=1;
else v[i][j]=0;
}
}
dp[0][0]=v[0][0];
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(v[i][j]){
if(i>0) dp[i][j]+=dp[i-1][j];
if(j>0) dp[i][j]+=dp[i][j-1];
dp[i][j]%=mod;
}
}
}
cout<<dp[n-1][n-1]<<endl;
return 0;
}
```
:::
# [Book Shop](https://cses.fi/problemset/task/1158/)
:::spoiler 解法:
經典背包問題。
令$dp[i][j]$為只拿前$i$項物品,預算為$j$時可獲得的最大頁數,則有以下兩種情況:
對於第$i$個物品
不拿:
$$
dp[i][j]=dp[i-1][j]
$$
拿:
$$
dp[i][j]=dp[i-1][j-w[i]]+v[i]
$$
其中$w$為重量(價格),$v$為價值(頁數)
對兩種情況取$\max$
複雜度$\text{O}(nx)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,x;
cin>>n>>x;
vector<int> price(n),pages(n);
for(int i=0;i<n;i++) cin>>price[i];
for(int i=0;i<n;i++) cin>>pages[i];
vector<vector<int>> dp(n+1,vector<int>(x+1));
for(int i=1;i<n+1;i++){
for(int j=1;j<x+1;j++){
if(j>=price[i-1]){
dp[i][j]=max(dp[i-1][j],dp[i-1][j-price[i-1]]+pages[i-1]);
}
else dp[i][j]=dp[i-1][j];
}
}
cout<<dp[n][x]<<endl;
return 0;
}
```
:::
# [Array Description](https://cses.fi/problemset/task/1746)
:::spoiler 解法:
其實和[Grid Paths](https://hackmd.io/apWfaWY0T1i47d0xhGHYNw?both#Grid-Paths)非常像,只不過轉移點變為三個,另外需要注意不要超出邊界。
複雜度$\text{O}(nm)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
ll n,m,mod=1e9+7;
cin>>n>>m;
vector<vector<int>> dp(n,vector<ll>(m+1));
vector<int> v(n);
for(ll i=0;i<n;i++){
cin>>v[i];
}
if(v[0]>0) dp[0][v[0]]=1;
else{
for(ll i=1;i<=m;i++){
dp[0][i]=1;
}
}
for(int i=1;i<n;i++){
if(v[i]>0){
dp[i][v[i]]=dp[i-1][v[i]-1]+dp[i-1][v[i]];
if (v[i]<m) dp[i][v[i]]+=dp[i-1][v[i]+1];
}else{
for(int j=1;j<=m;j++){
if(j<m){
dp[i][j+1]+=dp[i-1][j];
}
if(j>1){
dp[i][j-1]+=dp[i-1][j];
}
dp[i][j]+=dp[i-1][j];
dp[i][j]%=mod;
}
}
}
int ans=0;
for(ll i=0;i<=m;i++){
ans+=dp[n-1][i];
ans%=mod;
}
cout<<ans<<endl;
return 0;
}
```
:::
# [Counting Towers](https://cses.fi/problemset/task/2413)
:::spoiler 解法:
一樣先從狀態和轉移去想,狀態顯而易見的,就是令$dp[i]$為$n=i$時的答案,那麼轉移呢?
轉移就是找$dp[i]$和$dp[i-1]$、$dp[i-2]$、$dp[i-3]$……有什麼關係。
因此試著從$2*i$的圖形中找$2*(i-1)$,發現似乎大部分都可以由$2*(i-1)$的圖形以某種方式**長**出來,因此或許可以藉由觀察最下方兩排來得出一些轉移方式,但似乎沒有一個較不失一般性的描述。於是嘗試**分情形討論**:
![](https://hackmd.io/_uploads/ByXPNqYYn.png)
我們從左上開始編號1~8
發現1、2、4上半部的情形是相同的,3、5、6、7、8的上半部也是相同的,因此總共就是$3*(dp[i-1]中最底層沒有豎的情形)+5*(dp[i-1]中最底層有豎的情形)$,但是因為下一層($dp[i+1]$)也會用到,因此分開紀錄。
令$dp[0][i]$為$n=i$時,最底層沒有豎的情形;
令$dp[1][i]$為$n=i$時,最底層有豎的情形。
則:
$$
dp[0][i]=2\times dp[0][i-1]+dp[1][i-1]\\
dp[1][i]=dp[0][i-1]+4\times dp[1][i-1]
$$
複雜度$\text{O}(n)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int mod=1e9+7;
vector<vector<ll>> dp(2,vector<ll>(2e6));
dp[0][1]=1;
dp[1][1]=1;
for(int i=2;i<2e6;i++){
dp[0][i]=dp[0][i-1]*2+dp[1][i-1];
dp[1][i]=dp[1][i-1]*4+dp[0][i-1];
dp[0][i]%=mod;
dp[1][i]%=mod;
}
int t;
cin>>t;
while(t--){
int a;
cin>>a;
cout<<(dp[0][a]+dp[1][a])%mod<<endl;
}
return 0;
}
```
:::
:::spoiler 延伸:
[I. 鐵路鋪設 (rail)](https://tioj.ck.tp.edu.tw/problems/2259)
這題需要使用矩陣快速冪來加速,不過可以先試著找找看遞迴式。
:::
# [Edit Distance](https://cses.fi/problemset/task/1639)
:::spoiler 解法:
定義$dp[i][j]$為第一個字串前$i$(1-based)個字元和第二個字串前$j$(1-based)個字元的最小edit distance。
接下來以範測觀察如何轉移,從$dp[4][5]$來看,很明顯因為最後一個字元相同,所以$dp[4][5]=dp[3][4]$。如果不同,如$dp[3][4]$,考慮所有可能的最後的操作,若是加字(刪字的操作可以都由加字來代替,因此不考慮),則轉移點是$dp[2][4]$或$dp[3][3]$,若為改字,則轉移點為$dp[2][3]$。因此:
若$s_1[i]=s_2[j]$,
$$
dp[i][j]=dp[i-1][j-1]
$$
若$s_1[i]\neq s_2[j]$,
$$
dp[i][j]=min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1
$$
複雜度$\text{O}(nm)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
string a,b;
cin>>a>>b;
int n=a.size(),m=b.size();
vector<vector<int>> dp(n+1,vector<ll>(m+1));
for(ll i=0;i<m+1;i++){
dp[0][i]=i;
}for(ll i=0;i<n+1;i++){
dp[i][0]=i;
}
for(ll i=1;i<n+1;i++){
for(ll j=1;j<m+1;j++){
if(a[i-1]==b[j-1]){
dp[i][j]=dp[i-1][j-1];
}else{
dp[i][j]=min({dp[i-1][j],dp[i][j-1],dp[i-1][j-1]})+1;
}
}
}
cout<<dp[n][m]<<endl;
return 0;
}
```
:::
# [Rectangle Cutting](https://cses.fi/problemset/task/1744)
:::spoiler 解法:
令$dp[i][j]$為$a=i$且$b=j$時的答案。
觀察到任意尺寸的矩形(正方形除外)的最佳分割方式一定有一刀是會將矩形一分為二的(證明:Trivial!),因此枚舉此線所有可能的切割位置,最後再取最小值。即若$i\neq j$,則:
$$
\begin{aligned}
dp[i][j]=\min(&\mathop{\min}_{0<k<i} dp[k][j]+dp[i-k][j]+1,
\\ &\mathop{\min}_{0<k<j} dp[i][k]+dp[i][j-k]+1)
\end{aligned}
$$
複雜度$\text{O}((\max(a,b))^3)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
vector<vector<int>> dp(501,vector<int> (501,1e9));
for(int i=1;i<501;i++){
for(int j=1;j<501;j++){
if(i==j){
dp[i][j]=0;
continue;
}
for(int k=1;k<j;k++){
dp[i][j]=min(dp[i][j],dp[i][j-k]+dp[i][k]+1);
}
for(int k=1;k<i;k++){
dp[i][j]=min(dp[i][j],dp[i-k][j]+dp[k][j]+1);
}
}
}
int a,b;
cin>>a>>b;
cout<<dp[a][b]<<endl;
return 0;
}
```
:::
# [Money Sums](https://cses.fi/problemset/task/1745)
:::spoiler 解法:
其實和[Coin Combinations II](https://hackmd.io/apWfaWY0T1i47d0xhGHYNw?view#Coin-Combinations-II)幾乎一模一樣,只不過每個硬幣都只能用一次,要解決也非常簡單,只要在遍歷dp陣列時**由後往前**就可以避免重複使用的問題(因為每一個轉移點都是上一輪留下來的),如果不太能理解可以試著開二維的dp陣列或者直接看code。
複雜度$\text{O}(n^2\max (x))$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,ans=0;
cin>>n;
vector<int> v(n);
for(int i=0;i<n;i++){
cin>>v[i];
}
vector<bool> dp(1e5+1);
dp[0]=1;
for(int i:v){
for(int j=1e5;j>=i;j--){
if(dp[j-i]==1&&dp[j]==0){
ans++;
dp[j]=1;
}
}
}
cout<<ans<<endl;
for(int i=1;i<=1e5;i++){
if(dp[i]) cout<<i<<" ";
}
cout<<endl;
}
```
:::
# [Removal Game](https://cses.fi/problemset/task/1097)
:::spoiler 解法:
令$dp[i][j]$為陣列剩下$[i,j]$時,先手能得到的最大分數。
每一個dp值的轉移點一定是長度紹一的陣列而來。因此從$j-i=0$的情況開始觀察如何計算。
若$j-i=0$,$dp[i][j]=v[i]$
若$j-i=1$,
$$
\begin{aligned}
dp[i][j]&=\max (v[i],v[j])
\\ &=\max (v[i]+v[j]-dp[i+1][j],v[i]+v[j]-dp[i][j-1])
\\ &=v[i]+v[j]-\min(dp[i+1][j],dp[i][j-1])
\end{aligned}
$$
若$j-i=2$,
$$
dp[i][j]=v[i]+v[i+1]+v[j]-\min(dp[i+1][j],dp[i][j-1])
$$
不難觀察到:
$$
dp[i][j]=\sum_{k=i}^{j} v[k]-\min(dp[i+1][j],dp[i][j-1])
$$
換句話說,從$[i,j]$頭或尾拿走一張就會剩下$[i+1,j]$或$[i,j-1]$而對手也會拿最好的(也就是$dp[i+1][j]$或$dp[i][j-1]$),而這區間拿到的就是(區間總和)-(對手拿走的)。
區間總和可以簡單的利用前綴和計算。
複雜度$\text{O}(n^2)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int main(){
int n;
cin>>n;
vector<ll> pre(n+1);
vector<vector<ll>> dp(n,vector<ll>(n));
for(int i=0;i<n;i++){
cin>>pre[i+1];
pre[i+1]+=pre[i];
}
for(int k=0;k<n;k++){
for(int i=0;i+k<n;i++){
int j=i+k;
if(k==0){
dp[i][j]=pre[i+1]-pre[i];
continue;
}
dp[i][j]=pre[j+1]-pre[i]-min(dp[i+1][j],dp[i][j-1]);
}
}
cout<<dp[0][n-1]<<endl;
return 0;
}
```
:::
# [Two Sets II](https://cses.fi/problemset/task/1093/)
先備知識:模逆元
:::spoiler 解法:
換個角度思考:在$[1,n]$的數字中,有多少種組合使得總合為$\frac{n(n+1)}{4}$(即$\frac{\sum_{i=1}^{n}i\;}{2}$),沒錯! 就是[Money Sums](https://hackmd.io/apWfaWY0T1i47d0xhGHYNw?view#Money-Sums)!
最後要注意,答案要除以二(因為要分兩邊),但是遇到取模,所以就要使用模逆元。
複雜度$\text{O}(n^3)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int main(){
int n,mod=1e9+7;
cin>>n;
ll tar=n*(n+1)/2;
if(tar%2==1){
cout<<0<<endl;
return 0;
}
vector<ll> dp(2e5);
dp[0]=1;
for(int i=1;i<=n;i++){
for(int j=2e5;j>=i;j--){
dp[j]+=dp[j-i];
dp[j]%=mod;
}
}
cout<<dp[tar/2]*500000004%mod<<endl;
return 0;
}
```
:::
# [Increasing Subsequence](https://cses.fi/problemset/task/1145/)
先備知識:二分搜
:::spoiler 解法:
**$\text{O}(n^2)$作法:**
令$dp[i]$為以$i$結尾的LIS長度,則:
$$
dp[i]=\mathop{\max}_{j<i, v[j]<v[i]} dp[j]+1
$$
---
**$\text{O}(n\log n)$作法:**
觀察平方做法後發現,有一些位置不會用到:
若$dp[a]=dp[b]$,且$a<b$,則$v[a]\geq v[b]$(因為若$v[a]<v[b]$,表示$b$可以接在$a$後面,則$dp[a]=dp[b]$不成立),且對於所有$i>b$,可以接在$b$後的不一定可以接在$a$後,因此$a$就可以不用考慮了。
整理一下:若$dp[a]=dp[b]$,且$a<b$,則$a$可以不用考慮。
也就是說對於相同的dp值,我們只要維護**位置最後面**的就好了。
所以我們需要改一下dp的定義:
令$dp[i]$為長度為$i$的嚴格遞增序列的結尾中位置最後方的數字。
由左到右遍歷,根據定義,所有數字在遍歷到時都會被加入到dp序列(因為所有數字都會是當前的數字中位置最後面的),問題是要加入到哪一個dp值。要讓LIS越長越好,所以要加入到dp序列中最後一個比自己小的數字後方,並且由上方的觀察發現,dp序列會嚴格遞增,也就是說,要將每個數字加入到dp序列中的`lower_bound`。
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,ans=0;
cin>>n;
vector<int> dp={0};
for(int i=0;i<n;i++){
int a;
cin>>a;
if(dp.back()<a){
dp.push_back(a);
ans++;
}
dp[lower_bound(dp.begin(),dp.end(),a)-dp.begin()]=a;
}
cout<<ans<<endl;
return 0;
}
```
:::
# [Projects](https://cses.fi/problemset/task/1140/)
先備知識:排序、二分搜、離散化
:::spoiler 開始之前
先去寫[Movie Festival](https://cses.fi/problemset/task/1629/),這題是所有價值皆相等的特殊情形。
:::
:::spoiler 解法:
寫過上面那一題應該知道要照結束時間排序了,不再贅述。
定義$dp[i]$為在$i$這個時間點能得到的最大價值,則:
若有一場電影$[a,i]$,價值$w$,則:
$$
dp[i]=max(dp[i-1],dp[a-1]+w)
$$
若無,則
$$
dp[i]=dp[i-1]
$$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int main(){
int n;
cin>>n;
vector<vector<int>> pro(n,vector<int>(3));
vector<int> num;
for(int i=0;i<n;i++){
cin>>pro[i][0]>>pro[i][1]>>pro[i][2];
num.push_back(pro[i][0]);
num.push_back(pro[i][1]);
}
sort(num.begin(),num.end());
num.resize(unique(num.begin(),num.end())-num.begin());
for(int i=0;i<n;i++){
for(int j=0;j<2;j++){
pro[i][j]=lower_bound(num.begin(),num.end(),pro[i][j])-num.begin();
}
}
sort(pro.begin(),pro.end(),[](auto i,auto j){
return i[1]<j[1];
});
vector<ll> dp(4e5+1);
ll ans=0,pos=1;
for(auto i:pro){
while(pos<i[1]+1){
dp[pos]=max(dp[pos],dp[pos-1]);
pos++;
}
dp[pos]=max(dp[pos],dp[i[0]]+i[2]);
ans=max(ans,dp[pos]);
}
cout<<ans<<endl;
return 0;
}
```
:::
# [Elevator Rides](https://cses.fi/problemset/task/1653)
先備知識:位元運算
:::spoiler 解法:
請參考[位元dp](https://hackmd.io/hTQrRJDaQ2C2X9UouWengw?view#%E4%BD%8D%E5%85%83dp)
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,x;
cin>>n>>x;
vector<int> v(n);
for(int i=0;i<n;i++){
cin>>v[i];
}
vector<int> dp(1<<n),last(1<<n);
dp[0]=1;
last[0]=0;
for(int i=1;i<=1<<n;i++){//窮舉子集合
dp[i]=INT_MAX;
last[i]=INT_MAX;
for(int j=0;j<n;j++){//窮舉轉移點
if(i&(1<<j)){
int a=i-(1<<j);//轉移點
if(last[a]+v[j]<=x){
if(dp[a]<dp[i]||(dp[a]==dp[i]&&last[a]+v[j]<last[i])){
dp[i]=dp[a];
last[i]=last[a]+v[j];
}
}else {
if(dp[a]+1<dp[i]||(dp[a]+1==dp[i]&&v[j]<last[i])){
dp[i]=dp[a]+1;
last[i]=v[j];
}
}
}
}
}
cout<<dp[(1<<n)-1]<<endl;
return 0;
}
```
:::
:::spoiler 延伸:
[Hamiltonian Flights](https://cses.fi/problemset/task/1690)
[Team Building](https://codeforces.com/contest/1316/problem/E)
[Matching](https://atcoder.jp/contests/dp/tasks/dp_o?lang=en)
[貨郎問題](https://judge.tcirc.tw/ShowProblem?problemid=d089)
:::
# [Counting Tilings](https://cses.fi/problemset/task/2181)
先備知識:二元運算
:::spoiler 解法:
這題不能用和[Rectangle Cutting](https://hackmd.io/apWfaWY0T1i47d0xhGHYNw?view#Rectangle-Cutting)一樣的解法,因為存在以下構造方式:
![](https://hackmd.io/_uploads/B11QrxhYh.png)
定義$dp[i][j][S]$為前$i-2$行以及第$i-1$行的前$j$行皆填滿,且輪廓(即圖中膚色的地方)填充狀態為集合$S$的方法數。以下圖為例,即為$dp[2][3][0110010]$(皆為0-based)
![](https://hackmd.io/_uploads/SJfXV7hK2.png)
(注:$\oplus$為$\text{XOR}$)
* 若(i,j)有放:
* 若為橫放: $dp[i][j][S]=dp[i][j-1][S\oplus 2^i]$
![](https://hackmd.io/_uploads/r1-9Q7hK2.png)
* 若為直放(條件:$(i-1,j)$為空): $dp[i][j][S]=dp[i][j-1][S\oplus 2^{i-1}]$
![](https://hackmd.io/_uploads/Hy6-vQnYh.png)
* 若(i,j)沒放: $dp[i][j][S]=dp[i][j-1][S\oplus 2^i]$
![](https://hackmd.io/_uploads/Hk5gdQnt2.png)
另外需特別考慮$i=0$的情況,由於情況和上方類似(僅不包含放直的情況),故在此省略。
複雜度$\text{O}(nm2^n)$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m,mod=1e9+7;
cin>>n>>m;
vector<vector<vector<int>>> dp(n,vector<vector<int>>(m,vector<int> (1<<n)));
dp[0][0][(1<<n)-2]=1;
for(int j=0;j<m;j++){
for(int i=0;i<n;i++){
for(int s=0;s<1<<n;s++){
if(i==0){
if(j>0) dp[i][j][s]=dp[n-1][j-1][s^1];
}
else{
if(s&(1<<i)){
dp[i][j][s]=dp[i-1][j][s^(1<<i)];
if(s&(1<<(i-1))){
dp[i][j][s]+=dp[i-1][j][s^(1<<(i-1))];
}
}
else{
dp[i][j][s]=dp[i-1][j][s^(1<<i)];
}
}
dp[i][j][s]%=mod;
}
}
}
cout<<dp[n-1][m-1][(1<<n)-1]<<endl;
return 0;
}
```
:::
:::spoiler 參考資料:
[USACO GUIDE](https://usaco.guide/adv/dp-more?lang=cpp#code)有一個將空間壓縮到一維($\text{O}(2^n)$)的寫法。
:::
# [Counting Numbers](https://cses.fi/problemset/task/2220)
:::spoiler 解法:
令$dp[i][j]$ 為 $[j\times 10^{i},(j+1)\times 10^{i})$ 中的答案(也就是所有$j$開頭的$i+1$位數的答案),則:
$$
dp[i][j]=\sum_{0\leq k<10,k\neq j \vee j=0} dp[i-1][k]
$$
稍微觀察,或者print出來時就會發現$dp[i][1]$到$dp[i][9]$都一樣
那麼就非常簡單了!
以$321$為例:拆成$[0,300)$、$[300,320)$、$[320,322)$三個部分做計算。
但是在實作上,有不少地方要注意,例如:$[300,320)$其實是$(dp[1][0]-dp[0][0])+dp[1][1]$,因為$300$不合法;另外,若前綴已經不合法了,那麼剩下的位數也都不用算了。
複雜度$\text{O}(\log_{10}(\max(a,b)))$
:::
:::spoiler code:
```cpp=
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int main(){
vector<vector<ll>> dp(20,vector<ll>(10));
for(int i=0;i<10;i++){
dp[0][i]=1;
}
for(int i=1;i<20;i++){
for(int j=0;j<10;j++){
for(int k=0;k<10;k++){
if(j==0 || j!=k) dp[i][j]+=dp[i-1][k];
if(j!=0 && i>1 && k==0) dp[i][j]-=dp[i-2][k];
}
}
}
ll a,b,ans1=0,ans2=0;
vector<int> v1,v2;
cin>>a>>b;
b++;
while(a){
v1.push_back(a%10);
a/=10;
}
while(b){
v2.push_back(b%10);
b/=10;
}
for(int i=v1.size()-1;i>=0;i--){
for(int j=0;j<v1[i];j++){
if(i!=v1.size()-1 && v1[i]>0 && i>0) ans1+=dp[i][0]-dp[i-1][0];
else ans1+=dp[i][j];
}
if(i!=v1.size()-1 && v1[i]>v1[i+1]) ans1-=dp[i][v1[i+1]];
if(v1[i]==v1[i+1]) break;
}
for(int i=v2.size()-1;i>=0;i--){
for(int j=0;j<v2[i];j++){
if(i!=v2.size()-1 && v2[i]>0 && i>0) ans2+=dp[i][0]-dp[i-1][0];
else ans2+=dp[i][j];
}
if(i!=v2.size()-1 && v2[i]>v2[i+1]) ans2-=dp[i][v2[i+1]];
if(v2[i]==v2[i+1]) break;
}
cout<<ans2-ans1<<endl;
}
```
:::