《算法竞赛进阶指南》读书笔记汇总
这里面是我在阅读《算法竞赛进阶指南》这本书时的一些思考,有兴趣可以瞧瞧!
如若发现什么问题,可以通过评论或者私信作者提出。希望各位大佬不吝赐教!
许多问题都可以使用二叉堆来进行优化,下面直接看例题吧。
状压dp基本介绍
对于某些问题,我们需要在动态规划的“状态”中记录一个集合,保存“轮廓”的详细信息,以便进行状态转移。
通常,若所记录集合中的元素都是小于
K
K
K的自然数,且集合大小不超过
N
N
N,那么我们可以将这个集合表示成一个
N
N
N位
K
K
K进制数,用一个
[
0
,
K
N
−
1
]
[0,K^{N-1}]
[0,KN−1]范围内的十进制整数,作为状态中的某一维。这种把集合表示成整数记录dp状态的方法我们称为状态压缩动态规划。然后由于整数范围有限,这类问题的一般特征就是
N
N
N很小。
让我们通过几道题熟悉一下状压dp的基本应用。
【例题】蒙德里安的梦想(AcWing291)
题目链接
思路:这是一道状态压缩dp的经典例题。首先我们先观察一下题目的特点,我们按行来看,对于某一行
i
i
i的空格而言,只有三种情况,要么是
2
∗
1
2*1
2∗1小方格的上半部分,要么是
2
∗
1
2*1
2∗1小方格的下半部分,要么是
1
∗
2
1*2
1∗2小方格一部分。如图:
对于第一种而言,下一行必须补充完这个长方形的下半部分,对于第二种和第三种而言,下一行放哪种都可以(当然也要合法,接下来会说到)
那么有了这个特性之后,我们可以用
1
1
1表示第一种情况,用
0
0
0表示第二种和第三种情况,比如下图:
那么某一行的状态就可以表示成一个二进制数啦!
我们定义
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示第
i
i
i行的状态为
j
j
j时,前
i
i
i行分割方案的总数,其中
j
j
j是用十进制整数记录的
M
M
M位二进制数。那么初始状态为
f
[
0
]
[
0
]
f[0][0]
f[0][0],目标状态为
f
[
N
]
[
0
]
f[N][0]
f[N][0]。
下面我们考虑状态转移:
由于影响到第
i
i
i行状态的只有第
i
−
1
i-1
i−1行,那么我们考虑第
i
i
i行的状态
j
j
j是否可以由第
j
j
j行的状态
k
k
k转移过来。不难发现,当
k
k
k中某一位为
1
1
1时,
j
j
j中对应的那一位必须为
0
0
0;当
k
k
k中某一位为
0
0
0时,
j
j
j中对应的那一位可以为
1
或
0
1或0
1或0。所以第一个条件为:
(1)j & k == 0
另外,我们还需要判断k中放第三种情况的连续位置个数是否为偶数个,就是横着放是否可以放得下。那么如何得到那些横着放的位置呢。我们发现,
j
∣
k
j | k
j∣k之后得到的二进制数中,
0
0
0的位置就是横着放的位置,判断一下连续
0
0
0的个数是否为偶数即可,可以预处理所有状态是否合法(用
s
t
st
st数组)。所以第二个条件就是:
(2)st[j | k] == true
那么就可以轻松的写出代码啦!
AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 12;
LL f[N][1 << N];
bool st[1 << N];
int n, m;
int main()
{
while(cin >> n >> m, n || m)
{
for (int i = 0; i < 1 << m; i ++ )
{
bool flag = 0, cnt = 0;
for (int j = 0; j < m; j ++ )
if (i >> j & 1) flag |= cnt, cnt = 0;
else cnt ^= 1;
st[i] = flag | cnt ? 0 : 1;
}
memset(f, 0, sizeof f);
f[0][0] = 1;
for (int i = 1; i <= n; i ++ )
for (int j = 0; j < 1 << m; j ++ )
for (int k = 0; k < 1 << m; k ++ )
if ((j & k) == 0 && st[j | k])
f[i][j] += f[i - 1][k];
cout << f[n][0] << endl;
}
return 0;
}
【例题】炮兵阵地(AcWing292)
题目链接
思路:这一题和上一题差不多,但是这一题影响第
i
i
i行状态的有前两行,所以我们的状态需要加上一维。我们定义
f
[
i
]
[
j
]
[
k
]
f[i][j][k]
f[i][j][k]表示,前
i
i
i行已经安排完,第
i
i
i行的状态为
k
k
k,第
i
−
1
i - 1
i−1行的状态为
j
j
j,并且所有炮兵满足题目要求,炮兵数量的最大值。
转移的时候,我们枚举第
i
−
2
i - 2
i−2行的状态,判断状态是否合法即可。判断合法也比较简单,满足以下条件即可:
(1)对于某个行状态,某两个相邻炮兵距离不能小于等于2
(2)对于某两个相邻行状态,炮兵所处位置不能在同一列
(3)炮兵不能放在山地上
详细实现见代码。
这题还卡空间,用滚动数组优化即可。
AC代码:
#include <iostream>
using namespace std;
const int N = 110, M = 11;
int n, m;
int f[3][1 << M][1 << M];
int g[N];
char G[N][M];
int ones[1 << M];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < 1 << m; i ++ )
for (int j = 0; j < m; j ++ )
if (i & (1 << j))
ones[i] ++;
for (int i = 1; i <= n; i ++ )
{
scanf("%s", G[i] + 1);
for (int j = 1; j <= m; j ++ )
if (G[i][j] == 'H') g[i] += (1 << (j - 1));
}
for (int i = 0; i < 1 << m; i ++ )
if (!(i & g[1] || (i & (i << 1)) || (i & (i << 2))))
f[1][i][0] = ones[i];
for (int i = 0; i < 1 << m; i ++ )
for (int j = 0; j < 1 << m; j ++ )
if (!(i & j || i & g[2] || j & g[1] || (i & (i << 1)) || (i & (i << 2)) || (j & (j << 1)) || (j & j << 2)))
f[2][i][j] = ones[i] + ones[j];
for (int i = 3; i <= n; i ++ )
{
for (int j = 0; j < 1 << m; j ++ )
{
if (j & g[i] || (j & (j << 1)) || (j & (j << 2)))
continue;
for (int k = 0; k < 1 << m; k ++ )
{
if (k & j || k & g[i - 1] || (k & (k << 1)) || (k & (k << 2)))
continue;
for (int hh = 0; hh < 1 << m; hh ++ )
{
if (hh & k || hh & j || hh & g[i - 2] || (hh & (hh << 1)) || (hh & (hh << 2)))
continue;
f[i % 3][j][k] = max(f[i % 3][j][k], f[(i - 1) % 3][k][hh] + ones[j]);
}
}
}
}
int ans = 0;
for (int i = 0; i < 1 << m; i ++ )
for (int j = 0; j < 1 << m; j ++ )
ans = max(f[n % 3][i][j], ans);
printf("%d\n", ans);
return 0;
}
另类思路:这题还有另一个做法,就是用三进制的状态压缩。我们用
2
2
2表示这个格子放了炮兵,用
1
1
1表示这个格子不放炮兵,同一列上一行的格子放了炮兵,用
0
0
0表示这个格子不放炮兵,同一列上一行的格子也不放炮兵。那么根据题意,只有
0
0
0的下一行才能放
2
2
2。
我们考虑前一行的每一个状态可以转移向当前行的哪些状态,需要满足以下条件:
(1)当第
i
i
i行第
j
j
j列为
2
2
2时,第
i
+
1
i + 1
i+1行第
j
j
j列必须为1
(2)当第
i
i
i行第
j
j
j列为
1
1
1时,第
i
+
1
i + 1
i+1行第
j
j
j列必须为0
(3)山地格子不能填
2
2
2
(4)如果一个格子填了
2
2
2,他后面的两个格子就不能填
2
2
2了
我们定义
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示前
i
i
i行已经安排完,第
i
i
i行状态为
j
j
j的最大炮兵数目。其中
j
j
j是一个用十进制整数表示的三进制数。转移的时候直接dfs出下一行的所有合法状态即可。
细节看代码吧
AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 105, M = 60000;
int n, m;
char str[N][11];
int f[N][M];
bool g[N][11];
int pre[11], cur[11];
void Ten_to_Three(int x, int a[])
{
for (int i = m - 1; i >= 0; i -- )
{
a[i] = x % 3;
x /= 3;
}
}
void Three_to_Ten(int& x, int a[])
{
x = 0;
for (int i = 0; i < m; i ++ )
x = x * 3 + a[i];
}
void dfs(int row, int col, int sum)
{
if (col == m)
{
int res;
Three_to_Ten(res, cur);
f[row][res] = max(f[row][res], sum);
return;
}
dfs(row, col + 1, sum);
if (!g[row][col] && !pre[col] && (col < 1 || cur[col - 1] < 2) && (col < 2 || cur[col - 2] < 2))
{
cur[col] = 2;
dfs(row, col + 1, sum + 1);
cur[col] = 0;
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ )
{
scanf("%s", str[i]);
for (int j = 0; j < m; j ++ )
g[i][j] = (str[i][j] == 'H');
}
memset(f, -1, sizeof f);
f[0][0] = 0;
int up = 1;
for (int i = 1; i <= m; i ++ )
up *= 3;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < up; j ++ )
if (f[i][j] != -1)
{
Ten_to_Three(j, pre);
for (int k = 0; k < m; k ++ )
cur[k] = max(pre[k] - 1, 0);
dfs(i + 1, 0, f[i][j]);
}
int ans = 0;
for (int i = 0; i < up; i ++ )
ans = max(ans, f[n][i]);
printf("%d\n", ans);
return 0;
}
习题
【练习】岛和桥(AcWing1194)
题目链接
思路:由题意得,每一个点都只会经过一次,我们不难想到用二进制数表示每一个点是否被经过,然后由于题目要求的权值涉及一个三元组,那么与上一题一样,影响当前点也就是最后一个经过的点的点有两个,就是倒数第二个和倒数第三个经过的点。那么我们可以定义
f
[
s
t
a
t
e
]
[
i
]
[
j
]
f[state][i][j]
f[state][i][j]表示当前已经过点的集合为
s
t
a
t
e
state
state,最后一个经过的点为
j
j
j,倒数第二个经过的点为
i
i
i,满足条件所有方案中权值的最大值。
转移的时候可以枚举倒数第三个经过的点
k
k
k进行转移。
接下来就是合法性的判定啦。也比较简单,三个点都必须两两不同且
i
i
i和
j
j
j必须相连,
k
k
k和
i
i
i必须相连。
路径数目,就在求最大值的时候顺便用数组存一下就可以了。
AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 15;
int val[N];
int r[N][N];
int f[1 << N][N][N], g[1 << N][N][N];
int n, m;
int main()
{
int T;scanf("%d", &T);
while (T --)
{
memset(r, 0, sizeof r);
memset(f, -0x3f, sizeof f);
scanf("%d%d", &n, &m);
int sum = 0;
for (int i = 0; i < n; i ++ )
{
scanf("%d", &val[i]);
sum += val[i];
}
for (int i = 0; i < m; i ++ )
{
int u, v;
scanf("%d%d", &u, &v);
u --, v --;
r[u][v] = r[v][u] = val[u] * val[v];
}
for (int i = 0; i < n; i ++ )
f[1 << i][i][n] = 0, g[1 << i][i][n] = 1;
for (int state = 0; state < 1 << n; state ++ )
for (int i = 0; i < n; i ++ )
{
if (!(state >> i & 1)) continue;
int pre = state ^ (1 << i);
for (int j = 0; j < n; j ++ )
{
if (!(pre >> j & 1)) continue;
if (!r[i][j]) continue;
int tmp = pre ^ (1 << j) ^ (1 << n);
for (int k = 0; k <= n; k ++ )
{
if (!(tmp >> k & 1)) continue;
int t = f[pre][j][k] + r[i][j] + (r[i][k] ? r[i][k] * val[j] : 0);
if (t > f[state][i][j])
{
f[state][i][j] = t;
g[state][i][j] = g[pre][j][k];
}
else if (t == f[state][i][j])
g[state][i][j] += g[pre][j][k];
}
}
}
int ans = 0, num = 0;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n; j ++ )
if (ans < f[(1 << n) - 1][i][j])
{
ans = f[(1 << n) - 1][i][j];
num = g[(1 << n) - 1][i][j];
}
else if (ans == f[(1 << n) - 1][i][j])
{
num += g[(1 << n) - 1][i][j];
}
if (ans) printf("%d %d\n", ans + sum, num / 2);
else puts("0 0");
}
return 0;
}
芯片(AcWing328)
题目链接
暂未完成