愤怒的小鸟(状压DP)

题目描述

Kiana最近沉迷于一款神奇的游戏无法自拔。

简单来说,这款游戏是在一个平面上进行的。

有一架弹弓位于(0,0)处,每次Kiana可以用它向第一象限发射一只红色的小鸟,小鸟们的飞行轨迹均为形如的曲线,其中a,b是Kiana指定的参数,且必须满足a<0。

当小鸟落回地面(即x轴)时,它就会瞬间消失。

在游戏的某个关卡里,平面的第一象限中有n只绿色的小猪,其中第i只小猪所在的坐标为(xi,yi)。

如果某只小鸟的飞行轨迹经过了(xi,yi),那么第i只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;

如果一只小鸟的飞行轨迹没有经过(xi,yi),那么这只小鸟飞行的全过程就不会对第i只小猪产生任何影响。

例如,若两只小猪分别位于(1,3)和(3,3),Kiana可以选择发射一只飞行轨迹为的小鸟,这样两只小猪就会被这只小鸟一起消灭。

而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。

这款神奇游戏的每个关卡对Kiana来说都很难,所以Kiana还输入了一些神秘的指令,使得自己能更轻松地完成这个游戏。这些指令将在【输入格式】中详述。

假设这款游戏一共有T个关卡,现在Kiana想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。由于她不会算,所以希望由你告诉她。

输入输出格式

输入格式:

第一行包含一个正整数T,表示游戏的关卡总数。

下面依次输入这T个关卡的信息。每个关卡第一行包含两个非负整数n,m,分别表示该关卡中的小猪数量和Kiana输入的神秘指令类型。接下来的n行中,第i行包含两个正实数(xi,yi),表示第i只小猪坐标为(xi,yi)。数据保证同一个关卡中不存在两只坐标完全相同的小猪。

如果m=0,表示Kiana输入了一个没有任何作用的指令。

如果m=1,则这个关卡将会满足:至多用只小鸟即可消灭所有小猪。

如果m=2,则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少只小猪。

保证1<=n<=18,0<=m<=2,0<xi,yi<10,输入中的实数均保留到小数点后两位。

上文中,符号分别表示对c向上取整和向下取整

输出格式:

对每个关卡依次输出一行答案。

输出的每一行包含一个正整数,表示相应的关卡中,消灭所有小猪最少需要的小鸟数量

输入输出样例

输入样例#1:
2
2 0
1.00 3.00
3.00 3.00
5 2
1.00 5.00
2.00 8.00
3.00 9.00
4.00 8.00
5.00 5.00
输出样例#1:
1
1
输入样例#2:
3
2 0
1.41 2.00
1.73 3.00
3 0
1.11 1.41
2.34 1.79
2.98 1.49
5 0
2.72 2.72
2.72 3.14
3.14 2.72
3.14 3.14
5.00 5.00
输出样例#2:
2
2
3
输入样例#3:
1
10 0
7.16 6.28
2.02 0.38
8.33 7.78
7.68 2.09
7.46 7.86
5.77 7.44
8.24 6.72
4.42 5.11
5.42 7.79
8.15 4.99
输出样例#3:
6

说明

【样例解释1】

这组数据中一共有两个关卡。

第一个关卡与【问题描述】中的情形相同,2只小猪分别位于(1.00,3.00)和 (3.00,3.00),只需发射一只飞行轨迹为y = -x^2 + 4x的小鸟即可消灭它们。

第二个关卡中有5只小猪,但经过观察我们可以发现它们的坐标都在抛物线 y = -x^2 + 6x上,故Kiana只需要发射一只小鸟即可消灭所有小猪。

【数据范围】

代码

#include<iostream>  
#include<cstdio>  
#include<cstdlib>  
#include<cmath>  
#include<cstring>  
#include<algorithm>  
using namespace std;
const double eps=1e-10;
double x[20],y[20],a,b;
int f[1<<20],l[20*20],tot;
void calc(double x1,double y1,double x2,double y2){
	a=((y1/x1)-(y2/x2))/(x1-x2);
    b=(y1-a*x1*x1)/x1;
}
bool pd(double x,double y){
	if( fabs(a*x*x+x*b-y) < eps)return true;
	else return false;
}
int main(){
#ifndef ONLINE_JUDGE
freopen("input.in","r",stdin);
freopen("output.out","w",stdout);
#endif
int i,j,k,m,n,T;
	scanf("%d",&T);
	while(T--){
		scanf("%d%d",&n,&m);
		for(i=0;i<n;i++)
			scanf("%lf%lf",&x[i],&y[i]);
		memset(f,63,sizeof(f));
		memset(l,0,sizeof(l));
		tot=0;
		for(i=0;i<n;i++)
			for(j=i+1;j<n;j++){
				if(fabs(x[i]-x[j])<eps)continue;
				calc(x[i],y[i],x[j],y[j]);
				if(a>-eps)continue;
				for(k=0;k<n;k++)
					if(pd(x[k],y[k]))
						l[tot]|=1<<k;
				++tot;
			}
		for(i=0;i<n;i++)l[tot++]|=1<<i;
		sort(l,l+tot);
		tot=unique(l,l+tot)-l;
		f[0]=0;
		for(i=0;i<(1<<n);i++)
			for(j=0;j<tot;j++)
				if(f[i]+1<f[i|l[j]])
					f[i|l[j]]=f[i]+1;
		printf("%d\n",f[(1<<n)-1]);
	}
		return 0;
}
			


<think>我们首先需要理解用户的需求:用户希望了解缩动态规划(DP)的详细原理和应用。根据引用资料,我们可以总结出DP的核心思想、适用条件以及基本实现方法。 ### 缩动态规划(DP)详解 缩动态规划是一种通过将态信息缩(通常使用二进制)来表示动态规划态的方法,适用于态中每个元素只有两种可能(如存在/不存在、选中/未选中)且态规模较小(通常不超过32个元素)的问题。 #### 1. 适用条件 - **态单元简单**:每个态单元只有两种态(例如0/1)。 - **态规模小**:态单元的数量不能太大(通常不超过32,因为一个32位整数可以表示32个态单元)。 - **需要态**:如果不缩,态数量会非常大(例如$2^n$级别),导致无法存储。 #### 2. 基本原理 DP的核心在于用一个整数的二进制位来表示一个集合。例如,一个长度为$n$的二进制数,每一位(0或1)表示某个元素的态(如是否被选中)。这样,一个整数就代表了一个集合。 例如,有5个元素,用5位二进制数表示: - 二进制数`10101`(十进制21)表示第0、2、4个元素被选中(从右向左,最低位为第0位)。 - 这样,我们就可以用一个整数(如21)来代表一个集合。 #### 3. 位运算基础 DP中常用的位运算操作: - **左移(<<)**:`1 << i` 表示将1左移i位,得到第i位为1,其余为0的数。常用于构造某个态。 - **与(&)**:判断某一位是否为1,例如`state & (1<<i)`,如果结果非0,则第i位为1。 - **或(|)**:将某一位设为1,例如`state | (1<<i)`。 - **异或(^)**:将某一位取反,例如`state ^ (1<<i)`。 - **取反(~)**:将每一位取反,例如`~state`(注意整数位数,避免高位干扰)。 #### 4. DP的步骤 DP的解题步骤与普通DP类似,但态表示使用了缩的整数: 1. **定义态**:通常用$dp[i][state]$表示处理到第$i$个阶段(或位置)且当前态为$state$(一个缩的整数)时的最优解(或方案数)。 2. **态转移**:根据问题的规则,从之前的态转移到当前态。通常需要枚举所有可能的前一个态,检查是否满足转移条件。 3. **初始化**:确定初始态(如$dp[0][0]=0$)。 4. **结果提取**:根据最终态(如$dp[n][final\_state]$)得到答案。 #### 5. 经典问题举例:行商问题(TSP) 问题描述:有$n$个城市,行商从起点0出发,访问每个城市恰好一次后回到起点,求最短路径。 - **态表示**:$dp[state][i]$表示已经访问过的城市集合为$state$,当前位于城市$i$的最短路径长度。 - **态转移**:$dp[state][i] = \min\{ dp[state \setminus \{i\}][j] + dist[j][i] \}$,其中$j$是上一个访问的城市,且$j$在$state$中,$state \setminus \{i\}$表示从集合$state$中移除城市$i$(即态中第$i$位置0)。 - **缩**:$state$是一个$n$位的二进制数,第$k$位为1表示城市$k$已经访问过。 #### 6. 代码示例(TSP问题) ```python def tsp(dist): n = len(dist) # 态总数:2^n total_states = 1 << n # 初始化dp数组:dp[state][i]表示在态state下,当前在i城市的最短路径 dp = [[float('inf')] * n for _ in range(total_states)] # 起点:态1<<0表示只包含0号城市,当前在0城市,路径长度为0 dp[1][0] = 0 # 遍历所有态 for state in range(total_states): # 遍历当前态下的每个城市 for i in range(n): # 如果当前态中不包含i,跳过 if not (state & (1 << i)): continue # 尝试从其他城市j转移到i for j in range(n): # 如果态中包含j,且j不是i,则尝试转移 if i != j and (state & (1 << j)): # 从态state中移除i,得到前一个态prev_state prev_state = state ^ (1 << i) # 更新dp[state][i] dp[state][i] = min(dp[state][i], dp[prev_state][j] + dist[j][i]) # 最终态:所有城市都访问过(全1),并且回到起点0 res = float('inf') for i in range(1, n): # 最后一步从i回到0 res = min(res, dp[total_states-1][i] + dist[i][0]) return res ``` #### 7. 优化与技巧 - **滚动数组**:当态转移只依赖前一阶段时,可优化空间。 - **预处理合法态**:对于某些问题,可以先预处理出所有合法态(如不冲突的态),减少枚举量。 - **剪枝**:在态转移过程中,通过条件判断提前终止无效转移。 #### 8. 应用场景 DP常用于组合优化问题,如: - 行商问题(TSP) - 铺砖问题(用1x2砖块铺满网格) - 任务分配问题(每个任务分配给不同的人,每人只能做一个) - 棋盘覆盖问题(如炮兵布阵) ### 总结 缩动态规划通过将缩为整数,有效解决了态空间过大问题。其核心在于位运算的应用和态转移的设计。掌握DP需要熟悉位运算操作,并能够将问题态抽象为二进制表示。 §§相关问题§§ 1. DP中常用的位运算操作有哪些? 2. 如何判断一个态是否包含某个元素? 3. 行商问题(TSP)的态转移方程是什么? 4. DP适用于哪些类型的问题? 5. 在缩中,如何表示一个集合的并集、交集和差集?</think>### 缩动态规划(DP)详解 #### 一、核心概念 DP是一种将**集合缩为整数**的动态规划技术。它适用于态中的每个元素只有两种可能(如存在/不存在、选中/未选中),且态规模较小(通常不超过32个元素)的问题。核心思想是将一个集合映射为一个整数的二进制位: - **二进制表示**:用整数的二进制位表示集合元素态 例如:集合 $\{A,C\}$ 在3元素系统中可表示为二进制 `101`(十进制5) - **缩**:将多维缩为单整数 态 $dp[i][S]$ → $dp[S]$,其中 $S$ 是缩后的整数态 #### 二、适用条件(引用[3]) 1. **态单元二元化**:每个态单元仅需两种态(0/1) 2. **态规模小**:态单元数 $n \leq 32$(int存储上限) 3. **态可缩**:态数据能映射为二进制整数 4. **态依赖复杂**:多个元素态共同影响决策(引用[2]) #### 三、位运算基础(关键操作) | 操作 | 符号 | 示例(二进制) | 作用 | |---------------|------|----------------------------|--------------------------| | 判断元素存在 | `&` | `state & (1<<i)` | 检查第 $i$ 位是否为1 | | 添加元素 | `\|` | `state \| (1<<i)` | 将第 $i$ 位置1 | | 删除元素 | `&~` | `state & ~(1<<i)` | 将第 $i$ 位置0 | | 切换元素态 | `^` | `state ^ (1<<i)` | 翻转第 $i$ 位 | | 枚举子集 | `&` | `sub = (sub-1) & state` | 遍历态的所有子集 | #### 四、算法步骤(以经典问题为例) **问题:行商问题(TSP)** 求访问所有城市(每个城市仅一次)并返回起点的最短路径 1. **态定义** $dp[state][i]$:当前已访问城市集合为 $state$(缩整数),最后位于城市 $i$ 的最短路径 2. **态转移** $$ dp[state][i] = \min_{j \in state} \{ dp[state \setminus \{i\}][j] + dist[j][i] \} $$ 其中 $state \setminus \{i\}$ 用位运算实现:`state ^ (1<<i)` 3. **初始化** - 起点:$dp[1<<0][0] = 0$(从城市0出发) - 其他态初始化为无穷大 4. **结果提取** $$ \min_{i} \{ dp[full\_state][i] + dist[i][0] \} $$ $full\_state = (1<<n)-1$(所有城市已访问) #### 五、代码实现(TSP问题) ```python def tsp(dist): n = len(dist) total_states = 1 << n # 态总数 2^n dp = [[float('inf')] * n for _ in range(total_states)] # 初始化:从0号城市出发 dp[1 << 0][0] = 0 for state in range(total_states): for i in range(n): if not (state & (1 << i)): # 跳过未访问城市 continue for j in range(n): if state & (1 << j): # j必须在已访问集合中 prev_state = state ^ (1 << i) # 移除当前城市 dp[state][i] = min(dp[state][i], dp[prev_state][j] + dist[j][i]) # 返回最终结果(回到起点0) full_state = (1 << n) - 1 return min(dp[full_state][i] + dist[i][0] for i in range(n)) ``` #### 六、典型应用场景 1. **棋盘覆盖问题** - 如炮兵布阵(引用[4]):每个位置放/不放炮兵 → 二进制位表示 - 约束:同行炮兵间距≥2 → 位运算检查 `state & (state>>1)` 或 `state & (state>>2)` 2. **子集选择问题** - 如背包变种:选择物品组合满足特定条件 - 态:$dp[mask]$ 表示选中物品集合为 $mask$ 的最优解 3. **路径覆盖问题** - 如最小权路径覆盖:用二进制表示已覆盖节点 #### 七、优化技巧 1. **滚动数组**:空间优化(如 $dp[state]$ 只依赖 $dp[state']$) 2. **预处理合法态**:提前计算满足约束的态集合 3. **剪枝**:跳过无效态(如 `state` 中不包含必要元素时) #### 八、复杂度分析 - **时间复杂度**:$O(2^n \times n^2)$(TSP为例) - **空间复杂度**:$O(2^n \times n)$ 当 $n>20$ 时需谨慎使用(引用[3]) > DP通过将高维缩为整数,显著降低了态空间维度,是解决小规模组合优化问题的利器(引用[1][2])。其核心在于**用位运算实现集合操作**,将复杂的集合关系转化为整数运算。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值