1. 浮点二分法求解矩形分割线
1.1 问题背景与核心思路
这个算法问题要求我们在二维平面上找到一条水平分割线,使得该线两侧的矩形面积之和尽可能接近。具体来说,给定一组矩形(每个矩形由其左下角坐标和边长确定),我们需要找到一个y坐标值,使得所有矩形在这个y值以下的部分面积之和,与y值以上部分面积之和的差值最小。
浮点二分法是解决这类问题的经典方法。与整数二分不同,浮点二分通过设置精度阈值来控制循环终止条件。在这个实现中,我们使用1e-6(即0.000001)作为精度阈值,这意味着当左右边界的差值小于这个值时,我们认为已经找到了足够精确的解。
1.2 算法实现细节
cpp复制constexpr double F = 1E-6; // 精度阈值
using ll = long long;
class Solution {
public:
double separateSquares(vector<vector<int>>& a) {
ll sums = 0;
ll mx = 0;
// 计算总面积和最大y坐标
for(auto& v : a) {
sums += 1LL * v[2] * v[2];
mx = max(mx,1LL * (v[1] + v[2]));
}
auto check = [&](double mid) -> bool {
double cur = 0;
for(auto&v : a) {
if(v[1]>=mid) continue;
cur += 1.0 * v[2] * min(mid-v[1]*1.0,1.0*v[2]);
}
return cur >= sums - cur;
};
double l = 0, r = mx * 1.0;
while(r - l > F) {
double mid = (l + r) * 1.0 / 2;
if(check(mid))
r = mid-F; // 下值大,向下压线
else
l = mid+F;
}
return (l + r) * 1.0 / 2;
}
};
1.3 关键点解析
-
精度控制:浮点二分不像整数二分那样通过边界交叉终止,而是通过设置精度阈值F=1e-6来控制循环终止条件。当r-l ≤ F时,循环结束。
-
check函数设计:check函数计算当前分割线mid以下所有矩形的面积之和。如果这个面积大于等于总面积的一半,说明分割线需要下移(r = mid-F),否则上移(l = mid+F)。
-
加速技巧:在调整边界时,我们不是简单地将边界设为mid,而是加了小偏移量F(r = mid-F或l = mid+F),这可以加速收敛过程。
注意:浮点运算存在精度问题,直接比较浮点数是否相等可能不可靠。因此,我们总是使用差值比较(r-l > F)来判断循环终止条件。
2. 二叉搜索树转循环双向链表
2.1 问题描述与解法思路
这个问题要求我们将一棵二叉搜索树转换为一个循环双向链表,并且链表中的节点仍然保持有序(即中序遍历顺序)。转换后的链表首尾需要相连形成循环结构。
核心思路是利用中序遍历的特性,在遍历过程中维护一个前驱指针pre,将当前节点的left指向pre,同时让pre的right指向当前节点,从而构建双向链表。最后将首尾节点相连形成循环。
2.2 算法实现与解析
cpp复制class Solution {
public:
Node* treeToDoublyList(Node* root) {
if (!root) return nullptr;
Node* ret = nullptr;
Node* pre = nullptr;
int mn = INT_MAX;
auto dfs = [&](this auto&& dfs, Node* node) {
if (!node) return;
dfs(node->left); // 中序遍历先处理左子树
if(node->val < mn) {
mn = node->val;
ret = node; // 记录最小值节点作为链表头
}
// 构建双向链表
node->left = pre;
if (pre) pre->right = node;
pre = node;
dfs(node->right);
};
dfs(root);
// 构建循环链表(首尾相连)
pre->right = ret;
ret->left = pre;
return ret;
}
};
2.3 关键步骤说明
-
中序遍历:使用递归方式进行中序遍历(左-根-右),确保节点按升序处理。
-
链表构建:在访问每个节点时:
- 将当前节点的left指向pre(前驱节点)
- 如果pre不为空,将pre的right指向当前节点
- 更新pre为当前节点
-
循环连接:遍历完成后,pre指向最后一个节点,ret指向最小节点(链表头)。将pre->right指向ret,ret->left指向pre,完成循环连接。
-
最小值记录:在中序遍历过程中,比较节点值并记录最小值节点,这个节点将作为循环链表的头节点返回。
提示:这种方法利用了二叉搜索树中序遍历有序的特性,时间复杂度为O(n),空间复杂度为O(h)(递归栈空间,h为树高)。
3. 图中两点间路径边权按位与最小值
3.1 问题描述与两种解法
给定一个带权无向图,对于每个查询(s_i, t_i),需要找到从s_i到t_i的路径,使得路径上所有边权的按位与结果最小。如果两点不连通,则返回-1。
这个问题有两种主要解法:DFS连通块法和并查集法。两种方法都需要计算连通块内所有边权的按位与值。
3.2 DFS连通块解法
cpp复制class Solution {
public:
vector<int> minimumCost(int n, vector<vector<int>>& edges, vector<vector<int>>& query) {
vector<vector<pair<int,int>>> g(n);
// 构建邻接表
for(auto& v:edges) {
g[v[0]].push_back({v[1],v[2]});
g[v[1]].push_back({v[0],v[2]});
}
vector<int> cc_and; // 存储每个连通块的按位与值
vector<int> ids(n,-1); // 记录每个节点属于哪个连通块
auto dfs = [&](auto&& dfs, int x) -> int {
ids[x] = cc_and.size();
int and_ = -1; // 初始化为全1(按位与的特性)
for(auto& [nex,val]:g[x]) {
and_ &= val;
if(ids[nex] == -1) {
and_ &= dfs(dfs,nex);
}
}
return and_;
};
// 遍历所有节点,划分连通块
for(int i=0; i<n; i++) {
if(ids[i] == -1) {
cc_and.push_back(dfs(dfs,i));
}
}
vector<int> ans;
for(auto& q:query) {
if(q[0] == q[1]) {
ans.push_back(0); // 相同节点按位与结果为0
} else {
ans.push_back(ids[q[0]]!=ids[q[1]] ? -1 : cc_and[ids[q[0]]]);
}
}
return ans;
}
};
3.3 并查集解法
cpp复制class Solution {
public:
vector<int> fa; // 并查集父节点数组
vector<int> and_; // 存储每个集合的按位与值
int find(int x) {
if(fa[x] != x) {
fa[x] = find(fa[x]);
}
return fa[x];
}
vector<int> minimumCost(int n, vector<vector<int>>& edges, vector<vector<int>>& query) {
// 初始化并查集
for(int i=0; i<n; i++) {
fa.push_back(i);
and_.push_back(-1); // 初始化为全1
}
// 处理边,合并连通块
for(auto& v:edges) {
int fx = find(v[0]);
int fy = find(v[1]);
and_[fy] &= v[2]; // 合并边权
if(fx != fy) {
and_[fy] &= and_[fx]; // 合并两个连通块的按位与值
fa[fx] = fy;
}
}
vector<int> ans;
for(auto& q:query) {
if(q[0] == q[1]) {
ans.push_back(0); // 相同节点按位与结果为0
} else {
ans.push_back(find(q[0])==find(q[1]) ? and_[find(q[0])] : -1);
}
}
return ans;
}
};
3.4 算法比较与选择
-
DFS解法:
- 需要构建图的邻接表表示
- 通过DFS遍历划分连通块
- 在DFS过程中计算连通块内所有边权的按位与值
- 时间复杂度:O(n + m)(n为节点数,m为边数)
-
并查集解法:
- 不需要显式构建图结构
- 在合并操作时维护连通块的按位与值
- 路径压缩优化后时间复杂度接近O(mα(n)),其中α(n)是反阿克曼函数
实际应用中,如果查询次数远大于边数,并查集解法通常更高效,因为预处理后每个查询可以在近似O(1)时间内完成。
4. 算法实现中的常见问题与调试技巧
4.1 浮点二分的精度问题
在实现浮点二分时,常见的陷阱包括:
-
精度设置不合理:精度过高可能导致无限循环,过低可能得不到足够精确的解。通常1e-6到1e-8是合理范围。
-
终止条件错误:避免直接比较浮点数相等,应该使用差值比较。
-
初始边界选择:确保解在初始的[l, r]范围内。在矩形分割问题中,r初始化为最大y坐标是正确的。
调试技巧:
- 打印每次迭代的l、r和mid值,观察收敛情况
- 检查check函数的逻辑是否正确
- 验证最终结果是否满足题目要求
4.2 树转链表的指针操作
双向链表构建中常见的错误:
-
前驱指针未正确更新:确保每次处理节点后都更新pre指针
-
循环连接遗漏:容易忘记最后将首尾节点相连
-
空指针访问:在访问pre->right前必须检查pre是否为空
调试技巧:
- 中序遍历打印节点,验证顺序是否正确
- 逐步检查每个节点的left和right指针
- 验证链表是否能正向和反向完整遍历
4.3 图算法的连通性处理
在图算法实现中需要注意:
-
未访问标记初始化:ids数组必须初始化为-1或其他特殊值
-
自环边处理:题目中特别处理了s_i == t_i的情况
-
按位与的初始值:应该初始化为全1(即-1),因为任何数与全1按位与都是它本身
调试技巧:
- 打印连通块划分结果
- 验证每个连通块的按位与值计算是否正确
- 检查查询处理逻辑是否覆盖所有边界情况
在实际编程竞赛或面试中,理解这些算法的核心思想并能够正确处理边界情况至关重要。建议通过大量练习来熟悉这些模式,并积累调试经验。