You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

13 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 468. 魔法阵

洛谷

一、题目描述

六十年一次的魔法战争就要开始了,大魔法师准备从附近的魔法场中汲取魔法能量。

大魔法师有 m 个魔法物品,编号分别为 1,2,…,m

每个物品具有一个魔法值,我们用 x_i 表示编号为 i 的物品的魔法值。

每个魔法值 x_i 是不超过 n 的正整数,可能有多个物品的魔法值相同。

大魔法师认为,当且仅当四个编号为 a,b,c,d 的魔法物品满足 x_a<x_b<x_c<x_dx_bx_a=2(x_dx_c),并且 x_bx_a<(x_cx_b)/3 时,这四个魔法物品形成了一个魔法阵,他称这四个魔法物品分别为这个魔法阵的 A 物品,B 物品,C 物品,D 物品。

现在,大魔法师想要知道,对于每个魔法物品,作为某个魔法阵的 A 物品出现的次数,作为 B 物品的次数,作为 C 物品的次数,和作为 D 物品的次数。

输入格式 输入文件的第一行包含两个空格隔开的正整数 nm

接下来 m 行,每行一个正整数,第 i+1 行的正整数表示 x_i,即编号为 i 的物品的魔法值。

保证每个 x_i 是分别在合法范围内等概率随机生成的。

输出格式 共输出 m 行,每行四个整数。

i 行的四个整数依次表示编号为 i 的物品作为 A,B,C,D 物品分别出现的次数。

保证标准输出中的每个数都不会超过 10^9

每行相邻的两个数之间用恰好一个空格隔开。

数据范围 1≤n≤15000,1≤m≤40000,1≤x_i≤n

输入样例

30 8
1
24
7
28
5
29
26
24

输出样例

4 0 0 0
0 0 1 0
0 2 0 0
0 0 1 1
1 3 0 0
0 0 0 2
0 0 2 2
0 0 1 0

二、暴力40分做法

4层循环枚举每个物品,物品上限m<=40000,四层就是40000*40000*40000*40000,死的透透的,好处就是好想好做,可以骗一部分分数。

#include <bits/stdc++.h>
using namespace std;
const int N = 40010;
int n, m;
int q[N];

// 40分
bool check(int a, int b, int c, int d) {
    if (a >= b || b >= c || c >= d) return 0;
    if ((b - a) != 2 * (d - c)) return 0;
    if (3 * (b - a) >= (c - b)) return 0;
    return 1;
}
int g[N][4];

int main() {
#ifndef ONLINE_JUDGE
    freopen("468.in", "r", stdin);
#endif
    cin >> n >> m;
    // 魔法值都是不超过n的正整数,似乎没啥用
    // m个魔法物品

    for (int i = 1; i <= m; i++) cin >> q[i]; // 读入每个魔法物品的魔法值

    for (int a = 1; a <= m; a++)
        for (int b = 1; b <= m; b++)
            for (int c = 1; c <= m; c++)
                for (int d = 1; d <= m; d++)
                    if (check(q[a], q[b], q[c], q[d]))
                        g[a][0]++, g[b][1]++, g[c][2]++, g[d][3]++;

    // a这个枚举到的数字出现了一次它是做为a位置出现的
    // 找到一组合法的a,b,c,d

    // 输出结果
    for (int i = 1; i <= m; i++)
        printf("%d %d %d %d\n", g[i][0], g[i][1], g[i][2], g[i][3]);
    return 0;
}

三、暴力65分做法

既然4层每层枚举物品的办法行不通那能不能考虑变化一下枚举的内容呢我们观察发现上帝为你关上了一扇门就会为你打开一扇窗此题中的魔法值上限n<=15000的!

不是很大,我们能不能考虑枚举魔法数值呢?

但是如果我们枚举每个魔法数值,魔法数值有重复怎么办呢?

题目提示:每个魔法值 X_i 是不超过 n 的正整数,可能有多个物品的魔法值相同。

当然重复的信息不能丢失,需要记录下来每个魔法值有几个,这提示我们用桶,一看n<=15000,用桶来保存魔法值的个数是没有问题的,我们设cnt[N]来保存每个魔法值的个数。

继续,如果我们枚举出了一组合法的魔法值组合(a,b,c,d),那么这些魔法值(a,b,c,d)可能是哪些物品的呢?因为最后我们需要回答的是每个魔法物品在四个位置出现的次数,不能不关心是哪些物品啊! 当然是魔法值等于(a,b,c,d)的魔法物品,设为 (A',A'',A'''),(B',B''),(C'),(D',D'') 那么如果出现了一次(a,b,c,d),在现实物品组合中可能是 $(A',B',C',D')\ (A',B'',C',D')\ (A',B'',C',D'')\ ...$

组合数就是3*2*1*2。 这里还有一个小弯弯,就是人家最终问的是物品i,也就是可以理解为物品A'出现的次数,那么就是\frac{3*2*1*2}{3}

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N = 15010, M = 40010;
int n, m;
int x[M];
LL cnt[N];
LL num[N][4];
// 65分 4层循环按桶的思路枚举每个魔法值暴力枚举a,b,c,d
LL read() {
    LL x = 0, f = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-') f = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 3) + (x << 1) + (ch ^ 48);
        ch = getchar();
    }
    return x * f;
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("468.in", "r", stdin);
#endif
    n = read(), m = read();
    for (int i = 1; i <= m; i++) {
        x[i] = read();
        cnt[x[i]]++;
    }

    for (int a = 1; a <= n; a++)
        for (int b = a + 1; b <= n; b++)
            for (int c = b + 1; c <= n; c++)
                for (int d = c + 1; d <= n; d++) {
                    if ((b - a) & 1 || 3 * (b - a) >= (c - b)) continue;
                    if ((b - a) != 2 * (d - c)) continue;
                    LL ans = cnt[a] * cnt[b] * cnt[c] * cnt[d];
                    num[a][0] += ans;
                    num[b][1] += ans;
                    num[c][2] += ans;
                    num[d][3] += ans;
                }
    for (int i = 1; i <= n; i++)
        for (int j = 0; j < 4; j++)
            num[i][j] /= cnt[i] ? cnt[i] : 1;

    for (int i = 1; i <= m; i++) {
        for (int j = 0; j < 4; j++)

            printf("%lld ", num[x[i]][j]);
        puts("");
    }
    return 0;
}

四、暴力85分做法

要求求出满足x_a<x_b<x_c<x_d,x_b-x_a=2(x_d-x_c)x_b-x_a<\frac{x_c-x_b}{3}a,b,c,d的数量。

为了去掉一层循环,结合以前的经验,我们知道可以通过数学办法推导一下x_d= \frac{x_b-x_a+2x_c}{2}

所以我们可以省去一维的枚举,做到O(n^3)枚举,实测在洛谷上能拿到85分.

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N = 15010, M = 40010;
int n, m;     // 魔法值的上限是n,个数是m
int x[M];     // 原始的魔法值
LL cnt[N];    // 每个魔法值计数用的桶
LL num[N][4]; // 以某个魔法值i为a,b,c,d时的个数记录在num[i][0],num[i][1],num[i][2],num[i][3]中,也就是答案

// 85分 3层循环按桶的思路枚举每个魔法值暴力枚举a,b,c,然后利用数学办法计算出d
// 17/20 85分
// 快读
LL read() {
    LL x = 0, f = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-') f = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 3) + (x << 1) + (ch ^ 48);
        ch = getchar();
    }
    return x * f;
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("468.in", "r", stdin);
#endif
    n = read(), m = read();
    for (int i = 1; i <= m; i++) {
        x[i] = read();
        cnt[x[i]]++; // 记录每个魔法值的个数
    }

    // 不再枚举每个输入的顺序而是枚举每个魔法值原因是魔法值的上限是固定的n
    for (int a = 1; a <= n; a++) // 枚举每个魔法值上限是n
        for (int b = a + 1; b <= n; b++)
            for (int c = b + 1; c <= n; c++) {
                if ((b - a) & 1 || 3 * (b - a) >= (c - b)) continue; // 把已知条件反着写符合这样要求的直接continue掉
                int d = b - a + c * 2 >> 1;                          // d可以通过数学办法计算获得
                // 这里有一个数学的小技巧,就是先求总的个数,再除掉自己的个数
                // 现在枚举到的每个(a,b,c,d)组合都是一种合法的组合,同时,由于每个数值不止一个,根据乘法原理,需要累乘个数才是答案
                LL ans = cnt[a] * cnt[b] * cnt[c] * cnt[d];
                // if (ans) cout << a << " " << b << " " << c << " " << d << endl;
                num[a][0] += ans;
                num[b][1] += ans;
                num[c][2] += ans;
                num[d][3] += ans;
            }
    for (int i = 1; i <= n; i++)
        for (int j = 0; j < 4; j++)
            num[i][j] /= cnt[i] ? cnt[i] : 1;

    for (int i = 1; i <= m; i++) {  // 枚举每个序号
        for (int j = 0; j < 4; j++) // 此序号作为a,b,c,d分别出现了多少次
            // 举栗子i=2,x[i]=5,也就是问你:5这个数分别做为a,b,c,d出现了多少次
            printf("%lld ", num[x[i]][j]);
        puts("");
    }
    return 0;
}

五、递推优化解法

依旧是对 x_b-x_a=2(x_d-x_c)进行分析,我们设t=x_d-x_c,则x_b-x_a=2⋅t;再分析第二个条件X_bX_a<(X_cX_b)/3,我们可以得到X_cX_b>6⋅t,我们给他补全成等号,就是X_cX_b=6⋅t+k

所以这四个数在数轴上的排列如图所示

左边红色部分框出的AB是绑定的,右边绿色部分框出的CD也是绑定的。 因此整个系统共有三个自由度:t、红色部分、绿色部分。

同时枚举三个自由度的计算量过大。在1秒内,我们只能枚举其中两个自由度。

所以我们会有一个不成熟的思路:在1-n/9范围内枚举t,把a,b,c,dt表示出来。

那么如何计算呢?枚举D。当我们枚举到一个D值的时候,与之对应的C值是确定的(不受k影响),而A值和B值却不一定。因此我们可以找到最大的与之对应的AB值。

但是有可能会存在一组A值、B值要比当前计算到的小,怎么办呢?不妨设有可能存在的比最大值小的A值为A_1B值为B_1,计算到的为A_2B_2

A_1<A_2 \& \& B_1<B_2时,只要A_2B_2能组成魔法阵,A_1B_1一定可以(k只是大于0的数,而对k的上界没有限制,当我们把k放大时,就可以构造出A_1B_1了)。

由于是顺序枚举,所以我们可以 记录一下之前有多少组合法解类似于前缀和),最后再用 乘法原理 计算。

同样的方法,我们从A的上界往A的下界枚举记录 后缀和 然后计算即可。

首先枚举t。接下来并列枚举绿色部分和红色部分: 从左到右枚举绿色部分,当绿色部分固定后,则C应该累加的次数是所有满足要求的ABcnt[A] * cnt[B] 的和,再乘以cnt[D]。其中cnt[A], cnt[B], cnt[D]A,B, D出现的次数。所有满足要求的AB就是整个线段左边的某个前缀,因此可以利用前缀和算法来加速计算。cnt[D]同理可得。 从右到左枚举红色部分可做类似处理。

Code

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N = 15010, M = 40010;
int n, m, x[M], num[4][N], cnt[N];

// 快读
LL read() {
    LL x = 0, f = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-') f = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 3) + (x << 1) + (ch ^ 48);
        ch = getchar();
    }
    return x * f;
}

int main() {
    n = read(), m = read();
    for (int i = 1; i <= m; i++) { // m个魔法值
        x[i] = read();
        cnt[x[i]]++; // 每个魔法值对应的个数
    }
    int sum, A, B, C, D;

    for (int t = 1; t * 9 + 1 <= n; t++) { // k最小是1那么9t+1=max(x[D])=n
        sum = 0;
        for (D = 9 * t - 1; D <= n; D++) { // 枚举D
            C = D - t;                     // 表示C
            B = C - 6 * t - 1;             // 根据C推出最大的B
            A = B - 2 * t;                 // 推出最大的A
            sum += cnt[A] * cnt[B];        // 计算当前A和B的情况
            num[2][C] += cnt[D] * sum;     // num[2][C]+=cnt[A]*cnt[B]*cnt[C]
            num[3][D] += cnt[C] * sum;     // num[3][D]+=cnt[A]*cnt[B]*cnt[D]
        }
        sum = 0;
        for (A = n - 9 * t - 1; A; A--) { // 倒序枚举A
            B = A + 2 * t;
            C = B + 6 * t + 1;         // C的最小值
            D = C + t;                 // D的最小值
            sum += cnt[C] * cnt[D];    // 计算当前C和D的情况 (涵盖了比C,D大的小所有C',D'的cnt乘积和)
            num[0][A] += cnt[B] * sum; // num[0][A]+=cnt[B]*cnt[C]*cnt[D]
            num[1][B] += cnt[A] * sum; // num[1][B]+=cnt[A]*cnt[C]*cnt[D]
        }
    }
    for (int i = 1; i <= m; i++) {
        for (int j = 0; j < 4; j++)
            printf("%d ", num[j][x[i]]);
        puts("");
    }
    return 0;
}