Java算法~复习Day1

目录

动态规划(DP)

一、爬楼梯

思路:

代码:

二、背包问题

题目描述:

思路:

代码:

三、最长递增子序列

题目描述:

思路:

代码:

四、最大子序和

题目描述:

思路:

代码:

五、打家劫舍

题目描述:

思路:

代码:

六、编辑距离

题目描述:

思路:

代码:

七、最长回文子序列

题目描述:

思路:

代码:

八、01背包

题目描述:

思路:

代码:

九、柿子树森林

题目描述:

思路:

代码:

迭代算法 

一、斐波那契数列

题目描述:

思路:

二、阶乘

题目描述:

思路:

三、最大公约数

题目描述:

思路:

四、快速排序

题目描述:

思路:

回溯算法

一、全排列

题目描述:

思路:

二、子集

题目描述:

思路:

三、N皇后问题

题目描述:

思路:

四、组合总和

题目描述:

思路:

五、组合

题目描述:

思路:

六、分割回文串

题目描述:

思路:

贪心算法

贪心策略:每次选择性价比最高(即单位重量所能装的物品价值最大)的物品放入背包中。

代码实现:


动态规划(DP)

动态规划是一种算法思想,它主要用于解决需要做出许多决策才能到达最优解的问题。在这类问题中,每个决策都可以影响后续决策的结果,而导致时间复杂度呈指数级增长。通过动态规划思想,我们可以将问题分解成小的子问题,并利用之前求得的子问题解来逐步推导出最优解。

下面我们来看看如何用Java实现一些经典的动态规划问题。

一、爬楼梯

题目描述:

你正在爬一个 有 n 阶楼梯,每次你可以爬 1 阶或 2 阶。请问,你可以爬到楼梯顶部的所有不同方式有多少种?

思路:

该问题可以转化为求斐波那契数列的第n项。因此,我们可以使用动态规划来进行求解。我们定义一个数组dp[],其中dp[i]表示到达第i阶楼梯时的走法数目。显然有以下状态转移方程:

dp[i] = dp[i-1] + dp[i-2]

代码:

public int climbStairs(int n) {
    if(n<=2){
        return n;
    }
    int[] dp = new int[n+1];
    dp[1] = 1;
    dp[2] = 2;
    for(int i=3; i<=n; i++){
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
}

二、背包问题

题目描述:

有一个容量为c的背包和n个物品,每个物品有自己的价值和重量。现在需要选出一些物品放入背包中,使得在满足背包容量限制的情况下,所选物品的总价值最大。

思路:

该问题可以使用0/1背包问题来进行求解。具体实现思路如下:

首先我们定义两个数组:weight[](保存每个物品的重量)和value[](保存每个物品的价值)。接着,我们定义一个dp[i][j]数组表示前i个物品,在容量为j的情况下,最多能装的物品的总价值。则该问题的状态转移方程为:

if(weight[i-1] > j){
    dp[i][j] = dp[i-1][j];
}else{
    dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i-1]]+value[i-1]);
}

代码:

public int knapsack(int c, int n, int[] weight, int[] value) {
    int[][] dp = new int[n+1][c+1];
    for(int i=1; i<=n; i++){
        for(int j=1; j<=c; j++){
            if(weight[i-1] > j){
                dp[i][j] = dp[i-1][j];
            }else{
                dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i-1]]+value[i-1]);
            }
        }
    }
    return dp[n][c];
}

三、最长递增子序列

题目描述:

给定一个无序的整数数组,找出其中最长上升子序列的长度。

思路:

该问题可以用动态规划来进行求解。我们定义一个dp[]数组,其中dp[i]表示以第i个数字为结尾的最长上升子序列的长度。则该问题的状态转移方程为:

if(nums[j] < nums[i]){    dp[i] = Math.max(dp[i], dp[j]+1);    }

代码:

public int lengthOfLIS(int[] nums) {
    int n = nums.length;
    int[] dp = new int[n];
    Arrays.fill(dp, 1);
    int res = 0;
    for(int i=0; i<n; i++){
        for(int j=0; j<i; j++){
            if(nums[j] < nums[i]){
                dp[i] = Math.max(dp[i], dp[j]+1);
            }
        }
        res = Math.max(res, dp[i]);
    }
    return res;
}

四、最大子序和

题目描述:

给定一个整数数组nums,找到一个具有最大和的连续子数组(至少包含一个元素),返回该子数组的最大和。

思路:

该问题可以用动态规划来进行求解。我们定义一个dp[]数组,其中dp[i]表示以第i个数字为结尾的最大和。则该问题的状态转移方程为:

dp[i] = Math.max(nums[i], dp[i-1]+nums[i]);

代码:

public int maxSubArray(int[] nums) {
    int n = nums.length;
    int[] dp = new int[n];
    dp[0] = nums[0];
    int res = dp[0];
    for(int i=1; i<n; i++){
        dp[i] = Math.max(nums[i], dp[i-1]+nums[i]);
        res = Math.max(res, dp[i]);
    }
    return res;
}

五、打家劫舍

题目描述:

你是一个专业的小偷,计划偷窃沿街的房屋。每间房屋都有一定的现金,阻止你的唯一警察联系方式就是相邻的两家房屋装有互联网报警系统,如果同一时间抢了两家相邻的屋子会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,能够偷窃到的最高金额。

思路:

该问题可以用动态规划来进行求解。我们定义一个dp[]数组,其中dp[i]表示前i个房屋中能偷到的最大值。则该问题的状态转移方程为:

dp[i] = Math.max(dp[i-1], dp[i-2]+nums[i-1]);

代码:

public int rob(int[] nums) {
    int n = nums.length;
    if(n == 0){
        return 0;
    }
    int[] dp = new int[n+1];
    dp[0] = 0;
    dp[1] = nums[0];
    for(int i=2; i<=n; i++){
        dp[i] = Math.max(dp[i-1], dp[i-2]+nums[i-1]);
    }
    return dp[n];
}

六、编辑距离

题目描述:

给定两个单词word1和word2,计算出将word1转换成word2所使用的最少操作数。你可以对一个单词进行以下三种操作:插入一个字符、删除一个字符、替换一个字符。

思路:

该问题可以用动态规划来进行求解。我们定义一个dp[][]数组,其中dp[i][j]表示第一个单词的前i个字母与第二个单词前j个字母之间的编辑距离。则该问题的状态转移方程为:

if(word1.charAt(i-1) == word2.charAt(j-1)){
    dp[i][j] = dp[i-1][j-1];
}else{
    dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i-1][j-1])) + 1;
}

代码:

public int minDistance(String word1, String word2) {
    int m = word1.length();
    int n = word2.length();
    int[][] dp = new int[m+1][n+1];
    for(int i=0; i<=m; i++){
        dp[i][0] = i;
    }
    for(int j=0; j<=n; j++){
        dp[0][j] = j;
    }
    for(int i=1; i<=m; i++){
        for(int j=1; j<=n; j++){
            if(word1.charAt(i-1) == word2.charAt(j-1)){
                dp[i][j] = dp[i-1][j-1];
            }else{
                dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i-1][j-1])) + 1;
            }
        }
    }
    return dp[m][n];
}

七、最长回文子序列

题目描述:

给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000。

思路:

该问题可以用动态规划来进行求解。我们定义一个二维数组dp[][], 其中dp[i][j]表示s[i..j]中最长回文子序列的长度。则该问题的状态转移方程为:

if(s.charAt(i) == s.charAt(j)){
    dp[i][j] = dp[i+1][j-1] + 2;
}else{
    dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
}

代码:

public int longestPalindromeSubseq(String s) {
    int n = s.length();
    int[][] dp = new int[n][n];
    for(int i=n-1; i>=0; i--){
        dp[i][i] = 1;
        for(int j=i+1; j<n; j++){
            if(s.charAt(i) == s.charAt(j)){
                dp[i][j] = dp[i+1][j-1] + 2;
            }else{
                dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
            }
        }
    }
    return dp[0][n-1];
}

八、01背包

题目描述:

有一个容量为V的背包和N个物品,每个物品有重量w和价值v两个属性。现在要求选择一些物品放入背包中,使得它们的总重量不超过V,且总价值最大。求出这个最大价值。

思路:

该问题可以用动态规划来进行求解。我们定义一个一维数组dp[],其中dp[j]表示考虑前i个物品,在容量为j的背包中能拿到的最大价值。则该问题的状态转移方程为:

dp[j] = Math.max(dp[j], dp[j-w[i]]+v[i]);

代码:

public int knapsack(int V, int N, int[] w, int[] v){
    int[] dp = new int[V+1];
    for(int i=0; i<N; i++){
        for(int j=V; j>=w[i]; j--){
            dp[j] = Math.max(dp[j], dp[j-w[i]]+v[i]);
        }
    }
    return dp[V];
}

九、柿子树森林

题目描述:

有n棵草棚依次分别种了a1,a2,a3...an颗柿子号。现在有一只牛可以做原地不动却能吃到无限多的柿子,每棵树自上而下编号为1~ai,同时每棵树独立计算,其结果相加,计算树森林的最优摘取策略,最后输出偷取的总价值。

思路:

该问题可以用01背包问题的变形来进行求解,即将体积看作是重量w,价值看作数量v。设dp[i][j]表示前i棵树在背包容量为j时的最大价值,则该问题的状态转移方程为:

if(a[i][j]==0) dp[i][j]=dp[i-1][j];
else dp[i][j]=max(dp[i-1][j],dp[i][j-a[i][j]]+a[i][j]);

代码:

public int shizishu(int n, int[][] a){
    int V = 2*10000; // 因为题目中柿子数最多为10000颗,所以背包最大容量定为20000
    int[][] dp = new int[n+1][V+1];
    for(int i=1; i<=n; i++){
        for(int j=0; j<a[i][0]; j++){
            dp[i][j] = dp[i-1][j];
        }
        for(int j=a[i][1]; j<=V; j++){
            dp[i][j] = Math.max(dp[i-1][j], dp[i][j-a[i][1]]+a[i][2]);
        }
    }
    return dp[n][V];
}

迭代算法 

一、斐波那契数列

题目描述:

写一个函数,求解斐波那契数列第n项的值,n<=1000。

思路:

斐波那契数列定义为:f(0) = 0,f(1) = 1,f(n) = f(n-1) + f(n-2),n>=2。根据定义,可以用递归实现,但由于存在大量的重复计算,效率较低。因此,我们采用迭代的方式来对该问题进行求解。代码如下所示:

public static int fibonacci_iterative(int n){
    if(n == 0) return 0;
    if(n == 1) return 1;
    int F_n_minus_2 = 0;
    int F_n_minus_1 = 1;
    int F_n = 0;
    for(int i=2; i<=n; i++){
        F_n = F_n_minus_2 + F_n_minus_1;
        F_n_minus_2 = F_n_minus_1;
        F_n_minus_1 = F_n;  
    }
    return F_n;
}

二、阶乘

题目描述:

写一个函数,求解n的阶乘,n<=20。

思路:

阶乘的定义为:n! = n*(n-1)(n-2)...21。可以使用循环进行求解,当然也可以使用递归实现。这里我们采用循环来求解n的阶乘。代码如下所示:

public static int factorial_iterative(int n){
    int result = 1;
    for(int i=1; i<=n; i++){
        result *= i;
    }
    return result;
}

三、最大公约数

题目描述:

写一个函数,求解两个整数a,b的最大公约数。

思路:

最大公约数可以使用欧几里得算法进行求解,例如GCD(a,b) = GCD(b,a%b),其中%为取模运算符。可以采用递归实现该算法,也可以使用迭代实现。这里我们采用迭代方式实现欧几里得算法。代码如下所示:

public static int gcd_iterative(int a, int b){
    while(b != 0){
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

四、快速排序

题目描述:

写一个基于快速排序思想的排序方法,使得给定数组按升序排列。

思路:

快速排序是一种基于分治思想的算法,在处理大规模数据时表现优异。其基本思想为选择一个基准元素,将数组中小于基准元素的部分移到左边,大于基准元素的部分移到右边,然后对左右两部分重复此过程,直到全部有序。代码如下所示:

public static void quick_sort(int[] arr, int left, int right){
    if(left < right){
        int pivot = partition(arr, left, right);
        quick_sort(arr, left, pivot-1);
        quick_sort(arr, pivot+1, right);
    }
}

public static int partition(int[] arr, int left, int right){
    int pivot = arr[left];
    int i = left, j = right;
    while(i < j){
        while(i < j && arr[j] >= pivot) j--;
        arr[i] = arr[j];
        while(i < j && arr[i] <= pivot) i++;
        arr[j] = arr[i];
    }
    arr[i] = pivot;
    return i;
}

回溯算法

一、全排列

题目描述:

输入一个数组,输出该数组的所有排列方式。

思路:

全排列可以使用回溯算法进行求解,基本思路为对于每个位置,在剩下的其他位置中选择一个元素填入,直到填满整个数组。代码实现如下所示:

public static List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    backtracking(nums, 0, new ArrayList<>(), res);
    return res;
}

public static void backtracking(int[] nums, int index, List<Integer> temp, List<List<Integer>> res){
    if(index == nums.length){
        res.add(new ArrayList<>(temp));
        return;
    }
    for(int i=0; i<nums.length; i++){
        if(temp.contains(nums[i])) continue;
        temp.add(nums[i]);
        backtracking(nums, index+1, temp, res);
        temp.remove(temp.size()-1); 
    }
}

二、子集

题目描述:

输入一个数组,输出该数组的所有子集。

思路:

子集问题同样可以使用回溯算法进行求解,但具体实现有所不同。如果将每次递归时的结果都加入到结果列表中,则会出现重复的子集。因此需要在递归调用时,限定新的起点只能从上次结果中的下一位开始。代码实现如下所示:

public static List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    backtrack(nums, 0, new ArrayList<>(), res);
    return res;
}

public static void backtrack(int[] nums, int index, List<Integer> temp, List<List<Integer>> res){
    res.add(new ArrayList<>(temp));
    for(int i=index; i<nums.length; i++){
        temp.add(nums[i]);
        backtrack(nums, i+1, temp, res);
        temp.remove(temp.size()-1); 
    }
}

三、N皇后问题

题目描述:

求解N个皇后在NxN的棋盘中互不冲突的放置方案。

思路:

N皇后问题同样可以使用回溯算法求解,具体实现方法为,在每一行中寻找合法放置位置,并递归到下一行。需要注意的是,在判断每个位置是否合法时,需要考虑该位置所连接的对角线是否已经被其他皇后占据。代码实现如下所示:

  

public static List<List<String>> solveNQueens(int n) {
    boolean[] col = new boolean[n]; // 是否有皇后在该列
    boolean[] diagonal1 = new boolean[2*n-1]; // 左上至右下的对角线是否有皇后
    boolean[] diagonal2 = new boolean[2*n-1]; // 右上至左下的对角线是否有皇后
    List<List<String>> res = new ArrayList<>();
    backtracking(n, 0, col, diagonal1, diagonal2, new StringBuilder(), res);
    return res;
}

public static void backtracking(int n, int row, boolean[] col, boolean[] diagonal1, boolean[] diagonal2, StringBuilder temp, List<List<String>> res){
    if(row == n){
        res.add(generateBoard(temp.toString(), n));
        return;
    }
    for(int i=0; i<n; i++){
        int id1 = row-i+n-1;
        int id2 = row+i;
        if(col[i] || diagonal1[id1] || diagonal2[id2]) continue;
        char[] rowChars = new char[n];
        Arrays.fill(rowChars, '.');
        rowChars[i] = 'Q';
        temp.append(String.valueOf(rowChars));
        col[i] = true;
        diagonal1[id1] = true;
        diagonal2[id2] = true;
        backtracking(n, row+1, col, diagonal1, diagonal2, temp, res);
        col[i] = false;
        diagonal1[id1] = false;
        diagonal2[id2] = false;
        temp.deleteCharAt(temp.length()-1); 
    }
}

public static List<String> generateBoard(String str, int n) {
    List<String> board = new ArrayList<>();
    for(int i = 0; i < n; i++) {
        board.add(str.substring(i*n, (i+1)*n));
    }
    return board;
}

四、组合总和

题目描述:

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidate 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。

思路:

组合总和问题同样可以使用回溯算法进行求解。需要注意的是,如果允许重复选择则在递归调用时从当前位置开始,而不是从下一个位置开始,否则会出现重复结果。同时需要记录当前已经选择的元素之和,当达到目标值时再将当前结果加入到结果列表中。代码实现如下所示:

public static List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> res = new ArrayList<>();
    Arrays.sort(candidates); // 先排序
    backtracking(candidates, target, 0, new ArrayList<>(), 0, res);
    return res;
}

public static void backtracking(int[] nums, int target, int index, List<Integer> temp, int sum, List<List<Integer>> res){
    if(sum == target){
        res.add(new ArrayList<>(temp));
        return;
    }
    for(int i=index; i<nums.length; i++){
        if(sum + nums[i] > target) break; // 剪枝
        temp.add(nums[i]);
        backtracking(nums, target, i, temp, sum+nums[i], res);  // 重复使用哪个元素
        temp.remove(temp.size()-1); 
    }
}

五、组合

题目描述:

给定两个整数 n 和 k,返回 1...n 中所有可能的 k 个数的组合。

思路:

组合问题同样可以使用回溯算法进行求解。每次在剩下的未选择元素中选择一个,直到选择了k个元素为止。需要注意的是,在递归调用时从当前位置的下一位开始,否则会出现重复结果。代码实现如下所示:

public static List<List<Integer>> combine(int n, int k) {
    List<List<Integer>> res = new ArrayList<>();
    backtracking(n, k, 1, new ArrayList<>(), res);
    return res;
}

public static void backtracking(int n, int k, int start, List<Integer> temp, List<List<Integer>> res){
    if(temp.size() == k){
        res.add(new ArrayList<>(temp));
        return;
    }
    for(int i=start; i<=n; i++){
        temp.add(i);
        backtracking(n, k, i+1, temp, res); // 从下一位开始选择,避免重复
        temp.remove(temp.size()-1); 
    }
}

六、分割回文串

题目描述:

给定一个字符串,将该字符串分割成不同的子串,使得每个子串都是回文串。返回所有可能的分割方案。

思路:

利用回溯算法来求解分割回文串问题。对于每种分割方式,首先判断当前子串是否为回文串,如果是则继续向下搜索,如果不是则直接回溯上一层。在搜索过程中维护一个临时变量temp,存储当前已经分割的子串。当temp中包含了所有的字符时,则说明已将字符串分割完毕,并加入到结果集中。代码实现如下所示:

  

public static List<List<String>> partition(String s) {
    List<List<String>> res = new ArrayList<>();
    backtracking(s, 0, new ArrayList<>(), res);
    return res;
}

public static void backtracking(String s, int index, List<String> temp, List<List<String>> res){
    if(index == s.length()){
        res.add(new ArrayList<>(temp));
        return;
    }
    for(int i=index; i<s.length(); i++){
        if(isPalindrome(s, index, i)){
            temp.add(s.substring(index, i+1));
            backtracking(s, i+1, temp, res);
            temp.remove(temp.size()-1); 
        }
    }
}

public static boolean isPalindrome(String s, int start, int end){
    while(start < end){
        if(s.charAt(start++) != s.charAt(end--)) return false;
    }
    return true;
}

贪心算法

Java贪心算法是指根据当前情况做出最优选择,并依次得到最终结果的算法。它通常用于优化问题,例如最短路径、最小生成树、背包问题等。下面以贪心算法求解背包问题为例:

贪心策略:每次选择性价比最高(即单位重量所能装的物品价值最大)的物品放入背包中。

代码实现:

import java.util.Arrays;

class Knapsack {
    private static double maxValue(int[] weight, int[] value, int capacity) {
        int n = weight.length;
        double[] unitValue = new double[n];
        for (int i = 0; i < n; i++) {
            unitValue[i] = (double) value[i] / weight[i];
        }
        Item[] items = new Item[n];
        for (int i = 0; i < n; i++) {
            items[i] = new Item(weight[i], value[i], unitValue[i]);
        }
        Arrays.sort(items);
        double maxValue = 0;
        for (int i = 0; i < n && capacity > 0; i++) {
            if (items[i].weight <= capacity) {
                maxValue += items[i].value;
                capacity -= items[i].weight;
            } else {
                maxValue += capacity * items[i].unitValue;
                capacity = 0;
            }
        }
        return maxValue;
    }

    static class Item implements Comparable<Item> {
        int weight;
        int value;
        double unitValue;
        
        public Item(int weight, int value, double unitValue) {
            this.weight = weight;
            this.value = value;
            this.unitValue = unitValue;
        }

        @Override
        public int compareTo(Item other) {
            return (int) (other.unitValue - this.unitValue);
        }
    }

    public static void main(String[] args) {
        int[] weight = {10, 20, 30};
        int[] value = {60, 100, 120};
        int capacity = 50;
        double result = maxValue(weight, value, capacity);
        System.out.println("Maximum value we can obtain is: " + result);
    }
}

代码注释:

行1:导入Java内置的Arrays模块,用于排序item数组。

行4-14:定义一个内部类Item,表示每件物品,包括重量、价值和单位价值三个属性,并根据单位价值从大到小进行排序。

行16-38:定义方法maxValue,接收重量、价值和背包容量3个参数,并返回最大总价值。

首先计算出每种物品的单位价值,存储在unitValue数组中。

然后将每件物品的重量、价值和单位价值封装在Item对象中,并按照单位价值从高到低排序。

最后依次选取每件物品,如果这件物品的重量小于等于剩余容量,则将其全部放入背包中,然后更新剩余容量和最大总价值;否则,只将部分物品放入背包中,并结束循环。

行40-47:定义主函数main,对maxValue方法进行测试。

另外,以下是一些其他适合使用贪心算法求解的经典问题:

部分背包问题:每种物品可以装进去部分而不必装满(也称为分数背包问题)。

硬币找零问题:用最少数量的硬币组成某个金额。

区间覆盖问题:在一系列线段中选取最少的线段,使它们的长度刚好能覆盖给定区间。

图的着色问题:用最少的颜色给图上的每个节点染色,使相邻节点不能有相同的颜色。

以上都是比较常见的应用场景,在实际工程开发中,我们还需要根据具体问题选择合适的算法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值