2022.02.23 关于一些贪心算法...(1)
0.贪心算法核心思想
说在前面:南湖论剑团队寒假一开始的三期就搞这个贪心算法,但是说来惭愧,当时不懂贪心,当时做了几道题,今天稍微整理整理一些粗浅知识....若能帮到读者,那自然是非常好的。
ps:参考自OI WiKi
贪心,考虑的是局部最优解。但要达成全局最优解,务必需要一个解释来证明,局部最优即全局最优中的一步。
程序中的体现也许就一句话,但是他怎么来的还是必须弄清楚。
证明的主要方法
1.反证法,假设两个元素交换以后,结果却不会变得更好,那么此时推断是最优解。可能有点抽象,后面会有实际例子说明一下的。
2.归纳法。先求得边界情况(一般n=1时)求出最优解F1,然后证明对于每个n,若能证明Fn->Fn+1。即可得证。
与dp的区别
初期学习贪心容易与动态规划dp弄混淆。这里再记录一下两者适用情况便于后续学习,简单来说贪心没有反悔药,但dp可以。
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
贪心的方法
1.排序解法,他的原则是先处理后选择。
2.后悔解法,他的原则是先选择后处理。
读者若细心品尝,可能还是有点抽象。后续直接看题目来感受吧,多说无益。
1. lqz与四十大盗(先处理后选择)
题目复盘
有天lqz上山砍柴,发现了40个大盗对着门念念叨叨,于是等这40个大盗走了之后他也对着门念了一声"芝麻开门"。门打开了,里面竟然是一个藏宝洞……
题目描述
lqz走进了装满宝藏的藏宝洞。藏宝洞里面有 N(N≤100) 堆金币,第 i堆金币的总重量和总价值分别是 m_i,v_i(1≤m**i,v**i≤100)。lqz有一个承重量为 T(T≤1000) 的背包,但并不一定有办法将全部的金币都装进去。他想装走尽可能多价值的金币。所有金币都可以随意分割,分割完的金币重量价值比(也就是单位价格)不变。请问lqz最多可以拿走多少价值的金币?
输入格式
第一行两个整数 N,T。
接下来 N 行,每行两个整数 m_i,v_i。
输出格式
一个实数表示答案,输出两位小数
输入输出样例
输入
4 50
10 60
20 100
30 120
15 45
**输出 **
240.00
思路复盘
乍一眼看很像背包问题,但这里金币是可分割的,那按照直觉来讲,拿单位重量下价值最高的物品肯定是最优解。事实确实是这样,既然是单位重量,直接按照单位重量的价值从高到低拿就完了,直到背包空间不够。
有一点要注意:单位重量的价值,我们会用V/M来表示,但是...两个整型相除可是有误差的。所以排序的时候考虑交叉相乘,这也是我错了的原因 =.=
#include <iostream>
#include <algorithm>
using namespace std;
struct ob
{
int w;
int v;
} c[105];
bool cmp(ob a, ob b)
{
return a.v * b.w > b.v * a.w; //为防止精度问题,采用交叉相乘
}
int main()
{
int n, t;
double ans;
cin >> n >> t;
for (int i = 0; i < n; i++)
{
cin >> c[i].w >> c[i].v;
}
sort(c, c + n, cmp);
int i;
for (i = 0; i < n && t!=0; i++)
{
if(t>=c[i].w){
t = t - c[i].w;
ans = ans + c[i].v * 1.0;
}
else{
ans = ans + 1.0 * t * c[i].v /c[i].w;
t=0;
}
}
printf("%.2lf\n", ans);
return 0;
}
2.洛谷P1031 均分纸牌
题目复盘
均分纸牌问题:有 N 堆纸牌,编号分别为 1,2,…, N。每堆上有若干张,但纸牌总数必为 N 的倍数。可以在任一堆上取若于张纸牌,然后移动。
移牌规则为:在编号为 1 堆上取的纸牌,只能移到编号为 2 的堆上;在编号为 N 的堆上取的纸牌,只能移到编号为 N-1 的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。
现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。
例如 N=4,4 堆纸牌数分别为:
① 9 ② 8 ③ 17 ④ 6
移动3次可达到目的:
从 ③ 取 4 张牌放到 ④ (9 8 13 10) -> 从 ③ 取 3 张牌放到 ②(9 11 10 10)-> 从 ② 取 1 张牌放到①(10 10 10 10)。
输入输出样例
输入
4
9 8 17 6
输出
3
说明/提示
【题目来源】
NOIP 2002 提高组第一题
思路复盘
拿题目的例子举例(9,8,17,6),我们需要有一个思维就是删繁就简,最后我们要的是每一堆的数字相同,那这个数字我们是可以算出来的,他是(9+8+17+6)/4=10,我们把每一项减去10,得到(-1,-2,7,-4)那么只需要让每一项操作让其为0即可。
现在的问题转化为如何让(-1,-2,7,-4)为0,我们审好题目,他移动的规则是:
移出的牌数减少,移入的牌数变多
但是反过来移的话,就是移出的牌多,移入的牌少咯??故减去负数的操作是可行的。
那我们直接贪心,从左到右逐个化0,题目要求相邻取牌,化0的方法也很简单粗暴,从左到右,如果左边的不是0,那就直接让其减去原先的数变成0(因为减去负数和正数都是可行的),然后让相邻右边的数加上他原来左边相邻的数。
#include<iostream>
using namespace std;
int n,aver_sum;
int a[105];
int count=0;
void slove()
{
int i=0,j=n-1;
for(int i=0;i<n;i++) a[i]-=aver_sum;
//缩小区间
while(i<n&&a[i]==0) i++;
while(j>1&&a[j]==0) j--;
while(i<j)
{
*(a+i+1)+=*(a+i);
*(a+i)=0;
i++;::count++;
while(i<n&&a[i]==0) i++;
// cout<<"i="<<i<<",j="<<j<<endl;
// for(int i=0;i<n;i++)
// {
// cout<<*(a+i)<<",";
// }
// cout<<endl;
}
std::cout<<::count<<endl;
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
scanf("%d", a + i);
aver_sum += *(a + i);
}
aver_sum /= n;
slove();
return 0;
}
3.旅行家的预算
问题描述
一个旅行家想驾驶汽车以最少的费用从一个城市到另一个城市(假设出发时油箱是空的)。 给定两个城市之间的距离D1、 汽车油箱的容量C(以升为单位)、每升汽油能行驶的距离D2、 出发点每升汽油价格P和沿途油站数N(N可以为零),油站i离出发点的距离Di、每升汽油价格Pi(i=1,2,……N)。 计算结果四舍五入至小数点后两位。如果无法到达目的地,则输出“No Solution”。
输入格式
第一行为4个实数D1、C、D2、P与一个非负整数N;接下来N行,每行两个实数Di、Pi。
输出格式
如果可以到达目的地,输出一个实数(四舍五入至小数点后两位),表示最小费用;否则输出“No Solution”(不含引号)。
样例输入
275.6 11.9 27.4 2.8 2
102.0 2.9
220.0 2.2
样例输出
26.95
思路晚些补,可以先看注释,所有的情况如何进行贪心选择已经罗列出来了。
#include<iostream>
using namespace std;
struct st{
double distance;
double price;
}a[10];
int n;
double d,d2,c,p,ans;
/*
Considering there Strategy:
1. No solution:
use this station's all petrol until over my car's capacity but can't access the next station.
2.compare the station's price I can access when full my car:
2.1 if the local station cheapest
then buy the most oil towards next_station
and then I access the netx_Station the left oil=capacity - distance/d2
2.2 if this station is not the cheapst one among all the accessible station
then I only buy the least petrol to make sure access the cheapest one.
and when I access the next_station,the left oil = 0
*/
int main()
{
int flag=0;
cin>>d>>c>>d2>>p>>n;
a[0].distance=0;a[0].price=p;
a[n+1].distance=d;a[n+1].price=0;
for(int i=1;i<=n;i++)
{
cin>>a[i].distance>>a[i].price;
if(a[i].distance-a[i-1].distance>c*d2)
flag=true;
}
if(flag)
{
cout<<"No Solution"<<endl;
return 0;
}
int index=0;
int next_index=0;
double oil;
while(index<=n)
{
index=next_index;
for(next_index=index+1;next_index<=n+1;next_index++){
//Consider the local optimal solution
if(a[next_index].distance-a[index].distance>c*d2){
next_index = next_index-1;
break;
}
if(a[next_index].price<=a[index].price){
break;
}
}
if(a[next_index].price<=a[index].price)
{
//2.2
ans+=((a[next_index].distance-a[index].distance)/d2-oil)*a[index].price;
oil=0;
}
else{
//2.1
ans+=(c-oil)*a[index].price;
oil=c-(a[next_index].distance-a[index].distance)/d2;
}
}
printf("%.2lf\n",ans);
return 0;
}
4.国王的游戏(高精度+反证法)
题目描述
恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 n 位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。
国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。
输入输出格式
输入格式:
第一行包含一个整数 n,表示大臣的人数。
第二行包含两个整数 a和 b,之间用一个空格隔开,分别表示国王左手和右手上的整数。
接下来 n 行,每行包含两个整数 a 和 b,之间用一个空格隔开,分别表示每个大臣左手和右手上的整数。
输出格式:
输出只有一行,包含一个整数,表示重新排列后的队伍中获奖赏最多的大臣所获得的金币数。
输入输出样例
输入样例#1:
3
1 1
2 3
7 4
4 6
输出样例#1:
2
说明
【输入输出样例说明】
按 1、2、3 号大臣这样排列队伍,获得奖赏最多的大臣所获得金币数为 2;
按 1、3、2 这样排列队伍,获得奖赏最多的大臣所获得金币数为 2;
按 2、1、3 这样排列队伍,获得奖赏最多的大臣所获得金币数为 2;
按 2、3、1 这样排列队伍,获得奖赏最多的大臣所获得金币数为 9;
按 3、1、2 这样排列队伍,获得奖赏最多的大臣所获得金币数为 2;
按 3、2、1 这样排列队伍,获得奖赏最多的大臣所获得金币数为 9。
因此,奖赏最多的大臣最少获得 2 个金币,答案输出 2。
【数据范围】
对于 20%的数据,有 1≤ n≤ 10,0 < a、b < 8;
对于 40%的数据,有 1≤ n≤20,0 < a、b < 8;
对于 60%的数据,有 1≤ n≤100;
对于 60%的数据,保证答案不超过 10^9;
对于 100%的数据,有 1 ≤ n ≤1,000,0 < a、b < 10000。
NOIP 2012 提高组 第一天 第二题
晚点再补。。。睡觉了
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
typedef int ll;
const int LEN = 5005;
struct Person
{
int l;
int r;
}p[1010];
int n;
int sum_fz[LEN]={0},result[LEN]={0},ans[LEN]={0};
int read()
{
int x=0,f=1;//f表示正负
char c=getchar();
while(!(c>='0'&&c<='9'))
{
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9')
{
x=x*10+c-'0';
c=getchar();
}
x=x*f;
return x;
}
bool cmp(Person p1,Person p2)
{
if(p1.l*p1.r==p2.l*p2.r) return p1.l<p2.l;
return p1.l*p1.r<p2.l*p2.r;
}
void mul(int *sum_fz,int l)
{
for(int i=LEN-2;i>=0;i--) sum_fz[i]=sum_fz[i]*l;
for(int i=0;i<=LEN-2;i++){
sum_fz[i+1]+=(sum_fz[i]/10);
sum_fz[i]=sum_fz[i]%10;
}
}
void div(int *sum_fz,int *result,int r)
{
memset(result,0,sizeof(result));
int x=0;
for(int i=LEN-1;i>=0;i--) //高位到低位
{
x=x*10+sum_fz[i];
result[i]=x/r;
x%=r;
}
}
bool whichismore(int *a,int *b)
{
for(int i=LEN-1;i>=0;i--)
{
if(a[i]>b[i]) return true;else
if(a[i]<b[i]) return false;
}
return false;
}
void display(int *a)
//主要是得找到第一位不是0的数
{
bool flag=false;
for(int i=LEN-1;i>=0;i--)
{
if(!flag){
if(a[i]!=0) flag=true;
else continue;
}
printf("%d",a[i]);
}
}
void replace(int *result,int *ans)
{
for(int i=LEN-1;i>=0;i--)
{
ans[i]=result[i];
}
}
int main()
{
n=read();
for(int i=0;i<=n;i++){
p[i].l=read();p[i].r=read();
}
sort(p+1,p+n+1,cmp);
int i=0;
sum_fz[0]=1;
for(int i=0;i<=n;i++){
div(sum_fz,result,p[i].r);
if(whichismore(result,ans)) replace(result,ans);
mul(sum_fz,p[i].l);
}
display(ans);
return 0;
}