zoukankan      html  css  js  c++  java
  • DP 从入门到退役

    DP入门

    • 将一个大问题,化解为小问题,通过小问题的最优解进而推出大问题的最优解
    最优化
    • 记忆化,记搜时,开一个数组对已知进行记录
    状态表示
    1. 有最优子结构:子问题的最优解可以有效构造出大问题的最优解
    2. 简洁性:尽可能简化状态
    3. 全面性:要统筹考虑问题

    可以考虑从暴力DP写起

    状转
    • 类似分类讨论 考虑边界
    复杂度
    • 一般为状态数 ( imes)状转复杂度
    优化
    • 压缩维数
    • 加速转移

    最长上升子序列

    (dp_i = {max(dp_j) + 1 | j<i ,a_j < a_i})
    (n^2)枚举的暴力DP


    P1091 合唱队型

    题目传送门
    要保留的人数 = 最长上升子序列 + 最长下降子序列 - 1
    ans = n - 要保留的人数

    #include <cmath>
    #include <cstdio> 
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 1e5 + 100;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int t[N], f[N], g[N];
    int main() {
      int n = read();
      for (int i = 1; i <= n; i++)
        t[i] = read();
      for (int i = 1; i <= n; i++) {
        f[i] = 1;
        for (int j = 1; j < i; j++) {
          if (t[j] < t[i] && f[j] + 1 > f[i])
            f[i] = f[j] + 1;
        }
      }
      for (int i = n; i >= 1; i--) {
        g[i] = 1;
        for (int j = n; j > i; j--) {
          if (t[j] < t[i] && g[j] + 1 > g[i])
            g[i] = g[j] + 1;
        }
      }
      int ans = 0;
      for (int i = 1; i <= n; i++) {
        ans = max(f[i] + g[i] - 1, ans);
      }
      printf("%d", n - ans);
      system("pause");
      return 0;
    }
    

    p1018 乘积最大

    (f_{i,a})表示第(i)位上已经放了(a)个乘号的最大价值
    (f_{i,a} = max{f_{i,a} , f_{j,a-1} *cut_{j+1,i} })

    (cut{x,y}) 表示 (x o y) 这一块的数字

    然后写个高精度即可

    
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 100;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    struct node {
      int v;
      bool exi;
      int c[N];
    } cut[N][10], ans[N];
    int n, k;
    char s[N];
    int a[N];
    node culc(int l, int r) {
      node e;
      e.v = r - l + 1, e.exi = true;
      for (int i = 1; i <= e.v; i++) {
        e.c[i] = a[r - i + 1];
      }
      return e;
    }
    node mul(node e1, node e2) {
      node emul;
      emul.exi = true, emul.v = e1.v + e2.v - 1;
      for (int i = 1; i <= emul.v; i++)
        emul.c[i] = 0;
      for (int i = 1; i <= e1.v; i++) {
        for (int j = 1; j <= e2.v; j++) {
          emul.c[i + j - 1] += e1.c[i] * e2.c[j];
        }
      }
      int q = 0;
      for (int i = 1; i <= emul.v; i++) {
        emul.c[i] += q;
        q = emul.c[i] / 10;
        emul.c[i] %= 10;
      }
      while (q > 0) {
        emul.c[++emul.v] = q % 10;
        q /= 10;
      }
      return emul;
    }
    node Max(node e1, node e2) {
      if (!e1.exi || e1.v < e2.v)
        return e2;
      if (!e2.exi || e2.v < e1.v)
        return e1;
      for (int i = e1.v; i >= 1; i--) {
        if (e1.c[i] > e2.c[i])
          return e1;
        else if (e2.c[i] > e1.c[i])
          return e2;
      }
      return e1;
    }
    
    int main() {
      n = read(), k = read();
      scanf("%s", s);
      for (int i = 0; i < n; i++)
        a[i + 1] = s[i] - '0';
      for (int i = 1; i <= n; i++) {
        ans[i].exi = false;
        for (int j = 1; j <= k; j++)
          cut[i][j].exi = false;
      }
      for (int i = 1; i < n; i++) {
        cut[i][1] = culc(1, i);
        for (int j = 2; j <= k; j++) {
          for (int fr = j - 1; fr < i; fr++) {
            if (cut[fr][j - 1].exi)
              cut[i][j] = Max(cut[i][j],
              mul(cut[fr][j - 1], culc(fr + 1, i)));
          }
        }
        if (cut[i][k].exi) {
          ans[i] = mul(cut[i][k], culc(i + 1, n));
        }
      }
      node lastans;
      lastans.exi = false;
      for (int i = 1; i < n; i++) {
        node temp = Max(ans[i], lastans);
        lastans = temp;
      }
      for (int i = lastans.v; i >= 1; i--) {
        printf("%d", lastans.c[i]);
      }
      system("pause");
      return 0;
    }
    

    最长公共子序列

    (dp_{i,j})表示(S串)(i)为和(T串)(j)位的最长公共子序列

    • (dp_{i,j} = {dp_{i-1,j-1} + 1|S_i = T_j})
    • (dp_{i,j} = {max(dp_{i-1,j},dp_{i,j-1} )|S_i != T_j})
    • (ans = dp_{n,m})

    notice:
    为什么第一个转移没有取( ext{max})?仔细研究一下可以发现它的性质


    最长公共上升子序列 LCIS

    LCIS

    • (dp_{i,j})表示(S串)(i)为和(T串)(j)位的最长公共上升子序列 (t)表示LCIS的结尾位置
    • (dp_{i,j} = {dp_{i- 1, j} | S_i != T_j})
    • (dp_{i,j} = {max(dp_{i - 1,j} , dp_{i-1,t} + 1) | S_i = T_i })
    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <cstring>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 1100;
    int read()  int s = 0 , f = 0 ; char ch = getchar() ;
      while(!isdigit(ch)) f |= (ch == '-') , ch = getchar();
      while(isdigit(ch)) s = s * 10 + (ch ^ 48) , ch = getchar();
      return f ? -s : s;
    }
    int a[N], b[N];
    int f[N][N] ,g[N][N];
    int n ,m;
    void print(int p) {
      if(!p) return;
      print(g[n][p]);
      printf("%d ",b[p]);
    }
    int main() {
      n = read();
      for (int i = 1 ; i <= n ;i++) a[i] = read();
      m = read();
      for (int i = 1 ; i <= m ; i++) b[i] = read();
      for(int i = 1 ; i <= n; i++) {
        int t = 0;
        for(int j = 1; j <= m ;j++) {
          f[i][j] = f[i-1][j];
          g[i][j] = g[i-1][j];
          if(a[i] == b[j] && f[i- 1][t] + 1 > f[i][j]) {
            f[i][j] = f[i- 1][t] + 1;
            g[i][j] = t;
          }
          if(b[j] < a[i] && f[i - 1][j] > f[i - 1][t]) 
           t = j;
        }
      }
      int p = 0;
      for(int i = 1 ; i <= m ;i++) {
        if(f[n][i] > f[n][p]) p = i;
      }
      cout << f[n][p] <<"
    ";
      print(p);
    //   system("pause");
      return 0;
    }
    

    记搜

    开数组记录已有的答案,减少递归的层数

    滑雪
    solution:

    口胡一个做法; (n^2)枚举起点,会发现每一个点的能到达的点的个数是一定的,可以考虑记忆每个能到达的位置的个数,直接(n^3)暴力即可

    code:

    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 110;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int mapp[N][N];
    int ans[N][N], n, m;
    const int dx[4] = {0, 0, 1, -1};
    const int dy[4] = {1, -1, 0, 0};
    int dfs(int x, int y) {
      if (ans[x][y])
        return ans[x][y];
      ans[x][y] = 1;
      for (int i = 0; i < 4; i++) {
        if (x + dx[i] < 1 || x + dx[i] > n || y + dy[i] < 1 || y + dy[i] > m)
          continue;
        if (mapp[x][y] > mapp[x + dx[i]][y + dy[i]]) {
          dfs(x + dx[i], y + dy[i]);
          ans[x][y] = max(ans[x][y], ans[x + dx[i]][y + dy[i]] + 1);
        }
      }
      return ans[x][y];
    }
    int main() {
      n = read(), m = read();
      for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++)
          mapp[i][j] = read();
      }
      int ANS = -1;
      for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
          ANS = max(dfs(i, j), ANS);
        }
      }
      printf("%d", ANS);
      system("pause");
      return 0;
    }
    

    拓扑上DP

    DAG上DP
    扔一道计数DP
    食物链
    记搜板子题

    因为是( ext{DAG}) 会发现每个点所能到达的食物链的最底端是一定的不是DAG也有这样的性质 对每个搜索到的点所能到达的入度为零的点进行计数,从每个入度为零的点开始搜索,食物链是从头到尾全着才行对每次搜素统计答案即可

    code

    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <queue>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 2e5 + 100;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    struct Edge {
      int from, to, net;
    } e[N << 1];
    int head[N], nume;
    void add_edge(int from, int to) {
      e[++nume] = (Edge){from, to, head[from]};
      head[from] = nume;
    }
    int rd[N], cd[N];
    queue<int> q;
    ll dp[N];
    ll dfs(int x) {
      if (dp[x])
        return dp[x];
      ll ans = 0;
      if (!cd[x] && rd[x])
        ans++;
      for (int i = head[x]; i; i = e[i].net) {
        int to = e[i].to;
        ans += dfs(to);
      }
      dp[x] = ans;
      return ans;
    }
    int main() {
      int n = read(), m = read();
      for (int i = 1, u, v, w; i <= m; i++) {
        u = read(), v = read();
        add_edge(u, v);
        rd[v]++;
        cd[u]++;
      }
      ll ans = 0;
      for (int i = 1; i <= n; i++) {
        if (!rd[i])
          ans += dfs(i);
      }
      printf("%lld", ans);
      system("pause");
      return 0;
    }
    

    序列DP

    物流运输

    • (f_i)表示前(i)天的运输最小成本
    • (f_i = min{f_j + w( j + 1 ,i) * (i - j) | j < i})
    • 然后在图中跑一个最短路,求出(w(j+1,i))的权值,( ext{n<=100})暴力 (DP) 即可

    上面的式子太抽象了,考虑一下一些细节如果写:

    1. 首先输入的时候处理一个(sig_{i,j})数组,表示第(i)个码头到第(j)能否经过这条道路,(1)表示不行(0)表示可以通过

    2. 再开一个(dis_{i,j})数组表示第([i,j])区间(指日期)内到达(m)的最短距离,考虑如何求得(dis)数组,暴力即可,因为要求多遍的最短路,重新开一个数组(sel)(sig)里面所存的限制转移到(sel)内,转移完成就跑一遍短路,记录到(dis)内,(dis)数组所存的值就是上面转移方程中(w(j+1,i))

    3. 最后写一个(n^2)的状转即可

    4. 考虑一个问题,如果(dis)内存是一个极大值,即不能通过,怎么办?那就直接赋值为极大值,因为不能通过所以一定会被舍弃掉

    notice:

    • 注意每次跑最短路的时候最短路数组,标记数组也要清空
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <queue>
    #include <string>
    #define ll long long
    
    using namespace std;
    const int N = 1120;
    const int inf = 0x3f3f3f3f;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    struct Edge {
      int from, to, dis, net;
    } e[N << 1];
    int head[N], nume;
    void add_edge(int from, int to, int dis) {
      e[++nume] = (Edge){from, to, dis, head[from]};
      head[from] = nume;
    }
    struct node {
      int po, dis;
      bool operator<(const node& b) const { return dis > b.dis; }
    };
    priority_queue<node> q;
    int Dis[N], dis[N][N], dp[N];
    bool sig[N][N], sel[N];
    int n, m, k, ee;
    bool vis[N];
    int dij() {
      memset(Dis, 0x3f, sizeof(Dis));
      memset(vis, 0, sizeof(vis));
      Dis[1] = 0;
      q.push((node){1, 0});
      while (!q.empty()) {
        node fr = q.top();
        q.pop();
        if (vis[fr.po])
          continue;
        vis[fr.po] = true;
        for (int i = head[fr.po]; i; i = e[i].net) {
          int to = e[i].to;
          if (sel[to])
            continue;
          if (Dis[to] > Dis[fr.po] + e[i].dis) {
            Dis[to] = Dis[fr.po] + e[i].dis;
            if (!vis[to]) {
              q.push((node){to, Dis[to]});
            }
          }
        }
      }
      return Dis[m];
    }
    int main() {
      memset(dis, 63, sizeof(dis));
      n = read(), m = read(), k = read(), ee = read();
      for (int i = 1, u, v, w; i <= ee; i++) {
        u = read(), v = read(), w = read();
        add_edge(u, v, w), add_edge(v, u, w);
      }
      int d = read();
      for (int i = 1; i <= d; i++) {
        int t = read(), x = read(), y = read();
        for (int j = x; j <= y; j++)
          sig[t][j] = 1;
      }
      for (int i = 1; i <= n; i++) {
        for (int j = i; j <= n; j++) {
          memset(sel, 0, sizeof(sel));
          for (int l = i; l <= j; l++)
            for (int r = 1; r <= m; r++)
              sel[r] |= sig[r][l];
          dis[i][j] = dij();
        }
      }
      dp[0] = 0;
      for (int i = 1; i <= n; i++) {
        dp[i] = (dis[1][i] == inf) ? inf : dis[1][i] * i;
        for (int j = 0; j < i; j++) {
          if (dis[j + 1][i] == inf)
            continue;
          dp[i] = min(dp[i], dp[j] + dis[j + 1][i] * (i - j) + k);
        }
      }
      printf("%d", dp[n]);
      system("pause");
      return 0;
    }
    

    粉刷匠

    • 一条木板:(g_{i,j})表示前(i)个格子刷了(j)次最多正确的格子

    • (g_{i,j} = max{g_{k,j-1} + w(k+1,i)~~ |~~ k< i})

    • 多条木板: (f_{i,j})表示前(i)个木板刷(j)次的最大答案

    • (f_{i,j} = max{f_{i-1,k} + g_{i,m,j-k}})

    最后一个转移方程中 (g) 多了一维,这一维的意思是第几块木板

    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 70;
    const int M = 3000;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int n, m, t;
    char s[N * 10][M];
    
    int g[N][N][M];
    int dp[N][M];
    int val(int id, int l, int r) {
      int ans = 0;
      for (int i = l; i <= r; i++)
        ans += (s[id][i] == '0');
      return max(ans, r - l + 1 - ans);
    }
    int main() {
      n = read(), m = read(), t = read();
      for (int i = 1; i <= n; i++)
        scanf("%s", s[i] + 1);
      for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
          for (int k = 1; k <= m; k++)
            for (int l = k - 1; l <= j; l++)
              g[i][j][k] = max(g[i][j][k], g[i][l][k - 1] + val(i, l + 1, j));
      for (int i = 1; i <= n; i++)
        for (int j = 1; j <= t; j++)
          for (int k = 0; k <= min(j, m); k++)
            dp[i][j] = max(dp[i][j], dp[i - 1][j - k] + g[i][m][k]);
      int ans = 0;
      for (int i = 1; i <= t; i++) {
        ans = max(ans, dp[n][i]);
      }
      printf("%d", ans);
      return 0;
    }
    
    

    其实可以发现里面有一部分( ext{val})这一块是可以通过前缀和而优化出来的,会降低一维的时间复杂度,但是也无所谓( ext{n,m<=50}),总体的时间复杂度貌似很低,最慢的一个我才跑了100ms完全可以随便写,不写前缀和优化也没问题


    CF314E

    (dp_{i,j})表示到第(i)个字符还有(j)个左括号

    分类讨论

    如果第(i+1)为左括号,则能转移到(dp_{i+1,j+1})

    如何第(i+1)为右括号,则能转移到(dp_{i+1,j-1})

    如果第(i+1)为问号,则能转移到(dp_{i+1,j-1}与dp_{i+1,j+1})

    会发现,第一维对于状转没有任何的限制或影响,省了去就好

    /*
    Author:Imy_isLy
    知识点: 括号匹配,区间DP
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ui unsigned int
    using namespace std;
    const int N = 1e5 + 100;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    char s[N];
    ui dp[N], ans = 1;
    int cntp;
    signed main() {
      int n = read();
      if (n & 1) {
        printf("0");
        return 0;
      }
      int mid = n >> 1;
      dp[0] = 1;
      scanf("%s", s + 1);
      for (int i = 1; i <= n; i++) 
        if (s[i] == '?') {
        for (int j = i >> 1; j >= i - mid && j; j--)
          dp[j] += dp[j - 1];
        } else
          cntp++;
      for (int i = 1; i <= mid - cntp; i++)
         ans = ans * 25;
      cout << ans * dp[mid];
      // system("pause");
      return 0;
    }
    

    bzoj 4922

    卡特兰数

    [f_n = sumlimits_{i=0}^{n-1}f_i imes f_{n-i-1} ]

    1. 1 ~ n 由此进栈,求有多少种出栈序列
    1. 由n对括号形成的合法括号序列,求有多少种排列方式

    单调队列优化DP

    一个人比你小还比你强,那你就退役吧

    琪露诺

    简单DP ? 首先考虑(n^2)的转移方程,因为位置( ext{x})只能由位置([x-r,x-l])转移过来, 那就很明显了

    (f_{i} = max{f_{j} | jin[i-r,i-l] } + a_i)

    这时候考虑优化,会发现,(max{f_{j} | jin[i-r,i-l] })这一块只要取的是
    最大值,很显然可以单调队列优化我也不知道为啥,每次只保留([i-r,i-l])内的最大值即可

    /*
    Author:Imy_isLy
    知识点:单调队列优化DP
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 2e5 + 100;
    const int inf = 2147483647;
    //=============================================================
    int f[N], n, l, r, ans = -inf;
    int q[N], head = 1, tail = 0,a[N];
    //=============================================================
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    
    void insert(int i) {
      for (; f[i] >= f[q[tail]] && tail >= head;)
        tail--;
      q[++tail] = i;
    }
    
    int query(int x) {
      for (; q[head] + r < x;)
        head++;
      return q[head];
    }
    
    int main() {
      memset(f, 128, sizeof(f));
      f[0] = 0;
      n = read(), l = read(), r = read();
      for (int i = 0; i <= n; i++)
        a[i] = read();
      for (int i = l; i <= n; i++) {
        insert(i - l);
        int fr = query(i);
        f[i] = f[fr] + a[i];
        if (i + r > n)
          ans = max(ans, f[i]);
      }
      cout << ans;
      system("pause");
      return 0;
    }
    

    区间DP

    (dp_{i,j})表示区间([l,r])( ext{DP})

    合并石子

    (dp_{i,j})表示区间([i,j])的最小体力值

    (dp_{i,j} = min{dp_{i,k} + dp_{k,j}} + sum_{i,j})

    /*
    Author:Imy_isLy
    知识点:区间DP
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 320;
    const int inf = 0x3f3f3f3f;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int a[N], f1[N][N], f2[N][N], sum[N];
    int main() {
      int n = read();
      for (int i = 1; i <= 2 * n; i++) {
        if (i <= n)
          a[i] = read();
        a[i + n] = a[i];
        sum[i] = sum[i - 1] + a[i];
      }
      memset(f1, 63, sizeof(f1));
      for (int i = 0; i <= 2 * n; i++)
        f1[i][i] = 0;
      for (int len = 2; len <= n; len++) {
        for (int l = 1; l <= 2 * n - len + 1; l++) {
          int r = l + len - 1;
          for (int k = l; k < r; k++) {
            f1[l][r] = min(f1[l][r], f1[l][k] + f1[k + 1][r] + sum[r] - sum[l - 1]);
            f2[l][r] = max(f2[l][r], f2[l][k] + f2[k + 1][r] + sum[r] - sum[l - 1]);
          }
        }
      }
      int ans1 = 1e9, ans2 = 0;
      for (int i = 1; i <= n; i++)
        ans1 = min(ans1, f1[i][i + n - 1]), ans2 = max(ans2, f2[i][i + n - 1]);
      printf("%d
    %d", ans1, ans2);
      system("pause");
      return 0;
    }
    
    

    有一个要求更高的石子合并,我不会……SDOI 2008

    可以先看这个 再看这个学一下


    CF245H

    (dp_{i,j})表示区间([i,j])回文串的个数

    (dp_{i,j} = dp_{i,j-1} + dp_{i-1,j} - dp_{i - 1,j - 1} + ([i,j]是否为回文串))

    • 首先我们可以知道(dp_{i,i})肯定为1,然后以这个点为中心点,向左右两个方向同时扩展

    • 这里要考虑奇偶两种情况,如果为偶数的回文串直接跑就OK,单独考虑奇数,右指针就要向右移一位,把中心位置空出来

    • 然后求一个二维前缀和就好了

    /*
    Author:Imy_isLy
    知识点: 区间DP ,二维前缀和
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 5e3 + 100;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    char s[N];
    
    int dp[N][N];
    
    int main() {
      scanf("%s", s + 1);
    
      int len = strlen(s + 1), q = read();
      for (int mid = 1; mid <= len; mid++) {
        for (int l = mid, r = mid; l && r <= len & s[l] == s[r]; l--, r++)
          dp[l][r]++;
        for (int l = mid, r = mid + 1; l && r <= len && s[l] == s[r]; --l, r++)
          dp[l][r]++;
      }
      for (int i = 1; i <= len; i++)
        for (int j = 1; j <= len; j++)
          dp[i][j] += dp[i][j - 1] + dp[i - 1][j] - dp[i - 1][j - 1];
      while (q--) {
        int l = read(), r = read();
        
        printf("%d
    ", dp[r][r] - dp[l - 1][r] - dp[r][l - 1] + dp[l - 1][l - 1]);
      }
      system("pause");
      return 0;
    }
    

    括号匹配

    简化题面

    给定一个字符串求至少添加几个 ((~~或者~~) ~~或者~~ [~~或者~~ ]) 能让 括号两两匹配

    刚开始没有思路,题解是一种很重要的思想:正难则反

    就可以的对这一整段区间进行动态规划,处理这一段区间有多少括号已经匹配,最后用字符串的长度减去 (f[1][len]) 就是答案

    (f_{l,r})表示([l,r])区间内的匹配的括号的数量

    [f_{l,r} = max{f_{l+1,r-1} + 2 ~~| ~ s_l == s_r } \ f_{l,r} = max{f_{l,k} + f_{k + 1,r}} ]

    然后跑一个枚举断点的动态规划就OK

    /*
    Author:Imy_isLy
    知识点:
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 120;
    const int inf = 0x3f3f3f3f;
    //=============================================================
    char s[N];
    int f[N][N];
    //=============================================================
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch)) f |= (ch == '-'), ch = getchar();
      while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    
    int main() {
      scanf("%s", s + 1);
      int len = strlen(s + 1);
      for (int l = len; l; --l) {
        for (int r = l + 1; r <= len; ++r) {
          if ((s[l] == '(' && s[r] == ')') ||
              (s[l] == '[' && s[r] == ']'))
            f[l][r] = f[l + 1][r - 1] + 2;
          for (int k = l; k < r; ++k) {
            f[l][r] = max(f[l][r], f[l][k] + f[k + 1][r]);
          }
        }
      }
      cout << (len - f[1][len]);
      system("pause");
      return 0;
    }
    

    P4025 Bohater

    没有血量上限。所以先打回血怪,这里分类一下,因为有的回血怪我们打不死

    eg : 初始血量 5 , 掉血 100 回血10000

    所以,回血怪先打掉血少的的,掉血怪先打回血多的就好了

    /*
    Author:Imy_isLy
    知识点: 贪心
    */
    #include <algorithm>
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define int long long
    using namespace std;
    const int N = 1e5 + 100;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int n, z;
    struct node {
      int id, blood, d, a;
    } enemy[N];
    int ans[N];
    bool cmp(node a, node b) {
      if (a.blood * b.blood < 0)
        return a.blood > b.blood;
      else if (a.blood < 0 && b.blood < 0)
        return a.a > b.a;
      else
       return a.d < b.d;
    }
    signed main() {
      n = read(), z = read();
      for (int i = 1, d, a; i <= n; i++) {
        enemy[i].d = read(), enemy[i].a = read();
        enemy[i].id = i, enemy[i].blood = enemy[i].a - enemy[i].d;
      }
      sort(enemy + 1, enemy + 1 + n, cmp);
      for (int i = 1; i <= n; i++) {
        z -= enemy[i].d;
        if (z <= 0) {
          puts("NIE");
          system("pause");
          return 0;
        }
        ans[i] = enemy[i].id;
        z += enemy[i].a;
      }
      puts("TAK");
      for (int i = 1; i <= n; i++) {
        printf("%d ", ans[i]);
      }
      system("pause");
      return 0;
    }
    

    POJ 3280

    • (dp_{i,j})表示区间([i,j])的变为回文串的最小代价

    [dp_{i,j} = minegin{cases} dp_{i+1,j} + min(add_{s_i},del_{s_i}) \ dp_{i,j-1} + min(add_{s_j},del_{s_j}) end{cases}]

    • 枚举一个区间的长度,然后(n^2)转移即可
    /*
    Author:Imy_isLy
    知识点:
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 2010;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int n, m;
    char s[N];
    int add[600], del[600], dp[N][N];
    int main() {
      n = read(), m = read();
      scanf("%s", s + 1);
      for (int i = 1; i <= n; i++) {
        char ch;
        cin >> ch;
        add[(int)ch] = read(), del[(int)ch] = read();
      }
      for (int k = 1; k <= m; k++) {
        for (int i = 1; k + i <= m; i++) {
          int j = k + i;
          dp[i][j] = min(dp[i + 1][j] + min(add[s[i]], del[s[i]]),
                         dp[i][j - 1] + min(add[s[j]], del[s[j]]));
          if (s[i] == s[j]) {
            if (j - i == 1)
              dp[i][j] = 0;
            else
              dp[i][j] = min(dp[i][j], dp[i + 1][j - 1]);
          }
        }
      }
      printf("%d", dp[1][m]);
      system("pause");
      return 0;
    }
    

    区间DP处理环形问题

    1. 断环为链,然后把链复制一遍存到链的后面

    一种思想并不一定是DP处理

    区间DP的话 (dp_{i,i+n-1})取出最优解

    能量项链

    我也不知道为啥循环的时候第二维枚举左端点……,为啥第一维枚举调不出来啊……怪……

    大概如果第一维枚举长度的话就不出现这种这种问题了

    (dp_{l,r} = max{dp_{l,k} + dp_{k+1,r} + a_l imes a_{k + 1} imes a_{r + 1} })

    暴力DP 就好

    /*
    Author:Imy_isLy
    知识点:
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 320;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int a[N];
    int f[N][N];
    int main() {
      int n = read();
      for (int i = 1; i <= n; i++) {
        a[i] = read();
        a[i + n] = a[i];
      }
      // for (int l = 1; l <= n; l++)
      //   for (int r = l + 1; r <= 2 * n && r - l + 1 <= n; r++)
      //     for (int k = l + 1; k < r; k++)
      //       f[l][r] =
      //           max(f[l][r], f[l][k] + f[k + 1][r] + a[l] * a[k + 1] * a[r + 1]);
      for (int j = 2; j <= 2 * n - 1; j++)
        for (int i = j - 1; i > 0 && j - i < n; i--)
          for (int k = i; k < j; k++)
            f[i][j] =
                max(f[i][k] + f[k + 1][j] + a[i] * a[k + 1] * a[j + 1], f[i][j]);
      int ans = -1e9;
      for (int i = 1; i <= n; i++)
        ans = max(ans, f[i][i + n - 1]);
      printf("%d", ans);
      system("pause");
      return 0;
    }
    

    小结:


    背包DP模型

    背包笔记

    记录方案数:

    • (dp[i][j] = max( dp[i-1][j],dp[i-1][j+c[i]]+w[i]))状态转移过程中如果发现(dp[i-1][j] = dp[i-1][j+c[i]]+w[i])那么就是两种的方案数相加,否则的话就取最大的方案数

    输出方案:

    • 如果相等就随便选一个,如果不等就选大的那一组的选择,
      两种题型都是开一个数组进行记录

    (dp_{i,j} = max{(dp_{i - 1,j - c_i imes k + w_i imes k} | k <= cnt_i})

    单调队列优化多重背包,根据同余类进行分类

    背包中的第二维,(j)和能转移过来的(j - c imes k)(mod~~c_i)意义下同余的,也就是说可以对于第二维按照(c_i)同余类进行分类
    会发现不同类的互不影响,因为不同类之间不能够相互转化
    (f_i = dp_{i- 1,j * c_i + r}) r是我们枚举(c_i)的一个类

    (dp_{i,j imes c_i + r} = max{f_k + (j - k) imes w_{i} | j - k <= cnt_i})
    (dp_{i,j imes c_i + r} = max{f_k - k imes w_i | j - k <= cnt_i})
    (dp_{i,j imes c_i + r} = max{dp_{i - 1, j imes c_{i} + r - (j - k) imes c_i} + (j - k) imes w_i | j - k <= cnt_i})
    炒了一遍还是不是很会这种的做法

    对于第二维在求方案数: 不能通过二进制拆分优化和单调队列优化

    分组背包

    • 背包空间为(V),每一组内有多个物品,每组中只能选一个,求最大发的费用

    [dp_{i,j} = max_{k = 1}^{size_i}{dp_{i-1,j- c_{i,k}}+ w_{i,k}} ]

    vijos 1240

    无语题…不知道为啥感觉这种DP很恶心,

    里面有一个狠有意思的结论,最多有可能有一对夫妇住在同一个房间内,因为如果有两对,我们可以把这拆开,男男 女女这样分配挺残忍的,不让人夫妻一块住

    (dp_{i,j,k,0})表示前( ext{i})个房间住了( ext{j})个男人( ext{k})个女人,没有夫妻住同一个房间

    (dp_{i,j,k,1})表示前( ext{i})个房间住了( ext{j})个男人( ext{k})个女人,有一对夫妻住同一个房间

    仔细观察,其实发现,题目的本质是一个分组背包,一个房间就是一个物品,这个物品有花费和价值,这个价值是可以扩大背包容量的,这些房间并不一定是要填满的,

    [dp_{i,j,k,0} = min{dp_{i-1,j,k,0} , dp_{i-1,j-t,k,0} + p_i,dp_{i-1,j,k-t,0} + p_i } ]

    (t)是通过枚举得来的,因为不知道当前这个房间放几个男/女人是最优的,如果放一对夫妻的话,类似于上面的转移,一样转移即可,无非就多了一个转移的状态

    [dp_{i,j,k,1} = min{dp_{i-1,j,k,1} , dp_{i-1,j-t,k,1} + p_i,dp_{i-1,j,k-t,1} + p_i,dp_{i-1,j-1,k-1,0} + p_i } ]

    #include <iostream>
    #include <cstring>
    using namespace std;
    const int N = 310;
    int m,f,r,c,b[N],p[N],dp[N][N][N][2];
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int main() {
    	m = read(), f= read(),r = read(),c = read();
    	for (int i=1; i<=r; i++)
    		cin>>b[i]>>p[i];
    	memset(dp,127,sizeof(dp));
    	int temp=dp[0][0][1][1],ans=temp;
    	dp[0][0][0][0]=0;
    	for (int i=1; i<=r; i++) //第几个房间
    		for (int j=m; j>=0; j--)//住下了 j 个男人
    			for (int k=f; k>=0; k--) { // 住了 k 个女人
    				if (b[i]>=2 && j>=1 && k>=1) // 这个房间可以住 一对夫妻
    					dp[i][j][k][1]=min(dp[i-1][j-1][k-1][0]+p[i],dp[i-1][j][k][1]) 
    				dp[i][j][k][0]=min(dp[i][j][k][0],dp[i-1][j][k][0]); // 不管住不住人,这一步都能转移
    				for (int t=1; t<=b[i]; t++) {
    					if (j-t>=0) {
    						dp[i][j][k][0]=min(dp[i][j][k][0],dp[i-1][j-t][k][0]+p[i]); 
    						dp[i][j][k][1]=min(dp[i][j][k][1],dp[i-1][j-t][k][1]+p[i]);
                // 这一步貌似是说有 t 个男人住进这个房间 
    					}
    					if (k-t>=0) {
    						dp[i][j][k][0]=min(dp[i][j][k][0],dp[i-1][j][k-t][0]+p[i]);
    						dp[i][j][k][1]=min(dp[i][j][k][1],dp[i-1][j][k-t][1]+p[i]);
                // 这一步貌似是说有 t 个女人住进这个房间 
    					}
    				}
    			}
    	ans=min(dp[r][m][f][0],dp[r][m][f][1]);
    	if (ans==temp) cout <<"Impossible"<<endl;
    	else cout<<ans<<endl;
      system("pause");
    	return 0;
    }
    

    整数划分问题

    暴力Dp
    (dp_{i,j,sum})表示前(i)个数选了(j)个,和为(sum)的方案数是多少
    正解:

    直接设(dp_{i,j})表示把(i)划分成(j)个数的方案数

    [dp_{i,j} = dp_{i - j,j} + dp_{i - 1,j-1} ]

    小结

    (DP) 最值 不必但心重叠问题 ,因为重叠之后仍然是最大值


    数位DP

    • 记搜为常见的实现方式
    • 头铁可以写递推

    但是显然两只算法我都不会,自己以前写过一篇博客有兴趣的可以看一下


    树形DP

    1. 与树和图的生成树有关的DP

    2. 以每棵子树为子结构命,在父节点合并,树的天然子结构,

    3. 可以借助DFS序或者BFS序来解决问题

    4. 套数据结构(树剖)

    5. 树上问题的时间复杂度要考虑均摊

    6. 设状态一般为(f_i)子树的最优价值或方案数顺便带有附加信息(k)

    树上最大独立集

    没有上司的舞会

    [egin{cases} dp_{i,0} = sumlimits_{jin son}max(dp_{j,0},dp_{j,1})\ dp_{i,1} = sumlimits_{jin son} dp_{i,0} + 1 end{cases}]


    树的直径

    模板传送门

    (f_i)表示(i)这个点到子树里面的最远点是多远,然后对于每一个点(u)以这个点为根的最远路径的距离,然后找到次长链加起来即可,(f_{son_i} + e_i)
    DP:
    (dp_{x} = max(dep_{v in son}) - dep_x)

    dfs: 从一个点随便跑一遍DFS找到最远点,然后从最远点再跑一遍DFS 找到最远点长度,就是树的直径

    两遍深搜实现方式:
    code:

    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 1e4 + 100;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    struct Edge {
      int from, to, net;
    } e[N << 1];
    int head[N], nume;
    int sec;
    void add_edge(int from, int to) {
      e[++nume] = (Edge){from, to, head[from]};
      head[from] = nume;
    }
    int n, d[N];
    void dfs(int x, int fa) {
      for (int i = head[x]; i; i = e[i].net) {
        int to = e[i].to;
        if (to == fa)
          continue;
        d[to] = d[x] + 1;
        if (d[to] > d[sec])
           sec = to;
        dfs(to, x);
      }
    }
    int main() {
    
      n = read();
      for (int i = 1, u, v; i < n; i++) {
        u = read(), v = read();
        add_edge(u, v), add_edge(v, u);
      }
      dfs(1, 0);
      d[sec] = 0, dfs(sec, 0);
      printf("%d", d[sec]);
      return 0;
    }
    

    树形DP

    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 1e4 + 100;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int n, f[N];
    struct Edge {
      int from, to, net;
    } e[N << 1];
    int head[N], nume;
    void add_edge(int from, int to) {
      e[++nume] = (Edge){from, to, head[from]};
      head[from] = nume;
    }
    int d1[N], d2[N], d;
    void dfs(int x, int fa) {
      d1[x] = d2[x] = 0;
      for (int i = head[x]; i; i = e[i].net) {
        int to = e[i].to;
        if (to == fa)
          continue;
        dfs(to, x);
        int t = d1[to] + 1;
        if (t > d1[x])
          d2[x] = d1[x], d1[x] = t;
        else if (t > d2[x])
          d2[x] = t;
      }
      d = max(d, d1[x] + d2[x]);
    }
    int main() {
      n = read();
      for (int i = 1, u, v; i < n; i++) {
        u = read(), v = read();
        add_edge(u, v), add_edge(v, u);
      }
      dfs(1, 0);
      printf("%d", d);
      return 0;
    }
    

    Tree chain problem

    算是个神仙题目? 但是本质很很清晰在树剖上做Dp, 但貌似本身是一个题套题
    咕了,懒得写


    三色二叉树

    • 重点是如何建树,因为是二叉树考虑父亲儿子表示法,以前没有写过挺……奇怪的挺奇技淫巧的
    • (f_{i,0/1/2})分别表示当前点涂为红蓝绿,绿色节点的最大值,(f_{i,0/1/2})为最小值,直接根据小限制进行记忆化搜索,左右儿子分别遍历即可
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define int long long
    using namespace std;
    const int N = 5e5 + 100;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    char s[N];
    int len, rt, cnt, ch[N][2];
    int opt, root;
    void build(int& x) {
      x = ++cnt;
      opt = s[cnt] - '0';
      if (opt == 0)
        return;
      if (opt == 1)
        build(ch[x][0]);
      if (opt == 2)
        build(ch[x][0]), build(ch[x][1]);
    }
    int f[N][3], g[N][3];
    void dfs(int x) {
      if (x == 0)
        return;
      int l = ch[x][0], r = ch[x][1];
      dfs(l), dfs(r);
      f[x][0] = max(f[l][1] + f[r][2], f[l][2] + f[r][1]) + 1;
      f[x][1] = max(f[l][0] + f[r][2], f[l][2] + f[r][0]);
      f[x][2] = max(f[l][0] + f[r][1], f[l][1] + f[r][0]);
      g[x][0] = min(g[l][1] + g[r][2], g[l][2] + g[r][1]) + 1;
      g[x][1] = min(g[l][0] + g[r][2], g[l][2] + g[r][0]);
      g[x][2] = min(g[l][0] + g[r][1], g[l][1] + g[r][0]);
      return;
    }
    signed main() {
      g[1][0] = g[1][1] = 0;
      g[1][2] = 1;
      f[1][2] = 1;
      scanf("%s", s + 1);
      build(root);
      dfs(1);
      printf("%lld %lld", max(f[1][0], max(f[1][1], f[1][2])),
             min(g[1][0], min(g[1][1], g[1][2])));
      system("pause");
      return 0;
    }
    

    小奇挖矿


    树上背包

    (f_{i,j} = max{i,j-1} + f_{son,k} | k in {1……j-1})


    换根法


    基环树

    基环树是一种特殊的树,树最明显的特点是 (n) 个点 ( ext{n-1}) 条边,而基环树,就是 (n) 个点 (n) 条边,显然会出现环,基环树就是这样


    骑士

    以没有上司的舞会为原型的基环树DP

    首先考虑为什么会图会变成基环树,有( ext{n})个点并且有( ext{n})条边,
    树是有( ext{n})个点( ext{n-1})条边,那给树加一条边,一定会出现一个环,所以题目给出的一定是一棵基环树

    假如说有这样一个基环树,我们首先断环成链,我们断开 1 和 2 这两个点之间的连边,于是我们分别以 1 号和 2 号节点为根,形成了这样两棵树

    然后我们就开始跑DP 就是没有上司的舞会那个很简单的DP,然后我们考虑如何处理环,我们就对每次断开的 (f_{root1,0})(f_{root2,0})(max)

    为什么两次都是对这个点不选去(max)?理由很简单

    • 当取(f_{root1,0})时,我们( ext{root2} ext{root1})为根的树中是可选可不选的,同理当取(f_{root2,0})时,我们( ext{root1} ext{root2})为根的树中是可选可不选的,已经包含了两个点都不选和两个点选一个的情况,所以答案就是每次都断开环上的一条边,然后取以(root1)(root2)为根的当前点不选的最大值,然后求和即可
    for (int i = 1; i <= n; i++) {
        if (vis[i])
          continue;
        dfs(i, 0);
        dp(root1, 0);
        int ans_ = f[root1][0];
        dp(root2, 0);
        ans += max(ans_, f[root2][0]);
      }
    

    这个地方千万不能写成

    for (int i = 1; i <= n; i++) {
        if (vis[i])
          continue;
        dfs(i, 0);
        dp(root1, 0);
        dp(root2, 0);
        ans += max(f[root1][0], f[root2][0]);
      }
    

    因为以(root2)为根再跑一遍DP后(f_{root1,0})的值会改变只有我这个sb没有意识到这个问题调了半天

    /*
    Author:Imy_isLy
    知识点:基环树Dp
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define int long long
    using namespace std;
    const int N = 1e6 + 100;
    //=============================================================
    int n, val[N];
    int del, f[N][2], root1, root2;
    bool vis[N];
    struct Edge {
      int from, to, net;
    } e[N << 1];
    int head[N], nume = 1, ans;
    //=============================================================
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    void add_edge(int from, int to) {
      e[++nume] = (Edge){from, to, head[from]};
      head[from] = nume;
    }
    void dp(int x, int fa) {
      f[x][1] = val[x], f[x][0] = 0;
      for (int i = head[x]; i; i = e[i].net) {
        int to = e[i].to;
        if (to == fa)
          continue;
        if (i == del || i == (del ^ 1))
          continue;
        dp(to, x);
        f[x][0] += max(f[to][0], f[to][1]);
        f[x][1] += f[to][0];
      }
    }
    void dfs(int x, int fa) {
      v
      is[x] = true;
      for (int i = head[x]; i; i = e[i].net) {
        int to = e[i].to;
        if (to == fa)
          continue;
        if (vis[to]) {
          del = i;
          root1 = to, root2 = x;
          continue;
        }
        dfs(to, x);
      }
    }
    signed main() {
      n = read();
      for (int i = 1; i <= n; i++) {
        val[i] = read();
        int x = read();
        add_edge(i, x), add_edge(x, i);
      }
      for (int i = 1; i <= n; i++) {
        if (vis[i])
          continue;
        dfs(i, 0);
        dp(root1, 0);
        int ans_ = f[root1][0];
        dp(root2, 0);
        ans += max(ans_, f[root2][0]);
      }
      printf("%lld", ans);
      system("pause");
      return 0;
    }
    

    城市环路

    和上面一样的最大点独立集问题,只不过因为数据的原因,上一个题可以是基环树森林,而这个题只有一颗基环树,那就稍微改一下求和就可以了
    不改也能过

    /*
    Author:Imy_isLy
    知识点:
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 1e6 + 100;
    //=============================================================
    int n, val[N];
    double K;
    struct Edge {
      int from, to, net;
    } e[N << 1];
    int head[N], nume = 1;
    int root1, root2, del, ans;
    int f[N][2];
    bool vis[N];
    //=============================================================
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    void add_edge(int from, int to) {
      e[++nume] = (Edge){from, to, head[from]};
      head[from] = nume;
    }
    void dp(int x, int fa) {
      f[x][0] = 0, f[x][1] = val[x];
      for (int i = head[x]; i; i = e[i].net) {
        int to = e[i].to;
        if (to == fa)
          continue;
        if (i == del || i == (del ^ 1)) continue;
          dp(to, x);
        f[x][0] += max(f[to][0], f[to][1]);
        f[x][1] += f[to][0];
      }
    }
    void dfs(int x, int fa) {
      vis[x] = 1;
      for (int i = head[x]; i; i = e[i].net) {
        int to = e[i].to;
        if (to == fa)
          continue;
        if (vis[to]) {
          del = i;
          root1 = to, root2 = x;
          continue;
        }
        dfs(to, x);
      }
    }
    int main() {
      n = read();
      for (int i = 1; i <= n; i++)
        val[i] = read();
      for (int i = 1, u, v; i <= n; i++) {
        u = read() + 1, v = read() + 1;
        add_edge(u, v), add_edge(v, u);
      }
      scanf("%lf", &K);
      for (int i = 1; i <= n; i++) {
        if (vis[i]) continue;
          dfs(i, i);
        dp(root1, root1);
        int ans_ = f[root1][0];
        dp(root2, root2);
        ans = max(ans_, f[root2][0]);
      }
      printf("%.1lf", K * ans);
      system("pause");
      return 0;
    }
    

    IOI 2008 Island

    我是sb我要回去重新学单调队列优化Dp了……

    首先,题目要求是基环树森林的最长直径,思路不是很难想,但是很难写,我写到一半就傻了

    先说暴力,首先断环为链,然后老规矩,以这两个点为根,跑树形DP,求出树的最长链,然后这个复杂度大概是( ext{O(环长 + n^2)})的……这很显然……会T到飞……

    考虑优化:

    首先在基环树上找到环,然后在环上搞一个前缀和(s_i),环的总长度为(sum),以(i)为根的最长链为(d_i)

    [ans = max{max{ s_i - s_j,sum - (s_i - s_j) + d_i + d_j}} ]

    当&i&一定时,(s_i + d_i,sum - s_i + d_i)是一个定值,那就维护一个(j)就好了即(s_j - d_j)最小(s_j + d_j) 最大即可

    #include <cstdio>
    #include <iostream>
    #include <queue>
    #include <bits/stdc++.h>
    #define ll long long
    using namespace std;
    const int N = 1e6 + 100;
    ll n, nume = 0, cnt, ans1, ans, st, ans2, ans3;
    ll dis[2 * N], head[N], to[2 * N], net[2 * N];
    ll vis[N], vis2[N], id[N], d[N], dp[2 * N], s[N];
    void add(int x, int y, int z) {
      dis[++nume] = z, to[nume] = y, net[nume] = head[x], head[x] = nume;
    }
    bool dfs(int now, int la) {
      if (vis[now] == 1) {
        vis[now] = 2, id[++cnt] = now, vis2[now] = 1;
        return 1;
      }
      vis[now] = 1;
      for (int i = head[now]; i; i = net[i])
        if (i != ((la - 1) ^ 1) + 1 && dfs(to[i], i)) {
          if (vis[now] != 2)
            id[++cnt] = now, vis2[now] = 1, s[cnt] = s[cnt - 1] + dis[i];
          else {
            s[st - 1] = s[st] - dis[i];
            return 0;
          }
          return 1;
        }
      return 0;
    }
    void tree_dp(int now) {
      vis2[now] = 1;
      for (int i = head[now]; i; i = net[i]) {
        int y = to[i];
        if (vis2[y])
          continue;
        tree_dp(y);
        ans1 = max(ans1, d[now] + d[y] + dis[i]);
        d[now] = max(d[now], d[y] + dis[i]);
      }
    }
    ll brt(int root) {
      st = cnt + 1, ans2 = 0, ans3 = 0;
      dfs(root, 0);
      for (int i = st; i <= cnt; i++) {
        ans1 = 0;
        tree_dp(id[i]);
        ans2 = max(ans2, ans1);
        dp[i + cnt - st + 1] = dp[i] = d[id[i]];
        s[i + cnt - st + 1] = s[i + cnt - st] + s[i] - s[i - 1];
      }
      deque<int> q;
      for (int i = st; i <= 2 * cnt - st + 1; i++) {
        while (q.size() && q.front() <= i - cnt + st - 1)
          q.pop_front();
        if (q.size())
          ans3 = max(ans3, dp[i] + dp[q.front()] + s[i] - s[q.front()]);
        while (q.size() && dp[q.back()] - s[q.back()] <= dp[i] - s[i])
          q.pop_back();
        q.push_back(i);
      }                        
      return max(ans2, ans3);  
    }
    int main() {
      cin >> n;
      for (int i = 1; i <= n; i++) {
        int y, z;
        scanf("%d %d", &y, &z);
        add(i, y, z);
        add(y, i, z);
      } 
      for (int i = 1; i <= n; i++)
        if (!vis2[i]) 
          ans += brt(i);
      cout << ans;
      system("pause");
      return 0;
    }
    

    DP 优化

    加速状态转

    1. 前缀和优化
    2. 数据结构优化

    [egin{cases} 1. 单调队列\ 2. 树状数组 ~~| ~~线段树 end{cases}]

    逆序对

    单调队列优化

    • 一般情况下可以把原来的DP 式子进行简化变成(dp_[i] = max(f_j) +g_i) ((g_i)(j)是无关变量)并且(j)的取值是一段连续的区间,具有单调性

    bzoj 1855股票交易

    设状态: (f_{i,j}) 表示第( ext{i})天,有( ext{j})张股票的最大收益

    考虑分类讨论,因为存在买与不买,卖与不卖这四种情况

    1. 凭空买:

    直接附初值 (f_{i,j} = { - j * ap | j <= maxP })

    1. 不买也不买:

    反正不买也不卖,持有的股票数是不变的,那就从前往后递推就好了 (f_{i,j} = max{f_{i-1,j}})

    1. 我要继续追加(继续买

    因为两次交易要相隔( ext{W})天,设当前第( ext{x})天,拥有的股票为(k),那上一次交易最近是在第( ext{x - W - 1})天,万一这一天没有发生交易,可以通过情况二,把之前的转移到(f_{x - W - 1, k})

    (f_{i,j} = max{f_{i - w - 1, k} - (j - k) imes ap | j - as<= k < j })

    1. 我要卖!

    同样要相隔( ext{W})天,设当前是第( ext{x})天,拥有的股票为( ext{k}),那上一次交易最近是在第( ext{x - W - 1})天,万一这一天没有发生交易,也可以通过情况二,把之前的转移到(f_{x - W - 1, k}) ,把上面的转移稍微改一下就好

    (f_{i,j} = max{f_{i - w - 1, k} + (k - j) imes bp | j < k <= j + bs })

    • 把3,4两种情况的转移方程化简一下,就变成了这样一个东西

    • 3:(f_{i,j} = max{f_{i - w - 1, k}+ k imes ap} - j imes ap)(~~~~)限制: (j - as<= k < j)

    • 4:(f_{i,j} = max{f_{i - w - 1, k}+ k imes bp} - j imes bp)(~~~~)限制: (j < k <= j + bs)

    然后发现在股票相同的同一层内,max外面的是一个常数,问题就变成了(K=?)时能取到最大值,(k)属于满足转移条件的一个区间,然后单调队列优化就好了,单调队列里存的就是那个(K)

    买的时候,是顺着买的,正序枚举莫得问题,卖的时候,需要倒叙枚举,因为由一个股票多的状态转移到股票少的状态;

    买:比如当bs等于3的时候,我们枚举j到2时,我们股票为3 4 5时可以卖出。所以我们可以选择的区间是3~5.可以正序枚举的原因是在情况2时我们把最优解向后转移了

    卖:同理,我们的每卖掉股票时, 可以卖成 x - bs(3)的数。数字越小,对后面的数可以操作的越多。倒序枚举就可以维护单调最优

    ……浪费了半天,意识到,我们第 4 个是由股票多的情况向股票少的情况进行转移,正序枚举显然不满足这个条件很显然,但我思考了半天因为如果正序枚举,在单调队列中的(k)确实是满足单调性的,但是(K)不是从比(j)大的地方转移到(j)这个(K)是比(j)小的

    /*
    Author:Imy_isLy
    知识点:
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 2010;
    //=============================================================
    int n, maxP, W;
    int ap, bp, as, bs, f[N][N];
    int q[N];
    //=============================================================
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    //=============================================================
    int main() {
      n = read(), maxP = read(), W = read();
      memset(f, 128, sizeof(f));
      for (int i = 1; i <= n; i++) {
        ap = read(), bp = read(), as = read(), bs = read();
        for (int j = 0; j <= as; ++j)
          f[i][j] = -j * ap;
        for (int j = 0; j <= maxP; ++j)
          f[i][j] = max(f[i][j], f[i - 1][j]);
        if (i <= W)  continue;
        int head = 1, tail = 0;
        for (int j = 0; j <= maxP; ++j) {
          while (head <= tail && q[head] < j - as)
            head++;
          while (head <= tail && f[i - W - 1][q[tail]] + q[tail] * ap <= f[i - W - 1][j] + j * ap)
            tail--;
          q[++tail] = j;
          if (head <= tail)
            f[i][j] = max(f[i][j], f[i - W - 1][q[head]] + q[head] * ap - j * ap);
        }
        head = 1, tail = 0;
        for (int j = maxP; j >= 0; --j) {
          while (head <= tail && q[head] > j + bs)
            head++;
          while (head <= tail && f[i - W - 1][q[tail]] + q[tail] * bp <= f[i - W - 1][j] + j * bp)
            tail--;
          q[++tail] = j;
          if (head <= tail)
            f[i][j] = max(f[i][j], f[i - W - 1][q[head]] + q[head] * bp - j * bp);
        }
      }
      int ans = -2147483647;
      for (int i = 0; i <= maxP; i++)
        ans = max(ans, f[n][i]);
      printf("%d", ans);
      system("pause");
      return 0;
    }
    

    CF372C Watching Fireworks is Fun

    (f_{i,j})表示看第( ext{i})个烟花时,位置为( ext{j})的最大快乐值,

    (f_{i,j} = max{f_{i-1,k} + b_i - |a_i-j| })

    其中要满足条件(j - (t_i-t_{i-1}) imes d <= k <= j + (t_i - t_{i - 1}) imes d))

    如果(i和j)是确定的,那么(~~~~- |a_i-j| + b_i~~~~) 也是一个定值,可以从( ext{max})里面提出来,那原式就变成了

    (f_{i,j} = max{f_{i-1,k}} - |a_i-j| + b_i)

    按常理说这样就可以了,但是CF的题它不可能这么简单……,它会MLE,可以发现,(i)这一维只会从(i-1)这一维转移过来,可以用滚动数组,把第一维给压掉

    /*
    Author:Imy_isLy
    知识点:
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define int  long long
    
    using namespace std;
    const int N = 1.5e5+10;
    const int M = 310;
    //=============================================================
    int f[2][N], n, m, d, ans = -1e18, sum;
    int a[M], b[M], t[M], q[N];
    //=============================================================
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    //=============================================================
    signed main() {
      n = read() , m = read() , d = read();
      for (int i = 1 ; i <= m ; i++) {
        a[i] = read() , b[i] = read() , t[i] = read();
      }
      memset(f,128,sizeof(f));
      for (int i = 1 ; i <= n ; i++) f[0][i] = 0;
      int fl = 1;
      for (int i = 1 ; i <= m ; i++) {
        int head = 1 , tail = 0 , k = 1;
        for(int j = 1 ; j <= n ; j++) {
          while(k <= min(n,j + d * (t[i] - t[i - 1]))) {
            while(head <= tail && f[fl ^ 1][q[tail]] <= f[fl^1][k]) tail--;
            q[++tail] = k;
            k++;
          }
          while(head <= tail && q[head] < max(1ll,j - d * (t[i] - t[i - 1]))) head++;
          f[fl][j] = f[fl ^ 1][q[head]] + b[i] - abs(a[i] - j);
        }
        fl ^= 1;
      }
      for (int i = 1 ; i <= n ; i++) ans = max(ans,f[fl ^ 1][i]);
      printf("%lld",ans);
      system("pause");
      return 0;
    }
    

    HDU 4374 One Hundred layer

    前缀和&单调队列优化转移的合法点都是一整个区间

    树状数组&线段树优化


    LIS

    (dp_i =max{dp_j~~|~~a[j] < a[i] &&j < i} + 1)

    权值线段树或树状数组优化

    二分优化


    NOIP 2008 传纸条


    状压DP

    • 数据范围一维较正常一维很小,较容易识别

    互不侵犯

    状压DP入门题, 设(f_{i,j,s})表示第(i)行放棋子的方式(s),已经放了(s)个棋子

    (f_{i,j,s} += f_{i - 1 , j - cnt(s),T})
    转移条件 :(((T<<1|T>>1)&S) == 0)
    --------------------------------------by zhx

    那么问题就转化了,转化为了如果何找到(T)会发现,(T)中1的个数完全可以通过预处处理出来,到时候如果($)满足条件,可以通过预处理出来的数组(sit_x)进行统计答案
    我不太适应上面的转移方程,我还是习惯Kersen 的做法
    (dp_{i,j,cnt}) 表示第(i)行的排列方式为 (j) 放了(cnt)个棋子

    (sit_x)表示哦(x)(1)的个数,记当前放置棋子为(gs)

    (f_{i,j,s} = sum f_{i - 1,k,s - gs_j})

    转移条件:

    (sit_j & sit_k == 0)

    (((sit_j << 1) & sit_k) == 0)

    ((sit_j & (sit_k << 1) )== 0)

    code:

    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 120;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int s0;
    ll f[20][120][120],num[120],s[120];
    int main() {
      int n = read(), k = read();
      for (int i = 0; i < (1 << n); i++) {
        if (i & (i << 1))
          continue;
        int kk = 0;
        for (int j = 0; j < n; j++)
          if (i & (1 << j))
            kk++;
        s[++s0] = i, num[s0] = kk;
      }
      f[0][1][0] = 1;
      for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= s0; j++) {
          for (int k_ = num[j]; k_ <= k; k_++) {
            for (int t = 1; t <= s0; t++) {
              if (!(s[t] & s[j]) && !(s[t] & (s[j] >> 1)) && !(s[t] & (s[j] << 1)))
                f[i][j][k_] += f[i - 1][t][k_ - num[j]];
            }
          }
        } 
      }
      ll ans = 0;
      for (int i = 1; i <= s0; i++)
        ans += f[n][i][k];
      printf("%lld", ans);
      system("pause");
      return 0;
    }
    

    POJ 2411

    枚举子集的DP:

    愤怒的小鸟

    • 状压DP枚举子集的一种思想

    • 首先发现输入的(m)对于答案是没有任何影响的,是个混淆的东西

    • 两个点确定一条二次函数,可以(n^3)预处理一下,看看这条二次函数能经过多少点

    • (f[i|num[j][k]]=min(f[i|num[j][k]],f[i]+1) | nump[j][k])为预处理出来的数组,(f_{s})是子集(s)的最小需求

    • 然后转移就可以了

    • 注意一个优化,要不然会TLE ,我们从前一直往后打就了可以了,找到一个可以打到的,就直接break因为后面如果还能达到,会被重新覆盖掉

    • 虽然这么说不大好,因为DP要求的是局部的最优解,从而引出整体最优解, 如果不考虑后面,确实当前已经是最优解了,但是后面如果还能打到前面的,纯粹就是捎带的夹带私活,把前面的活抢了 所以可以直接转移

    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = (1 << 19);
    const double esp = 1e-6;
    const int inf = 0x3f3f3f3f;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    double x[20], y[20];
    int num[20][20], f[N];
    
    int main() {
      int T = read();
      while (T--) {
        int n = read(), m = read();
        for (int i = 1; i <= n; i++)
          scanf("%lf%lf", &x[i], &y[i]);
        for (int i = 1; i <= n; i++)
          for (int j = 1; j <= n; j++)
            num[i][j] = 0;
    
        for (int i = 1; i < n; i++) {
          for (int j = i + 1; j <= n; j++) {
            if (fabs(x[i] - x[j]) < esp)
              continue;
            double a = (y[j] / x[j] - y[i] / x[i]) / (x[j] - x[i]);
            if (a >= 0)
              continue;
            double b = y[i] / x[i] - a * x[i];
            for (int k = 1; k <= n; k++) {
              if (fabs(a * x[k] + b - y[k] / x[k]) < esp) {
                num[i][j] |= (1 << (k - 1));
              }
            }
          }
        }
    
        for (int i = 0; i <= (1 << n) - 1; i++)
          f[i] = inf;
        f[0] = 0;
    
        for (int i = 0; i <= (1 << n) - 1; i++) {
          for (int j = 1; j <= n; j++) {
            if (!(i & (1 << (j - 1)))) {
              for (int k = j; k <= n; k++) {
                if (j == k)
                  f[i | (1 << (j - 1))] = min(f[i | (1 << (j - 1))], f[i] + 1);
                if (fabs(x[j] - x[k]) < esp)
                  continue;
                f[i | num[j][k]] = min(f[i | num[j][k]], f[i] + 1);
              }
              break;
            }
          }
        }
        printf("%d
    ", f[(1 << n) - 1]);
      }
      system("pause");
      return 0;
    }
    

    斜率优化DP

    要求输出(N)个数字的(a_i),输出的时候可以连续的输出,每一段的代价为这一段的平方和加上一个常数(M)
    (dp_i = min{dp_j + (s_{i + 1} - s_j)^2 + M | j < i})

    斜率具有单调性,可以,类似于单调队列的思想,如果后者的比前者优,那前者可以删,
    单调队列进行维护即可


    [HNOI2008]玩具装箱

    ([l,r])区间内的代价为((r - l + sumlimits_{i=l}^r c_i-L)^2)

    (sumlimits_{i=l}^r c_i)可以通过前缀和处理出来,等于(sum_r - sum_{l-1})

    原式等于(r - l + sum_r - sum_{l-1} - L)

    然后就可以得到一个(n^2)的转移方程

    (f_i)表示前(i)个数的和

    (f_i = min_{j<i}{f_j + (sum_i - sum_{j} + i - j - 1- L)^2})

    将式子进一步处理

    • (s_i = sum_i + i)
    • (L^{'} = L +1)

    (f_{i} = min{f_j + (s_i - s_j - L^{'})})

    将含有(j)的都移到同一边

    (f_{i} - (s_i -L^{'})^2 = min{f_j + s_j^2 + 2 imes s_j(L^{'} - s_i)})

    设函数:

    [egin{aligned} x_i &= s_i\ y_i &= f_i + s_i^2\ k_i &= -2(L' - s_i)\ b_i &= f_i - (s_i - L')^2 end{aligned}]

    转移方程变成了(b_i = min_{j < i}{y_j-k_ix_j})
    ((x_i,y_i))看成二维平面上的点,则(k_i)表示直线斜率,(b_i)表示过((x_i.y_i))的斜率为(k_i)的直线 截距,那么问题就转化为了选择合适的(j)然后最小化截距,因为上面的式子中,(b_i)的等式后面中((s_i - L')^2)是一个定值,所以最小化截距就是最小化(f_i)

    图片摘自Oi-Wiki

    然后下凸包的函数跑斜率优化就OK

    #include <algorithm>
    #include <cctype>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #define LL long long
    #define double long double
    const int kN = 5e4 + 10;
    using namespace std;
    //=============================================================
    int n, head = 1, tail, q[kN];
    LL L, s[kN], f[kN];
    //=============================================================
    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;
    }
    double getk(int x, int y) {
      if (s[x] == s[y])
        return (f[y] + s[y] * s[y] > f[x] + s[x] * s[x] ? 1e18 : -1e18);
      return (double)(((f[y] + s[y] * s[y]) - (f[x] + s[x] * s[x])) / (s[y] - s[x]));
    }
    int Query(int now_) {
      while (head < tail && getk(q[head], q[head + 1]) <= (double)(-2 * (L - s[now_])))
        ++head;
      return q[head];
    }
    void Insert(int now_) {
      while (head < tail && getk(q[tail - 1], q[tail]) >= getk(q[tail - 1], now_))
        --tail;
      q[++tail] = now_;
    }
    //=============================================================
    int main() {
      n = read(), L = read() + 1;
      Insert(0);
      for (int i = 1, j = 0; i <= n; ++i) {
        s[i] = s[i - 1] + read() + 1;
        j = Query(i);
        f[i] = f[j] + 1ll * (s[i] - s[j] - L) * (s[i] - s[j] - L);
        // cout << "j: " << j << "
    ";
        Insert(i);
      }
      printf("%lld
    ", f[n]);
      system("pause");
      return 0;
    }
    

    P2900 [USACO08MAR]Land Acquisition G

    因为数据是无序的所以,我们首先对数据进行排序,以(x)为第一优先级降序排序,以(y)为第二优先级从小到大排序,然后发现,第二维并非具有绝对的单调性,那把这一段处理一下,让其具有单调性即可

    bool cmp(node x, node y) {
      return x.x != y.x ? x.x > y.x : x.y < y.y;
    }
      for (int i = 1; i <= n; i++)
        a[i] = (node){read(), read()};
      sort(a + 1, a + 1 + n, cmp);
      int temp = 0;
      for (int i = 1; i <= n; i++) {
        if (a[i].y > a[temp].y)
          a[++temp] = a[i];
      }
      n = temp;
    

    对于一段区间([l,r])其代价为(x_l imes y_r)

    (f_i)表示按这种顺序划分后,前(i)片土地的最小代价和,然后枚举上一段的结尾位置

    (f_i = min_{j = 0}^{i-1} {f_j + x_{j+1} imes y_i})

    很明显的(n^2)转移,考虑优化,因为存在乘积的形式,所以考虑斜率优化,设:

    [egin{aligned} x_i &= -A_{i+1}\ y_i &= f_i\ k_i &= B_i\ b_i &= f_i end{aligned}]

    那问题就又变成了最小化截距的问题了,用单调队列维护下凸包,然后转移即可

    /*
    Author:Imy_isLy
    知识点:
    */
    #include <algorithm>
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 5e4+100;
    //=============================================================
    int n;
    struct node {
      ll x, y;
    } a[N];
    ll f[N];
    int q[N], head = 1, tail = 0;
    //=============================================================
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    bool cmp(node x, node y) {
      return x.x != y.x ? x.x > y.x : x.y < y.y;
    }
    double getk(int x, int y) {
      return (double)(f[y] - f[x]) / (-a[y + 1].x + a[x + 1].x);
    }
    void insert(int now) {
      while (head < tail && getk(q[tail - 1], q[tail]) >= getk(q[tail - 1], now))
        tail--;
      q[++tail] = now;
    }
    int query(int now) {
      while (head < tail && getk(q[head], q[head + 1]) <= a[now].y)
        head++;
      return q[head];
    }
    //=============================================================
    int main() {
      n = read();
      for (int i = 1; i <= n; i++)
        a[i] = (node){read(), read()};
      sort(a + 1, a + 1 + n, cmp);
      int temp = 0;
      for (int i = 1; i <= n; i++) {
        if (a[i].y > a[temp].y)
          a[++temp] = a[i];
      }
      n = temp;
      insert(0);
      for (int i = 1, j = 0; i <= n; i++) {
        j = query(i);
        f[i] = f[j] + 1ll * a[j + 1].x * a[i].y;
        insert(i);
      }
      printf("%lld",f[n]);
      system("pause");
      return 0;
    }
    

    P4072 [SDOI2016]征途

    记化成的(m)段,每一段的和为(x_i)

    方差的公式转化:

    [egin{aligned} S^2 =& frac{1}{m} sumlimits_{i = 1}^{m}(x_i - overline{x})^2\ =& frac{1}{m} sumlimits_{i = 1}^{m}(x_i^2 - 2x_i overline{x} + overline{x}^2)\ =& frac{1}{m}x_i^2-2frac{1}{m}sumlimits_{i=1}^{m}x_ioverline{x} + frac{1}{m}sumlimits_{i=1}^m overline{x}^2\ =& frac{1}{m}sumlimits_{i=1}^mx_i^2-2overline{x}^2+overline{x}^2\ =& frac{1}{m}sumlimits_{i=1}^mx_i^2-overline{x}^2 end{aligned} ]

    原式要求的是(m^2cdot S^2)

    通过上面的变形,可以直接得到

    [egin{aligned} m^2cdot S^2 =& msumlimits_{i=1}^mx_i^2 - m^2overline{x}^2\ =& msumlimits_{i=1}^mx_i^2 - (moverline{x})^2 end{aligned} ]

    假如说原来的数字为(a_?)

    然后发现((moverline{x})^2)这一块就是所有数字和的平方等价于((sumlimits_{i=1}^na_i)^2)把原式进行处理变成一样的形式变成了((sumlimits_{i=1}^mb_i)^2)

    (f_{i,k})表示前(i)个数分成(k)(b_i)

    [f_{i,k} = min_{j<i}{f_{j,k-1} + (sumlimits_{l=j+1}^{i}a_l)^2} ]

    记一个前缀和(sum)

    [egin{aligned} f_{i,k} &= min_{j<i} left{ f_{j,k-1} + left( sum_i - sum_j ight)^2 ight}\ f_{i,k} &= min_{j<i} left{ f_{j,k-1} + sum_i^2 - 2sum_icdot sum_j +sum_j^2 ight} end{aligned} ]

    (i)移到等号的同一边

    [f_{i,k} - sum_{i}^2= left(f_{j,k - 1} + sum_j^2 ight) - 2sum_icdot sum_j ]

    进行转化

    [egin{aligned} x_i &= sum_i\ y_i &= f_{i,k-1} + sum_i^2\ k_i &= 2sum_i\ b_i &= f_{i,k} - sum_i^2\ end{aligned} ]

    然后就可以得出(b_i = y_j -k_ix_j),当(i)一定时(k_i,sum_i)是定值,然后问题就又变成了最小化截距问题了

    这个题在一些很细节的地方卡精度

    /*
    Author:Imy_isLy
    知识点:
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    #define double long double
    using namespace std;
    const int N = 3010;
    //=============================================================
    int n, m, a[N];
    ll f[N][N], sum[N];
    int q[N], head = 1, tail = 0;
    //=============================================================
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    double getk(int id, int x, int y) {
      if (sum[x] == sum[y])
        return f[y][id] + sum[y] * sum[y] > f[id][x] + sum[x] * sum[x] ? 1e18
                                                                       : -1e18;
      return (double)(((double)(f[y][id] + sum[y] * sum[y]) -
                       (double)(f[x][id] + sum[x] * sum[x])) /
                      (sum[y] - sum[x]));
    }
    void insert(int id, int now_) {
      while (head < tail &&
             getk(id, q[tail - 1], q[tail]) >= getk(id, q[tail - 1], now_))
        --tail;
      q[++tail] = now_;
    }
    int query(int id, int now_) {
      while (head < tail &&
             getk(id, q[head], q[head + 1]) <= (double)(2 * sum[now_]))
        head++;
      return q[head];
    }
    //=============================================================
    int main() {
      n = read(), m = read();
      for (int i = 1; i <= n; i++) {
        a[i] = read();
        sum[i] = sum[i - 1] + a[i];
      }
      memset(f, 63, sizeof(f));
      f[0][0] = 0;
      for (int k = 1; k <= m; ++k) {
        head = 1, tail = 0;
        insert(k - 1, 0);
        for (int i = 1; i <= n; i++) {
          if (i >= k) {
            int j = query(k - 1, i);
            f[i][k] = f[j][k - 1] + (sum[i] - sum[j]) * (sum[i] - sum[j]);
          }
          insert(k - 1, i);
        }
      }
      printf("%lld", 1ll * m * f[n][m] - 1ll * sum[n] * sum[n]);
      system("pause");
      return 0;
    }
    

    任务安排
    斜率优化的套路题,上面几个题虽然很难,但是不够恶心,会发现,当一个物品的属性变成两个之后,就想让自己多长几个脑子

    给定n个物品,每一个物品都有 ((t,g))两个属性,给定一个常数(s)将物品进行分段,第(i)段区间([l,r])的价值为((is + sumlimits_{j= l}^rt_j) imes sumlimits_{k = l}^rg_i)

    建议看这篇博客学习?


    [APIO2010]特别行动队

    先扯一句 :

    求斜率我用X 除以 Y 算咋回事

    solution:

    (f_i) 表示 前 (i)个人的最大战力

    (f_i = f_j + a imes (sum_i - sum_j)^2 + b imes (sum_i - sum_j) + c)


    设存在 (j < k~~&& ~~ f_j > f_k)

    (f_j + a imes (sum_i - sum_j)^2 + b imes (sum_i - sum_j) + c > \ f_k + a imes (sum_i - sum_k)^2 + b imes (sum_i - sum_k) + c)


    [f_j + a imes (sum_i^2 -2 imes sum_i cdot sum_j + sum_j^2) \ + b imes (sum_i - sum_j) \>\ f_k + a imes (sum_i^2 + 2 imes sum_i cdot sum_j - sum_k^2) \ + b imes (sum_i - sum_k) ]


    [f_j + a imes sum_i^2 -2a imes sum_i cdot sum_j + a imes sum_j^2 \ + b imes sum_i - b imes sum_j \ \>\ f_k + a imes sum_i^2 -2a imes sum_i cdot sum_k + a imes sum_k^2 \ + b imes sum_i - b imes sum_k ]


    [-2a imes sum_i cdot sum_k + 2a imes sum_i cdot sum_j + \>\ - f_j + f_k + a imes sum_k^2 - a imes sum_j^2\ - b imes sum_k + b imes sum_j \ ]


    [-2 acdot sum_i (sum_j - sum_k) > f_k - f_j + a imes (sum_k^2 - sum_j^2 ) + b imes (sum_k - sum_j) ]


    [2 acdot sum_i > frac{(f_k + a imes sum_k^2 - b imes sum_k ) - (f_j + a imes sum_j^2 - b imes sum_j)}{(sum_k - sum_j)} ]

    /*
    Author:Imy_isLy
    知识点:
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define int long long
    using namespace std;
    const int N = 1e6 + 100;
    const int inf = 0x3f3f3f3f;
    //=============================================================
    int f[N], sum[N];  // 表示前 i 个人分成?组的最大价值
    int q[N], head = 1, tail = 0;
    int a, b, c, n;
    //=============================================================
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch)) f |= (ch == '-'), ch = getchar();
      while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    
    int X(int x) {
      return sum[x];
    }
    
    int Y(int x) {
      return f[x] + a * sum[x] * sum[x] - b * sum[x];
    }
    
    double get_K(int x, int y) {
      return (Y(x) - Y(y)) / (X(x) - X(y));
    }
    
    void Insert(int now_) {
      while (head < tail && get_K(q[tail - 1], q[tail]) < get_K(q[tail - 1], now_))
        --tail;
      q[++tail] = now_;
    }
    
    int query(int now_) {
      while (head < tail && get_K(q[head], q[head + 1]) > 2 * a * sum[now_])
        head++;
      return q[head];
    }
    signed main() {
      n = read(), a = read(), b = read(), c = read();
      Insert(0);
      for (int i = 1; i <= n; ++i) {
        sum[i] = sum[i - 1] + read();
        int j = query(i);
        // cout << j <<"
    ";
        f[i] = f[j] + a * (sum[i] - sum[j]) * (sum[i] - sum[j]) + b * (sum[i] - sum[j]) + c;
        Insert(i);
      }
      cout << f[n];
      system("pause");
      return 0;
    }
    

    数学期望与期望DP

    期望:

    (e(x) = val_x imes P(x))

    (e(x + y) = e(x) + e(y))

    如果两个事件互为独立事件则

    (e(x imes y) = e(x) imes e(y))


    bzoj 1867 钉子和小球


    bzoj 6091 摘苹果


    BZOJ 4832 抵制克苏恩


    NOIP 2016 换教室

    概率期望 ( ext{DP})

    因为最终要求的期望与边权挂钩,而且要求最后期望值最小,可发现点数为300.,可以莽一个( ext{Floyed})求一个全源最短路

    (f_{i,j,0/1})表示前(i)个课程申请了(j)次,且第(i)个是否申请时的最小期望值
    (C1=c[i−1],C2=d[i−1],C3=c[i],C4=d[i])

    [dp[i][j][0]=minegin{cases} dp[i−1][j][0]+dis[C1][C3]\ dp[i−1][j][1]+dis[C1][C3]∗(1−k[i−1])+dis[C2][C3]∗k[i−1] end{cases}]

    [dp[i][j][1]=minegin{cases} dp[i−1][j−1][0]+dis[C1][C3]∗(1−k[i])+dis[C1][C4]∗k[i]\ dp[i−1][j−1][1]+dis[C2][C4]∗k[i]∗k[i−1]+dis[C2][C3]∗k[i−1]∗(1−k[i])+dis[C1][C4]∗(1−k[i−1])∗k[i]+dis[C1][C3]∗(1−k[i−1])∗(1−k[i]) end{cases}]

    我写不明白了~

    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 2e3 + 5;
    const double inf = 1e17 + 5;
    int n, m, v, e, c[N][2], dis[305][305];
    double k[N], dp[N][N][2], ans;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch)) f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))  s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int main() {
      n = read(), m = read(), v = read(), e = read();
      for (int i = 1; i <= n; i++) c[i][0] = read();
      for (int i = 1; i <= n; i++) c[i][1] = read();
      for (int i = 1; i <= n; i++) scanf("%lf", &k[i]);
      for (int i = 1; i <= v; i++)
        for (int j = 1; j <= v; j++)
          dis[i][j] = 1e9, dis[i][i] = dis[i][0] = dis[0][i] = 0;
      for (int i = 1; i <= e; i++) {
        int x = read(), y = read(), w = read();
        dis[x][y] = dis[y][x] = min(dis[x][y], w);
      }
      for (int k = 1; k <= v; k++)
        for (int i = 1; i <= v; i++)
          for (int j = 1; j <= v; j++)
            dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
      for (int i = 0; i <= n; i++)
        for (int j = 0; j <= m; j++)
          dp[i][j][0] = dp[i][j][1] = inf;
      dp[1][0][0] = dp[1][1][1] = 0;
      for (int i = 2; i <= n; i++) {
        dp[i][0][0] = dp[i - 1][0][0] + dis[c[i - 1][0]][c[i][0]];
        for (int j = 1; j <= min(i, m); j++) {
          int C1 = c[i - 1][0], C2 = c[i - 1][1], C3 = c[i][0], C4 = c[i][1];
          dp[i][j][0] = 
                     min(dp[i][j][0], 
                     min(dp[i - 1][j][0] + dis[C1][C3], 
                        dp[i - 1][j][1] 
                      + dis[C1][C3] * (1 - k[i - 1]) 
                      + dis[C2][C3] * k[i - 1]));
          dp[i][j][1] = 
                     min(dp[i][j][1], 
                     min(dp[i - 1][j - 1][0] 
                      + dis[C1][C3] * (1 - k[i]) 
                      + dis[C1][C4] * k[i],
                        dp[i - 1][j - 1][1] 
                      + dis[C2][C4] * k[i] * k[i - 1] 
                      + dis[C2][C3] * k[i - 1] * (1 - k[i]) 
                      + dis[C1][C4] * (1 - k[i - 1]) * k[i] 
                      + dis[C1][C3] * (1 - k[i - 1]) * (1 - k[i])));
        }
      }
      ans = inf;
      for (int i = 0; i <= m; i++)
        ans = min(ans, min(dp[n][i][0], dp[n][i][1]));
      printf("%.2lf", ans);
      system("pause");
      return 0;
    }
    

    BZOJ 1076 奖励关

    因当前的宝物能不能拿有限制,所以把限制给状压了 ((j & sta[i]) == sta[i])就满足了限制

    转移方程:

    [f_{i,s} = sumlimits_{k in gift} max{ f_{i-1,s},f_{i-1,scup k} + p_i} ]

    下面的代码是倒序枚举的(i)所以是(i+1)

    /*
    Author:Imy_isLy
    知识点:状压DP
    */
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    int K, n, p[20], sta[1 << 15];
    double f[105][1 << 15];
    int main() {
      K = read(), n = read();
      for (int i = 1, x; i <= n; i++) {
        p[i] = read();
        while (x = read()) {
          sta[i] = sta[i] | (1 << x - 1);
        }
      }
      for (int i = K; i; --i) {
        for (int j = 0; j < (1 << n); j++) {
          for (int k = 1; k <= n; k++) {
            if ((j & sta[k]) == sta[k])
              f[i][j] += max(f[i + 1][j], f[i + 1][j | (1 << k - 1)] + p[k]);
            else
              f[i][j] += f[i + 1][j];
          }
          f[i][j] /= n;
        }
      }
      printf("%.6lf", f[1][0]);
      system("pause");
      return 0;
    }
    

    BZOJ 4318 OSU!

    貌似和DP没有啥关系……就是纯粹求个数学期望

    (p_{i,j})表示结束位置为 (i) 的连续(1)串的,其长度为(j)的概率

    ((x + 1) ^ 3)

    (Longrightarrow x^3 + 3 imes x^2 + 3 imes x + 1)

    可以发现式子中多了这么一部分(3 imes x^2 + 3 imes x + 1)

    可以维护一个(x^2)的期望 和 (x) 的期望

    (x : l1[i] = (l1[i - 1] + 1) * p_i)

    (x^2 :l2[i] = (l2[i - 1] + 2 imes l1[i-1] + 1) imes p_i)

    (ans[i] = ans[i - 1] + (3 imes l2[i-1] + 3 imes l1[i-1]) imes p[i])

    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <string>
    #define ll long long
    using namespace std;
    const int N = 1e5 + 100;
    int read() {
      int s = 0, f = 0;
      char ch = getchar();
      while (!isdigit(ch))
        f |= (ch == '-'), ch = getchar();
      while (isdigit(ch))
        s = s * 10 + (ch ^ 48), ch = getchar();
      return f ? -s : s;
    }
    double l[N], l2[N];
    int main() {
      int n = read();
      double ans = 0;
      for (int i = 1; i <= n; i++) {
        double x;
        scanf("%lf", &x);
        l[i] = (l[i - 1] + 1) * x;
        l2[i] = (l2[i - 1] + 2 * l[i - 1] + 1) * x;
        ans += (3 * l2[i - 1] + 3 * l[i - 1] + 1) * x;
      }
      printf("%.1lf", ans);
      system("pause");
      return 0;
    }
    
    

    没学会的题:

    1. 梦幻岛宝珠 分层DP?

    upd:

    2021.4.14
    今天退役啦,hhhh滚回去卷文化课啦

    Future never has to do with past time,but present time dose.
  • 相关阅读:
    ES数据库重建索引——Reindex(数据迁移)
    ES数据库搜索
    Elasticsearch及相关插件的安装
    初识ES数据库
    Oracle数据库(二)
    Oracle数据库(一)
    DBUtils和连接池
    动态页面技术(EL/JSTL)
    Istio安装
    idea正则替换下划线为驼峰
  • 原文地址:https://www.cnblogs.com/-wzd233/p/14658545.html
Copyright © 2011-2022 走看看