17. 按摩师
一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
注意:本题相对原题稍作改动
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。
示例 2:
输入: [2,7,9,3,1]
输出: 12
解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。
示例 3:
输入: [2,1,4,5,3,1,1,3]
输出: 12
解释: 选择 1 号预约、 3 号预约、 5 号预约和 8 号预约,总时长 = 2 + 4 + 3 + 3 = 12。
思路
1.动态规划法。最大的序列和为 Max(前一位之前最大的序列和,前两位之前最大的序列和加上现在着这个数)
public int massage(int[] nums) {
int[] dp = new int[nums.length];
if (nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
dp[0] = nums[0];
dp[1] = Math.max(dp[0], nums[1]);
//最大的序列和为 Max(前一位之前最大的序列和,前两位之前最大的序列和加上现在着这个数)
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[nums.length - 1];
}
2.基于思路1优化,使用两个变量循环表示前一位之前最大的序列和和前两位之前最大的序列和。
public int massage(int[] nums) {
int fast = 0, slow = 0;
//fast为往前一位 slow为往前两位
for (int i = 0; i < nums.length; i++) {
int dp = Math.max(fast, slow + nums[i]);
slow = fast;
fast = dp;
}
return fast;
}
扩展
打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
1.数组组成了环形,不能从第一个房子偷到最后一个房子,所以统计从第一个房子不偷情况下最高金额,再统计最后一个房子不偷的情况下的最高金额,取他们的最大值,统计方法与上题一样。
public int rob(int[] nums) {
if (nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
//将数组分为两份,第一份统计第一个房子不偷第二份统计最后一个房子不偷。
return Math.max(robTwice(Arrays.copyOfRange(nums, 1, nums.length)),
robTwice(Arrays.copyOfRange(nums, 0, nums.length - 1)));
}
public int robTwice(int[] nums) {
int fast = 0, slow = 0;
//fast为往前一位 slow为往前两位
for (int i = 0; i < nums.length; i++) {
int dp = Math.max(fast, slow + nums[i]);
slow = fast;
fast = dp;
}
return fast;
}
斐波那契数
斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:F(0) = 0, F(1) = 1F(N) = F(N - 1) + F(N - 2), 其中 N > 1.给定 N,计算 F(N)。
1.动态规划思路,非递归,耗时少。
public int fib(int n){
if(n==0 || n==1){
return n;
}
int fn1=0;
int fn2=1;
for(int i=2;i<=n;i++){
fn2+=fn1;
fn1=fn2-fn1;
}
return fn2;
}
2.使用递归,代码简洁,但是耗时较久。
public int fib(int N) {
if (N<=1){
return N;
}
return fib(N-1)+fib(N-2);
}
最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
1.使用中心扩展法,遍历字符串,统计每个数相邻的数及其两边的数是否相等,即是否构成回文串。然后向两边扩展,每次记录回文串长度,最后选出长度最大的字符串。
public String longestPalindrome(String s) {
int longLength = 0, index = 0;
for (int i = 0; i < s.length(); i++) {
int left = i - 1, right = i + 1, temp = 1;
//如果左边构成回文串,继续向左边拓展
while (left >= 0 && s.charAt(i) == s.charAt(left)) {
temp++;
left--;
}
//如果右边构成回文串,继续向右边拓展
while (right < s.length() && s.charAt(i) == s.charAt(right)) {
temp++;
right++;
}
//左右两边构成回文串,同时拓展
while (right < s.length() && left >= 0 && s.charAt(left) == s.charAt(right)) {
temp += 2;
left--;
right++;
}
//选出回文串最初的长度和开始下标
if (longLength < temp) {
longLength = temp;
//left最后进行了--,所以要加一
index = left + 1;
}
}
return s.substring(index, index + longLength);
}
2.使用动态规划法,要判断一个字符串是否是回文,只需要判断这个字符串的左右两边字符相等且他们中包含的字符串也是回文串即可。
public String longestPalindrome(String s) {
//如果是空或者一位,直接返回
if (s == null || s.length() < 2) {
return s;
}
int length = s.length(), longLength = 1, index = 0;
//动态规划二位数组,dp[left][right]=true 表示left到right这段索引的字符串为回文串
boolean[][] dp = new boolean[length][length];
//依次往后面遍历,类似暴力法,只不过用动态规划作为判断条件
for (int right = 1; right < s.length(); right++) {
for (int left = 0; left < right; left++) {
//如果左右字符相等且子字符串为回文串,则当前字符串也为回文串。right-left<3表示left和初始状态没有公共部分
if (s.charAt(right) == s.charAt(left) && (right - left < 3 || dp[left + 1][right - 1])) {
dp[left][right] = true;
if (longLength < right - left + 1) {
longLength = right - left + 1;
index = left;
}
}
}
}
return s.substring(index, index + longLength);
}
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
1.动态规划法,要到n阶,那么先要到n-1阶随后走一步或者n-2阶随后两步两种情况。所以将问题化为计算跳到倒数第二个台阶和倒数第一个台阶之和。非递归写法。
public int climbStairs(int n) {
if (n == 0) {
return 1;
}
if (n == 1 || n == 2) {
return n;
}
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
2.递归写法
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
return climbStairs(n - 1) + climbStairs(n - 2);
}
最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
1.动态规划,遍历数组,如果之前统计的值为负数,则舍弃掉重新统计,然后判断舍弃之前与当前值的值比较,取出最大值。直至遍历结束。
public int maxSubArray(int[] nums) {
int res = nums[0];
int sum = 0;
for (int i=0;i<nums.length;i++) {
//如果之前的值为正数,就继续统计
if (sum > 0)
sum += nums[i];
//如果之前的值为负数,就舍弃掉重新计算
else
sum = nums[i];
//取出当前值和以前的最大值中较大的。
res = Math.max(res, sum);
}
return res;
}