第三届云南大学程序设计竞赛(YNUCPC - 2024) 官方题解

总览

出题人:OctalLihg
友情客串:Catherine、Catgok、小帅、GPT、派蒙
特别鸣谢:GPT同学对题面构造工作提供的支持!


感谢所有参与内测的ICPC集训队队员:
① Ai,give me a name全队(Catgok、Nocoldinsummer、fexla)
② Ulterior Motive全队(KrowFeather、False0099、Leexxxx)
③ 特别鸣谢,远在USA的 Felix 队长(ICPC集训队第一任队长)对验题工作的大力支持!


本次校赛大部分题目(所有难题)的idea都来自我的队友Lihg,我的工作还是挺轻松的,就帮忙出出数据和写写题面,所以写题解的任务就还是我自己来吧。

本文为官方题解,也鼓励各位YNU的小伙伴们发布自己的题解ヾ(◍°∇°◍)ノ゙

赛前故事:
一听说要出校赛了,Lihg一下子扔给了我8个题的idea,让我随便选,看样子是蓄谋已久了
我一看,什么难题、中档题、防AK题都有了,虽然出题人表示都很一眼(狗头)
由此,Octal也可以摆烂了
由此,本次校赛的难度就被拉高了…
所以,善良的Octal还是不能摆烂,删了几个更难的题,自己出了几个简单题。
最后还是要感谢Lihg,贡献了几道非常不错的题目。
而且,他出的这些题,数据都很难出,为Lihg哥哥点赞o( ̄▽ ̄)d。


赛前预估是三个简单题(1–2),四个中档题(3–4),两个ICPC正赛难度题(5)。(更难的题被octal删了)

赛后update:
前期榜严重歪了T_T,01_Dreamer同学速度K了C题,导致大家都去写C(哭笑)。

其实从思维上说,B、H要更简单一些T_T

第二个偏的是E,其实很早的时候404liuyan(好像是研一学长)就拿下了一血,但可能是LaTeX把大家吓住了,导致很晚才有第二个人开E题。对我们算竞选手来说,类似题写多了,可能会觉得E比C要简单一些。

另一个一直没人开的题就是I题,I其实是一道纯纯的脑筋急转弯题,没涉及到算法。个人猜测:参赛集训队的退役队员们可能太久没打CF,对这些题手生了;而现役队员们看前排大佬都没写这题,也都不敢去试。还好最后知非同学力挽狂澜,让Lihg的题没白出。

总的来说还不错,每道题都有人AC了,有点ICPC的味道了,嘻嘻。

预估难度知识点FirstBlood出题人
A1 ★简单数学01_DreamerLihg
B2 ★数学、模拟(进制转换)Frank000octal
CSTD做法:3 ★
其他做法:4 ★
STD做法:二分 + 前缀和
其他做法:① 线段树 + 二分
② 离线查询 + 双指针 ③单调队列 + 二分
01_DreamerLihg
D4 – 5 ★思维、树状数组 + 二分01_DreamerLihg
E3 ★思维、组合数学404liuyanoctal
F5 ★拓扑排序 / 思维 + 并查集 / tarjan缩点JALJAYOLihg
G5 ★树状数组 + DFNZZFreyaLihg
H1 – 2 ★模拟、分类讨论cqsbz_cxyoctal
I4 ★思维、模拟Frank000Lihg

A 幸运数字(1)

【数学】【签到】
首先,根据数学常识, 1 − n 1-n 1n 中有多少个 k k k 的倍数,用 n / k n/k n/k (整除)即可获得。
本题中我们要求 [ L , R ] [L, R] [L,R] 中有多少个3的倍数,只需要用 [ 1 , R ] [1, R] [1,R] 中3的倍数的个数 减去 [ 1 , L − 1 ] [1, L-1] [1,L1] 中3的倍数的个数,即可。
由于设定是签到题,就没有卡int,不需要开long long就能过。

#include<bits/stdc++.h>
using namespace std;

int main()
{
    int l, r;
    cin >> l >> r;
    cout << r/3 - (l-1)/3 << endl;
    return 0;
}

B 幸运数字(2)

【数学】【模拟】【签到】
每个数位只有0、3、6、9,四个数字,我们可以把这四个数字等价为0、1、2、3。
问题就转化为了在四进制下,第 N N N 个自然数是什么。(大家联想一下,十进制其实就是每一个数位都只有 0-9 十种数字的数)
那么,写一个十进制转四进制的代码即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline void solve()
{
    ll n;
    cin >> n;
    n --; // 自然数从0开始,第一个自然数是0
    vector<int> r;
    // 用vector来模拟栈
    // 用普通数组来模拟栈or直接用stack<int>都行
    while(n) {
        r.push_back(n % 4);
        n /= 4;
    }
    if(r.size() == 0) cout << 0;
    while(!r.empty()) { // 倒序输出
        // vector用起来和栈是一样的
        cout << r.back()*3;
        r.pop_back();
    }
    cout << endl;
}
int main()
{
    int t;
    cin >> t;
    while(t --)
        solve();
    return 0;
}

C 应急食品

【二分】【前缀和】
先讲讲二分查找的做法吧,学过二分查找的小伙伴们应该都知道,在一个序列中二分查找一个数,需要满足一个前提:序列有序。(勿喷,狭义上可以这么理解)
但是,假如原数组有正有负,得到的前缀和数组是摆动的,不符合二分查找的前提条件。所以如果我们还想用二分查找的话,首要任务是让序列变得有序。
那如何让序列单调递增呢?

做法一

一、一种做法是维护一个前缀和的前缀最大值即可

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
#define pll pair<ll, ll>
#define endl '\n'
typedef long long ll;
inline void solve()
{
    int n, q;
    cin >> n >> q;
    vector<ll> pre(n+5);
    for(int i=1; i<=n; i++) {
        ll x;
        cin >> x;
        pre[i] = pre[i-1] + x;
    }
    for(int i=1; i<=n; i++)
        pre[i] = max(pre[i], pre[i-1]);
    
    while(q --) {
        ll x;
        cin >> x;
        int l = 1, r = n;
        while(l < r) {
            int mid = l+r >> 1;
            if(pre[mid] >= x) r = mid;
            else l = mid + 1;
        }
        if(pre[l] >= x) cout << l << endl;
        else cout << -1 << endl;
    }
}
int main()
{
    solve();
    return 0;
}

做法二

二、第二种做法类似单调队列,我们维护一个单调的数组,只留下有用的数。
这里附上Felix大佬的验题代码:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
using ll = long long;
vector<ll> pre_sum = {0ll};
vector<ll> indices = {0ll};
// 下标是indices[i]的前缀和是pre_sum[i]
ll a[100005], n, q;

int main()
{
    ios::sync_with_stdio(false);
    cin >> n >> q;
    for(ll i=1;i<=n;i++)
        cin >> a[i];
    ll sum = 0;
    for(ll i=1;i<=n;i++)
    {
        sum += a[i];
        if(sum > pre_sum.back())
        {
            pre_sum.push_back(sum);
            indices.push_back(i);
        }
    }
    while(q--)
    {
        ll x; cin >> x;
        if(x > pre_sum.back())
            cout << "-1\n";
        else
        {
            auto pos = lower_bound(pre_sum.begin(), pre_sum.end(), x) - pre_sum.begin();
            cout << indices[pos] << "\n";
        }
    }

    return 0;
}

做法三

三、当然,本题其实还有个离线做法
把所有的询问存下来,从小到大排个序

因为在有解的情况下,更小的x对应的答案也一定是更小的

在这种单调一致性的条件下,就有点类似双指针的感觉, i i i j j j 一起往后移。

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
#define pll pair<ll, ll>
#define endl '\n'
typedef long long ll;
inline void solve()
{
    int n, q;
    cin >> n >> q;
    vector<ll> pre(n+1);
    for(int i=1; i<=n; i++) {
        ll a;
        cin >> a;
        pre[i] = pre[i-1] + a;
    }
    vector<pll> qs(q+1);
    for(int i=1; i<=q; i++) {
        cin >> qs[i].first;
        qs[i].second = i;
    }
    sort(qs.begin()+1, qs.end(), [&](pll a, pll b) {
        return a.first < b.first;
    }); // 把询问从小到大排序
    vector<int> res(q+1, -1); // 记录答案
    int j = 1;
    for(int i=1; i<=q; i++) {
        while(j <= n && qs[i].first > pre[j])
            j ++;
        if(j > n) break;
        res[qs[i].second] = j;
    }

    for(int i=1; i<=q; i++) {
        cout << res[i] << endl;
    }
}
int main()
{
    cout << fixed << setprecision(10);
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

线段树 + 二分的做法就属于是“大炮打蚊子”了,不在此赘述。

D 逆序对传说

【树状数组+二分】【思维】
区间第 K K K 大模板题(树状数组+二分模板题)
假如我们初始有一个序列,我们在序列末尾添加一个数,新增的逆序对数 = = = 此前比该数更大的数的个数

在回到这道题上来,我们从后往前对这个 a a a 序列做一个差分。

a i − a i − 1 a_i-a_{i-1} aiai1 代表 p i p_i pi 所带来的逆序对数,即 i i i 位置之前比 p i p_i pi 的大的数的个数。

鉴于原序列是一个 1 − n 1-n 1n 的排列,每个 p i p_i pi 都是固定元素集合中的元素。

我们把剩余未安排的元素看成一个集合,如果比 p i p_i pi 大的数有 m m m 个,那么 p i p_i pi 就是这个第 m + 1 m+1 m+1 大的数。

问题就转化为了维护区间第 K K K 大的模型。

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
#define pll pair<ll, ll>
#define endl '\n'
typedef long long ll;
template<typename T>
struct Fenwick {
    int n;
    vector<T> tr;
    Fenwick(int n) : n(n), tr(n + 1, 0){}
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, T c) { 
        for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
    }
    T sum(int x) {
        T res = T();
        for(int i = x; i; i -= lowbit(i)) res += tr[i];
        return res;
    }
    T sum(int l, int r) {
        return sum(r) - sum(l - 1);
    }
};
inline void solve()
{
    int n;
    cin >> n;
    vector<ll> a(n+1);
    a[1] = 0;
    for(int i=2; i<=n; i++)
        cin >> a[i];
    Fenwick<ll> bit(n+1);
    for(int i=1; i<=n; i++)
        bit.add(i, 1);
    vector<int> res(n+1);
    for(int i=n; i>=1; i--) {
        int l=1, r=n;
        ll k = a[i] - a[i-1] + 1;
        while(l < r) {
            int mid = (l+r+1)/2;
            if(bit.sum(mid, n) >= k) l = mid;
            else r = mid - 1;
        }
        res[i] = l;
        bit.add(l, -1);
    }
    for(int i=1; i<=n; i++)
        cout << res[i] << " ";
    cout << endl;
}
int main()
{
    int t;
    cin >> t;
    while(t --)
        solve();
    return 0;
}

E 集合运算

【组合数学】
题目要求所有元素个数 > = 2 >=2 >=2 S S S 的子集中第二大的元素之和,即求:S中的各个元素作为第二大元素出现在子集中的次数与该元素乘积之和。

我们可以先将所给集合元素从小到大排序,然后从左到右依次遍历 1 到 n-1 位置上的数(第n个元素不用遍历,因为它不可能是第二大),观察可发现:以该数作为子集中第二大元素,需要从该数右边的比它大的数中选一个元素(有 n − i n-i ni 种选法),然后该数左边的 i − 1 i-1 i1 个元素都有选与不选两种情况(有 2 i − 1 2^{i - 1} 2i1 种情况)。

由乘法原理可知,遍历到第i个位置时,以该元素作为第二大元素在子集中出现的次数有 ( n − i ) × 2 i − 1 (n - i) × 2 ^{i - 1} (ni)×2i1 种情况,累加 ( n − i ) × 2 i − 1 × a [ i ] (n - i) × 2 ^{i - 1} × a[i] (ni)×2i1×a[i] 即可求得所需解。

这里需要注意,由于数据范围较大,会爆long long,需要进行取模处理。而取模需要注意的是,不能有中间量的溢出,所以要每运算一次就取模一次,具体操作详见代码。

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
#define pll pair<ll, ll>
#define endl '\n'
typedef long long ll;
const int mod = 1e9+7;
ll qmi(ll a, ll b, ll mod)
{
	ll res = 1;
	while(b) {
		if(b & 1) res = res*a%mod;
		a = a*a%mod;
		b >>= 1;
	}
	return res;
}
inline void solve()
{
    ll n;
    cin >> n;
    vector<ll> a(n+1);
    for(int i=1; i<=n; i++)
        cin >> a[i];
    sort(a.begin()+1, a.end());
    ll res = 0, k = 1;
    for(ll i=1; i<=n-1; i++) {
        res = (res + a[i] * qmi(2, i-1, mod) % mod * (n-i) % mod) % mod;
    }
    cout << res << endl;
}
int main()
{
    solve();
    return 0;
}

如果不会快速幂,其实用一个变量迭代也能做,相当于每进行一轮循环就×2。

F 帮Lihg关下灯

做法一

【拓扑排序】【基环树】
对这道题进行数学建模:对于每一个关联关系,我们都可以看作是一条有向边,每个灯就是图上的每个节点。

仔细一想,构成的图就是一个基环树森林(多个基环树)。

我们用拓扑排序跑一遍,遇到开着的灯就关,就可以只留下多个环。

而在环上模拟关灯的话,只要考虑1的个数的奇偶性就行了。因为我们可以发现:开关操作要么1不变、要么多两个1,要么少2个1,1的个数的奇偶性是不变的。如果是偶数个灯,就一定有办法变为只剩0个灯;反之,如果是奇数,就一定不行。

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
#define pll pair<ll, ll>
#define endl '\n'
typedef long long ll;
inline void solve()
{
    int n;
    cin >> n;
    vector<int> a(n+1);
    for(int i=1; i<=n; i++)
        cin >> a[i];
    vector<int> fa(n+1);
    for(int i=1; i<=n; i++)
        fa[i] = i;
    function<int(int)> find = [&](int x) {
        if(x == fa[x]) return x;
        return fa[x] = find(fa[x]);
    };
    vector<int> deg(n+1), p(n+1);
    for(int i=1; i<=n; i++) {
        cin >> p[i];
        int u = find(i);
        int v = find(p[i]);
        if(u != v) fa[u] = v;
        deg[p[i]] ++;
    }
    queue<int> q;
    for(int i=1; i<=n; i++)
    if(deg[i] == 0) {
        q.push(i);
    }
    while(!q.empty()) {
        int i = q.front();
        q.pop();
        if(a[i]) {
            a[p[i]] = 1-a[p[i]];
        }
        deg[p[i]] --;
        if(deg[p[i]] == 0) q.push(p[i]);
    }
    vector<int> cnt(n+1);
    for(int i=1; i<=n; i++)
    if(deg[i] && a[i] == 1) {
        cnt[find(i)] ++;
    }
    for(int i=1; i<=n; i++)
    if(cnt[i] % 2) {
        cout << "No" << endl;
        return;
    }
    cout << "Yes" << endl;
}
int main()
{
    cout << fixed << setprecision(10);
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

Felix大佬的验题代码思路更清晰、也更直观,还有生动的注释,推荐大家学习一下orz。

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
using ll = long long;
// 并不是……哇靠,原来是,拓扑排序!!硬生生想复杂了呜呜呜呜呜呜呜呜呜
int p[100005], n, a[100005], in_degree[100005];

bool check_cycle(int start)
{
    int light = a[start]; //环上有几个灯开着
    in_degree[start] = 0; //把环从世界上抹去!!F**k 环!!
    for(int i = p[start]; i != start; i = p[i])
    {
        in_degree[i] = 0;
        light += a[i];
        a[i] = 0; //环里的都给我关了!!
    }
    return light % 2 == 0;
}

void toposhin_start(queue<int> &q) //拓扑神,启动!
{
    while(!q.empty())
    {
        int now = q.front();
        q.pop();
        if(a[now] == 1)
        {
            a[now] = 0;
            a[p[now]] = !a[p[now]];;
        }
        in_degree[p[now]] --;
        if(in_degree[p[now]] == 0)
            q.push(p[now]);
    }
}

int main()
{
    ios::sync_with_stdio(false);
    cin >> n;
    for(int i=1;i<=n;i++)
        cin >> a[i];
    memset(in_degree, 0, sizeof(in_degree));
    for(int i=1;i<=n;i++)
    {
        cin >> p[i];
        in_degree[p[i]] ++;
    }
    queue<int> starts;
    for(int i=1;i<=n;i++)
        if(in_degree[i] == 0)
            starts.push(i);
    toposhin_start(starts); //拓扑排序结束之后,就只剩下一个一个一个一个的环了
    //处理环
    for(int i=1;i<=n;i++)
        if(in_degree[i] == 1)
            if(!check_cycle(i))
            {
                cout << "No";
                return 0;
            }
    cout << "Yes";
    return 0;
}

/*
11
1 0 1 0 1 0 0 0 1 1 0
2 3 4 1 6 7 8 9 7 7 7
 */

/*
15
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
2 3 4 5 6 7 1 13 14 13 14 13 14 1 1
 */

/*
11
1 1 0 1 0 1 1 1 1 1 0
2 3 11 5 3 3 3 3 8 9 10
 */

做法二

【并查集】
但仔细一想,我们发现基环树枝叶上的1总会收束到环上;同时,在收束到环之后,1的个数的奇偶性和收束前也一定是一样的。
所以其实拓扑排序这一步是多余的,我们只需要统计初始状态下每个连通块中1的个数即可。

若出现某一个连通块中1的个数是奇数,就一定无法全关。

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
#define pll pair<ll, ll>
#define endl '\n'
typedef long long ll;
inline void solve()
{
    int n;
    cin >> n;
    vector<int> a(n+1);
    for(int i=1; i<=n; i++)
        cin >> a[i];
    vector<int> fa(n+1);
    for(int i=1; i<=n; i++)
        fa[i] = i;
    function<int(int)> find = [&](int x) {
        if(x == fa[x]) return x;
        return fa[x] = find(fa[x]);
    };
    vector<int> deg(n+1), p(n+1);
    for(int i=1; i<=n; i++) {
        cin >> p[i];
        int u = find(i);
        int v = find(p[i]);
        if(u != v) fa[u] = v;
        deg[p[i]] ++;
    }
    vector<int> cnt(n+1);
    for(int i=1; i<=n; i++)
    if(a[i] == 1) {
        cnt[find(i)] ++;
    }
    for(int i=1; i<=n; i++)
    if(cnt[i] % 2) {
        cout << "No" << endl;
        return;
    }
    cout << "Yes" << endl;
}
int main()
{
    solve();
    return 0;
}

PS:这题也可以用强连通分量缩点来写,本质和并查集做法差不多,就是更暴力一点。
我个人觉得没必要那样,想学习的同学可以私我要代码。

G 帮Lihg开灯

【树状数组】【DFS序】

树状数组+DFN序模板题,想补题的同学,学习一下这两个知识点就行了。

学会了之后,使用这个工具模拟即可!

一个有意思的事:这道题和刚刚结束的3月31日的CSP认证的最后一题知识点撞了,那道题是这道题的一个加强版

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
#define pll pair<ll, ll>
#define endl '\n'
typedef long long ll;

template<typename T>
struct Fenwick {
    int n;
    vector<T> tr;
    Fenwick(int n) : n(n), tr(n + 1, 0){}
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, T c) { 
        for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;
    }
    T sum(int x) {
        T res = T();
        for(int i = x; i; i -= lowbit(i)) res += tr[i];
        return res;
    }
    T sum(int l, int r) {
        return sum(r) - sum(l - 1);
    }
};
inline void solve()
{
    int n;
    cin >> n;
    vector<int> g[n+1];
    for(int i=1; i<n; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    vector<int> p(n+5);
    for(int i=1; i<=n; i++) {
        int a;
        cin >> a;
        p[a] = i;
    }
    vector<int> dfn(n+5), sz(n+5);
    int cnt = 0;
    function<void(int, int)> dfs = [&](int u, int fa) {
        dfn[u] = ++cnt;
        sz[u] = 1;
        for(auto v: g[u]) {
            if(v == fa) continue;
            dfs(v, u);
            sz[u] += sz[v];
        }
    };
    dfs(1, 0);
    Fenwick<int> bit(n+5);
    vector<int> res(n+1);
    for(int i=1; i<=n; i++) {
        bit.add(dfn[p[i]], 1);
        res[p[i]] = bit.sum(dfn[p[i]], dfn[p[i]]+sz[p[i]]-1);
    }
    for(int i=1; i<=n; i++)
        cout << res[i] << " ";
}
int main()
{
    solve();
    return 0;
}

H 能量之地

【模拟】

做法一

这道题主要有两个做法,一种做法就是老老实实地模拟蛇形走位的过程。用一个变量来维护,向左还是向右。应该是比较简洁的形式了。

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
#define pll pair<ll, ll>
#define endl '\n'
typedef long long ll;
inline void solve()
{
    int n, m;
    cin >> n >> m;
    vector<vector<int>> a(n+1,vector<int>(m+1));
    for(int i=1; i<=n; i++)
    for(int j=1; j<=m; j++)
        cin >> a[i][j];
    int x=1, y=1, tot=0;
    bool p = false;
    ll sum = 0;
    while(true) { // 永真循环
        sum += 1ll*a[x][y];
        if(x == n && y == m) break; // 走到(n, m)就退出循环
        if(y == m && tot%2 == 0 || y == 1 && tot%2 == 1) {
            x ++;
            if(p) p = false, tot ++;
            else p = true;
        } else {
            if(tot % 2 == 0) y ++;
            else y --;
        }
    }
    cout << sum << endl;
}
int main()
{
    solve();
    return 0;
}

做法二

另一种做法是以一行为单位算总共分行号%4=0,1,2,3有4种情况但这个做法需要处理的边界条件比较多,验题的时候不少验题人用这个做法贡献了几发wa,所以推荐使用第一种做法。
这里附上本题主角Catgok的验题代码!

#include <map>
#include <set>
#include <cmath>
#include <iostream>
#include <algorithm>
#include <assert.h>
#define int long long
#define PII pair<int, int>
using namespace std;

const int N = 2e5 + 9, INF = 1e18 + 9, P = 1e9 + 7;
int T, n, m, x;

void solve() {
    int ans = 0;
    cin >> n >> m;
    for(int i = 0; i < n; i++) {
        for(int j = 0; j < m; j++) {
            cin >> x;
            if(i % 2 == 0) ans += x;
            else if(i % 4 == 1 && j == m - 1) ans += x;
            else if(i % 4 == 3 && j == 0) ans += x;
            if(i == n - 1 && i % 4 == 2 && j != m - 1) ans -= x;
        }
    }
    cout << ans;
}

signed main() {
    ios::sync_with_stdio(false);
    int t = 1;
    // cin >> t;

    while(t--) solve();
    return 0;
}

I 轮回与更迭

【思维】【模拟】
本题有两个操作:①改变值;②左移

我们不难发现,每一行是独立的,与其他行没有关系。

再者,对于all可行方案,先左移完了再改变,一定是最省事的!

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
#define pll pair<ll, ll>
#define endl '\n'
typedef long long ll;
inline void solve()
{
    int n, m;
    cin >> n >> m;
    vector<vector<int>> a(n+1, vector<int>(m+5));
    for(int i=1; i<=n; i++) {
        for(int j=1; j<=m; j++) {
            int x;
            cin >> x;
            x -= (i-1)*m;
            a[i][j] = x;
        }
    }
    int res = 0;
    for(int i=1; i<=n; i++) {
        vector<int> cnt(m); //cnt表示左移i次之后,为达成条件需要改变的数的个数
        for(int j=1; j<=m; j++) { 
            if(a[i][j] < 1 || a[i][j] > m) continue;
            cnt[(j-a[i][j]+m) % m] ++;
        }
        int minn = 1e9;
        for(int i=0; i<m; i++)
            minn = min(minn, i+m-cnt[i]);
        res += minn;
    }
    cout << res << endl;
}
int main()
{
    solve();
    return 0;
}
  • 31
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值