这个视频给我最大的感触就是算法模板。以前大概能感觉出来很多问题有相似之处,但是没有想过是不是有模板。用回溯递归为例,我翻了一下N-Queues问题和Subsets问题的递归解法比对一下,确实相似的地方很多。至于为什么当初没有想过算法模板,洞察力不够可能是一方面,另一方面应该要属于算法答案的多重性。对于同一类型的问题,给人第一感觉的解法可能是不一样,比如A题我先想到了一个递归解法,B题我一下想到了非递归的解法,虽然是同一类型这样却找不到共同点了。
有了模板,很多事就轻松多了。就像我渣渣的英语,因为有作文模板的存在,水平是次了点,但作文是分丢不了。不知道后面董老师会给多少模板,肯定是越完备越好了,还是比较期待的。当然,对于基础题目的熟练掌握是必须的,这个无需解释的。没有基础,再多的模板也算是白搭。
首先董老师给出个实战算法的策略:
1. 相似题目
- 考察算法的类型实际上不多,应该多总结。
- 关注质多于量,对做过的题目能完美的解决。
2. 找出适合同一类题目的模板程序
- 类似于英语作文,其实同一类的题目也是有模板框架的。
- 以回溯递归的题目为例子,仔细点可以发现这些答案的共同点有很多
3. 对基础题熟练掌握
1.Memmove()
从基础题开始,以Memmove()为例,考察对基础的掌握程度。Memmove()算是经典老题了,它考察的关键点就在于有没考虑到内存重叠的处理情况。
void *memmove(void *dest, void const *src, size_t n)
{
register char *dp = dest;
register char const *sp = src;
//通过dp和sp的比较,可以得出dp的哪端,确定是没有重叠的
if (dp < sp) {
while(n-- !=0)
*dp++ == *sp++;
} else {
dp += n;
sp += n;
while (n-- != 0)
*--dp = *--sp;
}
return dest;
}
2.无重复元素的Subsets()
接着是Subsets()问题,也就是求所有元素的子集。它的解法当然不止一种,起码有递归和非递归两种。这里是以递归为例子,它应该和N-Queues问题一样属于NP问题,其时间复杂度也不用怀疑了,肯定是指数量级的。
除了N-Queues,和Subsets同一类型的题目还有很多,比如:Combination Sum I & II,Permutations,Combination,数独……然而它们的解题思路都是相当一致的,中心思想都是用一个循环递归处理子问题。
以Subsets(没有重复元素)为例,它的核心问题在于某个元素到底是:放or不放,则先假设放入,进入子问题递推处理,之后再将其移除,具体代码如下:
void subsets(int [] num)
{
ArrayList<Integer> path = new ArrayList<Integer>();
Arrays.sort(num);
subsetsHelper(path, num, 0);
}
void subsetsHelper(ArrayList<Integer> path, int[] num, int pos)
{
outputToResult(path);
for (int i = pos; i < num.length; i++)
{
path.add(num[i]);
subsetsHelper(path, num, i + 1);
path.remove(path.size() - 1);
}
}
- sort是不是可以去掉?就这题来说,是的。但是如果数组中有重复元素呢,这时要么用sort,要么用set。这里加了一个sort进行排序,一方面可以使输出的结果从小到大排列看起来比较舒服,另一方面也能与之后复杂情况的代码保持一致性。
- 关键代码就是subsetsHelper中的for循环。即首先假设num[i]放入到path中,然后子问题递归处理,最后将num[i]移除,继续循环。
- {1,2,3}示意图如下。请注意输出的顺序。
3.有重复元素的Subsets()
那么如果集合中有重复元素呢,仔细观察,可以发现:我们只关心取多少个某重复元素,而不关心取得是第几个。也就是对重复元素循环时跳过递归处理,只对第一个未处理的元素进行递归处理(当处理第一个递归时,会关注于重复元素的个数)。具体代码如下:
void subsets(int [] num)
{
ArrayList<Integer> path = new ArrayList<Integer>();
Arrays.sort(num);
subsetsHelper(path, num, 0);
}
void subsetsHelper(ArrayList<Integer> path, int[] num, int pos)
{
outputToResult(path);
for (int i = pos; i < num.length; i++)
{
if (i > 0 && i != pos && num[i]==num[i-1])
continue;
path.add(num[i]);
subsetsHelper(path, num, i + 1);
path.remove(path.size() - 1);
}
}
没错,看起来简单的多了中间的if语句。我们分析一下,if中的要点吧:
- sort是必须的。如果没有再前面使用sort,那么则需要使用set来判断是否已经重用了某元素。
- i != pos 其实满巧妙的。它不仅能够使我们不关注于取得哪个元素,而且和for循环中初始值i = pos配合取得多次不同个数的相同元素情况。
- {1,2,2}示意图如下。也请注意输出的顺序。
最后,关于排列组合的问题,这个估计还能再写一篇,之后再填吧。