贪心算法

所谓贪心算法,就是不断地选取当前最优策略的方法设计算法。在问题求解时,总是做出现在看起来是最好的选择。

关于贪心算法,没什么好说的,因为它没有固定的算法框架,本文通过几个问题的解决来介绍下贪心算法以及该算法的应用。

︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿

田忌赛马

田忌赛马的故事大家都听过。讲的是孙膑为田忌调整了作战策略,使得整体实力较弱的田忌的马战胜了实力较强的齐威王的马。策略也很简单,总共就分上中下三等马,用下等马对战齐王的上等马,上等马对战齐王的中等马,中等马对战齐王的下等马,这样明明整体实力较弱,但也能通过总比分2:1赢得比赛。

齐王比赛输了之后,很不服气,他认为区区三匹马的比较胜负只是运气罢了。于是,它便拿出了整个马场的马要再和田忌比一次,并告诉田忌赢一局赢200金,输一局输200金,平局就不用出钱,并且给了田忌三天时间考虑。田忌有了上次孙膑的策略之后,决定自己设计策略。

假设田忌知道自己和齐王的所有的马的跑的快慢的等级,该如何设计策略,使得田忌尽量多赢钱或者尽量少输钱。

我们结合贪心算法这个名称和这个比赛来分析一下问题。所谓贪心算法的宗旨就是一个字-----贪。放到上面例子中,就是尽可能赢多的钱。而当我们拿到一匹马,怎么才能让这匹马为我们尽可能多的赢'钱',或者说创造尽可能多的价值呢。就算不能创造己方的直接价值,也要让对方消耗最多的价值。回到题目就是,如果这匹马有能赢过的对手马,就选择能赢过的马当中最高等级的一匹。如果这匹马遇上谁都是输,那就选择对手马中最强的一匹,死也要拉个垫背的。我们要做的是贪,榨干每匹马的价值,这就是贪心的本质。

为了复杂度,我们可以对双方的马先进行一个排序,然后按顺序比较过去,talk is cheap, 我们来看看伪代码。

​int main()

{

    1.输入你和齐王所有的马匹(共n匹)速度等级,你的存放在数组vl1,    齐王的存放在vl2中,累计的钱存放在sum中

    2.将vl1,vl2降序排序

    3.遍历你的马匹,逐一与齐王的作比较

    for (i = 0; i < n; i++)

    {

        /* 标识是否有能战胜的马 */

        int flag = 0;

        for (j = 0; j < n; j++)

        {

            /* 若齐王的这匹马比过,跳过 */

            if (vl2[j] == -1)  continue;

            /* 若比齐王的快,标记flag */

            if (vl1[i] > vl2[j])

            {

                flag = 1;

                // 因为顺序排列过,所以最先能赢的就是能赢的

                // 当中最强的,比较后将速度置为-1,认为这两匹

                // 是比过的,后面不再使用。

                vl1[i] = -1; vl2[j] = -1;

                break;

            }

        }

        if (flag == 1)

          sum += 200;

        /* 此时说明你剩余最快的马已经不比齐王最慢的马快了,           也就是你之后所有的马要么打平要么输了。 */

        else

        {

            /* 若最快的马已经比齐王最慢的还慢了,直接扣钱,            否则标记马匹,就当无事发生 */

            if (vl1[i] < vl2[n-1-i])

            {

                sum -= 200;

            }

            vl1[i] = -1; vl2[n-1-i] = -1;

        }

    }

    4. 输出结果sum

​    return 0;

}

大致的程序就如上所示,事实上上述代码效率还是很低下的,时间复杂度为O(n^2),可以通过每次修改比较的位置使复杂度达到O(n)。不过这边主要介绍贪心的思想,所以不拓展讲其他解法和优化写法了。

︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿︿

均分纸牌

这也是一道贪心的经典例题,虽然它贪心的不那么明显。

描述:


有n堆纸牌,编号分别为 1,2,…, n。每堆上有若干张,但纸牌总数必为n的倍数。可以在任一堆上取若干张纸牌,然后移动。移牌规则为:在编号为1的堆上取的纸牌,只能移到编号为 2 的堆上;在编号为 n 的堆上取的纸牌,只能移到编号为n-1的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。

例如 n=4,4堆纸牌数分别为: 

① 9 ② 8 ③ 17 ④ 6

移动3次可达到目的:

从 ③ 取4张牌放到④(9 8 13 10)->

从③取3张牌放到 ②(9 11 10 10)->

从②取1张牌放到①(10 10 10 10)。

思路:

1、题目中说了纸牌总数是n的倍数,那么平均值必为整数。

2、因为只能从相邻的牌堆拿牌,而且要求最少次数。所以得一次移动到位,不能来回匀。按照贪心思想(移动尽可能少的次数的情况),就是让当前牌堆一次移动就达到目标牌堆数(即平均数),缺多少就一次直接从边上拿多少。所以一开始,就把目标数定为最终需要达到的平均数就是本题的贪心思想。

3、我们逻辑上从左往右遍历,如果当前牌堆数量就是平均值,继续下一堆。如果不是平均值,我们逻辑上一定是从右边拿牌,拿的牌数为(平均值 - a[i]),即使拿的牌数是负的也没关系,因为正负仅仅表示拿进或者拿出。这样就不用考虑再从左边匀的情况,因为左边的已经之前给了你,你只需要一路往前就行了。

代码如下:

int main()

{

   int n, i, sum, average;

   /* 输入牌堆数和每堆牌的牌数 */

   scanf("%d", &n);

   for (i = 0; i < n; i++)

   {

       scanf("%d", &a[i]);

       sum += a[i];

   }

   /* 求得牌的平均值 */

   average = sum / n;

   int step = 0;

      /* 遍历牌堆 */

   for (i = 0; i < n; i++)

   {

       if (a[i] == average)

           continue;

       /* 只能从相邻牌堆取,所以将需要的牌数传给下一堆 */

       a[i + 1] -= average - a[i];

       step ++;

   }

      printf("%d\n", step);

   return 0;

}

你可能感兴趣的:(贪心算法)