牛客周赛 Round 111

第一次尝试倒开 qwq,没想到还意外的挺成功的,最后也是成功 ak 了喵 qwq

这里写的都是我赛时的做法,并非最佳做法,但是还是希望能帮到你 qwq,提供一个参考喵

A 小红的阶梯

直接判断就好了 qwq

void solve() {
	int a, b, c;
	std::cin >> a >> b >> c;
	
	if (b == a + 1 && c == b + 1) {
		std::cout << "Yes\n";
	} else {
		std::cout << "No\n";
	}
}

B 小红的数组取数

简单推下式子

我们要使这个式子尽可能大。

可以发现第一项是固定的,是两个数组和的差值;而第二项要尽可能大,让 尽可能大就好了,也就是选最大的 和最小的 。我这里用的是 ,感觉方便一点 。

void solve() {
    int n;
    std::cin >> n;
     
    std::vector<int> a(n), b(n);
    for (int i = 0; i < n; i++) {
        std::cin >> a[i];
    }
    for (int i = 0; i < n; i++) {
        std::cin >> b[i];
    }
     
    std::cout << std::min_element(a.begin(), a.end()) - a.begin() + 1 << " " << std::max_element(b.begin(), b.end()) - b.begin() + 1 << "\n";
}

C 小红抽卡

这是一个比较经典的问题。

题目要求是需要把第 个元素放到第一位,但是这个 特别大,我们一次次手动模拟肯定是会超时的。能不能不改变数组呢?

当然是可以的!我们可以观察一下,假设我们一开始数组头的位置是在 ,每次把第 个元素搬到前面去,就相当于在模 的意义下,让 往左移一格,也就是 (这里 是为了不让下标出现负数),这样的话,每移动 次, 其实都会回到开始的位置,那么我们只需要先把 模上 (每左移 格就回到原点),再在模 的意义下让 减去 (在模 的意义下左移 格),得到的就是最终的数组头的下标,这里用取模的话,建议数组使用 的下标,会方便很多!

输出的时候,先把 的元素输出完,再输出没有输出过的元素就好了,因为题目里的操作只会对 的元素造成影响。

void solve() {
    i64 n, k, x;
    std::cin >> n >> k >> x;
    k %= x;
     
    std::vector<int> a(n);
    for (int i = 0; i < n; i++) {
        std::cin >> a[i];
    }
     
    int p = (x - k) % x;
    std::vector<int> vis(n);
    while (!vis[p]) {
        std::cout << a[p] << " ";
        vis[p] = 1;
        p = (p + 1) % x;
    }
     
    p = 0;
    while (vis[p]) {
        p++;
    }
    while (p < n) {
        std::cout << a[p] << " ";
        p++;
    }
     
    std::cout << "\n";
}

D 小红的好数对

(update : 这题好像题面没说清楚qwq,但是根据样例 (样例 貌似也是赛后加的 qwq),题意应该是把 时的 认作两种不同的拼接方式)

这题挺好的 qwq,很多写法,这里提供的是 写法,从除法的本质入手,感觉挺对的 qwq

我们要让拼接之后的数是 的倍数,我们不妨思考一下,怎么样才能让拼接后的数是 的倍数。

我们可以回顾一下我们小时候学的列竖式除法的计算过程,对于第一位,除以 ,余了一个数,再把后面一位抄下来,再除 ...... 其实这样想就不难发现,我们只要让 的余数的后面接上 (前面的位不会对结果产生任何影响),如果这样产生的新的数还能被 整除,那么这个数也就一定能被 整除。

模完 之后,得到的余数一定是在 之间的,那么我们就可以 啦,记 表示在 之前的所有数里(不包含第 个数),有多少个数模 的余数是 表示在 之后的,其他都和前面一样。对于 数组,每次转移的时候,先把前面第 个位置的所有 值全部搬过来,然后再更新这一位的余数所带来的影响, 数组同理。

然后我们再遍历一遍原数组,对于每一位 ,我们遍历所有 的余数,计算这些余数后面拼接 之后的数值,看看能不能被 整除,可以的话,我们就直接加上 的对应数值。然后我们还要倒着跑一遍,一样的计算。(注意: 拼接的顺序不同,结果也会不同!这两个数不一定都会被 整除!我就在这里获得了 分的好成绩 qwq )

对于数的拼接,我们记前面的数为 , 接在后面的数为 ,且 在十进制下的长度为 (也就是这个数有多少位),那么拼接起来的数就是 。可以快速幂优化一下,但是不优化应该也没问题 qwq,应该还是能过的。

i64 power(i64 a, i64 b) { // 快速幂
    i64 res = 1;
    for (; b; a *= a, b >>= 1) {
        if (b & 1) {
            res *= a;
        }
    }
    return res;
}
 
void solve() {
    int n;
    std::cin >> n;
     
    std::vector<int> a(n + 1);
    for (int i = 1; i <= n; i++) {
        std::cin >> a[i];
    }
     
    std::vector<std::vector<int>> pre(n + 1, std::vector<int>(10 + 1));
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= 10; j++) {
            pre[i][j] = pre[i - 1][j];
        }
        pre[i][a[i] % 11]++;
    }
     
    std::vector<std::vector<int>> suf(n + 2, std::vector<int>(10 + 1));
    for (int i = n; i >= 1; i--) {
        for (int j = 0; j <= 10; j++) {
            suf[i][j] = suf[i + 1][j];
        }
        suf[i][a[i] % 11]++;
    }
     
    i64 ans = 0;
    for (int i = 2; i <= n; i++) { // 顺着跑一遍
        int len = 0, x = a[i];
        while (x) {
            len++;
            x /= 10;
        }
         
        for (int j = 0; j <= 10; j++) {
            if ((1LL * j * power(10, len) + a[i]) % 11 == 0) {
                ans += pre[i - 1][j];
            }
        }
    }
     
    for (int i = n - 1; i >= 1; i--) { // 倒着跑一遍
        int len = 0, x = a[i];
        while (x) {
            len++;
            x /= 10;
        }
 
        for (int j = 0; j <= 10; j++) {
            if ((1LL * j * power(10, len) + a[i]) % 11 == 0) {
                ans += suf[i + 1][j];
            }
        }
    }
     
    std::cout << ans << "\n";
}

E 小芳的排列构造

构造题。

由于下面的讨论会用到 这个条件,这里我们先分类讨论一下 时的答案:

时,只有 这一个数, 在算 的时候会被考虑一次,算 的时候也会被考虑一次,所以答案只能是 ,其他的数都不行,此时数组为

否则 ,我们先考虑最大和最小能达到的和是多少。

总体分析一下,首先 一定会被考虑两次,算 会被考虑到,算 也会被考虑到,因为 大于 的所有数。

然后对于其他数,一定 至多被考虑一次,因为无论怎么放,总有一边有 这个数存在,所以不能大于这一边的所有数。而对于数 ,它一定会被考虑到一次,因为一边有 这个数,不会被考虑到,另一边的所有数都一定小于 ,所以有一个固定的贡献。其他的数如果放到 的外侧,就可能产生一次贡献,否则一定不会产生贡献,因为一侧有 , 另一侧有 ,都不会让这些数产生贡献。

那么,答案最小的情况就是, 产生两次贡献, 产生一次贡献,其他数都不产生贡献,此时答案为 ;答案最大的情况是, 产生两次贡献,其他数均产生一次贡献,此时答案为

所以答案的范围就是

然后我们考虑怎么构造对应值的数组 。

首先数组的初态是 ,这样数组能产生最小贡献。

如果 在上面说的范围内,我们可以开一个变量 表示还需要多少贡献才能到 ,于是我们可以贪心地从 这个范围内从大到小取数,可以证明是一定能凑出来的,这一部分的严谨数学证明放在了整篇题解的结尾。每取一个数,如果小于等于当前的 ,就把它加到数组的最后面,并且让 减去这个数,否则放到 之间 。这样构造, 后面的数是递减的,每个都会产生一次贡献,其他的数不会产生贡献 。

void solve() {
	i64 n, k;
	std::cin >> n >> k;
	
    if (n == 1) {
		std::cout << 1 << "\n";
		return;
	}
    
	if (k < 3 * n - 1 || k > n * (n + 1) / 2 + n) {
		std::cout << -1 << "\n";
		return;
	}
	
	std::vector<int> ans;
	ans.push_back(n);
	
	i64 dif = k - 3 * n + 1;
	std::vector<int> f; // 开个辅助数组,不然真的往中间插复杂度太高了
	for (int i = n - 2; i >= 1; i--) {
		if (i <= dif) {
			f.push_back(i);
			dif -= i;
		} else {
			ans.push_back(i);
		}
	}
	
	ans.push_back(n - 1);
	for (int i = 0; i < f.size(); i++) {
		ans.push_back(f[i]);
	}
	
	for (int i = 0; i < n; i++) {
		std::cout << ans[i] << " \n"[i == n - 1];
	}
}

F 小红的排列构造

构造题。

先来考虑最少和最多需要多少次才能把数组还原成顺序。

首先是最小次数,很明显,一次最多改变两个数的位置,也最多能使两个数回到自己的位置上,对于 是偶数的情况,最少需要 次,此时数组为,把 数组,第一个和最后一个交换,第二个和倒数第二个交换 ...... ,而对于 是奇数的情况,在进行完和偶数一样的操作后,中间的数还满足 ,所以还需要再加一次,假设中间三个数从小到大是 ,那么可以排成 ,这样只比原来多一次,总结一下,可以化成一个式子,也就是 次。

然后是最大次数,因为题目意思是一定会采取最优方式交换,那么最优方式最劣的情况就是 次,每次让一个数回到位置上,剩下的不会回到位置上,在交换完第 个数后,最后一个数会自动归位,所以是 次,此时数组可能是 ,这种应该是 次的,**但是我们下面的构造不会这么构造 **。

这样就能筛选掉所有的无解的情况。

否则有解,我们考虑从初态,也就是最小次数的情况开始扩展。

记变量 ,表示还要多加多少次数,由于按照上面的构造方式,现在第一个只和最后一个交换,第二只和倒数第二个交换 ...... 那么,只要交换 ,就能让次数加一,因为,你最后一定要换回来再和最后面的交换,这样不断交换最终能达到最大的 次 。

void solve() {
    int n, k;
    std::cin >> n >> k;
     
    if (k < (n + 1) / 2 || k > n - 1) {
        std::cout << -1 << "\n";
        return;
    }
     
    std::vector<int> a(n);
    std::iota(a.begin(), a.end(), 1); // 这个函数表示把数组 a 的所有元素填充为 [1, 2, ..., n]
     
    for (int i = 0; i < n / 2; i++) { // 先和后面的交换
        std::swap(a[i], a[n - 1 - i]);
    }
    std::swap(a[n / 2], a[(n + 1) / 2]); // n 为奇数就多交换一次,n 为偶数时这两个下标是相等的
     
    for (int i = 0; i < k - (n + 1) / 2; i++) { // 补充次数
        std::swap(a[i], a[i + 1]);
    }
     
    for (int i = 0; i < n; i++) {
        std::cout << a[i] << " \n"[i == n - 1];
    }
}

结尾附上 E 题,对于 内的所有数都能被 选部分个数恰好一次求和所得到的证明喵 qwq,证明由 yanami_anna 提供,太强啦 qwq,"贪心一定要证明啊!".jpg alt