duyifei0 2019-06-27
自从开始做公众号开始,就一直在思考,怎么把算法的训练做好,因为思海同学在算法这方面的掌握确实还不够。因此,我现在想做一个“365算法每日学计划”。
“计划”的主要目的: 1、想通过这样的方式监督自己更努力的学习算法。 2、想和小伙伴们“组团”一起来学习交流学习算法过程中的点点滴滴。 “ 计划”的主要内容: 1、数据结构和算法的基础知识巩固。 2、逐步进阶的oj算法训练。 “计划”的时间安排:每周三和周六 文章有点长,希望能耐心的看完。 ——说在前面 “算法每日学”计划01打卡: 问题描述 已知一个正整数N,问从1~N中任选出三个数,他们的最小公倍数最大可以为多少。 输入格式 输入一个正整数N。 输出格式 输出一个整数,表示你找到的最小公倍数。 样例输入 9 样例输出 504 数据规模与约定 1 <= N <= 106。
解题思路与实现
下面这个思路是“算法每日学交流社区”的小伙伴给出的,感谢小伙伴们的支持与关注。
思路分析:
最大 最小公倍数,联想到两个数的求最大最小公倍数,即两个数的乘积(注:连续的两个自然数是互斥的)。
同样,我们可以拿最后三个数来做考虑。
1.当n为奇数时,n,n-1,n-2为奇偶奇,里面只有一个偶数,所以不会有2这个因子。这三个数相差不到3,所以也不会有因子3,故符合题意。
2.当n为偶数时,n,n-1,n-2为偶奇偶,此时n,n-2肯定含有因子2,所以除于2不值得。所以考虑将n-2 换成n-3,变成奇偶奇,此时也有一个问题,
n和n-3,如果n%3==0,则除于3更不值得。仍根据奇偶奇的原则,变动偶数n为n-2,此时换成n-1,n-2,n-3和1情况一样。故此时符合题意。
所以根据上面的分析,我们可以写出下面的代码:
1import java.util.Scanner;
2public class Main{
3 public static void main(String[] args) {
4 Scanner scanner = new Scanner(System.in);
5 long n = scanner.nextLong();
6 long result;
7 if(n<3)
8 result=n;
9 else{
10 if(n%2!=0)
11 result=n(n-1)(n-2);
12 else if(n%3!=0)
13 result=n(n-1)(n-3);
14 else
15 result=(n-1)(n-2)(n-3);
16 }
17 System.out.println(result);
18 }
19}
其实,上面的算法是用到了贪心的思想,大概是这样的思路:
从最大的三个数开始考虑,如果最大的数为奇数,那么相邻的三个数中有两个奇数,最大公约数为1,最小公倍数就为n(n-1)(n-2). 如果为偶数,那么往后移,考虑n(n-1)(n-3),这时n和n-3相差3,式子满足条件的前提是n不能被3整除,否则结果只能是(n-1)(n-2)(n-3).
这个题目因为用到了贪心的思想,所以下面介绍一下贪心算法。
贪心算法
一、基本概念:
所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。
所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。
二、贪心算法的基本思路:
1.建立数学模型来描述问题。 2.把求解的问题分成若干个子问题。 3.对每一子问题求解,得到子问题的局部最优解。 4.把子问题的解局部最优解合成原来解问题的一个解。
三、贪心算法适用的问题
贪心策略适用的前提是:局部最优策略能导致产生全局最优解。
实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。
四、贪心算法的实现框架
1 从问题的某一初始解出发;
2 while (能朝给定总目标前进一步)
3 {
4 利用可行的决策,求出可行解的一个解元素;
5 }
6 由所有解元素组合成问题的一个可行解;
五、贪心策略的选择
因为用贪心算法只能通过解局部最优解的策略来达到全局最优解,因此,一定要注意判断问题是否适合采用贪心算法策略,找到的解是否一定是问题的最优解。
六、例题分析
下面是一个可以试用贪心算法解的题目,贪心解的确不错,可惜不是最优解。
例题① [背包问题] 有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。 要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。 物品 A B C D E F G 重量 35 30 60 50 40 10 25 价值 10 40 30 50 35 40 30
分析: 目标函数: ∑pi最大 约束条件是装入的物品总重量不超过背包容量:∑wi<=M( M=150) (1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优? (2)每次挑选所占重量最小的物品装入是否能得到最优解? (3)每次选取单位重量价值最大的物品,成为解本题的策略。
值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。 贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。
可惜的是,它需要证明后才能真正运用到题目的算法中。
一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。 对于例题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:
(1)贪心策略:选取价值最大者。反例:
W=30 物品:A B C 重量:28 12 12 价值:30 20 20
根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。
(2)贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。 (3)贪心策略:选取单位重量价值最大的物品。反例:
W=30 物品:A B C 重量:28 20 10 价值:28 20 10
根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。
例题②[均分纸牌]有N堆纸牌,编号分别为1,2,…,n。每堆上有若干张,但纸牌总数必为n的倍数.可以在任一堆上取若干张纸牌,然后移动。移牌的规则为:在编号为1上取的纸牌,只能移到编号为2的堆上;在编号为n的堆上取的纸牌,只能移到编号为n-1的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。例如:n=4,4堆纸牌分别为:① 9 ② 8 ③ 17 ④ 6 移动三次可以达到目的:从③取4张牌放到④ 再从③区3张放到②然后从②去1张放到①。
输入输出样例:4
9 8 17 6
屏幕显示:3
算法分析:设a[i]为第I堆纸牌的张数(0<=I<=n),v为均分后每堆纸牌的张数,s为最小移动次数。
我们用贪心算法,按照从左到右的顺序移动纸牌。如第I堆的纸牌数不等于平均值,则移动一次(即s加1),分两种情况移动:
1.若a[i]>v,则将a[i]-v张从第I堆移动到第I+1堆; 2.若a[i]<v,则将v-a[i]张从第I+1堆移动到第I堆。
为了设计的方便,我们把这两种情况统一看作是将a[i]-v从第I堆移动到第I+1堆,移动后有a[i]=v; a[I+1]=a[I+1]+a[i]-v.
在从第I+1堆取出纸牌补充第I堆的过程中可能回出现第I+1堆的纸牌小于零的情况。
如n=3,三堆指派数为1 2 27 ,这时v=10,为了使第一堆为10,要从第二堆移9张到第一堆,而第二堆只有2张可以移,这是不是意味着刚才使用贪心法是错误的呢?
我们继续按规则分析移牌过程,从第二堆移出9张到第一堆后,第一堆有10张,第二堆剩下-7张,在从第三堆移动17张到第二堆,刚好三堆纸牌都是10,最后结果是对的,我们在移动过程中,只是改变了移动的顺序,而移动次数不便,因此此题使用贪心法可行的。
Java源程序
1public class Greedy {
2 public static void main(String[] args) {
3 int n = 0, avg = 0, s = 0;
4 Scanner scanner = new Scanner(System.in);
5 ArrayList<Integer> array = new ArrayList<Integer>();
6 System.out.println("Please input the number of heaps:");
7 n = scanner.nextInt();
8 System.out.println("Please input heap number:");
9 for (int i = 0; i < n; i++) {
10 array.add(scanner.nextInt());
11 }
12 for (int i = 0; i < array.size(); i++) {
13 avg += array.get(i);
14 }
15 avg = avg / array.size();
16 System.out.println(array.size());
17 System.out.println(avg);
18 for (int i = 0; i < array.size() - 1; i++) {
19 s++;
20 array.set(i + 1, array.get(i + 1) + array.get(i) - avg);
21 }
22 System.out.println("s:" + s);
23 }
24}
利用贪心算法解题,需要解决两个问题:
一是问题是否适合用贪心法求解。我们看一个找币的例子,如果一个货币系统有三种币值,面值分别为一角、五分和一分,求最小找币数时,可以用贪心法求解;如果将这三种币值改为一角一分、五分和一分,就不能使用贪心法求解。用贪心法解题很方便,但它的适用范围很小,判断一个问题是否适合用贪心法求解,目前还没有一个通用的方法,在信息学竞赛中,需要凭个人的经验来判断。
二是确定了可以用贪心算法之后,如何选择一个贪心标准,才能保证得到问题的最优解。在选择贪心标准时,我们要对所选的贪心标准进行验证才能使用,不要被表面上看似正确的贪心标准所迷惑,如下面的例子。
例题③[最大整数]设有n个正整数,将它们连接成一排,组成一个最大的多位整数。
例如:n=3时,3个整数13,312,343,连成的最大整数为34331213。
又如:n=4时,4个整数7,13,4,246,连成的最大整数为7424613。
输入:n N个数 输出:连成的多位数
算法分析:此题很容易想到使用贪心法,在考试时有很多同学把整数按从大到小的顺序连接起来,测试题目的例子也都符合,但最后测试的结果却不全对。按这种标准,我们很容易找到反例:12,121应该组成12121而非12112,那么是不是相互包含的时候就从小到大呢?也不一定,如12,123就是12312而非12123,这种情况就有很多种了。是不是此题不能用贪心法呢?
其实此题可以用贪心法来求解,只是刚才的标准不对,正确的标准是:先把整数转换成字符串,然后在比较a+b和b+a,如果a+b>=b+a,就把a排在b的前面,反之则把a排在b的后面。
java源程序
1public static void main(String[] args) {
2 String str = "";
3 ArrayList<String> array = new ArrayList<String>();
4 Scanner in = new Scanner(System.in);
5 System.out.println("Please input the number of data:");
6 int n = in.nextInt();
7 System.out.println("Please input the data:");
8 while (n-- > 0) {
9 array.add(in.next());
10 }
11 for (int i = 0; i < array.size(); i++)
12 for (int j = i + 1; j < array.size(); j++) {
13 if ((array.get(i) + array.get(j)).compareTo(array.get(j) + array.get(i)) < 0) {
14 String temp = array.get(i);
15 array.set(i, array.get(j));
16 array.set(j, temp);
17 }
18 }
19 for (int i = 0; i < array.size(); i++) {
20 str += array.get(i);
21 }
22 System.out.println("str=:" + str);
23 }
24}
贪心算法所作的选择可以依赖于以往所作过的选择,但决不依赖于将来的选择,也不依赖于子问题的解,因此贪心算法与其他算法相比具有一定的速度优势。如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。
这就是贪心的一些思想和基本的应用了,如果还有什么问题,可以留言或者私信我!
参考资料
https://blog.csdn.net/a925907195/article/details/41314549
往期推荐
“面试不败计划”: java语言基础面试题(二) ”365算法每日学计划”:02打卡-线性表(赠书活动第①期预告) 并发基础篇(一): 线程介绍