package class01; import java.util.Arrays; /* * 给定两个数组x和hp,长度都是N。 * x数组一定是有序的,x[i]表示i号怪兽在x轴上的位置 * hp数组不要求有序,hp[i]表示i号怪兽的血量 * 为了方便起见,可以认为x数组和hp数组中没有负数。 * 再给定一个正数range,表示如果法师释放技能的范围长度(直径!) * 被打到的每只怪兽损失1点血量。 * 返回要把所有怪兽血量清空,至少需要释放多少次aoe技能? * 三个参数:int[] x, int[] hp, int range * 返回:int 次数 * */ public class Code06_AOE { // 纯暴力解法 // 太容易超时 // 只能小样本量使用 public static int minAoe1(int[] x, int[] hp, int range) { boolean allClear = true; for (int i = 0; i < hp.length; i++) { if (hp[i] > 0) { allClear = false; break; } } if (allClear) { return 0; } else { int ans = Integer.MAX_VALUE; for (int left = 0; left < x.length; left++) { if (hasHp(x, hp, left, range)) { minusOneHp(x, hp, left, range); ans = Math.min(ans, 1 + minAoe1(x, hp, range)); addOneHp(x, hp, left, range); } } return ans; } } public static boolean hasHp(int[] x, int[] hp, int left, int range) { for (int index = left; index < x.length && x[index] - x[left] <= range; index++) { if (hp[index] > 0) { return true; } } return false; } public static void minusOneHp(int[] x, int[] hp, int left, int range) { for (int index = left; index < x.length && x[index] - x[left] <= range; index++) { hp[index]--; } } public static void addOneHp(int[] x, int[] hp, int left, int range) { for (int index = left; index < x.length && x[index] - x[left] <= range; index++) { hp[index]++; } } // 为了验证 // 不用线段树,但是贪心的思路,和课上一样 // 1) 总是用技能的最左边缘刮死当前最左侧的没死的怪物 // 2) 然后向右找下一个没死的怪物,重复步骤1) public static int minAoe2(int[] x, int[] hp, int range) { // 举个例子: // 如果怪兽情况如下, // 怪兽所在,x数组 : 2 3 5 6 7 9 // 怪兽血量,hp数组 : 2 4 1 2 3 1 // 怪兽编号 : 0 1 2 3 4 5 // 技能直径,range = 2 int n = x.length; int[] cover = new int[n]; // 首先求cover数组, // 如果技能左边界就在0号怪兽,那么技能到2号怪兽就覆盖不到了 // 所以cover[0] = 2; // 如果技能左边界就在1号怪兽,那么技能到3号怪兽就覆盖不到了 // 所以cover[1] = 3; // 如果技能左边界就在2号怪兽,那么技能到5号怪兽就覆盖不到了 // 所以cover[2] = 5; // 如果技能左边界就在3号怪兽,那么技能到5号怪兽就覆盖不到了 // 所以cover[3] = 5; // 如果技能左边界就在4号怪兽,那么技能到6号怪兽(越界位置)就覆盖不到了 // 所以cover[4] = 6(越界位置); // 如果技能左边界就在5号怪兽,那么技能到6号怪兽(越界位置)就覆盖不到了 // 所以cover[5] = 6(越界位置); // 综上: // 如果怪兽情况如下, // 怪兽所在,x数组 : 2 3 5 6 7 9 // 怪兽血量,hp数组 : 2 4 1 2 3 1 // 怪兽编号 : 0 1 2 3 4 5 // cover数组情况 : 2 3 5 5 6 6 // 技能直径,range = 2 // cover[i] = j,表示如果技能左边界在i怪兽,那么技能会影响i...j-1号所有的怪兽 // 就是如下的for循环,在求cover数组 int r = 0; for (int i = 0; i < n; i++) { while (r < n && x[r] - x[i] <= range) { r++; } cover[i] = r; } int ans = 0; for (int i = 0; i < n; i++) { // 假设来到i号怪兽了 // 如果i号怪兽的血量>0,说明i号怪兽没死 // 根据我们课上讲的贪心: // 我们要让技能的左边界,刮死当前的i号怪兽 // 这样能够让技能尽可能的往右释放,去尽可能的打掉右侧的怪兽 // 此时cover[i],正好的告诉我们,技能影响多大范围。 // 比如当前来到100号怪兽,血量30 // 假设cover[100] == 200 // 说明,技能左边界在100位置,可以影响100号到199号怪兽的血量。 // 为了打死100号怪兽,我们释放技能30次, // 释放的时候,100号到199号怪兽都掉血,30点 // 然后继续向右寻找没死的怪兽,像课上讲的一样 if (hp[i] > 0) { int minus = hp[i]; for (int index = i; index < cover[i]; index++) { hp[index] -= minus; } ans += minus; } } return ans; } // 正式方法 // 关键点就是: // 1) 线段树 // 2) 总是用技能的最左边缘刮死当前最左侧的没死的怪物 // 3) 然后向右找下一个没死的怪物,重复步骤2) public static int minAoe3(int[] x, int[] hp, int range) { int n = x.length; int[] cover = new int[n]; int r = 0; // cover[i] : 如果i位置是技能的最左侧,技能往右的range范围内,最右影响到哪 for (int i = 0; i < n; i++) { while (r < n && x[r] - x[i] <= range) { r++; } cover[i] = r - 1; } SegmentTree st = new SegmentTree(hp); st.build(1, n, 1); int ans = 0; for (int i = 1; i <= n; i++) { int leftHP = st.query(i, i, 1, n, 1); if (leftHP > 0) { ans += leftHP; st.add(i, cover[i - 1] + 1, -leftHP, 1, n, 1); } } return ans; } public static class SegmentTree { private int MAXN; private int[] arr; private int[] sum; private int[] lazy; public SegmentTree(int[] origin) { MAXN = origin.length + 1; arr = new int[MAXN]; for (int i = 1; i < MAXN; i++) { arr[i] = origin[i - 1]; } sum = new int[MAXN << 2]; lazy = new int[MAXN << 2]; } private void pushUp(int rt) { sum[rt] = sum[rt << 1] + sum[rt << 1 | 1]; } private void pushDown(int rt, int ln, int rn) { if (lazy[rt] != 0) { lazy[rt << 1] += lazy[rt]; sum[rt << 1] += lazy[rt] * ln; lazy[rt << 1 | 1] += lazy[rt]; sum[rt << 1 | 1] += lazy[rt] * rn; lazy[rt] = 0; } } public void build(int l, int r, int rt) { if (l == r) { sum[rt] = arr[l]; return; } int mid = (l + r) >> 1; build(l, mid, rt << 1); build(mid + 1, r, rt << 1 | 1); pushUp(rt); } public void add(int L, int R, int C, int l, int r, int rt) { if (L <= l && r <= R) { sum[rt] += C * (r - l + 1); lazy[rt] += C; return; } int mid = (l + r) >> 1; pushDown(rt, mid - l + 1, r - mid); if (L <= mid) { add(L, R, C, l, mid, rt << 1); } if (R > mid) { add(L, R, C, mid + 1, r, rt << 1 | 1); } pushUp(rt); } public int query(int L, int R, int l, int r, int rt) { if (L <= l && r <= R) { return sum[rt]; } int mid = (l + r) >> 1; pushDown(rt, mid - l + 1, r - mid); int ans = 0; if (L <= mid) { ans += query(L, R, l, mid, rt << 1); } if (R > mid) { ans += query(L, R, mid + 1, r, rt << 1 | 1); } return ans; } } // 为了测试 public static int[] randomArray(int n, int valueMax) { int[] ans = new int[n]; for (int i = 0; i < n; i++) { ans[i] = (int) (Math.random() * valueMax) + 1; } return ans; } // 为了测试 public static int[] copyArray(int[] arr) { int N = arr.length; int[] ans = new int[N]; for (int i = 0; i < N; i++) { ans[i] = arr[i]; } return ans; } // 为了测试 public static void main(String[] args) { int N = 50; int X = 500; int H = 60; int R = 10; int testTime = 50000; System.out.println("测试开始"); for (int i = 0; i < testTime; i++) { int len = (int) (Math.random() * N) + 1; int[] x2 = randomArray(len, X); Arrays.sort(x2); int[] hp2 = randomArray(len, H); int[] x3 = copyArray(x2); int[] hp3 = copyArray(hp2); int range = (int) (Math.random() * R) + 1; int ans2 = minAoe2(x2, hp2, range); int ans3 = minAoe3(x3, hp3, range); if (ans2 != ans3) { System.out.println("出错了!"); } } System.out.println("测试结束"); N = 500000; long start; long end; int[] x2 = randomArray(N, N); Arrays.sort(x2); int[] hp2 = new int[N]; for (int i = 0; i < N; i++) { hp2[i] = i * 5 + 10; } int[] x3 = copyArray(x2); int[] hp3 = copyArray(hp2); int range = 10000; start = System.currentTimeMillis(); System.out.println(minAoe2(x2, hp2, range)); end = System.currentTimeMillis(); System.out.println("运行时间 : " + (end - start) + " 毫秒"); start = System.currentTimeMillis(); System.out.println(minAoe3(x3, hp3, range)); end = System.currentTimeMillis(); System.out.println("运行时间 : " + (end - start) + " 毫秒"); } }