1996. 打乱字母 - 贪心、二分
1996. 打乱字母 - 贪心、二分
题意
给定\(n\)个字符串,每个字符串的排序可以任意调整,求每个字符串在这种情况下的最大、最小字典序排名。
模型转换
首先我们把字典序当作一个数值,数值越小,排名越靠前。由于可以调整,每个字符串都对应了一个数值变化范围。值得注意的是,这个变化范围本身就有最大值和最小值。
这样,我们就会有\(n\)个这样的范围,虽然每个数都不确定的,但根据我们要求的量——例如最高位置,我们可以很贪心的想到,若要求第\(i\)个字符串的最高位置,不是只要让它取范围里的最小值,让其它字符串取范围里的最大值就好了吗?
感觉非常正确,但还需要进一步的证明。
贪心证明
对于第\(i\)个字符的最高位置而言:
欲证贪心解是最优解,只需证:
- \(贪心解 \ge 最优解\)
- \(贪心解 \le 最优解\)
对于第一项,我们希望找出最高位置,则\(一般解 \ge 最优解\),而贪心解是一般解的其中一个,故\(贪心解 \ge 最优解\).
对于第二项,我们常用反证法以及调整法,这里使用反证法证明它不成立。
以后看着这张图回忆吧 == ,有点懒 =_=
最小位置同理。
证毕之后我们就可以考虑实现的问题了。
代码实现
要实现的功能有:
- 让一个字符串\(i\)取范围里的最小值/最大值
- 让其它字符串取范围里的最大值/最小值
- 查询经过上述变化后字符串\(i\)的排名
康康数据范围:\(1 \le N \le 50000\),这就要求我们使用$O(n\log n) $以内的算法。
第一项,可以对一个字符串进行排序实现。
第二项,可以在输入的时候额外开两个数组,记录每一项的最大值/最小值。
第三项,根据范围的提示,由于要查询每一个数,所以平均一个数的复杂度是\(O(\log n)\)的,这就提示我们使用二分查找。
这样下来雏形就差不多了,剩下的就是一些细节问题。
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 50010;
string a[N], big[N], small[N];
int n;
int main(void){
cin >> n;
for (int i = 1; i <= n; i ++ ){
cin >> a[i];
big[i] = small[i] = a[i];
sort(big[i].begin(), big[i].end(), greater<char>() ); // 降序排序,字典序大
sort(small[i].begin(), small[i].end());
}
sort(big + 1, big + n + 1);
sort(small + 1, small + n + 1);
for (int i = 1; i <= n; i ++ ){
sort(a[i].begin(), a[i].end()); // 升序,字典序小,排名靠前
int l = 1, r = n; // 此处要在big数组中二分出第一个小于 x 的位置,且靠前,找出 x 的最前排名
while (l < r){
int mid = l + r >> 1;
if (big[mid] >= a[i]) r = mid; //这种情况mid不需要加一
else l = mid + 1;
}
cout << l << ' ';
reverse(a[i].begin(), a[i].end()); // 降序,字典序大,排名靠后
l = 1, r = n; // 此处要在small数组中二分出第一个大于 x 的位置,且靠后,找出 x 的最后排名
while (l < r){
int mid = l + r + 1>> 1;
if (small[mid] <= a[i]) l = mid;
else r = mid - 1;
}
cout << l << endl;
}
return 0;
}