题目链解:https://www.luogu.org/problemnew/show/P1631
二分枚举判断答案几乎是无敌的存在几十秒求出有m次操作的最值问题!
这题刚看完有点蒙,两个for循环的话不但爆空间,也爆时间啊!
看了大佬解释后,发现有3种做法,二分答案或优先队列(堆),还有一种类似数据结构的归并排序从前往后一一比较的思想每次选出前面几个最小的。个人倾向于二分答案。
想到二分答案并不容易,有3点关键:
1.题目给的是有序的!(有序,单调一般都能二分)
2.从小达到排,找到最大的停止!(最大值!关键二分)
3.这是一个范围问题,最小范围和最大范围上限已知!(明显二分!)
(选n个数,也就是n次,和移动石头m一样的。。)
(二分枚举判断答案本质:只要有m次操作的最值问题都可用)
另外需要注意的是,第一个for循环枚举答案的时候,完全可以找到一个点跳出达到很大的剪枝优化!(后面肯定不会有更小的了就退出完成)
1 #include<iostream> 2 #include<algorithm> 3 using namespace std; 4 const int maxn=1e6; 5 int a[maxn],b[maxn]; 6 int c[maxn];//结果数组要开大,因为可能有重复 7 int n; 8 9 int judge(int x) 10 { 11 int cnt=0; 12 for(int i=1;i<=n;i++) 13 { 14 if(cnt>n) break;//数选够了退出 15 if(a[i]+b[1]>x) break;//这个优化很重要(整个循环退出),不用枚举到底了,如果第i个数+b第一个都比x大后面(包括下一轮)肯定所有更大! 16 //这个>和j循环里的>意义不同更重大,j循环里的>退出,下一轮可能还有更小的!但第一个>退出就代表下一轮也不可能更小了! 17 for(int j=1;j<=n;j++) 18 { 19 if(a[i]+b[j]<x)//选了这个数,数的个数+1 20 { 21 cnt++; 22 if(cnt>n) break; 23 } 24 else if(a[i]+b[j]>x) break;//>这一轮退出(因为下一轮可能有更小的) 25 } 26 } 27 28 return cnt; 29 } 30 31 int main() 32 { 33 ios::sync_with_stdio(false); cin.tie(0); 34 35 cin>>n; 36 for(int i=1;i<=n;++i) cin>>a[i]; 37 for(int i=1;i<=n;++i) cin>>b[i]; 38 39 int l=a[1]+b[1],r=a[n]+b[n]; 40 int ans=0; 41 while(l<=r) 42 { 43 int mid=(l+r)/2; 44 45 int t=judge(mid); 46 if(t<=n) 47 { 48 l=mid+1; 49 ans=mid; 50 } 51 else r=mid-1; 52 } 53 54 int p=0; 55 for(int i=1;i<=n;++i) 56 { 57 if(a[i]+b[1]>ans) break;//和judge里优化一样不再重述 58 for(int j=1;j<=n;++j) 59 { 60 if(a[i]+b[j]<=ans) 61 { 62 c[++p]=a[i]+b[j]; 63 } 64 else break; 65 } 66 } 67 sort(c+1,c+1+p); 68 69 for(int i=1;i<=n-1;i++) cout<<c[i]<<" "; 70 cout<<c[n]<<endl; 71 72 return 0; 73 }
数学优化,前i*j个小的元素下表绝对不会超过i*j,即下标超过i*j的都必定是第n大元素之后的!
1 #include <iostream> 2 #include <algorithm> 3 #include <queue> 4 using namespace std; 5 const int maxn=1e6; 6 int a[maxn],b[maxn]; 7 int n; 8 struct px 9 { 10 int x; 11 bool operator<(const px &a) const 12 { 13 return x>a.x; 14 } 15 px(){}; 16 px(int X):x(X){}; 17 }; 18 priority_queue<px> que; 19 20 int main() 21 { 22 ios::sync_with_stdio(false); cin.tie(0); 23 24 cin>>n; 25 for(int i=1;i<=n;++i) cin>>a[i]; 26 for(int i=1;i<=n;++i) cin>>b[i]; 27 28 for(int i=1;i<=n;i++) 29 { 30 for(int j=1;j*i<=n;j++) 31 { 32 que.push(px(a[i]+b[j])); 33 } 34 } 35 36 for(int i=1;i<=n-1;i++) 37 { 38 cout<<que.top().x<<' '; 39 que.pop(); 40 } 41 cout<<que.top().x<<endl; 42 43 return 0; 44 }
有序表合并的方法,类似数据结构的归并排序思想一个一个比较
因为A和B是递增的,所以最小的n个数肯定是A和B前几个数的组合
1 #include <iostream> 2 #include <cstdio> 3 #include <queue> 4 using namespace std; 5 const int maxn=1e6+5; 6 int a[maxn],b[maxn]; 7 int n; 8 struct px 9 { 10 int x; 11 bool operator<(const px &a) const 12 { 13 return x>a.x; 14 } 15 px(){}; 16 px(int X):x(X){}; 17 }; 18 priority_queue<px> que; 19 20 int main() 21 { 22 ios::sync_with_stdio(false); cin.tie(0); 23 24 cin>>n; 25 for(int i=1;i<=n;i++) cin>>a[i]; 26 for(int i=1;i<=n;i++) cin>>b[i]; 27 28 int A=1,B=1;//设置两个指针,各指向数组的开头,然后依次往后走! 29 que.push(px(a[A]+b[B]));//这个肯定最小,放进堆里 30 while(que.size()<n*3)//多选一些(多选多少可能要试几次,*2错误,*3就过了),然后从堆里面输出最小的n个 31 { 32 if(a[A]<=b[B])//每次比较指针指向的数,小的那个指针向后移 33 { 34 A++; 35 for(int i=1;i<=B;i++) que.push(px(a[A]+b[i]));//然后下一个指向的数与b的前B个数组合,扔进堆里 36 } 37 else 38 { 39 B++; 40 for(int i=1;i<=A;i++) que.push(px(a[i]+b[B]));//同上 41 } 42 } 43 for(int i=1;i<=n-1;i++) 44 { 45 cout<<que.top().x<<' '; 46 que.pop(); 47 } 48 cout<<que.top().x<<endl; 49 50 return 0; 51 }
还有一道洛谷P2085跟上题几乎一样,有2种做法,二分答案和有序表合并思想从小到大一个一个比较。
二分90ms
1 #include<iostream> 2 #include<algorithm> 3 using namespace std; 4 typedef long long ll; 5 const int maxn=1e6; 6 ll a[maxn],b[maxn],c[maxn]; 7 ll d[maxn]; 8 int n,m; 9 10 int judge(ll x) 11 { 12 int cnt=0; 13 for(int i=1;i<=n;i++) 14 { 15 if(cnt>m) break; 16 17 for(int j=1;j<=m;j++) 18 { 19 if(a[i]*j*j+b[i]*j+c[i]<x) 20 { 21 cnt++; 22 if(cnt>m) break; 23 } 24 else if(a[i]*j*j+b[i]*j+c[i]>x) break; 25 } 26 } 27 28 return cnt; 29 } 30 31 int main() 32 { 33 ios::sync_with_stdio(false); cin.tie(0); 34 35 cin>>n>>m; 36 ll Min=1e10,Max=0; 37 for(int i=1;i<=n;i++) 38 { 39 cin>>a[i]>>b[i]>>c[i]; 40 41 Min=min(Min,a[i]+b[i]+c[i]); 42 Max=max(Max,a[i]*m*m+b[i]*m+c[i]); 43 } 44 ll l=Min,r=Max; 45 ll ans=0; 46 while(l<=r) 47 { 48 ll mid=(l+r)/2; 49 50 int t=judge(mid); 51 if(t<=m) 52 { 53 l=mid+1; 54 ans=mid; 55 } 56 else r=mid-1; 57 } 58 59 int p=0; 60 for(int i=1;i<=n;i++) 61 { 62 if(p==m) break; 63 for(int j=1;j<=m;j++) 64 { 65 ll t=a[i]*j*j+b[i]*j+c[i]; 66 if(t<=ans) 67 { 68 d[++p]=t; 69 } 70 else break; 71 } 72 } 73 74 sort(d+1,d+1+p); 75 76 for(int i=1;i<=m-1;i++) cout<<d[i]<<" "; 77 cout<<d[m]<<endl; 78 79 return 0; 80 }
有序表合并比较3000ms
1 #include <iostream> 2 #include <algorithm> 3 #include <queue> 4 using namespace std; 5 typedef long long ll; 6 const int maxn=1e6; 7 ll a[maxn],b[maxn],c[maxn]; 8 ll d[maxn],e[maxn]; 9 ll n,m; 10 11 int main() 12 { 13 ios::sync_with_stdio(false); cin.tie(0); 14 15 cin>>n>>m; 16 for(int i=1;i<=n;i++) 17 { 18 cin>>a[i]>>b[i]>>c[i]; 19 d[i]=1; 20 } 21 22 for(int i=1;i<=n;i++)//m个数,保证每次取到的都是最小的!这个方法也可以不用优先队列普通数组存就可以! 23 { 24 ll ans=1e9,ansj=0; 25 ll t=0; 26 for(int j=1;j<=2;j++) 27 { 28 t=a[j]*d[j]*d[j]+b[j]*d[j]+c[j]; 29 if(t<ans) 30 { 31 ans=t; 32 ansj=j; 33 } 34 } 35 e[i]=ans; 36 d[ansj]++; 37 } 38 39 for(int i=1;i<=m-1;i++) 40 { 41 cout<<e[i]<<' '; 42 } 43 cout<<e[n]<<endl; 44 45 return 0; 46 }
可以看到,二分枚举判断答案几乎是无敌的存在几十秒求出有m次操作的最值问题!
完。