状态压缩DP

目录

一,状态压缩DP

二,OJ实战

POJ 1185 炮兵阵地

POJ 1321 棋盘问题

POJ 2411 Mondriaan's Dream

POJ 3254 Corn Fields

POJ 2441 Arrange the Bulls

HDU - 1565 方格取数(1)


一,状态压缩DP

与数位DP相反,数位DP是把0-999这个长1000的一维空间分解成10*10*10的三维空间,

而状态压缩空间是把a*a*a...*a的n维空间组合成长L的一维空间,其中L=a^n

一般的,a=2

 

二,OJ实战

POJ 1185 炮兵阵地

题目:

Description

司令部的将军们打算在N*M的网格地图上部署他们的炮兵部队。一个N*M的地图由N行M列组成,地图的每一格可能是山地(用"H" 表示),也可能是平原(用"P"表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示: 
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。 
现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。 

Input

第一行包含两个由空格分割开的正整数,分别表示N和M; 
接下来的N行,每一行含有连续的M个字符('P'或者'H'),中间没有空格。按顺序表示地图中每一行的数据。N <= 100;M <= 10。

Output

仅一行,包含一个整数K,表示最多能摆放的炮兵部队的数量。

Sample Input

5 4
PHPP
PPHH
PPPP
PHPP
PHHP

Sample Output

6

首先,要判断一行里面有没有2个选中的p是互斥的。

bool ok(int n)		//保证每2个1之间至少有2个0
{
	int a = 3, b = 5;
	for (int i = 0; i < 10; i++)
	{
		if ((n & a) == a)return false;
		if ((n & b) == b)return false;
		a *= 2;
		b *= 2;
	}
	return true;
}

经过24小时的奋战(真的有24小时了,虽然中间做了很多其他的事情,不过这个坎在我心中,我必须得迈过去)

经过各种优化,终于以532ms的时间AC了(限制是2000)

代码:

#include<iostream>
#include<string.h>
using namespace std;

long long sum[2][1024][1024];
int list[100], a, b, n, m, f[1024];
char c;
int ok[60] = { 0, 1, 2, 4, 8, 9, 16, 17, 18, 32, 33, 34, 36, 64, 65, 66, 68, 72, 73, 128, 129, 130, 132,
    136, 137, 144, 145, 146, 256, 257, 258, 260, 264, 265, 272, 273, 274, 288, 289, 290, 292,
    512, 513, 514, 516, 520, 521, 528, 529, 530, 544, 545,546,548,576,577,578,580,584, 585 };
int r1, r2, r3;

int main()
{
	cin >> n >> m;
	memset(list, 0, sizeof(list));
	memset(sum, 0, sizeof(sum));
	memset(f, 0, sizeof(f));
	for (int i = 0; i < 1024; i++)for (int j = 0; j < m; j++)if (i & (1 << j))f[i]++;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
		{
			cin >> c;
			if (c == 'P')list[i] += (1 << j);
		}
	}
	long long s = 0;
	if (n == 1)
	{
		for (int j = 0; j < 60; j++)
			if ((ok[j] & list[0]) == ok[j] && s < f[ok[j]])s = f[ok[j]];
		cout << s;
		return 0;
	}
	for (int i = 0; i < n - 1; i++)	//第i行和第i+1行
	{
		a = i % 2, b = 1 - a;
		for (int j = 0; j < 60; j++)		//第i行
		{
			r2 = ok[j];
			if ((r2 & list[i]) != r2)continue;
			for (int k = 0; k < 60; k++)		//第i+1行
			{
				r3 = ok[k];
				if ((r3 & list[i + 1]) != r3 || (r3 & r2) != 0)continue;
				sum[b][r2][r3] = f[r3];				
				if (i == 0) {
					sum[b][r2][r3] += f[r2];
					continue;
				}
				for (int l = 0; l < 60; l++)	//第i-1行
				{
					r1 = ok[l];
					if ((r1 & list[i - 1]) == r1 && (r1 & r2) == 0 && (r1 & r3) == 0 && sum[b][r2][r3] < sum[a][r1][r2] + f[r3])
					{
						sum[b][r2][r3] = sum[a][r1][r2] + f[r3];
					}
				}
			}
		}
	}
	for (int j = 0; j < 60; j++)for (int k = 0; k < 60; k++)
		if (s < sum[b][ok[j]][ok[k]])s = sum[b][ok[j]][ok[k]];
	cout << s;
	return 0;
}

优化1:直接把ok函数变成了ok数组。

关键就是把第4层 l 的循环从1024次运算变成了60次。

优化2:把 f 函数也变成了 f 数组,因为比较长,虽然没有硬编码,但是在核心循环之前预处理了。

我已经快麻木了

突然感觉这个题目好恶心啊。。。

POJ 1321 棋盘问题

题目:

Description

在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。

Input

输入含有多组测试数据。 
每组数据的第一行是两个正整数,n k,用一个空格隔开,表示了将在一个n*n的矩阵内描述棋盘,以及摆放棋子的数目。 n <= 8 , k <= n 
当为-1 -1时表示输入结束。 
随后的n行描述了棋盘的形状:每行有n个字符,其中 # 表示棋盘区域, . 表示空白区域(数据保证不出现多余的空白行或者空白列)。 

Output

对于每一组数据,给出一行输出,输出摆放的方案数目C (数据保证C<2^31)。

Sample Input

2 1
#.
.#
4 4
...#
..#.
.#..
#...
-1 -1

Sample Output

2
1

情不自禁的想起来,以前自己玩游戏,遇到计算量特别大的关卡,玩不过去,可以编程解决的就会编程解决。

最变态的时候写过22层的循环,因为问题那个游戏真的好难好难。。。

于是我写了一个8层的循环。。。。

代码:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[8];		//输入的棋盘状态
char c;
int sum;
int result;		//最后的结果
 
int main()
{
	int n, k;
	while (cin >> n >> k)
	{
		if (n == -1)break;
		memset(list, 0, sizeof(list));
		result = 0;
		for (int i = 0; i < n; i++)		
		{
			for (int j = 0; j < n; j++)
			{
				cin >> c;
				if (c == '#')list[i] += (1 << j);
			}
		}
		for (int i7 = 0; i7 < (1 << n); i7++)//8层循环,要注意顺序,因为如果n<7的话i7只能是0
		{
			if (i7 != (i7&(-i7)))continue;
			if ((i7&list[7]) != i7)continue;
		for (int i6 = 0; i6 < (1 << n); i6++)
		{
			if (i6 != (i6&(-i6)))continue;	//1行只能放1个
			if ((i6&list[6]) != i6)continue;	//只能放在写了'#'的格子里面
			if (i6&i7)continue;		//1列只能放1个
		for (int i5 = 0; i5 < (1 << n); i5++)
		{
			if (i5 != (i5&(-i5)))continue;
			if ((i5&list[5]) != i5)continue;
			if (i5&(i6 | i7))continue;
		for (int i4 = 0; i4 < (1 << n); i4++)
		{
			if (i4 != (i4&(-i4)))continue;
			if ((i4&list[4]) != i4)continue;
			if (i4&(i5 | i6 | i7))continue;
		for (int i3 = 0; i3 < (1 << n); i3++)
		{
			if (i3 != (i3&(-i3)))continue;
			if ((i3&list[3]) != i3)continue;
			if (i3&(i4 | i5 | i6 | i7))continue;
		for (int i2 = 0; i2 < (1 << n); i2++)
		{
			if (i2 != (i2&(-i2)))continue;
			if ((i2&list[2]) != i2)continue;
			if (i2&(i3 | i4 | i5 | i6 | i7))continue;
			for (int i1 = 0; i1 < (1 << n); i1++)
			{
				if (i1 != (i1&(-i1)))continue;
				if ((i1&list[1]) != i1)continue;
				if (i1&(i2 | i3 | i4 | i5 | i6 | i7))continue;
				for (int i0 = 0; i0 < (1 << n); i0++)
				{
					if (i0 != (i0&(-i0)))continue;
					if ((i0&list[0]) != i0)continue;
					if (i0&(i1 | i2 | i3 | i4 | i5 | i6 | i7))continue;
					sum = 0;
					if (i0)sum++;			//整个棋盘放了多少个棋子
					if (i1)sum++;
					if (i2)sum++;
					if (i3)sum++;
					if (i4)sum++;
					if (i5)sum++;
					if (i6)sum++;
					if (i7)sum++;
					if (sum == k)result++;
				}
			}
		}
		}
		}
		}
		}
		}
		cout << result << endl
	}
	return 0;
}

(为了把这个代码弄的这么好看,我也是拼了。。。)

很显然,这个方法是很慢很慢的(虽然我AC了,但是这肯定是不行的)

于是我又写了新的代码:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[8];		//输入的棋盘状态
int r[8][256];
char c;
int sum;
int k;
 
bool ok(int n)
{
	int s = 0;
	for (int a = 1; a < 256; a *= 2)if (n & a)s++;
	if (s>k)return false;
	return true;
}
 
int main()
{
	int n;
	while (cin >> n >> k)
	{
		if (n == -1)break;
		memset(list, 0, sizeof(list));
		memset(r, 0, sizeof(r));
		for (int i = 0; i < n; i++)		//输入
		{
			for (int j = 0; j < n; j++)
			{
				cin >> c;
				if (c == '#')list[i] += (1 << j);
			}
		}
		for (int i = 0; i < (1 << n); i++)
		if ((i&list[0]) == i && ok(i)&&(i&(-i))==i)r[0][i] = 1;
		for (int i = 1; i < n; i++)for (int j = 0; j < (1 << n); j++)
		if (ok(j))for (int kk = 0; kk < (1 << n); kk++)
		if ((j&kk) == kk && ((j^kk)&list[i]) == (j^kk) && ((j^kk)&(-(j^kk))) == (j^kk))
		r[i][j] += r[i - 1][kk];
		sum = 0;
		for (int i = 0; i < (1 << n); i++)
		{
			int s = 0;
			for (int a = 1; a < 256; a *= 2)if (i & a)s++;
			if(s==k)sum += r[n - 1][i];
		}
		cout << sum << endl;
	}
	return 0;
}

主要就是r

r[i][j] 表示的是前i行的状态是 j 的情况数。

比如说,i=4,j=1011,那么前4行的状态是第1,2,4个被取了。(之所以用这样的顺序是因为维度是不确定的,这样方便很多)

可能是第1行取1,第2行取2,第4行取4,

也有可能是第1行取2,第2行取4,第4行取1,等等。。。

POJ 2411 Mondriaan's Dream

题目:

Description

Squares and rectangles fascinated the famous Dutch painter Piet Mondriaan. One night, after producing the drawings in his 'toilet series' (where he had to use his toilet paper to draw on, for all of his paper was filled with squares and rectangles), he dreamt of filling a large rectangle with small rectangles of width 2 and height 1 in varying ways. 
Expert as he was in this material, he saw at a glance that he'll need a computer to calculate the number of ways to fill the large rectangle whose dimensions were integer values, as well. Help him, so that his dream won't turn into a nightmare!

Input

The input contains several test cases. Each test case is made up of two integer numbers: the height h and the width w of the large rectangle. Input is terminated by h=w=0. Otherwise, 1<=h,w<=11.

Output

For each test case, output the number of different ways the given rectangle can be filled with small rectangles of size 2 times 1. Assume the given large rectangle is oriented, i.e. count symmetrical tilings multiple times.

Sample Input

1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0

Sample Output

1
0
1
2
3
5
144
51205

这个题目,讲道理思路是很多的。

但是要想找到合适的方式来表达状态,确实不容易。

如果不是今天在这之前做了另外一个题目:POJ 1321 棋盘问题

我可能还真不会想到这个方法。

代码:

#include<iostream>
#include<string.h>
using namespace std;
 
 
int h, w;
long long dp[11][2048];	//前i行的和
long long sum;
 
//对于每一行,横为0,竖为1,所以00011是不可能的,00100是可能的
bool ok(int n)		//每2个0都是在一起的
{
	n += (1 << w);	//因为不知道维度的奇偶性
	while (n)
	{
		if (n % 2)n /= 2;
		else
		{
			if (n % 4)return false;
			n /= 4;
		}
	}
	return true;
}
 
int main()
{
	while (cin >> h >> w)
	{
		if (h == 0)break;
		if (h == 1)
		{
			cout << (w + 1) % 2 << endl;
			continue;
		}
		if (h % 2 && w % 2)
		{
			cout << 0 << endl;
			continue;
		}
		memset(dp, 0, sizeof(dp));
		for (int i = 0; i < (1 << w); i++)if(ok(i))dp[0][i] = 1;
		for (int i = 1; i < h-1; i++)
		{
			for (int j = 0; j < (1 << w); j++)
			{
				for (int k = 0; k < (1 << w); k++)
				{
					if (j&k)continue;
					if (ok(j^k))
					{
						dp[i][j] += dp[i - 1][k];
					}
				}
				
			}
		}
		sum = 0;
		for (int k = 0; k < (1 << w); k++)if (ok(k))sum += dp[h - 2][k];
		cout << sum << endl;
	}
	return 0;
}

首先,怎么样把一行状态压缩。

如果骨牌是横着的,那么就是00,如果骨牌是竖着的,那么就是1(上面1行或者下面一行也有个1)

然后直接按照顺序串联起来。

然后,dp二维数组是什么呢?

就是前i行的和为j的情况数。

我觉得我并没有说清楚前i行的和是什么鬼,不过不重要,

这么说吧,如果第i行的某个格子是00中的0,或者11中下面的1,那么它对应 j 里面的0,

如果它是11中上面的1,即它和它下面一行的那个格子组成骨牌,那么它对应 j 里面的1。

也就是相当于,画一条横线标明前i行,j 就是那些被割开的骨牌。

POJ 3254 Corn Fields

题目:

Description

Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) square parcels. He wants to grow some yummy corn for the cows on a number of squares. Regrettably, some of the squares are infertile and can't be planted. Canny FJ knows that the cows dislike eating close to each other, so when choosing which squares to plant, he avoids choosing squares that are adjacent; no two chosen squares share an edge. He has not yet made the final choice as to which squares to plant.

Being a very open-minded man, Farmer John wants to consider all possible options for how to choose the squares for planting. He is so open-minded that he considers choosing no squares as a valid option! Please help Farmer John determine the number of ways he can choose the squares to plant.

Input

Line 1: Two space-separated integers:  M and  N 
Lines 2..  M+1: Line  i+1 describes row  i of the pasture with  N space-separated integers indicating whether a square is fertile (1 for fertile, 0 for infertile)

Output

Line 1: One integer: the number of ways that FJ can choose the squares modulo 100,000,000.

Sample Input

2 3
1 1 1
0 1 0

Sample Output

9

这个题目,因为递推比较简单,而且m和n也比较小,所以总体比较简单。

代码:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[13];
int r[13][4096];
 
bool ok(int n)		//一行里面不能有相邻的2个
{
	for (int i = 3; i <= 3072; i *= 2)if ((n&i) == i)return false;
	return true;
}
 
int main()
{
	int m, n;
	int a;
	cin >> m >> n;
	memset(list, 0, sizeof(list));
	for (int i = 1; i <= m; i++)		//输入
	{
		for (int j = 0; j < n; j++)
		{
			cin >> a;
			if (a)list[i] += (1 << j);
		}
	}
	memset(r, 0, sizeof(r));
	for (int i = 1; i <= m; i++)for (int j = 0; j < (1 << n); j++)		//求解
	if (ok(j) && (j&list[i]) == j)		//单就这一行来说是满足的
	{
		if (i == 1)r[1][j] = 1;
		else for (int k = 0; k < (1 << n); k++)
			if ((k&j) == 0)r[i][j] = (r[i - 1][k] + r[i][j]) % 100000000;
	}
	int s = 0;
	for (int i = 0; i < (1 << n); i++)s = (s + r[m][i]) % 100000000;	//对最后一行求和
	cout << s;
	return 0;
}

POJ 2441 Arrange the Bulls

题目:

Description

Farmer Johnson's Bulls love playing basketball very much. But none of them would like to play basketball with the other bulls because they believe that the others are all very weak. Farmer Johnson has N cows (we number the cows from 1 to N) and M barns (we number the barns from 1 to M), which is his bulls' basketball fields. However, his bulls are all very captious, they only like to play in some specific barns, and don’t want to share a barn with the others. 
So it is difficult for Farmer Johnson to arrange his bulls, he wants you to help him. Of course, find one solution is easy, but your task is to find how many solutions there are. 
You should know that a solution is a situation that every bull can play basketball in a barn he likes and no two bulls share a barn. 
To make the problem a little easy, it is assumed that the number of solutions will not exceed 10000000.

Input

In the first line of input contains two integers N and M (1 <= N <= 20, 1 <= M <= 20). Then come N lines. The i-th line first contains an integer P (1 <= P <= M) referring to the number of barns cow i likes to play in. Then follow P integers, which give the number of there P barns.

Output

Print a single integer in a line, which is the number of solutions.

Sample Input

3 4
2 1 4
2 1 3
2 2 4

Sample Output

4

这个题目,也不知道为什么,就是超时

应该是因为数组开的太大了,要换个方法吧。。。

代码:

#include<iostream>
#include<string.h>
using namespace std;
 
int list[20];	//每一行输入的数据
int r[20][1048576];
int n, m;
 
int f(int num)		//求num一共有多少位是1
{
	int sum = 0;
	for (int i = 1; i < (1 << m); i *= 2)if (num&i)sum++;
	return sum;
}
 
int main()
{
	cin >> n >> m;
	int a, b;
	memset(list, 0, sizeof(list));
	for (int i = 0; i < n; i++)
	{
		cin >> a;
		for (int j = 0; j < a; j++)
		{
			cin >> b;
			list[i] += (1 << (b-1));
		}
	}
	for (int i = 1; i < (1 << m); i++)if ((i&(-i))==i && (i&list[0]) == i)r[0][i] = 1;
	int k;
	for (int i = 1; i < n; i++)for (int j = 1; j < (1 << m); j++)
	if (f(j) == i + 1)for (int t = 1; t < (1 << m); t *= 2)
	if ((t&list[i]) == t && (t&j) == t)r[i][j] += r[i - 1][t^j];
	int sum = 0;
	for (int i = 0; i < (1 << m); i++)sum += r[n - 1][i];
	cout << sum;
	return 0;
}

经过我仔细的推敲,发现超时有2个主要问题。

 

第一,for (int i = 1; i < n; i++)for (int j = 1; j < (1 << m); j++)if (f(j) == i + 1)这个循环是个很奇怪的循环!

我很想去掉外面的循环,直接int i= f(j) -1

当然,这样并不可行,因为j从1开始往上循环,i的值不是单调单调,这样结果自然是错的。

第二,满足f(j) == i + 1的 j 有好多好多,最多的一个例子,组合数C(20,10)有18万多!

但是很显然,其中有很多很多是需要内层 t 的循环才能排除掉的,这个代价可不小啊!

 

这2个问题困扰了我一天,我开始看网上的博客。

我只发现2种说法,一种是map,一种是滚动数组。(说实话,我都不会)

但是我稍微看了一下他们的代码,我觉得他们的代码并没有解决上面的2个问题!

于是我关掉他们的博客继续思考。。。

突然我发现只需要最普通的队列居然就可以同时解决上面的2个问题!

代码:

#include<iostream>
#include<string.h>
#include<queue>
using namespace std;
 
int list[20];	//每一行输入的数据
queue<int>q;
int r[1048576];
int n, m;
 
int main()
{
	cin >> n >> m;
	int a, b;
	memset(list, 0, sizeof(list));
	memset(r, 0, sizeof(r));
	for (int i = 0; i < n; i++)
	{
		cin >> a;
		for (int j = 0; j < a; j++)
		{
			cin >> b;
			list[i] += (1 << (b - 1));
		}
	}
	int i, j, t;
	q.push(0);
	r[0] = 1;
	int sum = 0;
	while (!q.empty())
	{
		i = q.front();
		q.pop();
		j = 0;
		for (int ii = 1; ii < (1 << m); ii *= 2)if (i&ii)j++;
		if (j == n)sum += r[i];
		else
		{
			for (t = 1; t < (1 << m); t *= 2)
			{
				if (i&t)continue;
				if ((t&list[j]) == t)
				{
					if (r[t | i] == 0) q.push(t | i);
					r[t | i] += r[i];					
				}
			}
		}
	}
	cout << sum;
	return 0;
}

(顺便一提,函数 f 仍然用了,只不过写到main函数里面去了,没有写成函数的形式)

HDU - 1565 方格取数(1)

题目:

给你一个n*n的格子的棋盘,每个格子里面有一个非负数。 
从中取出若干个数,使得任意的两个数所在的格子没有公共边,就是说所取的数所在的2个格子不能相邻,并且取出的数的和最大。

Input

包括多个测试实例,每个测试实例包括一个整数n 和n*n个非负数(n<=20)

Output

对于每个测试实例,输出可能取得的最大的和

Sample Input

3
75 15 21 
75 15 28 
34 70 5 

Sample Output

188

代码:

#include<iostream>
using namespace std;
 
int n, num[20000], s = 1, maxx[20][20000], number[20];
 
void build(int k)
{
	if (k >20)return;
	for (int i = 0; num[i] < 1 << (k - 2); i++)num[++s] = num[i] + (1 << (k - 1));
	build(k + 1);
}
 
int getsum(int j)
{
	int sum = 0;
	for (int i = 0; i < n; i++)sum += ((j&(1 << i))>0)*number[i];
	return sum;
}
 
int main()
{
	num[0] = 0, num[1] = 1;
	build(2);
	while (cin >> n)
	{
		if (n == 0)
		{
			cout << "0\n";
			continue;
		}
		for (int i = 0; i < n; i++)for (int j = 0; num[j] < (1 << n); j++)maxx[i][j] = 0;
		for (int i = 0; i < n; i++)cin >> number[i];
		for (int j = 0; num[j] < (1 << n); j++)maxx[0][j] = getsum(num[j]);
		for (int i = 1; i < n; i++)
		{
			for (int i = 0; i < n; i++)cin >> number[i];
			for (int j = 0; num[j] < (1 << n); j++)for (int k = 0; num[k] < (1 << n); k++)
			if ((num[j] & num[k]) == 0 && maxx[i][j] < maxx[i - 1][k] + getsum(num[j]))
				maxx[i][j] = maxx[i - 1][k] + getsum(num[j]);
		}
		int ans = 0;
		for (int j = 0; num[j] < (1 << n); j++)if (ans < maxx[n - 1][j])ans = maxx[n - 1][j];
		cout << ans << endl;
	}
	return 0;
}

最坑爹的就是n=0的情况。。。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页