|
|
## [$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_d,x_b−x_a=2(x_d−x_c)$,并且 $x_b−x_a<(x_c−x_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$。
|
|
|
|
|
|
每行相邻的两个数之间用恰好一个空格隔开。
|
|
|
|
|
|
**数据范围**
|
|
|
$1≤n≤15000,1≤m≤40000,1≤x_i≤n$
|
|
|
|
|
|
**输入样例**:
|
|
|
```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]; // 以某个魔法值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_b−X_a<(X_c−X_b)/3$,我们可以得到$X_c−X_b>6⋅t$,我们给他补全成等号,就是$X_c−X_b=6⋅t+k$
|
|
|
|
|
|
所以这四个数在数轴上的排列如图所示
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
左边红色部分框出的$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;
|
|
|
}
|
|
|
|
|
|
``` |