2023牛客寒假算法集训营6

A. 阿宁的签到题(签到)

题意:

输入一个数 x x x ,判断评分等级。

有以下等级:

  • ​ very easy ( 1 ≤ x ≤ 7 ) (1≤x≤7) (1x7)
  • ​ easy ( 7 < x ≤ 233 ) (7<x≤233) (7<x233)
  • ​ medium ( 233 < x ≤ 10032 ) (233<x≤10032) (233<x10032)
  • ​ hard ( 10032 < x ≤ 114514 ) (10032<x≤114514) (10032<x114514)
  • ​ very hard ( 114514 < x ≤ 1919810 ) (114514<x≤1919810) (114514<x1919810)
  • ​ 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 n1 轮,得到一个大背包。

例如:

初始有 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) (1in) ,可以将 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 klen 时,说明整个数组都变成了 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}⌋ k2n ,所以 n / 2 − k n / 2 - k n/2k 要么为 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 n1 ,那么没有多余的边,否则有多余的边。

代码:

#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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BraumAce

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值