概述
是一种在每次决策时都采取对当前情况下最有利的操作!!!,通过局部的最优性推导出整体最优性(貌似和dp有异曲同工之妙)
证明方法
为了达到局部最优性,我们通常要采取一些策略,但是为什么采取的策略是正确的,需要我们证明!
下面为常见的证明方法:
1.微扰(貌似类似于数学函数里面的极值点,左右移动都会使函数值变小)
通常采用临项交换的方法,证明在任何情况下,对局部最优的策略的微小改变都会让情况变得更糟(偏离我们期望的状态),通常是采用排序的策略,但是具体根据什么信息来排序要具体分析(排序后用临项交换的证明很像单调性的证明!!!)
2.反证法:数学上经常使用的方法;
3.数学归纳法:同样是数学里面经常用的方法;
4.决策包容性:
这个看起来很陌生,其实意思就是当你以最优策略行动后所造成的可能性包含其他策略的可能性。
这样的话,这个策略一定能找到最优的路径,因为这个策略包含的可能性足够多,能到达的状态也足够多,把其他策略的可能都包含进去了,那我们还不如直接用这个可能性多的呢。
5.范围缩放:
就是证明任何对最优策略作用范围的扩展不会让结果变坏,就是采取策略后结果不会变坏
上面的方法有点抽象,下面给出例题;
临项交换例题
http://noi-test.zzstep.com/contest/0x00%E3%80%8C%E5%9F%BA%E6%9C%AC%E7%AE%97%E6%B3%95%E3%80%8D%E4%BE%8B%E9%A2%98/0701%20%E5%9B%BD%E7%8E%8B%E6%B8%B8%E6%88%8F
链接可能没用,下面是题目内容;
国王邀请n个大臣来游戏,每个大臣左右手写下一个正数,国王也写,让这些大臣排成一排,国王分赏,国王站在队伍最前面,每个大臣得的金币数等于排在这个大臣前面的所有人(包括国王)的左手数的乘积除以他自己右手上的数,然后向下取整的结果,国王不希望某一个大臣得到特别多的,所以你来安排一下顺序来使得得金币最多的尽可能少?注意国王一直在最前面;
假设n个大臣左右的数字分别是啊a[1]~a[n]和b[1]~b[n],国王手里面是a[0],b[0];
假如我们交换i和i+1名大臣,交换之前两名大臣得的金币数为:
交换之后的金币数:
我们提取公因式,再来比较前后交换的大小,也就是:
两边同乘b[i]*b[i+1],变成比较下面:
因为都是正整数,所以b[i+1]小于等于a[i+1]*b[i+1],b[i]小于等于b[i]*a[i],
所以当b[i+1]*a[i+1]>=a[i]*b[i]时,左边小于右边,相反则大于右边;
也就是说当我们减少逆序对(就是两个数,底标小的反而数值大)时整体结果不会变糟,而增加结果也不会变好()
所以当逆序对为0时,为最佳,也就是按左右手乘积从小到大排序
由于有多次累乘,所以要用高精度乘法!!!
下面代码
//Author:XuHt
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1006;
int n, k[N*4], lk = 1, ans[N*4], la = 1, ans0[N*4], la0 = 1;
struct P {
int a, b;
bool operator < (const P x) const {
return a * b < x.a * x.b;
}
} p[N];
void gj1(int x) {//高精度乘法
for (int i = 1; i <= lk; i++) k[i] *= x;
lk += 4;
for (int i = 1; i <= lk; i++) {
k[i+1] += k[i] / 10;
k[i] %= 10;
}
while (!k[lk]) lk--;
}
void gj2(int x) {//高精度除法
int w = 0;
bool flag = 1;
for (int i = lk; i; i--) {
w = w * 10 + k[i];
ans0[i] = w / x;
w %= x;
if (ans0[i] && flag) {
la0 = i;
flag = 0;
}
}
}
bool pd() {//比较
if (la != la0) return la < la0;
for (int i = la; i; i--)
if (ans[i] != ans0[i]) return ans[i] < ans0[i];
return 0;
}
int main() {
cin >> n;
for (int i = 0; i <= n; i++) scanf("%d %d", &p[i].a, &p[i].b);
sort(p + 1, p + n + 1);
memset(k, 0, sizeof(k));
memset(ans, 0, sizeof(ans));
memset(ans0, 0, sizeof(ans0));
k[1] = 1;
gj1(p[0].a);
for (int i = 1; i <= n; i++) {
gj2(p[i].b);
if (pd()) {
memcpy(ans, ans0, sizeof(ans));
la = la0;
}
gj1(p[i].a);
}
for (int i = la; i; i--) cout << ans[i];
cout << endl;
return 0;
}
区间问题
选择不相交区间:数轴上面有n个开区间(ai,bi),选择尽量多的区间,使得这些区间两两没有公共点;
首先明确一种情况,如果x区间完全包含了y区间,那么选哪个区间更好,显然选x不划算,因为选x或者选y都是对答案贡献1,但是如果选区间更大的x对后续影响更大,选y后续的可能性显然包含了选y的可能性,所以选y更好
下面我们按照bi从小到大排序,并且分下面两种可能的情况:
1.如果a1>a2,区间二就包含在区间1里面
2.如果a1<=a2<=...<=ai,假如我们不选择区间1,那么就在区间2~i里面选择,区间1前面突出来的部分就被浪费了,而且选择的区间bi更大对后面的选择限制更多,假如我们选择区间1,那么我们就减少了对后面选择的限制,可能性更多;所以选择区间1不会让答案变坏,而且包含其他区间选择的后续可能;
所以我们的贪心策略就是先按照bi排序,选择第一个区间,之后把与其相交的排除,需要记录一下上次选择的编号,这样就能一次扫描得出结果;
还有一个问题:为什么要按照bi排序?如果我们换成ai呢这样是否可以用上面的方法???
先思考为什么不能用ai排序,如果用ai排序的话,同样我们可以分两种情况:
1.bi>bi+1,这样i区间就包含了i+1区间,那么我们选i+1区间
2.bi<=bi+1<=.... 和上面的第二种情况一样,那么我们就选区间i;
发现了吧,贪心策略变复杂了,如果我们按照bi排序,我们只要无脑选择第一个就行,但是如果我们按照ai排序,还要分情况选择;
下面我们思考为什么按照bi排序:
排序只是为了控制区间的一边端点,更好的分出各种情况!
bi可以看作第i个区间对i+1的区间的影响,因为只有bi才会对后面区间造成限制,而ai不会,所以我们按照bi排序,能通过考虑bi的大小分析选择对后续的可能选择的影响!!!(请认真思考一下)
区间选点问题:数轴上面有n个闭区间【ai,bi】。取尽可能少的点,使得每个区间都至少有一个点(不同区间内含的点可以是同一个)
这个我们一看就能想到:为了取尽可能少的点,我们要让每个点发挥的功效尽可能好!
那么我们同样对bi排序(bi相同就让ai从大到小排序),问题来了,咋样让每个点能尽其所有呢,当然是取最后一个点啦;
我们可以用微扰的方法来证明,这里交给读者思考,很简单,画图就行,加油!!!
同样为什么要按照bi排序,为什么bi相同时ai要从大到小排序???
为什么要按照bi排序我就不多说了,和上题的原因类似,为什么要在bi相同时按照ai从大到小排序呢?因为这样的话,出现区间包含的情况时小区间就会出现在前面,这样满足小区间的话,包含小区间的大区间也一定会满足!
区间覆盖问题:数轴上面有n个闭区间【ai,bi】,选择尽可能少的区间覆盖一条指定的线段[s,t];
与上面一样,区间包含,排序;
先预处理,把n个区间里面在指定区间外的部分截去;
然后在按照ai排序,如果1区间的起点不是s,则无解,否则就选择起点为s的最长区间(尽可能少!!!),然后记录新的起点bi,同样忽略bi之前的部分
例题:radar installation
poj1328
假设滑行是一条无限的直线。海岸的一边是陆地,另一边是海洋。每一个小岛都是位于海边的一个点。而任何位于海岸上的雷达装置只能覆盖d距离,所以半径装置可以覆盖海洋中的岛屿,如果它们之间的距离最多为d。
我们使用笛卡尔坐标系,定义滑行是x轴。海的一面在x轴之上,陆地的一面在x轴之下。给定海洋中每个岛屿的位置,以及雷达装置覆盖的距离,您的任务是编写一个程序来找到覆盖所有岛屿的雷达装置的最小数量。注意,岛屿的位置是由它的x-y坐标表示的。
分析:此题是二维的,如果我们将其转化为一维数轴上面,是不是就和我们的区间覆盖问题一样了呢!
先把每一个岛屿能被雷达圈住的范围求出来(雷达在x轴上面的范围)分别是left[i],right[i];
然后把left数组排序,并且用pos记录前面一台雷达的坐标;
如果pos<left[i],则pos=r[i];否则,让前一台圈这一个岛屿,pos=min(pos,r[i]);
//Author:XuHt
#include <cmath>
#include <cstdio>
#include <iostream>
#include <algorithm>
#define ld long double
using namespace std;
const int N = 1006;
const double INF = -0x3f3f3f3f, eps = 0.000001;
int n, d, num = 0;
struct P {
int x, y;
double l, r;
bool operator < (const P x) const {
return l < x.l;
}
} p[N];
void Radar_Installation() {
for (int i = 1; i <= n; i++) scanf("%d %d", &p[i].x, &p[i].y);
bool b = 1;
for (int i = 1; i <= n; i++)
if (p[i].y > d) {//不可能圈住
b = 0;
break;
}
if (!b) {
cout << "Case " << ++num << ": -1" << endl;
return;
}
for (int i = 1; i <= n; i++) {//一维化
ld k = sqrt((ld)d * d - (ld)p[i].y * p[i].y);
p[i].l = p[i].x - k, p[i].r = p[i].x + k;
}
sort(p + 1, p + n + 1);
int ans = 1;
double pos = -INF;
for (int i = 1; i <= n; i++) {
if (pos + eps < p[i].l) {
ans++;
pos = p[i].r;
} else pos = min(p[i].r, pos);
}
cout << "Case " << ++num << ": " << ans << endl;
}
int main() {
while (cin >> n >> d && n && d) Radar_Installation();
return 0;
}
例题:传说中的车 uva11134
在一个n*n(1<=n<=5000)的棋盘上放置n个车,每个车都只能在给定的一个矩形(xli,xri,yli,yri)里放置,使其n个车两两不在同一行和同一列,判断并给出解决方案。
这个题目在二维上面很难写,那么是否可以降维或者把x轴和y轴分开来考虑!
显然我们可以把x和y分开来考虑,这样的话就转化为在[1,n]的区间中,有一些区间,在每一个区间中选一个点,使最终恰好覆盖[1,n]中的这n个点。
我们把每个区间的右端点进行排序,相同的无所谓(仔细想想为什么??),然后从左到右扫描
下面给出代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 5005;
struct qujian
{
int xl, xr, yl, yr;
int x, y;
int num;
inline bool operator < (const qujian& oth) const//重载小于号,方便打乱之后排回来
{
return num < oth.num;
}
} qj[maxn];
inline bool cmp_x(qujian a, qujian b)//以r为关键字进行排序
{
return a.xr < b.xr;
}
inline bool cmp_y(qujian a, qujian b)
{
return a.yr < b.yr;
}
bool have[maxn];
inline void solve(int n)
{
for(int i = 1; i <= n; ++i)
{
scanf("%d%d%d%d", &qj[i].xl, &qj[i].yl, &qj[i].xr, &qj[i].yr);
qj[i].num = i;//在读入时记录标号,方便打乱之后排回来
}
memset(have, 0, sizeof(have));//多组数据,注意初始化
sort(qj+1, qj+n+1, cmp_x);
for(int i = 1; i <= n; ++i)
{
int x = qj[i].xl;
while(have[x] && x <= qj[i].xr)//找到第一个没有被选过的点
x++;
if(x > qj[i].xr)
{
puts("IMPOSSIBLE ");//udebeg上有空格,但不加似乎也能过
return;
}
else
{
qj[i].x = x;
have[x] = true;
}
}
memset(have, 0, sizeof(have));
sort(qj+1, qj+n+1, cmp_y);
for(int i = 1; i <= n; ++i)
{
int y = qj[i].yl;
while(have[y] && y <= qj[i].yr)
y++;
if(y > qj[i].yr)
{
puts("IMPOSSIBLE ");
return;
}
qj[i].y = y;
have[y] = true;
}
sort(qj+1, qj+n+1);//排回来
for(int i = 1; i <= n; ++i)
printf("%d %d\n", qj[i].x, qj[i].y);
}
int main()
{
int n;
while(scanf("%d", &n) == 1 && n)
solve(n);
}
决策包容性问题
田忌赛马:田忌与齐王赛马,两个人各出一匹马,赢得200两,输亏200两,平就无,已知两个人每匹马的速度,问田忌最多能赢多少钱;
首先很明显先对两个人的速度排序,再用双指针来表示两个人最快的和最慢的马;
下面分情况讨论:
1.当田忌最快的马比齐王的快,我们选择直接比赛,因为每匹马对结果的贡献最多就是200,所以直接比赛行;
2.当田忌最快的马比齐王的慢,我们可以用田忌最慢的和齐王最快的比,因为反正一定会输一场比赛,我们不如保留对我们后面比赛最有利的马;
3.当田忌最快的马和齐王的一样快,这里我们就要思考,是直接比赛还是用慢的马去消耗齐王的?
如果直接比赛那就是平,如果用慢马的话就是输,后续田忌的快马最优也是赢一场,平均下来还是平
但是如果我们仔细分类,如果田忌的慢马比齐王的慢马快,那么一样的直接比赛;如果更慢,那不如直接去消耗齐王的快马,反正一定会输一场;如果两个慢马平呢?还是去让慢马和齐王快马比!!!因为虽然这样输一场后面再赢回来和直接打平没什么区别,但是这样输一场赢一场我们消耗了齐王的快马,对后面更有利!
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int maxn=1005;
int tj[maxn], qw[maxn];
int main()
{
int n, i, res, max1, max2, min1, min2, cnt;
while(~scanf("%d", &n) && n)
{
for(i=0; i<n; i++)
scanf("%d", &tj[i]);
for(i=0; i<n; i++)
scanf("%d", &qw[i]);
sort(tj, tj+n);
sort(qw, qw+n);
res=0;
max1=max2=n-1;
min1=min2=0;
cnt=0;
while((cnt++)<n)
{
if(tj[max1]>qw[max2])
{
res += 200;
max1--;
max2--;
}
else if(tj[max1]<qw[max2])
{
res -= 200;
min1++;
max2--;
}
else
{
if(tj[min1]>qw[min2])
{
res += 200;
min1++;
min2++;
}
else
{
if(tj[min1]<qw[max2]) res -= 200;
min1++;
max2--;
}
}
}
printf("%d\n", res);
}
return 0;
}
当然,这些题目只是比较经典的,不要死记硬背,我们要记住贪心是一种思想而不是套路!!!