动态规划:状压dp

状压 DP 是动态规划的一种,通过将状态压缩为整数来达到优化转移的目的。

例一:互不侵犯

在 N×N 的棋盘里面放 K 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共 8 个格子。

输入格式

只有一行,包含两个数 N,K。

输出格式

所得的方案数

输入输出样例对于全部数据,1≤N≤9,0≤K≤N×N。

思路:用二进制数模拟放置的状态,如100010是第一个和第五个有放。然后考虑可行性,先考虑同一行,不能有相邻的,可以通过位运算模拟。再考虑相邻两行,通过‘|’运算符check。接着进行dp,f[i][j][k]表示前i行放置了j个国王此时第i行状态为k,然后用f[i][j][k]加上i-1行时的全部情况,即

f[i][j][k]+=f[i-1][j-count(k)//减去那一行的国王数][m];

代码:

#include<iostream>
#include<vector>
using namespace std;
int n, k;
long long f[2][110][1 << 10];
vector<int>p;
vector<int>q[1 << 10];
bool check(int x)
{
	for (int i = 0; i < n; i++)
	{
		if ((x >> i & 1) && (x >> (i + 1) & 1))
		{
			return false;
		}
	}
	return true;
}
int count(int x)
{
	int res = 0;
	for (int i = 0; i < n; i++)
	{
		res += x >> i & 1;
	}
	return res;
}
int main()
{
	cin >> n >> k;
	for (int i = 0; i < 1 << n; i++)
	{
		if (check(i))
		{
			p.push_back(i);
		}
	}
	for (int i = 0; i < p.size(); i++)
	{
		for (int j = 0; j < p.size(); j++)
		{
			if ((p[i] & p[j]) == 0 && (check(p[i] | p[j])))
			{
				q[i].push_back(j);
			}
		}
	}
	f[0][0][0] = 1;
	for (int i = 1; i <= n + 1; i++)
	{
		for (int j = 0; j <= k; j++)
		{
			
			for (int r = 0; r < p.size(); r++)
			{
				f[i & 1][j][p[r]] = 0;
				int cnt = count(p[r]);
				if(j>=cnt)
				{
					for (int l : q[r])
					{
						f[i & 1][j][p[r]] += f[(i - 1) & 1][j - cnt][p[l]];
					}
				}
			}
		}
	}
	cout << f[(n + 1) & 1][k][0];
	return 0;
}

例二:吃奶酪

房间里放着 n 块奶酪。一只小老鼠要把它们都吃掉,问至少要跑多少距离?老鼠一开始在 (0,0) 点处。

输入格式

第一行有一个整数,表示奶酪的数量 n。

第 2 到第 (n+1) 行,每行两个实数,第 (i+1) 行的实数分别表示第 i 块奶酪的横纵坐标 xi​,yi​。

输出格式

输出一行一个实数,表示要跑的最少距离,保留 2 位小数。

思路:f[i][j] 表示当前位于点 i,已经访问过的点的状态为 j 时的最短路径长度。这里使用二进制状态压缩来表示哪些点已经被访问过。

f[i][k]=min(f[j][k-(1<<(i-1))]+dis(i,j),f[i][k]);通过这个式子不断更新,最后f[][1<<(i-1)-1]的最大值即为答案.

代码:

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
using namespace std;
int n;
double x[20];
double y[20];
double f[17][1 << 17];
double dis(int a, int b)
{
	return sqrt(pow(x[a] - x[b], 2) + pow(y[a] - y[b], 2));
}
int main()
{
	cin >> n;
	x[0] = 0;
	y[0] = 0;
	for (int i = 0; i <= n; i++)
	{
		for (int j = 1; j < (1 << n); j++)
		{
			f[i][j] = 99999999;
		}
	}
	for (int i = 1; i <= n; i++)
	{
		cin >> x[i] >> y[i];
	}
	for (int i = 1; i <= n; i++)
	{
		f[i][1 << (i - 1)] = dis(0, i);
	}
	for (int k = 1; k < (1 << n); k++)
	{
		for (int i = 1; i <= n; i++)
		{
			if ((k & 1 << (i - 1)) == 0)
			{
				continue;
			}
			for (int j = 1; j <= n; j++)
			{
				if (i == j)
				{
					continue;
				}
				if ((k & 1 << (j - 1)) == 0)
				{
					continue;
				}
				f[i][k] = min(f[i][k], f[j][k - (1 << (i - 1))] + dis(i, j));
			}
		}
	}
	double ans = 99999999;
	for (int i = 1; i <= n; i++)
	{
		ans = min(ans, f[i][(1 << n) - 1]);
	}
	printf("%.2lf", ans);
	return 0;
}

例三:关灯问题

现有 n 盏灯,以及 m 个按钮。每个按钮可以同时控制这 n 盏灯——按下了第 i 个按钮,对于所有的灯都有一个效果。按下 i 按钮对于第 j 盏灯,是下面 3 中效果之一:如果 ai,j​ 为 1,那么当这盏灯开了的时候,把它关上,否则不管;如果为 −1 的话,如果这盏灯是关的,那么把它打开,否则也不管;如果是 0,无论这灯是否开,都不管。

现在这些灯都是开的,给出所有开关对所有灯的控制效果,求问最少要按几下按钮才能全部关掉。

输入格式

前两行两个数,n,m。

接下来 m 行,每行 n 个数 ,ai,j​ 表示第 i 个开关对第 j 个灯的效果。

输出格式

一个整数,表示最少按按钮次数。如果没有任何办法使其全部关闭,输出 −1。

思路:将灯的状态用二进制模拟,利用bfs遍历搜索。

代码:

#include<iostream>
#include<queue>
using namespace std;
int n, m;
int a[105][15];
bool used[1 << 15];
struct node
{
	int ss;
	int step;
};
int main()
{
	cin >> n;
	cin >> m;
	queue<node>q;
	for (int i = 1; i <= m; i++)
	{
		for (int j = 1; j <= n; j++)
		{
			cin >> a[i][j];
		}
	}
	q.push({ (1 << n) - 1, 0 });
	used[(1 << n) - 1] = true;
	int s = -1, step;
	while (!q.empty())
	{
		node ss = q.front();
		s = ss.ss;
		if (s == 0)
		{
			step = ss.step;
			break;
		}
		q.pop();
		for (int i = 1; i <= m; i++)
		{
			s = ss.ss;
			step = ss.step;
			for (int j = 1; j <= n; j++)
			{
				if (a[i][j] == 1 && (1 << (j - 1) & s))
				{
					s ^= 1 << (j - 1);
				}
				else if (a[i][j] == -1 && !(1 << (j - 1) & s))
				{
					s |= 1 << (j - 1);
				}
			}
			if (!used[s])
			{
				used[s] = true;
				step++;
				q.push({ s,step });
			}

		}
	}
	if (s == 0)
	{
		cout << step;
	}
	else
	{
		cout << -1;
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值