第一章 动态规划 状态压缩DP

1、基本概述

状态压缩dp和状态机一样,都是一种特殊的状态表示方式。状态机用一系列小状态表示某一状态。状态压缩dp用二进制数进行表示。虽然看代码起来时间复杂度比较高,但是很多的情况都给剪枝掉了。
状态压缩的题目主要分成两种

  1. 棋盘式(基于连通性)
  2. 集合

2、棋盘式

1.蒙德里安的梦想

1. 题目

在这里插入图片描述

求把 N×M 的棋盘分割成若干个 1×2 的的长方形,有多少种方案。

例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案,如上图所示。

输入格式
输入包含多组测试用例。

每组测试用例占一行,包含两个整数 N 和 M。
当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。

输出格式
每个测试用例输出一个结果,每个结果占一行。

数据范围
1≤N,M≤11
输入样例:
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
输出样例:
1
0
1
2
3
5
144
51205 

2. 分析

日字形的条,可以横着放,也可以竖着放。如果我们确定了所有横着放的条,剩下的全都用竖着放的条去填充(横着放的时候保证留下合理空间),那么总的摆放种类就跟横着放的种类相同了。 就好像有十个空位,我们用五个1和五个2去填充,问有几种摆放方法。我们只要确定了五个1的位置,其余位置填充上2就可以了。所以五个1的摆放种类跟总的摆放种类一样。

下面考虑在这个棋盘中横着的小条的摆放数量。我们将每个竖着的列的状态用一个二进制数表示。我们只在棋盘中摆放横着的小条,对于起始的块所在的位置为1,其余块为0。所以第一列为1001,第二列为0000。
在这里插入图片描述
DP过程分析如下。
在这里插入图片描述
由于长条的长度为2,如果一个长条从第1列开始摆放,它将影响到第2列,而不会影响到第3列。如下图,第一列摆放的长条会影响第二列但不会影响第三列。
在这里插入图片描述
所以在判定某个i-1的状态是否能转移到i的时候,要对这两个状态一起进行查看。例如对于f[i,j]和f[i - 1,k]

首先j和k不能冲突,此时第一列为1001,第二列为1000,在第一行存在冲突。所以当j和k不冲突的时候,有j & k == 0。因为这样没有任何相邻的两列有放置小横条的起始块。
在这里插入图片描述
第二,因为剩下的空间要留给竖着的小条,所以每列的空白的格子必须是偶数个,所以下面的情况也是不可以的,第二列的空白格子是1个第三列也是。对于第i列的空白来说,他出现的条件是i-1列没有起始块,同时第i列也没有起始块。也就是说,我们要对j | k进行判断才行。如果j | k为0的位,才是空白位。
在这里插入图片描述
符合这两个条件的j和k,就可以让f[i-1,k]转移到f[i,j],可以转移的就把数值加在f[i,j]上。
为了方便处理,我们需要对于可转移的状态进行预处理。
还有两个事情需要注意:

  1. 因为对于第一列 ,小横条可以随意摆放。也就代表着作为入口的第0列的不能有起始的小横条(否则第一列就不能随意摆放了,我们真正的起始列就是第一列)。f[0][j]代表前0列已经摆好,且第0列开始摆放的长条状态为j的摆法数量。由于第一列的摆放是自由的,所以j!=0的f[0][j]都是不可行的,因为会影响第一列的自由。只有f[0][0]是合法的,并且在这种情况下,摆放的方案只有一种,就是什么小横条也不摆,全摆竖着的,这样f[0][0]=1。
  2. 对于结果。f[m][0]代表前m列已经摆好,且第m列开始摆放的长条状态为0的摆法数量。这就是我们的结果。

3. 代码

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 12, M = 1 << N;
typedef long long ll;
ll f[N][M];
bool st[M];
vector<vector<int>> state(M);
int main()
{
    int n,m;
    while(cin >> n >> m, n || m)
    {
        //1、记录下哪些状态不包含连续奇数个0,赋予true,包含奇数个0的赋false
        for(int i = 0; i < 1 << n; i ++)
        { 
            bool isTrue = true;
            int cnt = 0;
            int j = i;
            for(int k = 1; k <= n; k ++)
            {
                if(j & 1)
                {
                    if(cnt % 2)
                    {
                        isTrue = false;
                        break;
                    }
                    cnt = 0;
                }else
                {
                    cnt ++;    
                }
                j >>= 1;
            }
            //对最后一段没有遇到1的连续0进行校验
            if(cnt % 2) isTrue = false;
            st[i] = isTrue;
        }
        // 2、预处理哪两个状态之间可以相互转换
        for(int i = 0; i < 1 << n; i ++)
        {
            //由于是一次多组数据进行测试,所以要进行clear
            state[i].clear();
            for(int j = 0; j < 1 << n; j ++)
            {
                if(((j & i) == 0) && st[j|i])
                {
                    state[i].push_back(j);
                }
            }
        }
        //连续读入,注意清除
        memset(f,0,sizeof f); 
        //dp
        f[0][0] = 1;
        for(int i = 1; i <= m; i ++)
        {
            for(int j = 0; j < 1 << n; j ++)
            {
                for(int k : state[j])
                {
                	//可转移的状态进行累加
                    f[i][j] += f[i - 1][k];
                }
            }
        }
        cout << f[m][0] << endl;
    }
      
}

2.小国王

1.题目

在 n×n 的棋盘上放 k 个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。

输入格式
共一行,包含两个整数 n 和 k。

输出格式
共一行,表示方案总数,若不能够放置则输出0。

数据范围
1≤n≤10,
0≤k≤n2 
输入样例:
3 2
输出样例:
16

2.分析

在这里插入图片描述
与上题类似,首先还是要预处理所有合法状态,根据题目要求就是不能在相邻的两个格子中放置国王。然后我们还要预处理所有合法的状态转移。在本题中就是在相同的列有国王的状态之间不能转移,斜对角有相邻国王的状态不能转移。
在预处理完上述信息之后,再进行dp。具体看代码。

3. 代码

 #include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
const int N = 12, M = 1 << N, K = 110;
//前i行已经摆好,共用了j个王,第i行的状态为k 的情况的数量
long long f[N][K][M];
vector<int>num[M],state;

int cnt[M];
//记录当前状态是够合法
bool st[M];
//判断当前状态n是否有两个相邻的国王
bool check(int n)
{
    return !(n & n >> 1);
}
//判断当前状态n中国王的数量
int count(int n)
{
    int res = 0;
    while(n){
        res += n & 1;
        n >>= 1;
    }
    return res;
}

int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    for(int i = 0; i < 1 << n; i ++)
    {
        //记录所有状态的1的数量
        cnt[i] = count(i);
        //将所有满足第一道条件的状态存入state,并在st中标记
        if(check(i))
        {
            state.push_back(i);
            st[i] = true;
        }
    }
    
    for(int i = 0; i < state.size(); i ++)
    {
        for(int j = 0; j < state.size(); j ++)
        {
            //对于所有合法的状态,判断他们之间的转移是否合法,并将合法的转移存储到num中
            int a = state[i],b = state[j];
            if(!(a & b) && st[a | b]){
                num[a].push_back(b);
            }
        }
    }
    
    //前0行花了0个王状态为0的方案数为1
    f[0][0][0] = 1;
    //已经摆了i行
    for(int i = 1; i <= n + 1; i ++)
    {
        //摆了j个国王
        for(int j = 0; j <= k; j ++)
        {
            //第i行摆放的状态为x
            for(int t = 0; t < state.size(); t ++)
            {
                int x = state[t];
                for(auto a : num[x])
                {
                    //肯定要从数量小于j的状态转移过来
                    if(j >= cnt[a]){
                        f[i][j][a] += f[i - 1][j - cnt[a]][x];
                    }
                }
            }
        }
    }
    //输出答案,已经摆完了n+1行花了k个国王,并且第n+1行啥也没摆的数量
    printf("%lld",f[n + 1][k][0]);
}

3. 玉米田

1. 题目

农夫约翰的土地由 M×N 个小方格组成,现在他要在土地里种植玉米。
非常遗憾,部分土地是不育的,无法种植。
而且,相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。
现在给定土地的大小,请你求出共有多少种种植方法。
土地上什么都不种也算一种方法。

输入格式
第 1 行包含两个整数 M 和 N。

第 2..M+1 行:每行包含 N 个整数 0 或 1,用来描述整个土地的状况,1 表示该块土地肥沃,0 表示该块土地不育。

输出格式
输出总种植方法对 108 取模后的值。

数据范围
1≤M,N≤12
输入样例:
2 3
1 1 1
0 1 0
输出样例:
9

2. 分析

在这里插入图片描述
还是首先预处理所有合法的状态,然后预处理所有合法的状态转移,对于合法的转移,将数量进行累加。最后在f[n+1][0]中得到答案。详见代码。不同的是这里还牵扯一个某些地不能种植的要求。

3. 代码

#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N = 14, M = 1 << 12,mod = 1e8;
int f[N][M];
vector<int> state,num[M];
int g[N];
//判断该状态是否合法
bool check(int n)
{
    return !(n & (n >> 1));
}

int main()
{
    int n,m;
    cin >> m >> n;
    //用一个二进制数来表示一行地的状态,用0来表示能种,1表示不能种,这样后面判断当前状态x能不能种,比较方便,只需要判断g[i] & x的值即可。
    for(int i = 1; i <= m; i ++)
    {
        for(int j = 0; j < n; j ++)
        {
            int t;
            cin >> t;
            g[i] += (!t) * (1 << j);
        }
    }
    
    //预处理合法状态
    for(int i = 0; i < 1 << n; i ++)
    {
        if(check(i)) state.push_back(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))
            {
                num[a].push_back(b);
            }
        }
    }
    //初始化入口
    f[0][0] = 1;
    //已经摆放了i行
    for(int i = 1; i <= m + 1; i ++)
    {
        for(int j = 0; j < state.size(); j ++)
        {	//第i行种植状态为x
            int x = state[j];
            //判断当前行能否种植x状态
            if(!(g[i] & x))
            {
            	//如果可以,遍历所有合法状态转移
               for(int a : num[x])
               {
               		//累加所有值
                    f[i][x] = (f[i][x] + f[i - 1][a]) % mod;
                } 
            }
        }
    }
    cout << f[m + 1][0];
}

4. 炮兵阵地

1. 题目

司令部的将军们打算在 N×M 的网格地图上部署他们的炮兵部队。
一个 N×M 的地图由 N 行 M 列组成,地图的每一格可能是山地(用 H 表示),也可能是平原(用 P 表示),如下图。
在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:

在这里插入图片描述

如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。
图上其它白色网格均攻击不到。
从图上可见炮兵的攻击范围不受地形的影响。
现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。

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

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

数据范围
N≤100,M≤10
输入样例:
5 4
PHPP
PPHH
PPPP
PHPP
PHHP
输出样例:
6

2. 分析

在这里插入图片描述总体思路与前几个题目类似,首先根据题目要求预处理出所有符合条件的状态,而后,预处理出所有合法的状态转移。与前几题不同的是,炮兵的攻击距离为2,并且图中有不能部署的地方。这样的话,每一行的状态会影响到后两行的,所以不仅记录当前状态j,也记录前一行的状态k。具体见代码。由于开辟的空间过大,所以需要使用滚动数组进行数据存储。

3. 代码

#include<cstring>
#include<iostream>
#include<algorithm>
#include<unordered_map>
using namespace std;
const int N = 110, M = 13, K = 1 << 10;
long long f[2][K][K];
vector<int> state;
vector<int> num[K];
//g用来记录地形,cnt记录当前状态的炮兵数量
int g[N],cnt[K];

// 用于判断是否是合法状态
bool check(int n)
{
    return !(n & n >> 1) && !(n & n >> 2);
}
// 计算当前状态的炮兵数量
int count(int n)
{
    int res = 0;
    while(n)
    {
        if(n & 1) res ++;
        n >>= 1;
    }
    return res;
}

int main()
{
    int n,m;
    cin >> n >> m;
    // 1. 读取地图,用一个二进制数表示一个行的状态,并将不能放置炮兵的地方('H')设为1
    for(int i = 1; i <= n; i ++)
    {
        string tmp;
        cin >> tmp;
        for(int j = 0; j < m; j ++)
        {
            g[i] += (tmp[j] == 'H') << j;
        }
    }
    //2. 预处理合法状态
    for(int i = 0; i < 1 << m; i ++)
    {
        if(check(i)){
            state.push_back(i);
            cnt[i] = count(i);
        } 
    }
    //3. 预处理合法转移状态,我们不直接处理三层之间的合法关系,
    //我们这里还是处理相邻两层的关系,到时候用的时候只需要在i-2和i -1以及
    //i-1和i之间进行处理即可
    for(int i = 0; i < state.size(); i ++)
    {
        for(int j = 0; j < state.size(); j ++)
        {
            int a = state[i];
            int b = state[j];
            if(!(a & b))
            {
                num[a].push_back(b);
            }
        }
    }
    
    //4 . dp
    //摆完第i行
    for(int i = 1; i <= n + 2; i ++)
    {
        //第i行摆放的状态为x
        for(int cur : state)
        {
            //判断这个状态是不是可以在i这行的地形上进行部署,如果不可以continue
            if(cur & g[i]) continue;
            //看看有哪些的状态可以转移到cur
            for(int pre_one : num[cur])
            {
                //看看有哪些的状态可以转移到pre_one
                for(int pre_two: num[pre_one])
                {
                    //现在pre_two 和 pre_one 之间可以转换,pre_one和cur之间可以转换
                    //还不知道 cur 和 pre_two之间会不会有影响,所以还需要校验一下
                    if(cur & pre_two) continue;
                    //i & 1 使用滚动数组
                    f[i & 1][cur][pre_one] = max(f[i & 1][cur][pre_one],f[(i - 1) & 1][pre_one][pre_two] + cnt[cur]);
                }
            }
        }
    }
    //5. 输出结果,输出一个已经摆完第n + 2 行,并且n + 2 行摆放的状态为0,n + 1行状态为0的所有摆法的最大值,这正是我们刚摆完第n行的情形.也是我们想要的结果

    cout << f[(n + 2) & 1][0][0];
}

3、集合类

1. 最短Hamilton路径

1. 题目

给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。
Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。

输入格式
第一行输入整数 n。

接下来 n 行每行 n 个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为 a[i,j])。
对于任意的 x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]≥a[x,z]。

输出格式
输出一个整数,表示最短 Hamilton 路径的长度。

数据范围
1≤n≤20
0≤a[i,j]≤10^7
输入样例:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0
输出样例:
18

2. 分析

我们所需要的不是整个方案,而只是方案最优解,所以我们无需进行暴力枚举。
在这里插入图片描述
f[i,j]的定义根据题目所求求起点 0 到终点 n−1 的最短 Hamilton 路径来看还是比较自然的。
i是一个二进制数,其第k位为1代表走过第k个节点。从0走到j可能有很多路径,我们的集合划分按照如何到达第j个节点进行划分。
举例来说,例如从第3个选择来。
其路径为:0---------3-j
后面的3-i的距离由题目给出记为w[3][j],只需要0-------------3的距离最短。所以我们要的是从0到3的所有路径中,不经过j的走法的最小值。这正是f[3][i - {j}]。i - {j}代表j中将j这个节点删掉。
所以 f [ i , j ] = m i n ( f [ j − 0 ] [ 0 ] + w [ 0 ] [ i ] , . . , ) f[i,j] = min(f[j - {0}][0] + w[0][i],..,) f[i,j]=min(f[j0][0]+w[0][i],..,)

3. 代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 20, M = 1 << N;
int a[N][N];
int f[M][N];
int main()
{
    int n;
    cin >> n;
    for(int i = 0; i < n; i ++)
    {
        for(int j = 0; j < n; j ++)
        {
            cin >> a[i][j];
        }
    }
    //给所有其他情况赋负权值
    memset(f,0x3f,sizeof f);
    //入口从0走到0,只经过0这个点,i的状态为000000...1也就是1
    f[1][0] = 0;
    
    for(int i = 0; i < 1 << n; i ++)
    {
        for(int j = 0; j < n; j ++)
        {
            if((i >> j & 1))
            {
               for(int k = 0; k < n; k ++)
               {
                    //若为true这证明j到过i和k节点
                    if((i >> k & 1))
                    {
                        //j - (1 << i) 代表去掉第i个点,也就是到过k没到过i的节点,由这个状态转移到j
                        f[i][j] = min(f[i][j],f[i - (1 << j)][k] + a[k][j]);
                    }
                }
            }
         }
    }
    cout << f[(1 << n) - 1][n - 1];
}

2. 愤怒的小鸟

1. 题目

Kiana 最近沉迷于一款神奇的游戏无法自拔。   
简单来说,这款游戏是在一个平面上进行的。 
有一架弹弓位于 (0,0) 处,每次 Kiana 可以用它向第一象限发射一只红色的小鸟, 小鸟们的飞行轨迹均为形如 y=ax2+bx 的曲线,其中 a,b 是 Kiana 指定的参数,且必须满足 a<0。
当小鸟落回地面(即 x 轴)时,它就会瞬间消失。
在游戏的某个关卡里,平面的第一象限中有 n 只绿色的小猪,其中第 i 只小猪所在的坐标为 (xi,yi)。 
如果某只小鸟的飞行轨迹经过了 (xi,yi),那么第 i 只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行; 
如果一只小鸟的飞行轨迹没有经过 (xi,yi),那么这只小鸟飞行的全过程就不会对第 i 只小猪产生任何影响。 
例如,若两只小猪分别位于 (1,3) 和 (3,3),Kiana 可以选择发射一只飞行轨迹为 y=−x2+4x 的小鸟,这样两只小猪就会被这只小鸟一起消灭。 
而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。 
这款神奇游戏的每个关卡对 Kiana 来说都很难,所以 Kiana 还输入了一些神秘的指令,使得自己能更轻松地完成这个这个游戏。   
这些指令将在输入格式中详述。 
假设这款游戏一共有 T 个关卡,现在 Kiana 想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。  
由于她不会算,所以希望由你告诉她。
注意:本题除 NOIP 原数据外,还包含加强数据。

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

如果 m=0,表示 Kiana 输入了一个没有任何作用的指令。
如果 m=1,则这个关卡将会满足:至多用 ⌈n/3+1⌉ 只小鸟即可消灭所有小猪。
如果 m=2,则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少 ⌊n/3⌋ 只小猪。
保证 1≤n≤18,0≤m≤2,0<xi,yi<10,输入中的实数均保留到小数点后两位。
上文中,符号 ⌈c⌉ 和 ⌊c⌋ 分别表示对 c 向上取整和向下取整,例如 :⌈2.1⌉=⌈2.9⌉=⌈3.0⌉=⌊3.0⌋=⌊3.1⌋=⌊3.9⌋=3。

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

输入样例:
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

注意本题的参数 m 是要求大数据搜索优化的,也就是用 Dancing Links 做的 重复覆盖问题
但这不在本篇题解的讨论范围之内 ----- 来自彩虹铅笔的题解

因为题目要求的数据是小于18的所以可以使用状态压缩,否则要使用DLX

2. 分析

本题是一个重复覆盖问题。题目的含义本质上就是第一象限有很多点,问用多少条从原点出发的抛物线可以将他们全部覆盖。
考虑所有经过原点的开口向下的抛物线。对于一般的抛物线方程 y = a x 2 + b y + c y=ax^2 + b y + c y=ax2+by+c,a应当小于0,并且c等于0.
将每个小猪看成一个点(x,y)
首先我们要预处理出来经过那些点的所有抛物线所覆盖的所有点。我们关心的抛物线有两种,
第一种是只经过(0,0)和该点本身的那类抛物线,对于这类抛物线我们不需要知道具体的方程本身,因为他只是用来经过点(x,y)的不会覆盖其他点。其使用的场景就是例如有两个点在同一列上,此时没有抛物线可以同时经过这两个点,所以需要用两条抛物线对其进行覆盖。
第二种是经过(0,0)以及(x1,y1)、(x2,y2)的抛物线,这条抛物线可以具体求出来,我们同时需要求出来这条抛物线还可以经过哪些点,从而最大限度的使用一条抛物线。经过推导抛物线的a和b可以如下进行表示
来源
在这里插入图片描述
具体我们见代码

3. 代码

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
#define s second
#define f first
const int M = 1 << 18, N = 20;
const double eps = 1e-8;
int f[M];
//path[i][j] 存储着经过i和j的抛物线所经过的所有节点,其值是一个二进制数,用来表示某个节点是否被经过
int path[N][N];
typedef pair<double,double> PDD;
vector<PDD> pigs;

//浮点数比较
int cmp(double a, double b)  
{
    if (fabs(a - b) < eps) return 0;
    if (a > b) return 1;
    return -1;
}

int main()
{
    int T;
    cin >> T;
    while(T --)
    {
        //注意有多个数据的时候要清空所有该初始化的东西。。。
        memset(path,0,sizeof path);
        memset(f,0x3f,sizeof f);
        pigs.clear();
        
        int n,m;
        cin >> n >> m;
        //1. 读入所有点
        for(int i = 0; i < n; i ++)
        {
            double x,y;
            cin >> x >> y;
            pigs.push_back({x,y});
        }
        //2. 初始化所有抛物线所经过的节点
        for(int i = 0; i < n; i ++)
        {
            //2.1 先处理所有的单节点抛物线
            path[i][i] += 1 << i;
            //2.2 初始化所有经过双节点的抛物线
            for(int j = 0; j < n; j ++)
            {
                // if(i == j) continue;
                //不能直接i == j就continue了,i!=j可能也有x1 == x2的情况
                //这里要排除掉x1 == x2 的情况,因为这种情况的两个点无法组成抛物线
                double x1 = pigs[i].f, y1 = pigs[i].s;
                double x2 = pigs[j].f, y2 = pigs[j].s;
                
                if (!cmp(x1, x2)) continue;

                //计算出抛物线方程
                double a = (y1 / x1 - y2 / x2) / (x1 - x2);
                double b = (y1 / x1) - a * x1;
                
                //只取a < 0的情况
                if(cmp(a,0) >= 0) continue;
                
                //看看这条抛物线都经过了哪些节点
                for(int k = 0; k < n; k ++)
                {
                    double x = pigs[k].f;
                    double y = pigs[k].s;
                    
                    if(!cmp(y,a * x * x + b * x)) 
                    {
                        path[i][j] += 1 << k;
                    }
                }
            }
        }
        
        //3 . 进行dp
        f[0] = 0;
        //对于当前i状态的覆盖方案
        //由于 1 << n - 1 使我们的目标状态,1 << n - 1就不需要处理了
        for(int i = 0; i < 1 << n - 1; i ++)
        {
            //我们找到他还没有覆盖的节点
            for(int j = 0; j < n; j ++)
            {
                if(!(i >> j & 1)) 
                {
                    //用不同的抛物线去更新成不同状态next
                    for(int k = 0; k < n; k ++)
                    {
                        //下一个状态next,也就是加上这个path[k][j]之后的状态
                        int next = i | path[k][j];
                        f[next] = min(f[next],f[i] + 1);
                    }
                }
            }
            
        }
        cout << f[(1 << n) - 1] << endl;
    }
}

参考资料

Acwing用户秦淮岸灯火阑珊题解
Acwing用户彩色铅笔题解
Acwing

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值