阅读视图

发现新文章,点击刷新页面。

美国12月二手房销售升至2023年以来最快水平

全美房地产经纪人协会(NAR)周三公布的数据显示,12月二手房销量增长5.1%,至折合年率435万套,创下2023年2月以来最高水平。借贷成本下降和房价涨幅放缓推动美国所有主要地区的购房活动回暖。售价中值同比上涨0.4%至40.54万美元,创下两年半以来的最小涨幅。NAR首席经济学家Lawrence Yun在声明中表示:“第四季度,市场状况开始改善,抵押贷款利率下降,房价增速放缓。”(财联社)

马斯克称特斯拉将把FSD功能转为按月订阅制

特斯拉首席执行官埃隆・马斯克于周三表示,这家电动汽车制造商将停止以固定价格售卖完全自动驾驶(监管版)软件,后续该功能将仅提供按月订阅服务。“特斯拉将于2月14日后停止售卖完全自动驾驶软件。” 马斯克在其社交平台X的一早间帖子中写道,“此后,完全自动驾驶功能将仅支持按月订阅。”(新浪财经)

汇丰据悉考虑出售新加坡保险业务,估值或超10亿美元

据知情人士透露,在行政总裁Georges Elhedery牵头全球业务重组之际,汇丰控股正在探索针对其新加坡保险业务的不同选项,包括将其出售。该行和一家财务顾问正在评估汇丰人寿(新加坡)私人有限公司,其在交易中的估值可能超过10亿美元。他们表示,其他的保险商和投资公司已经表达了初步兴趣。(新浪财经)

美国12月二手房销售升至2023年以来最快水平

全美房地产经纪人协会(NAR)周三公布的数据显示,12月二手房销量增长5.1%,至折合年率435万套,创下2023年2月以来最高水平。借贷成本下降和房价涨幅放缓推动美国所有主要地区的购房活动回暖。售价中值同比上涨0.4%至40.54万美元,创下两年半以来的最小涨幅。NAR首席经济学家Lawrence Yun在声明中表示:“第四季度,市场状况开始改善,抵押贷款利率下降,房价增速放缓。”(财联社)

美联储发布经济状况“褐皮书”

当地时间1月14日,美联储发布全国经济形势调查报告(也称“褐皮书”)。“褐皮书”显示,在12个联邦储备区中,有8个地区的整体经济活动以轻微至温和的速度增长,3个地区报告没有变化,1个地区报告温和下降。这比过去三个报告周期有所改善,此前大多数地区报告经济活动变化不大。“褐皮书”显示,对未来活动的展望略显乐观,多数地区预计未来数月将实现小幅至适度增长。(央视新闻)

马斯克旗下xAI因Grok生成色情内容遭加州司法部调查

埃隆・马斯克旗下的xAI公司目前正面临美国方面的调查,调查工作由加州州检察长罗布・邦塔牵头。此前,该公司开发的人工智能聊天机器人兼图像生成工具Grok,被曝可让用户大批量生成并发布基于真人照片的未经本人同意的露骨图像。(新浪财经)

微软对Anthropic人工智能的投入有望达到5亿美元

据一位知情人士透露,微软已悄然跻身Anthropic的顶级客户行列,近期其每年用于采购Anthropic人工智能技术、为自家产品提供支持的开支,有望达到近5亿美元。与此同时,微软正更积极地向云服务客户推广Anthropic的人工智能模型,这或将为双方带来更多营收。(新浪财经)

谷歌在Gemini中推出个人智能功能

谷歌现已开放一款全新人工智能工具的用户测试,该工具可整合来自Gmail、谷歌相册等多款应用的信息,为用户在Gemini聊天机器人中提供个性化答复。谷歌在周三发布的一篇博客中表示,这款名为个人智能的功能目前已向个人账户开放使用。(新浪财经)

美股三大指数集体收跌,携程跌超17%

36氪获悉,1月14日收盘,美股三大指数集体下跌,道指跌0.09%,纳指跌1%,标普500指数跌0.53%。大型科技股普跌,甲骨文跌超4%,亚马逊、微软、Meta跌超2%,英伟达、特斯拉、奈飞跌超1%,苹果、谷歌小幅下跌。热门中概股多数下跌,携程跌超17%,富途控股、老虎证券跌超5%,拼多多跌近4%,理想汽车、蔚来、小鹏汽车跌超2%;36氪涨超8%,哔哩哔哩涨超6%,阿里巴巴涨超1%。

拒绝 `setInterval`!手撕“死了么”生命倒计时,带你看看 60FPS 下的 Web Worker 优雅多线程

拒绝 setInterval!手撕“死了么”生命倒计时,带你看看 60FPS 下的 Web Worker 优雅多线程

摘要:还在用 setInterval 写倒计时?难怪你的 App 切到后台就“假死”。今天从“死了么”APP 的核心痛点出发,带你用 Web Worker + RAF 重构高精度计时器。拒绝时间偏差,这才是理工男对待生命的严谨态度。

image.png

写在前面:焦虑的具象化 🕒

最近朋友圈被一款叫“死了么”的 APP 刷屏了(其实就是各种 Life Countdown 类应用)。看着屏幕上那个不断跳动的数字,精确到毫秒地计算着你离“删库跑路”(划掉)——离“百年之后”还剩多少时间,确实让人有一种被时间追着砍的紧迫感。

作为一个“代码洁癖患者”,我第一时间下载体验了一下。UI 很酷,但当我把它挂在后台,刷了一会儿掘金再切回来时,发现倒计时竟然卡顿了一瞬间,然后才跳到了正确的时间。

不能忍!绝对不能忍! 😡

对于普通用户这叫“卡顿”,对于我们开发者来说,这是对 Event Loop 的亵渎!很多同学在大一学 JS 的时候,老师都教过 setInterval 做倒计时,但今天我要告诉你:在生产环境的高精度倒计时里,setInterval 就是个骗子。

今天,我们就来扒开“时间”的底裤,用 Web Worker + requestAnimationFrame 手搓一个永不偏差、丝般顺滑的生命倒计时组件。


1. 为什么 setInterval 是个“渣男”?💔

在面试的时候,如果面试官问你:“setInterval(fn, 1000) 真的是每 1000ms 执行一次吗?”

你要是敢说是,那基本就回去等通知了。

1.1 单线程的“银行柜台”悲剧

JS 的主线程就像只有一个柜台的银行。

setInterval 并不是“准时执行”,而是“准时把任务扔进排队大厅(任务队列)”。

如果柜台正在处理一个大客户(比如一段耗时的 for 循环,或者复杂的 DOM 渲染),你的定时器回调就得在后面干等。

⚠️ 高能预警:这就是著名的 Event Loop 阻塞。你以为过了 1 秒,实际可能已经过了 1.5 秒。

1.2 浏览器的“节能模式”背刺

更坑的是,为了省电,现代浏览器(Chrome/Safari)对后台标签页极其残忍。如果你的页面切到了后台,setInterval 的执行频率会被强行降频到 1 秒甚至更久

这也是为什么我切回 APP 时会看到时间“跳变”的原因——因为计时器在后台“睡着”了。


2. 破局:Web Worker —— 找个“分身”来计时 🕵️‍♂️

既然主线程(UI 线程)又忙又不靠谱,那我们就开个“外挂”。

Web Worker 允许我们在主线程之外运行脚本。它就像是银行里的 VIP 专属柜台,完全不受主线程 DOM 渲染和 UI 卡顿的影响。哪怕主线程在进行复杂的 Canvas 渲染,Worker 里的计时器依然稳如老狗。

2.1 架构设计:主仆分离

我们要实现一个优雅的架构

  1. Worker 线程:只负责“滴答”,每隔一段固定时间(比如 100ms)向主线程发一个“心跳包”。
  2. Main 线程:负责“渲染”,接收到心跳后,利用 requestAnimationFrame 更新 UI。

这种模式在游戏开发中叫 “逻辑与渲染分离” ,非常高级。😎


3. Talk is Cheap, Show me the Code 💻

我们要实现一个 Hook:useLifeCountdown

Step 1: 编写那个“不知疲倦”的 Worker

首先,创建一个 timer.worker.js。这是我们的独立时间守护者

JavaScript

// timer.worker.js

let timerId = null;
let interval = 1000;

// 监听主线程指令
self.onmessage = (e) => {
  const { action, payload } = e.data;

  if (action === 'START') {
    interval = payload || 1000;
    // 💡 即使在这里用 setInterval,由于 Worker 是独立线程
    // 它不会受主线程 UI 卡顿影响,也不会因为页面后台而轻易降频(大部分情况)
    timerId = setInterval(() => {
      // 只发送“脉冲”,不发送具体时间,减少数据传输量
      self.postMessage({ type: 'TICK' });
    }, interval);
  } else if (action === 'STOP') {
    if (timerId) {
      clearInterval(timerId);
      timerId = null;
    }
  }
};

Step 2: 主线程的“优雅”接收 (Vue3/React 通用逻辑)

这里用 TypeScript 写一个 Class 来封装,显得咱们比较专业。

TypeScript

// PreciseTimer.ts

export class PreciseTimer {
  private worker: Worker;
  private startTime: number;
  private duration: number; // 倒计时总时长(毫秒)
  private callback: (remaining: number) => void;

  constructor(duration: number, callback: (time: number) => void) {
    this.duration = duration;
    this.callback = callback;
    this.startTime = Date.now();

    // 💡 实例化 Worker (注意 Vite/Webpack 的引入方式可能不同)
    this.worker = new Worker(new URL('./timer.worker.js', import.meta.url));
    
    this.worker.onmessage = (e) => {
      if (e.data.type === 'TICK') {
        this.syncTime();
      }
    };
  }

  // 核心:基于系统时间的校准机制
  private syncTime() {
    const now = Date.now();
    // 逝去的时间
    const elapsed = now - this.startTime; 
    // 剩余时间
    const remaining = Math.max(0, this.duration - elapsed);

    // 🔥 重点:虽然 Worker 触发了 update,但我们要用 requestAnimationFrame 
    // 确保 UI 更新与屏幕刷新率同步,避免画面撕裂
    requestAnimationFrame(() => {
      this.callback(remaining);
    });

    if (remaining <= 0) {
      this.stop();
    }
  }

  public start() {
    // 告诉 Worker:每 16ms (约 60FPS) 叫我一次
    // 实际上我们可以设置得大一点,比如 50ms,因为 syncTime 会计算精准插值
    this.worker.postMessage({ action: 'START', payload: 20 }); 
  }

  public stop() {
    this.worker.postMessage({ action: 'STOP' });
    this.worker.terminate(); // 杀掉 Worker,释放内存
  }
}

💡 极客细节:

细心的同学发现了,我在 syncTime 里重新计算了 Date.now() - startTime。

为什么要这么做?

因为 Worker 的 setInterval 虽然稳定,但长期运行依然会有微小的累积误差。“时间戳差值法” 是消除误差的终极奥义——无论中间 tick 此时准不准,我每次计算的都是物理世界的绝对时间差。这就是**“无状态”**计时的精髓。


4. 视觉层:让焦虑“流动”起来 (Canvas 粒子) 🎨

有了精准的时间内核,剩下的就是皮囊了。为了致敬“死了么”,我们不用枯燥的 <div> 文字,我们用 Canvas 画一个生命进度条

(为了不占篇幅,这里只放核心渲染逻辑)

JavaScript

function drawLifeBar(ctx, percentage) {
  // 清空画布
  ctx.clearRect(0, 0, width, height);
  
  // 渐变色:从生机勃勃的绿 -> 焦虑的黄 -> 绝望的红
  const gradient = ctx.createLinearGradient(0, 0, width, 0);
  gradient.addColorStop(0, '#4ade80'); // Green
  gradient.addColorStop(0.5, '#facc15'); // Yellow
  gradient.addColorStop(1, '#ef4444'); // Red
  
  ctx.fillStyle = gradient;
  
  // 使用贝塞尔曲线画出液体的流动感
  // 这里的 offset 可以根据 performance.now() 动态变化,产生波浪效果
  ctx.beginPath();
  ctx.moveTo(0, 0);
  ctx.lineTo(width * percentage, 0);
  ctx.lineTo(width * percentage, height);
  ctx.lineTo(0, height);
  ctx.fill();
}

当 PreciseTimer 的回调触发时,我们将 remaining / total 传给这个 drawLifeBar。

你会发现,哪怕你此时在狂拖浏览器窗口,或者并在几十个 Tab 页中反复横跳,这个进度条的推进依然稳如泰山,丝滑如德芙。


总结与升华:技术之外的思考 🤔

这就是我们作为技术人对“生命倒计时”的回应。

我们用 Web Worker 对抗了浏览器的后台节流,用 时间戳差值 对抗了运行时的累积误差,用 RAF 对抗了视觉卡顿。

我们总是试图在代码里追求 0ms 的误差,追求 O(1) 的复杂度。但回到现实,我们自己人生的“倒计时”——那个最终的 clearInterval,却是无法重构的。

“死了么”APP 火爆的背后,不是因为技术多牛,而是它戳中了当代年轻人的“时间焦虑”。

所以,写完这个 Demo,我合上电脑,决定今晚不修那个该死的 Bug 了。 人生苦短,对老己好一点,出去吃个宵夜犒劳一下老己,好好休息一下(有空记得给老妈打电话)。

毕竟,代码可以回滚,人生只有一次 Commit。


互动时刻 💬

你的“生命倒计时”还剩多少?

  1. 不到 30%,求求别卷了,躺平吧 🛌
  2. 刚过 20%,扶我起来,我还能学!📚
  3. 本人:我觉得我和前端加一起还能再活 500 年!(手动狗头)

码字不易,如果你觉得这个 Worker 方案有点东西,给小老弟点个赞吧!👍


每日一题-最大化网格图中正方形空洞的面积🟡

给你一个网格图,由 n + 2 条 横线段 和 m + 2 条 竖线段 组成,一开始所有区域均为 1 x 1 的单元格。

所有线段的编号从 1 开始。

给你两个整数 n 和 m 。

同时给你两个整数数组 hBars 和 vBars 。

  • hBars 包含区间 [2, n + 1] 内 互不相同 的横线段编号。
  • vBars 包含 [2, m + 1] 内 互不相同的 竖线段编号。

如果满足以下条件之一,你可以 移除 两个数组中的部分线段:

  • 如果移除的是横线段,它必须是 hBars 中的值。
  • 如果移除的是竖线段,它必须是 vBars 中的值。

请你返回移除一些线段后(可能不移除任何线段),剩余网格图中 最大正方形 空洞的面积,正方形空洞的意思是正方形 内部 不含有任何线段。

 

示例 1:

输入:n = 2, m = 1, hBars = [2,3], vBars = [2]
输出:4
解释:左边的图是一开始的网格图。
横线编号的范围是区间 [1,4] ,竖线编号的范围是区间 [1,3] 。
可以移除的横线段为 [2,3] ,竖线段为 [2] 。
一种得到最大正方形面积的方法是移除横线段 2 和竖线段 2 。
操作后得到的网格图如右图所示。
正方形空洞面积为 4。
无法得到面积大于 4 的正方形空洞。
所以答案为 4 。

示例 2:

输入:n = 1, m = 1, hBars = [2], vBars = [2]
输出:4
解释:左边的图是一开始的网格图。
横线编号的范围是区间 [1,3] ,竖线编号的范围是区间 [1,3] 。
可以移除的横线段为 [2] ,竖线段为 [2] 。
一种得到最大正方形面积的方法是移除横线段 2 和竖线段 2 。
操作后得到的网格图如右图所示。
正方形空洞面积为 4。
无法得到面积大于 4 的正方形空洞。
所以答案为 4 。

示例 3:

输入:n = 2, m = 3, hBars = [2,3], vBars = [2,3,4]
输出:9
解释:左边的图是一开始的网格图。
横线编号的范围是区间 [1,4] ,竖线编号的范围是区间 [1,5] 。
可以移除的横线段为 [2,3] ,竖线段为 [2,3,4] 。
一种得到最大正方形面积的方法是移除横线段 2、3 和竖线段 3、4 。
操作后得到的网格图如右图所示。
正方形空洞面积为 9。
无法得到面积大于 9 的正方形空洞。
所以答案为 9 。

 

提示:

  • 1 <= n <= 109
  • 1 <= m <= 109
  • 1 <= hBars.length <= 100
  • 2 <= hBars[i] <= n + 1
  • 1 <= vBars.length <= 100
  • 2 <= vBars[i] <= m + 1
  • hBars 中的值互不相同。
  • vBars 中的值互不相同。

不排序,线性时间复杂度(Python/Java/C++/Go)

阅读理解题,难度在读题上。

贪心地,删的线段越多,面积越大,那就先把所有能删的线段都删掉,计算最大的矩形,长宽分别是多少。

取长宽的最小值,即为正方形的边长(多删的线段撤销删除)。

以 $\textit{hBars}$ 为例:

  • 不删,最长长度是 $1$。
  • 删除一条线段,最长长度是 $2$。
  • 删除两条编号相邻的线段,最长长度是 $3$。
  • 删除三条编号连续的线段(例如 $2,3,4$),最长长度是 $4$。
  • 依此类推。

所以本题要做的是,把数组排序后,求最长连续递增子数组的长度加一。

正方形的边长是长宽的最小值,其平方即为正方形的面积。

优化前

###py

class Solution:
    # 返回 a 排序后的最长连续递增子数组的长度
    def f(self, a: List[int]) -> int:
        a.sort()
        mx = cnt = 0
        for i, x in enumerate(a):
            if i > 0 and x == a[i - 1] + 1:
                cnt += 1
            else:
                cnt = 1  # 重新计数
            mx = max(mx, cnt)
        return mx

    def maximizeSquareHoleArea(self, n: int, m: int, hBars: List[int], vBars: List[int]) -> int:
        side = min(self.f(hBars), self.f(vBars)) + 1
        return side * side

###java

class Solution {
    public int maximizeSquareHoleArea(int n, int m, int[] hBars, int[] vBars) {
        int side = Math.min(f(hBars), f(vBars)) + 1;
        return side * side;
    }

    // 返回 a 排序后的最长连续递增子数组的长度
    private int f(int[] a) {
        Arrays.sort(a);
        int mx = 1;
        int cnt = 1;
        for (int i = 1; i < a.length; i++) {
            if (a[i] == a[i - 1] + 1) {
                cnt++;
                mx = Math.max(mx, cnt);
            } else {
                cnt = 1; // 重新计数
            }
        }
        return mx;
    }
}

###cpp

class Solution {
    // 返回 a 排序后的最长连续递增子数组的长度
    int f(vector<int>& a) {
        ranges::sort(a);
        int mx = 1, cnt = 1;
        for (int i = 1; i < a.size(); i++) {
            if (a[i] == a[i - 1] + 1) {
                cnt++;
                mx = max(mx, cnt);
            } else {
                cnt = 1; // 重新计数
            }
        }
        return mx;
    }

public:
    int maximizeSquareHoleArea(int, int, vector<int>& hBars, vector<int>& vBars) {
        int side = min(f(hBars), f(vBars)) + 1;
        return side * side;
    }
};

###go

// 返回 a 排序后的最长连续递增子数组的长度
func f(a []int) (mx int) {
slices.Sort(a)
cnt := 0
for i, x := range a {
if i > 0 && x == a[i-1]+1 {
cnt++
} else {
cnt = 1 // 重新计数
}
mx = max(mx, cnt)
}
return mx
}

func maximizeSquareHoleArea(_, _ int, hBars, vBars []int) int {
side := min(f(hBars), f(vBars)) + 1
return side * side
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(h\log h+v\log v)$,其中 $h$ 为 $\textit{hBars}$ 的长度,$v$ 为 $\textit{vBars}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。忽略排序的栈开销。

优化

128. 最长连续序列 的技巧优化,见 我的题解

###py

class Solution:
    # 128. 最长连续序列
    def longestConsecutive(self, nums: List[int]) -> int:
        st = set(nums)  # 把 nums 转成哈希集合
        ans = 0
        for x in st:  # 遍历哈希集合
            if x - 1 in st:  # 如果 x 不是序列的起点,直接跳过
                continue
            # x 是序列的起点
            y = x + 1
            while y in st:  # 不断查找下一个数是否在哈希集合中
                y += 1
            # 循环结束后,y-1 是最后一个在哈希集合中的数
            ans = max(ans, y - x)  # 从 x 到 y-1 一共 y-x 个数
        return ans

    def maximizeSquareHoleArea(self, n: int, m: int, hBars: List[int], vBars: List[int]) -> int:
        side = min(self.longestConsecutive(hBars), self.longestConsecutive(vBars)) + 1
        return side * side

###java

class Solution {
    public int maximizeSquareHoleArea(int n, int m, int[] hBars, int[] vBars) {
        int side = Math.min(longestConsecutive(hBars), longestConsecutive(vBars)) + 1;
        return side * side;
    }

    // 128. 最长连续序列
    private int longestConsecutive(int[] nums) {
        Set<Integer> st = new HashSet<>();
        for (int num : nums) {
            st.add(num); // 把 nums 转成哈希集合
        }

        int ans = 0;
        for (int x : st) { // 遍历哈希集合
            if (st.contains(x - 1)) { // 如果 x 不是序列的起点,直接跳过
                continue;
            }
            // x 是序列的起点
            int y = x + 1;
            while (st.contains(y)) { // 不断查找下一个数是否在哈希集合中
                y++;
            }
            // 循环结束后,y-1 是最后一个在哈希集合中的数
            ans = Math.max(ans, y - x); // 从 x 到 y-1 一共 y-x 个数
        }
        return ans;
    }
}

###cpp

class Solution {
    // 128. 最长连续序列
    int longestConsecutive(vector<int>& nums) {
        unordered_set<int> st(nums.begin(), nums.end()); // 把 nums 转成哈希集合
        int ans = 0;
        for (int x : st) { // 遍历哈希集合
            if (st.contains(x - 1)) { // 如果 x 不是序列的起点,直接跳过
                continue;
            }
            // x 是序列的起点
            int y = x + 1;
            while (st.contains(y)) { // 不断查找下一个数是否在哈希集合中
                y++;
            }
            // 循环结束后,y-1 是最后一个在哈希集合中的数
            ans = max(ans, y - x); // 从 x 到 y-1 一共 y-x 个数
        }
        return ans;
    }

public:
    int maximizeSquareHoleArea(int, int, vector<int>& hBars, vector<int>& vBars) {
        int side = min(longestConsecutive(hBars), longestConsecutive(vBars)) + 1;
        return side * side;
    }
};

###go

// 128. 最长连续序列
func longestConsecutive(nums []int) (ans int) {
has := map[int]bool{}
for _, num := range nums {
has[num] = true // 把 nums 转成哈希集合
}

for x := range has { // 遍历哈希集合
if has[x-1] { // 如果 x 不是序列的起点,直接跳过
continue
}
// x 是序列的起点
y := x + 1
for has[y] { // 不断查找下一个数是否在哈希集合中
y++
}
// 循环结束后,y-1 是最后一个在哈希集合中的数
ans = max(ans, y-x) // 从 x 到 y-1 一共 y-x 个数
}
return
}

func maximizeSquareHoleArea(_, _ int, hBars, vBars []int) int {
side := min(longestConsecutive(hBars), longestConsecutive(vBars)) + 1
return side * side
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(h+v)$,其中 $h$ 为 $\textit{hBars}$ 的长度,$v$ 为 $\textit{vBars}$ 的长度。
  • 空间复杂度:$\mathcal{O}(h+v)$。

相似题目

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

n、m参数不用考虑,只需对hBars、vBars排序遍历计算,0ms双百

Problem: 100138. 最大化网格图中正方形空洞的面积

[TOC]

思路

分析题意:

  • 抽掉一根线,则空出空间: 2

  • 若存在连续 l 根线,抽出后,空出空间为: l + 1

  • hBars.length、vBars.length 至少为1,即必有线被抽掉,至少有空间 2

因此,贪心思路来考虑:

  • 第一步、对 hBars 和 vBars 排序

  • 第二步、分别求出 hBars 和 vBars 的连续最长线段

  • 第三步、取 hBars 和 vBars 的连续最长线段两者的较小值,平方后返回

此题,n、m 两个参数可以不用到。

Code

执行用时分布0ms击败100.00%;消耗内存分布6.47MB击败100.00%

###C

int cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}
int maxlen(int* lst, int len) {
    qsort(lst, len, sizeof(int), cmp);
    int ma = 2, l = 2;
    for (int i = 0; i < len - 1; ++ i)
        if (lst[i + 1] - lst[i] == 1) { if (++ l > ma) ma = l; }
        else l = 2;
    return ma;
}
int maximizeSquareHoleArea(int n, int m, int* hBars, int hBarsSize, int* vBars, int vBarsSize) {
    int h = maxlen(hBars, hBarsSize), v = maxlen(vBars, vBarsSize);
    return h < v ? h * h : v * v;
}

###Python3

class Solution:
    def maximizeSquareHoleArea(self, n: int, m: int, hBars: List[int], vBars: List[int]) -> int:
        def maxlen(lst):
            ma, l = 2, 2
            for x, y in pairwise(sorted(lst)):
                if y - x == 1: 
                    l += 1
                    ma = max(ma, l)
                else:
                    l = 2
            return ma
        return min(maxlen(hBars), maxlen(vBars)) ** 2

您若还有不同方法,欢迎贴在评论区,一起交流探讨! ^_^

↓ 点个赞,点收藏,留个言,再划走,感谢您支持作者! ^_^

【 排序】java双百 - 枚举差值为1

Problem: 100138. 最大化网格图中正方形空洞的面积

[TOC]

1.png

解题方法

其实这个题没有看上去那么难

只需要排序之后取两个数组差值为1的最大个数即可

计算差值为一的元素个数是因为需要统计最大能切割出多大的正方形

(周赛wa三次真的要吐血了X_X)

复杂度

时间复杂度: $O(nlogn + mlogm)$ 主要取决于排序

Code

###Java

class Solution {
    public int maximizeSquareHoleArea(int n, int m, int[] hBars, int[] vBars) {
        int h = 1;
        int v = 1;
        Arrays.sort(hBars);
        Arrays.sort(vBars);
        int ht = 1;
        int vt = 1;
        for(int i = 1;i < hBars.length;i++){
            if(hBars[i] - hBars[i - 1] == 1) ht++;
            if(hBars[i] - hBars[i - 1] != 1){
                ht = 1;
            }
            h = Math.max(ht,h);
        }
        for(int i = 1;i < vBars.length;i++){
            if(vBars[i] - vBars[i - 1] == 1) vt++;
            if(vBars[i] - vBars[i - 1] != 1){
                vt = 1;
            }
            v = Math.max(vt,v);
        }
        int l = Math.min(v + 1,h + 1);
        return l * l;
    }
}

10分钟复刻爆火「死了么」App:vibe coding 实战(Expo+Supabase+MCP)

视频链接:10分钟复刻爆火「死了么」App:vibe coding 实战

仓库地址:github.com/minorcell/s…

202602

最近“死了么”App 突然爆火:内容极简——签到 + 把紧急联系人邮箱填进去。 它的产品形态很轻,但闭环很完整: 你每天打卡即可;如果你连续两天没打,系统就给紧急联系人发邮件。

恰好我最近在做 Supabase 相关调研,就顺手把它当成一次“极限验证”:

  • 我想看看:Expo + Supabase 能不能把后端彻底“抹掉”
  • 我也想看看:Codex + MCP 能不能把“建表 / 配置 / 写代码”这整套流程进一步压缩
  • 以及:vibe coding 到底能不能真的做到:跑起来、能用、闭环通

结论是:能。并且我录了全过程,从建仓库到 App 跑起来能用,全程 10 分钟

我复刻的目标:只保留“核心闭环”

我没打算做一个完整产品,只做最小闭环:

  1. 用户注册 / 登录(邮箱 + 密码 + 邮箱验证码)
  2. 首页打卡:每天只能打一次,展示“连续打卡 xx 天”
  3. 我的:查看打卡记录 / 连续天数
  4. 紧急联系人:设置一个邮箱
  5. 连续两天没打卡就发邮件(定时任务 + 邮件发送)

页面风格:简约、有活力(但不追求 UI 细节)。

技术栈:把“后端”交给 Supabase,把“体力活”交给 Agent

  • 前端:React Native + Expo(TypeScript)
  • 后端:Supabase(Auth + Postgres + RLS)
  • 自动化:Supabase Cron + Edge Functions Supabase 的定时任务本质是 pg_cron,可以跑 SQL / 调函数 / 发 HTTP 请求(包括调用 Edge Function)。(Supabase)
  • Agent:Codex(通过 Supabase MCP 直接连 Supabase) Supabase 官方有 MCP 指南,并且强调了安全最佳实践(比如 scope、权限、避免误操作)。(Supabase)

我整个过程的体验是:

以前你要在“前端 / SQL / 控制台 / 文档”之间来回切。 现在你只需要把需求写清楚,然后盯着它干活,偶尔接管一下关键配置。

两天没打卡发邮件:用 Cron + Edge Function,把事情做完

这是这个 App 最关键的“闭环”。

方案:每天跑一次定时任务

  • Cron:每天固定时间跑(比如 UTC 00:10)
  • 任务内容:找出“已经两天没打卡”的用户
  • 动作:调用 Edge Function 发邮件

Supabase 官方文档推荐的组合是:pg_cron + pg_net,定时调用 Edge Functions。(Supabase)

你也可以不调用 Edge Function,直接让 Cron 发 HTTP webhook 给你自己的服务。 但既然目标是“不写后端”,那就让 Edge Function 处理就行。

Edge Function:负责“发邮件”

注意:Supabase Auth 的邮件(验证码)是它自己的系统邮件; 你要给紧急联系人发提醒,通常需要接第三方邮件服务(Resend / SendGrid / Mailgun / SES 之类)。

Supabase 文档里也提到:定时调用函数时,敏感 token 建议放到 Supabase Vault 里。(Supabase)

Edge Function(伪代码示意):

// 1) 查数据库:哪些人超过 2 天没打卡
// 2) 取紧急联系人邮箱
// 3) 调用邮件服务 API 发送提醒

Cron 每天跑一次就够了: 这个产品的语义不是“立刻报警”,而是“连续两天都没动静”。

MCP + Codex:我觉得最爽的地方

如果你只看结果,你会觉得“这不就是一个 CRUD App 吗”。

但我觉得真正有意思的是过程:

  • 它不仅写前端代码
  • 它还能“像个人一样”去把 Supabase 后台的事情做掉:建表、加约束、开 RLS、写策略、甚至提示你哪里要手动补配置

而 Supabase MCP 的官方定位,就是让模型通过标准化工具安全地操作你的 Supabase 项目(并且强调先读安全最佳实践)。(Supabase)

我这次几乎没写代码,最大的精力消耗其实是两件事:

  1. 把提示词写清楚(尤其是“规则”和“边界条件”)
  2. 对关键点做人工复核(RLS、唯一约束、邮件配置)

我现在会怎么写提示词

我发现 vibe coding 成功率最高的提示词,不insane,反而“啰嗦”:

  • 先写“模块和流程”
  • 再写“数据约束”(每天只能一次、断档怎么处理)
  • 再写“安全策略”(RLS 怎么开)
  • 最后写“验收标准”(做到什么算跑通)

你给得越具体,它越像一个靠谱同事; 你给得越模糊,它越容易“自作主张”。

附录

我这次用的提示词(原文)

需求:使用expo和supabase开发一个移动端APP: 死了么

## 功能:

### 用户注册:

1. 描述:在app进入页面,用户需要输入邮箱和密码以及确认密码,进行注册。
2. 流程:
   - 使用supabase的auth进行校验,发送验证码注册邮箱到用户邮箱,用户需要在页面输入邮箱中的验证码。
   - 注册成功之后即可进入app首页

### 首页打卡:

1. 描述:用户进入首页,只有一个大大的打卡功能;“今日活着”,点击即可完成打卡功能
2. 流程:
   - supabase需要记录用户的打卡信息
   - 打开成功时,提示用户已经“你已连续打卡xx日,又活了一天”

### “我的”

1. 用户可以在“我的”页面查看自己的打卡记录,连续打卡时间
2. 用户可以设置紧急联系人,当检测到用户连续两天没有打卡时,会发送一封紧急联系的邮件到紧急联系人邮箱

## 其他:

1. 用户每天只能打卡一次
2. 页面简约、有活力

> 你可以使用supabase的mcp进行所有的操作,
❌