贪心T2反野题解

T2 反 野

题目背景

\(wzry\) 中,打野是前期节奏的引领者,是中期运营的指挥者,也是后期团战的核心。但在进行惊心动魄的团战前,首先你要有良好的发育。其中,在合适的时机反对面的野区是一个非常重要的点,不仅可以获得更多资源,也可以遏制对方打野的发育。

题目描述

在我方野区中有 \(n\) 种不同的野怪,杀死第 \(i\) 种野怪可以提供的金币为 \(a_i\) ,假设每一种野怪都有无数只。为了方便,我们把野怪种数为 \(n\) ,提供金币数组为 \(a_1..._n\) 的野区记作( \(n\)\(a\) )。

刷野区得到的经济可以表示为 \(t_i\) \(∗\) $ a_i $ 的和,对于一个非负整数 \(x\) ,若 \(t_i\) \(∗\) \(a_i\) 的和等于 \(x\) ,那么 \(x\) 则可以被表示出来。反之则不能被表示出来。例如在野区 \(n\) = 3,\(a\) = [ 2 ,5, 9 ]中,1, 3就无法被表示出来。

现在我方韩信想找到一部分地方的野区,满足和我方野区是等价的。两个野区( \(n\) , \(a\) )和( \(m\) , \(b\) )是等价的,当且仅当对于任意非负整数 \(x\) ,它要么均可以被两个野区表示出来,要么不能被其中任何一个表示出来。

现在韩信想要找到一个尽可能小的野区( \(m\) , \(b\) ),减少被抓死的可能性。野区( \(m\) , \(b\) )满足与自家野区( \(n\) , \(a\) )等价,且 \(m\) 尽可能小。由于韩信苦心练习二一横扫接大招,忘记学习意识了,所以需要你来编程解决这个问题:找到最小的 \(m\)

输入格式

输入文件的第一行包含整数 \(T\) ,表示数据的组数。

接下来按照如下格式分别给出 \(T\) 组数据。每组数据的第一行包含一个正整数 \(n\) 。接下来一行包含 \(n\) 个由空格隔开的正整数 \(a_i\)

输出格式

输出文件共有 \(T\) 行,对于每组数据,输出一行一个正整数,表示所有与( \(n\) , \(a\) )等价的野区( \(m\) , \(b\) )中,最小的\(m\)

输入输出样例

输入 #1

2
4
3 19 10 6
5
11 29 13 19 17

输出 #1

2
5

说明/提示

在第一组数据中,野区(2,[3,10])和给出的野区( \(n\) , \(a\) )等价,并可以验证不存在\(m\)<2的等价的野区,因此答案为2。在第二组数据中,可以验证不存在 \(m\) < \(n\) 的等价的野区,因此答案为5

【数据范围与约定】

数据范围

对于 100% 的数据,满足 \(1 ≤ T ≤ 20\) , $ n $ , \(a_i ≥ 1\)

题解(P5020 货币系统)

由于出题时间短,我只能嫖了一道NOIp2018提高组的题目,只是改了一下题面,其它都一模一样。

出完题之后我开始思考这个题到底应该怎么做。其实我看到这个题之后心里想:哇,这个题也太麻烦了,不仅要使有的 \(x\) 都能被两个集合表示出来,而且还要使有的 \(x\) 都不能被表示出来,这咋办啊,难道要暴力搜索,枚举出所有情况然后再找吗?但是这样不就裂开了吗,我出的是贪心啊。

感性理解

但是我在思考的过程中,有一种冥冥的感觉:答案集合里的数应该都包含在所给集合里面。换句话说,设所给集合为 \(A\) ,答案集合为 \(B\) ,那么 $ B \subseteq A $ 。我们首先感性理解一下,如果说 \(B\) 集合中存在一个不在 \(A\) 中的元素,还存在 \(x\) 使得可以被两个集合中的元素表示出来。因为 \(x\) 是任意的,所以我们就让 \(x\) 为那个不在 \(A\) 中但是在 \(B\) 中的元素,因为 \(x\) 就是这个元素,所以肯定能被 \(B\) 表示出来,那么它同时也要能被 \(A\) 表示出来才满足题意,那么只能通过 \(A\) 中元素加和的方式得出。但是 \(A\) 中这些加和的元素在 \(A\) 中,为了满足题意 \(B\) 中也必须能凑出这些元素。那么我们这个时候可以发现,如果我可以通过 \(B\) 中那些更小的元素来凑出 \(A\) 中大一些的元素,再通过加和凑出那个一开始我们选的那个存在于 \(B\) 中而不存在于 \(A\) 中的元素,那么我们为什么要选一开始那个元素呢?因为我们要使答案集合里的元素个数尽可能少,所以一定不能选择可以通过原有元素加和表示出来的元素,因为这样相当于是浪费。例如:\(B\) 中有一个元素为11,\(A\) 中的 6 , 5 可以通过加和来表示出11,然后 \(B\) 中的 2 , 3(6 ,5) 又可以通过加和的方式表示出 6 , 5 ,那么我们可以直接用 2 , 3表示出11,就不再需要11这个数了,因为11能表示的数我一定可以通过 2 , 3来表示出来。不一定有这种符合条件的情况,我只是举个例子,不要杠。

证明

是不是感觉我刚刚说的很有道理?但还是要证明。要是在考场上,我还没证完呢人家都做完\(T3\)了,所以我觉得还是感性理解比较重要\(QWQ\)

我能不能说一句“详情查看洛谷P5020货币系统题解“然后离开

证明之后的思路

当我们证明 $ B \subseteq A $ 之后,那么其实这道题就转变成立一道完全背包问题。首先我们将 \(A\) 集合中的元素从小到大排序,由于最小的元素肯定不能通过其它元素加和得到,所以答案集合里面肯定要包含这个最小的数。而且认真想一想很容易能想到,如果不能由 \(B\) 集合中的元素通过加和得到,那么我们肯定是要把小的数先扔进去更优。比如说 \(B\) 中有个10,这时候 \(A\) 中有19 , 9,如果我们不排序的话,我们既要将19装进 \(B\) ,也要将9装进 \(B\) ,而这样显然就不是最优解,这里也算是一个小小的贪心。当我们找到第一个数之后就可以进行完全背包了。首先循环 \(A\) 集合里面的元素,每一个元素我都要在 \(B\) 集合里面做一次完全背包。如果可以通过当前 \(B\) 集合里面的数表示出来(其实也就是可以通过 \(A\) 集合里面其它的数表示出来,这样的数其实是废物,因为有没有它没有影响),那么我就继续找下一个 \(A\) 中的元素。反之,如果不能用 \(B\) 中的元素通过加和得到,那么我们直接将答案++ ,然后将这个数直接加入到 \(B\) 集合中。

代码

#include
#include
#include
#include
#include
using namespace std;
int t,n,sum;//sum记录B中元素的个数 
int a[110],b[110];//最差的情况也是m=n,所以数组开到110即可 
bool f[25010];//f数组记录这个数能不能由B中的数加和得到,由于它相当于背包中的体积,所以数组开到数组a中元素的最大值 
int main()
{
	scanf("%d",&t);
	while(t--){
		memset(a,0,sizeof(a));
		memset(b,0,sizeof(b));
		memset(f,0,sizeof(f));//不要忘记每组数据都要清空数组 
		scanf("%d",&n);
		for(int i=1;i<=n;i++){
			scanf("%d",&a[i]);
		}
		sort(a+1,a+n+1);
		b[1]=a[1];//将第一个数加入B 
		sum=1;
		f[0]=1;//首先要将f[0]初始化,因为如果两个数相等肯定要由f[0]转移而来,显然这是合法的,所以f[0]=1 
		for(int i=2;i<=n;i++){
			for(int k=1;k<=sum;k++){
				for(int j=b[k];j<=a[i];j++){//完全背包的板子 
				    if(f[j-b[k]]==1){
						f[j]=1;
					}
				}
			}
			if(f[a[i]]!=1){//如果B中的元素不能加和得到A中的第i个元素,那么长度加1并将这个数加入B中 
				sum++;
				b[sum]=a[i];
			}
		} 
		printf("%d\n",sum);
	}
	return 0;
}

然后你猜怎么着?我写的std裂了。

\(TLE+80pts\)

那么我们考虑怎么优化。

法一:

\(卡常优化+吸氧+inilne+register+快读快写=AC\)

AC代码

#define fastcall __attribute__((optimize("-O3")))
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")
#pragma GCC optimize("-fgcse")
#pragma GCC optimize("-fgcse-lm")
#pragma GCC optimize("-fipa-sra")
#pragma GCC optimize("-ftree-pre")
#pragma GCC optimize("-ftree-vrp")
#pragma GCC optimize("-fpeephole2")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-fsched-spec")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("-falign-jumps")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-falign-labels")
#pragma GCC optimize("-fdevirtualize")
#pragma GCC optimize("-fcaller-saves")
#pragma GCC optimize("-fcrossjumping")
#pragma GCC optimize("-fthread-jumps")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-freorder-blocks")
#pragma GCC optimize("-fschedule-insns")
#pragma GCC optimize("inline-functions")
#pragma GCC optimize("-ftree-tail-merge")
#pragma GCC optimize("-fschedule-insns2")
#pragma GCC optimize("-fstrict-aliasing")
#pragma GCC optimize("-falign-functions")
#pragma GCC optimize("-fcse-follow-jumps")
#pragma GCC optimize("-fsched-interblock")
#pragma GCC optimize("-fpartial-inlining")
#pragma GCC optimize("no-stack-protector")
#pragma GCC optimize("-freorder-functions")
#pragma GCC optimize("-findirect-inlining")
#pragma GCC optimize("-fhoist-adjacent-loads")
#pragma GCC optimize("-frerun-cse-after-loop")
#pragma GCC optimize("inline-small-functions")
#pragma GCC optimize("-finline-small-functions")
#pragma GCC optimize("-ftree-switch-conversion")
#pragma GCC optimize("-foptimize-sibling-calls")
#pragma GCC optimize("-fexpensive-optimizations")
#pragma GCC optimize("inline-functions-called-once")
#pragma GCC optimize("-fdelete-null-pointer-checks")//卡常优化代码
#include
#include
#include
#include
#include
using namespace std;
int t,n,sum;//sum记录B中元素的个数 
int a[110],b[110];//最差的情况也是m=n,所以数组开到110即可 
bool f[25010];//f数组记录这个数能不能由B中的数加和得到,由于它相当于背包中的体积,所以数组开到数组a中元素的最大值 
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}//快读
inline void write(int x){//别忘了inline        
    if(x<0){        
        putchar('-');        
        x=-x;        
    }        
    if(x>9){
        write(x/10);
	}
    putchar(x%10+'0');     
    return;        
}//快写        
int main()
{
	t=read();
	while(t--){
		memset(a,0,sizeof(a));
		memset(b,0,sizeof(b));
		memset(f,0,sizeof(f));//不要忘记每组数据都要清空数组 
		n=read();
		for(register int i=1;i<=n;i++){
            a[i]=read();
		}
		sort(a+1,a+n+1);
		b[1]=a[1];//将第一个数加入B 
		sum=1;
		f[0]=1;//首先要将f[0]初始化,因为如果两个数相等肯定要由f[0]转移而来,显然这是合法的,所以f[0]=1 
		for(register int i=2;i<=n;i++){
			for(register int k=1;k<=sum;k++){
				for(register int j=b[k];j<=a[i];j++){//完全背包的板子+3个register 
					if(f[j-b[k]]==1){
						f[j]=1;
					}
				}
			}
			if(f[a[i]]!=1){//如果B中的元素不能加和得到A中的第i个元素,那么长度加1并将这个数加入B中 
				sum++;
				b[sum]=a[i];
			}
		} 
		write(sum);
		printf("\n");
	}
	return 0;
}

其实一开始我不抱着这样能卡过去的希望,但事实告诉我们,它做到了,而且还快了不少......(不加优化的速度是优化后的两倍)

https://www.luogu.com.cn/record/35185584

https://www.luogu.com.cn/record/35186206

经过测试,删掉四十多行代码但是吸氧可以AC,留着代码不吸氧也可以AC。但是如果不吸氧也不写四十多行代码就会TLE。(就恰好是我们考场上的条件fuck

人在江湖没文化,一句wc行天下

法2:优化循环,减少冗余情况的遍历

虽然优化强,但是难道你在考场上要背过四十多行代码(删掉这四十多行会 \(T\) 掉两个点,也就是90pts)加快读快写嘛?不过好像真的可以。但是我们还是要追求正解对吧。
其实两层循环就能搞定它。不过我拘泥于它的板子,所以用了三层循环。还是没有理解明白背包,幸亏没讲背包,不然误人子弟

#include
#include
#include
using namespace std;
int f[25005];
int a[105];
int main()
{
    int i,j,n,T,ans;
    scanf("%d",&T);
    while(T--){
        memset(f,0,sizeof(f));
        scanf("%d",&n);ans=n;
        for(i=1;i<=n;i++) scanf("%d",&a[i]);
        sort(a+1,a+n+1);
        f[0]=1;
        for(i=1;i<=n;i++){
            if(f[a[i]]){
                ans--;//如果有一个数可以由前面的数通过加和得到,那么不要这个数
                continue;
            }
            for(j=a[i];j<=a[n];j++){
                f[j]=f[j]|f[j-a[i]];//否则用完全背包来看能不能表示后面的数
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

题解完了。嫖了题又嫖了题解,我是垃圾吧

Thanks

你可能感兴趣的:(贪心T2反野题解)