枚举的一些分析

递增三元组——用统计量(前缀和)化简枚举

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.这种枚举类型的题目首先要分析数据范围,之后根据数据范围再确定算法,否则就会做很多的无用功

posted @ 2021-02-27 02:51  kanbujian55  阅读(76)  评论(0)    收藏  举报