现阶段DP优化

noip 范围的dp 优化

  • 加速状态转移

    1. 前缀和优化(计数)
    2. 单调队列优化
    3. 树状数组或线段树优化(线段树区间和阔以)

    原理:找到数字之间的规律,加速状态

  • 精简状态

    对于基本状态,应使得式子精简,

前缀和优化

长度 \(n\) 逆序对为 \(k\) 的排列有几锅?

\(K <=200\)

\(k <= 2000\)

排列计数问题经典套路

满足排列套路,把1-n往后查,

\(dp[i][j]\)\(i\) 个数 产生 \(j\) 的逆序对的方案数,

因为新插入的数是更大的所以,分别考虑插在最后边,次后边...

\(dp[i+1][j+x]+=dp[i][j](x\ belong\ 0,1,2,...i+1)\)

往后转移

上面的式子向前转移为

\(dp[i][j] = \sum_{k=0}^{i-1}dp[i-1][j-k]\)

我们通过式子可以发现有取和的操作,如果每次操作都开一层循环来计算的话,时间复杂度会多以个 \(n\), 但是我们用前缀和优化则可以 \(O(1)\) 实现

做法如下:

方程:上述

我们设:

\(f[i][j] = \sum_{k=0}^{j}dp[i][k]\)

表示前 \(i\) 个数 产生 \(j\) 的逆序对的方案数的总和(\(i\) 固定)

则转移为:

\(dp[i][j] = f[i-1][j]-f[i-1][j-i]\)

这样直接转移则可以 \(O(1)\)

loj6077

在这个 \(dp\) 基础上,做容斥原理,通过之前讲的整数划分的模型 \(dp\) 求出容斥系数即可

总结

发现求和操作可以利用前缀和相减, 达到 \(O(1)\)

转移区间是求和问题

单调队列优化(区间最大值)

\(O(n)\) 转移 均摊到每个转移复杂度变小

一般形式

\[dp[i] = max\{f[j]\}+g[i]\\ dp[i] = max\{dp[j]+f[j]\}+g[i]\\ dp[l][i] = max\{dp[l-1][j]+f[j]\}+g[i] \]

注\ \(j\) 取值是一段连续区间,区间的两个端点随 \(i\) 的增大而增大的区间

如果左端固定,那么可以直接记录前缀最小值(固定扩大窗口,记录前缀和即可)

bzoj 1855 股票买卖

暴力DP状态

\(f[i][j]=\begin{cases}f[i-1][j]&no\ do\\f[i-1-w][k]-ap[i]\times(j-k)&buy\\ f[i-1-w][k]+bp[i]\times(k-j)&sell\end{cases}\)

解释一下为什么上边的\(j\), \(k\) 的关系,

当买入时, 股票数在增加,故 \(j > k\)

当卖出时, 股票数在减少,故 \(k > j\)

单调队列优化

取出其中一个化简得

\(f[i][j] = f[i-w-1][k]+ap[i]\times k- ap[i] \times j\)

发现存在 \(k\) 的式子是与先前的状态有关的,只有后一式子对\(i,j\) 的才产生影响,这种模型符合单调队列,

并且我们需要的决策点 \(k\)\(j\) 存在一定的区间关系,即当 \(j\) 改变时,\(k\) 也在移动,进而形成了一个固定区间的移动窗口,以为窗口右移,存在单调性,故可以用单调队列

亮眼的地方:找到与坐标无关的(坐标值表包括决策内容,即\(j\))和 与坐标有关的

利用的原因,我们想减少时间复杂度,因此可以将 \(k\) 那一维省去,利用单调队列,让队列中时刻保持当前最有用的多个决策点(这也是单调队列的普遍原理)

这样我们可以在转移时总时间复杂度达到 \(O(N)\) 转移,均摊下来就是 \(O(N)\)

买入部分

for (int j = 0; j <= maxp; j++)
{
	f[i][j] = f[i-1][j]//no do
	if (i <= w)//当不够w时,区间 w 只能进行一次操作,并且只能买股票
	{
		if(j <= as[i]) f[i][j] = max(f[i][j], j*ap[i])//控制买的数量,as表示一天买入最大值
	}
	while (head <= tail && id[head] < j - as[i]) head++;
    if (head <= tail) f[i][j] = max(f[i][j], q[head] - j * ap[i]);
    while (head <= tail && q[tail] < f[i - w - 1][j] + j * ap[i]) tail--;
    q[++tail] = f[i - w - 1][j] + j * ap[i];
    id[tail] = j;
}

hdu4374

误区: 当时我自己设的状态为 \(i\) 层 走了 \(j\) 步的金币最大值

但是问题是 \(j\) 仅仅只知道步数而不知道具体位置在哪,是无法进行转移的(无方向性)

这也告诉我:状态的设定要关系到前后的传递性

之后我有再次推出正确的转移式子,这也是今天推出的第一个式子(我太菜了

思路

\(dp[i][j]\) 表示 从i层走完了到达 \(i\)\(j\) 位置时的最大金币数。

\(sum[i][j]\) 表示 \([i,j]\)\([i,1]\) 的前缀和即金币数

那么转移式子:

\[dp[i][j] = max\{dp[i-1][k]+sum[i][j]-sum[i][k-1]\}\\ dp[i][j] = max\{dp[i-1][k]+sum[i][k]-sum[i][j-1]\} \]

查阅了一下资料解答了我的疑惑

我的疑惑,我自己的递推式是

\(f[i][j] = max\{f[i][k]+sum[j]-sum[k]\}\)

按照端点的思想,就是应该在同层枚举断点才对呀

而正解值确实由 \(i-1\) 即上一层转移而来

解答:我的递推式有个问题,就是只能同层转移而并不能全局转移,正解的转移刚好可以弥补这个缺点,而且考虑的方向只需左右

那么式子化简得

\(dp[i][j] = max\{dp[i-1][k]-sum[i][k-1]\}+sum[i][j]\)

模板,利用单调队列维护即可了

int n,m,x,t,a[105][10005],dp[105][10005],sum[105][10005];
int q[10005],front,rear,ans_l[10005],ans_r[10005];
 
int main()
{
    while(~scanf("%d%d%d%d",&n,&m,&x,&t))
    {
        memset(dp,0,sizeof(dp));
        memset(sum,0,sizeof(sum));
        for(int i=1; i<=n; i++)
        {
            for(int j=1; j<=m; j++)
            {
                scanf("%d",&a[i][j]);
                sum[i][j]=sum[i][j-1]+a[i][j];
            }
        }
        for(int i=n; i>=1; i--)
        {
            front=0;
            rear=0;
            q[rear++]=1;
            ans_l[1]=1;
            for(int j=2; j<=m; j++)
            {
                while(front<rear&&dp[i+1][q[rear-1]]-sum[i][q[rear-1]-1]<=dp[i+1][j]-sum[i][j-1])
                {
                    rear--;
                }
                q[rear++]=j;
                if(q[front]+t<j)
                    front++;
                ans_l[j]=q[front];
            }
            front=0;
            rear=0;
            q[rear++]=m;
            ans_r[m]=m;
            for(int j=m-1; j>=1; j--)
            {
                while(front<rear&&dp[i+1][q[rear-1]]+sum[i][q[rear-1]]<=dp[i+1][j]+sum[i][j])
                {
                    rear--;
                }
                q[rear++]=j;
                if(q[front]-t>j)
                    front++;
                ans_r[j]=q[front];
            }
            int t1,t2;
            for(int j=1; j<=m; j++)
            {
                t1=dp[i+1][ans_l[j]]+sum[i][j]-sum[i][ans_l[j]-1];
                t2=dp[i+1][ans_r[j]]+sum[i][ans_r[j]]-sum[i][j-1];
                dp[i][j]=max(t1,t2);
            }
        }
        printf("%d\n",dp[1][x]);
    }
    return 0;
}

常用范围

多重背包

基环树求直径

区别

最大值和方案数

转移都是以区间决策点方案求和的话就可以前缀和相减

前缀和--决策区间不一定单调递增---计数

单调队列--决策区间必须是单调区间,注意是决策区间---最优dp值

线段树或者树状数组优化

LIS加强版

\(n <= 10^5\)

看出来直接套就可以了

条件: 区间最值,带修改

这里解释一下区间最值和修改在哪里(以后会忘

修改的地方为每次转移时 \(dp[i]\) 值的更新(重点!)

线段树和树状数组

特征 每次坐标小于某个值的权值中的最大值

用数学语言讲:\(j<i,a[j]=max(1....i-1)\)

权值作为下标记录区间,权值线段树, 数据结构维护的是权值,而非位置

时间复杂度 \(O(NlogN)\)

\[f[i]=max\{f[j]|a[i] < a[j],i>j\}+1 \]

void updata(int p, int dp)
{
	for (int i = p; i <= n; i += i & -i) mx[i] = max(mx[i] , dp);
}//单点更改,用dp值进行操作
// 若当钱p更改后,那么后面的一直到n都会更改,这就有效的维护了dp的性质,不断更新,且对未知位置也是新的更改,(后面的位置)
int query(int p)
{
	int ans = 0;
	for (int i = p; i; i -= i & -i) ans = max(ans, mx[i]);
	return ans;//转移时就可 logn 得到答案
}

总结

优化是在暴力式子上的简便,所以式子是前提

精简状态

挖掘题目性质特点

LIS 加长版

因为比较大小,无序了解数字的具体,利用离散化

换一思路

找一个最大值 \(k\) 满足条件 存在 \(dp[j]=k\&\& a[j] < a[i]\)

按照我的理解,以下是贪心做法的正确性

  1. \(h[k]\) 表示 \(dp[j] =k\) 的所有 \(j\) 当中最小的 \(a[j]\),言而简之\(a[j]\) 就是当前长度为 \(k\) 的最长上升子序列的最小值
  2. 利用这一特性可以比较 \(h[k]\) 进而比较更优决策
  3. \(h[k]\) 满足单调不下降,就是说“长度为 \(k\) 的最后一个最小值,一定小于 \(k+1\) 的最小值”,如果不满足则后者优于前者
  4. 这样对于一个 \(a[i]\) 就可进行替换,找到最大的 \(k\), 满足 \(h[k]\) 是小于 \(a[i]\) 的,之后就可以 \(dp[i] = k + 1\)

如果前者大于后者,那么后者长度为k+1,那么可以提取k的子序列,他的元素要比上一个要小,那么上一就不是最小值了,不符合定义条件

总的来说:二分 + 贪心(正确性即奇妙的性质)

//len == k
int n, a[B], f[B], len = 1; 
int main() {
  cin >> n;
  for(int i = 1; i <= n; i++) cin >> a[i], f[i] = inf;
  f[1] = a[1];//初始化
  for (int i = 2; i <= n; i++){
    if(a[i] > f[len]) f[++len] = a[i];
    else {
      int l = 0, r = len;
      while(l < r){
        int mid = (l + r) / 2;
        if(f[mid] > a[i]) r = mid;
        else l = mid + 1;
      }
      f[l] = min(a[i], f[l]);
    }
  }
  cout << len << endl;
  return 0;
}

STL简化

for (int i = 2; i <= n; i++)
  if(f[len] <= a[i]) f[++len] = a[i];
  else{
    int p = upper_bound(f + 1, f + 1 + len, a[i]) - d;
    f[p] = i;
  }
  cout << len;
  /*
    用upper_bound 而不用  lower_bound
    是因为 上升序列允许相等,所以替换时只替换大于 
  */

Noip 2008 传纸条

暴力的思路其实感觉也挺难想的(上课没想出来

先分析主干条件,两张纸袋同时传递而且不能重合那么为了保证不重合就必须知道他的位置,故我们用位置作为维度表示状态:

\(f[i][j][x][y]\) 表示两纸分别从起点到 \((i,j),(x,y)\) 时的好心程度之和最大,转移有四种状态

\(f[i][j][x][y]=mp[i][j]+mp[x][y]+max\{f[i-1][j][x-1][y],f[i][j-1][x-1][y],f[i-1][j][x][y-1],f[i][j-1][x][y-1]\}\)

打个特判,把冲突的部分冷掉就可以了

最后输出\(f[m][n][m][n]\),注意是同向跑!!!!

但是我们只让他同向但在时间上并没有限制

故我们做出优化,让她们同时走

为什么同时更好呢?

同时走可以只记录他们的纵坐标,这样的复杂度就变成了 \(O(n^3)\) ,位置的表示就可以推出 步数 = 横坐标+纵坐标,枚举三个单位,推出第四个单位,而步数在中间做得是中间量,那么转移状态为:

\(f[i][j][k]= mp[i][j] + mp[k][i+j-k]+max\{f[i-1][j][k-1],f[i-1][j][k],f[i][j-1][k-1],f[i][j-1][k]\}\)

此题可以得出:状态有时并非需要精确表示,如果可以通过某种方式简化位置,但却可以在使用时正确表示位置的话,那么这种答案会更优,这也就是精简式子

#define N 55

int a[N][N];
int opt[2*N][N][N];

int main(){
    int k, x, m, n, add;
    scanf("%d %d", &m, &n);
    for (int i = 1; i <= m; i ++)
        for (int j = 1; j <= n; j ++)
            scanf("%d", &a[i][j]);
    opt[2][1][1] = 0;
        for (k = 3; k <= m + n; k ++)
            for (int i = 1; i <= min(k - 1, m); i ++)
                for (x = 1; x <= min(k - 1, m); x ++){
                    if (i == x) add = a[i][k - i];
                        else add = a[i][k - i] + a[x][k - x];
                    opt[k][i][x] = max(opt[k][i][x],opt[k - 1][i - 1][x - 1]);
                    opt[k][i][x] = max(opt[k][i][x],opt[k - 1][i][x]);
                    opt[k][i][x] = max(opt[k][i][x],opt[k - 1][i - 1][x]);
                    opt[k][i][x] = max(opt[k][i][x],opt[k - 1][i][x - 1]);
                        opt[k][i][x] += add;
                }
    printf("%d\n", opt[m + n][m][m]);

    return 0;
}
posted @ 2021-02-06 21:09  zxsoul  阅读(54)  评论(0编辑  收藏  举报