练习二

题目一(来自华为OD)

给定一个数组arr,长度为n,表示n个格子的分数,并且这些格子首尾相连,孩子不能选相邻的格子,不能回头选,不能选超过一圈,但是孩子可以决定从任何位置开始选,也可以什么都不选,返回孩子能获得的最大分值。

public static int getMaxScore(int[] arr) {
    if (arr == null || arr.length == 0) {
        return 0;
    }
    if (arr.length == 1) {
        return arr[0];
    }
    int f1 = process(arr, 1, 0, 1);       //0位置的数没有被选择,所以从1位置开始,最后一个位置也可以考虑选择
    int f2 = arr[0] + process(arr, 1, 1, 0);   //0位置的数被选择,从1位置开始,最后一个位置不可以考虑
    return Math.max(f1, f2);
}

/**
 * 得到最大的分数值
 *
 * @param arr 所给数组
 * @param i   从i位置开始
 * @param pre i位置的前面一个数是否选择了,若被选择了,pre为1
 * @param end n-1位置的数能否被考虑,若可以考虑,end为1
 * @return 返回最大分数
 */
public static int process(int[] arr, int i, int pre, int end) {
    if(i==arr.length-1) {        //到数组的最后位置了
        if (pre == 1 || end == 0) {     //如果前一个数字被选择了或者最后一个位置不能考虑,返回0
            return 0;
        } else {
            return arr[i];
        }
    }else {
        //i位置还没到最后
        //可能性1:不要i位置
        int p1=process(arr,i+1,0,end);
        //可能性2:要i位置
        int p2=0;
        if(pre==0){
            p2=arr[i]+process(arr,i+1,1,end);
        }
        return Math.max(p1,p2);
    }
}

题目二(来自华为OD)

image

package leetcode;


public class leetcode1234 {
    public static int balancedString(String s) {
        int[] arr = new int[s.length()];      //用于将四个字符转换成0,1,2,3
        int[] num = new int[4];       //用于统计在滑动窗口外各个字符出现的次数
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            arr[i] = c == 'Q' ? 0 : (c == 'W' ? 1 : (c == 'E' ? 2 : 3));        //将Q->0,W->1,E->2,R->3
            num[arr[i]]++;
        }
        if (num[0] == num[1] && num[1] == num[2] && num[2] == num[3]) {     //如果初始4个字符的次数相等,本身就是一个平衡字符串,直接返回0
            return 0;
        }
        int ans = s.length();
        int length = s.length();
        for (int l = 0, r = 0; l < length; l++) {
            while (!ok(num, l, r) && r < length) {  //当前l...r范围内不能满足,且r还没有到末尾,将r右移,则滑动窗口外该字符次数-1
                num[arr[r++]]--;
            }
            if (ok(num, l, r)) {    //如果当前l...r范围内满足,若长度小于ans,则更新ans
                ans = Math.min(r - l, ans);
            } else {  //执行到此处只有可能r已经到了末尾,但是还是不能满足,则直接退出循环
                break;
            }
            num[arr[l]]++;      //若当前l....r范围满足了,尝试将l向右移,看看有没有更短的范围,所以l指向的字符次数+1
        }
        return ans;
    }

    /**
     * 在[l....r)的范围上(左闭右开)修改能不能达到平衡字符串,类似于一个滑动窗口
     *
     * @param num 词频表
     * @param l   左边界
     * @param r   右边界
     * @return 返回能不能达到平衡
     */
    public static boolean ok(int[] num, int l, int r) {
        int max = Math.max(Math.max(num[0], num[1]), Math.max(num[2], num[3]));      //找到4个字符中出现次数最多的,则其他小于该次数的字符都需要通过改变l....r范围里的字符达到平衡
        int need = 4 * max - num[0] - num[1] - num[2] - num[3];      //将其余字符出现的次数都达到max需要多少范围
        int rest = r - l - need;      //全部字符都达到max后,还剩余多少范围
        return rest % 4 == 0 && rest >= 0;      //如果剩余的次数大于等于0并且对4取余后为0,则说明l...r范围能够使字符串达到平衡
    }


    public static void main(String[] args) {
        String s = "QQQW";
        System.out.println(balancedString(s));
    }
}

题目三(来自华为OD)

分裂乘积最大问题。一个数字n,一定要分成k份,得到的乘积尽量大是多少。

//暴力递归
public static int maxValue1(int n, int k) {
    if (k == 0 || n < k) {
        return -1;
    }
    return process1(n, k);
}

//剩余的数字rest,一定要拆成j份,返回最大乘积
public static int process1(int rest, int j) {
    if (j == 1) {
        return rest;
    }
    int ans = Integer.MIN_VALUE;
    for (int cur = 1; cur <= rest && (rest - cur) >= (j - 1); cur++) {
        int curAns = cur * process1(rest - cur, j - 1);
        ans = Math.max(ans, curAns);
    }
    return ans;
}

//贪心的解
public static int maxValue2(int n, int k) {
    if (k == 0 || n < k) {
        return -1;
    }
    int a = n / k;
    int b = n % k;
    int ans = 1;
    for (int i = 0; i < b; i++) {
        ans *= a + 1;
    }
    for (int i = 0; i < k - b; i++) {
        ans *= a;
    }
    return ans;
}

//贪心的解,这是最优解
//但是如果结果很大,让求余数
public static int maxValue3(long n, long k) {
    if (k == 0 || n < k) {
        return -1;
    }
    int mod = 1000000007;
    long a = n / k;
    long b = n % k;
    long part1 = power(a + 1, b, mod);
    long part2 = power(a, k - b, mod);
    return (int) (part1 * part2) % mod;
}

//返回a的n次方%mod的结果
public static long power(long a, long n, int mod) {
    long ans=1;
    long tmp=a;
    while(n!=0){
        if((n&1)!=0){
            ans=(ans*tmp)%mod;
        }
        n>>=1;
        tmp=(tmp*tmp)%mod;
    }
    return ans;
}

题目四(来自华为OD)

一个图像有n个像素点,存储在一个长度为n的数组arr里,每个像素点的取值范围[0, s]的整数。
请你给图像每个像素点值加上一个整数k(可以是负数),像素值会自动截取到[0, s]范围。
当像素值<0,会更改为0,当新像素值>s,会更改为s。
这样就可以得到新的arr,想让所有像素点的平均值最接近中位值s/2,向下取整请输出这个整数k,如有多个整数k都满足最接近,输出小的那个。
1<= n<=10^6
1<=s<=10^18
package interview;

import java.util.Arrays;


public class problem04 {
    // 来自华为OD,学员问题
    // 一个图像有n个像素点,存储在一个长度为n的数组arr里,
    // 每个像素点的取值范围[0,s]的整数
    // 请你给图像每个像素点值加上一个整数k(可以是负数)
    // 像素值会自动截取到[0,s]范围,
    // 当像素值<0,会更改为0,当新像素值>s,会更改为s
    // 这样就可以得到新的arr,想让所有像素点的平均值最接近中位值s/2, 向下取整
    // 请输出这个整数k, 如有多个整数k都满足, 输出小的那个
    // 1 <= n <= 10^6
    // 1 <= s <= 10^18

    //暴力方法
    public static int best1(int[] arr, int s) {
        int half = s / 2;   //中位值
        int average = Integer.MIN_VALUE;      //数组的平均值
        int ans = -s;
        for (int i = -s; i < s; i++) {
            int curAverage = average1(arr, i, s);
            if (Math.abs(half - curAverage) < Math.abs(half - average)) {   //如果离s/2更近,则更新average和ans
                average = curAverage;
                ans = i;
            }
        }
        return ans;
    }

    //计算数组的平均值
    public static int average1(int[] arr, int i, int s) {
        int sum = 0;
        for (int num : arr) {
            int v = num + i;
            if (v < 0) {
                sum += 0;
            } else {
                sum += Math.min(v, s);
            }
        }
        return sum / arr.length;
    }

    //优化一下,但不是最优解
    public static int best2(int[] arr, int s) {
        int l = -s;
        int r = s;
        int m = 0;
        int half = s / 2;
        int average = -s;
        int ans = 0;
        while (l <= r) {    //二分法查找k
            m = (l + r) / 2;
            int curAverage = average1(arr, m, s);
            if ((Math.abs(half - curAverage) < Math.abs(half - average)) || ((Math.abs(half - curAverage) == Math.abs(half - average)) && m < ans)) {  //如果距离s/2更近,或者距离s/2一样,但是m更小
                average = curAverage;
                ans = m;
            }
            if (curAverage >= half) {
                r = m - 1;
            } else {
                l = m + 1;
            }
        }
        return ans;
    }

    //最优方法,O(N * logN) + O(logS *  logN)
    public static int best3(int[] arr, int s) {
        Arrays.sort(arr);   //数组排序,从小到大
        int[] sum = new int[arr.length];  //记录前缀和
        sum[0] = arr[0];
        for (int i = 1; i < arr.length; i++) {
            sum[i] = sum[i - 1] + arr[i];
        }
        int l = -s;
        int r = s;
        int m = 0;
        int half = s / 2;
        int average = -s;
        int ans = 0;
        while (l <= r) {
            m = (l + r) / 2;
            int curAverage = average3(arr, sum, m, s);
            if ((Math.abs(half - curAverage) < Math.abs(half - average)) || ((Math.abs(half - curAverage) == Math.abs(half - average)) && m < ans)) {  //如果距离s/2更近,或者距离s/2一样,但是m更小
                average = curAverage;
                ans = m;
            }
            if (curAverage >= half) {
                r = m - 1;
            } else {
                l = m + 1;
            }
        }
        return ans;
    }

    public static int average3(int[] arr, int[] pre, int k, int s) {
        int n = arr.length;
        if (k < 0) {
            int l = bsZero(arr, k);
            int sum = rangeSum(pre, l + 1, n - 1);
            return (sum + (k * (n - l - 1))) / n;
        } else {
            int r = bsS(arr, k, s);
            int sum = rangeSum(pre, 0, r - 1);
            return (sum + (k * r) + (s * (n - r))) / n;
        }
    }

    //利用二分法找到arr数组中arr[i]+k<=0最靠右的那个数的位置
    public static int bsZero(int[] arr, int k) {
        int ans = -1;
        int l = 0;
        int r = arr.length - 1;
        int m;
        while (l <= r) {
            m = (l + r) / 2;
            if (arr[m] + k <= 0) {
                ans = m;
                l = m + 1;
            } else {
                r = m - 1;
            }
        }
        return ans;
    }

    //利用二分法找到arr数组中arr[i]+k>=s最靠左的那个数的位置
    public static int bsS(int[] arr, int k, int s) {
        int ans = arr.length;
        int l = 0;
        int r = arr.length - 1;
        int m;
        while (l <= r) {
            m = (l + r) / 2;
            if (arr[m] + k >= s) {
                ans = m;
                r = m - 1;
            } else {
                l = m + 1;
            }
        }
        return ans;
    }

    //返回l....r范围上的和
    public static int rangeSum(int[] pre, int l, int r) {
        if (l > r) {
            return 0;
        }
        return l == 0 ? pre[r] : (pre[r] - pre[l - 1]);
    }

    // 为了测试
    public static int[] randomArray(int n, int s) {
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = (int) (Math.random() * (s + 1));
        }
        return arr;
    }

    // 为了测试
    public static void main(String[] args) {
        int N = 50;
        int S = 500;
        int testTimes = 10000;
        System.out.println("测试开始");
        for (int i = 0; i < testTimes; i++) {
            int n = (int) (Math.random() * N) + 1;
            int s = (int) (Math.random() * S) + 1;
            int[] arr = randomArray(n, s);
            int ans1 = best1(arr, s);
            int ans2 = best2(arr, s);
            int ans3 = best3(arr, s);
            if (ans1 != ans2 || ans1 != ans3) {
                System.out.println("出错了!");
            }
        }
        System.out.println("测试结束");
    }
}

题目五(来自华为OD)

保证一定是nn的正方形,实现从里到外转圈打印的功能
如果n是奇数,中心点唯一,比如
a b c
d e f
g h i
e是中心点,依次打印 : e f i h g d a b c
如果n是偶数,中心点为最里层2
2的右下点
比如
a b c d e f
g h i j k l
m n o p q r
s t u v w x
y z 0 1 2 3
4 5 6 7 8 9
最里层是
o p
u v
v是中心点,依次打印 : v u o p q w ....

public static void print(char[][] m) {
    int n = m.length;
    for (int a = (n - 1) / 2, b = (n - 1) / 2, c = n / 2, d = n / 2; a >= 0; a--, b--, c++, d++) {      //a和b是左上角点的坐标,c和d是右下角点的坐标
        loop(m, a, b, c, d);
    }
    System.out.println();
}

public static void loop(char[][] m, int a, int b, int c, int d) {
    if(a==c){       //左上角和右下角重合了,说明只有一个点了
        System.out.print(m[a][b]+" ");
    }else{
        for(int row=a+1;row<=c;row++){      //打印右边
            System.out.print(m[row][d]+" ");
        }
        for(int col=d-1;col>=b;col--){      //打印下边
            System.out.print(m[c][col]+" ");
        }
        for(int row=c-1;row>=a;row--){      //打印左边
            System.out.print(m[row][b]+" ");
        }
        for(int col=b+1;col<=d;col++){      //打印上边
            System.out.print(m[a][col]+" ");
        }
    }
}

public static void main(String[] args) {
    char[][] map1 = {
            { 'a' }
    };
    print(map1);

    char[][] map2 = {
            { 'a', 'b' },
            { 'c', 'd' }
    };
    print(map2);

    char[][] map3 = {
            { 'a', 'b', 'c' },
            { 'd', 'e', 'f' },
            { 'g', 'h', 'i' }
    };
    print(map3);

    char[][] map4 = {
            { 'a', 'b', 'c', 'd' },
            { 'e', 'f', 'g', 'h' },
            { 'i', 'j', 'k', 'l' },
            { 'm', 'n', 'o', 'p' }
    };
    print(map4);

    char[][] map5 = {
            { 'a', 'b', 'c', 'd', 'e' },
            { 'f', 'g', 'h', 'i', 'j' },
            { 'k', 'l', 'm', 'n', 'o' },
            { 'p', 'q', 'r', 's', 't' },
            { 'u', 'v', 'w', 'x', 'y' }
    };
    print(map5);

    char[][] map6 = {
            { 'a', 'b', 'c', 'd', 'e', 'f' },
            { 'g', 'h', 'i', 'j', 'k', 'l' },
            { 'm', 'n', 'o', 'p', 'q', 'r' },
            { 's', 't', 'u', 'v', 'w', 'x' },
            { 'y', 'z', '0', '1', '2', '3' },
            { '4', '5', '6', '7', '8', '9' },
    };
    print(map6);
}

题目六(来自华为)

image

package leetcode;

import java.util.Arrays;
import java.util.TreeMap;

public class leetcode2071 {

    // 第一种方法,二分答案法。完成工作的任务数量范围在0~tasks.length-1,判断在这个范围内的任务数量需要多少个大力丸才能完成,若所需大力丸数量小于等于题目所给,则这是个候选解,往右查找看看能不能完成更多的任务。
    // 利用二分答案法的前提是要有单调性,例如本题中如果完成4个任务需要1个大力丸,那么完成5个任务的时候,需要大力丸的数量一定是大于等于1个的。
    // 时间复杂度O(n*(logn)^2)
    public static int maxTaskAssign1(int[] tasks, int[] workers, int pills, int strength) {
        int l = 0;    //二分答案的左边界
        int r = tasks.length;     //二分答案的右边界
        int m, ans = 0;            //m所需完成任务数,如果要完成m个任务,则选取最小工作量的m个任务,选取最厉害的m个工人
        Arrays.sort(tasks);     //对任务所需的力量数升序排列
        Arrays.sort(workers);   //对工人力量数升序排列
        while (l <= r) {
            m = (l + r) >> 1;     //当前要完成m个任务
            //需要完成[0...m-1]范围上的任务,选择的是[workers.length - m, workers.length - 1]范围上的工人
            if (yeah1(tasks, 0, m - 1, workers, workers.length - m, workers.length - 1, strength) <= pills) {    //所需大力丸少于题目所给即是一个备选答案
                ans = m;
                l = m + 1;
            } else {
                r = m - 1;
            }
        }
        return ans;
    }

    //返回需要多少的大力丸
    public static int yeah1(int[] tasks, int tl, int tr, int[] workers, int wl, int wr, int strength) {
        if (wl < 0) {       //如果没有m个工人,返回最大值,说明完不成m个任务
            return Integer.MAX_VALUE;
        }
        TreeMap<Integer, Integer> taskMap = new TreeMap<>();   //存储相同工作量的任务数量
        for (int i = tl; i <= tr; i++) {
            taskMap.put(tasks[i], taskMap.getOrDefault(tasks[i], 0) + 1);
        }
        int ans = 0;
        for (int i = wl; i <= wr; i++) {
            Integer job = taskMap.floorKey(workers[i]);     //获得小于等于当前工人力量最近的一个任务
            if (job != null) {      //如果job不为空,说明存在这样的任务
                int times = taskMap.get(job); //获得当前任务的数量
                if (times - 1 == 0) {     //如果当前任务数量只有1个,被当前工人做了之后就没当前任务了,从taskMap中删除当前任务
                    taskMap.remove(job);
                } else {        //如果当前任务数量有多个,对taskMap更新数量即可
                    taskMap.put(job, taskMap.get(job) - 1);
                }
            }else{      //job为空,说明当前工人一个任务都完不成,需要吃大力丸了
                int change_strength = workers[i] + strength;        //吃了大力丸之后拥有的力量
                ans+=1;
                Integer job2 = taskMap.floorKey(change_strength);
                if(job2==null){     //如果吃了大力丸之后还是一个任务都完不成,返回最大值,说明完不成m个任务
                    return Integer.MAX_VALUE;
                }else{
                    int times = taskMap.get(job2); //获得当前任务的数量
                    if (times - 1 == 0) {     //如果当前任务数量只有1个,被当前工人做了之后就没当前任务了,从taskMap中删除当前任务
                        taskMap.remove(job2);
                    } else {        //如果当前任务数量有多个,对taskMap更新数量即可
                        taskMap.put(job2, taskMap.get(job2) - 1);
                    }
                }
            }
        }
        return ans;
    }

    //滑动窗口法
    //总体思路:新建一个双端队列,遍历到一个工人,如果队列为空,把他能做的任务从尾部插入队列,从头部弹出一个任务给当前工人做,如果当前工人没有任务可做,则需要嗑药,把嗑完药之后能做的任务加入队列,从尾部弹出一个任务给当前工人做
    public static int maxTaskAssign(int[] tasks, int[] workers, int pills, int strength) {
        int[] help=new int[tasks.length];       //模拟双端队列
        int l=0;
        int r=tasks.length;
        int m,ans=0;
        Arrays.sort(tasks);
        Arrays.sort(workers);
        while(l<=r){
            m=(l+r)>>1;
            if(yeah(tasks,0,m-1,workers,workers.length-m,workers.length-1,strength,help)<=pills){
                ans=m;
                l=m+1;
            }else{
                r=m-1;
            }
        }
        return ans;
    }

    //返回需要多少的大力丸
    private static int yeah(int[] tasks, int tl, int tr, int[] workers, int wl, int wr, int strength, int[] help) {
        if(wl<0){
            return Integer.MAX_VALUE;
        }
        int l=0;    //双端队列的指针
        int r=0;    //双端队列的指针
        int ti=tl;
        int ans=0;
        for(int wi=wl;wi<=wr;wi++){
            for(;ti<=tr&&tasks[ti]<=workers[wi];ti++){      //把当前这个工人能做的所有任务从尾部加入到双端队列中
                help[r++]=ti;
            }
            if(l<r&&tasks[help[l]]<=workers[wi]){   //如果队列头部的这个任务所需的力量小于等于当前工人所拥有的力量,则把头部这个任务交给当前工人做,左指针向后移,相当于把当前任务弹出了
                l++;
            }else{      //如果队列头部的这个任务所需的力量大于当前工人所拥有的力量,就需要嗑药了
                for(;ti<=tr&&tasks[ti]<=workers[wi]+strength;ti++){     //把嗑完药之后能做的任务都加入队列
                    help[r++]=ti;
                }
                if(l<r){        //嗑药数加一,从尾部弹出任务
                    ans++;
                    r--;
                }else{
                    return Integer.MAX_VALUE;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        int[] tasks = {3, 2, 1};
        int[] workers = {0, 3, 3};
        int pills = 1;
        int strength = 1;
        System.out.println(maxTaskAssign(tasks, workers, pills, strength));
    }
}

题目七(来自神策)

给定一个数组arr,表示连续n天的股价,数组下标表示第几天
指标X:任意两天的股价之和-此两天间隔的天数
比如:第3天,价格是10;第9天,价格是30,那么第3天和第9天的指标X=10+30-(9-3)=34
返回arr中最大的指标X

public static int maxX(int[] arr){
    if(arr==null||arr.length<2){
        return -1;
    }
    int preBest=arr[0]; //当前最大值是0+arr[0]
    int ans=0;
    for(int i=1;i<arr.length;i++){
        ans=Math.max(ans,arr[i]-i+preBest);
        preBest=Math.max(preBest,arr[i]+i);
    }
    return ans;
}

题目八(来自美团)

小团生日收到妈妈送的两个一模一样的数列作为礼物!
他很开心的把玩,不过不小心没拿稳将数列摔坏了!
现在他手上的两个数列分别为A和B,长度分别为n和m。
小团很想再次让这两个数列变得一样。他现在能做两种操作:
操作一是将一个选定数列中的某一个数a改成数b,这会花费|b-a|的时间,
操作二是选择一个数列中某个数a,将它从数列中丢掉,花费|a|的时间。
小团想知道,他最少能以多少时间将这两个数列变得再次相同!
输入描述:
第一行两个空格隔开的正整数n和m,分别表示数列A和B的长度。
接下来一行n个整数,分别为A1 A2…An
接下来一行m个整数,分别为B1 B2…Bm
对于所有数据,1 ≤ n,m ≤ 2000, |Ai|,|Bi| ≤ 10000
输出一行一个整数,表示最少花费时间,来使得两个数列相同。

//暴力递归
public static int minCost1(int[] A, int[] B) {
    return change1(A, B, 0, 0);
}

public static int change1(int[] A, int[] B, int indexA, int indexB) {
    if (indexA == A.length && indexB == B.length) {     //如果两个数组都到了最后,则花费时间为0
        return 0;
    }
    if (indexA != A.length && indexB == B.length) {     //如果A数组还没到最后,但是B数组已经到最后了,只能删除A数组中的值
        return A[indexA] + change1(A, B, indexA + 1, indexB);
    }
    if (indexA == A.length) {     //如果A数组到最后,但是B数组还没到最后,只能删除B数组中的值
        return B[indexB] + change1(A, B, indexA, indexB + 1);
    }
    //剩下的情况是两个数组都还没到最后
    //可能性1:删掉A[indexA]
    int p1 = A[indexA] + change1(A, B, indexA + 1, indexB);
    //可能性2:删掉B[indexB]
    int p2 = B[indexB] + change1(A, B, indexA, indexB + 1);
    //可能性3:同时删除掉A[indexA]和B[indexB],该可能性被可能性1和可能性2包含
//        int p3=A[indexA]+B[indexB]+change(A,B,indexA+1,indexB+1);
    //可能性4:将A[indexA]变成B[indexB],或者将B[indexB]变成A[indexA]
    int p4 = Math.abs(A[indexA] - B[indexB]) + change1(A, B, indexA + 1, indexB + 1);
    //可能性5:A[indexA]==B[indexB],不需要动。该可能性被可能性4包含
    return Math.min(p1, Math.min(p2, p4));
}

//记忆化搜索
public static int minCost2(int[] A, int[] B) {
    int n = A.length;
    int m = B.length;
    int[][] dp = new int[n + 1][m + 1];
    for (int i = 0; i < dp.length; i++) {
        Arrays.fill(dp[i], -1);
    }
    return change2(A, B, 0, 0, dp);
}

public static int change2(int[] A, int[] B, int indexA, int indexB, int[][] dp) {
    if (indexA == A.length && indexB == B.length) {     //如果两个数组都到了最后,则花费时间为0
        return 0;
    }
    if (dp[indexA][indexB] != -1) {
        return dp[indexA][indexB];
    }
    int ans = 0;
    if (indexA != A.length && indexB == B.length) {
        ans = A[indexA] + change1(A, B, indexA + 1, indexB);
    } else if (indexA == A.length) {     //如果A数组到最后,但是B数组还没到最后,只能删除B数组中的值
        ans = B[indexB] + change1(A, B, indexA, indexB + 1);
    } else {
        int p1 = A[indexA] + change2(A, B, indexA + 1, indexB, dp);
        int p2 = B[indexB] + change2(A, B, indexA, indexB + 1, dp);
        int p45 = Math.abs(A[indexA] - B[indexB]) + change2(A, B, indexA + 1, indexB + 1, dp);
        ans = Math.min(Math.min(p1, p2), p45);
    }
    dp[indexA][indexB] = ans;
    return dp[indexA][indexB];
}

//动态规划
public static int minCost3(int[] A, int[] B) {
    int m = A.length;
    int n = B.length;
    int[][] dp = new int[m + 1][n + 1];
    //初始化最后一列和最后一行,最后一个格子是0
    dp[m][n] = 0;
    for (int i = m - 1; i >= 0; i--) {        //初始化最后一列
        dp[i][n] = A[i] + dp[i + 1][n];
    }
    for (int i = n - 1; i >= 0; i--) {    //初始化最后一行
        dp[m][i] = B[i] + dp[m][i + 1];
    }
    //从下往上,从右往左填
    for (int i = m - 1; i >= 0; i--) {
        for (int j = n - 1; j >= 0; j--) {
            dp[i][j]=Math.min(A[i]+dp[i+1][j],Math.min(B[j]+dp[i][j+1],Math.abs(A[i]-B[j])+dp[i+1][j+1]));
        }
    }
    return dp[0][0];
}

public static int[] generateArray(int n){
    int[] arr=new int[n];
    Random r=new Random();
    for(int i=0;i<n;i++){
        arr[i]=r.nextInt(100)+1;
    }
    return arr;
}

public static void printArr(int[] arr){
    for (int i : arr) {
        System.out.print(i+"  ");
    }
    System.out.println();
}

public static void main(String[] args) {
    for(int i=0;i<1000;i++){
        int[] A=generateArray(7);
        int[] B=generateArray(8);
        if(minCost1(A,B)!=minCost2(A,B)||minCost1(A,B)!=minCost3(A,B)||minCost2(A,B)!=minCost3(A,B)){
            printArr(A);
            printArr(B);
            System.out.println("failed");
            break;
        }
        System.out.println("continue");
    }
    System.out.println("succeed");
}

题目九(来自美团)

来自美团
8.20笔试
题目2
小团在地图上放了3个定位装置,想依赖他们进行定位!
地图是一个n*n的棋盘,
有3个定位装置(x1,y1),(x2,y2),(x3,y3),每个值均在[1,n]内。
小团在(a,b)位置放了一个信标,
每个定位装置会告诉小团它到信标的曼哈顿距离,也就是对于每个点,小团知道|xi-a|+|yi-b|
求信标位置,信标不唯一,输出字典序最小的。
输入n,然后是3个定位装置坐标,
最后是3个定位装置到信标的曼哈顿记录。
输出最小字典序的信标位置。
1 <= 所有数据值 <= 50000

/**
 * 找到符合条件的信标
 *
 * @param n  n大小的棋盘
 * @param a  第一个定位装置的位置
 * @param b  第二个定位装置的位置
 * @param c  第三个定位装置的位置
 * @param ad 信标到第一个定位装置的曼哈顿距离
 * @param bd 信标到第二个定位装置的曼哈顿距离
 * @param cd 信标到第三个定位装置的曼哈顿距离
 * @return 返回字典序最小的信标位置
 */
public static int[] find(int n, int[] a, int[] b, int[] c, int ad, int bd, int cd) {
    //以下代码是为了找到半径最小的那个圆,x1就是半径最小的圆的圆心,r1就是最小的半径
    int[] x1 = null;
    int r1 = Integer.MAX_VALUE;
    int[] x2 = null;
    int r2 = 0;
    int[] x3 = null;
    int r3 = 0;
    if (ad < r1) {
        x1 = a;
        r1 = ad;
        x2 = b;
        r2 = bd;
        x3 = c;
        r3 = cd;
    }
    if (bd < r1) {
        x1 = b;
        r1 = bd;
        x2 = a;
        r2 = ad;
        x3 = c;
        r3 = cd;
    }
    if (cd < r1) {
        x1 = c;
        r1 = cd;
        x2 = a;
        r2 = ad;
        x3 = b;
        r3 = bd;
    }
    //下面就是图的宽度优先遍历
    int[] cur = {x1[0] - r1, x1[1]};        //找到距离圆心为r1的左边的点
    LinkedList<int[]> queue = new LinkedList<>();       //用于存放遍历到的节点
    HashSet<String> visited = new HashSet<>();          //用于判断节点是否遍历过
    ArrayList<int[]> ans = new ArrayList<>();           //存放候选节点
    queue.add(cur);
    visited.add(cur[0] + "_" + cur[1]);
    while (!queue.isEmpty()) {
        cur = queue.poll();
        if (cur[0] >= 1 && cur[0] <= n && cur[1] >= 0 && cur[1] <= n && distance(cur[0], cur[1], x2) == r2 && distance(cur[0], cur[1], x3) == r3) {        //如果当前点在棋盘上,并且距离x2点的距离为r2,距离x3点的距离为r3,说明是个备选点
            ans.add(cur);
        }
        if (ans.size() == 2) {      //最多只有两个备选点,如果已经找到了两个备选点就可以退出了
            break;
        }
        //对每个点的周围8个点都加入队列
        add(cur[0] - 1, cur[1], x1, r1, visited, queue);
        add(cur[0] - 1, cur[1] - 1, x1, r1, visited, queue);
        add(cur[0], cur[1] - 1, x1, r1, visited, queue);
        add(cur[0] + 1, cur[1] - 1, x1, r1, visited, queue);
        add(cur[0] + 1, cur[1], x1, r1, visited, queue);
        add(cur[0] + 1, cur[1] + 1, x1, r1, visited, queue);
        add(cur[0], cur[1] + 1, x1, r1, visited, queue);
        add(cur[0] - 1, cur[1] + 1, x1, r1, visited, queue);
    }
    //返回字典序小的
    if (ans.size() == 1 || ans.get(0)[0] < ans.get(1)[0] || (ans.get(0)[0] == ans.get(1)[0] && ans.get(0)[1] < ans.get(1)[1])) {
        return ans.get(0);
    }else{
        return ans.get(1);
    }
}

//返回(a,b)距离c点的曼哈顿距离
public static int distance(int a, int b, int[] c) {
    return Math.abs(a - c[0]) + Math.abs(b - c[1]);
}

//将距离c点长度为r的坐标加入队列和hashset中,服务于图的宽度优先遍历
public static void add(int a, int b, int[] c, int r, HashSet<String> visited, LinkedList<int[]> queue) {
    String key = a + "_" + b;
    if (distance(a, b, c) == r && !visited.contains(key)) {
        visited.add(key);
        queue.add(new int[]{a, b});
    }
}

题目十(来自微软)

给定一个字符串s,只含有0~9这些字符
你可以使用来自s中的数字,目的是拼出一个最大的回文数
使用数字的个数,不能超过s里含有的个数
比如 :
39878,能拼出的最大回文数是 : 898
00900,能拼出的最大回文数是 : 9
54321,能拼出的最大回文数是 : 5
最终的结果以字符串形式返回
str的长度为N,1 <= N <= 100000
测试链接 : https://leetcode.cn/problems/largest-palindromic-number/

/*
思路:对字符串中的数字进行词频统计,若有数字出现的频率大于等于2次,根据数字的大小排序,最大的数字放在最外围,依次往中间放,但是最外围的数字不能是0,直到所有的数字的频率都小于等于1次,最中间在放个数字最大的即可。
若一开始只有0的频率大于等于2次,在选频率为1的数字中数值最大的即可。
比如:num=0001230400,则最大回文数字就是4
num=12431982358290,其中0出现1次,1出现2次,2出现3次,3出现2次,4出现1次,5出现1次,8出现2次,9出现2次,则最大的数字是98321512389
 */
public static String largestPalindromic(String num) {
    if (num.length() == 1) {
        return num;
    }
    char[] chars = num.toCharArray();
    TreeMap<Character, Integer> map = new TreeMap<>(new Comparator<Character>() {
        @Override
        public int compare(Character o1, Character o2) {
            return o2 - o1;
        }
    });     //key:数字    value:数字出现的频率        降序排列
    for (char a : chars) {      //统计出现的频率
        map.put(a, map.getOrDefault(a, 0) + 1);
    }
    StringBuilder sb = new StringBuilder();       //用于拼接回文数
    Set<Map.Entry<Character, Integer>> entries = map.entrySet();
    ArrayList<Character> temp = new ArrayList<>();        //用于存放频率大于等于2次的数字
    String half = "";       //用于记录一半的回文串
    for (Map.Entry<Character, Integer> entry : entries) {
        if (entry.getValue() >= 2) {
            temp.add(entry.getKey());
        }
    }
    if (temp.size() == 1 && temp.get(0) == '0') {       //如果频率大于等于2的数字只有0,那就返回频率为1中数值最大的
        sb.append(map.ceilingKey('9'));
        return sb.toString();
    } else {     //执行到这里,要么是一个频率大于等于2的都没有,要么是频率大于等于2的不止0
        for (Map.Entry<Character, Integer> entry : entries) {
            while (entry.getValue() >= 2) {     //如果频率大于等于2次,开始拼接,每拼接一次,频率减2
                sb.append(entry.getKey());
                map.put(entry.getKey(), entry.getValue() - 2);
            }
        }
        half = sb.toString();
        //执行到这里说明所有数字出现的次数都小于2次了,如果map中还有记录,说明还有频率为1的数字,拼接上数值最大的即可
        if (map.size() != 0) {
            for (Map.Entry<Character, Integer> entry : entries) {
                if (entry.getValue() >= 1) {
                    sb.append(entry.getKey());
                    break;
                }
            }
        }
    }
    //sb再拼接上half的逆序即可
    sb.append(reverse(half, 0, half.length() - 1));
    return sb.toString();
}

//用于逆序
public static String reverse(String s, int left, int right) {
    char[] chars = s.toCharArray();
    if (chars.length == 0) {
        return s;
    }
    for (int i = left, j = right; i < j; i++, j--) {
        char temp = chars[i];
        chars[i] = chars[j];
        chars[j] = temp;
    }
    StringBuilder sb = new StringBuilder();
    for (char c : chars) {
        sb.append(c);
    }
    return sb.toString();
}

public static void main(String[] args) {
    String num = "444947137";
    System.out.println(largestPalindromic(num));
}

题目十一(来自微软)

给定两个数组A和B,比如
A = { 0, 1, 1 }
B = { 1, 2, 3 }
A[0] = 0, B[0] = 1,表示0到1有双向道路
A[1] = 1, B[1] = 2,表示1到2有双向道路
A[2] = 1, B[2] = 3,表示1到3有双向道路
给定数字N,编号从0~N,所以一共N+1个节点
题目输入一定保证所有节点都联通,并且一定没有环
默认办公室是0节点,其他1~N节点上,每个节点上都有一个居民
每天所有居民都去往0节点上班
所有的居民都有一辆5座的车,也都乐意和别人一起坐车
车不管负重是多少,只要走过一条路,就耗费1的汽油
比如A、B、C的居民,开着自己的车来到D居民的位置,一共耗费3的汽油
D居民和E居民之间,假设有一条路
那么D居民可以接上A、B、C,4个人可以用一辆车,去往E的话,就再耗费1的汽油
求所有居民去办公室的路上,最少耗费多少汽油
测试链接 : https://leetcode.cn/problems/minimum-fuel-cost-to-report-to-the-capital/

public static int cnt;

public static long minimumFuelCost(int[][] roads, int seats) {
    //根据二维数组构建图
    ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
    for (int i = 0; i <= roads.length; i++) {       //有n条路,说明有n+1个节点
        graph.add(new ArrayList<>());
    }
    for (int[] road : roads) {      //无向图,两个节点互相添加
        graph.get(road[0]).add(road[1]);
        graph.get(road[1]).add(road[0]);
    }
    //下面进行图的深度遍历
    int[] dfs = new int[graph.size()];        //用于标记一个节点是否被遍历过
    int[] size = new int[graph.size()];       //用于记录一个节点以其为头的图包括其自己一共有多少个节点
    long[] cost = new long[graph.size()];       //用于记录到达当前节点需要耗费多少升油
    cnt = 0;
    dfs(graph, 0, dfs, size, cost, seats);
    return cost[0];     //最终返回0节点得到的消耗即可
}

//用于计算每一个节点的消耗和节点数
public static void dfs(ArrayList<ArrayList<Integer>> graph, int cur, int[] dfs, int[] size, long[] cost, int seats) {
    dfs[cur] = ++cnt;     //标记一下,说明当前节点被遍历过了
    size[cur] = 1;        //记录一下当前节点的大小
    for (int next : graph.get(cur)) {   //开始深度遍历
        if (dfs[next] == 0) {   //如果当前节点没有遍历过
            dfs(graph, next, dfs, size, cost, seats);      //先获取它下一个节点的消耗和大小才能得到当前节点的消耗和大小
            size[cur] += size[next];
            cost[cur] += cost[next];      //消耗需要先加上下一个节点的消耗
            cost[cur] += (size[next]+seats-1)/seats;        //再加上从下一个节点到达当前节点的消耗,如果下一个节点总共6个人,一辆车只能坐5个人,则需要两辆车。(size[next]+seats-1)/seats就是向上取整
        }
    }
}

题目十二(来自网易)

小红拿到了一个仅由r、e、d组成的字符串
她定义一个字符e为"好e" : 当且仅当这个e字符和r、d相邻
例如"reeder"只有一个"好e",前两个e都不是"好e",只有第三个e是"好e"
小红每次可以将任意字符修改为任意字符,即三种字符可以相互修改
她希望"好e"的数量尽可能多
小红想知道,自己最少要修改多少次
输入一个只有r、e、d三种字符的字符串
长度 <= 2 * 10^5
输出最小修改次数

public static int minCost(String str) {
    int n = str.length();
    if (n < 3) {        //如果字符串长度小于3,是不可能得到好e的
        return -1;
    }
    int[] arr = new int[n];
    // d认为是0,e认为是1,r认为是2
    for (int i = 0; i < n; i++) {
        char cur = str.charAt(i);
        if (cur == 'd') {
            arr[i] = 0;
        } else if (cur == 'e') {
            arr[i] = 1;
        } else {
            arr[i] = 2;
        }
    }
    // 通过上面的转化,问题变成了:
    // 1的左右,一定要被0和2包围,这个1才是"好1"
    // 请让"好1"的尽量多,返回最少的修改代价
    int maxGood = 0;
    int minCost = Integer.MAX_VALUE;
    for (int prepre = 0; prepre < 3; prepre++) {
        for (int pre = 0; pre < 3; pre++) {
            int cost = arr[0] == prepre ? 0 : 1;
            cost += arr[1] == pre ? 0 : 1;
            Info cur = process(arr, 2, prepre, pre);
            if (cur.maxGood > maxGood) {
                maxGood = cur.maxGood;
                minCost = cur.minModify + cost;
            } else if (cur.maxGood == maxGood) {
                minCost = Math.min(minCost, cur.minModify + cost);
            }
        }
    }
    return minCost;
}

public static class Info {
    public int maxGood;     //表示好e的数量
    public int minModify;   //表示获得好e需要修改多少次

    public Info(int maxGood, int minModify) {
        this.maxGood = maxGood;
        this.minModify = minModify;
    }
}

/**
 * 返回从i位置开始能够获得最多多少个好e和对应的最小修改次数
 *
 * @param s      字符数组
 * @param i      起始位置
 * @param prepre i-2位置的字符
 * @param pre    i-1位置的字符
 * @return 返回从i位置开始能够获得最多多少个好e和对应的最小修改次数
 */
public static Info process(int[] s, int i, int prepre, int pre) {
    if (i == s.length) {        //如果越界了,就获得不了好e,修改次数也是0
        return new Info(0, 0);
    }
    int maxGood = 0;
    int minModify = Integer.MAX_VALUE;

    //可能性1:i位置字符变成r
    int curvalue1 = prepre == 0 && pre == 1 ? 1 : 0;    //只有当前面两个位置是de,当前字符变成r,才会有一个好e
    int curcost1 = s[i] == 2 ? 0 : 1;         //如果当前字符本身是r,就不需要修改
    Info info1 = process(s, i + 1, pre, 2);       //下一次从i+1位置开始遍历
    int p1Value=curvalue1+info1.maxGood;
    int p1Cost=curcost1+info1.minModify;

    //可能性1:i位置字符变成e
    int curvalue2 = 0;    //如果当前字符是e,那么是不可能凑成好e的
    int curcost2 = s[i] == 1 ? 0 : 1;         //如果当前字符本身是e,就不需要修改
    Info info2 = process(s, i + 1, pre, 1);       //下一次从i+1位置开始遍历
    int p2Value=curvalue2+info2.maxGood;
    int p2Cost=curcost2+info2.minModify;

    //可能性1:i位置字符变成d
    int curvalue3 = prepre == 2 && pre == 1 ? 1 : 0;    //只有当前面两个位置是re,当前字符变成d,才会有一个好e
    int curcost3 = s[i] == 0 ? 0 : 1;         //如果当前字符本身是d,就不需要修改
    Info info3 = process(s, i + 1, pre, 0);       //下一次从i+1位置开始遍历
    int p3Value=curvalue3+info3.maxGood;
    int p3Cost=curcost3+info3.minModify;

    if(maxGood<p1Value){        //更新好e的数量
        maxGood=p1Value;
        minModify=p1Cost;
    }else if(maxGood==p1Value){
        minModify=Math.min(minModify,p1Cost);
    }

    if(maxGood<p2Value){        //更新好e的数量
        maxGood=p2Value;
        minModify=p2Cost;
    }else if(maxGood==p2Value){
        minModify=Math.min(minModify,p2Cost);
    }
    if(maxGood<p3Value){        //更新好e的数量
        maxGood=p3Value;
        minModify=p3Cost;
    }else if(maxGood==p3Value){
        minModify=Math.min(minModify,p3Cost);
    }

    return new Info(maxGood,minModify);
}

题目十三(来自弗吉尼亚理工大学)

方案1 : {7, 10}
xxxx : {a , b}
1 2 3 4
FunnyGoal = 100
OffenseGoal = 130
找到一个最少方案数,让FunnyGoal、OffenseGoal,都大于等于一个值
定义如下尝试过程
贴纸数组stickers
stickers[i][0] : i号贴纸的Funny值
stickers[i][1] : i号贴纸的Offense值
index....所有的贴纸,随便选择。index之前的贴纸不能选择,
在让restFunny和restOffense都小于等于0的要求下,返回最少的贴纸数量

//暴力递归
public static int minStickers1(int[][] stickers, int funnyGoal, int offenseGoal) {
    return process1(stickers, 0, funnyGoal, offenseGoal);
}

/**
 * 返回使funnyGoal和offenseGoal都小于等于0最少需要多少张贴纸
 *
 * @param stickers        数组
 * @param index           从哪一个位置开始
 * @param restFunnyGoal   还剩余多少funnyGoal
 * @param restoffenseGoal 还剩余多少offenseGoal
 * @return 返回最少的贴纸数
 */
public static int process1(int[][] stickers, int index, int restFunnyGoal, int restoffenseGoal) {
    if (restFunnyGoal <= 0 && restoffenseGoal <= 0) {       //如果两个值都小于等于0,说明就不需要贴纸了
        return 0;
    }
    if (index == stickers.length) {     //如果越界了,还没使两个值都小于等于0,说明此方案不可能
        return Integer.MAX_VALUE;
    }

    //两种可能,一种要index位置的贴纸,一种不要index位置的贴纸
    //不要index位置的贴纸
    int p1 = process1(stickers, index + 1, restFunnyGoal, restoffenseGoal);
    //要index位置的贴纸
    int p2 = Integer.MAX_VALUE;
    int next = process1(stickers, index + 1, restFunnyGoal - stickers[index][0], restoffenseGoal - stickers[index][1]);
    if (next != Integer.MAX_VALUE) {        //如果要index位置的贴纸,其后续能够让两个值都小于等于0,就说明要index位置是可行的
        p2 = 1 + next;
    }
    return Math.min(p1, p2);
}

//记忆化搜索
public static int minStickers2(int[][] stickers, int funnyGoal, int offenseGoal) {
    int[][][] dp = new int[stickers.length][funnyGoal + 1][offenseGoal + 1];
    for (int[][] ints : dp) {
        for (int[] anInt : ints) {
            Arrays.fill(anInt, -1);      //数组中的数全部初始化为-1
        }
    }
    return process2(stickers, 0, funnyGoal, offenseGoal, dp);
}

public static int process2(int[][] stickers, int index, int restFunnyGoal, int restoffenseGoal, int[][][] dp) {
    if (dp[index][restFunnyGoal][restoffenseGoal] != -1) {
        return dp[index][restFunnyGoal][restoffenseGoal];
    }
    if (restFunnyGoal <= 0 && restoffenseGoal <= 0) {
        dp[index][restFunnyGoal][restoffenseGoal] = 0;
        return dp[index][restFunnyGoal][restoffenseGoal];
    }

    if (index == stickers.length) {
        return Integer.MAX_VALUE;
    }

    int p1 = process2(stickers, index + 1, restFunnyGoal, restoffenseGoal, dp);
    int p2 = Integer.MAX_VALUE;
    int next = process2(stickers, index + 1, Math.max(0, restFunnyGoal - stickers[index][0]), Math.max(0, restoffenseGoal - stickers[index][1]), dp);
    if(next!=Integer.MAX_VALUE){
        p2=1+next;
    }
    dp[index][restFunnyGoal][restoffenseGoal]=Math.min(p1,p2);
    return dp[index][restFunnyGoal][restoffenseGoal];
}

//动态规划
public static int minStickers3(int[][] stickers, int funnyGoal, int offenseGoal) {
    int n = stickers.length;
    int[][][] dp = new int[n + 1][funnyGoal + 1][offenseGoal + 1];
    for (int f = 0; f <= funnyGoal; f++) {
        for (int o = 0; o <= offenseGoal; o++) {
            if (f != 0 || o != 0) {
                dp[n][f][o] = Integer.MAX_VALUE;
            }
        }
    }
    for (int i = n - 1; i >= 0; i--) {
        for (int f = 0; f <= funnyGoal; f++) {
            for (int o = 0; o <= offenseGoal; o++) {
                if (f != 0 || o != 0) {
                    int p1 = dp[i + 1][f][o];
                    int p2 = Integer.MAX_VALUE;
                    int next2 = dp[i + 1][Math.max(0, f - stickers[i][0])][Math.max(0, o - stickers[i][1])];
                    if (next2 != Integer.MAX_VALUE) {
                        p2 = next2 + 1;
                    }
                    dp[i][f][o] = Math.min(p1, p2);
                }
            }
        }
    }
    return dp[0][funnyGoal][offenseGoal];
}

题目十四

给定区间的范围[xi,yi],xi<=yi,且都是正整数
找出一个坐标集合set,set中有若干个数字
set要和每个给定的区间,有交集
求set的最少需要几个数
比如给定区间 : [5, 8] [1, 7] [2, 4] [1, 9]
set最小可以是: {2, 6}或者{2, 5}或者{4, 5}

public static int minSet(int[][] ranges) {
    int n=ranges.length;
    int[][] events=new int[n<<1][3];
    for (int i = 0; i < ranges.length; i++) {
        //events[i]={a,b,c}
        //a=0代表是个开始事件,b位置就是结束时间,c位置就是这个事件的时间点
        //a=1代表是结束事件,b位置就无效了,c位置就是这个事件的时间点
        //一个ranges[i]产生两个事件
        //例如ranges[i]=[1,5],产生一个[0,5,1]事件和一个[1,X,5]的事件
        events[i][0]=0;
        events[i][1]=ranges[i][1];
        events[i][2]=ranges[i][0];
        events[i+n][0]=1;
        events[i+n][2]=ranges[i][1];
    }
    //按照结束时间排序
    Arrays.sort(events,(a,b)->(a[2]-b[2]));
    //建立一个set集合
    HashSet<Integer> set=new HashSet<>();
    int ans=0;
    for (int[] event : events) {
        if(event[0]==0){        //如果是个开始时间,就加入
            set.add(event[1]);
        }else{      //如果是个结束事件
            if(set.contains(event[2])) {    //set中存在此结束时间,答案加一、set清空
                ans++;
                set.clear();
            }
        }
    }
    return ans;
}

题目十五(来自字节)

来自字节
5.6笔试
给定一个数组arr,长度为n,最多可以删除一个连续子数组,
求剩下的数组,严格连续递增的子数组最大长度
n <= 10^6

// 暴力方法
// 为了验证
public static int maxLen1(int[] arr) {
    int ans = max(arr);
    int n = arr.length;
    for (int L = 0; L < n; L++) {
        for (int R = L; R < n; R++) {
            int[] cur = delete(arr, L, R);
            ans = Math.max(ans, max(cur));
        }
    }
    return ans;
}

//删除arr数组中L....R范围内的数字
public static int[] delete(int[] arr, int L, int R) {
    int n = arr.length;
    int[] ans = new int[n - (R - L + 1)];
    int index = 0;
    for (int i = 0; i < L; i++) {
        ans[index++] = arr[i];
    }
    for (int i = R + 1; i < n; i++) {
        ans[index++] = arr[i];
    }
    return ans;
}

//一个数字也不删的时候最长递增子数组
public static int max(int[] arr) {
    if (arr.length == 0) {
        return 0;
    }
    int ans = 1;
    int cur = 1;
    for (int i = 1; i < arr.length; i++) {
        if (arr[i] > arr[i - 1]) {
            cur++;
        } else {
            cur = 1;
        }
        ans = Math.max(ans, cur);
    }
    return ans;
}

//正式方法
// 时间复杂度O(N*logN)
public static int maxLen2(int[] arr) {
    if (arr.length == 0) {
        return 0;
    }
    int n = arr.length;
    int[] sorted = new int[n];
    for (int i = 0; i < n; i++) {
        sorted[i] = arr[i];
    }
    Arrays.sort(sorted);
    SegmentTree st = new SegmentTree(n);
    st.update(rank(sorted, arr[0]), 1);
    int[] dp = new int[n];
    dp[0] = 1;
    int ans = 1;
    // 一个数字也不删!长度!
    int cur = 1;
    for (int i = 1; i < n; i++) {
        int rank = rank(sorted, arr[i]);
        // (dp[i - 1] + 1)
        int p1 = arr[i - 1] < arr[i] ? (dp[i - 1] + 1) : 1;
       // rank : 就是当前的数字
       // 1~rank-1 : 第二个信息的max
        int p2 = rank > 1 ? (st.max(rank - 1) + 1) : 1;
        dp[i] = Math.max(p1, p2);
        ans = Math.max(ans, dp[i]);
        if (arr[i] > arr[i - 1]) {
            cur++;
        } else {
            cur = 1;
        }
        // 我的当前值是rank
        // 之前有没有还是rank的记录!
        if (st.get(rank) < cur) {
            st.update(rank, cur);
        }
    }
    return ans;
}

//找到sorted数组中值为num的最左位置
public static int rank(int[] sorted, int num) {
    int l = 0;
    int r = sorted.length - 1;
    int m = 0;
    int ans = -1;
    while (l <= r) {
        m = (l + r) / 2;
        if (sorted[m] >= num) {
            ans = m;
            r = m - 1;
        } else {
            l = m + 1;
        }
    }
    return ans + 1;
}

//线段树结构
public static class SegmentTree {
    private int n;
    private int[] max;
    private int[] update;

    public SegmentTree(int maxSize) {
        n = maxSize + 1;        //0位置不用,从1位置开始
        max = new int[n << 2];      //如果有5个数,得用15个格子存储,让其达到满二叉树的范围,所以max数组的大小是4n
        update = new int[n << 2];   //跟max数组一样
        Arrays.fill(update, -1);
    }

    public int get(int index) {
        return max(index, index, 1, n, 1);
    }

    public void update(int index, int c) {
        update(index, index, c, 1, n, 1);
    }

    public int max(int right) {
        return max(1, right, 1, n, 1);
    }

    private void pushUp(int rt) {
        max[rt] = Math.max(max[rt << 1], max[rt << 1 | 1]);
    }

    private void pushDown(int rt, int ln, int rn) {
        if (update[rt] != -1) {
            update[rt << 1] = update[rt];
            max[rt << 1] = update[rt];
            update[rt << 1 | 1] = update[rt];
            max[rt << 1 | 1] = update[rt];
            update[rt] = -1;
        }
    }

    private void update(int L, int R, int C, int l, int r, int rt) {
        if (L <= l && r <= R) {
            max[rt] = C;
            update[rt] = C;
            return;
        }
        int mid = (l + r) >> 1;
        pushDown(rt, mid - l + 1, r - mid);
        if (L <= mid) {
            update(L, R, C, l, mid, rt << 1);
        }
        if (R > mid) {
            update(L, R, C, mid + 1, r, rt << 1 | 1);
        }
        pushUp(rt);
    }

    private int max(int L, int R, int l, int r, int rt) {
        if (L <= l && r <= R) {
            return max[rt];
        }
        int mid = (l + r) >> 1;
        pushDown(rt, mid - l + 1, r - mid);
        int ans = 0;
        if (L <= mid) {
            ans = Math.max(ans, max(L, R, l, mid, rt << 1));
        }
        if (R > mid) {
            ans = Math.max(ans, max(L, R, mid + 1, r, rt << 1 | 1));
        }
        return ans;
    }
}

题目十六(来自京东)

给定一个长度为3N的数组,其中最多含有0、1、2三种值
你可以把任何一个连续区间上的数组,全变成0、1、2中的一种
目的是让0、1、2三种数字的个数都是N
返回最小的变化次数

public static int minTimes2(int[] arr) {
    int[] cnt = new int[3];       //用于统计0,1,2出现的次数
    for (int i : arr) {
        cnt[i]++;
    }
    if (cnt[0] == cnt[1] && cnt[0] == cnt[2]) {     //如果三个数出现的次数一样,就不需要修改了
        return 0;
    }
    int length = arr.length;
    int n = length / 3;     //每个数字都得出现n次
    if ((cnt[0] < n && cnt[1] < n) || (cnt[0] < n && cnt[2] < n) || (cnt[1] < n && cnt[2] < n)) {       //如果三个数中有两个数的次数都小于n,则必须要修改2次
        return 2;
    } else {      //三个数中只有一个数的次数小于n
        return once(arr, cnt, n) ? 1 : 2;
    }
}

/**
 * 用1次修改机会能否使三个数出现的次数都为n
 *
 * @param arr 原始数组
 * @param cnt 0,1,2出现频率的数组
 * @param n   三个数字都应该出现的次数
 * @return 用1次修改机会能否使三个数出现的次数都为n
 */
public static boolean once(int[] arr, int[] cnt, int n) {
    int lessV = cnt[0] < n ? 0 : (cnt[1] < n ? 1 : 2);       //找到三个数字中出现次数少于n的
    int lessT = cnt[lessV];       //出现次数少于n的数的出现次数
    if (cnt[0] > n && modify(arr, 0, cnt[0], lessV, lessT)) {
        return true;
    }
    if (cnt[1] > n && modify(arr, 1, cnt[1], lessV, lessT)) {
        return true;
    }
    if (cnt[2] > n && modify(arr, 2, cnt[2], lessV, lessT)) {
        return true;
    }
    return false;
}

/**
 * 返回只用1次修改机会能否将lessV的次数和moreV的次数都变成arr.length/3
 *
 * @param arr   数组
 * @param moreV 次数多余arr.length/3的数
 * @param moreT moreV出现的次数
 * @param lessV 次数少于arr.length/3的数
 * @param lessT lessV出现的次数
 * @return 返回只用1次修改机会能否将lessV的次数和moreV的次数都变成arr.length/3
 */
public static boolean modify(int[] arr, int moreV, int moreT, int lessV, int lessT) {
    int[] cnt = new int[3];
    cnt[lessV] = lessT;
    cnt[moreV] = moreT;
    int aim = arr.length / 3;       //目标次数
    int L = 0;        //滑动窗口的左边界
    int R = 0;        //滑动窗口的右边界
    //滑动窗口是左闭右开的
    while (R < arr.length || cnt[moreV] <= aim) {
        //cnt[moreV]的含义是窗口外,moreV出现的次数
        if (cnt[moreV] > aim) {     //如果次数大于aim,右边界向右移,并且包含进来的字符的个数减1
            cnt[arr[R++]]--;
        } else if (cnt[moreV] < aim) {       //如果次数小于aim,左边界向右移,并且移除滑动窗口的字符的个数加1
            cnt[arr[L++]]++;
        } else {      //出现次数较多的数的次数减为了aim
            if (cnt[lessV] + R - L < aim) {     //如果次数少的那个数,记为a,加上把滑动窗口内的数字都变成a还是不够aim,说明滑动窗口不够大
                cnt[arr[R++]]--;
            }else if(cnt[lessV] + R - L > aim){     //如果次数少的那个数,记为a,加上把滑动窗口内的数字都变成a超过aim,说明滑动窗口太大了
                cnt[arr[L++]]++;
            }else{      //如果次数少的那个数出现的次数也变为aim了,说明可以只修改一次
                return true;
            }
        }
    }
    return false;
}
posted @ 2024-04-26 18:01  死不悔改奇男子  阅读(3)  评论(0编辑  收藏  举报