zoukankan      html  css  js  c++  java
  • 暑期集训 动态规划专题(不是概览 大概)(就是没有前导只有题目的意思) :)

    暑期集训 动态规划专题(不是概览 大概)

    tags: 动态规划


    一、做题

    (一) To the Max - POJ-1050

    思路:
    题意:给定一个(N*N)的矩阵,求其子矩阵最大的和。
    首先是降维,分析小问题:若我们规定了子矩阵的行高,对于一个高为N的子矩阵我们要求其高同样为N的子矩阵怎么求。

    首先求出每一列的和。问题就变成了求数列子串最大和问题。

    然后回到我们的问题上去。在高(N)的矩阵中如何找出子矩阵最大和。
    先枚举第i行上面子矩阵的高度(1~i)。确定高度之后再在前缀和数组内dp子串最大和即可。

    /**/#include<iostream>
    /**/#include<cmath>
    using namespace std;
    const int MAX = 105;
    int num[MAX][MAX];      //"列前缀和"数组
    int up[MAX*MAX];
    int down[MAX*MAX];
    int main() {
        int N;cin>>N;
        int ans = 0;
        for(int i=1;i<=N;i++) {
           int dp[MAX] = {0};                  //这是MAX个dp数组一种情况,不是一个dp数组MAX种情况
            for(int j=1;j<=N;j++) {
                cin>>num[i][j];
                num[i][j] += num[i-1][j];       //计算当列列前缀和
                for(int k=0;k<i;k++) {          //枚举子矩阵高度
                    //dp第i行为底,高度为k的子矩阵的"列前缀和"数列以第j列结尾的子串最大和
                    dp[k] = max(dp[k],0)+num[i][j]-num[i-k][j]; 
                    ans = max(ans,dp[k]);       //更新答案
                }
            }
        }
        cout<<ans<<endl;
        // system("pause");
        return 0;
    }
    

    我知道我讲得不清楚,我日后会补动图的。

    (二)Camels - Codeforces - 14E

    思路:
    题意:(n(nleqslant 20))个位置每个位置放一个数字(x(1leqslant xleqslant 4))。要求得到的序列有(t(1leqslant tleqslant 10))个驼峰和(t-1)个…驼谷(?大雾)。求有多少种方法。
    (也就是说开头必须是升序结尾必须是降序,否则驼峰驼谷的数目不对)
    然后这道题是一道高维dp问题。不知道算是数位dp还是计数dp。反正是一道高维dp。: )
    定义:$$dp[i][j][k][l]表示第i个数是j在第k个驼峰的[1:上升/0:下降]面$$
    规定峰顶是属于上升面,谷底属于下降面。
    于是有以下状态
    CF14E.png-31.1kB
    所以需要枚举:

    • 每一位
    • 每一种数字
    • 在哪一个驼峰
    • 此时它前一个数字是什么

    最后我们把 第n个数字是1234在第t个驼峰的下降面的结果加起来就是最后的答案
    代码:(dp的代码真短,可就是写不出来)

    /**/ #include<stdio.h>
    typedef long long ll;
    // dp[i][j][k][l] 第i个数是j在第k个驼峰上的上升/下降段
    ll dp[20+5][4+2][10+5][2];
    int main() {
        int n,t;scanf("%d%d",&n,&t);
        dp[2][2][1][2] = 1;
        dp[2][3][1][3] = 2;
        dp[2][4][1][4] = 3;
        for(int i=3;i<=n;i++)               //每一位
            for(int j=1;j<=4;j++)           //每一种数字
                for(int k=1;k<=t;k++)       //在哪一个驼峰
                    for(int r=1;r<=4;r++) { //此时它前一个数字是什么
                        if(r<j) {   //前一个数字小 当前数字在上升面
                            dp[i][j][k][5] += dp[i-1][r][k][6];     //前一个数字和当前数字在同一个驼峰
                            dp[i][j][k][7] += dp[i-1][r][k-1][0];   //当前数字在新的驼峰
                        }
                        if(r>j) {   //前一个数字大 当前数字在下降面 两个数字一定在同一驼峰
                            dp[i][j][k][0] += dp[i-1][r][k][0];
                            dp[i][j][k][0] += dp[i-1][r][k][8];     //当前数字是下降面开头
                        }
                    }
        ll ans = 0;
        for(int i=1;i<=4;i++) {
            ans += dp[n][i][t][0];  //第n个数字是i在最后一个驼峰下降面
        }
        printf("%lld
    ",ans);
        return 0;
    }
    

    (三)Valley Numer - HDU - 6148

    思路:
    题意:中文
    数位dp,高维dp?,awsldp。
    状态:(dp(i,j,up,limit))   第i位为j在 上升/非上升 且 是/否 达到上届时的情况总数。
    转移:
    从高位向低位转移
    枚举下一位数字,并判断:

    • 下一位数字是否合法

    limit限制中不允许超限
    上升趋势中不允许下降

    • 下一位数字将改变哪些状态

    pos+1
    下一位limit = 上一位limit为真 && 当前位数字到上界
    下一位数字是否打破下降趋势

    到这里 我发现dp总是离不开一个叫做枚举的,听起来有点暴力的东西。哦还有递推。
    代码 注意取模

    /**/#include<stdio.h>
    /**/#include<string.h>
    typedef long long ll;
    const int MAX = 105;
    const int MOD = 1e9+7;
    char n[MAX];int maxlen;     //上限数字  上限数字长度
    ll dp[MAX][11][2];          //状态数组
    ll dfs(int pos,int num,int up,int limit) {      //递归 位置 数字 升降 上限
        if(pos==maxlen) {   //如果到最后一位了
            if(num==-1) return 0;   //如果num==-1 代表数字还没有开头,即-1及-1之前都是前导零
            else return 1;          //确定了最后一个数字,返回1
        }
        if(!limit && num!=-1 && dp[pos][num][up] != -1) {   //没有限制,且记忆化过该状态,直接返回
            return dp[pos][num][up];
        }
        //开始计算
        ll ans = 0;
        int x = limit?n[pos]-'0':9;     //确定枚举上界
        for(int i=0;i<=x;i++) {     //(limit&&i==x) == 1 则下一位仍然受限
            //----------------------------前导零特判
            if(num==-1) {               //如果当前位是-1,即从这以前都是前导零
                if(i==0) {              //当下一位仍然枚举零时,把前导零标记沿袭到下一层
                    ans = (ans + dfs(pos+1,-1,0,limit&&i==x))%MOD;  //下一位为-1,非上升趋势,被限制
                }else {                 //如果枚举的不是零,则前导零终止
                    ans = (ans + dfs(pos+1,i,0,limit&&i==x))%MOD;
                }
            }else {
                if(i<num) {             //如果枚举下一位小于当前位
                    if(up) {            //如果当前位处于上升趋势 则本次枚举为非法状态 跳过
                        //什么都不做
                    }else {             //反之 正常递归
                        ans = (ans + dfs(pos+1,i,0,limit&&i==x))%MOD;
                    }
                }else if(i == num) {    //如果枚举下一位等于当前位,正常递归
                    ans = (ans + dfs(pos+1,i,up,limit&&i==x))%MOD;
                }else {                 //如果枚举下一位大于当前位,递归up状态写为1。
                    ans = (ans + dfs(pos+1,i,1,limit&&i==x))%MOD;
                }
            }
        }
        //判断 如果当前位num==-1即代表(有前导零)(是开头)那么就不需要记忆化
        //     或者当前位被限制,那么被限制的状态不需要记忆化,因为每次限制不同。
        //只能记忆化没有限制也没有前导零的情况 且只对同一次询问有效
        return num==-1||limit?ans:dp[pos][num][up]=ans;
    }
    int main() {
        int t;scanf("%d",&t);
        while(t--) {
           //状态数组初始化为-1 记忆化没用 因为是从高往低求的,即使状态数字相同状态也不相同
            memset(dp,-1,sizeof(dp));
            scanf("%s",n);
            maxlen = strlen(n);
            printf("%lld
    ",dfs(0,-1,0,1));         //从第一位之前以-1开始。
        }
        return 0;
    }
    
    附带一组测试数据
    Input:
    30
    100
    200
    300
    400
    500
    600
    700
    800
    900
    1000
    1100
    1200
    1300
    1400
    1500
    1600
    1700
    1800
    1900
    2000
    2100
    2200
    2300
    2400
    2500
    2345
    3456
    7890
    10000000000
    100000000000000000000

    Output:
    100
    156
    214
    275
    340
    410
    486
    569
    660
    760
    815
    870
    906
    934
    955
    970
    980
    986
    989
    991
    1046
    1102
    1159
    1187
    1208
    1168
    1481
    3221
    11974249
    188307253

    (四)Apocalypse Someday - POJ - 3208

    思路:
    题意:找出第n大的含有666的数字
    首先找出大范围,在根据大范围枚举小范围
    令dp[i][j]为i位数处于怜恤j个6状态的情况函数。有

    [dp[i][0] = (dp[i-1][0] + dp[i-1][9] + dp[i-1][2])*9 ]

    [dp[i][10] = dp[i-1][0] ]

    [dp[i][2] = dp[i-1][11] ]

    [dp[i][3] = dp[i-1][2] + dp[i-1][3]*10 ]

    最后一个方程,因为当出现过连续三个6之后再考虑其末尾就没有意义了。
    或者说,只有前面没有出现过三个6,末尾6的个数才有意义。

    预处理得到整个数组后。使用试填法得到第n大的含有666的数字。试填法是什么?还不如叫枚举呢。
    从高位向低位,从小到大枚举。总m位第i位为j的情况数有dp[m-i][3]种。当j等于6时,或者已经枚举过连续三个6时。设当前已有连续k个6,则当前情况数需要再加上dp[m-i-k][1~3-k]。
    得到情况数之后与n比较。若n大,则n减去当前情况数并继续枚举。
    若n小,则当前位的数字就确定了。
    因为我们要的是第n大的数。必须从高到低从小到大。所以>这里有一点需要注意:dp数组的状态既能正着看也能倒着看。这是从高位向低位枚举的重要条件。不然的话第30行的代码就无法实现。

    代码:

    /**/#include<stdio.h>
    inline int max(int a,int b) { return a>b?a:b; }
    typedef long long ll;
    const int MAXN = 50;
    ll dp[MAXN][4];
    int init() {
        dp[0][0] = 1;
        for(int i=1;i<MAXN;i++) {
            dp[i][0] = dp[i-1][0] * 9 + dp[i-1][12]*9 + dp[i-1][2]*9;
            dp[i][13] = dp[i-1][0];
            dp[i][2] = dp[i-1][14];
            dp[i][3] = dp[i-1][2] + dp[i-1][3]*10;
            //因为情况数是含前导零的,所以dp[i][j]包含了1~i位的所有情况
        }
    }
    int main() {
        init();
        int t;scanf("%d",&t);
        while(t--) {
            int n;scanf("%d",&n);
            int m = 2;
            while(dp[++m][3]<n);            //判断第n大的数有多少位
            int k = 0;          /当前枚举到的连续6的个数
            for(int i=m;i>0;i--) {          //从高位向低位枚举
                for(int j=0;j<10;j++) {     //枚举当前位的数字(或者说枚举十次)
                    ll tmp = dp[i-1][3];    //除开这一位,剩下的能够组成要求数的情况数
                    if(k==3 || j==6) {      //如果枚举到当前位是6,或者已经枚举过了连续3个6。
                                            //那么就需要加上剩下的数能够和当前位组合出连续3个6的情况数
                                            //要注意一点,这里预处理的数组数据其实是倒着用的。
                        for(int l=max(0,3-k-(j==6));l<3;l++) {
                            tmp += dp[i-1][l];
                        }
                    }
                    if(tmp<n) n -= tmp;     //把目前枚举过的数据数目从n中减去,或者也可以记录下来。
                    else {                  //如果超过了n
                                            //超过了n则代表当前位的数字已经确定下来了
                        if(k<3 && j==6) {   //更新k(枚举到的连续6的个数)
                            k++;
                        }
                        if(k<3 && j!=6) {
                            //如果当前枚举到的数字不是6,并且k还没有到3,那么连续六就断了。k归零
                            //反之如果k已经到三了那么接下来是不是6就无所谓了
                            k=0;
                        }
                        printf("%d",j);     //把当前位确定的数输出。
                        break;
                    }
                }
            }
            printf("
    ");
        }
        return 0;
    }
    
    附带一组测试数据
    Input:
    8
    100
    20
    50000000
    49999999
    5
    10
    20
    30

    Output:
    54666
    10666
    6668056399
    6668056398
    4666
    6663
    10666
    16664

    (五)Gerald and Giant Chess - Codeforces 559C

    思路:
    题意:有一个(h*w (1leqslant h,m leqslant 10^5))的网格。其中有(n(1leqslant nleqslant 2000))个黑色格子。现在小可爱要从左上角的格子走到右下角但不能碰到任何黑色格子。问小可爱有多少种走法。(只能向下或向右走)
    这道题是dp专题里的题目,但实际上有一小半是排列组合。

    • 首先对于从((a,b))走到((c,d)),有(C_{c-a+d-b}^{c-a})种方案。令左上角坐标为((1,1))
    • 接下来我们要找出所有经过黑格子的路径总数。通过dp递推可以得到。
      我看题解偷来的思路是:将终点也当作一个黑格子。令dp[i]为只经过第i个黑格子的路径总数。
      可知若i左上角没有黑格子的话 (dp[i] = C_{x_i-1+y_i-1}^{x_i-1})
      当i左上角有其他黑格子时,有$$dp[i] -= sum_{j=1}{i}dp[j]*C_{x_i-x_j+y_i-y_j}{x_i-x_j}$$
      其中dp[j]是i左上角的所有黑格子。
      CF.png-38.8kB

    代码:

    /**/#include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const ll MOD = 1e9+7;
    const int MAX = 2e5+11;
    ll qpow(ll a, ll b) {        //快速幂
        ll res = 1;
        while(b) {
            if(b&1) res = (res*a) % MOD;
            a = (a*a) % MOD;
            b >>= 1;
        }
        return res;
    }
    struct node {               //点结构体,用于排序
        int x,y;
    }point[MAX];
    bool cmp(const node& a, const node& b) { //比较函数
        return a.x==b.x ? a.y<b.y : a.x<b.x;
    }
    ll dp[MAX] = {0};           //dp数组
    ll inv[MAX] = {1}, fac[MAX] = {1};
    ll C(ll n, ll m) {           //组合数函数
        return m ? ((fac[n]*((inv[m]*inv[n-m])%MOD))%MOD) : 1;
    }
    int main() {
        //初始化逆元
        for(int i=1;i<MAX;i++) {
            fac[i] = (fac[i-1]*i) % MOD;
            inv[i] = qpow(fac[i],MOD-2);
        }
        int n, m, k;
        scanf("%d%d%d", &n, &m, &k);
        for(int i=1; i<=k; i++) {
            scanf("%d%d", &point[i].x, &point[i].y);
        }
        sort(point+1, point+k+1, cmp);
        point[++k] = {n,m};     //把终点当作黑格子
        for(int i=1; i<=k; i++) {     //递推
            //先是到第i个黑格子的总情况数
            dp[i] = C(point[i].x+point[i].y-2, point[i].x-1) % MOD;
            for(int j=1; j<i; j++) {  //减去经过左上角的黑格子的情况数
                //排开不在左上角的黑格子
                if(point[j].x>point[i].x || point[j].y>point[i].y) continue;
                dp[i] -= dp[j] * C( point[i].x-point[j].x + point[i].y-point[j].y , 
                                    point[i].x-point[j].x);
                dp[i] = (dp[i] + MOD*2) % MOD;
            }
        }
        //输出只经过终点的所有情况
        printf("%lld", (dp[k]%MOD+MOD*10)%MOD);
        return 0;
    }
    
    附带一组测试数据
    ---Input:
    100 100 10
    3 3
    3 4
    3 10
    3 20
    3 30
    3 40
    3 50
    3 60
    3 70
    3 80

    Output:
    836395822


    ---Input:
    100 100 10
    24 56
    56 66
    56 23
    53 56
    24 25
    99 99
    45 55
    55 98
    25 47
    69 89

    Output:
    328738153


    ---Input:
    100 100
    3
    99 99
    99 100
    100 99

    Output:
    0
  • 相关阅读:
    ajax同步和异步
    vue组件
    type of的返回值有哪些
    git配置
    vue 获取时间戳对象转换为日期格式
    JavaScript运行机制
    单页面开发首屏加载慢,白屏如何解决
    单页面和多页面开发的优缺点
    【安全测试】sql注入
    【Python学习一】使用Python+selenium实现第一个自动化测试脚本
  • 原文地址:https://www.cnblogs.com/xglync/p/11340669.html
Copyright © 2011-2022 走看看