题目:给定两个数列
a1,a2,…,an
和
b1,b2,…,bn
,你可以用这两个数列来产生新数列
c1,c2,…,cn
. 产生的方式是令
ci=ai+bi
或
ai−bi
. 用
cmax
和
cmin
分别表示
c1,c2,…,cn
的最大值和最小值,求
cmax−cmin
的最小值. 每个
ai
和
bi
的取值都是-100 000 000到100 000 000范围内的整数.
算法一:
暴力穷举数列c的所有可能,看
cmax−cmin
能取得的最小值是多少. 因为每个
ci
有两种可能的取值,所以总共有
2n
个可能的数列,算法复杂度
O(N2N)
.
算法二:
我们换个角度来想,算法一之所以复杂度高,是因为数列c的可能性太多了,那问题中有没有哪些值可能性并没有那么多,但只要我们枚举这些值就可以得到答案呢?有,比如
cmax
和
cmin
. 不管是
cmax
还是
cmin
,它们都只能取某个
ci=ai+bi
或
ai−bi
,因而都只有
2n
种取值可能. 这样我们就可以得到更好的算法,我们枚举
cmax
和
cmin
的所有可能取值(步骤1),然后判断对于每个i,
ai+bi
和
ai−bi
是否至少有一个落在[
cmin
,
cmax
]的范围内(步骤2). 步骤1的复杂度是
O(N2)
,步骤2的复杂度是
O(N)
,算法总复杂度
O(N3)
. 程序如下:
#include <cstdio>
const int N = 100 + 10;
const int inf = 1000000000;
int A[N], B[N];
int n;
inline bool in_range(int left, int right, int x)
{
return x >= left && x <= right;
}
bool check(int left, int right, int& d)
{
int tmp, i;
if (left > right)
{
tmp = left;
left = right;
right = tmp;
}
for (i=0; i<n; i++) if (!in_range(left, right, A[i] + B[i]) && !in_range(left, right, A[i] - B[i])) return false;
d = right - left;
return true;
}
int main()
{
int res, t, d, i, j, k;
scanf("%d", &t);
while (t --)
{
scanf("%d", &n);
for (i=0; i<n; i++) scanf("%d", &A[i]);
for (i=0; i<n; i++) scanf("%d", &B[i]);
if (n == 1)
{
printf("0\n");
continue;
}
res = inf;
for (i=0; i<n-1; i++)
for (j=i+1; j<n; j++)
{
if (check(A[i] + B[i], A[j] + B[j], d) && d < res) res = d;
if (check(A[i] + B[i], A[j] - B[j], d) && d < res) res = d;
if (check(A[i] - B[i], A[j] + B[j], d) && d < res) res = d;
if (check(A[i] - B[i], A[j] - B[j], d) && d < res) res = d;
}
printf("%d\n", res);
}
return 0;
}
算法三:
我们再进一步考虑
cmax
和
cmin
是否都需要枚举?其实不需要,我们可以只枚举
cmin
,因为对于每个
cmin
,我们希望
cmax
尽可能小,所以其实对于每个
i
,
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 100 + 10;
const int inf = 1000000000;
int A[N], B[N];
int n;
bool check(int left, int& d)
{
int right, i;
right = left;
for (i=0; i<n; i++)
if (A[i] + B[i] >= left && A[i] - B[i] >= left) right = max(right, min(A[i] + B[i], A[i] - B[i]));
else if (A[i] + B[i] >= left) right = max(right, A[i] + B[i]);
else if (A[i] - B[i] >= left) right = max(right, A[i] - B[i]);
else return false;
d = right - left;
return true;
}
int main()
{
int res, t, d, i, j;
scanf("%d", &t);
while (t --)
{
scanf("%d", &n);
for (i=0; i<n; i++) scanf("%d", &A[i]);
for (i=0; i<n; i++) scanf("%d", &B[i]);
res = inf;
for (i=0; i<n; i++)
{
if (check(A[i] + B[i], d) && d < res) res = d;
if (check(A[i] - B[i], d) && d < res) res = d;
}
printf("%d\n", res);
}
return 0;
}
算法四:
前面的算法,枚举c也好,枚举
cmin
或
cmax
,其实都是从局部出发的,我们可以试试从宏观的角度来思考这个问题. 宏观来看,就是数轴上有2N个点,每个点会属于某个i,我们希望在数轴上选最短的一段,使得这一段内的点分属于1到N,或者说使得对于每个i,
ai+bi
和
ai−bi
至少有一个在这一段中. 于是我们可以用O(N)扫描的方法,来解决这个问题. 因为用到排序,所以算法的总复杂度是O(NlogN). 程序如下:
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 100 + 10;
const int inf = 1000000000;
struct ITEM
{
int val, id;
ITEM(int val=0, int id=0):val(val), id(id){}
};
int A[N], B[N], cnt[N];
ITEM C[2*N];
int n;
bool operator < (const ITEM& a, const ITEM& b)
{
return a.val < b.val;
}
int main()
{
int res, t, s, i, j;
scanf("%d", &t);
while (t --)
{
scanf("%d", &n);
for (i=0; i<n; i++) scanf("%d", &A[i]);
for (i=0; i<n; i++) scanf("%d", &B[i]);
for (i=0; i<n; i++)
{
C[i*2] = ITEM(A[i] + B[i], i);
C[i*2+1] = ITEM(A[i] - B[i], i);
cnt[i] = 0;
}
sort(C, C + 2 * n);
s = 0;
for (i=0; i<2*n; i++)
{
cnt[C[i].id] ++;
if (cnt[C[i].id] == 1) s ++;
if (s == n) break;
}
j = 0;
for (; cnt[C[j].id]==2; j++) cnt[C[j].id] --;
res = C[i].val - C[j].val;
for (i++; i<2*n; i++)
{
cnt[C[i].id] ++;
for (; cnt[C[j].id]==2; j++) cnt[C[j].id] --;
res = min(res, C[i].val - C[j].val);
}
printf("%d\n", res);
}
return 0;
}
总结一下,我们设计和优化算法,常见的思路一个是尽可能的减少可能性,可能性越少,需要枚举的就越少;一个是多变换看待问题的角度,有时换个角度就能更容洞悉问题的本质.