zoukankan      html  css  js  c++  java
  • dp的进阶 (一)

     熟练掌握dp的定义方法。

    ①四维dp的转移,生命值转移时候需要注意的

    ②集合的定义,判断二进制内部是否有环

    ③很难想到的背包问题

    ④博弈类型的dp

    ⑤排列组合类型dp

    ⑥01背包的变种(01背包+完全背包)

    ⑦codeforces Good bye 2016 E 线段树维护dp区间合并

    一:http://www.cnblogs.com/heimao5027/p/5988770.html

    四维dp 或者 剪枝 + dfs Codeforces Beta Round #6 (Div. 2 Only) D

    定义方法:

    定义dp[i][j][k][z]表示目前攻击第i个人,j表示第i-1个人的生命,k表示第i个人的生命,z表示第i+1个人的生命。然后转移需要注意一下以下方面

    ①简单的转移:dp[i][j - b][k - a][z - b] = dp[i][j][k][z];

    ②如果j-b以后的生命值为0了,那么dp[i+1][k - a][z - b][h[i + 2]]=dp[i][j][k][z] + 1;

    ③如果j本来就为0,那么dp[i+1][k][z][h[i + 2]] = dp[i][j][k][z];

    学习转移:如果其中一个的生命值为0,可以往下一个位置进行转移

    二:http://codeforces.com/problemset/problem/11/D

    状压dp判断连通环的个数

    题目大意:给你n个点,m条边,找该图中有几个换

    思路:定义dp[i][j]表示i是圈的集合,j表示该集合的终点,定义起点为这些走过的点里面最小的。然后dp就表示为i集合中所有的环的种类数

    //看看会不会爆int!数组会不会少了一维!
    //取物问题一定要小心先手胜利的条件
    #include <bits/stdc++.h>
    using namespace std;
    #define LL long long
    #define ALL(a) a.begin(), a.end()
    #define pb push_back
    #define mk make_pair
    #define fi first
    #define se second
    #define haha printf("haha
    ")
    const int maxn = 19;
    LL dp[1 << maxn][maxn + 5];
    int a[maxn + 5][maxn + 5];
    int n, m;
    
    void get_min(int val, int &cnt, int &st){
        int pos = 0;
        while (val){
            if (val & 1) st = min(st, pos), cnt++;
            val >>= 1;
            pos++;
        }
    }
    ///定义dp[i][j]表示i是圈的集合,j表示该集合的终点,定义起点为这些走过的点里面最小的
    int main(){
        cin >> n >> m;
        for (int i = 1; i <= m; i++){
            int u, v; scanf("%d%d", &u, &v);
            u--, v--;
            a[u][v] = a[v][u] = 1;
        }
        LL ans = 0;
        for (int i = 0; i < n; i++) dp[1 << i][i] = 1;///初始化
        for (int i = 1; i < (1 << n); i++){
            int cnt = 0, st = n;
            get_min(i, cnt, st);
            for (int j = 0; j < n; j++){
                if (dp[i][j]){///目前集合为i,终点为j。而且根据放入集合的顺序,j必然是在i集合里面的
                    if (a[st][j] && cnt >= 3){///放入集合的最终的终点一定得是在st的旁边。否则会将形不成环的放入ans中
                        ans += dp[i][j];
                    }
                    for (int k = st + 1; k < n; k++){///放入终点大于st的。根据定义st必然为最小的起点。
                        ///然后k<st的已经在更小的状态中全部都计算过了
                        if (a[j][k] && !(i & (1 << k))){
                            dp[i | (1 << k)][k] += dp[i][j];
                        }
                    }
                }
            }
        }
        /*haha;
        for(int i = 0; i < n; i++){
            for (int j = 0; j < (1 << n); j++){
                printf("%lld ", dp[j][i]);
            }
            cout << endl;
        }*/
        cout << ans / 2 << endl;
        return 0;
    }
    View Code

    学习:集合的定义、终点的定义的学习

    当然,看到有位大牛使用记忆化写的。

    /*************
    看了大神的博客才有所感悟啊,记忆化搜索+状态压缩。。。。太神了...
    这种复杂度,近百万的DFS复杂度居然没有TLE,果然经验不足,菜鸟一只。
    用状态压缩枚举起点和可能经过的点。
    可以判定的简单通路 i->j,存在的条数为sum(i->k) 其中k,j之间有边。
    当然,每次计算通路个数的时候,可以借每一个k来判断回路的条数,当然只能算一次。
    这样加出来的回路会有重复,因为可能把顺逆两种方向运动的回路都考虑进去。
    记忆化搜索的好处是可以精确的计算每次遍历点的情况,而如果单纯只是循环的话,却没有这么
    灵活。
    ****************/
    #define LL long long
    #include<cstdio>
    #include<cstring>
    #define haha printf("haha
    ")
    const int LMT=22,LMS=1<<19;
    LL dp[LMS][LMT],ans;
    int n,start,tem,gra[LMT][LMT];
    
    int get_one(int x){
        int res=0;
        do
        res += x&1;
        while(x >>= 1);
        return res;
    }
    
    int left(int x){
        int pos = 0;
        while (x){
            if (x & 1) return pos;
            x >>= 1; pos++;
        }
        return pos;
    }
    
    LL dfs(int mas,int end){
        if(dp[mas][end] >= 0)return dp[mas][end];
        LL res = 0;
        for(int j = start; j < n; j++){
            if(gra[j][end] && ((1 << j) & mas) && (tem == 2 || j != start)){
                ///if (tem == 2 && j == start) haha;
                /**j是可以等于start的,当tem==2,因为这个时候集合里面就只有两个元素了
                所以只有在这样的情况下,j才能等于tem**/
                tem--;
                res+=dfs(mas^(1<<end),j);
                tem++;
            }
        }
        if(tem > 2 && gra[end][start]){
            ans += res;
        }
        dp[mas][end] = res;
        return res;
    }
    
    int main(){
        int m;
        scanf("%d%d",&n,&m);
        memset(dp,-1,sizeof(dp));
        while(m--){
            int u,v;
            scanf("%d%d",&u,&v);
            u--;v--;
            gra[u][v] = gra[v][u] = 1;
        }
        for(int i = 0;i < n; i++) dp[1 << i][i] = 1;
        for(int t = 0; t < (1 << n); t++){
            start = left(t);///以集合中最小的为起点
            tem = get_one(t);///有几个1
            for(int j = start + 1; j < n && tem > 1; j++){
                if(gra[j][start] && ((1<<j) & t)){///以集合中最小的周围的两个为终点
                    dfs(t,j);
                }
            }
        }
        printf("%I64d
    ",ans / 2);
        return 0;
    }
    View Code

    三:http://codeforces.com/problemset/problem/730/J

    题目大意:你有n个杯子,每个杯子里面有a[i]升水,每个杯子的容量为b[i]。问,用最少的杯子来装这些水,在该前提下,倒水所用的时间最短(倒1L水需要1秒)

    思路:我的心路历程很复杂= =。dp一直定义不对,起初是(balabal,还是写在代码中了,这里就不提了)

    然后说一下正确的,定义dp(i,j)表示i个水杯,里面有jL水的最大容量。然后转移就好了。

    //看看会不会爆int!数组会不会少了一维!
    //取物问题一定要小心先手胜利的条件
    #include <bits/stdc++.h>
    using namespace std;
    #pragma comment(linker,"/STACK:102400000,102400000")
    #define LL long long
    #define ALL(a) a.begin(), a.end()
    #define pb push_back
    #define mk make_pair
    #define fi first
    #define se second
    #define haha printf("haha
    ")
    const int maxn = 100 + 5;
    int n;
    /*
    定义dp(i, j)
    假设当前使用i个杯子装水,可装容量为j,所需要的最短时间
    
    定义dp(i, j, f),表示i~j区间内装f个杯子,所需要的最少时间
    
    定义dp(i, j)
    当前为第i个杯子,可用容量为j,所需要的最少的杯子个数
    如果杯子个数一样,那么就记录下时间
    并且维护一下前一个该区间的可用容量
    
    定义dp(i, j)
    用i个杯子,目前i个杯子里面的含水量为j的容量最大的值
    */
    int b[maxn];
    int dp[maxn][maxn * maxn];
    pair<int, int> a[maxn];
    
    int main(){
        scanf("%d", &n);
        int watersum = 0;
        for (int i = 1; i <= n; i++){///已有体积
            scanf("%d", &a[i].fi);
            watersum += a[i].fi;
        }
        for (int i = 1; i <= n; i++){///容量
            scanf("%d", &a[i].se);
            b[i] = a[i].se;
        }
        sort(b + 1, b + 1 + n);
        int cnt = 0, sum = 0;
        for (int i = n; i > 0; i--){
            sum += b[i];
            cnt++;
            if (sum >= watersum) break;
        }
        memset(dp, -1, sizeof(dp));
        dp[0][0] = 0;
        for (int i = 1; i <= n; i++){
            for (int j = sum; j >= a[i].fi; j--){
                for (int k = i; k >= 1; k--){
                    if (dp[k - 1][j - a[i].fi] != -1){
                        dp[k][j] = max(dp[k][j], dp[k - 1][j - a[i].fi] + a[i].se);
                    }
                }
            }
        }
        int ans = 0x3f3f3f3f;
        for (int i = sum; i >= 0; i--){
            if (dp[cnt][i] != -1 && dp[cnt][i] >= watersum){
                ans = min(ans, watersum - i);
            }
        }
        printf("%d %d
    ", cnt, ans);
        return 0;
    }
    View Code

    学习:讲道理这道题我一直想着正向推,结果思维定式了。这题应该反过来定义最大容量然后逆向推的,我还是太菜了

    四:Codeforces Round #376 (Div. 2) E

    题目大意:每次都选左边k(2<=k<=m)个合并,然后那个人得到合并以后的val,并且将这个val放在最左边。问两个人都采取最优的方法。问差值最大的几?

    思路一:dfs的方法

    因为每次都至少取两个,且每次取过以后就合并,所以不难想到要维护一下前缀和。然后就是我们发现,每次取的时候要么是某个人接着取,要么就是那个人不取了,换成另外一个人取,所以每个位置会有两种决策,但是前缀和的维护还是不变的。所以我们用dfs可以很简单的时间这种思路。复杂度O(n)

    //看看会不会爆int!数组会不会少了一维!
    //取物问题一定要小心先手胜利的条件
    #include <bits/stdc++.h>
    using namespace std;
    #pragma comment(linker,"/STACK:102400000,102400000")
    #define LL long long
    #define ALL(a) a.begin(), a.end()
    #define pb push_back
    #define mk make_pair
    #define fi first
    #define se second
    #define haha printf("haha
    ")
    /*
    题目大意:
    每次都选左边k(2<=k<=m)个合并,然后那个人得到合并以后的val,并且将这个val放在最左边
    问两个人都采取最优的方法。问差值最大的几?
    */
    const int maxn = 200000 + 5;
    LL sum[maxn];
    int n;
    
    LL dfs(int pos){
        if (pos == n) return sum[n];
        LL val = dfs(pos + 1);
        return max(val, sum[pos] - val);
    }
    
    int main(){
        scanf("%d", &n);
        for (int i = 1; i <= n; i++){
            LL val; scanf("%lld", &val);
            sum[i] = sum[i - 1] + val;
        }
        printf("%lld
    ", dfs(2));
        return 0;
    }
    View Code

    关键:要明白每一次取的val是要么一个人接着取,要么就换一个人。我之所以卡住了是因为每次dp的时候都想要把所有的都合并起来,所以没有想到这里

    五:Codeforces Round #187 (Div. 1) C

    题目大意:给你一个len=n的数字串,你需要找出他所有的非空非严格递增的子序列。然后把所有子序列的和输出即可。

    例如样例二的解释为:给你一个1 2 2的串,他的子串有:{1}{2}{1,2}{2,2}{1,2,2}

    思路:orz,以前没想到自己竟然做过一遍,然后做过一遍以后现在再做又不会了,果然是没有定时复习的缘故吧?

    定义dp(i)表示目前以数字i为结尾的所有子串的val和,那么dp[K] =( dp[0] + dp[1] + dp[2] +... + dp[K] ) * K  + K; 所以要用到个东东动态统计前缀和

    举例:1 2 3

    第一步:{1}

    第二步:{1,2} {2}

    第三步:{1,2,3}{2,3}{3}

    所以dp(k)的转移方程就很容易可以写出来了

    #include<bits/stdc++.h>
    
    using namespace std;
    
    typedef long long ll;
    const int mod = 1e9 + 7;
    const int maxa = 1000000 + 50;
    int n;
    ll a[maxa];
    ll b[maxa];
    ll tree[maxa << 2];
    
    ll sum(ll x){
        ll res = 0;
        while (x){
            res = (res + tree[x]) % mod;
            x -= x & -x;
        }
        return res % mod;
    }
    
    void add(ll x, ll val){
        while (x <= maxa){
            tree[x] =  (tree[x] + val) % mod;
            x += x & -x;
        }
    }
    
    int main(){
        scanf("%d", &n);
        ll res = 0;
        for (int i = 1; i <= n; i++){
            ll t;
            scanf("%lld", &t);
            ll tmp = sum(t);
            tmp = (tmp * t + t) % mod;
            res = (res + tmp - a[t] + mod) % mod;
            add(t, tmp - a[t]);
            a[t] = tmp;
        }
        printf("%lld
    ", res);
        return 0;
    }
    View Code

    关键:题目一定要读懂,明白方式搭配

    六:HDU 3033

    题目大意:题意:有S款运动鞋,一个n件,总钱数为m,求不超过总钱数且每款鞋子至少买一双的情况下,使价值最大。如果 有一款买不到,就输出“Impossible"。

    思路:这个题的关键还是在于初始化,如果我们一开始把dp初始化为0,则当所有鞋子的价值都是0时,我们就无法区分是买不全那几款鞋子还是能买全但最大价值是0;因此,要把S!=0的dp[S][j]初始化为-1,便于区分。
    这是一个带分组的01背包问题,与普通分组背包不同的是,这个分组背包关键在于每组至少取1个,而不是最多1个。 
    dp[i][k]是不选择当前鞋子;
    dp[i-1][k-v[j]]+w[j]是选择当前鞋子,但是是第一次在本组中选,由于开始将该组dp赋为了-1,所以第一次取时,必须由上一组的结果推知,这样才能保证得到全局最优解;//01背包部分
    dp[i][k-v[j]]+w[j]表示选择当前鞋子,并且不是第一次取。//完全背包部分
    //看看会不会爆int!数组会不会少了一维!
    //取物问题一定要小心先手胜利的条件
    #include <bits/stdc++.h>
    using namespace std;
    #pragma comment(linker,"/STACK:102400000,102400000")
    #define LL long long
    #define ALL(a) a.begin(), a.end()
    #define pb push_back
    #define mk make_pair
    #define fi first
    #define se second
    #define haha printf("haha
    ")
    /*
    题目大意:有n个鞋子,有m元,k类鞋子
    输入n行,每行表示种类,价格,和val
    并且每种鞋子都至少选一个
    */
    const int maxn = 10000 + 5;
    vector<pair<int, LL> > ve[30];
    int n, m, k;
    LL dp[30][maxn];
    /*
    定义dp(i, j)表示目前是第i种商品,还剩下j元
    */
    LL solve(){
        memset(dp, -1, sizeof(dp));
        LL ans = -1;
        for (int j = 0; j <= m; j++) dp[0][j] = 0;
        for (int i = 1; i <= k; i++){
            for (int f = 0; f < ve[i].size(); f++){
                for (int j = m; j >= ve[i][f].fi; j--){
                    //if (dp[i - 1][j - ve[i][f].fi] == -1) continue;
                    dp[i][j] = max(dp[i - 1][j - ve[i][f].fi] + ve[i][f].se, max(dp[i][j], dp[i][j - ve[i][f].fi] + ve[i][f].se));
                }
            }
        }
        for (int i = 0; i <= m; i++){
            ans = max(ans, dp[k][i]);
        }
        return ans;
    }
    
    int main(){
        while (scanf("%d%d%d", &n, &m, &k) == 3){
            for (int i = 1; i <= k; i++) ve[i].clear();
            for (int i = 1; i <= n; i++){
                int a, b, c; scanf("%d%d%d", &a, &b, &c);
                if (b > m) continue;
                ve[a].push_back(mk(b, 1LL * c));
            }
            LL val = solve();
            if (val >= 0) printf("%lld
    ", val);
            else puts("Impossible");
        }
        return 0;
    }
    /*
    5 50 3
    1 10 20
    1 15 30
    3 10 60
    3 15 30
    2 10 10
    ans : 130
    */
    View Code

    关键:既要掌握01背包的dp,又要掌握完全背包的dp,

    七:链接:这里

    codeforces Good bye 2016 E 线段树维护dp区间合并

    题目大意:给你一个字符串,范围为‘0’~'9',定义一个ugly的串,即串中的子串不能有2016,但是一定要有2017,问,最少删除多少个字符,使得串中符合ugly串?

    思路:定义dp(i, j),其中i=5,j=5,因为只需要删除2016当中其中一个即可,所以一共所需要删除的字符和需要的字符为20176,因此i和j只要5就够了。

    然后转移就是dp(i,i) = 0, 如果说区间大小为1的话,那么如果是2017中的一个,那么就是dp(pos, pos+1) = 0, dp(pos,pos) = 1。但是如果pos为'6'这个字符,那么定义6这个pos=x转移就要为dp[x-1][x-1] = dp[x][x] = 1.然后我们再去转移就好了。

    然后最后一定要注意线段树的合并顺序哦

    //看看会不会爆int!数组会不会少了一维!
    //取物问题一定要小心先手胜利的条件
    #include <bits/stdc++.h>
    using namespace std;
    #pragma comment(linker,"/STACK:102400000,102400000")
    #define LL long long
    #define ALL(a) a.begin(), a.end()
    #define pb push_back
    #define mk make_pair
    #define fi first
    #define se second
    #define haha printf("haha
    ")
    /*
    定义dp(x,i,j)表示区间[0,x)内能够获得的2017的前缀长度为i的,至少要删除多少个字符
    最少要删除多少个才能构成前缀为j的2017,然后我们不需要去管x,只需要用线段树去维护i,j即可。
    */
    const int inf = 0x3f3f3f3f;
    const int maxn = 200000 + 5;
    struct Node{
        int dp[5][5];
    }tree[maxn << 2];
    int n, q;
    char ch[maxn];
    map<char, int> id;
    
    inline void init(Node &t){
        for (int i = 0; i < 5; i++)
            for (int j = 0; j < 5; j++)
                t.dp[i][j] = inf;
    }
    
    Node Merge(Node a, Node b){
        Node tmp; init(tmp);
        for (int i = 0; i <= 4; i++)
            for (int j = i; j <= 4; j++)
                for (int k = i; k <= j; k++)
                    tmp.dp[i][j] = min(tmp.dp[i][j], a.dp[i][k] + b.dp[k][j]);
        return tmp;
    }
    
    void display(Node t){
        for (int i = 0; i < 5; i++){
            for (int j = 0; j < 5; j++)
                printf("%d ", t.dp[i][j]);
            cout << endl;
        }
    }
    
    void buildtree(int l, int r, int o){
        if (l == r){
            init(tree[o]);
            for (int i = 0; i < 5; i++)
                tree[o].dp[i][i] = 0;
            int pos = id[ch[l]];
            if (pos <= 3) tree[o].dp[pos][pos] = 1, tree[o].dp[pos][pos + 1] = 0;
            if (pos == 4) tree[o].dp[3][3] = tree[o].dp[4][4] = 1;
            return ;
        }
        int mid = (l + r) / 2;
        if (l <= mid) buildtree(l, mid, o << 1);
        if (r > mid) buildtree(mid + 1, r, o << 1 | 1);
        tree[o] = Merge(tree[o << 1], tree[o << 1 | 1]);
        //display(tree[o]);
    }
    
    Node ans;
    void query(int ql, int qr, int l, int r, int o){
        if (ql <= l && qr >= r) {
            if (ql == l) ans = tree[o];
            else ans = Merge(ans, tree[o]);
            return ;
        }
        int mid = (l + r) / 2;
        if (ql <= mid) query(ql, qr, l, mid, o << 1);
        if (qr > mid) query(ql, qr, mid + 1, r, o << 1 | 1);
    }
    
    int main(){
        int cnt = 4;
        id['2'] = 0, id['0'] = 1, id['1'] = 2, id['7'] = 3, id['6'] = 4;
        for (char i = '3'; i <= '9'; i++) if(id.count(i) == 0) id[i] = ++cnt;
        cin >> n >> q;
        scanf("%s", ch + 1);
        buildtree(1, n, 1);
        for (int i = 1; i <= q; i++){
            int ql, qr; scanf("%d%d", &ql, &qr);
            init(ans);
            query(ql, qr, 1, n, 1);
            if (ans.dp[0][4] >= inf) puts("-1");
            else printf("%d
    ", ans.dp[0][4]);
        }
        return 0;
    }
    View Code

    关键:线段树合并

    八:

    九:

    十:

    十一:

    十二:

    十三:

  • 相关阅读:
    转 TClientDataSet的地位
    让Delphi的DataSnap发挥最大效率
    Delphi2010强化的反射
    Delphi的内存管理及内存泄露问题
    DataSnap 传输过滤器
    delphi dataset not in edit or insert mode
    CSS中Zoom属性的一些介绍
    Linux进程分析
    MIPS处理器介绍
    Linux内存管理
  • 原文地址:https://www.cnblogs.com/heimao5027/p/5993482.html
Copyright © 2011-2022 走看看