2022年4月周赛习题笔记

目录

  • 4月第一周
    • 1. ACWing
      • 1.1 字符串价值
      • 1.2 最长连续子序列
      • 1.3 最大子矩阵
    • 2. LeetCode
      • 2.1 单周赛
        • 2.1.1 2224. 转化时间需要的最少操作数
        • 2.1.2 2225. 找出输掉零场或一场比赛的玩家
        • 2.1.3 2226. 每个小孩最多能分到多少糖果
      • 2.2 双周赛
        • 2.2.1 2220. 转换数字的最少位翻转次数
        • 2.2.2 2221. 数组的三角和
        • 2.2.3 2222. 选择建筑的方案数
  • 4月第二周
    • 1. LeetCode
      • 1.1 6037. 按奇偶性交换后的最大数字
      • 1.2 2232. 向表达式添加括号后的最小结果
      • 1.3 2233. K 次增加后的最大乘积
    • 2. ACWing
      • 2.1 取石子
      • 2.2 卡牌
      • 2.3 查询字符串
  • 4月第三周
    • 1. ACWing
      • 1.1 数字母
      • 1.2 玩游戏(约瑟夫环)
      • 1.3 找回数组
    • 2. LeetCode
      • 2.1 单周赛
        • 2.1.1 2243. 计算字符串的数字和
        • 2.1.2 2244. 完成所有任务需要的最少轮数
        • 2.1.3 2245. 转角路径的乘积中最多能有几个尾随零
        • 2.1.4 2246. 相邻字符不同的最长路径
      • 2.2 双周赛
        • 2.2.1 2239. 找到最接近 0 的数字
        • 2.2.2 2240. 买钢笔和铅笔的方案数
        • 2.2.3 2241. 设计一个 ATM 机器
  • 4月第四周
    • 1. ACwing
      • 1.1 吃鸡蛋
      • 2.2 三仙归洞
      • 2.3 构造数组
  • 4月第五周(4.30)
  • 1、LeetCode 双周赛
    • 1.1 6051. 统计是给定字符串前缀的字符串数目
    • 1.2 6052. 最小平均差
    • 1.3 6053. 统计网格图中没有被保卫的格子数
    • 1.4 6054. 逃离火灾
  • 2、ACwing 周赛
    • 2.1 4413. 组队
    • 2.2 4414. 子序列
    • 2.3 4415. 点的赋值

4月第一周

1. ACWing

竞赛题目链接

1.1 字符串价值

#include 
#include 
#include 
#include 

using namespace std;

const int N = 100010;

unordered_map<char, int> mp;

int main()
{
    int a1, a2, a3, a4;
    cin >> a1 >> a2 >> a3 >> a4;
    mp['1'] = a1, mp['2'] = a2, mp['3'] = a3, mp['4'] = a4;
    
    string s;;
    cin >> s;
    
    int res = 0;
    for (int i = 0; i < s.size(); i ++ )
        res += mp[s[i]];
    printf("%d\n", res);
    
    return 0;
}

1.2 最长连续子序列

算法思路: 双指针

#include 
#include 
#include 

using namespace std;

const int N = 500010, M = 1000010;

int n, m;
int w[N], cnt[M]; // w存储数值,cnt[i]表示i在区间中的个数

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
    
    int res = 0, l, r; // res表示最长区间长度,l、r分别表示区间左右端点
    
    for (int i = 1, j = 1, t = 0; i <= n; i ++ ) // i、j表示指针,t表示区间不同元素个数
    {
        // 如果该值是首次出现
        if (cnt[w[i]] == 0) t ++ ;
        cnt[w[i]] ++ ;
        
        // 从区间尾部弹出去一个
        while (t > m) 
        {
            // 如果该区间内只有它一个
            if (cnt[w[j]] == 1) t -- ;
            cnt[w[j]] -- ;
            j ++ ;
        }
        
        if (i - j + 1 > res)
        {
            res = i - j + 1;
            l = j, r = i;
        }
    }
    
    printf("%d %d\n", l, r);
    
    return 0;
}

1.3 最大子矩阵

算法思路:

求子矩阵和的方法:

对于一个任意连续子矩阵,长边是(a1, a2, a3),宽边是(b1, b2 b3),则子矩阵的和为a1 * (b1 + b2 + b3) + a2 * (b1 + b2 + b3) + a3 * (b1 + b2 + b3) = (a1 + a2 + a3) * (b1 + b2 + b3),所以子矩阵的和就等于子矩阵的(长边之和) * (宽边之和)

后面的解释看代码吧,视频讲解我反而感觉不好理解。这个题目的关键就是要能将求任意子矩阵面积的最大值等价成求长和宽区间长度乘积最大且,该长度的区间上的和的最小值乘积要小于x

Tips: 枚举每个长度下该长度内每个值之和的最小值的代码。

#include 
#include 
#include 

using namespace std;

const int N = 2010, INF = 1e9;

int n, m, x;
int s1[N], s2[N]; // 分别表示第一个和第二个前缀和
int a[N], b[N]; // 分别表示每个数组长度中的最小值

int main()
{
    scanf("%d%d", &n, &m);
    
    // 计算第一个数组前缀和
    for (int i = 1; i <= n; i ++ )
    {
        int x;
        scanf("%d", &x);
        s1[i] = s1[i - 1] + x;
    }
    
    // 计算第二个数组前缀和
    for (int i = 1; i <= m; i ++ )
    {
        int x;
        scanf("%d", &x);
        s2[i] = s2[i - 1] + x;
    }
    
    // 枚举每个长度下该长度内每个值之和的最小值
    for (int len = 1; len <= n; len ++ )
    {
        a[len] = INF;
        for (int i = 1; i + len - 1 <= n; i ++ )
        {
            int j = i + len - 1;
            a[len] = min(a[len], s1[j] - s1[i - 1]);
        }
    }
    
    for (int len = 1; len <= m; len ++ )
    {
        b[len] = INF;
        for (int i = 1; i + len - 1 <= m; i ++ )
        {
            int j = i + len - 1;
            b[len] = min(b[len], s2[j] - s2[i - 1]);
        }
    }
    
    int res = 0;
    scanf("%d", &x);
    for (int i = 1, j = m; i <= n; i ++ ) // 枚举每个区间长度
    {
        while (j && b[j] > x / a[i]) // 这里除法处理更好,防止溢出
            j -- ;
        res = max(res, i * j);
    }
    
    printf("%d\n", res);
    
    return 0;
}

2. LeetCode

2.1 单周赛

2.1.1 2224. 转化时间需要的最少操作数

LeetCode 2224

算法思路:

先统计出两个时间差的分钟数,然后在使用贪心的思路求出最少操作数。

class Solution {
typedef pair<int, int> PII;

public:
    int convertTime(string current, string correct) {
        // 处理和存储数据
        PII cu, co;
        cu.first = (current[0] - '0') * 10 + current[1] - '0';
        cu.second = (current[3] - '0') * 10 + current[4] - '0';
        co.first = (correct[0] - '0') * 10 + correct[1] - '0';
        co.second = (correct[3] - '0') * 10 + correct[4] - '0';

        int res = 0, cnt = 0; // res表示最少操作数,cnt表示时间差额

        cnt = (co.first - cu.first) * 60 + (co.second - cu.second);

        int up[4] = {1, 5, 15, 60};
        for (int i = 3; i >= 0; i -- )
        {
            int j = up[i];
            while (cnt / j)
            {
                res += cnt / j;
                cnt = cnt % j;
            }
        }
        
        return res;
    }
};

2.1.2 2225. 找出输掉零场或一场比赛的玩家

LeetCode 2225

算法思路:

通过map来记录每个人的输的场数(不需要开一个map来记录赢得场数),最后通过输的场数来返回值。

class Solution {
public:
    vector<vector<int>> findWinners(vector<vector<int>>& matches) {
        vector<int> w, l;
        map<int, int> mp;
        for (auto c : matches)
        {
            mp[c[0]];     // 胜利的人
            mp[c[1]] ++ ; // 输了的人
        }

        for (auto c : mp)
        {
            if (c.second == 0) // 全胜的人
                w.push_back(c.first);
            if (c.second == 1) // 恰赢一场的人
                l.push_back(c.first);
        }

        return vector<vector<int>>{w, l};
    }
};

2.1.3 2226. 每个小孩最多能分到多少糖果

LeetCode 2226

算法思路: 可以将其转换为二分问题来做。具体思路看官方题解。

同样的思路可以解决这个题目:2187. 完成旅途的最少时间

class Solution {
public:
    int maximumCandies(vector<int>& candies, long long k) {
        long long sum;
        for (auto c : candies)
            sum += c;
        
        // 分别表示可以分得的最少和最多的糖果数目
        long long low = 0, high = sum / k;
        while (low != high)
        {
            long long mid = (low + high + 1) / 2; // 每个人获得mid个糖果
            long long heap = 0; // 按每个人mid个糖果分配可以分得的堆数
            for (int num : candies)
                heap += num / mid;
            /* 如果当前分得的堆的数目小于小孩的的数目,使mid减小,即high减小 */
            if (heap < k)
                high = mid - 1;
            /* 这里利用的是贪心:如果k=mid,则希望找到更多地mid,就增加mid,即low增大 */
            else 
                low = mid;
        }
        return (int)low;
    }
};

2.2 双周赛

2.2.1 2220. 转换数字的最少位翻转次数

题目链接

需要对startgoal不同的二进制位进行操作,所以先对它们进行异或运算,再统计出异或运算结果中1的个数即可。

class Solution {
public:
    int minBitFlips(int start, int goal) 
    {
        int res = 0, x = start ^ goal;
        while (x)
        {
            res += x & 1;
            x = x >> 1;
        }

        return res;    
    }
};

2.2.2 2221. 数组的三角和

题目链接

class Solution {
public:
    int triangularSum(vector<int>& nums) 
    {
        int n = nums.size();
        for (int i = n - 2; i >= 0; i -- )
            for (int j = 0; j <= i; j ++ )
                nums[j] = (nums[j] + nums[j + 1]) % 10;

        return nums[0];     
    }
};

2.2.3 2222. 选择建筑的方案数

题目链接

这个题目的含义实质上是求子序列为010或者101的个数。

算法思路1:动态规划

举例解释:

对于序列01001,有210401

  • 现在末尾加个0,序列变成010010,则01的数量依然是4
  • 现在末尾加个1,序列变成010011,则10的数量依然是2

因此有:

  • 增加0会增加010的个数,同时会增加10的个数;
  • 增加1会增加101的个数,同时会增加01的个数;
class Solution {
public:
    long long numberOfWays(string s) {
        long long ans = 0, n0 = 0, n1 = 0, n10 = 0, n01 = 0;
        for (char i : s)
        {
            /*
            循环不变式(loop invariant)
                n0 等于 s.substr(0,i)中 值为 "0" 的子序列的数量
                n1 等于 s.substr(0,i)中 值为 "1" 的子序列的数量
                n10 等于 s.substr(0,i)中 值为 "10" 的子序列的数量
                n01 等于 s.substr(0,i)中 值为 "01" 的子序列的数量

            对于新增的字符s[i], 讨论以s[i]结尾的所有新增子序列, 然后更新n0,n1,n10,n01
            */
            if (i == '1')
            {
                n01 += n0;
                n1 ++ ;
                ans += n10;
            }
            else
            {
                n10 += n1;
                n0 ++ ;
                ans += n01;
            }
        }
        return ans;
    }
};

算法思路2:前缀和

对任意一个位置,以它为中心构建合法相邻建筑的数量,分两种情况讨论:

  • 若该位置为0,该位置左侧1的数量*该位置右侧1的数量。这样可以构成101
  • 若该位置为1,该位置左侧0的数量*该位置右侧0的数量。这样可以构成010

所以我们可以:

  1. 从左往右遍历一次,统计每个位置左侧0或者1的数量。 统计0还是1取决于该位置是1还是0
  2. 再从右往左遍历一次,统计每个位置右侧侧0或者1的数量 ;
  3. 同一位置,左侧01数量,和右侧01数量相乘,即为以该位置贡献的答案数量,对每个位置的贡献量求和,即为返回答案。
class Solution {
public:
    long long numberOfWays(string s) {
        int n = s.size();
        long long ans = 0;

        int count[n]; // 用于统计左右侧字符的数量
        memset(count, 0, sizeof count);
        int zeroCount = 0, oneCount = 0;

        // 从右往左遍历,统计每个位置右侧0或1字符的数量
        for (int i = n - 1; i >= 0; i -- )
        {
            if (s[i] == '0') count[i] = oneCount; // 如果位置为0,统计右侧1的数量
            else count[i] = zeroCount; // 如果位置为1,统计右侧0的数量

            zeroCount += s[i] == '0' ? 1 : 0;
            oneCount += s[i] == '1' ? 1 : 0;
        }

        zeroCount = 0, oneCount = 0;

        // 从左往右遍历,统计每个位置左侧0或者1字符的数量。这里我们与右侧数量直接相乘得到该位置贡献量
        for (int i = 0; i < n; i ++ )
        {
            if (s[i] == '0') count[i] *= oneCount; //该位置为0,则统计左侧1的数量,并与右侧数量相乘
            else count[i] *= zeroCount; //该位置为1,则统计左侧0的数量,并与右侧数量相乘
            
            zeroCount += s[i] == '0' ? 1 : 0;
            oneCount += s[i] == '1' ? 1 : 0;
            
            ans += count[i];
        }
        
        return ans;
    }
};

4月第二周

1. LeetCode

1.1 6037. 按奇偶性交换后的最大数字

题目链接:LT 6037

这给题要注意审题,题目要求是将所给正整数中每个数奇偶性相同,而不是每个数下标的奇偶性相同!

注意两个函数:

数值stringto_string(int / long / double...)
stringint / float / longatoi(string.c_str()) / atof(string.c_str()) / atol(string.c_str())

class Solution {
public:
    int largestInteger(int num) {
        string str = to_string(num);
        for (int i = 0; i < str.size(); i ++ )
            for (int j = str.size() - 1; j >= i; j -- )
                if ((str[i] - str[j]) % 2 == 0 && str[i] < str[j])
                    swap(str[i], str[j]);
      
        return atoi(str.c_str());
    }
};

1.2 2232. 向表达式添加括号后的最小结果

题目链接:LT 2232

注意对几个函数的使用:find()、substr()、stoi()、to_string()

class Solution {
public:
    string minimizeResult(string expression) {
        int n = expression.size();
        int mid = expression.find('+');
        int best = 2e9;
        string ans;
        
        for (int i = 0; i < mid; i ++ )
        {
            for (int j = mid + 1; j < n; j ++ )
            {
                int p = (i == 0 ? 1 : stoi(expression.substr(0, i)));
                int q = stoi(expression.substr(i, mid - i));
                int r = stoi(expression.substr(mid + 1, j - mid));
                int s = (j == n - 1 ? 1 : stoi(expression.substr(j + 1, n - j - 1)));
                int result = p * (q + r) * s;
                if (result <= best)
                {
                    best = result;
                    ans = expression.substr(0, i) + "(" + expression.substr(i, j - i + 1) + ")" + expression.substr(j + 1, n - j - 1);
                }
            }
        }
        
        return ans;
    }
};

1.3 2233. K 次增加后的最大乘积

使用优先队列,可以证明每次将最小的数 +1k次操作后可以得到最大乘积。

注意: 最结果的取模运算,容易出错。

class Solution {
public:
    int maximumProduct(vector<int>& nums, int k) {
        priority_queue<int, vector<int>, greater<int>> q;
        int MOD = 1000000007;

        long long res = 1; // 注意这里的long long
        for (auto c : nums)
            q.push(c);
        
        for (int i = 0; i < k; i ++ )
        {
            int x = q.top();
            q.pop();
            x ++ ;
            q.push(x);
        }

        while (!q.empty())
            res = (res * q.top()) % MOD, q.pop();
        
        return (int)res;
    }
};

2. ACWing

2.1 取石子

题目链接ACWing 4396

#include 

using namespace std;

int n1, n2, k1, k2;

int main()
{
    cin >> n1 >> n2 >> k1 >> k2;
    if (n1 <= n2) 
        puts("Second");
    else 
        puts("First");
        
    return 0;
}

2.2 卡牌

题目链接:ACWing 4397

算法思路:

对于每一张牌求出其正面和反面数值的差值d[i]以及正面所有数值的和sum,要使数值尽可能地小且正面朝上的卡牌数量不少于k,则只需要减去d[i]< 0最小的n - k张牌即可。

#include 
#include 

using namespace std;

const int N = 2e5 + 10;

int n, k, sum;
int a[N], b[N], d[N];

int main()
{
    scanf("%d%d", &n, &k);
    
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]), sum += a[i];
    for (int i = 1; i <= n; i ++ ) scanf("%d", &b[i]), d[i] = b[i] - a[i];
    
    sort(d + 1, d + n + 1);
    
    for (int i = 1; i <= n - k; i ++ )
    {
        if (d[i] >= 0) break;
        else sum += d[i];
    }
    
    printf("%d\n", sum);
    
    return 0;
}

2.3 查询字符串

题目链接:ACWing 4398

代码一: 暴力枚举,会TLE

每一次将每个字符串在已给出的字符串中进行查找,找到res++,这样会超时。

#include 
#include 
#include 
#include 

using namespace std;

const int N = 50010;

int n, q;
string str[N];
string p;

int main()
{
    scanf("%d", &n);
    for(int i = 0; i < n; i ++ )
        cin >> str[i];
    
    scanf("%d", &q);
    
    while (q -- )
    {
        int res = 0, k = 0;
        cin >> p;
        for(int j = 0; j < n; j ++ )
        {
            if (str[j].find(p) != str[j].npos)
                {
                    res ++ ;
                    k = j;
                }
        }
        if (res)
            printf("%d %s\n", res, str[k].c_str());
        else 
            printf("0 -\n");
    }
    
    return 0;
}

代码二:

由题可知,每个字符串长度是1~8,所以一个字符串(长度为8)最多有8 + 7 + 6 ... + 1 = 36个子串。一共有1e5个字符串,总共3.6e5个字符串。

unordered_map:对于每个询问子串p,有多少个字符串包含这个子串;
unoedered_map:对于每个询问子串p,包含这个子串的字符串中的某一个;

#include 
#include 
#include 
#include 
#include 

using namespace std;

int main()
{
    unordered_map<string, int> cnt;
    unordered_map<string, string> hash;
    
    int n, m;
    cin >> n >> m;
    
    while (n -- )
    {
        string str;
        cin >> str;
        
        unordered_set<string> S; // 存储字符串的所有子串,方便判重
        
        // 枚举所有子串
        for (int i = 0; i < str.size(); i ++ )
             for (int j = i; j < str.size(); j ++ )
                S.insert(str.substr(i, j - i + 1));
        
        for (auto& s : S)
        {
            cnt[s] ++ ;
            hash[s] = str;
        }
    }
    
    cin >> m;
    while (m -- )
    {
        string str;
        cin >> str;
        
        cout << cnt[str] << ' ';
        if (!cnt[str]) puts("-");
        else cout << hash[str] << endl;
    }
    
    return 0;
}

4月第三周

1. ACWing

1.1 数字母

题目链接

#include 
#include 
#include 
#include 

using namespace std;

int main()
{
    unordered_set<char> hash;

    char c;
    while (cin >> c)
        if (c >= 'a' && c <= 'z')
            hash.insert(c);

    cout << hash.size() << endl;
    return 0;
}

1.2 玩游戏(约瑟夫环)

题目链接

#include 
#include 
#include 
#include 

using namespace std;

int n, m;

int main()
{
    cin >> n >> m;
    
    queue<int> q;
    for (int i = 1; i <= n; i ++ ) q.push(i);
    
    while (m -- )
    {
        int a;
        cin >> a;
        a %= q.size();
        
        for (int i = 0; i < a; i ++ )
        {
            q.push(q.front());
            q.pop();
        }
        
        cout << q.front() << ' ';
        q.pop();
    }
    
    return 0;
}

1.3 找回数组

题目链接

算法思路:

由题意:ai - a(i-1) = x(i - 1) % k(这里的i、i-1是指下标),又因为1 <= n <= 1000,所以直接可以枚举k的取值即可。对于每一个k的取值,比如k = 3的时候,有

  • x0 = a1
  • x1 = a2 - a1
  • x2 = a3 - a2
  • x0 = a4 - a3
  • x1 = a5 - a4
    所以只需要判断a1是否等于a4 - a3a2 - a1是否等于a5 - a4即可。
#include 
#include 
#include 

using namespace std;

const int N = 1010;

int n;
int a[N];

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ )  cin >> a[i];
    
    vector<int> res;
    for (int k = 1; k <= n; k ++ )
    {
        bool is_match = true;
        for (int i = k + 1; i <= n; i ++ )
            if (a[i] - a[i - 1] != a[i - k] - a[i - k - 1])
            {
                is_match = false;
                break;
            }
            
        if (is_match) res.push_back(k);
    }
    
    cout << res.size() << endl;
    for (auto k : res)
        cout << k << ' ';
    cout << endl;
    
    return 0;
}

2. LeetCode

2.1 单周赛

2.1.1 2243. 计算字符串的数字和

题目链接

这个题一点都不难,但是要注意逻辑!!!逻辑一定要清晰!!!

class Solution {
public:
    string digitSum(string s, int k)
    {
        int len = s.size();
        if (len <= k) return s;

        string sb = s, tmp;

        while (sb.size() > k)
        {
            tmp = "";

            for (int i = 0; i < sb.size(); i += k) // 这里i的变化!
            {
                int sum = 0, index = i;
                while (index < sb.size() && index < i + k) // 这里的两个判断条件!
                {
                    sum += sb[index] - '0';
                    index ++ ;
                }
                tmp += to_string(sum);
            }

            sb = tmp;
        }

        return sb;
    }
};

2.1.2 2244. 完成所有任务需要的最少轮数

题目链接

算法思路

将每一个数出现的次数放进哈希表中,然后分类讨论:

  • 若这种任务只有1个,则无法完成;
  • 若这种任务只有2个,可一次完成;
  • 若这种任务数量为3的倍数,显然每次完成3个是最快的;
  • 若这种任务数量模3后余1,则需要每次完成3个,最后4个分两次完成;
  • 若这种任务数量模3后余2,则需要每次完成3个,最后一次完成2个。
class Solution {
public:
    int minimumRounds(vector<int>& tasks)
    {
        unordered_map<int, int> cnt;
        for (auto x : tasks) cnt[x] ++ ;

        int ans = 0;
        for (auto c : cnt)
        {
            int x = c.second;
            if (x == 1) return -1;
            else if (x == 2) ans ++ ;
            else if (x % 3 == 0) ans += x / 3;
            else ans += x / 3 + 1;
        }
        
        return ans;
    }
};

2.1.3 2245. 转角路径的乘积中最多能有几个尾随零

题目链接

#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int maxTrailingZeros(vector<vector<int>>& grid)
{
    // 总体思路:枚举四种拐角路径->[左上,左下,右上,右下]
    // 这里由于是求路径中尾随0的的最大个数,因此肯定是路径某个方向取最长的结果(多了某个格子不影响)
    // 我们要找尾随0的个数必须要10因子,必然可以分解为2与5因子,[2,5]的对数就是尾随0个数->min(2的个数,5的个数)
    int m = grid.size(), n = grid[0].size();

    // 创建四个二维数组分别代表:grid[i - 1][j - 1](含)左边的2,5因子总个数;grid[i][j](含)上边的2,5因子总个数
    // r2、c2分别代表每一行、每一列中2的因子的数量,r5、c5同理
    int r2[m + 1][n + 1], r5[m + 1][n + 1], c2[m + 1][n + 1], c5[m +1][n + 1];

    // 默认左边界和上边界的为r2[i][0]=c2[0][j]=r5[i][0]=c5[0][j]=0
    memset(r2, 0, sizeof r2), memset(r5, 0, sizeof r5);
    memset(c2, 0, sizeof c2), memset(c5, 0, sizeof c5);

    // 遍历每个格子完善r2,r5,c2,c5
    for (int i = 1; i <= m; i ++ )
    {
        for (int j = 1; j <= n; j ++ )
        {
            int cur = grid[i - 1][j - 1];
            int two = 0, five = 0;
            while (cur % 2 == 0) two ++ , cur /= 2; // 求出cur中2的因数个数
            while (cur % 5 == 0) five ++ , cur /= 5; // 求出cur中5的因数个数
            // 求前缀和
            r2[i][j] = r2[i][j - 1] + two;
            r5[i][j] = r5[i][j - 1] + five;
            c2[i][j] = c2[i - 1][j] + two;
            c5[i][j] = c5[i - 1][j] + five;
        }
    }

    int ans = 0;
    // 遍历四种拐弯方向(其余的都可以进行等价)
    for (int i = 1; i <= m; i ++ )
    {
        for (int j = 1; j <= n; j ++ )
        {
            // grid[i-1][j-1]为拐弯的格子,总体计算方法就是横竖的2或者5因子个数相加,注意避免重叠
            // 左边向右出发,然后向上走
            ans = max(ans, min(r2[i][j] + c2[i-1][j], r5[i][j] + c5[i - 1][j])); // 注意这里的 -1 是为了避免重复
            // 左边向右出发,然后向下走
            ans = max(ans, min(r2[i][j] + c2[m][j] - c2[i][j], r5[i][j] + c5[m][j] - c5[i][j]));
            // 右边向左出发,然后向上走
            ans = max(ans, min(r2[i][n] - r2[i][j] + c2[i][j], r5[i][n] - r5[i][j] + c5[i][j]));
            // 右边向左出发,然后向下走
            ans = max(ans, min(r2[i][n] - r2[i][j] + c2[m][j] - c2[i - 1][j], r5[i][n] - r5[i][j] + c5[m][j] - c5[i - 1][j]));
        }
    }

    return ans;
}

int main()
{
    vector<vector<int>> grid = {{23,17,15,3,20}, {8,1,20,27,11}, {9,4,6,2,21}, {40,9,1,10,6}, {22,7,4,5,3}};
    int res = maxTrailingZeros(grid);

    cout << res << endl;

    return 0;
}

2.1.4 2246. 相邻字符不同的最长路径

2.2 双周赛

2.2.1 2239. 找到最接近 0 的数字

题目链接

class Solution {
public:
    int findClosestNumber(vector<int>& nums)
    {
        int res = 0, min = 1e9;
        for (auto c : nums)
        {
            int x = abs(c);
            if (x < min) {
                res = c;
                min = x;
            }
            else if (x == min)
                if (c > 0)
                    res = c;
        }
        return res;
    }
};

2.2.2 2240. 买钢笔和铅笔的方案数

题目链接

class Solution {
public:
    long long waysToBuyPensPencils(int total, int cost1, int cost2)
    {
        long long sum = 0;
        long long cost1Num = total / cost1;
        for (int i = 0; i <= cost1Num; i ++ )
        {
            int max = 1;
            if (total - cost1 * i >= cost2)
                max += (total - cost1 * i) / cost2;
            sum += max;
        }

        return sum;
    }
};

2.2.3 2241. 设计一个 ATM 机器

题目链接

逻辑要清晰!!!

class ATM {
private:
    long cnt[5]; // 存储不同货币值的个数,int可能会越界
    int nums[5] = {20, 50, 100, 200, 500};
public:
    ATM() {
        memset(cnt, 0, sizeof cnt);
    }

    void deposit(vector<int> banknotesCount) {
        for (int i = 0; i < 5; i++)
            cnt[i] += banknotesCount[i];
    }

    vector<int> withdraw(int amount) {
        vector<int> res(5, 0);

        for (int i = 4; i >= 0; i--) {
            if (amount >= nums[i] && cnt[i] > 0) {
                int num = (amount / nums[i] > cnt[i]) ? (int) cnt[i] : (amount / nums[i]);
                res[i] = num;
                amount -= nums[i] * num;
            }
        }

        // 若果能取出来
        if (!amount) {
            // 更新ATM中的钱
            for (int i = 0; i < 5; i++)
                cnt[i] -= res[i];

            return res;
        }

        return {-1};
    }
};

4月第四周

1. ACwing

1.1 吃鸡蛋

题目链接

没有通过的案例:100 100

我的code:

#include 

using namespace std;

const int N = 110;

int cnt[N];

int main()
{
    int n, m;
    cin >> n >> m;

    for (int i = 1; i <= n; i ++ ) cnt[i] ++ ;

    int res = 0;
    for (int i = 1; i <= 100; i ++ )
    {
        if (i % m == 0)
        {
            int j = i + 1;
            while (cnt[j]) j ++ ;
            cnt[j] ++ ;
        }

        if (cnt[i] == 0)
        {
            res = i - 1;
            break;
        }
    }

    cout << res << endl;

    return 0;
}

正确code:

#include 

using namespace std;

int n, m;

int main()
{
    cin >> n >> m;
    for (int i = 1;; i ++ )
    {
        if (!n)
        {
            cout << i - 1;
            break;
        }
        n -- ;
        if (i % m == 0) n ++ ;
    }

    return 0;
}

2.2 三仙归洞

题目链接

可以发现规律,不管初始时x位于哪个位置,都是6次变换为一个周期。

#include 

using namespace std;

int main()
{
    int n, x;
    cin >> n >> x;

    string state = "012";
    n %= 6;
    
    for (int i = 1; i <= n; i ++)
    {
        if (i % 2) swap(state[0], state[1]);
        else swap(state[1], state[2]);
    }
    
    cout << state[x] << endl;
    return 0;
}

2.3 构造数组

题目链接

算法思路

首先,由题意bi = b(i+1) 或者 bi + 1 = b(i +1)可知b是一个单调递增序列。又若bi = bj,那么bi ~ bj全相等。

所以,由上面分析可知,对于每一段(第一段除外)的值都有两种选择,要么和前一段相等,要么比前一段多1。而对于第一段,因为b1 = 0,故只有一种选择,即全0。若总共有m段,那么数组b就会有2^m - 1种。

方法一:区间合并
如果两个区间有交集,那么这两个区间内所有的的值必相等。合并后有几个区间,m的值就知道了。

这个代码会TLE

#include 
#include 
#include 
#include 

using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;

const int N = 200010, MOD = 998244353;

int n;
PII q[N];

int main()
{
    scanf("%d", &n);
    unordered_map<int, int> L, R; // 分别表示每个数出现的最左边和最右边的位置
    for (int i = 1; i <= n; i ++ )
    {
        int a;
        scanf("%d", &a);
        R[a] = i;
        if (!L.count(a)) L[a] = i; // 如果第一次出现,更新下最左边的位置
    }
    
    // 把每个元素放进区间里面
    int m = 0; // 表示区间的数量
    for (auto& [k, v] : L) q[m ++ ] = {L[k], R[k]};
    
    // 区间合并
    sort(q, q + m);
    
    int cnt = 0; // 统计区间数量
    
    int st = -1, ed = -1;
    for (int i = 0; i < m; i ++ )
        if (q[i].x <= ed) ed = max(ed, q[i].y);
        else 
        {
            cnt ++ ;
            st = q[i].x, ed = q[i].y;
        }
    
    int res = 1;
    for (int i = 0; i < cnt - 1; i ++ )
        res = res * 2 % MOD;
        
    printf("%d\n", res);
    
    return 0;
}

纠正:初始化哈希表的大小,见代码21行

#include 
#include 
#include 
#include 

using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;

const int N = 200010, MOD = 998244353;

int n;
PII q[N];

int main()
{
    scanf("%d", &n);
    unordered_map<int, int> L(300000), R(300000); // 分别表示每个数出现的最左边和最右边的位置
    for (int i = 1; i <= n; i ++ )
    {
        int a;
        scanf("%d", &a);
        R[a] = i;
        if (!L.count(a)) L[a] = i; // 如果第一次出现,更新下最左边的位置
    }
    
    // 把每个元素放进区间里面
    int m = 0; // 表示区间的数量
    for (auto& [k, v] : L) q[m ++ ] = {L[k], R[k]};
    
    // 区间合并
    sort(q, q + m);
    
    int cnt = 0; // 统计区间数量
    
    int st = -1, ed = -1;
    for (int i = 0; i < m; i ++ )
        if (q[i].x <= ed) ed = max(ed, q[i].y);
        else 
        {
            cnt ++ ;
            st = q[i].x, ed = q[i].y;
        }
    
    int res = 1;
    for (int i = 0; i < cnt - 1; i ++ )
        res = res * 2 % MOD;
        
    printf("%d\n", res);
    
    return 0;
}

4月第五周(4.30)

1、LeetCode 双周赛

1.1 6051. 统计是给定字符串前缀的字符串数目

题目链接

class Solution {
public:
    int countPrefixes(vector<string>& words, string s) {
        int res = 0;
        for (auto str : words)
        {
            if (str.size() > s.size())
                continue;
            
            int len = str.size(), i = 0;
            
            while (i < len)
            {
                if (str[i] == s[i])
                    i ++ ;
                else break;
            }
                
            if (i == len) res ++ ;
        }
        
        return res;
    }
};

1.2 6052. 最小平均差

题目链接

前缀和,注意数据要 long long

class Solution {
public:
    int minimumAverageDifference(vector<int>& nums)
    {
       int n = nums.size();
       vector<long long> s(n + 1, 0);
       for (int i = 1; i <= n; i ++ ) // 处理前缀和
           s[i] = s[i - 1] + nums[i - 1];

       long long res = 10e9;
       int index = 0;
       for (int i = 1; i <= n; i ++ )
       {
           long long left = s[i] / i;
           long long right;
           if (n == i)
               right = 0;
           else
               right = (s[n] - s[i]) / (n - i);

           if (abs(left - right) < res)
           {
               res = abs(left - right);
               index = i - 1;
           }
       }

       return index;
    }
};

1.3 6053. 统计网格图中没有被保卫的格子数

题目链接

模拟+bfs

class Solution {
public:
    int dx[4] = {0, 1, 0, -1}, dy[4] = {-1, 0, 1, 0};

    int countUnguarded(int m, int n, vector<vector<int>> &guards, vector<vector<int>> &walls) {
        char grid[m][n];
        memset(grid, 'N', sizeof grid);

        for (auto guard: guards)
            grid[guard[0]][guard[1]] = 'G';
        for (auto wall: walls)
            grid[wall[0]][wall[1]] = 'W';

        for (auto guard: guards) {
            int x = guard[0], y = guard[1];
            for (int i = 0; i < 4; i++) {
                int a = x + dx[i], b = y + dy[i];
                while (a >= 0 && a < m && b >= 0 && b < n && grid[a][b] != 'G' && grid[a][b] != 'W') {
                    grid[a][b] = 'I';

                    // 继续朝该方向前进
                    a += dx[i];
                    b += dy[i];
                }
            }
        }

        int res = 0;
        for (int i = 0; i < m; i++)
            for (int j = 0; j < n; j++)
                if (grid[i][j] == 'N')
                    res++;

        return res;
    }
};

1.4 6054. 逃离火灾

题目链接

二分+bfs

注: C++11 Lambda表达式(匿名函数)

static const int dirs[4][2] = {{-1, 0},
                               {1,  0},
                               {0,  -1},
                               {0,  1}};

class Solution {
    bool check(vector<vector<int>> &grid, int t) {
        int m = grid.size(), n = grid[0].size();

        bool fire[m][n]; // 记录着过火的位置
        memset(fire, 0, sizeof fire);
        vector<pair<int, int>> f; // f记录每分钟里面火能蔓延到的位置

        for (int i = 0; i < m; i++)
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    fire[i][j] = true;
                    f.emplace_back(i, j);
                }
            }

        // 计算 1 分钟后的火势情况,对f中的每一个火的坐标做一次bfs
        // 这个是 C++11 Lambda表达式(匿名函数),具体解释看代码上面的链接
        auto spread_fire = [&]() -> void {
            vector<pair<int, int>> nf; // nf中只保留了新的扩展的火的坐标,原来的坐标丢弃了
            for (auto &[i, j]: f)
                for (auto &d: dirs) {
                    int x = i + d[0], y = j + d[1];
                    if (0 <= x && x < m && 0 <= y && y < n && !fire[x][y] && grid[x][y] != 2) {
                        fire[x][y] = true;
                        nf.emplace_back(x, y);
                    }
                }
            f = move(nf); // 不保留nf中的元素,将nf中的元素移到f中
        };

        while (t-- && !f.empty()) spread_fire(); // 扩充至多 t 分钟的火势; f.empty == 0 表示没有新的坐标可以燃火
        if (fire[0][0]) return false; // 起点着火

        bool vis[m][n]; // 记录人走过的位置
        memset(vis, 0, sizeof vis);
        vis[0][0] = true;
        vector<pair<int, int>> q; // q记录人每分钟能走到的位置
        q.emplace_back(0, 0);

        // 停留t分钟后,人开始移动,同时火每分钟也会蔓延到相邻位置
        // 对人每分钟能走到的地方bfs,看能否走到安全屋
        while (!q.empty()) {
            vector<pair<int, int>> nq; // 记录人在每一分钟能走到的地方
            for (auto &[i, j]: q)
                if (!fire[i][j]) // 如果该处没有着火
                    for (auto &d: dirs) {
                        int x = i + d[0], y = j + d[1];
                        if (0 <= x && x < m && 0 <= y && y < n && !fire[x][y] && !vis[x][y] && grid[x][y] != 2) {
                            if (x == m - 1 && y == n - 1) return true; // 暂时安全
                            vis[x][y] = true;
                            nq.emplace_back(x, y);
                        }
                    }
            q = move(nq);
            spread_fire(); // 扩充1分钟火势
        }
        return false;
    }

public:
    int maximumMinutes(vector<vector<int>> &grid) {
        int m = grid.size(), n = grid[0].size();
        int left = -1, right = m * n;

        // 二分寻找最大停留分钟数
        while (left < right) {
            int mid = (left + right + 1) / 2;
            if (check(grid, mid)) left = mid;
            else right = mid - 1;
        }

        return left < m * n ? left : 1e9;
    }
};

2、ACwing 周赛

2.1 4413. 组队

题目链接

#include 

using namespace std;

const int N = 2010;

int n, k;

int main()
{
    cin >> n >> k;
    int count = 0;
    for (int i = 0; i < n; i ++ )
    {
        int x;
        cin >> x;
        if (x + k <= 5) count ++ ;
    }
    
    cout << count / 3 << endl;
    
    return 0;
}

2.2 4414. 子序列

题目链接

算法思路

分类讨论:

  • 如果所有正数之和为奇数,则其为解;
  • 如果所有正数之和为偶数:
    • 负偶数,不选,因为不改变奇偶性且不会使和变大;
    • 正偶数,必选,会使和变大;
    • 负奇数:最多选择一个,因为如果选择两个及以上,那么两个奇数就构成一个偶数,不会改变奇偶性,且总和不会变大。也就是要么选择一个,要么一个都不选;
    • 正奇数:最多只能有一个不选。因为如果有两个及以上的正奇数,加上两个正奇数不会改变奇偶性,且总和会变大。也就是要么全选,要么只有一个不选;
      因此对于上面四种情况,前两种情况时一定的,即只选择正偶数;对于后面两种情况,要使结果是奇数,只能操作其中一种,两者取一个max即可。
#include 
#include 
#include 

using namespace std;

const int INF = 1e8;

int main()
{
    int n;
    scanf("%d", &n);
    
    int sum = 0;
    int a = -INF, b = INF; // a存储最大负奇数,b存储最小正奇数
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        if (x > 0) sum += x; // 所有正数相加
        if (x < 0 && x % 2) a = max(a, x);
        if (x > 0 && x % 2) b = min(b, x);
    }
    
    if (sum % 2) printf("%d\n", sum);
    else printf("%d\n", max(sum + a, sum - b));
    
    return 0;
}

2.3 4415. 点的赋值

题目链接

算法思路

因为每个不连通的连通块相互独立,分别求每个连通块的方案数,然后求乘积。

又有要使一条边上两个端点的和为奇数,必须是奇数 + 偶数。所以对于每一个连通图,可以将所有奇数、偶数分别放在不同的集合中,然后连线可以构成一个二分图。所以如果这个图有方案,那么这个图肯定是二分图;反之,如果这个图是二分图,那么它一定存在解。

对于一个连通块的方案数,若这个连通块构成的图是二分图,那么由二分图的性质,对任意一个点,只要其确定了属于哪一个集合,那么这个图中剩余点属于的集合就确定了。假设有左边集合有x个元素,右边集合有y个元素,且由题知所有数仅为1、2、3中之一:

  • 假设左边为奇数,右边为偶数,那么可以知道每个奇数有2种取法,每个偶数仅有1种取法,那么这种方案总共有2^x + 1^y = 2^x种取法;
  • 假设左边为偶数,右边为奇数,同理可知这种方案有2^y种取法;

综上,若一个图是二分图,边总有2^x + 2^y种取法,其中xy分别为图中奇数和偶数的个数。

#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 300010, M = N * 2, MOD = 998244353;

int n, m;
int h[N], e[M], ne[M], idx;
int col[N]; // 染色
int s1, s2; // 集合点数

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

int pow2(int k)
{
    int res = 1;
    while (k -- ) res = res * 2 % MOD;
    return res;
}

bool dfs(int u, int c)
{
    col[u] = c;
    if (c == 1) s1 ++ ;
    else s2 ++ ;
    
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (col[j] && col[j] != 3 - c) return false;
        if (!col[j] && !dfs(j, 3 - c)) return false;
    }
    
    return true;
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T -- )
    {
        scanf("%d%d", &n, &m);
        
        /* 这里不能全部初始化,会TLE */        
        /* 每个字节为4,本题只用到了 n + 1 个点 */
        memset(h, -1, (n + 1) * 4); 
        memset(col, 0, (n + 1) * 4);
        idx = 0;
        
        // 读入边
        while (m -- )
        {
            int a, b;
            scanf("%d%d", &a, &b);
            add (a, b), add(b, a);
        }
        
        int res = 1;
        for (int i = 1; i <= n; i ++ ) // 枚举所有连通块
            if (!col[i]) // 当前没有没染色,说明是一个新的连通块
            {
                s1 = s2 = 0;
                if (dfs(i, 1)) 
                    res = (LL)res * (pow2(s1) + pow2(s2)) % MOD;
                else 
                {
                    res = 0;
                    break;
                }
            }
            
        printf("%d\n", res);
    }
    
    return 0;
} 

你可能感兴趣的:(c++,算法)