股神暴跌 99.97%,纽交所草台班子的一面

文章讨论了比亚迪的招聘政策倾向于录用高学历员工,导致可能的职级倒挂现象。作者通过编程题目(LeetCode1222)展示了如何在棋盘游戏中模拟皇后的攻击路径,暗示了在实际工作中解决问题的逻辑。
摘要由CSDN通过智能技术生成

股神"暴跌"?

北京时间6月3日21点50分左右,美股开盘后出现「技术性问题」,多只股票因巨幅波动停牌。

alt

其中巴菲特老爷子的「伯克希尔哈撒韦 A」暴跌 99.97%,直接从每股 62.57W 美刀跌至 185 美刀。

更具魔幻的是,在「打到最低价」到「触发停牌」的瞬间,有 270 股成交了。

alt

由于时间窗口只有瞬间,所以必然不是人能反应过来的,大致是一些包含「止损」功能和包含「抄底」功能的量化之间倒了一手。

但无论如何,这些交易的卖手,加起来已经亏损 1.6 亿美元。

目前最新的消息是:纽交所与其他非上市交易特权(UTP)交易所一起,裁定撤销美国东部时间上午 9:50-9:51 期间,伯克希尔哈撒韦所有价格为 603718.30 美元或以下、与 CTA SIP 问题相关的错误交易。

开盘价 62W 美刀,裁定成交记录在 60W 刀以下的交易无效。

也就是量化凭本事赚到的小钱,纽交所承认了,但其余的泼天富贵统统收回。

收回也没事,抄到技术故障大底的同学,至少收获了一顿空欢喜。

对此,大家怎么看?自己是否有过类似的空欢喜经历呢?

...

回归主线。

来一道和「腾讯」相关的算法题。

题目描述

平台:LeetCode

题号:698

给定一个整数数组  nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。

示例 1:

输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4

输出: True

说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。

示例 2:

输入: nums = [1,2,3,4], k = 3

输出: false

提示:

  • 每个元素的频率在 范围内

搜索 + 剪枝(贪心策略)

n 个数均分成 k 份,且 nk 数据范围为 16,容易想到搜索剪枝。

先对一些显然的无解情况进行分析:记 「若 不为 的倍数,必然是无法实现均分,直接返回 false(可行性剪枝)」

同时可知单个集合的总和为

设计搜索函数为 boolean dfs(int idx, int cur, int cnt, boolean[] vis),各项参数含义如下:

  • cur 为当前集合的元素和(初始值为 );
  • cnt 是已凑成多少个总和为 的集合(初始值为 ,当 时,我们搜索到了合法方案,返回 true,否则对 cnt 进行加一操作,并将 cur 置零,搜索下个集合);
  • vis 用于记录哪些 已被使用;
  • idx 是搜索关键,其含义为搜索空间的分割点。即对于当前正在搜索的集合,我们不会每次都扫描整个 nums 来找添加到该集合的下一个元素,而是能够明确下一个元素必然在 的左边或右边。 具体的,我们知道若最终存在合法方案,必然每个 都均棣属于某个具体集合。我们考虑搜索某个集合的组成元素时,按照「从大到小」的方式进行搜索(起始先对 nums 进行排序),这样能够 「确保若上一个被加入该集合的元素为 ,则下一个被添加的元素 必然位于 的左边,即从下标 开始往前搜索(顺序性剪枝)」; 同时,也正是我们按照「从大到小」的方式进行搜索, 「确保了当前集合的搜索,无须对已搜索到的集合进行调整」。 也就是说我们搜索的第一个集合是所有 中的最大值所在的那个集合;二次搜索是所有 减去第一个集合后剩余元素中最大值所在的集合 ... 这引导我们, 「如果当前集合如果连第一个值都无法搜到(即剩余元素的最大值不能作为当前集合的元素),必然无解(可行性剪枝)」

这样的「搜索 + 剪枝」的解法本质是利用了「贪心」来做策略:「我们每个回合的搜索总是在搜索「剩余未使用元素的最大值」所在的那个集合,并且按照「优先使用大数值」的原则来构造。」

可证明该做法的正确性:由于搜索的是「剩余未使用元素的最大值」所在的那个集合,因此剩余未使用元素必然在集合内,若被搜索到的其余元素参与集合构造导致有解变无解(即需要将其余元素进行替换才能确保有解),根据我们「从大到小」搜索下一个元素原则,替换过程必然不会使集合元素个数变少,即总是会拿不少于 K 个的元素来替换当前集合的 K 个元素(总和相同),从而可推断该替换并非必须。

Java 代码:

class Solution {
    int[] nums;
    int n, t, k;
    public boolean canPartitionKSubsets(int[] _nums, int _k) {
        nums = _nums; k = _k;
        int tot = 0;
        for (int x : nums) tot += x;
        if (tot % k != 0return false// 可行性剪枝
        Arrays.sort(nums);
        n = nums.length; t = tot / k;
        return dfs(n - 100new boolean[n]);
    }
    boolean dfs(int idx, int cur, int cnt, boolean[] vis) {
        if (cnt == k) return true;
        if (cur == t) return dfs(n - 10, cnt + 1, vis);
        for (int i = idx; i >= 0; i--) {  // 顺序性剪枝
            if (vis[i] || cur + nums[i] > t) continue;  // 可行性剪枝
            vis[i] = true;
            if (dfs(i - 1, cur + nums[i], cnt, vis)) return true;
            vis[i] = false;
            if (cur == 0return false// 可行性剪枝
        }
        return false;
    }
}

C++ 代码:

class Solution {
public:
    vector<int> nums;
    int n, t, k;
    bool dfs(int idx, int cur, int cnt, vector<bool>& vis) {
        if (cnt == k) return true;
        if (cur == t) return dfs(n - 10, cnt + 1, vis);
        for (int i = idx; i >= 0; i--) {
            if (vis[i] || cur + nums[i] > t) continue;
            vis[i] = true;
            if (dfs(i - 1, cur + nums[i], cnt, vis)) return true;
            vis[i] = false;
            if (cur == 0return false;
        }
        return false;
    }
    bool canPartitionKSubsets(vector<int>& _nums, int _k) {
        nums = _nums; k = _k;
        int tot = 0;
        for (int x : nums) tot += x;
        if (tot % k != 0return false;
        sort(nums.begin(), nums.end());
        n = nums.size(); t = tot / k;
        vector<boolvis(n, false);
        return dfs(n - 100, vis);
    }
};

Python 代码:

class Solution:
    def canPartitionKSubsets(self, _nums, _k):
        nums, k = _nums, _k
        tot = sum(nums)
        if tot % k != 0:
            return False
        nums.sort()
        n, t = len(nums), tot // k
        def dfs(idx, cur, cnt, vis):
            if cnt == k:
                return True
            if cur == t:
                return dfs(n - 10, cnt + 1, vis)
            for i in range(idx, -1-1):
                if vis[i] or cur + nums[i] > t:
                    continue
                vis[i] = True
                if dfs(i - 1, cur + nums[i], cnt, vis):
                    return True
                vis[i] = False
                if cur == 0:
                    return False
            return False
        return dfs(n - 100, [False] * n)

TypeScript 代码:

let nums: number[];
let n: number, t: number, k: number;
function canPartitionKSubsets(_nums: number[], _k: number): boolean {
    nums = _nums; k = _k;
    let tot = 0
    for (let x of nums) tot += x
    if (tot % k != 0return false
    nums.sort((a,b)=>a-b)
    n = nums.length; t = tot / k
    return dfs(n - 100new Array<boolean>(n).fill(false))
};
function dfs(idx: number, cur: number, cnt: number, vis: boolean[]): boolean {
    if (cnt == k) return true
    if (cur == t) return dfs(n - 10, cnt + 1, vis)
    for (let i = idx; i >= 0; i--) {
        if (vis[i] || cur + nums[i] > t) continue
        vis[i] = true
        if (dfs(idx - 1, cur + nums[i], cnt, vis)) return true
        vis[i] = false
        if (cur == 0return false
    }
    return false
}
  • 时间复杂度:爆搜剪枝不分析时空复杂度
  • 空间复杂度:爆搜剪枝不分析时空复杂度

模拟退火

数据范围为 16 自然也是很好的调参运用题。

「因为将 个数划分为 份,等效于用 个数构造出一个「特定排列」,然后对「特定排列」进行固定模式的构造逻辑,就能实现「答案」与「目标排列」的对应关系。」

基于此,我们可以使用「模拟退火」进行求解。

单次迭代的基本流程:

  1. 随机选择两个下标,计算「交换下标元素前对应序列的得分」&「交换下标元素后对应序列的得分」
  2. 如果温度下降(交换后的序列更优),进入下一次迭代
  3. 如果温度上升(交换前的序列更优),以「一定的概率」恢复现场(再交换回来)

我们可以制定如下规则来衡量当前排列与合法排列的差异程度:将当前的 nums 从前往后划分成总和不超过 tk 份,并将 k 份与 t 的差值之和,作为对当前排列的差异程度评级 diff,当出现 diff = 0 说明找到了合法排列,可结束搜索。

该计算规则可确保排列变化的连续性能有效体现在 diff 数值上。

Java 代码(2024/06/01 可通过):

class Solution {
    int[] nums;
    int n, tval, k;
    Random random = new Random(20220920);
    double hi = 1e9, lo = 1e-4, fa = 0.95;
    int N = 600;
    boolean ans;
    int calc() {
        int diff = tval * k;
        for (int i = 0, j = 0; i < n && j < k; j++) {
            int cur = 0;
            while (i < n && cur + nums[i] <= tval) cur += nums[i++];
            diff -= cur;
        }
        if (diff == 0) ans = true;
        return diff;
    }
    void sa() {
        shuffle(nums);
        for (double t = hi; t > lo && !ans; t *= fa) {
            int a = random.nextInt(n), b = random.nextInt(n);
            if (a == b) continue;
            int prev = calc();
            swap(nums, a, b);
            int cur = calc();
            int diff = cur - prev;
            if (Math.log(diff / t) > random.nextDouble()) swap(nums, a, b);
        }
    }
    public boolean canPartitionKSubsets(int[] _nums, int _k) {
        nums = _nums; k = _k;
        int tot = 0;
        for (int x : nums) tot += x;
        if (tot % k != 0return false;
        n = nums.length; tval = tot / k;
        while (!ans && N-- > 0) sa();
        return ans;
    }
    void shuffle(int[] nums) {
        for (int i = n; i > 0; i--) swap(nums, random.nextInt(i), i - 1);
    }
    void swap(int[] nums, int a, int b) {
        int c = nums[a];
        nums[a] = nums[b];
        nums[b] = c;
    }
}

TypeScript 代码(2024/06/01 可通过):

let nums: number[];
let n: number, tval: number, k: number
let ans: boolean
let hi: number, lo: number, fa: number, N: number
function getRandom(max: number): number {
    return Math.floor(Math.random() * (max + 1))
}
function calc(): number {
    let diff = tval * k
    for (let i = 0, j = 0; i < n && j < k; j++) {
        let cur = 0
        while (i < n && cur + nums[i] <= tval) cur += nums[i++]
        diff -= cur
    }
    if (diff == 0) ans = true
    return diff
}
function sa(): void {
    shuffle(nums)
    for (let t = hi; t > lo && !ans; t *= fa) {
        const a = getRandom(n - 1), b = getRandom(n - 1)
        if (a == b) continue
        const prev = calc()
        swap(nums, a, b)
        const cur = calc()
        const diff = cur - prev
        if (Math.log(diff / t) > Math.random()) swap(nums, a, b)
    }
}
function canPartitionKSubsets(_nums: number[], _k: number): boolean {
    nums = _nums; k = _k
    let tot = 0
    for (let x of nums) tot += x
    if (tot % k != 0return false
    n = nums.length; tval = tot / k
    hi = 1e7; lo = 1e-4; fa = 0.977; N = 420;
    ans = false
    while (!ans && N-- > 0) sa()
    return ans
}
function shuffle(nums: number[]): void {
    for (let i = n; i > 0; i--) swap(nums, getRandom(i), i - 1)
}
function swap(nums: number[], a: number, b: number): void {
    const c = nums[a]
    nums[a] = nums[b]
    nums[b] = c
}
  • 时间复杂度:启发式搜索不分析时空复杂度
  • 空间复杂度:启发式搜索不分析时空复杂度

最后

给大伙通知一下 📢 :

全网最低价 LeetCode 会员目前仍可用 ~

📅 年度:有效期加赠两个月!!; 季度:有效期加赠两周!!

🧧 年度:获 66.66!!; 季度:获 22.22!!

🎁 年度:参与当月丰厚专属实物抽奖(中奖率 > 30%)!!

专属链接:leetcode.cn/premium/?promoChannel=acoier

我是宫水三叶,每天都会分享算法知识,并和大家聊聊近期的所见所闻。

欢迎关注,明天见。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值