一、单调栈
单调栈 (Monotone Stack) 是一种栈的数据结构,它的每个节点都必须按照递增的顺序存储数据,即栈中的元素都必须按照递增的顺序排列。同时,栈顶元素必须是一个单调递增的元素,即如果栈中有两个元素,一个元素比另一个元素大,那么前者必须是栈顶元素,后者则是次栈顶元素。
单调栈的实现可以使用数组或者链表来实现。使用数组实现单调栈时,需要保证数组的下标从 0 开始,栈顶元素对应的数组下标为栈的大小减 1。在遍历栈的过程中,如果发现栈顶元素比当前元素大,则需要将栈顶元素向后移动一位,同时将次栈顶元素提到栈顶。
单调栈在排序算法中使用的比较广泛,例如快速排序中就可以使用单调栈来维护排好序的数组。此外,单调栈还可以用于解决一些递归算法的问题,例如递归求和、递归排序等。
以下是使用数组实现单调栈的 Python 代码示例:
class MonotoneStack:
def __init__(self):
self.stack = []
self.size = 0
def is_empty(self):
return self.size == 0
def push(self, value):
self.stack.append(value)
self.size += 1
def pop(self):
if self.is_empty():
raise ValueError("Stack is empty")
self.size -= 1
return self.stack.pop()
def peek(self):
if self.is_empty():
raise ValueError("Stack is empty")
return self.stack[self.size - 1]
def size(self):
return self.size
其中,MonotoneStack
类表示一个单调栈,包含栈顶元素、栈的大小等属性。push()
方法用于将元素添加到栈中,pop()
方法用于从栈中删除元素,peek()
方法用于查看栈顶元素,is_empty()
方法用于检查栈是否为空,size()
方法用于返回栈的大小。
以下是使用该代码实现一个简单的单调栈的示例:
stack = MonotoneStack()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack.peek()) # 输出 3
print(stack.pop()) # 输出 3
print(stack.peek()) # 输出 2
stack.push(4)
print(stack.peek()) # 输出 4
print(stack.pop()) # 输出 4
print(stack.peek()) # 输出 2
在这个示例中,我们首先创建一个单调栈 stack
,并向其中添加了三个元素。然后,我们使用 peek()
方法查看栈顶元素,使用 pop()
方法删除栈顶元素,并使用 peek()
方法再次查看栈顶元素。最后,我们向栈中添加了第四个元素,但此时栈已经为空,因此 peek()
方法返回的是次栈顶元素,即 2
。
应用:
单调栈是一种栈的数据结构,它的每个节点都必须按照递增的顺序存储数据,即栈中的元素都必须按照递增的顺序排列。同时,栈顶元素必须是一个单调递增的元素,即如果栈中有两个元素,一个元素比另一个元素大,那么前者必须是栈顶元素,后者则是次栈顶元素。
单调栈在计算机科学中有广泛的应用,其中一些应用包括:
-
排序算法:单调栈可以用于快速排序、归并排序、堆排序等排序算法中,用于维护排好序的数组。
-
递归算法:单调栈可以用于解决一些递归算法的问题,例如递归求和、递归排序等。
-
数据结构:单调栈可以用于实现一些数据结构,例如线段树、二叉搜索树等。
-
神经网络:单调栈可以用于神经网络的训练和预测中,用于存储神经网络的输入和输出数据,并支持神经网络的反向传播算法。
总之,单调栈是一种非常重要的数据结构,它在计算机科学中有广泛的应用,能够解决许多复杂的问题。
刷题:
1.给出项数为 n 的整数数列 a1…n。 定义函数 f(i)代表数列中第 i 个元素之后第一个大于 ai 的元素的下标。若不存在,则 f(i)=0。 试求出 f(1…n)。
题解:
我们可以使用动态规划来求解函数 f(1...n)。设 dp[i][j] 表示在前 i 个元素中,第 j 个元素之后第一个大于 ai 的元素的下标。
对于第 i 个元素 ai,它在数列中的下标为 i。因此,dp[i][0] = i。
假设在前 i-1 个元素中,第 j 个元素之后第一个大于 ai 的元素的下标为 dp[i-1][j],那么:
- 如果第 j 个元素之后的下标为 k,且 k 大于等于 i,则第 i 个元素之后的下标为 k+1。因此,dp[i][j+1] = dp[i-1][j] + 1。
- 如果第 j 个元素之后的下标为 k,且 k 小于等于 i-1,则第 i 个元素之后的下标为 k+1。因此,dp[i][j+1] = dp[i-1][j]。
因此,我们可以得到以下递推式:
dp[i][j+1] = max(dp[i-1][j+1], dp[i-1][j]) + 1
其中,max(dp[i-1][j+1], dp[i-1][j]) 表示在前 i-1 个元素中,第 j 个元素之后第一个大于 ai 的元素的下标的最大值。
最终的答案为:
f(i) = max(dp[1][i], dp[2][i], ..., dp[n][i])
其中,dp[1][i], dp[2][i], ..., dp[n][i] 分别表示在前 i-1 个元素中,第 1、2、...、n 个元素之后第一个大于 ai 的元素的下标的最大值。
需要注意的是,对于第 i 个元素 ai,它的下标为 i,因此 dp[i][j] = dp[i-1][j]。
最终,我们得到了函数 f(1...n) 的求解方法:
f(i) = max(dp[1][i], dp[2][i], ..., dp[n][i])
其中,dp[1][i], dp[2][i], ..., dp[n][i] 分别表示在前 i-1 个元素中,第 1、2、...、n 个元素之后第一个大于 ai 的元素的下标的最大值。
以下是使用 Python 语言实现的代码:
def f(ai, ai_list):
n = len(ai_list)
dp = [[0] * (n + 1) for _ in range(n + 1)]
dp[0][0] = i
for i in range(1, n + 1):
for j in range(i):
dp[i][j] = dp[i - 1][j]
if ai_list[j] > ai:
dp[i][j] = max(dp[i][j], dp[i - 1][j + 1])
dp[i][j] = max(dp[i][j], dp[i - 1][j])
return dp[n][n]
其中,ai 和 ai_list 分别表示数列 a1...n 和数列 a1...n 的前缀和数组。
函数 f(ai, ai_list) 的输入参数为数列 ai 和 ai_list,输出为函数 f(1...n) 的值。
该函数使用动态规划来求解函数 f(1...n)。具体来说,dp[i][j] 表示在前 i-1 个元素中,第 j 个元素之后第一个大于 ai 的元素的下标。
对于第 i 个元素 ai,它的下标为 i。因此,dp[i][j] = dp[i-1][j]。
递推式为:dp[i][j+1] = max(dp[i-1][j+1], dp[i-1][j]) + 1。
最终的答案为:f(i) = max(dp[1][i], dp[2][i], ..., dp[n][i])。
该函数的时间复杂度为 O(n^2),空间复杂度为 O(n)。
我们可以使用二分答案的方法优化时间复杂度。
假设我们已经找到了前 i 个元素中第 j 个元素之后第一个大于 ai 的元素的下标,即 dp[i][j],那么我们可以使用二分答案的方法找到第 i 个元素之后第一个大于 ai 的元素的下标。
具体来说,我们可以对数组 dp[1...n][1...n] 使用二分答案的方法进行排序,时间复杂度为 O(nlogn)。然后,我们可以使用二分查找来找到第 i 个元素之后第一个大于 ai 的元素的下标,时间复杂度为 O(logn)。
下面是一个使用 Python 语言实现的优化后的算法代码:
def f(ai, ai_list):
n = len(ai_list)
dp = [[0] * (n + 1) for _ in range(n + 1)]
dp[0][0] = i
for i in range(1, n + 1):
for j in range(i):
dp[i][j] = dp[i - 1][j]
if ai_list[j] > ai:
left, right = 0, n
while left <= right:
mid = (left + right) // 2
if dp[mid][j] >= dp[left][j] and dp[mid][j] >= dp[right][j]:
right = mid - 1
else:
left = mid + 1
dp[i][j] = left
return dp[n][n]
该函数的时间复杂度为 O(nlogn),空间复杂度为 O(n)。相比于动态规划的算法实现,使用二分答案的方法可以极大地提高算法的时间复杂度。