LeetCode 第211场周赛

LeetCode 第211场周赛

1. 两个相同字符之间的最长子字符串

给你一个字符串 s,请你返回 两个相同字符之间的最长子字符串的长度 ,计算长度时不含这两个字符。如果不存在这样的子字符串,返回 -1 。
子字符串 是字符串中的一个连续字符序列。

示例 1:
输入:s = “aa”
输出:0
解释:最优的子字符串是两个 ‘a’ 之间的空子字符串。

示例 2:
输入:s = “abca”
输出:2
解释:最优的子字符串是 “bc” 。

提示:
1 <= s.length <= 300
s 只含小写英文字母

解析

本题是例行签到题。题目给定一个只包含小写英文字母的字符串,要求找到两个相同字符之间(不含头尾)的最多字符数量,也即长度。
由于本题数据范围很小,直接暴力求解。最暴力的解法,就是枚举26个字母,找到他们在字符串最左和最右出现的位置,然后求中间有多少个字符。全局最长的长度,一定是26个字母中的一个作为首尾字符的。
C++代码如下:

  int maxLengthBetweenEqualCharacters(string s) {
    int maxL = -1;
    for (int i = 0; i < 26; ++i) {
      char cur = 'a' + i;
      int start = 0, end = s.size() - 1;
      while (start < s.size() && s[start] != cur)++start;
      while (end >= 0 && s[end] != cur)--end;
      int tmp = end - start - 1;
      maxL = max(maxL, tmp);
    }
    return maxL;
  }

本题可以继续优化,典型的空间换时间,构造两个长度为26的数组,分别记录每个字母第一次(最左)和最后一次(最右)出现的位置,遍历一遍字符串即可填充数组。数组可以用-1等非法下标来标记没出现过的字符。最后遍历每个字母,计算其出现最左和最右之间的长度,求极值即可。

int maxLengthBetweenEqualCharacters(string s) {
        int maxL = -1;
        vector<int>first(26, -1), last(26, -1);
        for (int i = 0; i < s.size(); ++i) {
        if (first[s[i] - 'a'] == -1) first[s[i] - 'a'] = i;
        last[s[i] - 'a'] = i;
        }
        for (int i = 0; i < 26; ++i) {
        if (first[i] != -1 && last[i] != -1) maxL = max(maxL, last[i] - first[i] - 1);
        }
        return maxL;
    }

2. 执行操作后字典序最小的字符串

给你一个字符串 s 以及两个整数 a 和 b 。其中,字符串 s 的长度为偶数,且仅由数字 0 到 9 组成。

你可以在 s 上按任意顺序多次执行下面两个操作之一:

累加:将 a 加到 s 中所有下标为奇数的元素上(下标从 0 开始)。数字一旦超过 9 就会变成 0,如此循环往复。例如,s = “3456” 且 a = 5,则执行此操作后 s 变成 “3951”。
轮转:将 s 向右轮转 b 位。例如,s = “3456” 且 b = 1,则执行此操作后 s 变成 “6345”。
请你返回在 s 上执行上述操作任意次后可以得到的 字典序最小 的字符串。

如果两个字符串长度相同,那么字符串 a 字典序比字符串 b 小可以这样定义:在 a 和 b 出现不同的第一个位置上,字符串 a 中的字符出现在字母表中的时间早于 b 中的对应字符。例如,"0158” 字典序比 “0190” 小,因为不同的第一个位置是在第三个字符,显然 ‘5’ 出现在 ‘9’ 之前。

示例 1:
输入:s = “5525”, a = 9, b = 2
输出:“2050”
解释:执行操作如下:
初态:“5525”
轮转:“2555”
累加:“2454”
累加:“2353”
轮转:“5323”
累加:“5222”
累加:“5121”
轮转:“2151”
累加:"2050"​​​​​​​​​​​​
无法获得字典序小于 “2050” 的字符串。

提示:
2 <= s.length <= 100
s.length 是偶数
s 仅由数字 0 到 9 组成
1 <= a <= 9
1 <= b <= s.length - 1

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lexicographically-smallest-string-after-applying-operations
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解析

本题为只包含0-9数字的字符串提供了2种操作,一种是对奇数下标位置字符做增加(实际应该是数值层间上增加后对10取余,再转为字符),一种循环右移一定位数。能够进行任意多次数的两种操作,问能够达到的字典序最小的字符串。
本题粗略分析,由于增加运算是增加后对10取余(或称为个位上的尾数)而每次增加的数值是一定的,也就是参数a,那么一个数位上可能的状态就是最多0-9这10种,一些情况还达不到10种(最简单的,数值为5,a=5,只有【0,5】两种取值)。同样的,轮转也是类似的效果,字符串长度和轮转的个数都是固定的,只存在有限多个不同顺序的组合。
虽然乍一看这些操作组合会导致一个字符串可能变换的状态非常多,但实际上本题确实不会超时。(我一开始没想道如何优化,就提交了暴力解,结果居然直接过了!)

实际上因为每次都只能对奇数位置同时增加(轮转后增加相当于对偶数操作),奇偶两部分每部分都有10个取值;而轮转操作k次每次b个字符,本身相当于从末尾取出 k ∗ b % l k*b\%l kb%l个字符放在头部,l是字符串长度。因此所有可能的状态也就是 1 0 2 ∗ l 10^2*l 102l种,加上每次变换的复杂度是O(l),而为了保存有序状态的复杂度是O(NlgN)所以暴力求解的复杂度为 O ( 100 ∗ l 2 ∗ l o g ( 100 ∗ l 2 ) ) O(100*l^2*log(100*l^2)) O(100l2log(100l2))当然实际上并没有这么大,考虑到l最大为100这个数值大约在 1 0 8   1 0 9 10^8~10^9 108 109之间,暴力求解不优化实际上很极限。提交后发现用时844ms,差点就超了。

因此,本题直接BFS暴力求解即可。我们构造一个集合set记录所有出现过的状态,每次得到一个新状态,就将其放入集合,并分别进行2种操作,得到新的2个字符串;如果一个状态已经出现在集合中了,就无需算下去了,因为这个状态的子状态(通过若干操作可得的字符串)必然也讨论过了。

string opA(string s, int a) {
    int i = 1;
    string t = s;
    while (i < t.size()) {
      t[i] = ((t[i] - '0' + a) % 10) + '0';
      i = i + 2;
    }
    return t;
  }
  string opB(string s, int b) {
    int n = s.size();
    string t = s.substr(n - b) + s.substr(0, n - b);
    return t;
  }
  void bfs(string s, int a, int b, set<string>& ss) {
    if (ss.count(s)) return;
    ss.insert(s);
    string t1 = opA(s, a), t2 = opB(s, b);
    bfs(t1, a, b, ss);
    bfs(t2, a, b, ss);
  }
  string findLexSmallestString(string s, int a, int b) {
    set<string>ss;
    bfs(s, a, b, ss);
    return *ss.begin();
  }

代码如上,很直接。opA和opB分别是题目给定的两个操作,bfs是递归搜索函数,初始我们传入原始字符串s,将其加入集合,同时计算经过a和b操作变换后的字符串,递归做下去。当遇到一个状态已经出现在集合中了这条递归路就直接返回即可。

本题虽然暴力能过,但存在很大优化空间。
例如力扣中文网站上lucifer1004大佬的题解
虽然解法思路都是枚举,但是由于按照外层循环轮转,内层循环自增的原则,不会出现重复或死循环的情况,因此不需要set记录所有的状态,每次枚举都和当前最优作比较,直接更新最优解,节省了排序和记录的时间与空间,避免了递归解法太深带来的时间开销。该方法提交后显示时间116ms,差不多省去了一个lgN的时间。

3. 无矛盾的最佳球队

假设你是球队的经理。对于即将到来的锦标赛,你想组合一支总体得分最高的球队。球队的得分是球队中所有球员的分数 总和 。

然而,球队中的矛盾会限制球员的发挥,所以必须选出一支 没有矛盾 的球队。如果一名年龄较小球员的分数 严格大于 一名年龄较大的球员,则存在矛盾。同龄球员之间不会发生矛盾。

给你两个列表 scores 和 ages,其中每组 scores[i] 和 ages[i] 表示第 i 名球员的分数和年龄。请你返回 所有可能的无矛盾球队中得分最高那支的分数 。

示例 1:
输入:scores = [1,3,5,10,15], ages = [1,2,3,4,5]
输出:34
解释:你可以选中所有球员。

示例 2:
输入:scores = [4,5,6,5], ages = [2,1,2,1]
输出:16
解释:最佳的选择是后 3 名球员。注意,你可以选中多个同龄球员。

提示:
1 <= scores.length, ages.length <= 1000
scores.length == ages.length
1 <= scores[i] <= 106
1 <= ages[i] <= 1000

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-team-with-no-conflicts
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解析

本题给定了球员的年龄和分数,并且一个球员的分数低于年龄比自己小的球员的话就会冲突,无法共存。题目要求我们找出不冲突的组合使得分数和最高。
本题为一道动态规划问题。首先如果对球员按照年龄从小到大、年龄相同分数从小到大顺序进行排序。这样后一个人的年龄满足非递减(递增或一致),因此,两个人是否冲突就只需要看排在后面的人的分数是否低于前面的即可。

既然年龄是递增状态,要想不冲突,最终队员列表如果依然按照年龄排序,那么分数也要保证是递增(允许相同)才可以。这样相对于排序后的分数数组,实际上就是要我们从中选取一个不减小的子序列,并计算子序列的和,求这个“不降低”子序列的最大和。

本题重点就是,按照年龄递增排序后,分数也保持递增或相等(不能减小),等价于题目要求的不冲突,只要完成了这一步转化,就很容易了。

递增子序列是经典的动态规划问题。我们定义dp[i]表示第i个球员结尾(最后一个入选队员)时的最大分数。状态转移方程如下:
d p [ i ] = m a x ( d p [ j ] , 0 ) + s c o r e s [ i ] , 0 ≤ j < i , s c o r e s [ i ] ≥ s c o r e s [ j ] dp[i] = max(dp[j],0)+scores[i],0\le jdp[i]=max(dp[j],0)+scores[i],0j<i,scores[i]scores[j]
每个队员结尾的最大分数,就是遍历之前所有年龄小于等于该球员、同时分数也小于等于该球员的不冲突球员,以他们结尾的最大分数再加上当前球员的分数。因为之前球员结尾的不冲突分数已经计算过了,并且以他们结尾的最大分数组队情况肯定是不冲突的(每次都选择不冲突里最大的)。由于dp记录的是“结尾”,也就是表明这个球员是年龄最大或年龄一致分数最高的,所以i如果跟j不冲突,那么对于以j结尾取得最大分数的方案中的其他球员(年龄或分数都不会超过j)肯定也不会跟i冲突。

算法时间复杂度是 O ( N 2 ) O(N^2) O(N2),不过人数最多也就1000,完全不会超时。

C++代码如下:

int bestTeamScore(vector<int>& scores, vector<int>& ages) {
    int maxS = 0,len=scores.size();
    vector<pair<int, int>>vp;
    for (int i = 0; i < len; ++i) {
      vp.push_back({ ages[i],scores[i] });
    }
    sort(vp.begin(), vp.end());
    vector<int>dp(len, 0);
    dp[0] = vp[0].second;
    maxS = dp[0];
    for (int i = 1; i < len; ++i) {
      int tmp = 0;
      for (int j = i - 1; j >= 0; --j) {
        if (vp[i].second >= vp[j].second) {
          tmp = max(tmp, dp[j]);
        }
      }
      dp[i] = tmp + vp[i].second;
      maxS = max(maxS, dp[i]);
    }
    return maxS;
  }

实际上按照题目所谓不冲突的设定,换言之,年龄分数同时保持递增,优先分数排序也是可以的。从实际意义上理解,就是在优先分数排序后,在年龄数组里寻找递增的子序列。

本题重点就在于将题目的不冲突翻译成排序后的递增子序列问题,实际上本题最直接的思路可能是0-1背包,但是人数最大1000分数最大100000,这个二维dp会超时,所以要变换一下思路。同时要理解dp记录的是每个人结尾的最大分数,意味着取得这个分数的时候,所选的人不冲突,并且这个位置的人是年龄最大(或分数最高)的,所以后面的人跟该位置不冲突,就跟该位置结尾最大分数的方案里所有人都不冲突。

4. 带阈值的图连通性

有 n 座城市,编号从 1 到 n 。编号为 x 和 y 的两座城市直接连通的前提是: x 和 y 的公因数中,至少有一个 严格大于 某个阈值 threshold 。更正式地说,如果存在整数 z ,且满足以下所有条件,则编号 x 和 y 的城市之间有一条道路:

x % z == 0
y % z == 0
z > threshold
给你两个整数 n 和 threshold ,以及一个待查询数组,请你判断每个查询 queries[i] = [ai, bi] 指向的城市 ai 和 bi 是否连通(即,它们之间是否存在一条路径)。

返回数组 answer ,其中answer.length == queries.length 。如果第 i 个查询中指向的城市 ai 和 bi 连通,则 answer[i] 为 true ;如果不连通,则 answer[i] 为 false 。

示例 1:
LeetCode 第211场周赛_第1张图片
输入:n = 6, threshold = 2, queries = [[1,4],[2,5],[3,6]]
输出:[false,false,true]
解释:每个数的因数如下:
1: 1
2: 1, 2
3: 1, 3
4: 1, 2, 4
5: 1, 5
6: 1, 2, 3, 6
所有大于阈值的的因数已经加粗标识,只有城市 3 和 6 共享公约数 3 ,因此结果是:
[1,4] 1 与 4 不连通
[2,5] 2 与 5 不连通
[3,6] 3 与 6 连通,存在路径 3–6

解析

本题的中文描述和样例实际很容易让人误解。我一来是以为连通仅代表两个城市的编号数值存在大于阈值的公约数(或称两数的最大公约数大于阈值)即可。但实际上这是题目“道路”的含义,就是无向图的边。但题目query里问的是是否存在“路径”path,不是问二者有没有边,而是问二者是不是连通,可以从多条边构成的路径访问依然是连通的。例如9和16,显然二者互质,但如果阈值是2,那么16和12 存在4这个公约数,12和9之间又存在3这个公约数,所以这两条边构成了16-12-9的路径。坑爹的地方在于,样例根本没有这种情况,点击提交才会发现一组输入里有这个问题。不过也对,按照是否存在边来看的话,确实太简单了,不足以作为hard和最后一题。

本题暴力解法的思路比较直接,通过公约数和阈值能够找到图中所有的边,然后根据查询的两城市编号BFS能不能找到判断是否存在路径;或者BFS建立所有城市间的连通关系,查表即可。但显然,不优化必然超时。

本题复杂性原因在于边数太多。例如数字k大于阈值,而在1-n中有 x = n / k x=n/k x=n/k个k的倍数(向下取整),理论上这些点两两之间都有一条边,那就是 x 2 x^2 x2条,但实际上并不需要,我们只需要“相邻”的连在一起 x − 1 x-1 x1条边就够了,依然可以保证这些点是彼此相连的。

本题的解决思路是并查集。这里参考了力扣上的题解

class UnionFind {
  int n;
  vector<int> parent, size;

public:
  UnionFind(int n) {
    this->n = n;
    parent = vector<int>(n);
    size = vector<int>(n, 1);
    for (int i = 0; i < n; ++i)
      parent[i] = i;
  }

  int find(int idx) {
    if (parent[idx] == idx)
      return idx;
    return parent[idx] = find(parent[idx]);
  }

  void connect(int a, int b) {
    int fa = find(a), fb = find(b);
    if (fa != fb) {
      if (size[fa] > size[fb]) {
        parent[fb] = fa;
        size[fa] += size[fb];
      } else {
        parent[fa] = fb;
        size[fb] += size[fa];
      }
    }
  }
};

class Solution {
public:
    vector<bool> areConnected(int n, int threshold, vector<vector<int>>& queries) {
int len = queries.size();
    vector<bool>ans(len, false);
    UnionFind uf(n + 1);
    for (int i = threshold + 1; i <= n; ++i) {
      for (int j = 2 * i; j <= n; j += i) {
          uf.connect(i, j);
      }
    }
    for (int i = 0; i < len; ++i) {
      int a = queries[i][0], b = queries[i][1];
      if (uf.find(a) == uf.find(b)) ans[i] = true;
    }
    return ans;
    }
};

并查集UnionFind包含成员变量n用于记录城市数量,parent和size数组分别记录每个节点的父节点编号与连接的数量。建立连接也并不需要遍历两两数对,而是枚举超过阈值、小于等于n的所有数值,并找到具有这一因数的数字,让他们都和这个数建立连接(比如遍历到3的时候,会让3跟6、3跟9建立连接,而不是建立6和9之间的连接。这样可以避免冗余的边。

连接过程中如果二者父节点一致,表明以及建立连接了;反之,则选择连接较多的那个,另一个作为其子节点。这样,存在边的节点都会连接向同一个父节点。判定过程中,只需查找二者的父节点是否相等,就能判断是否存在路径。

你可能感兴趣的:(算法与数据结构,LeetCode,C++)