阿翰 剑指offer 之 Day 24 数学 中等

目录

数学

1 剪绳子

1.  数学推导

​2. 贪心思想

3. 动态规划

2 和为s的连续正数序列

1. 枚举+暴力

2. 数学优化

2.2 double写法

3. 双指针

3.2 双指针+- 和比较

3 圆圈中最后剩下的数字

1. 模拟

2. 数学 


数学

1 剪绳子

剑指 Offer 14- I. 剪绳子https://leetcode-cn.com/problems/jian-sheng-zi-lcof/

1.  数学推导

PS:

推论一: 将绳子 以相等的长度等分为多段 ,得到的乘积最大。

xxx

推论二: 尽可能将绳子以长度 3 等分为多段时,乘积最大。

xxx

class Solution {
    public int cuttingRope(int n) {
        if(n <= 3) return n - 1;
        int a = n / 3, b = n % 3;
        if(b == 0) return (int)Math.pow(3, a);
        if(b == 1) return (int)Math.pow(3, a - 1) * 4;
        return (int)Math.pow(3, a) * 2;
    }
}

2. 贪心思想

 来源

贪心算法的思路是:当n大于4时,尽可能把绳子分成长度为3的小段,这样乘积最大,当n<=4时,不再剪直接乘上即可,
是有一种自上而下的思想,具体的数学证明可以参考各位大佬的题解。 
来源:力扣(LeetCode)作者:lan-ch

class Solution {
    public int cuttingRope(int n) {
        if(n == 2)return 1;
        if(n == 3)return 2;
        if(n == 4)return 4;
        int res = 1;
        while(n > 4){
            res *= 3;
            n -= 3;
        }
        return res*n;
    }
}

3. 动态规划

class Solution {
    public int cuttingRope(int n) {
        //定义dp数组,dp[i]表示长度为i的绳子剪成m端后长度的最大乘积(m>1)
        int dp[] = new int[n+1];
        //初始化
        dp[2] = 1;
        //目标:求出dp[n]
        //dp[2]已知,从dp[3]开始求,直到求出dp[n]
        for(int i = 3;i <= n;i++){
            //首先对绳子剪长度为j的一段,其中取值范围为 2 <= j < i
            for(int j = 2;j < i;j++){
                //转移方程如下
                dp[i] = Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));
                //Math.max(j*(i-j),j*dp[i-j]是由于减去第一段长度为j的绳子后,可以继续剪也可以不剪
                //Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]))是当j不同时,求出最大的dp[i]
            }
        }
        //现在已经求出每个长度i对应的最大乘积,返回dp[n]
        return dp[n];
    }
}

2 和为s的连续正数序列

剑指 Offer 57 - II. 和为s的连续正数序列https://leetcode-cn.com/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/

1. 枚举+暴力

枚举每个正整数为起点,判断以它为起点的序列和 \textit{sum}sum 是否等于 \textit{target}target 即可,由于题目要求序列长度至少大于 22,所以枚举的上界为 ⌊target​ / 2⌋。

class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> vec = new ArrayList<int[]>();
        int sum = 0, limit = (target - 1) / 2;
        for (int i = 1; i <= limit; i++) {
            for (int j = i; ; j++) {
                sum += j;
                if(sum > target)  {
                    sum = 0;
                    break;
                }else if(sum == target) {
                    int [] res = new int[j - i + 1];
//                    i --> j 
                    for (int k = i; k <= j; k++) {
                        res[k - i] = k;
                    }
                    vec.add(res);
                    sum = 0;
                    break;
                }
            }
        }
        return vec.toArray(new int[vec.size()][]);
    }
}

2. 数学优化

方法一在枚举每个正整数为起点判断的时候是暴力从起点开始累加sum 和判断是否等于 target 。但注意到,如果知道起点 x 和终点 y ,那么 x 累加到 y 的和由求和公式可以知道是(x+y)(y-x+1) / 2,那么问题就转化为了是否存在一个正整数 y(y>x) ,满足等式 (x+y)(y-x+1) / 2 = target 转化一下变成 y^2+y-x^2+x-2*target =0 这是一个关于 y 的一元二次方程,其中 a=1,b=1,c=-x^2+x-2 * target 直接套用求根公式即可 O(1) 解得 y ,判断是否整数解需要满足两个条件:

  • 判别式 b^2 −4ac 开根需要为整数
  • 最后的求根公式的分子需要为偶数,因为分母为 2 
class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> vec = new ArrayList<int[]>();
        int sum = 0, limit = (target - 1) / 2; // (target - 1) / 2 等效于 target / 2 下取整
        for (int x = 1; x <= limit; ++x) {
            long delta = 1 - 4 * (x - (long) x * x - 2 * target);// b^2 - 4ac
            if (delta < 0) {
                continue;
            }
            int delta_sqrt = (int) Math.sqrt(delta + 0.5);
            if ((long) delta_sqrt * delta_sqrt == delta && (delta_sqrt - 1) % 2 == 0) {
                int y = (-1 + delta_sqrt) / 2; // 另一个解(-1-delta_sqrt)/2必然小于0,不用考虑
                if (x < y) {
                    int[] res = new int[y - x + 1];
                    for (int i = x; i <= y; ++i) {
                        res[i - x] = i;
                    }
                    vec.add(res);
                }
            }
        }
        return vec.toArray(new int[vec.size()][]);
    } 
}

因为sqrt函数在某些情况下有精度误差,加0.5是为了避免精度误差,后面判就是因为如果不是整数的话转int的时候delta_sqrt的小数部分会被省去,所以这时候delta_sqrt * delta_sqrt == delta就不再成立,而整数就不会,后面一个判断就是题解里说的为了保证整除要判断分母是否为2的倍数,否则不能整除2。

2.2 double写法

class Solution {
    public int[][] findContinuousSequence(int target) {
        int i = 1;
        double j = 2.0;
        List<int[]> res = new ArrayList<>();
        while(i < j) {
            j = (-1 + Math.sqrt(1 + 4 * (2 * target + (long) i * i - i))) / 2;
            if(i < j && j == (int)j) {
                int[] ans = new int[(int)j - i + 1];
                for(int k = i; k <= (int)j; k++)
                    ans[k - i] = k;
                res.add(ans);
            }
            i++;
        }
        return res.toArray(new int[0][]);
    }
}

3. 双指针

此方法其实是对方法一的优化,因为方法一是没有考虑区间与区间的信息可以复用,只是单纯的枚举起点,然后从起点开始累加,而该方法就是考虑到了如果已知 [l,r] 的区间和等于 target ,那么枚举下一个起点的时候,区间 [l+1,r] 的和必然小于 target ,就不需要再从 l+1 再开始重复枚举,而是从 r+1开始枚举,充分的利用了已知的信息来优化时间复杂度。

class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> vec = new ArrayList<int[]>();
        for (int l = 1, r = 2; l < r;) {
            int sum = (l + r) * (r - l + 1) / 2;
            if (sum == target) {
                int[] res = new int[r - l + 1];
                for (int i = l; i <= r; ++i) {
                    res[i - l] = i;
                }
                vec.add(res);
                l++;
            } else if (sum < target) {
                r++;
            } else {
                l++;
            }
        }
        return vec.toArray(new int[vec.size()][]);
    }
}

复杂度分析

  • 时间复杂度:由于两个指针移动均单调不减,且最多移动 ⌊target​/2⌋  次,即方法一提到的枚举的上界,所以时间复杂度为O(target) 。
  • 空间复杂度:O(1) ,除了答案数组只需要常数的空间存放若干变量。

3.2 双指针+- 和比较

class Solution {
    public int[][] findContinuousSequence(int target) {
        int i = 1, j = 2, s = 3;
        List<int[]> res = new ArrayList<>();
        while(i < j) {
            if(s == target) {
                int[] ans = new int[j - i + 1];
                for(int k = i; k <= j; k++)
                    ans[k - i] = k;
                res.add(ans);
            }
            if(s >= target) {
                s -= i;
                i++;
            } else {
                j++;
                s += j;
            }
        }
        return res.toArray(new int[0][]);
    }
}

List<int[]> res = new ArrayList<>();   

最后再来一个res.toArray(new int[0][])即可化为int[][].

3 圆圈中最后剩下的数字

剑指 Offer 62. 圆圈中最后剩下的数字https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/

1. 模拟

class Solution {
    public int lastRemaining(int n, int m) {
        ArrayList<Integer> list = new ArrayList<>(n);
        for (int i = 0; i < n; i++) {
            list.add(i);
        }
        int idx = 0;
        while (n > 1) {
            idx = (idx + m - 1) % n;
            list.remove(idx);
            n--;
        }
        return list.get(0);
    } 
}

2. 数学 

最后只剩下一个元素,假设这个最后存活的元素为 num, 这个元素最终的的下标一定是0 (因为最后只剩这一个元素),
所以如果可以推出上一轮次中这个num的下标,然后根据上一轮num的下标推断出上上一轮num的下标,
直到推断出元素个数为n的那一轮num的下标,那就可以根据这个下标获取到最终的元素了。推断过程如下:

首先最后一轮中num的下标一定是0, 这个是已知的。
那上一轮应该是有两个元素,此轮次中 num 的下标为 (0 + m)%n = (0+3)%2 = 1; 说明这一轮删除之前num的下标为1;
再上一轮应该有3个元素,此轮次中 num 的下标为 (1+3)%3 = 1;说明这一轮某元素被删除之前num的下标为1;
再上一轮应该有4个元素,此轮次中 num 的下标为 (1+3)%4 = 0;说明这一轮某元素被删除之前num的下标为0;
再上一轮应该有5个元素,此轮次中 num 的下标为 (0+3)%5 = 3;说明这一轮某元素被删除之前num的下标为3;
....

因为要删除的序列为0-n-1, 所以求得下标其实就是求得了最终的结果。比如当n 为5的时候,num的初始下标为3,
 所以num就是3,也就是说从0-n-1的序列中, 经过n-1轮的淘汰,3这个元素最终存活下来了,也是最终的结果。

总结一下推导公式:(此轮过后的num下标 + m) % 上轮元素个数 = 上轮num的下标
class Solution {
    public int lastRemaining(int n, int m) {
        int ans = 0;
        // 最后一轮剩下2个人,所以从2开始反推
        for (int i = 2; i <= n; i++) {
            ans = (ans + m) % i;
        }
        return ans;
    }
} 

我是沙比

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值