USACO4.1 Beef McNuggets【数学/结论】
吐槽/心路历程
打开这道题的时候:*&@#%*#?!这不是小凯的疑惑吗?好像还是个加强版的?我疑惑了。原来$USACO$才是真的强,不知道什么时候随随便便就押中了题目。
对于我这种蒟蒻来说,这种有结论的题真是令人头疼,又不会证明,只能猜,要是猜错了就身败名裂了。
如果是考试的时候写这种题的话,我会直接上一个完全背包,并且价值不会开到题目骗我的那个$2,000,000,000$,差不多估摸着复杂度能过就这么写。
但是还是没搞懂为什么有一个上界,然后超过那个上界的答案会输出$0$(明明程序跑出来就是凑不了那么多啊)
就是这组数据:
4
252
250
254
256
上网看别人的题解我更疑惑了,都直接甩结论的啊喂,神仙啊,怎么想到的啊喂(ノ"◑ ◑)ノ"(。•́︿•̀。)
可能我这种数论渣渣在考场上只能猜结论然后暴力验证什么了
最后还是硬着头皮看了一波最强押题人$USACO$的英文官方题解(又开始折磨我这个英语渣渣了):
第一种做法:
一来就说如果不存在最大不能买到的块数,所有盒子大小的最大公约数大于$1$,先特判这种情况(也就是如果这一堆数的最大公因数不为1,就输出0)
然后从小到大进行更新,如果$X$能被凑出来,那么$X+V_i$也能被凑出来。
然后又开始甩结论了:如果有连续$256$个值都能被凑出来,那么从此之后所有的大小都可以被凑出来。
事实上,只要有连续的$min(V_i)$个值都能被凑出来,那么从此之后所有的大小都可以被凑出来。
(题外话:记笔记 短语 from here on out 从此以后)
所以...我还是要证结论?
先证这个吧:
只要有连续的$min(V_i)$个值都能被凑出来,那么从此之后所有的大小都可以被凑出来
设最小的那个数为$V_0$。
首先考虑特殊情况,特别地,如果从$1$到$V_0$都可以被凑出来的话,那么后面的数可以由$V_0*k$+前面凑出1到$V_0$的方法来凑($k$为正整数)
然后是一般情况,假设是从$i$到$i+V_0$都可以被凑出来,相同地,后面的数都可以用$V_0*k$++前面凑出$i$到$i+V_0$的方法来凑($k$为正整数)
那么问题来了,如果一直没有连续的$V_0$个数都能被凑出来怎么办呢?
那就是不存在不能买到块数的上限,也就是$gcd$特判的那种情况。
那么,再证公约数那个:
特殊情况:如果有$1$,所有的方案都能够被凑出来,输出$0$
考虑到能够被凑出来的数只能是$gcd$的倍数。因为每个数都可以被分解成为$k*gcd$的形式,能够被凑出来的数也可以被表达为$$m_1*k_1*gcd+m_2*k_2*gcd+m_3*k_3*gcd+...+m_n*k_n*gcd=gcd*(m_1*k_1+...+m_n*k_n)$$
一定是$gcd$的倍数。(要注意反过来不成立,$gcd$的倍数不一定能被凑出来)
如果$gcd!=1$,那么被凑出来的数是不连续的,也就是没有上界。
如果$gcd==1$,当$i$(能够被凑出来的数从小到大排序形成的队列的项数(下标))足够大时,就会形成一串连续的数,当这一串连续的数达到了$V_0$个,就是之前讨论过的那种情况了。
想通之后还是不难吧。
1 /* 2 ID: Starry21 3 LANG: C++ 4 TASK: nuggets 5 */ 6 #include<cstdio> 7 #include<algorithm> 8 using namespace std; 9 #define N 300005 10 bool f[N]; 11 int n,a[15]; 12 int gcd(int x,int y) 13 { 14 if(y==0) return x; 15 return gcd(y,x%y); 16 } 17 int main() 18 { 19 //freopen("nuggets.in","r",stdin); 20 //freopen("nuggets.out","w",stdout); 21 scanf("%d",&n); 22 for(int i=1;i<=n;i++) 23 scanf("%d",&a[i]); 24 sort(a+1,a+n+1); 25 int d=a[1]; 26 bool flag=0; 27 for(int i=2;i<=n;i++) 28 { 29 d=gcd(d,a[i]); 30 if(a[i]==1) flag=1; 31 } 32 if(flag||d>1) 33 { 34 puts("0"); 35 return 0; 36 } 37 int cnt=0,j=1,ans=0; 38 f[0]=1; 39 while(1) 40 { 41 for(int i=1;i<=n;i++) 42 { 43 if(j<a[i]) continue; 44 if(f[j-a[i]]) 45 { 46 f[j]=1,cnt++; 47 break; 48 } 49 } 50 if(!f[j]) cnt=0,ans=j; 51 if(cnt==a[1]) 52 break; 53 j++; 54 } 55 printf("%d\n",ans); 56 }
第二种做法:
就是小凯的疑惑。
确实疑惑,我太疑惑了,D1T1居然做了一个上午?还有半个月就考联赛了,退役算了哭唧唧。
第一种做法明白了之后,瞬间知道了开头那里为什么超过上界之后直接输出$0$,因为那就是$gcd>1$,不存在不能买到块数的上限。
(放了张图片,快要受不了这个老是吞掉我的数学公式的编辑器了,证明放在了小凯的疑惑里)
这里不是两个互质的数,但是即使我们只用两个数(可以感性理解,如果在这两个数上再多一些数,原来不能凑成的一些数就可以被凑成,上界(有一定概率)会缩小,所以只用两个数的上界是最大的),这两个数最大就是$256$,然后上界就是$256*256-2*256$,在这个范围内进行完全背包就可以了。如果最后出来的答案超过了上界,就是不存在不能买到块数的上限。
1 /* 2 ID: Starry21 3 LANG: C++ 4 TASK: nuggets 5 */ 6 #include<cstdio> 7 #include<algorithm> 8 using namespace std; 9 #define N 512 10 #define M 65536 11 bool f[M+10]; 12 int n,a[15]; 13 int main() 14 { 15 freopen("nuggets.in","r",stdin); 16 freopen("nuggets.out","w",stdout); 17 scanf("%d",&n); 18 for(int i=1;i<=n;i++) 19 scanf("%d",&a[i]); 20 sort(a+1,a+n+1); 21 if(a[1]==1) 22 { 23 printf("0\n"); 24 return 0; 25 } 26 f[0]=1; 27 int temp; 28 for(int i=0;i<=M;i++) 29 { 30 if(f[i]) 31 for(int j=1;j<=n;j++) 32 { 33 if(i+a[j]>M) continue; 34 f[i+a[j]]=1; 35 } 36 else temp=i; 37 } 38 if(temp>M-N) temp=0; 39 printf("%d\n",temp); 40 }
第三种做法:
网上看到的,很高能,很佩服,可惜没有时间仔细研究(挖坑待填)
总结
感觉第一种做法要顺理成章一些(虽然我写得不太顺理成章),在考虑凑数的时候手推一些什么性质的时候可能会想到发掘这些结论。而第二种做法对于我这种数学渣渣来说是不会在$dp$明显会挂掉的情况下用数学去分析范围的。
最后,发现了自己去年在洛谷上过的非常暴力的做法(年少无知),对,它过了;当然,在"严谨"的$USACO$上,它,过不了!
1 /* 2 ID: Starry21 3 LANG: C++ 4 TASK: nuggets 5 */ 6 #include<cstdio> 7 #include<algorithm> 8 using namespace std; 9 #define LIMIT 20000000 10 int n,a[15],ans=-1; 11 bool f[LIMIT]; 12 int main() 13 { 14 freopen("nuggets.in","r",stdin); 15 freopen("nuggets.out","w",stdout); 16 scanf("%d",&n); 17 for(int i=1;i<=n;i++) 18 scanf("%d",&a[i]); 19 f[0]=true; 20 for(int i=0;i<=LIMIT;i++) 21 if(f[i]) 22 for(int j=1;j<=n;j++) 23 { 24 if(i+a[j]>LIMIT) continue; 25 f[i+a[j]]=true; 26 } 27 28 else ans=i; 29 if(ans==-1||ans>=LIMIT-1) printf("0"); 30 else printf("%d",ans); 31 }