别忘了请点个赞+收藏+关注支持一下博主喵!!!
关注博主,更多蓝桥杯nice题目静待更新:)
思维与贪心
一、重复字符串
【问题描述】
· 如果一个字符串S恰好可以由某个字符串重复K次得到,我们就称S是K次重复字 符串。例如abcabcabc可以看作是abc重复3次得到,所以abcabcabc是3次重复字符串。
同理aaaaaa既是2次重复字符串,又是3次重复字符串和6次重复字符串。
现在给定一个字符串S,请你计算最少要修改其中几个字符,可以使S变为一个K 次重复字符串?
【输入格式】
第一行包含一个整数K。 第二行包含一个只含小写字母的字符串S。 其中,1⩽K⩽105,1⩽|S|⩽105。其中|S|表示S的长度。
【输出格式】
输出一个整数代表答案。如果S无法修改成K次重复字符串,输出−1。
【样例输入】
【样例输出】
2
解析:
先来分析,什么情况下S无法修改成K次重复字符串?
(1)S 的长度小于K。K 次重复字符串的长度至少为K。因为我们只能修改字符串,无 法添加字符串,所以当S的长度小于K时,S一定无法修改成K次重复字符串。
(2)S 的长度不是K 的倍数。若S 是K 次重复字符串,则S是由某个字符串重复K 次构成,所以S的长度一定是K的倍数。同样因为无法添加字符串,所以当S的长度不为 K 的倍数时,S一定无法修改成K次重复字符串。 第2种情况实际上是包含了第1种情况的。因此判断S能否修改为K次重复字符串,只要 判断S的长度是否是K的倍数即可。
(3)对于其他情况,S 一定可以修改为K 次重复字符串。
那么要如何修改,才能保证修改的次数最少呢? 我们来分析一下。
假设我们以最少的修改次数将字符串S 修改成了K 次重复字符串(记为S′)。根据定 义,S′ 可由某个字符串重复K 次得到。
由于K是已知的,所以只要我们能求出某个字符串,就可将S′还原。还原了S′后,就可 以将其和S一一比对,以求出需要修改的次数。于是问题就可以转换成如何求出某个字符串。
我们设某个字符串为T,S′ 可由T 重复K 次得到(|T|= |S| 们设|T| 的值为n),如下图所示。
根据K次重复字符串的性质,如果我们将字符串T 展开为T1,T2,...,Tn(T1 表示T 的 第1个字符,T2 表示T 的第2个字符,...,Tn 表示T 的第n个字符),则满足以下条件。
例如,S 的长度为6(|S|=6)、K =3,则n= =2,S′ 可由3个T 重复得到,如下图所示。
观察上图可以发现,如果将S′中与T1相同的部分组合在一起、与T2相同的部分组合在一 起······ 与Tn 相同的部分组合在一起,就会得到一个n×K的矩阵(n×K = × K= |S|如下表所示。
该矩阵为字符串S′ 对应的矩阵,它满足每一行所有的元素值都相同。
S′ 是我们对S 进行了最少次数的修改后得到的K 次重复字符串。从已知条件推断,它与S唯一的区别就在于它对应矩阵的每一行的所有元素值都相同。所以,只要用最少的修改 次数将S对应矩阵的每一行元素都修改为相同的元素,得到的就是S′,而这最少的修改次数 就是我们想要的答案。
由于矩阵的行与行之间是互不关联的,因此最少的修改次数就等价于每行的最少修改次数之和。那么针对矩阵的某一行,如果我们要让所有元素的值都为x,则需要修改的次数就 为矩阵的列数−该行中值为x的元素个数,即为K−x出现的次数。
显然,要使修改的次数最少,就要将该行的所有元素都修改为出现次数最多的元素。于是我们可以得出贪心策略,即将每一行的所有元素都修改为出现次数最多的元素。
由于修改某一行对其他行不会造成任何影响,且每一行的修改次数都是最少的,所以可 以保证使用该策略得到的修改次数一定最少,而这最少的修改次数就是我们所要的答案。
总结:我们在刚开始思考的时候,将问题转换为了如何求解某个字符串T,但最后并没 有实际地解决这个问题,而是通过这个问题进一步去分析以得到最终的答案。其实很多时候, 我们在面对一个问题时,都不能很快有一条清晰的解题思路。这时,与其马马虎虎猜结论、猜 做法,不如脚踏实地,一步步深入思考,发掘解题的核心。
参考代码如下【时间复杂度为O(n×K)=O |S| K×K =O(|S|)】
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, k, ma[N], cnt[30];
char s[N];
signed main() {
cin >> k >> (s + 1); // 从s[1]开始存储字符串
// 当S不是K的倍数时,无论S如何修改,也无法成为K次重复字符串
if (strlen(s + 1) % k) return cout << -1 << '\n', 0;
n = strlen(s + 1) / k;
int ans = 0;
for (int i = 1; i <= n; i++) {
// cnt[j]记录第i行第j个元素出现的次数。初始化cnt数组所有元素的值为0
for (int j = 0; j <= 25; j++) cnt[j] = 0;
int ma = 0; // ma统计第i行出现最多的元素
for (int j = 1; j <= k; j++) {
int x = s[i + (j - 1) * n]; // S对应矩阵的第i行的第j列个元素
cnt[x - 'a']++; // 该元素出现的次数+1
ma = max(ma, cnt[x - 'a']); // 更新出现次数最多的元素
}
ans += k - ma;
}
cout << ans << '\n';
return 0;
}
二、翻硬币
【问题描述】
小明正在玩一个“翻硬币”的游戏。
桌上放着排成一排的若干硬币。我们用∗表示正面,用o表示反面(是小写字母,不是零)。
比如,可能情形是∗∗oo∗∗∗oooo;
如果同时翻转左边的两个硬币,则变为oooo∗∗∗oooo。
现在小明的问题是,如果已知了初始状态和要达到的目标状态,每次只能同时翻转 相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?
我们约定:把翻动相邻的两个硬币叫作一步操作。
【输入格式】
两行等长的字符串,分别表示初始状态和要达到的目标状态。
每行的长度<1000。
【输出格式】
一个整数,表示最小操作步数。
【样例输入】
【样例输出】
5
解析:
本题没有提到当无法将所有硬币的初始状态转换为目标状态时需要输出什么,因此可笃定题目必有解。
依题意,每次操作将同时翻转两枚硬币。为了方便表示,我们可认为对第i枚硬币的操作会同时翻转第i枚硬币和第i+1枚硬币。题目要求的是用最少的操作将硬币的初始状态 转换为目标状态。那么,如何求这最少的操作次数呢?我们先从每次操作的意义入手。
已知对某枚硬币操作一次,它与它相邻的硬币就会翻转一次。那么如果再对该硬币操作 一次呢?显然,这两枚硬币就会回到初始状态,相当于没有操作。从这里可以得出,对于第i 枚硬币,我们只会操作0次或者1次。
再来思考下,一枚硬币最终的状态会受什么影响?
以第i枚硬币为例。只有当我们对第i枚硬币或第i−1枚硬币操作之后,第i枚硬币的状态才会发生改变。这两种操作对第i枚硬币的影响实际上是相同的,都是令其翻转。因此, 第i枚硬币的最终状态不会受两种操作的先后顺序影响,只会受两种操作的操作次数影响。
推广到所有硬币,可得出结论:操作的先后顺序,对最终结果不会造成影响,如下图所示。
通过上文的分析,我们得到了以下两条重要的信息。
(1)对于第i枚硬币,我们只会操作0次或者1次。
(2)操作的先后顺序对最终结果不会造成影响。
有了这两条重要的信息,我们便可以开始模拟硬币的翻转。
显然,当第i枚硬币的初始状态和目标状态不同时,我们就要将其翻转。将其翻转的方 法可能有以下两种。
(1)对第i枚硬币进行操作。
(2)对第i−1枚硬币进行操作。
两种方法都能令第 i 枚硬币的状态转换为目标状态,但不同的方法对其他硬币的影响可能 是不一样的,我们并不好去预估这两种方法对其他硬币带来的影响。因此,我们最好从第1枚 硬币开始入手。因为第1枚硬币前并没有其他硬币,所以要想改变它的状态,只能对其自身进 行操作。
根据信息1可得如果第1枚硬币的初始状态和目标状态相同,我们就不进行操作;反之, 必须进行操作(操作次数为1),使其状态变为目标状态。
那么,在我们处理完第 1 枚硬币(状态变为目标状态)后,还会对第 1 枚硬币进行操 作吗?
答案是不会的。如果还对第1枚硬币进行操作,则其状态将变得与目标状态不同,状态 不同就又得再对第1枚硬币进行操作,使其状态变为目标状态。这样这两次操作的作用就被 互相抵消了,如下图所示。所以当第1枚硬币的状态变为目标状态后,我们就不会再对第1 枚硬币进行操作了。
因为不会再对第1枚硬币进行操作,所以我们就只要将注意力集中在剩余硬币上。
那剩余硬币要怎么处理呢?同样也是从这当中的第1枚硬币入手。
例如,有5枚硬币,它们的初始状态为∗∗∗∗∗,终止状态为o∗o∗∗。如果使用上述方法, 则硬币的状态变化将如下图所示。
可以发现,我们每次的操作都会满足以下两个特性。
(1)必要性:只有当被操作的硬币状态和目标状态不同时,才进行操作。
(2)独立性:不会影响已处理好(达到目标状态)的硬币。
显然,满足了必要性和独立性的操作即最优操作。每次都使用最优操作,那么总的操作 次数就会是最少的。
参考代码如下【时间复杂度为 O(n)】
#include <bits/stdc++.h>
using namespace std;
signed main() {
int cnt = 0;
string s, t;
cin >> s >> t; // s表示硬币的初始状态,t表示硬币的目标状态
for (int i = 0; i < s.size() - 1; i++) {
if (s[i] != t[i]) { // 如果第i枚硬币的初始状态和目标状态不同,则对第i枚硬币进行操作
// 操作会改变第i枚硬币、第i+1枚硬币的状态
// 由于操作之后第i枚硬币的状态必然会是目标状态,且之后不会再对第i枚硬币进行操作,所以可以省去对第i硬币的实际修改
if (s[i + 1] == '*') s[i + 1] = 'o';
else s[i + 1] = '*';
cnt++;
}
}
cout << cnt << '\n';
return 0;
}
本题也可以换一种角度思考。
由于我们已笃定题目一定有解,所以根据操作的要求(每次翻转两枚硬币)可知初始状 态不为目标状态的硬币个数一定为偶数。
当第i枚硬币的初始状态和目标状态不同时,就必须对其进行操作,使其状态变为目标 状态。由于每次至少要翻转两枚硬币,所以对第i枚硬币的操作会影响与它相邻的硬币(第 i+1枚硬币)的状态,且影响分以下两种情况。
(1)第i+1枚硬币的初始状态不为目标状态→第i+1枚硬币的状态变为目标状态。
(2)第i+1枚硬币的初始状态为目标状态→第i+1枚硬币的状态不再为目标状态。
对于第1种情况,操作完成后第i枚硬币、第i+1枚硬币的状态都达到了目标状态,因此我们无法确定下一枚初始状态不为目标状态的硬币的位置,就要继续寻找,直到找到初始 状态不为目标状态的硬币。
对于第2种情况,操作完成后第i枚硬币的状态会达到目标状态,但第i+1枚硬币的 状态不会,所以我们可以确定下一次要对第i+1枚硬币进行操作。当然如果对第i+1枚硬 币的操作又造成了第2种情况,则需要继续对第i+2枚硬币进行操作。
显然,只有在某次操作出现了第1种情况后,连续的操作才会停止,如下图所示。
由上可得,我们的操作是由硬币的初始状态不为目标状态开始,也是由硬币的初始状态 不为目标状态结束,操作的次数为硬币编号。因此,我们可以成对记录初始状态不为目标状 态的硬币编号,而每一对硬币编号的差值的和就是答案。
参考代码如下 【时间复杂度为O(n)】
#include <bits/stdc++.h>
using namespace std;
signed main() {
int cnt = 0;
string s, t;
cin >> s >> t; // S表示硬币的初始状态,T表示硬币的目标状态
vector<int> vec; // vec用来存储初始状态不等于目标状态的硬币的编号
for (int i = 0; i < s.size(); i++) {
if (s[i] != t[i]) vec.push_back(i);
}
for (int i = 0; i < vec.size() - 1; i += 2) { // 成对枚举
cnt += vec[i + 1] - vec[i]; // 操作次数 = 结束硬币的编号 - 开始硬币的编号
}
cout << cnt << '\n';
return 0;
}
三、乘积最大
【问题描述】
给定N个整数A1,A2,...,AN。
请你从中选出K个数,使其乘积最大。 请你求出最大的乘积,由于乘积可能超出整型范围,你只需输出乘积除以109+9的 余数。
注意,如果X<0,我们定义X除以109+9的余数是−X除以109+9的余数,即:0−((0−x)%109 +9)。
【输入格式】
第一行包含两个整数N,K。 以下N 行每行一个整数Ai。 其中,1⩽K⩽N⩽105,−105 ⩽Ai ⩽105。
【输出格式】
输出一个整数,表示答案。
【样例输入】
【样例输出】
999100009
解析:
方式1:满分做法
不妨设序列中有cnt1个非负数、cnt2 个负数(cnt1 + cnt2 = N)。
如果cnt1 = N,那么我们只要把最大的K 个数取出相乘即可,几乎没有难度。但如果 cnt1 < N,题目的难度就提高了,我们就需要讨论取负数的情况。
根据“负负得正”的性质,为了让乘积的结果最大,我们要尽量成对地取负数。
为什么说是尽量呢? 因为负数的个数可能是奇数。如果是奇数的话,最终的负数可能只 剩下1个,就无法成对地取负数。所以有了负数的情况是较为复杂的,但存在一些特殊情况, 如下图所示。
(1)K =N。所有数都必须选取。
(2)cnt2 = 0。所有数都是非负数,那么取K 个最大的数可令结果最大。
(3)cnt1 = 0。所有数都是负数,那么分以下两种情况。
• 如果K为奇数,则乘积必然为负数,所以取绝对值最小的K个数可令结果最大。
• 如果K为偶数,则乘积必然为正数,所以取绝对值最大的K个数可令结果最大。 对于剩余的情况,序列中一定会包含负数与非负数,且K 一定小于N。
• K < N = cnt1 + cnt2 → K ⩽ cnt1+cnt2−1
• cnt1 > 0,cnt2 > 0
我们可以按照K,cnt1, cnt2 的奇、偶性,将所有情况分解为以下8类。
(1)K 为奇数,cnt1 为奇数,cnt2 为奇数。
(2)K 为奇数,cnt1 为奇数,cnt2 为偶数。
(3)K 为奇数,cnt1 为偶数,cnt2 为奇数。
(4)K 为奇数,cnt1 为偶数,cnt2 为偶数。
(5)K 为偶数,cnt1 为奇数,cnt2 为奇数。
(6)K 为偶数,cnt1 为奇数,cnt2 为偶数。
(7)K 为偶数,cnt1 为偶数,cnt2 为奇数。
(8)K 为偶数,cnt1 为偶数,cnt2 为偶数。
为了保证结果尽可能大,我们要尽量取偶数个负数和若干个非负数。
因为剩余情况满足K⩽cnt1+cnt2−1,所以无论如何,我们都一定可以取偶数个负数、 若干个非负数,使得总个数等于K,即无论属于以上哪一类,都可令乘积的结果大于等于0。
提示如下:
可我们要如何取数才能保证结果在非负数的情况下乘积最大呢?
我们可以采用贪心策略,即每次选择符号相同、乘积最大的两个数,直到取完K 个数, 如下图所示。
但这样可能会面临一个问题,即当K为奇数时,最后只能取一个数。而在此之前所有的 非负数都已经被取完(只剩下负数),我们就只能取负数,导致最终的结果变为负数。
按照贪心策略,若K=5,则取数的步骤如下。
如何解决这个问题呢? 方法很简单,我们只要保证在取最后一个数的时候,至少还剩有 一个非负数可取。
显然,当K ⩽cnt1 时,就不会产生上面的情况。但是当cnt1 < K 时,我们至少要取 K−cnt1 个负数。我们可以先成对地选取负数,直到K ⩽cnt1后,再用上述贪心策略。
假设cnt1=2, cnt2=6, K = 5 则取数的步骤如下。
参考代码如下 【时间复杂度为 O(nlogn)】
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10, mod = 1e9 + 9;
int n, k, cnt1 = 0, cnt2 = 0, res = 1, a[N];
signed main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
if (a[i] >= 0) cnt1++; // 统计非负数的个数
else cnt2++; // 统计负数的个数
}
sort(a + 1, a + 1 + n); // 排序
// 对特殊情况进行处理
if (n == k) {
for (int i = 1; i <= n; i++) {
res *= a[i];
res %= mod;
}
cout << res << '\n';
}
// 对特殊情况进行处理
else if (!cnt2) {
for (int i = n; i >= n - k + 1; i--) {
res *= a[i];
res %= mod;
}
cout << res << '\n';
}
// 对特殊情况进行处理
else if (!cnt1) {
if (k & 1) {
for (int i = n; i >= n - k + 1; i--) {
res = res * a[i] % mod;
}
} else {
for (int i = 1; i <= k; i++) {
res = res * a[i] % mod;
}
}
cout << res << '\n';
}
else {
// 当 cnt1 < k 时,成对取负数,直到 k <= cnt1
int p = 1;
while (k > cnt1) {
res *= (a[p] * a[p + 1]) % mod;
res %= mod;
p += 2;
k -= 2;
}
// 每次可以选择符号相同、乘积最大的两个数,直到取完 K 个数
int p1 = p, p2 = n;
while (k > 1) {
if (a[p1] * a[p1 + 1] >= a[p2] * a[p2 - 1]) {
res *= (a[p1] * a[p1 + 1]) % mod;
res %= mod;
p1 += 2;
} else {
res *= (a[p2] * a[p2 - 1]) % mod;
res %= mod;
p2 -= 2;
}
k -= 2;
}
// 如果 K 为奇数,即最后只能取一个数,那么要选择非负数才能保证乘积最大
if (k) {
res = res * a[p2] % mod;
}
cout << res << '\n';
}
return 0;
}
方式2:能拿40%分数的做法
考虑动态规划。
题目让我们从N个数中选择K个使得乘积最大。
我们可以根据题目的要求,定义dp[i][j], 其含义为从前i个数中选择j个的最大乘积。这样,答案可以用dp[N][K]表示。 接着来推导dp[i][j]的转移方程。
我们从第i个数入手。第i个数有两种状态:选和不选。如果选了,则dp[i][j]的值等价 于从前i−1个数选j−1个数求最大乘积,再乘以第i个数;如果不选,则dp[i][j]的含义等价于前i−1个数选了j个数求最大乘积。
由此便可得出dp[i][j]的转移方程。
不过这个转移方程并不包含所有可能。我们知道一个最大的乘积可能是由两个正数相乘 得到,也可能是由两个负数相乘得到。因此,我们还需要维护一个f[i][j],其含义为从前i个 数中选择j个数求最小乘积。
dp[i][j]也可能是由前i−1个数选择了j−1个数求最小乘积,再乘以第i个数得到。所 以,dp[i][j]的完整转移方程应为
f[i][j]的转移方程与dp[i][j]的类似,就不赘述。
转移的时间复杂度为O(N2)。
提示如下,代码就自己写吧qwq:
方式3:能拿60%分数的做法
在40% 分数的做法中,我们同时维护了从前i个数中选j 个数的最大乘积、最小乘积 (dp[i][j]、f[i][j]),还用上了字符串代替整型来模拟计算过程,这是相当麻烦的。那么,如何优化这些烦琐的步骤呢?
(1)优化既要记录最大乘积,又要记录最小乘积。
回想一下,我们为什么要记录最小乘积呢?
根据负负得正的性质,最大乘积可能由若干个非负数以及偶数个负数相乘得到(由若干个非负数以及奇数个负数构成的最小乘积再乘上一个负数得到)。
那如果只乘上奇数个负数呢?
根据满分做法中的讨论,只有当序列中不存在非负数时,才有可能乘上奇数个负数。对于这种情况,可以特别处理。但对于剩余的情况,最大乘积不会包含奇数个负数。
既然如此,为什么不将负数两两打包在一起,视为一个整体呢?这样,就能保证每次的乘 积结果都是非负数,就不需要记录最小乘积,如下图所示。
可是将两个数打包在一起后,要怎么取数呢?
我们换个角度来思考。假设有一个容量为K的背包,以及若干个物品。每个物品都有自 己的体积和价值,其中一些物品的大小为2(打包在一起的两个负数),一些物品的大小为1 (非负数),那么如何选择物品可以使得背包能够恰好被装满,且所装物品的总价值最大?
这样,题目就被转换为经典的01背包问题。于是dp[i][j]的定义就为装入前i个物品,且 背包的物品体积总和为j可以使价值最大(即乘积最大)。其转移式为dp[i][j]=max(dp[i− 1][j], dp[i − 1][j − w] × v)(w, v 分别表示物品的体积和价值)。
最后的答案为dp[n][k]。
(2)优化使用字符串来代替整型。
回想一下,我们为什么要使用字符串来代替整型进行计算呢?
因为乘积可能超出整型范围,从而导致我们无法进行大小的比较。
但是题目中是让我们通过取模来避免超出整型范围,那为什么不按照题目的提示来呢?
因为题目只要求对最后的乘积结果取模。如果在求解过程中取模,就可能导致大小关系 出现变化,如下页图所示。
那有没有什么办法既能使大小关系不出现变化,又能保证乘积结果不超过整型范围呢?
有的。这里介绍一个小技巧——用对数(如log2)表示正数的大小关系。
•如果X>Y,则log2X>log2Y。
•如果a×b>c×d,则log2(a×b)=log2a+log2b>log2(c×d)=log2c+log2d。
将乘法替换为对数的加法,可以保证数值不超过整型。
dp[i][j]=max(dp[i−1][j],dp[i−1][j−w]+log2v)(w,v分别表示物品的体积和价值)。
不过这样记录的结果是对数值,并不能作为实际的答案。所以我们还得再定义一个 ans[i][j],其含义为装入前i个物品且物品的体积总和为j所能获得的最大价值(即最大乘 积)。当dp[i][j]被更新时,同步更新ans[i][j]即可,参考代码如下。
if (dp[i - 1][j] > dp[i][j]) {
dp[i][j] = dp[i - 1][j];
ans[i][j] = ans[i - 1][j];
}
// ...
if (dp[i - 1][j - w] + log2(v) > dp[i][j]) {
dp[i][j] = dp[i - 1][j - w] + log2(v);
ans[i][j] = ans[i - 1][j - w] * v % mod;
}
能拿60%分数的做法对应的完整代码如下。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e3 + 10, mod = 1e9 + 9;
int a[N], ans[N][N];
double dp[N][N];
signed main() {
int n, k, cnt1 = 0, cnt2 = 0, res = 1;
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + 1 + n); // 将数组排序
for (int i = 1; i <= n; i++) {
if (a[i] < 0) cnt2++;
else cnt1++;
}
// 对特殊情况进行处理
if (k == n) {
for (int i = 1; i <= n; i++) res = res * a[i] % mod;
return cout << res << '\n', 0;
}
// 对特殊情况进行处理
if (cnt2 == n) {
if (k & 1) {
for (int i = n; i >= n - k + 1; i--) res = res * a[i] % mod;
} else {
for (int i = 1; i <= k; i++) res = res * a[i] % mod;
}
return cout << res << '\n', 0;
}
vector<pair<int, int>> vec;
vec.push_back(make_pair(0, 0));
for (int i = 1; i <= n; i++) {
// 将负数两两打包在一起,视为一个物品
if (a[i] < 0 && a[i + 1] < 0) {
vec.push_back(make_pair(a[i] * a[i + 1], 2));
i++;
}
// 将正数单独作为一个物品
else if (a[i] > 0) vec.push_back(make_pair(a[i], 1));
}
memset(dp, -0x3f, sizeof(dp));
dp[0][0] = 0;
ans[0][0] = 1; // 初始化
for (int i = 1; i < vec.size(); i++) {
int v = vec[i].first, w = vec[i].second;
for (int j = 0; j <= k; j++) {
// 状态转移
dp[i][j] = dp[i - 1][j];
ans[i][j] = ans[i - 1][j];
if (j - w >= 0) {
if (dp[i][j] < dp[i - 1][j - w] + log2(v)) {
dp[i][j] = dp[i - 1][j - w] + log2(v);
ans[i][j] = ans[i - 1][j - w] * v % mod;
}
}
}
}
cout << ans[vec.size() - 1][k] << '\n';
return 0;
}
四、皮亚诺曲线距离【难】
【问题描述】
皮亚诺曲线是一条平面内的曲线。
下图给出了皮亚诺曲线的1阶情形,它是从左下角出发,经过一个3×3的方格中 的每一个格子,最终到达右上角的一条曲线。
下图给出了皮亚诺曲线的2阶情形,它是经过一个32×32 的方格中的每一个格子 的一条曲线。它是将1阶曲线的每个方格由1阶曲线替换而成的。
下图给出了皮亚诺曲线的3阶情形,它是经过一个33×33 的方格中的每一个格子 的一条曲线。它是将2阶曲线的每个方格由1阶曲线替换而成的。
皮亚诺曲线总是从左下角开始出发,最终到达右上角。
我们将这些格子放到坐标系中,对于k阶皮亚诺曲线,左下角的坐标是(0,0),右 角坐标是(3k−1,3k−1),右下角坐标是(3k−1,0),左上角坐标是(0,3k−1)。
给定k阶皮亚诺曲线上的两个点的坐标,请问这两个点之间,如果沿着皮亚诺曲线走,距离是多少?
【输入格式】
输入的第一行包含一个正整数k,皮亚诺曲线的阶数。
第二行包含两个整数x1,y1,表示第一个点的坐标。
第三行包含两个整数x2,y2,表示第二个点的坐标。
其中有,0⩽k⩽100,0⩽x1,y1,x2,y2<3k,x1,y1,x2,y2⩽1018次方。数据保证答案不 超过1018次方。
【输出格式】
输出一个整数,表示给定的两个点之间的距离。
【样例输入】
【样例输出】
8
解析:
本题是一道“思维”大题,它极其考验选手的观察力、分析力及判断力,难度颇高。
显然,想要根据观察规律直接求出皮亚诺曲线上任意两个点的坐标几乎不可能。因此,我 们需要对问题进行优化。
对于皮亚诺曲线上两个不同的点(x1,y1)、(x2,y2),它们的位置关系有以下两种。
•从起点(0,0)出发,先走到(x1,y1)再走到(x2,y2),那么从(0,0)走到(x2,y2)就可以分解为
(0,0)→(x1,y1)→(x2,y2)。
•从起点(0,0)出发,先走到(x2,y2)再走到(x1,y1),那么从(0,0)走到(x1,y1)就可以分解为
(0,0)→(x2,y2)→(x1,y1)。
假设(x1,y1)、(x2,y2)到点(0,0)的距离分别为a,b,则它们之间的距离必然为|b−a|, 如下图所示。
于是,求解 (x1,y1) 到 (x2,y2) 的问题就可简化为求点 (x1,y1)、(x2,y2) 到点 (0,0) 的 距离。
如何求解一个点到起点(0,0)的距离呢?
我们接着优化。 根据定义可得,一个n阶的皮亚诺曲线一定可以由9个n−1阶的皮亚诺曲线拼凑构成, 方程如下。
若按照先后顺序将n阶的皮亚诺曲线划分为9个块(九宫格),则每个块都将会是一个 n−1 阶的皮亚诺曲线。因此,我们可以通过缩放将点映射到块上来简化问题。
对于点(x,y),其缩放后坐标会变为,比如在3阶皮亚诺曲线中坐标为(22, 13) 的点缩放后的坐标就为,即 (2,1)。
3 阶皮亚诺曲线在对点缩放后会有以下几种情况。
对于一个块,我们可以为其添加两个新的概念。
(1)块的出口:表示一个块内距离起点(0,0)最近的点。
(2)块的大小:表示从进入块到离开块所要走的步数(一个 n−1 阶的块的大小等于 3n−1 ×3n−1)。
设(x,y) 位于九宫格的第i 个块,则它到起点的距离可以分解为以下两部分。
(1)(x,y) 到第 i 个块的出口的距离。
(2)第i个块的出口到第1个块的出口的距离。
通过观察可以得知第i个块的出口到第1个块的出口的距离等于i−1个块的大小,即 (i −1)×3n−1 ×3n−1。接下来,我们只要能求出 (x,y) 到第 i 个块的出口的距离,就可求出 (x, y) 到起点的距离。
我们知道,第i个块的结构相当于一个n−1阶的皮亚诺曲线。根据已有的分析可知,当 n−1 很大时,我们是很难求出(x,y)到n−1阶皮亚诺曲线的出口的距离的。不过如果n−1 很小,比如n−1=1时,我们就可以通过暴力、模拟,甚至肉眼的观察轻松求出(x,y)到 n−1 阶皮亚诺曲线的出口的距离。那么,有没有办法将n−1以大化小呢?
如果我们将第i个块看作是一个全新的n−1阶的皮亚诺曲线,则求(x,y)到第i个块 的出口的距离就等价于求(x,y)到全新的n−1阶皮亚诺曲线的出口的距离。
如果我们将这个全新的n−1阶皮亚诺曲线划分为新的九宫格,并设(x,y)位于新的九 宫格中的第i′ 个块,那么此时(x,y)距离新的九宫格起点的出口又可以分为两部分。
(1)(x,y) 到新九宫格第 i′ 个块的出口的距离。
(2)新九宫格第i′ 个块的出口到新九宫格第1个块的距离。
从新九宫格第i′ 个块的出口到新九宫格第1个块的出口的距离距离为(i′−1)×3n−2× 3n−2;而求 (x,y) 到第 i′ 个块的出口的距离可以重复上述步骤,将其转换为新的问题,如下图所示。
需要注意的是,n−1阶的九宫格和n阶九宫格的块的顺序(结构)不一定相同。
提示如下:
整个求解通过层层转换的方式将问题逐渐简化为一个与原问题相似,但规模较小的问题 来求解,与递归的性质极其吻合,因此我们可以用递归的方式来完成求解。
首先声明一个递归函数calc(n,x,y,??),用于计算位于n阶皮亚诺曲线中的点(x,y) 距离第1个块的出口的距离,?? 用来表示这个n阶皮亚诺曲线的结构(同阶的皮亚诺曲线的结构不一定相同,比如第1个块与第2个块)。
根据前文,可以分析出以下两种情况。
那么,要怎么表示n阶皮亚诺曲线的结构,又要如何解决结构的变化呢?
我们可以定义一个3×3的二维数组mp[][],并用mp[0][0]、mp[0][1]、mp[0][2]、mp[1][0]、 mp[1][1]、mp[1][2]、mp[2][0]、mp[2][1]、mp[2][2]分别表示对应位置的块的编号,再根据上图 中3阶九宫格每个块变化的规律,对mp进行修改即可。
至此,本题的解题分析完成,具体代码如下:
参考代码如下【时间复杂度为O(9×log31018次方)】
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll p[40];
map<pair<int, int>, int> mp;
ll calc(int n, ll x, ll y, map<pair<int, int>, int> mp) {
ll x_ = x / p[n - 1], y_ = y / p[n - 1]; // 将 (x, y) 缩放得到 (x_, y_)
if (n == 1) return mp[make_pair(x_, y_)] - 1; // 如果 n = 1,返回所在块的编号 - 1
ll ans = (mp[make_pair(x_, y_)] - 1) * p[n - 1] * p[n - 1];
map<pair<int, int>, int> mp1; // mp1 记录 n-1 阶皮亚诺曲线的变化
if ((x_ == 0 && y_ == 1) || (x_ == 2 && y_ == 1)) {
for (int i = 0; i <= 2; i++) {
for (int j = 0; j <= 2; j++) {
mp1[make_pair(2 - i, j)] = mp[make_pair(i, j)];
}
}
} else if ((x_ == 1 && y_ == 2) || (x_ == 1 && y_ == 0)) {
for (int i = 0; i <= 2; i++) {
for (int j = 0; j <= 2; j++) {
mp1[make_pair(i, 2 - j)] = mp[make_pair(i, j)];
}
}
} else if (x_ == 1 && y_ == 1) {
for (int i = 0; i <= 2; i++) {
for (int j = 0; j <= 2; j++) {
mp1[make_pair(2 - i, 2 - j)] = mp[make_pair(i, j)];
}
}
} else {
mp1 = mp;
}
return ans + calc(n - 1, x % p[n - 1], y % p[n - 1], mp1);
}
signed main() {
p[0] = 1;
for (int i = 1; i < 40; i++) p[i] = p[i - 1] * 3;
mp[make_pair(0, 0)] = 1;
mp[make_pair(0, 1)] = 2;
mp[make_pair(0, 2)] = 3;
mp[make_pair(1, 2)] = 4;
mp[make_pair(1, 1)] = 5;
mp[make_pair(1, 0)] = 6;
mp[make_pair(2, 0)] = 7;
mp[make_pair(2, 1)] = 8;
mp[make_pair(2, 2)] = 9;
ll k, x1, y1, x2, y2;
cin >> k >> x1 >> y1 >> x2 >> y2;
// 由于 3^39 > 10^18,所以 39 阶皮亚诺曲线已包含了横、纵坐标在 1~10^18 范围内的所有点
// 所以从 39 阶皮亚诺曲线开始递归即可
ll a = calc(39, x1, y1, mp);
ll b = calc(39, x2, y2, mp);
cout << abs(a - b) << '\n';
return 0;
}
别忘了请点个赞+收藏+关注支持一下博主喵!!!!
关注博主,更多蓝桥杯nice题目静待更新:)