AcWing
  • 首页
  • 课程
  • 题库
  • 更多
    • 竞赛
    • 题解
    • 分享
    • 问答
    • 应用
    • 校园
  • 关闭
    历史记录
    清除记录
    猜你想搜
    AcWing热点
  • App
  • 登录/注册

AcWing 524. 愤怒的小鸟【状压DP+重复覆盖问题】    原题链接    困难

作者: 作者的头像   一只野生彩色铅笔 ,  2021-07-25 23:53:41 ,  所有人可见 ,  阅读 3356


54


9

最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划

题目描述

给定 $n$ 个 pig 的坐标,其中第 $i$ 个 pig 的坐标为 $(x_i,y_i)$

弹弓的坐标是 $(0,0)$,弹弓一次可以发射一条轨迹是抛物线的子弹,并且可以消灭该条抛物线上所有的 pig

求解一个方案,使得弹弓发射的所有子弹可以消灭所有的 pig 且弹弓 发射次数最少

本题的参数 $m$ 是要求大数据搜索优化的,也就是用 Dancing Links 做的 重复覆盖问题
但这不在本篇题解的讨论范围之内(以后有机会可以更)

分析

首先分析一下我们用弹弓发射的子弹的轨迹有哪些特点:$y=ax^2+bx+c$

  1. 一条经过原点的抛物线 $c = 0$
  2. 抛物线的开口朝下 $a<0$

这样抛物线方程就可以设为:$y=ax^2+bx$

方程中有两个参数 $a$ 和 $b$,因此我们可以用具体两个点的坐标来唯一的确定一条 抛物线

参数的计算公式如下:

$$ {\begin{cases} y_1 = ax_1^2 + bx_1 \\\ y_2 = ax_2^2 + bx_2\end{cases}} \quad \Rightarrow \quad {\begin{cases} a = \dfrac{\dfrac{y_1}{x_1} - \dfrac{y_2}{x_2}}{x_1 - x_2}\\\ b = \dfrac{y_1}{x_1} - ax_1\end{cases}} $$

IMG_6CC9004A626B-1.jpeg

于是我们就可以预处理出最多 $n^2$ 条抛物线,然后用这些抛物线对 点集 进行覆盖即可

用已知两点预处理出来的抛物线一定要满足合法(即$a<0$)

然后对于两点构成的抛物线,我们还要处理出他穿过的其他的点

这样预处理的时间复杂度就是 $O(n^3)$


到此处位置,我们就把问题转化为, 重复覆盖问题 (大家可以用搜索优化 Dancing Links 处理该问题 )

本题解采用 状态压缩DP 来完成

闫氏DP分析法

状态表示—集合$f_i$:当前点集覆盖状态为 $i$ 的方案($i$是采用二进制压缩存储的点集覆盖状态)
状态表示—属性$f_i$:方案用的抛物线数量最少 $Min$
状态表示—计算$f_i$:

$$ f_{ne} = min(f_i) + 1 $$

其中 $ne$ 是状态 $i$ 枚举到新抛物线,并进行覆盖以后生成的新状态 $ne$

初始状态 :$f_0$
目标状态 :$f_{1\cdots 1}$

Code

时间复杂度:
1. 预处理 $O(n^3)$
2. 状压DP $O(n2^n)$

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

#define x first
#define y second

using namespace std;

typedef pair<double, double> PDD;

const int N = 20, M = 1 << N;
const double eps = 1e-8;

int n, m;
PDD ver[N];
int path[N][N];
int f[M];

int cmp_lf(double a, double b)  //浮点数比较
{
    if (fabs(a - b) < eps) return 0;
    if (a > b) return 1;
    return -1;
}
void init_path()    //预处理所有的抛物线
{
    memset(path, 0, sizeof path);
    for (int i = 0; i < n; ++ i)
    {
        path[i][i] = 1 << i;    //单独生成一条只经过(0,0)和(x,y)的抛物线
        for (int j = 0; j < n; ++ j)
        {
            double x1 = ver[i].x, y1 = ver[i].y;
            double x2 = ver[j].x, y2 = ver[j].y;
            if (!cmp_lf(x1, x2)) continue;  //单独一个点无法生成参数a,b的抛物线
            double a = (y1 / x1 - y2 / x2) / (x1 - x2);
            double b = (y1 / x1) - a * x1;
            if (cmp_lf(a, 0.0) >= 0) continue;  //a < 0
            //参数a,b的抛物线已生成,枚举他经过的所有点
            for (int k = 0; k < n; ++ k)
            {
                double x = ver[k].x, y = ver[k].y;
                if (!cmp_lf(y, a * x * x + b * x)) path[i][j] += 1 << k;
            }
        }
    }
}
void solve()
{
    //input
    cin >> n >> m;
    for (int i = 0; i < n; ++ i) cin >> ver[i].x >> ver[i].y;
    //prework
    init_path();
    //dp
    memset(f, 0x3f, sizeof f);
    f[0] = 0;
    for (int cur_st = 0; cur_st + 1 < 1 << n; ++ cur_st)
    {
        int t = -1;
        for (int i = 0; i < n; ++ i)
            if (!(cur_st >> i & 1))
                t = i;
        for (int i = 0; i < n; ++ i)
        {
            int ne_st = path[t][i] | cur_st;
            f[ne_st] = min(f[ne_st], f[cur_st] + 1);
        }
    }
    cout << f[(1 << n) - 1] << endl;
}
int main()
{
    int T = 1; cin >> T;
    while (T -- ) solve();
    return 0;
}

15 评论


用户头像
种花家的小兔子   2022-03-31 21:19      2    踩      回复

巨巨,为什么只用找到一个二进制位表示0的点,就用它来更新。。 正常不应该枚举所有情况吗?

        for (int i = 0; i < n; ++ i)
            if (!(cur_st >> i & 1))
                t = i;
        for (int i = 0; i < n; ++ i)
        {
            int ne_st = path[t][i] | cur_st;
            f[ne_st] = min(f[ne_st], f[cur_st] + 1);
        }
用户头像
最傻的猪   2022-09-14 19:20         踩      回复

我也觉得要全部枚举。。。

用户头像
最傻的猪   2022-09-14 19:29         踩      回复

https://www.luogu.com.cn/problem/solution/P2831

用户头像
cai_xing   2023-09-01 10:08         踩      回复

因为i是逐渐增大的,后面也会遍历到的


用户头像
Silvervale   2023-03-18 20:55      1    踩      回复

同问:为什么转移时随便找到当前未被覆盖的某一列 xx,然后枚举所有包含 xx 的行j来选择即可,而不是遍历所有情况???

用户头像
codingMIKU   2023-05-06 17:59      1    踩      回复

https://www.luogu.com.cn/problem/solution/P2831
这里有详细解释。简略来说,就是因为遇到没被杀死的猪,就要杀。先杀和后杀不影响最终解的结果,所以遇到之后就先想办法把这只猪杀了。

用户头像
codingMIKU   2023-05-06 18:13      1    踩      回复

如果这题转移的方程不是求min,而是像求方案数那样相加,肯定就不能;只找第一个未覆盖的列了,因为要统计所有转移的状态。这题之所以能这样搞,就是因为减少这一些状态不影响最优答案。


用户头像
Peter_5   2021-08-08 11:49      1    踩      回复

这题dp的循环是从状态转移方程等式的右边向左边推得啊…好不习惯

用户头像
做ac梦w   2022-08-10 22:06         踩      回复

确实 不过之前写过这种形式就还好


用户头像
taez   2022-11-08 09:56         踩      回复

n个点为什么能有n^2条抛物线

用户头像
asdqwezxc   2022-12-31 11:18      1    踩      回复

随便两个点都能确定一条抛物线


用户头像
Windofbitter   2022-04-11 00:46         踩      回复

谢谢,看懂了


用户头像
ture_6   2021-08-17 11:28         踩      回复

大佬太强了 %%%


用户头像
CZL   2021-07-27 20:36         踩      回复

终究还是铅笔哥教会我了

用户头像
一只野生彩色铅笔   2021-07-27 20:41         踩      回复

hh小崔崔很棒


App 内打开
你确定删除吗?
1024
x

© 2018-2025 AcWing 版权所有  |  京ICP备2021015969号-2
用户协议  |  隐私政策  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标 qq图标
请输入绑定的邮箱地址
请输入注册信息