目录
一、单调队列
例:滑动窗口的最大值
给定一个数组和滑动窗口的大小,请找出所有滑动窗口里的最大值。
例如,如果输入数组[2, 3, 4, 2, 6, 2, 5, 1]及滑动窗口的大小3,那么一共存在6个滑动窗口,它们的最大值分别为[4, 4, 6, 6, 6, 5]。
注意:
- 数据保证k大于0,且k小于等于数组长度。
样例
输入:[2, 3, 4, 2, 6, 2, 5, 1] , k=3
输出: [4, 4, 6, 6, 6, 5]
解答python版本:
from collections import deque
class Solution(object):
def maxInWindows(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: List[int]
"""
if not nums:
return []
res=[]
q=deque([])
for i in range (len(nums)):
if i<k-1:
while q and q[-1]<nums[i]:
q.pop()
q.append(nums[i])
else:
while q and q[-1]<nums[i]:
q.pop()
q.append(nums[i])
res.append(q[0])
if q and nums[i-k+1]==q[0]:
q.popleft()
return res
'''
利用deque的双端队列实现单调队列,单调队列中的元素从队头到队尾单调递减
实现这一机制的关键就是每次在队尾添加元素时要和前面元素进行比较,直接pop()
然后再添加
对于控制队列长度的机制,可以保证队列不空的基础上,如果nums[i-k+1]==q[0],
队头元素的话,就popleft
'''
注意细节是i<k-1的情况,这样从第k个数才开始输出,否则只会输出k-1个数
「单调队列」 的核⼼思路和「单调栈」 类似。 单调队列的 push ⽅法依然在队尾添加元素, 但是要把前⾯⽐新元素⼩的元素都删掉:
class MonotonicQueue {
private:
deque<int> data;
public:
void push(int n) {
while (!data.empty() && data.back() < n)
data.pop_back();
data.push_back(n);
}
};
你可以想象, 加⼊数字的⼤⼩代表⼈的体重, 把前⾯体重不⾜的都压扁了,直到遇到更⼤的量级才停住。
直接一炮轰走,保持单调,强的一批、
- 注意一点是在i<k-1时在往队列push数字时同样要注意pop()末尾元素,保证第一个队列仍然可以保持单调性
二、单调栈
单调栈⽤途不太⼴泛, 只处理⼀种典型的问题, 叫做 Next Greater Element
Next Greater Number 的原始问题:给你⼀个数组, 返回⼀个等⻓的数组, 对应索引存储着下⼀个更⼤元素, 如果没有更⼤的元素, 就存-1。
不好⽤语⾔解释清楚, 直接上⼀个例⼦:
给你⼀个数组 [2,1,2,4,3], 你返回数组 [4,2,4,-1,-1]。
解释:第⼀个 2 后⾯⽐ 2 ⼤的数是 4; 1 后⾯⽐ 1 ⼤的数是 2;第⼆个 2 后⾯⽐ 2 ⼤的数是 4; 4 后⾯没有⽐ 4 ⼤的数, 填 -1;3 后⾯没有⽐ 3 ⼤的数, 填-1。
vector<int> nextGreaterElement(vector<int>& nums) {
vector<int> ans(nums.size()); // 存放答案的数组
stack<int> s;
for (int i = nums.size() - 1; i >= 0; i--) { // 倒着往栈⾥放
while (!s.empty() && s.top() <= nums[i]) { // 判定个⼦⾼矮
s.pop(); // 矮个起开, 反正也被挡着了。 。 。
}
ans[i] = s.empty() ? -1 : s.top(); // 这个元素⾝后的第⼀个⾼个
s.push(nums[i]); // 进队, 接受之后的⾝⾼判定吧!
}
return ans;
}
这就是单调队列解决问题的模板。 for 循环要从后往前扫描元素, 因为我们借助的是栈的结构, 倒着⼊栈, 其实是正着出栈。 while 循环是把两个“⾼个”元素之间的元素排除, 因为他们的存在没有意义, 前⾯挡着个“更⾼”的元素, 所以他们不可能被作为后续进来的元素的 Next Great Number 了
三、部分简单笔试题目
1、0交换排序
题目描述:长度为n的数组乱序存放着0至n-1,现在只能进行0与其他数的交换,完成以下函数。
public void sort(int[] array, int len) {
for(int i = len - 1; i > 0; i--) {
if(array[i] == i) //判断是否处于正确位置
continue;
//对未处于正确位置的数字进行下面两步交换
swapWithZero(array, len, array[i]);//交换0与i位置的数字
swapWithZero(array, len, i);//交换0与数字i
}
2、0-1背包整数求和问题
四、全排列---回溯问题经典题解
回溯模板:
def permutation(s):
"""
input : 'abc'
output: ['abc','acb','bac','bca','cab','cba']
a 的全排列 a
ab 的全排列 ba, ab
abc 的全排列 cab, acb, abc + cba, bca, bac
本质就是一颗决策树的遍历+剪枝过程,每次都在候选集中选出特定一个得到路径,直到抵达正确的结束条件,记录下这一条路径,然后退回上一步
1、先了解3个关键名词:
1. 路径 - 遍历到当前节点的路径
2. 选择 - 此时可供选择的候选集
3. 结束条件 - 加入最终结果的条件
2、回溯法模板:
res = []
def backtrack(choice, path):
if not choice:
res.append(path)
return
for i in range(len(choice)):
current_choice = choice[:i] + choice[i + 1:]
current_path = path + [choice[i]]
backtrack(current_choice, current_path)
backtrack(s, [])
return res
输入一组数字(可能包含重复数字),输出其所有的排列方式。
样例
输入:[1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
解法python版本:
##无重复数字时候
class Solution:
def permutation(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
res=[]
def backtrack(nums,temp):
if not nums:
res.append(temp)
return
for i in range(len(nums)):
current_chioce=nums[:i]+nums[i+1:]
current_path = temp+[nums[i]]
backtrack(current_chioce,current_path)
backtrack(nums, [])
return res
#有重复数字时候需要进行剪枝
#当当前元素和前一个元素值相同(此处隐含这个元素的index>0),并且前一个元素还没有被使用过的时候,我们要剪枝
class Solution:
def permutation(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
nums.sort()
res=[]
def backtrack(nums,temp):
if not nums:
res.append(temp)
return
for i in range(len(nums)):
if i>0 and nums[i] == nums[i-1]:
continue
current_chioce=nums[:i]+nums[i+1:]
current_path = temp+[nums[i]]
backtrack(current_chioce,current_path)
backtrack(nums, [])
return res
(二)、N皇后问题
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例:
输入: 4
输出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
board =[['.']*n for _ in range (n)]
res=[]
def backtrack(board,row):
if row ==len(board):
tmp_list=[]
for i in board:
tmp = ''.join(i)
tmp_list.append(tmp)
res.append(tmp_list)
#res+=1 即可以统计返回的结果数result
for col in range(len(board[0])):
if not isValid(board,row,col):
continue
board[row][col] = 'Q'
backtrack(board,row+1)
board[row][col] = '.'
#在所在列,右上,左上部分搜索是否有效
def isValid(board,row,col):
n=len(board)
for i in range(n):
if board[i][col] == 'Q':
return False
row0,col0=row,col
while row0>0 and col0<n-1:
row0-=1
col0+=1
if board[row0][col0] =='Q':
return False
row1,col1 = row,col
while row1>0 and col1>0:
row1-=1
col1-=1
if board[row1][col1]=='Q':
return False
return True
backtrack(board,0)
return res
五、二分法
二分查找模板
两种划分模式:中点mid是属于左区间还是右区间的
当我们将区间[l, r]划分成[l, mid]和[mid + 1, r]时,其更新操作是r = mid或者l = mid + 1;,计算mid时不需要加1。
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
二.需要加1的情况
当我们将区间[l, r]划分成[l, mid - 1]和[mid, r]时,其更新操作是r = mid - 1或者l = mid;,此时为了防止死循环,计算mid时需要加1。
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
六、双指针问题
滑动窗口:最⼩覆盖⼦串
给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。
示例:
输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"
说明:
如果 S 中不存这样的子串,则返回空字符串 ""。
如果 S 中存在这样的子串,我们保证它是唯一的答案。
class Solution:
def minWindow(self, s: str, t: str) -> str:
ans=s+s
l=0
n=len(s)
target = Counter(t)
counter=defaultdict(lambda:0)
def contains(target,counter):
if len(counter) < len(target):
return False
for k in counter:
if k not in target or counter[k]<target[k]:
return False
return True
for r in range(n):
if s[r] in target:
counter[s[r]] +=1
while l < n and contains(target,counter):
if r-l+1 < len(ans):
ans=s[l:r+1]
if s[l] in target:
counter[s[l]]-=1
l+=1
return "" if ans==s+s else ans
'''
太难了,属于滑动串口的经典题目,但是要用计数器,哈希表,字典来记录滑动窗口中的字符是否与target相匹配,right指针不断右移,并且在字典中添加,对于left指针在满足条件的基础中右移动
其中有两个注意的点:
1、res=s+s,初始化为2s,是防止在判断过程中可能s全部是结果,在更新是区分是" "还是s
2、while l<n 的条件下l左移,是应对s==t的情况,保证指针可以右移,因为要求contains()条件满足,所以left指针并不会跑到right的右边
'''
Leetcode 438. 找到字符串中所有字母异位词
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
字母异位词指字母相同,但排列不同的字符串。
不考虑答案输出的顺序。
示例 1:
输入:
s: "cbaebabacd" p: "abc"
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
ans=[]
l=0
n=len(s)
target = Counter(p)
counter=defaultdict(lambda:0)
def contains(target,counter):
if len(counter) < len(target):
return False
for k in counter:
if k not in target or counter[k]<target[k]:
return False
return True
for r in range(n):
if s[r] in target:
counter[s[r]] +=1
while l < n and contains(target,counter):
if r-l+1 == len(p):
ans.append(l)
if s[l] in target:
counter[s[l]]-=1
l+=1
return ans
'''
解法同上,直接在判断if r-l+1 == len(p)的条件下添加索引即可
'''
规律性模板:
通过上⾯三道题, 我们可以总结出滑动窗⼝算法的抽象思想:
int left = 0, right = 0;
while (right < s.size()) {
window.add(s[right]);
right++;
while (valid) {
window.remove(s[left]);
left++;
}
}
七、栈的定义与应用
class内部定义栈,并且实现反转字符串、括号匹配和后缀表达式实现
#在定义函数时,内置函数改写需要加下划线,自己定义可以直接定义
class Stack():
def __init__(self):
self.items=[]
def push(self,item):
self.items.append(item)
def pop(self):
return self.items.pop()
def top(self):
return self.items[len(self.items)-1]
def isEmpty(self):
return self.items==[]
def size(self):
return len(self.items)
def reverse(strs):
s=Stack()
output=''
for substr in strs:
s.push(substr)
while not s.isEmpty():#要记得加括号,调用函数
output+=s.pop()
return output
print(reverse('apple'))
'''
# 利用栈判断括号平衡Balanced parentheses
bug1 不能直接判断等于false,要前面加not否定.表示栈为空
bug2 while要主要退出循环的条件和index自动增加
在class内部定义函数引用时传入参数必须用self,同时引用时也要用self.match()
'''
def packmatch(strs):
index=0
balance=True
s=[]
while index<len(strs) and balance:
if strs[index] in "({[":
s.append(strs[index])
else:
if not s:
balance=False
else:
top = s.pop() #要注意用pop函数返回的函数值,而top返回的是列表元素
if not match(top,strs[index]):#不能直接判断等于false,要前面加not否定
balance=False
index+=1 #while要主要退出循环的条件和index自动增加
if balance and not s:
return True
else:
return False
def match(m1,m2):
a='({['
b=')}]'
return a.index(m1)==b.index(m2)
#print(packmatch('[{[[]]}]'))
'''
#pop()和top()有很大区别,pop移除一个元素并返回移除函数值,但top只是返回堆顶值
bug1 哈希判断值的时候必须返回堆顶值
bug2 要注意判断堆最后是否为空,只要非空时才往出取值
并且处理思路是数字、左右括号、运算符,数字统计加入辅助数组中,左括号压栈,右括号弹出元素直到和左括号匹配
运算符和栈顶元素比较,栈顶元素优先级高时pop到辅助数组中,最后输出后缀表达式,优先级利用哈希表实现
因此在转换过程中必须要建栈数据结构
'''
def convert(target):
dic={'*':3,'/':3,'+':2,'-':2,'(':1}
prec = {}
prec['*'] = 3
prec['/'] = 3
prec['+'] = 2
prec['-'] = 2
prec['('] = 1
stk=Stack()
copy=[]
tars = target.split()
for tar in tars:
if tar in 'ABCDEFGHIJKL' or tar in '0123456789':
copy.append(tar)
elif tar =='(':
stk.push(tar)
elif tar==')':
token = stk.pop()
while(token!='('):
copy.append(token)
token = stk.pop()
else:
while (not stk.isEmpty()) and dic[tar] <=dic[stk.top()]:
copy.append(stk.pop())
stk.push(tar)
while (not stk.isEmpty()):
copy.append(stk.pop())
return ''.join(copy)
print(convert("A * B + C * D"))
print(convert("( A + B ) * C - ( D - E ) * ( F + G )"))
八、递归思想
递归详解
首先说明一个问题,简单阐述一下递归,分治算法,动态规划,贪心算法这几个东西的区别和联系,心里有个印象就好。
递归是一种编程技巧,一种解决问题的思维方式;分治算法和动态规划很大程度上是递归思想基础上的(虽然动态规划的最终版本大都不是递归了,但解题思想还是离不开递归),解决更具体问题的两类算法思想;贪心算法是动态规划算法的一个子集,可以更高效解决一部分更特殊的问题。
分治算法将在这节讲解,以最经典的归并排序为例,它把待排序数组不断二分为规模更小的子问题处理,这就是 “分而治之” 这个词的由来。显然,排序问题分解出的子问题是不重复的,如果有的问题分解后的子问题有重复的(重叠子问题性质),那么就交给动态规划算法去解决!
递归详解
介绍分治之前,首先要弄清楚递归这个概念。
递归的基本思想是某个函数直接或者间接地调用自身,这样就把原问题的求解转换为许多性质相同但是规模更小的子问题。我们只需要关注如何把原问题划分成符合条件的子问题,而不需要去研究这个子问题是如何被解决的。递归和枚举的区别在于:枚举是横向地把问题划分,然后依次求解子问题,而递归是把问题逐级分解,是纵向的拆分。
以下会举例说明我对递归的一点理解,如果你不想看下去了,请记住这几个问题怎么回答:
- 如何给一堆数字排序? 答:分成两半,先排左半边再排右半边,最后合并就行了,至于怎么排左边和右边,请重新阅读这句话。
- 孙悟空身上有多少根毛? 答:一根毛加剩下的毛。
- 你今年几岁? 答:去年的岁数加一岁,1999 年我出生。
递归代码最重要的两个特征:结束条件和自我调用。自我调用是在解决子问题,而结束条件定义了最简子问题的答案。
int func(你今年几岁) {
// 最简子问题,结束条件
if (你1999年几岁) return 我0岁;
// 自我调用,缩小规模
return func(你去年几岁) + 1;
}
其实仔细想想,**递归运用最成功的是什么?我认为是数学归纳法。**我们高中都学过数学归纳法,使用场景大概是:我们推不出来某个求和公式,但是我们试了几个比较小的数,似乎发现了一点规律,然后编了一个公式,看起来应该是正确答案。但是数学是很严谨的,你哪怕穷举了一万个数都是正确的,但是第一万零一个数正确吗?这就要数学归纳法发挥神威了,可以假设我们编的这个公式在第 k 个数时成立,如果证明在第 k + 1 时也成立,那么我们编的这个公式就是正确的。
那么数学归纳法和递归有什么联系?我们刚才说了,递归代码必须要有结束条件,如果没有的话就会进入无穷无尽的自我调用,直到内存耗尽。而数学证明的难度在于,你可以尝试有穷种情况,但是难以将你的结论延伸到无穷大。这里就可以看出联系了 —— 无穷。
递归代码的精髓在于调用自己去解决规模更小的子问题,直到到达结束条件;而数学归纳法之所以有用,就在于不断把我们的猜测向上加一,扩大结论的规模,没有结束条件,从而把结论延伸到无穷无尽,也就完成了猜测正确性的证明。
为什么要写递归
首先为了训练逆向思考的能力。递推的思维是正常人的思维,总是看着眼前的问题思考对策,解决问题是将来时;递归的思维,逼迫我们倒着思考,看到问题的尽头,把解决问题的过程看做过去时。
第二,练习分析问题的结构,当问题可以被分解成相同结构的小问题时,你能敏锐发现这个特点,进而高效解决问题。
第三,跳出细节,从整体上看问题。再说说归并排序,其实可以不用递归来划分左右区域的,但是代价就是代码极其难以理解,大概看一下代码(归并排序在后面讲,这里大致看懂意思就行,体会递归的妙处):
void sort(Comparable[] a){
int N = a.length;
// 这么复杂,是对排序的不尊重。我拒绝研究这样的代码。
for (int sz = 1; sz < N; sz = sz + sz)
for (int lo = 0; lo < N - sz; lo += sz + sz)
merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
}
/* 我还是选择递归,简单,漂亮 */
void sort(Comparable[] a, int lo, int hi) {
if (lo >= hi) return;
int mid = lo + (hi - lo) / 2;
sort(a, lo, mid); // 排序左半边
sort(a, mid + 1, hi); // 排序右半边
merge(a, lo, mid, hi); // 合并两边
}
看起来简洁漂亮是一方面,关键是可解释性很强:把左半边排序,把右半边排序,最后合并两边。而非递归版本看起来不知所云,充斥着各种难以理解的边界计算细节,特别容易出 bug 且难以调试,人生苦短,我更倾向于递归版本。
显然有时候递归处理是高效的,比如归并排序,有时候是低效的,比如数孙悟空身上的毛,因为堆栈会消耗额外空间,而简单的递推不会消耗空间。比如这个例子,给一个链表头,计算它的长度:
/* 典型的递推遍历框架,需要额外空间 O(1) */
public int size(Node head) {
int size = 0;
for (Node p = head; p != null; p = p.next) size++;
return size;
}
/* 我偏要递归,万物皆递归,需要额外空间 O(N) */
public int size(Node head) {
if (head == null) return 0;
return size(head.next) + 1;
}
写递归的技巧
我的一点心得是:**明白一个函数的作用并相信它能完成这个任务,千万不要试图跳进细节。**千万不要跳进这个函数里面企图探究更多细节,否则就会陷入无穷的细节无法自拔,人脑能压几个栈啊。
先举个最简单的例子:遍历二叉树。
void traverse(TreeNode* root) {
if (root == nullptr) return;
traverse(root->left);
traverse(root->right);
}
这几行代码就足以扫荡任何一棵二叉树了。我想说的是,对于递归函数traverse(root)
,我们只要相信:给它一个根节点root
,它就能遍历这棵树,因为写这个函数不就是为了这个目的吗?所以我们只需要把这个节点的左右节点再甩给这个函数就行了,因为我相信它能完成任务的。那么遍历一棵N叉数呢?太简单了好吧,和二叉树一模一样啊。
void traverse(TreeNode* root) {
if (root == nullptr) return;
for (child : root->children)
traverse(child);
}
至于遍历的什么前、中、后序,那都是显而易见的,对于N叉树,显然没有中序遍历。
以下详解 LeetCode 的一道题来说明:给一课二叉树,和一个目标值,节点上的值有正有负,返回树中和等于目标值的路径条数,让你编写 pathSum 函数:
/* 来源于 LeetCode PathSum III: https://leetcode.com/problems/path-sum-iii/ */
root = [10,5,-3,3,2,null,11,3,-2,null,1],
sum = 8
10
/ \
5 -3
/ \ \
3 2 11
/ \ \
3 -2 1
Return 3. The paths that sum to 8 are:
1. 5 -> 3
2. 5 -> 2 -> 1
3. -3 -> 11
/* 看不懂没关系,底下有更详细的分析版本,这里突出体现递归的简洁优美 */
int pathSum(TreeNode root, int sum) {
if (root == null) return 0;
return count(root, sum) +
pathSum(root.left, sum) + pathSum(root.right, sum);
}
int count(TreeNode node, int sum) {
if (node == null) return 0;
return (node.val == sum) +
count(node.left, sum - node.val) + count(node.right, sum - node.val);
}
题目看起来很复杂吧,不过代码却极其简洁,这就是递归的魅力。我来简单总结这个问题的解决过程:
首先明确,递归求解树的问题必然是要遍历整棵树的,所以二叉树的遍历框架(分别对左右孩子递归调用函数本身)必然要出现在主函数 pathSum 中。那么对于每个节点,他们应该干什么呢?他们应该看看,自己和脚底下的小弟们包含多少条符合条件的路径。好了,这道题就结束了。
按照前面说的技巧,根据刚才的分析来定义清楚每个递归函数应该做的事:
PathSum 函数:给他一个节点和一个目标值,他返回以这个节点为根的树中,和为目标值的路径总数。
count 函数:给他一个节点和一个目标值,他返回以这个节点为根的树中,能凑出几个以该节点为路径开头,和为目标值的路径总数。
/* 有了以上铺垫,详细注释一下代码 */
int pathSum(TreeNode root, int sum) {
if (root == null) return 0;
int pathImLeading = count(root, sum); // 自己为开头的路径数
int leftPathSum = pathSum(root.left, sum); // 左边路径总数(相信他能算出来)
int rightPathSum = pathSum(root.right, sum); // 右边路径总数(相信他能算出来)
return leftPathSum + rightPathSum + pathImLeading;
}
int count(TreeNode node, int sum) {
if (node == null) return 0;
// 我自己能不能独当一面,作为一条单独的路径呢?
int isMe = (node.val == sum) ? 1 : 0;
// 左边的小老弟,你那边能凑几个 sum - node.val 呀?
int leftBrother = count(node.left, sum - node.val);
// 右边的小老弟,你那边能凑几个 sum - node.val 呀?
int rightBrother = count(node.right, sum - node.val);
return isMe + leftBrother + rightBrother; // 我这能凑这么多个
}
还是那句话,明白每个函数能做的事,并相信他们能够完成。
总结下,PathSum 函数提供的二叉树遍历框架,在遍历中对每个节点调用 count 函数,看出先序遍历了吗(这道题什么序都是一样的);count 函数也是一个二叉树遍历,用于寻找以该节点开头的目标值路径。好好体会吧!
九、DFS深度优先遍历搜索问题---FloodFill算法
FloodFill 算法最直接的⼀个应⽤就是「颜⾊填充」 , 就是Windows 绘画本中那个⼩油漆桶的标志, 可以把⼀块被圈起来的区域全部
染⾊
问题抽象成⼀个⼆维矩阵(图⽚其实就是像素点矩阵) ,然后从某个点开始向四周扩展, 直到⽆法再扩展为⽌。
对于⼀个坐标, ⼀定会被上下左右的坐标搜索。 被重复搜索时, 必须保证递归函数能够能正确地退出, 否则就会陷⼊死循环
一般引入辅助数组,相当于使⽤⼀个特殊值 -1 代替 visited 数组的作⽤, 达到不⾛回头路的效果。
矩阵中的路径
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。
路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。
如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。
注意:
- 输入的路径不为空;
- 所有出现的字符均为大写英文字母;
样例
matrix=
[
["A","B","C","E"],
["S","F","C","S"],
["A","D","E","E"]
]
str="BCCE" , return "true"
str="ASAE" , return "false"
class Solution(object):
def hasPath(self, matrix, string):
"""
:type matrix: List[List[str]]
:type string: str
:rtype: bool
"""
def dfs(matrix,string,u,x,y):
if matrix[x][y] != string[u]:
return False
if u==len(string)-1:
return True
res=matrix[x][y]
matrix[x][y]='*'
dx=[-1,0,1,0];dy=[0,-1,0,1]
for i in range(4):
a=x+dx[i]
b=y+dy[i]
if a>=0 and a<len(matrix) and b>=0 and b<len(matrix[0]):
if dfs(matrix,string,u+1,a,b):
return True
matrix[x][y] = res
return False
for i in range(len(matrix)):
for j in range(len(matrix[0])):
if dfs(matrix,string,0,i,j):
return True
return False
'''
#两个位置的Bug,首先是matrix[x][y] != string[u] 递归出口判断的是string[u]位置上的值,
不是u索引
第二处是dfs内部调用的时候是参入参数是a,b;而不是x,y
'''
机器人的运动范围
地上有一个 mm 行和 nn 列的方格,横纵坐标范围分别是 0∼m−10∼m−1 和 0∼n−10∼n−1。
一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格。
但是不能进入行坐标和列坐标的数位之和大于 k的格子。
请问该机器人能够达到多少个格子?
样例1
输入:k=7, m=4, n=5
输出:20
样例2
输入:k=18, m=40, n=40
输出:1484
解释:当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。
但是,它不能进入方格(35,38),因为3+5+3+8 = 19。
class Solution(object):
def movingCount(self, threshold, rows, cols):
"""
:type threshold: int
:type rows: int
:type cols: int
:rtype: int
"""
# if threshold<1 or rows<0 or cols<0:
# return 0
visited = [[1]*cols for _ in range(rows)]
return self.findgrid(threshold,rows, cols,visited,0,0)
def judge(self,threshold,i,j):
if sum(map(int,str(i)+str(j)))<=threshold:
return True
else:
return False
def findgrid(self,threshold,rows, cols,visited,i,j):
count=0
if i>=0 and i<rows and j>=0 and j<cols and self.judge(threshold,i,j) and visited[i][j]:
visited[i][j] = 0
count =1+self.findgrid(threshold,rows, cols,visited,i-1,j)\
+self.findgrid(threshold,rows, cols,visited,i,j-1)\
+self.findgrid(threshold,rows, cols,visited,i+1,j)\
+self.findgrid(threshold,rows, cols,visited,i,j+1)
return count
'''
首先是辅助函数的传入参数中需要加入visited数组,记录过程中状态的变化
其次是数组初始化
判断过程中用到了python里自带的map函数,可以将字符串按数位匹配为int类型
'''
岛屿问题的一般思路:DFS从树到图的遍历的过渡,包括四叉树的遍历+边界条件+回溯标记
基本模板如下:
void dfs(int[][] grid, int r, int c) {
// 判断 base case
if (!inArea(grid, r, c)) {
return;
}
// 如果这个格子不是岛屿,直接返回
if (grid[r][c] != 1) {
return;
}
grid[r][c] = 2; // 将格子标记为「已遍历过」
// 访问上、下、左、右四个相邻结点
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}