序列dp 区间 dp 泛刷
清北学堂刷题及题解
1技巧:
- 只有当每个位置不同时才有枚举断点的必要。
- 必要时dp画转移图有助于对转移顺序的理解。
- 要充分利用所设状态带来信息。
- 注意旋转坐标系可以改变坐标变化从而带来做法。
- dp需要考虑对样本空间不重不漏的划分。
- 可以通过对阶段的巧妙选取可以简化状态。
2题目:
2.1Flappy Bird
注意改变坐标系: \((x,y)\to (x,\dfrac{x+y}{2})\)
注意坐标的处理即可,这道题与dp关系不大
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define ull unsigned long long
#define N number
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
inline int Min(int a,int b){
return a>b?b:a;
}
inline int Max(int a,int b){
return a>b?a:b;
}
template <typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
template <typename T> inline void write(T x) {
if(x < 0) x=-x,putchar('-');
if(x > 9) write(x / 10);
putchar(x%10+'0');
}
template <typename T> inline void writeln(T x) {
write(x);
puts("");
}
int n,X,last,l,r;
signed main(){
read(n);read(X);
for(int i=1;i<=n;i++){
int x,a,b;read(x);read(a);read(b);
if(a>=b){
printf("NIE\n");
return 0;
}
if(x>=X) break;
r+=x-last;last=x;
a=((x+a)*1.0/2)+1;b=((x+b-1)/2);
r=Min(r,b);l=Max(l,a);
if(l>r){
printf("NIE\n");
return 0;
}
}
writeln(l);
return 0;
}
2.2 Clear the String
简单的一道区间 dp ,这里放上我的题解。
关注到数据范围,这道题是一道区间 dp ,于是我们很自然的设状态 \(f_{i,j}\) 表示删去 \([i,j]\) 所花费的最小代价。
我们进行转移,转移时多枚举一个断点,但是我们这样转移少考虑了一种情况,即枚举断电后,左边一段和右边一段是一起删的。
实际上我们只需要考虑左端点和右端点一起被删的情况,因为如果不是这样的话,显然我们枚举断点可以照顾到,只有左端点和右端点一起被删时,无论怎样枚举断点都无法兼顾这种情况。
考虑到如果左右端点一起被删,那么一定是最后一个被删,我们考虑最后一个删除。
贪心的想,如果左右端点能够被删,就把他们一起删,这样做肯定不会比不把左右节点一起删更差。
如果能删,那么所用次数减 \(1\) 。
所以我们就有了一下转移:
代码:
#define N 510
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
inline int Min(int a,int b){
return a<b?a:b;
}
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int n,f[N][N];
char s[N];
int main(){
scanf("%d%s",&n,s+1);
memset(f,INF,sizeof(f));
for(int i=1;i<=n;i++) f[i][i]=1;
for(int i=2;i<=n;i++){//枚举区间长度
for(int j=1;j<=n-i+1;j++){//枚举左端点
int l=j,r=j+i-1;
for(int k=l;k<=r-1;k++){//枚举断点
f[l][r]=Min(f[l][r],f[l][k]+f[k+1][r]);
}
if(s[l]==s[r]) f[l][r]--;//29到32:转移
}
}
printf("%d",f[1][n]);
return 0;
}
2.3中国象棋
看到题目,是一道计数 dp ,因为列与列、行与行之间并没有差异,所以我们只需要关心哪些列放了多少车,哪些行放了多少车,考虑把他们涉及到状态里,但我们可以通过把行作为阶段,强制每行只能放2个来简化状态,以及因为列的总数我们知道,所以状态里我们只需要记当前是第几行,有多少列放了 \(0\) 个车,有多少列放了 \(1\) 个车。
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define ull unsigned long long
#define N 101
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
const int mod=9999973;
inline int Max(int a,int b){
return a>b?a:b;
}
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int f[N][N][N],n,m,c[N][N],ans;//f[i][j][k]表示放到了第i行,有j列放了0个炮,有k列放了1个炮的方案。
inline void prework(){
int maxx=Max(n,m);
for(int i=0;i<=maxx;i++)
for(int j=0;j<=2&&j<=i;j++)
if(i==0||j==0) c[i][j]=1;
else c[i][j]=c[i-1][j]+c[i-1][j-1];
}
signed main(){
n=read();m=read();
prework();
f[1][m][0]=1;f[1][m-2][2]=c[m][2];f[1][m-1][1]=m;
for(int i=1;i<=n-1;i++){
for(int j=0;j<=m;j++){
for(int k=0;k<=m;k++){//f[i][j][k]
if(!f[i][j][k]) continue;
int now=f[i][j][k];
f[i+1][j][k]+=f[i][j][k];
if(j>=1) f[i+1][j-1][k+1]+=now*c[j][1]%mod;
if(k>=1) f[i+1][j][k-1]+=now*c[k][1]%mod;
if(j>=2) f[i+1][j-2][k+2]+=now*c[j][2]%mod;
if(k>=2) f[i+1][j][k-2]+=now*c[k][2]%mod;
if(j>=1) f[i+1][j-1][k]+=now*j*k%mod;
// cout<<i<<" "<<j<<" "<<k<<" "<<f[i][j][k]<<endl;
}
}
}
for(int i=0;i<=m;i++)
for(int j=0;j<=m;j++){
ans+=f[n][i][j];
ans%=mod;
// if(f[n][i][j]){
// cout<<n<<" "<<i<<" "<<j<<" "<<f[n][i][j]<<endl;
// }
}
printf("%d",ans);
return 0;
}
2.4RGB Sequence
考虑只要知道每种颜色最后一次出现的下标就可以知道每一段区间满足不满足标准。
我们也不需要每次都处理不合法的情况,只是需要在它更新别人时判断出来它是不合法的,或是让已经判断出的不合法的来更新它。根据归纳法容易得出,不存在其他情况。
状态设计
状态中需要记录每种颜色最后结尾位置 \(i,j,k\) ,而总长度 \(len=\max(i,j,k)\) ,所以状态为 \(f_{i,j,k}\) ,因为3种颜色并无差别,所以我们不妨设\(i\)是最靠右的颜色下标, \(k\) 是最靠左的颜色下标。
由此,考虑第 \(i+1\) 个是什么颜色,有:
上面三种情况分别对应填 \(i,k,j\) 所代表的颜色。注意,状态 \(i,j,k\) 所代表的颜色是不固定的。
因为不知道最开始 \(i\) 所代表的颜色是哪一种颜色,所以答案要乘 \(3\) ,注意不是乘 \(6\) ,因为最开始 \(i\) 所代表的颜色已经不重不漏的划分完了该样本空间。
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define ull unsigned long long
#define N 400
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
inline int Max(int a,int b){
return a>b?a:b;
}
struct rode{
int l,x;
rode() {}
rode(int l,int x) : l(l),x(x) {}
};
vector<rode> a[N];
int n,t,f[N][N][N],ans;
inline bool check(int q,int w,int c){
int r=q;
int num;
for(int i=0;i<a[r].size();i++){
num=1;int l=a[r][i].l,x=a[r][i].x;
if(l<=w) num++;
if(l<=c) num++;
if(num<x||num>x) return 0;
}
return 1;
}
signed main(){
n=read();t=read();
for(int i=1;i<=t;i++){
int l=read(),r=read(),x=read();
a[r].push_back(rode(l,x));
}
f[1][0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=i-1;j++){
for(int k=0;k<=Max(0,j-1);k++){
if(!check(i,j,k)) continue;
(f[i+1][j][k]+=f[i][j][k])%=mod;
(f[i+1][i][j]+=f[i][j][k])%=mod;
(f[i+1][i][k]+=f[i][j][k])%=mod;
if(i==n) (ans+=f[i][j][k])%=mod;
}
}
}
cout<<ans*3%mod;
return 0;
}
2.5[POI2015]MYJ
这道题是我做过的最难的一道区间 dp 。
2.5.1思路
看到 \(n\) 最大只有,而 \(m\) 有 \(4000\) ,我们想到这个题的时间复杂度应该是 \(O(n^3m)\) 左右的,毕竟这已经是极限。
进而发现一段区间内所花的钱数与这段区间的最小值有关,于是我们考虑把最小值放进状态里。考虑区间dp,我们枚举最小值的位置来进行转移。
2.5.2状态设计和转移方程式
注意到 \(m\) 只有 \(4000\) ,而 \(c_i\) 可能很大,所以我们首先对 \(c_i\) 离散化。
设状态 \(f_{i,j,k}\) 表示区间 \([i,j]\) (注意 \(i\) 可能等于 \(j\) ,这样写不是很符合规范,但不影响理解)中最小值为 \(k\) 时所有人花的钱的总和的最大值。
那么有下面的转移方程式:
其中,上面涉及的所有变量都是整数, \(m\) 代表的含义如题目所示, \(cost_{x,k}\) 表示把 \(x\) 赋值为最小值 \(k\) 所产生的花费。
什么意思呢?我们首先枚举这个最小值 \(k\) 出现的位置,那么我们就把原问题划分成了两个子问题,由于 \(k\) 是最小值,所以需要满足划分后两边的小区间的最小值要不小于 \(k\) 。
如何计算 \(cost_{x,k}\) 呢? 我们可以直接算出在 \([i,j]\) 中会有多少人在 \(x\) 这个位置花费,最后在再乘上 \(k\) 。前者我们考虑提前预处理出来,看一个数组存储,但是这样的话数组是四维的,太大开不下。所以我们不如动态预处理:在dp的过程中会先枚举 \(i\) 和 \(j\) ,之后我们开一个二维的数组,算出在区间 \([i,j]\) 中的 \(cnt_{x,k}\) (表示在该区间中把位置 \(x\) 赋值为 \(k\) 后有多少人回来这里花费),然后我们dp转移即可。
注意:
-
\(x\) 的取值范围为 \([i,j]\) 而非 \((i,j)\) 。
可能有些读者认为当 \(x\) 取值为 \(i\) 或 \(j\) 时,对应的 \(x-1\) 和 \(x+1\) 没有意义,但实际上这个时候 \(cost_{x,k}\) 是有意义的,我们不能漏考虑掉这种情况。且容易发现虽然 \(x-1,x+1\),所对应的 \(f\) 值有时不合法,但是它们不合法时的值都是 \(0\) ,所以不影响正确性。
-
在转移时我们记 \(g_{i,j,k}=f_{i,j,q},q\geq k\),在维护 \(f\) 的同时维护 \(g\) ,这样就可以做到 \(O(n)\) 的转移——我们只需要枚举断点 \(x\) 。
2.5.3打印解
这里我开了两个数组, \(pre_{i,j,k}\) 表示 \(f_{i,j,q'}=\max(f_{i,j,q},q\geq k)\) 取得最值时枚举的断点;而 \(co_{i,j,k}\) 表示 \(f_{i,j,q},q\geq k\) 取得最大值的颜色,即满足 \(q\geq k\) 的使 \(f_{i,j,q}\) 最大的 \(q\) 。
显然,这两个数组是可以顺带维护的,具体实现请看代码。
2.5.4代码
#define dd double
#define ld long double
#define ll long long
#define ull unsigned long long
#define N 52
#define M 4010
using namespace std;
const int INF=0x3f3f3f3f;
inline int Max(int a,int b){
return a>b?a:b;
}
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
template<typename T> inline void write(T x){
if(x<0) x=-x,putchar('-');
if(x>9) write(x/10);
putchar(x%10+'0');
}
int d[M],rk[M];
int n,m,a[M],b[M],c[M];
inline void intt(){
n=read();m=read();
for(int i=1;i<=m;i++) a[i]=read(),b[i]=read(),c[i]=read();
for(int i=1;i<=m;i++) d[i]=c[i];
sort(d+1,d+m+1);
int nm=unique(d+1,d+m+1)-d-1;
for(int i=1;i<=m;i++){
int nowc=lower_bound(d+1,d+nm+1,c[i])-d;
rk[nowc]=c[i];
c[i]=nowc;
}
}
int cost[N][M];
int f[N][N][M],g[N][N][M],point[N][N][M],co[N][N][M],lin[M];
inline int dp(){
int ans=-INF;
for(int i=1;i<=n;i++){
for(int j=1;j<=n-i+1;j++){
int l=j,r=j+i-1;
for(int k=1;k<=n;k++) for(int x=1;x<=m;x++) cost[k][x]=0;
// for(int k=l;k<=r;k++)
// for(int x=1;x<=m;x++)
// if(l<=a[x]&&b[x]<=r&&a[x]<=k&&k<=b[x]) cost[k][c[x]]++;
for(int k=1;k<=m;k++) if(l<=a[k]&&b[k]<=r) for(int x=a[k];x<=b[k];x++) cost[x][c[k]]++;
for(int k=l;k<=r;k++)
for(int x=m;x>=1;x--)
cost[k][x]+=cost[k][x+1];
for(int k=m;k>=1;k--){
for(int x=l;x<=r;x++){
if(f[l][r][k]<=g[l][x-1][k]+g[x+1][r][k]+cost[x][k]*rk[k]){
f[l][r][k]=g[l][x-1][k]+g[x+1][r][k]+cost[x][k]*rk[k];
lin[k]=x;
}
}
if(g[l][r][k+1]>f[l][r][k]){
g[l][r][k]=g[l][r][k+1];
point[l][r][k]=point[l][r][k+1];
co[l][r][k]=co[l][r][k+1];
}
else{
g[l][r][k]=f[l][r][k];
point[l][r][k]=lin[k];
co[l][r][k]=k;
}
if(i==n) ans=Max(ans,f[l][r][k]);
}
}
}
return ans;
}
int ans,color[N];
inline void print(int l,int r,int k){
int w=-1,col=co[l][r][k];
color[w=point[l][r][k]]=co[l][r][k];
if(w==0){
printf("NOOOOOOOOOOOOOOOOO\n");
return;
}
if(l<w) print(l,w-1,col);
if(w<r) print(w+1,r,col);
}
int main(){
intt();
ans=dp();
write(ans);putchar('\n');
print(1,n,1);
for(int i=1;i<=n;i++) write(rk[color[i]]),putchar(' ');
return 0;
}
代码中的注意事项:
-
\(lin_k\) 表示在区间 \([l,r]\) 中使 \(f_{l,r,k}\) 取得最值得断点。
-
我们利用了一个类似于前缀和的方式来计算 \(cut\) (在代码里是 \(cost\) ),这种方式不难理解,如果值 \(k\) 能够被计算,那么比 \(k\) 小的值也肯定能被计算。
-
注意在 \(61\) 行的不大于号和在第 \(66\) 行的大于号,这两个符号都不能轻易的改成小于号和不小于号,这是因为如果在 print 函数中出现 \(w\) 等于 \(0\) 的情况,那么你的这个程序就停不下来了,而以上两个符号确保了不会出现 \(0\) 。
为什么呢?实际上,出现 \(0\) 的原因是因为你在记录的时候遵循这样的一个原则:只要不是更优就不更新。但这里要遵循的原则确恰恰相反:只要达到最优值或是更优就更新。 \(61\) 行的符号保证了 \(lin\) 数组不为 \(0\) ,而 \(66\) 行的符号保证了相等时取 \(lin\) 的值。这样就防止了 \(0\) 的出现。
我已经在 print 函数中加入了防止程序停不下来的措施,读者可以自行更改这两个符号,看一看有什么不同。
-
注意到 \(52\) 到 \(54\) 行被我注释掉的部分,实际上这三行和第 \(55\) 行的作用是一样的,但是如果你用注释掉的部分替代第 \(55\) 行会超时,这提示我们在有判断语句时适当的调整循环顺序可以减少优化常数。
P2569
1 状态设计
很自然的一个状态: \(f_{i,j}\) 表示在第 \(i\) 天拥有 \(j\) 个股票所能得到的最大钱数。
有四个转移:
其中,第一个转移是从第 \(i\) 天开始买 \(j\) 个股票,实际上是第一次买股票,第二个转移是第 \(i\) 天什么也没有买,第三个转移是买股票,第四个转移是卖股票。
不难发现,朴素 dp 的时间复杂度是 \(O(t\times maxp^2)\)。
2 优化
因为许多题解都以第三个方程为例,我们以第四个方程为例,并解释为什么要从后往前枚举。
我们首先把后面那个东西拆开:
观察到我们的决策集合变化,\(j\) 每增加 \(1\),我们的决策集合就整体向右移一位(如果不考虑临界条件),不难发现这是一个滑动窗口,可以使用单调队列。
下面我们解释一下从右向左枚举的好处,并不是说从左向右枚举不行,如果我们从左向右枚举的话,第一个转移的决策集合大小是 \(bp_i\) ,也就是说我们需要提前进队 \(bp_i\) 次。但如果我们从右向左枚举的话,决策集合大小是从 \(0\) 开始的。我们可以不断的进队。可以降低编程复杂度。
3 代码
在转移时,我们需要加一步判断队列是否为空,维护转移的合法性。
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define ull unsigned long long
#define N 2010
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
inline int Max(int a,int b){
return a>b?a:b;
}
inline int Min(int a,int b){
return a<b?a:b;
}
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
template<typename T> inline void write(T x) {
if(x < 0) x=-x,putchar('-');
if(x > 9) write(x / 10);
putchar(x%10+'0');
}
template<typename T> inline void writeln(T x) {
write(x);
puts("");
}
int t,maxp,w,ap[N],bp[N],as[N],bs[N],f[N][N];
int q[N],l,r,ans=-INF;
int main(){
read(t);read(maxp);read(w);
for(int i=1;i<=t;i++){
read(ap[i]);read(bp[i]);
read(as[i]);read(bs[i]);
}
memset(f,-INF,sizeof(f));
for(int i=1;i<=t;i++){
for(int j=0;j<=as[i];j++) f[i][j]=-1*j*ap[i];
for(int j=0;j<=maxp;j++) f[i][j]=Max(f[i-1][j],f[i][j]);
if(i-w-1<=0) continue;
l=r=0;
for(int j=0;j<=maxp;j++){
int left=Max(0,j-as[i]),right=j-1;
while(l<r&&q[l+1]<left) l++;
if(l<r){
int k=q[l+1];
f[i][j]=Max(f[i][j],f[i-w-1][k]-(j-k)*ap[i]);
}
while(l<r&&f[i-w-1][q[r]]+q[r]*ap[i]<=f[i-w-1][j]+j*ap[i]) r--;
q[++r]=j;
}
l=r=0;
for(int j=maxp;j>=0;j--){
int left=j+1,right=Min(j+bs[i],maxp);
while(l<r&&q[l+1]>right) l++;
if(l<r){
int k=q[l+1];
f[i][j]=Max(f[i][j],f[i-w-1][k]+(k-j)*bp[i]);
}
while(l<r&&f[i-w-1][q[r]]+q[r]*bp[i]<=f[i-w-1][j]+j*bp[i]) r--;
q[++r]=j;
}
}
for(int j=0;j<=maxp;j++){
ans=Max(ans,f[t][j]);
}
write(ans);
return 0;
}