AcWing 524 愤怒的小鸟

题目描述:

Kiana最近沉迷于一款神奇的游戏无法自拔。   简单来说,这款游戏是在一个平面上进行的。 

有一架弹弓位于 (0, 0) 处,每次Kiana可以用它向第一象限发射一只红色的小鸟, 小鸟们的飞行轨迹均为形如 y = ax^2 + bx 的曲线,其中 a, b 是Kiana指定的参数,且必须满足 a < 0 。

当小鸟落回地面(即 x 轴)时,它就会瞬间消失。

在游戏的某个关卡里,平面的第一象限中有 n 只绿色的小猪,其中第 i 只小猪所在的坐标为 (xi, yi) 。 

如果某只小鸟的飞行轨迹经过了 (xi, yi) ,那么第 i 只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行; 

如果一只小鸟的飞行轨迹没有经过 (xi, yi) ,那么这只小鸟飞行的全过程就不会对第 i 只小猪产生任何影响。 

例如,若两只小猪分别位于 (1, 3) 和 (3, 3) ,Kiana可以选择发射一只飞行轨迹为 y = −x^2 + 4x 的小鸟,这样两只小猪就会被这只小鸟一起消灭。 

而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。 

这款神奇游戏的每个关卡对Kiana来说都很难,所以Kiana还输入了一些神秘的指令,使得自己能更轻松地完成这个这个游戏。   

这些指令将在输入格式中详述。 

假设这款游戏一共有 T 个关卡,现在Kiana想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。  

由于她不会算,所以希望由你告诉她。

注意:本题除NOIP原数据外,还包含加强数据。

输入格式

第一行包含一个正整数T,表示游戏的关卡总数。

下面依次输入这T个关卡的信息。

每个关卡第一行包含两个非负整数n,m,分别表示该关卡中的小猪数量和Kiana输入的神秘指令类型。

接下来的n行中,第i行包含两个正实数(xi,yi),表示第i只小猪坐标为(xi,yi),数据保证同一个关卡中不存在两只坐标完全相同的小猪。

如果m=0,表示Kiana输入了一个没有任何作用的指令。

如果m=1,则这个关卡将会满足:至多用⌈n/3+1⌉只小鸟即可消灭所有小猪。

如果m=2,则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少 ⌊n/3⌋只小猪。

保证1≤n≤18,0≤m≤2,0<xi,yi<10,输入中的实数均保留到小数点后两位。

上文中,符号 ⌈c⌉ 和 ⌊c⌋ 分别表示对 c 向上取整和向下取整,例如 :⌈2.1⌉=⌈2.9⌉=⌈3.0⌉=⌊3.0⌋=⌊3.1⌋=⌊3.9⌋=3。

输出格式

对每个关卡依次输出一行答案。

输出的每一行包含一个正整数,表示相应的关卡中,消灭所有小猪最少需要的小鸟数量。

输入样例:

2
2 0
1.00 3.00
3.00 3.00
5 2
1.00 5.00
2.00 8.00
3.00 9.00
4.00 8.00
5.00 5.00

输出样例:

1
1

分析:

题意是平面上的一些位置有若干只小猪,它们都有各自的坐标,需要从原点发射小鸟去消灭小猪,发射的轨迹成y=ax^2+bx的抛物线,其中a<0,要求最少发射多少只小鸟才能消灭所有的小猪。本题是与精确覆盖问题类似的重复覆盖问题,即给定01矩阵,要求选择尽量少的行,将所有列覆盖住。这里标准做法是使用 Dancing Links,这里暂且用状态压缩去求解本题(实际上是懒得再去学习Dancing Links,以后遇见了再写吧)。

首先要做的就是求出能够覆盖所有小猪的所有抛物线方程,y = ax^2+bx,两个未知数a和b,需要两个小猪的坐标即可确定,有y1=ax1^2+bx1,y2=1x2^2+bx2,变形得y1/x1=ax1+b,y2/x2=ax2+b,求得a=(y1/x1-y2/x2)/(x1-x2),b = y1/x1-ax1。所以可以二重循环枚举一下抛物线经过的两个坐标,只要这两个坐标合法(x坐标不同,不然分母不存在)。求得的a小于0的话就可以存下来抛物线经过的点了,开始读入坐标存进了一个二元组q[i],接着用path[i][j]表示过q[i]和q[j]的抛物线方程经过哪些小猪的坐标。这里注意到小猪的坐标和存储它们的q数组的下标是一一对应的,所以可以用q的下标表示哪些小猪是抛物线上的,假设有五只小猪,就可以用类似于10110表示经过了哪些小猪了,所以path实际存储的就是压缩后的二进制状态。另外,path[i][i]表示经过第i只小猪的抛物线经过的坐标,只需要存入1 << i即可。当然,求出抛物线方程后还需要代入所有坐标,看看该抛物线经过哪些坐标才能够用path存储状态。

存储好压缩后的状态后,我们要考虑的就是如何从path数组中选出最少的抛物线去覆盖掉所有的小猪。具体点就是比如有五只小猪,第一个抛物线覆盖1,2小猪,第二个覆盖2,3,第三个覆盖3,4,第五个覆盖1,4,第六个覆盖2,3,5.那么只需要选择第五个和第六个就可以覆盖所有的小猪了。等价的可以转换成一个最短路模型,起点是0,终点是状态111...11,中间有各种状态,要求从起点到终点的最短路径。比较好理解的是dfs的思路,设dfs(st)表示从st状态到终点的最短距离,下面设计dfs的步骤。

首先考虑边界情况,当st == (1<<n)-1时表明遍历到了终点,此时返回0即可,从终点到终点的距离自然是0,这是递归基。一般情况下,st不全是1,我们遍历下st的各位,找到还没有覆盖的位置t,然后开始枚举path[t][i],用res存储st到终点的距离,则res = min (res,dfs(st | path[t][i]),为了避免重复计算,用f[st]存储从状态st到终点的距离,初始情况下f数组都设置为-1,在dfs过程中,只要f[st]不等于-1,说明已经遍历过了,直接返回即可,这样的记忆化搜索比直接动态规划的做法效率更高。另外,为了继续剪枝,在枚举path[t][i]时只有加上path[t][i]后覆盖的坐标有所增加才有用,所以只需要dfs能够增加覆盖坐标数的path,即在path[t][i] | st > st时,才去dfs(path|st)。

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
const double eps = 1e-6;
const int N = 20,M = 1 << 20;
int n,m,path[N][N],f[M];
pair<double,double> q[20];
int dfs(int st){
    if(f[st] != -1) return f[st];
    int res = 0x3f3f3f3f;
    if(st == (1 << n) - 1)    return f[st] = 0;
    int t = 0;
    for(int i = 0;i < n;i++){
        if(!(st >> i & 1)){
            t = i;
            break;
        }
    }
    for(int i = 0;i < n;i++){
        if(path[t][i] | st > st)  res = min(res,dfs(path[t][i] | st)+1);
    }
    return f[st] = res;
}
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&m);
        memset(path,0,sizeof path);
        for(int i = 0;i < n;i++)   scanf("%lf%lf",&q[i].first,&q[i].second);
        for(int i = 0;i < n;i++){
            path[i][i] = 1 << i;
            for(int j = 0;j < n;j++){
                double x1=q[i].first,y1=q[i].second,x2=q[j].first,y2=q[j].second;
                if(fabs(x1 - x2) < eps) continue;
                double a = (y1/x1-y2/x2)/(x1-x2),b = y1/x1-a*x1;
                if(a >= 0)   continue;
                int st = 0;
                for(int k = 0;k < n;k++){
                    double x = q[k].first,y = q[k].second;
                    if(fabs(a*x*x+b*x-y) < eps) st += 1 << k;
                }
                path[i][j] = st;
            }
        }
        memset(f,-1,sizeof f);
        printf("%d\n",dfs(0));
    }
    return 0;
}

与dfs的解法相比,动态规划的做法可能不好理解而且效率并不高。用f[i] 表示当前已经覆盖的列是i时的最小行数。

转移时随便找到当前未被覆盖的某个位置 x,然后枚举所有包含 x 的path[x][i]同时更新最小值即可。

即状态转移方程为:f[i | j] = min(f[i | j], f[i] + 1)。为什么这种做法比记忆化搜索要低效呢,因为比如path一共有u个状态,dfs遍历的只是这些状态相互之间并起来的状态,可能并不会遍历到0到(1<<n)-1之间所有的状态,而动态规划的解法则需要遍历这之间所有的状态,所以计算量更大。

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
const double eps = 1e-6;
const int N = 20,M = 1 << 20;
int n,m,path[N][N],f[M];
pair<double,double> q[20];
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&m);
        memset(path,0,sizeof path);
        for(int i = 0;i < n;i++)   scanf("%lf%lf",&q[i].first,&q[i].second);
        for(int i = 0;i < n;i++){
            path[i][i] = 1 << i;
            for(int j = 0;j < n;j++){
                double x1=q[i].first,y1=q[i].second,x2=q[j].first,y2=q[j].second;
                if(fabs(x1 - x2) < eps) continue;
                double a = (y1/x1-y2/x2)/(x1-x2),b = y1/x1-a*x1;
                if(a >= 0)   continue;
                int st = 0;
                for(int k = 0;k < n;k++){
                    double x = q[k].first,y = q[k].second;
                    if(fabs(a*x*x+b*x-y) < eps) st += 1 << k;
                }
                path[i][j] = st;
            }
        }
        memset(f,0x3f,sizeof f);
        f[0] = 0;
        for(int i = 0;i + 1 < 1 << n;i++){
            int t = 0;
            for(int j = 0;j < n;j++){
                if(!(i >> j & 1)){
                    t = j;
                    break;
                }
            }
            for(int j = 0;j < n;j++)    f[i|path[t][j]] = min(f[i|path[t][j]],f[i] + 1);
        }
        printf("%d\n",f[(1<<n)-1]);
    }
    return 0;
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值