Zth's Blog

记录学习路上的点滴

0%

体育节 Namomo 每日一题div1 03-31

题目链接

题目

题意

给出一个长度为$n$的整数序列$a$, 将$a$重排, 设$d_i = max \{a_1, a_2, … a_i \} - min \{a_1, a_2, … a_i \}$, 最小化$\sum_{i = 1}^n d_i$

数据范围

$1 \leq n \leq 2000, 1 \leq a_i \leq 10^9$

题解

解法1

显然是区间dp题

设$mx_i = max \{a_1, a_2, … a_i \}, mn_i = min \{a_1, a_2, … a_i \}$

如果我们排好的序列中, 有一个$mx_i = mx_{i - 1}, mn_i = mn_{i - 1}, a_i \neq a_{i - 1}$, 那么这个序列肯定不会是最优的, 因为交换$a_i, a_{i - 1}$会得到更优的答案

也就是说我们每次确定$a_i$时, 如果$a_i \neq a_{i - 1}$, 那$mx, mn$必须要改变一个

那么思路就有了, 我们将$a$数组从小到大排好序, 然后枚举一个$len$, 让前$len$个数作为$mn$倒序依次出现, 剩下的数作为$mx$依次出现, 然后就是区间dp了

但是这样做显然是$O(n^3)$的, 过不了$2000$的数据

优化

直觉告诉我从中间分开是最优的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <cstdio>
#include <algorithm>

int n;
int a[2003];
int mn[1003], mx[1003];
long long f[1003][1003], ans;

int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);

std:: sort(a + 1, a + n + 1);

int mid = n / 2;
if(mid * 2 == n)
{
for(int i = 1; i <= mid; i ++)
mn[i] = a[i], mx[i] = a[i + mid];

std:: reverse(mn + 1, mn + mid + 1);

f[1][1] = mx[1] - mn[1];
for(int i = 2; i <= mid; i ++)
f[1][i] = f[1][i - 1] + mx[i] - mn[1], f[i][1] = f[i - 1][1] + mx[1] - mn[i];

for(int i = 2; i <= mid; i ++)
for(int j = 2; j <= mid; j ++)
f[i][j] = std:: min(f[i - 1][j], f[i][j - 1]) + mx[j] - mn[i];

ans = f[mid][mid];
}
else
{
for(int i = 1; i <= mid; i ++)
mn[i] = a[i], mx[i] = a[i + mid];
mx[mid + 1] = a[n];

std:: reverse(mn + 1, mn + mid + 1);

f[1][1] = mx[1] - mn[1];
for(int i = 2; i <= mid; i ++)
f[1][i] = f[1][i - 1] + mx[i] - mn[1], f[i][1] = f[i - 1][1] + mx[1] - mn[i];
f[1][mid + 1] = f[1][mid] + mx[mid + 1] - mn[1];

for(int i = 2; i <= mid; i ++)
for(int j = 2; j <= mid + 1; j ++)
f[i][j] = std:: min(f[i - 1][j], f[i][j - 1]) + mx[j] - mn[i];

ans = f[mid][mid + 1];

for(int i = 1; i <= mid; i ++)
mn[i] = a[i], mx[i] = a[i + mid + 1];
mn[mid + 1] = a[mid + 1];

std:: reverse(mn + 1, mn + mid + 2);

f[1][1] = mx[1] - mn[1];
for(int i = 2; i <= mid; i ++)
f[1][i] = f[1][i - 1] + mx[i] - mn[1], f[i][1] = f[i - 1][1] + mx[1] - mn[i];
f[mid + 1][1] = f[mid][1] + mx[1] - mn[mid + 1];

for(int i = 2; i <= mid + 1; i ++)
for(int j = 2; j <= mid; j ++)
f[i][j] = std:: min(f[i - 1][j], f[i][j - 1]) + mx[j] - mn[i];

ans = std:: min(ans, f[mid + 1][mid]);
}

printf("%lld\n", ans);

return 0;
}

肯定不对, wa

继续优化

按照我们一开始的思路, $f$数组初始化应该先选择$a_{len}, a_{len + 1}$, 然后$i$从$len - 1$到$1$, $j$从$len + 2$到$n$, $f[i][j] = min(f[i + 1][j], f[i][j - 1]) + a[j] - a[i]$, 这实际上是选择一个中点, 然后向两边拓展, 枚举中点$O(n)$, 向两边拓展$O(n^2)$, 总体$O(n^3 + nlog)$

那如果我们反过来呢?

上述的dp思路由于选择的中点不同导致初始状态不同, 所有不同中点的dp过程中没有公共子问题重叠, 观察到虽然他们的起点不一样, 但是终点都是$f[1][n]$

那我们考虑从两边往中间拓展, 这样初始状态就一样了, 存在了子问题重叠, 按照正常顺序添加的话$a_1, a_n$肯定会放在最后边, 而且一定会造成$a_n - a_1$的贡献,而且要保证每次添加$mx, mn$至少一个发生变化, 两边还是要按顺序添加的 , 尽量让最大/最小极值尽可能在后面出现, 所以我们实际上就是在倒着模拟整个添加过程, 在过程中dp, 肯定是正确的

最后$O(n)$枚举中点, 这样时间复杂度就变成了$O(n^2 + nlog + n)$, $2000$稳过

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <cstdio>
#include <algorithm>

int n;
int a[2003];
long long f[2003][2003], ans = 1e18;

int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);

std:: sort(a + 1, a + n + 1);
// 16, 17行是初始化边界
for(int i = n; i > 1; i --) f[1][i] = f[1][i + 1] + a[i] - a[1];
for(int i = 1; i < n; i ++) f[i][n] = f[i - 1][n] + a[n] - a[i];
// 区间dp
for(int i = 2; i < n; i ++)
for(int j = n - 1; j > i; j --)
f[i][j] = std:: min(f[i - 1][j], f[i][j + 1]) + a[j] - a[i]; // 转移

for(int i = 1; i < n; i ++) ans = std:: min(ans, f[i][i + 1]); // 枚举中点得出答案

printf("%lld\n", ans);

return 0;
}

DP

种类很多, 优化也多样, 还没怎么学, 目前只能凭感觉优化