【线性DP】

【线性DP】

线性DP,最简单的一类dp
所以还是要尽量写对

全都要!!!!!

https://ac.nowcoder.com/acm/contest/102896/E

思路

看到 n=1e4 k=1e3 为什么不开二维状态呢?
考虑dp[i][j]为第i个点在第j次的时的最大值 (记录次数!!!)
可得状态转移方程为

dp[i][j]=max(dp[i][j],dp[i-dx][j-1]+a[i])

代码

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef pair<int,int> PII;
typedef long long ll;
const ll INF=0x3f3f3f3f3f3f3f3f;
ll abss(ll a){return a>0?a:-a;}
ll max_(ll a,ll b){return a>b?a:b;}
ll min_(ll a,ll b){return a<b?a:b;}
bool cmpll(ll a,ll b){return a>b;}
const int N=1e4+10;
const int M=1e3+10;
ll dp[N][M];
int n,k;
ll a[N];
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      cin>>n>>k;
      for(int i=1;i<=n;i++) cin>>a[i];
      memset(dp,-0x3f,sizeof dp);
      //注意初始化只能为前面的六个数
      dp[0][0]=0;dp[1][1]=a[1];dp[2][1]=a[2];dp[3][1]=a[3];dp[4][1]=a[4];dp[5][1]=a[5];dp[6][1]=a[6];
      for(int i=1;i<=n;i++){
            for(int j=1;j<=k;j++){
                  for(int k=1;k<=6;k++){
                        int nx=i-k;
                        if(nx<0) break;
                        dp[i][j]=max_(dp[i][j],dp[nx][j-1]+a[i]);
                  }
            }
      }
      ll ans=-INF;
      for(int i=1;i<=n;i++){
            ans=max_(ans,dp[i][k]);
      }
      cout<<ans;
      return 0;
}

Igor and Mountain

https://codeforces.com/contest/2091/problem/F
小细节很多的一题线性dp

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef pair<int,int> PII;
typedef long long ll;
ll abss(ll a){return a>0?a:-a;}
ll max_(ll a,ll b){return a>b?a:b;}
ll min_(ll a,ll b){return a<b?a:b;}
bool cmpll(ll a,ll b){return a>b;}
const ll mod=998244353;
const int N=2010;
int t;
int n,m,d;
/*
【动态规划】
(都以0为索引)
dp[i][j][f] f两维:可以理解为该行用了两只手还是一只手
f==0 在该层只使用了一只手->说明可以在该层添加第二只手
f==1 在该层已经使用了两只手->从下层选

※初始化:dp[n-1][j][f==0/1]=1

※状态转移:
同层:dp[i][j][0]=dp[i][j][0]+dp[i][j-d~j+d][1](从两个持有转移过来一个持有给他)-dp[i][j][1](扣掉自己点的)
从下层转移:范围sqrt(d*d-1)->可等效为dx=d-1
因为可以只用1只手吊着->f==0/f==1的情况都可以转移
dp[i][j][f]=dp[i][j][f]+dp[i+1][j-dx~j+dx][0]

->优化:区间求和->前缀和缩成O(1)->每一层dp完求一次前缀和

※结果:第0层每个只用一只手的个数相加(f==1只是辅助)
*/
ll dp[N][N][2],sdp[N][N][2];
void solve(){
      cin>>n>>m>>d;
      vector<string> s(n+1);
      for(int i=1;i<=n;i++){
            string tmp;
            cin>>tmp;
            tmp=' '+tmp;
            s[i]=tmp;
      }
      //初始化清0
      for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                  dp[i][j][0]=dp[i][j][1]=0;
                  sdp[i][j][0]=sdp[i][j][1]=0;
            }
      }
      //for(int i=1;i<=n;i++) cout<<s[i]<<endl;
      //for(int i=1;i<=m;i++) cout<<sdp[n][i][0]<<endl;
      //注意:计算前缀和的时候不要直接取模!!!会有负数
      for(int i=n;i>=1;i--){
            if(i==n){
                  //初始化给值
                  for(int j=1;j<=m;j++){
                        if(s[i][j]=='X') dp[i][j][0]=dp[i][j][1]=1;
                  }
                  //求前缀和
                  for(int j=1;j<=m;j++){
                        sdp[i][j][1]=(sdp[i][j-1][1]+dp[i][j][1]);
                  }
                  //算同层转移的情况
                  for(int j=1;j<=m;j++){
                        if(s[i][j]=='X'){
                              ll res=(sdp[i][min(m,j+d)][1]-sdp[i][max(0,j-d-1)][1])%mod;
                              dp[i][j][0]=(dp[i][j][0]+res-dp[i][j][1])%mod;
                        }
                  }
                  //再求前缀和
                  for(int j=1;j<=m;j++){
                        sdp[i][j][0]=(sdp[i][j-1][0]+dp[i][j][0]);
                  }
                  continue;
            }
            for(int j=1;j<=m;j++){
                  //初始化
                  if(s[i][j]=='#'){
                        continue;
                  }
                  //状态转移
                  //注意前缀和的处理方式:因为f=0要依靠f=1 ->先算f1并且计算前缀和
                  if(i<n){
                        int dx=d-1;
                        ll res=(sdp[i+1][min(m,j+dx)][0]-sdp[i+1][max(0,j-dx-1)][0])%mod;
                        dp[i][j][1]=(dp[i][j][1]+res)%mod;
                  }    
                  
            }
            //计算前缀和
            for(int j=1;j<=m;j++){
                  sdp[i][j][1]=(sdp[i][j-1][1]+dp[i][j][1]);
            }
            //再算f=0
            for(int j=1;j<=m;j++){
                  if(s[i][j]=='#'){
                        continue;
                  }
                  ll res=(sdp[i][min(m,j+d)][1]-sdp[i][max(0,j-d-1)][1])%mod;
                  dp[i][j][0]=(dp[i][j][0]+res)%mod;
                  dp[i][j][0]=(dp[i][j][0]-dp[i][j][1])%mod;
                  if(i<n){
                        int dx=d-1;
                        ll res1=(sdp[i+1][min(m,j+dx)][0]-sdp[i+1][max(0,j-dx-1)][0])%mod;
                        dp[i][j][0]=(dp[i][j][0]+res1)%mod;
                  }
            }
            //再算前缀和
            for(int j=1;j<=m;j++){
                  sdp[i][j][0]=(sdp[i][j-1][0]+dp[i][j][0]);
            }
      }
      ll ans=0;
      for(int i=1;i<=m;i++) ans=(ans+dp[1][i][0])%mod;
      cout<<ans<<endl;
}
signed main(){
      ios::sync_with_stdio(0);
      cin.tie(0);
      cout.tie(0);
      cin>>t;
      while(t--) solve();
      return 0;
}

Bowls and Beans

https://atcoder.jp/contests/abc404/tasks/abc404_e
最简单的一类dp
注意状态的设计

const int INF=1e9;
int n;
/*
【动态规划思路】
求出[i-c[i],i-1]放到0号碗的最小操作次数

状态:dp[i] 从i号放到0号需要操作多少次
转移:dp[i]=min(dp[i-c[i]],dp[i-c[i]+1],...dp[i-1])+1

若i号位有豆子:dp[i]=0(直接挪走) ans+=dp[i](挪走这些豆子需要的代价)
i后面的豆子可以和i一起走,不需要再经历i的过程
*/
void solve(){
    cin>>n;
    vector<int> c(n+1,0),a(n+1,0),dp(n+1,INF);
    for(int i=1;i<n;i++) cin>>c[i];
    for(int i=1;i<n;i++) cin>>a[i];
    int ans=0;
    dp[0]=0;
    for(int i=1;i<n;i++){
        for(int k=i-c[i];k<i;k++){
            dp[i]=min(dp[i],dp[k]+1);// dp[k]+1 :每次多走1步
        }
        if(a[i]){
            ans+=dp[i];
            dp[i]=0;
        }
    }
    cout<<ans<<endl;
}

Toxel 与 PCPC II

https://codeforces.com/gym/105158/attachments
别一上来就想分块!

从最朴素的dp思路想起:
dp[i]:debug前i行代码所需最少时间
->从j转移到i
dp[i]=min(dp[i],dp[j]+a[i]+(j-i)^4)
->考虑j的遍历方式:debug数量很小,代价很高
->完全可以往后遍历常数位
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef pair<int,int> PII;
typedef long long ll;
ll abss(ll a){return a>0?a:-a;}
ll max_(ll a,ll b){return a>b?a:b;}
ll min_(ll a,ll b){return a<b?a:b;}
bool cmpll(ll a,ll b){return a>b;}
const ll INF=0x3f3f3f3f3f3f3f3f;
int n,m;
void solve(){
    cin>>n>>m;
    vector<int> a(m+1,0);
    for(int i=1;i<=m;i++){
        cin>>a[i];
    }
    vector<ll> dp(m+1,INF);
    dp[0]=0;
    for(int i=1;i<=m;i++){
        for(int j=i-1;j>=max(0,i-50);j--){
            dp[i]=min_(dp[i],dp[j]+(ll)a[i]+1LL*(i-j)*(i-j)*(i-j)*(i-j));
        }
    }
    cout<<dp[m]<<endl;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int T=1;
    //cin>>T;
    while(T--) solve();
    return 0;
}

小柒的交替数组

https://ac.nowcoder.com/acm/contest/111921/B

题目大意

找连续01串,长度需大于题目所给m
可无限次修改位置上数字奇偶性来达成目的,问最少修改多少次

思路

(1)m性质-->串的性质
m奇数 010 101 头尾不同
m偶数 1010 0101 头尾相同
(2)考虑线性dp:
状态表示: a[i][0] 以偶数为起点交替时 到达当前点时的操作数
         a[i][1] 以奇数为起点交替时 到达当前点时的操作数

代码

int n;
int m;
void solve(){
    cin>>n>>m;
    vector<int> a(n+1,0);
    for(int i=1;i<=n;i++){
        cin>>a[i];
        a[i]=a[i]%2;
    }
    vector<array<int,2>> dp(n+2);
    int ans=inf_int;
    for(int i=1;i<=n;i++){
        if(a[i]==0){
            dp[i][0]=dp[i-1][1];
            dp[i][1]=dp[i-1][0]+1;
        }
        else{
            dp[i][1]=dp[i-1][0];
            dp[i][0]=dp[i-1][1]+1;
        }
        if(i>=m){
            if(m%2==1){
                ans=min(ans,dp[i][0]-dp[i-m][1]);
                ans=min(ans,dp[i][1]-dp[i-m][0]);
            }
            else{
                ans=min(ans,dp[i][1]-dp[i-m][1]);
                ans=min(ans,dp[i][0]-dp[i-m][0]);
            }
        }
    }
    cout<<ans<<endl;
}

【和区间有关的线性dp】

小红的双排列删除得分

https://ac.nowcoder.com/acm/contest/112576/E

如何判断一道题需要DP?

(1)无贪心性质
(2)可以把大问题化为子问题
(3)无后效性:思考了这个子问题,这个子问题内部没有新的问题

题目大意

双排列,每次可以选择2个相同的数,删去区间,贡献为区间和
求最大贡献

思路

image

代码

int n;
void solve(){
    cin>>n;
	vector<i64> a(2*n+1,0);
	vector<i64> s(2*n+1,0);
	vector<int> pos(n+1,2*n+1);
	for(int i=1;i<=2*n;i++){
		cin>>a[i];
		s[i]=s[i-1]+a[i];
		pos[a[i]]=min(i,pos[a[i]]);
	}
	vector<i64> dp(2*n+1,0);
	for(int r=1;r<=2*n;r++){
		int l=pos[a[r]];
		dp[r]=dp[r-1];
		if(l<r) dp[r]=max64(dp[r],dp[l-1]+(s[r]-s[l-1]));
	}
	cout<<dp[2*n]<<endl;
}

Segments Covering

https://codeforces.com/contest/2125/problem/D

题目大意

n个区间有概率的覆盖[l,r]
求每个单元格1~m恰好只被覆盖一次的概率

思路

考虑一个区间选or不选->很适合DP dp[r]=dp[r]和(dp[l-1]+操作)
【推式子+线性DP】
(1)
最开始考虑所有都不选->概率为1-p/q全部相乘
那么如果一个区间要选贡献为(p/q)/(1-p/q):乘上选的,除去不选的
(2)
dp[i]1~i恰好只被覆盖一次的概率
考虑枚举右端点更新dp,对于每一个区间:
dp[r]=dp[r]+dp[l-1]*val
【和传统dp的不同】
因为每个区间都要被选
因此:
每种方案之间要相加
单种方案内的概率相乘

代码

int n,m;
void solve(){
    cin>>n>>m;
    vector<array<i64,4>> pos(n);
    for(int i=0;i<n;i++){
        for(int k=0;k<4;k++) cin>>pos[i][k];
    }
    vector<vector<P64>> res(m+1);
    i64 tot=1;
    for(int i=0;i<n;i++){
        i64 per=pos[i][2]%mod;
        per=per*qmi(pos[i][3],mod-2,mod)%mod;
        i64 miper=(pos[i][3]-pos[i][2]+mod)%mod;
        miper=miper*qmi(pos[i][3],mod-2,mod)%mod;
        tot=tot*miper%mod;
        i64 val=per*qmi(miper,mod-2,mod)%mod;
        res[pos[i][1]].push_back({pos[i][0],val});
    }
    vector<i64> dp(m+1,0);
    dp[0]=1;
    for(int i=1;i<=m;i++){
    	if(res[i].empty()) continue;
        for(auto [l,val]:res[i]){
            dp[i]=(dp[i]+(dp[l-1]*val)%mod)%mod;
        }
    }
    //有重合都得算?
    i64 ans=tot%mod*dp[m]%mod;
    if(ans<0) ans+=mod;
    cout<<ans<<endl;
}
posted @ 2025-03-03 12:07  White_ink  阅读(16)  评论(0)    收藏  举报