2024ccpc全国邀请赛(郑州)暨第六届ccpc河南省赛(ABFHJKLM)

2024 c c p c 中国大学生程序设计竞赛(郑州全国邀请赛) \Huge{2024ccpc中国大学生程序设计竞赛(郑州全国邀请赛)} 2024ccpc中国大学生程序设计竞赛(郑州全国邀请赛)

P r o b l e m s   A 、 B 、 F 、 H 、 J 、 K 、 L 、 M \huge{Problems~A、B、F、H、J、K、L、M} Problems ABFHJKLM


题目链接:Dashboard - 2024 National Invitational of CCPC (Zhengzhou), 2024 CCPC Henan Provincial Collegiate Programming Contest - Codeforces

写在前面… 破铜烂铁选手,这次拿到了邀请赛的铜牌🥉。再接再励!

补题环节…
题解中的标程只放了伪代码,完整模板我放在了最后。

Problem A. Once In My Life

题意

给定两个整数 n , d n,d n,d,然后要求构造一个数字 k k k,要求 n × k n\times k n×k的值的数位中包含 0...9 0...9 0...9至少一次,并且 d ( 1 ≤ d ≤ 9 ) d(1\le d\le 9) d(1d9)至少两次。

思路

赛时的一道签到题,可是过题数好少。

我们按照顺序来构造即可:

  • 我们考虑先构造出 N = n × k N=n \times k N=n×k,那么 1234567890 + d 1234567890+d 1234567890+d即符合题意。
  • 然后我们考虑在 N N N后面加上若干位数字使得在不改变 N N N的前面 10 10 10位的情况下能够被 n n n整除。
  • 上一步的具体方法为:
    • N N N左移 n n n的位数位,然后加上 n n n(把 n n n放在 N N N后边),然后减去 N % n N\%n N%n,就可以被 n n n整除了。

标程

#define int long long
void Solved() {
    int n, d; cin >> n >> d;
    int len = to_string(n).size();
    int luck = (1234567890 + d) * pow(10, len);

    luck += n;
    luck -= luck % n;

    cout << luck / n << endl;
}

Problem B. 扫雷 1

题意

进行 n n n轮游戏,每轮会获得一个扫雷币,每轮可以买地雷探测器,给出每轮的地雷探测器的价格,求最多能买多少个地雷探测器?

思路

可以维护一个单调队列,每次存这位置地雷探测器的价格和下标。在单调队列里第 i i i个位置下标前攒的扫雷币都可以用这个价格来买。

标程

#define int long long 
#define fi first 
#define se second

void Solved() {
    int n; cin >> n;
    vector<PII> a;
    for(int i = 1; i <= n; i ++ ) {
        int x; cin >> x;
        while(!a.empty() && a.back().fi >= x) a.pop_back();
        a.push_back({x, i});
    }

    int res = a[0].se / a[0].fi;
    int t = a[0].se % a[0].fi, len = a.size();
    for(int i = 1; i < len; i ++ ) {
        res += (a[i].se - a[i - 1].se + t) / a[i].fi;
        t = (a[i].se - a[i - 1].se + t) % a[i].fi;
    }
    
    cout << res << endl;
}

Problem F. 优秀字符串

题意

给出优秀字符串的定义:

  • 长度为5。
  • 第三个字符和第五个字符相同。
  • 前四个字符互不相同。

求优秀字符串个数。

思路

签到题,模拟即可。

标程

void Solved() {
    int n; cin >> n;
    int res = 0;
    for(int i = 1; i <= n; i ++ ) {
        string s; cin >> s;
        if(s.size() != 5) continue;
        if(s[2] != s[4]) continue;
        bool f = 1;
        for(int i = 0; i < 4; i ++ )
            for(int j = i + 1; j < 4; j ++ )
                if(s[i] == s[j]) f = 0;
        res += f;
    }
    
    cout << res << endl;
}

Problem H. 随机栈

题意

题目给出 2 n 2n 2n次操作,每次操作有两种情况:

  • − 1 -1 1:从当前集合中取出一个数。
  • − 1 -1 1:将当前数字放入集合中。

两种情况各 n n n次,求最后取出的数字数组为递增(小于等于后一项)的概率,概率 p q \frac{p}{q} qp表示为: p × q − 1 m o d    998244353 p\times q^{-1} mod~~998244353 p×q1mod  998244353

思路

题目要求输出的数字数组为递增,我们可以通过贪心策略每次只取当前集合中最小的数字;如果当前的最小数字小于前面已选择的数字,那么将不可能构造出升序序列,概率为 0 0 0

题中对应的两种操作我们可以通过大根堆和 m a p map map实现。

但是这道题的一个难点是在求概率上:

  • 容易想到,概率中的分子 p p p即为当前集合中最小数的个数;分母 q q q即为当前集合中的数字个数。

  • 由于概率需要取模,所以需要用到乘法逆元。

  • 在循环模拟的过程中,分子 p p p和分母 q q q会非常大,但是我们如果在循环中直接求逆元,会超时。

  • 可以在循环过程中将分子分母分别保存,然后在循环外求逆元即可。

标程

const int mod = 998244353;

int quick_mi(int a,int b) {
    int ans = 1;
    while(b) {
        while(b % 2 == 0)
            a = a * a % mod, b = b / 2;
        ans = ans * a % mod; b = b - 1;
    }
    return ans ;
}

void solve() {
    int n; cin >> n;
    for(int i = 1; i<= 2 * n; i++){
        cin >> arr[i];
    }
    priority_queue<int,vector<int>,greater<int>> que;
    int maxx = 0;
    vector<int> z, m;
    
    for(int i = 1; i <= 2 * n; i++){
        if(arr[i] > -1){
            que.push(arr[i]); mp[arr[i]] ++;
        } else {
            int temp = que.top();
            if(temp < maxx){
                cout << "0" << endl; return;
            }
            maxx = max(maxx,temp);
            z.push_back(mp[temp]); m.push_back(que.size());
            
            que.pop(); mp[temp]--;
        }
    }
    int ans = 1;
    for(int i : z){
        ans *= i; ans %= mod;
    }
    for(int i : m){
        ans = ans * quick_mi(i, mod - 2); ans = ans % mod;
    }
    cout << ans <<endl;
}

Problem J. 排列与合数

题意

给出一个五位整数,然后将其每位重新排列,组成一个合数并输出;如果无法构造,则输出-1。

思路

签到题

构造合数只需将其中的合数位放在最后即可(注意前导零的情况)。

但是如果没有合数的情况呢?

题目样例中已经给出,五位都是奇数的情况直接输出97531即可。

所以说没有 − 1 -1 1的情况,不用考虑。

标程

void Solved() {
    string s; cin >> s;
    deque<int> dq;
    int sum = 0;
    for(int i = 0; i < 5; i ++ ) {
        int x = s[i] - '0';
        if(x & 1) dq.push_front(x), sum ++;
        else dq.push_back(x);
    }
    if(sum == 5) {
        cout << "97531\n";
    } else {
        for(int i : dq) cout << i; cout << endl;
    }
}

Problem K. 树上问题

题意

给出一个由 n n n各节点组成的无根树,编号为 1... n 1...n 1...n,每个节点有一个正整数点权a[i]。

现在定义美丽节点:如果一个节点作为根节点,当其他所有节点的点权都不小于其父节点点权的 1 2 \frac{1}{2} 21时,当前根节点为美丽节点

思路

考虑从边入手:

  • 若x与y之间有边,那么共有两种情况:

    1. a [ x ] × 2 < a [ y ] a[x] \times 2 < a[y] a[x]×2<a[y],将 x x x看作子节点,那么y及其所有祖宗节点都不为美丽节点。
    2. a [ x ] × 2 > a [ y ] a[x] \times 2 > a[y] a[x]×2>a[y],将 y y y看作子节点,那么x及其所有祖宗节点都不为美丽节点。
  • 所以我们只需遍历所有边,并且将上述所有情况的祖宗节点标记即可,最后没有被标记的即为美丽节点

  • 直接遍历会导致超时,通过观察会发现,被标记过的点的祖宗节点必定被标记,不需要再次进行标记,在循环的时候可以提前返回。

  • 但是剪枝后会出现如下情况:

  • 1
    3
    3 1 1
    1 2
    1 3

  • 这种情况是因为可行解有超过一个父节点,不符合树的定义。这种情况需要记录每个节点的父节点,然后进行判断。

标程

vector<int> a(N), b[N], fa;
vector<bool> f;
vector<PII> edge;
int n, flag = 1;

void init() {
    f.clear(); f.resize(n + 1); 
    fa.clear(); fa.resize(n + 1);
    edge.clear(); flag = 1;
    for(int i = 1; i <= n; i ++ ) b[i].clear(), fa[i] = -1;
}

void biaoji(int x) {
    if(f[x] || fa[x] == -1) return;
    f[x] = 1;
    for(auto i : b[x]) {
        if(i == fa[x]) continue;
        if(fa[i] != -1 && fa[i] != x) {flag = 0; return;}
        fa[i] = x;
        biaoji(i);
    }
}

void Solved() {
    cin >> n;
    init();
    
    for(int i = 1; i <= n; i ++ ) cin >> a[i];
    for(int i = 1; i < n; i ++ ) {
        int x, y; cin >> x >> y;
        b[x].push_back(y); b[y].push_back(x);
        if(a[x] * 2 < a[y]) {       //把x当作子节点,y当作父节点
            if(fa[y] != -1) flag = 0;
            fa[y] = x;
        }
        if(a[y] * 2 < a[x]){        //把y当作子节点,x当作父节点
            if(fa[x] != -1) flag = 0;
            fa[x] = y;
        }
    }

    for(int i = 1; i <= n; i ++ ) {
        biaoji(i);
    }

    int sum = 0;
    for(int i = 1; i <= n; i ++ ) {
        if(!f[i]) sum ++;           //未被标记的即为美丽节点
    }
    
    if(flag == 0) cout << "0\n";
    else cout << sum << endl;
}

Problem L. Toxel与PCPC II

题意

有一份长度为 n n n的代码,一共有 m m m行有错,并且给出有bug的行标,修复bug的规则为:

每次能选择一个数字 i i i,然后修复前 i i i行的所有bug,设前 i i i行的bug数为 x x x,则本次需要花费的时间为: t i = i + x 4 t_i=i+x^4 ti=i+x4

求最少的修复所有bug时间。

思路

这道题我们赛时通过数据范围pass了dp的思路,然后考虑贪心,后来发现不太对。

考虑用dp思路:

我们可以用 f [ i ] f[i] f[i]表示修复前i个bug需要的最短时间,那么状态转移方程为:
f [ i ] = min ⁡ 1 ≤ j ≤ i ( f [ j ] + a [ i ] + ( i − j ) 4 ) f[i]=\min_{1\le j\le i}(f[j]+a[i]+(i-j)^4) f[i]=1jimin(f[j]+a[i]+(ij)4)

但是 O ( n 2 ) O(n^2) O(n2)的时间复杂度是无法通过的,我们考虑优化:

  • 2 1 4 = 194481 , 2 2 4 = 234256 21^4=194481,22^4=234256 214=194481,224=234256
  • 2 1 4 < 2 e 5 21^4<2e5 214<2e5,是可能会被优化的,但是 2 2 4 > 2 e 5 22^4>2e5 224>2e5不会被优化。所以说是不会出现同时修复超过22处bug的情况。
  • 所以我们可以将第二维枚举的范围改为22就行,那么时间复杂度将优化为 O ( n n 4 ) O(n\sqrt[4]{n}) O(n4n )

标程

#define int long long
vector<int> a(N), f(N, LONG_MAX);//init
void Solved() {
    int n, m; cin >> m >> n;

    for(int i = 1; i <= n; i ++ ) {
        cin >> a[i];
    }
    f[0] = 0;
    f[1] = a[1] + 1ll;//init

    auto pow4 = [](int x)->int {return x * x * x * x;};

    for(int i = 2; i <= n; i ++ ) {
        int j  = 1;
        if(i - j + 1 >= 22) j = i - 21;//注意边界
        for(; j <= i; j ++ )
            f[i] = min(f[i], f[j - 1] + a[i] + pow4(i - j + 1));
    }

    cout << f[n] << endl;
}

Problem M. 有效算法

题意

给出两个长度为 n n n的数组 a , b a,b a,b,要求对每个 a i a_i ai进行以下操作正好一次:

  • a i a_i ai变成满足 ∣ a i − x ∣ ≤ k × b i |a_i−x|\le k\times b_i aixk×bi的任意整数 x x x

求出最小的非负整数 k k k,使得存在一个 x x x能够将操作后的 a a a数组按位等于 b b b数组。

思路

题目的数据范围比较大 ( 2 ≤ n ≤ 3 × 1 0 5 ) (2\le n \le 3 \times 10^5) (2n3×105)

根据数据范围猜测,这道题只能在 O ( n l o g n ) O(nlog_n) O(nlogn)的时间复杂度以内过掉。

然后看题,题目要求找出最小的 k k k k k k能够确定出 a i a_i ai变化的范围;那么 k k k必定是有序的,即若 k k k可行,那么 k + 1... k+1... k+1...也必定可行。

根据题目中的操作,我们可以将其分解为:

  • a i ≥ x a_i \ge x aix:原不等式可化为: a i − k × b i ≤ x a_i-k\times b_i \le x aik×bix
  • a i ≤ x a_i \le x aix:原不等式可化为: k × b i + a i ≥ x k \times b_i+a_i \ge x k×bi+aix

因此可以求出 x x x的区间,若区间合法,则 k k k满足要求,否则不满足要求。

标程

#define int long long 
vector<int> a, b;
int n; 

bool check(int k) {
    int mi = 0, mx = LONG_MAX;
    for(int i = 1; i <= n; i ++ ) {
        mi = max(mi, a[i] - k * b[i]);
        mx = min(mx, k * b[i] + a[i]);
        if(mi > mx) return false;
    }

    return true;
}

void Solved() {
    cin >> n;
    a.resize(n + 1); b.resize(n + 1);
    
    for(int i = 1; i <= n; i ++ ) cin >> a[i];
    for(int i = 1; i <= n; i ++ ) cin >> b[i];

    int l = 0, r = 1e9, mid;
    while(l < r) {
        mid = l + r >> 1;
        if(check(mid)) r = mid;
        else l = mid + 1;
    }

    cout << l << endl;
}

模板

#include<bits/stdc++.h>

using namespace std;

#define IOS ios::sync_with_stdio(false); cin.tie(nullptr), cout.tie(nullptr);
#define LL long long 
#define ULL unsigned long long 
#define PII pair<int, int>
#define lowbit(x) (x & -x)
#define Mid ((l + r) >> 1)
#define ALL(x) x.begin(), x.end()
#define endl '\n'
#define fi first 
#define se second

const int INF = 0x7fffffff;
const int mod = 1e9 + 7;
const int N = 2e5 + 10;	

void Solved() {
	
	
	
}

signed main(void) {
    IOS

	int ALL = 1; 
	// cin >> ALL;
	while(ALL -- ) Solved();
	// cout << fixed;//强制以小数形式显示
	// cout << setprecision(n); //保留n位小数

    return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值