
🧑💻博主名称:鱼子星_
✅数据结构专栏:【数据结构】
✅算法竞赛专栏:【算法竞赛】
✅C++系列专栏:【C++从零开始系列】
Hi~欢迎来到鱼子星的《算·法》篇章,本文为题目解析篇,主要讲解3个算法题,涉及到算法的知识有:
- 递归
- 动态规划
- 线段树
| 题目链接 | 算法知识 |
|---|---|
| LeetCode - 236.二叉树的最近公共祖先 | 递归,栈 |
| LeetCode - 516.最长回文子序列 | 动态规划,字符串 |
| 【洛谷】P2184 贪婪大陆 | 线段树 |
一. 二叉树的最近公共祖先
题目描述
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
最近公共祖先的定义为:"对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。"
示例 1:

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
提示:
- 树中节点数目在范围 2, 1e5 内。
- -1e9 <= Node.val <= 1e9
- 所有 Node.val 互不相同 。
- p != q
- p 和 q 均存在于给定的二叉树中。
题目解析
查找最近公共祖先,首先要确定什么情况算找到了公共祖先,情况有两种:
- 当p或q为根节点时,且另外一个节点为该根节点的孩子节点。如下图所示,假设此时要找5和4的最近公共祖先。这两个节点的公共祖先只有5和5到根节点的路径上所有的节点,那么此时最近的公共祖先就是5了。

- 当p和q分别在一个节点的左右两边,该节点就是p和q的最近公共祖先。当p和q的关系不满足情况一时,就只有这种情况,此时树中只有节点5满足情况二,所以5就是6和7的最近公共祖先。

解法一:暴力查找节点+递归
因为题目中说了p和q一定存在且不相等,所以核心思路就是找上面的两种情况。我们不妨写一个查找节点的函数Find,对于当前节点,查找其左子树和右子树是否存在p和q,情况有3种:
- p和q分别在当前节点的左右子树,那么当前节点就是它们的最近公共祖先,返回该节点即可。
- p和q都在当前节点的左子树,那么最近公共祖先肯定在当前节点的左子树。我们递归去当前节点的左子树继续查找,查找的结果直接返回即可。
- p和q都在当前节点的右子树,那么最近公共祖先肯定在当前节点的右子树。递归去右子树继续查找,查找到的结果直接返回即可。
我们在代码中还可以加一句特殊的判断来应对情况一。如果当前节点为q或者p时,直接返回当前的节点。如果节点为nullptr,那也直接返回。
cpp
class Solution {
public:
bool find(TreeNode* root, TreeNode* x)
{
if(root == nullptr)
return false;
if(root == x)
return true;
return find(root->left, x) || find(root->right, x);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == nullptr || root == p || root == q)
return root;
bool qfindLeft = find(root->left, q);
bool qfindright = !qfindLeft;
bool pfindLeft = find(root->left, p);
bool pfindright = !pfindLeft;
// p和q分别在root的左右,说明root就是它们的最近公共祖先
if((qfindLeft && pfindright) || (qfindright && pfindLeft))
{
return root;
}
else if(qfindLeft && pfindLeft)
{
return lowestCommonAncestor(root->left, p, q);
}
else // qfindright != nullptr && pfindright != nullptr
{
return lowestCommonAncestor(root->right, p, q);
}
}
};
时间复杂度 : O ( n 2 ) O(n^2) O(n2)
空间复杂度 : O ( n ) O(n) O(n)
解法二:路径查找+相交链表问题
对于节点p和节点q,我们可以分别将它们到根节点的路径保存下来,于是,问题就变成了查找这两条路径的第一个相同的节点,该节点就是p和q的最近公共祖先。
如何保存p和q到根节点的路径?我们可以从根节点开始往下查找节点p和节点q,对于当前节点,如果p或者q在其子树中,那就保存当前节点,反之不保存。因为是自顶向下查找,但是需要的路径是自底向上的,所以我们使用栈(stack)来保存路径。
cpp
class Solution {
public:
bool getPath(TreeNode* root, TreeNode* x, stack<TreeNode*>& st)
{
if(root == nullptr)
return false;
//先将当前节点加入在路径中,后续再判断是否要删除
st.push(root);
if(root == x)
return true;
if(getPath(root->left, x, st))
return true;
if(getPath(root->right, x, st))
return true;
// 走到这里说明x节点不在当前节点的子树中
// 弹出当前节点
st.pop();
return false;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == nullptr)
return root;
stack<TreeNode*> qPath;
stack<TreeNode*> pPath;
// 找到从根节点到q和p节点的路径
getPath(root, q, qPath);
getPath(root, p, pPath);
// 先让两条路径保持一样的长度
while(qPath.size() != pPath.size())
{
if(qPath.size() > pPath.size())
qPath.pop();
else
pPath.pop();
}
// 直接两条路径同时删除找第一个相同的节点
while(qPath.top() != pPath.top())
{
qPath.pop();
pPath.pop();
}
return qPath.top();
}
};
时间复杂度 : O ( n ) O(n) O(n)
空间复杂度 : O ( n ) O(n) O(n)
二. 最长回文子序列
题目描述
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入 :s = "bbbab"
输出 :4
解释:一个可能的最长回文子序列为 "bbbb" 。
提示:
1 <= s.length <= 1000- s 仅由小写英文字母组成
题目解析
经验:遇到回文串 ,子序列这类关键字的题目,一般都是使用区间dp来解决。
解法一:动态规划
讲解动态规划的题目,我们一般按照动态规划的三部曲:
- 状态表示 :
dp[i][j]表示字符串s的 i , j i, j i,j区间的子串的最长回文子序列的长度 - 状态转移方程 :
s[i] == s[j],且不使用si和sj配对:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1);s[i] == s[j],且使用si和sj配对:dp[i][j] = max(dp[i][j], dp[i + 1][j - 1] + 2);
分析:si和sj配对后,形成了一对新的回文序列,且这对回文序列包裹了s[i + 1] ~ s[j - 1]中最长回文子序列。s[i] != s[j],此时只有一种情况:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1);
- 最终结果 :
dp[0][s.size() - 1];
cpp
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
vector<vector<int>> f(n, vector<int>(n, 0));
for(int i = 0; i < n; i++) f[i][i] = 1;
for(int len = 2; len <= n; len++)
{
for(int i = 0; i + len - 1 < n; i++)
{
int j = i + len - 1;
f[i][j] = max(f[i + 1][j], f[i][j - 1]);
if(s[i] == s[j])
{
f[i][j] = max(f[i][j], f[i + 1][j - 1] + 2);
}
}
}
return f[0][n - 1];
}
};
三. 贪婪大陆
题目背景
面对蚂蚁们的疯狂进攻,小 FF 的 Tower defence 宣告失败......人类被蚂蚁们逼到了 Greed Island 上的一个海湾。现在,小 FF 的后方是一望无际的大海,前方是变异了的超级蚂蚁。小 FF 还有大好前程,他可不想命丧于此, 于是他派遣手下最后一批改造 SCV 布置地雷以阻挡蚂蚁们的进攻。
题目描述
小 FF 最后一道防线是一条长度为 n n n 的战壕,小 FF 拥有无数多种地雷,而 SCV 每次可以在 L , R L, R L,R 区间埋放同一种不同于之前已经埋放的地雷。由于情况已经十万火急,小 FF 在某些时候可能会询问你在 L ′ , R ′ L',R' L′,R′ 区间内有多少种不同的地雷,他希望你能尽快的给予答复。
输入格式
第一行为两个整数 n n n 和 m m m, n n n 表示防线长度, m m m 表示 SCV 布雷次数及小 FF 询问的次数总和。
接下来有 m m m 行,每行三个整数 q , l , r q,l,r q,l,r:
- 若 q = 1 q=1 q=1,则表示 SCV 在 l , r l, r l,r 这段区间布上一种地雷;
- 若 q = 2 q=2 q=2,则表示小 FF 询问当前 l , r l, r l,r 区间总共有多少种地雷。
输出格式
对于小 FF 的每次询问,输出一个答案(单独一行),表示当前区间地雷种数。
输入输出样例 #1
输入 #1
5 4
1 1 3
2 2 5
1 2 4
2 3 5
输出 #1
1
2
说明/提示
- 对于 30 % 30\% 30% 的数据, 0 ≤ n 0 \le n 0≤n, m ≤ 1000 m \le 1000 m≤1000。
- 对于 100 % 100\% 100% 的数据, 0 ≤ n 0 \le n 0≤n, m ≤ 10 5 m \le 10^5 m≤105。
题目解析
本题就是经典的区间修改和查询的题目,可以使用线段树来解决。
解法一:线段树
使用线段树时,首先要考虑要维护区间的什么信息,这里最容易想到的就是:维护每个区间地雷的种数。但是这个思路是不行的,很容易举出反例。例如,分别在 1 , 3 , 3 , 4 1, 3,3, 4 1,3,3,4,询问 2 , 3 2, 3 2,3一共有几种地雷,这种情况下就解不出来了。
我们从最终的结果来考虑,一个区间的地雷种数其实可以由下面的公式得出:
区间[left, right]的地雷种数 = [1, right]的开始地雷的种数 − - − [1, left - 1]的结束地雷的种数
证明 :假设 1 , r i g h t 1, right 1,right的开始地雷有x种, 1 , l e f t − 1 1, left-1 1,left−1的结束地雷有y种,那x - y的地雷种数就是所有从 1 , r i g h t 1, right 1,right区间开始且没有在 1 , l e f t − 1 1, left - 1 1,left−1结束的地雷种数,这些地雷一定都和 l e f t , r i g h t left, right left,right区间有交集,所以x - y就是答案。
所以我们需要维护的区间信息为:一个区间开始地雷的种数和结束地雷的种数。代码中我们使用cnt0表示开始地雷的种数,cnt1表示结束地雷的种数。
cpp
#include<iostream>
using namespace std;
#define lc p << 1
#define rc p << 1 | 1
const int N = 1e5 + 10;
struct Node
{
int l, r, cnt[2];
}tr[N * 4];
int n, m;
void pushup(int p)
{
tr[p].cnt[0] = tr[lc].cnt[0] + tr[rc].cnt[0];
tr[p].cnt[1] = tr[lc].cnt[1] + tr[rc].cnt[1];
}
void build(int p, int l, int r)
{
tr[p] = {l, r, 0, 0};
if(l == r) return;
int mid = (l + r) >> 1;
build(lc, l, mid);
build(rc, mid + 1, r);
pushup(p);
}
void modify(int p, int x, int k)
{
int l = tr[p].l, r = tr[p].r;
if(l == r && r == x)
{
tr[p].cnt[k]++;
return;
}
int mid = (l + r) >> 1;
if(x <= mid) modify(lc, x, k);
else modify(rc, x, k);
pushup(p);
}
int query(int p, int l, int r, int k)
{
int x = tr[p].l, y = tr[p].r;
if(l <= x && r >= y)
{
return tr[p].cnt[k];
}
int mid = (x + y) >> 1;
int sum = 0;
if(l <= mid) sum += query(lc, l, r, k);
if(r > mid) sum += query(rc, l, r, k);
return sum;
}
int main()
{
cin >> n >> m;
build(1, 1, n);
while(m--)
{
int q, l, r;
cin >> q >> l >> r;
if(q == 1)
{
modify(1, l, 0);
modify(1, r, 1);
}
else
{
cout << query(1, 1, r, 0) - query(1, 1, l - 1, 1) << endl;
}
}
return 0;
}