Leetcode 1024 视频拼接
翻译一下这个题:在固定的集合内寻找符合条件的组合,设组合中相邻的两个元素分别为 pre[],current[] ,则组合需要:
1. 两段视频拼接是没有空隙,也就是 current[0] <= pre[1]
2. 不要重复组合,需要:current[1] > pre[1]
3. 组合要涵盖 0 到 T,头元素 head[0] 必须为 0 ,尾元素 tail[1] 必须大于等于 T
找到符合上面条件的所有组合,找出其中元素最少的组合。
首先整个源数组是乱序的,在遍历所有组合时,需要不断的随机挑选元素。这使得计算过程中,需要不断的记录已经挑选过的元素,避免重复挑选同一个元素。也就是说,遍历的过程中难免会用到回溯的手法对已经挑选过的元素进行记录和删除。这使得整个计算过程很难建立缓存,因为除了需要缓存问题的入参坐标,还需要缓存回溯的数组,需要的额外空间太惊人。
所以第一步需要对源数组进行排序,排序后可以朝着一个方向进行遍历,这样就避免了对已选择元素的记录,可以更好的建立缓存来避免重复计算。
动态规划解法:
//头结点集合 final List<int[]> heads = new LinkedList<int[]>(); //最大尾部值 int maxEnd; public int videoStitching(int[][] clips, int T) { sort(clips); findSomething(clips); if (heads.size() == 0 || maxEnd < T) { return -1; } int re = Integer.MAX_VALUE; Map<Integer, Integer> cache = new HashMap<Integer, Integer>(); for (int i = 0; i < heads.size(); i++) { int an = videoStitching(clips, T, heads.get(i)[1], i, cache); if (an == -1) { continue; } re = Math.min(an, re); } return re == Integer.MAX_VALUE ? -1 : re; } /** * @Author Niuxy * @Date 2020/7/18 8:41 下午 * @Description 以第 point 个元素结尾,当前尾部值为 end ,目标尾部值为 T 的所需的最少元素数 */ private final int videoStitching(int[][] clips, int T, int end, int point, Map<Integer, Integer> cache) { if (end >= T) { System.out.println(clips[point][0] + "," + clips[point][1]); return 1; } if (point == clips.length) { return 0; } int key = end + point * 1113; if (cache.containsKey(key)) { return cache.get(key); } //达成目标所需最小元素数 int re = Integer.MAX_VALUE; int next = point + 1; //尝试所有拼接可能 for (int i = point + 1; i < clips.length; i++) { //不满足拼接条件 if (clips[i][1] <= clips[point][1] || clips[i][0] > clips[point][1]) { continue; } int an = videoStitching(clips, T, clips[i][1], i, cache); if (an != -1) { re = Math.min(re, an + 1); } } re = re == Integer.MAX_VALUE ? -1 : re; //缓存结果。复用 cache.put(key, re); return re; } /** * @Author Niuxy * @Date 2020/7/18 4:42 下午 * @Description 按视频开始时间排序 */ private final void sort(int[][] clips) { for (int i = 0; i < clips.length; i++) { for (int j = i + 1; j < clips.length; j++) { if (clips[j][0] < clips[i][0]) { int[] temp = clips[j]; clips[j] = clips[i]; clips[i] = temp; } } } } /** * @Author Niuxy * @Date 2020/7/18 8:40 下午 * @Description 筛选头元素,并找出尾元素最大值 */ private final void findSomething(int[][] clips) { for (int[] num : clips) { maxEnd = maxEnd > num[1] ? maxEnd : num[1]; if (num[0] == 0) { heads.add(num); } } }
暴力的遍历整个解空间是认识问题及其解空间的第一步。遍历时如果需要使用回溯的手法对过程进行记录,需要针对遍历的依据进行排序,将随机遍历变为顺序遍历,优化掉回溯部分,方便分治问题的定义。(本身就需要记录过程的问题除外,如八皇后,或者本题改为“返回所有可能的组合”)。
定义分治问题,也就是找到一种问题的定义,使得问题可以由其本身表示(规模大的问题由规模小的相同问题表示)。
找到问题的定义,是分治中最复杂的一步,也是最有意思的一步。
往往因为各种各样的问题,使得状态转移方程很难推导。在对解空间有一定认识后,通常会得出一个初步的问题定义方式以遍历解空间。该方式大方向上往往没错,但不一定是理想的分治方式。比如计算过程中没有太多的重复计算,或者问题粒度太细以至于无法使用缓存来提升计算效率。
改变问题的定义方式,往往是在最初版本的基础上变通,在一些关键逻辑上进行取反。
无序变有序便是其中一种,常用的取反逻辑还有选择变为不选择、找极大变为找极小、视角从局部到个体、甚至是整个问题的转化,围魏救赵。
找到正确的逻辑组合需要大量的练习积累经验、以及将解决问题的逻辑嚼碎,找到可以突破的点。从这个角度看,生活中的任何问题,都可以归结到算法问题中。