A. 阿宁的签到题(签到)
题意:
输入一个数 x x x ,判断评分等级。
有以下等级:
- very easy ( 1 ≤ x ≤ 7 ) (1≤x≤7) (1≤x≤7)
- easy ( 7 < x ≤ 233 ) (7<x≤233) (7<x≤233)
- medium ( 233 < x ≤ 10032 ) (233<x≤10032) (233<x≤10032)
- hard ( 10032 < x ≤ 114514 ) (10032<x≤114514) (10032<x≤114514)
- very hard ( 114514 < x ≤ 1919810 ) (114514<x≤1919810) (114514<x≤1919810)
- can not imagine ( 1919810 < x ) (1919810<x) (1919810<x)
根据以上划分输出等级。
思路:
依题意输出字符串。
代码:
#include <bits/stdc++.h>
#define ll long long
#define endl '\n'
using namespace std;
const int N = 2e5 + 10;
int main()
{
ll x;
cin >> x;
if (x <= 7) cout << "very easy" << endl;
else if (x <= 233) cout << "easy" << endl;
else if (x <= 10032) cout << "medium" << endl;
else if (x <= 114514) cout << "hard" << endl;
else if (x <= 1919810) cout << "very hard" << endl;
else cout << "can not imagine" << endl;
return 0;
}
B. 阿宁的倍数(暴力枚举)
题意:
给定一个长度为 n n n 的 a a a 数组,下标从 1 1 1 开始,有 q q q 次操作。
修改操作:数组末尾增加一个数 x x x ,数组长度加 1 1 1 。
询问操作:有多少个 i i i ( i > x ) (i > x) (i>x) ,满足 a i a_i ai 是 a x a_x ax 的倍数?
接下来 q q q 行,每行两个数 o p op op , x x x ,代表一次操作。
如果 o p op op 是 1 1 1 代表是修改操作;如果是 2 2 2 代表是询问操作。
保证至少有一次询问,输出每次询问操作的结果。
思路:
在线做法
暴力枚举当前数的因子,在因子的桶 v v v 对应位置 + 1 +1 +1 。 并且记录,前缀中有多少个当前数,记在桶 u u u 里。(预处理数组和修改数组都这样做)
询问时,答案就是这个数的个数,减去前缀这个数的个数,分别在桶 v , u v,u v,u 中找到。
代码:
#include <bits/stdc++.h>
#define ll long long
#define endl '\n'
using namespace std;
const int N = 2e5 + 10;
int n, q;
int a[N * 2];
int op[N], x[N];
int u[N * 2], v[N];
int main()
{
cin >> n >> q;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= q; i++)
cin >> op[i] >> x[i];
for (int i = 1; i <= n; i++){
for (int j = 1; j * j <= a[i]; j++){
if (a[i] % j == 0){
//如果是a[i]的因子
v[j]++;
if (j != a[i] / j){
v[a[i] / j]++;
}
}
}
u[i] = v[a[i]];
}
for (int i = 1; i <= q; i++){
if (op[i] == 1){
a[++n] = x[i];
for (int j = 1; j * j <= a[n]; j++){
if (a[n] % j == 0) {
//同理,如果是x[i]的因子
v[j]++;
if (j != a[n] / j) {
v[a[n] / j]++;
}
}
}
u[n] = v[a[n]];
}
else{
// v[a[x[i]]]是a[x[i]]的数量
// u[x[i]]是前缀x[i]中a[x[i]]的数量
cout << v[a[x[i]]] - u[x[i]] << endl;
}
}
return 0;
}
C. 阿宁的大背包(贪心)
题意:
有 n n n 个背包,大小分别从 1 1 1 到 n n n 。现要将这 n n n 个背包合成一个大背包。
先将这
n
n
n 个背包排成一行。
每一轮合成,从相邻的两个背包得到一个新的背包,大小为两个背包的大小之和,旧背包消失。
共合成
n
−
1
n−1
n−1 轮,得到一个大背包。
例如:
初始有
4
4
4 个背包,顺序为
[
1
,
4
,
3
,
2
]
[1,4,3,2]
[1,4,3,2]。
第一轮:
[
5
,
7
,
5
]
[5,7,5]
[5,7,5]。
第二轮:
[
12
,
12
]
[12,12]
[12,12]。
第三轮:
[
24
]
[24]
[24]。
4
4
4 个背包得到一个大小为
24
24
24 的背包。
问这 n n n 个背包,初始的顺序是什么,才能获得最大的背包?
思路:
贪心的思维:要想背包最大,则用到的小背包要尽可能大且尽可能多,所以初始摆放顺序一定是大的放中间,小的放两边。也就是组合数的特点:中间大,两边小。
整体的合成顺序类似于杨辉三角。
因为数据量较小,可以直接用一个二维数组来存储所有结果。
先预处理好初始的背包顺序,再每个挨个合成,到最后只剩一个,即是答案。
代码:
#include <bits/stdc++.h>
#define ll long long
#define endl '\n'
using namespace std;
const int N = 1010;
const int mod = 1e9 + 7;
ll a[N][N];
int main()
{
int n;
cin >> n;
int p = 1;
for (int i = 1, j = n; i <= j; i++, j--) //先预处理出初始背包顺序
{
if (i == j){
a[1][i] = p;
break;
}
a[1][i] = p; p++;
a[1][j] = p; p++;
}
//杨辉三角直接合成,注意每次取模即可
for (int i = 2; i <= n; i++){
for (int j = 1; j <= (n - i) + 1; j++){
a[i][j] = (a[i - 1][j] + a[i - 1][j + 1]) % mod;
}
}
printf("%lld\n", a[n][1]);
for (int i = 1; i <= n; i++)
cout << a[1][i] << ' ';
cout << endl;
return 0;
}
D. 阿宁的毒瘤题(前缀和 + 枚举)
题意:
给定一个长度为 n n n 且只包含小写字母的字符串 s 。
定义毒瘤程度为 s 串中 udu
子序列的个数。
现要求只修改字符串 s 中的一个字符(也可以不修改),使得修改后的毒瘤程度最小。
输出修改后的字符串 s 。
思路:
先计算出每个字符有多少个和它关联的 udu
子序列。找到关联最多的字符,只需要将这个字符改掉,就可以少掉最多的 udu
子序列。
对于字符 d
,在它两边各找一个 u
字符,组成 udu
子序列,因此,将它左右两边的 u
的数量相乘,即可得到关联的子序列数。
而对于字符 u
,我们用两遍 前缀和,一次从前往后,一次从后往前,来寻找其前面和后面出现的 u
的数量。并且它和它左边的 ud
子序列组成 udu
子序列,和右边的 du
子序列组成 udu
子序列,记录下每次关联后的子序列数。
最后再遍历一遍,找到关联数最多的字符位置,替换掉后再输出字符串 s 即可。
代码:
#include <bits/stdc++.h>
#define ll long long
#define endl '\n'
using namespace std;
const int N = 2e5 + 10;
char s[N];
ll cnt[N];
ll preu[N], sufu[N];
int main()
{
cin >> s + 1;
int n = strlen(s + 1);
//前缀和
for (int i = 1; i <= n; i++){
preu[i] = preu[i - 1];
if (s[i] == 'u'){
preu[i]++;
}
}
//后缀和
for (int i = n; i >= 1; i--) {
sufu[i] = sufu[i + 1];
if (s[i] == 'u'){
sufu[i]++;
}
}
for (int i = 1; i <= n; i++){
if (s[i] == 'd'){
// 将左右两边的'u'的数量相乘
cnt[i] = preu[i - 1] * sufu[i + 1];
}
}
//u是左边'u'的数量,d是左边"ud"子序列的数量
ll u = 0, d = 0;
for (int i = 1; i <= n; i++){
if (s[i] == 'u'){
u++;
//当前s[i]和左边"ud"子序列组成"udu"子序列,新增d个"udu"
cnt[i] += d;
}
else if (s[i] == 'd'){
//当前s[i]和左边"u"子序列组成"ud"子序列,新增u个"ud"
d += u;
}
}
//再求一边右边的
//u是右边'u'的数量,d是右边"du"子序列的数量
u = 0, d = 0;
for (int i = n; i >= 1; i--){
if (s[i] == 'u'){
u++;
//当前s[i]和右边"du"子序列组成"udu"子序列,新增d个"udu"
cnt[i] += d;
}
else if (s[i] == 'd'){
//当前s[i]和右边"u"子序列组成"du"子序列,新增u个"du"
d += u;
}
}
// 找到最多"udu"子序列的位置
ll temp = 0;
for (int i = 1; i <= n; i++){
temp = max(temp, cnt[i]);
}
for (int i = 1; i <= n; i++){
if (temp == cnt[i]){
s[i] = 'a';
break; //改完就退出
}
}
cout << s + 1 << endl;
return 0;
}
F. 阿宁的二进制(二进制的性质 + 贪心)
题意:
给定一个长度为 n n n 的 a a a 数组,下标从 1 1 1 开始。
定义 f ( x ) f(x) f(x) 表示为 x x x 在二进制表示下 1 1 1 的个数。
例如: 5 5 5 的二进制是 101 101 101 ,因此 f ( 5 ) = 2 f(5) = 2 f(5)=2 。
有 q q q 次询问,每次询问后数组恢复原样。
在每次询问中,给出一个 k k k。在一次操作中,可以选择一个数 i i i ( 1 ≤ i ≤ n ) (1≤i≤n) (1≤i≤n) ,可以将 a i a_i ai 修改为 f ( a i ) f(a_i) f(ai) 。
现在恰好进行 k k k 次操作,要求操作完后整个数组的最大值最小,问这个最大值是多少?
思路:
f ( x ) f(x) f(x) 就是将 x x x 变小(除了 1 1 1 ),而且在 $ 10^9$ 范围内,最多 4 4 4 次就可以将每个数都变成 1 1 1。
当整个数组全是 1 1 1 的时候,就该结束操作了,即使还有操作次数剩余。
贪心的想,要使最后操作的最大值最小,那么每次操作都要选最大的数变小,然后再选变化后的数组中最大的数进行上述操作。
我们可以,将整个数组都变成 1 1 1 ,对于每次操作中间出现的数,都将其存在一个 b b b 数组中,然后从大到小排序。假设 b b b 数组的长度为 l e n len len ,当 k ≥ l e n k \ge len k≥len 时,说明整个数组都变成了 1 1 1 ,直接输出 1 1 1 即可,否则输出第 k + 1 k + 1 k+1 个数(前 k k k 个数都变小了)
代码:
#include <bits/stdc++.h>
#define ll long long
#define endl '\n'
using namespace std;
const int N = 2e5 + 10;
int a[N];
//判断二进制表示中1的个数
int f(int n)
{
int cnt = 0;
while (n){
cnt++;
n &= (n - 1);
}
return cnt;
}
int main()
{
int n, q;
cin >> n >> q;
for (int i = 1; i <= n; i++){
cin >> a[i];
}
vector<int> v; //存储中间数的数组
for (int i = 1; i <= n; i++){
v.push_back(a[i]);
while (a[i] > 1){
v.push_back(f(a[i]));
a[i] = f(a[i]);
}
}
sort(v.begin(), v.end(), greater<int>()); //从大到小排序
while (q--){
int k;
cin >> k;
if (k >= v.size()) cout << 1 << endl;
else cout << v[k] << endl; //输出v[k]是因为下标从0开始的
}
return 0;
}
G. 阿宁的整数配对(优先队列 + 数论)
题意:
有一个长度为 n n n 的整数数组 a a a 。
要求在其中选出恰好 k k k 对整数,使得每对整数相乘并求和尽可能大。
问最终得到的值是多少?
思路:
在这些数里面,既有正数也有负数还有 0 0 0 。
要使得结果尽可能最大,那就要尽可能使得每对整数都是正数。
因为正数乘以正数结果是正数; 负数乘以负数结果是正数。所以 尽量同号的配一对,且尽量让绝对值大的先配对。
我们可以用两个 __优先队列__来存储,将正数和 0 0 0 放在一个从大到小排序的优先队列中,将负数放在一个从小到大的优先队列中,因为一对负数越小相乘后结果越大。
每次分别从两个优先队列中取出前面两个数,比较大小,大的值将其加入到答案当中,小的那对数放回原优先队列中,即保持原来的顺序。重复上述操作,直至队列为空或还剩下一个数时结束。
这时再判断:因为 k ≤ ⌊ n 2 ⌋ k \le ⌊\frac{n}{2}⌋ k≤⌊2n⌋ ,所以 n / 2 − k n / 2 - k n/2−k 要么为 0 0 0 ,要么还剩 1 1 1 个数。可以推断出,在可取的情况下,对于这两个队列,每次都取出两个数,到最后有三种情况:要么都为空;要么一个为空一个还剩 1 1 1 个数;要么都只剩 1 1 1 个数。
所以最后分类讨论,对于都只剩 1 1 1 个数的情况,取出两数相乘加入到答案中即可。
代码:
#include <bits/stdc++.h>
#define ll long long
#define endl '\n'
using namespace std;
const int N = 2e5 + 10;
int main()
{
int n, k;
cin >> n >> k;
priority_queue<int>a; //存正数和0,从大到小
priority_queue<int, vector<int>, greater<int>> b; //存负数,从小到大
for (int i = 1; i <= n; i++){
int x;
cin >> x;
if (x >= 0) a.push(x);
else b.push(x);
}
ll ans = 0;
while (k--){
int x, y;
int x1, x2, y1, y2;
if (a.size() >= 2 && b.size() >= 2){
x1 = a.top(); a.pop();
x2 = a.top(); a.pop();
x = x1 * x2;
y1 = b.top(); b.pop();
y2 = b.top(); b.pop();
y = y1 * y2;
//cout << x << ' ' << y << endl;
if (x >= y) //判断,取大的一对,放回小的一对
{
ans += (ll)x;
b.push(y1);
b.push(y2);
}
else {
ans += (ll)y;
a.push(x1);
a.push(x2);
}
}
else if (b.size() < 2 && a.size() >= 2){
x1 = a.top(); a.pop();
x2 = a.top(); a.pop();
x = x1 * x2;
ans += (ll)x;
}
else if (a.size() < 2 && b.size() >= 2){
y1 = b.top(); b.pop();
y2 = b.top(); b.pop();
y = y1 * y2;
ans += (ll)y;
}
else if (a.size() == 1 && b.size() == 1){
x = a.top(); a.pop();
y = b.top(); b.pop();
ans += (ll)(x * y);
}
}
cout << ans << endl;
return 0;
}
H. 阿宁讨伐虚空(枚举)
题意:
给定一个正整数
x
x
x ,在 [L, R]
区间内等概率选一个整数
y
y
y ,求
y
<
x
y < x
y<x 的概率。
思路:
直接求 y y y 的概率不好求,转化为求 x x x 。
分别讨论 x x x 小于区间, x x x 在区间内, x x x 大于区间的情况即可。
代码:
#include <bits/stdc++.h>
#define ll long long
#define endl '\n'
using namespace std;
const int N = 2e5 + 10;
int main()
{
double x, l, r;
cin >> x >> l >> r;
if (x <= l) printf("%.16f\n", 0.0);
else if (x > r) printf("%.16f\n", 1.0);
else {
double res = 1.0 * (x - l) / (r - l + 1);
printf("%.16f\n", res);
}
return 0;
}
I. 阿宁前往沙城(BFS)
题意:
沙城中有 n n n 个城市,沙神住在第 n n n 号城市。我们要从 1 1 1 号城市前往 n n n 号城市。
给定两个整数 n , m n,m n,m 。
接下来 m m m 行,每行三个整数 u , v , w u,v,w u,v,w ,表示 u u u 号城市和 v v v 号城市有道路相连,通过该道路需要 w w w 的时间(道路是双向的)。
有一个技能:选择两条道路,使其中一条道路的通过时间变为 1 1 1 ,并毁灭另一条道路。被毁灭的道路无法通行,且无法再被技能选择。
可以在任何时候使用任意次该技能,且使用技能不消耗时间。
问最少需要多少时间才能到达 n n n 号城市?
思路:
可以走一条边,将刚走的这一条边毁灭,下一条边的通过时间改为 1 1 1 。 因此,除了要走的第一条边,剩下的边通过时间都可以改成 1 1 1 。
为了使第一条的通过时间也改成 1 1 1,找到一条多余的边毁灭即可。
同时为了找到多余的边,从终点开始 BFS ,所有的边权当作 1 1 1 。
如果 1 1 1 到 n n n 的距离是 n − 1 n−1 n−1 ,那么没有多余的边,否则有多余的边。
代码:
#include <bits/stdc++.h>
#define ll long long
#define endl '\n'
#define PII pair<int, int>
#define INF 0x3f3f3f3f
using namespace std;
const int N = 2e5 + 10;
int n, m;
vector<PII> edge[N];
queue<int> q;
int d[N];
void bfs()
{
memset(d, 0x3f, sizeof(d));
d[n] = 0;
q.push(n);
while (!q.empty()){
int t = q.front();
q.pop();
for (auto &e : edge[t]){
int idx = e.first;
if (idx == 1) continue;
if (d[idx] > d[t] + 1){
d[idx] = d[t] + 1;
q.push(idx);
}
}
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++){
int u, v, w;
cin >> u >> v >> w;
edge[u].push_back({v, w});
edge[v].push_back({u, w});
}
bfs();
int ans = INF;
for (auto &e : edge[1]){
int idx = e.first, w = e.second;
// 1和idx有一条边,权值是w
// idx到n的最短路是d[idx]
// 因此1到n的最短路是d[idx]+1
// 也就是1到n的需要最少走过d[idx]+1条边
if (m > d[idx] + 1){
//说明1和idx这一条边的权值w可以改成1
w = 1;
}
ans = min(d[idx] + w, ans);
}
//特判n
if (n == 1) ans = 0;
cout << ans << endl;
return 0;
}