模 归 一 化

将一个字符 向后 移位的意思是将这个字符用字母表中 下一个 字母替换(字母表视为环绕的,所以 'z' 变成 'a')。

当你看到类似上面这种文字描述的时候,你就要想到 模归一化

环形取模(Circular Mod / Positive Mod)技巧

为什么需要?

当数据是环形的时候,例如:

  • 字母 'a'~'z'
  • 星期 1~7
  • 时钟 0~23
  • 环形数组
  • 循环队列

都会遇到:

加法可能超过最大值

减法可能变成负数

所以需要把结果映射回合法区间。


Java 的 % 并不是数学意义上的取模

很多人第一次都会踩坑。

例如

java 复制代码
-1 % 26 == -1
-2 % 26 == -2
-27 % 26 == -1

Java 的 %取余(remainder),不是数学里的 Mod。

因此:

java 复制代码
(a + b) % m

结果可能是负数。

注意:Python 取模会变成正数


最经典的写法

所以几乎所有涉及环形运算都会写成

java 复制代码
((x % m) + m) % m

或者

java 复制代码
(x + m) % m

(当能够保证 x > -m 时)

作用就是:

把负余数转换成正余数。

例如

java 复制代码
-3 % 26 = -3

(-3 + 26) % 26

=23

于是

复制代码
-3

↓

23

代表:

前移3位 == 后移23位


为什么要再 % 一次?

很多人疑惑:

java 复制代码
(x % m + m)

为什么还要 % m

举个例子。

假设

java 复制代码
x = 27

那么

复制代码
27 % 26 = 1

1+26=27

已经又超过了。

所以最后还需要

复制代码
27%26

=1

因此完整模板始终都是

java 复制代码
((x % m) + m) % m

它保证结果一定属于

复制代码
[0,m-1]

字符偏移为什么这样写?

例如

java 复制代码
'a'~'z'

实际上就是

复制代码
0~25

所以都会先减去 'a'

java 复制代码
c-'a'

得到

复制代码
0~25

然后

复制代码
当前位置+移动次数

最后

复制代码
%26

再加回来

复制代码
+'a'

模板就是

java 复制代码
(char)((c-'a'+offset)%26+'a')

如果 offset 可能为负数

就变成

java 复制代码
offset=((offset%26)+26)%26;

(char)((c-'a'+offset)%26+'a')

这几乎是所有字符循环题目的固定模板。


可以总结成一个套路

以后只要看到下面这些关键词:

  • 循环
  • 字母
  • 星期
  • 时钟
  • 数组循环
  • Rotation
  • Shift

脑子里就应该想到

先归一化(Normalize),再取模(Modulo)。

java 复制代码
offset=((offset%m)+m)%m;

然后

java 复制代码
ans=(cur+offset)%m;

应用例子

2381. 字母移位 II

参考代码(Java)

java 复制代码
class Solution {
    public String shiftingLetters(String S, int[][] shifts) {
        int n = S.length();

        int[] diff = new int[n + 1];
        for (int[] q : shifts) {
            int start = q[0], end = q[1], dir = q[2] * 2 - 1;
            diff[start] += dir;
            diff[end + 1] -= dir;
        }

        // 前缀和
        for (int i = 1; i < n; i++) {
            diff[i] += diff[i - 1];
        }

        char[] s = S.toCharArray();
        for (int i = 0; i < n; i++) {
            // 模归一化
            s[i] = (char)(((s[i] - 'a' + diff[i]) % 26 + 26) % 26 + 'a');
        }

        return new String(s);
    }
}

更多例子用法,评论区高手补充