刷题周记(九)——#状压DP:最短Hamilton路径、小国王(互不侵犯)、玉米田(Corn Fields G)、愤怒的小鸟、吃奶酪、炮兵阵地、宝藏 #区间DP:清空字符串#DP:关灯问题II

——2020年12月20日(周日)——————————————————

状压DP

用二进制表示某个 状态 ,然后枚举目标状态,再枚举这个目标状态可以由哪几种状态转移过来,将这几种状态的方案数相加就是当前目标状态的方案数。

一、最短Hamilton路径(模板题)

题目

模板题
理解之后很简单的。
总之就是:
第一维枚举一下当前目标状态(我们想要得到的状态)
第二维枚举一下当前目标状态下,最后一个进来的点
然后枚举一下这个点是与哪个点连起来的…………

这不就是图论里Prim算法的原理嘛!
生成一棵最小生成树:
从点1开始建立连通块,
不断地用当前距离最小的点更新其它 连通块之外的 点的距离,
然后将这个点加入连通块。

只是这里状态转移的时候会有一些条件限制,而且枚举的方法也不一样。

#include<bits/stdc++.h>
using namespace std;
const int N = 20;
int m[N][N];
int f[1 << N][N];
int main(){
    memset(f, 0x3f, sizeof f);
    int n; cin >> n; 
    for(int i = 0; i < n; i ++)
        for(int j = 0; j < n; j ++)
            scanf("%d", &m[i][j]);
    //这里枚举的是当前最后路径状态
    f[1][0] = 0;
    for(int i = 1; i < (1 << n); i ++){
        //这里枚举的是当前最终状态下最后一个进来的点
        //(还要判断一下当前枚举到得最终状态下是否存在这个点)
        for(int j = 0; j < n; j ++) if((i >> j) & 1)
            //这里枚举k点,j点是从k点转移来的。
            for(int k = 0; k < n; k ++) if((i ^ (1 << j)) >> k & 1)
                f[i][j] = min(f[i][j], f[i ^ (1 << j)][k] + m[k][j]);
    }
    cout << f[(1 << n) - 1][n -1];
    return 0;
}

二、玉米田(P1879 [USACO06NOV]Corn Fields G)——(棋盘式)

题目来源1
题目来源2
输入得时候有个点:
/*得到该行的对应二进制数。
为什么要倒过来储存呢?
因为后面的代码 !(state[j] & w[i]) 是用来判断当前合法状态能否用这一行的玉米田表示出来的,
中间用的符号是 “与(&)”,如果不倒过来用1的话,如果无论这一位上面不需要种有玉米,难道就不能被表示出来吗?
举例,当前这一行玉米田输入的是 1 1 1,1 1 1可以表示0 0 0 这个状态,但是如果用(111&000)显然为0,这就矛盾了。
于是我们反过来记录,并将最后的判断结果再一次反过来,也就是!(000&000)这样就可以得到想要的结果了。
换个说法:
玉米田第i行的输入状态:1 0 1
要表示的状态1:0 0 0
101&000 = 0,不可以被表示,
!(010&001) = 1,可以被表示。
要表示的状态2:0 0 1
101&001 = 1,可以被表示,
!(010&001) = 1,可以被表示。
要表示的状态3:1 0 1
101&101 = 5,可以被表示,
!(010&001) = 1,可以被表示。
那么这一对比就很明显了,这样倒过来就是为了保证要表示000这个合法状态的时候不会出错,
另行判断的话代码会变得很复杂,为了保证代码的思路更加清晰,于是用这种巧妙的办法来规避。
*/

#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 14, M = 1 << 12, mod = 1e8;
int n, m;
int w[N];
vector<int> state;
vector<int> head[M];
int f[N][M];

//检查是否有连续的1出现
bool check(int state){
    for (int i = 0; i + 1 < m; i ++ )
        if ((state >> i & 1) && (state >> i + 1 & 1))
            return false;
    return true;
}

int main(){
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ )
        for (int j = 0; j < m; j ++ ){
            int t; cin >> t;
            w[i] += !t * (1 << j);
        }
    
    //得到所有的合法状态。
    for (int i = 0; i < 1 << m; i ++ )
        if (check(i))
            state.push_back(i);

    //得到所有与合法状态a相容的合法状态b
    for (int i = 0; i < state.size(); i ++ )
        for (int j = 0; j < state.size(); j ++ ){
            int a = state[i], b = state[j];
            if (!(a & b))
                head[i].push_back(j);
        }

    f[0][0] = 1;
    //行数
    for (int i = 1; i <= n + 1; i ++ )
        //找第i行的合法状态
        for (int j = 0; j < state.size(); j ++ )
            //合法状态j在这一行可以被表示出来。
            if (!(state[j] & w[i]))
                //然后找第i - 1行的合法状态
                for (int k : head[j])
                    //对应合法状态应该是上一行相容的所有合法状态的方案数的总和
                    f[i][j] = (f[i][j] + f[i - 1][k]) % mod;
                        
    cout << f[n + 1][0] << endl;
    return 0;
}

以下是二次写的,不知道哪里错了……

#include<bits/stdc++.h>
using namespace std;
const int N = 14;

int n, m;
int a[N];
vector<int> state;
vector<int> S[N];
int f[N][1 << N];

bool check(int state)
{
    for (int i = 0; i + 1 < n; i ++ )
        if (!(state >> i & 1) && !((state >> i + 1) & 1))
            return false;
    return true;
}

int main(){
	cin >> n >> m;
	//土地状态 
	for(int i = 1; i <= m; i ++)
		for(int j = 1; j <= n; j ++)
		{
			int c; cin >> c;
			a[i] = (a[i] << 1) | c;
		}
	
	int inf = 1 << n - 1;
	
	//求所有合法状态 :0代表种,1代表不种 ,左右合法 
    for (int i = 0; i < 1 << m; i ++ )
        if (check(i))
            state.push_back(i);
	
	for(int i = 0; i < state.size(); i ++)
		for(int j = 0; j < state.size(); j ++)
			//上下合法
			if((state[i] | state[j]) == inf)
				S[i].push_back(j);
			
	
	f[0][inf - 1] = 1;
	//行 
	for(int k = 1; k <= m + 1; k ++)
		//当前行的状态 
		for(int i = 0; i < state.size(); i ++){
			int mrk = state[i];
			if( ( mrk | a[k] ) == inf){
			//上一行的状态
				for(int j = 0; j < S[i].size(); j ++){
					int pre_mrk = state[S[i][j]];
					if(pre_mrk | a[k - 1] == inf)
						f[k][mrk] += f[k - 1][pre_mrk];	
			}
		}
	}
	
	cout << f[m + 1][inf];
	return 0;
} 

三、小国王(P1896 [SCOI2005]互不侵犯)——(棋盘式)

题目来源1:luogu
题目来源2:Acwing

#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 12, M = 1 << 10, K = 110;
int n, m;
//这里是合法状态,由于是用二进制来进行表示,所以一个整数就是一个状态。
vector<int> state;
int cnt[M];
vector<int> head[M];
LL f[N][K][M];

//这里检查的是相邻的两位之间有没有都是一得情况
bool check(int state)
{
    for (int i = 0; i < n; i ++ )
        if ((state >> i & 1) && (state >> i + 1 & 1))
            return false;
    return true;
}

//这里数的是当前合法状态下有多少个1
int count(int state)
{
    int res = 0;
    for (int i = 0; i < n; i ++ ) res += state >> i & 1;
    return res;
}

int main()
{
    cin >> n >> m;

    //这里是初始化,先将所有的合法状态压缩成一个数,然后入队state,并数出里面有多少个1
    for (int i = 0; i < 1 << n; i ++ )
        if (check(i))
        {
            state.push_back(i);
            cnt[i] = count(i);
        }
    
    //枚举所有的合法状态(的下标),并找找其它合法状态有多少是和它相容的。
    for (int i = 0; i < state.size(); i ++ )
        for (int j = 0; j < state.size(); j ++ )
        {
            int a = state[i], b = state[j];
            if ((a & b) == 0 && check(a | b))
                head[i].push_back(j);
        }

    
    f[0][0][0] = 1;
    //这里枚举的是行数。为什么要枚举多一行?
    //这是因为最后还要进行一个汇总的过程,干脆就多枚举一行顺便得到答案,减少代码量。
    for (int i = 1; i <= n + 1; i ++ )
        //枚举国王的数量
        for (int j = 0; j <= m; j ++ )
            //枚举所有合法状态(的下标)
            for (int a = 0; a < state.size(); a ++ )
                //然后找到与a相容的合法状态b
                for (int b : head[a]){
                    //记住a是下标,cnt记录的元素是状态。
                    int c = cnt[state[a]];
                    //当然,这个数量不可以超过枚举到的国王数。
                    if (j >= c) 
                        //当前状态应该是由所有上一行的状态为b,且放了j - c个棋子的方案的总和。
                        f[i][j][a] += f[i - 1][j - c][b];
                }
    //最后输出答案
    cout << f[n + 1][m][0] << endl;

    return 0;
}

——2020年11月21日(周一)——————————————————

?设计密码(我也不知道什么时候会做,这周是状压DP)

题目

——2020年11月22日(周二)——————————————————

一、清空字符串

莫名其妙又又没有保存,写了半天气死…………

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 610;
char s[N] , s1[N];
int n , ans , len;
int dp[N][N];
int main()
{
	memset(dp , 0x3f , sizeof dp);
	scanf("%d" , &len);
	scanf("%s" , s1 + 1);
	for(int i = 1 ; i <= len ;)
	{
		int j = i + 1;
		while(s1[i] == s1[j] && j <= len) j++;
		s[++n] = s1[i];
		i = j;
	}
	for(int i = 1 ; i <= n ; i++) dp[i][i] = 1;
	//得到新的字符串并初始化 
	for(int k = 1 ; k <= n ; k++)
	{
		for(int l = 1 ; l + k <= n ; l++)
		{
			int r = l + k;
			//这里先忽略两头的字符,因为最优解肯定是从中间开始合并的,那么就留着两头中的一个最后解决,因为最后肯定只剩下头和尾了。
			dp[l][r] = min(dp[l + 1][r] , dp[l][r - 1]);
			for(int j = l + 1 ; j < r ; j++)
				dp[l][r] = min(dp[l][r] , dp[l + 1][j] + dp[j + 1][r]);
			for(int j = l ; j < r - 1 ; j++)
				dp[l][r] = min(dp[l][r] , dp[l][j] + dp[j + 1][r - 1]);
			if(s[l] != s[r])
				dp[l][r]++;
//			cout << dp[l][r] << " ";
		}
//		cout << endl;
	}
	//谁也不知道为什么这里还要再跑一遍找最优答案,不过就是有一个点过不了,郁闷。
	for(int j = 1 ; j < n ; j++)
		dp[1][n] = min(dp[1][n] , dp[1][j] + dp[j + 1][n]);
	int res = dp[1][n];
	printf("%d" , res);
	return 0;
}

二、愤怒的小鸟(NOIP2016提高组)

的确够恶心,将抛物线和状压DP结合,题目里面还给了一个没用的变量m(居然还一本正经地解释了一大堆奇怪的用途)就更离谱了。
下面这张图是得到一个 过两个定点的抛物线 的方程式推导。
在这里插入图片描述
随便选一个猪猪进行打击那一步实在麻烦,结合某个例子来理解会更清晰。
注释版代码:

这个是允许的误差值,因为是浮点数double,所以无法像整数那样直接判断相等。
const double eps = 1e-8;

输入的小猪位置
PDD q[N];

这里是“可以打到第i个小猪和第j个小猪的抛物线可以打到的小猪”的状态:1表示这个小猪打爆了,0表示这个小猪还活得好好的。
int path[N][N];

这里表示满足 打击后的状态变成M 的情况下需要的最少抛物线数。
int f[M];

这里就是double的按精度比较大小的办法
int cmp(double x, double y)
{
    if (fabs(x - y) < eps) return 0;
    if (x < y) return -1;
    return 1;
}

        for (int i = 0; i < n; i ++ )
        {
            只打到一个小猪的状态就是只有这一位上为1。
            path[i][i] = 1 << i;
            
            for (int j = 0; j < n; j ++ )
            {
                
                将两个小猪的横纵坐标提出来
                double x1 = q[i].x, y1 = q[i].y;
                double x2 = q[j].x, y2 = q[j].y;
                
                不是同一条直线上的才可以进行下一步
                if (!cmp(x1, x2)) continue;
                
                这个可以找到一个同时穿过两个点的抛物线,绝对可以的,试着把二次函数图象变得夸张点就会明白了。
                double a = (y1 / x1 - y2 / x2) / (x1 - x2);
                double b = y1 / x1 - a * x1;
                
                a接近0的时候是不合法的,-b/2a中分母不能为0;
                if (cmp(a, 0) >= 0) continue;
                
                state就是状态了
                int state = 0;
                
                然后枚举一下这个抛物线可以又穿过哪些点。
                for (int k = 0; k < n; k ++ )
                {
                    double x = q[k].x, y = q[k].y;
                    
                    要是x代入后得到的y于这个点的y相等的话,这个状态上对应的位置就可以变为1if (!cmp(a * x * x + b * x, y)) state += 1 << k;
                }
                
                然后将“可以打到第i个小猪和第j个小猪的抛物线可以打到的小猪“这个状态记录一下。
                path[i][j] = state;
            }
        }
        
        memset(f, 0x3f, sizeof f);
        f[0] = 0;
        
        枚举2^n - 2种状态因为全是1的情况不用更新了。
        for (int i = 0; i + 1 < 1 << n; i ++ )
        {
            int x = 0;
            
            枚举第j位上的数,找到第一个0,也就是找到第一个还没有被打到的小猪,为什么呢?因为我们要打爆它啊。
            其实等于是随便找一个0来进行打击啦,这样的效果最后是和全部0都打一遍是一样的。
            举例子:假设有三个猪猪,三个猪猪做不到同时打击,但是有每两个我们都可以有办法打到.
            
            首先是将所有的状态枚举出来。
            path[1][1] = 001; path[1][2] = 011; path[1][3] = 101;
            path[2][1] = 011; path[2][2] = 010; path[2][3] = 110;
            path[3][1] = 101; path[3][2] = 110; path[3][3] = 100;
            
            f[000] = 0: f[001] = 1, f[011] = 1, f[101] = 1;
            其实到下面这步就已经搞定了,后面都没必要了都。不过这是数据问题,其它数据不一定能过,这只能说明这个方法实际操作十分可行。
            f[001] = 1: f[011] = 2, f[011] = 2, f[111] = 2;
            有些状态时不会被更新到的,不难看出也没有更新的必要,也没有用它来更新其它状态的必要。
            f[010] = 0x3f: f[011] = 1, f[011] = 1, f[111] = 2;
            f[011] = 1: f[111] = 2 , f[111] = 2, f[111] = 2;
            f[100] = 0x3f: f[101] = 1, f[111] = 2, f[101] = 1;
            f[101] = 1: f[111] = 2, f[111] = 2, f[111] = 2;
            f[111] = 2就没必要再搞了,因为已经搞定了。
            不难发现,其中会有一些值没有被更新,因为有它没它都是一样的答案,没它的话反而时间上会减少,岂不美哉?
            不死心的话再看看全部遍历一遍的结果:……以后再补上吧,浪费时间,将上面的手模一遍差不多了

			其实最主要的原因是,这条抛物线上有可能有其它的点,有时两个点之间往往有很多个点。
			二进制状态间进行或运算的时候可以一次搞定很多个点,于是不必要一个个地来枚举进行状态转移。
			这是二进制状态转移的优势,有些状态可以很简单地不漏地就表示出来。
            
            for (int j = 0; j < n; j ++ )
                if (!(i >> j & 1)){
                    x = j;
                    break;
                }
                
            枚举第j位上的数,
            for (int j = 0; j < n; j ++ )
                path[x][j]表示“能打到第x个小猪以及第j个小猪的抛物线”
                f[i | path[x][j]]表示达到两条抛物线组合起来后状态,需要的最少抛物线数。
                最后还要和f[i]+1也就是原来状态自己随便加上一条抛物线的数量比较一下。
                f[i | path[x][j]] = min(f[i | path[x][j]], f[i] + 1);
        }        

纯净版代码:

#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>

#define x first
#define y second

using namespace std;

typedef pair<double, double> PDD;

const int N = 18, M = 1 << 18;
const double eps = 1e-8;

int n, m;
PDD q[N];
int path[N][N];
int f[M];

int cmp(double x, double y)
{
    if (fabs(x - y) < eps) return 0;
    if (x < y) return -1;
    return 1;
}

int main()
{
    int T; cin >> T;
    while (T -- )
    {
        cin >> n >> m;
        for (int i = 0; i < n; i ++ ) cin >> q[i].x >> q[i].y;
        
        memset(path, 0, sizeof path);
        for (int i = 0; i < n; i ++ )
        {
            path[i][i] = 1 << i;
            for (int j = 0; j < n; j ++ )
            {
                double x1 = q[i].x, y1 = q[i].y;
                double x2 = q[j].x, y2 = q[j].y;
                if (!cmp(x1, x2)) continue;
                double a = (y1 / x1 - y2 / x2) / (x1 - x2);
                double b = y1 / x1 - a * x1;
                if (cmp(a, 0) >= 0) continue;
                int state = 0;
                for (int k = 0; k < n; k ++ )
                {
                    double x = q[k].x, y = q[k].y;
                    if (!cmp(a * x * x + b * x, y)) state += 1 << k;
                }
                path[i][j] = state;
            }
        }
        
        memset(f, 0x3f, sizeof f);
        f[0] = 0;
        for (int i = 0; i + 1 < 1 << n; i ++ )
        {
            int x = 0;
            for (int j = 0; j < n; j ++ )
                if (!(i >> j & 1))
                {
                    x = j;
                    break;
                }
                
            for (int j = 0; j < n; j ++ )
                f[i | path[x][j]] = min(f[i | path[x][j]], f[i] + 1);
        }
        cout << f[(1 << n) - 1] << endl;
    }

    return 0;
}

三、关灯问题II(状压??)

题目

经典状压问题,感觉还好。

好像不能用状压来做……
好吧,的确有问题

以下是一开始的有问题的分析

for( i = (1<<n)-1;i>=0;i--){//枚举状态
		for( j = 1; j <= m; j ++){//枚举开关
			int now = i;//我们的now最终会变为,按完这个开关后的状态.
			for( l = 1;l <= n;l ++){//枚举控制的灯.
				if(a[j][l] == 0)continue;//不操作
				//以下是还原到上一步的状态。 
				if(a[j][l] == 1 and (i & (1<<(l-1)))) now ^= (1<<(l-1));//第一个操作, 关灯
				if(a[j][l] == -1 and !(i & (1<<(l-1)))) now ^= (1<<(l-1));//第二个操作,开灯
			}
			f[now] = min(f[now], f[i]+1);//常规操作,求最小操作次数.
            //因为我们可以通过i状态操作按一个开关到达now状态.
            //所以是f[i]+1.
		}
	}

纯净版代码(有问题的)

	
#include<bits/stdc++.h>
using namespace std;
const int N = 11, M = 1000;
int n, m, f[1 << N], a[M][N];
int main() {
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= m; ++i) 
		for(int j = 1; j <= n; ++j) 
			scanf("%d", &a[i][j]);
	memset(f, 0x3f, sizeof f);
	f[0] = 0;
	for(int i = 0; i < (1 << n); ++i) {
		for(int k = 1; k <= m; ++k) {
			int s = i;
			for(int j = 0; j < n; ++j)
				if((a[k][j + 1] == 1 && !(i & 1 << j)) || (a[k][j + 1] == -1 && i & 1 << j))
					s ^= 1 << j;
			f[s] = min(f[s], f[i] + 1);		
		}			
	}
	if(f[(1 << n) - 1] == 0x3f3f3f3f)
		puts("-1");
	else
		printf("%d\n",f[(1 << n) - 1]);
	return 0;
}

用状压来做会有些数据无法过去(但是洛谷还是给AC就离谱),比如:
3
3
1 -1 1
0 1 -1
0 0 1
用程序出来的答案是-1,但是我们手模一遍:
分析数据:
假如按下1,对A来说会关上,对B来说会打开,对C来说会关上,即强制变成:010
假如按下2,对A来说会不变,对B来说会关上,对C来说会打开,即强制变成:?01
假如按下3,对A来说会不变,对B来说会不变,对C来说会关上,即强制变成:??0 (?代表不管)
一开始状态为:1 1 1
按下第1个开关变成: 0 1 0
按下第2个开关变成: 0 0 1
按下第3个开关变成: 0 0 0
再来用我们写的程序手模一遍……

这是调试程序,写的什么不重要。

#include<bits/stdc++.h>
using namespace std;
const int N = 11, M = 1000;
int n, m, f[1 << N], a[M][N];

void out(int s){
	for(int i = 0; i < 3; i ++)
		printf("%d", (s >> i) & 1);
}

int main(){
	freopen("t.in", "r", stdin);
	freopen("t.out", "w", stdout);
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= m; ++i) 
		for(int j = 1; j <= n; ++j) 
			scanf("%d", &a[i][j]);
	memset(f, 0x3f, sizeof f);
	f[0] = 0;
	//当前状态 
	for(int i = 0; i < (1 << n); ++i){
		//开关数 
		bool ou = 1;
		for(int k = 1; k <= m; ++k) {
			//得到当前状态,以便还原状态时候用 
			int s = i;
			//第j位上的灯 
			for(int j = 0; j < n; ++j)
				//对应效果和对应状态 
				if((a[k][j + 1] == 1 && !(i & (1 << j))) || (a[k][j + 1] == -1 && i & (1 << j)))
					s ^= 1 << j;		
			f[s] = min(f[s], f[i] + 1);
			if(s != i){
				out(s);
				printf(" %d\n", f[s]);		
				ou = 0;
			}
		}
		if(ou){
			out(i);
			cout << endl;
		}	
	}
	if(f[(1 << n) - 1] == 0x3f3f3f3f)
		puts("-1");
	else
		printf("%d\n",f[(1 << n) - 1]);
	return 0;
}

这是结果

101 1
010 1
001 1
101 1
110 1061109567
101 1
101 1
010 1
011 2
101 1
110 1061109567
111 1061109567
101 1
010 1
001 1
101 1
110 2
101 1
101 1
010 1
011 2
101 1
110 2
111 1061109567
-1
可以看到,111这个状态最后是没有被任何状态更新的
也就是说,我们这个状态转移是错误的。(据说是有后效性没有处理,但我个人理解为这个转移本身就不符合题意)

以下是最终的写法(不是状压DP)

#include<bits/stdc++.h>
using namespace std;
int f[2][1 << 11];
int a[100][10];
int n, m;
//当前状态,开关序号 
int work(int now, int k){
	for(int i = 0; i < n; i ++){
		//如果是操作1并且当前位置是1 
		if(a[k][i] == 1 && (now >> i) & 1) now ^= (1 << i);
		//如果是操作-1并且当前位置是0 
		if(a[k][i] == -1 && !((now >> i) & 1)) now ^= (1 << i);
	}
	return now;
}

int main(){
	cin >> n >> m;
	for(int i = 1; i <= m; i ++)
		for(int j = 0; j < n; j ++)
			cin >> a[i][j];
	
	int inf = (1 << n) - 1;
	f[0][inf] = 1;
	int now_h = 0;
	//从第一行开始不断地往下走 (滚动数组)
	for(int i = 0; i < m * m; i ++){
		int nxt_h = 1 - now_h;
		//一定要记得清空下一行!!!
		memset(f[nxt_h], 0, sizeof f[nxt_h]);
		//然后是枚举当前行的状态j可以是什么 
		for(int j = inf; j > 0; j --){
			if(!f[now_h][j]) continue;
// 			//然后将所有可以走到的下一个状态变成1 
// 			//这里枚举开关 
			for(int k = 1; k <= m; k ++)
				f[nxt_h][work(j, k)] = 1; 
		}
		//如果在当前这一步就已经完成了最终状态,那就结束吧; 
		if(f[nxt_h][0]){
			printf("%d", i + 1);
			return 0;
		}
		now_h = nxt_h;
	}
	cout << "-1" << endl;
	return 0;
}

——2020年11月18日(周三)——————————————————

一、吃奶酪

题目

和哈密顿最短路径很像,转移方程也是一模一样。

注释版代码

#include<bits/stdc++.h>
using namespace std;
const int N = 16, M = 1 << 16;
double f[M][N];
double x[N], y[N];

double dist(int i, int j){
    return sqrt( (x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]) );
}

int main(){
	memset(f, 127, sizeof f);
	int n; cin >> n;
	for(int i = 0; i < n; i ++) cin >> x[i] >> y[i];
	
	//记得初始化每一个点到原点的距离
	for(int i = 0; i < n; i ++) f[1 << i][i] = sqrt(x[i] * x[i] + y[i] * y[i]);
	
	//状态 
	for(int i = 1; i < (1 << n); i ++)
		//找到当前状态下所有为1的位置作为新加入的点
		for(int j = 0; j < n; j ++) if((i >> j) & 1){
				//把这一位去掉1(还原状态)后,再枚举有哪些点可以做接点
				//调了半天居然是运算顺序出了问题,果然不熟悉的运算括号不能省。
				for(int k = 0; k < n; k ++) if(((i ^ (1 << j)) >> k) & 1)
						f[i][j] = min(f[i][j], f[i ^ (1 << j)][k] + dist(k, j));
			}
		
    double ans = 1e6;
    for(int i = 0; i < n; i ++) ans = min(ans, f[(1 << n) - 1][i]);
	printf("%.2lf", ans);
	return 0;
}
//这里用来调试看数组状态的
// 	for(int i = 0; i < n; i ++){
// 		for(int j = 0; j < n; j ++)
// 			cout << dist[i][j] << " ";
		
// 		cout << endl;
// 	}
// 	for(int i = 0; i < 1 << n; i ++) cout << f[i] << " ";

纯净版代码

#include<bits/stdc++.h>
using namespace std;
const int N = 16, M = 1 << 16;
double f[M][N];
double x[N], y[N];

double dist(int i, int j){
    return sqrt( (x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]) );
}

int main(){
	memset(f, 127, sizeof f);
	int n; cin >> n;
	for(int i = 0; i < n; i ++) cin >> x[i] >> y[i];
	
	for(int i = 0; i < n; i ++) f[1 << i][i] = sqrt(x[i] * x[i] + y[i] * y[i]);
	
	for(int i = 1; i < (1 << n); i ++)
		for(int j = 0; j < n; j ++) if((i >> j) & 1){
				for(int k = 0; k < n; k ++) if(((i ^ (1 << j)) >> k) & 1)
						f[i][j] = min(f[i][j], f[i ^ (1 << j)][k] + dist(k, j));
			}
		
    double ans = 1e6;
    for(int i = 0; i < n; i ++) ans = min(ans, f[(1 << n) - 1][i]);
	printf("%.2lf", ans);
	return 0;
}
	

二、炮兵阵地

题目
注释版代码

#include<bits/stdc++.h>
using namespace std;
const int N = 110, M = 10, S = 1 << M;
int n, m;
int g[N];
//行数,上一行,本行
int f[2][S][S];
vector<int> state;
int cnt[S];

//检查本行内部是否合法
bool check(int s){
	for(int i = 0; i < m; i ++)
	    //如果s >> (i + 1) & 1中不加括号,也是先算加法再进行位运算。
		if((s >> i & 1) && ((s >> i + 1 & 1) || (s >> i + 2 & 1)))
			return false;
	return true;
}

//找这个状态里面有多少个1
int count(int s){
	int res = 0;
	while(s){
		res += s & 1;
		s >>= 1;
	}
	return res;
}

int main(){
	cin >> n >> m;
	for(int i = 0; i < n; i ++)
		for(int j = 0; j < m; j ++){
			char c; cin >> c;
			//1是山地,0是平原,表示可以塞一个炮兵。
			if(c == 'H') g[i] += 1 << j;
		}
	
	//为state寻找所有本行内部合法的状态。
	for(int i = 0; i < 1 << m; i ++)
		if(check(i)){
			state.push_back(i);
			//顺便找找当前合法状态里面有多少个1
			cnt[i] = count(i);
		}
	
	//行数,最后可以表示摆到到n + 1行并且第n + 1行和第n行都一个也没有放的最多数量;
	//其实就是恰好摆到第n - 1行的最大数量。
	for(int i = 0; i < n + 2; i ++)
		for(int u = 0; u < state.size(); u ++)
		    for(int j = 0; j < state.size(); j ++)
		    	for(int k = 0; k < state.size(); k ++){
					int a = state[u], b = state[j], c = state[k];
					//首先是炮兵之间没有交集,以免他们炸到自己的炮友(同一个炮兵部队的战友)。
					if((a & b) || (a & c) || (b & c)) continue;
					//并且本行炮兵没有被安排在山上,扛着大炮上山什么的炮兵可不干。
					if(g[i] & c) continue;
					//然后就是滚动数组
					//与上一行的对应合法状态的各种合法状态相比
					f[i & 1][j][k] = max(f[i & 1][j][k], f[i - 1 & 1][u][j] + cnt[c]);
				}
				/*
				这三行枚举可以随便换位置,本人亲测,没有影响。
				这里我就按照下面的顺序来写了。
				u:第i - 2行
				j:第i - 1行
				k:本行
				*/
	cout << f[n + 1 & 1][0][0] << endl;
	return 0;
}

纯净版代码

#include<bits/stdc++.h>
using namespace std;
const int N = 110, M = 10, S = 1 << M;
int n, m;
int g[N];
int f[2][S][S];
vector<int> state;
int cnt[S];

bool check(int s){
	for(int i = 0; i < m; i ++)
		if((s >> i & 1) && ((s >> i + 1 & 1) || (s >> i + 2 & 1)))
			return false;
	return true;
}

int count(int s){
	int res = 0;
	while(s)
		res += s & 1,s >>= 1;
	return res;
}

int main(){
	cin >> n >> m;
	for(int i = 0; i < n; i ++)
		for(int j = 0; j < m; j ++){
			char c; cin >> c;
			if(c == 'H') g[i] += 1 << j;
		}
	
	for(int i = 0; i < 1 << m; i ++)
		if(check(i)){
			state.push_back(i);
			cnt[i] = count(i);
		}
	
	for(int i = 0; i < n + 2; i ++)
		for(int u = 0; u < state.size(); u ++)
		    for(int j = 0; j < state.size(); j ++)
		    	for(int k = 0; k < state.size(); k ++){
					int a = state[u], b = state[j], c = state[k];
					if((a & b) || (a & c) || (b & c)) continue;
					if(g[i] & c) continue;
					f[i & 1][j][k] = max(f[i & 1][j][k], f[i - 1 & 1][u][j] + cnt[c]);
				}
	cout << f[n + 1 & 1][0][0] << endl;
	return 0;
}

——2020年11月19日(周四)——————————————————

一、宝藏

题目

学习自这篇博客ACwing.

注释版代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << 12, INF = 0x3f3f3f3f;
int n, m;
//距离数组
int d[N][N];
//第j行是状态i的情况下的最大价值
int f[M][N], g[M];

int main()
{
    scanf("%d%d", &n, &m);
    //一开始全是无穷大,表示没有通路
    memset(d, 0x3f, sizeof d);
    //然后就是自己到自己的通路长度为0
    for (int i = 0; i < n; i ++ ) d[i][i] = 0;
    //对输入的距离进行处理。
    while (m -- ){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        //为了方便起见我们的数从0开始算起。
        a --, b --;
        //取最小值。
        d[a][b] = d[b][a] = min(d[a][b], c);
    }
    
    //然后枚举每一种状态,
    for (int i = 1; i < 1 << n; i ++ )
        //找到这个状态的每一位数
        for (int j = 0; j < n; j ++ )
            //如果第j位是1
            if (i >> j & 1)
                //再找另外一个点
                for (int k = 0; k < n; k ++ )
                    //如果两点之间可以开辟一条通路
                    if (d[j][k] != INF)
                        //那么就找到所有可以拓展到的点。
                        g[i] |= 1 << k;
            

    memset(f, 0x3f, sizeof f);
    //将所有免费的通道先初始化
    //表示的是只打通到第1层(只有一个根节点)的时候的最大价值
    for (int i = 0; i < n; i ++ ) f[1 << i][0] = 0;
    
    //枚举当前更新到的状态
    for (int i = 1; i < 1 << n; i ++ )
        /*
        i的所有非全集子集S作为前j - 1层的点,剩余点作为第j层的点。
        枚举子集
        i: 10101
        j: 10100 10011 10010 10001 10000 01111
        显然,j & i后得到的一定是i的子集,然后我们还要加上一个条件判断是否可以转移到i;
        当然
        */
        for (int j = (i - 1) & i; j; j = (j - 1) & i)
            //栗子:i:1011 j:1001 g[j] = 1111,也就是说,j可以拓展到1011。
            //i ^ j: 1011 ^ 1001 = 0010
            if ((g[j] & i) == i){
                //由于j是i的真子集,所以异或之后得到的1一定是i为1,j为0,也就是j转移得到i,相同的不必转移。
                int remain = i ^ j; int cost = 0;
                //找到remain中所有为1的点,也就是j中所有不同于i的点,这是我们要进行转移的位置
                for (int k = 0; k < n; k ++ ) if (remain >> k & 1){
                        int t = INF;
                        //t表示将k这个点接进来的最小花费
                        //找到j(上一个状态)中所有为1的点作为接点
                        for (int u = 0; u < n; u ++ ) if (j >> u & 1)
                            //找到k与所有接点之间最小距离(接进来的最小花费)
                            t = min(t, d[k][u]);
                        // cost代表将所有k点加上的总花费,也就是将上一状态补足节点到当前状态的总花费。
                        cost += t;
                }
                //k是高度
                for (int k = 1; k < n; k ++ ) 
                    //因为是经过的宝藏屋的数量,所以总花费乘以层数就可以了。
                    f[i][k] = min(f[i][k], f[j][k - 1] + cost * k);
            }
            
    //结果就在所有层上状态是1 << n) - 1的结果之间
    int res = INF;
    for (int i = 0; i < n; i ++ ) res = min(res, f[(1 << n) - 1][i]);

    printf("%d\n", res);
    return 0;
}
/*状态f[i][j]表示:
集合:所有包含i中所有点,且树的高度等于j的生成树
属性:最小花费
状态计算:枚举i的所有非全集子集S作为前j - 1层的点,剩余点作为第j层的点。
那么我们就要知道S中第j - 1层有多少个点
核心: 求出第j层的所有点到S的最短边,将这些边权和乘以j,直接加到f[S][j - 1]上,即可求出f[i][j]。
*/

纯净版代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << 12, INF = 0x3f3f3f3f;
int n, m;
int d[N][N];
int f[M][N], g[M];

int main()
{
    scanf("%d%d", &n, &m);
    memset(d, 0x3f, sizeof d);
    for (int i = 0; i < n; i ++ ) d[i][i] = 0;
    while (m -- ){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        a --, b --;
        d[a][b] = d[b][a] = min(d[a][b], c);
    }
    
    for (int i = 1; i < 1 << n; i ++ )
        for (int j = 0; j < n; j ++ )
            if (i >> j & 1)
                for (int k = 0; k < n; k ++ )
                    if (d[j][k] != INF)
                        g[i] |= 1 << k;
            

    memset(f, 0x3f, sizeof f);
    for (int i = 0; i < n; i ++ ) f[1 << i][0] = 0;
    
    //枚举当前更新到的状态
    for (int i = 1; i < 1 << n; i ++ )
        for (int j = (i - 1) & i; j; j = (j - 1) & i)
            if ((g[j] & i) == i){
                int remain = i ^ j; int cost = 0;
                for (int k = 0; k < n; k ++ ) if (remain >> k & 1){
                        int t = INF;
                        for (int u = 0; u < n; u ++ ) if (j >> u & 1)
                            t = min(t, d[k][u]);
                        cost += t;
                }
                for (int k = 1; k < n; k ++ ) 
                    f[i][k] = min(f[i][k], f[j][k - 1] + cost * k);
            }
            
    int res = INF;
    for (int i = 0; i < n; i ++ ) res = min(res, f[(1 << n) - 1][i]);

    printf("%d\n", res);
    return 0;
}

——2020年11月20日(周五)——————————————————

——2020年11月21日(周六)——————————————————

——(完)——————————————————

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值