11.16、11.17、11.18灵神题单补充

11/16

每日一题:3240. 最少翻转次数使二进制矩阵回文 II

思路:由一般到特殊,分类讨论

image-20241116175126625

当a为2 * 2时,如果回文的话,所有元素应该都是一样,1的个数为0或4,恰好满足条件;当a扩大为4 * 4时,发现也只需要看左上角四个元素,利用对称性即可得到对称位置的元素。

由于所有行和列都必须是回文的,所以要满足:

a[i][j] = a[i][n−1−j] = a[m−1−i][j] = a[m−1−i][n−1−j]

也就是这四个数要么都是 0,要么都是 1。其中 0 ≤ i < ⌊m/2⌋, 0 ≤ j < ⌊n/2⌋。设cnt 1 =a[i][j]+a[i][n−1−j]+a[m−1−i][j]+a[m−1−i][n−1−j]把这四个数都变成 0 需要翻转 cnt 1次,都变成 1 需要翻转4−cnt 1次。两种情况取最小值,把min(cnt 1 ,4−cnt 1 )加入答案。

分析:1 的数目被 4 整除

分类讨论:

  • 如果 m 和 n 都是偶数:由于上面四个数四个数一组,都翻转成了 0 或者 1,所以 1 的数目能被 4 整除自动成立。

  • 如果 m 是奇数,n 是偶数:正中间一排需要翻转成回文的,且 1 的数目需要能被 4 整除。

  • 如果 m 是偶数,n 是奇数:正中间一列需要翻转成回文的,且 1 的数目需要能被 4 整除。

  • 如果 m 和 n 都是奇数:正中间一排和正中间一列都需要翻转成回文的,且 1 的数目需要能被 4 整除。除了正中央的格子以外,每个格子都有镜像位置,这些格子两个数两个数一组,都翻转成了 0 或者 1。那么翻转之后,不考虑正中央的格子,1 的数目就是偶数。

    所以正中央的格子必须是 0,否则最终 1 的数目是奇数,无法被 4 整除。

  • 如何处理正中间一排和正中间一列,是本题的重点。

image-20241116175217269

首先,如果 m 和 n 都是奇数,那么正中央的格子 (⌊m/2⌋,⌊n/2⌋) 必须是 0,把其元素值加入答案。

然后统计正中间一排(如果 m 是奇数)和正中间一列(如果 n 是奇数)的格子:

diff镜像位置不同的数对个数。注意统计的是数对
cnt 1镜像位置相同的 1 的个数。注意统计的是个数
diff 对 0 和 1 必须翻转其中一个数,所以答案至少要增加 diff。什么情况下,可以只增加 diff?

分类讨论:

  1. 如果 cnt 1 是 4 的倍数,那么只需把这 diff 对 0 和 1 中的 1 全部变成 0,这样 1 的个数就是 4 的倍数了。所以把 diff 加入答案。
  2. 如果 cnt1 不是 4 的倍数,由于 cnt 1 是偶数,其除以 4 必定余 2(此时cnt1 % 4 == 2)(注意这说明 cnt 1 ≥2)。
    1. 继续讨论:
      如果 diff>0,把其中一对数都变成 1,(cnt1+2后为4的倍数)。其余 diff−1 对数全部变成 0,这样 1 的个数就是 4 的倍数了。所以同样地,把diff加入答案。
    2. 如果 diff=0,我们只能把 cnt 1 中的两个 1 变成 0(多了2个1),使得 1 的个数是 4 的倍数。所以把答案增加 2。

综上所述:

  • 如果 diff>0,额外把diff加入答案。
  • 如果 diff=0,额外把 cnt 1 mod 4 加入答案。
class Solution {
public:
    int minFlips(vector<vector<int>>& a) {
        int m = a.size(), n = a[0].size();
        int ans = 0;
        for (int i = 0; i < m / 2; i++) {
            for (int j = 0; j < n / 2; j++) {
                int cnt1 = a[i][j] + a[i][n - 1 - j] + a[m - 1 - i][j] + a[m - 1 - i][n - 1 - j];
                ans += min(cnt1, 4 - cnt1); // 全为 1 或全为 0
            }
        }

        if (m % 2 && n % 2) {
            // 正中间的数必须是 0
            ans += a[m / 2][n / 2];
        }

        int diff = 0, cnt1 = 0;
        if (m % 2) {
            // 统计正中间这一排
            for (int j = 0; j < n / 2; j++) {
                if (a[m / 2][j] != a[m / 2][n - 1 - j]) {
                    diff++;
                } else {
                    cnt1 += a[m / 2][j] * 2;
                }
            }
        }
        if (n % 2) {
            // 统计正中间这一列
            for (int i = 0; i < m / 2; i++) {
                if (a[i][n / 2] != a[m - 1 - i][n / 2]) {
                    diff++;
                } else {
                    cnt1 += a[i][n / 2] * 2;
                }
            }
        }

        return ans + (diff ? diff : cnt1 % 4);
    }
};

825. 适龄的朋友

思路:滑动窗口

根据题意,x 向 y 发送好友请求,只需满足 x=y 且\(0.5 * ages[x]+7<ages[y]≤ages[x]\)
注意,只要满足了 \(ages[y]≤ages[x]\)​,题目的第三个条件一定为假。

由于 n 很大而 ages[i]≤120,我们可以用一个长为 121 的 cnt 数组统计每个年龄的人数。枚举年龄 ageX,我们需要知道:可以发送好友请求的最小年龄 ageY 是多少。
年龄在区间 [ageY,ageX] 中的人数。

由于 ageX 越大,ageY 也越大,可以用滑动窗口解决。如果你不了解滑动窗口。窗口内维护年龄在区间 [ageY,ageX] 中的人数 cntWindow

如果发现 cntWindow>0,说明存在可以发送好友请求的用户:当前这cnt[ageX]个用户可以与 cntWindow 个用户发送好友请求,根据乘法原理,这有 cnt[ageX]⋅cntWindow 个。其中有 cnt[ageX] 个好友请求是自己发给自己的,不符合题目要求,要减去。所以把cnt[ageX]⋅cntWindow−cnt[ageX]加入答案。

细节ageY≤ 0.5 * ageX+7 等价于 ageY * 2≤ageX+14。由于当 ageX 增加 1 时,ageY 至多增加 1,所以滑动窗口的内层 while 循环至多循环一次,可以改成 if 语句。

注:年龄可以从 15 开始枚举,但考虑到如果题目条件改了,就不适用了,所以简单起见,从 0 开始枚举。

class Solution {
public:
    int numFriendRequests(vector<int>& ages) {
        int cnt[121]{};
        for (int age : ages) {
            cnt[age]++;
        }

        int ans = 0, cnt_window = 0, age_y = 0;
        for (int age_x = 0; age_x < 121; age_x++) {
            cnt_window += cnt[age_x];
            if (age_y * 2 <= age_x + 14) { // 不能发送好友请求,则窗口向右滑动
                cnt_window -= cnt[age_y];
                age_y++;
            }
            if (cnt_window > 0) { // 存在可以发送好友请求的用户
                ans += cnt[age_x] * cnt_window - cnt[age_x];
            }
        }
        return ans;
    }
};

206. 反转链表

思路:递归、迭代法

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
     ListNode *pre = nullptr ,*cur = head;
     while(cur){
      ListNode *tmp = cur->next;
      cur->next = pre;
      pre = cur;
      cur = tmp; 
     } 
     return pre;
    }
};
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
      if(!head || !head->next)  return head;
      ListNode* last = reverseList(head->next);
      head->next->next = head;
      head->next = NULL;

      return last;
    }
};

92. 反转链表 II

思路:在翻转链表I的基础上

翻转l-r部分肯定要记下l的前一个节点r的后一个节点。同时由于可能从头开始翻转,定义虚拟头结点dummyHead,令p0 = dummyHead,走l-1步后来到l的前一个节点,此时进行r - l 次翻转即可翻转l-r部分,且最终pre停在翻转后的头结点cur指向r的后一个节点,此时p0->next的链还没断,只需将p0->next->next 指向cur,再将p0->next 指向pre。

image-20241117223328808

image-20241117223428508

image-20241117223450407

class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        ListNode* dummyHead = new ListNode(0,head);
        ListNode* p0 = dummyHead;
        //p0从dummyHead走l - 1步来到待翻转部分的前一个节点
        for (int i = 0; i < left - 1; i++) {
           p0 = p0->next;
        }

        ListNode* pre = nullptr;
        ListNode* cur = p0->next;
        //翻转r - l 次,最后pre停在反转后的头结点上,cur停在下一个节点。
        for (int i = 0; i < right - left + 1; i++) {
           ListNode* nxt = cur->next;
           cur->next = pre;
           pre = cur;
           cur = nxt;
        }
        //此时已经l->r翻转完毕,l->next = nullptr但是p0->next并没有断
        p0->next->next = cur;
        p0->next = pre;
        return dummyHead->next;
    }
};

每日一题:661. 图片平滑器

思路:第一眼觉得似曾相识,可以扩充矩阵为(m + 2)* (n + 2),在外面加一圈0然后再算,不过边界条件还是很复杂,没判断出来。

法一:笨方法直接四重循环遍历

法二:前缀和

根据求前缀和数组公式:

sum[i + 1][j + 1] = sum[i][j + 1] + sum[i + 1][j] - sum[i][j] + img[i][j];

Sum(矩阵和) = s[x2][y2] - s[x2][y1- 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]

对于某个 ans[i][j] 而言,我们可以直接计算出其所在 item 的左上角 (a,b)=(i−1,j−1) 以及其右下角 (c,d)=(i+1,j+1),同时为了防止超出原矩阵,我们需要将 (a,b) 与 (c,d) 对边界分别取 max 和 min。

以上 x1 = a , y1 = b , x2 = c , y2 = d代入公式:Sum(矩阵和) = s[c][d] - s[c][b - 1] - s[a - 1][d] + s[a - 1][b - 1]

当有了合法的 (a,b) 和 (c,d) 后,我们可以直接计算出 item 的单元格数量(所包含的行列乘积)及 item 的单元格之和(前缀和查询),从而算得 ans[i][j]

因为sum数组从1开始,结果ans从0开始,故所有值加1.

int cnt = sum[c + 1][d + 1] - sum[a][d + 1] - sum[c + 1][b] + sum[a][b];

class Solution {
public:
    vector<vector<int>> imageSmoother(vector<vector<int>>& img) {
        int m = img.size(), n = img[0].size();
        vector<vector<int>> ret(m, vector<int>(n));
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int num = 0, sum = 0;
                for (int x = i - 1; x <= i + 1; x++) {
                    for (int y = j - 1; y <= j + 1; y++) {
                        if (x >= 0 && x < m && y >= 0 && y < n) {
                            num++;
                            sum += img[x][y];
                        }
                    }
                }
                ret[i][j] = sum / num;
            }
        }
        return ret;
    }
};
class Solution {
public:
    vector<vector<int>> imageSmoother(vector<vector<int>>& img) {
        int m = img.size();
        int n = img[0].size();
        vector<vector<int>> sum(m + 2 , vector<int>(n + 2 , 0));
        for (int i = 0; i < m ; i++) {
           for (int j = 0; j < n ; j++) {
             sum[i + 1][j + 1] = sum[i][j + 1] + sum[i + 1][j] - sum[i][j] + img[i][j];
           }
        }
        vector<vector<int>> res(m , vector<int>(n , 0));
        
        for (int i = 0; i < m; i++) {
           for (int j = 0; j < n; j++) {
              int a = max(0 , i - 1);//左
              int b = max(0 , j - 1);//上
              int c = min(m - 1, i + 1);//下
              int d = min(n - 1 , j + 1);//右
              int area = (c - a + 1) * (d - b + 1);//计算面积,除数
               int cnt = sum[c + 1][d + 1] - sum[a][d + 1] - sum[c + 1][b] + sum[a][b];

               res[i][j] = cnt / area;
           }
        }
        return res;
    }
};
posted @ 2024-11-20 22:55  七龙猪  阅读(1)  评论(0)    收藏  举报
-->