NOIP2016 提高组 愤怒的小鸟
比较板的状压dp,结果做了3天才写完。
算法一
暴力搜索所有猪的分组情况,同组要满足能一根抛物线打完。时间复杂度 \(O(n^n \times n)\),实现的好的话大概能过 \(60pts\)。最难写的大概是函数判断的部分。想一次写对就一定要打好草稿先理清思路。这是经验之谈。
算法二
知道怎么写函数判断之后,就熟悉了这个问题,于是开始大胆起来想状压。
定义:记 \(f[s]\) 表示打死的猪的集合为 \(s\),最少用几只鸟。
转移:我们找一个 \(s\) 的子集 \(t\),表示最新一步打死 \(t\) 这个集合里的猪(一步),能否一步打死 \(t\) 中所有猪可以 \(O(2^n)\) 预处理 \(g[t]\)。有转移:
初始化 \(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]\),有转移:
直接做就是 \(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篇公开博客!)