《算法竞赛进阶指南》——贪心总结

简单的写一下书上的贪心题吧。

给树染色(color a tree)

题目描述:
                    

一颗树有 n 个节点,这些节点被标号为:1,2,3…n,每个节点 i 都有一个权值 A[i]。

现在要把这棵树的节点全部染色,染色的规则是:

根节点R可以随时被染色;对于其他节点,在被染色之前它的父亲节点必须已经染上了色。

每次染色的代价为T*A[i],其中T代表当前是第几次染色。

求把这棵树染色的最小总代价。

输入格式

第一行包含两个整数 n 和 R ,分别代表树的节点数以及根节点的序号。

第二行包含 n 个整数,代表所有节点的权值,第 i 个数即为第 i 个节点的权值 A[i]。

接下来n-1行,每行包含两个整数 a 和 b ,代表两个节点的序号,两节点满足关系: a 节点是 b 节点的父节点。

除根节点外的其他 n-1 个节点的父节点和它们本身会在这 n-1 行中表示出来。

同一行内的数用空格隔开。

输出格式

输出一个整数,代表把这棵树染色的最小总代价。

数据范围

1 <= n <= 1000,
1 <= A[i] <= 1000

输入样例:

5 1
1 2 1 2 4
1 2
1 3
2 4
3 5

输出样例:

33
代码:
#include
using namespace std;
struct nodes{
	int fa,sum,size;//fs储存父结点编号,sum储存这个节点合并的数之和,size储存合并的数的个数
	double avr;//储存合并之后的平均权值
};
nodes p[1010];
int n,root;
long long ans=0;//存答案

int find()//用来找每一次循环中平均权值最大的点
{
	int res=0;//先将初始值赋为0
	double minn=0;//
	for(int i=1;i<=n;i++)
	{
		if(i!=root&&p[i].avr>minn)//找到除了根节点外最大的,因为根节点没有父节点,所以不能找根节点(但根节点可以被别人看做父节点从而参与计算)
		{
			minn=p[i].avr;//
			res=i;//
		}
	}
	return res;//
}


int main()
{
	cin>>n>>root;
	for(int i=1;i<=n;i++)
	{
		cin>>p[i].avr;//第一次输入的数即是平均值
		p[i].sum=p[i].avr;
		p[i].size=1;//初始值为1
		ans+=p[i].sum;//因为最开始每一个数都是一个单独的,所以此时要先进行累加
	}
	for(int i=1;i<=n-1;i++)
	{
		int x,y;
		cin>>x>>y;
		p[y].fa=x;//储存
	}
	for(int i=1;i<=n-1;i++)
	{
		int ver=find();//找到平均权值最大的点
		int f=p[ver].fa;//找到这个点的father结点
		ans+=p[ver].sum*p[f].size;//进行答案更新,将ver节点的值放在father结点之后
		p[ver].avr=-1;//将ver节点平均值赋为负数,表示已经选过。
		for(int j=1;j<=n;j++)
			if(p[j].fa==ver)
			p[j].fa=f;//将ver节点的所有子节点重新赋值为ver的父结点
		p[f].sum+=p[ver].sum;//更新父结点
		p[f].size+=p[ver].size;//
		p[f].avr=(double)p[f].sum*1.0/p[f].size;//
		
	}
	cout<<ans;
	return 0;
}
解析:

这道题有点像并查集呢。这道题的数学证明对于我这种数学废柴来说真的是有点困难。
按照题目要求,要想染色一个点必须要先染色它的父节点。而我们怎样确定哪一个数该被染色呢?其实就是要看这个数的权值是不是最大的,一般来说,权值最大的应该首先被染色,但是由于限制,所以要先染色父节点,然后马上染色子节点(这两个点看成一个整体)。
但是如果这样的话,就没有办法确定染完父节点之前染哪一个,(比如如果父节点很小,而有一个父节点的兄弟节点很大的话)。所以这里有一个数学证明:
《算法竞赛进阶指南》——贪心总结_第1张图片
再有就是怎样累加答案的问题,因为这个是一点一点处理,答案一点一点更新,当前后关系改变时,答案也会改变。
其实整个过程就是在一点一点合并,只不过每次合并时是将平均权值最大的点(组)和它的父节点合并,每次合并时要将它放到父节点的后面,但是因为序数改变了,所以要用父结点中已经合并的元素个数*子节点中的总数(这个比较容易证明我就不写了)。
还是写一写:
假设有b1 ~ bn和a1 ~ am这两组数。
b数组需要放在a数组之前。
ans=b1+2b2…+nbn+(n+1)a1+…(n+m)am;
因为之前就已经累加过a数组,但是当时a数组是从1开始排序。所以如果还要累加的话,需要再加上n(a1+…+am).

然后就还有一些细节需要处理,见代码注释。

国王游戏:

题目描述:
                    

恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。

首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。

然后,让这 n 位大臣排成一排,国王站在队伍的最前面。

排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:

排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。

国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。

注意,国王的位置始终在队伍的最前面。

输入格式

第一行包含一个整数 n,表示大臣的人数。

第二行包含两个整数 a 和 b,之间用一个空格隔开,分别表示国王左手和右手上的整数。

接下来 n 行,每行包含两个整数 a 和 b,之间用一个空格隔开,分别表示每个大臣左手和右手上的整数。

输出格式

输出只有一行,包含一个整数,表示重新排列后的队伍中获奖赏最多的大臣所获得的金币数。

数据范围

1 <= n <= 1000
0< a,b <10000

输入样例:

3
1 1
2 3
7 4
4 6

输出样例:

2
代码:
#include
using namespace std;
int n;
int k_a,k_b;
struct mnst
{
    int l,r;
}p[1009];
vector<int >maxx;   
bool com(mnst x,mnst y)
{
    return x.l*x.r<y.l*y.r;//排序比大小
}
vector<int > mul(vector<int > a,int b)
{
    vector<int >c;
    int t=0;
    for(int i=0;i<a.size();i++)
    {
        t+=a[i]*b;
        c.push_back(t%10);
        t/=10;
    }
    while(t) c.push_back(t%10),t/=10;
    return c;
}//高精度乘法
vector<int > div(vector<int > a,int b)
{
    vector<int >c;
    int t=0;
    bool is_first=0;
    for(int i=a.size() -1 ;i >= 0 ;--i)
    {
        t=t*10+a[i];
        int x=t/b;
        if(x||is_first)
        {
            is_first=1;
            c.push_back(x);
        }
        t%=b;
    }
    return vector<int >(c.rbegin(),c.rend());
} //高精度除法
vector<int > max_vec(vector<int > a,vector<int > b)
{
    if(a.size()>b.size()) return a;
    if(a.size()<b.size()) return b;
    if(vector<int >(a.rbegin(),a.rend())>vector<int >(b.rbegin(),b.rend()))
    return a;
    return b;
}//比较大小
void output()
{
    for(int i=maxx.size()-1;i>=0;--i)
        cout<<maxx[i];
    cout<<endl; 
}//输出
int main()
{
    cin>>n;
    cin>>k_a>>k_b;
    for(int i=1;i<=n;i++)
    {
        cin>>p[i].l>>p[i].r;
    }
    sort (p+1,p+n+1,com);
    vector<int >sum;
    sum.push_back(1);
    sum=mul(sum,k_a);

    for(int i=1;i<=n;i++)
    {
        maxx=max_vec( div(sum,p[i].r) ,maxx);
        sum=mul(sum,p[i].l);    
    }
    output();
    return 0;
}

这道题主要涉及到贪心的证明,用邻项交换来比较大小从而证明(此处省略证明过程)
然后就是高精度算法(事实证明我忘完了)

还有一道题

耍杂技的牛:

                    

农民约翰的N头奶牛(编号为1..N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。

奶牛们不是非常有创意,只提出了一个杂技表演:

叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。

奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。

这N头奶牛中的每一头都有着自己的重量$W_i$以及自己的强壮程度$S_i$。

一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。

您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。

输入格式

第一行输入整数N,表示奶牛数量。

接下来N行,每行输入两个整数,表示牛的重量和强壮程度,第i行表示第i头牛的重量$W_i$以及它的强壮程度$S_i$。

输出格式

输出一个整数,表示最大风险值的最小可能值。

数据范围

$1 \le N \le 50000$,
$1 \le W_i \le 10,000$,
$1 \le S_i \le 1,000,000,000$

输入样例:

3
10 3
2 5
3 3

输出样例:

2
#include
using namespace std;
typedef pair<int ,int > PLL;
PLL cow[50010];
bool com(PLL a,PLL b)
{
    return a.first + a.second < b.first + b.second;
}
int main()
{
    int n;
    cin>>n;
    for(int i = 1;i <= n;i++)
    {
        cin>>cow[i].first>>cow[i].second;
    }
    sort(cow + 1,cow + n + 1,com);
    int maxx = -99999999;
    int sum=0;
    for(int i = 1;i <= n;i++)
    {
        maxx = max(maxx , sum - cow[i].second);
        sum += cow[i].first;
    }
    cout<<maxx;
    return 0;
}

防晒

                   

有C头奶牛进行日光浴,第i头奶牛需要minSPF[i]到maxSPF[i]单位强度之间的阳光。

每头奶牛在日光浴前必须涂防晒霜,防晒霜有L种,涂上第i种之后,身体接收到的阳光强度就会稳定为SPF[i],第i种防晒霜有cover[i]瓶。

求最多可以满足多少头奶牛进行日光浴。

输入格式

第一行输入整数C和L。

接下来的C行,按次序每行输入一头牛的minSPF和maxSPF值,即第i行输入minSPF[i]和maxSPF[i]。

再接下来的L行,按次序每行输入一种防晒霜的SPF和cover值,即第i行输入SPF[i]和cover[i]。

每行的数据之间用空格隔开。

输出格式

输出一个整数,代表最多可以满足奶牛日光浴的奶牛数目。

数据范围

$1 \le C,L \le 2500$,
$1 \le minSPF \le maxSPF \le 1000$,
$1 \le SPF \le 1000$

输入样例:

3 2
3 10
2 5
1 5
6 2
4 1

输出样例:

2

这道题仿佛是要用匈牙利算法(好像就是在一个二分图中使这个图中不存在增广路径来得到最大匹配):
算法详情见链接,写的挺完备的
二分图+匈牙利算法

#include
using namespace std;
struct cow{
	int sl,bg;
};
cow a[2509];
struct fss{
	int s,cover;
};
fss b[2509];
bool com(cow x,cow y)
{
	return x.sl<y.sl;
}
bool cm(fss x,fss y)
{
	return x.s<y.s;
}
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d%d",&a[i].sl,&a[i].bg);
	for(int i=1;i<=m;i++)
		scanf("%d%d",&b[i].s,&b[i].cover);
	sort(a+1,a+n+1,com);
	sort(b+1,b+m+1,cm);
	int ans=0;
	for(int j=n;j>=1;--j)
	{
//		cout<
		for(int i=m;b[i].s>=a[j].sl;--i)
		{
			if(b[i].s>=a[j].sl&&b[i].s<=a[j].bg&&b[i].cover>0)
			{
				b[i].cover--;
				ans++;
				break;
			}
		}
	}
	cout<<ans;
	return 0;
}

雷达:

                    

假设海岸是一条无限长的直线,陆地位于海岸的一侧,海洋位于另外一侧。

每个小岛都位于海洋一侧的某个点上。

雷达装置均位于海岸线上,且雷达的监测范围为d,当小岛与某雷达的距离不超过d时,该小岛可以被雷达覆盖。

我们使用笛卡尔坐标系,定义海岸线为x轴,海的一侧在x轴上方,陆地一侧在x轴下方。

现在给出每个小岛的具体坐标以及雷达的检测范围,请你求出能够使所有小岛都被雷达覆盖所需的最小雷达数目。

输入格式

第一行输入两个整数n和d,分别代表小岛数目和雷达检测范围。

接下来n行,每行输入两个整数,分别代表小岛的x,y轴坐标。

同一行数据之间用空格隔开。

输出格式

输出一个整数,代表所需的最小雷达数目,若没有解决方案则所需数目输出“-1”。

数据范围

$1 \le n \le 1000$

输入样例:

3 2
1 2
-3 1
2 1

输出样例:

2

这道题的贪心还是挺简单,就是处理的时候要把每一个小岛处理成一条线段。然后按照线段终值从小到大排序,顺序遍历,若需要添加新的雷达则添加到这条线段的末尾。

#include
using namespace std;
struct island{
double s,e;
};
island a[1010];
int n;
int d;
int ans;
const double eps=1e-6;
void change(double x,double y,int number)
{
	double dx=sqrt(d * d - y * y);
	a[number].s = x - dx;
	a[number].e = x + dx;
}
bool com(island x,island y)
{
	return x.e<y.e;
}
int main()
{
	scanf("%d%d",&n,&d);
	double x,y;
	for(int i=1;i<=n;i++)
	{
		scanf("%lf%lf",&x,&y);
		if(y>d)
		{
			cout<<-1;return 0;
		}
		change(x,y,i); 
	}
	sort(a+1,a+n+1,com);
	double now=-1e10;
	for(int i=1;i<=n;i++)
	{
		if(now + eps < a[i].s)
		{
			ans++;
			now = a[i].e;
		}
	}
	cout<<ans<<endl;
	return 0;
}

任务:

                    

今天某公司有M个任务需要完成。

每个任务都有相应的难度级别和完成任务所需时间。

第i个任务的难度级别为$y_i$,完成任务所需时间为x_i$ 分钟。

如果公司完成此任务,他们将获得(500 * $x_i$ + 2 * $y_i$)美元收入。

该公司有N台机器,每台机器都有最长工作时间和级别。

如果任务所需时间超过机器的最长工作时间,则机器无法完成此任务。

如果任务难度级别超过机器的级别,则机器无法完成次任务。

每台机器一天内只能完成一项任务。

每个任务只能由一台机器完成。

请为他们设计一个任务分配方案,使得该公司能够最大化他们今天可以完成的任务数量。

如果有多种解决方案,他们希望选取赚取利润最高的那种。

输入格式

输入包含几个测试用例。

对于每个测试用例,第一行包含两个整数N和M,分别代表机器数量和任务数量。

接下来N行,每行包含两个整数$x_i,y_i$,分别代表机器最长工作时间和机器级别。

再接下来M行,每行包含两个整数$x_i,y_i$,分别代表完成任务所需时间和任务难度级别。

输出格式

对于每个测试用例,输出两个整数,代表公司今天可以完成的最大任务数以及他们将获得的收入。

数据范围

$1 \le N,M \le 100000$,
$0 < x_i < 1440$,
$0 \le y_i \le 100$

输入样例:

1 2
100 3
100 2
100 1

输出样例:

1 50004

跟牛栏有点像,然后就是按照x为第一关键字,y为第二关键字从大到小排序,顺序遍历。

#include
using namespace std;
typedef pair<int ,int > PLL;
PLL op[100010],task[100010];  
int n,m;
int main()
{
	 while(cin>>n>>m)
    {
        for(int i=1;i<=n;i++)
           cin>>op[i].first>>op[i].second;
        for(int i=1;i<=m;i++)
            cin>>task[i].first>>task[i].second;
        sort(op+1,op+n+1);
        sort(task+1,task+m+1);
        multiset<int >ys;
       unsigned long long ans=0,res=0;
        for(int i=m,j=n;i>=1;--i)
        {
            while(op[j].first>=task[i].first&&j>=1) ys.insert(op[j--].second);
            auto x=ys.lower_bound(task[i].second);
            if(x!=ys.end())
            {
                ans++;
                res+=task[i].first*500+task[i].second*2;
                ys.erase(x);
            }
        }
        cout<<ans<<" "<<res<<endl; 
    }
    
    return 0;
}

畜栏预定:

题目描述
这道题很简单的贪心,但是有一点就是为了不超时用一个小根堆来储存每一个牛栏的最后时间。
代码:

#include
using namespace std;
priority_queue<pair<int ,int>,vector<pair<int,int > > ,greater<pair<int,int > > >q;
struct cow{
	int s,e,l,number;
};
cow a[50009];
bool com(cow x,cow y)
{
	return x.s<y.s;
}
bool com2(cow x,cow y)
{
	return x.number<y.number;
}
int n;
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&a[i].s,&a[i].e);
		a[i].number=i;
	}
		
	sort(a+1,a+n+1,com);
	int count=0;
	for(int i=1;i<=n;i++)
	{
		if(!q.empty()&&a[i].s>q.top().first)
		{
			int num=q.top().second;
			q.pop();
			q.push(make_pair(a[i].e,num));
			a[i].l=num;
		}
		else
		{
			q.push(make_pair(a[i].e,++count));
			a[i].l=count;
		}
	}
	sort(a+1,a+n+1,com2);
	cout<<count<<endl;
	for(int i=1;i<=n;i++)
	{
		printf("%d\n",a[i].l);
	}
}

总的来说:

贪心可能分为以下几类吧:
1.通过计算来证明排序方式。
2.通过分析二分图的最大匹配问题(无增广路径——匈牙利算法)或者其他跟图论有关的。
3.简单的思考。

你可能感兴趣的:(《算法竞赛进阶指南》——贪心总结)