算法周赛笔记(7月第2周)— LeetCode 第250场

·  阅读 231

题目

LeetCode - 1935

LeetCode - 1935: 可以输入的最大单词数

键盘出现了一些故障,有些字母键无法正常工作。而键盘上所有其他键都能够正常工作。

给你一个由若干单词组成的字符串 text ,单词间由单个空格组成(不含前导和尾随空格);另有一个字符串 brokenLetters ,由所有已损坏的不同字母键组成,返回你可以使用此键盘完全输入的 text 中单词的数目。

示例:

输入:text = "leet code", brokenLetters = "lt" 输出:1 解释:无法输入 "leet" ,因为字母键 'l' 和 't' 已损坏。

题解

签到题,简单模拟即可(虽然是签到题,但我还是花了20分钟😓)。

对于每个单词,需要判断这个单词中是否包含破损的字母,最简单的想法就是:对于所有破损的字母,插入到一个哈希表中,然后对每个单词,遍历单词的全部字母,依次判断每个字母是否在哈希表中出现过即可。由于一共只有26个小写字母,我的做法是直接开一个大小为26的布尔数组,来表示某个字母是否破损。

class Solution {
    public int canBeTypedWords(String text, String brokenLetters) {
		int ctn = 0;
		boolean[] st = new boolean[26];
		for (int i = 0; i < brokenLetters.length(); i++) {
			int index = brokenLetters.charAt(i) - 'a';
			st[index] = true;
		}

		boolean flag = false; // 某个单词是否存在破损字母
		for (int i = 0; i < text.length(); i++) {
			// 当前单词已破损, 并且没有达到下一个单词的分隔符, 直接跳过
			if (flag && text.charAt(i) != ' ') continue;
			else if (text.charAt(i) == ' ') {
                // 达到单词分隔符, 判断前一个单词是否破损
				if (!flag) ctn++;
				flag = false; // 重置flag
				continue;
			}
			int index = text.charAt(i) - 'a';
			if (st[index]) flag = true;
		}
        // 最后一个单词末尾没有空格符, 需要单独判断一下最后的flag
		if (!flag) ctn++;
		return ctn;
	}
}
复制代码

LeetCode - 1936

LeetCode - 1936: 新增的最少台阶数

给你一个 严格递增 的整数数组 rungs ,用于表示梯子上每一台阶的 高度 。当前你正站在高度为 0 的地板上,并打算爬到最后一个台阶。

另给你一个整数 dist 。每次移动中,你可以到达下一个距离你当前位置(地板或台阶)不超过 dist 高度的台阶。当然,你也可以在任何正 整数 高度处插入尚不存在的新台阶。

返回爬到最后一阶时必须添加到梯子上的 最少 台阶数。

示例:

输入:rungs = [1,3,5,10], dist = 2 输出:2 解释: 现在无法到达最后一阶。 在高度为 7 和 8 的位置增设新的台阶,以爬上梯子。 梯子在高度为 [1,3,5,7,8,10] 的位置上有台阶。

题解

签到题,简单模拟即可。

遍历 rungsrungs 数组,当遍历到第 ii 个位置时,若 rungs[i+1]rungs[i]distrungs[i+1] - rungs[i] \le dist ,则无需加梯子;若 rungs[i+1]rungs[i]>distrungs[i+1] - rungs[i] \gt dist 时,需要添加梯子,需要添加的最少梯子数是 rungs[i+1]rungs[i]dist1\lceil\frac{rungs[i+1]-rungs[i]}{dist}\rceil-1

解释:当在两个位置之间添加 11 个梯子时,最多能够支持连接 2×dist2 \times dist 的距离,比如想从高度 33 走到 77,而 dist=2dist = 2,则只需要在高度 55 的位置添加一个梯子,就可以达到目的。当添加 22 个梯子时,最多能支持连接 3×dist3 \times dist 的距离,比如从高度33 走到 1212,而 dist=3dist = 3,则只需要在高度 6699 的位置添加 22 个梯子即可;所以添加 nn 个梯子,能连接 (n+1)×dist(n+1) \times dist 的距离。用 gapgap 表示需要连接的距离,则最少需要 gapdist1\lceil\frac{gap}{dist}\rceil-1 个梯子。

class Solution {
    public int addRungs(int[] rungs, int dist) {
		int ctn = 0;
		for (int i = 0; i < rungs.length; i++) {
			int pre = i == 0 ? 0 : rungs[i - 1]; // 注意是从高度0开始爬梯子
			int gap = rungs[i] - pre;
			if (gap <= dist) continue; // 无需加梯子
			else {
				int q = gap / dist;
				int r = gap % dist;
				if (r > 0) q++; // 向上取整
				ctn += q - 1;
			}
		}
		return ctn;
	}
}
复制代码

LeetCode - 1937

LeetCode - 1937: 扣分后的最大得分

给你一个 m x n 的整数矩阵 points (下标从 0 开始)。一开始你的得分为 0 ,你想最大化从矩阵中得到的分数。

你的得分方式为:每一行 中选取一个格子,选中坐标为 (r, c) 的格子会给你的总得分 增加 points[r][c]

然而,相邻行之间被选中的格子如果隔得太远,你会失去一些得分。对于相邻行 r 和 r + 1 (其中 0 <= r < m - 1),选中坐标为 (r, c1) 和 (r + 1, c2) 的格子,你的总得分 减少 abs(c1 - c2) 。(abs表示取绝对值)

请你返回你能得到的 最大 得分。

示例:

输入:points = [[1,2,3],[1,5,1],[3,1,1] 输出:9 解释: 蓝色格子是最优方案选中的格子,坐标分别为 (0, 2),(1, 1) 和 (2, 0) 你的总得分增加 3 + 5 + 3 = 11 。 但是你的总得分需要扣除 abs(2 - 1) + abs(1 - 0) = 2 你的最终得分为 11 - 2 = 9

题解

经过思考发现,只有相邻两行会导致扣分。我们不妨设定,下面的一行会受上面一行的影响。

举个例子,当在第 ii 行,选择了第 jj 列时。那么前 ii 行一定有一个最大得分,不妨将其设为 f(i,j)f(i,j)

比如上面的示例,当处理第 11 行的所有列时,容易得到 f(0,0)=1f(0,1)=2f(0,2)=3f(0,0)=1,f(0,1)=2,f(0,2)=3 (第一行比较特殊)

当处理第二行时,对于第一列,只需要遍历前一行的所有列,则可得出,第二行选择第一列时,前两行的最大可能得分,容易得到f(1,0)=2f(1,0)=2,前一行无论选择 112233f(1,0)f(1,0) 的计算结果都一样是 22,对于第二行第二列同理,可以得到 f(1,1)=7f(1,1)=7(第一行选择 22 或者 33),第三列 f(1,2)=4f(1,2)=4

同理,当处理第 33 行时,只需要关注第 22 行的每一列的最大可能得分即可(无需再关注第 11 行)

f(2,0)=9f(2,0)=9f(2,1)=8f(2,1)=8f(2,2)=7f(2,2)=7

我们用 f(i,j)f(i,j) 来表示第 ii 行选择第 jj 列时,前 ii 行的最大可能得分。

随后在计算第 i+1i+1 行的各个列的最大可能得分时,只需要关注第 ii 行各个列的最大得分即可,因为扣分只是发生在相邻两行,而与更前面的行所选择的列无关。

所以,对于一行中的每一列,只需要维护,选择这一列时,可能的最大得分即可。

如何求出某一行的某一列的最大可能得分呢?只需要枚举上一行所有列的最大得分,依次计算,取最大值即可。

容易得到状态转移方程:f(i,j)=max(f(i1,k)+pi,j+abs(jk))f(i,j)=max(f(i-1,k)+p_{i,j}+abs(j-k)) ,其中 pi,jp_{i,j} 就是题目给出的整数矩阵points[i][j]k[1,n]k \in [1,n]

这样的话,对于整个 m×nm \times n 的矩阵,共有 m×nm \times n 个状态,而求解每个状态 (i,j)(i,j) 对应的 f(i,j)f(i,j),需要遍历前一行的所有列。即,求解每个 f(i,j)f(i,j) 需要的时间复杂度是 O(n)O(n),则总的时间复杂度就是 O(m×n2)O(m \times n^2),这样会超时。我们需要想办法进行优化。

jkj \ge k 时,abs(jk)=jkabs(j-k)=j-k;当 j<kj \lt k 时,abs(jk)=kjabs(j-k)=k-j

则上面的状态转移方程可以转化为

f(i,j)=pi,j+j+max(f(i1,k)k)jkf(i,j)=p_{i,j}+j+max(f(i-1,k)-k),j \ge k

f(i,j)=pi,jj+max(f(i1,k)+k)j<kf(i,j)=p_{i,j}-j+max(f(i-1,k)+k),j \lt k

所以,求解 f(i,j)f(i,j), 只需要求解 当 jkj \ge k 时,f(i1,k)kf(i-1,k)-k 的最大值;当 j<kj \lt k 时,f(i1,k)+kf(i-1,k)+k 的最大值,两者再选最大即可。

即对于 jj 的左边的列(jkj \ge k),求解 max(f(i1,k)k)max(f(i-1,k)-k),对于 jj 右边的列(j<kj \lt k),求解 max(f(i1,k)+k)max(f(i-1,k)+k)

则只需要遍历两次 i1i-1 行的所有列,即可求出第 ii 行的所有列的 f(i,j)f(i,j),时间复杂度就优化成了 O(m×n)O(m \times n)

最终的答案就是最后一行的所有列的得分中的最大者,即 ans=max(f(m1,k))ans = max(f(m-1,k))k[0,n)k \in [0,n),注意下标从 00 开始,这与前面的讲解有点出入。

由于每次只需要关注第 i1i-1 行和 第 ii 行,所以我们可以用 22 个一维数组进行操作即可。

先上一个比赛过程中我的写法(未优化的)

class Solution {
    public long maxPoints(int[][] points) {
		int m = points.length, n = points[0].length;
		int[] temp = new int[n];
		int[] res = new int[n];
		int max = 0;
		for (int i = 0; i < m; i++) {
			// 处理每一行
			for (int j = 0; j < n; j++) {
				// 处理每一列
				if (i == 0) {
					temp[j] = points[i][j];
					res[j] = points[i][j];
				} else {
					// 取最大
					int t = 0;
					for (int k = 0; k < n; k++) {
						t = Math.max(t, points[i][j] + temp[k] - Math.abs(k - j));
					}
					res[j] = t;
				}
				max = Math.max(res[j], max);
			}
			System.arraycopy(res, 0, temp, 0, n);
		}
		return max;
	}
}
复制代码

好家伙!差2个用例(哭

当时弄死就是想不到可以如何优化,唉,还得多练

下面上一个优化后的写法

class Solution {
    
    /**
	 * f(i, j) = p[i][j] + max { f(i - 1, k) - abs(j - k) }
	 * ==> 化简
	 * f(i, j) = p[i][j] + max { f(i - 1, k) - (j - k) }   , k <= j
	 * f(i, j) = p[i][j] + max { f(i - 1, k) - (k - j) }   , k > j
	 * ==> 化简, 提出 j 
	 * f(i, j) = p[i][j] - j + max { f(i - 1, k) + k }   ,  k <= j
	 * f(i, j) = p[i][j] + j + max { f(i - 1, k) - k }   ,  k > j
	 *
	 * 对于当前行的 k 属于 0~n-1 的每个列, 都可以计算出 f(i - 1, k) + k 和  f(i - 1, k) - k, 只需要遍历两次即可, 时间复杂度 O(2n)
	 * */
    public long maxPoints(int[][] points) {
		
        int m = points.length, n = points[0].length;

		long[] res = new long[n]; // 用一维数组来存储 f(i,j)

		long[] leftMax = new long[n]; // 某一列左侧的最大的 f(i-1,k) + k

		long[] rightMax = new long[n]; // 某一列右侧的最大的 f(i-1,k) - k

		long ans = 0;

		// 初始化第一行
		for (int i = 0; i < n; i++) res[i] = points[0][i];
		// 从第二行开始
		for (int i = 1; i < m; i++) {
			// 先处理出上一行的数据, 先找到j右侧的最大值
			long max = res[n - 1] - n + 1; // 最后一个k的位置的 f(i-1,k) - k , k = n - 1
			for(int j = n - 1; j >= 0; j--) rightMax[j] = max = Math.max(res[j] - j, max);
			// 再找到 j 左侧的最大值
			max = res[0]; // 第一个k的位置的 f(i-1,k) + k , k = 0
			for(int j = 0; j < n; j++) leftMax[j] = max = Math.max(res[j] + j, max);
			// 更新该行的数据
			for(int j = 0; j < n; j++) res[j] = points[i][j] + Math.max(leftMax[j] - j, rightMax[j] + j);
		}
		// 处理结束后, 找出最大值
		for (int i = 0; i < n; i++) ans = Math.max(res[i], ans);
		return ans;
	}
}
复制代码

终于!!搞了2天终于把这道题给搞懂了!(哭

LeetCode - 1938

LeetCode - 1938: 查询最大基因差

给你一棵 n个节点的有根树,节点编号从 0​n - 1。每个节点的编号表示这个节点的 独一无二的基因值 (也就是说节点 x​ 的基因值为 x​)。两个基因值的 基因差 是两者的 异或和 。给你整数数组 parents ,其中 parents[i] 是节点 i 的父节点。如果节点 x 是树的 ,那么 parents[x] == -1

给你查询数组 queries ,其中 queries[i] = [nodei, vali] 。对于查询 i ,请你找到 valipi 的 最大基因差 ,其中 pi 是节点 nodei 到根之间的任意节点(包含 nodei 和根节点)。更正式的,你想要最大化 vali XOR pi

请你返回数组 ans ,其中 ans[i] 是第 i 个查询的答案。

题解

先上一个比赛过程中我的写法(暴力,没有优化,报TLE了)

class Solution {
    public int[] maxGeneticDifference(int[] parents, int[][] queries) {
		int[] ans = new int[queries.length];
		for (int i = 0; i < queries.length; i++) {
			int node = queries[i][0], val = queries[i][1];
			int t = 0;
			while(node != -1) {
				t = Math.max(node ^ val, t);
				node = parents[node];
			}
			ans[i] = t;
		}
		return ans;
	}
}
复制代码

可以将所有查询先存起来(离线),然后再用DFS+Trie树(字典树)

具体思路后续补充 TODO

看完题解后,自己动手写了一遍(注意开数组时,在方法内部开,不要用最大的长度开,那样会超时,坑死我了!)

class Solution {
	// 最大值 2 * 10^5 , 即最大 2^18, 只需要18位即可
	// 用数组模拟来做吧
	// 节点的个数为 10^5

	final int HIGH_BIT = 17; // 只需要18位, 最高位需要左移 17

	int idx; // 节点编号, trie 树

	int[][] trie; // trie 树

	int[] ctn; // 节点计数, 主要方便从 trie 树中删除某个元素

	int[] h, e, ne;

	int idx2;

	int treeRoot;

	// DFS + Trie
	// TLE
	public int[] maxGeneticDifference(int[] parents, int[][] queries) {
		// 动态开数组
		// 如果开全局数组, 则需要把长度开到最大, 那样全部用例加起来的执行时间会超时
        trie = new int[parents.length * 18][2];
        ctn = new int[parents.length * 18];
		// 先将全部的查询存起来, 采用 LinkedHashMap, 以保证插入顺序不变
		Map<Integer, List<Integer>> queriesMap = new HashMap<>();
		List<Pair> queriesList = new ArrayList<>(queries.length);
		for (int i = 0; i < queries.length; i++) {
			int node = queries[i][0];
			int val = queries[i][1];
			queriesList.add(new Pair(node, val));
			if (queriesMap.containsKey(node)) queriesMap.get(node).add(val);
			else queriesMap.put(node, new ArrayList<>(Arrays.asList(val)));
		}

		h = new int[parents.length];
		e = new int[parents.length];
		ne = new int[parents.length];
		Arrays.fill(h, -1);

		Map<Pair, Integer> resMap = new HashMap<>(queries.length);
		// 建树
		buildTree(parents);
		// 深搜
		dfs(treeRoot, queriesMap, resMap);
		int[] res = new int[queries.length];
		for (int i = 0; i < queries.length; i++) {
			Pair query = queriesList.get(i);
			res[i] = resMap.get(query);
		}
		return res;
	}

	public void dfs(int x, Map<Integer, List<Integer>> queriesMap, Map<Pair, Integer> resMap) {
		add(x);
		if (queriesMap.containsKey(x)) {
			List<Integer> list = queriesMap.get(x);
			for (int i : list) {
				int res = query(i);
				resMap.put(new Pair(x, i), res);
			}
		}
		for (int i = h[x]; i != -1 ; i = ne[i]) {
			int j = e[i];
			dfs(j, queriesMap, resMap);
		}
		remove(x);
	}

	public void buildTree(int[] parents) {
		for (int i = 0; i < parents.length; i++) {
			if (parents[i] == -1) treeRoot = i;
			else {
				e[idx2] = i;
				ne[idx2] = h[parents[i]];
				h[parents[i]] = idx2++;
			}
		}
	}

	// 添加一个数到 Trie 树
	public void add(int x) {
		int p = 0; // root
		for (int i = HIGH_BIT; i >= 0 ; i--) {
			int u = (x >> i) & 1; // 获取当前位
			if (trie[p][u] == 0) trie[p][u] = ++idx; // 不存在这个节点, 直接新增一个
			ctn[trie[p][u]]++; // 节点计数
			p = trie[p][u];
		}
	}

	// 从 Trie 树中移除一个数
	public void remove(int x) {
		int p = 0;
		for (int i = HIGH_BIT; i >= 0 ; i--) {
			int u = (x >> i) & 1;
			ctn[trie[p][u]]--; // 直接计数减1即可
			p = trie[p][u];
		}
	}

	// 从 Trie 数中找到一个和 x 异或结果最大的数, 并输出结果
	public int query(int x) {
		int p = 0, ans = 0;
		for (int i = HIGH_BIT; i >= 0 ; i--) {
			int u = (x >> i) & 1;
			u = 1 - u;
			ans = ans << 1; // 先乘2
			if (trie[p][u] > 0 && ctn[trie[p][u]] > 0) {
				// 这个节点真实存在, 则走过去
				ans++;
				p = trie[p][u];
			} else {
				p = trie[p][1 - u]; // 否则, 走到另一边
			}
		}
		return ans;
	}
}

class Pair {
		int node;
		int val;

		public Pair(int node, int val) {
			this.node = node;
			this.val = val;
		}

		@Override
		public boolean equals(Object o) {
			if (this == o) return true;
			if (o == null || getClass() != o.getClass()) return false;
			Pair pair = (Pair) o;
			return node == pair.node &&
					val == pair.val;
		}

		@Override
		public int hashCode() {
			return Objects.hash(node, val);
		}
}
复制代码

上周比赛时,4道题只做出了前2道。第三道和第四道都只写出了暴力解法,不知道如何下手优化。

好在,经过两天的看题解和撸代码。终于!把这周周赛消化完了!给自己鼓个爪,啪啪啪!

下周再接再厉!

下周见!

(完)

分类:
后端
标签: