729.我的日程安排表I

目标

实现一个 MyCalendar 类来存放你的日程安排。如果要添加的日程安排不会造成 重复预订 ,则可以存储这个新的日程安排。

当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生 重复预订 。

日程可以用一对整数 startTime 和 endTime 表示,这里的时间是半开区间,即 [startTime, endTime), 实数 x 的范围为, startTime <= x < endTime 。

实现 MyCalendar 类:

  • MyCalendar() 初始化日历对象。
  • boolean book(int startTime, int endTime) 如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true 。否则,返回 false 并且不要将该日程安排添加到日历中。

示例:

输入:
["MyCalendar", "book", "book", "book"]
[[], [10, 20], [15, 25], [20, 30]]
输出:
[null, true, false, true]
解释:
MyCalendar myCalendar = new MyCalendar();
myCalendar.book(10, 20); // return True
myCalendar.book(15, 25); // return False ,这个日程安排不能添加到日历中,因为时间 15 已经被另一个日程安排预订了。
myCalendar.book(20, 30); // return True ,这个日程安排可以添加到日历中,因为第一个日程安排预订的每个时间都小于 20 ,且不包含时间 20 。

说明:

  • 0 <= start < end <= 10^9
  • 每个测试用例,调用 book 方法的次数最多不超过 1000 次。

提示:

  • Store the events as a sorted list of intervals. If none of the events conflict, then the new event can be added.

思路

判断给定区间是否与已有区间相交,如果不相交将其加入已有区间。

最直接的想法是枚举每一个区间,判断是否相交,如果不相交则加入集合。判断区间 [a, b)[c, d) 是否相交,可以固定一个区间,然后让另一个区间滑动,可以发现相交需要满足 d > a && c < b,注意取等号是不相交的。

当然也可以使用二叉搜索树。

TreeSet 中查找特定元素的 API:

  • ceiling 返回的是 大于等于 target 的最小元素/ null
  • floor 返回的是 小于等于 target 的最大元素/ null
  • higher 返回的是 大于 target 的最小元素 / null
  • lower 返回的是 小于 target 的最大元素 / null

代码


/**
 * @date 2025-01-02 9:58
 */
class MyCalendar {

    TreeSet<int[]> ts = new TreeSet<>((a, b) -> a[0] - b[0]);

    public MyCalendar() {

    }

    public boolean book(int startTime, int endTime) {
        int[] interval = {startTime, endTime};
        if (ts.isEmpty()) {
            ts.add(interval);
            return true;
        }
        int[] param = new int[]{endTime, 0};
        // 查找 起点 大于等于 endTime 的 起点最小的区间,即要插入间隙的右边区间 right
        int[] right = ts.ceiling(param);
        // 如果 right 是第一个元素 或者 前面一个区间的右边界 end 小于等于 startTime,说明不相交
        if (right == ts.first() || ts.lower(param)[1] <= startTime) {
            ts.add(interval);
            return true;
        }
        return false;
    }

}

性能

3280.将日期转换为二进制表示

目标

给你一个字符串 date,它的格式为 yyyy-mm-dd,表示一个公历日期。

date 可以重写为二进制表示,只需要将年、月、日分别转换为对应的二进制表示(不带前导零)并遵循 year-month-day 的格式。

返回 date 的 二进制 表示。

示例 1:

输入: date = "2080-02-29"
输出: "100000100000-10-11101"
解释:
100000100000, 10 和 11101 分别是 2080, 02 和 29 的二进制表示。

示例 2:

输入: date = "1900-01-01"
输出: "11101101100-1-1"
解释:
11101101100, 1 和 1 分别是 1900, 1 和 1 的二进制表示。

说明:

  • date.length == 10
  • date[4] == date[7] == '-',其余的 date[i] 都是数字。
  • 输入保证 date 代表一个有效的公历日期,日期范围从 1900 年 1 月 1 日到 2100 年 12 月 31 日(包括这两天)。

思路

将日期转化为二进制表示。

代码


/**
 * @date 2025-01-01 18:53
 */
public class ConvertDateToBinary3280 {

    public String convertDateToBinary(String date) {
        return new StringBuilder().append(Integer.toBinaryString(Integer.parseInt(date.substring(0,4))))
                .append("-")
                .append(Integer.toBinaryString(Integer.parseInt(date.substring(5,7))))
                .append("-")
                .append(Integer.toBinaryString(Integer.parseInt(date.substring(8,10)))).toString();
    }

}

性能

3219.切蛋糕的最小总开销II

目标

有一个 m x n 大小的矩形蛋糕,需要切成 1 x 1 的小块。

给你整数 m ,n 和两个数组:

  • horizontalCut 的大小为 m - 1 ,其中 horizontalCut[i] 表示沿着水平线 i 切蛋糕的开销。
  • verticalCut 的大小为 n - 1 ,其中 verticalCut[j] 表示沿着垂直线 j 切蛋糕的开销。

一次操作中,你可以选择任意不是 1 x 1 大小的矩形蛋糕并执行以下操作之一:

  1. 沿着水平线 i 切开蛋糕,开销为 horizontalCut[i] 。
  2. 沿着垂直线 j 切开蛋糕,开销为 verticalCut[j] 。

每次操作后,这块蛋糕都被切成两个独立的小蛋糕。

每次操作的开销都为最开始对应切割线的开销,并且不会改变。

请你返回将蛋糕全部切成 1 x 1 的蛋糕块的 最小 总开销。

示例 1:

输入:m = 3, n = 2, horizontalCut = [1,3], verticalCut = [5]
输出:13
解释:
沿着垂直线 0 切开蛋糕,开销为 5 。
沿着水平线 0 切开 3 x 1 的蛋糕块,开销为 1 。
沿着水平线 0 切开 3 x 1 的蛋糕块,开销为 1 。
沿着水平线 1 切开 2 x 1 的蛋糕块,开销为 3 。
沿着水平线 1 切开 2 x 1 的蛋糕块,开销为 3 。
总开销为 5 + 1 + 1 + 3 + 3 = 13 。

示例 2:

输入:m = 2, n = 2, horizontalCut = [7], verticalCut = [4]
输出:15
解释:
沿着水平线 0 切开蛋糕,开销为 7 。
沿着垂直线 0 切开 1 x 2 的蛋糕块,开销为 4 。
沿着垂直线 0 切开 1 x 2 的蛋糕块,开销为 4 。
总开销为 7 + 4 + 4 = 15 。

说明:

  • 1 <= m, n <= 10^5
  • horizontalCut.length == m - 1
  • verticalCut.length == n - 1
  • 1 <= horizontalCut[i], verticalCut[i] <= 10^3

思路

有一块 m x n 的蛋糕,horizontalCut[i] 表示水平切第 i 行的开销,verticalCut[i] 表示垂直切第 i 列的开销。求将蛋糕切成 1 x 1 小块的最小代价。

切蛋糕的最小总开销I 相比数据范围扩大了,返回值是 long 型。

代码


/**
 * @date 2024-12-26 16:38
 */
public class MinimumCost3219 {

    public long minimumCost(int m, int n, int[] horizontalCut, int[] verticalCut) {
        int horizontalPart = m;
        int verticalPart = n;
        Arrays.sort(horizontalCut);
        Arrays.sort(verticalCut);
        int h = 0;
        int v = 0;
        long res = 0;
        while (h < m - 1 || v < n - 1) {
            int hcost = h >= m - 1 ? Integer.MAX_VALUE : horizontalCut[h];
            int vcost = v >= n - 1 ? Integer.MAX_VALUE : verticalCut[v];
            if (hcost < vcost) {
                res += verticalPart * hcost;
                horizontalPart--;
                h++;
            } else {
                res += horizontalPart * vcost;
                verticalPart--;
                v++;
            }
        }
        return res;
    }

}

性能

1367.二叉树中的链表

目标

给你一棵以 root 为根的二叉树和一个 head 为第一个节点的链表。

如果在二叉树中,存在一条一直向下的路径,且每个点的数值恰好一一对应以 head 为首的链表中每个节点的值,那么请你返回 True ,否则返回 False 。

一直向下的路径的意思是:从树中某个节点开始,一直连续向下的路径。

示例 1:

输入:head = [4,2,8], root = [1,4,4,null,2,2,null,1,null,6,8,null,null,null,null,1,3]
输出:true
解释:树中蓝色的节点构成了与链表对应的子路径。

示例 2:

输入:head = [1,4,2,6], root = [1,4,4,null,2,2,null,1,null,6,8,null,null,null,null,1,3]
输出:true

示例 3:

输入:head = [1,4,2,6,8], root = [1,4,4,null,2,2,null,1,null,6,8,null,null,null,null,1,3]
输出:false
解释:二叉树中不存在一一对应链表的路径。

说明:

  • 二叉树和链表中的每个节点的值都满足 1 <= node.val <= 100 。
  • 链表包含的节点数目在 1 到 100 之间。
  • 二叉树包含的节点数目在 1 到 2500 之间。

思路

判断二叉树中是否存在给定的路径 head,路径以链表的形式给出。

dfs 判断是否存在相同的路径。这道题的难点在于没有要求起点为 root,因此需要枚举所有节点为起点的情况。针对每一个起点我们最多检查 100 次,总共大概 2.5 * 10^5 次。

代码


/**
 * @date 2024-12-30 8:56
 */
public class IsSubPath1367 {

    public ListNode head;

    public boolean isSubPath(ListNode head, TreeNode root) {
        this.head = head;
        return dfs(root, head);
    }

    public boolean dfs(TreeNode cur, ListNode node) {
        if (node == null) {
            return true;
        }
        if (cur == null) {
            return false;
        }
        return cur.val == node.val && (dfs(cur.left, node.next) || dfs(cur.right, node.next))
                || node == head && (dfs(cur.left, head) || dfs(cur.right, head));
    }

}

性能

1366.通过投票对团队排名

目标

现在有一个特殊的排名系统,依据参赛团队在投票人心中的次序进行排名,每个投票者都需要按从高到低的顺序对参与排名的所有团队进行排位。

排名规则如下:

  • 参赛团队的排名次序依照其所获「排位第一」的票的多少决定。如果存在多个团队并列的情况,将继续考虑其「排位第二」的票的数量。以此类推,直到不再存在并列的情况。
  • 如果在考虑完所有投票情况后仍然出现并列现象,则根据团队字母的字母顺序进行排名。

给你一个字符串数组 votes 代表全体投票者给出的排位情况,请你根据上述排名规则对所有参赛团队进行排名。

请你返回能表示按排名系统 排序后 的所有团队排名的字符串。

示例 1:

输入:votes = ["ABC","ACB","ABC","ACB","ACB"]
输出:"ACB"
解释:
A 队获得五票「排位第一」,没有其他队获得「排位第一」,所以 A 队排名第一。
B 队获得两票「排位第二」,三票「排位第三」。
C 队获得三票「排位第二」,两票「排位第三」。
由于 C 队「排位第二」的票数较多,所以 C 队排第二,B 队排第三。

示例 2:

输入:votes = ["WXYZ","XYZW"]
输出:"XWYZ"
解释:
X 队在并列僵局打破后成为排名第一的团队。X 队和 W 队的「排位第一」票数一样,但是 X 队有一票「排位第二」,而 W 没有获得「排位第二」。

示例 3:

输入:votes = ["ZMNAGUEDSJYLBOPHRQICWFXTVK"]
输出:"ZMNAGUEDSJYLBOPHRQICWFXTVK"
解释:只有一个投票者,所以排名完全按照他的意愿。

说明:

  • 1 <= votes.length <= 1000
  • 1 <= votes[i].length <= 26
  • votes[i].length == votes[j].length for 0 <= i, j < votes.length
  • votes[i][j] 是英文 大写 字母
  • votes[i] 中的所有字母都是唯一的
  • votes[0] 中出现的所有字母 同样也 出现在 votes[j] 中,其中 1 <= j < votes.length

思路

投票人对所有团队的排名用一个字符串 votes[i] 表示,团队的先后顺序就代表排名。根据所有投票人的排名对团队排名,排名第一次数最多的为第一名,如果相同则根据排名第二的次数排名,以此类推,如果最终都相同,则以团队名在字母表的先后顺序排名。

考虑自定义数据结构,维护团队在每一个名次的得票数,然后使用优先队列自定义排序。

代码


/**
 * @date 2024-12-29 18:36
 */
public class RankTeams1366 {

    public static class Rank {
        public int name;
        public int[] cnt;

        public Rank(int name, int[] cnt) {
            this.name = name;
            this.cnt = cnt;
        }
    }

    public String rankTeams(String[] votes) {
        int n = votes[0].length();
        PriorityQueue<Rank> q = new PriorityQueue<>((a, b) -> {
            int[] cnta = a.cnt;
            int[] cntb = b.cnt;
            int i = 0;
            while (i < n && cnta[i] == cntb[i]) {
                i++;
            }
            if (i == n) {
                return a.name - b.name;
            }
            return cntb[i] - cnta[i];
        });

        int[][] cnt = new int[26][n];
        for (String vote : votes) {
            for (int i = 0; i < vote.length(); i++) {
                int c = vote.charAt(i) - 'A';
                cnt[c][i]++;
            }
        }
        for (int i = 0; i < 26; i++) {
            q.offer(new Rank(i, cnt[i]));
        }
        char[] chars = new char[n];
        int i = 0;
        while (i < n) {
            chars[i++] = (char) (q.poll().name + (int) 'A');
        }
        return new String(chars);
    }

}

性能

3046.分割数组

目标

给你一个长度为 偶数 的整数数组 nums 。你需要将这个数组分割成 nums1 和 nums2 两部分,要求:

  • nums1.length == nums2.length == nums.length / 2 。
  • nums1 应包含 互不相同 的元素。
  • nums2也应包含 互不相同 的元素。

如果能够分割数组就返回 true ,否则返回 false 。

示例 1:

输入:nums = [1,1,2,2,3,4]
输出:true
解释:分割 nums 的可行方案之一是 nums1 = [1,2,3] 和 nums2 = [1,2,4] 。

示例 2:

输入:nums = [1,1,1,1]
输出:false
解释:分割 nums 的唯一可行方案是 nums1 = [1,1] 和 nums2 = [1,1] 。但 nums1 和 nums2 都不是由互不相同的元素构成。因此,返回 false 。

说明:

  • 1 <= nums.length <= 100
  • nums.length % 2 == 0
  • 1 <= nums[i] <= 100

思路

判断数组中同一元素的出现次数不超过两次。

代码


/**
 * @date 2024-12-28 19:16
 */
public class IsPossibleToSplit3046 {

    public boolean isPossibleToSplit(int[] nums) {
        int[] cnt = new int[101];
        for (int num : nums) {
            if (++cnt[num] > 2) {
                return false;
            }
        }
        return true;
    }
}

性能

3159.查询数组中元素的出现位置

目标

给你一个整数数组 nums ,一个整数数组 queries 和一个整数 x 。

对于每个查询 queries[i] ,你需要找到 nums 中第 queries[i] 个 x 的位置,并返回它的下标。如果数组中 x 的出现次数少于 queries[i] ,该查询的答案为 -1 。

请你返回一个整数数组 answer ,包含所有查询的答案。

示例 1:

输入:nums = [1,3,1,7], queries = [1,3,2,4], x = 1
输出:[0,-1,2,-1]
解释:
第 1 个查询,第一个 1 出现在下标 0 处。
第 2 个查询,nums 中只有两个 1 ,所以答案为 -1 。
第 3 个查询,第二个 1 出现在下标 2 处。
第 4 个查询,nums 中只有两个 1 ,所以答案为 -1 。

示例 2:

输入:nums = [1,2,3], queries = [10], x = 5
输出:[-1]
解释:
第 1 个查询,nums 中没有 5 ,所以答案为 -1 。

说明:

  • 1 <= nums.length, queries.length <= 10^5
  • 1 <= queries[i] <= 10^5
  • 1 <= nums[i], x <= 10^4

思路

查询给定数字 x 在数组 nums 中第 queries[i] 次出现的下标。

一次遍历记录所有 x 的下标即可。

代码


/**
 * @date 2024-12-27 6:40
 */
public class OccurrencesOfElement3159 {

    public int[] occurrencesOfElement_v1(int[] nums, int[] queries, int x) {
        int n = nums.length;
        int max = 0;
        for (int i = 0; i < n; i++) {
            if (nums[i] == x) {
                nums[max++] = i;
            }
        }
        int length = queries.length;
        for (int i = 0; i < length; i++) {
            queries[i] = queries[i] > max ? -1 : nums[queries[i] - 1];
        }
        return queries;
    }

}

性能

3083.字符串及其反转中是否存在同一子字符串

目标

给你一个字符串 s ,请你判断字符串 s 是否存在一个长度为 2 的子字符串,在其反转后的字符串中也出现。

如果存在这样的子字符串,返回 true;如果不存在,返回 false 。

示例 1:

输入:s = "leetcode"
输出:true
解释:子字符串 "ee" 的长度为 2,它也出现在 reverse(s) == "edocteel" 中。

示例 2:

输入:s = "abcba"
输出:true
解释:所有长度为 2 的子字符串 "ab"、"bc"、"cb"、"ba" 也都出现在 reverse(s) == "abcba" 中。

示例 3:

输入:s = "abcd"
输出:false
解释:字符串 s 中不存在满足「在其反转后的字符串中也出现」且长度为 2 的子字符串。

说明:

  • 1 <= s.length <= 100
  • 字符串 s 仅由小写英文字母组成。

思路

判断字符串 s 中长度为 2 的子串是否在其反转后的字符串中出现,返回 truefalse

直接的想法是将 s 中所有长度为 2 的子串放入 hashset,然后判断倒序字符串的长度为 2 的子串是否在集合中。

网友题解使用 int[] 记录所有出现过的长度为 2 的子串,下标表示第一个字母,第二个字母则使用 int 值的 bit 位表示。

代码


/**
 * @date 2024-12-26 8:47
 */
public class IsSubstringPresent3083 {

    public boolean isSubstringPresent(String s) {
        int n = s.length();
        int[] exists = new int[26];
        for (int i = 0; i < n - 1; i++) {
            int a = s.charAt(i) - 'a';
            int b = s.charAt(i + 1) - 'a';
            exists[a] |= 1 << b;
            if ((exists[b] >> a & 1) == 1) {
                return true;
            }
        }
        return false;
    }

}

性能

3218.切蛋糕的最小总开销 I

目标

有一个 m x n 大小的矩形蛋糕,需要切成 1 x 1 的小块。

给你整数 m ,n 和两个数组:

  • horizontalCut 的大小为 m - 1 ,其中 horizontalCut[i] 表示沿着水平线 i 切蛋糕的开销。
  • verticalCut 的大小为 n - 1 ,其中 verticalCut[j] 表示沿着垂直线 j 切蛋糕的开销。

一次操作中,你可以选择任意不是 1 x 1 大小的矩形蛋糕并执行以下操作之一:

  1. 沿着水平线 i 切开蛋糕,开销为 horizontalCut[i] 。
  2. 沿着垂直线 j 切开蛋糕,开销为 verticalCut[j] 。

每次操作后,这块蛋糕都被切成两个独立的小蛋糕。

每次操作的开销都为最开始对应切割线的开销,并且不会改变。

请你返回将蛋糕全部切成 1 x 1 的蛋糕块的 最小 总开销。

示例 1:

输入:m = 3, n = 2, horizontalCut = [1,3], verticalCut = [5]
输出:13
解释:
沿着垂直线 0 切开蛋糕,开销为 5 。
沿着水平线 0 切开 3 x 1 的蛋糕块,开销为 1 。
沿着水平线 0 切开 3 x 1 的蛋糕块,开销为 1 。
沿着水平线 1 切开 2 x 1 的蛋糕块,开销为 3 。
沿着水平线 1 切开 2 x 1 的蛋糕块,开销为 3 。
总开销为 5 + 1 + 1 + 3 + 3 = 13 。

示例 2:

输入:m = 2, n = 2, horizontalCut = [7], verticalCut = [4]
输出:15
解释:
沿着水平线 0 切开蛋糕,开销为 7 。
沿着垂直线 0 切开 1 x 2 的蛋糕块,开销为 4 。
沿着垂直线 0 切开 1 x 2 的蛋糕块,开销为 4 。
总开销为 7 + 4 + 4 = 15 。

说明:

  • 1 <= m, n <= 20
  • horizontalCut.length == m - 1
  • verticalCut.length == n - 1
  • 1 <= horizontalCut[i], verticalCut[i] <= 10^3

思路

有一块 m x n 的蛋糕,horizontalCut[i] 表示水平切第 i 行的开销,verticalCut[i] 表示垂直切第 i 列的开销。求将蛋糕切成 1 x 1 小块的最小代价。

需要注意每次切的蛋糕必须是整块的,并不能将几块蛋糕排到一起切。

我们应该先沿着代价最大的位置切吗?比如大小为 2 x 100 的蛋糕,水平切的代价为 99,垂直切每一列的代价为 100。先切每一列代价为 99 * 100,然后对切开的 100 块水平切 100 次,代价为 100 * 99,总代价为 2 * 99 * 100。如果先水平切一次,代价为 99。然后需要垂直切 2 * 99 次,代价为 2 * 99 * 100,总代价为 99 + 2 * 99 * 100。这种贪心策略应该是可行的,因为每切一次块数会增加,现在不切代价大的,后面再切的时候代价会增加。

选择沿某一水平或垂直线切割时,需要记录水平 和 垂直方向蛋糕块的数量,用来计算代价。每切一次,横向与纵向的蛋糕块就会增加一个。

刚开始不确定能否使用贪心策略,考虑如何表示哪些切过了,哪些没切,卡了很久。关键点是如何将问题抽象建模,使用分治思想,不要面向过程。每切一次之后,问题转化为切剩余蛋糕的子问题。我们可以使用记忆化搜索解空间,求出最小值。

代码


/**
 * @date 2024-12-25 10:41
 */
public class MinimumCost3218 {

    public int minimumCost_v1(int m, int n, int[] horizontalCut, int[] verticalCut) {
        int horizontalCutPart = 1;
        int verticalCutPart = 1;
        Arrays.sort(horizontalCut);
        Arrays.sort(verticalCut);
        int h = horizontalCut.length - 1;
        int v = verticalCut.length - 1;
        int res = 0;
        while (h >= 0 || v >= 0) {
            if (h < 0){
                res += horizontalCutPart * verticalCut[v];
                v--;
                continue;
            }
            if (v < 0){
                res += verticalCutPart * horizontalCut[h];
                h--;
                continue;
            }
            if (horizontalCut[h] > verticalCut[v]) {
                res += verticalCutPart * horizontalCut[h];
                horizontalCutPart++;
                h--;
            } else if (horizontalCut[h] <= verticalCut[v]) {
                res += horizontalCutPart * verticalCut[v];
                verticalCutPart++;
                v--;
            }
        }

        return res;
    }

    int[] rowCost;
    int[] colCost;
    int[][][][] mem;

    public int minimumCost(int m, int n, int[] horizontalCut, int[] verticalCut) {
        this.rowCost = horizontalCut;
        this.colCost = verticalCut;
        mem = new int[m + 1][m + 1][n + 1][n + 1];
        // rowStart rowEnd colStart colEnd 表示蛋糕的边界
        //   0   1   2   3   4   5  ... n
        // 0  ———————————————————
        //   |   |   |   |   |   |
        // 1  ———————————————————
        //   |   |   |   |   |   |
        // 2  ———————————————————
        //   |   |   |   |   |   |
        // 3 ———————————————————
        //   |   |   |   |   |   |
        // 4 ———————————————————
        //   |   |   |   |   |   |
        // 5  ———————————————————
        // ...
        // m
        return dfs(0, m, 0, n);
    }

    public int dfs(int rowStart, int rowEnd, int colStart, int colEnd) {
        if (rowEnd - rowStart == 1 && colEnd - colStart == 1) {
            return 0;
        }
        int res = Integer.MAX_VALUE;
        for (int i = rowStart + 1; i < rowEnd; i++) {
            if (mem[rowStart][i][colStart][colEnd] == 0) {
                mem[rowStart][i][colStart][colEnd] = dfs(rowStart, i, colStart, colEnd);
            }
            if (mem[i][rowEnd][colStart][colEnd] == 0) {
                mem[i][rowEnd][colStart][colEnd] = dfs(i, rowEnd, colStart, colEnd);
            }
            res = Math.min(res, rowCost[i - 1] + mem[rowStart][i][colStart][colEnd] + mem[i][rowEnd][colStart][colEnd]);
        }
        for (int i = colStart + 1; i < colEnd; i++) {
            if (mem[rowStart][rowEnd][colStart][i] == 0) {
                mem[rowStart][rowEnd][colStart][i] = dfs(rowStart, rowEnd, colStart, i);
            }
            if (mem[rowStart][rowEnd][i][colEnd] == 0) {
                mem[rowStart][rowEnd][i][colEnd] = dfs(rowStart, rowEnd, i, colEnd);
            }
            res = Math.min(res, colCost[i - 1] + mem[rowStart][rowEnd][colStart][i] + mem[rowStart][rowEnd][i][colEnd]);
        }
        return res;
    }

}

性能