1. 程式人生 > 實用技巧 >貪心T2反野題解

貪心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<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
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裂了。

\(T L E+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<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
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)加快讀快寫嘛?不過好像真的可以。但是我們還是要追求正解對吧。