欢迎大家加QQ群:623375442。五一节放假还是两道Hard,这个是没想到的。

100659. Maximum Product of Two Digits

给定一个正整数 n。

返回 任意两位数字 相乘所得的 最大 乘积。

注意:如果某个数字在 n 中出现多次,你可以多次使用该数字。

测试样例:

输入:n = 31

输出:3

解释:n 的数字是 [3, 1]。任意两位数字相乘的结果为:3 * 1 = 3。最大乘积为 3。

解答:遍历整数 n 的每一位数字,维护两个最大的数字 max[0](最大值)和 max[1](第二大值)。对于当前提取的数字 tmp:

  • 如果 tmp > max[0],则更新 max[1]=max[0],max[0]=tmp;
  • 否则如果 tmp > max[1],则更新 max[1]=tmp。

最终返回 max[0] * max[1] 即为所求最大乘积。

class Solution {
    public int maxProduct(int n) {
        // max[0] 存储最大的数字,max[1] 存储第二大的数字
        int[] max = {0, 0};
        // 逐位提取 n 的数字
        while (n > 0) {
            int tmp = n % 10;  // 取出最低位
            // 如果当前数字大于 max[0],则更新最大值和第二大值
            if (tmp > max[0]) {
                max[1] = max[0];
                max[0] = tmp;
            } 
            // 否则如果大于第二大值,则更新第二大值
            else if (tmp > max[1]) {
                max[1] = tmp;
            }
            n /= 10;  // 去掉最低位
        }
        // 返回最大的两个数字的乘积
        return max[0] * max[1];
    }
}

100626. Fill a Special Grid

给你一个非负整数 N,表示一个 2^N x 2^N 的网格。你需要用从 0 到 2^(2^N) - 1 的整数填充网格,使其成为一个 特殊 网格。一个网格当且仅当满足以下 所有 条件时,才能称之为 特殊 网格:

  • 右上角象限中的所有数字都小于右下角象限中的所有数字。
  • 右下角象限中的所有数字都小于左下角象限中的所有数字。
  • 左下角象限中的所有数字都小于左上角象限中的所有数字。
  • 每个象限也都是一个特殊网格。

返回一个 2^N x 2^N 的特殊网格。

注意:任何 1x1 的网格都是特殊网格。

测试样例:

输入:N = 1

输出:[[3,0],[2,1]]

解释: 每个象限的数字如下:

  • 右上角:0
  • 右下角:1
  • 左下角:2
  • 左上角:3

由于 0 < 1 < 2 < 3,该网格满足给定的约束条件。

解答:对于大小为 2^N x 2^N 的网格,递归地将其分为四个象限,依次填充:

  1. 右上象限(起始数字 start 开始);
  2. 右下象限(紧接上一区域);
  3. 左下象限;
  4. 左上象限。

每个子象限也是大小减半的“特殊网格”,起始值累加遍历至完整填充。最终保证四个象限的数值区间依次递增。

class Solution {
    public int[][] specialGrid(int N) {
        int len = 1 << N;           // 计算网格边长 2^N
        int[][] res = new int[len][len];
        // 从整个网格的 (0,0) 到 (len-1,len-1),起始数字 0,开始填充
        helper(0, 0, len - 1, len - 1, 0, res);
        return res;
    }

    /**
     * 递归填充子网格
     * @param lx    子网格左上角 x 坐标
     * @param ly    子网格左上角 y 坐标
     * @param rx    子网格右下角 x 坐标
     * @param ry    子网格右下角 y 坐标
     * @param start 当前子网格填充的起始值
     * @param res   最终结果网格
     * @return      返回下一个子网格应使用的起始数字
     */
    private int helper(int lx, int ly, int rx, int ry, int start, int[][] res) {
        // 如果子网格为 1x1,直接填值
        if (lx == rx && ly == ry) {
            res[lx][ly] = start;
            return start + 1;
        } else {
            // 计算中点,分成四个象限
            int dx = (lx + rx) / 2, dy = (ly + ry) / 2;
            // 1. 右上 (lx, dy+1) 到 (dx, ry)
            start = helper(lx, dy + 1, dx, ry, start, res);
            // 2. 右下 (dx+1, dy+1) 到 (rx, ry)
            start = helper(dx + 1, dy + 1, rx, ry, start, res);
            // 3. 左下 (dx+1, ly) 到 (rx, dy)
            start = helper(dx + 1, ly, rx, dy, start, res);
            // 4. 左上 (lx, ly) 到 (dx, dy)
            return helper(lx, ly, dx, dy, start, res);
        }
    }
}

100636. Merge Operations for Minimum Travel Time

给你一个长度为 l 公里的直路,一个整数 n,一个整数 k 和 两个 长度为 n 的整数数组 position 和 time 。

数组 position 列出了路标的位置(单位:公里),并且是 严格 升序排列的(其中 position[0] = 0 且 position[n - 1] = l)。

每个 time[i] 表示从 position[i] 到 position[i + 1] 之间行驶 1 公里所需的时间(单位:分钟)。

你 必须 执行 恰好 k 次合并操作。在一次合并中,你可以选择两个相邻的路标,下标为 i 和 i + 1(其中 i > 0 且 i + 1 < n),并且:

  • 更新索引为 i + 1 的路标,使其时间变为 time[i] + time[i + 1]。
  • 删除索引为 i 的路标。

返回经过 恰好 k 次合并后从 0 到 l 的 最小总旅行时间(单位:分钟)。

测试样例:

输入:l = 10, n = 4, k = 1, position = [0,3,8,10], time = [5,8,3,6]

输出:62

解释: 合并下标为 1 和 2 的路标。删除下标为 1 的路标,并将下标为 2 的路标的时间更新为 8 + 3 = 11。

合并后:position 数组:[0, 8, 10], time 数组:[5, 11, 6]

总旅行时间:40 + 22 = 62 ,这是执行 1 次合并后的最小时间。

解答:使用三维记忆化深度优先搜索(DFS + DP),状态为 (pos, remain, add):

  • pos:当前处理的路标索引;
  • remain:剩余需要执行的合并次数;
  • add:由于前面合并操作累积到当前区段的附加时间。

在位置 pos,尝试将它与后续某个位置 j 之间所有中间路标一次性合并,计算此段的行驶时间并递归到下一个未合并的路标 j。取所有方案的最小值。通过 dp[pos][remain][add] 进行剪枝和结果缓存。

class Solution {
    private static final int MAX = Integer.MAX_VALUE / 2;  // 避免溢出的“大”值

    // 内部封装数据,加速访问
    class Data {
        int l, n, k;
        int[] position, time;
        int[] prefixTime;         // time 前缀和,用于快速计算合并附加时间
        Integer[][][] dp;         // 记忆化 DP 表

        public Data(int _l, int _n, int _k, int[] _position, int[] _time) {
            l = _l; n = _n; k = _k;
            position = _position;
            time = _time;
            prefixTime = new int[n + 1];
            // 构建前缀和:prefixTime[i+1] = time[0] + ... + time[i]
            for (int i = 0; i < n; i++) {
                prefixTime[i + 1] = prefixTime[i] + time[i];
            }
            int sumTime = prefixTime[n];
            // dp[pos][remain][add],add 的最大可能值为 sumTime
            dp = new Integer[n][k + 1][sumTime + 1];
        }
    }

    public int minTravelTime(int l, int n, int k, int[] position, int[] time) {
        Data data = new Data(l, n, k, position, time);
        // 从第 0 个路标开始,还需合并 k 次,初始 add 为 0
        return helper(0, k, 0, data);
    }

    /**
     * @param pos     当前路标索引
     * @param remain  剩余的合并次数
     * @param add      累积附加的 time 值
     * @param data    封装数据
     * @return        最小旅行时间
     */
    private int helper(int pos, int remain, int add, Data data) {
        // 如果已经到达最后一个路标
        if (pos == data.n - 1) {
            // 合并次数正好用完,则后续无路段,时间为 0;否则返回“无效”大值
            return (remain == 0 ? 0 : MAX);
        }
        // 记忆化剪枝
        if (data.dp[pos][remain][add] == null) {
            int best = MAX;
            // 尝试将 pos 后面的若干标点一次性合并到 j
            for (int j = pos + 1; j < data.n; j++) {
                int used = j - pos - 1;  // 需要用掉的合并次数
                if (used > remain) break;  // 超过剩余次数,不可行

                // 计算 pos 到 j 这一段行驶距离与速率
                int dist = data.position[j] - data.position[pos];
                int rate = data.time[pos] + add;  // 合并后当前路段速率
                int costBlock = dist * rate;      // 时间花费

                // 计算下一段的 add 值:如果 used > 0,就要累加中间那些 time 值
                int newAdd = (used > 0)
                        ? data.prefixTime[j] - data.prefixTime[pos + 1]
                        : 0;
                // 递归至 j,并扣除 used 次合并
                int tmp = helper(j, remain - used, newAdd, data);
                if (tmp < MAX) {
                    best = Math.min(best, costBlock + tmp);
                }
            }
            data.dp[pos][remain][add] = best;
        }
        return data.dp[pos][remain][add];
    }
}

100652. Find Sum of Array Product of Magical Sequences

给你两个整数 M 和 K,和一个整数数组 nums。

  • seq 的序列长度为 M。
  • 0 <= seq[i] < nums.length
  • 2^seq[0] + 2^seq[1] + ... + 2^seq[M - 1] 的 二进制形式 有 K 个 置位。
    这个序列的 数组乘积 定义为 prod(seq) = (nums[seq[0]] nums[seq[1]] ... * nums[seq[M - 1]])。

返回所有有效 魔法 序列的 数组乘积 的 总和 。

由于答案可能很大,返回结果对 10^9 + 7 取模。

置位 是指一个数字的二进制表示中值为 1 的位。

测试样例:

输入:M = 5, K = 5, nums = [1,10,100,10000,1000000]

输出:991600007

解释: 所有 [0, 1, 2, 3, 4] 的排列都是魔法序列,每个序列的数组乘积是 10^13。

解答:用四维记忆化 DFS,状态 (pos, um, uk, key):

  • pos:当前遍历到 nums 的索引;
  • um:已选元素数目;
  • uk:当前累计的二进制置位数(除最高位以外的低位);
  • key:当前二进制和的“剩余”值,用于逐步计算置位数。

在 pos 处,可选择不取或取若干次 nums[pos],并用组合数 C(m-um, i) 枚举取 i 次的方案数,同时累积乘积和统计新产生的置位数。达到边界时,若正好选满 M 个且置位数为 K,则计为 1 种有效方案。所有方案乘积值合并并对 1_000_000_007 取模。

class Solution {
    class Data {
        Long[][][][] dp;    // dp[pos][um][uk][key]
        int[] nums;
        int m, k;

        public Data(int m, int k, int[] nums) {
            this.nums = nums;
            this.m = m;
            this.k = k;
            dp = new Long[nums.length][m + 1][k + 1][32];
        }
    }

    private static int[][] combine;
    private static final int mod = 1_000_000_007;

    // 预处理组合数 C[n][k]
    static {
        combine = new int[31][31];
        combine[0][0] = 1;
        for (int i = 1; i <= 30; ++i) {
            for (int j = 0; j <= i; ++j) {
                if (j == 0 || j == i) {
                    combine[i][j] = 1;
                } else {
                    combine[i][j] = (combine[i - 1][j - 1] + combine[i - 1][j]) % mod;
                }
            }
        }
    }

    public int magicalSum(int m, int k, int[] nums) {
        Data data = new Data(m, k, nums);
        return (int) helper(0, 0, 0, 0, data);
    }

    /**
     * @param pos  当前处理的 nums 索引
     * @param um   已选元素总数
     * @param uk   当前已统计的二进制置位数(低位部分)
     * @param key  当前累计二进制和,用于统计置位
     */
    private long helper(int pos, int um, int uk, int key, Data data) {
        // 超出限制时无效,直接返回 0
        if (um > data.m || uk > data.k) return 0;
        // 遍历完 nums
        if (pos == data.nums.length) {
            // 恰好选满 m 且总置位数为 k 时,算作 1 种
            if (um == data.m && uk + count(key) == data.k) {
                return 1;
            } else {
                return 0;
            }
        }
        if (data.dp[pos][um][uk][key] == null) {
            long res = 0;
            // 情况一:不取 nums[pos],但要处理 key 的最低位置位数
            res = helper(pos + 1, um, uk + (key % 2), key / 2, data);

            // 情况二:取 i 次 nums[pos],i 从 1 到 m-um
            long mul = 1;
            for (int i = 1; i <= data.m - um; ++i) {
                mul = (mul * data.nums[pos]) % mod;               // 当前选 i 次的乘积
                int c = combine[data.m - um][i];                  // 从剩余位置选 i 次的组合数
                long ways = helper(
                    pos + 1,
                    um + i,
                    uk + ((key + i) % 2),                         // 新增的置位
                    (key + i) / 2,                                // 更新的 key
                    data
                );
                res = (res + mul * c % mod * ways % mod) % mod;    // 累加各方案贡献
            }
            data.dp[pos][um][uk][key] = res;
        }
        return data.dp[pos][um][uk][key];
    }

    // 统计整数 key 的二进制中 1 的个数
    private int count(int key) {
        int cnt = 0;
        while (key > 0) {
            cnt += key & 1;
            key >>= 1;
        }
        return cnt;
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *