线性dp
线性dp应该是dp中比较简单的一类,不过也有难的。(矩乘优化递推请出门右转)
线性dp一般是用前面的状态去推后面的,也有用后面往前面推的,这时候把循环顺序倒一倒就行了。如果有的题又要从前往后推又要从后往前推...那它还叫线性dp吗?
传球游戏:https://www.luogu.org/problemnew/show/P1057
题意概述:一些人围成一个圈,每次可以把球传给左右两个人,求m步后回到第一个人手里的方案数。
这题大概也可以矩乘?不过递推就可以了,$dp[i][j]$表示传了j步,现在在i手里的方案数。转移:
$dp[i][j]=dp[i+1][j-1]+dp[i-1][j-1]$
边界再处理一下就行了。
# include <cstdio> # include <iostream> using namespace std; int n,m; long long dp[31][31]; int main() { scanf("%d%d",&n,&m); dp[1][0]=1; for (int i=1;i<=m;i++) { dp[1][i]=dp[2][i-1]+dp[n][i-1]; dp[n][i]=dp[n-1][i-1]+dp[1][i-1]; for (int j=2;j<n;j++) dp[j][i]=dp[j-1][i-1]+dp[j+1][i-1]; } printf("%d",dp[1][m]); return 0; }
最长(上升/下降/不上升/不下降)子序列类
题意概述:不用概述吧。
写$blog$是为了备忘,但是我觉得$n^{2}$做法不可能忘,于是就不写了吧!
$NlogN$的做法有两种,一种是用树状数组,一种是二分,就按照最长上升来讲,其他的也差不多啦;
树状数组:一开始很难理解,后来看了一个很不错的课件才明白的,其实也不难。将数字的大小作为树状数组的下标,每次从0到$a_{i}-1$取最大值加一即为以$a_{i}$结尾的最长上升序列长度。
这里还有一个小问题:如果$a_{i}$比较大,时间复杂度就比较低了,可以考虑离散化一下,就又回到了$NlogN$。
二分:用$d[i]$保存以长度为$i$的上升子序列中最小的一个结尾,这样构造下去,$d$数组是单调上升的,可以二分一下看看目前的这个数应该插入到哪里或者是直接扔掉,可以很容易的保证复杂度(因为最长上升子序列的长度不可能超过$n$)。
看一个用二分写的题吧(树状数组也差不多...)
导弹拦截:https://www.luogu.org/problemnew/show/P1020
# include <cstdio> # include <iostream> # include <cstring> # define R register int using namespace std; int a[100009],d[100009]; int n=0,j,len=1,l,r,mid; int sfind(int x) { l=1; r=len; while (l<r) { mid=(l+r)>>1; if(d[mid]>=x) l=mid+1; else r=mid; } return l; } int zfind(int x) { l=0; r=len; while (l<r) { mid=(l+r)>>1; if(d[mid]>=x) r=mid; else l=mid+1; } return l; } int main() { while(scanf("%d",&a[++n])==1); --n; for (R i=1;i<=n;i++) { if(a[i]<=d[len]) d[++len]=a[i]; else { j=sfind(a[i]); d[j]=a[i]; } } printf("%d\n",len); len=0; memset(d,-1,sizeof(d)); for(R i=1;i<=n;i++) { if(a[i]>d[len]) d[++len]=a[i]; else { j=zfind(a[i]); d[j]=a[i]; } } printf("%d",len); return 0; }
首先就是求一个最长不上升子序列,然后第二问可以通过一些奇妙的性质(Dilworth定理)转换为求最长上升子序列。这个定理好像很麻烦的样子...等我学到偏序再回来补这个。
Milky-way学姐的测试:
转换一下发现就是求最长下降子序列的长度。
# include <cstdio> # include <iostream> # include <cstring> using namespace std; const int maxn=100001; int mid,l,r,j,T,n,k,ans,len; long long a[maxn],d[maxn]; int Find(int x) { l=0; r=len; while (l<r) { mid=(l+r)>>1; if(d[mid]>x) l=mid+1; else r=mid; } return l; } int main() { scanf("%d",&T); while (T) { scanf("%d%d",&n,&k); for (int i=1;i<=n;i++) scanf("%lld",&a[i]); memset(d,-1000000001,sizeof(d)); len=1; for (int i=1;i<=n;i++) { if(a[i]<d[len]) d[++len]=a[i]; else { j=Find(a[i]); d[j]=a[i]; } } if(len>=k) printf("No\n"); else printf("Yes\n"); T--; } return 0; } D
友好城市:https://www.luogu.org/problemnew/show/P2782
转换一下又是最长不下降子序列。
木棍加工:https://www.luogu.org/problemnew/show/P1233
题意概述:有n根木棍需要加工,如果某个木棍比另一个木棍短,且比他窄,两者连着加工就只算一份时间,求最少时间。
多么亲切而又熟悉的题目啊!首先可以按照长短(或者是宽窄)排个序,就从根本上解决了一半的问题,再求一个最长上升子序列就是答案咯。
# include <cstdio> # include <iostream> # include <algorithm> # define R register int using namespace std; struct woo { int l,w; }a[5005]; int ans=0,n,rx,rf,dp[5005]; char rc; bool cmp(woo a,woo b) { return a.l>b.l; } int read() { rx=0,rf=1,rc=getchar(); while (!isdigit(rc)) { if(rc=='-') rf=-rf; rc=getchar(); } while (isdigit(rc)) { rx=(rx<<3)+(rx<<1)+(rc^48); rc=getchar(); } return rx*rf; } int main() { n=read(); for (R i=1;i<=n;i++) a[i].l=read(),a[i].w=read(); sort(a+1,a+1+n,cmp); for (R i=1;i<=n;i++) { dp[i]=1; for (R j=1;j<=i;j++) if(a[i].w>a[j].w) dp[i]=max(dp[i],dp[j]+1); ans=max(ans,dp[i]); } printf("%d",ans); return 0; }
护卫队:https://www.luogu.org/problemnew/show/P1594
题意概述:一队车队过桥,每辆车有其速度和质量,每次可以选择一组连续的,总重不超过限制的车一起过桥,速度以最低速度为准,求最短的总时间。
一开始想的有点复杂$f[i][j]$表示前i个分j组的最短时间,后来发现对分组数并没有什么限制,所以直接扔掉第2维就好啦。在计算$f[i]$的时候,可以枚举j,只要i到j的总重不超过限重,就用来更新一下。这道题的总重要用前缀和优化,最小值可以用st表。还有$f[i]$的初始最大值可能会设置的不够大,这时候可以用$-1$表示,如果是$-1$就强制更新。
# include <cstdio> # include <iostream> # include <cmath> # define R register int using namespace std; const int maxn=1005; int n; long long mw,l,w[1005],S[1005],dp[1005][12]; double f[1005]; long long mi(int l,int r) { if(l>r) swap(l,r); int k=log2(r-l+1); return min(dp[l][k],dp[r-(1<<k)+1][k]); } int main() { scanf("%lld%lld%d",&mw,&l,&n); for (R i=1;i<=n;++i) { scanf("%lld%lld",&w[i],&dp[i][0]); S[i]=S[i-1]+w[i]; } for (R j=1;(1<<j)<=n;++j) for (R i=1;i+(1<<j)-1<=n;++i) dp[i][j]=min(dp[i][j-1],dp[i+(1<<(j-1))][j-1]); for (R i=1;i<=n;i++) f[i]=9223372036854775807ll; f[0]=0; for (R i=1;i<=n;++i) { for (int j=i;j>=1;j--) { if(S[i]-S[j-1]>mw) break; f[i]=min(f[i],f[j-1]+(double)l/mi(i,j)); } } printf("%.1lf",f[n]*60); return 0; }
山区建小学:http://noi.openjudge.cn/ch0206/7624/
还没做...
低价购买:https://www.luogu.org/problemnew/show/P1108
题意概述:最长下降子序列统计方案数,要求不能出现重复的序列。
如果没有后半句要求,这道题就非常简单了,加上这个要求其实也并没有难很多。
如果两个子序列是重复的,那么必然每个元素都是一一对应的。如果从前往后递推就可以不用考虑前面更多个元素,只要保证最后一个元素不相同就必然不会重复了。为了好处理,我们只考虑除了最后一位以外的去重,输出时再判断最后一位。注意一下这个递推式 $$f_i=\begin{Bmatrix}
f_j(dp_j+1>dp_i)\\f_i+f_j (dp_j+1==dp_i)
\end{Bmatrix}$$
所以说只要用来更新答案的j的a[j]两两不相同就一定保证不会重复,那会不会少呢?其实是会的,因为同一个元素结尾的子序列长度不一定相同啊。但是我们只关心最大值,而且靠后的元素能形成的子序列一定包含了所有前面的相同元素所能构成的子序列,所以倒着枚举,对于每个a[j]只取一次值。那怎么看每个a[j]是否用来更新过呢?用$set$...所以说这就成了一个特别高端的算法!红黑树优化dp!
1 // luogu-judger-enable-o2 2 # include <cstdio> 3 # include <iostream> 4 # include <set> 5 6 using namespace std; 7 8 int n; 9 int a[5005]; 10 int dp[5005]; 11 long long f[5005]; 12 set <int> s; 13 14 int main() 15 { 16 scanf("%d",&n); 17 for (int i=1;i<=n;++i) 18 scanf("%d",&a[i]); 19 for (int i=1;i<=n;++i) 20 { 21 dp[i]=1,f[i]=1; 22 s.clear(); 23 for (int j=i-1;j>=1;--j) 24 { 25 if(a[j]<=a[i]) continue; 26 if(dp[j]+1>dp[i]) 27 { 28 s.clear(); 29 dp[i]=dp[j]+1; 30 f[i]=f[j]; 31 s.insert(a[j]); 32 } 33 else 34 { 35 if(dp[j]+1==dp[i]) 36 { 37 if(s.find(a[j])!=s.end()) continue; 38 f[i]+=f[j]; 39 s.insert(a[j]); 40 } 41 } 42 } 43 } 44 int ans=0; 45 long long num=0; 46 s.clear(); 47 for (int i=1;i<=n;++i) 48 ans=max(ans,dp[i]); 49 for (int i=n;i>=1;--i) 50 { 51 if(ans==dp[i]) 52 { 53 if(s.find(a[i])!=s.end()) continue; 54 num+=f[i]; 55 s.insert(a[i]); 56 } 57 } 58 printf("%d %lld",ans,num); 59 return 0; 60 }
等以后再看到这题可以试一试$NlogN$的做法,然而我也不知道是否有这种做法。
花店橱窗布置:https://www.luogu.org/problemnew/show/P1854
题意概述:f朵花,v个瓶子,要求花要按照顺序插进瓶子里(不用连续),每朵花在每个瓶子里能创造的价值都是不一样的,求最大价值并输出方案。
单调队列,滚动数组都可以试一试,不过这题$1<=f,v<=100$岂不是随便做。
$f[i][j]$表示前i朵花插在前j个花瓶的最大价值。为了方便,我们不妨钦定第i朵花就插在j号瓶子里,这么一来转移就非常简单了:$f[i][j]=a[i][j]+max(f[i-1][k])(k<j)$,对于每一个dp状态再记录一个前驱方便输出方案,一开始一定要给f数组初始化为极小值,否则如果出现负数就会WA。
1 # include <cstdio> 2 # include <iostream> 3 # include <cstring> 4 5 using namespace std; 6 7 int f,v; 8 int dp[105][105],a[105][105],p[105][105]; 9 int sta[105],Top=0; 10 11 int main() 12 { 13 scanf("%d%d",&f,&v); 14 for (int i=1;i<=f;++i) 15 for (int j=1;j<=v;++j) 16 scanf("%d",&a[i][j]); 17 memset(dp,128,sizeof(dp)); 18 dp[0][0]=0; 19 for (int i=1;i<=f;++i) 20 for (int j=1;j<=v;++j) 21 { 22 for (int k=0;k<j;++k) 23 { 24 if(dp[i-1][k]>dp[i][j]) 25 { 26 dp[i][j]=dp[i-1][k]; 27 p[i][j]=k; 28 } 29 } 30 dp[i][j]+=a[i][j]; 31 } 32 int ans=0,x; 33 for (int i=1;i<=v;++i) 34 if(dp[f][i]>ans) 35 ans=max(ans,dp[f][i]),x=i; 36 printf("%d\n",ans); 37 while (x) 38 { 39 sta[++Top]=x; 40 x=p[f][x]; 41 f--; 42 } 43 for (int i=Top;i>=1;--i) 44 printf("%d ",sta[i]); 45 return 0; 46 }
过河:https://www.luogu.org/problemnew/show/P1052
题意概述:长度为l的序列上有m个特殊的点,从0出发,每次可以在$[i+s,i+t]$中选出一个点转移过去,求最小化跳到特殊点的数量。($l<=10^9,m<=100,s,t<=10$)
题意非常有误导性,一开始还以为是每次必须跳到某一个特殊点上...其实还是比较简单的,我觉得都不用写了。但是l的范围非常大,用朴素的做法肯定不行,发现m,s,t的范围都不是很大(暗示我们这是一道最小生成树)可以手玩一下,如果两个石子之间的空隙非常大(这种情况是一定会出现的),因为这之间没有石子,所以有相当一部分的答案是不会变化的(好像这么说也不大对...不知道该怎么表述了),总之这么大的空隙是非常没有用的,我们可以直接把它缩起来,这个缩空间的界限非常模糊,也可以说都对,我一开始是直接%t,不知道是写挂了还是思路错了只得了10分,后来看了一个神奇的题解运用扩欧求出来可以把大的空间缩成t*(t-1),也有用1-10的lcm的。这里要注意一个问题,如果s=t,就只有一种走法了,建议特判,尤其是第一种缩点法不能应对这种情况。统计答案不用找到终点,其实只要从最后一个石子往后统计上10个左右就可以了。
注意事项:如果要对拍一定要分清哪个是改过的哪个没改过,不要对拍用的是一份,交到OJ上的是另一份,因为这个原因浪费了3次提交qwq
1 # include <cstdio> 2 # include <iostream> 3 # include <cstring> 4 # include <algorithm> 5 6 using namespace std; 7 8 int las,l; 9 int ans=104,s,t,m,x; 10 int a[105]; 11 int dp[300050]; 12 bool vis[300009]; 13 14 int main() 15 { 16 scanf("%d%d%d%d",&l,&s,&t,&m); 17 memset(dp,127,sizeof(dp)); 18 dp[0]=0; 19 for (int i=1;i<=m;++i) 20 scanf("%d",&a[i]); 21 sort(a+1,a+m+1); 22 if(s==t) 23 { 24 ans=0; 25 for(int i=1;i<=m;i++) 26 if(a[i]%s==0) 27 ans++; 28 printf("%d\n",ans); 29 return 0; 30 } 31 for (int i=1;i<=m;++i) 32 { 33 x=a[i]; 34 a[i]=a[i]-las; 35 las=x; 36 } 37 for (int i=1;i<=m;++i) 38 { 39 if(a[i]>=t*(t-1)) a[i]=t*(t-1); 40 a[i]+=a[i-1]; 41 vis[a[i]]=1; 42 } 43 for (int i=0;i<=30005;++i) 44 { 45 if(vis[i]) dp[i]++; 46 for (int j=s;j<=t;++j) 47 dp[i+j]=min(dp[i+j],dp[i]); 48 } 49 for (int i=1;i<=20;++i) 50 ans=min(ans,dp[a[m]+i]); 51 printf("%d",ans); 52 return 0; 53 }
严格$n$元树:https://www.luogu.org/problemnew/show/P4295
题意概述:(这次我决定抄题面)如果一棵树的所有非叶节点都恰好有$n$个儿子,那么我们称它为严格$n$元树。如果该树中最底层的节点深度为$d$(根的深度为$0$),那么我们称它为一棵深度为$d$的严格$n$元树。
一开始把式子弄得特别复杂,后来发现运用一点容斥的思想可以使式子变得简单许多.一个简单的想法是一棵深度为$d$的严格$n$元树肯定有$n$个深度为$x-1$的儿子,但是这样真的对吗?显然是错误的,因为只要它有一个儿子的深度到达$d-1$就足以使他成为一棵深度为$d$的树了.难道要运用组合数来判断每棵子树的深度情况吗...?还有一种思考方法:每个儿子的深度是$<=x-1$的,记录一个前缀和,保存深度$<=x$的树的数目的总和.每个点选取$n$个深度$<=d$的子树之后就会出现好多不合法的树,这些树的深度都是$<=d$的,而且也涵盖了所有的这种树,于是就有了一个简单的转移方程:
$f_i=s_{i-1}^n-s_{i-1}+1$
这题可以说是"想题十分钟,高精半小时"的典范了.
1 # include <cstdio> 2 # include <iostream> 3 # include <cstring> 4 # define R register int 5 6 using namespace std; 7 8 int n,d; 9 struct intt 10 { 11 int a[10000]; 12 int len; 13 intt() 14 { 15 memset(a,0,sizeof(a)); 16 len=0; 17 } 18 }f,s; 19 20 intt mul (intt a,intt b) 21 { 22 intt c; 23 for (R i=1;i<=a.len;++i) 24 for (R j=1;j<=b.len;++j) 25 c.a[i+j-1]+=a.a[i]*b.a[j]; 26 c.len=a.len+b.len+2; 27 for (R i=1;i<=c.len;++i) 28 { 29 c.a[i+1]+=c.a[i]/10; 30 c.a[i]%=10; 31 } 32 while (c.len&&c.a[ c.len ]==0) c.len--; 33 return c; 34 } 35 36 intt qui (intt a,int b) 37 { 38 intt s; 39 s.a[1]=1; 40 s.len=1; 41 while (b) 42 { 43 if(b&1) s=mul(s,a); 44 a=mul(a,a); 45 b=b>>1; 46 } 47 return s; 48 } 49 50 void add_one() 51 { 52 int l=1; 53 f.a[1]++; 54 while (f.a[l]>=10) 55 { 56 f.a[l+1]+=f.a[l]/10; 57 f.a[l]%=10; 58 l++; 59 } 60 f.len=max(f.len,l); 61 } 62 63 intt add (intt a,intt b) 64 { 65 intt c; 66 c.len=max(a.len,b.len)+1; 67 for (R i=1;i<=c.len;++i) 68 { 69 c.a[i]+=a.a[i]+b.a[i]; 70 c.a[i+1]+=c.a[i]/10; 71 c.a[i]%=10; 72 } 73 while (c.len&&c.a[ c.len ]==0) c.len--; 74 return c; 75 } 76 77 intt sub (intt a,intt b) 78 { 79 for (R i=1;i<=a.len;++i) 80 { 81 a.a[i]-=b.a[i]; 82 if(a.a[i]<0) a.a[i]+=10,a.a[i+1]--; 83 } 84 while (a.len&&a.a[ a.len ]==0) a.len--; 85 return a; 86 } 87 int main() 88 { 89 scanf("%d%d",&n,&d); 90 f.a[1]=1,s.len=f.len=1,s.a[1]=2; 91 for (R i=2;i<=d;++i) 92 { 93 f=qui(s,n); 94 f=sub(f,s); 95 add_one(); 96 s=add(s,f); 97 } 98 for (R i=f.len;i>=1;--i) printf("%d",f.a[i]); 99 return 0; 100 }
牡牛和牝牛:https://www.lydsy.com/JudgeOnline/problem.php?id=3398
题意概述:一共有$n$头牛排队,要求任意两只牡牛之间至少要有$K$只牝牛,求方案数;每头牛可以是牡牛也可以是牝牛,同一种牛内部不再区分.$n<=10^5$
虽然是排列组合的课后习题,但是用$dp$显然更好做一点.$dp_i$表示第$i$头是牡牛的方案数,转移时加上$\sum_{t=0}^{i-k}$,统计答案时要全部统计.前缀和优化可以做到$O(N)$.似乎还有一种更简单的思路:$f_i$表示当前填到第$i$头的方案数,如果第$i$头选牡牛,那么前面$k$头等于说已经固定了,从$f_{i-k-1}$转移,如果选牝牛,那么之前的选什么都行,直接从$f_{i-1}$转移。
1 # include <cstdio> 2 # include <iostream> 3 # define mod 5000011 4 # define R register int 5 6 using namespace std; 7 8 const int maxn=100005; 9 int n,k,dp[maxn],s[maxn]; 10 11 int main() 12 { 13 scanf("%d%d",&n,&k); 14 for (R i=0;i<=k;++i) 15 { 16 dp[i]=1; 17 s[i]=dp[i]; 18 if(i) s[i]+=s[i-1]; 19 if(s[i]>mod) s[i]-=mod; 20 } 21 for (R i=k+1;i<=n;++i) 22 { 23 dp[i]=s[i-k-1]; 24 if(i) s[i]=dp[i]+s[i-1]; 25 else s[i]=dp[i]; 26 if(s[i]>mod) s[i]-=mod; 27 } 28 printf("%d",s[n]%mod); 29 return 0; 30 }
---shzr