Y
K
N
U
F

题解 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)

只是往不太一样的方向建模了而已

posted @ 2025-07-07 21:51  樓影沫瞬_Hz17  阅读(22)  评论(0)    收藏  举报