枚举的一些分析
递增三元组——用统计量(前缀和)化简枚举
https://www.acwing.com/problem/content/1238/
视频:https://www.acwing.com/video/644/
题目要求很简单,给出三个数组,找出满足题意的所有组合的个数。
暴力枚举
刚开始没在意,直接暴力枚举
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[N], b[N], c[N];
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i ++)
cin >> a[i];
for(int i = 0; i < n; i ++)
cin >> b[i];
for(int i = 0; i < n; i ++)
cin >> c[i];
sort(a, a + n);
sort(b, b + n);
sort(c, c + n);
int ans = 0;
for(int i = n - 1; i >= 0; i --)
for(int j = n - 1; j >= 0; j --)
{
if(a[i] >= b[j])break;
for(int k = n - 1; k >= 0; k --)
{
if(b[j] >= c[k])break;
ans ++;
}
}
cout << ans;
return 0;
}
但是,碰到10000的数据就超时了,分析一下时间复杂度:
三个sort函数 : 3 * n *log(n)
三层输入循环:3 * n
三层计算:最坏的情况是n ^ 3
大大的超过了1e9的时间复杂度,显然是不行的。
初步引入统计量
瞄了一眼网上的题解,说是可以用统计量的方式进行统计排序
初步想法是:设a[i]就是数组a中i出现的次数,a[N]记录一共出现了几个不同的数
代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[3][N];
int main()
{
int n;
cin >> n;
int res;
for(int i = 0; i < 3; i ++)
for(int j = 0; j < n; j ++)
{
cin >> res;
if(!a[i][res])a[i][N - 1] = max(res, a[i][N - 1]);
a[i][res] ++;
}
int ans = 0;
for(int i = 0; i <= a[0][N - 1]; i ++)
if(a[0][i])
for(int j = i + 1; j <= a[1][N - 1]; j ++)
if(a[1][j])
for(int k = j + 1; k <= a[2][N - 1]; k ++)
if(a[2][k])ans += a[0][i] * a[1][j] * a[2][k];
cout << ans;
return 0;
}
但是,还是TLE,
时间复杂度分析:
在计算过程中的时间复杂度:N * N * N,在一定程度上反而增大了,因为有的数可能没出现过,但是还是要判断一下
进一步分析
转化一下思维,先分析数据,由于是100000的数据,N*N的时间复杂度就会超时,所以需要把循环限制在一层,把时间复杂度限制在nlgn以内。
题目要求的是ai < bj < ck,可以看到b数组处在核心位置,所以需要枚举一次b,然后只需要将满足条件的a,c的个数相乘就能够完成。
这就转化到前缀和问题,创建一个数组,ac[i],代表数组a中处于区间[0 , i]的数字出现的个数,那么显然c[i]就得是后缀和,代表处于区间[i, N]的数字的个数
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[N], b[N], c[N];
int ac[N], cc[N];
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i ++)
cin >> a[i];
for(int i = 0; i < n; i ++)
cin >> b[i];
for(int i = 0; i < n; i ++)
cin >> c[i];
for(int i = 0; i < n; i ++)
{
ac[a[i]] ++;
cc[c[i]] ++;
}//这里的ac[i]代表数组a中i的个数
for(int i = 1; i < N; i ++)
ac[i] += ac[i - 1];//这里才是前缀和
for(int i = N; i > 2; i --)
cc[i - 1] += cc[i];//后缀和
long long ans = 0;
for(int i = 0; i < n; i ++)
if(b[i])ans += (long long)ac[b[i] - 1] * cc[b[i] + 1];//注意数据有可能非常大,注意类型转换
cout << ans;
return 0;
}
分析一下时间复杂度
输入:3*n
计算a,c数组中每个数的个数:n
计算前缀、后缀和:2 * N
结果计算:n
总计:5*n + 2 * N <= 7 * N = 700000
是O(n)级别的时间复杂度
当然,本题还有其他思路,比如:排序+二分, 双指针
排序+二分
对于二分,显然是基于有序数组,所以第一步就是对三个数组进行排序,
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[N], b[N], c[N];
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i ++)
cin >> a[i];
for(int i = 0; i < n; i ++)
cin >> b[i];
for(int i = 0; i < n; i ++)
cin >> c[i];
sort(a, a + n);
sort(b, b + n);
sort(c, c + n);
long long ans = 0;
for(int i = 0; i < n; i ++)
{
int l = 0, r = n - 1, pos1, pos2;
int mid;
while(l < r)
{
mid = (r + l) >> 1;
if(a[mid] < b[i])
{
l = mid + 1;
continue;
}
if(a[mid] >= b[i])
{
r = mid;
continue;
}
}
pos1 = a[r] < b[i] ? r : r - 1;
l = 0, r = n - 1;
while(l < r)
{
mid = (r + l) >> 1;
if(c[mid] <= b[i])
{
l = mid + 1;
continue;
}
if(c[mid] > b[i])
{
r = mid;
continue;
}
}
pos2 = c[l] > b[i] ? l : l + 1;
ans += (long long)(n - pos2) * (pos1 + 1);
}
cout << ans;
return 0;
}
双指针
因为对三个数组排好序后,随着bi的后移,a中小于bi的个数会增加,c中大于bi的个数会减少,只需要不断移动指针即可
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[N], b[N], c[N];
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i ++)
cin >> a[i];
for(int i = 0; i < n; i ++)
cin >> b[i];
for(int i = 0; i < n; i ++)
cin >> c[i];
sort(a, a + n);
sort(b, b + n);
sort(c, c + n);
long long ans = 0;
int pos1 = 0, pos2 = 0;
for(int i = 0; i < n; i ++)
{
while(a[pos1] < b[i] && pos1 < n)
pos1 ++;
while(c[pos2] <= b[i] && pos2 < n)
pos2 ++;
ans += (long long)pos1 * (n - pos2);
}
cout << ans;
return 0;
}
虽然是两层循环嵌套,但是指针的移动是单向的,并且不发生回溯,所以等价于一层循环,还是远远小于n^2级别的。
注意:
1.由于数据是在100000的范围,而且出现了乘法,所以要考虑int存储数据是否会发生溢出,是否需要类型转换
2.这种枚举类型的题目首先要分析数据范围,之后根据数据范围再确定算法,否则就会做很多的无用功

浙公网安备 33010602011771号