- 最長上升子序列問題
- 循環數組最大子段和問題
- 正整數分組問題
- 多重揹包問題
- 多重部分和問題
- 劃分數問題
- 多重集組合數問題
- 最大子矩陣和問題
- 數位dp問題
1、最長上升子序列問題
題目:
有一個長爲n的數列a0,a1,…,an-1。請求出這個序列中最長的上升子序列的長度。上升子序列指的是對於任意的 i< j 都滿足ai< aj 的子序列。
思路:
定義dp[i]爲長度爲i+1的上升子序列中末尾元素的最小值(不存在的話爲INF)
//最長上升子序列問題
int dp[Max_n];
void solve(){
memset(dp,0x3f,sizeof(dp));
for(int i=0;i<n;i++)
*lower_bound(dp,dp+n,a[i])=a[i];
printf("%d\n",lower_bound(dp,dp+n,inf)-dp);
}
1、循環數組最大子段和問題
題目:
N個整數組成的循環序列a[1],a[2],a[3],…,a[n],求該序列如a[i]+a[i+1]+…+a[j]的連續的子段和的最大值。當所給的整數均爲負數時和爲0。(2 ≤N ≤ 50000,-10^9 ≤ a[i]≤10^9)
思路:
分情況討論:1. 最優的最大字段和在中間部分。2.最優的最大字段和在首尾兩端,此時中間部分是個最小字段和,用sum-中間部分最小字段和即可得到。
//循環數組最大子段和問題
int n;
int a[Max_n];
ll sum=0,Max=0,Min=0;
void solve(){
ll t1=0,t2=0;
for(int i=0;i<n;i++){
if(t1>0)t1+=a[i];
else t1=a[i];
if(t1>Max)Max=t1;
if(t2<0)t2+=a[i];
else t2=a[i];
if(t2<Min)Min=t2;
}
printf("%I64d\n",max(Max,sum-Min));
}
2、正整數分組問題
題目:
將一堆正整數分爲2組,要求2組的和相差最小。例如:1 2 3 4 5,將1 2 4分爲1組,3 5分爲1組,兩組和相差1,是所有方案中相差最少的。(N ≤100, 所有正整數的和≤10000)
思路:
重量和價值都相等的01揹包變形。定義dp[i][j]表示爲從前i個數中,總和不超過j的最大值。
//正整數分組問題
int n,s[110];
int dp[10010];
void solve(){
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
for(int j=sum/2;j>=s[i];j--)
dp[j]=max(dp[j],dp[j-s[i]]+s[i]);
printf("%d\n",sum-2*dp[sum/2]);
}
3、多重揹包問題
題目:
有n種重量、價值和數量分別爲wi,vi,ci的物品,從這些物品中挑選出總重量不超過W的物品,求出挑選物品價值總和的最大值。(1≤n≤100,1≤W≤50000)
思路:
二進制優化多重揹包。
//多重揹包問題
int n,W;
int w[Max_n],v[Max_n],c[Max_n]; //重量、價值和數量
int dp[Max_W];
void ZeroOne_Pack(int w,int v){
for(int i=W;i>=w;i--)
dp[i]=max(dp[i],dp[i-w]+v);
}
void Complete_Pack(int w,int v){
for(int i=w;i<=W;i++)
dp[i]=max(dp[i],dp[i-w]+v);
}
int Multi_Pack(){
memset(dp,0,sizeof(dp));
for(int i=0;i<n;i++){
if(w[i]*c[i]>=W)Complete_Pack(w[i],v[i]);
else {
int k=1;
while(k<c[i]){
ZeroOne_Pack(w[i]*k,v[i]*k);
c[i]-=k;
k<<=1;
}
ZeroOne_Pack(w[i]*c[i],v[i]*c[i]);
}
}
return dp[W];
}
4、多重部分和問題
題目:
有n種不同大小的數字ai,每種各ci個,判斷是否可以從這些數字之中選出若干使它們的和恰好爲k。(1 ≤n≤100 , 1≤K≤100000)
思路:
定義dp[i+1][j]爲前 i 種數加和得到 j 時第 i 種數最多能剩餘多少(不能加和得到 i 的情況爲 -1)
//多重部分和問題
int n,k;
int a[Max_n],c[Max_n];
int dp[Max_k];
bool solve(){
memset(dp,-1,sizeof(dp));
for(int i=0;i<n;i++){
dp[0]=c[i];
for(int j=1;j<=k;j++){ //遞推關係
if(dp[j]>=0)dp[j]=c[i];
else if(j>=a[i]&&dp[j-a[i]]>0)dp[j]=dp[j-a[i]]-1;
else dp[j]=-1;
}
}
if(dp[k]>=0)return true;
else return false;
}
5、劃分數問題
題目:
有n個無區別的物品,將它們劃分成不超過m組,求出劃分方法數模M的餘數。(1 ≤m≤n≤1000)
思路:
考慮n的m劃分ai(i=1,2,3…),如果對於每個ai>0,那麼{ai-1}就對應了n-m的m劃分,另外如果存在ai=0,那麼就對應了n的m-1劃分。定義dp[i][j]爲 i 的 j 劃分的總數,則dp[i][j]=dp[i][j-1]+dp[i-j][j]。
//劃分數問題
int n,m;
int dp[Max_n][Max_m];
void solve(){
memset(dp,0,sizeof(dp));
dp[0][0]=1;
for(int i=0;i<=n;i++){
for(int j=1;j<=n;j++){
if(i>=j)dp[i][j]=(dp[i][j-1]+dp[i-j][j])%M;
else dp[i][j]=dp[i][j-1];
}
}
printf("%d\n",dp[n][m]);
}
6、多重集組合數問題
題目:
有n種物品,第i種物品有ai個。不同種類的物品可以互相區分但相同種類的無法區分。從這些物品中取出m個的話,有多少種取法?求出方案數模M的餘數。(1≤n≤1000,1≤m≤1000,1≤ai≤1000,2≤M≤10000)
思路:
定義dp[i][j]爲從前i中物品中取出j個的組合總數。
1. 當j≤a[i]時,dp[i][j]=dp[i-1][j]+dp[i-1][j] 2. 當j>s[i]時,dp[i][j]=dp[i-1][j]+dp[i-1][j]-dp[i-1][j-1-a[i]]。
//多重集組合數問題
int n,m;
int dp[2][Max_n];
void solve(){
memset(dp,0,sizeof(dp));
dp[0][0]=dp[1][0]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(j<=a[i])dp[i&1][j]=(dp[i&1][j-1]+dp[(i-1)&1][j])%mod;
else dp[i&1][j]=(dp[i&1][j-1]+dp[(i-1)&1][j]-dp[(i-1)&1][j-1-a[i]]+mod)%mod;
//在有取餘的情況下,要避免減法運算的結果出現負數
}
}
printf("%d\n",dp[n&1][m]);
}
7、最大子矩陣和問題
題目:
一個N*M的矩陣,找到此矩陣的一個子矩陣,並且這個子矩陣的元素的和是最大的,輸出這個最大的值。如果所有數都是負數,就輸出0。(2 <= N,M <= 500)
思路:
最後的子矩陣一定在某兩行之間,枚舉所有1<=i<=j<=N,表示最終子矩陣選取的行範圍。分別求出第i行到第j行之間的每一列的和,第i行到第j行之間的最大子矩陣和對應於這個和數組的最大子段和。
//最大子矩陣和問題
int n,m;
int sum[Max_m];
int map[Max_n][Max_m];
void solve(){
int Max=0;
for(int i=1;i<=n;i++){
memset(sum,0,sizeof(sum));
for(int j=i;j<=n;j++){
for(int k=1;k<=m;k++)sum[k]+=map[j][k];
int ans=0;
for(int k=1;k<=m;k++){ //求子矩陣的最大字段和
if(ans>=0)ans+=sum[k];
else ans=sum[k];
if(ans>Max)Max=ans;
}
}
}
printf("%d\n",Max);
}
7、數位dp問題
思路:
dp思想,枚舉到當前位置pos,狀態爲state(這個就是根據題目來的,可能很多,畢竟dp千變萬化)的數量(既然是計數,dp值顯然是保存滿足條件數的個數)
typedef long long ll;
int a[20];
ll dp[20][state];//不同題目狀態不同
ll dfs(int pos,/*state變量*/,bool lead/*前導零*/,bool limit/*數位上界變量*/)//不是每個題都要判斷前導零
{
//遞歸邊界,既然是按位枚舉,最低位是0,那麼pos==-1說明這個數我枚舉完了
if(pos==-1) return 1;/*這裏一般返回1,表示你枚舉的這個數是合法的,那麼這裏就需要你在枚舉時必須每一位都要滿足題目條件,也就是說當前枚舉到pos位,一定要保證前面已經枚舉的數位是合法的。不過具體題目不同或者寫法不同的話不一定要返回1 */
//第二個就是記憶化(在此前可能不同題目還能有一些剪枝)
if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];
/*常規寫法都是在沒有限制的條件記憶化,這裏與下面記錄狀態是對應,具體爲什麼是有條件的記憶化後面會講*/
int up=limit?a[pos]:9;//根據limit判斷枚舉的上界up;這個的例子前面用213講過了
ll ans=0;
//開始計數
for(int i=0;i<=up;i++)//枚舉,然後把不同情況的個數加到ans就可以了
{
if() ...
else if()...
ans+=dfs(pos-1,/*狀態轉移*/,lead && i==0,limit && i==a[pos]) //最後兩個變量傳參都是這樣寫的
/*這裏還算比較靈活,不過做幾個題就覺得這裏也是套路了
大概就是說,我當前數位枚舉的數是i,然後根據題目的約束條件分類討論
去計算不同情況下的個數,還有要根據state變量來保證i的合法性,比如題目
要求數位上不能有62連續出現,那麼就是state就是要保存前一位pre,然後分類,
前一位如果是6那麼這意味就不能是2,這裏一定要保存枚舉的這個數是合法*/
}
//計算完,記錄狀態
if(!limit && !lead) dp[pos][state]=ans;
/*這裏對應上面的記憶化,在一定條件下時記錄,保證一致性,當然如果約束條件不需要考慮lead,這裏就是lead就完全不用考慮了*/
return ans;
}
ll solve(ll x)
{
int pos=0;
while(x)//把數位都分解出來
{
a[pos++]=x%10;//個人老是喜歡編號爲[0,pos),看不慣的就按自己習慣來,反正注意數位邊界就行
x/=10;
}
return dfs(pos-1/*從最高位開始枚舉*/,/*一系列狀態 */,true,true);//剛開始最高位都是有限制並且有前導零的,顯然比最高位還要高的一位視爲0嘛
}
int main()
{
ll le,ri;
while(~scanf("%lld%lld",&le,&ri))
{
//初始化dp數組爲-1,這裏還有更加優美的優化,後面講
printf("%lld\n",solve(ri)-solve(le-1));
}
}