OpenClaw 最近实在太火了。
它究竟有多火?不仅体现在仅用不到三个月时间,就在 GitHub 上力压 React,成为软件类项目 Star 历史第一(要知道,React 的这个地位是靠长达13年的积累才获得的)。更直观的体现是,上周腾讯宣布提供现场安装 OpenClaw 服务时,那堪称“疯狂”的场面。

现场聚集了数千人,涵盖各行各业和各个年龄段。这还是个工作日!更离谱的是,腾讯事前并没有做太多预热宣传。即便派出了大量工程师协助,依然供不应求,连马化腾都在朋友圈感叹“没想到这么火”。

活动当天,有些声音质疑腾讯,认为所谓的安装服务只是个幌子,最终目的是推销自家的云服务器。但我相信,真正去过现场、了解规则的人不会有这种想法。
首先,不止腾讯,阿里、字节都在其云服务官网提供了“一键安装 OpenClaw”的服务。目的当然是降低使用门槛,但过程中消耗的计算资源(Token)自然会绑定在自家的云服务上。这属于典型的“醉翁之意不在酒”,是商业上的捆绑销售。不过,这种行为属于纯商业选择,没有强迫使用,也不好做过多的评价。
而腾讯那天的活动,是支持协助大家进行本地安装的,并没有强制要求必须购买腾讯云服务。单就这一点而言,其实没什么可指责的。
反观某些没有自家云产品的大厂,为了接住 OpenClaw 这波“泼天的流量”,居然真的官方推出了收费安装服务,而且价格不菲。

就在腾讯活动的第二天,某平台联合服务商上线了 OpenClaw 远程部署服务:不含聊天应用配置的安装服务要价 395 元,包含聊天应用的则要 695 元。怎么说呢,你可以理解为这是在为“有使用需求且不差钱的用户”提供一种省事的渠道。但在我看来,这多少有点“啥钱都想挣”的意思。如果真心想帮用户解决问题,定价不会如此“高端”。
那么,你怎么看?你会为了用上 OpenClaw,花几百块钱请人上门或远程安装吗?欢迎大家在 云栈社区 交流看法。
聊完了行业见闻,按照惯例,来一道和“范围”、“边界”相关的算法题放松一下。这道题关于如何用最短的绳子围起所有的树,本质上是一个求解 二维凸包 的经典问题,非常考验对 计算机科学基础 中几何与算法的理解。
题目描述
平台:LeetCode
题号:587
在一个二维的花园中,有一些用 (x, y) 坐标表示的树。由于安装费用十分昂贵,你的任务是先用最短的绳子围起所有的树。只有当所有的树都被绳子包围时,花园才能围好栅栏。
你需要找到正好位于栅栏边界上的树的坐标。
示例 1:

输入: [[1,1],[2,2],[2,0],[2,4],[3,3],[4,2]]
输出: [[1,1],[2,0],[4,2],[3,3],[2,4]]
示例 2:

输入: [[1,2],[2,2],[4,2]]
输出: [[1,2],[2,2],[4,2]]
解释: 即使树都在一条直线上,你也需要先用绳子包围它们。
注意:
- 所有的树应当被围在一起。你不能剪断绳子来包围树或者把树分成一组以上。
- 输入的整数在
0 到 100 之间。
- 花园至少有一棵树。
- 所有树的坐标都是不同的。
- 输入的点没有顺序。输出顺序也没有要求。
二维凸包(Andrew 算法)
这是一道「二维凸包」的板子题。我们将使用 Andrew 算法 来求解凸包上的所有点(即围成所有点的最小周长边界)。这个算法逻辑清晰,将凸包分为「上凸壳」和「下凸壳」两部分来处理。

算法基本流程如下:
- 排序:对所有点进行双关键字排序,首先按
x 坐标升序排序,若 x 相同则按 y 坐标升序。按 x 排序是为了能确定一个扫描方向(例如从左到右),而按 y 排序则保证了在处理某些特殊情况时的正确性。
- 维护栈:使用一个栈(这里用数组模拟)来维护当前被认为是凸包边界的点(更准确地说,是维护构成边界的边)。栈中相邻的两个点代表凸包的一条边。
- 分别构建凸壳:
- 构建下凸壳:从左到右遍历排序后的点。对于每个新点
c,检查栈顶的两个点 a 和 b(b 为栈顶)构成的向量 $\vec{ab}$ 与向量 $\vec{ac}$ 的关系。如果 $\vec{ac}$ 在 $\vec{ab}$ 的顺时针方向(即叉积小于等于0),说明 b 点可能是凹陷的,需要弹出栈顶 b 点。重复此过程直到栈内元素少于2个或不再需要弹出,然后将当前点 c 入栈。
- 构建上凸壳:从右到左遍历排序后的点,逻辑与构建下凸壳完全对称。需要注意的是,下凸壳已经构建好的点不能再次使用(可以用一个布尔数组标记),并且检查栈顶元素的条件从“栈内元素不少于2个”变为“栈内元素大于下凸壳的结束位置”。
具体判断向量方向时,我们可以利用向量叉积的几何意义。下图展示了判断的逻辑:

代码实现:
我们使用数组 stk 模拟栈,tp 表示栈顶指针。注意,起点会被入栈两次(分别作为下凸壳的起点和上凸壳的终点),因此最终输出结果时,栈底和栈顶是同一个点,我们通常只取其中一个。
class Solution {
// 向量相减
int[] subtraction(int[] a, int[] b) {
return new int[]{a[0] - b[0], a[1] - b[1]};
}
// 叉乘
double cross(int[] a, int[] b) {
return a[0] * b[1] - a[1] * b[0];
}
// 计算向量ab转向向量ac过程中扫过的面积(叉积),用于判断方向
double getArea(int[] a, int[] b, int[] c) {
return cross(subtraction(b, a), subtraction(c, a));
}
public int[][] outerTrees(int[][] trees) {
// 1. 双关键字排序
Arrays.sort(trees, (a, b)->{
return a[0] != b[0] ? a[0] - b[0] : a[1] - b[1];
});
int n = trees.length, tp = 0;
int[] stk = new int[n + 10];
boolean[] vis = new boolean[n + 10];
// 2. 构建下凸壳
stk[++tp] = 0; // 起点入栈,先不标记
for (int i = 1; i < n; i++) {
int[] c = trees[i];
while (tp >= 2) {
int[] a = trees[stk[tp - 1]], b = trees[stk[tp]];
if (getArea(a, b, c) > 0) vis[stk[tp--]] = false; // ac在ab的逆时针方向,删除b点
else break;
}
stk[++tp] = i;
vis[i] = true;
}
// 3. 构建上凸壳
int size = tp; // 下凸壳的节点数
for (int i = n - 1; i >= 0; i--) {
if (vis[i]) continue; // 下凸壳的点不再考虑
int[] c = trees[i];
while (tp > size) { // 注意这里是 > size
int[] a = trees[stk[tp - 1]], b = trees[stk[tp]];
if (getArea(a, b, c) > 0) tp--; // 同理,逆时针则弹出
else break;
}
stk[++tp] = i;
// vis[i] = true; // 这里标记与否不影响结果
}
// 4. 构造结果,栈中下标从1到tp-1为凸包点(tp和0是同一个起点)
int[][] ans = new int[tp - 1][2];
for (int i = 1; i < tp; i++) ans[i - 1] = trees[stk[i]];
return ans;
}
}
- 时间复杂度:排序复杂度为 $O(n \log n)$,构建凸壳的两遍扫描复杂度为 $O(n)$。整体复杂度为 $O(n \log n)$。
- 空间复杂度:$O(n)$,用于栈和标记数组。
最后
无论是分析 OpenClaw 引发的行业现象,还是钻研一道经典的 算法题,背后都需要清晰的逻辑和扎实的功底。技术圈的动态总是精彩纷呈,而底层的 算法与数据结构 知识则是我们理解和创造这一切的基石。希望今天的分享,既能带给你一些行业思考,也能在算法学习上对你有所帮助。