目录
1.P2085 最小函数值
题目
https://www.luogu.com.cn/problem/P2085
分析
从题目的"输出将这 n 个函数所有可以生成的函数值排序后的前 m 个元素"可以看出是堆排序的TopK问题
方法1:暴力求解
求出所有函数中的m个值,然后取前m个,n个函数各有m个值,时间复杂度过高,为,不建议使用
方法2:二次函数的性质(推荐!)
以测试用例分析画表:
会发现:x==4及之后x的值,均是最小,只有它的函数值需要入队
原因:的最高次的系数为1,阶位最高比
和
都要小,同是二次函数,二次项前的系数决定阶位的高低,因此有:
从图像上也可以看出:
算法:首先将x==1的所有函数值计算出来,然后入堆,然后每次拿出最小的那个函数值来打印,把对应的之后删除堆顶元素,把对应的下一个函数值f(x+1)入堆
因为每次拿出最小的那个函数值,所以建立小根堆
因为要将对应的下一个函数值入堆,所以需要知道是哪个函数算的,因此要记录函数的编号,要算f(x+1),需要知道x,因此入堆的元素是结构体,建小根堆需要运算符重载
struct node
{
ull f_x;//f(x)函数值
ull num;//函数的编号
ull x;
bool operator<(const node& _node) const
{
return f_x>_node.f_x;//建立小根堆
}
};
例如
设f(x)=4x^2+5x+3,编号为1;g(x)=3x^2+4x+5,编号为2;h(x)=x^2+7x+1,编号为3;先将{12,1,1}、{12,2,1}、{9,3,1}入堆,发现函数值9最小,因此删除堆顶元素后将{h(1+1),3,2}入堆,以此类推......
注意:删除堆顶元素只能进行m次
代码
#include <iostream>
#include <queue>
#define endl "\n"
using namespace std;
typedef unsigned long long ull;
const int N=1e5+10;
struct node
{
ull f_x;//f(x)函数值
ull num;//函数的编号
ull x;
bool operator<(const node& _node) const
{
return f_x>_node.f_x;//建立小根堆
}
};
priority_queue<node> q;
ull A[N],B[N],C[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
ull n,m;
cin>>n>>m;
for (ull i=1;i<=n;i++)
cin>>A[i]>>B[i]>>C[i];
for (ull j=1;j<=n;j++)
q.push({A[j]+B[j]+C[j],j,1});//x=1的所有函数值入队
while(m--)
{
node tmp=q.top();
cout<<q.top().f_x<<" ";
q.pop();
q.push({A[tmp.num]*(tmp.x+1)*(tmp.x+1)+B[tmp.num]*(tmp.x+1)+C[tmp.num],tmp.num,tmp.x+1});
}
return 0;
}
提交结果
2.P1631 序列合并
fhttps://www.luogu.com.cn/problem/P1631
分析
线索"从小到大表示这 N 个最小的和",显然属于TopK问题
方法1:建两个堆
由于是从小到大,应该建大根堆,将a[i]+b[j]的结果存入其中,保持大根堆内含N个元素,如果直接打印其堆顶元素的话结果是从大到小的,可以再建一个小根堆,将大根堆的堆顶元素存入小根堆,最后再打印大根堆的堆顶元素
第一版代码
#include <iostream>
#include <queue>
using namespace std;
const int N1=1e6+10;
const int N2=1e3+10;
typedef unsigned long long ull;
ull n,a[N1],b[N1],c[N2],d[N1],s;
priority_queue<ull,vector<ull>,less<ull>> tmp_heap;
priority_queue<ull,vector<ull>,greater<ull>> result_heap;
int main()
{
cin>>n;
for (int i=0;i<n;i++)
{
cin>>a[i];
}
for (int i=0;i<n;i++)
{
cin>>b[i];
}
for (int i=0;i<n;i++)
{
for (int j=0;j<n;j++)
{
c[s]=a[i]+b[j];
if (tmp_heap.size()<n)
{
tmp_heap.push(c[s]);
}
else if (tmp_heap.top()>c[s])
{
tmp_heap.pop();
tmp_heap.push(c[s]);
}
s++;
}
}
while (tmp_heap.size())
{
result_heap.push(tmp_heap.top());
tmp_heap.pop();
}
while(result_heap.size())
{
cout<<result_heap.top()<<" ";
result_heap.pop();
}
return 0;
}
提交结果
超出内存的限制,其实不用创建c[N]数组,直接使用临时变量存储a[i]+b[j]的结果,节约空间
第二版代码
#include <iostream>
#include <queue>
using namespace std;
const int N1=1e6+10;
const int N2=1e3+10;
typedef unsigned long long ull;
ull n,a[N1],b[N1],d[N1],c;
priority_queue<ull,vector<ull>,less<ull>> tmp_heap;
priority_queue<ull,vector<ull>,greater<ull>> result_heap;
int main()
{
cin>>n;
for (int i=0;i<n;i++)
{
cin>>a[i];
}
for (int i=0;i<n;i++)
{
cin>>b[i];
}
for (int i=0;i<n;i++)
{
for (int j=0;j<n;j++)
{
c=a[i]+b[j];
if (tmp_heap.size()<n)
{
tmp_heap.push(c);
}
else if (tmp_heap.top()>c)
{
tmp_heap.pop();
tmp_heap.push(c);
}
}
}
while (tmp_heap.size())
{
result_heap.push(tmp_heap.top());
tmp_heap.pop();
}
while(result_heap.size())
{
cout<<result_heap.top()<<" ";
result_heap.pop();
}
return 0;
}
提交结果
解决方法:有超时,说明for循环的次数过多,注意到题目的"单调不降序列",应尽量减少循环次数,观察发现,设外循环为变量i,内循环为变量j,当i不变时,随着j的增大a[i]+b[j]的值也在增大或者不变,当a[i]+b[j]在增大时,必定存在某个状态有tmp_heap.top()<c,不用入堆,应该立刻退出内循环,减少循环次数
第三版代码
#include <iostream>
#include <queue>
using namespace std;
const int N1=1e6+10;
const int N2=1e3+10;
typedef unsigned long long ull;
ull n,a[N1],b[N1],d[N1],c;
priority_queue<ull,vector<ull>,less<ull>> tmp_heap;
priority_queue<ull,vector<ull>,greater<ull>> result_heap;
int main()
{
cin>>n;
for (int i=0;i<n;i++)
{
cin>>a[i];
}
for (int i=0;i<n;i++)
{
cin>>b[i];
}
for (int i=0;i<n;i++)
{
for (int j=0;j<n;j++)
{
c=a[i]+b[j];
if (tmp_heap.size()<n)
{
tmp_heap.push(c);
}
else if (tmp_heap.top()>c)
{
tmp_heap.pop();
tmp_heap.push(c);
}
if (tmp_heap.top()<c)
break;
}
}
while (tmp_heap.size())
{
result_heap.push(tmp_heap.top());
tmp_heap.pop();
}
while(result_heap.size())
{
cout<<result_heap.top()<<" ";
result_heap.pop();
}
return 0;
}
提交结果
方法2:只建一个堆
只建小根堆,由题目的"单调不降序列"
先将a[i]+b[0]入堆(堆中元素正好N个),之后每次先出堆顶元素,再入剩下的元素(a[i]+b[1]、a[i]+b[2]、...、a[i]+b[n]),这样每次出堆的元素都是当前堆中最小的,执行N次出堆即可,时间复杂度为
以题目的测试用例为例
2+1=3 6+1=7 6+1=7
2+4=6 6+4=10 6+4=10
2+8=10 6+8=14 6+8=14
一行一行看,会发现:
a[1] + b[1] ≤ a[1] + b[2] ≤ a[1] + b[3] ≤ ... ≤ a[1] + b[n] a[2] + b[1] ≤ a[2] + b[2] ≤ a[2] + b[3] ≤ ... ≤ a[2] + b[n] a[3] + b[1] ≤ a[3] + b[2] ≤ a[3] + b[3] ≤ ... ≤ a[3] + b[n] ......
先将a[i]+b[1]入堆(因为是每一行不等式中最小的那个),之后取堆顶元素后再入堆,执行N次
能只将和入堆吗?如果不用两重循环的话
答:不能,只将和入堆会找不到下一个入堆的元素,入堆的应该是结构体
代码
#include <iostream>
#include <queue>
using namespace std;
const int N1=1e6+10;
int n,a[N1],b[N1];
struct node
{
int i;
int j;
int sum;
bool operator<(const node& x)const
{
return sum>x.sum;
}
};
priority_queue<node> heap;
int main()
{
cin>>n;
for (int i=0;i<n;i++)
cin>>a[i];
for (int i=0;i<n;i++)
cin>>b[i];
//前n个入堆
for (int i=0;i<n;i++)
{
heap.push({i,0,a[i]+b[0]});
}
//剩下的入堆
for (int k=0;k<n;k++)
{
node tmp=heap.top();
heap.pop();
cout<<tmp.sum<<" ";
if (tmp.j+1<n)
{
heap.push({tmp.i,tmp.j+1,a[tmp.i]+b[tmp.j+1]});
}
}
return 0;
}