动态规划新手篇

  • 动态规划

    大学期间,接触的第二个大类算法。然而当时止于知识积累,觉得这个算法奇妙又有魅力但是不会写。今细读topcoder的一篇动态规划,从新手到专家的翻译版博客,完成了入门修炼。

    动态规划(Dynamic Programming)它是一种考虑问题的思想(对于初级的我来说),他没有一定的范式来总结。它非常非常有意思,它的应用随处可见,比如地图中搜索a到b的路径。

    它的思想总体来说会分几个要点:1.这个问题是个迭代的问题,即求d(i)的解必须在d(i-1)的基础上来寻找;2.他存在重叠子问题,比如找钱问题,5块钱找的最少硬币数会在4块钱的基础上寻找;3.它存在状态转移方程,诸如d(i)=max{d(i-1)+1,a}此类形式的最佳状态转移;4.它的结果是全局最优解,因为每次子问题的解都是从子子问题的解集中取最优并计算出当前最优解,这与贪心算法有着细小的区别,因为贪心算法是从当前节点往大解集进发时候选择最优而不是从子问题中总结出最优。

    初级解决的是一维的问题,刚开始的时候会有点迷茫,因为分析问题的过程是别人写的,有很多看不懂。在自己尝试分析的时候总结出来了一点经验,循着经验和感觉,分析问题越来越easy。

    talk is cheap,show me the code!

    这句话是在这篇博客上出现的,事实上很多人写算法博客就只贴代码,忘了写详细的分析过程,这样会很有碍于表达能力。所以我既要贴代码也要贴废话。

  •  硬币问题

    这个问题是入门第一个问题,我们假设有1,3,5这三种面值的硬币,给定一个数,最少使用的硬币组合是什么?比如13

    分析:这是一个典型又典型的动态规划问题,首先13块钱可以考虑是12块钱的最优解上加上1,为什么呢?我们从i=1开始分析:1块钱的时候只用一个硬币是显然的;i=2时候,因为面值还是只有1的硬币可用,所以用两个硬币;i=3的时候,就会有新的面额可以用了,所以它的解是:min{d(2)+1,d(3-3)+1}(d(0)=0),显然它的解是1。所以i=4的时候,是d(4) = d(4-1)+1,d(4)=2。

    思维过程:欲求d(13)必须知道d(12)的解,加上1。

    计算过程:i=1,计算到i=13

    code:

package DynamicProgramming;
/**
 * 存在1块 3块 5块面值的纸币
 * @author MacBook
 *
 */
public class LessMoney {
    //mix{d(i-1) + 1} i<3
    //min{d(i-3) + 1,d(i-1) +1} 3=<i<5
    //min{d(i-5) + 1,d(i-1) + 1} i>=5
    public int counting(int money){
        int[] state = new int[money+1];
        state[0] = 0;
        for(int i=1 ; i<=money; i++){
            int before = getBeforeState(i);
            int number1 = state[before]+1;
            int number2 = state[i-1]+1;
            int min = Math.min(number1, number2);
            state[i] = min;
        }
        for(int a : state)
            System.out.print(a+" ");
        System.out.println();
        return state[money];
    }
    //计算上态函数 d(i-m)
    public int getBeforeState(int currentState){
        if(currentState < 3 && currentState >0){
            return currentState -1;
        }else if(currentState >= 3 && currentState < 5){
            return currentState -3;
        }else if(currentState >= 5)
            return currentState -5;
        else {
            return -1;
        }
    }
    public static void main(String args[]){
        LessMoney lm = new LessMoney();
        System.out.println(lm.counting(15));
        
    }
}

 

    这里还有一个问题,就是高额的状态转移方程,为什么是-5而不比较-3呢?有点点想不通。

 

  • 最长非降子段(longest increasing subsequence)

    这是入门遇到的第一个坎,因为这个问题很经典,一不小心就理解错误了。这个问题是求数组中的递增子段的长度,什么意思呢?比如1,3,5,6这样的子段。但是1,3,2,5,6它的最长非降子段却是1,3,5,6。因为它所谓的子段是不连续也无所谓的!造成我写了一个O(n)的计算方法,别人说最快也是O(n*logn)我深表怀疑。

    分析:欲求d(i)的值,必须求出d(i-i)的值(套路)。

       以1,3,2,5,6为例:i=1时候,最小子段为1;i=2,子段1,3;i=3,这就开始注意了,因为A[i]=2,它小与A[i-1],所以出现状态转移方程d(i)=max{d(j)+1,1},这里仍然是1,3子段;i=4时候,1,3,5;i=5时,1,3,5,6。

    这里我采用逆序查找最长子段,当i时候,从i->0查找以A[i]为最大元素的子段。

    它的常规算法是O(n*n)复杂度,改进算法是O(n*logn),改进算法的思路是:维护一个单调序列(栈),当元素大于栈顶元素时候入栈,当元素小于栈顶元素时候,二分查找比它大的第一个元素,替换之。

    code:

package DynamicProgramming;


/**
 * longest increasing subsequence
 * A[1],A[2],...,A[n] 求出最长非降自段和
 * @author MacBook
 *
 */
public class LIS {
    /*
     * 状态转移方程:d(i) = max{1,d(i-1)+1},其中,A[i-1]<=A[i]
     */
    public static void main(String args[]){
        LIS l = new LIS();
        int[] subsequence={5,3,4,8,6,7,2,9,10,21,15};
        System.out.println(l.counting3(subsequence));
        
    }
    //连续非降子序列
    public void counting(int[] subsequence){
        int len = 0;
        int [] state = new int[subsequence.length];//状态集合
        for(int i=0;i<subsequence.length ;i++){
            if(i == 0){
                len++;
                state[i] = len;
            }else{
                if(subsequence[i]>subsequence[i-1])
                {
                    len++;
                }else{
                    len = 1;
                }
                int statecode = Math.max(state[i-1], len);
                state[i] = statecode;
            }
        }
        for(int a:state){
            System.out.print(a+" ");
        }
        System.out.println();
    }
    //若计算A[j]的最大子序列 则计算A[j-1]所有元素作为最大元素的字段和 加上当前元素并记录--算法复杂度O(n*n)
    public void counting1(int[] subsequence){
        int lenTemp = 1;
        int[] liss = new int[subsequence.length];
        liss[0] = 1;
        for(int i=0;i<subsequence.length;i++){
            for(int j = i;j>=0;j--){
                if(subsequence[j]<subsequence[i])
                {
                    if(lenTemp < liss[j]+1)
                    {
                        lenTemp = liss[j]+1;
                    }
                }
            }
            liss[i] = lenTemp;
            lenTemp = 1;
        }
        for(int a:liss)
            System.out.print(a+" ");
        System.out.println();
    }
    
    //改进算法 使用一个栈来维护一个单调序列,读到sub的元素时候:
    //1.如果比栈顶元素大,则放在栈顶;2.如果比栈顶元素小,则二分查找替换单挑序列中比它大的第一个元素
    public int counting3(int[] subsequence){
        int [] array = new int[subsequence.length];
        //初始化栈
        array[0] = subsequence[0];
        int index = 0;
        for(int i=0;i<subsequence.length;i++){
            if(subsequence[i]>array[index]){
                array[++index] = subsequence[i];
            }
            else if(subsequence[i]<array[index]){
                int location = bisection(array,subsequence[i],0,index);
                //如果相应位置的值等于查询值 则替换它下一个元素
                array[location] = subsequence[i];
            }
        }
        return index+1;
    }
    //二分查找查询比元素大的位置
    //寻找比sign大的第一个元素,如果存在与sign相等的元素,再行判定
    public int bisection(int[] array,int sign,int start,int end){
        int mid = 0;
        while(start<end)
        {
            
            mid = (start + end)/2;
            if(sign>array[mid]){
                start = mid;
            }else{
                end = mid;
            }
            if(start+1 == end)
            {
                mid = end;
                break;
            }
        }
        return mid;
        
    }
}
  • ZigZag

    这个东西让我查了好久(因为我没直接点进博客的链接,不好的习惯),起初我以为是z字形算法,后来是topcoder的竞赛题,乍一看,挺简单,因为和LIS的机制是一样的。它的问题是这样的:zigzag数组,是一个正负增长率交替的数组,1,5,2,3这样的。更坑爹的是2,8,3,4,5,7这样的也存在zigzag数组。它是2,8,3,5。像LIS一样,使用动态规划计算它的长度。

    分析:仍然套路,欲求d(i)必先求d(i-1),不同的是需要记录i-1元素的最大长度的当前增长率。比如上例,计算到3这个元素的时候,它之前是8,所以它的当前增长率是positive(正的)。

    code:

package DynamicProgramming;

/**
 *    2003 TCCC Semifinals 3
 * 题目:给定的一个序列,他的增长是按照正负正负交替的,他叫做zigzag序列,
 * 程序实现输入一个序列计算他的最长zigzag序列,包括它的子序列,最后返回最长zigzag序列的长度。 类比:lis
 * 
 * @author MacBook
 * Problem Statement
 *         A sequence of numbers is called a zig-zag sequence if the differences
 *         between successive numbers strictly alternate between positive and
 *         negative. The first difference (if one exists) may be either positive
 *         or negative. A sequence with fewer than two elements is trivially a
 *         zig-zag sequence. For example, 1,7,4,9,2,5 is a zig-zag sequence
 *         because the differences (6,-3,5,-7,3) are alternately positive and
 *         negative. In contrast, 1,4,7,2,5 and 1,7,4,5,5 are not zig-zag
 *         sequences, the first because its first two differences are positive
 *         and the second because its last difference is zero. Given a sequence
 *         of integers, sequence, return the length of the longest subsequence
 *         of sequence that is a zig-zag sequence. A subsequence is obtained by
 *         deleting some number of elements (possibly zero) from the original
 *         sequence, leaving the remaining elements in their original order.
 */
public class Zigzag {
    public static void main(String args[]){
        Zigzag z = new Zigzag();
        int [] subsequence ={70, 55, 13, 2, 99, 2, 80, 80, 80, 80, 100, 19, 7, 5, 5, 5, 1000, 32, 32};
        z.counting1(subsequence);
    }
    //欲计算d[i]的值,需要计算d[0]-d[i-1]中最大值(与a[i]组合形成zigzag序列的)+1
    //d(i) = max{d(j)+1},j∈(0,i-1) && 形成zigzag序列
    public void counting1(int[] subsequence) {
        int [] pre = new int[subsequence.length];//记录本元素上一个zigzag序列元素
        int [] liss = new int [subsequence.length];//计算当前元素最长zigzag序列
        boolean [] ispositive = new boolean[subsequence.length];//计算当前元素的下一个增长率
        pre[0] = 0;
        liss[0] = 1;
        //经过测试 --固定起手正负会影响最终结果的正确性,所以初始化的时候需要在i=1时候计算正负
        if(subsequence[1]-subsequence[0] < 0)    
            ispositive[0] = false;
        else if(subsequence[1] > 0)
            ispositive[0] = true;
        for(int i=1;i<subsequence.length;i++){
            for(int j=0;j<i;j++){
                //如果出现zigzag的特征:
                //1.判断当前序列+1是否大于已存序列,大于则存
                //2.将增长特征取反
                //3.前驱标记j存入数组
                if(subsequence[i]-subsequence[j]>0 && ispositive[j] == true){
                    if(liss[j]+1>liss[i]){
                        liss[i] = liss[j]+1;
                        ispositive[i] = false;
                        pre[i] = j;
                    }
                }else if(subsequence[i]-subsequence[j]<0 && ispositive[j] == false){
                    if(liss[i]+1>liss[i]){
                        liss[i] = liss[j]+1;
                        ispositive[i] = true;
                        pre[i] = j;
                    }
                }
            }
        }
        for(int a:pre)
            System.out.print(a+" ");
        System.out.println();
        for(int a:liss)
            System.out.print(a+" ");
        System.out.println();
        for(boolean a:ispositive)
            System.out.print(a+" ");
        System.out.println();
    }
}
  • BadNeighbors

    问题描述:当前节点捐钱,邻居节点则不会捐钱,计算数组中能够募集的最大捐款数。注意,首位和尾位是一对邻居。

    分析:LIS的变式,难点是首位和尾位的计算,因为计算d(i)的时候让然会加入d(0),所以根据事理分析,它的最优且合理的解,存在于i-1位。

    code:

package DynamicProgramming;

/**
 * tips:这个题目的大意是输入一个序列,按照跳位的方式求最大加起来的数值(因为邻居捐款之后它的临位不想捐款)
 *         首位和末尾是邻居
 * @author MacBook 
 * Problem Statement
 * 
 *         The old song declares "Go ahead and hate your neighbor", and the
 *         residents of Onetinville have taken those words to heart. Every
 *         resident hates his next-door neighbors on both sides. Nobody is
 *         willing to live farther away from the town's well than his neighbors,
 *         so the town has been arranged in a big circle around the well.
 *         Unfortunately, the town's well is in disrepair and needs to be
 *         restored. You have been hired to collect donations for the Save Our
 *         Well fund.
 * 
 *         Each of the town's residents is willing to donate a certain amount,
 *         as specified in the int[] donations, which is listed in clockwise
 *         order around the well. However, nobody is willing to contribute to a
 *         fund to which his neighbor has also contributed. Next-door neighbors
 *         are always listed consecutively in donations, except that the first
 *         and last entries in donations are also for next-door neighbors. You
 *         must calculate and return the maximum amount of donations that can be
 *         collected.
 */
public class BadNeighbors {
    public static void main(String args[]){
        BadNeighbors b = new BadNeighbors();
        int [] subsequence1 ={ 10, 3, 2, 5, 7, 8 };
        b.counting1(subsequence1);
        int [] subsequence2 ={11,15};
        b.counting1(subsequence2);
        int []subsequence3 ={ 7, 7, 7, 7, 7, 7, 7 };
        b.counting1(subsequence3);
        int []subsequence4 ={ 1, 2, 3, 4, 5, 1, 2, 3, 4, 5 };
        b.counting1(subsequence4);
        int []subsequence5 ={ 94, 40, 49, 65, 21, 21, 106, 80, 92, 81, 679, 4, 61,  
                  6, 237, 12, 72, 74, 29, 95, 265, 35, 47, 1, 61, 397,
                  52, 72, 37, 51, 1, 81, 45, 435, 7, 36, 57, 86, 81, 72};
        b.counting1(subsequence5);
    }
    //状态转移方程:d(i) = d(j) + a[i],并且j!=i-1
    public void counting1(int [] subsequence){
        int[] liss = new int[subsequence.length];
        int[] pre = new int[subsequence.length];
        int max = 0;
        pre[0] = 0;
        liss[0] = subsequence[0];
        //长度为2的有点特殊
        if (subsequence.length > 2) {
            for (int i = 1; i < subsequence.length; i++) {
                //不计算邻居捐款
                for (int j = 0; j < i - 1; j++) {
                        if (liss[j] + subsequence[i] > liss[i])
                        {
                            liss[i] = liss[j] + subsequence[i];
                            pre[i] = j;
                        }
                            
                }
                
            }
            // 因为最后一个和第一个是邻居
            max = liss[subsequence.length-2];
        }else if(subsequence.length == 2){
            max = Math.max(subsequence[0], subsequence[1]);
        }
        for(int a:pre)
            System.out.print(a+" ");
        System.out.println();
        System.out.println("max="+max);
    }
}
  • FlowerGarden

    有点看不懂题目刚开始,因为不像dp,不过后来还是解了,一句话总结,区间交集取小者前,全局大者前。

    只是一个排序,开花时间和枯萎时间有交集的就把高度小的放前面,全局数组是尽可能高的前。

    code:

package DynamicProgramming;

/**
 * 
 * @author MacBook
 *  Problem Statement
 * 
 *         You are planting a flower garden with bulbs to give you joyous
 *         flowers throughout the year. However, you wish to plant the flowers
 *         such that they do not block other flowers while they are visible.
 * 
 *         You will be given a int[] height, a int[] bloom, and a int[] wilt.
 *         Each type of flower is represented by the element at the same index
 *         of height, bloom, and wilt. height represents how high each type of
 *         flower grows, bloom represents the morning that each type of flower
 *         springs from the ground, and wilt represents the evening that each
 *         type of flower shrivels up and dies. Each element in bloom and wilt
 *         will be a number between 1 and 365 inclusive, and wilt[i] will always
 *         be greater than bloom[i]. You must plant all of the flowers of the
 *         same type in a single row for appearance, and you also want to have
 *         the tallest flowers as far forward as possible. However, if a flower
 *         type is taller than another type, and both types can be out of the
 *         ground at the same time, the shorter flower must be planted in front
 *         of the taller flower to prevent blocking. A flower blooms in the
 *         morning, and wilts in the evening, so even if one flower is blooming
 *         on the same day another flower is wilting, one can block the other.
 * 
 *         You should return a int[] which contains the elements of height in
 *         the order you should plant your flowers to acheive the above goals.
 *         The front of the garden is represented by the first element in your
 *         return value, and is where you view the garden from. The elements of
 *         height will all be unique, so there will always be a well-defined
 *         ordering.
 *         
 *         tips:输入 : 1.开花高度 2.开花开始时间 3.开花结束时间
 *                 规则 : 保证最大可能高度在第一位;如果开花周期交叉,则将高度矮的放在高度高的之前;如果周期不交叉,则序列不变。
 */
public class FlowerGarden {
    public static void main(String[] args) {
        FlowerGarden f = new FlowerGarden();
        //测试用例1
        int[] height1 = {5,4,3,2,1};
        int[] bloom1 = {1,5,10,15,20};
        int[] wilt1 = {5,10,14,20,25};
        f.counting(height1,bloom1,wilt1,height1.length);
        for(int a:height1)
            System.out.print(a+" ");
        System.out.println();
        //测试用例2
        int[] height2 = {5,4,3,2,1};
        int[] bloom2 = {1,5,10,15,20};
        int[] wilt2 = {5,10,15,20,25};
        f.counting(height2,bloom2,wilt2,height2.length);
        for(int a:height1)
            System.out.print(a+" ");
        //测试用例3
        int[] height3 = {5,4,3,2,1};
        int[] bloom3 = {1,5,10,15,20};
        int[] wilt3 = {4,9,14,19,24};
        f.counting(height3,bloom3,wilt3,height3.length);
        for(int a:height1)
            System.out.print(a+" ");
        //测试用例4
        int[] height4 = {1,2,3,4,5,6};
        int[] bloom4 = {1,3,1,3,1,3};
        int[] wilt4 = {2,4,2,4,2,4};
        f.counting(height4,bloom4,wilt4,height4.length);
        for(int a:height1)
            System.out.print(a+" ");
    }
    //不校验输入-->bloom(相同index)要比wilt的小
    public void counting(int[] height,int[] bloom,int[] wilt,int length){
        for(int i=0;i<length;i++){
            for(int j=0;j<i;j++){
                //如果存在交集
                if(bloom[i] <= wilt[j]){
                    if(height[j]>height[i]){
                        int temp = height[i];
                        height[i] = height[j];
                        height[j] = temp;
                        temp = bloom[i];
                        bloom[i] = bloom[j];
                        bloom[j] = temp;
                        temp = wilt[i];
                        wilt[i] = wilt[j];
                        wilt[j] = temp;
                    }
                }else{
                    if(height[j]<height[i]){
                        int temp = height[i];
                        height[i] = height[j];
                        height[j] = temp;
                        temp = bloom[i];
                        bloom[i] = bloom[j];
                        bloom[j] = temp;
                        temp = wilt[i];
                        wilt[i] = wilt[j];
                        wilt[j] = temp;
                    }
                }
            }
        }
    }
}
  • 总结

  
    任何难题,只要惧怕,你就解不出来。任何时候不要对自己说,难。

    

转载于:https://www.cnblogs.com/chentingk/p/5916893.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值