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.

358 lines
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$. 魔法阵](https://www.acwing.com/problem/content/470/)
[洛谷](https://www.luogu.com.cn/problem/P2119)
### 一、题目描述
六十年一次的魔法战争就要开始了,大魔法师准备从附近的魔法场中汲取魔法能量。
大魔法师有 $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$ 物品的次数。
**输入格式**
输入文件的第一行包含两个空格隔开的正整数 $n$ $m$。
接下来 $m$ 行,每行一个正整数,第 $i+1$ 行的正整数表示 $x_i$,即编号为 $i$ 的物品的魔法值。
保证每个 $x_i$ 是分别在合法范围内等概率随机生成的。
**输出格式**
共输出 $m$ 行,每行四个整数。
$i$ 行的四个整数依次表示编号为 $i$ 的物品作为 $A,B,C,D$ 物品分别出现的次数。
保证标准输出中的每个数都不会超过 $10^9$。
每行相邻的两个数之间用恰好一个空格隔开。
**数据范围**
$1n15000,1m40000,1x_in$
**输入样例**
```cpp {.line-numbers}
30 8
1
24
7
28
5
29
26
24
```
**输出样例**
```cpp {.line-numbers}
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$,死的透透的,好处就是好想好做,可以骗一部分分数。
```cpp {.line-numbers}
#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}$
```cpp {.line-numbers}
#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$分.
```cpp {.line-numbers}
#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]; // 以某个魔法值ia,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$
所以这四个数在数轴上的排列如图所示
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202309271307049.png)
左边红色部分框出的$A$和$B$是绑定的,右边绿色部分框出的$C$和$D$也是绑定的。
因此整个系统共有三个自由度:$t$、红色部分、绿色部分。
同时枚举三个自由度的计算量过大。在$1$秒内,我们只能枚举其中两个自由度。
所以我们会有一个不成熟的思路:在$1-n/9$范围内枚举$t$,把$a,b,c,d$拿$t$表示出来。
那么如何计算呢?枚举$D$。当我们枚举到一个$D$值的时候,与之对应的$C$值是确定的(不受$k$影响),而$A$值和$B$值却不一定。因此我们可以找到最大的与之对应的$A$值$B$值。
但是有可能会存在一组$A$值、$B$值要比当前计算到的小,怎么办呢?不妨设有可能存在的比最大值小的$A$值为$A_1$$B$值为$B_1$,计算到的为$A_2$和$B_2$
当$A_1<A_2 \& \& B_1<B_2$时,只要$A_2$和$B_2$能组成魔法阵,$A_1$和$B_1$一定可以($k$只是大于$0$的数,而对$k$的上界没有限制,当我们把$k$放大时,就可以构造出$A_1$和$B_1$了)。
由于是顺序枚举,所以我们可以 **记录一下之前有多少组合法解****类似于前缀和**),最后再用 **乘法原理** 计算。
同样的方法,我们从$A$的上界往$A$的下界枚举记录 **后缀和** 然后计算即可。
> 首先枚举$t$。接下来并列枚举绿色部分和红色部分:
> 从左到右枚举绿色部分,当绿色部分固定后,则$C$应该累加的次数是所有满足要求的$A$和$B$的 $cnt[A] * cnt[B]$ 的和,再乘以$cnt[D]$。其中$cnt[A], cnt[B], cnt[D]$是$A$,$B$, $D$出现的次数。所有满足要求的$A$和$B$就是整个线段左边的某个前缀,因此可以利用前缀和算法来加速计算。$cnt[D]$同理可得。
从右到左枚举红色部分可做类似处理。
#### $Code$
```cpp {.line-numbers}
#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;
}
```