第一题
这里求数字 n 的二进制数中1的个数的方法巧妙。
class Solution {
public int[] countBits(int num) {
int[] res = new int[num+1];
for(int i = 0 ; i <= num ; i++)
{
res[i] = popcount(i);
}
return res;
}
public int popcount(int x){
int count;
for(count = 0 ; x != 0 ; count++)
{
x &= x-1;
}
return count;
}
}
第二题
思路是leetcode官网题解:
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
boolean[][] dp = new boolean[n+1][n+1];
String res = "";
for(int l = 0 ; l < n ; l++)
{
for(int i = 0 ; i + l < n ; i++)
{
int j = i+l;
if(j >= n)
{
break;
}
if(l == 0)
{
dp[i][j] = true;
}else if(l == 1)
{
dp[i][j] = (s.charAt(i) == s.charAt(j));
}else
{
dp[i][j] = (s.charAt(i) == s.charAt(j)) && dp[i+1][j-1];
}
if(dp[i][j] && l+1 > res.length())
{
res = s.substring(i,j+1);
}
}
}
return res;
}
}
第三题
方法一:暴力法
枚举所有的可能。
class Solution {
public boolean checkSubarraySum(int[] nums, int k) {
int len = nums.length;
for(int start = 0 ; start < len-1 ; start++)
{
for(int end = start+1 ; end < len ; end++)
{
int sum = 0;
for(int temp = start ; temp <= end ; temp++)
{
sum += nums[temp];
}
if(sum == k || (k != 0 && sum % k == 0))
return true;
}
}
return false;
}
}
第四题
上第 n 阶台阶的方法 就等于 = 上(n-1) + 上(n-2) + 上(n-3)台阶的方法之和
所以状态转移方程为: dp[n] = dp[n-1] + dp[n-2] + dp[n-3]
下边取模的时候也需要仔细分析。
取模需要先对三个数中较大的两个数之和进行取模,再对三个数之和取模。
如果直接对每个数取模,假如每个数都距离1000000007 很近,取模之后不变,但是相加之后溢出,结果就为符数
如果直接对三个数之和取模,则三个数之和可能就直接会溢出
class Solution {
public int waysToStep(int n) {
int[] dp = new int[n+1];
if(n == 1)
return 1;
if(n == 2)
return 2;
if(n == 3)
return 4;
dp[1] = 1;
dp[2] = 2;
dp[3] = 4;
// 状态转移方程: dp[n] = dp[n-1] + dp[n-2] + dp[n-3] ;
for(int i = 4 ; i <= n ; i++)
{
dp[i] = (dp[i-1] + (dp[i-2] + dp[i-3]) % 1000000007 ) % 1000000007 ;
}
return dp[n];
}
}
第五题
解题思路:
遍历 s 字符串每一个字符,使用 i 指针指向字符串 t,如果在s中有一个字符在t中找不到的话,那么i指针会一直++,直到不符合while条件(也就是i = t.length),这时候后边再加一次,就会超出 t 的长度,所以s也就不是t的子串。
class Solution {
public boolean isSubsequence(String s, String t) {
int i = 0;
for(char c : s.toCharArray())
{
while(i < t.length() && c != t.charAt(i))
{
i++;
}
i++; // 无论 c 和 t.charAt(i) 是否相等,i都要++,i要向后遍历字符串t
}
return i <= t.length() ? true : false;
}
}
第六题
记忆化搜索和动态递归
记忆化搜索:自顶向下
动态递归:自底向上
使用记忆化搜索解法
class Solution {
static int[] memo ; // 添加一个数组进行记忆化搜索
public int integerBreak(int n) {
memo = new int[n+1];
return breakInteger(n);
}
public int breakInteger(int n){
// 1 递归终止条件
if(n == 1)
return 1; // n 为1的话,已经无法分割,返回1
if(memo[n] != 0)
{
return memo[n]; // 进行记忆化搜索,如果已经有值,就直接返回
}
int res = -1;
// 2 for循环找分割为两部分的乘积最大值
for(int i = 1 ; i <= n-1 ; i++)
{
// 3 这里有三种结果进行比较
// 1)res本身 2)不对n-i再进行分割 3) 对n-i再进行分割,找n-i的最大乘积
res = max3(res , i * (n-i) , i * breakInteger(n-i) );
}
memo[n] = res; // 记忆化搜索赋值
return res;
}
public int max3(int a,int b,int c){
return Math.max(Math.max(a,b),c);
}
}
使用动态规划解法
class Solution {
public int integerBreak(int n) {
// 1 定义动态规划数组
int[] dp = new int[n+1];
dp[1] = 1;
for(int i = 2 ; i <= n ; i++)
for(int j = 1 ; j <= i-1 ; j++)
// 2 分割为 j i-j
dp[i] = max3(dp[i] , j * (i-j) , j * dp[i-j]);
return dp[n];
}
public int max3(int a,int b,int c){
return Math.max(Math.max(a,b),c);
}
}
第七题
解题思路:
状态转移方程: dp[i] = min( dp[i] , dp[i - k] + 1 );
找一个数 n , 组成 n 的平方数的最小的个数,那么只需要知道 n 减去一个平方数之后,组成这个数的平方数的最小个数 ,最后再加一即可!
class Solution {
public int numSquares(int n) {
// dp[i] = min(dp[i-k] + 1);
int[] dp = new int[n+1];
// 给dp数组赋值max_value
Arrays.fill(dp, Integer.MAX_VALUE);
// 求出n以下所有平方数,存放在数组中,方便遍历
int square = (int) Math.sqrt(n) + 1;
int[] squares = new int[square];
for(int i = 1 ; i < square ; i++)
{
squares[i] = i * i;
}
//记得给 dp[0] 赋值
dp[0] = 0;
for(int i = 1 ; i <= n ; i++)
{
for(int j = 1 ; j < square ; j++)
{
if(i < squares[i])
break;
dp[i] = Math.min(dp[i] , dp[i-squares[j]] + 1);
}
}
return dp[n];
}
}
第八题
如果看到动态规划题目,没有思路的话,不妨来使用暴力法来有个大致的思路。
使用记忆化搜索
class Solution {
static int[] memo ;
public int rob(int[] nums) {
int n = nums.length;
memo = new int[n];
return tryRob(nums,0);
}
public int tryRob(int[] nums , int index){
if(index >= nums.length)
return 0;
//使用记忆化搜索
if(memo[index] != 0)
{
return memo[index];
}
int res = 0;
for(int i = index ; i < nums.length ; i++)
{
// 偷取 nums[i] 并考虑偷取 nums[i+2 ... n-1] 范围内
res = Math.max( res , nums[i] + tryRob(nums,i+2) );
}
// 给记忆化数组赋值
memo[index] = res;
return res;
}
使用动态规划:
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 0)
{
return 0;
}
// dp[i] 表示偷取 i ... n-1 范围内的最多的钱
int[] dp = new int[n];
dp[n-1] = nums[n-1];// 偷取 n-1 ... n-1范围内最多的钱就是偷取第n-1个房子
for(int i = n-2 ; i >= 0 ; i--)
for(int j = i ; j < n ; j++)
// 原式:dp[i] = max(dp[i] , nums[j] + dp[j+2])
// 偷取 j 并考虑偷取 j+2~n-1 的最多的钱
// 但是因为 dp[j+2] j+2可能会大于等于n,发生数组越界,所以使用三元运算符判断一下是否越界
// dp[i] = nums[j] + dp[j+2],但是dp[i]所有遍历的结果中取最大的,所以就也和自己比较就成为了
// dp[i] = max(dp[i] , nums[j] + dp[j+2])
dp[i] = Math.max( dp[i] , nums[j] + ( j+2>=n ? 0 : dp[j+2] ) );
return dp[0];
}
}
第九题
第i个台阶只与第i-1和i-2个台阶有关,这里注意返回值!
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int[] dp = new int[n];
dp[0] = cost[0];
dp[1] = cost[1];
for(int i = 2 ; i < n ; i++)
{
dp[i] = cost[i] + Math.min(dp[i-1],dp[i-2]);
}
return Math.min(dp[n-1] , dp[n-2]);
第十题
简单 0-1背包问题
背包容量C,数组w表示物品大小,数组v表示物品价值,要求在背包中放入最大价值的东西。
递归解法
class Solution{
public static int main(int[] w,int[] v,int C){
int n = w.length();
// 0....n-1 件物品最大价值
return bestValue(w , v , C , n-1);
}
public static int bestValue(int[] w,int[] v,int c,int index){
if(index < 0 || c <= 0)
{
return 0; //如果没有物品选择 或者 容量小于等于0了,那么返回的价值就为0
}
int res = bestValue(w,v,c,index-1);//不选择
if(c >= w[index])
{
res = Math.max( res , v[index] + bestValue(w,v,c-w[index],index-1) );
}
return res;
}
}
第十一题
解题思路:
如何去寻找上升子序列呢?
遍历数组,使用 dp[i] 记录到 0…i的最长上升子序列的长度。
寻找 nums[i] 之前的数,如果比nums[i] 小,则 nums[i] 就可以跟在其后,所以上升子序列长度+1
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
/*
动态递归:
dp[j] = 1+dp[i](if nums[j]>nums[i])
*/
fill(dp , 1);
for(int i = 1 ; i < n ; i++)
for(int j = 0 ; j < i ; j++)
if(nums[j] < nums[i])
dp[i] = Math.max( dp[i] , dp[j] + 1 );
int res = 0;
for(int i = 0 ; i < n ; i++)
res = Math.max(res , dp[i]);
return res;
}
public static void fill(int[] a , int target){
for(int i = 0 ; i < a.length ; i++)
{
a[i] = target ;
}
}
}
第十二题
记忆化搜索
要将数组分成相等的两部分,只需要找到符合 sum/2 的子集即可。
class Solution {
public int[][] memo ;
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
for(int i = 0 ; i < n ; i++)
// 先求出总和,找到满足sum/2的子集即可
sum += nums[i];
if(sum % 2 != 0)
// 如果和是奇数,则不能分割
return false;
// 初始化数组
memo = new int[n][sum/2+1];
// 把数组都赋值为 -1
// 这里为什么使用int类型的数组呢?因为我们用到记忆化搜索还要表示未计算的数
// -1:未计算 0:false 1:true
fill(memo , -1);
return check(nums,n-1,sum/2);
}
public void fill(int[][] nums , int target){
for(int i = 0 ; i < nums.length ; i++){
for(int j = 0 ; j < nums[0].length ; j++)
{
nums[i][j] = target;
}
}
}
public boolean check(int[] nums,int index,int sum){
//如果总和为0,返回true
if(sum == 0)
return true;
//如果总和小于0 或 index小于0,找不到符合条件的子集,返回false
if(sum < 0 || index < 0)
{
return false;
}
//使用记忆化搜索
if(memo[index][sum] != -1)
{
return memo[index][sum] == 1 ? true : false ;
}
memo[index][sum] = ( check(nums , index-1 , sum )
|| check(nums , index-1 , sum-nums[index]) ) == true ? 1 : 0;
return memo[index][sum] == 1 ? true : false;
}
}
动态规划解法:
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for(int i = 0 ; i < nums.length ; i++)
{
sum += nums[i];
}
if(sum % 2 != 0)
{
return false;
}
int C = sum/2 ; //用C表示背包的容量
int n = nums.length; //用n表示数组的长度
//定义一个数组,dp[i]代表容量为i时,背包是否可以被放满
boolean[] dp = new boolean[C+1];
for(int i = 0 ; i <= C ; i++)
{
// 先对数组进行初始化
// 看看把数组中的第一个数放进去可不可以满足
dp[i] = ( nums[0] == i );
}
// 因为已经对第一个数进行初始化,所以i从1开始
for(int i = 1 ; i < n ; i ++)
{
for(int j = C ; j > nums[i] ; j--)
{
dp[j] = dp[j] || dp[j-nums[i]];
}
}
// 最后返回 dp[C],也就是容量为C时,是否可以放满
return dp[C];
}
}
第十三题
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
/*
采用自下向上解法:数组可以看作一下形式
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
从下边上来较小的数 + 本位置的数
状态转移方程: dp[i][j] = min(dp[i+1][j],dp[i+1][j+1]) + rows.get[j];
*/
int n = triangle.size();
int[][] dp = new int[n+1][n+1];
for(int i = n-1 ; i >= 0 ; i--)
{
//从下到上,获取每一行
List<Integer> rows = triangle.get(i);
for(int j = 0 ; j < rows.size() ; j++)
{
//遍历每一行中的元素
dp[i][j] = Math.min( dp[i+1][j] , dp[i+1][j+1] ) + rows.get(j);
}
}
return dp[0][0];
}
}
第十四题
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
// 特判只有一个格子的情况
if(m == 1 && n == 1)
{
int res = obstacleGrid[0][0] == 1 ? 0 : 1;
return res;
}
// 如果起点有障碍物,返回0
if(obstacleGrid[0][0] == 1)
return 0;
// 行和列各加 1 ,起点从[1][1]开始计算
int[][] dp = new int[m+1][n+1];
for(int i = 1 ; i <= m ; i++)
{
for(int j = 1 ; j <= n ; j++)
{
dp[1][1] = 1;
// 如果有障碍物,就设置为0,不能走,并跳过循环
if(obstacleGrid[i-1][j-1] == 1)
{
dp[i][j] = 0;
continue;
}
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
dp[1][1] = 1;
return dp[m][n];
}
}
第十五题
俄罗斯套娃
class Solution {
public int maxEnvelopes(int[][] envelopes) {
Arrays.sort(envelopes,new Comparator<int[]>(){
public int compare(int[] arr1 , int[] arr2){
if(arr1[0] == arr2[0])
{
//如果宽相等,按照高从大到小排序
return arr2[1] - arr1[1];
}else
{
//按照宽从小到大排序
return arr1[0] - arr2[0];
}
}
});
/*
先按照宽,从小到大排序
(但是有一种复杂的情况,如果宽相等的话,即使高相差较多,也无法放入)
所以,就需要考虑宽相等的情况
当宽相等时,把高按照从大到小的顺序排序
最后结果就是求高的最长上升子序列。
*/
int n = envelopes.length;
int[] res = new int[n];
for(int i = 0 ; i < n ; i++)
{
res[i] = envelopes[i][1];
}
return lengthOfLIS(res);
}
// 求最长上升子序列
public int lengthOfLIS(int[] nums){
int n = nums.length;
int[] dp = new int[n];
// 将数组初始化为1
fill(dp,1);
for(int i = 1 ; i < n ; i++)
{
for(int j = 0 ; j < i ; j++)
{
if(nums[j] < nums[i])
{
dp[i] = Math.max( dp[i] , dp[j]+1 );
}
}
}
int res = 0;
for(int i = 0 ; i < n ; i++)
{
res = Math.max(res,dp[i]);
}
return res;
}
public void fill(int[] nums , int target){
for(int i = 0 ; i < nums.length ; i++)
{
nums[i] = target;
}
}
}
十六题
class Solution {
public int[] countBits(int num) {
int[] f = new int[num+1];
f[0] = 0;
for(int i = 1 ; i <= num ; i++)
{
// f[2] = [0,1,1]
// 将每个数和 1 与,并除以2 也就是(i>>1)
f[i] = f[i >> 1] + i&1;
}
return f;
}
}
第十七题
错误代码
class Solution {
public int waysToChange(int n) {
int[] coins = new int[]{1,5,10,25};
int[] dp = new int[n+1];
dp[0] = 1;
for(int i = 1 ; i <= n ; i++)
{
for(int coin : coins)
{
/*
i = 1 dp[1] = dp[1] + dp[0] = 1
i = 2 dp[2] = dp[2] + dp[2 - 1] = 1
i = 3 dp[3] = dp[3] + dp[3-1] = 1
i = 4 dp[4] = dp[4] + dp[4-1] = 1
i = 5 dp[5] = dp[5] + dp[5-1] = 1
dp[5] = dp[5] + dp[5-5] = 2
i = 6 dp[6] = dp[6] + dp[6-1] = 2
dp[6] = dp[6] + dp[6-5] = 3
*/
if(i-coin < 0) break;
dp[i] = (dp[i] + dp[i-coin]) % 1000000007;
}
}
return dp[n];
}
}
上述代码错在哪里呢?
我们来看一下dp[6]的情况,也就是组成6元硬币的情况
coin = 1:
dp[6] = dp[6] + dp[5] 也就是 0 + dp[5] = 2
coin = 5:
dp[6] = dp[6] + dp[1] 也就是 2 + dp[1] = 3
所以组成6元硬币的情况有3种
但是实际上只有两种[1,1,1,1,1,1] 和 [1,5]
我们计算出来了3中是因为把[1,5] 和 [5,1]重复计算了
正确代码
class Solution {
public int waysToChange(int n) {
int[] coins = new int[]{1,5,10,25};
int[] dp = new int[n+1];
dp[0] = 1;
for(int coin : coins)
{
//对代码进行改进
for(int i = coin ; i <= n ; i++)
{
dp[i] = (dp[i] + dp[i-coin]) % 1000000007;
}
}
return dp[n];
}
}