dp练习集

动态规划(DP)

// 以下题目来自牛客网

删括号

f[i][j][k] 表示序列s的前i个匹配序列t的前j个,序列s删除部分左括号与右括号数量差为k的情况是否可行

答案为 f[sl][tl][0]

状态转移:

    当 f[i][j][k] 可行时

  1. s[i+1]==t[j+1] 且 k==0 则 f[i+1][j+1][k] = 1
  2. s[i+1]=='('  则s串删去当前括号可匹配,即 f[i+1][j][k+1] = 1
  3. s[i+1]==')'  则 k>0 时s串多删去一个左括号匹配,即 f[i+1][j][k-1] = 1
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

bool f[110][110][110];  //s前i个删去括号能否成为t前j个,左右括号差为k
char s[110], t[110];
int main() {
    scanf("%s", s+1);
    scanf("%s", t+1);
    int sl = strlen(s+1), tl = strlen(t+1);
    f[0][0][0] = 1;
    
    for(int i=0;i<sl;i++) {
        for(int j=0;j<tl;j++) {
            for(int k=0;k<sl;k++) if(f[i][j][k]) {
                if(k==0 && s[i+1]==t[j+1]) f[i+1][j+1][k] = 1;
                if(s[i+1]=='(') f[i+1][j][k+1] = 1;
                else if(k) f[i+1][j][k-1] = 1;
            }
        }
    }
    printf("%s
", f[sl][tl][0]?"Possible":"Impossible");
    return 0;
}
View Code

 回文子序列计数

错误思路:x[i] = 左右26个小写字母选取0~min(l[i][j], r[i][j]) (0<=j<26)的组合数之积。

正确求法:见代码。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int mod = 1e9+7;
typedef long long ll;
 
ll x[3010], dp[3010];   // dp[i]位回文子序列个数
// dp[i+1]
char s[3010];
int main()
{
    scanf("%s", s);
    int n = strlen(s);
    for(int i=0;i<n;i++) x[i] = 1;
    for(int i=1;i<n;i++) {
        ll sum = 0, tmp;
        for(int j=n-1;j>i;j--) {
            tmp = dp[j];
            if(s[j]==s[i-1]) {
                dp[j] = (dp[j] + sum + 1) % mod;
            }
            sum = (sum + tmp) % mod;
            x[i] = (x[i] + dp[j]) % mod;
        }
    }
 
    ll ans = 0;
    for(int i=0;i<n;i++) {
        ans = ans^((i+1) * x[i]) % mod;
    }
    printf("%lld
", ans);
    return 0;
}
View Code

 牛牛的计算机内存

状压dp

直接 dp[22][1<<20] 会MLE,只能用滚动数组记录状态。

int dp[1<<20];     // dp[S]: 前i条指令状态为S的最小代价
int state[1<<20]; // state[i]:j 指令状态i执行完后的内存状态为j

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;
int dp[1<<20];    // dp[S]: 前i条指令,访问完状态为S的最小代价
int state[1<<20]; // state[i]:S   前i条指令执行完状态为S 
int a[22];

int main() {
    
    memset(dp, INF, sizeof(dp));
    dp[0] = 0;
    
    int n, m;
    char ins[22];
    scanf("%d %d", &n, &m);
    for(int i=0;i<n;i++) {
        scanf("%s", ins);
        int k = 0;
        for(int j=0;j<m;j++) {
            a[i] = a[i]*2 + (ins[j]-'0');
            if(ins[j]=='1') ++k;
        }
        state[1<<i] = a[i];
        dp[1<<i] = k*k;
    }
    
      
    for(int S=0;S<(1<<n);S++) {
        if(dp[S]==INF) continue;

        for(int i=0;i<n;i++) {
            if((S>>i)&1) continue;
            
            int nexS = S|(1<<i), k = 0;
            for(int j=0;j<m;j++) {
                if((a[i]>>j)&1 && ((state[S]>>j)&1)==0) {
                    ++k;
                }
            }
            if(dp[nexS]>dp[S]+k*k) {
                dp[nexS] = dp[S] + k*k;
                state[nexS] = state[S]|a[i];
            }
        }
    }
    
    printf("%d
", dp[(1<<n)-1]);
    return 0;
}
View Code

棋盘的必胜策略

可以用 f[i][j][step] 记录到 mp[i][j] 用了step步的胜负状态,dfs即可。

  1. 如果下一步有必败态,当前则为必胜态
  2. 否则当前为必败态
  3. mp[i][j]终点为必败态
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

const int dx[] = {0, 0, 1, -1};
const int dy[] = {1, -1, 0, 0};

int r, c, k;
char mp[55][55];
int f[55][55][110];

bool check(int x, int y) {
    if(x<0||y<0||x>=r||y>=c)
        return false;
    if(mp[x][y]=='#')
        return false;
    return true;
}

int dfs(int x, int y, int k) {
    if(f[x][y][k]!=-1) 
        return f[x][y][k];
    if(mp[x][y]=='E')    // 走到终点,无法移动,必败
        return f[x][y][k] = 0; 
    if(k==0) 
        return 0;        // 走不了,必败
    
    for(int i=0;i<4;i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        if(check(nx, ny) && dfs(nx, ny, k-1)==0) 
            return f[x][y][k] = 1;
    }
    return f[x][y][k] = 0;
}

int main() {
    cin>>r>>c>>k;
    for(int i=0; i<r; i++)
        scanf("%s",mp[i]);
    memset(f, -1, sizeof(f));
    
    int sx, sy;
    for(int i=0;i<r;i++) {
        for(int j=0;j<c;j++) {
            if(mp[i][j] == 'T') {
                sx = i;
                sy = j;
            }
        }
    }
    
    printf("%s
", dfs(sx, sy, k)?"niuniu":"niumei");
    return 0;
}
View Code

看起来像博弈论,其实分析一下最多走两步就能确定胜负,不用搜索状态也能解决。

分析见代码。

#include<iostream>
#include<cstdio>
using namespace std;

const int dx[] = {0, 0, 1, -1};
const int dy[] = {1, -1, 0, 0};

int r, c, k;
char mp[55][55];
bool check(int x, int y) {
    if(x<0||y<0||x>=r||y>=c)
        return false;
    if(mp[x][y]=='#')
        return false;
    return true;
}
bool win(int x, int y) {
    for(int i=0;i<4;i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        if(check(nx, ny) && mp[nx][ny]=='E')
            return true;
    }
    return false;
}
int main() {
    cin>>r>>c>>k;
    for(int i=0; i<r; i++)
        scanf("%s",mp[i]);
    
    int sx, sy;
    for(int i=0;i<r;i++) {
        for(int j=0;j<c;j++) {
            if(mp[i][j] == 'T') {
                sx = i;
                sy = j;
            }
        }
    }
    
    bool f = false;  // 第一步能否走
    for(int i=0;i<4;i++) {
        int nx = sx + dx[i];
        int ny = sy + dy[i];
        if(check(nx, ny)) {
            f = true;
            if(mp[nx][ny]=='E')
                return 0 * printf("niuniu
");
        }
    }
    if(!f) { // 动不了
        return 0 * printf("niumei
");
    }
    if(k==1) { // 只走一步
        return 0 * printf("niuniu
");
    }
    if(k%2==0) { // 偶数步,往返走,走后必胜
        return 0 * printf("niumei
");
    }
    // 奇数步,第二步无法胜,第三步开始往返走,先走必胜
    for(int i=0;i<4;i++) {
        int nx = sx + dx[i];
        int ny = sy + dy[i];
        if(check(nx, ny) && mp[nx][ny]=='.' && !win(nx, ny)) {
            return 0 * printf("niuniu
");
        }
    }
    puts("niumei");
    return 0;
}
View Code

牛牛与数组

状态转移很好写,记录一下前缀和,减去dp[i-1][j] j的整数倍的部分即为dp[i][j]

#include<iostream>
#include<cstdio>
using namespace std;
const int mod = 1e9+7;
int dp[12][100010];
int main() {
    int n, k;
    scanf("%d %d", &n, &k);
    
    for(int i=0;i<=k;i++) dp[0][i] = 1;

    for(int i=1;i<=n;i++) {
        int sum = 0; 
        for(int j=1;j<=k;j++)
            sum = (sum + dp[i-1][j]) % mod;
        
        for(int j=1;j<=k;j++) {
            int sum1 = 0;
            for(int l=2*j;l<=k;l+=j) {
                sum1 = (sum1 + dp[i-1][l])% mod;
            }
            dp[i][j] = ((sum - sum1)%mod+mod)%mod;
        }

    }

    printf("%d
", dp[n][k]);
    return 0;
}
View Code

牛牛去买球

n个盒子,每个盒子有a[i]个红球,b[i]个篮球,但a[i],b[i]有正负1的偏差,总和不变。买每个盒子的费用为c[i],求买k个相同的球的最小花费。

三种情况

  1. 买k个红球,每个盒子都当做a[i]-1个红球
  2. 买k个蓝球,每个盒子都当做b[i]-1个蓝球
  3. 买2k-1个球,至少保证有k个相同颜色的球

用滚动数组上限为最多的球数,而不是k。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

int dp[20010];
int a[10010], b[10010], c[10010];
int main() {
    int n, k; cin>>n>>k;
    for(int i=1;i<=n;i++)
        scanf("%d", &a[i]);
    for(int i=1;i<=n;i++)
        scanf("%d", &b[i]);
    for(int i=1;i<=n;i++)
        scanf("%d", &c[i]);
    
    int ans = 0x3f3f3f3f, up = 20000;
    memset(dp, 0x3f, sizeof(dp));
    dp[0] = 0;
    for(int i=1;i<=n;i++) {
        int v = a[i] - 1;
        for(int j=up;j>=v;j--) {
            dp[j] = min(dp[j], dp[j-v]+c[i]);
        }
    }
    for(int i=k;i<=2*k;i++) ans = min(ans, dp[i]);
    
    memset(dp, 0x3f, sizeof(dp));
    dp[0] = 0;
    for(int i=1;i<=n;i++) {
        int v = b[i] - 1;
        for(int j=up;j>=v;j--) {
            dp[j] = min(dp[j], dp[j-v]+c[i]);
        }
    }
    for(int i=k;i<=2*k;i++) ans = min(ans, dp[i]);
    
    memset(dp, 0x3f, sizeof(dp));
    dp[0] = 0;
    for(int i=1;i<=n;i++) {
        int v = a[i]+b[i];
        for(int j=up;j>=v;j--) {
            dp[j] = min(dp[j], dp[j-v]+c[i]);
        }
    }
    for(int i=2*k-1;i<=up;i++) ans = min(ans, dp[i]);
    if(ans==0x3f3f3f3f) ans = -1;
    printf("%d
", ans);
    return 0;
}
View Code

 小明打联盟

有3个小技能一个大招,大招的伤害值随时间线性变化。给定T时间,以及各个技能的释放时间和伤害值,问最大的伤害值是多少。

不考虑大招的话,就是多重背包问题。

把一个大招看成两个L, R时刻释放的大招d, e,中间时刻释放只会用一次。 (假设用两次m时刻的大招可以转化为大招e +  (2m-l)时刻的大招,还是相当于用一次)

然后再枚举L,R区间的最大伤害值即可。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

int t;
int v[5];
int w[5];
long long dp[100010];
int main() {
    while(scanf("%d", &t)!=EOF) {
        for(int i=0;i<3;i++) {
            scanf("%d %d", &v[i], &w[i]);
        }
        int L, R, temp, A;
        scanf("%d %d %d %d", &L, &R, &temp, &A);
        v[3] = L; w[3] = temp;
        v[4] = R; w[4] = temp + A*(R-L);
        
        memset(dp, 0, sizeof(dp));
        for(int i=0;i<5;i++) {
            for(int j=v[i];j<=t;j++) { // 多重背包
                dp[j] = max(dp[j], dp[j-v[i]]+w[i]);
            }
        }
        for(int j=L;j<=R;j++) {
            dp[t] = max(dp[t], dp[t-j]+temp+1LL*A*(j-L));
        }
        
        printf("%lld
", dp[t]);
    }
    
    return 0;
}
View Code

树形dp

// 以下题目来自洛谷

P1352 没有上司的舞会

状态转移方程很简单,1A 

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;

int n, fa[6010];
int w[6010];
vector<int> G[6010];

int dp[6010][2]; 
// dp[u][0] u没有参加
// dp[u][1] u参加

void dfs(int u, int fa) {
    dp[u][1] = w[u];
    for(int i=0;i<G[u].size();i++) {
        int v = G[u][i];
        if(v==fa) continue;

        dfs(v, u);
        dp[u][1] += dp[v][0];
        dp[u][0] += max(dp[v][0], dp[v][1]);
    }
}

int main() {
    scanf("%d", &n);
    for(int i=1;i<=n;i++) 
        scanf("%d", &w[i]);

    int u, v;
    for(int i=1;i<n;i++) {
        scanf("%d %d", &u, &v);
        G[u].push_back(v);
        G[v].push_back(u);
        fa[u] = v;
    }

    int rt = -1;
    for(int i=1;i<=n;i++) 
        if(!fa[i]) {
            rt = i;
            break;
        }
    dfs(rt, -1);
    printf("%d
", max(dp[rt][0], dp[rt][1]));
    return 0;
}
View Code

P2016 战略游戏

选出一棵树上最少的节点,能覆盖所有边。

这题结构跟上面类似,每一点放/不放两个状态。

查看题解有大佬指出这是最小点覆盖问题,使用匈牙利算法,对于无向图答案为 ans / 2 。

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 1510;


vector<int> G[maxn];
int n;
int f[maxn][2];

void dfs(int u, int fa) {
    f[u][1] = 1;
    for(int i=0;i<G[u].size();i++) {
        int v = G[u][i];
        if(v==fa) continue;

        dfs(v, u);

        f[u][0] += f[v][1];
        f[u][1] += min(f[v][0], f[v][1]);
    }
}

int main() {
    scanf("%d", &n);
    for(int i=0;i<n;i++) {
        int u, v, k;
        scanf("%d %d", &u, &k);
        while(k--) {
            scanf("%d", &v);
            G[u].push_back(v);
            G[v].push_back(u);
        }

    }
    dfs(1, -1);
    printf("%d
", min(f[1][0], f[1][1]));
    return 0;
}
View Code

P2015 二叉苹果树

保留K条边苹果树上的最大苹果数量。

注意子树边的数量写法:dfs儿子后 sz[u] += sz[v] + 1; 

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 110;

int n, K;
struct Edge {
    int to, w;
    Edge(int v, int ww):to(v), w(ww){}
};
vector<Edge> G[maxn];
int sz[maxn];
int dp[maxn][maxn];
// dp[u][i] : 以u为根的子树保留i条边的最多苹果数量

void dfs(int u, int fa) {
    for(int i=0;i<G[u].size();i++) {
        int v = G[u][i].to;
        if(v==fa) continue;

        dfs(v, u);
        sz[u] += sz[v] + 1;    // 边的数量
        
        for(int j=min(sz[u], K);j>=1;j--) { // 01背包,逆序
            for(int k=0;k<=min(sz[v], j-1);k++) {
                dp[u][j] = max(dp[u][j], dp[u][j-k-1] + dp[v][k] + G[u][i].w);
            }
        }
        
    }
}

int main() {
    scanf("%d %d", &n, &K);

    int u, v, w;
    for(int i=1;i<n;i++) {
        scanf("%d %d %d", &u, &v, &w);
        G[u].push_back(Edge(v, w));
        G[v].push_back(Edge(u, w));
    }

    dfs(1, -1);
    printf("%d
", dp[1][K]);
    return 0;
}
View Code

P2014 选课

课程之间有依赖关系,求选M门课程的最大学分。

将没有直接先修课的课程连在根为 0 的树上,从节点 0 dfs 即可。

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 310;

int n, K;
vector<int> G[maxn];
int sz[maxn], w[maxn];
int dp[maxn][maxn];
// dp[u][i] : 以u为根的子树选i门课的最大学分

void dfs(int u) {
    sz[u] = 1;
    for(int i=0;i<G[u].size();i++) {
        int v = G[u][i];

        dfs(v);
        sz[u] += sz[v];

        for(int j=min(sz[u], K);j>=1;j--) {
            for(int k=0;k<=min(j-1, sz[v]);k++) {
                dp[u][j] = max(dp[u][j], dp[u][j-k-1] + dp[v][k]);
            }
        }


    }
}

int main() {
    scanf("%d %d", &n, &K);

    int fa;
    for(int i=1;i<=n;i++) {
        scanf("%d %d", &fa, &w[i]);
        G[fa].push_back(i);
    }

    for(int i=1;i<=n;i++) dp[i][0] = w[i];
    dfs(0);
    printf("%d
", dp[0][K]);
    return 0;
}
View Code

P1270 “访问”美术馆

读入采用dfs形式给出美术馆的通过走廊的时间和藏画数量,问T时间内能盗窃多少幅画。

坑点:时间有效时间为 T - 1

记搜 / 树形dp 。由于要返回根节点,时间可以直接乘以 2 读入。

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 110;

int T, tot;
struct node {
    int cost, val;
}tree[maxn*4];
int dp[maxn*4][610];

void dfs(int u, int t) {
    if(dp[u][t] || t==0)  return; // 0为0直接返回

    if(tree[u].val) { // 根节点
        dp[u][t] = min(tree[u].val, (t-tree[u].cost)/5);
        return;
    }

    for(int i=0;i<=t-tree[u].cost;i++) {
        dfs(u*2, i);
        dfs(u*2+1, t-i-tree[u].cost);  // 右边剩下时间=  t - i - 2倍走廊时间

        dp[u][t] = max(dp[u][t], dp[u*2][i]+dp[u*2+1][t-i-tree[u].cost]);

    }
}

void build(int rt) {
    scanf("%d %d", &tree[rt].cost, &tree[rt].val);
    tree[rt].cost *= 2;
    if(!tree[rt].val) {
        build(rt*2);
        build(rt*2+1);
    }
}

int main() {

    scanf("%d", &T);
    build(1);

    dfs(1, T-1);

    printf("%d
", dp[1][T-1]);
    return 0;
}
View Code

数位DP

// 以下来自洛谷

P2657 [SCOI2009]windy数

求A,B区间内满足相邻两位数字之差大于等于2的整数个数。

注意是在 !lim && !zero 条件下记忆化,没加这个条件调了半天。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
typedef long long ll;

ll dp[12][11]; // dp[i][j]:长度为i中最高位是j的windy数的个数
int bit[12];
ll dfs(int pos, int lim, int last, int zero) {
    if(pos<0) return 1;

    if(!lim && !zero && dp[pos][last]!=-1) return dp[pos][last];

    int res = 0;
    int up = lim?bit[pos]:9;
    for(int i=0;i<=up;i++) {
        if(abs(i-last)<2) continue;

        res += dfs(pos-1, lim&&(i==up), zero&&(i==0)?100:i, zero&&(i==0));
    }
    if(!lim && !zero) dp[pos][last] = res;
    return res;
}

ll cal(ll x) {
    int cnt = 0;
    while(x) {
        bit[cnt++] = x%10;
        x /= 10;
    }
    memset(dp, -1, sizeof(dp));
    return dfs(cnt-1, 1, 100, 1);
}

int main() {
    ll A, B;

    while(cin>>A>>B)

    
    printf("%lld
", cal(B)-cal(--A));

    return 0;
}
View Code

洛谷题解翻到别人的代码处理:

原文地址:https://www.cnblogs.com/izcat/p/11302092.html