目录
【案例1】
【题目描述】
【思路解析】
先通过遍历数组得到整个数组的最小值和最大值,将【最小值,最大值】这个闭区间划分为n+1个小区间,然后整个数组的n个数字一定会分布在这n+1个小区间中,可能某一个区间会含有多个数,但是这些数中的相邻差值一定小于一个区间的长度,然后因为是n+1个小区间,就一定会有个别区间中没有数字,这些空区间左右的两边相邻数字差一定会大于一个区间的长度。(即这n+1个小区间,因为有空区间,所以给出了一个平凡解(区间长度),所以我们不需要再考虑一定小于平凡解的解,所以我们不必求解相同区间的相邻数字的解)。我们只用记录每个区间的最小值和最大值,然后在不同区间中求解。
【代码实现】
/**
* @ProjectName: study3
* @FileName: Ex1
* @author:HWJ
* @Data: 2023/9/16 15:57
* 利用平凡解优化的技巧
*/
public class Ex1 {
public static void main(String[] args) {
}
public static int getMaxDeviation(int[] arr){
if (arr.length < 2) {
return -1; // 只有一个数字的数组,无法得到两个相邻数的差
}
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
int len = arr.length;
for (int i = 0; i < len; i++) {
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
if (max == min){ // 满足此条件表示整个数组为常数数组。
return 0;
}
boolean[] hasNum = new boolean[len + 1];
int[] maxs = new int[len + 1];
int[] mins = new int[len + 1];
int index = 0;
for (int i = 0; i < len; i++) {
index = getIndex(len + 1, arr[i], min, max);
maxs[index] = hasNum[index] ? Math.max(maxs[index], arr[i]) : arr[i];
mins[index] = hasNum[index] ? Math.min(mins[index], arr[i]) : arr[i];
hasNum[index] = true;
}
int lastMax = maxs[0];
int res = Integer.MIN_VALUE;
for (int i = 1; i < len + 1; i++) {
if(hasNum[i]){
res = Math.max(mins[i] - lastMax, res);
lastMax = maxs[i];
}
}
return res;
}
public static int getIndex(int len, int num, int min, int max){
return (int) (num - min) * len / (max - min);
}
}
【案例2】
【题目描述】
给出n个数字a1,……an,问如何将这个数组划分为多个部分,使得这个划分中异或和等于0的部分尽可能多。问最多是多少。
【题目描述】
对于任何一个i位置上的数字,只有两种可能性,(1)它在最优化分情况下能使从k位置到i位置的异或和等于0 ,(2)它在最优划分情况下不能使某一个部分的异或和等于0。所以我们需要解决如果i位置能使最优划分情况下从k位置到i位置的异或和等于0,我们怎么找到这个最近的k位置。我们可以在遍历时记录从0-i位置的异或和,异或和为m,则上一次达到m的位置是k-1。利用这个记录结构我们可以找到那些部分能够使异或和为0,然后再满足不重复的条件下,得到最优划分情况。
【代码实现】
import java.util.HashMap;
/**
* @ProjectName: study3
* @FileName: Ex2
* @author:HWJ
* @Data: 2023/9/16 16:31
*/
public class Ex2 {
public static void main(String[] args) {
int[] arr= {3,2,1,4,0,4,0,3,2,1};
System.out.println(getBestDivision(arr));
}
public static int getBestDivision(int[] arr){
int[] division = new int[arr.length];
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0, -1); // 初始化记录表
int xor = 0;
for (int i = 0; i < arr.length; i++) {
xor ^= arr[i];
if (map.containsKey(xor)){
int pre = map.get(xor);
division[i] = pre == -1 ? 1 : Math.max(division[pre] + 1, division[i - 1]);
}
map.put(xor, i);
}
return division[arr.length - 1];
}
}
【案例3】
【题目描述】
【思路解析】
这道题可以看作使用普通币完成a面值有x种方法,纪念币完成m-a面值有y种方法。然后对于这样使用普通币和纪念币完成m面值的方法为 x*y。所以我们使用两个动态规划分别得到普通币完成0……m面值的方法数,纪念币完成0……m面值的方法数。
对于普通币的动态规划可以进行斜率优化。
1 | 0 | 0 | 1 | 0 |
1 | ||||
1 |
对于上面的表格可以做是普通币动态规划需要填充的表格,对于上面表格dp[i, j]表示使用0……i种,完成 j 面值的方法数。假设有三种货币分别为3,2,1,需要完成的面值为4。第一列根据定义表示为,使用 i种货币完成0面值的方法数,即不使用币,均为1。第一行表示为使用3这个货币,完成 j面值的方法数,然后第二行表示为使用 2和3这两个货币,完成 j面值的方法数,对于这一行任一j位置,它依赖与dp[i-1][j] dp[i-1][j - 3] dp[i-1][j - 6].......对于j - 3依赖dp[i-1][j - 3] dp[i-1][j - 6].......所以我们对于任一j位置可以优化为dp[i][j] = dp[i-1][j ] + dp[i][j - 3] .
【代码实现】
/**
* @ProjectName: study3
* @FileName: Ex3
* @author:HWJ
* @Data: 2023/9/16 17:09
*/
public class Ex3 {
public static void main(String[] args) {
int[] arr1 = {2,3,4};
int[] arr2 = {2,3,1};
System.out.println(dpWays(arr1, arr2, 8));
}
public static int dpWays(int[] arr1, int[] arr2, int m){
int[][] dp1 = new int[arr1.length][m + 1];
int[][] dp2 = new int[arr2.length][m + 1];
for (int i = 0; i < arr1.length; i++) {
dp1[i][0] = 1;
}
for (int i = arr1[0]; i < m + 1; i += arr1[0]) {
dp1[0][i] = 1;
}
for (int i = 1; i < arr1.length; i++) {
for (int j = 1; j < m + 1; j++) {
dp1[i][j] = dp1[i - 1][j] + ((j - arr1[i]) >= 0 ? dp1[i][j - arr1[i]] : 0);
}
}
for (int i = 0; i < arr2.length; i++) {
dp2[i][0] = 1;
}
dp2[0][arr2[0]] = 1;
for (int i = 1; i < arr2.length; i++) {
for (int j = 1; j < m + 1; j++) {
dp2[i][j] = dp2[i - 1][j] + ((j - arr2[i]) >= 0 ? dp2[i - 1][j - arr2[i]] : 0);
}
}
int ans = 0;
for (int i = 0; i < m + 1; i++) {
ans += dp1[arr1.length - 1][i] * dp2[arr2.length - 1][m - i];
}
return ans;
}
}
【案例4】
【题目描述】
【思路解析1】
利用两个指针进行外排序,时间复杂度为O(K)。最没有营养的解法。
【思路解析2】
因为是两个排好序的数组,那一定可以通过二分查找。
有两种情况第k位的数在A中,第k位的数在B中。
每次在A数组进行二分找到一个数,然后通过这个数在B数组进行二分查找他应该在那个位置。这样我就知道这个数在A排第a + 1位,在B排第b + 1位,那么他在总数中排a+b+1位,通过a+b+1和k的大小关系,我们可以决定下一次二分查找的策略。如果最后在A中找到了,就返回这个数,否则,在B中进行一次这样的查找过程。时间复杂度为O(logM*logN)。
【思路解析3】
上中位数的定义,因为是两个等长度的数组的上中位数,则总长度一定为偶数,则假设总长度为4,a b c d。处于中间的有两个数b 和 c,我们认为b是上中位数,c是下中位数。
我们定义一个函数,他能返回两个数组中的上中位数,要求两个数组等长度。
现在开始解析这个函数的实现方法。这里给出奇数的分析方法,偶数更简单,可自行实现。
a | b | c | d | e |
1 | 2 | 3 | 4 | 5 |
每次分别找到这两个数组的中间的那个数字(a.lenth - 1)/2,如果c==3,则返回他们的数值,他们一定是两个数组的中位数,否则不妨设c>3,那我们现在来考虑有那些数可能成为上中位数,标蓝的就是可能成为上中位数的可能性。但是这样划分的话,我们就没有办法进行递归了,因为我们要求的是两个数组一定要等长,所以我们单独验证3是否为上中位数,排除后用a b 4 5进行递归。递归的上中位数就是总体的中位数。
好了现在我们有了这个函数后,我们进行总体可能性的划分。
长数组的长度为 n,短数组的长度为m
(1)k <=m。
我们直接选取长数组的前k个元素和短数组的前k个元素进行求解上中位数,返回值就是第k位的数。
(2) m < k <= n
当m < k <= n时我们一定能筛选出某些数字一定不能作为第k位,然后删除它们,剩下的两个数组可能不是等长的(长数组会多一个),但是我们可以单独验证个别数来使两个数组变为等长的,然后用这两个剩下的数组进行求解上中位数。
(3)k > n
当k>n时我们一定能筛选出某些数字一定不能作为第k位,然后删除它们,剩下的两个数组应该是等长的,然后用这两个剩下的数组进行求解下中位数。但是我们的函数是求上中位数的,所以我们对这两个等长的数组的第一个元素进行检验,然后再对剩下的两个数组求上中位数。
因为每次取出的数来递归的长度不会超过m,则时间复杂度为O(logM).
【代码实现 仅实现最优解法的代码 对数器使用的是方法1】
package AdvancedPromotion2;
import java.util.Arrays;
import java.util.Random;
/**
* @ProjectName: study3
* @FileName: Ex4
* @author:HWJ
* @Data: 2023/9/17 15:23
*/
public class Ex4 {
public static void main(String[] args) {
Comparator();
}
public static int getKDigitNum(int[] arr1, int[] arr2, int k){ // 这里保证k合法, 即在调用函数之前,验证k的合法性。
int n = Math.max(arr1.length, arr2.length);
int m = Math.min(arr1.length, arr2.length);
int[] longs = n == arr1.length ? arr1 : arr2;
int[] shorts = n == arr1.length ? arr2 : arr1;
if (k <= m){
return upperMedian(shorts, longs, 0, k - 1, 0, k - 1);
} else if (k <= n) {
int l = k - m;
if (longs[l - 1] > shorts[m - 1]){
return longs[l - 1];
}else {
return upperMedian(shorts, longs, 0, m - 1, l, k - 1);
}
} else {
int s = k - n;
int l = k - m;
if (longs[l - 1] >= shorts[m - 1]){
return longs[l - 1];
} else if (shorts[s - 1] >= longs[n - 1]) {
return shorts[s - 1];
}else {
return upperMedian(shorts, longs, s, m - 1, l , n - 1);
}
}
}
// 求上中位数的函数
public static int upperMedian(int[] arr1, int[] arr2, int s1, int e1, int s2, int e2) {
if (s1 == e1) { // 当两个数组只有一个数时,进入baseCase
return Math.min(arr1[s1], arr2[s2]);
}
int mid1 = (e1 - s1) / 2 + s1;
int mid2 = (e2 - s2) / 2 + s2;
if (arr1[mid1] == arr2[mid2]) {
return arr1[mid1];
} else if (arr1[mid1] > arr2[mid2]) {
if ((e1 - s1) % 2 != 0) { // 数组长度为偶数情况下
return upperMedian(arr1, arr2, s1, mid1, mid2 + 1, e2);
} else { // 数组长度为奇数情况下
if (arr2[mid2] >= arr1[mid1 - 1]) {
return arr2[mid2];
} else {
return upperMedian(arr1, arr2, s1, mid1 - 1, mid2 + 1, e2);
}
}
} else {
if ((e1 - s1) % 2 != 0) { // 数组长度为偶数情况下
return upperMedian(arr1, arr2, mid1 + 1, e1, s2, mid2);
} else { // 数组长度为奇数情况下
if (arr1[mid1] >= arr2[mid2 - 1]) {
return arr1[mid1];
} else {
return upperMedian(arr1, arr2, mid1 + 1, e1, s2, mid2 - 1);
}
}
}
}
public static int compare(int[] arr1, int[] arr2, int k){
int p1 = 0;
int p2 = 0;
int ans = 0;
while (p1 + p2 < k){
if (p1 != arr1.length && p2 != arr2.length){
if (arr1[p1] < arr2[p2]){
ans = arr1[p1++];
}else {
ans = arr2[p2++];
}
}else {
if (p1 == arr1.length){
ans = arr2[p2++];
}else {
ans = arr1[p1++];
}
}
}
return ans;
}
public static void Comparator(){
Random random = new Random();
int times = 50000;
int size = 1000;
int max = 10000;
for (int i = 0; i < times; i++) {
int[] arr1 = new int[random.nextInt(size) + 100];
int[] arr2 = new int[random.nextInt(size) + 100];
for (int j = 0; j < arr1.length; j++) {
arr1[j] = random.nextInt(max);
}
for (int j = 0; j < arr2.length; j++) {
arr2[j] = random.nextInt(max);
}
int k = random.nextInt(arr1.length + arr2.length) + 1;
Arrays.sort(arr1);
Arrays.sort(arr2);
int ans1 = getKDigitNum(arr1, arr2, k);
int ans2 = compare(arr1, arr2, k);
if (ans1 != ans2){
System.out.println("失败!!!");
System.out.println(ans1 + " " + ans2 + " " + k);
System.out.println(Arrays.toString(arr1));
System.out.println(" ------------------ ");
System.out.println(Arrays.toString(arr2));
break;
}
}
System.out.println("成功!!!");
}
}