CSP历年复赛题-P2119 [NOIP2016 普及组] 魔法阵
原题链接:https://www.luogu.com.cn/problem/P2119
题意解读:在一组数里找出所有的Xa,Xb,Xc,Xd的组合,使得满足Xa<Xb<Xc<Xd, Xb-Xa=2(Xd-Xc), Xb-Xa<(Xc-Xb)/3,并统计出每个数作为A,B,C,D出现的次数。
解题思路:
1、枚举(O(n^4))
首先想到的是通过4重循环枚举所有可能的Xa,Xb,Xc,Xd,然后判断是否符合需要满足的条件,如果符合,就将这一组Xa,Xb,Xc,Xd所谓有效数字累计到答案A[Xa]++, B[Xb]++, C[Xc]++, D[Xd]++,A,B,C,D数组记录各个魔法值作为A,B,C,D出现的次数。显然已知4个变量之间的等式,没有必要4重循环枚举,只需要枚举3个变量即可,所以此代码就不写了。
2、枚举(O(n^3))
根据题目要求,魔法值一共有40000个,但每个魔法值不超过15000,也就是有很多同样的魔法值,那么相同的魔法值在形成Xa,Xb,Xc,Xd的组合时效果是一样的,没必要一一枚举,可以考虑去重。
我们设int magic[M]存储每种物品的魔法值,int h[N]记录每种魔法值对应的物品数,vector<int> magic2为去重排序后的魔法值
h[N]是桶数组,一方面可以记录每种魔法值的数量,另一方面可以起到计数排序的作用,因此我们对去重排序后的魔法值magic2进行枚举即可。
先看Xa<Xb<Xc<Xd, 枚举是可以先枚举Xa,再枚举Xb,然后枚举Xc,Xd可以计算得来
再看Xb-Xa=2(Xd-Xc), 可以有两个结论:a、Xb-Xa是偶数,这样在枚举中可以提前判断 b、Xd = (Xb - Xa + 2 * Xc) / 2
最后Xb-Xa<(Xc-Xb)/3,经过移项可得:Xc > 4 * Xb - 3 * Xa,可以在枚举到Xc时进行提前判断
由于每种魔法值有多个,而枚举对象的已经去重之后的魔法值,所以在找到一组有效的Xa, Xb, Xc, Xd时,如何计算作为A,B,C,D出现的次数呢?
根据乘法原理:
Xa作为A出现的次数是由Xb,Xc,Xd的个数决定的,从h[Xb]个Xb中选1个,从h[Xc]个Xc中选1个,从h[Xd]个Xd中选1个都可以和Xa组成有效的组合,
所以有A[Xa] += h[Xb] * h[Xc] * h[Xd]
同理,
90分代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 15005, M = 40005;
int n, m;
int A[N], B[N], C[N], D[N]; //每种魔法值作为A、B、C、D的次数
int magic[M]; //每种物品的魔法值
int h[N]; //每种魔法值对应的物品数
vector<int> magic2; //去重排序后的魔法值
int main()
{
cin >> n >> m;
for(int i = 1; i <= m; i++)
{
cin >> magic[i];
h[magic[i]]++;
}
for(int i = 1; i <= N; i++)
{
if(h[i] > 0) magic2.push_back(i);
}
for(int i = 0; i < magic2.size(); i++) //枚举Xa
{
int Xa = magic2[i];
for(int j = i + 1; j < magic2.size(); j++) //枚举Xb
{
int Xb = magic2[j];
if((Xb - Xa) % 2 == 1) continue; //Xb-Xa=2(Xd-Xc),所以必须是偶数
for(int k = j + 1; k < magic2.size(); k++) //枚举Xc
{
int Xc = magic2[k];
if(Xc <= 4 * Xb - 3 * Xa) continue; //Xa、Xb、Xc要符合关系
int Xd = (Xb - Xa + 2 * Xc) / 2; //根据公式计算Xd
if(h[Xd])
{
//Xa,Xb,Xc,Xd魔法值作为A,B,C,D的个数,要取决于其他魔法值的个数,根据乘法原理相乘
A[Xa] += h[Xb] * h[Xc] * h[Xd];
B[Xb] += h[Xa] * h[Xc] * h[Xd];
C[Xc] += h[Xa] * h[Xb] * h[Xd];
D[Xd] += h[Xa] * h[Xb] * h[Xc];
}
}
}
}
for(int i = 1; i <= m; i++)
{
cout << A[magic[i]] << " " << B[magic[i]] << " " << C[magic[i]] << " " << D[magic[i]] << endl;
}
return 0;
}
3、枚举(O(n^2/9))
此题要得到100分,就需要换一种思维方式
已知:
Xa<Xb<Xc<Xd,
Xb-Xa=2(Xd-Xc),
Xb-Xa<(Xc-Xb)/3 => Xc-Xb > 3(Xb-Xa)
我们设t = Xd-Xc,则有Xb-Xa=2t,Xc-Xb > 6t,所以Xa,Xb,Xc,Xd的大致分布为:
因此,我们在统计所有符合要求的Xc,Xd时只需要枚举两个变量:t、Xd
9t < n - 1,即9t + 2 <= n
Xd > Xa + 9t,所以 1 + 9t < Xd <= n
Xc = Xd - t
所以,从小到大枚举所有的t、Xd,Xc可以计算出来,而符合要求的Xa,Xb就是Xc,Xd左边所有能放得下的Xa,Xb
这里可以不用一一枚举xa,xb的位置,只需要限定Xb与Xc的距离,最小为6t+1,这样随着Xd,Xc的枚举右移,Xa,Xb也往右移动,
将Xa,Xb对结果的贡献用前缀和记录下来,每次更新
sum_ab += h[Xa] * h[Xb]
C[Xc] += h[Xd] * sum_ab
D[Xd] += h[Xc] * sum_ab
同理,在统计所有符合要求的Xa,Xb是只需要枚举两个变量:t、Xa
这次要从右往左枚举Xa
9t + 2 <= n
1<=Xa<=n-9t-1
sum_cd += h[Xc] * h[Xd]
C[Xc] += h[Xd] * sum_cd
D[Xd] += h[Xc] * sum_cd
100分代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 15005, M = 40005;
int n, m;
int A[N], B[N], C[N], D[N]; //每种魔法值作为A、B、C、D的次数
int magic[M]; //每种物品的魔法值
int h[N]; //每种魔法值对应的物品数
vector<int> magic2; //去重排序后的魔法值
int main()
{
cin >> n >> m;
for(int i = 1; i <= m; i++)
{
cin >> magic[i];
h[magic[i]]++;
}
for(int i = 1; i <= N; i++)
{
if(h[i] > 0) magic2.push_back(i);
}
for(int t = 1; 9 * t + 2 <= n; t++)
{
int sum_ab = 0;
for(int Xd = 9 * t + 1; Xd <= n; Xd++)
{
int Xa = Xd - 9 * t - 1;
int Xb = Xa + 2 * t;
int Xc = Xd - t;
sum_ab += h[Xa] * h[Xb];
C[Xc] += h[Xd] * sum_ab;
D[Xd] += h[Xc] * sum_ab;
}
int sum_dc = 0;
for(int Xa = n - 9 * t - 1; Xa >= 1; Xa--){ //注意顺序
int Xb = Xa + t * 2;
int Xd = Xa + 9 * t + 1;
int Xc = Xd - t;
sum_dc += h[Xc] * h[Xd];
A[Xa] += sum_dc * h[Xb];
B[Xb] += sum_dc * h[Xa];
}
}
for(int i = 1; i <= m; i++)
{
cout << A[magic[i]] << " " << B[magic[i]] << " " << C[magic[i]] << " " << D[magic[i]] << endl;
}
return 0;
}