【线性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个相同的数,删去区间,贡献为区间和
求最大贡献
思路
代码
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;
}