满足三个特点:
1.转移要选取的决策较少。(一般在常数级别)
2.转移的步骤很多。(一般是1e10以上的级别)
3.每一步的转移方程一样。(和递推类似)
*一般满足转移方程:
一般转移方程为 f(n)=f(n−1)+f(n−2) ,决策只有两个,转移步骤一般很大,且每次转移都一样,满足优化条件。
不放设矩阵
每次要得到另一个矩阵
转移矩阵已经很明显:
显然, Bn=A1∗Tn−1 。而矩阵满足快速幂。可在 O(m3∗logn) 时间内解决。(m是每次转移决策数,此时m=2)。
1. f(n)=f(n−2)+f(n−1)+1
转移
转移矩阵
时间复杂度 O(33logn)
2. f(n)=f(n−2)+f(n−1)+n+1,s(n)为f(n)前缀和,求s(n)
转移
转移矩阵
时间复杂度 O(53logn)
“GT考试”题解
“迷路”题解
如果转移满足两个特点:
1.转移方程: f(x)=mini=1x−1f(i)+w[i,x] (w[i,x]) 为i,x转移的代价(已知条件)。
2. w 函数满足 w[i,j]+w[i1,j1]≤w[i,j1]+w[i,j1](i<i1,j<j1) 。
那么称这个转移满足决策单调性: g(x)=k 表示 mini=1x−1f(i)+w[i,x] 在 k 处取得,那么 ∀i≤j,s.t.g(i)≤g(j) 。
w 函数的性质一般难以观察,只用打表检验 g(x) 的性质即可。
考虑优化:
1. i 从 g(x−1) 开始枚举。
有时候可以达到 O(n) 的线性复杂度,但是如果 g(i)=1 则退化为 O(n2) 。
2. g(k) 单调递增,对于每一个 f(i) ,二分更新后面的g(k)(决策使用单调栈)。
严格 O(nlogn) 。
题意:
有 n 个玩具,要将它们分为若干组进行打包,每个玩具有一个长度 len[x] 。每一组必须是连续的一组玩具。如果将第 x 到第 y 个玩具打包到一组,那么它们的长度 l=j−i−1+∑k=ijlen[k] ,将这组玩具打包所需的代价等于 (l−L)2 。问将所有玩具打包的最小代价是多少。注意到每组玩具个数并没有限制。 n<=50000 。
题解:
打表观察到决策单调性后直接二分解决。这里贴一份代码:
#include
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
streambuf *ib,*ob;
inline int read()
{
char ch=ib->sbumpc();int i=0,f=1;
while(!isdigit(ch)){if(ch=='-')f=-1;ch=ib->sbumpc();}
while(isdigit(ch)){i=(i<<1)+(i<<3)+ch-'0';ch=ib->sbumpc();}
return i*f;
}
int buf[80];
inline void W(ll x)
{
if(!x){ob->sputc('-');return;}
if(x<0){ob->sputc('-');x=-x;}
while(x){buf[++buf[0]]=x%10,x/=10;}
while(buf[0])ob->sputc(buf[buf[0]--]+'0');
}
const int Maxn=5e4+50;
int n,head,tail;
ll len[Maxn],L,f[Maxn];
pii q[Maxn];
inline ll calc(int i,int j){return (j-i+len[j]-len[i-1]-L)*(j-i+len[j]-len[i-1]-L);}
inline int solve(int o,int now,int l,int r)
{
int ans=0;
while(l<=r)
{
int mid=(l+r)>>1;
if(f[o]+calc(o+1,mid)>=f[now]+calc(now+1,mid))ans=mid,r=mid-1;
else l=mid+1;
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);ib=cin.rdbuf();ob=cout.rdbuf();
n=read(),L=read();
for(int i=1;i<=n;i++)len[i]=1ll*read()+len[i-1];
q[head=tail=1]=make_pair(1,0);
for(int i=1;i<=n;i++)
{
f[i]=f[q[head].second]+calc(q[head].second+1,i);
((++q[head].first)>=q[head+1].first&&tail>head)?(head++):0;
int pos=n;
while(tail>=head&&(f[i]+calc(i+1,q[tail].first)<=f[q[tail].second]+calc(q[tail].second+1,q[tail].first)))pos=q[tail].first,tail--;
if(tail1,i);
else
{
pos=solve(q[tail].second,i,q[tail].first,pos);
if(pos)q[++tail]=make_pair(pos,i);
}
}
W(f[n]);ob->sputc('\n');
}
题意:
有 n 块土地需要购买,每块土地都是长方形的,有特定的长与宽。你可以 一次性购买一组土地,价格是这组土地中长的最大值乘以宽的最大值。比方说一块5*3 的土地和一块9*2的土地在一起购买的价格就是 9*3。最小化买下所有的土地的费用。
题解:
显然对于长宽都含于另一个长方体的长方体可以忽略。
那么剩下的长方体排布形式一定为:
即 ∀i≤j,s.t.xi<xj,yi>yj 。
又有转移方程: f(i)=mink=0i−1f(k)+w[k,i] ,其中 w[k,i]=yk·xi ,那么四边形不等式就很好证了:
因为: x4(y1−y2)≤x3(y1−y2) 得证。
满足决策单调性。
如果转移满足以下模型:
f(x)=mini=b[x]x−1g(i)+w[x](g(i)是只于i有关的决策,w[x]是只于x有关的常数,b[x]随x不降)
那么可以用单调队列维护决策表达到 O(n) 的时间复杂度。
怎么维护?
发现对于一个决策 g(i) 只会影响到一定的 f(x) ,且越靠后面的 g(i) 影响的 f(x) 也越靠后面(因为b[x]随x不降)。那么一个决策如果比前面的决策更优,前面的决策就可以直接丢掉。(后面能够被丢掉决策更新的状态一定能被更优的该决策更新)。每次取队首元素,均摊 O(1) 。
“生产商品”题解
*这类dp满足可选决策时单调的,但最优决策要在可选决策中选取最优值,一般用set或平衡树(Splay\Treap)维护.
如:
poj3017:Cut the Sequence
•问题描述
给定一个有n个非负整数的数列a,要求将其划分为若干个部分,使得每部分的和不超过给定的常数m,并且所有部分的最大值的和最小。其中n<=105。
例:n=8, m=17,8个数分别为2 2 2 | 8 1 8 |1 2,答案为12,分割方案如图所示。
•解法分析
刚开始拿到这道题目,首先要读好题:最大值的和最小。
首先设计出一个动态规划的方法
其中 f[i] 代表把前i个数分割开来的最小代价。 b[i]=min(j|sum[j+1,i]≤m) 可以 O(n) 实现。
直接求解复杂度最坏情况下( M 超大)是 O(n2) 的,优化势在必行。
几个性质
通过仔细观察,可以发现以下几点性质:
① 在计算状态 f(x) 的时候,如果一个决策k作为该状态的决策,那么可以发现第 k 个元素和第 x 个元素是不分在一组的。
② b[x] 随着 x 单调不降的,用这一点,可以想到什么?可以想到前面单调队列的一个限制条件。
③ 来看一个最重要的性质:如果一个决策 k 能够成为状态 f(x) 的最优决策,当且仅当 a[k]>∀a[j],j∈[k+1,x] 。为什么呢?其实证明非常非常容易(用到性质1),交给读者自己考虑。
单调队列优化可选决策
到此为止,我们可以这样做:由于性质三,每计算一个状态 f(x) ,它的有效决策集肯定是一个元素值单调递减的序列,我们可以像单调队列那样每次在队首删除元素,直到队首在数列中的位置小于等于 ,然后将a[x]插入队尾,保持队列的元素单调性。
这时候问题来了,队首元素一定是最佳决策点吗?我们只保证了他的元素值最大……如果扫一遍队列,只是常数上的优化,一个递减序足以将它否决。
我们观察整个操作,将队列不断插入、不断删除。对于除了队尾的元素之外,每个队列中的元素供当前要计算的状态的“值”是 f(q[x].position)+a[q[x+1].position] ,其中, q[x] 代表第x个队列元素, position 这代表他在原来数组中的位置,我们不妨把这个值记为 t 。那每一次在队首、队尾的删除就相当于删除 t ,每一次删除完毕之后又要插入一个新的 t ,然后需要求出队列中的t的最小值。
我们发现,完成上述一系列工作的最佳选择就是平衡树,这样每个元素都插入、删除、查找各一遍,复杂度为 O(logn) ,最后的时间复杂度是 O(nlogn) 。
例如:bzoj1791:Island
(这道题是真的难写。。)
首先,每一个基环外向树相当于一个环上有一些点挂了一颗子树,先把这个点的深度算出来,再考虑环上做DP的方法:
断开任意一条环边,再把这个环复制一遍,做 b[x]=x−n+1 的dp就好了。
给一份代码,可以参考参考
#include
using namespace std;
namespace IO
{
streambuf *ib,*ob;
int buf[50];
inline void init()
{
ios::sync_with_stdio(false);
cin.tie(NULL);cout.tie(NULL);
ib=cin.rdbuf();ob=cout.rdbuf();
}
inline int read()
{
char ch=ib->sbumpc();int i=0,f=1;
while(!isdigit(ch)){if(ch=='-')f=-1;ch=ib->sbumpc();}
while(isdigit(ch)){i=(i<<1)+(i<<3)+ch-'0';ch=ib->sbumpc();}
return i*f;
}
inline void W(long long x)
{
if(!x){ob->sputc('0');return;}
if(x<0){ob->sputc('-');x=-x;}
while(x){buf[++buf[0]]=x%10;x/=10;}
while(buf[0])ob->sputc(buf[buf[0]--]+'0');
}
}
typedef long long ll;
typedef pair<int,ll> pil;
typedef pair<int,int> pii;
const int Maxn=1e6+50;
int n,ecnt=1,tail,tailtmp,bz,bg,last[Maxn],from[Maxn],vis[Maxn],ins[Maxn],id[Maxn];
ll dp[Maxn][2],ans,res;
pii statmp[Maxn];
pil sta[Maxn];
struct E{int to,val,nxt;}edge[Maxn*2];
inline void add(int x,int y,int z)
{
edge[++ecnt].to=y;edge[ecnt].nxt=last[x];last[x]=ecnt;edge[ecnt].val=z;
edge[++ecnt].to=x;edge[ecnt].nxt=last[y];last[y]=ecnt;edge[ecnt].val=z;
}
inline void dfs(int now,int val)
{
vis[now]=1;statmp[++tailtmp]=make_pair(now,val);id[now]=tailtmp;
for(int e=last[now];e;e=edge[e].nxt)
{
int v=edge[e].to;
if((e^1)==from[now])continue;
if(!vis[v])from[v]=e,dfs(v,edge[e].val);
else {statmp[id[v]].second=edge[e].val;bz=1;bg=id[v];return;}
if(bz)return;
}
tailtmp--;
}
inline void dfs2(int now,int f=0)
{
vis[now]=1;
for(int e=last[now];e;e=edge[e].nxt)
{
int v=edge[e].to;
if(ins[v]||v==f)continue;
dfs2(v,now);
ll t=dp[v][0]+edge[e].val;
if(t>=dp[now][0])dp[now][1]=dp[now][0],dp[now][0]=t;
else if(t>dp[now][1])dp[now][1]=t;
ans=max(ans,dp[now][1]+dp[now][0]);
}
}
pil q[Maxn*2];
int qhead,qtail;
inline ll calc(int i)
{
for(int i=bg;i<=tailtmp;i++)sta[++tail]=statmp[i],ins[sta[tail].first]=1;
for(int i=1;i<=tail;i++)dfs2(sta[i].first);
memcpy(sta+tail+1,sta+1,sizeof(pil)*tail);int len=tail*2;
for(int i=1;i<=len;i++)sta[i].second+=sta[i-1].second;
ans=max(ans,dp[sta[1].first][0]);q[qhead=qtail=1]=make_pair(1,dp[sta[1].first][0]-sta[1].second);
for(int i=2;i<=len;i++)
{
int lim=i-tail+1;
while(q[qhead].first0]+q[qhead].second+sta[i].second;
ans=max(ans,t);
t=dp[sta[i].first][0]-sta[i].second;
while(qhead<=qtail&&q[qtail].second<=t)qtail--;
q[++qtail]=make_pair(i,t);
}
return ans;
}
int main()
{
IO::init();n=IO::read();
for(int i=1;i<=n;i++)
{
int y=IO::read(),z=IO::read();
add(i,y,z);
}
for(int i=1;i<=n;i++)
{
if(!vis[i])
{
tailtmp=bg=bz=tail=ans=0;
dfs(i,0);
res+=calc(i);
}
}
IO::W(res);IO::ob->sputc('\n');
}
首先,多重背包的转移方程是:
如果决策连续就是裸的单调队列。现在考虑不连续:
首先可以发现一个 x 只会取与它模 c[i] 相同的状态,那么在模意义下分别DP就好了。
for(int i=1;i<=n;i++)
{
for(int j=0;j1]=make_pair(f[j],0);
for(int k=j+c[i];k<=m;k+=c[i])
{
int a=k/c[i],t=f[k]-a*w[i];
while(head<=tail&&q[tail].first<=t)tail--;
q[++tail]=make_pair(t,a);
while(head<=tail&&q[head].second+s[i]
模型:
这个模型写的比较抽象, 其实它的涵盖范围是很广的。 首先, a[i], b[i]不一定要是常量, 只要他们与决策无关, 都可以接受; 另外, g(j)和 h(j)不管是常量还是变量都没有关系, 只要他们是一个由i决定的二元组就可以了。
为了方便描述, 把这个模型做如下转化:
可以发现,状态现在就是许多条直线,而我们的任务是选出一条直线,使它经过前面出现的某一点,且的纵截距最小。 可以想象有一组斜率相同的直线自负无穷向上平移, 所碰到的第一个数据点就是最优决策。
这个时候, 有一个重要的性质, 那就是: 所有最优决策点都在平面点集的凸包上。
基于这个事实, 我们可以开发出很多令人满意的算法。
这时, 根据直线斜率与数据点分布的特征, 可以划分为两种情况:
这样的模型是比较好处理的, 因为这个时候由于斜率变化是单调的, 所以决策点必然在凸壳上单调移动。我们只需要维护一个单调队列和一个决策指针,每次决策时作这样几件事:
1.决策指针( 也就是队首) 后移, 直至最佳决策点。
2.进行决策。
3.将进行决策之后的新状态的二元组加入队尾, 同时作 Graham−Scan 式的更新操作维护凸壳。
算法的时间复杂度为 O(n) 。
再来观察这个转移方程:
很明显,斜率 −x[i] 单调递减, y[j+1] 单调递减,满足条件。上斜率优化就行了。
#include
#include
#include
#include
using namespace std;
const int Maxn=5e4+50;
streambuf *ib,*ob;
typedef long long ll;
inline void init()
{
ios::sync_with_stdio(false);
cin.tie(NULL);cout.tie(NULL);
ib=cin.rdbuf();ob=cout.rdbuf();
}
inline int read()
{
char ch=ib->sbumpc();int i=0,f=1;
while(!isdigit(ch)){if(ch=='-')f=-1;ch=ib->sbumpc();}
while(isdigit(ch)){i=(i<<1)+(i<<3)+ch-'0';ch=ib->sbumpc();}
return i*f;
}
int buf[50];
inline void W(ll x)
{
if(!x){ob->sputc('0');return;}
if(x<0){ob->sputc('-');x=-x;}
while(x)buf[++buf[0]]=x%10,x/=10;
while(buf[0])ob->sputc(buf[buf[0]--]+'0');
}
struct point
{
ll x,y;
point(ll x=0,ll y=0):x(x),y(y){}
friend inline point operator -(const point &a,const point &b)
{
return point(a.x-b.x,a.y-b.y);
}
friend inline ll dot(const point &a,const point &b)
{
return a.x*b.y-a.y*b.x;
}
}p[Maxn],q[Maxn];
int n,tot,head,tail;
ll f[Maxn];
inline bool comp(const point &a,const point &b)
{
return a.y>b.y||(a.y==b.y&&a.x>b.x);
}
inline ll calc(int pos,int x)
{
return q[pos].y+p[x].x*q[pos].x;
}
int main()
{
init();
n=read();
for(int i=1;i<=n;i++)
{
int x=read(),y=read();
p[i]=point(x,y);
}
sort(p+1,p+n+1,comp);
int mx=p[tot=1].x;
for(int i=2;i<=n;i++)
{
if(p[i].x<=mx)continue;
p[++tot]=p[i];mx=p[i].x;
}
q[head=tail=1]=point(p[1].y,0);
for(int i=1;i<=tot;i++)
{
while(head=calc(head+1,i))head++;
f[i]=calc(head,i);
while(tail>=2&&dot(point(p[i+1].y,f[i])-q[tail-1],q[tail]-q[tail-1])<=0)tail--;
q[++tail]=point(p[i+1].y,f[i]);
if(head>tail)head=tail;
}
W(f[tot]);ob->sputc('\n');
}
转移方程:
设 x=b[j],y=f[j]+b[j]2 ,显然 2∗a[i] 单调增, x 单调增,可以上板了。
设所有前面的仓库转移到该仓库花费费用为 Dilivercost[i] ,前面仓库的容量总和为 Sum[i] ,该仓库距离为 i ,建造所花费用 Buildcost[i] 。那么有转移:
同样写出来可以斜率优化。
(a只有小于0维护上凸包,若a大于0则维护下凸包)
这个没什么好说,用平衡树维护,做到 nlogn (CDQ分治也可以做)
转移方程
不满足单调,一般两种解法:
1.CDQ分治,保证处理完左边之后维护左半凸包并处理右边:http://paste.ubuntu.com/25699099/
2.set(平衡树)维护凸包,当然这种操作没有CDQ简便,最好写CDQ.:http://paste.ubuntu.com/25699108/