1.安排工作以达到最大收益
题目:
有一些工作:difficulty[i] 表示第 i 个工作的难度,profit[i] 表示第 i 个工作的收益。
现在我们有一些工人。worker[i] 是第 i 个工人的能力,即该工人只能完成难度小于等于 worker[i] 的工作。
每一个工人都最多只能安排一个工作,但是一个工作可以完成多次。
举个例子,如果 3 个工人都尝试完成一份报酬为 1 的同样工作,那么总收益为 $3。如果一个工人不能完成任何工作,他的收益为 $0 。
我们能得到的最大收益是多少?
思路:
对于每个难度的工作,可以记录小于等于该难度工作对应的最大收益。初始化一维数组dp,长度是max(...difficulty,...workers)+1,
dp[i]表示的是难度小于等于i时的最大收益
先记录给定的,每个难度的工作对应的收益,然后dp转移方程是:
dp[i] = max(dp[i-1], map.get(i))
时间复杂度O(max(n,10^5)),空间复杂度O(max(n,10^5))
/**
* @param {number[]} difficulty
* @param {number[]} profit
* @param {number[]} worker
* @return {number}
*/
var maxProfitAssignment = function(difficulty, profit, worker) {
const l = difficulty.length;
const map = new Map();
for (let i = 0; i < l; i++) {
map.set(difficulty[i], Math.max(map.get(difficulty[i]) || 0, profit[i]));
}
const max = Math.max(...difficulty, ...worker);
const dp = new Array(max + 1).fill(0);
for (let i = 1; i <= max; i++) {
dp[i] = Math.max(dp[i - 1], map.get(i) || 0);
}
return worker.reduce((sum, item) => sum + dp[item], 0);
};
2.隐藏个人信息
题目:
给你一条个人信息字符串 S,它可能是一个 邮箱地址 ,也可能是一串 电话号码 。
我们将隐藏它的隐私信息,通过如下规则:
1. 电子邮箱
定义名称 name 是长度大于等于 2 (length ≥ 2),并且只包含小写字母 a-z 和大写字母 A-Z 的字符串。
电子邮箱地址由名称 name 开头,紧接着是符号 '@',后面接着一个名称 name,再接着一个点号 '.',然后是一个名称 name。
电子邮箱地址确定为有效的,并且格式是 "name1@name2.name3"。
为了隐藏电子邮箱,所有的名称 name 必须被转换成小写的,并且第一个名称 name 的第一个字母和最后一个字母的中间的所有字母由 5 个 '*' 代替。
2. 电话号码
电话号码是一串包括数字 0-9,以及 {'+', '-', '(', ')', ' '} 这几个字符的字符串。你可以假设电话号码包含 10 到 13 个数字。
电话号码的最后 10 个数字组成本地号码,在这之前的数字组成国际号码。注意,国际号码是可选的。我们只暴露最后 4 个数字并隐藏所有其他数字。
本地号码是有格式的,并且如 "***-***-1111" 这样显示,这里的 1 表示暴露的数字。
为了隐藏有国际号码的电话号码,像 "+111 111 111 1111",我们以 "+***-***-***-1111" 的格式来显示。在本地号码前面的 '+' 号和第一个 '-' 号仅当电话号码中包含国际号码时存在。例如,一个 12 位的电话号码应当以 "+**-" 开头进行显示。
注意:像 "(",")"," " 这样的不相干的字符以及不符合上述格式的额外的减号或者加号都应当被删除。
最后,将提供的信息正确隐藏后返回。
思路:先判断是邮箱还是电话号码,邮箱的话,用@分割字符串,然后对第一个name取首尾字符,然后其他转成小写字母。
号码的话,用正则提取数字,然后判断长度是否大于10,如果大于10,说明是国际号码,对应加上前缀
/**
* @param {string} S
* @return {string}
*/
var maskPII = function(S) {
if (S.includes("@")) {
const [name1, name2] = S.split("@");
const l1 = name1.length;
return `${name1[0].toLowerCase()}*****${name1[
l1 - 1
].toLowerCase()}@${name2.toLowerCase()}`;
} else {
S = S.replace(/\D+/g, "");
const l = S.length;
const local = `***-***-${S.slice(l - 4)}`;
return `${l > 10 ? `+${'*'.repeat(l - 10)}-` : ""}${local}`;
}
};
3.字符串中的查找和替换
题目:
某个字符串 S
需要执行一些替换操作,用新的字母组替换原有的字母组(不一定大小相同)。
每个替换操作具有 3 个参数:起始索引 i
,源字 x
和目标字 y
。规则是:如果 x
从原始字符串 S
中的位置 i
开始,那么就用 y
替换出现的 x
。如果没有,则什么都不做。
举个例子,如果 S = “abcd”
并且替换操作 i = 2,x = “cd”,y = “ffff”
,那么因为 “cd”
从原始字符串 S
中的位置 2
开始,所以用 “ffff”
替换它。
再来看 S = “abcd”
上的另一个例子,如果一个替换操作 i = 0,x = “ab”,y = “eee”
,以及另一个替换操作 i = 2,x = “ec”,y = “ffff”
,那么第二个操作将不会执行,因为原始字符串中 S[2] = 'c'
,与 x[0] = 'e'
不匹配。
所有这些操作同时发生。保证在替换时不会有任何重叠: S = "abc", indexes = [0, 1], sources = ["ab","bc"]
不是有效的测试用例。
思路:
在第一次遍历的时候,不能直接替换原字符串,因为这样会影响后续的查找和替换操作,所以第一次遍历时,可以先记录可以替换的的位置,即对应的indexes的下标。
然后,第二次遍历字符串S,这时需要替换了。因为需要从前往后替换,所以在执行替换之前,要对之前记录的下标进行升序排序。
在替换时,用一个变量pre记录上一次替换的结束位置,对于每一个符合条件的indexes[i]:
先拼接上一次替换的结束位置到当前替换的起始位置之间的字符串(因为两次替换之间是有空隙的)
然后拼接目标字符串,即target[indexes[i]]
最后更新pre的值pre = indexes[i] + sources[i].length
在最后遍历完可以替换的数组之后,别忘了拼接上S剩下的字符串
时间复杂度O(n)两次遍历 空间复杂度O(m) n是S的长度,m是indexes的长度
/**
* @param {string} S
* @param {number[]} indexes
* @param {string[]} sources
* @param {string[]} targets
* @return {string}
*/
var findReplaceString = function(S, indexes, sources, targets) {
const l = indexes.length;
const res = [];
for (let i = 0; i < l; i++) {
const target = sources[i];
if (S.slice(indexes[i], indexes[i] + target.length) === target) {
res.push(i);
}
}
res.sort((a, b) => indexes[a] - indexes[b]);
let v = "";
let pre = 0;
for (const i of res) {
v += S.slice(pre, indexes[i]);
v += targets[i];
pre = indexes[i] + sources[i].length;
}
const sl = S.length;
v += S.slice(pre, sl);
return v;
};
4.图像重叠
题目:
给出两个图像 A 和 B ,A 和 B 为大小相同的二维正方形矩阵。(并且为二进制矩阵,只包含0和1)。
我们转换其中一个图像,向左,右,上,或下滑动任何数量的单位,并把它放在另一个图像的上面。之后,该转换的重叠是指两个图像都具有 1 的位置的数目。
(请注意,转换不包括向任何方向旋转。)
最大可能的重叠是什么?
思路:枚举每一种可能的偏移量
时间复杂度O(n4)空间复杂度O(n2)
/**
* @param {number[][]} img1
* @param {number[][]} img2
* @return {number}
*/
var largestOverlap = function(img1, img2) {
const len = img1.length
const count = Array(2 * len + 1)
.fill(0)
.map(() => Array(2 * len + 1).fill(0))
for (let i = 0; i < len; i++) {
for (let j = 0; j < len; j++) {
if (img1[i][j] === 1) {
for (let i2 = 0; i2 < len; i2++) {
for (let j2 = 0; j2 < len; j2++) {
if (img2[i2][j2] === 1) {
count[i - i2 + len][j - j2 + len] += 1
}
}
}
}
}
}
let ans = 0
for (const row of count) {
ans = Math.max(...row, ans)
}
return ans
};
5.新21点
题目:
爱丽丝参与一个大致基于纸牌游戏 “21点” 规则的游戏,描述如下:
爱丽丝以 0
分开始,并在她的得分少于 K
分时抽取数字。 抽取时,她从 [1, W]
的范围中随机获得一个整数作为分数进行累计,其中 W
是整数。 每次抽取都是独立的,其结果具有相同的概率。
当爱丽丝获得不少于 K
分时,她就停止抽取数字。 爱丽丝的分数不超过 N
的概率是多少?
思路:动态规划。dp[i]表示分数为i时的概率。那么有
dp[i] = (dp[i-1] + dp[i-2]+ ... dp[i-w]) * 1/w
为了节约时间,用前缀和的方式记录dp[0] - dp[i]的概率和
/**
* @param {number} N
* @param {number} K
* @param {number} W
* @return {number}
*/
var new21Game = function(N, K, W) {
const dp = new Array(N + 1).fill(0);
const preSum = new Array(N + 1).fill(0);
dp[0] = preSum[0] = 1;
for (let i = 1; i < N + 1; i++) {
if (i - W <= 0) { // 被减的前缀区间消失
if (i <= K) { // 当前分数i没有超过K
dp[i] = preSum[i - 1] / W;
} else { // 当前分数i超过了K,i取K
dp[i] = preSum[K - 1] / W;
}
} else { // 正常的两个前缀区间相减
if (i <= K) { // 当前分数i没有超过K
dp[i] = (preSum[i - 1] - preSum[i - W - 1]) / W;
} else { // 当前分数i超过了K,i取K
dp[i] = (preSum[K - 1] - preSum[i - W - 1]) / W;
}
}
preSum[i] = preSum[i - 1] + dp[i];
}
return preSum[N] - preSum[K - 1];
};