题目描述
n 个学生排成一排,学生编号分别是 1 到 n,n 为 3 的整倍数。
老师随机抽签决定将所有学生分成 m 个 3 人的小组(n == 3 * m) ,
为了便于同组学生交流,老师决定将小组成员安排到一起,也就是同组成员彼此相连,同组任意两个成员之间无其它组的成员。
因此老师决定调整队伍,老师每次可以调整任何一名学生到队伍的任意位置,计为调整了一次, 请计算最少调整多少次可以达到目标。
注意:对于小组之间没有顺序要求,同组学生之间没有顺序要求。
输入描述
第一行输入初始排队顺序序列
第二行输入分组排队顺序序列
输出描述
最少调整多少次数
用例
输入 4 2 8 5 3 6 1 9 7
6 3 1 2 4 8 7 9 5
输出 1
说明
分组分别为:6,3,1一组,2,4,8一组,7,9,5一组
初始排队顺序中,只要将5移动到1后面,变为:
4 2 8 3 6 1 5 9 7
即可满足分组排队顺序要求。
因此至少需要调整1次站位。
输入 8 9 7 5 6 3 2 1 4
7 8 9 4 2 1 3 5 6
输出 0
说明 无
输入 7 9 8 5 6 4 2 1 3
7 8 9 4 2 1 3 5 6
输出 1
策略:从前向后遍历分组数组(所有数字替换成其分组的组数)
设 f(n)数组是小朋友数字全部映射成其组号的数组
for(int i =0;i<n;i++)
1)如果当前数字是连续3个,那么 i+=3;
2)如果当前的数字是连续两个,找到后面的这个数字移动到这两个连续数字的前面,并从数组中删除,count++,i+=3;
3)如果当前的数字是单独1个,那么查看后面的两个这样的数字时候是否是连续的,
A)如果是,那么直接把当前数字移动到后面两个连续数字的前面并删除,count++
B)如果不是,那么把后面两个数字移动到i的前面,count+=2,i+=2,并删除后面移动的这两个数
进入下一次迭代。
粗略的猜想证明:
PS:显然 每一次迭代i前面的数字都是已经分好组的
想要调整次数最少,也就等价于不动的数字最多,3个数字连续相同不动,2个数字连续相同也不动,当3个相同的数字都是离散的,那么把后面的两个数字移动到最前那个数字的最前面,可以尽量使其他可能因为当前数字分散的数字合并。
2024.6.19号更新
#include <bits/stdc++.h>
using namespace std;
/*
建立双端队列,从两头开始遍历(双端队列有两个端口:首端口,末端口)
观察[121122] 其实只需要先合并末端元素,只需要做一次调整即可完成合并,
所以我们采用双端队列进行策略调整
1:(贪心)若两头中至少有一个连续三个相同数,那么直接删除连续三个相同数,调整次数加0
2:若两头中一个连续三个相同数都没有没有,那么若两头中含有两个连续相同的数,那么删除两个连续相同的这个数
并且 在对队列中删除剩下一个这个数(相当于把队列中删除剩下一个这个数移动到端前和端前的两个连续相同的
这个数合并后删除),调整次数加1
总结:这里也用贪心策略,我给出的解释是 队列两头端口的元素,他们如果是两个连续相同的元素,那么就贪心地
选择移动另外一个合并到端口只需要一次调整,这不仅使得端口处的元素合并了,也改变了队列内部的元素
分裂的情况,增加了别的元素合并的可能性。所以我们关于端口连续相同两个元素的总体决策思想就是移动其
在队列中的另外一个元素到端口处合并。
那么如果首端和末端都是如此,有没有优先那一端合并的区别呢?
不失一般性,我们只需要思考有没有一种可能先合并右端口元素会比先合并左端口元素最后的代价少1的情况
假设[11**2**1**22] 如果我们先花一次代价先合并2 得到[1****11**222]
如果我们先花一次代价先合并1,得到[111**2****22]
那么问题就变成了合并完成[1****11**]的代价是否可能比合并[**2****22]的代价更小
其实先合并2再合并1 或者 先合并1再合并2,他们对队列内其他元素分裂情况的影响都是相同的,所以不用在意
合并顺序的问题。
总结:以上两种情况直接贪心思想就可以解释:
1)总之要想合并元素,尽量使得移动次数减小,那么对于连续相同个数n(n>=2)的首末端元素来说,
一定是移动队列内部的另3-n个节点是最省的
2) 优先考虑首末端元素的原因是这样可以有效缓解内部其他元素的分裂情况!
3:若两端头的元素连续相同个数都是1,
{
(3.1)如果 两端的节点的另外两个元素在队列中都是离散的,我们直接暴力递归,尝试首端先合并,再尝试末端合并
例如:[101201220]
1)如果我们 -花2次代价合并0-->[112122000]-花1次代价合并1-->[111222000],
总共花费3次代价
2)如果我们 -花2次代价合并1-->[111020220]-花2次代价合并0-->[111222000],
总共花费4次代价
总结:所以说遇到这种情况,直接暴力递归都尝试一下
(3.2)两端只有一端(记作x端,x属于{首,末})的节点的另外两个元素在队列中是严格离散的,
比如[311132232] 中首端的3再队列中是严格离散的,但2不是
那么我们优先合并 x端
直接把这个数在队列中的另外两个数移动到这个数的前面,
合并,调整次数加2
例如:[311132232] 我们优先合并3 而不是2,因为3的另外两个数在队列中是离散的,
但2的另外两个数在队列中是连续的。
这里给的解释是 :即便把2移动到队列中连续的两个2中(即调整成311132223)
也改变不了将3合并至少还需要2次调整的事实
但是把3优先合并变成333111222,却意外使得2也合并了!
这也满足了尽量使已经内部连续的2保持不动,移动那些完全离散的端口元素
(上面的操作是将队列内部的两个3移动到首端的3前面合并,需要花费两次代价)
总结:以上操作,将2移动到内部不会改变队列中其他元素的分裂情况,
少了一个其他元素意外合并的可能性,所以贪心地选择合并3是合理的,
它改变了其他位置的元素
的合并情况
(3.3)那么如果这两个端头元素 在队列中的都是 另外两个与之id相同数都是连续的,
那么直接把这两端的任意一端移动到其另外两个数的相邻位置,即与在队列中的另外两个数合并。
调整次数加1
例如:[132233112]-移动首端1->[322331112]-移动首端3->[223331112]-移动末端2->[222333111]
总结:这样的操作并不影响队列中的其他元素的分裂情况,
所以可以都把两端的元素都按以上操作都合并到内部
}
*/
struct Node{
int sequentNum;//连续相同的个数
int id; //当前节点的编号
Node(int id1,int sequentNum1){
id = id1;
sequentNum = sequentNum1;
}
Node(){
}
};
void SplitStringToVec(string& s,vector<int>& vec,char gap){
s += gap;
size_t found = s.find(gap);
while(found != string::npos){
vec.push_back(stoi(s.substr(0,found)));
s = s.substr(found+1);
found = s.find(gap);
}
return ;
}
void MergeUpdateDeque(deque<Node> &dq){
deque<Node> tempDq(dq.begin(),dq.end());
dq.clear();
while(!tempDq.empty()){
Node& node = tempDq.front();
if(dq.empty() || dq.back().id != node.id){
dq.push_back(node);
}else{
dq.back().sequentNum++;
}
tempDq.pop_front();
}
}
// (backword等于true时逆向检查,backword等于false时正向检查)
// 检查dq队列中端元素的另外两个元素是否是连续的
// return true-分离的,false-连续,
// 出参:
// 当返回true时,(分离时,另外两个元素第一个遍历到的节点指针分别存方在it1)
// 当返回false时,(连续时候,包含另外两个元素的唯一节点指针存放在it1)
bool IsHaveTwoSepartate(deque<Node> &dq,bool backword,
deque<Node>::iterator& it1){
bool separateFlag = false; //和端元素相同的另外两个元素是否是连续的 true-连续,false-离散
deque<Node>::iterator it;
Node curNode;
//先检查端元素在队列中的另两个元素是否是连续的
if(!backword){
// 从前向后遍历
it = dq.begin();
curNode = *it;
it++;
}else{
// 从后向前遍历
it = dq.end();
it--;
curNode = *it;
it--;
}
int size = dq.size() -1;
while(size>0){
if(it->id != curNode.id){
}else{
if(it->sequentNum == 2){
separateFlag = false;
// 另两个数字连续
it1 = it;
break;
}else if(it->sequentNum == 1){
separateFlag = true;
it1 = it;
break;
}
}
if(!backword){
it++;
}else{
it--;
}
size--;
}
return separateFlag;
}
void EraseDequeElement(deque<Node> & dq,int id){
f:
for(auto it = dq.begin();it!=dq.end();it++){
if(it->id == id){
dq.erase(it);
goto f;
}
}
}
int dfs(deque<Node>& dq){
int ret = 0;
if(!dq.empty()){
Node first = dq.front(); //取出第一个
if(first.sequentNum == 3){
dq.pop_front();
return dfs(dq);
}else if(first.sequentNum == 2){
dq.pop_front();
// 删除队列中的另一个
for(auto it = dq.begin();it!=dq.end();it++){
if(it->id != first.id){
continue;
}else{
dq.erase(it);
MergeUpdateDeque(dq); // 把可能因删除节点而合并的节点合并
ret++;
break;
}
}
ret += dfs(dq);
return ret;
}else{
// 121122 其实只需要做一次调整即可,所以需要双端队列调整
Node last = dq.back(); //找到最后一个
if(last.sequentNum == 3){
dq.pop_back();
return dfs(dq);
}else if(last.sequentNum == 2){
dq.pop_back();
for(auto it = dq.begin();it!=dq.end();it++){
if(it->id != last.id){
continue;
}else{
ret++;
dq.erase(it);
MergeUpdateDeque(dq); // 把可能因删除节点而合并的节点合并
break;
}
}
ret += dfs(dq);
return ret;
}else{
// 两端头的数本身都是仅仅连续1个(sequentNum == 1)
// 那么找队列内部(除了自己队列内部的其他与端头数id相同的元素) 是连续还是离散的
// 这里的策略是把三个数全部都离散的数移动到端头合并
// 例如:311132232 我们优先合并3 而不是2
// 这里给的解释是 :即便把2移动到队列中连续的两个2中(即调整成311132223)
// 也改变不了将3合并至少还需要2次调整的事实
// 但是把3优先合并编程333111222,却使得2也合并了!
//(将端头的数字移至内部和队列内部的两个连续的数合并合并)
//寻找是否存在一个首端或者尾端,它的另外两个元素在队列中是否是离散的
deque<Node>::iterator firstTwoContinueNode1;// 记录队列内部和首端id相同的节点位置
deque<Node>::iterator lastTwoContinueNode1;// 记录队列内部和末端id相同的节点位置
bool firstSeparateFlag = IsHaveTwoSepartate(dq,false,firstTwoContinueNode1);
bool lastSeparateFlag = IsHaveTwoSepartate(dq,true,lastTwoContinueNode1);
if(firstSeparateFlag && lastSeparateFlag){
// 选择删除合并端首
if(last.id!=first.id){
deque<Node> dq1(dq.begin(),dq.end());
dq1.pop_front();
EraseDequeElement(dq1,first.id);
MergeUpdateDeque(dq1);
int cnt = dfs(dq1);
dq.pop_back();
EraseDequeElement(dq,last.id);
MergeUpdateDeque(dq);
//选择删除末端
cnt = min(cnt,dfs(dq));
ret += cnt;
ret += 2;
}else{
dq.pop_back();
EraseDequeElement(dq,last.id);
MergeUpdateDeque(dq);
ret += dfs(dq);
ret += 2;
}
return ret;
}else if(firstSeparateFlag){
dq.pop_front();
EraseDequeElement(dq,first.id);
MergeUpdateDeque(dq);
ret += dfs(dq);
ret+=2;
return ret;
}else if(lastSeparateFlag){
dq.pop_back();
EraseDequeElement(dq,last.id);
MergeUpdateDeque(dq);
//选择删除末端
ret += dfs(dq);
ret += 2;
return ret;
}else{
// 末端元素和首端元素,他们的另外两个元素在队列中都是连续的
firstTwoContinueNode1->sequentNum++;
lastTwoContinueNode1->sequentNum++;
dq.pop_front();
dq.pop_back();
// 无需更新队列,无需调用MergeUpdateDeque
ret+= dfs(dq);
ret+=2;
return ret;
}
}
}
}
return ret;
}
int main() {
deque<Node> dq;
vector<int> nums;
vector<int> ids;
string s;
getline(cin,s);
SplitStringToVec(s,nums,' ');
getline(cin,s);
SplitStringToVec(s,ids,' ');
map<int ,int > mp_numToid;
map<int,Node*> sameTwoFlag;
for(int i = 0;i<ids.size();i++){
mp_numToid[ids[i]] = i/3;
}
for(int i = 0;i<nums.size();i++){
if(dq.empty() || dq.back().id != mp_numToid[nums[i]]){
dq.push_back(Node(mp_numToid[nums[i]],1));
}else{
dq.back().sequentNum++;
}
}
//-------------
// for(auto it = dq.begin();it!=dq.end();it++){
// cout<<it->id<<',';
// }
// cout<<endl;
//--------------
cout<<dfs(dq);
}