\ \ \ \ \ \ \, 很多时候我们的dp式子如下形式,是一个递推形式 f ( m , n ) f_{(m,n)} f(m,n):
f ( i , j ) = a ( 1 , j ) ⋅ f ( i − 1 , 1 ) + a ( 2 , j ) ⋅ f ( i − 1 , 2 ) + ⋯ + a ( n , j ) ⋅ f ( i − 1 , n ) f_{(i,j)}=a_{(1,j)}\cdot f_{(i-1,1)}+a_{(2,j)}\cdot f_{(i-1,2)}+\cdots+a_{(n,j)}\cdot f_{(i-1,n)} f(i,j)=a(1,j)⋅f(i−1,1)+a(2,j)⋅f(i−1,2)+⋯+a(n,j)⋅f(i−1,n)
  \ \ \ \ \ \ \, 显然,若是 a ( i , j ) a_{(i,j)} a(i,j)参数确定,复杂度也是 O ( m n 2 ) O(mn^2) O(mn2)的,多数情况下不会达到这个复杂度,但是也是接受不了的,有些题目 m m m给的特别大, n n n比较小,我们就考虑矩阵优化:
  \ \ \ \ \ \ \, 首先设定初始矩阵 A A A,也就是原dp式子的初始化项:
A = [ f ( 0 , 1 ) f ( 0 , 2 ) ⋯ f ( 0 , n ) ] A= \begin{bmatrix} f_{(0,1)} & f_{(0,2)} & \cdots & f_{(0,n)}\\ \end{bmatrix} A=[f(0,1)f(0,2)⋯f(0,n)]
  \ \ \ \ \ \ \, 然后设定转移矩阵 B B B:
B = [ a ( 1 , 1 ) a ( 1 , 2 ) ⋯ a ( 1 , n ) a ( 2 , 1 ) a ( 2 , 2 ) ⋯ a ( 2 , n ) ⋮ a ( n , 1 ) a ( n , 2 ) ⋯ a ( n , n ) ] B= \begin{bmatrix} a_{(1,1)} & a_{(1,2)} & \cdots & a_{(1,n)}\\a_{(2,1)} & a_{(2,2)} & \cdots & a_{(2,n)}\\\vdots\\a_{(n,1)} & a_{(n,2)} & \cdots & a_{(n,n)}\\ \end{bmatrix} B=⎣⎢⎢⎢⎡a(1,1)a(2,1)⋮a(n,1)a(1,2)a(2,2)a(n,2)⋯⋯⋯a(1,n)a(2,n)a(n,n)⎦⎥⎥⎥⎤
  \ \ \ \ \ \ \, 根据矩阵乘法的定义,很容易得到:
A × B = [ f ( 1 , 1 ) f ( 1 , 2 ) ⋯ f ( 1 , n ) ] A\times B=\begin{bmatrix} f_{(1,1)} & f_{(1,2)} & \cdots & f_{(1,n)}\\ \end{bmatrix} A×B=[f(1,1)f(1,2)⋯f(1,n)]
  \ \ \ \ \ \ \, 推广得到:
A × B m = [ f ( m , 1 ) f ( m , 2 ) ⋯ f ( m , n ) ] A\times B^m=\begin{bmatrix} f_{(m,1)} & f_{(m,2)} & \cdots & f_{(m,n)}\\ \end{bmatrix} A×Bm=[f(m,1)f(m,2)⋯f(m,n)]
  \ \ \ \ \ \ \, 根据矩阵乘法满足交换律,所以我们可以先算出 B m B^m Bm,再 A A A乘之,即可得到答案,矩阵快速幂如下,复杂度优化到 O ( log m ⋅ n 3 ) O(\log m \cdot n^3) O(logm⋅n3):
struct matrix{int n,m,a[N][N];};
inline matrix operator *(const matrix &a,const matrix &b){
matrix ret;ret.n=a.n;ret.m=b.m;
for(int i=1;i<=a.n;i++)
for(int j=1;j<=b.m;j++){
ret.a[i][j]=0;
for(int k=1;k<=a.m;k++)
ret.a[i][j]+=a.a[i][k]*b.a[k][j];
}
return ret;
}
inline matrix power(matrix A,matrix B,int m){
for(;m;m>>=1,B=B*B)if(m&1)A=A*B;
return A;
}
  \ \ \ \ \ \ \, 模板题目,递推式是:
a n = p ⋅ a ( n − 1 ) + q ⋅ a ( n − 2 ) a_{n}=p\cdot a_{(n-1)}+q\cdot a_{(n-2)} an=p⋅a(n−1)+q⋅a(n−2)
  \ \ \ \ \ \ \, 所以我们令:
f i , 2 = [ a i − 1 , a i ] f_{i,2}=[a_{i-1},a_{i}] fi,2=[ai−1,ai]
  \ \ \ \ \ \ \, 那么有:
f ( i , 1 ) = 0 ⋅ f ( i − 1 , 1 ) + 1 ⋅ f ( i − 1 , 2 ) f_{(i,1)}=0\cdot f_{(i-1,1)}+1\cdot f_{(i-1,2)} f(i,1)=0⋅f(i−1,1)+1⋅f(i−1,2)
f ( i , 2 ) = p ⋅ f ( i − 1 , 1 ) + q ⋅ f ( i − 1 , 2 ) f_{(i,2)}=p\cdot f_{(i-1,1)}+q\cdot f_{(i-1,2)} f(i,2)=p⋅f(i−1,1)+q⋅f(i−1,2)
  \ \ \ \ \ \ \, 所以我们把 A A A, B B B矩阵设置为:
A = [ a 1 a 2 ] A= \begin{bmatrix} a_{1} & a_{2} \end{bmatrix} A=[a1a2]
B = [ 0 q 1 p ] B= \begin{bmatrix} 0 & q\\1 &p \end{bmatrix} B=[01qp]
  \ \ \ \ \ \ \, 那么我们的答案就在 A × B n − 2 A\times B^{n-2} A×Bn−2的第二项。
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cctype>
#include<cstdio>
#include<vector>
#include<string>
#include<queue>
#include<stack>
#include<cmath>
#include<map>
#include<set>
using namespace std;
const int inf=0x7fffffff;
const double eps=1e-10;
const double pi=acos(-1.0);
//char buf[1<<15],*S=buf,*T=buf;
//char getch(){return S==T&&(T=(S=buf)+fread(buf,1,1<<15,stdin),S==T)?0:*S++;}
inline int read(){
int x=0,f=1;char ch;ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-') f=0;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch&15);ch=getchar();}
if(f)return x;else return -x;
}
const int N=5;
long long n,m;
int p,q;
struct matrix{int n,m;long long a[N][N];}A,B;
inline matrix operator *(const matrix &a,const matrix &b){
matrix ret;ret.n=a.n;ret.m=b.m;
for(int i=1;i<=a.n;i++)
for(int j=1;j<=b.m;j++){
ret.a[i][j]=0ll;
for(int k=1;k<=a.m;k++)
ret.a[i][j]=(ret.a[i][j]+a.a[i][k]*b.a[k][j]%m)%m;
}
return ret;
}
inline matrix power(matrix A,matrix B,int m){
for(;m;m>>=1,B=B*B)if(m&1)A=A*B;
return A;
}
int main()
{
A.n=1;A.m=B.m=B.n=2;
p=read();q=read();
A.a[1][1]=read();A.a[1][2]=read();
B.a[1][1]=0;B.a[1][2]=q;
B.a[2][1]=1;B.a[2][2]=p;
scanf("%lld%lld",&n,&m);
A=power(A,B,n-2);
printf("%lld\n",A.a[1][2]);
return 0;
}
  \ \ \ \ \ \ \, 对于一个dp转移方程式,其中取最小或最大:
f i = s o l v e ( i , j ) [ l < j < r ] f_{i}={\rm solve}(i,j)[l<j<r] fi=solve(i,j)[l<j<r]
  \ \ \ \ \ \ \, 若是可以化成如下形式:
f ( i ) = F ( j ) + g ( i ) [ l < j < r ] f_(i)=F(j)+g(i)[l<j<r] f(i)=F(j)+g(i)[l<j<r]
  \ \ \ \ \ \ \, 既 j j j 造成的贡献与 i i i 没有关系,并且 j j j 造成的贡献我们需要取最大或者最小时,并且范围 [ l < j < r ] [l<j<r] [l<j<r]有单调性时,我们可以利用单调队列来优化dp,从 O ( n 2 ) O(n^2) O(n2)优化到 O ( n ) O(n) O(n)。
  \ \ \ \ \ \ \, 核心想法是,建立一个容器,我们把 F ( j ) F(j) F(j) 造成的贡献按照单调性加入该容器,若是现在需要新加入一个元素,那么就从队尾开始,把比他造成贡献不优的踢出去(原dp取 M i n \rm Min Min 的话就是需要 F ( j ) F(j) F(j) 比队尾小,反之就是要大)。
  \ \ \ \ \ \ \, 当然这个时候队首将会是最优秀的 j j j,不过我们的范围限制还可能单调变化,于是我们又需要把队首那些范围不对的都踢掉,于是现在队首就是我们要的 j j j 了,带入原dp即可。
  \ \ \ \ \ \ \, 由于所有元素都最多进入容器一次,又最多被踢一次,所以复杂度是 O ( n ) O(n) O(n)的,模板如下:
int q=1,p=0,Q[N];
for(int i=1;i<=n;i--){
while(q<=p&&better(F(i),F(Q[p])))p--;
Q[++p]=i;
while(q<=p&&!in_lim(i,Q[q]))q++;
if(q<=p)f[i]=F(Q[q])+g(i);
}
  \ \ \ \ \ \ \, 这题显然会有一个dp方程, f ( i , j ) f_{(i,j)} f(i,j)表示在第 i i i天手里有 j j j张股票的最大收益:
-
直接购买:
f ( i , j ) = − a P i × j [ 0 ≤ j ≤ a S i ] f_{(i,j)}=-aP_i\times j[0\leq j\leq aS_i] f(i,j)=−aPi×j[0≤j≤aSi]
-
不行动:
f ( i , j ) = f ( i − 1 , j ) f_{(i,j)}=f_{(i-1,j)} f(i,j)=f(i−1,j)
-
买入:
f ( i , j ) = M a x k = j − a S i j − 1 ( f ( i − w − 1 , k ) − ( j − k ) × a P i ) f_{(i,j)}={\rm Max}_{k=j-aS_i}^{j-1}\left(f_{(i-w-1,k)}-(j-k)\times aP_i\right) f(i,j)=Maxk=j−aSij−1(f(i−w−1,k)−(j−k)×aPi)
-
卖出:
f ( i , j ) = M a x k = j + 1 j + b S i ( f ( i − w − 1 , k ) + ( k − j ) × b P i ) f_{(i,j)}={\rm Max}_{k=j+1}^{j+bS_i}\left(f_{(i-w-1,k)}+(k-j)\times bP_i\right) f(i,j)=Maxk=j+1j+bSi(f(i−w−1,k)+(k−j)×bPi)
  \ \ \ \ \ \ \, 复杂度为 O ( n 3 ) O(n^3) O(n3),主要是后面两个操作花时间了,所幸,后面两个都可以斜率优化,复杂度优化为 O ( n 2 ) O(n^2) O(n2)
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cctype>
#include<cstdio>
#include<vector>
#include<string>
#include<queue>
#include<stack>
#include<cmath>
#include<map>
#include<set>
using namespace std;
const int inf=0x7fffffff;
const double eps=1e-10;
const double pi=acos(-1.0);
//char buf[1<<15],*S=buf,*T=buf;
//char getch(){return S==T&&(T=(S=buf)+fread(buf,1,1<<15,stdin),S==T)?0:*S++;}
inline int read(){
int x=0,f=1;char ch;ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-') f=0;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch&15);ch=getchar();}
if(f)return x;else return -x;
}
const int N=2010;
int n,m,w;
int f[N][N];
int aP[N],bP[N],aS[N],bS[N];
int q,p,Q[N];
int main()
{
n=read();m=read();w=read();
for(int i=1;i<=n;i++)
aP[i]=read(),bP[i]=read(),
aS[i]=read(),bS[i]=read();
memset(f,128,sizeof(f));
for(int i=1;i<=n;i++){
for(int j=0;j<=aS[i];j++)f[i][j]=-j*aP[i];
for(int j=0;j<=m;j++)f[i][j]=max(f[i][j],f[i-1][j]);
if(i<=w)continue;
q=1,p=0;
for(int j=0;j<=m;j++){
while(q<=p&&f[i-w-1][Q[p]]+Q[p]*aP[i]<=f[i-w-1][j]+j*aP[i])p--;
Q[++p]=j;
while(q<=p&&Q[q]<j-aS[i])q++;
if(q<=p)f[i][j]=max(f[i][j],f[i-w-1][Q[q]]+(Q[q]-j)*aP[i]);
}
q=1,p=0;
for(int j=m;j>=0;j--){
while(q<=p&&f[i-w-1][Q[p]]+Q[p]*bP[i]<=f[i-w-1][j]+j*bP[i])p--;
Q[++p]=j;
while(q<=p&&Q[q]>j+bS[i])q++;
if(q<=p)f[i][j]=max(f[i][j],f[i-w-1][Q[q]]+(Q[q]-j)*bP[i]);
}
}
int ans=0;
for(int i=0;i<=m;i++)ans=max(ans,f[n][i]);
printf("%d\n",ans);
return 0;
}
  \ \ \ \ \ \ \, 对于一个dp式子,我们尝试将它化成3个部分:
- 只与 i i i 有关的( A i A_i Ai);
- 只与 j j j 有关的( D j D_j Dj);
- 与 i i i 和 j j j 同时有关的( B i ⋅ C j B_i\cdot C_j Bi⋅Cj);
  \ \ \ \ \ \ \, 所以我们会得到形如这样子的式子:
B i ⋅ C j + A i = D j {\color{#9D5}B_i}\cdot C_j+{\color{#F00}A_i}={\color{#0CE}D_j} Bi⋅Cj+Ai=Dj
  \ \ \ \ \ \ \, 那么这个时候,我们把 B i \color{#9D5}B_i Bi当做斜率, C j C_j Cj是横坐标, D j \color{#0CE}D_j Dj是纵坐标,我们想要的是 A i \color{#F00}A_i Ai最大或者是最小,那么答案一定是下面的那一层点里面诞生,需要满足的是:
-
两个点之间的斜率必须优于 B i \color{#9D5}B_i Bi,若是取 M a x \rm Max Max就是需要大于,否者就是小于。
-
最优的 j j j取值一定是斜率尽量优的。
  \ \ \ \ \ \ \, 那么我们的想法是维护这个斜率组成的凸壳,若是 B i \color{#9D5}B_i Bi单调,我们可以考虑单调队列,若是不单调,那么我们在单调队列的基础上面,就不能从队首踢,就是维护一整个凸壳,每次询问在上面二分。
  \ \ \ \ \ \ \, 下面给出单调队列版的模板:
int Q[N],q,p;
long double Slope(int i){};//B(i)
long long X(int i){}//C(j)
long long Y(int j){}//D(j)
long double slope(int i,int j){
if(X(i)==X(j))return 1.0*inf;//max_inf,min_-inf;
return (1.0*(Y(i)-Y(j)))/(1.0*(X(i)-X(j)));
}
int main()
{
q=p=1;
for(int i=1;i<=n;i++){
while(q<p&&better(slope(Q[q],Q[q+1]),Slope))++q;
f[i]=F(Q[q]);//原dp方程
while(q<p&&better(slope(i,Q[p-1]),slope(Q[p],Q[p-1])))--p;
Q[++p]=i;
}
return 0;
}
  \ \ \ \ \ \ \, 本题dp方程如下:
f i = M i n j = 1 i − 1 ( f j + ( S u m i − S u m j + i − j − L − 1 ) 2 ) f_i={\rm Min}_{j=1}^{i-1}\left(f_j+(Sum_i-Sum_j+i-j-L-1)^2\right) fi=Minj=1i−1(fj+(Sumi−Sumj+i−j−L−1)2)
  \ \ \ \ \ \ \, 化成如下形式:
2 × ( S u m i + i ) ( S u m j + j + L + 1 ) + ( f i − ( S u m i + i ) 2 ) = ( f j + ( S u m j + j + L + 1 ) 2 ) {\color{#9D5}2\times(Sum_i+i)}(Sum_j+j+L+1)+{\color{#F00}\left(f_i-(Sum_i+i)^2\right)}={\color{#0CE}\left(f_j+(Sum_j+j+L+1)^2\right)} 2×(Sumi+i)(Sumj+j+L+1)+(fi−(Sumi+i)2)=(fj+(Sumj+j+L+1)2)
  \ \ \ \ \ \ \, 我们发现 2 × ( S u m i + i ) {\color{#9D5}2\times(Sum_i+i)} 2×(Sumi+i)是单调的,于是我们想用单调队列来维护这个凸壳:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstdlib>
#include<cstring>
#include<string>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<cmath>
#include<cctype>
using namespace std;
const int inf=0x7fffffff;
const double eps=1e-10;
const double pi=acos(-1.0);
//char buf[1<<15],*S=buf,*T=buf;
//char getch(){return S==T&&(T=(S=buf)+fread(buf,1,1<<15,stdin),S==T)?0:*S++;}
inline long long read(){
long long x=0,f=1;char ch;ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-') f=0;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch&15);ch=getchar();}
if(f)return x;else return -x;
}
const int N=50010;
double f[N],sum[N],L;
int q,p,Q[N];
int n;
double fn(int i){return sum[i]+(double)i;}
double X(int i){return fn(i)+L;}
double Y(int i){return f[i]+(fn(i)+L)*(fn(i)+L);}
double slope(int i,int j){return (Y(i)-Y(j))/(X(i)-X(j));}
int main()
{
n=(int)read();L=(double)read()+1.0;
for(int i=1;i<=n;i++)
sum[i]=(double)read(),sum[i]+=sum[i-1];
q=p=1;
for(int i=1;i<=n;i++){
while(q<p&&slope(Q[q],Q[q+1])<2*fn(i))++q;
f[i]=f[Q[q]]+(fn(i)-fn(Q[q])-L)*(fn(i)-fn(Q[q])-L);
while(q<p&&slope(i,Q[p-1])<slope(Q[p-1],Q[p])) --p;
Q[++p]=i;
}
printf("%lld\n",(long long)f[n]);
return 0;
}
  \ \ \ \ \ \ \, WQS二分和斜率优化很相似,多了一个宽度限制,既需要干好取 m m m个。和斜率优化一样,我们需要把式子化成下面的形式:
B i ⋅ C j + A i = D j {\color{#9D5}B_i}\cdot C_j+{\color{#F00}A_i}={\color{#0CE}D_j} Bi⋅Cj+Ai=Dj
  \ \ \ \ \ \ \, 在二分之前,我们二分一个特殊的值 v v v,取 k k k个物品时,总贡献会多 k v kv kv,在进行dp的同时,若 v v v越大,取的物品越少,那么我们检查在 v v v,的时候有多少个物品被选就好了,注意使用WQS二分需要满足 v v v越大,取的物品越少,既需要满足取 x x x个时的总贡献斜率不增,说白了,就是需要满足选的越多越好。
  \ \ \ \ \ \ \, 模板如下:
int Q[N],q,p;
long double Slope(int i){};//B(i)
long long X(int i){}//C(j)
long long Y(int j){}//D(j)
long double slope(int i,int j){
if(X(i)==X(j))return 1.0*inf;//max_inf,min_-inf;
return (1.0*(Y(i)-Y(j)))/(1.0*(X(i)-X(j)));
}
bool cheak(long long v){
q=p=1;
for(int i=1;i<=n;i++){
while(q<p&&better(slope(Q[q],Q[q+1]),Slope))++q;
f[i]=F(Q[q])+v;//原dp方程
tot[i]=tot[Q[q]]+1;
while(q<p&&better(slope(i,Q[p-1]),slope(Q[p],Q[p-1])))--p;
Q[++q]=i;
}
return tot[n]<=m;
}
int main()
{
while(l<r){
long long mid=l+r>>1;
if(cheak(mid))r=mid;
else l=mid+1;
}
cheak(l);
printf("%lld\n",f[n]-l*m);
return 0;
}
  \ \ \ \ \ \ \, 这个题是我们团队准备的,原式是来恶心人的,化简下来就是这个东西:
( ∑ i = 1 n x i + 1 ) 2 \left(\sum_{i=1}^nx_i+1\right)^2 (i=1∑nxi+1)2
  \ \ \ \ \ \ \, dp方程显然就是这个东西:
f i = M i n j = 1 i − 1 f j + ( S u m i − S u m j + 1 ) 2 f_i={\rm Min}_{j=1}^{i-1}f_j+\left(Sum_i-Sum_j+1\right)^2 fi=Minj=1i−1fj+(Sumi−Sumj+1)2
  \ \ \ \ \ \ \, 化成如下形式:
2 × S u m i ⋅ S u m j + ( f i − ( S u m i + 1 ) 2 ) = f j + S u m j 2 − 2 × S u m j {\color{#9D5}2\times Sum_i}\cdot Sum_j+{\color{#F00}\left(f_i-(Sum_i+1)^2\right)}={\color{#0CE}f_j+Sum_j^2-2\times Sum_j} 2×Sumi⋅Sumj+(fi−(Sumi+1)2)=fj+Sumj2−2×Sumj
  \ \ \ \ \ \ \, 感性思考一下,当 m m m越大,答案一定小越小,那么我们就可以使用WQS二分:
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cctype>
#include<cstdio>
#include<vector>
#include<string>
#include<queue>
#include<stack>
#include<cmath>
#include<map>
#include<set>
using namespace std;
const int inf=0x7fffffff;
const double eps=1e-10;
const double pi=acos(-1.0);
//char buf[1<<15],*S=buf,*T=buf;
//char getch(){return S==T&&(T=(S=buf)+fread(buf,1,1<<15,stdin),S==T)?0:*S++;}
inline int read(){
int x=0,f=1;char ch;ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-') f=0;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch&15);ch=getchar();}
if(f)return x;else return -x;
}
const int N=100010;
int n,m,tot[N];
long long sum[N],f[N],Q[N];
long long X(int a){return sum[a];}
long long Y(int a){return f[a]+sum[a]*sum[a]-2ll*sum[a];}
double solpe(int i,int j){return 1.0*(Y(i)-Y(j))/(X(i)-X(j));}
bool cheak(long long v){
int q=1,p=1;
for(int i=1;i<=n;i++){
while(q<p&&solpe(Q[q+1],Q[q])<2.0*sum[i])q++;
f[i]=f[Q[q]]+(sum[i]-sum[Q[q]]+1)*(sum[i]-sum[Q[q]]+1)+v;
tot[i]=tot[Q[q]]+1;
while(q<p&&solpe(i,Q[p])<solpe(Q[p-1],Q[p]))p--;
Q[++p]=i;
}
return tot[n]<=m;
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++)sum[i]=sum[i-1]+1ll*read();
long long l=1,r=(sum[n]*sum[n])/2,mid;
while(l<r){
long long mid=l+r>>1;
if(cheak(mid))r=mid;
else l=mid+1;
}
cheak(l);
printf("%lld\n",f[n]-l*m);
return 0;
}