0%

问题

有若干个城市,任何两个城市之间的距离都是确定的,现要求一旅行商从某城市出发必须经过每一个城市且只在一个城市逗留一次,最后回到出发的城市,问如何事先确定一条最短的线路已保证其旅行的费用最少?

还有 背包、装箱 问题

注: 递归,时间复杂度为n!

贪心算法

在对问题求解时,总是做出当前情况下的最好选择,否则将来可能会后悔,故名“贪心”。这是一种算法策略,每次选择得到的都是局部最优解。选择的策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

求解的问题分成若干个子问题,每一个子问题求解,得到子问题的局部最优解,子问题的局部最优解合成原问题的一个解

步骤

  1. 从某一个城市开始,每次选择一个城市,直到所有的城市被走完。

  2. 每次在选择下一个城市的时候,只考虑当前情况,保证迄今为止经过的路径总距离最小。

     从问题的某一初始解出发;
         while (能朝给定总目标前进一步)
         { 
               利用可行的决策,求出可行解的一个解元素;
         }
     由所有解元素组合成问题的一个可行解
    

缺点

  • 不能保证最终为最优解
  • 不能用来求最大最小解问题
  • 无后效性

代码实现

package noah;
 
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
 
public class TxTsp {
 
    private int cityNum; // 城市数量
    private int[][] distance; // 距离矩阵
 
    private int[] colable;//代表列,也表示是否走过,走过置0
    private int[] row;//代表行,选过置0
 
    public TxTsp(int n) {
        cityNum = n;
    }
 
    private void init(String filename) throws IOException {
        // 读取数据
        int[] x;
        int[] y;
        String strbuff;
        BufferedReader data = new BufferedReader(new InputStreamReader(
                new FileInputStream(filename)));
        distance = new int[cityNum][cityNum];
        x = new int[cityNum];
        y = new int[cityNum];
        for (int i = 0; i < cityNum; i++) {
            // 读取一行数据,数据格式1 6734 1453
            strbuff = data.readLine();
            // 字符分割
            String[] strcol = strbuff.split(" ");
            x[i] = Integer.valueOf(strcol[1]);// x坐标
            y[i] = Integer.valueOf(strcol[2]);// y坐标
        }
        data.close();
 
        // 计算距离矩阵
        // ,针对具体问题,距离计算方法也不一样,此处用的是att48作为案例,它有48个城市,距离计算方法为伪欧氏距离,最优值为10628
        for (int i = 0; i < cityNum - 1; i++) {
            distance[i][i] = 0; // 对角线为0
            for (int j = i + 1; j < cityNum; j++) {
                double rij = Math
                        .sqrt(((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j])
                                * (y[i] - y[j])) / 10.0);
                // 四舍五入,取整
                int tij = (int) Math.round(rij);
                if (tij < rij) {
                    distance[i][j] = tij + 1;
                    distance[j][i] = distance[i][j];
                } else {
                    distance[i][j] = tij;
                    distance[j][i] = distance[i][j];
                }
            }
        }
 
        distance[cityNum - 1][cityNum - 1] = 0;
 
        colable = new int[cityNum];
        colable[0] = 0;
        for (int i = 1; i < cityNum; i++) {
            colable[i] = 1;
        }
 
        row = new int[cityNum];
        for (int i = 0; i < cityNum; i++) {
            row[i] = 1;
        }
 
    }
    
    public void solve(){
        
        int[] temp = new int[cityNum];
        String path="0";
        
        int s=0;//计算距离
        int i=0;//当前节点
        int j=0;//下一个节点
        //默认从0开始
        while(row[i]==1){
            //复制一行
            for (int k = 0; k < cityNum; k++) {
                temp[k] = distance[i][k];
                //System.out.print(temp[k]+" ");
            }
            //System.out.println();
            //选择下一个节点,要求不是已经走过,并且与i不同
            j = selectmin(temp);
            //找出下一节点
            row[i] = 0;//行置0,表示已经选过
            colable[j] = 0;//列0,表示已经走过
            
            path+="-->" + j;
            //System.out.println(i + "-->" + j);
            //System.out.println(distance[i][j]);
            s = s + distance[i][j];
            i = j;//当前节点指向下一节点
        }
        System.out.println("路径:" + path);
        System.out.println("总距离为:" + s);
        
    }
    
    public int selectmin(int[] p){
        int j = 0, m = p[0], k = 0;
        //寻找第一个可用节点,注意最后一次寻找,没有可用节点
        while (colable[j] == 0) {
            j++;
            //System.out.print(j+" ");
            if(j>=cityNum){
                //没有可用节点,说明已结束,最后一次为 *-->0
                m = p[0];
                break;
                //或者直接return 0;
            }
            else{
                m = p[j];
            }
        }
        //从可用节点J开始往后扫描,找出距离最小节点
        for (; j < cityNum; j++) {
            if (colable[j] == 1) {
                if (m >= p[j]) {
                    m = p[j];
                    k = j;
                }
            }
        }
        return k;
    }
 
 
    public void printinit() {
        System.out.println("print begin....");
        for (int i = 0; i < cityNum; i++) {
            for (int j = 0; j < cityNum; j++) {
                System.out.print(distance[i][j] + " ");
            }
            System.out.println();
        }
        System.out.println("print end....");
    }
 
    public static void main(String[] args) throws IOException {
        System.out.println("Start....");
        TxTsp ts = new TxTsp(48);
        ts.init("c://data.txt");
        //ts.printinit();
        ts.solve();
    }
}

背包问题是动态规划领域中一个重要且经典的问题,涉及在限定容量的背包中如何选择物品,使得物品的总价值最大化或总重量最小化。主要有两类背包问题:0-1背包问题和完全背包问题。

  1. 0-1背包问题:每个物品要么被选择一次,要么不选。

  2. 完全背包问题:每个物品可以被选择无限次。

以下是背包问题的一般步骤:

  1. 定义状态:通常使用一个二维数组 dp[i][j],其中 dp[i][j] 表示前 i 个物品在背包容量为 j 时的最优解(价值或重量)。

  2. 状态转移方程:对于每个物品,可以选择将其放入背包(如果容量允许),也可以选择不放入。状态转移方程通常为:

    1
    dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])

    其中 weight[i] 表示第 i 个物品的重量,value[i] 表示第 i 个物品的价值。

  3. 边界条件:初始时,当没有物品可选时,dp[0][j] 都应为 0,当背包容量为 0 时,dp[i][0] 也都为 0。

  4. 计算顺序:通常从小规模问题开始,逐步计算出大规模问题的最优解。

下面是一个0-1背包问题的 Java 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class KnapsackProblem {
public int knapsack(int[] weights, int[] values, int capacity) {
int n = weights.length;
int[][] dp = new int[n + 1][capacity + 1];

for (int i = 1; i <= n; i++) {
for (int j = 1; j <= capacity; j++) {
if (weights[i - 1] <= j) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}

return dp[n][capacity];
}

public static void main(String[] args) {
KnapsackProblem knapsackProblem = new KnapsackProblem();
int[] weights = {2, 3, 4, 5};
int[] values = {3, 4, 5, 6};
int capacity = 5;
int maxValue = knapsackProblem.knapsack(weights, values, capacity);
System.out.println("Maximum value: " + maxValue);
}
}

在这个示例中,我们使用动态规划解决了0-1背包问题。通过遍历每个物品和背包容量,根据状态转移方程计算在当前情况下的最优解。最终,dp[n][capacity] 就是问题的最优解,表示在前 n 个物品中选择放入容量为 capacity 的背包所能获得的最大价值。

请注意,背包问题的动态规划解法也可能因为问题的具体要求而有所变化,但通常都涵盖了上述的状态定义、状态转移方程和边界条件。

线性动态规划(Linear Dynamic Programming)在许多问题中都有广泛的应用,其中一个典型的问题是最长递增子序列(Longest Increasing Subsequence,简称 LIS)问题。LIS 问题的目标是找到一个给定序列中的最长递增子序列的长度,其中递增子序列指的是序列中的元素按照顺序递增排列。

以下是最长递增子序列问题的一般步骤:

  1. 定义状态:通常使用一个数组 dp,其中 dp[i] 表示以第 i 个元素为结尾的最长递增子序列的长度。

  2. 状态转移方程:对于第 i 个元素,我们需要找到前面所有比它小的元素中,以它们为结尾的最长递增子序列的最大长度。状态转移方程可以表示为:

    1
    dp[i] = max(dp[j] + 1), 其中 0 <= j < i 且 nums[j] < nums[i]

    这意味着我们在计算 dp[i] 时,会考虑前面所有满足条件的 dp[j] 的最大值加 1。

  3. 边界条件:初始时,每个元素自成一个长度为 1 的递增子序列,所以 dp[i] = 1

  4. 计算顺序:通常从小规模问题开始,逐步计算出大规模问题的最优解。

下面是一个最长递增子序列问题的 Java 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class LongestIncreasingSubsequence {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);

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);
}
}
}

int maxLength = 0;
for (int len : dp) {
maxLength = Math.max(maxLength, len);
}

return maxLength;
}

public static void main(String[] args) {
LongestIncreasingSubsequence lis = new LongestIncreasingSubsequence();
int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
int maxLength = lis.lengthOfLIS(nums);
System.out.println("Length of longest increasing subsequence: " + maxLength);
}
}

在这个示例中,我们使用动态规划解决了最长递增子序列问题。通过遍历每个元素,根据状态转移方程计算以当前元素为结尾的最长递增子序列长度。最终,我们在 dp 数组中找到最大的长度,即为最长递增子序列的长度。

请注意,LIS 问题的动态规划解法可能因为问题的具体要求而有所变化,但通常都涵盖了上述的状态定义、状态转移方程和边界条件。

石子合并问题是一个经典的动态规划问题,其目标是找到一种合并方式,使得合并石子的总代价最小。在这个问题中,一开始有一排石子,每个石子有一个权值,合并两个相邻的石子可以获得它们的权值之和作为代价。问题的关键在于如何安排合并的顺序,以获得最小的总代价。

下面是石子合并问题的一般步骤:

  1. 定义状态:通常使用一个二维数组 dp[i][j] 表示合并第 i 到第 j 个石子所需的最小代价。

  2. 状态转移方程:假设要合并第 i 到第 j 个石子,可以将它们分为两部分:第 ik 个石子和第 k+1j 个石子,其中 i <= k < j。那么合并的代价就是将这两部分合并的代价加上这两部分的权值之和。状态转移方程可以表示为:

    1
    dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum[i][j]), 其中 i <= k < j

    其中 sum[i][j] 表示第 i 到第 j 个石子的权值之和。

  3. 边界条件:当 i == j 时,只有一个石子,无需合并,所以 dp[i][i] = 0

  4. 计算顺序:通常从小规模问题开始,逐步计算出大规模问题的最优解。

下面是一个简单的石子合并问题的 Java 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class StoneMergeProblem {
public int mergeStones(int[] stones) {
int n = stones.length;
int[] prefixSum = new int[n + 1];
for (int i = 1; i <= n; i++) {
prefixSum[i] = prefixSum[i - 1] + stones[i - 1];
}

int[][] dp = new int[n][n];
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
dp[i][j] = Integer.MAX_VALUE;
for (int k = i; k < j; k++) {
dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k + 1][j] + prefixSum[j + 1] - prefixSum[i]);
}
}
}
return dp[0][n - 1];
}

public static void main(String[] args) {
StoneMergeProblem problem = new StoneMergeProblem();
int[] stones = {3, 1, 4, 2};
int minCost = problem.mergeStones(stones);
System.out.println("Minimum cost to merge stones: " + minCost);
}
}

在这个示例中,我们使用了动态规划来解决石子合并问题。首先计算了石子的前缀和,然后使用二维数组 dp 来存储合并石子的最小代价。根据状态转移方程,我们通过计算不同的区间长度和区间起始位置,逐步填充 dp 数组,最终得到合并所有石子的最小代价。

需要注意的是,石子合并问题的动态规划解法通常要结合问题的具体要求来确定状态转移方程,边界条件等。

环形动态规划(Circular Dynamic Programming)是一种动态规划的应用,适用于解决环形问题,即问题中的元素或状态在某种意义上是循环的,首尾相连的。这类问题在动态规划中需要考虑元素之间的相互关系,以及环形带来的特殊情况。

一般来说,环形动态规划的核心思想是将环形问题转化为线性问题,然后通过适当的状态转移方程和边界条件来求解。以下是环形动态规划的一般步骤:

  1. 问题转化:将环形问题转化为线性问题。可以通过复制一份原始数据,将环形结构“展开”成线性结构,从而将问题转化为已知的线性动态规划问题。

  2. 定义状态:定义动态规划的状态,通常与问题的特性相关。对于环形问题,需要考虑首尾相连的情况,可能需要多设置一个状态来表示“首尾相连”的状态。

  3. 确定状态转移方程:根据问题的性质,确定状态之间的转移关系。考虑环形结构,需要确保转移关系在环形首尾相连的情况下也能正确地进行。

  4. 确定边界条件:环形问题的边界条件可能相对复杂,需要确保首尾相连的元素能够正确处理。这些边界条件可能需要在状态转移方程中单独处理。

  5. 计算顺序:通常从小规模问题开始,按照线性结构的顺序逐步计算状态,直到计算出整个环形结构的状态。

下面以一个具体问题为例,来说明环形动态规划的应用。假设有一个环形数组,每个元素代表一个房屋,每个房屋中有一定数量的金钱。由于房屋是环形排列的,意味着第一个和最后一个房屋也相邻。要求在不偷相邻房屋的情况下,计算可以偷到的最大金钱数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CircularDPExample {
public int rob(int[] nums) {
int n = nums.length;
if (n == 0) {
return 0;
} else if (n == 1) {
return nums[0];
}

// Calculate the maximum amount that can be robbed if starting from the first house.
int[] dp1 = new int[n];
dp1[0] = nums[0];
dp1[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < n - 1; i++) {
dp1[i] = Math.max(dp1[i - 1], dp1[i - 2] + nums[i]);
}

// Calculate the maximum amount that can be robbed if starting from the second house.
int[] dp2 = new int[n];
dp2[1] = nums[1];
for (int i = 2; i < n; i++) {
dp2[i] = Math.max(dp2[i - 1], dp2[i - 2] + nums[i]);
}

// The maximum amount that can be robbed will be the maximum of the two scenarios.
return Math.max(dp1[n - 2], dp2[n - 1]);
}

public static void main(String[] args) {
CircularDPExample example = new CircularDPExample();
int[] nums = {2, 7, 9, 3, 1};
int maxAmount = example.rob(nums);
System.out.println("Maximum amount that can be robbed: " + maxAmount);
}
}

在这个例子中,我们通过定义两个状态数组 dp1dp2,分别表示从第一个房屋和第二个房屋开始偷窃时的最大金额。通过动态规划的思想,我们逐步计算出每个状态的最大金额,最终得到在不偷相邻房屋的情况下可以偷到的最大金钱数。

请注意,以上仅是环形动态规划的一个简单例子。实际应用中,问题可能会更加复杂,需要根据问题的具体情况来定义状态、状态转移方程和边界条件。

状压动态规划(Bitmask Dynamic Programming,状压DP)是一种优化技巧,通常用于解决组合优化问题,特别是集合的子集问题。以下是一个使用状压DP解决集合的最短路径问题的Java代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class BitmaskDP {

public static int shortestPath(int n, int[][] dist) {
int totalStates = 1 << n; // Total possible subsets

int[][] dp = new int[n][totalStates];
for (int[] row : dp) {
Arrays.fill(row, Integer.MAX_VALUE);
}
dp[0][1] = 0; // Starting state: only the first city visited

for (int mask = 1; mask < totalStates; mask++) {
for (int u = 0; u < n; u++) {
if ((mask & (1 << u)) != 0) { // Check if u is in the current subset
for (int v = 0; v < n; v++) {
if (v != u && (mask & (1 << v)) != 0) { // Check if v is in the current subset and not equal to u
dp[u][mask] = Math.min(dp[u][mask], dp[v][mask ^ (1 << u)] + dist[v][u]);
}
}
}
}
}

int minPath = Integer.MAX_VALUE;
for (int u = 1; u < n; u++) {
minPath = Math.min(minPath, dp[u][totalStates - 1] + dist[u][0]);
}

return minPath;
}

public static void main(String[] args) {
int n = 4; // Number of cities
int[][] dist = {
{0, 10, 15, 20},
{10, 0, 35, 25},
{15, 35, 0, 30},
{20, 25, 30, 0}
};

int shortest = shortestPath(n, dist);
System.out.println("Shortest Path: " + shortest);
}
}

在这个示例中,shortestPath 方法使用状压DP解决了集合的最短路径问题。算法通过构建一个二维数组 dp 来保存状态转移,从而计算最短路径。

请注意,状压DP通常用于解决集合的子集问题,例如在TSP(Traveling Salesman Problem)中寻找最短路径。实际应用中,可能需要根据问题的需求进行适当的修改。

深度优先

深度优先遍历各个节点,需要使用到栈(Stack)这种数据结构。stack的特点是是先进后出。整个遍历过程如下:

  1. 首先将A节点压入栈中,stack(A);

  2. 将A节点弹出,同时将A的子节点C,B压入栈中,此时B在栈的顶部,stack(B,C);

  3. 将B节点弹出,同时将B的子节点E,D压入栈中,此时D在栈的顶部,stack(D,E,C);

  4. 将D节点弹出,没有子节点压入,此时E在栈的顶部,stack(E,C);

  5. 将E节点弹出,同时将E的子节点I压入,stack(I,C);

…依次往下,最终遍历完成

public void depthFirst() {

    Stack<Map<String, Object>> nodeStack = new Stack<Map<String, Object>>();

    Map<String, Object> node = new HashMap<String, Object>();

    nodeStack.add(node);

    while (!nodeStack.isEmpty()) {

        node = nodeStack.pop();

        System.out.println(node);

        //获得节点的子节点,对于二叉树就是获得节点的左子结点和右子节点

        List<Map<String, Object>> children = getChildren(node);

        if (children != null && !children.isEmpty()) {

            for (Map child : children) {

                nodeStack.push(child);

            }

        }

    }

}

广度优先

广度优先遍历各个节点,需要使用到队列(Queue)这种数据结构,queue的特点是先进先出,其实也可以使用双端队列,区别就是双端队列首尾都可以插入和弹出节点。整个遍历过程如下:

  1. 首先将A节点插入队列中,queue(A);

  2. 将A节点弹出,同时将A的子节点B,C插入队列中,此时B在队列首,C在队列尾部,queue(B,C);

  3. 将B节点弹出,同时将B的子节点D,E插入队列中,此时C在队列首,E在队列尾部,queue(C,D,E);

  4. 将C节点弹出,同时将C的子节点F,G,H插入队列中,此时D在队列首,H在队列尾部,queue(D,E,F,G,H);

  5. 将D节点弹出,D没有子节点,此时E在队列首,H在队列尾部,queue(E,F,G,H);

…依次往下,最终遍历完成

public void breadthFirst() {

    Deque<Map<String, Object>> nodeDeque = new ArrayDeque<Map<String, Object>>();

    Map<String, Object> node = new HashMap<String, Object>();

    nodeDeque.add(node);

    while (!nodeDeque.isEmpty()) {

        node = nodeDeque.peekFirst();

        System.out.println(node);

        //获得节点的子节点,对于二叉树就是获得节点的左子结点和右子节点

        List<Map<String, Object>> children = getChildren(node);

        if (children != null && !children.isEmpty()) {

            for (Map child : children) {

                nodeDeque.add(child);

            }

        }

    }

}

下面是分别解决最长上升子序列、最大连续子序列和和最长公共子串问题的Java代码示例:

最长上升子序列(Longest Increasing Subsequence):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class LongestIncreasingSubsequence {

public static int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
int maxLen = 0;

for (int i = 0; i < n; i++) {
dp[i] = 1; // Minimum length is 1 (the element itself)
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxLen = Math.max(maxLen, dp[i]);
}

return maxLen;
}

public static void main(String[] args) {
int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
int lisLength = lengthOfLIS(nums);
System.out.println("Length of Longest Increasing Subsequence: " + lisLength);
}
}

最大连续子序列和(Maximum Subarray Sum):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MaximumSubarraySum {

public static int maxSubArraySum(int[] nums) {
int n = nums.length;
int maxSum = nums[0];
int currentSum = nums[0];

for (int i = 1; i < n; i++) {
currentSum = Math.max(nums[i], currentSum + nums[i]);
maxSum = Math.max(maxSum, currentSum);
}

return maxSum;
}

public static void main(String[] args) {
int[] nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
int maxSum = maxSubArraySum(nums);
System.out.println("Maximum Subarray Sum: " + maxSum);
}
}

最长公共子串(Longest Common Substring):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LongestCommonSubstring {

public static int longestCommonSubstring(String s1, String s2) {
int m = s1.length();
int n = s2.length();
int[][] dp = new int[m + 1][n + 1];
int maxLength = 0;

for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
maxLength = Math.max(maxLength, dp[i][j]);
}
}
}

return maxLength;
}

public static void main(String[] args) {
String s1 = "ABABC";
String s2 = "BABCBA";
int commonSubstringLength = longestCommonSubstring(s1, s2);
System.out.println("Length of Longest Common Substring: " + commonSubstringLength);
}
}

这些示例分别展示了如何解决最长上升子序列、最大连续子序列和和最长公共子串问题。实际应用中,根据问题的需求可能需要进行适当的修改。

数位动态规划(Digit Dynamic Programming,数位DP)是一种动态规划算法,用于解决涉及数字各位数的组合问题。以下是一个数位DP的Java代码示例,用于计算在区间 [1, N] 内,所有数字的各位之和等于给定值 K 的个数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class DigitDP {

public static int countNumbersWithDigitSum(int N, int K) {
int[][] dp = new int[N + 1][K + 1];
int MOD = (int) 1e9 + 7;

dp[0][0] = 1;

for (int i = 1; i <= N; i++) {
for (int j = 0; j <= K; j++) {
for (int d = 0; d <= 9; d++) {
if (j >= d) {
dp[i][j] = (dp[i][j] + dp[i - 1][j - d]) % MOD;
}
}
}
}

int result = 0;
for (int d = 1; d <= 9; d++) {
if (K >= d) {
result = (result + dp[N][K - d]) % MOD;
}
}

return result;
}

public static void main(String[] args) {
int N = 2;
int K = 2;

int count = countNumbersWithDigitSum(N, K);
System.out.println("Count of numbers with digit sum " + K + " in range [1, " + N + "]: " + count);
}
}

在这个示例中,countNumbersWithDigitSum 方法使用数位DP解决了计算各个数字各位数之和等于给定值 K 的数字个数问题。算法通过构建一个二维数组 dp 来保存状态转移,从而计算符合条件的数字个数。

这个示例是一个数位DP的实现,实际应用中可能需要根据不同的问题进行适当的修改。

插头DP(Socket DP)是一种动态规划算法,通常用于解决一些组合优化问题,如硬币找零问题、背包问题等。以下是一个插头DP的Java代码示例,用于解决硬币找零问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SocketDP {

public static int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;

for (int coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}

return dp[amount] > amount ? -1 : dp[amount];
}

public static void main(String[] args) {
int[] coins = {1, 2, 5};
int amount = 11;

int minCoins = coinChange(coins, amount);

if (minCoins == -1) {
System.out.println("It's impossible to make the amount with the given coins.");
} else {
System.out.println("Minimum number of coins to make amount " + amount + ": " + minCoins);
}
}
}

在这个示例中,coinChange 方法使用插头DP解决了硬币找零问题。算法通过构建一个一维数组 dp 来保存每个金额所需的最小硬币数量。通过逐步填表,我们可以找到构成给定金额的最少硬币数量。

这个示例是一个插头DP的实现,实际应用中可能需要根据不同的问题进行适当的修改。