码蹄杯模拟赛 Python 解题复盘总结

比赛时间:2026年4月24日
比赛平台:码蹄杯(Matiji)
参赛语言:Python 3


前言

本次模拟赛遇到了三道比较有代表性的算法题,涉及枚举优化二维前缀和降维区间查询(莫队算法)等核心知识点。本文将详细复盘每道题的解题思路、踩坑记录与优化过程,帮助自己在正式比赛前查漏补缺。


第一题:统计合法四元组

题目大意

给定长度为 n n n 的数组 a a a,统计满足以下条件的四元组 ( q , w , e , r ) (q, w, e, r) (q,w,e,r) 的数量:

  • 下标严格递增: 1 ≤ q < w < e < r ≤ n 1 \leq q < w < e < r \leq n 1q<w<e<rn
  • 数值满足: a q < a w = a e > a r a_q < a_w = a_e > a_r aq<aw=ae>ar
  • 结果对 10007 10007 10007 取模

数据范围 n ≤ 3000 n \leq 3000 n3000

我的初始思路

看到"统计四元组",第一反应是四重循环暴力枚举,但 O ( n 4 ) O(n^4) O(n4) n = 3000 n=3000 n=3000 时直接爆炸( 3000 4 ≈ 8 × 10 13 3000^4 \approx 8 \times 10^{13} 300048×1013),肯定超时。

优化思路:拆分 + 预处理

核心思想:把四元组拆成中间一对相等的数,左右分别统计

对于每一对 ( w , e ) (w, e) (w,e) 满足 w < e w < e w<e a w = a e a_w = a_e aw=ae

  • left[w]:在 w w w 左边( q < w q < w q<w),满足 a q < a w a_q < a_w aq<aw 的数量
  • right[e]:在 e e e 右边( r > e r > e r>e),满足 a r < a e a_r < a_e ar<ae 的数量

则以 ( w , e ) (w, e) (w,e) 为中间点的合法四元组数量为:left[w] * right[e]

代码实现

MOD = 10007

def main():
    import sys
    input = sys.stdin.read().split()
    n = int(input[0])
    a = list(map(int, input[1:n+1]))
    
    # 预处理 left[i]: i 左边比 a[i] 小的数的个数
    left = [0] * n
    for i in range(n):
        cnt = 0
        for j in range(i):
            if a[j] < a[i]:
                cnt += 1
        left[i] = cnt
    
    # 预处理 right[i]: i 右边比 a[i] 小的数的个数
    right = [0] * n
    for i in range(n-1, -1, -1):
        cnt = 0
        for j in range(i+1, n):
            if a[j] < a[i]:
                cnt += 1
        right[i] = cnt
    
    ans = 0
    # 遍历所有 w < e 且 a[w] == a[e] 的对
    for w in range(n):
        for e in range(w+1, n):
            if a[w] == a[e]:
                ans = (ans + left[w] * right[e]) % MOD
    
    print(ans % MOD)

if __name__ == "__main__":
    main()

踩坑记录

关于 right 数组的循环方向

刚开始对 right 序列移动下标写错,后来想到要从后往前遍历:

for i in range(n-1, -1, -1):

原因right[i] 统计的是 i i i 右边 a [ i ] a[i] a[i] 小的数。必须从数组末尾开始,先处理最右边的元素(右边没有数,直接为0),再往左处理。如果正着遍历,计算 i=0 时后面的数还没处理,无法统计右边。

数组 含义 遍历方向 原因
left[i] i i i 左边比 a [ i ] a[i] a[i] 从左到右 统计左边
right[i] i i i 右边比 a [ i ] a[i] a[i] 从右到左 统计右边

复杂度分析

  • 预处理 left O ( n 2 ) O(n^2) O(n2)
  • 预处理 right O ( n 2 ) O(n^2) O(n2)
  • 统计所有 ( w , e ) (w,e) (w,e) 对: O ( n 2 ) O(n^2) O(n2)
  • 总复杂度 O ( n 2 ) O(n^2) O(n2),对于 n = 3000 n=3000 n=3000 3000 2 = 9 , 000 , 000 3000^2 = 9,000,000 30002=9,000,000,Python 可以轻松通过。

第二题:统计 1 多于 0 的子矩阵

题目大意

给定 n × m n \times m n×m 的 0-1 网格,统计有多少个子矩阵满足:矩阵中 1 的数量 > 0 的数量。

数据范围 n , m ≤ 100 n, m \leq 100 n,m100

核心思路:数值转换 + 降维 + 前缀和

第一步:数值转换

把 0 变成 -1,1 保持 1。则:

  • 子矩阵中 1 的数量 > 0 的数量
  • ⇔ \Leftrightarrow 子矩阵元素之和 > 0

第二步:枚举上下边界,降维成一维

固定上边界 top 和下边界 bottom,把这两行之间的每一列压缩成一个数 col_sum[j],表示第 j j j 列从 topbottom 的元素和。

原二维问题转化为:在 col_sum 数组中,统计有多少个子数组的和 > 0。

第三步:前缀和 + 二分统计

col_sum 计算前缀和 pre,则子数组 [l, r) 的和 = pre[r] - pre[l]

要求 pre[r] - pre[l] > 0,即 pre[r] > pre[l](其中 l < r l < r l<r)。

问题转化为:统计前缀和数组中,满足 l < r l < r l<rpre[r] > pre[l] ( l , r ) (l, r) (l,r) 对数。

可以用树状数组归并排序统计逆序对的思路解决,这里用 bisect 维护有序列表:

import bisect

sorted_pre = []
cnt = 0
for x in pre:
    idx = bisect.bisect_left(sorted_pre, x)  # 找有多少个 pre[l] < x
    cnt += idx
    bisect.insort(sorted_pre, x)  # 插入当前值

完整代码

def main():
    import sys
    import bisect
    input = sys.stdin.read().split()
    ptr = 0
    n = int(input[ptr]); ptr += 1
    m = int(input[ptr]); ptr += 1
    
    grid = []
    for _ in range(n):
        row = list(map(int, input[ptr:ptr+m]))
        ptr += m
        # 把 0 变成 -1,1 保持 1
        transformed = [1 if x == 1 else -1 for x in row]
        grid.append(transformed)
    
    ans = 0
    
    # 枚举上边界 top
    for top in range(n):
        col_sum = [0] * m
        # 枚举下边界 bottom
        for bottom in range(top, n):
            # 把当前 bottom 行加到 col_sum
            for j in range(m):
                col_sum[j] += grid[bottom][j]
            
            # 计算前缀和
            pre = [0] * (m + 1)
            for j in range(m):
                pre[j+1] = pre[j] + col_sum[j]
            
            # 统计 pre[r] > pre[l] 的对数
            sorted_pre = []
            cnt = 0
            for x in pre:
                idx = bisect.bisect_left(sorted_pre, x)
                cnt += idx
                bisect.insort(sorted_pre, x)
            
            ans += cnt
    
    print(ans)

if __name__ == "__main__":
    main()

复杂度分析

  • 枚举 top O ( n ) O(n) O(n)
  • 枚举 bottom O ( n ) O(n) O(n)
  • 更新 col_sum O ( m ) O(m) O(m)
  • 前缀和 + 二分统计: O ( m log ⁡ m ) O(m \log m) O(mlogm)
  • 总复杂度 O ( n 2 ⋅ m log ⁡ m ) O(n^2 \cdot m \log m) O(n2mlogm)

对于 n , m ≤ 100 n, m \leq 100 n,m100 100 2 × 100 × log ⁡ 100 ≈ 7 × 10 6 100^2 \times 100 \times \log 100 \approx 7 \times 10^6 1002×100×log1007×106,完全可以通过。

暴力解法对比

如果不优化,直接枚举所有子矩阵的左上角和右下角,再用二维前缀和求和,复杂度是 O ( n 2 m 2 ) O(n^2 m^2) O(n2m2) 100 4 = 10 8 100^4 = 10^8 1004=108,Python 可能会超时。


第三题:区间众数出现次数(莫队算法)

题目大意

给定长度为 n n n 的序列, q q q 次询问区间 [ l , r ] [l, r] [l,r] 中,出现次数最多的数的出现次数。

数据范围 n , q ≤ 10 4 n, q \leq 10^4 n,q104

我的初始代码(超时)

import sys
input = lambda: sys.stdin.readline().strip()

n, q = map(int, input().split())
a = list(map(int, input().split()))

for _ in range(q):
    flag = [0] * 1000001  # ❌ 致命错误1:每次创建百万级数组
    l, r = map(int, input().split())
    for i in range(l-1, r):
        flag[a[i]] += 1
    print(max(flag))  # ❌ 致命错误2:每次扫描百万级数组求max

超时原因分析

  1. 每次查询创建 [0]*1000001:查询 1 万次 = 创建 100 亿个空间,内存和时间都爆炸
  2. 每次遍历整个区间统计:最坏情况 10 4 × 10 4 = 10 8 10^4 \times 10^4 = 10^8 104×104=108 次循环
  3. 每次 max(flag) 扫描 100 万个数:巨量无用操作
  4. 逐行 input() + 逐次 print():IO 极慢

改进版(能运行但仍可能超时)

import sys

def main():
    data = sys.stdin.read().split()
    ptr = 0
    n = int(data[ptr])
    q = int(data[ptr+1])
    ptr += 2
    a = list(map(int, data[ptr:ptr+n]))
    ptr += n

    output = []
    for _ in range(q):
        l = int(data[ptr]) - 1
        r = int(data[ptr+1]) - 1
        ptr += 2

        cnt = {}
        max_cnt = 0
        for i in range(l, r+1):
            num = a[i]
            cnt[num] = cnt.get(num, 0) + 1
            if cnt[num] > max_cnt:
                max_cnt = cnt[num]
        output.append(str(max_cnt))
    
    print('\n'.join(output))

main()

改进点

  • 用字典代替百万级数组
  • 边遍历边更新最大值,避免最后 max()
  • 一次性读入 + 一次性输出

:数据范围 n , q = 10 4 n,q=10^4 n,q=104 时,最坏情况仍是 10 8 10^8 108 次操作,Python 大概率超时

终极解法:莫队算法

莫队算法是解决无修改区间查询的经典离线算法,核心思想是通过合理排序查询,减少区间移动的总次数。

莫队核心思想
  • 分块:把数组分成大小为 n \sqrt{n} n 的块
  • 排序查询:按左端点所在块排序,同一块内按右端点排序(奇偶优化)
  • 滑动窗口:用两个指针维护当前区间,只通过"加一个数"或"删一个数"来调整
  • 维护答案:实时维护当前区间的最大出现次数
莫队代码(AC版本)
import sys
import math

def main():
    data = sys.stdin.read().split()
    idx = 0
    n = int(data[idx])
    q = int(data[idx+1])
    idx += 2
    a = list(map(int, data[idx:idx+n]))
    idx += n

    # ========== 莫队核心1:分块 ==========
    block = int(math.sqrt(n)) + 1

    # 读取查询,保存原始顺序
    queries = []
    for i in range(q):
        l = int(data[idx]) - 1
        r = int(data[idx+1]) - 1
        queries.append((l, r, i))
        idx += 2

    # ========== 莫队核心2:排序 ==========
    # 按左端点块号排序,同一块内偶数块r升序,奇数块r降序(减少指针移动)
    queries.sort(key=lambda x: (x[0] // block, x[1] if (x[0]//block) % 2 == 0 else -x[1]))

    # ========== 莫队核心3:滑动指针 ==========
    res = [0] * q           # 存储答案
    cnt = [0] * (10**6 + 10)  # 每个数字出现次数
    frq = [0] * (n + 10)     # 出现次数为c的数字有多少个
    maxf = 0                # 当前最大出现次数
    cl, cr = 0, -1          # 当前指针位置

    # 添加数字x到当前区间
    def add(x):
        nonlocal maxf
        frq[cnt[x]] -= 1
        cnt[x] += 1
        frq[cnt[x]] += 1
        if cnt[x] > maxf:
            maxf = cnt[x]

    # 从当前区间删除数字x
    def remove(x):
        nonlocal maxf
        frq[cnt[x]] -= 1
        cnt[x] -= 1
        frq[cnt[x]] += 1
        # 如果当前最大次数的freq为0,说明没有数字出现这么多次了
        if frq[maxf] == 0:
            maxf -= 1

    # 处理每个查询
    for l, r, i in queries:
        # 扩展左边界
        while cl > l:
            cl -= 1
            add(a[cl])
        # 扩展右边界
        while cr < r:
            cr += 1
            add(a[cr])
        # 收缩左边界
        while cl < l:
            remove(a[cl])
            cl += 1
        # 收缩右边界
        while cr > r:
            remove(a[cr])
            cr -= 1
        
        res[i] = maxf

    # 按原始顺序输出
    print('\n'.join(map(str, res)))

if __name__ == "__main__":
    main()

复杂度对比

算法 时间复杂度 操作次数(n=q=10^4)
暴力(百万数组) O ( q ⋅ n + q ⋅ V ) O(q \cdot n + q \cdot V) O(qn+qV) ~ 10 12 10^{12} 1012(卡死)
暴力(字典优化) O ( q ⋅ n ) O(q \cdot n) O(qn) 10 8 10^8 108(超时)
莫队算法 O ( n n ) O(n\sqrt{n}) O(nn ) ≈ 10 6 \approx 10^6 106(AC)

通用技巧总结

1. 快速读入

# 最快:一次性读入所有数据
data = sys.stdin.read().split()

# 较快:逐行快速读入
input = sys.stdin.readline

千万不要用普通 input() 处理大数据!

2. 快速输出

# 把结果存列表,最后一次性输出
output = []
# ... 处理中 append ...
print('\n'.join(map(str, output)))

千万不要每次查询都 print()

3. 区间查询问题选型

问题类型 推荐算法 复杂度
无修改,离线查询 莫队算法 O ( n n ) O(n\sqrt{n}) O(nn )
有修改,离线查询 带修莫队 O ( n 5 / 3 ) O(n^{5/3}) O(n5/3)
在线查询,可预处理 线段树/树状数组 O ( n log ⁡ n ) O(n \log n) O(nlogn)
静态区间最值 ST表 O ( n log ⁡ n ) O(n \log n) O(nlogn) 预处理, O ( 1 ) O(1) O(1) 查询

4. Python 竞赛避坑

坑点 解决方案
创建超大数组 [0]*10**6 用字典或 collections.Counter
多次 max() 扫描 边遍历边维护最大值
四重/五重循环 考虑降维、前缀和、莫队等优化
递归深度超限 sys.setrecursionlimit(300000)
整数溢出 Python 自动处理大整数,放心

赛后反思

  1. 算法储备不足:第三题完全没想到莫队算法,只会暴力,这是最大的教训
  2. 复杂度估算不准:没有第一时间估算 10 8 10^8 108 在 Python 中会超时
  3. 代码习惯不好:初始代码用了 flag = [0]*1000001 这种致命写法
  4. IO 优化缺失:没有养成用 sys.stdin.read() 的习惯

比赛官网https://matiji.net/matibei

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐