CDQ分治优化DP

昨天被学军的公开赛虐傻了,才发现自己还不会用CDQ优化DP,吓得赶紧去填坑。。。

普通的CDQ就是对二分操作,计算前半部分的插入对后半部分的询问的影响。

那么如何用CDQ优化DP呢?

看一道例题:

NOI2007 cash

不难推出平方的dp方程:

f[i] = max(f[i-1], f[j]/(R[j]*A[j]+B[j])*R[j]*A[i] + f[j]/(R[j]*A[j]+B[j])*B[i])   (j

复杂度是O(n^2)的

由于这些变量中并不存在单调关系,所以使不能用单调性优化的

观察式子,我们设x[j] = f[j]/(R[j]*A[j]+B[j])*R[j],y[j] = f[j]/(R[j]*A[j]+B[j])

不难吧转移方程写成如下形式

f[i] = max(f[i-1], x[j]*A[i] + y[j]*B[i])   (j

点积的形式?

不难发现,这样的最大值一定是在点集(x[j],y[j])的凸包上的点,更确切的说,这个点能与点(A[i],B[i])组成凸包的偏上的切线

很明显我们可以用平衡树来维护上凸壳,然后再凸壳上找到切线。

问题似乎的到了解决,但是这样的变成复杂度太高了,几乎很难在赛场上写出并调试正确。

于是我们就有了一个强有力的替代品——CDQ分治。

我们可以看到dp的过程就是不断的计算出新的f值再用其组成新的点来放进凸包中

其实我们可以把这个过程理解成这样:计算f[1],在平面上插入(x[1],y[1]),计算f[2],在平面上插入(x[2],y[2])……

这样我们就可以用CDQ分治来优化这个过程了

具体过程如下:

Solve(l,r)

     Solve(l,mid)

     利用f[l~mid]更新f[mid+1,r]

     Solve(mid+1,r)

这题就是利用l~mid之间的点组成的凸壳来更新mid+1~r的dp值,由于凸包上的边斜率单调,所以可以离线后把询问的斜率排序,O(n)搞出来

由于用到了排序,所以总的复杂度为O(nlog^2n)

代码很短的,而我写得很丑。。。。

#include 
#include 
#include 
 
using namespace std;
const double Eps = 1e-12;
const int Maxn = 100005;
int n,i,j,cnt1,cnt2,tot;
double f[Maxn], R[Maxn];
double A[Maxn], B[Maxn];
int p[Maxn];
 
struct Point{
  double x,y;
  Point operator -(const Point &a)const{
    return (Point){x-a.x, y-a.y};
  }
  bool operator <(const Point &a)const{
    return (x>1, k=0;
  for (int i=l,j=mid+1;i<=mid||j<=r;k++)
    if (j>r || (i<=mid && q[i]f[l]) f[l] = f[l-1];
    dot[l].x = f[l]/(R[l]*A[l]+B[l])*R[l];
    dot[l].y = f[l]/(R[l]*A[l]+B[l]);
    return;
  }
  int mid = (l+r)>>1;
  solve(l,mid);
 
  up[cnt1=1] = dot[l];
  for (int i=l+1;i<=mid;i++){
    while (cnt1>1 && mult(up[cnt1-1],up[cnt1],dot[i])>=-Eps) cnt1--;
    up[++cnt1] = dot[i];
  }
  dw[cnt2=1] = dot[l];
  for (int i=l+1;i<=mid;i++){
    while (cnt2>1 && mult(dw[cnt2-1],dw[cnt2],dot[i])<=Eps) cnt2--;
    dw[++cnt2] = dot[i];
  }
  tot = 0;
  for (int i=1;i1;i--) con[++tot] = dw[i];
  if (tot==0) con[++tot]=dot[l];
 
  if (tot<=2){
    for (int i=mid+1;i<=r;i++)
      for (int j=1;j<=tot;j++)
      if (f[i] Eps ) j=j%tot+1;
      if (f[p[i]]

回到学军模拟赛19T1,

同样可以先把方程列出来:

f[i] = min(f[j]+(a[i]-a[j])^2)  (j=b[i])

与上题不同的是这里多了一个a[j]>=b[i]的限制,我们逐步来分析。

如果a[i]单调,这很明显是可以用单调性来优化的。

我们可以利用CDQ强行上单调性,也就是说把l~mid强行按a值排序再更新mid+1~r——就是强行造出单调队列,再在这个队列里二分找到最优解

题目里面给了不考虑b的限制的部分分,利用上面的方法就可以了

加上b的限制也是很简单的:把mid+1~r这部分按b排序,在插入l~mid的时候,适时地查询更新mid+1~r的解,复杂度不变

#include 
#include 
#include 
#include 
 
using namespace std;
const int Maxn = 100005;
typedef long long LL;
LL f[Maxn];
int A[Maxn], B[Maxn];
int q[Maxn], p[Maxn];
int n,i,head,tail;
 
bool cmpA(const int &a,const int &b){
  return A[a] > A[b];
}
 
bool cmpB(const int &a,const int &b){
  return B[a] > B[b];
}
 
LL F(int j,int k){
  return ( f[j]+(LL)A[j]*A[j] )-( f[k]+(LL)A[k]*A[k] );
}
 
LL G(int j,int k){
  return (A[j]-A[k]);
}
 
LL cc(int j,int i){
  return f[j] + (LL)(A[j]-A[i])*(A[j]-A[i]);
}
 
LL calc(int x){
  if (head>tail) return f[n+1];
  LL ret = tail;
  int L = 1, R = tail-1;
  while (L<=R){
    int mid = (L+R)>>1;
    if (cc(q[mid],x) < cc(q[mid+1],x))
      R = mid-1, ret = mid;
    else L = mid+1;
  }
  ret = q[ret];
  return cc(ret,x);
}
 
void solve(int l,int r){
  if (l==r) return;
  int mid = (l+r)>>1;
  solve(l,mid);
 
  int i,j;
  for (i=l;i<=r;i++)
    p[i] = i;
  sort(p+l,p+mid+1,cmpA);
  sort(p+mid+1,p+r+1,cmpB);
  head = 1; tail = 0;
  for (i=l,j=mid+1;i<=mid;i++){
    while (j<=r && B[p[j]]>A[p[i]]){
      LL tmp = calc(p[j]);
      if (tmp=f[n+1]) printf("-1\n");
    else
  printf("%.4lf\n",(double)sqrt((long double)f[n]) );
  return 0;
}


你可能感兴趣的:(CDQ分治,dp&递推,日常)