贪心算法常见题目
把数组排成最小的数
剑指 Offer 45. 把数组排成最小的数 :输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
贪心策略:若拼接字符串 x + y > y + x,则 x “大于” y ;反之,若 x + y < y + x ,则 x “小于” y。
x “小于” y 代表:排序完成后,数组中 x 应在 y 左边;“大于” 则反之。
根据以上规则,套用任何排序方法对 nums 执行排序即可。
代码:
1 2 3 4 5 6 7 8 9 10 11 public String minNumber (int [] nums) { String[] strs = new String[nums.length]; for (int i = 0 ; i < nums.length; i++) strs[i] = String.valueOf(nums[i]); Arrays.sort(strs, (x, y) -> (x + y).compareTo(y + x)); StringBuilder res = new StringBuilder(); for (String s : strs) res.append(s); return res.toString(); }
会议室问题
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体的项目),你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。 返回这个最多的宣讲场次。
贪心策略:按照结束时间 从小到大排序,优先选择结束时间更小的会议。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public int intervalIntersection (int [][] programs) { Arrays.sort(programs, (o1, o2) -> o1[1 ] - o2[1 ]); int count = 0 ; int curTime = 0 ; for (int i = 0 ; i < programs.length; i++) { if (curTime <= programs[i][0 ]) { count++; curTime = programs[i][1 ]; } } return count; }
小虎买苹果
方法一:普通贪心策略——先尽可能多的买 8个的袋子
如果要买的苹果数量是奇数,则没有答案,直接返回 -1。
如果要买的苹果是偶数,则开始贪心:
先尽可能多的买 8 个的袋子,判断此时剩余的苹果能不能被 6 整除,如果可以找到了最佳方案。
如果不行,减少一个 8个的袋子,再看剩余的苹果能不能被 6 整除。
重复该过程直到 8个的袋子为 0,如果剩余的苹果还不能被 6 整除,则说明没有满足条件的装法。
这种贪心策略会优先考虑买 8个的袋子,从而能保证得到的结果是最优的。
方法一的优化版本
可以对方法一进行优化:当剩余的苹果数量大于 24 时不需要再往下尝试了,后续不可能在存在可行方案了。这里的 24 是 6 和 8 的最小公倍数。如果该数字能被 6 整除,则其肯定能被 8 整除。但是在上面的尝试中已经表明了该数字不能被 8 整除,否则也不会遍历到当前位置。因此这个数字肯定也不能被 6 整除。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class AppleMinBags { public static int minBags (int n) { if (n <= 5 ) { return -1 ; } if (n % 2 == 1 ) { return -1 ; } int bag8 = n / 8 ; int bag6 = -1 ; int rest = n - bag8 * 8 ; while (bag8 >= 0 && rest < 24 ) { bag6 = minBagBase6(rest); if (bag6 != -1 ) { break ; } bag8--; rest = n - bag8 * 8 ; } return bag6 != -1 ? bag6 + bag8 : -1 ; } public static int minBagBase6 (int n) { return (n % 6 == 0 ) ? n / 6 : -1 ; } public static int minBagAwesome (int apple) { if ((apple & 1 ) != 0 ) { return -1 ; } if (apple < 18 ) { return apple == 0 ? 0 : (apple == 6 || apple == 8 ) ? 1 : (apple == 12 || apple == 14 || apple == 16 ) ? 2 : -1 ; } return (apple - 18 ) / 8 + 3 ; } public static void main (String[] args) { int max = Integer.MAX_VALUE; int testTime = 100000000 ; for (int test = 0 ; test < testTime; test++) { int apple = (int ) (Math.random() * max); if (minBags(apple) != minBagAwesome(apple)) { System.out.println("error" ); } } } }
避免洪水泛滥
1488. 避免洪水泛滥 :你的国家有无数个湖泊,所有湖泊一开始都是空的。当第 n 个湖泊下雨的时候,如果第 n 个湖泊是空的,那么它就会装满水,否则这个湖泊会发生洪水。你的目标是避免任意一个湖泊发生洪水。
给你一个整数数组 rains ,其中:
rains[i] > 0
表示第 i 天时,第 rains[i] 个湖泊会下雨。
rains[i] == 0
表示第 i 天没有湖泊会下雨,你可以选择 一个湖泊并抽干这个湖泊的水。
请返回一个数组 ans ,满足:
ans.length == rains.length
如果 rains[i] > 0
,那么 ans[i] == -1
。
如果 rains[i] == 0
,ans[i] 是你第 i 天选择抽干的湖泊。
如果有多种可行解,请返回它们中的 任意一个 。如果没办法阻止洪水,请返回一个 空的数组 。
https://leetcode-cn.com/problems/avoid-flood-in-the-city/solution/avoid-flood-in-the-city-by-ikaruga/、https://leetcode-cn.com/problems/avoid-flood-in-the-city/solution/java-hashmap-hashset-treeset-mian-xiang-vxeda/
思路:
将晴天的日期全部记录到 TreeSet 中
使用 HashMap 来记录每个湖泊上一次下雨的日期
遇到晴天时先不用管抽哪个湖
当下雨时,湖泊已经水满了时,我们可以查询到上次下雨的日期
通过这个日期在晴天记录中查找对应的晴天日期。在有序数据中使用二分查找
如果找到了,就可以使用那一天抽水,找不到就不可避免的洪水了。找到了更新下雨表中当前湖泊的日期为当前日期。
其中:
核心思想 : 遇到可以抽水的天时,把它存起来,遇到满水湖泊需要抽水时,观察上次该湖泊满水时与当前日期之间是否有可抽水天气 , 取最靠近上次满水的那一个晴天 。
HashMap 作用 :记录某湖泊最近一次 蓄满水的日期(同一时刻某个湖泊只会存一个日期,当遇到同一湖泊的另一个下雨日期时,就要进行处理,处理完毕后更新该湖泊的下雨日期为当前日期)。
TreeSet 作用 :存晴天日期,用于二分查找出对某需要抽水湖泊的最佳抽水日期(距离该湖泊上一次下雨的最近日期)。
空闲的晴天 :设为1。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public int [] avoidFlood(int [] rains) { if (rains == null || rains.length < 1 ) { return rains; } TreeSet<Integer> sunnyDaySet = new TreeSet<>(); HashMap<Integer, Integer> rainyDayMap = new HashMap<>(); int [] res = new int [rains.length]; Arrays.fill(res, -1 ); for (int i = 0 ; i < rains.length; i++) { if (rains[i] == 0 ) { sunnyDaySet.add(i); res[i] = 1 ; continue ; } if (!rainyDayMap.containsKey(rains[i])) { rainyDayMap.put(rains[i], i); continue ; } Integer recentDay = sunnyDaySet.higher(rainyDayMap.get(rains[i])); if (recentDay == null ) { return new int [0 ]; } res[recentDay] = rains[i]; rainyDayMap.put(rains[i] , i); sunnyDaySet.remove(recentDay); } return res; }
课程表 III
630. 课程表 III :这里有 n 门不同的在线课程,按从 1 到 n 编号。给你一个数组 courses
,其中 courses[i] = [durationi, lastDayi]
表示第 i 门课将会持续上 durationi
天课,并且必须在不晚于 lastDayi
的时候完成。你的学期从第 1 天开始。且不能同时修读两门及两门以上的课程。返回你最多可以修读的课程数目。
贪心策略:对于两门课,如果后者的关闭时间较晚 ,那么我们先学习前者,再学习后者,总是最优的 。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public int scheduleCourse (int [][] courses) { Arrays.sort(courses, (c1, c2) -> c1[1 ] - c2[1 ]); PriorityQueue<int []> heap = new PriorityQueue<>((c1, c2) -> c2[0 ] - c1[0 ]); int day = 0 ; for (int [] c : courses) { if (day + c[0 ] <= c[1 ]) { day += c[0 ]; heap.offer(c); } else if (!heap.isEmpty() && heap.peek()[0 ] > c[0 ]) { day -= heap.poll()[0 ] - c[0 ]; heap.offer(c); } } return heap.size(); }
超级洗衣机
517. 超级洗衣机 :假设有 n 台超级洗衣机放在同一排上。开始的时候,每台洗衣机内可能有一定量的衣服,也可能是空的。在每一步操作中,你可以选择任意 m (1 <= m <= n) 台洗衣机,与此同时将每台洗衣机的一件衣服送到相邻的一台洗衣机。
给定一个整数数组 machines
代表从左至右每台洗衣机中的衣物数量,请给出能让所有洗衣机中剩下的衣物的数量相等的最少的操作步数 。如果不能使每台洗衣机中衣物的数量相等,则返回 -1 。
思路:遍历每一个位置,判断当前位置 i 的左侧一共需要多少件衣服(需要拥有的 - 实际拥有的):
该值如果大于 0,说明实际拥有的比较少,需要当前位置以及右侧给其输入 衣服
该值如果小于 0,说明实际拥有的比较多,需要给当前位置以及右侧输出 衣服
那么当前位置需要进行的轮数为:
如果 i 位置左右皆为负 ,代表自己需要给左右提供衣服,那么轮次至少为 |左侧| + |右侧|
如果 i 位置左右一正一负 ,或者都为正 ,则轮次至少为 max(|左侧| + |右侧|)
遍历每个位置,计算出最大的值就是系统瓶颈,也就是答案了。
具体的贪心策略:计算出当前位置左侧区域与右侧区域的“衣服总数 - 最终平均分配后剩余的衣服总数”的结果 sub
:
如果 sub
为 0,代表这个区域不需要再添加或移走衣服了,已经符合答案要求了
如果当前位置左侧右侧区域的 sub
都为负数,则说明当前位置的衣服太多,两个不够,当前位置的多余的衣服应该分给左右侧区域。执行的操作步数至少为:左右侧的 sub
的绝对值的和 ,因为每次只能从当前洗衣机移动一件给左或右侧区域
如果当前位置左右侧的 sub
一正一负,则说明一侧多,一侧少,那么执行的步骤至少为:左右侧的 sub
的绝对值的最大值 :最大值将其一部分给右侧区域,一部分给当前位置如果当前位置左右侧的sub,瓶颈就在衣服更多的那一侧
如果当前位置左右侧的 sub
都为正,则说明左右侧都多,都应该给当前位置,那么执行的步骤至少为:左右侧的 sub
的绝对值的最大值:左右侧同时给当前位置衣服,那么瓶颈就在衣服更多的那一侧,要等他都给完(少的那一侧是和多的那一侧同时给的,所以已经给完了)
将左侧和右侧区域是为一个整体,加入最左侧缺一个,最右侧多一个,那么只需要一步就可以把最右侧的移动给最左侧(每一台都把一件给左边的,该过程是同步的,是被视为一步的)
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Solution { public int findMinMoves (int [] machines) { if (machines == null || machines.length == 0 ) { return -1 ; } int N = machines.length; int sum = 0 ; for (int i = 0 ; i < N; i++) { sum += machines[i]; } if ((sum % N) != 0 ) { return -1 ; } int leftSub = 0 ; int rightSub = 0 ; int leftSum = 0 ; int avg = sum / N; int ans = 0 ; for (int i = 0 ; i < N; i++) { leftSub = leftSum - avg * i; rightSub = (sum - leftSum - machines[i]) - avg * (N - i - 1 ); if (leftSub < 0 && rightSub < 0 ) { ans = Math.max(ans, Math.abs(leftSub) + Math.abs(rightSub)); } else { ans = Math.max(ans, Math.max(Math.abs(leftSub), Math.abs(rightSub))); } leftSum += machines[i]; } return ans; } }
吃苹果最大数目
有一棵特殊的苹果树,一连 n
天,每天都可以长出若干个苹果。在第 i
天,树上会长出 apples[i]
个苹果,这些苹果将会在 days[i]
天后(也就是说,第 i + days[i]
天时)腐烂,变得无法食用。也可能有那么几天,树上不会长出新的苹果,此时用 apples[i] == 0
且 days[i] == 0
表示。
你打算每天 最多 吃一个苹果来保证营养均衡。注意,你可以在这 n
天之后继续吃苹果。给你两个长度为 n
的整数数组 days
和 apples
,返回你可以吃掉的苹果的最大数目。
贪心策略:到了每天,先吃最先过期的苹果 ,用一个优先级队列 维护二元数组(过期的那一天,苹果数量),代表在这一天将有某些数量的苹果过期
阶段一:每到一天,先将堆中的过期的苹果弹出;然后吃掉一个苹果(更新堆顶二元数组的苹果数量,如果吃成0了,也弹出)
阶段二:当过了n 天后,树不会再长苹果了,只能吃。此时每次计算堆苹果数组,取Math.min(过期天数 - 当前天数i, 苹果数量)
,重复该过程直到堆空
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public int eatenApples (int [] apples, int [] days) { if (apples == null || days == null || apples.length != days.length) { return 0 ; } int n = apples.length; PriorityQueue<int []> heap = new PriorityQueue<>((o1, o2) -> o1[0 ] - o2[0 ]); int [] cur = null ; int res = 0 ; int i = 0 ; for (; i < n; i++) { if (apples[i] != 0 ) { heap.add(new int []{i + days[i], apples[i]}); } while (!heap.isEmpty()) { cur = heap.peek(); if (cur[0 ] <= i) { heap.poll(); } else { break ; } } if (!heap.isEmpty()) { cur = heap.peek(); if (--cur[1 ] == 0 ) { heap.poll(); } res++; } } while (!heap.isEmpty()) { if (heap.peek()[0 ] <= i) { heap.poll(); continue ; } cur = heap.poll(); int day = Math.min(cur[0 ] - i, cur[1 ]); res += day; i += day; } return res; }
跳跃游戏 I
55. 跳跃游戏 :给定一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
贪心策略:依次遍历数组中的每一个位置,并实时维护 最远可以到达的位置 。对于当前遍历到的位置 x,如果它在 最远可以到达的位置 的范围内,那么我们就可以从起点通过若干次跳跃到达该位置,因此我们可以用 x + nums[x] 更新 最远可以到达的位置 。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public boolean canJump (int [] nums) { if (nums == null || nums.length == 0 ) { return false ; } int mostRight = 0 ; for (int i = 0 ; i < nums.length; i++) { if (i <= mostRight) { mostRight = Math.max(mostRight, nums[i] + i); if (mostRight >= nums.length - 1 ) { return true ; } continue ; } return false ; } return true ; }
跳跃游戏 II
45. 跳跃游戏 II :给你一个非负整数数组 nums
,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。假设你总是可以到达数组的最后一个位置。
贪心策略:遍历过程中,在当前区间内 [preEnd, end] 的某个位置起跳,记录其起跳后的最远距离 mostRight。在 i 遍历到当前区间的 end 位置时才开始真正的“逻辑”起跳,因为只有遍历完才知道该在哪里起跳。逻辑起跳后,step++,同时更新下一个区间的 end 为 mostRight。
注意逻辑起跳 的概念(提前预约起跳,但还没真正起跳),只是记录了下次起跳后能到达的最远位置,并未真正起跳
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public int jump (int [] nums) { if (nums == null || nums.length == 0 ) { return 0 ; } int step = 0 ; int mostRight = 0 ; int end = 0 ; for (int i = 0 ; i < nums.length - 1 ; i++) { mostRight = Math.max(mostRight, nums[i] + i); if (i == end) { step++; end = mostRight; } } return step; }
Magic 操作
给一个包含n个整数元素的集合a,一个包含m个整数元素的集合b。定义magic操作为,从一个集合中取出一个元素,放到另一个集合里,且操作过后每个集合的平均值都大大于于操作前。注意以下两点:
不可以把一个集合的元素取空,这样就没有平均值了
值为x的元素从集合b取出放入集合a,但集合a中已经有值为x的元素,则a的平均值不变(因为集合元素不会重复),b的平均值可能会改变(因为x被取出了)
问最多可以进行多少次magic操作?
规律 :
平均值相等时,无论如何都无法做 Magic 操作。因为不论拿出大于平均值、等于平均值还是小于平均值的数字,都会使得两个集合中至少有一个不符合要求
平均值不相等时:
永远不可能从小的集合里拿出放到大的集合里 。因为小的集合里拿出大的会让自己均值变小,拿出小的或相等的,会让对方的均值变小
只能从大的集合里拿出放到小的集合里(并且该数字在小集合里不存在)。而且需要拿出大于小的集合均值,且小于大的集合均值的数字
因此,流程为:
先区分两个集合的均值大小。选出大的集合与小的集合的均值间的范围,例如 (50, 100),只有这些范围的数字可以从大的集合中拿出放到小的集合里。
每次拿出时,贪心策略为:先拿出该区间内的最小的数字 (前提是不能在小集合中存在)。
思想:从大集合中先拿出最小的一个值,意味着该数字对大集合是最拖后腿的值,将其去掉后,能最大程度的提高大集合的均值。同时最小的值也意味着,该值是对小集合均值最小幅度的提升。这样就能使得进行 Magic 操作的次数尽量多。
代码实现时,需要在每次移动前,判断该值是否小于当前大集合的平均值 (平均值会随着数组的变化而变化,需要在每次移动数字前重新计算)并且大于小集合的平均值。同时还需要判断该值是否已经在小集合中存在(存在的数字不能移动)。并且需要使用 double 类型计算均值 ,防止整数类型的均值丢失精度从而造成区间计算错误
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public static int maxOps (int [] arr1, int [] arr2) { double sum1 = 0 ; for (int i = 0 ; i < arr1.length; i++) { sum1 += (double ) arr1[i]; } double sum2 = 0 ; for (int i = 0 ; i < arr2.length; i++) { sum2 += (double ) arr2[i]; } if (avg(sum1, arr1.length) == avg(sum2, arr2.length)) { return 0 ; } int [] arrMore = null ; int [] arrLess = null ; double sumMore = 0 ; double sumLess = 0 ; if (avg(sum1, arr1.length) > avg(sum2, arr2.length)) { arrMore = arr1; sumMore = sum1; arrLess = arr2; sumLess = sum2; } else { arrMore = arr2; sumMore = sum2; arrLess = arr1; sumLess = sum1; } Arrays.sort(arrMore); HashSet<Integer> setLess = new HashSet<>(); for (int num : arrLess) { setLess.add(num); } int moreSize = arrMore.length; int lessSize = arrLess.length; int ops = 0 ; for (int i = 0 ; i < arrMore.length; i++) { double cur = (double ) arrMore[i]; if (cur < avg(sumMore, moreSize) && cur > avg(sumLess, lessSize) && !setLess.contains(arrMore[i])) { sumMore -= cur; moreSize--; sumLess += cur; lessSize++; setLess.add(arrMore[i]); ops++; } } return ops; }
组成三角形
在迷迷糊糊的大草原上,小红捡到了n根木棍,第i根木棍的长度为i,小红现在很开心。想选出其中的三根木棍组成美丽的三角形。但是小明想捉弄小红,想去掉一些木棍,使得小红任意选三根木棍都不能组成三角形。
请问小明最少去掉多少根木棍呢?给定N,返回至少去掉多少根?
思路:该题的贪心策略是:在N个木棍中,保留符合斐波那契数列的木棍,其余都去掉,就能保证是去掉最少的木棍。
例如,给定N=17,则就从 [1, 17] 中选出斐波那契数列:1, 2, 3, 5, 8, 13。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static int minDelete (int m) { if (m < 4 ) { return 0 ; } int k_2 = 2 ; int k_1 = 3 ; int num = 3 ; while (k_2 + k_1 <= m) { num++; k_1 += k_2; k_2 = k_1 - k_2; } return m - num; }
预处理技巧
染色问题
牛牛有一些排成一行的正方形。每个正方形已经被染成红色或者绿色。牛牛现在可以选择任意一个正方形然后用这两种颜色的任意一种进行染色,这个正方形的颜色将会被覆盖。牛牛的目标是在完成染色之后,每个红色R都比每个绿色G距离最左侧近。牛牛想知道他最少需要涂染几个正方形。如样例所示:s = RGRGR
我们涂染之后变成 RRRGG 满足要求了,涂染的个数为2,没有比这个更好的涂染方案。
思路:遍历每一个位置,以当前位置为分界线,
统计左侧有多少个G,将其染成R
统计右侧有多少个R,将其染成G
二者的和就是当前位置作为分界线时的涂染个数。
但如果每到一个位置都需要遍历左右侧的区间统计R和G的个数,则时间复杂度为 O(N^2)。我们可以使用预处理 技巧:先遍历两遍数组,分别统计出每个位置左侧有多少个G,以及每个位置右侧有多少个R,然后再遍历一遍数组,就可以使用预处理的数组快速计算出当前位置的染色数量,从而加速算法。进一步优化 :预处理时只遍历right数组,然后在遍历left数组的过程中就可以计算答案。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public class ColorLeftRight { public static int minPaint (String s) { if (s == null || s.length() < 1 ) { return 0 ; } char [] str = s.toCharArray(); int N = str.length; int [] right = new int [N]; right[N - 1 ] = str[N - 1 ] == 'R' ? 1 : 0 ; for (int i = N - 2 ; i >= 0 ; i--) { right[i] = right[i + 1 ] + (str[i] == 'R' ? 1 : 0 ); } int left = 0 ; int res = Integer.MAX_VALUE; for (int i = 0 ; i <= N; i++) { if (i == 0 ) { res = right[0 ]; } else if (i == N) { left += str[i - 1 ] == 'G' ? 1 : 0 ; res = Math.min(res, left); } else { left += str[i - 1 ] == 'G' ? 1 : 0 ; res = Math.min(res, right[i] + left); } } return res; } public static void main (String[] args) { System.out.println(minPaint("GGGGGR" )); } }
最大的正方形边框
最暴力的方法:遍历矩阵中的每一个位置,在该位置处遍历此处可以支持的边框的大小(扩到矩阵边界为止),然后遍历该边框内的值,判断是否都是1。这种方式时间复杂度为 O(N^4),不可采纳。
预处理技巧:
1、判断边框内是否都为1 :创建两个辅助数组 right 和 down,分别保存:
right[]:包含当前位置在内,往后连续为1的个数,多扩一列(为了最后一列的计算方便)
down[]:包含当前位置在内,往下连续为1的个数,多扩一行(为了最后一行的计算方便)
注意 :这里的 right 和 down 的范围要比原矩阵多一列和一行 ,这样就能快速计算出最后一行和最后一列的值,不需要单独初始化最后一行和最后一列。
事先遍历一遍整个矩阵,将 right 和 down 内的值初始化完毕,后面使用该矩阵即可快速计算出当前框内是否都为1。方法为:判断边框左上角,右上角和左下角的 right/down 中的值即可:
1 2 3 4 5 6 if (right[i][j] >= size && down[i][j] >= size && right[i + size - 1 ][j] >= size && down[i][j + size - 1 ] >= size) { return true ; }
2、遍历矩阵 :不同于正向思维中遍历到每一个位置后再遍历可能的边框情况。我们选用逆向思维 ,先确定边框的范围(从大到小) ,然后再判断在当前边框大小的情况下,原矩阵中哪些位置可以作为边框的左上角被遍历到 ,在这些左上角位置开始调用封装好的方法判断此位置画框能否都是1。这样就能从边框最大的情况开始,一旦找到了该边框尺寸情况下某个位置都是1,则就可以直接返回了,不需要再遍历更小边框的情况了,无疑节省了大量时间。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 public class MaxBorderSize { public static int getMaxBorderSize (int [][] m) { if (m == null || m.length < 1 ) { return 0 ; } int [][] right = new int [m.length][m[0 ].length + 1 ]; int [][] down = new int [m.length + 1 ][m[0 ].length]; setBorderMapSimple(m, right, down); for (int size = Math.min(m.length, m[0 ].length); size > 0 ; size--) { if (meets(m, right, down, size)) { return size; } } return 0 ; } private static boolean meets (int [][] m, int [][] right, int [][] down, int size) { int row = m.length; int col = m[0 ].length; for (int i = 0 ; i < row - size + 1 ; i++) { for (int j = 0 ; j < col - size + 1 ; j++) { if (right[i][j] >= size && down[i][j] >= size && right[i + size - 1 ][j] >= size && down[i][j + size - 1 ] >= size) { return true ; } } } return false ; } public static void setBorderMap (int [][] m, int [][] right, int [][] down) { int row = m.length; int col = m[0 ].length; if (m[row - 1 ][col - 1 ] == 1 ) { right[row - 1 ][col - 1 ] = 1 ; down[row - 1 ][col - 1 ] = 1 ; } for (int i = row - 2 ; i >= 0 ; i--) { if (m[i][col - 1 ] == 1 ) { right[i][col - 1 ] = 1 ; down[i][col - 1 ] = down[i + 1 ][col - 1 ] + 1 ; } } for (int j = col - 2 ; j >= 0 ; j--) { if (m[row - 1 ][j] == 1 ) { down[row - 1 ][j] = 1 ; right[row - 1 ][j] = right[row - 1 ][j + 1 ] + 1 ; } } for (int i = row - 2 ; i >= 0 ; i--) { for (int j = col - 2 ; j >= 0 ; j--) { if (m[i][j] == 1 ) { right[i][j] = right[i][j + 1 ] + 1 ; down[i][j] = down[i + 1 ][j] + 1 ; } } } } public static void setBorderMapSimple (int [][] m, int [][] right, int [][] down) { int row = m.length; int col = m[0 ].length; if (m[row - 1 ][col - 1 ] == 1 ) { right[row - 1 ][col - 1 ] = 1 ; down[row - 1 ][col - 1 ] = 1 ; } for (int i = row - 1 ; i >= 0 ; i--) { for (int j = col - 1 ; j >= 0 ; j--) { if (m[i][j] == 1 ) { right[i][j] = right[i][j + 1 ] + 1 ; down[i][j] = down[i + 1 ][j] + 1 ; } } } } public static int [][] generateRandom01Matrix(int rowSize, int colSize) { int [][] res = new int [rowSize][colSize]; for (int i = 0 ; i != rowSize; i++) { for (int j = 0 ; j != colSize; j++) { res[i][j] = (int ) (Math.random() * 2 ); } } return res; } public static void printMatrix (int [][] matrix) { for (int i = 0 ; i != matrix.length; i++) { for (int j = 0 ; j != matrix[0 ].length; j++) { System.out.print(matrix[i][j] + " " ); } System.out.println(); } } public static void main (String[] args) { int [][] matrix = generateRandom01Matrix(7 , 8 ); printMatrix(matrix); System.out.println(getMaxBorderSize(matrix)); } }