标签: 算法
初始问题:幼儿园小孩排成一行,但是男孩和女孩相邻会冲突,现在你是老师,每次只能调换相邻两个小孩的位置,要使男女冲突最少,至少要调换多少次?
输入情况: 用形如'BBBGGBB'的一个字符串表示队伍,'B'表示男孩,'G'表示女孩。
输入情况:最少的调换次数。
目标是使男女冲突最少,可以理解为把男孩和女孩分开两边,左边全站男孩,右边全站女孩(或者左边全站女孩,右边全站男孩),那就只有中间的一个男孩一个女孩相邻了,显然这样冲突最少。
例如:'BBGBG' 变成 'BBBGG' 或者 ' GGBBB'
思路一:每次只能调换相邻两个小孩的位置,这个问题类似与冒泡排序,即每次调换两个冲突的小孩的位置,那么每次循环下来,就把一个小孩调换(移动)到最左边或者最右边,两重循环即可把所有孩子调换好,需要调换时使用计数器加一记录即可。
// 从右往左冒泡
// @s 小孩队列
// @move 该移动的小孩,'B'为男孩,'G'为女孩
public static int bubbleCount(char[] s, char move) {
int count = 0;
for (int i = 0; i < s.length - 1; i++) {
for (int j = s.length - 1; j > i; j--) {
// 若相邻两小孩性别不同,需要调换
if (s[j] == move && s[j] != s[j - 1]) {
swap(s[j], s[j - 1]);
count++;
}
}
}
return count;
}
这种方法使用了冒泡排序算法,复杂度为\(O(n^2)\),能算出移动男孩或者移动女孩的次数,且修改了原队列的值。
要注意,只移动男孩或只移动女孩,有时并不是最优解。例如队列'BBBBBGB'时,如果移动男孩(这里统一指往左边移动),只需要将最右边的男孩往左移动1次。但移动的是女孩时,则需要调换5次。即实际上,调换男孩是调换次数最少的换法。所以要考虑这两种情况的最小值,即min(bubbleCount(s, 'B'), bubbleCount(s, 'G')
。
思路二:不改变原队列的值,只记录某个小孩右边的异性个数,该个数即该小孩需要调换(移动)的次数,问题是求得所有小孩移动次数的总和。
// 从左往右计数
// @s 小孩队列
// @move 该移动的小孩,'B'为男孩,'G'为女孩
public static int count(char[] s, char move) {
int count = 0;
for (int i = 0; i < s.length; i++) {
if (s[i] == move)
for (int j = i + 1; j < s.length; j++)
if (s[j] != move) count++;
return count;
}
}
这个方法循环计算了每个待移动的小孩右边的异性个数,复杂度为\(O(n^2)\)。有没有更加好的方法?我们观察到,从左到右计数时,异性的个数其实是重复计数的,所以我们可以使用一个变量存储起来,这时只需要循环一次即可解决问题,复杂度为\(O(n)\)。
// 从左往右计数
// @s 小孩队列
// @move 该移动的小孩,'B'为男孩,'G'为女孩
public static int count(char[] s, char move) {
int count = 0;
// 不需移动的小孩个数
int notMoveNum = 0;
for (int i = 0; i < s.length; i++) {
if (s[i] == move)
count += notMoveNum;
else
notMoveNum++;
}
return count;
}
实际上,这个方法记录的是某个小孩左边的异性个数。要记录右边的异性个数,只需要把循环条件反过来即可。
和上面冒泡算法的情况相同,最后要从移动男孩还是移动女孩两种情况中,选出移动次数最少的一种,即min(count(s, 'B'), count(s, 'G')
。