构造满足不等式的数组
构造长度为的整型数组,使对于任意,有
题解
思路
当数组长度为 1 时,任意数组都满足条件,长度为 2 时同理,例如数组,那么我们接下来可以想想能不能从这个短数组出发,构造出一个满足条件的长度为 4 的数组?然后再构造出更长的数组?
假设我们已经有了一个构造规则,根据数学归纳法,我们只需要证明以下两点(其中第一点已经不需要证明了):
一、数组长度为 1 时,满足条件
二、如果一个长度为 k 的数组也满足条件,则通过构造出的长度为 2k 的数组也满足条件
启发式:构造长度 4 和 8 的数组
接下来只需要求出即可。我们就从开始,要想让长度翻倍,可以将这个数组分别按两种规则变成两个长度为 2 的数组,然后拼起来长度就是 4 了。
所以我们搞两个映射,一个将元素映射到奇数域上,另一个映射到偶数域上:
这两个映射并不唯一,但是难想。要说清楚为什么是这两个映射不太容易,我们这里只是验证这样的映射方式能不能得到最终数组,然后记住这道题的解法。
原数组被映射成,实现了长度翻倍,因为目前长度只有 4,所以和必然一个是奇数一个是偶数,因此和必然不等于偶数。
长度不够导致有一种情况我们没有考虑到,即同在数组左半区和右半区的情况。因此还需要继续验证长度更长时能否满足条件。
同理,只有和奇偶性相同时才有可能不满足条件,即同在数组左半区和右半区。
假设都在左半区(左右半区都一样),由于左半区是由长度为 4 的数组根据线性变换而来,因此如果不等式在变换之前成立,则变换之后仍然成立。
而变换之前的数组,即,我们已经验证过确实满足条件,所以这样变换到长度为 8 的数组也是正确的。
一般情况:构造长度为 N 的数组
那么如何构造长度为的数组呢?这中间还有一个小问题,就是我们之前构造的数组长度全都是 2 的幂,而题目中给出的并不一定是。
从长度 m 的数组构造长度 2m的时,显然的任意一个子序列(即从中挑选一些位置的数,按照原来的相对位置重新组织成数组)仍然满足条件,所以长度为的数组可以认为成的子数组,也可以通过上述方法构造。
简而言之
映射
通过如下映射,可以将映射到:
数学归纳法
证明使用该映射规则,可以得到
-
显然,长度为 1 时满足条件
-
根据映射规则,长度为 2 的数组也可以从得到,且满足条件。
-
长度为时满足条件,则映射得到的长度 2k 的数组也满足条件。分类讨论 i 和 k 可能出现的位置(在数组的左半区还是右半区):
- 一个左一个右,和为奇数所以不等式成立
- 两个都在左(都在右),因为是满足条件的 k 长度数组线性变换来的,所以也满足条件
子序列仍满足条件
显然,如果 arr 满足条件,则 arr 的任何一个子序列都满足条件。因此,这个问题又两种方式可解:
- 递归:每次将问题的规模减半求解,只要每次把超过的部分减去即可。例如长度 4 生成了长度 8,而我当前子问题只需要长度 7,直接舍弃最后一个数即可。
- 迭代:按顺序生成长度 1,2,4... 的数组,直到生成了第一个长度大于 N 的数组,直接将多余部分舍弃。
代码
代码采用递归方式实现,函数在传入数组 arr 上直接操作,每次操作的长度为当前过程的 len 值。通过对 len 不断折半直到 len = 1,填入基数组 [1],然后不断返回上级递归过程,通过映射实现基数组长度扩展。
public class MakeArray {
public static int[] makeArray(int N) {
if (N < 1) {
return null;
}
int[] res = new int[N];
f(res, N);
return res;
}
private static void f(int[] arr, int len) {
if (len > arr.length) {
return;
}
if (len == 1) {
arr[0] = 1;
return;
}
int half = (len + 1) >> 1; // 生成 len 长度数组,需要长度至少为一半的"基"数组
f(arr, half); // 如 len = 5,需要先递归生成 len = 3 的
for (int i = 0; i < half; i++) { // 将上次递归结果扩大一倍,但是到 len 截止
// 左边的长度总是 >= 右边,但是差距不超过 1
if (i + half < len) {
arr[i + half] = arr[i] << 1; // f2 = 2i,这句必须放前面,因为之后会修改 arr[i]
}
arr[i] = (arr[i] << 1) - 1; // f1 = 2i - 1
}
}
}
举例:生成长度 11 的数组
- len = 11,目前没有基数组,所以需要对 len 折半递归,至少需要长度 6 的基数组
- len = 6,还是没有基数组,折半递归
- len = 3,折半
- len = 2
- len = 1,填入基数组 arr[0] = 1,返回上级递归
- 根据映射规则 [1] 生成 [1,2],返回上级
- len = 2
- [1,2] 生成 [1,3,2,4],但是 len = 3,因此只要前三项 [1,3,2]
- len = 3,折半
- [1,3,2] 生成 [1,5,3,2,6,4]
- len = 6,还是没有基数组,折半递归
- [1,5,3,2,6,4] 只保留前 11 项,最终得到 [1,9,5,3,11,7,2,10,6,4,12]