动态规划的基本思想就是将待求解问题分解为若干子问题,(如本题中我们将dp[i][j]分解为若干dp[i-1][j-x]的问题),先求解这些子问题并将结果保存起来( 我们用dp[][]二维数组保存子结果),若在求解较大的问题时用到较小子问题的结果,可以直接取用(求dp[i][j]时用dp[i-1][x]的结果),从而免去重复计算
池塘抽样算法
1. 先将前k个作为结果
2. 然后用后面的数随机替换前面的数.
public int[] getRandom(int k, int n) {
Random random = new Random();
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = i+1;
}
for (int i = k; i < n; i++) {
int m = random.nextInt(i);
res[m] = i+1;
}
return res;
}
二维空间安全点
1. 将y按照从到小排序
2. 遍历数组 如果当前的x >= 前一个x则为结果.
class Point{
int x;
int y;
}
class MyCompare implements Comparator<Point> {
@Override
public int compare(Point o1, Point o2) {
if (o1.y > o2.y) return -1;
if (o1.y < o2.y) return 1;
if (o1.x > o2.x) return -1;
if (o1.x < o2.x) return 1;
return 0;
}
}
class MaxPoint {
public void help (List<Point> data) {
Collections.sort(data, new MyCompare());
int minx = -1;
for (Point point : data) {
if (point.x >= minx) {
System.out.println(point);
}
minx = point.x;
}
}
}
随机生成不重复的数
1. 先在0-n-1范围内随机生成一个数, 放入结果数组
2. 用最后一个数替换随机生成的数的位置
3. 下次在0-n-2范围内随机生成一个数,放入结果数组
public int[] help(int n, int k) {
int[] res = new int[k];
int[] data = new int[n];
for (int i = 0; i < n; i++) {
data[i] = i+1;
}
Random random = new Random();
for (int i = 0; i < k; i++) {
int index = random.nextInt(n - i);
res[i] = data[index];
data[index] = data[n-1-i];
}
return res;
}
组合硬币
给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求换钱有多少种方法
dp[i][j]表示前i种硬币组成和为j的组合数
public int coins3(int[] arr, int aim){
if(arr==null || arr.length==0 || aim<=0)
return 0;
// dp的维度 arr.length+1,多了一种不使用任何币种
int[][] dp = new int[arr.length+1][aim+1];
// aim=0的时候
for(int i=0;i<=arr.length;i++)
dp[i][0] = 1;
for(int i=1;i<=arr.length;i++){
for(int j=1;j<=aim;j++){
// k为arr[i-1]有多少张
for(int k=0;k<=j/arr[i-1];k++){
dp[i][j] += dp[i-1][j - k*arr[i-1]];
}
}
}
return dp[arr.length][aim];
}
迷宫搜索问题(https://www.cnblogs.com/python27/p/3989435.html)
该问题在实现给定一些01方块构成的迷宫,其中1表示该方块不能通过,0表示该方块可以通过,并且给定迷宫的入口和期待的出口,要求找到一条连接入口和出口的路径。有了前面的题目的铺垫,套路其实都是一样的。在当前位置,对于周围的所有方块,判断可行性,对于每一个可行的方块,就是我们当前所有可能的choices;尝试一个choice,递归的判断是否能够导致一个solution,如果可以,return true;否则,尝试另一个choice。如果所有的choice都不能导致一个成功解,return false。剩下的就是递归终止的条件,当前所在位置如果等于目标位置,递归结束,return true。
private static final directs_x = {0, -1, 0, 1};
private static final directs_y = {-1, 0, 1, 0};
private boolean help(int[][] grid, int curx, int cury) {
if (curx == targetX && cury == targetY) {
return true;
}
for (int i = 0; i < 4; i++) {
int nextX = curx + direct_x[i];
int nextY = cury + direct_y[I];
if (isSafe(grid, nextX, nextY)) {
//仅仅做一个标志, 没有任何实际意义
grid[nextX][nextY] = 2;
boolean success = help(grid, nextX, nextY);
if (success) return true;
//回溯为可通过的值, 也就是0;
grid[nextX][nextY] = 0;
}
}
return false;
}
骑士旅行问题
在一个N*M的棋盘上,在任意位置放置一个骑士,骑士的走"日字", 问该骑士能否不重复遍历整个棋盘
public boolean help(int[][] grid, int row, int col, int val) {
//无路可走
if (noWay(grid, row, col)) {
//棋盘已经走完
if (isFull()) {
return true;
}
return false;
}
for (int i = 0; i < 8; i++) {
int nextx = row + x;
int nexty = col + y;
//说明还在棋盘范围内
if (isOk(grid, row, col)) {
val++;
grid[row][col] = val;
boolean find = help(grid, nextx, nexty, val);
if (find) return true;
grid[row][col] = 0;
val--;
}
}
return false;
}
子集和问题
给定一个数组, 问: 是否存在子集和等于给定值.
动态规划解法:
public boolean subset(int[] a, int n){
if (a.length == 0) return false;
return help(a, n, a.length);
}
private boolean help (int[] a, int sum, int index) {
// //减到0 说明满足
// if (n == 0) return true;
// //index从后往前,已经到第0个 但是n还不等于0 说明不满足
// if (index == 0) return false;
// //最后一个数比目标值大, 则舍弃最后一个数
// if (a[index-1] > n) return help(a, n, index-1);
// //最后一个数比目标值小, 可以选择包含最后一个数 也可以不包含
// return help(a, n, index-1) || help(a, n-a[index-1], index-1);
//dp[i][j] 表示 前j个元素的和是否能组成i
boolean[][] dp = new boolean[sum+1][index+1];
//前0个元素肯定不能组成i
for (int i = 0; i <= sum; i++) {
dp[i][0] = false;
}
//和为0 一定可以组成
for (int i =0; i <= index; i++) {
dp[0][i] = true;
}
//i表示目标 j表示元素下标
for (int i = 1; i <= sum; i++) {
for (int j = 1; j <= index; j++) {
//如果目标 小于 最后j-1元素 则跳过下一个j元素
if (i < a[j-1]){
dp[i][j] = dp[i][j-1];
}else {
//目标大于j-1元素 可以选择包含j元素 也可以不包含
dp[i][j] = dp[i][j-1] || dp[i-a[j-1]][j-1];
}
}
}
//返回sum是否能被数组元素所组成
return dp[sum][index];
}
哈密顿图问题
哈密顿图是一个无向图,由天文学家哈密顿提出,由指定的起点前往指定的终点,途中经过所有其他节点且只经过一次。在图论中是指含有哈密顿回路的图,闭合的哈密顿路径称作哈密顿回路,含有图中所有顶点的路径称作哈密顿路径
/**
* 哈密顿图问题
* @param grid 邻接矩阵表示图中2顶点是否相连
* @param target
* @return
*/
public boolean hamCycle (int[][] grid, int target) {
int[] path = new int[grid[0].length];
boolean[] visits = new boolean[grid[0].length];
visits[0] = true;
path[0] = 0;
return help(grid, target, visits, path, 1);
}
private boolean help(int[][] grid, int target, boolean[] visits, int[] path, int cur) {
if (cur == target) {
//如果当前顶点是目标顶点 判断目标顶点是否和顶点0相连
return grid[path[cur-1]][0] == 1;
}
//从1开始 是因为除了顶点0 的其他顶点
for (int i = 1; i < target; i++) {
//grid[path[cur-1]][i] 表示上一个顶点和当前顶点是否相连
if (!visits[i] && grid[path[cur-1]][i] == 1) {
visits[i] = true;
path[cur] = i;
//递归进入下一个顶点
if (help(grid, target, visits, path, i+1)) return true;
visits[i] = false;
path[cur] = -1;
}
}
return false;
}
数独问题
9*9数独不仅要求每行每列数字各不相同,而且要求每个小九宫格中的数字各不相同
public boolean soduku(int[][] grid, int n) {
boolean isEmpty = true;
int x = grid.length;
int y = grid[0].length;
int row = -1, col = -1;
for (int i = 0; i < x; i++) {
for (int j = 0; j < y; j++) {
if (grid[i][j] == 0) {
isEmpty = false;
row = i; col = j;
break;
}
}
if (!isEmpty)break;
}
if (isEmpty) return true;
for (int i = 1; i <= n; i++) {
if (isSafe(grid, row, col, i)) {
grid[row][col] = i;
if (soduku(grid, n)) return true;
grid[row][col] = 0;
}
}
return false;
}
public boolean isSafe(int[][] grid, int row, int col, int i) {
for (int j = 0; j < grid.length; j++) {
if (grid[j][col] == i) return false;
}
for (int j = 0; j < grid[0].length; j++) {
if (grid[row][j] == i) return false;
}
int sqrt = (int) Math.sqrt(grid.length);
int rowStart = row - row % sqrt;
int colStart = col - col % sqrt;
for (int x = rowStart; x < rowStart + sqrt; x++) {
for (int y = colStart; y < colStart + sqrt; y++) {
if (grid[x][y] == i) return false;
}
}
return true;
}
拔河比赛
一群人要分成两队,每个人都有自己的体重,要求这两对人的体重的和相差最小,并且两队人要平均分配,最大允许误差为1,分别输出最后的两队人的体重和,小的在前
/**
* 拔河比赛
*
* @param nums 每个人的体重
*/
public void tugWar(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
int len = nums.length;
//dp[i][j]代表前i个人 在给定j体重范围内获得的最大体重和 可以先看这篇背包问题 https://blog.csdn.net/qq_38410730/article/details/81667885
int[][] dp = new int[len +1][sum+1];
for(int i = 0; i < len; i++) {
dp[i][0] = 0;
}
for (int i = 0; i < sum;i++) {
dp[0][i] = 0;
}
int m = len;
//一半人
if ((len & 1) == 0){
m /= 2;
}else {
m = m/2+1;
}
//最大总体重的一半
int n = sum / 2;
for (int i = 0; i < len; i++) {
for (int x = m; m > 0; m--) {
for (int y = n; y >= nums[i]; y--) {
//前x人在给定y体重范围内的最大体重和 = max(前x人y体重范围内的最大体重和, 前x-1人去掉第x人剩余体重范围内的总体重和+第x人的体重)
dp[x][y] = Math.max(dp[x][y], dp[x-1][y-nums[i]]+nums[i]);
}
}
}
//题目要求输出时 小的在前
int one = Math.min(dp[m][n], sum - dp[m][n]);
int another = sum-one;
System.out.println("一堆人的体重: " + one);
System.out.println("另一堆人的体重: " + another);
}
密码算法问题
A cryptarithmetic puzzle is a mathematical game where the digits of some numbers are represented by letters. Each letter represents a unique digit.
For example, a puzzle of the form:
SEND
+ MORE
--------
MONEY
may have the solution:
{'S': 9, 'E': 5, 'N': 6, 'D': 7, 'M': 1, 'O', 0, 'R': 8, 'Y': 2}
Given a three-word puzzle like the one above, create an algorithm that finds a solution.
对应解法: https://www.cnblogs.com/lz87/p/11306841.html
private int[] resole = new int[26];
public int[] playPuzzle(String word1, String word2, String word3) {
Arrays.fill(resole, -1);
boolean[] used = new boolean[10];
if (word3.length() > word1.length() && word3.length() > word2.length()) {
resole[word3.charAt(0) - 'A'] = 1;
used[1] = true;
}
dfs(word1, word2, word3, word1.length() - 1, word2.length() - 1, word3.length() - 1, 0, used);
return resole;
}
/**
*
* @param w1
* @param w2
* @param w3
* @param i1 目前处理的是第几列
* @param i2
* @param i3
* @param carry
* @param used
* @return
*/
private boolean dfs(String w1, String w2, String w3, int i1, int i2, int i3, int carry, boolean[] used) {
if (i3 < 0) {
if (carry == 0) {
return true;
} else {
return false;
}
}
//如果i1还没处理完 而且当前字符还没有对应的数字
if (i1 >= 0 && resole[w1.charAt(i1) - 'A'] < 0) {
for (int d = 0; d <= 9; d++) {
if (used[d]) {
continue;
}
//给w1的i1假设 对应的数字
resole[w1.charAt(i1) - 'A'] = d;
used[d] = true;
if (dfs(w1, w2, w3, i1, i2, i3, carry, used)) {
return true;
} else {
resole[w1.charAt(i1) - 'A'] = -1;
used[d] = false;
}
}
return false;
}
//如果i2还没处理完 而且当前字符还没有对应的数字
if (i2 >= 0 && resole[w2.charAt(i2) - 'A'] < 0) {
for (int d = 0; d <= 9; d++) {
if (used[d]) {
continue;
}
resole[w2.charAt(i2) - 'A'] = d;
used[d] = true;
if (dfs(w1, w2, w3, i1, i2, i3, carry, used)) {
return true;
} else {
resole[w2.charAt(i2) - 'A'] = -1;
used[d] = false;
}
}
return false;
}
//计算当前列的i1和i2对应数字的和
int sum = carry;
sum += i1 >= 0 ? resole[w1.charAt(i1) - 'A'] : 0;
sum += i2 >= 0 ? resole[w2.charAt(i2) - 'A'] : 0;
//如果word3当前列有对应的数字 而且和上面的和取模后的值相等
if (resole[w3.charAt(i3) - 'A'] >= 0 && sum % 10 == resole[w3.charAt(i3) - 'A']) {
//则进行下一列递归
return dfs(w1, w2, w3, i1 - 1, i2 - 1, i3 - 1, sum / 10, used);
//否则 如果word3当前列有对应的数字 但是与上面的和取模后的值不相等 或者上面的和取模后的值 已经被其他字母占用
} else if (resole[w3.charAt(i3) - 'A'] >= 0 || used[sum % 10]) {
return false;
}
//给word3的字母赋值对应的数字
used[sum % 10] = true;
resole[w3.charAt(i3) - 'A'] = sum % 10;
//进行下一列递归
if (dfs(w1, w2, w3, i1 - 1, i2 - 1, i3 - 1, sum / 10, used)) {
return true;
}
//如果失败则回溯
used[sum % 10] = false;
resole[w3.charAt(i3) - 'A'] = -1;
return false;
}
穷举递归
给你一个字符串, 求字符串重排列后的所有可能
private void help(String have, String remain) {
if (remain.length() == 0) {
System.out.println(have);
return;
}
for (int i = 0; i < remain.length(); i++) {
help(have+remain.charAt(i), remain.substring(0,i) + remain.substring(i+1));
}
}
矩阵最小路径和
给定一个矩阵m,从左上角开始每次只能向右或者向下走,最后达到右下角的位置,路径上所有的数字累加起来就是路径和,返回所有的路径中最小的路径和。如果给定的m如大小看到的样子,路径1,3,1,0,6,1,0是所有路径中路径和最小的,所以返回12。
1 3 5 9
8 1 3 4
5 0 6 1
8 8 4 0
dp[i][j] 代表从[0,0]到[i,j]的最小路径和
public int pathSum(int[][] m){
if(m==null || m.length==0 || m[0].length==0)
return 0;
int[][] dp = new int[m.length][m[0].length];
dp[0][0] = m[0][0];
for(int i=1;i<m.length;i++)
dp[i][0] = m[i][0] + dp[i-1][0];
for(int i=1;i<m[0].length;i++)
dp[0][i] = m[0][i] + dp[0][i-1];
for(int i=1;i<m.length;i++)
for(int j=1;j<m[0].length;j++)
dp[i][j] = m[i][j] + Math.min(dp[i][j-1], dp[i-1][j]);
return dp[m.length-1][m[0].length-1];
}
最长递增子序列
给定数组arr,返回arr的最长递增子序列长度。比如arr=[2,1,5,3,6,4,8,9,7],最长递增子序列长度为[1,3,4,8,9],所以返回这个子序列的长度5。
dp[i] 代表前i个元素的最长递增子序列长度 dp[j]表示i的前一个元素的的最长递增子序列的长度
class Solution {
public int lengthOfLIS(int[] nums) {
int res = 0;
int len = nums.length;
if (len == 0) return res;
int[] dp = new int[len];
//1个元素最长递增子序列就是1
Arrays.fill(dp, 1);
for (int i = 0; i < len; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
//dp[i] = 上个数的最长递增子序列长度+1
//因为最长递增子序列不一定连续 所以这里取最大值
//第二个dp[i]代表上一个dp[i]的值 然后在几个值中找到最大值
//通过选最大值, 避免了这种情况, 2,1,4,3,5,8,6,7 将2,4,5,8作为结果的错误判断
dp[i] = Math.max(dp[i], dp[j]+1);
}
}
//最长子序列不一定在最后一个元素, 因此取最大值
res = Math.max(res, dp[i]);
}
return res;
}
}
字符串最长公共子序列
给定两个字符串str1和str2,返回两个字符串的最长公共子序列。例如:str1=“1A2C3D4B56”,str2=“B1D23CA45B6A”,"123456"或者"12C4B6"都是最长公共子序列,返回哪一个都行。
dp[i][j] 代表str1的[0,i]字符和str2的[0,j]字符的最长公共子序列
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
int[][] dp = new int[m+1][n+1];
//i,j 代表长度 所以从1开始
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
//如果当前字符相等, 则dp[i] = dp[i-1]+1;
if (text1.charAt(i-1) == text2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1] + 1;
}else {
dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]);
}
}
}
return dp[m][n];
}
}
0-1背包问题
一个背包有一定的承重W,有N件物品,每件都有自己的价值,记录在数组v中,也都有自己的重量,记录在数组w中,每件物品只能选择要装入背包还是不装入背包,要求在不超过背包承重的前提下,选出物品的总价值最大。
dp[i][j]代表前i个物品 在j的容量下的最大价值
public int knapsack(int W, int[] w, int[] v){
if(W<=0 || w==null || w.length==0 || v==null || v.length==0)
return 0;
int n = w.length;
int[][] dp = new int[n+1][W+1];
for(int i=1;i<=n;i++)
for(int j=1;j<=W;j++)
if (j-w[i-1]>=0)
//max(最后一个不装, 最后一个也装) 的 最大价值
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-w[i-1]]+v[i-1]);
else
//最后一个不装
dp[i][j] = dp[i-1][j];
return dp[n][W];
}