AtCoder 419-E 题解

\(\color{black}{题目传送门}\)

题面:

给你一个长度为 \(N\) 的数组 \(A\) 和两个数字 \(M,L\),你可以做以下操作若干次,最后你要使得数组 \(A\) 中的所有长度为 \(L\) 的连续子序列(子串)的和都是 \(M\) 的倍数

  • \(A\) 中的任意一个位置上的值增加 \(1\)

你需要求出最少操作次数。

\(1 \leq N,M \leq 500\)
\(1 \leq L \leq N\)
\(0 \leq A_i < M\)

\(\color{white}{别忘下看了啊啊啊!!!脑袋会爆!!!}\)

思路

1、转换

先说结论:

当且仅当以下两个条件同时成立,才能满足数组 \(A\) 中的所有长度为 \(L\) 的连续子序列(子串)的和都是 \(M\) 的倍数。
1、\((A_1+A_2+A_3+...+A_{L}) \mod m = 0\)
2、对于所有的\(i (L+1 \leq i \leq N)\) ,都要满足: \(\left |(A_i - A_{i-L}) \right | \mod m = 0\)

原因:如果满足这两个条件,可以用数学归纳法证明数组 \(A\) 中的所有长度为 \(L\) 的连续子序列(子串)的和都是 \(M\) 的倍数。

首先\((A_1+A_2+A_3+...+A_{L}) \mod m = 0\) 是肯定的,(条件1就是这个)。

接着,我们假设\((A_k+A_{k+1}+A_{k+2}+...+A_{k+L-1})\)\(M\) 的倍数
根据条件2,\((A_k+A_{k+1}+A_{k+2}+...+A_{k+L-1}) + (A_{k+L} - A_k)\)\(M\) 的倍数,将\((A_k+A_{k+1}+A_{k+2}+...+A_{k+L-1}) + (A_{k+L} - A_k)\)化简得到\(A_{k+1}+A_{k+2}+...+A_{k+L}\)

证毕

2、做法

部分1:预处理

注意到条件2。

我们可以先把所有的 \(A_i\) 按照 \(i \mod L\) 进行分组,然后由于条件2,所以最终要求每一组中 各个数字\(\mod m\)都要相等。

所以我们可以先预处理:记$cost_{i,j} (i < l, j < m) $表示:使第 \(i\) 组的所有的数字\(\mod m\)都等于 \(j\) 需要多少次操作。

部分2:动态规划做法(正推)

用动态规划解决:\((A_1+A_2+A_3+...+A_L) \mod m = 0\)的最少操作次数(\(A_i\)根据\(A_{i-L}\)而变化,变化的操作次数同样要算在\(dp\)里面)

(1)状态

\(dp_{i,j}\) 表示使 \((A_1+A_2+A_3+...+A_i) \mod m = j\) 的最少操作次数。

(2)转移

首先容易知道:如果 \((A_1+A_2+A_3+...+A_i) \mod m = j\),且\(A_{i+1} \mod m = k\) ,那么 \((A_1+A_2+A_3+...+A_{i+1}) \mod m = (j + k) \mod m\)

并且还能知道,把 \(A_{i+1}\) 所在组全部转换为 \(k\) , 花费是 \(cost_{i,k}\)

而把前\(i\)个值的和转换为 \(\mod j = 0\) 还需要花费 \(dp_{i,j}\)

注意:由于可能有多个方法转换为某一个相同位置,所以要尝试更新而不是直接更新。

转移:

\[dp_{i+1,j+k} = \min(dp_{i+1,j+k}, cost_{i,k}+dp_{i,j}) \]

(3) 初始化

由于求最小值,初始化为正无穷

但是把0个值的和转换为\(\mod m = 0\)明显是不需要操作即可实现的。
所以:\(dp_{0,0} = 0\)

(3)答案

答案是\((A_1+A_2+A_3+...+A_L) \mod m = 0\)的最少操作次数,明显就是\(dp_{l,0}\)

(4)伪代码

于是我们就可以写出(伪)代码:

memset(dp, 0x3f); // 初始化,把dp全部设成无穷大
dp[0][0] = 0; // 初始化,把0个值的和转换为mod m = 0明显是不需要操作即可实现的。
for (int i : 0 ~ L) // 只需要计算A的前L个值的和转换成 mod m = 0即可
    for (int j : 0 ~ M) // 表示A的前i个值 mod m = j , 由于是 mod m = j,所以j一定只能是 0~m
        for (int k : 0 ~ M) // 枚举 A i+1(及其所在组)转换成 mod m = 几
             int t = (j + k) % m; // 计算加上 A i+1 之后产生 前i+1个的和值(mod m)
             dp[i + 1][t] = min(dp[i + 1][t], cost[i][k] + dp[i][j]); // 尝试更新前i+1个的和值(mod m = t)的最少方案数

cout << dp[l][0] << endl; // 输出把前L个值的和转换成 mod m = 0 的最少操作数
 

总代码

#include <iostream>
#include <vector>
#include <string.h>
using namespace std;
int a[501]; // 输入
vector<int> v[501]; // 用于分组
long long cost[501][501], dp[501][501]; // cost 为预处理出来的使各组的所有的数字 \mod m 都等于每个值所分别需要的操作次数,dp为动态规划所用到的数组,具体见思路板块。
int main()
{
    int n, m, l;
    cin >> n >> m >> l;
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
        v[i % l].push_back(a[i]); // 分组
    }
    for (int i = 0; i < l; i++) // 枚举组
    {
        for (int j = 0; j < m; j++) // 枚举转换成哪个数字
        {
            for (int x : v[i]) // 对于这组的每个数字
            {
                cost[i][j] += (j + m - x) % m; // 预处理一下代价
            }
        }
    }
    memset(dp, 0x3f, sizeof(dp));  // 初始化,把dp全部设成无穷大
    dp[0][0] = 0;  // 初始化,把0个值的和转换为mod m = 0明显是不需要操作即可实现的。
    for (int i = 0; i < l; i++)  // 只需要计算A的前L个值的和转换成 mod m = 0即可
    {
        for (int j = 0; j < m; j++)  // 表示A的前i个值 mod m = j , 由于是 mod m = j,所以j一定只能是 0~m
        {
            for (int k = 0; k < m; k++)  // 枚举 A i+1(及其所在组)转换成 mod m = 几
            {
                int t = (j + k) % m;  // 计算加上 A i+1 之后产生 前i+1个的和值(mod m)
                dp[i + 1][t] = min(dp[i + 1][t], cost[i][k] + dp[i][j]); // 尝试更新前i+1个的和值(mod m = t)的最少方案数
            }
        }
    }
    cout << dp[l][0] << endl; // 输出把前L个值的和转换成 mod m = 0 的最少操作数
    return 0;
}

完结撒花! ☆(v)/

\(\tiny{难死了(¬∧¬)}\)

posted @ 2025-08-19 19:35  MichaelZeng  阅读(26)  评论(0)    收藏  举报