dfs,bfs

https://www.jianshu.com/p/a753d5c733ec

dfs

1 递归实现

1
2
3
def dfs(当前节点,子节点)
for node in 子节点
dfs(node,new 子节点)

310. 最小高度树

1
2
#### root 当前节点 path子节点 depth 深度
dfs(root,path,depth)

695. 岛屿的最大面积

1
2
###grid全部格点 cur_i,cur_j 当前节点的位置,用+1-1可以确定子节点
dfs(self, grid, cur_i, cur_j) :

2 迭代实现

bfs

1 迭代实现

root进队,root出队,root的子节点【a,b,c】进队,a出队,a的子节点进队,b出队,b的子节点进队。。。

访问顺序(出队顺序):root a b c 。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def levelOrder(self, root: 'Node') -> List[List[int]]:
if root==None:
return []
ans = []
q = [root]
while q:
# length = len(q)
# arr = []
# for _ in range(length):
node = q.pop(0)
# arr.append(node.val)
for chd in node.children:
q.append(chd)
# if arr:
ans.append(node.val)
return ans

429. N 叉树的层序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def levelOrder(self, root: 'Node') -> List[List[int]]:
ans = [] ##结果
q = [root] ##队列
while q:
length = len(q)
arr = [] #出队
for _ in range(length):
node = q.pop(0)
if not node:
continue
arr.append(node.val)
for chd in node.children:
q.append(chd)
if arr:
ans.append(arr)
return ans

2 递归实现

应用选择

时间复杂度都为O(n),n为全部节点

  1. BFS是用来搜索最短径路的解是比较合适的,比如求最少步数的解,最少交换次数的解,因为BFS搜索过程中遇到的解一定是离根最近的,所以遇到一个解,一定就是最优解,此时搜索算法可以终止。这个时候不适宜使用dfs,因为DFS搜索到的解不一定是离根最近的,只有全局搜索完毕,才能从所有解中找出离根的最近的解。(当然这个DFS的不足,可以使用迭代加深搜索ID-DFS去弥补)
  2. DFS适合搜索全部的解,而正因为要搜索全部的解,那么BFS搜索过程中,遇到离根最近的解,并没有什么用,也必须遍历完整棵搜索树;DFS搜索会搜索全部,但是相比之下 DFS不用记录过多信息,所以搜素全部解的问题,DFS显然更加合适。空间优劣上,DFS是有优势的,DFS不需要保存搜索过程中的状态,而BFS在搜索过程中需要保存搜索过的状态,而且一般情况需要一个队列来记录。

和回溯的关系

回溯是算法思想,dfs和bfs是搜索解空间的手段

参考

https://www.jianshu.com/p/e81a16a6f771

https://blog.csdn.net/weixin_41894030/article/details/95317440

单调栈

分类

单调栈也分为单调递增栈单调递减栈

  • 单调递增栈:单调递增栈就是从栈底到栈顶数据是从大到小
  • 单调递减栈:单调递减栈就是从栈底到栈顶数据是从小到大

举例:单调递增栈

现在有一组数10,3,7,4,12。从左到右依次入栈,则如果栈为空或入栈元素值小于栈顶元素值,则入栈;否则,如果入栈则会破坏栈的单调性,则需要把比入栈元素小的元素全部出栈。

10入栈时,栈为空,直接入栈,栈内元素为10。

3入栈时,栈顶元素10比3大,则入栈,栈内元素为10,3。

7入栈时,栈顶元素3比7小,则栈顶元素出栈,此时栈顶元素为10,比7大,则7入栈,栈内元素为10,7。

4入栈时,栈顶元素7比4大,则入栈,栈内元素为10,7,4。

12入栈时,栈顶元素4比12小,4出栈,此时栈顶元素为7,仍比12小,栈顶元素7继续出栈,此时栈顶元素为10,仍比12小,10出栈,此时栈为空,12入栈,栈内元素为12。

伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
stack<int> st;
//单调递增栈
for (遍历这个数组)
{
if (栈空 || 栈顶元素大于等于当前比较元素)
{
入栈;
}
else
{
while (栈不为空 && 栈顶元素小于当前元素)
{
栈顶元素出栈;
更新结果;
}
当前数据入栈;
}
}

应用

主要用于$O(n)$ 解决NGE问题(Next Greater Element)

  • 比当前元素更大的下一个元素
  • 比当前元素更大的前一个元素
  • 比当前元素更小的下一个元素
  • 比当前元素更小的前一个元素

参考

https://blog.csdn.net/lucky52529/article/details/89155694

leetcode常见套路

一.常见算法

分治策略,动态规划,回溯,分支限界,贪心策略

二.巧用数据结构

普通栈、单调栈

队列

字典树

三.技巧

双指针,滑窗,二分查找,排序,快慢指针,取余,位运算,递归,时空转化(hashtable),dfs/bfs

参考

https://zhuanlan.zhihu.com/p/358653377

https://zhuanlan.zhihu.com/p/341176507

https://zhuanlan.zhihu.com/p/358653377

https://zhuanlan.zhihu.com/p/341176507

字典树

一.核心思想

Trie tree,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较。Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。字典树的查询时间复杂度是O (L),L是待查字符串的长度。如果是普通的线性表结构,那么查询效率为O(NL),N为待查数据集的大小。

假设有b,abc,abd,bcd,abcd,efg,hii 这6个单词,那我们创建字典树如下:

二.应用

目的:利用汉语拼音缩写还原中文汉字

准备:数据集(包含中文汉字以及对应汉语缩写)

思想:1.基于汉语拼音缩写检索出数据集中对应的所有中文汉字 2.基于中文汉字出现频次排序,将top1作为汉语拼音的还原结果

代码

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
import  pandas as pd
import pickle
import os
import joblib

class TrieNode(object):
def __init__(self):
"""
Initialize your data structure here.
"""
self.data = {}###字母字符
self.data1={}###中文
self.is_word = False###标识是否汉字


class Trie(object):

def __init__(self):
self.root = TrieNode()

def insert(self, word,word1):
"""
Inserts a word into the trie.
:type word: str
:rtype: void
"""
node = self.root
for letter in word:
child = node.data.get(letter)
if not child:
node.data[letter] = TrieNode()
node = node.data[letter]
node.is_word = True
if word1 not in node.data1:
node.data1[word1]=1
else:
node.data1[word1]+=1

def search(self, word):
"""
Returns if the word is in the trie.
:type word: str
:rtype: bool
"""
node = self.root
for letter in word:
node = node.data.get(letter)
if not node:
return False
return node.is_word

def starts_with(self, prefix):
"""
Returns if there is any word in the trie
that starts with the given prefix.
:type prefix: str
:rtype: bool
"""
node = self.root
for letter in prefix:
node = node.data.get(letter)
if not node:
return False
return True

def get_start(self, prefix):
"""
Returns words started with prefix
:param prefix:
:return: words (list)
"""
def _get_key(pre, pre_node):
words_list = []
if pre_node.is_word:
words_list.append([pre,pre_node.data1])
for x in pre_node.data.keys():
words_list.extend(_get_key(pre + str(x), pre_node.data.get(x)))
return words_list

words = []
if not self.starts_with(prefix):
return words
# if self.search(prefix):
# words.append(prefix)
# return words
node = self.root
for letter in prefix:
node = node.data.get(letter)
return _get_key(prefix, node)


def find_result(self,string):
result =self.get_start(string)
result = sort_by_value(result[0][1])
result.reverse()
return result[0]


def sort_by_value(d):
return sorted(d.items(), key=lambda k: k[1]) # k[1] 取到字典的值。



def build_tree(data,save_path):

trie = Trie()
for element in data.values:
trie.insert(element[0], element[1])
joblib.dump(trie, save_path)
return

def load_tree(path):
trie = joblib.load(path)
return trie

if __name__ == '__main__':

###
build_tree(data,save_path)
###
tree=load_tree(save_path)
print(tree.find_result("XXXXXXXX"))

参考

https://zhuanlan.zhihu.com/p/28891541

常见算法总结

类别归类:蛮力,分治,动态规划,贪心,回溯,分支限界。

实现方式有两种,一般为基于迭代和基于递归。

一.蛮力Brute-force

没有什么好说,就是遍历或者枚举。

二.分治

分而治之,将大问题拆解成相互独立且相似的子问题。

步骤:1、先分 2 求解 3 .合并

实现方式:一般递归实现

三.动态规划

和暴力相比,dp利用了子问题间的依赖关系,减少了一些计算

适用条件:1.满足最优子结构性质,即最优解所包含的子问题的解也是最优的

构造递推式

0.确定维度,一般一维或者二维

1.注意含义

2.选择分隔点

​ a. 一般和前一步或者两步有关,比如最大子序和,$dp[i]=max(s[i],s[i]+dp[i-1])$

​ b. 但有时候需要遍历全部分割点,比如单词拆分,$dp[i]=dp[j] \ \& \& \ check(s[j..i−1])$,

​ c. 有时候动态选择分隔点,比如丑数,$ dp[i]=min(dp[p2]2,dp[p3]3,dp[p5]*5) $,其中$p2,p3,p5$动态变化

3.注意+, -

一定是用已有的递推没有的

举例比如62. 不同路径,$dp[i][j] = dp[i-1][j] + dp[i][j-1]$;

1
2
3
4
5
6
7
8
9
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[1]*n] + [[1]+[0] * (n-1) for _ in range(m-1)]
#print(dp)
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[-1][-1]

比如5. 最长回文子串,$dp[i][j]=dp[i+1][j-1] \ and \ s[i]==s[j]$

s = “babad”

n=5

(i,j)

(0,1) (1,2) (2,3) (3,4) (0,2) (1,3) (2,4) (0,3)

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
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
if n < 2:
return s

max_len = 1
begin = 0
# dp[i][j] 表示 s[i..j] 是否是回文串
dp = [[False] * n for _ in range(n)]
for i in range(n):
dp[i][i] = True

# 递推开始
# 先枚举子串长度
for L in range(2, n + 1):
# 枚举左边界,左边界的上限设置可以宽松一些
for i in range(n):
# 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
j = L + i - 1
# 如果右边界越界,就可以退出当前循环
if j >= n:
break

if s[i] != s[j]:
dp[i][j] = False
else:
if j - i < 3:
dp[i][j] = True
else:
dp[i][j] = dp[i + 1][j - 1]

# 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if dp[i][j] and j - i + 1 > max_len:
max_len = j - i + 1
begin = i
return s[begin:begin + max_len]

解的追踪:有时候需要借助备忘录搜索解,时间复杂度不超过计算备忘录

实现方式

递归实现:效率不高的原因在于子问题重复计算了,比起暴力哪个快?应该还是这个

迭代+备忘录:不一定全部都要存储,需要的存着了就可以了,每次子问题计算一次(空间换时间)

一般采用迭代+备忘录

状态压缩动态规划

https://jimmy-shen.medium.com/bitmask-state-compression-dp-39e7ba56978b

https://segmentfault.com/a/1190000038223084

https://blog.csdn.net/wxgaws/article/details/114693162

Usually, the state of DP can use limited variables to represent such a dp[i] , dp[i] [j] ,dp[i] [j] [k].

However, sometimes, the states of a DP problem may contain multiple statuses. In this case, we can think about using the state compression approaches to represent the DP state.

说白了就是在状态很多的时候对状态降维

目的:可以优化空间复杂度,不知道可以不可以优化时间复杂度

四.贪心

短视,局部最优(少考虑很多,计算量就下去了)

需要证明

五.回溯

一般用于可以用树型结构建模的问题

回溯法能系统地搜索一个问题的所有解,和暴力的区别在于回溯可以剪枝,以此减少计算量

实现方式:利用DFS搜索解空间,一般递归实现

https://zhuanlan.zhihu.com/p/93530380

https://zhuanlan.zhihu.com/p/112926891

六.分支限界

类似回溯,区别如下

代价函数

以极大值问题为例,以当前结点为根的子树能够得到的可行解的最大值

以极大值问题为例,当前已经得到的可行解的最大值

减枝

代价函数和界可以用于减枝,以极大值问题为例,界大于等于某结点代价函数,该结点就不需要继续搜索了

参考文献

https://blog.csdn.net/zm1_1zm/article/details/69224626

https://blog.csdn.net/wxgaws/article/details/114693162

https://segmentfault.com/a/1190000038223084

https://jimmy-shen.medium.com/bitmask-state-compression-dp-39e7ba56978b


:D 一言句子获取中...