0x01 引入
在考场时想了一个错误算法,口胡一下,或许对理解正解有点帮助。
我们考虑交换两个数产生的代价,你会发现我们需要让大的数重复被交换的次数尽可能少,减少它对后面的代价。
那么不难构思出一个按从大到小的顺序将每个数一步交换到应到的位置的算法。
bool cmp(node u, node v) { return u.x > v.x; }
int main() {
int n = read();
for (int i = 1; i <= n; i++) {
a[i].x = read();
a[i].index = i; // 保存它在原数组里的位置
b[i] = a[i].x;
}
sort(a + 1, a + n + 1, cmp); // 排序
for (int i = 1; i <= n; i++) { // 从大到小处理
if (a[i].x == b[n - i + 1])
continue;
ans += (a[i].x + b[n - i + 1]); // 累加答案(这个数和它之前所在的序列的那个数对应的排序后的位置上的数的和
Swap(b[a[i].index], b[n - i + 1]);
// 记得交换
}
printf("%lld
", ans);
return 0;
}
不过下来被自己hack掉了,因为这个算法考虑了最大的代价最小,但很有可能让后几个数产生的代价更大(重复交换次数更多),所以这是一个错误算法
0x02 正解
14在题解里引入了图论:(原文)考虑对序列 (a) 从小到大排序得到序列 (b) ,对于每一个 (i) 我们进行连边,将位置 (i) 向 (a_i) 在序列 (b) 出现的位
置连一条有向边
其实就是将我的那个错误算法里,需要交换的对应点连上有向边。则这个图一定由无数个环组成
毕竟这个序列是固定的,也就是说在排序后的序列里还是原序列里的那些数,那么如果要将原数列变换成有序的,一定会将某几个区间里的数进行轮换,才能满足,而如果把每次交换看做 (A-B),那么区间内轮换及是 (A-B),(B-C),(C-A)。其实就是环嘛。
举个例子:
a[] = 3 1 2 4 ----> b[] = 1 2 3 4 // 会发现就是元素 1 2 3 轮换。
图:
1 to 3, 2 to 1, 3 to 2, 4 to 4
由两个环(其中一个为自环)组成。
不难通过例子发现,如果连边后出现自环,那么就代表当前这个元素处于正确位置无需再交换了。
于是题目变为:给定某一些环,每次消耗某些代价将其变为 (n) 个自环。
利用引入,依然考虑如何贪心实现。
part1:对于每个环不难想到每次变换使用可以用的代价(权值)最小的边。
即:有环
1 --4--> 3, 2 --3--> 1, 3 --5--> 2;
我们将其变换为:(在除最小边外其余边中交换权值最小边的端点。
1 --4--> 3, 2 --3--> 2, 3 --5--> 1;
这时2出现自环,表示2到达需要到的位置了。1,3交换同上。最后一定能在part1条件下拿出最优解。
环上每个数一定会被交换到一次,所以代价为其和,在由于需要满足将其变为自环代价最小。在变成 (cnt) 个自环后,最小代价点的代价共被加了 (cnt - 2) 次( (cnt) 表示环上点的个数,排除最小点自己,排除最后一个自环不需交换,最后就是累加 (cnt - 2) 次)
sum + (cnt - 2) * k.
part2:不过很显然没有说不能环外与环内的交换,去让圆环权值变小,所以我们同样需要考虑对于每个环将环外的最小边加入当前这个环。
也就是:我们有两环:
1 --4--> 3, 2 --3--> 1, 3 --5--> 2, 4 --9--> 5, 5 --9--> 4;
对于 1-2-3 这个环我们将环外的最小值拉进来,尝试让当前环的代价再次变小。交换 1-4(权值最小)得:
1 --4--> 3, 2 --3--> 4, 3 --5--> 2, 4 --9--> 5, 5 --9--> 1;
最后,贪心总策略即是把 part1 与 part2 操作下来取最小值。
环上依然每个数一定会被交换一次,最小点会在被交换一次(将环外最小边拉入环的最小代价),这时环中就有了 (cnt + 1) 个点,全部利用环外最小点进行更新。
sum + k + mi * (cnt + 1).
当然从面对数据编程这个层次,此题是需要离散化的。
实现:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int INF = 0x3f3f3f3f;
inline int Min(int x, int y) { return x < y ? x : y; }
const int MAXN = 1e6 + 5;
int a[MAXN], t[MAXN];
bool vis[MAXN];
struct node {
int index, w;
node() {}
friend bool operator<(node a, node b) { return a.w < b.w; }
} s[MAXN];
int main() {
int n, mi = INF;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
mi = min(mi, a[i]);
s[i].index = i;
s[i].w = a[i];
}
sort(s + 1, s + n + 1);
for (int i = 1; i <= n; i++) t[s[i].index] = i;
LL ans = 0;
for (int i = 1; i <= n; i++) {
if (vis[i])
continue;
LL cnt = 1, sum = a[i];
int k = INF;
vis[i] = true;
for (int j = t[i]; j != i; j = t[j]) { // 跑环
vis[j] = true;
cnt++; // 求出环山上有多少点
sum += a[j]; // 环上数的总和(与代价相关
k = min(k, a[j]); // 求出环上最小边的端点
}
if (cnt > 1) // 不是自环
ans += min(sum + (cnt - 2) * k, sum + k + mi * (cnt + 1));
// 分别计算 part1, part2
}
printf("%lld
", ans);
return 0;
}