题目描述
难度分:1604
输入n(2≤n≤500),m(2≤m≤109)和长为n的数组a(1≤a[i]≤m−1)。
重复如下操作n−1次:
- 选择a中的两个数x和y,得到(xy+yx)%m分,然后从a中删除x或者y。
输出总得分的最大值。
输入样例1
4 10
4 2 3 2
输出样例1
20
输入样例2
20 100
29 31 68 20 83 66 23 84 69 96 41 61 83 37 52 71 18 55 40 8
输出样例2
1733
算法
最小生成树
这个题隐藏很深,其实这个操作就相当于每次在n个孤立点中找到两个点u和v,每次累加上这两个点带来的收益(a[u]a[v]+a[v]a[u])%m。然后这两个点其中的一个就废掉了,假设废掉了u,接下来如果再选到v点,那就是累加v与它其他邻居的收益点。
这个过程就像是每次将两个点用边连接起来,执行n−1次所有的n个孤立点就合并成了一个连通图。完全就是Kruskal算法求最小生成树的过程,而题目要求答案最大化,那就将所有边权取相反数。运行Kruskal算法的时候,将原来加边权的操作改为减边权,这样得到的就是我们本题要求的最大值。
复杂度分析
时间复杂度
每对点之间都可以连边,因此边集的大小为O(n2),将所有边按照边权排序的时间复杂度为O(n2log2n)。接下来运行Kruskal算法需要遍历n2条边,每条边需要进行并查集的合并操作,合并操作的时间复杂度为O(logn)(该对数的底数不是2,其实这个过程的时间复杂度很低,可以看成是一个稍大的常数),将所有孤立点变成一个连通图的时间复杂度为O(n2logn)。整个算法的时间复杂度为O(n2(log2n+logn))。
空间复杂度
空间消耗分为两个部分,一个是并查集O(n)的空间,另一个就是边集数组的消耗O(n2)。显然瓶颈在后者,整个算法的额外空间复杂度为O(n2)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 501;
int n, m, a[N], p[N];
int fast_power(int a, int k) {
int res = 1 % m;
while(k) {
if(k&1) res = (LL)res * a % m;
a = (LL)a * a % m;
k >>= 1;
}
return res;
}
int find(int x) {
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
void merge(int x, int y) {
int rx = find(x), ry = find(y);
if(rx != ry) {
p[rx] = ry;
}
}
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
p[i] = i;
}
vector<array<int, 3>> edges;
for(int i = 1; i < n; i++) {
for(int j = i + 1; j <= n; j++) {
edges.push_back({-((fast_power(a[i], a[j]) + fast_power(a[j], a[i])) % m), i, j});
}
}
sort(edges.begin(), edges.end());
LL ans = 0;
for(auto&edge: edges) {
int u = edge[1], v = edge[2], w = edge[0];
if(find(u) != find(v)) {
ans -= w;
merge(u, v);
}
}
printf("%lld\n", ans);
return 0;
}