[OpenJudge 4266] 排序的代价〔分治、贪心〕

题目链接:OpenJudge - 4:排序的代价

题目

总时间限制: 10000ms 单个测试点时间限制: 5000ms 内存限制: 65536kB
描述
现有一排装满货物的箱子,重量各不相同(都已标示在箱子上),为了进行后面的工作,需要将这些箱子按轻重有序放置,但只有一名工作人员来完成这项工作,由于空间有限,他只能通过不断交换两个箱子(可不相邻)的位置的方式来实现箱子的排序。他知道这样一定可以完成任务,但搬箱子很累,所以他希望找到一种最省力的方式来完成工作,假设一次交换两个箱子的代价为这两个箱子的重量之和,那么这项工作的总代价为此过程中所有“交换”的代价之和,请帮助他计算排列这些箱子的最小代价。

输入
输入包含多个数据实例,每个实例独占一行,每行数据是以空格分隔的非负整数,其中第一个整数表示箱子的个数(n),接下来为n个不同的正整数,分别表示n个箱子的重量,其顺序表示了箱子的初始顺序;当n=0时表示输入结束。
输出
对每个有效数据实例(n!=0)都输出一个整数,独占一行,表示该实例中排序箱子的最小代价。
样例输入

3 3 2 1
4 8 1 2 4
5 1 8 9 7 6
6 8 4 5 3 2 7
0

样例输出

4
17
41
34

提示
示例解释:
共有4个有效数据实例:
第一个数据实例为3 2 1,通过交换重为3和1的箱子即可,总代价为3+1=4;
第二个数据实例为8 1 2 4,代价最小的交换过程为
1↔2,1↔4,1↔8,总代价为(1+2)+(1+4)+(1+8) = 17;
第三个数据实例为1 8 9 7 6,代价最小的交换过程为
1↔6,1↔9,1↔7,1↔8,1↔6,总代价为(1+6)+(1+9)+(1+7)+(1+8)+(1+6)=41;
第四个数据实例为8 4 5 3 2 7,代价最小的交换过程为
3↔5,3↔4,2↔7,2↔8,总代价为(3+5)+(3+4)+(2+7)+(2+8)=34。

请注意边界条件和IO,n可能很大。




(注:题干没有给n的数量级,洛谷上是10^5.)


思路

这题好,锻炼思维.
首先感觉不好下手. 做过一个形似的题,问的是:

问题 1:求为了给一个n元序列排序所需最少的交换两个相邻元素的次数.

那里分析可以发现答案恰等于逆序数,构造过程如下:先将最小的元素m通过逐次交换移到它应在的位置targ=0,移动次数等于targ和原始位置pos的差,即等于问题“有几个元素在m左边且大于m”的答案;对第k小的元素重复即可. 但这题不要求相邻,而且每次交换都有个权值,跟逆序对应该没什么关系.


那么很自然就要问如下的问题:

问题 2:求为了给一个n元序列排序所需最少的交换两个任意元素的次数.

问题 2 的交换是完全自由的,这增加了难度. 显然排序的规则这时也不重要,我们只想把原先的n个数置换到某种次序上.
置换. 以前写着玩,用程序求将任何一个置换迭代进行的结果以及周期,发现置换经常不像一眼看上去那么成整体:随意写一个置换,它很容易分成几个小块,每一块很快就进入循环. 比如置换1,2,3,4,5,6,7,8,9,10→2,3,1,4,5,7,6,10,8,9中,8,9,10和1,2,3会产生两个周期为3的循环,6,7产生一个周期为2的循环,而4和5位置根本没变、是周期为1的自循环.

事实 1:有些数,或者说有些位置上的数是成环的.

这样问题 2 节省交换次数的思路就有了一个:只换每个环内的数,不换环间数. 因为集合\{环包含的位置\}=\{环包含的数所应在的目标位置\}.
在继续之前,需要定义什么是环. 不难想到一种刻画环的方式:设每个位置i上当前的数应当在的位置是targ(i),那么想象从i点跳到targ(i)点,再跳到targ(targ(i))点......这样最终会跳回i点,此时跳跃停止,形成的是一个环.
于是我们面对的就不是一个数组而是一张图了. 图有n个顶点,对应数组的n个位置,每个点i的入度和出度都是1,出边指向的点next(i)就是targ(i).

这种描述下,交换两个位置i,j上的数意味着什么?可以认为是在交换出边的终点next(i),next(j). 想象一下,这显然会产生如下效果:

事实/操作 2:交换环上i,j两处的箭头终点后,环被分为两个子环.

即每次环内交换会使环数严格增加1. 没有其它操作可以加得更多. 由于目标是把所有点都变成自环,使环数达到n,问题 2 的答案就应该是n-c,其中n为数组大小,c为环数.


接下来考虑原始问题:

问题 3:称一次交换的代价为所交换的二数之和. 允许任意交换元素. 求为了给一个n元序列排序所需最小的交换总代价.

问题 3 应该与问题 2 有紧密的关系了,并且要分析交换的具体步骤.
首先,尽量做环内交换的思路应该大致上不变,所以我们先分析环数最少,即为1的情形.
怎么把一个大小为size的环用尽可能小的代价拆成size个自环呢?考虑贪心. 是每次找到环内的两个最小权值点,执行操作 2 拆成两个环,各自再继续吗?
一开始,我也是这样猜测的,但第二组样例8\space1\space2\space4的做法是轮换使用最小元1,直接否定了这个策略. 那么轮换使用最小元实质上是什么?先看操作 2 在交换邻点时的特例.

事实/操作 2-1:交换环上i,next(i)两处的箭头终点后,环被分为两个子环,其中一个是自环next(i)→next(i).

即交换两个邻点相当于把第二个点从环上拆下来. 看来我们只用把最小元min与下个邻点挨个交换,就能解决一个环的情况了. 数学上也容易证明这样的代价最小(如果只有这个环). 若具体算出这个代价,它是min×(size-1)+\Sigma,其中\Sigma是环中去掉最小元的点权值和. 我们称这是这个环的normal_cost值.

若有多个环,自然打算对每个环都这样做. 然而如此是无法解释第三组样例1\space8\space9\space7\space6的操作的,该做法强行把1拉进了后面的四元环8\space9\space7\space6中,最后又将其解放出来. 由于后四个数太大,最后的代价还真的更小. 拉进一个环具体是说,

事实/操作 3:交换环C内一点i与某个成自环点o,将会使o=next(i),next(i)=next(o),从而将o纳入环C.

因此,对于每个环的处理我们还有一种备选方案:先把全局最小元与环内最小元交换(假定全局最小元及其所在的环已经处理完,是自环),然后对新的大小为size+1的环轮换用全局最小元. 称这样做的代价为环的alternative_cost值,其值为opt×size+\Sigma+min+(opt+min),其中opt为全局最小.

只需算出每个环的normal_cost、alternative_cost的较小者并求和,就可以得出总最小代价了.

确定每个点的next()(即该处值在排序后的序数)需要对序列排序,复杂度O(n \log n);DFS 找到每个环并求出两个代价值需要O(n)的复杂度. 算法总时间复杂度O(n\log n).

代码:

#include <bits/stdc++.h>

#define INF 1e9
#define IOS_SPEED std::ios::sync_with_stdio(false)

using std::cin;
using std::cout;
using std::vector;
using std::unordered_map;
using std::deque;
using std::sort;
using std::min;

typedef long long lli;

class cycle{ // 单个环
    public:
        vector<int> contn; // 环内点的位置/下标
        int min_val; lli sum; // 环内最小权值及权值和
        static int min_min_val; // 全局最小权值
        ~cycle(){contn.clear();}
        lli normal_cost(){return sum+(contn.size()-2)*min_val;}
        lli alternative_cost(){return sum+(contn.size()+1)*min_min_val+min_val;}
        lli actual_cost(){return min(normal_cost(), alternative_cost());} // 两种代价的较小者
};

int cycle::min_min_val = INF;

int size; // 数组大小
vector<int> val, sorted; // val: 原始数组; sorted: 排序后的数组
unordered_map<int, int> order; // order[x]: 值 x 应该出现在的位置, 本题中不会冲突
vector<cycle> cycles; // 所有环
deque<bool> visited; // DFS 寻环防止无限递归的记录数组

void build_cycle(int pos){ // 建立单个环
    if(visited[pos]) return;
    visited[pos] = true;
    cycles.back().contn.push_back(pos);
    cycles.back().min_val = min(cycles.back().min_val, val[pos]);
    cycles.back().sum += val[pos];
    build_cycle(order[val[pos]]);
}

lli sum_of_cycle_costs(){ // 建立所有环并求出答案
    cycle::min_min_val = sorted[0];
    for(int i=0; i<size; i++){
        if(!visited[i]){
            if(i==order[val[i]]){visited[i] = true; continue;}
            cycles.push_back({{}, (int)INF, 0});
            build_cycle(i);
        }
    }
    lli answer = 0;
    for(auto &i: cycles) answer += i.actual_cost();
    return answer;
}

int interface(){
    IOS_SPEED; cin >> size;
    if(size==0) return 1;
    int new_elem;
    for(int i=0; i<size; i++){
        cin >> new_elem;
        val.push_back(new_elem); sorted.push_back(new_elem); visited.push_back(false);
    }
    sort(sorted.begin(), sorted.end());
    for(int i=0; i<size; i++) order[sorted[i]] = i;
    cout << sum_of_cycle_costs() << "\n";
    val.clear(); sorted.clear(); order.clear(); cycles.clear(); visited.clear();
    return 0;
}

int main()
{
    while(1) if(interface()) break;
    return 0;
}
最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 1.插入排序 逐个处理待排序的记录,每个记录与前面已排序已排序的子序列进行比较,将它插入子序列中正确位置 代码 性...
    某WAP阅读 551评论 0赞 0
  • 阅读原文 堆排是基于堆的一种排序算法,对于堆的了解,请看什么是堆排序(如果对堆的插入和删除不清楚,强烈建议先看堆)...
    gyl_coder阅读 566评论 0赞 1
  • 排序 内部排序:全部数据可同时放入内存进行的排序。 外部排序:文件中数据太多,无法全部调入内存进行的排序。 插入...
    北风知我意阅读 1,163评论 0赞 0
  • 排序就是将一组对象按照某种逻辑顺序重新排列的过程,所有计算机系统都实现了各种排序算法以供系统和用户使用。学习排序算...
    shen888阅读 799评论 0赞 0
  • 1 初级排序算法 排序算法关注的主要是重新排列数组元素,其中每个元素都有一个主键。排序算法是将所有元素主键按某种方...
    深度沉迷学习阅读 1,683评论 0赞 1

友情链接更多精彩内容