DP大汇集

本文讲述了动态规划的五个关键概念:基本DP、树形DP(如“没有上司的舞会”)、换根DP、状态压缩DP(如“互不侵犯”)和数位DP,通过实例演示了这些技术在解决问题中的应用。
摘要由CSDN通过智能技术生成

一、何为DP

DP,全称动态规划,是解决一类决策最优解的问题的方法。

详情请见:

理论篇---->动态规划_百度百科 (baidu.com)

现实篇---->【算法 - 动态规划】从零开始学动态规划!(总纲) - 知乎 (zhihu.com) 

二、DP的分类

I 基本DP

        基本DP,都包含线性DP区间DP等,这里不过多介绍了。

II 树形DP

        树形DP,顾名思义,一个在树上进行的DP。

        主要推导方式是沿着根节点往下遍历,在回溯过来,在回溯过程中去进行DP的过程。

例题呈现 - 没有上司的舞会

题目大意

        给定n个节点的数值a_i,选取一定的节点计算数值总和,如果节点i的父亲节点被选取了,那么节点i将不能被选择。

题目解析

        本体是一个很典型的树形DP的题目。

        可以设f(i,j)i为当前记录到的节点编号,j为当前节点的选取状态(0为不选取,1为选取)。

        简单思考后就可以推理出状态转移方程:

        u为当前遍历到的节点,v为节点u的子节点。

        f(u,0)=\sum max(f(v,0),f(v,1)) 

        如果u节点没被选取,那么v节点就选取选取与不选取两种情况的最大值即可。
        f(u,1)=a_u+\sum f(v,0)

        如果u节点被选取,那么v节点只能选取不选取的情况,记住还要加上当前节点的数值共享。

正解代码 
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <list>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <unordered_map>
#include <vector>
// #include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;

#define bug printf("--------\n");
#define enter printf("\n");
#define debug(x) cout << #x << '=' << x << endl;
#define file(FILENAME) \
  freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout);
#define rep(i, a, b) for (int(i) = (a); (i) <= (b); ++(i))
#define inv(x, mod) ppow((x), (mod)-2)
#define pb push_back
#define mk make_pair
#define mem(t, v) memset((t), (v), sizeof(t));
#define DBG cerr << __LINE__ << ' ' << __FUNCTION__ << endl;
#define CLOSE ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);

#define N 6005
#define M 100005
#define inf 0x3f3f3f3f3f3f3f3f
#define mod 1000000009

ll n;
struct node{
  ll num;
  ll father;
  ll kid;
  ll kids[N];
}tree[N];
ll f[N][2];
void dfs(ll x){
  if(tree[x].kid==0){
    f[x][0]=0;
    f[x][1]=tree[x].num;
    return ;
  }
  for(ll i=1;i<=tree[x].kid;i++){
    dfs(tree[x].kids[i]);
    f[x][0]+=max(f[tree[x].kids[i]][0],f[tree[x].kids[i]][1]);
    f[x][1]+=f[tree[x].kids[i]][0];
  }
  f[x][1]+=tree[x].num;
}
int main() {
  // file("")
  // CLOSE
  cin>>n;
  for(int i=1;i<=n;i++){
    cin>>tree[i].num;
  }
  for(int i=1;i<=n-1;i++){
    ll a,b;
    cin>>a>>b;
    tree[a].father=b;
    tree[b].kids[++tree[b].kid]=a;
  }
  for(int i=1;i<=n;i++){
    if(tree[i].father==0){
      dfs(i);
      cout<<max(f[i][0],f[i][1]);
      return 0;
    }
  }
  return 0;
}

III 换根DP

        换根DP,是树形DP的一种拓展处理,主要用于处理树形DP存在不定根时的情况。

        主要推导方式是先选取任意一点作为根节点,进行一遍树形DP的处理,然后再根据题目的要求,利用已经求出来的一个节点作为根节点的答案去推导其他的答案。

例题呈现 - STA-Station

题目大意

        给定一个n个点的树,找到一个节点为根节点时,所有结点的深度之和最大。

题目解析

        第一遍的树形DP的状态转移方程很好推导,如下:

         u为当前遍历到的节点,v为节点u的子节点,size(i)为以i为根节点时的子树大小。

        g(u)=\sum g(v)+size(v)

        同时,size也需要记录啊,状态转移方程如下:

        size(u)=\sum size(v)

        最终完成后,我们得到了以一个节点为根节点时的答案。那我们接下来该怎么办?要一个一个点再去跑一遍这个过程吗?很显然,不行滴!

        那我们可以这样考虑,假设第一个当作根节点的节点编号为1,它有一个子节点2,我们可以旋转一下,把2提取到根节点上来,1变为2的子节点,以此类推得到答案。

        

        由此可得f(2)=g(2)+[(f(1)-g(2)-size(2))+(n-size(2))] .

        类比得知:f(v)=g(v)+[(f(u)-g(v)-size(v))+(n-size(v))].

        化简后的:f(v)=f(u)-2*size(v)+n

        这样就可以得到全部节点的答案了,找到最大值输出即可。

正解代码
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <list>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <unordered_map>
#include <vector>
// #include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;

#define bug printf("--------\n");
#define enter printf("\n");
#define debug(x) cout << #x << '=' << x << endl;
#define file(FILENAME) \
  freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout);
#define rep(i, a, b) for (int(i) = (a); (i) <= (b); ++(i))
#define inv(x, mod) ppow((x), (mod)-2)
#define pb push_back
#define mk make_pair
#define mem(t, v) memset((t), (v), sizeof(t));
#define DBG cerr << __LINE__ << ' ' << __FUNCTION__ << endl;
#define CLOSE ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);

#define N 1000005
#define M 100005
#define inf 0x3f3f3f3f3f3f3f3f
#define mod 1000000009

ll n;
ll g[N];
ll f[N];
ll tree_size[N];
vector<ll> edge[N];
void dfs_g(ll u, ll fa) {
  tree_size[u] = 1;
  for (ll v : edge[u]) {
    if (v == fa) continue;
    dfs_g(v, u);
    g[u] += (g[v] + tree_size[v]);
    tree_size[u] += tree_size[v];
  }
}
void dfs_f(ll u, ll fa) {
  for (ll v : edge[u]) {
    if (v == fa) continue;
    f[v] = f[u] - 2 * tree_size[v] + n;
    dfs_f(v, u);
  }
}
int main() {
  // file("")
  // CLOSE
  cin >> n;
  for (int i = 1; i <= n - 1; i++) {
    ll l, r;
    cin >> l >> r;
    edge[l].pb(r);
    edge[r].pb(l);
  }
  dfs_g(1, 0);
  f[1] = g[1];
  dfs_f(1, 0);
  ll maxx = 0;
  for (int i = 1; i <= n; i++) {
    maxx = max(maxx, f[i]);
  }
  for (int i = 1; i <= n; i++) {
    if (maxx == f[i]) {
      cout << i << ' ';
      return 0;
    }
  }
  return 0;
}

Ⅳ 状态压缩DP

        状态压缩DP(以下称为状压DP),是将一部分状态压缩成一个数字去进行处理的一种DP,一般可以解决在矩阵地图中有限制性的排列一些点的问题。

例题呈现 - 互不侵犯

题目大意

        在n*n的棋盘中放置k个国王,求有多少种情况。

        国王攻击范围:上、下、左、右、左上、左下、右上、右下。

题目解析

        本题直接暴力肯定会超时,那么就考虑dp解决。

        可以设f(i,j,k)i为当前遍历到的行数,j为当前行放置棋子的状态(用二进制表示,0为没有棋子,1为有棋子),k当前已经放置了多少个棋子。        

        简单推导可得:

        i为当前行数,j为当前行的状态,k为当前放置的棋子总数,p为枚举出来的上一行的状态,num(x)x的状态中包含的棋子数量。

        f(i,j,k)=f(i-1,p,k-num(j))

正解代码
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <list>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <unordered_map>
#include <vector>
// #include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;

#define bug printf("--------\n");
#define enter printf("\n");
#define debug(x) cout << #x << '=' << x << endl;
#define file(FILENAME) \
  freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout);
#define rep(i, a, b) for (int(i) = (a); (i) <= (b); ++(i))
#define inv(x, mod) ppow((x), (mod)-2)
#define pb push_back
#define mk make_pair
#define mem(t, v) memset((t), (v), sizeof(t));
#define DBG cerr << __LINE__ << ' ' << __FUNCTION__ << endl;
#define CLOSE ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);

#define N 100005
#define M 100005
#define inf 0x3f3f3f3f3f3f3f3f
#define mod 1000000009

ll f[11][601][101];
ll nnum[601];
ll cnt(ll x) {
  ll sum = 0;
  while (x) {
    if (x & 1) sum++;
    x >>= 1;
  }
  return sum;
}
int main() {
  //   file("a")
  // CLOSE
  ll n, m;
  cin >> n >> m;
  for (int i = 0; i <= (1 << n) - 1; i++) {
    nnum[i] = cnt(i);
    f[1][i][nnum[i]] = 1;
  }
  for (int i = 2; i <= n; i++) {
    for (int p = 0; p <= (1 << n) - 1; p++) {
      if (p & (p >> 1)) continue;
      for (int j = 0; j <= (1 << n) - 1; j++) {
        if (j & (j >> 1)) continue;
        if (j & p || j & (p << 1) || j & (p >> 1)) continue;
        for (int k = nnum[j]; k <= m; k++) {
          f[i][j][k] += f[i - 1][p][k - nnum[j]];
        }
      }
    }
  }
  ll ans = 0;
  for (int j = 0; j <= (1 << n) - 1; j++) ans += f[n][j][m];
  cout << ans;
  return 0;
}

V 数位DP

       数位DP,是解决一类在一个区间内选取满足要求的数字的一种方式。

  • 50
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值