Linking
题意
给定 n 个绿猪,每个绿猪给定第一象限的坐标
(
x
,
y
)
(x, y)
(x,y)。
每次可以从原点位置向第一象限发射一只小鸟,飞行轨迹为形如
y
=
a
x
2
+
b
x
y=ax^2+bx
y=ax2+bx 的曲线,需满足
a
<
0
a < 0
a<0。
如果某只小鸟的飞行轨迹经过了点
(
x
i
,
y
i
)
(x_i, y_i)
(xi, yi),那么第
i
i
i 只小猪就会被消灭掉,小鸟沿着轨迹继续飞行。
问,至少需要多少只小鸟能够消灭所有小猪?
( 1 ≤ n ≤ 18 , 0 < x i , y i < 10 ) (1≤n≤18,0<x_i,y_i<10) (1≤n≤18,0<xi,yi<10)
思路
n 的范围很小,很明显状态压缩。
两个点可以确定一条抛物线,用
p
a
t
h
[
i
,
j
]
path[i, j]
path[i,j] 的二进制标记出由点 i 和 点 j 构成的抛物线上的所有点。
给定两个点
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
(x_1, y_1), (x_2, y_2)
(x1,y1),(x2,y2),便可以确定抛物线
y
=
a
x
2
+
b
x
y = ax^2 + bx
y=ax2+bx 中的 a 和 b,得到抛物线方程,然后依次判断哪些点在这条抛物线上。
实现1:记忆化搜索
按照从 0 到 n-1 的顺序依次遍历所有点:
- 如果当前已经消除的小猪状态 s t a t e state state 中已经有当前小猪,那么不需要再发射小鸟,直接递归到下一位置。
- 否则需要发射一只小鸟,其抛物线经过当前小猪。遍历所有经过当前小猪的抛物线,小鸟数+1,更新 state 后递归到下一位置。
- 直到遍历到最后一只小猪或者状态中有所有小猪,停止递归。
记忆化剪枝:
定义 f[i, state]
表示到第 i 个小猪,消除小猪的状态为 state 时,所需要的最少小鸟数。
如果当前到第 i 个小猪,且状态为 state 时,发现当前位置的
f
[
i
,
s
t
a
t
e
]
f[i, state]
f[i,state] 已经存在,并且比当前答案更优,那么当前就不必继续往下递归了,直接 return。
时间复杂度
按理说是
O
(
n
n
)
O(n^n)
O(nn),但是由于加了剪枝,并且一种抛物线可能覆盖多只小猪,所以时间复杂度为
O
(
能过
)
O(能过)
O(能过)。
Code:
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
#define fi first
#define se second
/**/
const int N = 21, mod = 1e9+7;
int T, n, m;
pair<double, double> a[N];
int f[N][N];
int ans, cnt;
int st[19][1<<18];
int cmp(double x, double y){ //浮点数大小判断,忽略误差
if(fabs(x-y) < 1e-6) return 0;
if(x < y) return -1;
return 1;
}
void init(int x, int y) //预处理出哪些点在x点和y点构成的抛物线上
{
double x1 = a[x].fi, y1 = a[x].se;
double x2 = a[y].fi, y2 = a[y].se;
if(cmp(x1, x2) == 0) return; //抛物线中两点不会在同一竖线上
double aa = (y1/x1-y2/x2)/(x1-x2), b = y1/x1 - aa*x1;
if(aa >= 0) return; //a保证<0
for(int i=0;i<n;i++)
{
double t = aa*a[i].fi*a[i].fi + b*a[i].fi;
if(cmp(t, a[i].se) == 0)
{
f[x][y] |= (1<<i);
}
}
}
void dfs(int u, int state)
{
if(u == n || state == (1<<n)-1){
ans = min(ans, cnt);
return;
}
if(cnt >= ans) return;
if(state & 1<<u){
dfs(u+1, state);
return;
}
if(st[u][state] && st[u][state] <= cnt) return;
st[u][state] = cnt;
for(int i=0;i<n;i++)
{
if(f[i][u])
{
cnt++;
dfs(u+1, state | f[i][u]);
cnt--;
}
}
}
signed main(){
Ios;
cin >> T;
while(T--)
{
cin >> n >> m;
for(int i=0;i<n;i++) cin>>a[i].fi>>a[i].se;
mem(st, 0);
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
f[i][j] = 0;
init(i, j);
}
f[i][i] |= 1<<i; //有自己独占一个抛物线的情况
}
ans = 1e9, cnt = 0;
dfs(0, 0);
cout << ans << endl;
}
return 0;
}
实现2:类树形dp
这里把一种状态看作一个节点,状态与状态之间有连边。
用 f[state]
表示当前状态 state 到最终状态需要的最小花费。
从一个状态 state
更新为另一个状态 new_state
:
- 首先判断如果如果当前状态为最终状态了,return。
- 如果
f[new_state]
没有确定,那么dfs(new_state)
,先将f[new_state]
确定。 - 当前状态的
f[state]
和其能更新到的所有状态的f[new_state]
取最小值:f[state] = min(f[state, f[new_state] + 1)
,便能得到当前状态到最终状态的最小花费。
void dfs(int state)
{
if(state == (1<<n)-1) return;
int x = 0;
for(int i=0;i<n;i++)
{
if(!(state & 1<<i)){
x = i;break;
}
}
int ans = 1e9;
for(int i=0;i<n;i++)
{
if(!path[i][x]) continue;
int st = state | path[i][x];
if(!f[st]) dfs(st);
ans = min(ans, f[st]);
}
f[state] = ans + 1;
}
最终的答案便为 f[0]
。
状态的更新方式很像树形dp,时间复杂度未知。
Code:
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define mem(a,b) memset(a,b,sizeof a)
#define pb push_back
#define fi first
#define se second
#define endl '\n'
/**/
const int N = 18, mod = 1e9+7;
int T, n, m;
pair<double, double> a[N];
int path[N][N];
int f[1<<N];
int cmp(double x, double y)
{
if(fabs(x - y) <= 1e-6) return 0;
if(x < y) return -1;
return 1;
}
void init(int x, int y)
{
double x1 = a[x].fi, y1 = a[x].se;
double x2 = a[y].fi, y2 = a[y].se;
if(cmp(x1, x2) == 0) return;
double aa = (y1/x1-y2/x2)/(x1-x2), b = y1/x1 - aa*x1;
if(aa >= 0) return;
for(int i=0;i<n;i++)
{
double t = aa*a[i].fi*a[i].fi + b*a[i].fi;
if(cmp(t, a[i].se) == 0)
{
path[x][y] |= (1<<i);
}
}
}
void dfs(int state)
{
if(state == (1<<n)-1) return;
int x = 0;
for(int i=0;i<n;i++)
{
if(!(state & 1<<i)){
x = i;break;
}
}
int ans = 1e9;
for(int i=0;i<n;i++)
{
if(!path[i][x]) continue;
int st = state | path[i][x];
if(!f[st]) dfs(st);
ans = min(ans, f[st]);
}
f[state] = ans + 1;
}
signed main(){
Ios;
cin >> T;
while(T--)
{
cin >> n >> m;
for(int i=0;i<n;i++) cin >> a[i].fi >> a[i].se;
mem(path, 0);
for(int i=0;i<n;i++)
{
path[i][i] = 1<<i;
for(int j=0;j<n;j++)
{
init(i, j);
}
}
for(int i=0;i<1<<n;i++) f[i] = 0;
dfs(0);
cout << f[0] << endl;
}
return 0;
}
实现3:状压DP
由实现二可知,对于一种较小的所消灭小猪的状态可以或上一个抛物线消灭的小猪状态将更大的状态更新。
所以可以从小到大遍历所有的状态,由当前状态更新较大的状态。
更新方式与实现2相同,找到不在当前状态的一个小猪,当前状态 或上 其所在抛物线来更新状态。
for(int i=0;i<1<<n;i++)
{
if(i == (1<<n)-1) break;
int x = 0;
for(int j=0;j<n;j++)
{
if(!(i & 1<<j)){
x = j;break;
}
}
for(int j=0;j<n;j++)
{
if(!path[x][j]) continue;
f[i | path[x][j]] = min(f[i | path[x][j]], f[i]+1);
}
}
cout << f[(1<<n) - 1] << endl;
时间复杂度 O ( n ∗ 2 n ) O(n * 2^n) O(n∗2n)
Code:
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define mem(a,b) memset(a,b,sizeof a)
#define pb push_back
#define fi first
#define se second
#define endl '\n'
/**/
const int N = 18, mod = 1e9+7;
int T, n, m;
pair<double, double> a[N];
int path[N][N];
int f[1<<N];
int cmp(double x, double y)
{
if(fabs(x-y) < 1e-6) return 0;
if(x < y) return -1;
return 1;
}
void init(int x, int y)
{
double x1 = a[x].fi, y1 = a[x].se;
double x2 = a[y].fi, y2 = a[y].se;
if(cmp(x1, x2) == 0) return;
double aa = (y1/x1 - y2/x2)/(x1-x2);
double b = y1/x1 - aa*x1;
if(cmp(aa, 0) >= 0) return;
for(int i=0;i<n;i++)
if(cmp(aa*a[i].fi*a[i].fi + b*a[i].fi, a[i].se)==0) path[x][y] |= 1<<i;
}
signed main(){
Ios;
cin >> T;
while(T--)
{
cin >> n >> m;
for(int i=0;i<n;i++) cin >> a[i].fi >> a[i].se;
mem(path, 0);
for(int i=0;i<n;i++)
{
path[i][i] = 1<<i;
for(int j=0;j<n;j++){
init(i, j);
}
}
mem(f, 0x3f);
f[0] = 0;
for(int i=0;i<1<<n;i++)
{
if(i == (1<<n)-1) break;
int x = 0;
for(int j=0;j<n;j++)
{
if(!(i & 1<<j)){
x = j;break;
}
}
for(int j=0;j<n;j++)
{
if(!path[x][j]) continue;
f[i | path[x][j]] = min(f[i | path[x][j]], f[i]+1);
}
}
cout << f[(1<<n) - 1] << endl;
}
return 0;
}