刷算法的心得
前言
刷了270道算法,记录自己的心得体会,主要包括具体题型总结和抽象题型总结。
一、具体题型总结
具体的题型总结就是线性表的数组、链表、队列的操作和应用,树的四种遍历、回溯、剪枝,图的DFS、BFS等,动态规划,前缀树等。
二、抽象题型总结
我把每个知识点封装成一个component,如component-DFS、component-动态规划等。把数据结构应用看出plugin来辅助component完成算法题解。两类可自由组合。总结了五种题型。
1、预处理Pre + component
1)单词演变:预处理成图+BFS-component
//单向BFS+代码优化,减少运行时间。
public int ladderLength2(String beginWord, String endWord, List<String> wordList) {
//1-预处理:将worldList中的单词作为一个节点,将其之间的关系映射为图关系,然后物理结构预处理成数组式的邻接表。
//2-BFS操作:通过广度优先搜索来求源点到终点的最短路径。
for (String s : wordList) addEdge(s);
if (!wordId.containsKey(endWord)) return 0;
addEdge(beginWord);
int begin = wordId.get(beginWord), end = wordId.get(endWord);
int[] dis = new int[nodeNum];//用该数组既能记住起点到所有节点的最短路径,还能避免加入以访问过的节点避免死循环。
dis[begin] = 1;
Queue<Integer> queue = new LinkedList<>();
queue.offer(begin);
while (!queue.isEmpty()) {
int cur = queue.poll();
for (Integer id : edge.get(cur)) {
if (dis[id] == 0) {
dis[id] = dis[cur] + 1;
queue.offer(id);
}
if (id == end) return (dis[id] >>> 1) + 1;
}
}
return 0;
}
Map<String, Integer> wordId = new HashMap<>();
List<List<Integer>> edge = new ArrayList<>();
int nodeNum = 0;
private void addEdge(String s) {
addNode(s);
int len = s.length();
char[] chs = s.toCharArray();
for (int i = 0; i < len; i++) {
char originC = chs[i];
chs[i] = '*';
String newWord = String.valueOf(chs);
addNode(newWord);
int idx1 = wordId.get(s), idx2 = wordId.get(newWord);
edge.get(idx1).add(idx2);
edge.get(idx2).add(idx1);
chs[i] = originC;
}
}
private void addNode(String s) {
if (!wordId.containsKey(s)) {
wordId.put(s, nodeNum++);
edge.add(new ArrayList<>());
}
}
//双向BFS,通过减少没必要的distance计算,从而减少计算时间。2^8^ > 2^4^ + 2^4^(毕竟单向广度优先,越往后走,要以指数级扩展。)
public int ladderLength3(String beginWord, String endWord, List<String> wordList) {
for (String s : wordList) addEdge(s);
if (!wordId.containsKey(endWord)) return 0;
addEdge(beginWord);
int begin = wordId.get(beginWord), end = wordId.get(endWord);
int[] dis1 = new int[nodeNum];//用该数组既能记住起点到所有节点的最短路径,还能避免加入以访问过的节点避免死循环。
int[] dis2 = new int[nodeNum];
dis1[begin] = 1;
dis2[end] = 1;
Queue<Integer> queue1 = new LinkedList<>();
Queue<Integer> queue2 = new LinkedList<>();
queue1.offer(begin);
queue2.offer(end);
while (!queue1.isEmpty() && !queue2.isEmpty()) {
int curB = queue1.poll(), curE = queue2.poll();
for (Integer id : edge.get(curB)) {
if (dis1[id] == 0) {
dis1[id] = dis1[curB] + 1;
queue1.offer(id);
}
if (dis2[id] != 0) {
return dis1[id] + dis2[id] >>> 1;
}
}
for (Integer id : edge.get(curE)) {
if (dis2[id] == 0) {
dis2[id] = dis2[curE] + 1;
queue2.offer(id);
}
if (dis1[id] != 0 && dis2[id] != 0) {
return dis1[id] + dis2[id] >>> 1;
}
}
}
return 0;
}
2)开锁密码:预处理成图+component-BFS
package com.xhu.offer.offerII;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
//开锁密码
public class OpenLock {
//双向BFS-component
public int openLock(String[] deadends, String target) {
Set<String> dead = new HashSet<>();
for (String deadend : deadends) dead.add(deadend);
//为了避免BFS多一层就是一层指数级增长,所以先把begin==end判断了。
if (dead.contains("0000")) return -1;
if (target.equals("0000")) return 0;
Queue<String> q1 = new LinkedList<>();
Queue<String> q2 = new LinkedList<>();
q1.offer("0000");
q2.offer(target);
int[] dis1 = new int[10000];
int[] dis2 = new int[10000];
dis1[0] = 1;
dis2[Integer.parseInt(target)] = 1;
while (!q1.isEmpty() && !q2.isEmpty()) {
int r = computingDis(q1, dis1, dis2, dead);
if (r != -1) return r;
r = computingDis(q2, dis2, dis1, dead);
if (r != -1) return r;
}
return -1;
}
private int computingDis(Queue<String> q, int[] dis1, int[] dis2, Set<String> dead) {
String cur = q.poll();
int idx = Integer.parseInt(cur);
char[] chs = cur.toCharArray();
for (int i = 0; i < 4; i++) {
char originC = chs[i];
int t = (originC + 1 - '0') % 10 + '0';
chs[i] = (char) t;
String newS = String.valueOf(chs);
int j = Integer.parseInt(newS);
if (!dead.contains(newS) && dis1[j] == 0) {
q.offer(newS);
dis1[j] = dis1[idx] + 1;
}
if (dis2[j] != 0) {
return dis1[j] + dis2[j] - 2;
}
t = (originC + 9 - '0') % 10 + '0';
chs[i] = (char) t;
newS = String.valueOf(chs);
j = Integer.parseInt(newS);
if (!dead.contains(newS) && dis1[j] == 0) {
q.offer(newS);
dis1[j] = dis1[idx] + 1;
}
if (dis2[j] != 0) {
return dis1[j] + dis2[j] - 2;
}
chs[i] = originC;
}
return -1;
}
}
2、component + component
复原IP:component-DP + component-DFS
//和拆解成多少回文字符串一样,那里是满足是否为回文,这里是满足是否为不含前导为0且转化为的数字在0-255之间。
//dp+DFS
public List<String> restoreIpAddresses(String s) {
int len = s.length();
int[][] dp = new int[len][len];
//dp
for (int i = 0; i < len; i++) {
for (int j = i; j >= 0; j--) {
//超过三位数肯定大于255,而且后面的都大于3位数了,直接break。
if (i - j > 2) break;
//前导为0的肯定不行,只能看看后面是否有希望
if (s.charAt(j) == '0' && i != j) continue;
//筛选0-255的数字
int m = Integer.parseInt(s.substring(j, i + 1));
if (m <= 255) dp[i][j] = 1;
}
}
//dfs
dfs(s, dp, 0, 0);
return res;
}
List<String> res = new ArrayList<>();
StringBuilder sb = new StringBuilder();
//总结:急解决不了问题,反而会导致思路乱起来停滞不前,没有将压力转化为动力,而是压力自然变成了巨大无比的阻力。该调试调试,该理思路理思路...
private void dfs(String s, int[][] dp, int cur, int level) {
if (level == 4) {
if (cur == s.length()) res.add(sb.delete(sb.length() - 1, sb.length()).toString());//去掉最后的点
return;
}
for (int i = 0; i + cur < s.length(); i++) {
if (dp[cur + i][cur] == 0) continue;
sb.append(Integer.parseInt(s.substring(cur, cur + 1 + i))).append('.');
dfs(s, dp, cur + 1 + i, level + 1);
sb.delete(cur + level, sb.length());//bug1:直接从cur删是有问题的,没有考虑加的点所占的位置。
}
}
3、转化问题 + component +[plugin]
百度笔试题:找到数组内的两个最长子数组,要求这两个子数组的0和1的数字出现次数相等。
1)转化问题
可以看见两个数组可以拥有同样的中间部分,中间部分的0和1两者肯定是相等的,那么只需找到左边和右边相等的数字,让左右的之间的距离尽可能的大。
2)如何找到相等还要尽可能的大?可以component-双for循环 + plugin-剪枝。
package baidu;
import java.util.Scanner;
public class Main2 {
//总结:其实细心读题很关键,分析题转换问题很关键,往这方面走,而不是往套模板走。当然现在还没有到达套模板的境界,先刷800题。
//编程只是工具,一般编程不难,重要核心是剖析问题的本质,想办法解决,然后再用程序实现该办法。
//每天学习要进步,而不是老走老路,要不断反思,不断改变自己的思考和学习等方法,才能往前走,可以多去实践经历,会找到本质的成长,心也踏实一些。
private static void myFunc(String s) {
int len = s.length();
int mid = len >>> 1;
//找两端两个相等的两个数,让两端相减最大。
int max = 0, start = 0, end = 0;
for (int i = 0; i < mid; i++) {
//j - i + 1 > max剪枝
for (int j = len - 1; j - i + 1 > max && j >= mid; j--) {
if (s.charAt(i) == s.charAt(j)) {
int gap = j - i + 1;
if (gap > max) {
max = gap;
start = i + 1;
end = j + 1;
}
}
}
}
StringBuilder sb = new StringBuilder();
sb.append(start).append(' ').append(end - 1).append(' ').append(start + 1).append(' ').append(end);
System.out.println(start + " " + (end - 1) + " " + (start + 1) + " " + end);
}
public static void main(String[] args) {
Scanner scin = new Scanner(System.in);
String s = scin.nextLine().trim();
myFunc(s);
}
}
4、component + DS-plugin/普通plugin
最长斐波拉契数列:component-DP + HashMap-plugin
//hashmap处理+动态规划(往后遍历,每加一个元素,都要看前面那两个元素符合,所以需要固定一个元素,所以双循环,另一元素确定用map快速查找)
public int lenLongestFibSubseq(int[] arr) {
int len = arr.length, ans = 0;
Map<Integer, Integer> cache = new HashMap<>();//map解决前面可能符合条件太多的状态,以O(1)快速找到,而不是一个一个去试。
int[][] dp = new int[len][len];//i到j的最长斐波拉契数列
for (int i = 0; i < len; i++) {
for (int j = i + 1; j < len; j++) {
dp[i][j] = 2;
int gap = arr[j] - arr[i];
if (cache.containsKey(gap)) {
int l = cache.get(gap);
dp[i][j] = dp[l][i] + 1;
ans = Math.max(ans, dp[i][j]);
}
cache.put(arr[i], i);
}
}
return ans < 3 ? 0: ans;
}
5、理清逻辑就行(easy题型)
百度笔试题:把数组放大K倍
package baidu;
import java.util.Scanner;
public class Main1 {
//总结:理清思路,想好思路是否是一种可行的方案,这需要很多实践和积累才能快速的判断。
private static int[][] myFunc(int n, int k, int[][] nums) {
int newN = n * k;
int[][] res = new int[newN][newN];
int i = 0;
for (int[] num : nums) {
//扩展第i行
int j = 0;
for (int val : num) {
for (int u = j * k; u < j * k + k; u++) {
res[i * k][u] = val;
}
j++;
}
//复制开始一行到下面k - 1 行。
for (int u = i * k + 1; u < i * k + k; u++) {
for (int o = 0; o < newN; o++) {
res[u][o] = res[u - 1][o];
}
}
i++;
}
return res;
}
public static void main(String[] args) {
Scanner scin = new Scanner(System.in);
String[] strs = scin.nextLine().trim().split(" ");
int n = Integer.parseInt(strs[0]), k = Integer.parseInt(strs[1]);
int[][] nums = new int[n][n];
for (int i = 0; i < n; i++) {
strs = scin.nextLine().trim().split(" ");
for (int j = 0; j < n; j++) {
nums[i][j] = Integer.parseInt(strs[j]);
}
}
int[][] res = myFunc(n, k, nums);
for (int[] re : res) {
for (int i : re) {
System.out.print(i);
System.out.print(' ');
}
System.out.println();
}
}
}
总结
1)基本知识点是基本功,可去牛客网分类刷题,打牢基本功,即熟练掌握基本知识点,如数组、链表、栈、队列、二叉树、图、排序、查找、动态规划、前缀树的相关知识点。
2)在基本功掌握扎实了,封装知识点为component、plugin、问题刨析并转换,难题都是这三种的组合。这三种可以搭起任何难题。不过要自己多积累,多刷题,多总结。
3)除此之外,多见识学习总结别人的代码,上面的组合能解决问题,但是还有个关键点是尽可能的减少运行时间和空间消耗,多刷题见识各种coding技巧。如BFS中的dis数组一变量多用,既可以放在被访问的节点不被访问,还可以不记录当前队列的层数;双向BFS;虚节点;等等。
参考文献
[1] LeetCode 刷题+算法竞赛+找工作
[2] 牛客网 刷题+学习+交流+找工作神器