构造满足不等式的数组

204 阅读5分钟

构造满足不等式的数组

构造长度为NN的整型数组arrarr,使对于任意i<j<ki<j<k,有

arr[i]+arr[k]2arr[j]arr[i]+arr[k]\neq 2arr[j]

题解

思路

当数组长度为 1 时,任意数组都满足条件,长度为 2 时同理,例如数组[1,2][1, 2],那么我们接下来可以想想能不能从这个短数组出发,构造出一个满足条件的长度为 4 的数组?然后再构造出更长的数组?

假设我们已经有了一个构造规则ff,根据数学归纳法,我们只需要证明以下两点(其中第一点已经不需要证明了):

一、数组长度为 1 时,满足条件arr[i]+arr[k]2arr[j]arr[i]+arr[k]\neq 2arr[j]

二、如果一个长度为 k 的数组也满足条件,则通过ff构造出的长度为 2k 的数组也满足条件

启发式:构造长度 4 和 8 的数组

接下来只需要求出ff即可。我们就从[1,2][1, 2]开始,要想让长度翻倍,可以将这个数组分别按两种规则变成两个长度为 2 的数组,然后拼起来长度就是 4 了。

所以我们搞两个映射,一个将元素映射到奇数域上,另一个映射到偶数域上:

f1(i)=2i1f_1(i)=2i-1

f2(i)=2if_2(i)=2i

这两个映射并不唯一,但是难想。要说清楚为什么是这两个映射不太容易,我们这里只是验证这样的映射方式能不能得到最终数组,然后记住这道题的解法。

原数组被映射成[1,2][1,3,2,4][1, 2]\to[1,3,2,4],实现了长度翻倍,因为目前长度只有 4,所以arr[i]arr[i]arr[k]arr[k]必然一个是奇数一个是偶数,因此和必然不等于偶数2arr[j]2arr[j]

长度不够导致有一种情况我们没有考虑到,即i,ki,k同在数组左半区和右半区的情况。因此还需要继续验证长度更长时能否满足条件。

[1,3,2,4][1,5,3,7,2,6,4,8][1,3,2,4]\to[1,5,3,7,2,6,4,8]

同理,只有arr[i]arr[i]arr[k]arr[k]奇偶性相同时才有可能不满足条件,即i,ki,k同在数组左半区和右半区。

假设i,j,ki,j,k都在左半区(左右半区都一样),由于左半区是由长度为 4 的数组根据f1f_1线性变换而来,因此如果不等式arr[i]+arr[k]2arr[j]arr[i]+arr[k]\neq 2arr[j]在变换之前成立,则变换之后仍然成立。

而变换之前的数组,即[1,3,2,4][1,3,2,4],我们已经验证过确实满足条件,所以这样变换到长度为 8 的数组也是正确的。

一般情况:构造长度为 N 的数组

那么如何构造长度为NN的数组呢?这中间还有一个小问题,就是我们之前构造的数组长度全都是 2 的幂,而题目中给出的NN并不一定是。

从长度 m 的数组arrMarrM构造长度 2m的arr2Marr2M时,显然arr2Marr2M的任意一个子序列(即从中挑选一些位置的数,按照原来的相对位置重新组织成数组)仍然满足条件,所以长度为N<2mN<2m的数组可以认为成arr2Marr2M的子数组,也可以通过上述方法构造。

简而言之

映射

通过如下映射ff,可以将arr[0:m1]arr[0:m-1]映射到arr[0:2m1]arr[0:2m-1]

x={f1(i)=2i10i<mf2(i)=2imi<2mx = \begin{cases} f_1(i)=2i-1 &0\leq i<m \\ f_2(i)=2i &m\leq i<2m \end{cases}

数学归纳法

证明使用该映射规则,可以得到

  1. 显然,长度为 1 时满足条件

  2. 根据映射规则,长度为 2 的数组[1,2][1, 2]也可以从[1][1]得到,且满足条件。

  3. 长度为k,(k3)k,(k\geq3)时满足条件,则映射得到的长度 2k 的数组也满足条件。分类讨论 i 和 k 可能出现的位置(在数组的左半区还是右半区):

    1. 一个左一个右,和为奇数所以不等式成立
    2. 两个都在左(都在右),因为是满足条件的 k 长度数组线性变换来的,所以也满足条件

子序列仍满足条件

显然,如果 arr 满足条件,则 arr 的任何一个子序列都满足条件。因此,这个问题又两种方式可解:

  1. 递归:每次将问题的规模减半求解,只要每次把超过的部分减去即可。例如长度 4 生成了长度 8,而我当前子问题只需要长度 7,直接舍弃最后一个数即可。
  2. 迭代:按顺序生成长度 1,2,4... 的数组,直到生成了第一个长度大于 N 的数组,直接将多余部分舍弃。

代码

代码采用递归方式实现,函数ff在传入数组 arr 上直接操作,每次操作的长度为当前过程的 len 值。通过对 len 不断折半直到 len = 1,填入基数组 [1],然后不断返回上级递归过程,通过映射ff实现基数组长度扩展。

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],返回上级
      • [1,2] 生成 [1,3,2,4],但是 len = 3,因此只要前三项 [1,3,2]
    • [1,3,2] 生成 [1,5,3,2,6,4]
  • [1,5,3,2,6,4] 只保留前 11 项,最终得到 [1,9,5,3,11,7,2,10,6,4,12]