0. 前言
相关:
强相关:
1. 状压dp+棋盘式(基于连通性)
NOIP2016提高组
思路:
- 在此,该抛物线过原点,且开口向下。则可表示为 y = a x 2 + b x , a < 0 y=ax^2+bx,a <0 y=ax2+bx,a<0
- 只需要两个小猪坐标,即可唯一确定该抛物线。但两点不能具有相同的 x x x 坐标
- 预处理
- n n n 个点,最多对应 n 2 n^2 n2 条抛物线。且预处理这 n 2 n^2 n2 条抛物线能覆盖那些点。则问题转化为一个重复覆盖问题
- 重复覆盖问题:给定 01 矩阵,要求选择尽量少的行,将所有列至少包含一个 1
- 精确覆盖问题:给定 01 矩阵,要求选择尽量少的行,将所有列只包含一个 1
Dance Link
:舞蹈链算法是以上两个问题的最优解法。采用十字链表优化DFS
过程path[i][j]
数组:表示编号为i
的小猪和编号为j
的小猪所在的抛物线。将该抛物线上能覆盖的小猪编号的组织为二进制形式。例如:path[2][3] = 10110
则代表 2 号、3 号小猪构成的抛物线可以消灭编号为 5 号、3 号、2 号小猪
- 状态定义:
f[i]
:表示当前无盖状态i
表示的小猪所需的最少小鸟
- 状态计算:
- 找到
i
状态下没有被消灭的小猪编号x
,枚举可消灭它的抛物线path[x][j]
- 可将状态更新为
f[i | path[x][j]] = min(f[i | path[x][j]], f[i] + 1);
意思就是,当前没有覆盖x
号猪,那么就枚举所有能够覆盖x
号猪的抛物线path[x][j]
- 找到
另外,两点确定该抛物线方程,手推一下公式即可。
本题应该从暴搜入手,从暴搜优化角度来考虑,这个就是 dp
,记忆化搜索。最后的优化就是 dance link
专门解决重复覆盖问题。
总之,状压 dp
的这一步抽象还是很难的。看到这数据范围怕是第一想法都是暴搜…
代码:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#define x first
#define y second
using namespace std;
typedef pair<double, double> PDD;
const int N = 18, M = 1 << 18;
const double eps = 1e-6;
int n, m;
PDD q[N];
int path[N][N]; // path[i][j] 表示第i个点和第j个点构成的抛物线能覆盖的点的状态
int f[M]; // f[state] 表示覆盖state中点的最小抛物线数量
bool cmp(double x, double y) {
if (fabs(x - y) < eps) return true;
return false;
}
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); // 多组数据,清空
for (int i = 0; i < n; ++i) { // 枚举第一个点
path[i][i] = 1 << i; // 防止只有一个点,或构造不出开口向下抛物线导致状态无法更新
for (int j = 0; j < n; ++j) { // 枚举第二个点,两点将确定抛物线。注意判断开口向下
double x1 = q[i].x, y1 = q[i].y;
double x2 = q[j].x, y2 = q[j].y;
if (cmp(x1, x2)) continue; // 抛物线上两点不能具有相同x坐标
double a = (y1 / x1 - y2 / x2) / (x1 - x2);
double b = y1 / x1 - a * x1;
if (a > 0 || cmp(a, 0.0)) continue; // 系数a大于等于0,开口向下
for (int k = 0; k < n; ++k) { // 顺序枚举哪些点会被当前(i,j)构成的合法抛物线覆盖
// if (j == i) continue; // 可有可无,(i,i)点是无法构成抛物线的,所以path[i][j]中i==j的情况下合法抛物线都构造不出来
double x = q[k].x, y = q[k].y;
if (cmp(a * x * x + b * x, y)) path[i][j] += 1 << k; // 不会造成path[i][i]加两遍的情况,因为构造不出(i,i)这样的合法抛物线
}
}
}
memset(f, 0x3f, sizeof f);
f[0] = 0;
for (int i = 0; i + 1 < 1 << n; ++i) { // 顺序枚举所有状态,i应该小于全是1的情况,全是1则不用再更新
int x = 0; // 找到任意一个没有被覆盖的点
for (int j = 0; j < n; ++j)
if (!(i >> j & 1)) {
x = j; // 找到未覆盖的点,在这找到一个就可以了。因为所有状态下一定是可以将全部点覆盖完毕的
break;
}
for (int j = 0; j < n; ++j) // 枚举所有能覆盖x的抛物线
// path[x][j]存储能覆盖x点的新抛物线所能覆盖的状态
// 将其加入覆盖状态i中的最小抛物线数量,则状态i和path[x][j]中的点都能被同时覆盖了,所以采用 | 来更新状态
// 更新到 i | path[x][j] 状态,等于f[i]加了一条抛物线,和原有状态做比较取min即可得到覆盖 i | path[x][j]状态的最小抛物线数量
f[i | path[x][j]] = min(f[i | path[x][j]], f[i] + 1);
}
// 输出能覆盖1111...11,即所有点的最小抛物线数量
cout << f[(1 << n) - 1] << endl;
}
return 0;
}