【状压dp】南京icpc- AC chanllege 、POJ- 3254 Corn Fields (有关状压dp) 详解。。没写代码

首先我们来学习一下有关状压dp的内容。参考链接

状压DP全称状态压缩DP,指的是根据题目情况和数据范围压缩数组的维度。

对于本题(POJ 3254),常见的方法是将每一行的方格肥沃状态以及作物可能的种植状态用二进制表达,再压缩为十进制

题目的意思是种树,每行给你一堆0和1,要求不能种在相邻、十字花里面(上下左右)

【常用状态!!】

(1)(具体分析,此题中)i&(i<<1)

可以完美表示预处理中,左右相邻的情况。。左移2位,如果有一个地方是1的话,最后结果都是&(与操作)

比如0101

和   1010  (x)

比如0011

和   0110  就有一个地方会重合了!

 

& 是判断重合的吧,比如两个地方都有1的话 &一下 最后就返回一个1,没有重合就返回个0

“用 x & (x<<1)来判断一个数相邻两位是不是同时为1,假如同时为 1 则返回一个值,否则返回 0 ,这样就能优化掉一些状态

用 x & y 的布尔值来判断相同为是不是同时为1。”

 

10000&10000返回的也是10000哦

【是因为,一旦有一个1,返回的就不是0了】

(2)

状态S 变成 S ^ (1<<i) 代表第i个怪物被你从未去过,变成怪物被你干掉了

1<<i就是2的i次方,因为是1往左边移动了几位(可以想做是<<的方向就是一根箭,左移咯)。然后,异或一下,第i个的话1<<i的第i位就是1了,原来是0,变成了1

其实,^就是加,看状态的定义。^更方便一点

(3)

在for循环中,if(!flag)cur[i]+=(1<<(n-j));

在题目中,输入0是无所谓的,1是要种下东西的

如果怎么样的话,就:cur[i] 代表的是一整行的种植状况,加上左移n-j位

这里其实 是二进制和十进制之间的一种转化

量化:

若得到数据为0011,for循环里0,加上(n-j是3),左移3位变成1000

for循环里0,加上左移2位变为1100

for循环里1,不动

继续1,不动,最后0011的数据就得到了1100

至于为什么反着,大概后面会有用吧

如果是正的完全可以写成if(flag) cur+=

这样cur每一个int(ll)数字,就用来记录了每一行的数。(加的时候是二进制,但是表现的形式都是十进制。)

【如果有三进制,一样可以用来压缩。比如怪物:被你打死,打了但是没死,没打过,预处理一下3的几几次方大概就好了】

(4)

【关键代码!!】

先init处理好所有可能状态,存入top中(用的是1的s&(s<<1))读入的状态处理,用的是cur+(1<<n-j)

处理一下初始的dp状态for(int i=1;i<=top;i++) if(!(state[i]&cur[1]))dp[1][i]=1;//初始化dp  

对于第一行(cur 1),如果state(所有可能方案)和第一行的放的不冲突的话,就可以往下转移(如果&的结果有一个为1,结果就不为0 ----不为0其实就是,!flag的意思呀)

注意第一行这里是相反的,如果是11100,读入变成00011,测试状态的时候11100可达(?下一行的是这样就可以转移走  如果是  00100的话,肯定不能种在下一行去了。。 就不能转移走,这样反着读入,方便判断啦,二进制要熟悉起来

(5)上面的是在初始化边界,边界搞好之后,先枚举行,再往后枚举可以的状态。

要注意哦,二进制在这里可谓非常方便,形如10100(输入为这样),我们完全可以种成10000,所以保存相反的,01011,只要不和01011冲突,里面就可以随便种了,所以代码后面先枚举了可能的状态,如果这种种植方法和上面不冲突,那么先放一下。

然后state[i]还要和cur[i-1]再比较,此时再次玄幻起来了,因为我们要得到的dp[i][j]这个状态可以这么放,是要和前面任意一种i-1的放法相比的,意思就是dp这个状态只要可以从前面的任何一个i-1转移过来,都是合法的,然后我们就去枚举i-1,每次i-1里面符合了cur[i-1]的话, 存一下,和k也不冲突的话,就可以+上它前面过来的状态数了(记得取模)

其实就是暴力选择第一个,再暴力枚举第二个,可行的话就++。。。

这复杂度够大的。。。 但是一共12个点,撑死了8000,64000.000才1e7 8的样子吧而且完全没有那么多因为好多相邻的1已经被优化没了。。。

 

POJ这个题:我只是看懂了,没写。。 不过看到这个份儿上我感觉除了机械copy之外,就是会陷入找不同=、=

https://blog.csdn.net/qq_35935435/article/details/58593486 (其他参考:1 2

----------------------------------------------------------------------------------------------------

好了南京这个题,先copy一下题意,其实思想的一样的哦

有 n 个问题(n≤20 ),解决一个问题花费 1 分钟,但解决问题 i 时,必须已经解决问题 pi,1,pi,2,…,pi,sipi,1,pi,2,…,pi,si (记为问题 i的前驱问题)。当问题 i是你解决的第 t 个问题(在第 t 分钟)时,总分加上 ai⋅t+biai⋅t+bi。求总分的最大值。

注意不一定要解决所有问题,且一个问题的前驱问题可能是它自身。

好,那我们先来分析一下,从无到有的话呢,分析中我们可以先写一些有用的信息。(针对下面写代码的,有用的信息)

【分析我们有什么?】(准备工作)

好的吧大佬说一眼题,因为n才20嘛,首先,每个地方的状态都可以存下来,更改的时候,做了第几题,显然可以用前面五个里面提到的那个 【第i个状态改变了,那么我们 s^(1<<i)】

看看大佬怎么写的.....

状态转移方程很明显..  肯定就是,dp[i]=max dp[i-1]+ t*ai+bi

但是要注意满足四个条件

这里需要满足:

  1. i 比 i-1 仅仅多解决了一个问题 i。
  2. 问题 i 不被包含在状态 i-1中。
  3. 问题 i的前驱问题都已经被解决(即都包含在状态 i-1中)
  4. i-1 是可以到达的合法状态。(大佬的博客讲的好清楚)

【补充知识】

(1)计算1的个数:传入x。int sum = 0 ; while(x) {  sum+=x&1;  x=x>>1;   }【注意要写等号,只有一个不行呀,变化】

(注意,只要&1就好了,因为只看最后一位,然后不断后面压缩(理解成向右拉伸的话其实就是除以2咯)) 

(2) 算子集的时候,(f|s)==f

举例,如果11000 和10000,或,就是11000啊,没那么严格了,有真则真,所以UI和==f的话f就是大的那个(或者相等)

(3)用到p的时候其实是p-1,比如第i个用到了,从s变为 st| (1<<i-1)  为啥是i-1呢,比如10000  第四个用到了,作为1要左移3位变成了100,注意是i-1哦,检测的时候也是,比如看看第四位是不是还是处子之身,那么右移3位就好了,

诶,似乎不对,这里的表示方法是反着的

if (((st >> (i - 1)) & 1) == 0) 

(这里其实就是+ ,但是作为二进制写着方便。。 前面是用的^异或的不进位加法表示,一样的哦)

int nxt=st | (1 << (i - 1));

这样就解决了表示方式问题,也变得合法起来,试着写写。。最后取最大值

 

大佬博客:https://hyp1231.github.io/2018/09/02/20180902-2018nanjing-online-e/ 讲的很清楚。。

我又没写。。。

-------------------------9/8 好了好了自己差不多改了一下


#include<iostream>
#include <algorithm>
#include <cstring>
using namespace std;
#define maxn 21
#define onj shazi
typedef long long ll;

 int count1(int x) {
	int sum = 0;
		while (x){
			sum += x & 1;//if the last number is 1  sum++
			x=x >> 1;//这里写错了饿,。。检查不出来
		}
	return sum;
}   // 计算 x 的二进制表示中有几个 1
 bool ziji(int x1, int x2){
	 //这里默认了x1比较大
	 if (x1 == (x1 | x2))return true;//没有越界,的确是x1大
	 else return false;
 }
ll n;
ll nxt[maxn];
ll a[maxn], b[maxn];
ll dp[1 << maxn];//注意,这里为什么是1<<maxn呢   其实就是2的maxn次方!!
ll best;

int main() {
	cin >> n;//scanf("%d", &n);
	int s, p;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i] >> b[i] >>s;//scanf("%lld%lld%d", &a[i], &b[i], &s);
		for (int j = 1; j <= s; j++) {
			cin >> p;//scanf("%d", &p);
			nxt[i] = nxt[i] | (1 << (p - 1));
			//nxt数组默认是0,读入的时候每次读入一个p,
			//比如一个1  变成1(不移动)一个10 移动1位。
			//这里就是少了一位。。很迷。。 
			//不过,真的是这样!这样子是倒着的,上个题目里,是1<<n-j  这里是正的
			//但是同样,3 2 1 0  (注意为1的时候移动0位。除了maxn那里因为状态是满的没这样减一
		//	neighbor[i] |= 1 << (p - 1);
		}
	}

	memset(dp, -1, sizeof(dp));
	dp[0] = 0;
	//**********为什么dp只要一个维度就好了呢?
	/*
	for (int i = 1; i <= (1 << n); i++){
		if (dp[i] != -1){
			int t = count1(i);//
			for (int j = 1; j <= n; j++){//尝试在一些地方+1
				int tmp1 = (i | 1 << (j - 1));//若本来就有1,则不行,可是没法判断
				if (tmp1 == i)//没有解决但是判定的可以,没用
					continue;
				else if (ziji(i, tmp1))//满足,又是子集的话,
					dp[i] += t*a[j] + b[j]+dp[tmp1];		
			}
		}
	}*/
	//不行的。、找子集 ,应该是从这一个转移到下一个
	//大概思路可能没错
	//而且,要满足它的前驱,(多加一个判定条件)
	//总之,从这个点寻找下一个点插入即可。
	//-1的状态,只能从合法状态往后转移。!
	//这里比较容易忽略,如果忽然是111000 ,插入个111100是可以的,但是11100是架空的
	//注意一下,只有转移过去的状态是合法的
	//然后,要取max,要从小到大枚举就一定是要倒着的。。。
	//历经了01才能到11啊。。。。
	for (int st = 0; st < (1 << n); ++st) if (dp[st] != -1) {   // 如果不是非法状态
		int t = count1(st) + 1;  // 解决下一个问题的时间
//		cout<<"aaa";	
		for (int i = 1; i <= n; ++i)
		if (((st >> (i - 1)) & 1) == 0) {  // i 是还没有解决的问题
			if (!ziji(st, nxt[i])) continue;
			// 如果还没解决 i 的所有前驱问题,跳过

			int next_st = st | (1 << (i - 1));
			dp[next_st] = std::max(dp[next_st], dp[st] + t * a[i] + b[i]);
		}
	}

	for (int st = 1; st < (1 << n); ++st) best = std::max(best, dp[st]);
	printf("%lld\n", best);

	return 0;
}

=。=。。。。。  我那个 x=x<<1 写错了  (用在用在。  !! 判断有几个1的时候,针对传进来的右移)

啊!只是x<<1没用啊  要赋值

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值