zoukankan      html  css  js  c++  java
  • 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

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

  • 相关阅读:
    指针和引用作为函数参数传递
    cv::Mat.type()
    Matlab 双目标定工具箱
    invalid conversion from `const void*' to `void*'
    error: 'vector' is not a member of cv
    单例模式与静态成员
    RGBD SLAM V2 +Ubuntu 16.04+ROS KINETIC配置及运行
    EntityFrameworkCore + MySQL 主从复制应用读写分离
    Docker 搭建 MySQL8.0 主从复制环境
    Asp.Net Core 项目中使用 Serilog 输出日志到 Elasticsearch
  • 原文地址:https://www.cnblogs.com/izcat/p/11302092.html
Copyright © 2011-2022 走看看