枚举算法刷题笔记【蓝桥杯】

枚举

  • 枚举算法是我们在日常中使用到的最多的一个算法,它的核心思想就是:枚举所有的可能。
  • 枚举法的本质就是从所有候选答案中去搜索正确的解,使用该算法需要满足两个条件:
    (1)可预先确定候选答案的数量;
    (2)候选答案的范围在求解之前必须有一个确定的集合。
  • 枚举算法简单粗暴,他暴力的枚举所有可能,尽可能地尝试所有的方法。虽然枚举算法非常暴力,而且速度可能很慢,但确实我们最应该优先考虑的!因为枚举法变成实现最简单,并且得到的结果总是正确的。
  • 枚举算法分为循环枚举、子集枚举、排列枚举三种。

[蓝桥杯 2020 省 AB2] 回文日期

2020 年春节期间,有一个特殊的日期引起了大家的注意:2020 年 2 月 2 日。因为如果将这个日期按 yyyymmdd 的格式写成一个 8 8 8 位数是 20200202,恰好是一个回文数。我们称这样的日期是回文日期。

有人表示 20200202 是“千年一遇” 的特殊日子。对此小明很不认同,因为不到 2 年之后就是下一个回文日期:20211202 即 2021 年 12 月 2 日。

也有人表示 20200202 并不仅仅是一个回文日期,还是一个 ABABBABA 型的回文日期。对此小明也不认同,因为大约 100 100 100 年后就能遇到下一个 ABABBABA 型的回文日期:21211212 即 2121 年12 月12 日。算不上“千年一遇”,顶多算“千年两遇”。

给定一个 8 位数的日期,请你计算该日期之后下一个回文日期和下一个 ABABBABA 型的回文日期各是哪一天。

输入格式

输入包含一个八位整数 N N N,表示日期。

输出格式

输出两行,每行 1 1 1 个八位数。第一行表示下一个回文日期,第二行表示下
一个 ABABBABA 型的回文日期。

样例输入 #1

20200202

样例输出 #1

20211202
21211212

提示

对于所有评测用例, 10000101 ≤ N ≤ 92200229 10000101 \le N \le 92200229 10000101N92200229,保证 N N N 是一个合法日期的 8 8 8 位数表示。

蓝桥杯 2020 第二轮省赛 A 组 G 题(B 组 G 题)。

思路

  • 一开始想到的是纯暴力,根据给出的 N N N不断++,然后判断是否符合日期、回文、ABABBABA条件。看网上的信息好像这样也可以过,但我没试,因为这种解法没有写的必要
  • 然后,上网找到的一个解法比较巧妙:改变传统的枚举方式,不用 N + + N++ N++。通过给出的 N N N然后取其前4位,构造出回文和ABABBABA,接着判断是否符合日期条件。这样就减少了很多枚举项

题解

#include 
using namespace std;

int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

bool check(int date){
	int year = date/10000;
	int month = (date/100)%100;
	int day = date%100;
	if(month < 0 || month > 12) return false;
	if(day == 0 || month != 2 && day > days[month]) return false;
	if(month==2){
		int leap = year%4==0&&year%100!=0||year%400==0;
		if(day>days[month]+leap) return false;
	}
	return true;	
}
int main(){
  int N;
  int date1,date2;
  cin>>N;
  int year = N/10000;
  for(int i=year;;++i){
  	date1=i;
	int x=i;
  	for(int j=0;j<4;++j){
		date1 = date1*10+x%10;
		x/=10;
	}
	if(date1>N&&check(date1)) 
	{
		cout<<date1<<endl;
		break;
	}
  }
  
  int ab=N/1000000;
  for(int i=ab;;++i){
  	int a=i/10;
  	int b=i%10;
  	int x=b*10+a;
  	date2 = i*1000000+ i*10000 +x*100 +x;
	if(date2>N&&check(date2)) {
		cout<<date2<<endl;
		break;
	}
  }
}

[蓝桥杯 2020 省 AB2] 子串分值

对于一个字符串 S S S, 我们定义 S S S 的分值 f ( S ) f(S) f(S) S S S 中恰好出现一次的字符个数。例如 f ( ′ ′ a b a ′ ′ ) = 1 f\left({ }^{\prime \prime} \mathrm{aba}{ }^{\prime \prime}\right)=1 f(′′aba′′)=1 f ( ′ ′ a b c ′ ′ ) = 3 f\left({ }^{\prime \prime} \mathrm{abc}{ }^{\prime \prime}\right)=3 f(′′abc′′)=3 f ( ′ ′ a a a a ′ ′ ) = 0 f\left({ }^{\prime \prime} \mathrm{aaa} \mathrm{a}^{\prime \prime}\right)=0 f(′′aaaa′′)=0

现在给定一个字符串 S [ 0.. n − 1 ] S[0 . . n-1] S[0..n1](长度为 n n n),请你计算对于所有 S S S 的非空 子串 S [ i . . j ] ( 0 ≤ i ≤ j < n ) S[i . . j](0 \leq i \leq jS[i..j](0ij<n) f ( S [ i . . j ] ) f(S[i . . j]) f(S[i..j]) 的和是多少。

输入格式

输入一行包含一个由小写字母组成的字符串 S S S

输出格式

输出一个整数表示答案。

样例输入 #1

ababc

样例输出 #1

21

提示

对于 20 % 20 \% 20% 的评测用例, 1 ≤ n ≤ 10 1 \leq n \leq 10 1n10;

对于 40 % 40 \% 40% 的评测用例, 1 ≤ n ≤ 100 1 \leq n \leq 100 1n100;

对于 50 % 50 \% 50% 的评测用例, 1 ≤ n ≤ 1000 1 \leq n \leq 1000 1n1000;

对于 60 % 60 \% 60% 的评测用例, 1 ≤ n ≤ 10000 1 \leq n \leq 10000 1n10000;

对于所有评测用例, 1 ≤ n ≤ 100000 1 \leq n \leq 100000 1n100000

蓝桥杯 2020 第二轮省赛 A 组 H 题(B 组 H 题)。

思路

  • 自己思考的:尺取、然后判断每个字符是否重复出现。这种肯定TLE,时间复杂度 O ( n ) O(n) O(n)
  • 网上看到的一种非常巧妙的方法:每一个字母贡献的子串数,为其下标分别与前一个相同字母下标和后一个相同字母下标相减的绝对值相乘,再累计每一个字母即为答案
  • 具体为:ababc,第3个a到第1个a的位置,到最后的位置。据此统计出第3个 a a a f ( S [ i . . j ] ) f(S[i . . j]) f(S[i..j])贡献了多少值。这样时间复杂度降到 O ( n ) O(n) O(n)

题解

#include
using namespace std;

const int N=1e5+5;
int pre[N],nex[N],a[27];
string s;

int main(){
	cin>>s;
	s="0"+s;						 
	int n=s.length()-1;				
	for(int i=1;i<=n;i++)			 
	{
		int c=s[i]-'a';
		pre[i]=a[c];
		a[c]=i;
	}
	for(int i=0;i<26;i++)			    
		a[i]=n+1;
	for(int i=n;i;i--)				 
	{
		int c=s[i]-'a';
		nex[i]=a[c];
		a[c]=i;
	}
	long long int ans=0;
	for(int i=1;i<=n;i++)
		ans+=(long long)(i-pre[i])*(nex[i]-i);
	cout<<ans; 
}

[蓝桥杯 2018 省 A] 倍数问题

众所周知,小葱同学擅长计算,尤其擅长计算一个数是否是另外一个数的倍数。但小葱只擅长两个数的情况,当有很多个数之后就会比较苦恼。现在小葱给了你 n n n 个数,希望你从这 n n n 个数中找到三个数,使得这三个数的和是 K K K 的倍数,且这个和最大。数据保证一定有解。

输入格式

从标准输入读入数据。

第一行包括 2 2 2 个正整数 n n n K K K

第二行 n n n 个正整数,代表给定的 n n n 个数。

输出格式

输出一行一个整数代表所求的和。

样例输入 #1

4 3
1 2 3 4

样例输出 #1

9

提示

【样例解释】

选择 2 2 2 3 3 3 4 4 4

【数据约定】

对于 30 % 30\% 30% 的数据, n ≤ 100 n \le 100 n100

对于 60 % 60\% 60% 的数据, n ≤ 1000 n \le 1000 n1000

对于另外 20 % 20\% 20% 的数据, K ≤ 10 K \le 10 K10

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105 1 ≤ K ≤ 1 0 3 1 \le K \le 10^3 1K103,给定的 n n n 个数均不超过 1 0 8 10^8 108

时限 1 秒, 256M。蓝桥杯 2018 年第九届省赛

思路

  • 我刚开始看这道题的时候,感觉和下方的[蓝桥杯 2020 省 A1] 整数小拼接有点像,所以也是用尺取法+排序,得到大数符合 k k k的倍数,优化掉小数。但是这道题需要3个数,所以我以为就是3重循环。时间复杂度 O ( n 2 l o g n ) O(n^2logn) O(n2logn),应该是不能 A C AC AC
  • 看到别人的题解:数论+枚举 (确实这道题涉及到模,应该往数论的方向思考)
  • 三个数的和对k取余结果是0,可见这三个数对k取余的和对k取余是0。( (a+b+c)%k==(a%k+b%k+c%k)%k )
  • 可以排序后利用二维栈将余数相同的数按照从小到打的顺序存储(那么在出栈的时候就是从大到小了)其实也就只用维护最大的3个数

题解

#include
using namespace std;
int a[100005];
stack<int>num[1005];
int n,k;
int main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    sort(a+1,a+1+n);
    for(int i=1;i<=n;i++){
        num[a[i]%k].push(a[i]);
    }
    int res=0;
    for(int i=0;i<k;i++){
        if(num[i].empty()){
            continue;
        }
        for(int j=0;j<k;j++){
            if(num[j].empty()){
                continue;
            }
            int rm=(k-i-j+k)%k;
            int tmp=0,ans1,ans2,ans3;
            if(rm<j){
                continue;
            }
            if(num[i].size()){
                ans1=num[i].top();
                tmp+=ans1;
                num[i].pop();
                if(num[j].size()){
                    ans2=num[j].top();
                    tmp+=ans2;
                    num[j].pop();
                    if(num[rm].size()){
                        ans3=num[rm].top();
                        tmp+=ans3;
                        num[rm].pop();
                        res=max(res,tmp);
                        num[rm].push(ans3);
                    }
                    num[j].push(ans2);
                }
            }
            num[i].push(ans1);
        }
    }
    cout<<res<<endl;
}

[蓝桥杯 2018 省 B] 递增三元组

给定三个整数数组 A = [ A 1 , A 2 , ⋯   , A N ] A = [A_1, A_2,\cdots, A_N] A=[A1,A2,,AN] B = [ B 1 , B 2 , ⋯   , B N ] B = [B_1, B_2,\cdots, B_N] B=[B1,B2,,BN] C = [ C 1 , C 2 , ⋯   , C N ] C = [C_1, C_2,\cdots,C_N] C=[C1,C2,,CN]

请你统计有多少个三元组 ( i , j , k ) (i, j, k) (i,j,k) 满足:

  1. 1 ≤ i , j , k ≤ N 1 \le i, j, k \le N 1i,j,kN
  2. A i < B j < C k A_i < B_j < C_k Ai<Bj<Ck

输入格式

第一行包含一个整数 N N N

第二行包含 N N N 个整数 A 1 , A 2 , ⋯   , A N A_1, A_2,\cdots, A_N A1,A2,,AN

第三行包含 N N N 个整数 B 1 , B 2 , ⋯   , B N B_1, B_2,\cdots, B_N B1,B2,,BN

第四行包含 N N N 个整数 C 1 , C 2 , ⋯   , C N C_1, C_2,\cdots, C_N C1,C2,,CN

输出格式

一个整数表示答案

样例输入 #1

3
1 1 1
2 2 2
3 3 3

样例输出 #1

27

提示

对于 30 % 30\% 30% 的数据, 1 ≤ N ≤ 100 1 \le N \le 100 1N100

对于 60 % 60\% 60% 的数据, 1 ≤ N ≤ 1000 1 \le N \le 1000 1N1000

对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 1 0 5 1 \le N \le 10^5 1N105 0 ≤ A i , B i , C i ≤ 1 0 5 0 \le A_i, B_i, C_i \le 10^5 0Ai,Bi,Ci105

思路

  • 我自己思考:想到了对 A B C A B C ABC数组排序,然后取到第一个满足条件的数,则之后的数就不用遍历可以加和到结果上。但是,我想的仍然是类似尺取法,对 A C AC AC用指针,然后 B B B遍历,应该是不行的。所以,就像[蓝桥杯 2018 省 A] 倍数问题有3个操作对象的时候,应该不要首先考虑尺取法(因为尺取法是对2个对象的)
  • 看别人的题解:遍历 B B B数组,用 B B B数组确定 A A A C C C数组满足条件个数,然后再相乘
  • 我觉得这个解法也属于是变换枚举的思路吧

题解

#include 
#define ll long long
using namespace std;
int a[100005];
int b[100005];
int c[100005];

int main()
{
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        scanf("%d",&a[i]);
    }
    for(int i=0;i<n;i++)
    {
        scanf("%d",&b[i]);
    }
    for(int i=0;i<n;i++)
    {
        scanf("%d",&c[i]);
    }
    sort(a,a+n);
    sort(b,b+n);
    sort(c,c+n);
    ll aa=0;//a数组标记
    ll cc=0;//c数组标记
    ll ans=0;
    for(int i=0;i<n;i++)
    {
       // cout<<"----ai="<while(a[aa]<b[i]&&aa<n)//注意条件,不能等于
            aa++;
        while(c[cc]<=b[i]&&cc<n)//同上
            cc++;
      // cout<<"ai="<+=aa*(n-cc);
    }
    cout<<ans<<endl;
}

前缀和

前缀和是指某序列的前n项和,可以把它理解为数学上的数列的前n项和,而差分可以看成前缀和的逆运算。合理的使用前缀和与差分,可以将某些复杂的问题简单化。

[NOIP2005]校门外的树

某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米。我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置;数轴上的每个整数点,即0,1,2,……,L,都种有一棵树。

由于马路上有一些区域要用来建地铁。这些区域用它们在数轴上的起始点和终止点表示。已知任一区域的起始点和终止点的坐标都是整数,区域之间可能有重合的部分。现在要把这些区域中的树(包括区域端点处的两棵树)移走。你的任务是计算将这些树都移走后,马路上还有多少棵树。

输入描述

第一行有两个整数:L(1 <= L <= 10000)和 M(1 <= M <= 100),L代表马路的长度,M代表区域的数目,L和M之间用一个空格隔开。接下来的M行每行包含两个不同的整数,用一个空格隔开,表示一个区域的起始点和终止点的坐标。

输出描述

包括一行,这一行只包含一个整数,表示马路上剩余的树的数目。

示例一
输入

500 3
150 300
100 200
470 471

输出

298

备注

对于20%的数据,区域之间没有重合的部分;
对于其它的数据,区域之间有重合的情况。

  • 这道题是比较典型,比较简单的前缀和题目
  • 依此累加树数量的状态,最后只需遍历一遍数组即可得到答案
  • 注意一下——memset的用法(只能对char数组使用)

题解

#include
using namespace std;
int main(){
    int L,M,start,end,ans;
    char a[10005];
    scanf("%d%d",&L,&M);
    memset(a, '1', L+1);
    for(int i=0;i<M;++i){
        scanf("%d%d",&start,&end);
        memset(a+start, '0', end-start+1);
    }
    for(int i=0;i<strlen(a);++i){
        if(a[i]=='1')++ans;
    }
    printf("%d",ans);
}

后缀和

b[i]=a[i]+a[i+1]+…+a[n-2]+a[n-1];

二维前缀和

b[i][j]=b[i-1][j]+b[i][j-1]-b[i-1][j-1]+a[i][j]

差分

差分可以看成前缀和的逆运算。
差分数组
首先给定一个原数组a:a[1], a[2], a[3], a[n];

然后我们构造一个数组b : b[1] ,b[2] , b[3], b[i];

使得 a[i] = b[1] + b[2 ]+ b[3] +, + b[i]

也就是说,a数组是b数组的前缀和数组,反过来我们把b数组叫做a数组的差分数组。换句话说,每一个a[i]都是b数组中从头开始的一段区间和。

考虑如何构造差分b数组?


最为直接的方法:
如下:

a[0 ]= 0;
b[1] = a[1] - a[0];
b[2] = a[2] - a[1];
b[3] =a [3] - a[2];
b[n] = a[n] - a[n-1];
我们只要有b数组,通过前缀和运算,就可以在O(n) 的时间内得到a数组 。

二分

基本思想是将数组按照中间元素分为两个部分,然后判断需要查找的元素在哪一部分中,这样每次查找就可以将待查找的范围缩小一半,直到找到目标元素或者确定目标元素不存在为止。

[蓝桥杯 2021 省 B] 杨辉三角形

下面的图形是著名的杨辉三角形:

枚举算法刷题笔记【蓝桥杯】_第1张图片
提示

对于 20 % 20 \% 20% 的评测用例, 1 ≤ N ≤ 10 1 \leq N \leq 10 1N10;

对于所有评测用例, 1 ≤ N ≤ 1 0 9 1 \leq N \leq 10^9 1N109

蓝桥杯 2021 第一轮省赛 B 组 H 题。

思路

  • 我自己思考:根据给出的 N N N,然后取其一半,如果相加等于 N N N并且符合相邻可相加的条件就找出其位置。如果不符合,让 N / 2 N/2 N/2-1,然后继续判断,不断循环直到找到符合条件的为止。但是不知道怎么实现,而且这和我学过、做过的枚举算法都没有任何一点相似
  • 看别人的题解:发掘杨辉三角的规律:n最早出现的位置一定在左半边,而且最中间的是该行最大的数,因此只需记录有效数据即可
  • 对于同一行,列数越大对应的数值也越大。而且某一行的某一列的值为x,在列数不变的情况下,无论行数怎么变大都不会再出现比x小的数;同理再行数不变的情况下列数怎么变大也不会出现比x小的数。并且得知n小于等于10的0次方时,有效列数为0-16列。因此我们可以一列一列的考虑,由于随着行号的变大,数值时单调递增的,其知道了行号、列号对应的数值也就知道了,于是便可以二分行号,使用二分查找的方法来计算本题。

题解

#include 
typedef long long ll;
using namespace std;
ll N;
ll C(int a, int b)//求第i行第j列的值
{
	ll res = 1;
	for (ll i = a, j = 1; j <= b; i--, j++)
	{
		res = res * i / j;
		if (res > N)//如果中间结果超过N就直接返回
			return res;
	}
	return res;
}
int main()
{
	cin >> N;
	for (int k = 16; k >= 0; k--)//一列一列的找
	{
		ll l = 2 * k, r = max(N, l), mid;
		while (l <= r) {//对第k列二分查找行
			mid = l + (r - l) / 2;//二分行
			ll CC = C(mid, k);
			if (CC == N)
				break;
			else if (CC > N)
				r = mid - 1;
			else
				l = mid + 1;
		}
		if (C(mid, k) == N)
		{//第mid行、第k列的数就是N
			cout << (mid + 1) * mid / 2 + k + 1 << endl;
			//杨辉三角形的行数符号公差为1的等差数列,故用等差数列求和公式
			//加上第几列再加上1(因为列从0开始)即可得出该数的位置
			break;
		}
	}
}

尺取法

尺取法也叫双指针法

尺取法通常是对数组保存一对下标,即所选取的区间的左右端点,然后根据实际情况不断地推进区间左右端点以得出答案。尺取法比直接暴力枚举区间效率高很多,尤其是数据量大的时候,所以说尺取法是一种高效的枚举区间的方法,是一种技巧,一般用于求取有一定限制的区间个数或最短的区间等等。当然任何技巧都存在其不足的地方,有些情况下尺取法不可行,无法得出正确答案,所以要先判断是否可以使用尺取法再进行计算。

  • 尺取法通常适用于选取区间有一定规律,或者说所选取的区间有一定的变化趋势的情况,通俗地说,在对所选取区间进行判断之后,我们可以明确如何进一步有方向地推进区间端点以求解满足条件的区间,如果已经判断了目前所选取的区间,但却无法确定所要求解的区间如何进一步得到根据其端点得到,那么尺取法便是不可行的
  • 首先,明确题目所需要求解的量之后,区间左右端点一般从整个数组的起点开始,之后判断区间是否符合条件再根据实际情况变化区间的端点求解答案

[蓝桥杯 2020 省 A1] 整数小拼接

给定一个长度为 n n n 的数组 A 1 , A 2 , ⋯   , A n A_1,A_2,\cdots,A_n A1,A2,,An。你可以从中选出两个数 A i A_i Ai A j A_j Aj( i ≠ j i\neq j i=j),然后将 A i A_i Ai A j A_j Aj 一前一后拼成一个新的整数。例如 12345 可以拼成 1234534512。注意交换 A i A_i Ai A j A_j Aj 的顺序总是被视为 2 2 2 种拼法,即便是 A i = A j A_i=A_j Ai=Aj 时。

请你计算有多少种拼法满足拼出的整数小于等于 K K K

输入格式

第一行包含 2 2 2 个整数 n n n K K K

第二行包含 n n n 个整数 A 1 , A 2 , ⋯   , A n A_1,A_2,\cdots,A_n A1,A2,,An

输出格式

一个整数代表答案。

样例输入 #1

4 33
1 2 3 4

样例输出 #1

8

提示

对于 30 % 30\% 30% 的评测用例 1 ≤ n ≤ 1000 1\le n\le1000 1n1000 1 ≤ k ≤ 1 0 8 1\le k\le10^8 1k108 1 ≤ A i ≤ 1 0 4 1\le A_i\le10^4 1Ai104

对于所有评测用例, 1 ≤ n ≤ 1 0 5 1\le n\le10^5 1n105 1 ≤ k ≤ 1 0 10 1\le k\le10^{10} 1k1010 1 ≤ A i ≤ 1 0 9 1\le A_i\le10^9 1Ai109

蓝桥杯 2020 第一轮省赛 A 组 H 题。

思路

  • 自己思考能思考到尺取法,但是 O ( n 2 ) O(n^2) O(n2)配上 1 0 9 10^9 109数据量会TLE。但想不到什么优化的方法
  • 看到别人的思路:左指针从头开始,右指针从尾开始(尺取法)。如果拼接后的数字小于 k k k,那么因为 r r r 指针指向的是目前的最大值,所以 [ l , r ] [l,r] [l,r] 区间内的所有数字与 l l l 拼接的结果都小于 k k k,所以答案加上 r − l r - l rl,并且更新左指针 l + + l ++ l++
  • 如果拼接后的数字等于 k k k,同理答案加上 r − l r - l rl,但是此时需要同时更新左、右指针 l + + , r – − l ++, r –- l++,r
  • 如果拼接后的数字大于 k k k,不更新答案,更新右指针 r – − r –- r 即可。

题解

#include  
using namespace std;
const int N = 1e5 + 5;
typedef long long ll;
ll a[N];
string s[N], str;
ll n, k;
int cmp(string s1, string s2) { 
    if (s1.size() == s2.size()) { 
        if (s1 == s2) return 0;
        else if (s1 < s2) return 1;
        else return -1;
    }
    if (s1.size() < s2.size()) return 1;
    else return -1;
}
void init() {
	cin >> n >> k;
    for (int i = 1; i <= n; i ++ ) cin >> a[i];
    sort(a + 1, a + 1 + n);
    for (int i = 1; i <= n; i ++ ) s[i] = to_string(a[i]);
    str = to_string(k);
}
int main() {
	init();
    ll res = 0;
    int l = 1, r = n;
    while(l <= r) {
        int t = cmp(s[l] + s[r], str);
        if(t == 1) {
            res += r - l;
            l ++;
        } else if(t == 0) { 
            res += r - l;
            l ++, r --;
        } else r --; 
    }
    l = 1, r = n;
    while(l <= r) {
        int t = cmp(s[r] + s[l], str);
        if(t == 1) {
            res += r - l;
            l ++;
        } else if(t == 0) {
            res += r - l;
            l ++, r --;
        } else r --;  
    }
    cout << res;
}

[蓝桥杯 2018 省 B] 日志统计

小明维护着一个程序员论坛。现在他收集了一份“点赞”日志,日志共有 N N N 行。其中每一行的格式是 ts id,表示在 t s ts ts 时刻编号 i d id id 的帖子收到一个“赞”。

现在小明想统计有哪些帖子曾经是“热帖”。如果一个帖子曾在任意一个长度为 D D D 的时间段内收到不少于 K K K 个赞,小明就认为这个帖子曾是“热帖”。

具体来说,如果存在某个时刻 T T T 满足该帖在 [ T , T + D ) [T,T+D) [T,T+D) 这段时间内(注意是左闭右开区间)收到不少于 K K K 个赞,该帖就曾是“热帖”。

给定日志,请你帮助小明统计出所有曾是“热帖”的帖子编号。

输入格式

第一行包含三个整数 N N N D D D K K K

以下 N N N 行每行一条日志,包含两个整数 t s ts ts i d id id

输出格式

按从小到大的顺序输出热帖 i d id id。每个 i d id id 一行。

样例输入 #1

7 10 2  
0 1  
0 10    
10 10  
10 1  
9 1
100 3  
100 3

样例输出 #1

1  
3

提示

对于 50 % 50\% 50% 的数据, 1 ≤ K ≤ N ≤ 1000 1 \le K \le N \le 1000 1KN1000

对于 100 % 100\% 100% 的数据, 1 ≤ K ≤ N ≤ 1 0 5 1 \le K \le N \le 10^5 1KN105 0 ≤ i d , t s ≤ 1 0 5 0 \le id, ts \le 10^5 0id,ts105

时限 1 秒, 256M。蓝桥杯 2018 年第九届省赛

思路

  • 直接枚举肯定会超时,想到用双指针求出 [ T , T + D ) [T,T+D) [T,T+D)时间内的点赞数
  • 首先用一个二维数组记录在 i i i 贴子收到赞的时间 t t t

题解

#include 
using namespace std;
int t,n,k;
vector<int> q[100010];
int a[100010];
bool check(int x){
	if(q[x].size()<k) return 0;
	sort(q[x].begin(),q[x].end());
	int l=0,r=0,res=0;
	while(l<=r&&r<(int)q[x].size()){
		if(q[x][r]-q[x][l]<n) {
			r++;
			res=r-l;
			if(res>=k) return 1;
		}
        else l++;
	}
	return 0;
}
int main(){
	cin>>t>>n>>k;
	while(t--)	{
		int ts,id;
		cin>>ts>>id;
		q[id].push_back(ts);
	}
	int cnt=0;
	for(int i=0;i<=1e5;i++) if(check(i)) cout<<i<<endl;
}

关于尺取法的总结

  1. 右端点的循环结束条件来自题意
  2. 左端点的循环一般是从头到尾的
  3. l l l左端点, r r r右端点

总结

  • 枚举初始的思路我都能想到,这个比较简单,但是要拿到更好的分数或者是AC的话,基本都需要优化,优化经常想不到
  • 出现的方法中较多的是:改变枚举思路,往往我一开始想到的就是很传统的枚举方式,而根据题目条件适当改变枚举方式是值得学习的
  • 其他的就是尺取法和二分了,或者是枚举+别的算法思想
  • 有时候也需要多开数组(或者是pair,map),并查集和枚举算法的常用化简,或者是堆优化
  • 写题的时候首先往改变枚举思路思考因为这种方法出现的题目较多,然后在往其他学过的算法知识上靠,尽量别出现自己临场想的算法

你可能感兴趣的:(算法刷题笔记,算法)