序列动态DP讲稿

【题目背景】

数据下载:
链接:https://pan.baidu.com/s/1b6aqFPdOhKvj3XjPZVsT9w
提取码:o1if

关于此题, Luckyblock曾经写了个\(O(nm \log n)\)的sb程序,
误以为是\(m\log n\), 觉得自己很强, 结果被暴力踩了.
复杂度错了就是一个死, 被cdx当了笑料
在此 希望大家不要重蹈覆辙.


【题目描述】

一段区间的价值的定义如下:
可在区间内取任意个数, 这些数位置不能重复 且不能相邻.
其和的最大值为这段区间的价值.
如有序列(1,-1,-2,3,4,2,-1),则区间[4,6]的价值为5。

给定一数列, 要求支持下列两种操作:

  1. 单点修改
  2. 查询给定区间价值

【输入格式】

第一行包含两个整数 \(n\), \(m\), 表示数列长度和操作个数.
第二行包括\(n\)个用空格分隔的整数, 其中第\(i\)个数表示数列第\(i\)项的初始值.
接下来\(m\)行每行包括\(3\)个整数, 表示一个操作, 具体如下:

  1. 1 x y 将第x个位置修改为 y.
  2. 2 x y 查询区间\([x,y]\)的价值.

【输出格式】

输出包含若干行整数, 即为所有操作2的结果.


【样例输入】

6 3
1 1 4 5 1 4
2 1 6
1 6 41919
2 1 6

【样例输出】

10
41925

【数据范围】

对于\(30\%\)的数据, \(n,m\le 10\)
对于\(60\%\)的数据, \(n,m\le 3000\)
对于\(100\%\)的数据, \(n,m\le 10^5\).


【限制】

1000ms
256MB


真模板题


Solution

30% 数据:

\(n,m \le 10\)
专门造了两组数据 \(n,m = 5\)
您可以用各种神仙复杂度水过去。
没有特定的算法,也没有人写这个部分的分。


60% 数据:

\(n,m \le 3000\)
显然是个\(n^2\)的算法。

考虑只有一次询问,和最大子段和类似,可以DP。
\(f[i][0]\) 表示第i位不选时 最大和,
\(f[i][1]\)表示第i位不选时 最大和。

则有状态转移方程:
\(f[i][0] = \max(f[i-1][0],f[i-1][1])\)
\(f[i][1] = f[i - 1][0] + a[i]\)
即对第i位是否选择的分类讨论过程。

单次查询复杂度\(O(n)\)
修改操作直接\(O(1)\)修改a[i]的值。
有m次操作,算法总复杂度\(O(nm)\)

正确性比较显然,60分的人也很多,不再赘述了。
话说为什么你们的**都这么神似.

//
/*
By:Luckyblock
*/
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <ctype.h>
#define max std::max
#define ll long long
const int MARX = 1e5 + 10;
//=============================================================
int N, M, a[MARX], f[MARX][2];
//=============================================================
inline int read()
{
    int f = 1, w = 0; char ch = getchar();
    for(; !isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
    for(; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
    return f * w;
}
int Query(int L, int R)
{
    f[L - 1][0] = f[L - 1][1] = 0;
    for(int i = L; i <= R; i ++)
      f[i][0] = max(f[i - 1][0], f[i - 1][1]),
      f[i][1] = f[i - 1][0] + a[i];
    return max(f[R][0], f[R][1]);
}
//=============================================================
signed main()
{
    N = read(), M = read();
    for(int i = 1; i <= N; i ++) a[i] = read();
    for(int i = 1; i <= M; i ++)
    {
      int opt = read(), x = read(), y = read();
      if(opt == 1) a[x] = y;
      if(opt == 2) printf("%d\n", Query(x, y));
    }
    // system("pause");
}

100% 数据:

仔细端详状态转移方程,
可以发现它非常简约,非常优美。
从状态转移方程入手,进行优化。

定义广义矩阵乘法\(A\times B = C\)为:

\[\large C_{(i,j)} = \max\limits_{k=1}^{n}(A_{(i,k)} +B_{(k,j)}) \]

大概是这种感觉:

pic


对于 \(f[i - 1][0], f[i - 1][1]\),有:

\[\large \begin{bmatrix}f[i - 1][1]&f[i - 1][0]\end{bmatrix} \times \begin{bmatrix}-\inf&0\\a[i]&0\end{bmatrix} = \begin{bmatrix}f[i][1]&f[i][0]\end{bmatrix} \]

大概是这种感觉:

pic


初始值 \(f[L - 1][0] = f[L - 1][1] = 0\)
\([L,R]\)的价值 = \(\begin{bmatrix}0 & 0\end{bmatrix}\times \prod\limits_{i=L}^{R}A_i\)

简单证明后,发现广义矩阵乘法满足结合律。
若求得区间矩阵乘积,即可直接求得区间价值。

区间矩阵乘积 可用线段树维护。
单点修改时,修改叶节点矩阵中\(a[i]\)的值,再向上更新乘积即可,
大概是这种感觉:

pic


上代码:

//知识点:DDP
/*
By:Luckyblock
*/
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <ctype.h>
#define lson (now << 1)
#define rson (now << 1 | 1)
#define ll long long
#define max std::max
const int INF = 0x3f3f3f3f;
const int MARX = 1e5 + 10;
//=============================================================
struct Matrix
{
    int a[2][2];
    Matrix() {memset(a, 0, sizeof(a)); }
    Matrix operator * (const Matrix &b) const
    {
      Matrix ans;
      for(int i = 0; i < 2; i ++)
        for(int j = 0; j < 2; j ++)
        {
          int res = -INF;
          for(int k = 0; k < 2; k ++) res = max(res, a[i][k] + b.a[k][j]);
          ans.a[i][j] = res;
        }
      return ans;
    }
};
struct SegmentTree
{
    int L, R;
    Matrix sum;
} t[MARX << 2];
int N, M, val[MARX];
//=============================================================
inline int read()
{
    int f = 1, w = 0; char ch = getchar();
    for(; !isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
    for(; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
    return f * w;
}
void Pushup(int now) {t[now].sum = t[lson].sum * t[rson].sum;}
void Build(int now, int L, int R)
{
    t[now].L = L, t[now].R = R;
    if(L == R)
    {
      t[now].sum.a[0][0] = -INF,
      t[now].sum.a[1][0] = val[L];
      return ;
    }
    int mid = (L + R) >> 1;
    Build(lson, L, mid), Build(rson, mid + 1, R);
    Pushup(now);
}
void Modify(int now, int Pos, int Val)
{
    if(t[now].L == t[now].R) {t[now].sum.a[1][0] = Val; return ;}
    int mid = (t[now].L + t[now].R) >> 1;
    if(Pos <= mid) Modify(lson, Pos, Val);
    else Modify(rson, Pos, Val);
    Pushup(now);
}
Matrix Query(int now, int L, int R)
{
    if(L <= t[now].L && t[now].R <= R) return t[now].sum;
    int mid = (t[now].L + t[now].R) >> 1;
    if(R <= mid) return Query(lson, L, R);
    else if(L > mid) return Query(rson, L, R);
    else return Query(lson, L, R) * Query(rson, L, R);
}
//=============================================================
signed main()
{
    N = read(), M = read();
    for(int i = 1; i <= N; i ++) val[i] = read();
    Build(1, 1, N);
    for(int i = 1; i <= M; i ++)
    {
      int opt = read(), x = read(), y = read();
      if(opt == 1) Modify(1, x, y);
      else
      {
        Matrix ans;
        ans = ans * Query(1, x, y);
        printf("%d\n", max(ans.a[0][0], ans.a[0][1]));
      }
    }
    system("pause");
    return 0;
}

www

好了,你现在学会了序列动态DP了www
虽然不可思议,但动态DP就是这么简单(大雾雾雾


把这题扔到树上去,就变成了这个题:

洛谷 P4719 【模板】动态 DP
给定一棵树, 点带点权.
有m次操作, 每次操作给定x, y表示 修改点x的权值为y.
每次操作后 给出这棵树的最大权独立集的权值大小.

请大家愉快地切掉这道\(\color{black} {\text{NOI/NOI+/CTSC}}\)吧(((

posted @ 2019-12-31 21:56  Luckyblock  阅读(383)  评论(3编辑  收藏  举报