题解 P2831 [NOIP 2016 提高组] 愤怒的小鸟
P2831 愤怒的小鸟 最短路题解:
前言:
本人在做题时,写出了假的状压 dfs,突然发现满足最短路,故以最短路写之,最终 AC,发现题解没有人这样写,故作此篇。
解释:
容易想到将猪看作点,鸟看作抛物线,因为三点决定一条抛物线,算上 \((0,0)\) 需要两个点,于是每两个点构造一次,又想到这个抛物线可能会经过很多点,于是遍历一次,寻找之,但是别忘记排除 \(a \ge 0\) 的情况,再考虑排除完之后可能出现的无家可归的点,给予其单独一条抛物线(无需 \(a,b\) 的值),这样子在 \(O(n^3)\) 的时间里进行了预处理。
接下来考虑怎么处理得到的这些数据,注意到 \(n\) 极小,考虑状态压缩,将 \(0\) 看作初始状态,\(2^{n}-1\) 作为目标状态,按理应该 DP,但是我 DP 不好,所以想着用 dfs 试试,于是写出了状态压缩和 dfs 的奇妙结合,结果显然,只有 60pts。
dfs 部分代码:
inline void dfs(int i, int s, int c)
{
if (c > ans)//最优性
return;
if (s == (1 << n) - 1)
{
ans = std::min(ans, c);
return;
}
if (i > cntp)//边界
return;
dfs(i + 1, s, c);
if ((s | p[i].s) != s)//考虑贡献,进行剪枝
dfs(i + 1, s | p[i].s, c + 1);
}
但是,在 dfs 的书写过程中,注意到其实题干其实就是在问从 \(0\) 到 \(2^{n}-1\)(也就是目标状态)的最短路径。于是考虑将每一个属于 \(1\dots 2^{n}-1\) 的情况看作一个点,将情况的合并看作边,边权就是将要被合并的情况的权值,跑一下状态压缩和 dij 的奇妙结合,便可以得到答案。
尽管如此,时间并不足以让代码通过测试,因为边数点数都太过无限制了,会有太多重复无用的运算。
考虑减少无用的部分:对于每一个边或点,只要被遍历到就默认他只能作为边。或者理解为将点数压缩,既然将来一定会遍历到合并创造出的新点,那么就不再考虑把它作为点,只让他作为边。
最短路的复杂度 \(O((m+n) \log n)\) 其中边数 \(m\) 其实是题干中的 \(n^{2}2^{n}\)(极限情况下),点数 \(n\) 其实是 \(n^{2}\),故得到复杂度 \(O(n^{2}2^{n} \log n)\),算上多测和预处理,总复杂度 \(O(Tn^{2}2^{n} \log n)\) 。
(蒟蒻不太会分析复杂度,有误请指出)
完整代码:
#include <bits/extc++.h>
using std::cin;
using std::cout;
typedef long double Ld;
int n, m;
struct pig { //猪 其实代表点
Ld x, y;
};
struct bird { //鸟 其实代表每个抛物线
Ld a, b;
int s;//表示这个抛物线经过哪些点
};
pig b[30];
bird p[2010];
bool get_ab(bird &p, const pig b, const pig c) { //根据两点找出一条抛物线
p.a = (b.y * c.x / b.x - c.y) / ((b.x - c.x) * c.x);
p.b = (b.y / b.x) - (p.a * b.x);
return p.a >= -1e-8;//大于等于0与题意不符
}
void get_s(bird &p) { //找出这条抛物线经过哪些点
for (int i = 1; i <= n; i++)
if (abs(((b[i].y / b[i].x) - (p.a * b[i].x)) - p.b) < 1e-8)
p.s |= 1 << (i - 1);
}
int cnt;
bool used[30], vis[1 << 19];
int dis[1 << 19];
void dij() { //最短路 我也不知道这是什么 当dij吧
memset(dis, 63, sizeof dis);
memset(vis, 0, sizeof vis);
std::priority_queue<std::pair<int, int>> q;
for (int i = 1; i <= cnt; i++)
dis[p[i].s] = 1, q.push({-1, p[i].s});
while (!q.empty()) {
int s = q.top().second;
q.pop();
if (vis[s])
continue;
vis[s] = 1;
for (int l = 0; l < 1 << n; l++) {
if (dis[s | l] > dis[s] + dis[l]) {
dis[s | l] = dis[s] + dis[l];
if (!vis[s | l])
vis[s | l] = 1;
//这里剪了一刀狠的,考虑既然 s|v 将来会当做边所以就不让它当点了。(其实感觉有这个就已经不像最短路了)
}
}
}
}
int main() {
std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;//多测
while (T--) {
cnt = 0;//数有效的抛物线
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> b[i].x >> b[i].y;
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++)
if (get_ab(p[++cnt], b[i], b[j]))//处理点对
cnt--;//如果 a>=0 那么无效,去掉这个点
else
get_s(p[cnt]), used[i] = used[j] = 1;
for (int i = 1; i <= n; i++)//处理无线可归的点
if (!used[i])
p[++cnt].s = 1 << (i - 1);
//以上是预处理这些点
dij();//跑一下
cout << dis[(1 << n) - 1] << '\n';//华丽输出
memset(p, 0, sizeof p);
memset(b, 0, sizeof b);
memset(used, 0, sizeof used);//多测不清空,__________。
}
return 0;
}
}
后记:
本质还是O(Tn2^n)
只是往不太一样的方向建模了而已

浙公网安备 33010602011771号