题目大意:有n条边,问随机选3条边能构成一个三角形的概率是多少。
分析:能构成三角形的条件就是最长的边边长小于两条较短的边边长之和,从这个角度出发,答案的方案数可以为:枚举每一条边作为最长边,算出选较短的两条边其边长之和大于这条边的方案数,每条边的方案数加起来 除去 所有的选边方案即是答案。
设
n
u
m
[
i
]
num[i]
num[i] 为 边长之和为
i
i
i 的方案数,主要是如何计算出
n
u
m
[
i
]
num[i]
num[i]。
以边长为下标建数组(开桶),
c
n
t
[
i
]
cnt[i]
cnt[i]表示边长为i的边数
用FFT计算cnt[i] 和 cnt[i]的卷积,得出的数组
n
u
m
[
i
]
num[i]
num[i]就是和为
i
i
i 的方案数(两个数之和为 i 是交叉相加的过程,统计方案数则是卷积
c
n
t
[
k
]
=
∑
i
=
0
k
c
n
t
[
i
]
∗
c
n
t
[
k
−
i
]
cnt[k] = \sum_{i = 0}^kcnt[i] * cnt[k - i]
cnt[k]=∑i=0kcnt[i]∗cnt[k−i])
得出的 n u m [ i ] num[i] num[i]包含了重复的部分,首先要扣掉自己和自己相加的 n u m [ a [ i ] + a [ i ] ] − − num[a[i] + a[i]]-- num[a[i]+a[i]]−−。因为题目中选边是无序的,而卷积计算的是有序的,所以还需要除以2: n u m [ i ] = n u m [ i ] ÷ 2 num[i] = num[i]\div2 num[i]=num[i]÷2。
如果对
n
u
m
[
i
]
num[i]
num[i]数组求前缀和,枚举每条边时可以O(1)统计答案,但是cnt[i] 并不是边长小于某个数值的两条边加起来为
i
i
i 的方案数,需要扣掉不合法的。可以将边长数组
a
a
a 按边长大小排序,使得答案求解过程有序:枚举每条边作为最长边,另外两条边在
a
a
a 数组中的位置在当前这条边之前且边长之和大于这条边长的方案数,这样答案不会遗漏也不会重复。假设我们用
a
n
s
=
n
u
m
[
t
o
t
]
−
n
u
m
[
l
e
n
]
ans = num[tot] - num[len]
ans=num[tot]−num[len] 算出边长之和大于当前边长
l
e
n
len
len的方案数,要扣掉两条边不在当前位置i之前的情况,有三种:
(1)两条边都在 i 的后面 :
a
n
s
ans
ans -=
(
n
−
i
)
∗
(
n
−
i
−
1
)
2
\frac{(n - i) * (n - i - 1)} 2
2(n−i)∗(n−i−1)
(2)一条边在 i 的前面,一条边在 i 的后面
a
n
s
ans
ans -=
(
i
−
1
)
∗
(
n
−
i
)
(i - 1) * (n - i)
(i−1)∗(n−i)
(3)一条边是 i 自己
a
n
s
ans
ans -=
(
n
−
1
)
(n - 1)
(n−1)
这三种情况都是两条边边长之和
>
l
e
n
> len
>len的情况,排序之后使得答案求解过程变得有序,这三种情况很好计算。
代码:
/*
有n条边,选3条能组成三角形的概率
*/
#include<stdio.h>
#include<algorithm>
#include<string.h>
#include<math.h>
#include<iostream>
using namespace std;
const double pi = acos(-1.0);
const int maxn = 2e5 + 10;
long long t,n;
long long l,len;
long long a[4 * maxn];
long long num[4 * maxn],ans[4 * maxn];
struct complex{
double r,i;
complex(double _r = 0.0,double _i = 0.0) {
r = _r;
i = _i;
}
complex operator + (const complex & rhs) {
return complex(r + rhs.r,i + rhs.i);
}
complex operator - (const complex & rhs) {
return complex(r - rhs.r,i - rhs.i);
}
complex operator * (const complex & rhs) {
return complex(r * rhs.r - i * rhs.i,r * rhs.i + i * rhs.r);
}
};
complex tmp[4 * maxn];
void change(complex a[],int len) {
int tot = 0;
while((1 << tot) < len) tot++;
for(int i = 0; i < len; i++) {
int cur = 0;
for(int j = 0; j < tot; j++)
if(i >> j & 1) cur |= (1 << (tot - j - 1));
if(i < cur) swap(a[i],a[cur]);
}
}
void fft(complex a[],int len,int type) {
change(a,len);
for(int s = 2; s <= len; s <<= 1) {
complex wp = complex(cos(type * 2 * pi / s),sin(type * 2 * pi / s));
for(int j = 0; j < len; j += s) {
complex w = complex(1,0);
for(int k = 0; k < s / 2; k++) {
complex u = a[j + k];
complex t = w * a[j + k + s / 2];
a[j + k] = u + t;
a[j + k + s / 2] = u - t;
w = w * wp;
}
}
}
if(type == -1)
for(int i = 0; i < len; i++)
a[i].r /= len;
}
void solve() {
len = 1;
while(len < 2 * l) len <<= 1;
for(int i = 0; i < len; i++)
tmp[i] = complex(num[i],0);
fft(tmp,len,1);
for(int i = 0; i < len; i++)
tmp[i] = tmp[i] * tmp[i];
fft(tmp,len,-1);
for(int i = 0; i < len; i++)
ans[i] = (long long) (tmp[i].r + 0.5);
}
int main() {
scanf("%lld",&t);
while(t--) {
scanf("%lld",&n);
l = 0;
memset(num,0,sizeof num);
memset(ans,0,sizeof ans);
for(int i = 1; i <= n; i++) {
scanf("%lld",&a[i]);
l = max(l,a[i]);
num[a[i]]++;
}
l++;
sort(a + 1,a + n + 1,less<long long>());
solve();
for(int i = 1; i <= n; i++)
ans[a[i] + a[i]]--;
for(int i = 0; i <= len; i++)
ans[i] /= 2;
for(int i = 1; i <= len; i++)
ans[i] += ans[i - 1];
long long cnt = 0;
for(int i = 1; i <= n; i++) {
cnt += ans[len] - ans[a[i]];
cnt -= 1ll * (n - i - 1) * (n - i) / 2;
cnt -= 1ll * (i - 1) * (n - i);
cnt -= n - 1;
}
double res = (cnt * 6) / (1.0 * n * (n - 1) * (n - 2));
printf("%.7lf\n",res);
}
return 0;
}
总结:这题是FFT的练习题,但一开始并不能看出来要用到FFT。很多FFT的题都是如此,除了那些惯用的套路题,做题时应先分析如何解出答案(通常根据问题的性质,从暴力开始构思),对某些答案需要的变量做出假设(假设很关键,如果不敢做出这一步就认为这种方案一定不行最终只会与正解擦肩而过),使用算法对每一步可以优化的地方进行优化,当分析到某一步需要计算卷积,或者转换为卷积形式时可以套用FFT,卷积形式通常不太容易看出(对蒟蒻的我来说),需要多加练习思维。
FFT只是用来加速计算卷积的工具,用到FFT算法的题目通常都需要分析,FFT只是其中不可或缺的一步而不是全部。A题的魅力正是不断的思考,使用算法来攻克每一步难关最终得出答案。