这是一个不知道为什么反正就是想写成汇总的东西,整合一些DP题(其实是懒得一个个写题解)
因为沙茶博主DP很菜(什么都很菜,只是DP尤其菜)又做题少,所以写了这样一个记录沙茶博主在省选退役前刷的DP题的东西
一些零碎的知识点在知识总结里
好像上一行两句没有什么因果关系
好了沙茶博主把DP知识点也丢进这个东西里了,感觉这个可以拿来学(虽然除了沙茶自己没人看这玩意),因为沙茶博主写的时候基本那些东西都不扎实,跟重新学了一遍差不多
前一阵的链接们:
虚树DP:SDOI 2011 消耗战 HNOI 2014 世界树
凸优化:八省联考2018 林克卡特树
决策单调性:NOI 2009 诗人小G
树形DP:CF1118F2 Tree Cutting 国家集训队 Crash的文明世界
乱七八糟的东西们:九省联考2018 CoaT(写的暴力) WC 2018 州区划分(并没搞太懂)
----------------------------------------------------------
转化
注意每个人的分数区间不可相交,然后莫得了
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<map> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 using namespace std; 6 const int N=100005,inf=1e9; 7 struct a 8 { 9 int ll,rr,val; 10 }mem[N]; 11 int n,t1,t2,cnt,ans,va[N],maxi[4*N]; 12 map<pair<int,int>,int> mp; 13 void Mini(int &x,int y) 14 { 15 if(x>y) x=y; 16 } 17 bool cmp(a x,a y) 18 { 19 return x.ll==y.ll?x.rr<y.rr:x.ll<y.ll; 20 } 21 void Count(int l,int r) 22 { 23 pair<int,int> pr=make_pair(l,r); 24 if(!mp.count(pr)) mp[pr]=++cnt; 25 int id=mp[pr]; va[id]++; 26 mem[id]=(a){l,r,va[id]}; 27 } 28 void Change(int nde,int l,int r,int pos,int tsk) 29 { 30 if(l==r) 31 maxi[nde]=tsk; 32 else 33 { 34 int mid=(l+r)>>1,ls=2*nde,rs=2*nde+1; 35 if(pos<=mid) Change(ls,l,mid,pos,tsk); 36 else Change(rs,mid+1,r,pos,tsk); 37 maxi[nde]=max(maxi[ls],maxi[rs]); 38 } 39 } 40 int Query(int nde,int l,int r,int ll,int rr) 41 { 42 if(l>rr||r<ll) 43 return -inf; 44 else if(l>=ll&&r<=rr) 45 return maxi[nde]; 46 else 47 { 48 int mid=(l+r)>>1,ls=2*nde,rs=2*nde+1; 49 return max(Query(ls,l,mid,ll,rr),Query(rs,mid+1,r,ll,rr)); 50 } 51 } 52 int main() 53 { 54 scanf("%d",&n); 55 for(int i=1;i<=n;i++) 56 { 57 scanf("%d%d",&t1,&t2); 58 if(t2+1>n-t1) continue; 59 Count(t2+1,n-t1); 60 } 61 for(int i=1;i<=cnt;i++) 62 Mini(mem[i].val,mem[i].rr-mem[i].ll+1); 63 sort(mem+1,mem+1+cnt,cmp); 64 // for(int i=1;i<=cnt;i++) printf("%d %d %d ",mem[i].ll,mem[i].rr,mem[i].val); 65 for(int i=1;i<=cnt;i++) 66 { 67 int qry=Query(1,0,n,0,mem[i].ll-1); 68 int newa=qry+mem[i].val; 69 Change(1,0,n,mem[i].rr,newa),ans=max(ans,newa); 70 } 71 printf("%d ",n-ans); 72 return 0; 73 }
转化
互质归根结底是没有相同质因数,而每个数只可能有一个大于$sqrt n$的质因数。搞出来每个数小于$sqrt n$的质因数的状态和(是否有)大于$sqrt n$的质因数,按后者从小到大排序,大于$sqrt n$的质因数相同的一块DP(没有的每个单独DP)。然后基本莫得了,注意容斥掉两个人都没选的情况
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N=510,M=260,all=255; 6 const int pri[8]={2,3,5,7,11,13,17,19}; 7 long long n,mod,cnt,ans,dp[M][M],fir[M][M],sec[M][M]; 8 struct a 9 { 10 int sta,hug; 11 }num[N]; 12 bool cmp(a x,a y) 13 { 14 return x.hug==y.hug?x.sta<y.sta:x.hug<y.hug; 15 } 16 void Add(long long &x,long long y) 17 { 18 x+=y; 19 if(x>=mod) x-=mod; 20 } 21 void Pre() 22 { 23 for(int i=2;i<=n;i++) 24 { 25 int tmp=i; 26 for(int j=0;j<=7;j++) 27 if(tmp%pri[j]==0) 28 { 29 num[i].sta|=1<<j; 30 while(tmp%pri[j]==0) tmp/=pri[j]; 31 } 32 num[i].hug=tmp; 33 } 34 sort(num+2,num+1+n,cmp),dp[0][0]=1; 35 } 36 int main() 37 { 38 scanf("%lld%lld",&n,&mod),Pre(); 39 for(int i=2;i<=n;i++) 40 { 41 if(i==2||num[i].hug==1||num[i].hug!=num[i-1].hug) 42 { 43 for(int j=0;j<=all;j++) 44 for(int k=0;k<=all;k++) 45 fir[j][k]=sec[j][k]=dp[j][k]; 46 } 47 for(int j=all;~j;j--) 48 for(int k=all;~k;k--) 49 if(!(j&k)) 50 { 51 if(!(k&num[i].sta)) Add(fir[j|num[i].sta][k],fir[j][k]); 52 if(!(j&num[i].sta)) Add(sec[j][k|num[i].sta],sec[j][k]); 53 } 54 if(i==n||num[i].hug==1||num[i].hug!=num[i+1].hug) 55 { 56 for(int j=all;~j;j--) 57 for(int k=all;~k;k--) 58 if(!(j&k)) dp[j][k]=(fir[j][k]+sec[j][k]-dp[j][k]+mod)%mod; 59 } 60 } 61 for(int i=0;i<=all;i++) 62 for(int j=0;j<=all;j++) 63 if(!(i&j)) Add(ans,dp[i][j]); 64 printf("%lld",ans); 65 return 0; 66 }
分析
直接做不可做,分析性质,发现每次一定是拔一个后缀。因为后面不比它矮的拔了不会使答案变劣,后面比它矮的拔了还是没贡献,最后还拔走就行。
于是可以设计一个DP:$dp[i][j]$表示到$i$为止(i被)拔了$j$次的最长不下降子序列,转移为:
$dp[i][j]=max(dp[i][j],dp[k][h]+1)(a[i]+j>a[k]+h&&j>h)$
树状数组维护二维(前缀)最大值
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N=10005,M=505; 6 int n,k,mx,ans,a[N],bit[N][M]; 7 int Query(int x,int y) 8 { 9 int ret=0; 10 for(int i=x;i;i-=i&-i) 11 for(int j=y;j;j-=j&-j) 12 ret=max(ret,bit[i][j]); 13 return ret; 14 } 15 void Change(int x,int y,int v) 16 { 17 for(int i=x;i<=mx+k;i+=i&-i) 18 for(int j=y;j<=k;j+=j&-j) 19 bit[i][j]=max(bit[i][j],v); 20 } 21 int main() 22 { 23 scanf("%d%d",&n,&k),k++; 24 for(int i=1;i<=n;i++) 25 scanf("%d",&a[i]),mx=max(mx,a[i]); 26 for(int i=1;i<=n;i++) 27 for(int j=k;j;j--) 28 { 29 int len=Query(a[i]+j,j)+1; 30 ans=max(ans,len),Change(a[i]+j,j,len); 31 } 32 printf("%d ",ans); 33 return 0; 34 }
对未来的承诺
因为当前节点的决策影响了之后父亲的决策,一般设状态转移不了。所以在dp状态里先对未来承诺:设$dp[i][j][k]$表示以$i$为根的子树里放了$k$个伐木场,承诺$i$上面的第一个伐木场是$j$的最小代价,然后每次在当前节点到根的链上DP。
不知为何转移时加上子树size的限制就错了
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N=110,M=55; 6 int n,m,t1,t2,cnt,top; 7 int p[N],noww[N],goal[N],val[N]; 8 int wood[N],dep[N],stk[N],dp[N][N][M]; 9 void Mini(int &x,int y) 10 { 11 if(x>y) x=y; 12 } 13 void Link(int f,int t,int v) 14 { 15 noww[++cnt]=p[f],p[f]=cnt; 16 goal[cnt]=t,val[cnt]=v; 17 } 18 void DFS(int nde) 19 { 20 stk[++top]=nde; 21 for(int i=p[nde];i;i=noww[i]) 22 { 23 int g=goal[i]; 24 dep[g]=dep[nde]+val[i],DFS(g); 25 for(int j=1;j<=top;j++) 26 for(int k=m;~k;k--) 27 { 28 int anc=stk[j]; 29 dp[nde][anc][k]+=dp[g][anc][0]; 30 for(int h=1;h<=k;h++) 31 Mini(dp[nde][anc][k],dp[nde][anc][k-h]+dp[g][anc][h]); 32 } 33 } 34 top--; 35 for(int i=1;i<=top;i++) 36 { 37 int anc=stk[i],cst=wood[nde]*(dep[nde]-dep[anc]); 38 dp[nde][anc][0]+=cst; 39 for(int j=1;j<=m;j++) 40 dp[nde][anc][j]=min(dp[nde][anc][j]+cst,dp[nde][nde][j-1]); 41 } 42 } 43 int main() 44 { 45 scanf("%d%d",&n,&m),n++; 46 for(int i=2;i<=n;i++) 47 scanf("%d%d%d",&wood[i],&t1,&t2),Link(t1+1,i,t2); 48 DFS(1),printf("%d",dp[1][1][m]); 49 return 0; 50 }
明示你按$b$分组,每组内先做背包再合并起来。合并就是从低位向高位合并,同时注意m的这一位是否有值
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // luogu-judger-enable-o2 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 using namespace std; 6 int n,m,t1,t2,len; 7 long long dp[35][1100]; 8 int main() 9 { 10 while(scanf("%d%d",&n,&m)!=EOF) 11 { 12 if(n==-1) break; 13 memset(dp,len=0,sizeof dp); 14 for(int i=1;i<=n;i++) 15 { 16 scanf("%d%d",&t1,&t2); int pw=0; 17 while(t1%2==0) t1/=2,pw++; 18 for(int j=1000;j>=t1;j--) 19 dp[pw][j]=max(dp[pw][j],dp[pw][j-t1]+t2); 20 } 21 int tmp=m; 22 while(tmp) tmp>>=1,len++; len--; 23 for(int i=1;i<=len;i++) 24 for(int j=1000;~j;j--) 25 for(int k=0;k<=j;k++) 26 { 27 int lst=min(1000,(k<<1)+((m>>(i-1))&1)); 28 dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[i-1][lst]); 29 } 30 printf("%lld ",dp[len][1]); 31 } 32 return 0; 33 }
这题好的
我直接转洛谷题解了
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N=410,mod=1e9+7; 6 int n,m,nm,lim[N]; 7 int f1[2][N][N],s1[N][N]; 8 int f2[2][N],s2[N],ans[N*N]; 9 int Add(int x,int y) 10 { 11 x+=y; 12 if(x>=mod) x-=mod; 13 return x; 14 } 15 void Write(int x) 16 { 17 if(x>9) Write(x/10); 18 putchar(x%10|48); 19 } 20 int main() 21 { 22 register int i,j,k; 23 scanf("%d",&n); 24 for(i=1;i<=n;i++) 25 lim[i]=(n-i+1)*(n-1)+(i-1)*(i-2)/2; 26 auto p1=f1[0],p2=f1[1]; 27 m=n*min(2,n-1),nm=n*(n-1),ans[1]=p2[n][1]=1; 28 for(i=1;i<=n;i++) s1[n][1]=1; 29 for(i=2;i<=m;i++) 30 { 31 for(j=1;j<=n;j++) 32 if(i<=lim[j]) 33 for(k=1;k<=n;k++) 34 if(i-k+1>=n-j) 35 p1[j][k]=Add(p2[j][k],s1[j+1][k-1]); 36 for(j=n;j;j--) 37 for(k=1;k<=n;k++) 38 { 39 s1[j][k]=Add(s1[j+1][k],p1[j][k]); 40 ans[i]=Add(ans[i],p1[j][k]),p2[j][k]=0; 41 } 42 swap(p1,p2); 43 } 44 for(i=1;i<=n;i++) 45 for(j=1;j<=n;j++) 46 f2[1][i]=Add(f2[1][i],p2[i][j]),s2[i]=Add(s2[i],s1[i][j]); 47 auto q1=f2[0],q2=f2[1]; 48 for(i=m+1;i<=nm;i++) 49 { 50 for(j=1;j<=n;j++) 51 if(i<=lim[j]) 52 q1[j]=Add(q2[j],s2[j+1]); 53 for(j=n;j;j--) 54 { 55 s2[j]=Add(s2[j+1],q1[j]); 56 ans[i]=Add(ans[i],q1[j]),q2[j]=0; 57 } 58 swap(q1,q2); 59 } 60 for(i=1;i<=nm;i++) Write(ans[i]),putchar(' '); 61 return 0; 62 }
学了一个处理这种序列上线段乱搞问题的套路
把每个线段抽象成一个点,线段本身分成左右端点讨论建图,最后把问题转成一个图上问题
这里把线段的左右端点从大到小排序后讨论每个点$x$和它的下一个点$y$:
如果$x$是左端点,$y$是右端点:如果$x,y$就是一条线段直接把点权加上长度;否则用权值为长度的边连接$x->y$所代表的线段,表示他们中间这段如果给$y$队钥匙是可以省掉的
如果两个都是左/右端点就把左/右边那个的点权加上长度
如果$x$是右端点,$y$是左端点,那么说明这中间是断开的,直接贡献进去
最后我们得到了一坨链,就可以依次DP这些链了。设$dp[i][j][2]$表示决策到第$i$个队伍给出去$j$把钥匙,当前队伍给没给钥匙,$O(n^2)$DP即可
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N=4005; 6 struct a 7 { 8 int typ,pos,idx; 9 }pts[N]; 10 bool cmp(a x,a y) 11 { 12 return x.pos<y.pos; 13 } 14 int n,m,t1,t2,cnt,tot,mem; 15 int per[N],vis[N],dp[2][N][N]; 16 int pre[N],nxt[N],val[N],len[N],deg[N]; 17 int main() 18 { 19 scanf("%d%d",&n,&m); 20 for(int i=1;i<=n;i++) 21 { 22 scanf("%d%d",&t1,&t2); 23 pts[++cnt]=(a){0,t1,i}; 24 pts[++cnt]=(a){1,t2,i}; 25 } 26 sort(pts+1,pts+1+cnt,cmp); 27 for(int i=1;i<cnt;i++) 28 { 29 a a1=pts[i],a2=pts[i+1]; 30 int lth=a2.pos-a1.pos,x=a1.idx,y=a2.idx; 31 int xx=a1.typ,yy=a2.typ; 32 if(!xx&&yy) 33 { 34 if(x==y) mem+=lth; 35 else pre[y]=x,nxt[x]=y,len[x]=lth,deg[y]++; 36 } 37 else if(!xx&&!yy) val[x]+=lth; 38 else if(xx&&yy) val[y]+=lth; 39 else mem+=lth; 40 } 41 for(int i=1;i<=cnt;i++) 42 if(!vis[pts[i].idx]&&!deg[pts[i].idx]) 43 { 44 int nde=pts[i].idx; 45 per[++tot]=nde,vis[nde]=true; 46 while(nxt[nde]) nde=nxt[nde],per[++tot]=nde,vis[nde]=true; 47 } 48 auto p1=dp[0],p2=dp[1]; 49 for(int i=1;i<=n;i++) 50 { 51 int nde=per[i]; 52 for(int j=1;j<=m;j++) 53 { 54 p1[i][j]=max(p1[i-1][j],p2[i-1][j]); 55 if(!pre[nde]) p2[i][j]=max(p1[i-1][j-1],p2[i-1][j-1])+val[nde]; 56 else p2[i][j]=max(p1[i-1][j-1],p2[i-1][j-1]+len[pre[nde]])+val[nde]; 57 } 58 } 59 printf("%d",pts[cnt].pos-pts[1].pos-max(p1[n][m],p2[n][m])-mem); 60 return 0; 61 }
我太菜了,被高一学长吊打了
(按着一个错误的沙雕思路WA了一大串
设$dp[i]$表示合并到i为止的最少合并次数,转移是$dp[i]=dp[j]+i-j-1$,其中$j$是最靠后的满足$sum[i]-sum[j]>=lst[j]$的位置,$lst[j]$表示到$j$为止的最后一个数
显然移项之后就要找满足$sum[i]>=sum[j]+lst[j]$的最大的j,然后我不知道怎么想的认为这个东西可以set搞(显然tm是假的,就开始摁着改。
都省选了怎么还有这么制杖的人?
事实上权值线段树即可,注意开long long
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<set> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #define lli long long 6 using namespace std; 7 const int N=200005; 8 int n,tot,root; 9 lli sum[N],dp[N],lst[N]; 10 int val[48*N],son[48*N][2]; 11 int Query(int &nde,lli l,lli r,lli ll,lli rr) 12 { 13 if(l>=ll&&r<=rr) 14 return val[nde]; 15 else 16 { 17 lli mid=(l+r)>>1;int ret=0; 18 if(mid>=ll) ret=max(ret,Query(son[nde][0],l,mid,ll,rr)); 19 if(mid<rr) ret=max(ret,Query(son[nde][1],mid+1,r,ll,rr)); 20 val[nde]=max(val[son[nde][0]],val[son[nde][1]]); return ret; 21 } 22 } 23 void Insert(int &nde,lli l,lli r,lli pos,int tsk) 24 { 25 if(!nde) nde=++tot; 26 if(l==r) 27 val[nde]=max(val[nde],tsk); 28 else 29 { 30 lli mid=(l+r)>>1; 31 if(pos<=mid) Insert(son[nde][0],l,mid,pos,tsk); 32 else Insert(son[nde][1],mid+1,r,pos,tsk); 33 val[nde]=max(val[son[nde][0]],val[son[nde][1]]); 34 } 35 } 36 int main() 37 { 38 scanf("%d",&n); 39 for(int i=1;i<=n;i++) 40 scanf("%lld",&sum[i]),sum[i]+=sum[i-1]; 41 Insert(root,0,sum[n],0,0); 42 for(int i=1;i<=n;i++) 43 { 44 int pos=Query(root,0,sum[n],0,sum[i]); 45 dp[i]=dp[pos]+i-pos-1,lst[i]=sum[i]-sum[pos]; 46 if(lst[i]+sum[i]<=sum[n]) Insert(root,0,sum[n],lst[i]+sum[i],i); 47 } 48 // for(int i=1;i<=n;i++) printf("%d ",dp[i]);puts(""); 49 // for(int i=1;i<=n;i++) printf("%d ",lst[i]);puts(""); 50 printf("%lld",dp[n]); 51 return 0; 52 }
各种DP及他们的优化
DP优化的本质要么是删状态要么是改转移(废话
感觉DP这个东西更重理解
1.线性DP(基本看的Flashhu的博客)
就是当前的DP值由前面一个DP值加上个代价转移过来,这种DP我统称为线性DP
优化们
①做前缀和
这好像是通用的......例题的话 逆序对数列
②单调队列
1.转移是一块区间,而且区间单调地移动
2.当前不优的转移点将来一定不会更优
所以维护一个合法转移点的区间,每次从队头踢掉不合法的点,用队头更新,然后从队尾加入当前点,顺便把所有不优的都踢掉
例题有什么 瑰丽华尔兹 和 股票交易 之类的
这东西可以用来优化多重背包,对每个物品维护同余的单调队列,比二进制压缩还快那么一个log,然并卵
③数据结构优化
有个经典的东西叫做线段树优化DP
这种有时候会和单调线性结构结合
比如说 USACO12OPEN Bookshelf
现在感觉当时写麻烦了,可以看这个
线段树不仅是可以优化复杂度,还有整合信息的作用
比如说 CF833B The Bakery
④决策单调性优化
顾名思义,DP的决策点单调移动
有两种优化方法:单调线性结构(栈或队列)或者分治
(其实本质上没有区别,看你写哪个)
分治就记录当前求解区间和当前决策区间,每次暴力扫出来当前的最优决策点,往下递归就完了
好像是雅礼集训的题 珠宝(体积很小的01背包)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio> 2 #include<vector> 3 #include<cstring> 4 #include<algorithm> 5 #define ll long long 6 using namespace std; 7 const int N=1000005,M=305; 8 ll tr1[N],tr2[N],tmp[N],tep[N],sum[N]; 9 int n,m,t1,t2,sz,mx; vector<int> ve[M]; 10 bool cmp(ll a,ll b) 11 { 12 return a>b; 13 } 14 void Solve(ll *lst,ll *cur,int l,int r,int nl,int nr) 15 { 16 if(l>r) return; 17 if(nl==nr) 18 for(int i=l;i<=r;i++) 19 cur[i]=lst[nl]+sum[i-nl]; 20 else 21 { 22 if(l==r) 23 { 24 cur[l]=0; int lp=max(nl,l-sz),rp=min(nr,l); 25 for(int i=lp;i<=rp;i++) 26 cur[l]=max(cur[l],lst[i]+sum[l-i]); 27 } 28 else 29 { 30 int mid=(l+r)/2; cur[mid]=0; 31 int pt=-1,lp=max(nl,mid-sz),rp=min(nr,mid); 32 for(int i=lp;i<=rp;i++) 33 { 34 ll val=lst[i]+sum[mid-i]; 35 if(val>=cur[mid]) 36 cur[mid]=val,pt=i; 37 } 38 Solve(lst,cur,l,mid-1,nl,pt); 39 Solve(lst,cur,mid+1,r,pt,nr); 40 } 41 } 42 } 43 int main() 44 { 45 scanf("%d%d",&n,&m); 46 for(int i=1;i<=n;i++) 47 { 48 scanf("%d%d",&t1,&t2); 49 ve[t1].push_back(t2),mx=max(mx,t1); 50 } 51 mx=min(mx,m); ll *dp=tr1,*pd=tr2; 52 for(int i=1;i<=mx;i++) 53 if(!ve[i].empty()) 54 { 55 sz=ve[i].size(); 56 sort(ve[i].begin(),ve[i].end(),cmp); 57 for(int j=1;j<=sz;j++) 58 sum[j]=sum[j-1]+ve[i][j-1]; 59 for(int j=0,p;j<i;j++) 60 { 61 p=0; for(int k=j;k<=m;k+=i) tmp[++p]=dp[k]; 62 Solve(tmp,tep,1,p,1,p); 63 p=0; for(int k=j;k<=m;k+=i) pd[k]=tep[++p]; 64 } 65 for(int j=0;j<i;j++) pd[j]=dp[j]; swap(dp,pd); 66 } 67 for(int i=1;i<=m;i++) 68 dp[i]=max(dp[i-1],dp[i]),printf("%lld ",dp[i]); 69 return 0; 70 }
线性结构的话如果之前的决策区间不会过期可以决策栈,否则决策队列
类似单调队列的操作,就是注意弹队尾的时候可能最后一个决策区间是弹了一部分,要二分一下
NOI 2009 诗人小G
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cmath> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #define lli long long 6 #define double long double 7 using namespace std; 8 const int N=100005; 9 const lli inf=1e18; 10 struct a 11 { 12 int l,r,p; 13 }que[N]; 14 int T,n,m,k,f,b,top,pre[N],stk[N]; 15 lli len[N]; double dp[N]; char str[N][32]; 16 double Qpow(double x,int k) 17 { 18 if(k==1) return x; 19 double tmp=Qpow(x,k/2); 20 return k%2?tmp*tmp*x:tmp*tmp; 21 } 22 double Calc(int a,int b) 23 { 24 return dp[b]+Qpow(fabs(len[a]-len[b]-m-1),k); 25 } 26 int main() 27 { 28 scanf("%d",&T); 29 while(T--) 30 { 31 scanf("%d%d%d",&n,&m,&k); 32 for(int i=1;i<=n;i++) 33 { 34 scanf("%s",str[i]+1); 35 len[i]=len[i-1]+strlen(str[i]+1)+1; 36 } 37 que[f=b=0]=(a){1,n,0}; 38 for(int i=1;i<=n;i++) 39 { 40 while(f<b&&que[f].r<i) f++; 41 int pt=que[f].p; que[f].l++; 42 dp[i]=Calc(i,pt),pre[i]=pt; 43 while(f<b&&Calc(que[b].l,que[b].p)>=Calc(que[b].l,i)) b--; 44 int lp=que[b].l,rp=que[b].r,ps=rp+1; 45 while(lp<=rp) 46 { 47 int mid=(lp+rp)/2; 48 if(Calc(mid,i)<=Calc(mid,que[b].p)) rp=mid-1,ps=mid; 49 else lp=mid+1; 50 } 51 (ps==que[b].l)?b--:que[b].r=ps-1; 52 if(ps<=n) que[++b]=(a){ps,n,i}; 53 } 54 if(dp[n]>inf) puts("Too hard to arrange"); 55 else 56 { 57 printf("%lld ",(lli)dp[n]),top=0; 58 for(int i=n;i;i=pre[i]) stk[++top]=i; stk[++top]=0; 59 for(int i=top;i;i--) 60 for(int j=stk[i+1]+1;j<=stk[i];j++) 61 { 62 printf("%s",str[j]+1); 63 j==stk[i]?puts(""):putchar(' '); 64 } 65 } 66 puts("--------------------"); 67 } 68 return 0; 69 }
其实四边形不等式也可以归进决策单调性来
这玩意用的时候可能打决策表瞪眼比较好
如果有个DP实在不知道怎么优化可以胡猜它满足四边形不等式,再把边界放宽一点,乱搞技巧.JPG
⑤斜率优化
从这里开始我们数形结合了
感觉这东西非常让人(wo)头大
首先,我觉得也是最让人头大的(可能我脑回路不太正常),什么他娘的叫斜率优化?
这种DP的转移一般形如$dp[i]=max{dp[j]+a[i]*b[j]+c[j]+d[i]}$,$a,b,c,d$都是和下标有关的变量(当然那个max可以是min)
显然和j无关的在这个转移里都不用管,我们把剩下的东西分类整合一下
$(a[i]*b[j])+(dp[j]+c[j])$
那我们先说为什么会扯到斜率
如果你高一没有一直停课,你应该听你的高中数学老师讲过一个叫做线性规划的东西
就像上面这种玩意,图是百度随便找的
你把可行区域画出来,拿那条直线去切它,找截距的最值
所以呢?这和斜率优化有什么关系?
我们把每个位置的dp值看做一个二维平面上的点......
怎么就看做点了呢?
个人觉得最好理解的方法是直接倒腾那个式子,因为dp[j]+c[j]都只和j有关,我们就把它们统称f[j]
我们假设从n转移比从m转移要优,那么就有
$a[i]*b[n]+f[n]>a[i]*b[m]+f[m]$
$f[n]-f[m]>a[i]*(b[m]-b[n])$
$frac{f[n]-f[m]}{b[n]-b[m]}<-a[i]$
所以上面那个式子成立的时候从n转移比从m转移优
现在如果我们把f(即只和转移点有关的项)看做x坐标,b(即一开始既和转移点有关又与当前位置有关的项)看做y坐标,左边的东西就是一个斜率
那对应的我们把右边的-a[i]也看做是斜率,所有转移就是一堆以-a[i]为斜率的直线,实际进行转移的是每条直线的截距
我们说完了什么是斜率,然后怎么优化呢?
暴力做转移就是拿那个斜率在每个点都试一试
优化?想想那道高考题
显然只要在边上的点试一试就好了,当然,我们都知道这些点组成的这个东西叫做— —
凸包
现在的问题就是— —
1.维护好凸包 2.在凸包上找答案
找答案是一个通用的过程,所以斜率优化优化来优化去就是在— —
维护凸包
(下面这段话假设了每次新来的横坐标和询问斜率都单调)
我们找一个单调线性结构(参考决策单调性优化DP)存储凸包上的点,以单调队列为例,对于每个点
1.按照推出来的那个式子更新队头
2.更新答案
3.把那些将来不可能成为凸包上的点的点从队尾踢掉
问题又来了
我刚才说“下面这段话假设了每次新来的横坐标和询问斜率都单调”
那如果询问斜率不单调怎么办?
也就是说队头不能pop,那我们总不能每次暴力扫凸包吧
当然不,斜率不单调也是有一个最优点的,可以二分这个最优点
那如果新来的横坐标不单调怎么办?
也就是说做着做着突然发现前面又出来一个点
按时间CDQ分治,每层里把左半边按横坐标排序建凸包,把右半边放上去跑,然后递归右半边
可以看CEOI2017 Building Bridges
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #define lli long long 5 using namespace std; 6 const int N=100005; 7 int n,top,pos[N],stk[N]; 8 lli h[N],s[N],x[N],y[N],dp[N]; 9 void Maxi(lli &x,lli y){if(x<y) x=y;} 10 void Mini(lli &x,lli y){if(x>y) x=y;} 11 bool cmp(int a,int b) 12 { 13 return x[a]==x[b]?y[a]<y[b]:x[a]<x[b]; 14 } 15 bool Slope(int a,int b,int c) 16 { 17 return (y[a]-y[c])*(x[b]-x[c])>=(y[b]-y[c])*(x[a]-x[c]); 18 } 19 lli Calc(int a,int b) 20 { 21 return -2*h[b]*x[a]+y[a]; 22 } 23 void CDQ(int l,int r) 24 { 25 if(l==r) 26 x[l]=h[l],y[l]=dp[l]-s[l]+h[l]*h[l]; 27 else 28 { 29 int mid=(l+r)>>1; 30 CDQ(l,mid); 31 sort(pos+l,pos+1+mid,cmp),top=0; 32 for(int i=l;i<=mid;i++) 33 { 34 while(top>1&&Slope(pos[i],stk[top-1],stk[top])) top--; 35 stk[++top]=pos[i]; 36 } 37 for(int i=mid+1;i<=r;i++) 38 { 39 int ll=1,rr=top-1,re=top; 40 while(ll<=rr) 41 { 42 int midd=(ll+rr)>>1; 43 if(Calc(stk[midd],i)<Calc(stk[midd+1],i)) re=midd,rr=midd-1; 44 else ll=midd+1; 45 } 46 Mini(dp[i],Calc(stk[re],i)+s[i-1]+h[i]*h[i]); 47 } 48 CDQ(mid+1,r); 49 } 50 } 51 int main() 52 { 53 scanf("%d",&n); 54 for(int i=1;i<=n;i++) scanf("%lld",&h[i]); 55 for(int i=1;i<=n;i++) scanf("%lld",&s[i]); 56 for(int i=1;i<=n;i++) pos[i]=i,s[i]+=s[i-1]; 57 memset(dp,0x3f,sizeof dp),dp[1]=0; 58 CDQ(1,n),printf("%lld",dp[n]); 59 return 0; 60 }
终于在省选前两周学完了斜率优化,wsl
2.选取物品型的DP
前面说的同余单调队列优化多重背包和决策单调性优化(特定的)01背包
二进制压缩多重背包
NOIP内容,不说了
凸优化
例题:八省联考2018 林克卡特树
以选取的个数为为x轴,最优解为y轴。如果这是个上凸函数,这种DP有个套路的优化方法叫做凸优化:二分斜率,然后我们强制选取物品时额外付出斜率的代价。把原来的DP当成一个输入斜率输出切点的黑箱,相当于不限制数目地选取。最后得到一个最优情况下选出来的数目,根据这个数目调整二分上下界即可。
树形背包
虽然这样不好,但我还是要借一个东西来说一个沙茶原来不会的技巧
— —九省联考2018 CoaT(指统计这种贡献的方法)
多项式卷积优化
经常与生成函数一起用
对未来的承诺
如果当前的选取决策影响将来的选取,可以在状态里对将来进行承诺
比如IOI2005河流
3.其他奇奇怪怪的DP
状压DP
呃,没啥可说的?
看到范围很小记得想这个就行
数位DP
套路
看到了别忘了套路就行
(以上两种可以去洛谷博客找一找)
虚树DP
估计出了我又不会
至少记得怎么建虚树吧
(看博客去)
动态DP(DDP)
看NOIP2018保卫王国的题解
插头DP
状压走人,再问不会
斯坦纳树
看博客.jpg
所以最后这些就是让人去翻博客的=。=???