【ACWing】524. 愤怒的小鸟

题目地址:

https://www.acwing.com/problem/content/526/

Kiana最近沉迷于一款神奇的游戏无法自拔。简单来说,这款游戏是在一个平面上进行的。有一架弹弓位于 ( 0 ,   0 ) (0, 0) (0,0)处,每次Kiana可以用它向第一象限发射一只红色的小鸟,小鸟们的飞行轨迹均为形如 y = a x 2 + b x y=ax^2+bx y=ax2+bx的曲线,其中 a , b a,b a,b是Kiana指定的参数,且必须满足 a < 0 a<0 a<0。当小鸟落回地面(即 x x x轴)时,它就会瞬间消失。在游戏的某个关卡里,平面的第一象限中有 n n n只绿色的小猪,其中第 i i i只小猪所在的坐标为 ( x i , y i ) (x_i,y_i) (xi,yi)。如果某只小鸟的飞行轨迹经过了 ( x i , y i ) (x_i,y_i) (xi,yi),那么第 i i i只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;如果一只小鸟的飞行轨迹没有经过 ( x i , y i ) (x_i,y_i) (xi,yi),那么这只小鸟飞行的全过程就不会对第 i i i只小猪产生任何影响。例如,若两只小猪分别位于 ( 1 ,   3 ) (1, 3) (1,3) ( 3 ,   3 ) (3, 3) (3,3),Kiana可以选择发射一只飞行轨迹为 y = − x 2 + 4 x y=−x^2+4x y=x2+4x的小鸟,这样两只小猪就会被这只小鸟一起消灭。而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。这款神奇游戏的每个关卡对Kiana来说都很难,所以Kiana还输入了一些神秘的指令,使得自己能更轻松地完成这个这个游戏。这些指令将在输入格式中详述。假设这款游戏一共有 T T T个关卡,现在Kiana想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。由于她不会算,所以希望由你告诉她。

输入格式:
第一行包含一个正整数 T T T,表示游戏的关卡总数。下面依次输入这 T T T个关卡的信息。每个关卡第一行包含两个非负整数 n , m n,m n,m,分别表示该关卡中的小猪数量和Kiana输入的神秘指令类型。接下来的 n n n行中,第 i i i行包含两个正实数 ( x i , y i ) (x_i,y_i) (xi,yi),表示第i只小猪坐标为 ( x i , y i ) (x_i,y_i) (xi,yi),数据保证同一个关卡中不存在两只坐标完全相同的小猪。如果 m = 0 m=0 m=0,表示Kiana输入了一个没有任何作用的指令。如果 m = 1 m=1 m=1,则这个关卡将会满足:至多用 ⌈ n / 3 + 1 ⌉ ⌈n/3+1⌉ n/3+1只小鸟即可消灭所有小猪。如果 m = 2 m=2 m=2,则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少 ⌊ n / 3 ⌋ ⌊n/3⌋ n/3只小猪。保证 1 ≤ n ≤ 18 1≤n≤18 1n18 0 ≤ m ≤ 2 0≤m≤2 0m2 0 < x i , y i < 10 0<x_i,y_i<10 0<xi,yi<10,输入中的实数均保留到小数点后两位。上文中,符号 ⌈ c ⌉ ⌈c⌉ c ⌊ c ⌋ ⌊c⌋ c分别表示对 c c c向上取整和向下取整,例如: ⌈ 2.1 ⌉ = ⌈ 2.9 ⌉ = ⌈ 3.0 ⌉ = ⌊ 3.0 ⌋ = ⌊ 3.1 ⌋ = ⌊ 3.9 ⌋ = 3 ⌈2.1⌉=⌈2.9⌉=⌈3.0⌉=⌊3.0⌋=⌊3.1⌋=⌊3.9⌋=3 2.1=2.9=3.0=3.0=3.1=3.9=3

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

注:可以直接暴力求解,不需要用 m m m的条件。

法1:记忆化搜索。对于一组小猪的坐标,先预处理一下所有的穿过每两个小猪的抛物线(之所以是两个,因为题目中的抛物线可以由两点决定),能覆盖的小猪的状态,以 p [ i ] [ j ] p[i][j] p[i][j]来表示,其是一个整数,它的二进制位来描述覆盖状态, 1 1 1表示能覆盖该下标的小猪, 0 0 0表示不能覆盖。此处要注意几点:
1、给定两个小猪的坐标 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) ( x 2 , y 2 ) (x_2,y_2) (x2,y2),相当于求解线性方程组: { x 1 2 a + x 1 b = y 1 x 2 2 a + x 2 b = y 2 \begin{cases}x_1^2a+x_1b=y_1\\ x_2^2a+x_2b=y_2\end{cases} { x12a+x1b=y1x22a+x2b=y2可以解出 a a a b b b { a = y 1 x 1 − y 2 x 2 x 2 − x 1 b = y 1 x 1 − a x 1 \begin{cases} a=\frac{\frac{y_1}{x_1}-\frac{y_2}{x_2}}{x_2-x_1} \\b=\frac{y_1}{x_1}-ax_1\end{cases} { a=x2x1x1y1x2y2b=x1y1ax1可以用Cramer法则, a a a的值可以由下列行列式得到:
a = ∣ y 1 x 1 y 2 x 2 ∣ ∣ x 1 2 x 1 x 2 2 x 2 ∣ a=\frac{\left|\begin{array}{c} y_1 & x_1\\ y_2 & x_2 \end{array}\right|}{ \left|\begin{array}{c} x_1^2 & x_1\\ x_2^2 & x_2 \end{array}\right|} a=x12x22x1x2y1y2x1x2解出 a a a之后,代入第一个方程即可得 b b b
2、有的小猪可能无法与任意另一个小猪构成一个合法抛物线,比如这两个小猪位于同一条竖线(即 x 1 = x 2 x_1=x_2 x1=x2),或者求出的抛物线开口向上或退化为直线(即 a ≥ 0 a\ge 0 a0),这两种情况得舍弃;
3、如果某个小猪无法与任意其余的小猪构成一个合法抛物线,我们必须也分配一个抛物线给它,这种抛物线能经过的小猪状态存在 p [ i ] [ i ] p[i][i] p[i][i]里,也就是 p [ i ] [ i ] = 1 < < i p[i][i]=1<<i p[i][i]=1<<i。这样的抛物线虽然不唯一,但是它完全是用来覆盖小猪 i i i用的。
4、为了节省时间,我们可以只计算 i ≤ j i\le j ij时候的 p [ i ] [ j ] p[i][j] p[i][j]

预处理上述信息之后,接下来进行DFS,并用数组 f [ i ] f[i] f[i]来表示,当当前覆盖的状态为 i i i的时候,还需要至少多少个抛物线可以覆盖所有小猪。递归出口是当 i = 2 n − 1 i=2^n-1 i=2n1的时候,此时所有小猪都被覆盖了,不需要再加抛物线了,所以返回 0 0 0;否则,找到还没被覆盖的小猪下标,枚举所有可以覆盖这个小猪的抛物线,然后进入下一层递归。 f f f的作用是做记忆,如果有记忆则调取记忆,不需要重复计算了。代码如下:

#include <iostream>
#include <cstring>
#include <cmath>

#define x first
#define y second
using namespace std;

const int N = 20, M = 1 << 20;
const double eps = 1e-8;
int n, m;
// path[i][j]存经过小猪i和j的抛物线能覆盖的小猪状态
int path[N][N], f[M];
// 存小猪的坐标
pair<double, double> q[20];

// s是已经覆盖了多少小猪的状态的二进制表示,返回的是至少还要多少个抛物线可以覆盖所有小猪
int dfs(int s) {
    
    
    if (f[s] != -1) return f[s];
    if (s == (1 << n) - 1) return f[s] = 0;

    // 找到第一个未覆盖的小猪的下标,赋值给t
    int t = 0;
    for (int i = 0; i < n; i++)
        if (!(s >> i & 1)) {
    
    
            t = i;
            break;
        }

    int res = 0x3f3f3f3f;
    // 枚举与小猪t配对的另一个小猪的下标,构造抛物线覆盖之
    for (int i = 0; i < n; i++) {
    
    
        int c = 0;
        // 只有i <= j的path[i][j]是有效的
        if (i <= t) c = path[i][t];
        else c = path[t][i];

        // 只要覆盖小猪t和i的抛物线是存在的,那么它一定能覆盖t,
        // 也就一定能使得s向递归出口更进一步),则枚举之
        if (c) res = min(res, dfs(c | s) + 1);
    }

    return f[s] = res;
}

// 将path[i][j]初始化为能覆盖小猪i和j的抛物线能覆盖的所有小猪的状态;
// 因为path[i][j] = path[j][i],我们只计算i <= j的情况
void init_paths() {
    
    
    // 枚举抛物线覆盖的第一只小猪
    for (int i = 0; i < n; i++) {
    
    
        // 有可能存在某个点,必须得单独由一条抛物线来覆盖它,
        // 那么这种抛物线只覆盖小猪i,所以它的path值为1 << i
        path[i][i] = 1 << i;
        // 枚举覆盖的第二只小猪。j = i的情况已经在上一行枚举过
        for (int j = i + 1; j < n; j++) {
    
    
            double x1 = q[i].x, y1 = q[i].y, x2 = q[j].x, y2 = q[j].y;
            // 同一竖线上的两个点无法被同一条抛物线穿过,略过此种情况
            if (fabs(x1 - x2) < eps) continue;
            double a = (y1 / x1 - y2 / x2) / (x1 - x2), b = y1 / x1 - a * x1;
            // 略过开口向上的抛物线
            if (a >= 0) continue;
            // 计算此抛物线能覆盖的小猪的状态。首先它一定能覆盖小猪i和j
            int s = (1 << i) | (1 << j);
            for (int k = 0; k < n; k++) {
    
    
                // 略过已经被覆盖的i和j
                if (k == i || k == j) continue;
                double x = q[k].x, y = q[k].y;
                // 如果能覆盖小猪k,则计入状态
                if (fabs(a * x * x + b * x - y) < eps)
                    s += 1 << k;
            }
            
            path[i][j] = s;
        }
    }
}

int main() {
    
    
    int T;
    cin >> T;
    while (T--) {
    
    
        cin >> n >> m;

        for (int i = 0; i < n; i++) cin >> q[i].x >> q[i].y;
        
		memset(path, 0, sizeof path);
        init_paths();

        memset(f, -1, sizeof f);
		// 初始状态没有小猪被覆盖
        cout << dfs(0) << endl;
    }

    return 0;
}

每个case时间复杂度 O ( n 2 n ) O(n2^n) O(n2n),空间 O ( 2 n ) O(2^n) O(2n)

法2:动态规划。思路和上面是一样的,只不过写成了递推的形式。代码如下:

#include <iostream>
#include <cstring>
#include <cmath>

#define x first
#define y second
using namespace std;

const int N = 20, M = 1 << 20;
const double eps = 1e-8;
int n, m;
pair<double, double> q[N];
int path[N][N], f[M];

// 与上面完全一样
void init_paths() {
    
    
    for (int i = 0; i < n; i++) {
    
    
        path[i][i] = 1 << i;
        for (int j = i + 1; j < n; j++) {
    
    
            double x1 = q[i].x, y1 = q[i].y, x2 = q[j].x, y2 = q[j].y;
            if (fabs(x1 - x2) < eps) continue;
            double a = (y1 / x1 - y2 / x2) / (x1 - x2), b = y1 / x1 - a * x1;

            if (a >= 0) continue;
            int s = (1 << i) | (1 << j);
            for (int k = 0; k < n; k++) {
    
    
                if (k == i || k == j) continue;
                double x = q[k].x, y = q[k].y;
                if (fabs(a * x * x + b * x - y) < eps) 
                    s += 1 << k;
            }

            path[i][j] = s;
        }
    }
}

int main() {
    
    
    int T;
    cin >> T;
    while (T--) {
    
    
        cin >> n >> m;
        for (int i = 0; i < n; i++) cin >> q[i].x >> q[i].y;

        memset(path, 0, sizeof path);
        init_paths();

        memset(f, 0x3f, sizeof f);
        
        f[0] = 0;
        // f[i]的i不用枚举到1 << n,因为在其之前已经被递推到了。
        for (int i = 0; i < (1 << n) - 1; i++) {
    
    
        	// 找到还没被覆盖的小猪下标
            int t = 0;
            for (int j = 0; j < n; j++)
                if (!(i >> j & 1)) {
    
    
                    t = j;
                    break;
                }
			
			// 枚举能覆盖t的抛物线
            for (int j = 0; j < n; j++) {
    
    
                int c = 0;
                if (t <= j) c = path[t][j];
                else c = path[j][t];

                if (c) f[i | c] = min(f[i | c], f[i] + 1);
            }
        }

        cout << f[(1 << n) - 1] << endl;
    }

    return 0;
}

每个case时空复杂度与上同。

猜你喜欢

转载自blog.csdn.net/qq_46105170/article/details/114527902
今日推荐