2025牛客五一集训派对 每日签到题题解(万字详解,童叟无欺)

本文收录2025牛客五一集训派对day1-5每日的签到题,整体难度不高,大部分题都算比较适合算法入门的小白。为方便大家读题,以下每题都会先给出翻译成中文后的题目描述。

UPDATE2025.5.5:全部题解已更新完成,总体难度大约为d2 < d4 < d1 ≈ d3 << d5,大家可以由易到难酌情递进学习。

目录

DAY1- H - War of Inazuma (Easy Version)

一.题面

二.思路解析

1.前置知识

2.思路讲解

AC_code

DAY2- G - Lexicographical Maximum

一.题面

二.思路解析

1.前置知识

2.思路讲解

AC_code

DAY3 - G - Link with Monotonic Subsequence

一.题面

二.思路解析

1.前置知识

2.思路讲解​

AC_code

DAY4 - C - Concatenation

一.题面

二.思路解析

1.前置知识

2.思路讲解​

AC_code

DAY5 - N - Particle Arts

一.题面

二.思路解析

1.前置知识

2.思路讲解

AC_code


DAY1- H - War of Inazuma (Easy Version)

一.题面

时间限制:C/C++/Rust/Pascal: 2秒;其他语言: 4秒

空间限制:C/C++/Rust/Pascal: 1024M;其他语言: 2048M

题目描述

作为一名旅行者,你曾亲眼见证了稻妻的战争,战争双方分别是抵抗军和将军的军队。

稻妻的地图可以看作是一个 n 维超立方体,恰好有 2^n个顶点。每个顶点用一个从0到 2^n - 1 的整数标记。两个顶点相邻,当且仅当它们的 n 位二进制表示中恰好有且只有一位不同。

在这场稻妻战争中,一些顶点被抵抗军占据,另一些顶点被将军的军队占据。你还注意到,对于每个顶点 u,与 u 相邻且与 u 同属一方的顶点个数不超过\lceil \sqrt{n} \rceil

多年以后,你已经忘记了这场战争的细节。请你构造出满足上述所有要求的超立方体(即对每个顶点标记0或1的方案,满足条件)。


输入描述

输入只有一行,包含一个整数 n ,满足 $1 \leq n \leq 22$

输出描述

输出一个长度为 2^n的01字符串。其第 a_1 \dots a_n 位描述编号为 a_1, \dots, a_n 的顶点属于哪一方,0 表示抵抗军,1 表示将军的军队。这里的 a_1 \dots a_n 是该顶点的 n 位二进制表示。

只要满足所有要求的答案都将被接受。

测试样例1

输入:

2

输出:

0111

二.思路解析

1.前置知识

这题起码有洛谷绿题到蓝题的难度(主观判断),确实是难,涉及的也不只是简单的基础算法了。

  • 首先是用位运算处理二进制(几个要注意的点):

    • 1 << n 等价2^n

    • n & (n - 1)是将n的二进制表示最低位的1清零

  • 其次是Brian Kernighan算法(用builtin_popcount函数也行,本质是找二进制表示里面1的个数)

    • 循环n &= (n-1),每次消一个1,由此往复下去循环次数就是1的个数了

    • 函数代码:

int cnt_bin1(int n)
{
    int cnt = 0;
    while(n)
    {
        n &= (n - 1); // 清除最低位的1
        cnt++;
    }
    return cnt; // 返回1的个数
}

如果想数二进制表示的0的格式,那输出的时候用32 - cnt即可,也就是 return 32 - cnt;

  • 二分图(一种无向图)

    • 简而言之就是把所有点分成两组,每条边都只能连“组A—组B”,而不能“组A—组A”或“组B—组B”。

    • n维超立方体的所有顶点,可以用二进制中1计数的奇偶性分成两组,一组是1的个数为偶的点,一组是奇的点,每条边连接的两点必属于不同的组,因而可以说超立方体就是个二分图

2.思路讲解

读完题目可以知道,我们要给n维超立方体,对应2^n个顶点的每个点标记,0是抵抗军,1是幕府军(好像好多题目背景都是原),要求任一点的所有邻居中跟自己同阵营的最多不得超过 \lceil \sqrt{n} \rceil,而二进制数相差仅一位的两个点相邻,所以我们从二进制位运算这个方向解题找切口。

这时候引入一个二分图。题目提及了n维超立方体,而它其中的每一个顶点都可以唯一用一个n位的二进制数表示。每个点有n个邻居,分别也就是这n位中的某一位反转之后得到的点了。

由此,我们可以转换一下问题,将它等价成:在这张图里为每个点分阵营,使每个点和自己同阵营的邻居不多于 \lceil \sqrt{n} \rceil 

因为“将一个n位二进制数的其中任意一位从0变1/从1变0,会导致其中1的总数增/减1,进而使奇偶性变化”,所以我们可以用某点1的个数的奇偶性来做分组,1的个数为偶的顶点标0,为奇的标1,这样每条边都会连向不同阵营的点。然后每个点的n个邻居就全部是对立面,同阵营邻居数为零,不超过\lceil \sqrt{n} \rceil

本质是把n维超立方体看成一个二分图,用“1的个数奇偶”做分割,完美符合二分图定义,不存在同色相邻。至于超立方体可以去看看B站的可视化,虽说能不能想象得出来影响不大。

我们举个栗子,以 n=3 为例

顶点编号

二进制表示

1的个数

阵营(偶数0奇数1)

0

000

0

0

1

001

1

1

2

010

1

1

3

011

2

0

4

100

1

1

5

101

2

0

6

110

2

0

7

111

3

1

邻居001 010 100,全部幕府军(1),同色邻居0个,完全没有问题。

AC_code

#include<bits/stdc++.h>
using namespace std;
#define int long long
int cnt_bin1(int n)//brian kernighan算法
{
    int cnt = 0;
    while(n)
    {
        n &= (n - 1);//清除最低位的1
        cnt ++;
    }
    return cnt;//计算i的二进制表示的1的个数
    //return 32 - cnt;//找二进制表示的0的数目
}
signed main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int n;cin >> n;
    int piv = 1 << n;//n维超立方体有2^n个顶点(排列问题)
    string res(piv, '0');
    for(int i = 0;i < piv;i ++)
    {
        //int cnt1 = __builtin_popcount(i);
        int cnt1 = cnt_bin1(i);
        if(cnt1 % 2 == 0)res[i] = '0';
        else res[i] = '1';
    }
    cout << res << endl;
    return 0;
}

时间复杂度是O(2^n * k),k为最大1的个数是 n,所以整体O(n * 2^n),n最大是22,所以极限情况是4e6左右,没超时。

DAY2- G - Lexicographical Maximum

一.题面

时间限制: C/C++/Rust/Pascal 1秒,其他语言2秒
空间限制: C/C++/Rust/Pascal 256M,其他语言512M
64位输入输出格式: %lld

题目描述

Eibwen 是 Python 新手。

你可能知道,当你在命令行输入一个数字时,你的 Python 程序接收到的是包含该数字的字符串,而不是可用于计算的数字。这是一个新手可能不知道的有趣特性。

Eibwen 想找到一些给定数字中的最大值,所以他写了一个程序来排序这些数字列表,并打印排序后的列表中的最后一个元素。然而,作为新手,Eibwen 不知道这个特性。他实际上是将数字的字符串列表按字典序排序,并打印列表中最后的字符串。

现在 Eibwen 运行了他的程序,输入了从 1 到 n 的所有整数,却发现程序运行非常慢。你能帮助他找出这个程序的预期输出吗?


输入描述

唯一一行包含一个整数 n (1 ≤ n ≤ 10^1000000),表示 Eibwen 输入的数字大小。

输出描述

打印 Eibwen 程序的预期输出,即从 1 到 n 中按字典序排序的最大字符串。

测试样例1

输入:

616

输出:

99

二.思路解析

1.前置知识

这题大概是洛谷红题或者橙题的难度,主要考的是字符串数组的运用,涉及ASCII值的比较(找出在字典序中最大的并输出)

tips:字典序就是像字符串排序那样的顺序,比方说"100" < "21"  \leftarrow  '1' < '2'。

字典序中数字字符串的比较和正常情况下数字大小的比较是不一样的:

  • 首先比首位,从左至右逐位比,谁首位大谁就排在前边
  • 如果首位相同,再比第二位,以此类推
  • 如果位数不同而且前面一样,那么短的排就前面(如 "9" < "91")

2.思路讲解

读完题目我们可以晓得,要从 1 到 n 里找一个字符串数,使它最大,那么结合前面字典序的前置知识我们就可以推出一些关键点

  • 9开头一定比其他数字开头的大,比如三位数"900"、"9"、"99"、"999" 等都比"8xx"系列的大
  • 全9数字字典序都大,只要长度不超过n,肯定 \leq n
  • 如果n本身是9开头的,那么它就可能是最大字典序的数,反之就只能是长度比n少1位的全9

由上面的,我们梳理一下,就可以得到一个分支结构的伪代码框架:

  • n是1位数,不是9 \rightarrow 答案为9
  • n是多位数,首位不是9  \rightarrow 答案为"99...(n - 1个9)
  • n首位是9 \rightarrow 答案为n

举个栗子可能就更好理解了(就拿测试样例来举)

输入:616

616首位不是9而是6,1位"9"比"616"小,2位的"99"也比"616"小,3位的"999"比"616"大,但3位的"999"已经超界,所以合法区间字典序最大的是"99",输出"99"

AC_code

#include <iostream>
using namespace std;
#define int long long
signed main() 
{
    string n;cin >> n;
    int len = n.length();
    string a(len, '9');
    if (a <= n)cout << a;
    else 
    {
        string c1(len - 1, '9');
        string c2 = n;
        cout << (c1 > c2 ? c1 : c2);
    }
    return 0;
}

时间复杂度O(n),对题目的1e6数据而言是稳过的。

DAY3 - G - Link with Monotonic Subsequence

一.题面

时间限制:C/C++/Rust/Pascal 1 秒,其他语言 2 秒

空间限制:C/C++/Rust/Pascal 256 MB,其他语言 512 MB

特殊判题:64bit IO 格式:%lld

题目描述

首先,复习一些定义。如果你已经熟悉,可以跳过这部分。

一个序列 a 是序列 b 的递增(递减)子序列,如果 a 可以通过从 b 中删除若干(可能为零个或全部)元素得到,并且 a 中所有元素从头到尾是递增(递减)有序的。

一个排列是一个长度为 n 的数组,包含从 1 到 n 的所有不同整数,顺序任意。例如,[2, 3, 1, 5, 4] 是一个排列,但 [1, 2, 2] 不是排列(因为 2 出现了两次),而 [1, 3, 4] 也不是排列(因为 n = 3,但是出现了 4)。

以下是正式题目:

Link 有一个长度为 n 的数组。他正在学习最长递增子序列算法,因此对如下问题感兴趣:

定义一个排列 p 的值为 (\max(\text{lis}(p), \text{lds}(p))),其中 (\text{lis}(p)) 是排列 p 的最长递增子序列长度,(\text{lds}(p)) 是排列 p 的最长递减子序列长度。对于所有长度为 n 的排列,哪一个排列的值最小?


输入描述

每个测试包含多个测试用例。

  • 第一行包含一个整数 T ,表示测试用例数量,满足 (1 \le T \le 1000)。
  • 接下来每个测试用例一行,包含一个整数 (n) ,满足 (1 \le n \le 10^6)。
  • 并且保证所有测试用例中 n 的总和不超过 (10^6)。

输出描述

对于每个测试用例,输出一行,包含一个长度为 n 的排列。

如果有多个答案,输出其中任意一个即可。

测试样例1

输入:

3
1
2
3

输出:

1
1 2
1 3 2

二.思路解析

1.前置知识

参考同知识点的洛谷P1020导弹拦截,约莫这题有洛谷绿题的难度(主观评判),主要考的点在题目页给出了了,就是数学构造,结合一点贪心的LIS 和 LDS 的较大值最小化​

Erdos Szekeres:
这个定理是指任何长度为 (r−1)(s−1)+1 的序列,必存在长度为 r 的递增子序列或长度为 s 的递减子序列。简而言之就是要同时限制 LIS 和 LDS 的长度,得把序列分割为多段,使每段内的元素单调可控。

LIS(最长递增子序列):

这个题目已经给出了,就是给定一个序列,选出若干数,每个数都比前面位置选的数大且选的数量最大,例如[3 2 6 4 5 1] 的LIS是 [2 4 5],长度为3(其实直接瞪眼定义名的就比较好理解了)

这里附一个LIS的模板,虽然和解法不搭嘎但是可以做一个知识的延展:

int lis(vector< int>& nums) 
{
    int n = nums. size();
    if (n == 0) return 0;
    vector< int> dp(n, 1);
    for (int i = 0; i < n; i++) 
    {
        for (int j = 0; j < i; j ++) 
        {
            if (nums[i] >= nums[j]) 
            { 大于就是严格
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
    }
    return *max element(dp. begin(), dp. end());
}

LDS(最长递减子序列):

这个和LIS直接取反就行,选出若干元素,满足严格递减,长度最长,(这个直接瞪眼定义名也比较好理解)

分块构造:

简而言之就是我们在构造排列压缩LIS的时候,用打乱原顺序,将序列划分为若干块,然后块内的元素递减排列,块间的整体递增排列,这样就可以缩短LIS长度,可以理解成压缩。

2.思路讲解​

读完题目可以知晓我们得构造一个排列 使 (\max(\text{lis}(p), \text{lds}(p)))尽可能小。其中块数m,每块大小k,所以LIS 长度不超过块数 m,得跨块递增,每块选一个元素,然后LDS 长度不超过最大块的大小 k,也就是块内递减。​

那么顺一下思路:取m=⌈n​⌉,确保块数和块大小接近 n​(比方说 n=10,则 m=4,块分布是 3,3,2,2),然后分配块大小,基本块大小 k = ⌊n/m⌋ ,余数 r = n % m。前 m − r 个块大小为 k,后 r 个块大小为 k+1,总和为 n。最后块内与块间排列,块内递减,每个块从大到小填充(比方说块大小为 3 时生成 3,2,1)块间递增,下一块的起始值严格大于上一块的结束值(比方说首块生成 3,2,1,下一块起始为 4,生成 6,5,4)。

其中的有几个要注意的:​

  • LIS 的上限:块间递增选 m 个元素, LIS \leq m。
  • LDS 的上限:块内递减最多选 k+1 个元素,LDS \leq k+1。
  • 如果只是纯粹的倒序,或者单向分块,那只会压缩LIS,但LDS可能会很大,不行。

AC_code

#include <bits/stdc++.h>
using namespace std;
#define int long long
void op(int n, vector<int> &a)
{
    int m = sqrt(n);
    if (m * m < n)//保证m不小于根号n,这样后面才能均匀地分块
        m++;
    int k = n / m;//每块的最小大小
    int r = n % m;//多出来元素数
    int cur = 1;//当前准备处理的数字的起始位置
    for (int i = 0; i < m; i ++)
    {
        int size = (i >= (m - r)) ? (k + 1) : k;//均匀分配(前/后分配大都可以,不影响)
        int end = cur + size - 1;//这块的元素的编号,[cur, end]
        for (int j = end; j >= cur; j --)
            a.push_back(j);//倒序,降低LIS
        cur = end + 1;//下一个
    }
}
signed main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while (t--)
    {
        int n;
        cin >> n;
        vector<int> a;
        op(n, a);
        for (int i = 0; i < n; ++i)
            cout << a[i] << (i == n - 1 ? '\n' : ' ');
    }
    return 0;
}

时间复杂度是O(n),不会超时。

DAY4 - C - Concatenation

一.题面

时间限制:C/C++/Rust/Pascal 4秒,其他语言8秒

空间限制:C/C++/Rust/Pascal 1024M,其他语言2048M

64 位输入输出格式:%lld

题目描述

NIO 是 OIN 王国的国王。

他有 N 个孩子,想教他们如何数数。在 OIN 王国中,计数使用五进制,所以他的孩子们只能使用 0、1、2、3 和 4 来表示数字。

有一天,NIO 让他的孩子们写下他们最喜欢的数字。之后,他提出了一个问题:如果他把这些数字串联成一个大数,那么他能得到的最小的数是多少?不过这个问题对他来说太难了,你能帮帮他吗?


输入描述

输入第一行包含一个整数 N(1 ≤ N ≤ 2 x 10^6),表示 NIO 的孩子个数。

接下来有 N 行,每行包含一个字符串 s_i,表示第 i 个孩子喜欢的数字。字符串由数字0、1、2、3、4组成,但可能有前导零,因为孩子们还没完全学会数数。

所有输入数字串长度之和满足:\sum_{i=1}^{N} |s_i| \leq 2 \times 10^7

输出描述

输出一个整数,表示 NIO 可以得到的最小数字。

测试样例1

输入:

5
121
1
12
00
101

输出:

00101112112

备注

如果你设计出的算法时间复杂度是 O(|S| log |S|) 之类的,请三思再提交。除了线性复杂度以外的算法不太可能通过本题,但你可以尝试。祝你好运。

二.思路解析

1.前置知识

这道题如果是单纯的排序写法,还是属于比较简单的(反正我看到开场十分钟就a了九队,至少不用等着看哪题是签到题)。其实这道题有起码两种解法(目前接触到的),鉴于字典树的解法难度较高且讲解不易,所以我选择了虽然时间复杂度较高(O(S log n),近乎是极限过的)但是比较简单的字符串自定义排序的做法。

附:这个解法的比较排序不是简单的快排,如果是一般的快速排序(sort)的话时间复杂度会直接飙到O(n log n),正好撞上了题目备注的警告,复杂度必爆无疑。

线性复杂度:

题目备注给出了贴心提醒(“虽然不行但是你可以试试O(|S| log |S|)”),说不是线性复杂度以外算法过的概率不大,那这里先解释一下线性复杂度。线性复杂度是指程序运行时间大致随输入规模 n 线性增长,即时间与数据量成正比。就比方说遍历一次数组就是线性时间,复杂度为O(n)。放在这道题,线性算法的运行时间就大概是c × S,c是常数而且是固定的。

自定义排序:
其实这个在我的上一篇博客的D题讲解已经谈到:对于STL中的快排函数sort,我们可以在外头自定义排序的规则函数。cmp是bool函数,在快排标准库里的比较函数返回的也是bool值(就是ture/false),所以直接在快排中加上,变为sort(... , ... , cmp),就可以实现自定义排序。想进一步练习的可以去尝试这道字符串拼接,思路更简单但整体差不多。

bool cmp(const string& a, const string& b) {return a + b < b + a;}

ad:若 a 拼在 b 前面小于 b 拼在 a 前,a 就排前面,以保证了字典序最小。

2.思路讲解​

首先,直接拼接是不可行的,这样没办法做到结果最小,只是暴力求解而已。那么怎么求最优方案呢?贪心排序就是比较合适的解法,对字符串排序,设有字符串 ab,则 a 应排在 b 前,当且仅当 a+b < b+a,最终拼接成的长数一定字典序最小。排序排完了,题目提到了可以出现前导0,但是倘若全是零呢?这就是一个坑点,我们要对全0的情况进行一个特判,如果所有字符串都是全0,应该只输出单个0,而不是一长串0。这样我们整体的思路就构建完成了。

AC_code

#include <bits/stdc++.h>
#define int long long
using namespace std;
bool cmp(const string& a, const string& b){return a + b < b + a;}
signed main() 
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int n;cin >> n;
    vector<string> st(n);
    for (int i = 0; i < n; i ++)cin >> st[i];
    sort(st.begin(), st.end(), cmp);
    bool tag = all_of(st.begin(), st.end(), [](const string& s) {return s == "0" || s == "00"; });
    if (tag) 
    {
        cout << "0" << '\n';
        return 0;
    }
    string res;
    for (const auto& s : st)res += s;
    cout << res << "\n";
    return 0;
}

其中all_of也可以改成比较遍历计数的写法,时间复杂度都是O(n),但是性能比较哈人,容易爆内存,大家酌情选择。

    int cnt0 = 0;  
    for (const auto& s : st) 
    {  
    if (s != "0" && s != "00")cnt0 ++;  
    }  
    bool tag = (cnt0 == 0); 
最后验证一下时间复杂度:
  1. 输入n个字符串,长度和为S,时间复杂度是 O(S)。

  2. 排序:主体是 n 个字符串排序,由于我们设定了自定义比较函数,所以一次比较的时间复杂度是 O(|a| + |b|),最坏情况下字符串长度接近平均,那就是 S/n。最终整体的比较次数大约有 O(n log n),再乘上每次比较是的O(S/n),总排序时间复杂度为: O(n \log n \times \frac{S}{n}),也就是O(S \log n)

  3. 检查:all_of 的前导0检查, 时间复杂度和输入一样也是O(S) 。

  4. 拼接:也是 O(S)。

  • 所以我们汇总起来,整体主要耗时在排序,时间复杂度是 O(S log n),很极限哈。

题目数据是 (\sum |s_i| = S \leq 2 \times 10^7)),所以复杂度不会爆。

DAY5 - N - Particle Arts

一.题面

时间限制:C/C++/Rust/Pascal 1秒,其他语言2秒

空间限制:C/C++/Rust/Pascal 256M,其他语言512M

特殊评测方式:Special Judge,64位IO格式:%lld

题目描述

在一个封闭的 NIO 空间中,有 n 个 NIO 粒子,第 i 个粒子的能量为 (a_i) 焦耳。
这些 NIO 粒子非常特殊,它们会随机相互碰撞。当一个携带能量为 a 焦耳的粒子与另一个携带能量为 b 焦耳的粒子碰撞时,这两个粒子会湮灭,产生两个新粒子,这两个新粒子分别携带的能量为 ( a & b ) 和 ( a | b )(这里的 AND 和 OR 是按位与和按位或操作)。

这些粒子能量的方差明显不会减少,但遗憾的是,空间太小,作者没法写出完整的证明。经过足够长时间后,这些粒子能量的方差会收敛到一个稳定值。
你能求出这个稳定值吗?

方差定义

对于 n 个数,方差定义如下:

[ \sigma^2 = \frac{1}{n} \sum_{i=1}^n (x_i - \mu)^2 ]

其中均值(\mu)定义为:

[ \mu = \frac{1}{n} \sum_{i=1}^n x_i ]


输入描述

第一行包含一个整数 n (2 ≤ n ≤ 10^5),表示粒子数量。
第二行包含 n 个整数 (a_1, a_2, ..., a_n) (0 ≤ a_i < 2^{15}),表示粒子能量。

输出描述

输出一个最简分数 (\frac{a}{b})(b > 0),表示答案。
你应保证 (\gcd(a,b) = 1),或者当 (a = 0) 时,令 (b=1)

测试样例1

输入:

5
1 2 3 4 5

输出:

54/5

备注

温馨提示:请注意数据类型的使用。

二.思路解析

1.前置知识

这题是比起前几天来说,感觉是最难的一题,

位运算:

按位与 a & b 只有当 ab 同一位都是1时结果才是1,按位或 a | b 只要ab某位是1,结果该位就是1。

均值和方差:

这些数学知识比较基础我就草草带过了,均值是数据的平均值。方差是每个数与均值差的平方的平均,公式是:
                        \sigma^2 = \frac{1}{n} \sum_{i=1}^n (x_i - \mu)^2 = \frac{1}{n} \sum x_i^2 - \mu^2

2.思路讲解

题目说给 n 个粒子,每个粒子有一定的能量。每次随机选取两个粒子碰撞,产生两个新粒子,能量为原来两能量的按位与与按位或结果,无限多轮碰撞后粒子能量会达到某个稳定分布,要求这时所有粒子能量的方差。直接模拟碰撞过程不现实,因为次数很大,绝对爆时间复杂度。那就只能找数学规律计算了。其中的与或计算其实看到之后不用慌,我们把它转换成二进制的位运算操作就可以了。

接着我们分析一下,由于能量的碰撞规则按位进行混合的,按位与只有两数对应位同时为1时这位才是1,按位或只有两数对应位有一个为1这位才是1,如果观察每一位的1的个数情况,我们可以发现,试着把总体大问题拆分成每位统计,可以拉开一个切口。

假设我们统计所有粒子在某一位上的“1”的个数 c_k  。这个值无限碰撞过程会重组,变成稳定分布。可是我们会发现,1的位数没有在碰撞中凭空消失,也没有凭空产生,所以这些位的1总量会是守恒的。那守恒的规律是怎样的呢?最高位一般先稳定,然后再向低位传递。

然后我们就可以开始构思代码了,当前位的“1”的数量是固定的,假设为cur,把这些“1”分配到已有的能量组(mp中的各能量值)中。从大能量到小能量分配优先,让部分粒子能量变为 v + (1 << k),分配完后更新mpmp2

for (int k = 14; k >= 0; k--) {
    int cur = c[k];
    if (cur == 0) continue;

    unordered_map<int,int> mp2;
    int rem = cur;
    vector<int> key;
    for (auto &p : mp) key.push_back(p.first);
    sort(key.begin(), key.end(), greater<int>()); // 从大到小

    for (int v : key) {
        if (rem <= 0) {
            mp2[v] += mp[v];
            continue;
        }
        int cv = mp[v];
        if (cv == 0) continue;

        int take = min(cv, rem); // 赋予当前位1数量
        int nv = v + (1ll << k);

        mp2[nv] += take;
        mp2[v] += cv - take;
        rem -= take;
    }

    mp.swap(mp2);
}

然后计算方差,能量平方和 all,总能量和 sa

int all = 0;
for (auto &p : mp) all += p.first * p.first * p.second;

int sa = 0;
for (int k = 0; k < 15; ++k)
    sa += c[k] * (1ll << k);

int num = all * n - sa * sa;
int den = n * n;

最后把分数约分输出即可。

AC_code

#include <bits/stdc++.h>  
using namespace std;  
#define int long long  
int gcd(int a, int b) {return b == 0 ? a : gcd(b, a % b);}  
signed main()   
{  
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);  
    int n; cin >> n;  
    vector<int> a(n); 
    unordered_map<int, int> mp;//能量值 -> 粒子数
    for (int i = 0; i < n; ++i) cin >> a[i];  
    int c[15] = {0}; // c[k]表示第 k 位(二进制从 0 到 14)中“1”的个数 
    for (int x : a)  
        for (int k = 0; k < 15; ++k)  
            if (x & (1 << k))  
                c[k] ++;  
    mp[0] = n;  
    for (int k = 14; k >= 0; k--)   
    {  
        int cur = c[k];  // 当前位有多少个1需要分配 
        if (cur == 0) continue;  
        unordered_map<int, int> mp2;  
        int rem = cur;  // 剩余要分配的1数量  
        vector<int> key;  
        for (auto &p : mp) key.push_back(p.first);  
        sort(key.begin(), key.end(), greater<int>());  
        for (int v : key)  
        {  
            if (rem <= 0)  
            {  
                mp2[v] += mp[v];  
                continue;  
            }  
            int cv = mp[v]; // 当前能量粒子的数目 
            if (cv == 0) continue;  
            int take = min(cv, rem);// 能分配的最大数  
            int nv = v + (1ll << k);  // 为当前位赋值后新能量  
            mp2[nv] += take;// 分配这部分粒子赋予1    
            mp2[v] += cv - take;// 剩余粒子保持原能量  
            rem -= take;// 减少剩余1数量   
        }  
        mp.swap(mp2);//新状态 
    }  
    int all = 0;//总能量和   
    for (auto &p : mp) all += p.first * p.first * p.second;  
    int sa = 0;  
    for (int k = 0; k < 15; ++k) sa += (int)c[k] * (1ll << k);  
    int num = all * n - sa * sa;  
    int den = (int)n * n;  
    if (num == 0) cout << "0/1" << '\n';  
    else   
    {  
        int g = gcd(num, den);  
        int aa = num / g;  
        int bb = den / g;  
        if (bb < 0)  
        {  
            aa = -aa;  
            bb = -bb;  
        }  
        cout << aa << "/" << bb << "\n";  
    }  
    return 0;  
}   

时间复杂度是 O(n logn),能过。

以上就是全部的题解了,感谢阅读,如果感兴趣的话可以点个赞关注一下吧。。

THE END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值