P2831 [NOIP 2016 提高组] 愤怒的小鸟
状压 dp 好题。本题解参考这篇题解。
既然数据范围这么小,那我们肯定优先考虑状压 dp。
设 \(dp_S\) 表示当前已经打到的猪猪的集合是 \(S\),最少需要多少发小鸟。
我们刷表转移,考虑接下来一发小鸟会打到那些猪猪。
首先你既然要发射小鸟,总不能打出去一只猪都打不到吧。。。
第一种情况下,这一发只打了一只猪猪 (* ̄(oo) ̄),假设转移完后的集合为 \(S'\),则转移方程式为 \(dp_{S'}=\min(dp_{S'},dp_{S}+1)\)。
(为什么要单独考虑这种情况呢?因为有些猪无法和其他任何猪一起被打掉。或者说,它和其他猪只能构成直线或 \(a>0\) 的抛物线。)
第二种情况下,这一发打了很多只猪,或者说打到了一个抛物线上的很多猪,那么集合得合并上这一个抛物线上的很多猪猪。假设此时的集合为 \(T\),那么有 \(dp_{T}=\min(dp_{T},dp_S+1)\)。
第一种情况的 \(S'\) 很好表示。至于第二种情况,这说明这这个合法抛物线上有至少两头猪。我们记 \(line_{i,j}\) 为和 \(i,j\) 一个抛物线上的猪组成的集合(含 \(i,j\)),那么 \(line\) 数组可以 \(O(n^3)\) 求得,\(T\) 也能表示出来。
如果不太明白怎么求 \(line\) 的话,待会可以看我代码。
由于我们需要枚举集合 \(S\),并且对于每一个 \(S\) 都需要 \(n^2\) 枚举 \(line_{i,j}\),所以这种做法的时间复杂度是 \(O(Tn^22^n)\) 的。
我们考虑怎么优化。
首先是一个小优化:如果 \(i \in S\) 或 \(j \in S\) 的话,我们就没必要从这个 \(line_{i,j}\) 转移过来了。
我们分类讨论证明一下:
- 如果当前这根抛物线上只有 \(i,j\) 两点:
如果 \(i,j\) 都在 \(S\) 里,那你这一步转移毫无意义。如果其中一个在 \(S\) 里,那我们完全可以对另一个做第一种情况的单点转移。
- 如果当前这根抛物线上有 3 个点:
如果 \(i,j\) 都在 \(S\) 里,那么你完全可以用第 3 个点单点转移。如果其中一个在 \(S\) 里,那么你可以用另一个点和第 3 点的 \(line\) 集合转移。
- 如果当前这根抛物线上有至少 4 个点:
你完全可以用其他两个点的 \(line\) 集合转移当前状态,而不是 \(i,j\) 的 \(line\)。
综上我们证明了这个小优化是正确的。
但是不够,这是常数优化,不足以降这种做法的时间复杂度。
我们接着考虑第 2 个优化。
我先问一个问题:愤怒的小鸟都玩过吧?你先打 2,3 号猪后打 1 号猪,和先打 1 号猪再打 2,3 号猪,有区别吗?没有。
那你先打 1,4 号猪后打 2 号猪,和先打 2 号猪后打 1,4 号猪也没有区别。
所以对于一个状态 \(S\),我们设它第一个为 0 的位是 \(x\),也就是当前编号最小的没被打的猪。那你先打这头猪后打这头猪也没有区别,所以我们干脆现在就把它打了。
换句话说,我们要想打掉这头猪,一是考虑它单独被打的情况,二是枚举 \(line_{x,i}\),考虑它和其他猪一起被打的情况。
既然这里它一定要被打,所以我们枚举的一定是穿过 \(x\) 的抛物线,所以只需要枚举 \(line_{x,i}\) 就可以了。
这样我们枚举 \(line\) 的时间复杂度就从 \(O(n^2)\) 降为 \(O(n)\)。最终我们将时间复杂度优化为 \(O(Tn2^n)\),可以通过本题。
代码:
P2831
#include<bits/stdc++.h>
#define int long long
#define db double
#define mkp make_pair
#define x first
#define y second
using namespace std;
const int N=20;
const db eps=1e-12;
//进食后人:eps一定要开大,大概至少1e-6
int T,n,qwq,low[1<<N],line[N][N],dp[(1<<N)];
pair<db,db> a[N];
//注意,很多地方都要开double不是int
inline void LOW(){
//预处理x,low[i]即为题解里的x
for(int i=0;i<(1<<18);i++){
int j=0;
for(;i&(1<<j);j++);
low[i]=j;
}
}
inline void INIT(){
//多测记得初始化
//如果你的下标从0开始,清空的时候下标不要误写为1
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
line[i][j]=0;
}
}
for(int i=0;i<(1<<n);i++){
dp[i]=N;
}
}
inline void f(db &p,db &q,db x_1,db y_1,db x_2,db y_2){
//f:求出抛物线a,b用的
//这里简单说一下:
//x1*x1*a+x1*b=y1
//x2*x2*a+x2*b=y2
//将b用第一个式子表示出来得b=y1/x1-x1*a
//然后将b带入第2个式子,解出来a
//a最终等于底下p的那一坨式子
//最终把a带回去求b
p=(y_2-x_2/x_1*y_1)/(x_2*x_2-x_1*x_2);
q=(y_1/x_1)-x_1*p;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
LOW();
cin>>T;
while(T--){
cin>>n>>qwq;
for(int i=0;i<n;i++){
cin>>a[i].x>>a[i].y;
}
INIT();
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(fabs(a[j].x-a[i].x)<=eps) continue;
//横坐标相同的话一定不可能有抛物线穿过
db p,q;
f(p,q,a[i].x,a[i].y,a[j].x,a[j].y);
//如果是一条直线(a=0)或者抛物线开口向上(a>0)都是不可以的
if(p>=0||fabs(p)<=eps) continue;
//这里是在找哪些点在这个抛物线上
for(int k=0;k<n;k++){
if(fabs(p*a[k].x*a[k].x+q*a[k].x-a[k].y)<=eps){
line[i][j]|=(1<<k);
}
}
}
}
dp[0]=0;
//dp[S]:当前已经打死了 S 集合的猪猪^(* ̄(oo) ̄)^,求最小子弹数
for(int S=0;S<(1<<n);S++){
if(dp[S]==N) continue;
int x=low[S];//low[S]就是题解中x
//不要忘记单点转移
dp[(S|(1<<x))]=min(dp[(S|(1<<x))],dp[S]+1);
for(int i=0;i<n;i++){
if(S&(1<<i)) continue;
//不要忘记单点转移
dp[(S|(1<<i))]=min(dp[(S|(1<<i))],dp[S]+1);
//多个点在抛物线上的转移
dp[(S|line[x][i])]=min(dp[(S|line[x][i])],dp[S]+1);
}
}
//最终状态是当前的猪都被打了
int ans=dp[(1<<n)-1];
printf("%lld\n",ans);
}
return 0;
}

浙公网安备 33010602011771号