脑筋急转弯

本文介绍了多个LeetCode中的经典算法问题,如两数之和、二进制字符串操作、颜色分类、两数相除、随机集合操作、到达终点数字、分汤问题以及交替二进制字符串生成。通过详细解析代码实现,展示了如何运用数学思维和数据结构高效解决这些问题。同时,文章探讨了动态规划和记忆化搜索在优化算法中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 两数之和

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        d = defaultdict(int)
        for i, x in enumerate(nums):
            y = target - x
            if y in d: return [d[y], i]
            d[x] = i

2380. 二进制字符串重新安排顺序需要的时间

class Solution:
    def secondsToRemoveOccurrences(self, s: str) -> int:        
        t = cnt = 0 
        for c in s:
            if c == '0': cnt += 1
            elif cnt: t = max(t + 1, cnt) # 对于每一个 1 前面有 cnt 个 0 至少需要移动(或反转) cnt 次。
        return t
      # 001011
        ans = 0
        while '01' in s:
            ans += 1
            s = s.replace('01', '10')
        return ans

75. 颜色分类

class Solution:
    def sortColors(self, nums: List[int]) -> None:
    
        def f(i, j):
            nums[i], nums[j] = nums[j], nums[i]

        # i 跟 0 后, j 跟 1 后
        i = j = 0
        for k, x in enumerate(nums):
            if x == 1:
                f(j, k) # j 跟踪 1 快指针
                j += 1
            elif x == 0:
                f(i, k) # i 跟踪 0 慢指针
                if i < j: # 0, 1 交换,还需要 1, 2 交换
                    f(j, k)                                
                i += 1
                j += 1

29. 两数相除

class Solution:
    def divide(self, dividend: int, divisor: int) -> int:
        if dividend == -2147483648 and divisor == -1: # -2 ** 31 / -1 = 2147483648 溢出处理
            return 2147483647
        a, b, ans = abs(dividend), abs(divisor), 0
        for i in range(31, -1, -1):
            # 从 b 的最大倍数 2 ^ i 开始尝试。
            # b * 2^i <= a 换句话说 a/b = 2^i + (a-2^i*b)/b
            if (b << i) <= a:
                ans += 1 << i # 2 ** i
                a -= b << i
        return -ans if (dividend > 0) ^ (divisor > 0) else ans

380. O(1) 时间插入、删除和获取随机元素

变长数组可以在 O(1) 的时间内(通过下标)完成获取随机元素操作,但无法在 O(1) 的时间内判断元素是否存在,因此不能在 O(1) 的时间内完成插入和删除操作。哈希表可以在 O(1) 的时间内完成插入和删除操作,但无法根据下标定位到特定元素,因此不能在 O(1) 的时间内完成获取随机元素操作。结合变长数组和哈希表,变长数组中存储元素,哈希表中存储每个元素在变长数组中的下标。

class RandomizedSet:
    def __init__(self):
        self.q = []
        self.d = {}

    def insert(self, val: int) -> bool:
        if val in self.d: return False
        self.d[val] = len(self.q)
        self.q.append(val)
        return True

    def remove(self, val: int) -> bool:
        if val not in self.d: return False
        i = self.d[val]
        self.q[i] = self.q[-1] # 用末尾元素替换要删除的元素
        self.d[self.q[i]] = i # 加入字典
        self.q.pop() # 列表中删除
        del self.d[val] # 字典中删除
        return True

    def getRandom(self) -> int:
        return choice(self.q)

754. 到达终点数字

class Solution:
    def reachNumber(self, target: int) -> int:
        # 先假设 target > 0,如果不回头地往终点走 n 步,并恰好能走到终点,那么答案就是 n。
        # 如果 target < 0,把每一步取反,所以只需考虑大于 0 的情况。
        target = abs(target)
        s = i = 0
        # 超过终点,如果相距为偶数,则前面该偶数的一半时取反;如果是奇数再继续相加后的数一定会变成相距为偶数。
        while s < target or (s - target) % 2:  
            i += 1
            s += i
        return i

808. 分汤

假设 dp[a][b] 为 A 剩下 a 份, B 剩下 b 份时,求的 A 先分完的概率 + A 和 B 同时分完的概率 / 2 (也可以理解为对答案的贡献)

因为存在 4 种分配操作,那么当前情况下要求的概率就等于当前情况进行 4 种分配后得到的概率之和除以 4

dp[a][b] =(  dp[a - 100][b]  + dp[a - 75][b - 25] + dp[a - 50][b - 50] + dp[a - 25][b - 75] ) / 4

dp[n][n] 为答案
边界条件
dp[0][0] 代表着 A 和 B 都剩下 0 份,也就是同时分完的情况
注意 : 这种情况 A 先分完的概率为 0 ,同时分完的概率为 1
对答案的贡献为 0 + 1/2 也就是 0.5
dp[0][0] = 0.5
dp[0][y]( y≠0 y ≤ n )代表着 A 剩下 0 份时 B 还没分完的情况
这种情况 A 先分完的概率为 1 ,同时分完的概率为 0
对答案的贡献为 1 + 0/2 也就是 1
dp[0][y] = 1
dp[x][0]( x≠0 x ≤ n )代表着 A 剩下 x 份时 B 已经分完的情况
这种情况 A 先分完的概率为 0 ,同时分完的概率为 0
对答案的贡献为 0 + 0/2 也就是 0
dp[x][0] = 0

优化
因为 4 种分配操作都是等概率的,所以在一次分配中
A 平均被分出 E(A)=(4+3+2+1)/4= 2.5 份
B 平均被分出 E(B)=(0+1+2+3)/4= 1.5 份

所以 n 越大时 A 先分完的概率越接近 1 ,也就是 dp[n][n] 越接近 1
因为题目给出误差可为 10^-5 ,也就是说当答案大于 0.99999 时可以直接返回 1

public class Solution{
    public static void main(String[] args){
        for(int i = 500; i < 5000; i++){
            if(soupServings(i) > 0.99999){
                System.out.println(i);
                System.out.println(soupServings(i));
                break;
            }
        }
    }
}

运行结果为

4451
0.9999902113072546

所以当输入大于 4450 时,直接返回 1

public class Solution{
    public static double soupServings(int n)    {
        if(n > 4450) return 1;
        n = (int) Math.ceil(n / 25d);        
        double[][] dp = new double[n + 1][n + 1];        
        dp[0][0] = 0.5;        
        for(int i = 1; i <= n; i++){
            dp[0][i] = 1;
        }
        
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= n; j++){
                dp[i][j] = 0.25 * (dp[Math.max(0, i - 4)][j] + dp[Math.max(0, i - 3)][Math.max(0, j - 1)] + dp[Math.max(0, i - 2)][Math.max(0, j - 2)] + dp[Math.max(0, i - 1)][Math.max(0, j - 3)]);
            }
        }        
        return dp[n][n];
    }
}

方法二 : 动态规划(自顶向下)(优化)

在方法一中,对 dp 矩阵中的每个元素都进行了计算
但这实际上很多元素对于最终的答案都是无用的

那么自顶向下从 dp[n][n] 开始递推,就可以避免计算这些无用的值
首先,出现 dp[n][n] 的概率为 1 (因为我们从 dp[n][n] 开始)
dp[n][n] 进行 4 种分配后,每种情况对于开始时出现的概率都为 dp[n][n] / 4
再一次进行分配后依然如此
所以状态转移方程为

dp[a - 4][b]     = dp[a][b] / 4
dp[a - 3][b - 1] = dp[a][b] / 4
dp[a - 2][b - 2] = dp[a][b] / 4
dp[a - 1][b - 3] = dp[a][b] / 4

最后,由于要求 A 先分完的概率 + A 和 B 同时分完的概率 / 2
A 和 B 同时分完的概率等于 dp[0][0]
A 先分完的概率等于 dp[0][y]( y≠0 y ≤ n ) 之和
根据要求计算结果即可

public class Solution{
    public static double soupServings(int n){
        if(n > 4450) return 1;        
        n = (int) Math.ceil(n / 25d);        
        double[][] dp = new double[n + 1][n + 1];        
        dp[n][n] = 1;        
        double temp;        
        for(int i = n; i > 0; i--){
            for(int j = n; j > 0; j--){
                if(dp[i][j] == 0) continue;                
                temp = 0.25 * dp[i][j];                
                dp[Math.max(0, i - 4)][j] += temp;
                dp[Math.max(0, i - 3)][Math.max(0, j - 1)] += temp;
                dp[Math.max(0, i - 2)][Math.max(0, j - 2)] += temp;
                dp[Math.max(0, i - 1)][Math.max(0, j - 3)] += temp;
            }
        }
        
        dp[0][0] /= 2;        
        for(int i = 1; i <= n; i++)
        	dp[0][0] += dp[0][i];
        return dp[0][0];
    }
}

方法三 : 记忆化dfs(自顶向下)
用深度优先搜索进行遍历,并记忆化以避免重复计算

public class Solution{
    static double[][] dp;    
    public static double soupServings(int n){
        if(n > 4450) return 1;
        n = (int) Math.ceil(n / 25d);        
        dp = new double[n + 1][n + 1];        
        return dfs(n, n);
    }
    
    public static double dfs(int a, int b){
        if(a <= 0 && b <= 0) return 0.5;
        else if(a <= 0) return 1;
        else if(b <= 0) return 0;        
        if(dp[a][b] == 0)
        	dp[a][b] = 0.25 * (dfs(a - 4, b) + dfs(a - 3, b - 1) + dfs(a - 2, b - 2) + dfs(a - 1, b - 3));       
        return dp[a][b];
    }
}

作者:joneli
链接:https://leetcode.cn/problems/soup-servings/solution/by-joneli-ts7a/

1758. 生成交替二进制字符串的最少操作数

class Solution:
    def minOperations(self, s: str) -> int:
        '''
        a, b = 0, 0
        for i, c in enumerate(s):
            x = int(c)
            a += x ^ (i & 1) # 偶数 1 + 奇数 0
            b += x ^ 1 ^ (i & 1) # 偶数 0 + 奇数 1
        
        return min(a, b);
        '''
        a = b = flag = 0
        for c in s:
            if int(c) == flag:
                a += 1
            else:
                b += 1
            flag ^= 1
        return min(a, b)   

2549. 统计桌面上的不同数字

class Solution {
    public int distinctIntegers(int n) {
        // n 在桌面上, 下一轮 n - 1 就在桌面了。
        return n == 1 ? 1 : n - 1;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值