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.
原文地址:https://www.cnblogs.com/-wzd233/p/14658545.html