动态规划学习
动态规划学习
前言
以前我也算是接触过一点DP,陆陆续续学了一些背包问题,线性动规和区间动规。现在我再次发现了动规的重要性,决定在暑假里专门刷一些动规题。这篇blog主要记录我刷过的一些DP题。
引用 _皎月半撒花 大佬的一段话
动态规划自古以来是凌虐萌新的分水岭,但有些认为并没有这么重要——会打暴力,大不了记忆化。但是其实,动态规划学得好不好,可以彰显出一个的基本素养——能否富有逻辑地思考一些问题,以及更重要的——能否将数学、算筹学(决策学)、数据结构合并成一个整体并且将其合理运用。
推荐 (大佬的blog)
<什么是动态规划> —— by知乎大佬
<入门DP总结> ——by henry_y大佬
<DP专题练习> ——by henry_y大佬
<DP有哪些种类> —— by 范仁义
<树形DP入门6题> —— by seaupnice
正文 (这是我的qwq)
(PS:考完后一定记得来写题解)
[ 内容 ]
暂无
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 37 using namespace std; int n; int val[N],f[N][N],root[N][N]; void print(int l,int r) { if(l > r) return ; if(l == r) {printf("%d ",l); return ;} printf("%d ",root[l][r]); print(l,root[l][r]-1); print(root[l][r]+1,r); } int main() { scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",&val[i]) ,f[i][i] = val[i] ,f[i][i-1] = 1; for(int i=n;i>=1;--i) for(int j=i+1;j<=n;++j) for(int k=i;k<=j;++k) if(f[i][j] < f[i][k-1]*f[k+1][j]+f[k][k]) { f[i][j] = f[i][k-1]*f[k+1][j]+f[k][k]; root[i][j] = k; } printf("%d\n",f[1][n]); print(1,n); return 0; }
定义状态 :
是第 棵树
表示前面这棵树的前面这棵 是否比 前面这棵 高,高为1,矮为0
k表示前面这棵树种的多高的树
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 100007 #define M 5 #define Max(a,b,c,d) max(max(max(a,b),c),d) using namespace std; int n,ans; int t[N][M]; int f[N][M][M]; int main() { scanf("%d",&n); for(int i=1;i<=n;++i) for(int j=1;j<=3;++j) scanf("%d",&t[i][j]); for(int j=1;j<=3;++j) { for(int i=1;i<=3;++i) for(int k=0;k<=1;++k) f[1][k][i] = 0; f[1][1][j] = f[1][0][j] = t[1][j]; //种j for(int i=2;i<=n;++i) { f[i][0][2] = f[i-1][1][1] + t[i][2]; //前矮种2 ,前一位是1,再前高 f[i][0][3] = max(f[i-1][1][1],f[i-1][1][2]) + t[i][3]; //前矮种3 ,前一位是1或2,再前高 f[i][1][1] = max(f[i-1][0][2],f[i-1][0][3]) + t[i][1]; //前高种1 ,前一位是2或3,再前矮 f[i][1][2] = f[i-1][0][3] + t[i][2]; //前高种2 ,前一位是3,再前矮 } for(int i=1;i<j;++i) ans = max(ans,f[n][1][i]); for(int i=3;i>j;--i) ans = max(ans,f[n][0][i]); } printf("%d\n",ans); return 0; }
简单树形,只是把<最大子段和>放到树上来了而已。
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 16007 using namespace std; int n,cnt,ans; int val[N],f[N]; int head[N]; struct Edge { int next,to; }edge[N<<1]; void add(int u,int v) { edge[++cnt].next = head[u]; edge[cnt].to = v; head[u] = cnt; } void Dfs(int fa,int u) { // printf("Now u= %d\n",u); // f[u] = val[u]; for(int i=head[u];i;i=edge[i].next) { int v = edge[i].to; if(v != fa) { Dfs(u,v); f[u] = max(f[u],f[u]+f[v]); } } // printf("f = %d\n",f[u]); ans = max(ans,f[u]); return ; } int main() { scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",&val[i]) ,f[i] = val[i]; for(int i=1,u,v;i<=n-1;++i) scanf("%d%d",&u,&v) ,add(u,v) ,add(v,u); Dfs(0,1); printf("%d\n",ans); return 0; }
绿题难度疑似评高?
定义状态 : 表示及之前所以课题写了篇的最大值
状态转移方程 :
枚举k ; val = ai*k^bi
有点背包的味道。emm。
#include<algorithm> #include<iostream> #include<cstring> #include<cstdio> #include<cmath> #define N 207 #define int long long using namespace std; int n,m; int a[N],b[N]; int f[N][N]; signed main() { scanf("%lld%lld",&n,&m); for(int i=1;i<=m;++i) scanf("%lld%lld",&a[i],&b[i]); // printf("OK"); for(int i=1;i<=m;++i) for(int j=1;j<=n;++j) for(int k=0;k<=j;++k) { int val = a[i] * pow(k,b[i]); if(f[i][j]==0 || i==1) f[i][j] = f[i][j] = f[i-1][j-k] + val; else f[i][j] = min(f[i][j],f[i-1][j-k]+val); } printf("%lld\n",f[m][n]); return 0; }
DP大水题
定义状态 是时刻疲劳度最大跑步米数。
但是直接做我WA了,因此使用刷表法。
刷表法做的题不多,但他也是DP中的一种重要方法。刷表法是通过已知得到未知,顾名思义,就是通过已知数据把空白表填满。
就例如我现在有1万元,我还能工作10年,如果
每年可以赚2万,我可以通过刷表预先得知10年后我有21万元。但一般的动规是,我去年有10万,今年能赚5万,所以我今年有15万。具体的不同还是要靠自己慢慢体会。
边界
状态转移 (有max就对后面的数取max的意思,都看得懂吧)
已经是,只能休息
不是,选择休息
选择跑步
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 20007 using namespace std; int n,m; int d[N]; int f[N][N]; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) scanf("%d",&d[i]); f[1][1] = d[1]; for(int i=1;i<=n;++i) for(int j=0;j<=min(i,m);++j) //防止数组越界 { if(j==0) f[i][0] = max(f[i][0],f[i-1][0]); else f[i+j][0] = max(f[i+j][0],f[i][j]); //休息 刷上去 f[i+1][j+1] = max(f[i+1][j+1],f[i][j]+d[i+1]); //跑步 刷上去 } printf("%d\n",f[n][0]); return 0; }
这道题又综合了<最大子段和>的思想。
题意有点难懂,首先翻译一下题意:
先把个小朋友抽象为个点,每个点上有3个数值。
手牌值:题目会输入进来,这里用表示
特征值:这个点前面(包括这个点)连续若干个(最少有一个)点的手牌值之和的最大值。实际上就是最大子段和。这里用表示。
分数值(用表示):这个点 前面(不包含) 任意一个点(设为点) 使得+ 的值最大,+的最大值就是 点分数。
总结一下
从题目中输入
最大子段和
**由我们来**
最后的答案是要中最大的那个数,我们可以通过**边,边取得出**。
先来看一下怎么求(一开始我DP方程就推对了,就是因为这个求错坑了两天)
for(int i=1;i<=n;++i) { b[i] = max(a[i],a[i]+b[i-1]); mx = max(mx,b[i]); t[i] = mx; }
定义状态 是 的最大分数。
那么 一定是 前最大
我们还没试过
所以动态转移方程为
预处理
一定要预处理!
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 1000007 #define INF 0x7fffffff #define int long long using namespace std; int n,p,maxn; int a[N],b[N],t[N],f[N]; signed main() { scanf("%lld%lld",&n,&p); for(int i=1;i<=n;++i) scanf("%lld",&a[i]); int mx = -INF; for(int i=1;i<=n;++i) { b[i] = max(a[i],a[i]+b[i-1]); mx = max(mx,b[i]); t[i] = mx; } f[1] = t[1] ,f[2] = f[1] + t[1]; //预处理f[1],f[2] maxn = max(f[1],f[2]); //预处理maxn bool flag = 0; for(int i=3;i<=n;++i) { if(f[i-1]+t[i-1]<0 && f[i-1]>0) flag = 1; //爆longlong了, if(flag) f[i] = f[i-1] % p + t[i-1] % p; //特殊处理,中间取模 else f[i] = max(f[i-1],f[i-1]+t[i-1]); //没有爆就正常操作 maxn = max(maxn,f[i]) % p; //不影响取maxn } printf("%lld\n",maxn % p); return 0; }
是一道好题,思路新奇。
定义状态: 表示以作为结尾的矩阵中最大的正方形边长
如果是 ,那么动态转移方程为
你可能会问:为什么是这样?不要判断的上、左方是否为1吗?
其实你只要画图想想,如果上方不为,那么 ,其他同理。**只要的左上哪里最小,他就只能继承哪里**。
那么这个有什么用呢?
我们每知道一个矩阵里最大的正方形是多少,就把 ,记录下来。但我只记录最大的,不过大的里面包含小的。例如我记录了的最大正方形是,那么其实这里面肯定有大小为4,3,2的正方形。最后我们要用叠加的方式加入,和。所以,同理
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 257 using namespace std; int n,m; int map[N][N],f[N][N]; int num[N]; int main() { scanf("%d",&n); for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) scanf("%1d",&map[i][j]); //控制读入一个数字 for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) if(map[i][j] == 1) { f[i][j] = min(min(f[i-1][j],f[i][j-1]),f[i-1][j-1]) + 1; num[f[i][j]]++; } for(int i=n;i>=2;--i) num[i] += num[i+1]; for(int i=2;i<=n;++i) if(num[i]) printf("%d %d\n",i,num[i]); return 0; }
区间DP题
状态很好推,只有选右和选左两种情况
定义状态 :为选段的最大得分
玩家1的得分肯定是
因为是一轮一轮来,如果玩家1的选左边,玩家2的得分就是,所以:
如果玩家1 选了左边,总得分就是:
如果玩家1 选了右边,总得分就是:
动态转移方程:
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 207 using namespace std; int n; int a[N],sum[N]; int f[N][N]; int main() { scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",&a[i]) ,sum[i] = sum[i-1] + a[i] ,f[i][i] = a[i]; //记得初始化 // 倒序枚举才能使区间从小到大 for(int i=n;i>=1;--i) for(int j=i+1;j<=n;++j) f[i][j] = max(sum[j]-sum[i-1]-f[i+1][j],sum[j]-sum[i-1]-f[i][j-1]); // 与之前这个动态转移方程是等价的 printf("%d %d\n",f[1][n],sum[n]-f[1][n]); return 0; }
二维费用背包DP
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 27 #define max(a,b,c) max(max(a,b),c) using namespace std; int n,T,M; int a[N]; int f[N][N]; int main() { scanf("%d%d%d",&n,&T,&M); for(int i=1;i<=n;++i) scanf("%d",&a[i]); for(int k=1;k<=n;++k) for(int i=M;i>=1;--i) for(int j=T;j>=a[k];--j) f[i][j] = max(f[i][j],f[i-1][T]+1,f[i][j-a[k]]+1); printf("%d\n",f[M][T]); return 0; }
又是一道背包
#include<algorithm> #include<iostream> #include<cstring> #include<cstdio> #include<cmath> #define N 10007 #define MAXN 1007 using namespace std; int n,Len,B; int f[MAXN][MAXN]; struct Node { int l,L,r,F,C; bool operator < (const Node &a) { return l < a.l; } }a[N]; int main() { scanf("%d%d%d",&Len,&n,&B); for(int i=1;i<=n;++i) scanf("%d%d%d%d",&a[i].l,&a[i].L,&a[i].F,&a[i].C) ,a[i].r = a[i].l + a[i].L; sort(a+1,a+1+n); memset(f,-1,sizeof(f)); f[0][0] = 0; for(int i=1;i<=n;++i) for(int j=0;j<=B-a[i].C;++j) if(f[a[i].l][j] != -1) f[a[i].r][j+a[i].C] = max(f[a[i].r][j+a[i].C],f[a[i].l][j]+a[i].F); int ans = -1; for(int i=0;i<=B;++i) ans = max(ans,f[Len][i]); printf("%d\n",ans); return 0; }
第一道蓝题DP,方程一遍推对,好开心!
其实只要区间DP每行就好了,更矩阵没有多大关系。跟<P2734 游戏 A Game> 这个题目有点像,(我之前写了笔记)。
代码没打高精,只能拿60pts。
#include<algorithm> #include<iostream> #include<cstring> #include<cstdio> #include<cmath> #define N 107 #define int long long using namespace std; int n,m,ans; int a[N][N],f[N][N]; signed main() { scanf("%lld%lld",&n,&m); for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) scanf("%lld",&a[i][j]); for(int line=1;line<=n;++line) { memset(f,sizeof(f),0); for(int i=m;i>=1;--i) for(int j=i;j<=m;++j) { int k = m - (j-i); // printf("i=%d,j=%d,k=%d\n",i,j,k); if(i==j) f[i][j] = a[line][i]*pow(2,k); else f[i][j] = max(f[i+1][j]+a[line][i]*pow(2,k),f[i][j-1]+a[line][j]*pow(2,k)); // printf("### f[%d][%d] = %d\n",i,j,f[i][j]); } // printf("f = %d\n",f[1][m]); ans += f[1][m]; } printf("%lld\n",ans); return 0; }
简单树形DP,只有这个点来和不来两种状态,方程应该很好推了吧。QwQ
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 6007 using namespace std; int n,cnt; int a[N],head[N]; int f[N][2]; struct Edge { int next,to; }edge[N<<1]; void add(int u,int v) { edge[++cnt].next = head[u]; edge[cnt].to = v; head[u] = cnt; } void Dfs(int u,int fa) { f[u][1] = a[u]; for(int i=head[u];i;i=edge[i].next) { int v = edge[i].to; if(v != fa) { Dfs(v,u); f[u][1] = max(f[u][1],f[u][1]+f[v][0]); f[u][0] = max(f[u][0],(f[u][0]+=max(f[v][1],f[v][0]))); } } } int main() { scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",&a[i]); for(int i=1,u,v;i<=n-1;++i) scanf("%d%d",&u,&v) ,add(u,v) ,add(v,u); Dfs(1,0); printf("%d\n",max(f[1][1],f[1][0])); return 0; }
把也算成一个节点,就变成一颗树了,考虑树形DP
定义状态 是 **第个点为根的子树 ,选门**课程最大学分
动态转移方程
是儿子节点,意思是我选k门儿子的课程获得最大值
初始化每个点是自己的分值
注意枚举范围,每个点必须先选自己,不然选不了儿子
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 307 using namespace std; int n,m,cnt; int s[N],head[N]; int f[N][N]; struct Edge { int next,to; }edge[N<<1]; void add(int u,int v) { edge[++cnt].next = head[u]; edge[cnt].to = v; head[u] = cnt; } void Dfs(int u,int fa) { // printf("u=%d \n",u); for(int i=1;i<=m;++i) f[u][i] = s[u]; for(int i=head[u];i;i=edge[i].next) { int v = edge[i].to; if(v != fa) { Dfs(v,u); if(u!=0) for(int j=m;j>=1;--j) for(int k=1;k<j;++k) f[u][j] = max(f[u][j],f[u][j-k]+f[v][k]); // printf("f[%d][m] = %d\n",u,f[u][m]); } } // printf("u=%d \n",u); } int main() { scanf("%d%d",&n,&m); for(int i=1,v;i<=n;++i) scanf("%d%d",&v,&s[i]) ,add(i,v) ,add(v,i); Dfs(0,0); for(int i=head[0];i;i=edge[i].next) { int v = edge[i].to; for(int j=m;j>=1;--j) for(int k=1;k<=j;++k) f[0][j] = max(f[0][j],f[0][j-k]+f[v][k]); } printf("%d\n",f[0][m]); return 0; }
简单题,自己看吧。
#include<algorithm> #include<iostream> #include<cstring> #include<cstdio> #include<cmath> #define N 107 #define INF 0x3f3f3f using namespace std; int n,m,ans,I; int pre[N][N]; int a[N][N],f[N][N]; void print(int i,int j) { if(i > 1) print(i-1,pre[i][j]); printf("%d ",j); } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) scanf("%d",&a[i][j]); for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) { f[i][j] = -INF; for(int k=i-1;k<j;++k) if(f[i][j] < f[i-1][k]+a[i][j]) { f[i][j] = f[i-1][k]+a[i][j]; pre[i][j] = k; } } for(int i=n;i<=m;++i) if(ans < f[n][i]) ans = f[n][i] ,I = i; printf("%d\n",ans); print(n,I); return 0; }
发现还是更一下题解好一点。
看到 <=5 考虑直接五维DP。
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 107 using namespace std; int s,b,cnt; int id[1007]; int T[6],V[6],P[N],K[N][6]; int f[6][6][6][6][6]; int main() { int n,c,k,p; scanf("%d",&s); for(int i=1;i<=s;++i) { scanf("%d",&n); for(int j=1;j<=n;++j) { scanf("%d%d",&c,&k); if(!id[c]) id[c] = ++cnt; K[i][id[c]] = k; } scanf("%d",&p); P[i] = p; } scanf("%d",&b); for(int i=1;i<=b;++i) { scanf("%d%d%d",&c,&k,&p); if(!id[c]) id[c] = ++cnt; T[id[c]] = k; V[id[c]] = p; } for(int i=0;i<=T[1];++i) for(int j=0;j<=T[2];++j) for(int k=0;k<=T[3];++k) for(int l=0;l<=T[4];++l) for(int m=0;m<=T[5];++m) { int &t = f[i][j][k][l][m] = i*V[1] + j*V[2] + k*V[3] + l*V[4] + m*V[5]; for(int d=1;d<=s;++d) if(i-K[d][1]>=0 && j-K[d][2]>=0 && k-K[d][3]>=0 && l-K[d][4]>=0 && m-K[d][5]>=0) t = min(t,f[i-K[d][1]][j-K[d][2]][k-K[d][3]][l-K[d][4]][m-K[d][5]]+P[d]); } printf("%d\n",f[T[1]][T[2]][T[3]][T[4]][T[5]]); return 0; }
对于雪坡是没有必要DP的,我们需要设立一个数组来储存i能力值滑雪一次的最小时间
我们需要对每一节课DP,因为每一节课分为上和不上两种状态,两节课之间滑雪几次我们可以将两节课之间的时间除以ti[k] (k为能力值)
那么定义状态: 为 时刻 能力值 的最大滑雪次数
状态转移方程:
学习这节课,学习完课后的能力值为k,上一节课的结束为l ,这一节课的开始为s,看一下能课程与课程中间能滑雪多少次,对哪个状态发生了改变(
方程自己想)。不学习这节课,上一节课的能力为k,上一节课的结束为l,这一节课的结束为s,(
同上)。
自己想吧!
#include<algorithm> #include<iostream> #include<cstring> #include<cstdio> #include<cmath> using namespace std; int T,n,m; //有T时间 n个课程 m个坡 int ti[107]; //有i能力滑雪的最小时间 int f[10007][107]; struct Node { int St,End,capa; //capacity bool operator < (const Node &a) { return End < a.End; } }les[107]; int main() { memset(ti,127,sizeof(ti)); scanf("%d%d%d",&T,&n,&m); for(int i=1,st,Len,ai;i<=n;++i) { scanf("%d%d%d",&st,&Len,&ai); les[i] = (Node){st,st+Len,ai}; } for(int i=1,di,ci;i<=m;++i) { scanf("%d%d",&ci,&di); for(int j=ci;j<=100;++j) ti[j] = min(ti[j],di); } sort(les+1,les+1+n); les[0] = (Node){0,0,1}; /* for(int i=0;i<=n;++i,putchar('\n')) printf("test : s=%d E=%d capacity=%d",les[i].St,les[i].End,les[i].capa); for(int i=1;i<=T;++i) printf("test : ti[%d]=%d\n",i,ti[i]); */ for(int i=0;i<=n;++i) for(int j=0;j<i;++j) { int x = les[j].End ,y = les[i].St ,y1 = les[i].End; int k = les[j].capa ,k1 = les[i].capa; f[y1][k] = max(f[y1][k],f[x][k]+((y1-x)/ti[k])); if(x < y) f[y1][k1] = max(f[y1][k1],f[x][k]+((y-x)/ti[k])); } int ans = -1; for(int i=0;i<=n;++i) { int x = les[i].End ,k = les[i].capa; f[T][k] = max(f[T][k],f[x][k]+((T-x)/ti[k])); ans = max(ans,f[T][k]); } printf("%d\n",ans); return 0; }
这是一道好题,不过有点困不想讲思路了,给大家看一下别人的吧。
(注:以下均为转载)
这里提供一种新的思路:
状态:f[i][j][k][m] : 前i个物品 当前手中有j个"A" k个"B" m个"C"时的最小卸货次数
很明显对于第i个物品可以
1:)只取出来 暂时不装进去 前提就是当前手中货物数量<10
2:)取出来后 装进去 没有前提
所以转移方程就出来了:
f[i][j][k][m] = f[i-1][j-1][k][m] if(ob[i]=='A' && j)
f[i][j][k][m] = f[i-1][j][k-1][m] if(ob[i]=='B' && k)
f[i][j][k][m] = f[i-1][j][k][m-1] if(ob[i]=='C' && m)
以上三个均是 只取出来
f[i][0][k][m] = f[i][j][k][m] + 1 ;
f[i][j][0][m] = f[i][j][k][m] + 1 ;
f[i][j][k][0] = f[i][j][k][m] + 1 ;
这三个就是卸货啦qwq 还有一个大前提:j+k+m<=10
初值:f[0][0][0][0]=0 ; 其他均为+oo
目标:f[n][0][0][0]
#include<algorithm> #include<iostream> #include<cstring> #include<cstdio> #include<cmath> #define N 107 using namespace std; int n; int f[N][11][11][11]; char s[N]; int main() { memset(f,127,sizeof(f)); f[0][0][0][0] = 0; scanf("%d",&n); for(int i=1;i<=n;++i) cin >> s[i]; for(int i=1;i<=n;++i) for(int a=0;a<=10;++a) for(int b=0;b<=10;++b) for(int c=0;c<=10;++c) { if(a+b+c > 10) continue; if(s[i] == 'A' && a) f[i][a][b][c] = f[i-1][a-1][b][c]; if(s[i] == 'B' && b) f[i][a][b][c] = f[i-1][a][b-1][c]; if(s[i] == 'C' && c) f[i][a][b][c] = f[i-1][a][b][c-1]; f[i][0][b][c] = min(f[i][0][b][c],f[i][a][b][c]+1); f[i][a][0][c] = min(f[i][a][0][c],f[i][a][b][c]+1); f[i][a][b][0] = min(f[i][a][b][0],f[i][a][b][c]+1); } printf("%d\n",f[n][0][0][0]); return 0; }
是一道状压DP的好题。
// 思想很巧妙!设f[i][j] 为i状态压缩,j奶牛结尾。
// 每个序列不定奶牛结尾,只要满足差值就可以由上一个奶牛转移过来
太困不想写了。
// 思想很巧妙!设f[i][j] 为i状态压缩,j奶牛结尾。 // 每个序列不定奶牛结尾,只要满足差值就可以由上一个奶牛转移过来 #include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 20 #define MAXN 67000 #define int long long using namespace std; int n,K,maxn,ans; int s[N]; int f[MAXN][N]; bool check(int x,int y) { return abs(s[x]-s[y]) > K; } signed main() { scanf("%lld%lld",&n,&K); for(int i=1;i<=n;++i) scanf("%lld",&s[i]); maxn = (1<<n) - 1; for(int i=1;i<=n;++i) f[(1<<(i-1))][i] = 1; for(int i=1;i<=maxn;++i) for(int j=1;j<=n;++j) { if(f[i][j]) continue; //避免覆盖预处理 if(!(i & (1<<(j-1)))) continue; int state = i ^ (1<<(j-1)); for(int l=1;l<=n;++l) { if(l == j) continue; if(check(j,l)) f[i][j] += f[state][l]; } } for(int i=1;i<=n;++i) ans += f[maxn][i]; printf("%lld\n",ans); return 0; }
这道题是ygt大佬推给我的
方程一遍推对好开心!
题意:给出一段环状序列,即认为和是相邻的,选出其中连续不重叠且非空的K段使得这K段和最大。
环形分段最大子段和
定义状态: 表示 i位置 分j段 当前位置选不选 的最大和
转移方程:
环形处理:两次DP,一次不连接,一次强制选 1,n.
#include<algorithm> #include<iostream> #include<cstring> #include<cstdio> #include<cmath> #define Max(a,b,c) max(max(a,b),c) using namespace std; const int N = 2e5+7; int n,K=2,ans; int a[N]; int f[N][57][2]; //f[i][j][val]表示 i位置 分j段 当前选不选 int main() { scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",&a[i]); memset(f,128,sizeof(f)); //初始极小化 f[0][0][0] = f[0][0][1] = 0; //不强制选1,n for(int i=1;i<=n;++i) for(int j=0;j<=K;++j) { if(j>0) f[i][j][1] = Max(f[i-1][j][1],f[i-1][j-1][1],f[i-1][j-1][0])+a[i]; f[i][j][0] = max(f[i-1][j][1],f[i-1][j][0]); } ans = max(f[n][K][1],f[n][K][0]); memset(f,128,sizeof(f)); f[1][0][1] = a[1]; //选1,且不能单独列为一段,强制与n结合 for(int i=2;i<=n;++i) for(int j=0;j<=K;++j) { if(j>0) f[i][j][1] = max(f[i-1][j-1][1],f[i-1][j-1][0])+a[i]; f[i][j][1] = max(f[i][j][1],f[i-1][j][1]+a[i]); f[i][j][0] = max(f[i-1][j][0],f[i-1][j][1]); } ans = max(ans,f[n][K][1]); printf("%d\n",ans); return 0; }
是一道好题目,求最长下降子序列和方案总数
显然不是最长下降子序列那么简单
看到 n<=5000
的数据范围可以考虑 O(n2)
的DP求最长下降子序列,因为这样我们可以使用一些DP过程中的可利用条件
设为以 最长下降子序列的长度 ,为以 结尾且长度为下降子序列方案总数
思考:
我们求的时候是每一次从前面的状态转移,如果 是由 转移而来 那么是不是可以由转移过来呢? 可以,即 if(f[i]==f[j]+1&&a[i]<a[j]) t[i]+=t[j] (j<i)
当一个最长下降子序列长1时,也就是说只包含他自己,方案为数也为1 ,即 if(f[i]==1) t[i] = 1;
考虑两个子序列相同,如序列:
编号 01 02 03 04 05 06 07
[ ] 68 69 98 74 64 68 64
3,4,5 和 3,4,7 组成的序列一样,编号却不同,如何避免重复计算呢?
例如在求7这里 ,我们可以把 t[5] = 0;
方案数清空,删除这个序列。t[7]又能从t[4]这里继承方案数 即 if(f[i]==f[j]&&a[i]==a[j]) t[j]=0; (j<i)
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 5007 using namespace std; int n,maxx,sum; int a[N],f[N],t[N]; //t[i]存以i结尾的最长下降子序列的方案 int main() { scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",&a[i]); for(int i=1;i<=n;++i) { f[i] = 1; for(int j=1;j<i;++j) if(a[i]<a[j]) f[i] = max(f[i],f[j]+1); if(f[i]==1) t[i] = 1; //自己也算一种方案 maxx = max(maxx,f[i]); for(int j=1;j<i;++j) { if(f[i]==f[j] && a[i]==a[j]) //相当于同一种序列 t[j] = 0; //前一个序列没有了 , 这一个方案又可以用相同的方式加回来 else if(f[i]==f[j]+1 && a[i]<a[j]) t[i] += t[j]; //可以从上一个状态加上来 } } for(int i=1;i<=n;++i) if(f[i]==maxx) sum += t[i]; //加最长的方案 printf("%d %d\n",maxx,sum); return 0; }
一次交换即j变z ,z变j
定义f[i][a][b] 表示i位置把a个j变z b个z变j
#include<algorithm> #include<iostream> #include<cstring> #include<cstdio> #include<cmath> #define N 507 #define M 107 #define INF 0x3f3f3f using namespace std; int n,K; int f[N][M][M]; //有a个j变z ,b个z变j char s[N]; int main() { scanf("%d%d",&n,&K); cin>>s+1; memset(f,128,sizeof(f)); //赋极小值,取max f[0][0][0] = f[1][0][0] = f[1][s[1]=='j'][s[1]=='z'] = 0; //显然1不变为0 ,变也为0 for(int i=2;i<=n;++i) { for(int a=0;a<=K;++a) for(int b=0;b<=K;++b) { f[i][a][b] = f[i-1][a][b]; if(s[i-1]=='j' && s[i]=='z') f[i][a][b] = max(f[i][a][b],f[i-2][a][b]+1); if(a && s[i]=='j' && s[i-1]=='j') f[i][a][b] = max(f[i][a][b],f[i-2][a-1][b]+1); if(b && s[i]=='z' && s[i-1]=='z') f[i][a][b] = max(f[i][a][b],f[i-2][a][b-1]+1); if(a && b && s[i-1]=='z' && s[i]=='j') f[i][a][b] = max(f[i][a][b],f[i-2][a-1][b-1]+1); } } int ans = -INF; for(int i=0;i<=K;++i) ans = max(ans,f[n][i][i]); printf("%d\n",ans); return 0; }
悬线法
#include<algorithm> #include<iostream> #include<cstdio> #include<cmath> #define N 2007 using namespace std; int n,m,ans1,ans2; int le[N][N],ri[N][N],up[N][N]; bool map[N][N]; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) { scanf("%d",&map[i][j]); le[i][j] = ri[i][j] = j; up[i][j] = 1; } for(int i=1;i<=n;++i) for(int j=2;j<=m;++j) { if(map[i][j-1] != map[i][j]) le[i][j] = le[i][j-1]; //Ïò×óÑÓ³¤ } for(int i=1;i<=n;++i) for(int j=m-1;j>=1;--j) { if(map[i][j+1] != map[i][j]) ri[i][j] = ri[i][j+1]; } for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) { if(i>1 && map[i-1][j]!=map[i][j]) { up[i][j] = up[i-1][j] + 1; le[i][j] = max(le[i-1][j],le[i][j]); ri[i][j] = min(ri[i-1][j],ri[i][j]); } int a = ri[i][j] - le[i][j] + 1; int b = up[i][j]; int c = min(a,b); ans1 = max(ans1,c*c); ans2 = max(ans2,a*b); } printf("%d\n%d\n",ans1,ans2); return 0; }