zoukankan      html  css  js  c++  java
  • DAY 5 & 6

    DAY 5

    之前整过一个DP

    动态规划  DP

    啥是DP?

    DP等价于DAG!!!

    (1)无后效性:DP的所有状态之间组成一个DAG

    (2)最优子结构

    (3)阶段性

    (4)转移方程:如何计算状态

                              一般是顺序转移

                              有时候乱序,因为DP 是DAG,可以拓扑排序,然后从头for一遍

    (5)状态:题目要算的东西

    以斐波那契数列为例,求斐波那契数列第n项

    边界条件:

    1.递推: 用其他计算好的结果得到自己的结果

     

     2.递归:自己的值更新其他值

    任何DP都可以写出这两种,但是会出现情况,一种hin难写,另一种hin简单

    3.记忆化搜索

    斐波那契相当于一个递归,那就写一个递归

     

    复杂度O(数字大小,fn)

    斐波那数列的第n项接近2^n

    每次产生一个新数都要多次计算已经算过的项

    考虑一个东西算出来就存下来

    那么就记忆化搜索

    int f[233];
    bool g[233];
    
    int dfs(int n)
    {
        if (n==0) return 0;
        if (n==1) return 1;
        if (g[n]) return f[n];
        f[n] = dfs(n-2) + dfs(n-1);
        g[n]=true;
        return f[n];
    }

     

      01背包

    例题:采药

    N个物品,M个体积

    物品i,体积vi,价值wi

    头疼:设计状态

    放物品,考虑有什么未知量???

    第一维:放了多少物品,[i]放好了i个物品

    第二维:体积 [j] 已经放进去的物品,体积是多少

    F[i][j] 放进i个物品,体积为j的最大价值

    放进物品的编号<=i ,每个物品可以放或者不放,j已经放进去i个物品的体积之和

    如何转移???

    每次都考虑好了前i种物品,那么就考虑第i+1放或者不放

    如果不放第i +1个物品,体积不变,价值不变

    放了后体积会+vi,价值+wi

     f[ i ][ j ] = max( f[ i-1 ][ j ]  ,  f[ i-1 ][ j-v[i] ] + wi )

    外层for循环维度

      状态转移

    J要判断啊,不可以超过背包容积

    最后答案不一定是f[n][m],因为不一定体积越大价值越大,但是答案一定是考虑了所有n个物品,所以for一遍体积,ans取max

        for (int i=1;i<=n;i++)
            for (int j=0;j<=m;j++)
            {
                f[i][j] = f[i-1][j];
                if (j >= v[i]) f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
            }
            
        int ans=0;
        for (int a=0;a<=m;a++)
            ans = max(ans,f[n][a]);
        cout << ans << endl;

     

      无限背包问题

     

    从别人转移到自己

    放0个:f[i-1][j]

    放1个:f[i-1][j-vi]

    放2个:f[i-1][j-2*vi]

    放k个:f[i-1][j-k*vi]

    可以枚举第i个物品到底放了几个

    for (int i=1;i<=n;i++)
            for (int j=0;j<=m;j++)
                for (int k=0;k*v[i]<=j;k++)
                    f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);

    要判断k*v[i]<=j,保证体积不会爆炸

    But 3层循环 O(n^3),慢了hin多

    如何优化???

    其实不需要降维,只需要把01背包改一下

    f[ i ][ j ] = max( f[ i ][ j ] , f[ i ][ j-v[i] ] + w[ i ] )

     

    之前是由i-1这种i的上一层状态转移过来的

    此时就可以同一行的转移,横向转移,转移多少次就相当于一种物品放了多少个

    复杂度 : O(nm)

    for (int i=1;i<=n;i++)
            for (int j=0;j<=m;j++)
            {
                f[i][j] = f[i-1][j];
                if (j >= v[i]) f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);
            }
        int ans=0;
        for (int a=0;a<=m;a++)
            ans = max(ans,f[n][a]);
        cout << ans << endl;

    有限背包问题

     

    直接k枚举多少次 O(n^3)

    考虑优化???

    比如现在一个物品能放13次

    造体积为vi,价值为wi,只能用1次的物品

    造体积为2vi,价值为2wi,只能用1次的物品

    造体积为4vi,价值为4wi,只能用1次的物品

    造体积为6vi,价值为6wi,只能用1次的物品

    变成捆绑包的组合

    01背包问题

    O(n^2  *  k)

    如何处理捆绑包大小???

    相当于二进制拆分

    (1)如果一个数字可以被2^k这样111111分解,那么很显然捆绑包组合起来可以实现所有值

    (2)那如果不能111111这样分解,最后一定会剩下一个数字,把这个数字打包成一个捆绑包,也就是你算的时候,先算上这个数字,然后后面就变成一个可以被1111拆分的,就又回到上面情况

            比如:10101

                       1111可以表示1~1111的所有数字

                       剩下110

                       1~1111都加上110

                       就可以表示111~10101

                       所以就可以表示1~10101

    拆出来捆绑包个数k≈logn

    所以复杂度就是O( nm logn )

    只需要读入处理一下

    #include<iostream>
    
    using namespace std;
    
    int n,m,w[233],v[233];
    int f[233][233];
    
    int main()
    {
        cin >> n >> m;
        int cnt = 0;
        for (int a=1;a<=n;a++)
        {
            int v_,w_,z;
            cin >> v_>> w_ >> z;
            
            int x = 1;
            while (x <= z)
            {
                cnt ++;
                v[cnt] = v_*x;
                w[cnt] = w_*x;
                z-=x;
                x*=2;
            }
            if (z>0)  //Z最后有剩余,新建一个捆绑包 
            {
                cnt ++;
                v[cnt] = v_*z;
                w[cnt] = w_*z;
            }
        }
        n=cnt;
        for (int i=1;i<=n;i++)
            for (int j=0;j<=m;j++)
            {
                f[i][j] = f[i-1][j];
                if (j >= v[i]) f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
            }
        int ans=0;
        for (int a=0;a<=m;a++)
            ans = max(ans,f[n][a]);
        cout << ans << endl;
        return 0;
    }

    基础DP

     

    DP----鬼畜的数字三角形

    希望走过的路径最大

    变化量:走的过程中,位置变化

    f[i][j] 走到(i,j)时走过路径的最大值

    可以从左上角或者正上方转移过来

    转移方程:f[ i ][ j ] = max( f[ i-1 ][ j ] , f[ i-1 ][ j-1 ] ) + a[ i ][ j ]

    Joyoi

    http://www.joyoi.cn/

     数字三角形2

    PS: 实在不行加一维

    f[i][j] 走到(i,j) %100 后的最大值

    错误

    前面的最优不一定保证后面最优

     

    比如你现在在98 99中选择,99一定最优,但是下一步再选,就会降为hin小的数

    维度不够加一维

    所以考虑加一维度

    bool  f[i][j][k]表示走到[i][j]时的路径和%100=k是否可行

    #include<iostream>
    
    using namespace std;
    
    bool f[233][233][233];  //bool数组 
    
    int main()
    {
        cin >> n;
        for (int i=1;i<=n;i++)
            for (int j=1;j<=i;j++)
                cin >> a[i][j];
                
        f[1][1][a[1][1] % 100] = true;  //初始化 
        for (int i=1;i<n;i++)
            for (int j=1;j<=i;j++)
                for (int k=0;k<100;k++)
                    if (f[i][j][k])  //判断之前是否可达,否则走就没必要 
                    {
                        f[i+1][j][(k+a[i+1][j])%100]=true;
                        f[i+1][j+1][(k+a[i+1][j+1])%100]=true;
                    }
        
        for (int j=1;j<=n;j++)   //最后枚举在哪一列 
            for (int k=0;k<100;k++)  
                if (f[n][j][k]) ans=max(ans,k);
        cout << ans << endl;
        
        return 0;
    }

    最长上升子序列

    数据大的话就数据结构优化

    f[i] 以i为结尾的最长上升子序列的长度

    O(n^2)

    N<=1e5   --->线段树

    区间DP

    状压DP

    1. 平面上N个点,第i个点坐标(xi,yi),从1号点出发,在平面上任意走,求走遍所有点再回来的最短路径

    使得距离最短,一个点没必要走两次,每次从没走过的点选一个出来

    f[s][i]  i 在i点,s走过了哪些点

    状态压缩:把一个数组压缩成一个数

     

    自己更新别人

    所以我们要找没走过的点,枚举j,看看s的第j位二进制位是不是0

    最后答案看看停在那个点,最后还要回来

    #include<iostream>
    
    using namespace std;
    
    double f[][];
    double x[233],y[233];
    
    int main()
    {
        cin >> n;
        for (int a=0;a<n;a++)
            cin >> x[a] >> y[a];
        f=∞
        f[1][0]=0;
        for (int s=0;s<(1<<n);s++)
            for (int i=0;i<n;i++)
                if (f[s][i] < ∞)
                {
                    for (int j=0;j<n;j++)
                        if ( ((s>>j) & 1) == 0)
                        {
                            int news = s | (1<<j);
                            f[news][j] = min(f[news][j],f[s][i] + dis(i,j));
                        }
                }
        for (int i=0;i<n;i++)
            ans=min(ans, f[(1<<n)-1][i] + dis(i,0));
            
        return 0;
    }

    O(2^n  *  n^2)

    N<=22或者N<=20

    一定是先枚举s,然后s从小到大,因为走过的点是越来越多的

    旅行商问题 TSP问题

    P1171 售货员的难题

    P1879 [USACO06NOV]玉米田

     

    一个点四联通不种草

    F[i][s] 前i行已经种完了,而且第i行草种成了什么样,二进制数代表第i行每个位置中不中草

    此情况下的方案数

    i+1行的S’二进制没有相邻的1

    第i行的草和i+1行的草不相邻

    S&S’=0 上下没有相邻的草

    这个题恰好放k个国王,多了一个条件,多加一个维度

    f[i][s][j] 前i行国王已经放好了,已经放了j个国王,s记录第i行国王摆放状态

    P1896 [SCOI2005]互不侵犯

     数位DP

    按照数的每一位数字一位一位转移

     

    给定两个数L~R ,求有多少个数

    (1)前缀和转化

    0~R– 0~L-1

    问题转化0~x中有多少个数

    千  百   十  个

    3   2    4    5

    求多少个y,满足0<=y<=x

    满足条件的y最多4 位,一位一位填数

    从高位向低位填数

    f[i][j]   已经填到了i位,j=0填的数已经小于x,j=1填的数不确定是否小于x,目前填的数字和x一样,在这种情况下有多少个数

    转移,下一位填0~9中的哪个数

    数组z存x的每一位

    X只有L位,填到L+1位和x相等的方案数为1 ,都填 0

    自己更新别人

    J枚举小于还是等于

    考虑填k之后y会不会比x大

     

    #include<iostream>
    
    using namespace std;
    
    int solve(int x)
    {
        int l=0;
        while (x>0)
        {
            l++;
            z[l] = x%10;
            x/=10;
        }
        memset(f,0,sizeof(f));
        memset(g,0,sizeof(g));
        f[l+1][1]=1;
        g[l+1][1]=0;
        for (int i=l+1;i>=2;i--)
            for (int j=0;j<=1;j++)
                for (int k=0;k<=9;k++)
                {
                    if (j==1 && k>z[i-1]) continue;
                    int j_;
                    if (j==0) j_=0;
                    else if (k==z[i-1]) j_=1;
                    else j_=0;
                    f[i-1][j_] += f[i][j];
                    g[i-1][j_] += f[i][j] * k + g[i][j];
                }
        return g[1][0] + g[1][1];
    }
    
    int main()
    {
        cin >> l >> r;
        cout << solve(r) - solve(l-1) << endl;
        
        return 0;
    }

     

    多一个条件多一个维度

     

    F[i][j][k] 填好了前i位

    保证相邻两位>0

    P2657 [SCOI2009]windy

     

    F[i][j][k]  k存当前乘积的话,太大存不下

     

    多加维度

    r都是1位数相乘,>10的质因子不可能出现,所以r当中有hin多空位置

     

    分解质因数

    数组里面有hin多空的

    那就分解开

     

    因为还是会有浪费,因为这些质数不可能同时达到上界

    BFS预处理算出30000多个长成这个样子的数字  f[i][j][k] 直接用就好

     树形DP

    给你一棵N个点的树,让你求有多少个点

    树形DP必须是有根数,没有根就加根

    f[i] 以i为根的子树有多少个点

     

    树形DP转移:儿子节点整合转移到父亲节点

     

    在树上找到两个点,使得他们的距离最远

     

    A->LCA->B

    看做LCA向下的两段

    F[i][0] 从i点向下的最大路径长

    F[i][1] 从i点向下的次大路径长

     

    F[i][0]=max( f[pj][0] ) + 1

    假设最长路经过pk

    F[i][1] 在剩下点里面找最长路,不找 pk了

    #include<iostream>
    
    using namespace std;
    
    void dfs(int i)
    {
        for (p is i's son)
            dfs(p); //由于所有点都是从下面转过来的,所以先递归儿子
        
        for (p is i's son)
        {
            int v = f[p][0]+1;
            if (v>f[i][0]) //找到一条新的最长路
            {
                f[i][1]=f[i][0];
                f[i][0]=v;
            }
            else if (v>f[i][1]) f[i][1]=v;
        }   //否则和次长路比较
    }
    
    int main()
    {
        cin >> n;
        du ru he jian shu;
        
        dfs(1);
        
        return 0;
    }

    P3304 [SDOI2013]直径

    P4408 [NOI2003]逃学的小孩

     

    所有点之间路径的长度之和(边权都是1)

    F[i] 以i为根的子树有多少个点

    枚举两个点,找他们的路径

    转化:

    考虑一条边可以被多少条路径被经过

     

    第一个点在子树里,子树外

     

    P1352 没有上司的舞会

    f[i] 以i为根的子树,所得边权之和最大

    f[i][0  /   1] 在以i为根的子树里,0à i没选,1ài选了

    i选了,那么他的儿子都不能选

     

    f[i][j]=∑ f[j][0]  +  a[i]  (j shuyi son[i])

    i不选,他的儿子可以选或者不选

     

     

    P2016 战略游戏

    f[i][0/1] i的子树都被守护,i放不放士兵

    和上一个题几乎一样

    变式:

    每个士兵都会守护距离自己不超过2的节点

    f[i][0/1/2] i的子树被完全覆盖,从i向下走,距离最近的士兵的距离是0/1/2

    0à自己是兵

    1à儿子是兵

    2à孙子是兵

     

     

    g[j][0/1]

    确定了前j个儿子的取值,其中有没有一个儿子用自己的0值更新答案(有没有一个儿子上边有士兵)再来一个DP

    DP套DP

    P2279 [HNOI2003]消防局的设立

           N堆石子,a1,a2,a3,a4,...可以合并任意两堆石子,合并代价为两堆石子的异或 ^ 和,求最小代价,N<=16

    状压DP

    f[s] 把s堆石子合并成一堆得最小代价,那么ans= f [ 2^n  - 1 ]

    最后一次操作也是吧两边石子合成一堆

    S现在包含 5 3 2 0堆石子

    你现在会有以下的情况:

    枚举s的所有子集,然后和剩下的合并

     then

    枚举子集,子集所对应的数一定比s小,可以直接枚举一个a,比s小(从1枚举,0无意义)

      

    复杂度(2^n * 2^n = 4^n) 

    优化???

    能不能只去枚举s的子集,那么会变快

    模拟一下看看它在干哈

    复杂度(3^n)

    3^n  n=16,14  的状压

    #include<iostream>
    
    using namespace std;
    
    int main()
    {
        cin >> n;
        for (int a=0;a<n;a++)
            cin >> z[a];
        for (int a=0;a<(1<<n);a++)
            for (int b=0;b<n;b++)
                if (a & (1<<b))
                    sum[a] += z[b]; //a状态石子的总个数
        
        memset(f,0x3f,sizeof(f)); //求最小值,初始化无穷大 
        for (int a=0;a<n;a++)
            f[1<<a] = 0;  //把一堆石子合并,代价为0 
        
        //未优化
        /*    
        for (int s=0;s<(1<<n);s++)
            for (int a=1;a<s;a++)  //枚举s的子集 
                if ( (s|a) == s)  //判断是不是s的子集 
                    f[s] = min(f[s],f[a]+f[a^s]+(sum[a]^sum[a^s]));
        */
        
        //优化后            
        for (int s=0;s<(1<<n);s++)
            for (int a=(s-1)&s;a;a=(a-1)&s) //仅枚举s的子集,把a变成s的子集 
                f[s] = min(f[s],f[a]+f[a^s]+(sum[a]^sum[a^s]));
                    
        cout << f[(1<<n)-1] << endl;
            
            
    }

    博弈论DP

    1. 一个游戏G,两个人玩,Alice 和 Bob

    2.回合制游戏:a,b,a,b,a,b.......

    3.游戏没有平局

    4.胜负的区分方法:当某个人无法继续操作,他就输了,所以不存在计分 

    两人玩游戏,回合制,谁减到0谁就赢

    一般问的问题是,先手是否必胜/败

     so,如何Dp???

     游戏G有hin多状态,每进行一次操作,当前状态变成另一个状态,当没有状态可以转移,就必败

    必败态:谁在这个状态操作,谁必败

    必胜态:在此操作,对面进入必败态

     DP  DAG,拓扑处理

    如果从一个点出发,走一步能够走到必败态,他就是必胜态

    f[s] s是游戏的一个状态,f代表这个状态是必胜还是必败

    如果存在si是false,那么他就是必胜态

     回题目:

    未知量:还剩下多少数字,上一次对手减去的数字

    f[i][j] 数字还剩下 i,对手上一轮减的数字是 j,此时自己是必胜态还是必败态

    枚举1<=r<=k*j

    那么f[i][j] 可以转移到 f[i-r][r]

    博弈论DP建议记忆化搜索

    ALice第一步可以减去1~s-1的所有数字,一旦她可以转移到一个对手的必败态,那么她一定必胜

    DFS终止条件 i=0 ,自己就输了 ,还剩下0,就说明上一轮对手已经把数字减到0了

     f 必胜必败态记录,g记录是否被算过

    #include<iostream>
    
    using namespace std;
    
    bool f[][],g[][];
    //g[][]记忆化搜索,记录是否搜索过 
    bool dfs(int i,int j)
    {
        if (i==0) return false;
        if (g[i][j]) return f[i][j];
        g[i][j]=true;f[i][j]=false;
        for (int r=1;r<=i && r<=k*j;r++)
            if (dfs(i-r,r) == false) f[i][j]=true;
        return f[i][j];
    }
    
    int main()
    {
        cin >> s >> k;
        for (int a=1;a<s;a++)  //枚举Alice的转移状态 
            if (dfs(s-a,a) == false)  //一旦她可以转移到一个必败态,那她一定是必胜态 
            {
                cout << "Alice" << endl;
                return 0;
            }
        cout << "Bob" << endl;
        
        return 0;
    }

    Type 2

    两个人A,B,回合制,不能动为输

    一共N个游戏,每次Alice和Bob可以从中挑一个游戏玩

    取石子游戏

    N堆石子,a1,a2,a3.....an

    A,B每次可以从某一堆石子中取走任意多个石子。当谁没有办法再取石子,就输了

    N维DP???

     

    sg[0]=0  对于任何必败态的值为0

    sg[1]=  .... 找到1能转移到的所有状态的sg值,写出来,从中找一个最小的没有出现的自然数 

     ..........

    sg[n]  

     

    对于本题目,sg没有必要刻意算

    再看一下SG

     

     SG函数有啥用???

    如果一个游戏的SG!=0  先手必胜 

                             SG==0 先手必败

     如何变成N个游戏???

    SG定理:

    N个游戏的sg值等于每个游戏的sg值异或起来

     

    证明正确性,自行百度(然后你看了不到20'就会放弃)

    int main()
    {
        cin >> n;
        int ans=0;
        for (int a=1;a<=n;a++)
        {
            int v;
            cin >> v;
            ans = ans ^v;
        }
        if (ans!=0) cout << "Alice" << endl;
        else cout << "Bob" << endl;
    }

    P2197 【模板】nim游戏

     

    变式

    N堆石子,a1,a2,a3,.....an

    每次可以从一堆石子中取走1~4个石子

    PS:一般与sg函数有关的题,都是要找规律,手算SG值,写到程序里

    sg[0]=0

    sg[1]=1

    sg[2]=2

    sg[3]=3

    sg[4]=4

    sg[5]=0

    sg[6]=1

    .................

    sg[ai]=ai%5

    所以算每个数%5的异或和,非0 ,先手必胜,否则先手必败

    (20多道博弈论题目)

    把问题转化为基本取石子游戏 nim

     算出每堆得奇偶性

     把所有奇数堆的下标取出来,然后异或下标,等于0 ,先手必败,否则必胜

    操作有两种情况:

    (1)左偶右奇

     

    相当于把1从右向左移

    (2)左奇右奇

    有两堆一样的东西,相当于没有,因为异或起来为0

    所以就是把1不断往前移动,直到把1移到第一堆就无路可走了,因为此时就是只有第一堆是1 ,其余都是0,没法选啊

     因为这道题下标数就是这堆石子数

     每次向前移动1,下标减小,相当于石子数减小

    这样任意 i < j, 就可以把 j取成 j - 1, j - 2, j - 3 到 0,然后原题就变成那个简单题了

     就是说如果是奇数,我把奇数的那堆移到最左边就好了,如果是偶数,还是向左取,因为这样也是最优操作

     

     

    把所有下标为奇数的石子数目异或

    只要对手能把偶数搬到奇数位置,奇数都能被搬去偶数位置

    任何石子被搬到偶数位上都没用,不影响局面

    搬奇数的就好

     

     

     

    同上一个题

    只有距离终点的曼哈顿距离为奇数的格子上的棋子对于题目有用

    每个人的标记都是一样的

     f[s] 在s状态下,哪个位置有没有标记过,此情况下必胜或者必败,但是存不下

    考虑当做多个游戏

     sg[i]  对于一个长度为i的横条,是必败态还是必胜态

    所以本题就是sg[3]^sg[4]

    #include<iostream>
    #include<algorithm>
    #include<vector>
    
    using namespace std;
    
    int dfs(int n)
    {
        if (n==0) return 0;
        if (f[n]) return sg[n];
        f[n]=true;
        vector<int> z;  //记录能够转移到的所有sg值 
        for (int a=1;a<=n;a++)  //枚举中间可用点,把游戏拆成两份 
        {
            int l = max(a-3,0);  //看左右两边可用长度 
            int r = max(n-a-2,0);
            z.push_back( dfs(l) ^ dfs(r) ); //求出当前点能够转移到的所有状态的sg值 
        }
        
        sort(z.begin(),z.end());  //先排序 
        z.push_back(233333333); 
        //在数组后面放一个hin大的数字,防止越界,因为sg可能大于数组中的任何一个数
        
        //求sg值 
        for (int a=0,p=0;;a++)
        {
            if (z[p] != a)
            {
                sg[n] = a;
                return sg[n];
            }
            while (z[p]==a)  //sg值可能有重复 
                p++;
        }
    }
    
    int main()
    {
        cin >> n;
        if (dfs(n)) cout << "Alice" << endl;
        else cout << "Bob" << endl;
    }

    f[x][y] 当前状态为(x,y)时,是必胜态还是必败态

    注意题目n<=30000

    发现存不下

    优化:拆分

    x=2^a*3^b

     

    P3541 [POI2012]LIC-Bidding


     强烈安利

     acm.gdu.edu.cn

    ACM Steps  有hin多专题训练

  • 相关阅读:
    [codeforces] 97B Superset || 平面分治
    [hdu] 5696 区间的价值 || 序列分治
    [zoj] 1937 [poj] 2248 Addition Chains || ID-DFS
    [poj] 2286 The Rotation Game || ID-DFS
    [codeforces] 25E Test || hash
    luogu P1196 银河英雄传说
    luogu P1357 花园
    luogu P1156 垃圾陷阱
    luogu P1127 词链
    luogu P1131 时态同步
  • 原文地址:https://www.cnblogs.com/xiaoyezi-wink/p/11200659.html
Copyright © 2011-2022 走看看