目录
数学
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的连续正数序列
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 圆圈中最后剩下的数字
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;
}
}
我是沙比