NOIP2016 提高组 愤怒的小鸟

NOIP2016 提高组 愤怒的小鸟

比较板的状压dp,结果做了3天才写完。

算法一

暴力搜索所有猪的分组情况,同组要满足能一根抛物线打完。时间复杂度 \(O(n^n \times n)\),实现的好的话大概能过 \(60pts\)。最难写的大概是函数判断的部分。想一次写对就一定要打好草稿先理清思路。这是经验之谈。

算法二

知道怎么写函数判断之后,就熟悉了这个问题,于是开始大胆起来想状压。

定义:记 \(f[s]\) 表示打死的猪的集合为 \(s\),最少用几只鸟。

转移:我们找一个 \(s\) 的子集 \(t\),表示最新一步打死 \(t\) 这个集合里的猪(一步),能否一步打死 \(t\) 中所有猪可以 \(O(2^n)\) 预处理 \(g[t]\)。有转移:

\[f[s] = \min(f[s - t] + 1, f[s]) \]

初始化 \(f[0] = 0\),答案是 \(f[2^n - 1]\)

时间复杂度 \(O(3^n)\)。其实复杂度是错的,但是实际发现最卡常的是预处理 \(g[s]\) 的地方(double 实在太慢),然后发现把 fabs 去掉就能从 \(85 \to 100pts\)

#include<bits/stdc++.h>
#define F(i,l,r) for(int i(l);i<=(r);++i)
#define G(i,r,l) for(int i(r);i>=(l);--i)
using namespace std;
using ll = long long;
const int N = 20;
const double eps = 1e-8;
int T, n, m, ans;
int a[N], f[(1 << 18) + 5];
bool g[(1 << 18) + 5];
double x[N], y[N];
bool check(int num){
	double Y1 = y[a[1]], X1 = x[a[1]], Y2 = y[a[2]], X2 = x[a[2]];	
	if(X1 == X2) return 0;
	double fz = Y2 - (Y1 * X2) / X1,  fm = X2 * (X2 - X1);
	if(fz == 0 || fm == 0) return 0;
	double A = fz / fm;
	if(A >= 0) return 0;
	if(num > 2){
		double B = (Y2 - A * X2 * X2) / X2, X = x[a[num]], Y = y[a[num]], ret = A * X * X + B * X - Y;
		if(ret > eps ||  ret < -eps) return 0;
	}	
	return 1;
}
void dfs(int nw, int S, int num, bool flag){
	if(num > 1 && flag) flag = check(num);
	g[S] = flag;if(nw > n) return ;
	a[num + 1] = nw;
	dfs(nw + 1, S | (1 << (nw - 1)), num + 1, flag);
	dfs(nw + 1, S, num, flag);
}
void Main(){
	cin >> n >> m; F(i, 1, n) cin >> x[i] >> y[i];
	dfs(1, 0, 0, 1), f[0] = 0;
	F(s, 1, (1 << n) - 1){
		f[s] = n;
		for(int t = s; t >= 0; t = (t - 1) & s){
			if(g[s - t] && f[t] + 1 < f[s]) f[s] = f[t] + 1;
			if(!t) break;
		}
	} return cout << f[(1 << n) - 1] << '\n', void();
}
signed main(){
//	freopen("ex_angrybirds3.in","r",stdin);
//	freopen("angrybirds.out","w",stdout);
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	cin >> T; while(T --) Main();
	return fflush(0), 0;
}

算法三

发现只有 \(O(n^2)\) 条抛物线,考虑预处理出每条线能打死的猪的集合,记为 \(line[i]\),有转移:

\[f[s | line[i]] = \min(f[s] + 1, f[s | line[i]]) \]

直接做就是 \(O(2^nn^2)\) 的。

考虑用类似 CF11D - A Simple Task 的 trick,钦定该次选定的抛物线必须经过 \(s\) 中最低位 \(0\) 对应位置的那只猪。可以用 __lg(lowbit(~x)) 来快速查找。时间复杂度降为 \(O(2^nn)\)

#include<bits/stdc++.h>
#define F(i,l,r) for(int i(l);i<=(r);++i)
#define G(i,r,l) for(int i(r);i>=(l);--i)
#define lowbit(x) (-x & x)
using namespace std;
using ll = long long;
const int N = 20;
const double eps = 1e-8;
int T, n, m, ans, cnt = 0;
int a[N], f[(1 << 18) + 5], line[N * N];
double x[N], y[N];
vector<int> g[N];
void init(){
	F(i, 1, cnt) line[i] = 0;
	cnt = 0;
	F(i, 1, n){
		F(j, 1, n){
			if(i == j) {
				line[++ cnt] |= (1 << (i - 1));
				continue;	
			}
			double Y1 = y[i], X1 = x[i], Y2 = y[j], X2 = x[j];	
			if(X1 == X2) continue;
			double fz = Y2 - (Y1 * X2) / X1,  fm = X2 * (X2 - X1);
			if(fz == 0 || fm == 0) continue;
			double A = fz / fm;
			if(A >= 0) continue;
			++ cnt;
			line[cnt] |= (1 << (i - 1));
			line[cnt] |= (1 << (j - 1));
			double B = (Y2 - A * X2 * X2) / X2;
			F(k, 1, n){
				if(k == i || k == j) continue;
				double X = x[k], Y = y[k], ret = A * X * X + B * X - Y;
				if(ret > eps ||  ret < -eps) continue;
				line[cnt] |= (1 << (k - 1));
			}
		}
	}
	F(i, 1, n) F(j, 1, cnt) if((line[j] >> (i - 1)) & 1) g[i].push_back(j);
}
void Main(){
	cin >> n >> m; F(i, 1, n) cin >> x[i] >> y[i];
	init(); f[0] = 0;
	F(s, 1, (1 << n)) f[s] = n;
	F(s, 0, (1 << n) - 1) {
		int x = lowbit(~s);
		x = __lg(x) + 1;		
		for(auto i : g[x]) f[s | line[i]] = min(f[s] + 1, f[s | line[i]]);	
	}
	return cout << f[(1 << n) - 1] << '\n', void();
}
signed main(){
//	freopen("ex_angrybirds1.in","r",stdin);
//	freopen("angrybirds.out","w",stdout);
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	cin >> T; while(T --) Main();
	return fflush(0), 0;
}

总结

自己没能观察出来并利用 “只有 \(n^2\) 条抛物线这个性质”,导致本题竟然耗了 3 天才写完。感觉如果在草稿纸上手玩一下抛物线,应该会更容易发现。

但你的思维已经越来越敏锐了!

加油!

(第100篇公开博客!)

“人的脆弱和坚强都超乎自己的想象,有时候因为一句话就泪流满面,有时候回过头,发现自己已经走过了很长的路。”

posted @ 2024-11-17 18:52  superl61  阅读(2)  评论(0编辑  收藏  举报