秋季每日一题 2023
5198. 整理书籍
书架上有若干本书排成一排。每本书要么是大型书(用 L 表示),要么是中型书(用 M 表示),要么是小型书(用 S 表示)。
我们希望所有书能够从大到小有序排列,也就是说,所有大型书都在左侧,所有中型书都在中间,所有小型书都在右侧。为此,你可以进行任意次交换操作,每次可以任选两本书并交换它们的位置。请你计算,为了让所有书按要求有序排列,至少需要进行多少次交换操作。
输入格式
共一行,包含一个由 L、M、S 构成的字符串,表示初始时每个位置上的书的类型。
输出格式
一个整数,表示所需要的最少交换操作次数。
数据范围
输入字符串的长度范围 [1,5×105]。
输入样例1:
LMMMS
输出样例1:
0
样例1解释
无需任何操作,初始排列已经符合要求。
输入样例2:
LLSLM
输出样例2:
2
样例2解释
一种最佳方案如下:
- 第一步,交换 S 和 M,序列变为 LLMLS。
- 第二步,交换 M 和它右边的 L,序列变为 LLLMS。
考察算法:环图,贪心
/*本题采用环图的概念来做出理论解释;
根据输入可以得出序列的初始排列,同时根据题意可以得出序列的目标排列
由某位置i的 初始点指向目标序列点 可构成一个环图; 为了达到目标序列,也即该图需要改造成这N个点的自环图
*/
#include<bits/stdc++.h>
using namespace std;
const int N = 5*100000 + 5;
int main()
{
string str;
cin>>str;
int n = str.length(); //n可以理解为环图中顶点的个数
int a[N] = {0};
int s[3] = {0}; //用来记录三个数字各有多少个,降低时间复杂度
for(int i = 0;i < n;i ++) { //将初始的字母序列转化为数字序列a[]
char ch = str[i];
if(ch == 'L') a[i] = 0;
else if(ch == 'M') a[i] = 1;
else a[i] = 2;
s[a[i]] ++;
}
int b[N] = {0}; //用b[] 来记录目标序列
for(int i = 0,k = 0;i < 3;i ++){
for(int j = 0;j < s[i];j ++,k ++){
b[k] = i;
}
}
int e[3][3] = {0};
for(int i = 0;i < n;i ++){
e[a[i]][b[i]] ++; //用矩阵记录下环图,e[i][j] 中存储的是从i节点到j节点的边数
}
//接下来计算当前环图中最多可以输出多少环(由于每进行一次交换操作,最多可以增加一个环,所以交换次数 >= n - m)
//若要交换次数最少,则需要数出来的环数是当前图的最大值(先数所有小环再数大环)==>复习图论知识
int m = 0; //用来记录当前环图输出的环数
for(int i = 0;i < 3;i ++){
for(int j = i;j < 3;j ++){
int cnt = min(e[i][j],e[j][i]); //两点之间的最大环数取决于两点间的最小边数
m += cnt;
e[i][j] -= cnt;
e[j][i] -= cnt;
}
}
//小环计算完毕,现在计算大环(连接所有点的大环)
m += e[0][1] + e[1][0]; //正向大环和逆向大环不可能同时存在(但不确定保留的是正向环还是逆向环),此时必定可以拆解为N个节点两两构成的N个小环
cout<<n - m;
return 0;
}
5221. 交换座位 原题链接
圆桌上有 N个座位,顺时针依次编号为 1∼N。每个座位上都坐着一个人,每个人都属于以下三个组之一:A 组、B 组、C组。如果一个组内的所有成员都连续相邻的坐在一起,该组就会感到满意。我们希望通过交换操作使得所有组都感到满意。每次交换操作可以任选两人交换座位。请你计算,至少需要进行多少次交换操作才能使得所有组都感到满意。
注意:由于是圆桌,所以座位 1和座位 N是相邻的。
输入格式:
共一行,包含一个长度为 N的由 A,B,C组成的字符串,其中的第 i个字符表示初始时第 i个座位上的人的组别。
输出格式:
一个整数,表示所需进行的最少交换操作次数。
数据范围:
1≤N≤106
输入样例:
BABCBCACCA
输出样例:
2
样例解释
一种最佳方案为:
- 首先,令座位 1和座位 7上的人交换座位,所有人座次变为 AABCBCBCCA。
- 然后,令座位 4和座位 7上的人交换座位,所有人座次变为 AABBBCCCCA。
考察算法:环图,贪心,破环成链
在上题的基础上,本题的核心考察和整理书籍一致,将相同的编号移到相邻位置。从题设条件来说,本题从上题的链式变为了环状,需要破环成链
A,B,C 相邻在链式上做全排,有6种不同的情况:ABC, ACB, BAC, BCA ,CAB, CBA
在环上而言只分为两类:ABC(BCA,CAB)和 ACB(CBA,BAC)两种
//与整理书籍不同的是,对于接收到的初始序列,并不能构成环图,需要先确定在环图中的切点,破环成链,再用
//整理书籍中的方法
#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 2000010;
int n;
char str[N];
int s[3][N]; //一个前缀和数组,用来记录某个字母在区间[0,N]上出现的次数
int cnt[3]; //记录每一个字母出现的次数
//用来计算在区间U上,出现了多少个数字x
int get(PII u, int x)
{
return s[x][u.second - 1] - s[x][u.first - 1];
}
//统计在以下标u作为首发位置 长度为N的序列,最少需要替换多少次可以符合题意
int work(int u,int a,int b,int c)
{
int x = cnt[a], y = cnt[b], z = cnt[c]; //计算三个区间长度
PII A(u, u + x), B(u + x,u + x + y), C(u + x + y, u + n); //结合首发位和区间长度划分好三个区间,区间A(B/C)应该全为a(b/c)
//注意这三个区间都是左开右闭的区间
int ans = get(A,a) + get(B,b) + get(C,c); //先计算自环
//相当于计算两两点形成的小环(一共三个点)
ans += min(get(A,b), get(B,a));
ans += min(get(A,c), get(C,a));
ans += min(get(B,c), get(C,b));
ans += get(A, b) + get(B, a) - 2 * min(get(A, b), get(B, a));
//相当于执行整理书籍的e[i][j] -= cnt; e[j][i] -= cnt; 步骤
return n - ans;
}
int main()
{
scanf("%s", str + 1);
n = strlen(str + 1); //为了前缀和统计方便,从下标1开始读入数据
memcpy(str + 1 + n, str + 1, n); //是个环形,为了判断更加便捷,直接copy一份放在末尾
//进行初始化
for(int i = 1;i <= 2 * n;i ++){
//将字母转化为数字
int t = 0;
if(str[i] == 'B') t = 1;
else if(str[i] == 'C') t = 2;
//注意这里前缀和的使用
for(int j = 0;j < 3;j ++){
s[j][i] = s[j][i - 1];
}
if(i <= n) cnt[t] ++; //统计各个字母出现的次数
s[t][i] ++;
}
int ans = n;
for (int i = 1; i <= n; i ++ ){
ans = min(ans,work(i, 0,1, 2));
ans = min(ans,work(i, 0,2, 1));
}
cout<<ans;
return 0;
}