[NOIP2016 普及组] 魔法阵(枚举的典型例题,值得一看)

在透视这道题之前,先用一道小题来练练手

密码锁(这道题是我根据魔法阵改的)

Description

叮当在探索迷宫时被一扇大门挡住了去路,门上刻画了很多乱七八糟的数字,门被锁上了,无法打开,但是门上给出了提示:

1:正确按下门上的三个数字即可打开大门;

2:这三个数必须满足递增排列按下;

3:后两个数之差比前两个数之差的8倍还要大。

现在叮当想知道一共有多少种可能的情况来确定开门时间(对于门上不同位置的相同数字算不同的情况)。

Format
Input

第一行一个正整数n表示门上的数字(3<=n<=10000)。

第二行为n个正整数,表示每个数的值x(1<=x<=10000)。

Output

密码的可能情况。

Samples
Sample Input 1
11
2 2 3 3 4 4 4 13 13 13 13
Sample Output 1
40
Limitation

(2,3,13)共16种

(3,4,13)共24种

一共40种可能情况

对于30%的数据,3<=n<=500,1<=x<=500;

对于60%的数据,3<=n<=1500,x<=1500;

对于100%的数据,3<=n<=10000,1<=x<=10000;

思路

该题的本质就是选择三个数a,b,c,要求满足:

a<b<c,并且8*(b-a)<c-b很容易想到依次枚举a,b,c,由于是从小到大,我们可以把数组排序后,依次枚举,然后判断是否满足上面条件,时间复杂度为O(n^3)。

由于我们枚举的数可能不满足第二个条件,即枚举了很多无效的答案,所以效率很低,那么我们是 否可以保证我们在枚举的时候a,b,c, 一定就满足两个条件呢?

我们可以枚举a,然后枚举t=b-a,那么得到b的值 为a+t,可以计算出c的最小值为a+9*t+1。

如果按照这种方法枚举,那么我们的枚举方法就 不再是依次枚举每一个数了,因为我们没有办法直 接知道a+9*t+1到底是哪一个数。 那么我们可以换一种方法,枚举值,数的范围是 (1,10000),那么我们可以直接枚举1-10000,每个数 不止一种,先统计每种数的数量,再用到乘法原理 ,把a,b,c三个数的数量相乘。 在枚举的时候注意a,b,c的范围,尽量保证没有无 效的枚举,为了方便,其实我们可以先枚举t=b-a, 再枚举a,这样t的范围是[1,9*t+2<=m],a的范围是 [1,m-9*t-1],可以再枚举大的值k,k的范围是 [1,a+9*t+k<=m],而c=a+9*t+k。

这种枚举的方法,保证了枚举的所有数都是满足 题目给定条件的,时间复杂度相对于暴力枚举a,b,c 提高了81倍左右。

但是上述做法仍然不是该题的正解,该题的正解需要 用到继承种的单调性。 比如3 4 100满足条件,那么2 3 100,1 3 100是 不是仍然满足条件,也就是说,对于较大的a和b, 存在x个满足条件的c,那么对于比当前a,b小的情况 ,得到满足条件的c的个数y,那么实际上总的满足 条件的情况c一共有x+y个。 所以我们可以从大到小枚举a,累加当前情况下的 c的数量,当下一次枚举a的时候,答案为当前答案 加上前面的累加和,这样我们就没有必要再去枚举大于这个条件了,省去了一层循环。

ACcode
#include<bits/stdc++.h>
using namespace std;

int num[10100],maxn=INT_MIN;
int main(){
    int n,tmp;
    cin>>n;
    for (int i=0;i<n;i++){
    	cin>>tmp;
    	num[tmp]++;
    	maxn=max(maxn,tmp);
	}
	long long cnt=0;
    for (int t=1;9*t+2<=maxn;t++){
    	int s=0;
    	for (int a=maxn-9*t-1;a>=1;a--){
    		int b=a+t,c=b+8*t+1;
    		s+=num[c];
    		cnt+=num[a]*num[b]*s; 
		}
	} 
    cout<<cnt;
	return 0;
}

这时候就有人问了,密码锁跟魔法阵有什么关系呢?

别慌,我们先把魔法阵的题目看了。

题目背景

NOIP2016 普及组 T4

题目描述

六十年一次的魔法战争就要开始了,大魔法师准备从附近的魔法场中汲取魔法能量。

大魔法师有 m 个魔法物品,编号分别为 1,2,…,m。每个物品具有一个魔法值,我们用 Xi​ 表示编号为 i 的物品的魔法值。每个魔法值 Xi​ 是不超过 n 的正整数,可能有多个物品的魔法值相同。

大魔法师认为,当且仅当四个编号为 a,b,c,d 的魔法物品满足 Xa​<Xb​<Xc​<Xd​,Xb​−Xa​=2(Xd​−Xc​),并且Xb​−Xa​<(Xc​−Xb​)/3 时,这四个魔法物品形成了一个魔法阵,他称这四个魔法物品分别为这个魔法阵的 A 物品,B 物品,C 物品,D 物品。

现在,大魔法师想要知道,对于每个魔法物品,作为某个魔法阵的 A 物品出现的次数,作为 B 物品的次数,作为 C 物品的次数,和作为 D 物品的次数。

输入格式

第一行包含两个空格隔开的正整数 n,m。

接下来 m 行,每行一个正整数,第 i+1 行的正整数表示 Xi​,即编号为 i 的物品的魔法值。

保证 1≤n≤15000,1≤m≤40000,1≤Xi≤n。每个 Xi​ 是分别在合法范围内等概率随机生成的。

输出格式

共 m 行,每行 4 个整数。第 i 行的 4 个整数依次表示编号为 i 的物品作 为 A,B,C,D 物品分别出现的次数。

保证标准输出中的每个数都不会超过 10^9。每行相邻的两个数之间用恰好一个空格隔开。

样例 #1

样例输入 #1

30 8
1
24
7
28
5
29
26
24

样例输出 #1

4 0 0 0
0 0 1 0
0 2 0 0
0 0 1 1
1 3 0 0
0 0 0 2
0 0 2 2
0 0 1 0

样例 #2

样例输入 #2

15 15
1 
2 
3 
4 
5
6 
7 
8 
9
10
11
12
13
14
15

样例输出 #2

5 0 0 0
4 0 0 0
3 5 0 0
2 4 0 0
1 3 0 0
0 2 0 0
0 1 0 0
0 0 0 0
0 0 0 0
0 0 1 0
0 0 2 1
0 0 3 2
0 0 4 3
0 0 5 4
0 0 0 5

提示

【样例解释 1】

共有 5 个魔法阵,分别为:

物品 1,3,7,6,其魔法值分别为 1,7,26,29

物品 1,5,2,7,其魔法值分别为 1,5,24,26

物品 1,5,7,4,其魔法值分别为 1,5,26,28

物品 1,5,8,7,其魔法值分别为 1,5,24,26

物品 5,3,4,6,其魔法值分别为 5,7,28,29

以物品 5 为例,它作为 A 物品出现了 1 次,作为 B 物品出现了 3 次,没有作为 C 物品或者 D 物品出现,所以这一行输出的四个数依次为 1,3,0,0。

此外,如果我们将输出看作一个 m 行 4 列的矩阵,那么每一列上的 m 个数之和都应等于魔法阵的总数。所以,如果你的输出不满足这个性质,那么这个输出一定不正确。你可以通过这个性质在一定程度上检查你的输出的正确性。

【数据规模】

读完题后你看哈,密码锁是要求按下三个数字,且有两个要求,即枚举三个数字,而魔法阵是要求四个魔法物品的值满足三个要求,是不是差不多的呢,

先魔改一下这三个要求

设t=xd−xc

所以②可化为xb−xa=2t ④

将④代入③

2t<(xc−xb)/3

移项一下,就变成

6t<xc−xb ⑤

再魔改一下

设6t+k=xc−xb(就是把差的部分补上去)

所以就有了这个图:

 既然有了这个图,那是不是就可以按照密码锁的套路来枚举了呢?只不过是要先枚举t,再在t的循环内正着枚举D和倒着枚举A就可以了。

首先由上图易得t的范围是1<=t<=(n-1)/9,并且还可以得到C=D-t。因为让A,B,C,D满足条件的最小的k是1,所以就有A=D-9t-1,B=D-7t-1;

其次就要看枚举D和枚举A的范围,首先是D:由上图可知D的max是n,而最小值则是当k=1且A=1的时候,所以D的最小值为9∗t+2,再小是无法组成魔法阵的。然后是A:A的最小值显然是1,最大值则是在D取到最大值,也就是n的时候,所以A的最大值就是n-9t-1。

范围知道了,那就直接求不就完了?所以这道题是非常非常简单的啦

ACcode

#include<bits/stdc++.h>
#define LL long long
using namespace std;

LL sum[41000],maxn=INT_MIN;
LL cmp[41000];
LL a[15100],b[15100],c[15100],d[15100];
int main(){
    int m,n;
    cin>>m>>n;
    for (int i=0;i<n;i++){
    	cin>>cmp[i];
    	sum[cmp[i]]++;
    	maxn=max(maxn,cmp[i]);
	}
    for (int t=1;9*t+1<=m;t++){
    	LL s=0;
		for (int d1=9*t+2;d1<=m;d1++){
			int c1=d1-t,b1=c1-6*t-1,a1=b1-2*t;
			s+=sum[a1]*sum[b1];
			d[d1]+=s*sum[c1];
			c[c1]+=s*sum[d1];
		}//
		s=0;
    	for (int a1=m-9*t-1;a1>=1;a1--){
    		int b1=a1+2*t,c1=b1+6*t+1,d1=c1+t;
    		s+=sum[d1]*sum[c1];
    		a[a1]+=s*sum[b1];
    		b[b1]+=s*sum[a1];
		}//
	}
	for (int i=0;i<n;i++){
		cout<<a[cmp[i]]<<" "<<b[cmp[i]]<<" "<<c[cmp[i]]<<" "<<d[cmp[i]]<<endl;
	}
	return 0;
}

看了这么久,作者也写了这么久,能不能点一个赞,在收藏一下呢?最好的话在点个关注吧

谢谢啦!

  • 26
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值