文章目录
——2020年12月27日(周日)——————————————————
#树形DP
一、毛毛虫
简单的树形DP题目。
注释版代码
#include<bits/stdc++.h>
using namespace std;
const int N = 300010, M = 2 * N;
int m, n, ans = -1;
//以i为根节点的最大节点数
int f[N], sum[N];
int h[N], to[M], nxt[M], idx;
void add(int a, int b){
to[++idx] = b, nxt[idx] = h[a], h[a] = idx;
}
//now代表当前主干上的点数
void dfs(int now, int dad){
//sum[x]表示所有与x有直接关联的点,包括x的数量
sum[now] = 1;
for(int i = h[now], son; son = to[i], i ; i = nxt[i]){
if(son == dad) continue;
dfs(son, now);
sum[now] ++;
}
// 以now为头的毛毛虫 的最大点数
//一开始就一个头,接下来要搜索一副合适的身子
//根据f的定义,这里是不包括now的父节点的。
f[now] = sum[now];
//找到所有的子节点son;从son开始往下都是毛毛虫的身子。
//注意,son的写法,要放在第一个';'后面 才能每一轮都进行更新。
for(int i = h[now], son; son = to[i], i ; i = nxt[i]){
if(son == dad) continue;
//如果为根节点的话,无父节点,f[now]数量最后不用+1,因为f[now]里面是不包括父节点的。
//这里是不能省略的
//这样写是因为当前节点也可作为身体的连接点连接两边,但最多只有两条
if(now == 1) ans = max(ans, f[now] + f[son] - 1);
else ans = max(ans, f[now] + f[son]);
//当前毛毛虫的最大点数就是头的点数加上其中一个身体的点数减去重复的部分。
f[now] = max(f[now], f[son] + sum[now] - 1);
}
//这里是不能省略的
if(now == 1) ans = max(ans, f[now]);
else ans = max(ans, f[now] + 1);
}
int main(){
cin >> n >> m;
for(int i = 1, a, b; i <= m; i ++){
cin >> a >> b;
add(a, b); add(b, a);
}
//随便从哪一个点开始搜都一样
//默认1为根节点,因为输入的第一个点编号是1。所以将1作为根节点,1为父节点去扩展,这样不会跑偏。
dfs(1, 1);
cout << ans;
return 0;
}
纯代码
#include<bits/stdc++.h>
using namespace std;
const int N = 300010, M = 2 * N;
int m, n, ans = -1;
int f[N], sum[N];
int h[N], to[M], nxt[M], idx;
void add(int a, int b)
{
to[++idx] = b, nxt[idx] = h[a], h[a] = idx;
}
void dfs(int now, int dad)
{
sum[now] = 1;
for(int i = h[now], son; son = to[i], i ; i = nxt[i])
{
if(son == dad) continue;
dfs(son, now);
sum[now] ++;
}
f[now] = sum[now];
for(int i = h[now], son; son = to[i], i ; i = nxt[i]){
if(son == dad) continue;
if(now == 1) ans = max(ans, f[now] + f[son] - 1);
else ans = max(ans, f[now] + f[son]);
f[now] = max(f[now], f[son] + sum[now] - 1);
}
if(now == 1) ans = max(ans, f[now]);
else ans = max(ans, f[now] + 1);
}
int main(){
cin >> n >> m;
for(int i = 1, a, b; i <= m; i ++)
{
cin >> a >> b;
add(a, b); add(b, a);
}
dfs(1, 1);
cout << ans;
return 0;
}
——2020年12月28日(周一)——————————————————
——2020年12月29日(周二)——————————————————
——2020年12月30日(周三)——————————————————
#轮廓线DP(低配插头DP)
一、蒙德里安的梦想(第二遍)
仍然是这道题,用的算法高级了几十倍(时空间)
大致思路就是将某条轮廓上面的格子按照有没有被填充划分01状态
自上而下,从左到右的顺序,按照格子的状态转移(但是题目里面为了好写将左右倒过来)进行DP
题目
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
long long dp[2][1 << 11];
int n , m , INF;
int main()
{
while(scanf("%d%d" , &n , &m) != EOF)
{
if(n + m == 0) break;
memset(dp , 0 , sizeof dp);
dp[0][(1 << m) - 1] = 1;
INF = 1 << m;
int cur = 1;
for(int i = 0 ; i < n ; i++)
{
for(int j = 0 ; j < m ; j++)
{
//每一格子就是一轮,这点很重要。
cur = 1 - cur;
for(int mrk = 0 ; mrk < INF ; mrk++)
if(dp[cur][mrk])
{
//这里为往下面扩展 (,可以理解为从当前位置开始向上竖着塞
if(!(mrk >> j & 1))
dp[1 - cur][mrk | (1 << j)] += dp[cur][mrk];
else
{
//上面为1
//左边可填不填
//这里的左边应该是右边,毕竟这样的话二进制更好写。
//如果左边(右边,是从右边开始算的左边……拗口)也是0,那么就可以横着放一个
//压缩成的串是从右往左变,图是从左往右变……脑补一下
if(j > 0 && !(mrk >> (j - 1) & 1))
dp[1 - cur][mrk | (1 << (j - 1))] += dp[cur][mrk];
//最后一个情况就是什么都不放,留着下一行再处理。
dp[1 - cur][mrk ^ (1 << j)] += dp[cur][mrk];
//由于所有的转移都是从上一行转移而来的,那么只要不漏什么情况且合法,转移顺序并不影响最后结果。
}
}
memset(dp[cur] , 0 , sizeof dp[cur]);
}
}
printf("%lld\n" , dp[1 - cur][INF - 1]);
}
return 0;
}
——2020年12月31日(周四)——————————————————
一、蒙德里安的梦想(第三遍)
再来一遍……
WA了两遍,第一遍是因为位运算和加法的运算顺序,第二遍时因为没开long long……蟹。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll dp[2][1 << 11];
int n, m, INF;
int main(){
while(scanf("%d%d", &n, &m) != EOF){
if(m + n == 0) break;
INF = 1 << m;
memset(dp, 0, sizeof dp);
dp[0][INF - 1] = 1;
int cur = 1;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cur = 1 - cur;
for(int k = 0; k < INF; k ++){
if(dp[cur][k]){
if(!((k >> j) & 1)){
dp[1 - cur][k | (1 << j)] += dp[cur][k];
}
else{
if(j > 0 && !((k >> (j - 1)) & 1))
dp[1 - cur][k | (1 << (j - 1))] += dp[cur][k];
dp[1 - cur][k ^ (1 << j)] += dp[cur][k];
}
}
}
memset(dp[cur], 0, sizeof dp[cur]);
}
}
printf("%lld\n", dp[1 - cur][INF - 1]);
}
return 0;
}
二、Campus Design
Nanjing University of Science and Technology is celebrating its 60th anniversary. In order to make room for student activities, to make the university a more pleasant place for learning, and to beautify the campus, the college administrator decided to start construction on an open space.
The designers measured the open space and come to a conclusion that the open space is a rectangle with a length of n meters and a width of m meters. Then they split the open space into n x m squares. To make it more beautiful, the designer decides to cover the open space with 1 x 1 bricks and 1 x 2 bricks, according to the following rules:
1 All the bricks can be placed horizontally or vertically
2 The vertexes of the bricks should be placed on integer lattice points
3 The number of 1 x 1 bricks shouldn’t be less than C or more than D. The number of 1 x 2 bricks is unlimited.
4 Some squares have a flowerbed on it, so it should not be covered by any brick. (We use 0 to represent a square with flowerbet and 1 to represent other squares)
Now the designers want to know how many ways are there to cover the open space, meeting the above requirements.Input
There are several test cases, please process till EOF.
Each test case starts with a line containing four integers N(1 <= N <= 100), M(1 <= M <= 10), C, D(1 <= C <= D <= 20). Then following N lines, each being a string with the length of M. The string consists of ‘0’ and ‘1’ only, where ‘0’ means the square should not be covered by any brick, and ‘1’ otherwise.Output
Please print one line per test case. Each line should contain an integers representing the answer to the problem (mod 109 + 7).Sample Input
1 1 0 0
1
1 1 1 2
0
1 1 1 2
1
1 2 1 2
11
1 2 0 2
01
1 2 0 2
11
2 2 0 0
10
10
2 2 0 0
01
10
2 2 0 0
11
11
4 5 3 5
11111
11011
10101
11111Sample Output
0
0
1
1
1
2
1
0
2
954
第二题和上面的差不多,就是多了个1x1的砖块并且限制了1x1砖块的数量,所以我们在进行状态转移的时候要开多一维来表示当前状态用了多少个1x1的砖块,并在此基础上进行转移。.
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int md = (int)1e9 + 7;
int n, m, c, d;
int dp[2][23][1 << 10];
char mp[110][20];
//转移状态时的方案计算
//x(新的状态)会直接在原数组进行操作 y(不会改变)
void add(int &x, int y){
x += y;
if(x >= md) x -= md;
}
int solve(){
memset(dp, 0, sizeof(dp));
dp[0][0][(1 << m) - 1] = 1;
int cur = 1;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cur ^= 1;
//一直到上面还是和上一题时一样的
//这里枚举的是当前1x1的砖块的已经使用的数量
for(int k = 0; k <= d; k ++){
//枚举状态
for(int mask = 0; mask < (1 << m); mask ++){
//如果可以放砖块
if(mp[i][j] == '1'){
//如果上面的空间已经放了砖块
if(mask & (1 << j))
//那就塞个1x1的砖块好了
add(dp[cur ^ 1][k + 1][mask], dp[cur][k][mask]);
//如果左边(右边)还有 空间,就横着塞一个1x2的
if(j && !(mask & (1 << (j - 1))) && (mask & (1 << j)))
add(dp[cur ^ 1][k][mask | (1 << (j - 1))], dp[cur][k][mask]);
//最后就是什么都不放,
add(dp[cur ^ 1][k][mask ^ (1 << j)], dp[cur][k][mask]);
}
//0代表这里不应该放砖块
else{
//而且刚好上面的地方时放了砖块的,那就可以转移了
//因为如果上面没放的话,强行转移那么上面的砖块就空掉放不了了
if(mask & (1 << j)){
add(dp[cur ^ 1][k][mask], dp[cur][k][mask]);
}
}
}
}
memset(dp[cur], 0, sizeof dp[cur]);
}
}
int ret = 0;
for(int i = c; i <= d; i ++){
add(ret, dp[cur ^ 1][i][(1 << m) - 1]);
}
return ret;
}
int main(){
while(scanf("%d%d%d%d", &n, &m, &c, &d) != EOF){
for(int i = 0; i < n; i ++)
scanf("%s", mp[i]);
int ret = solve();
printf("%d\n", ret);
}
return 0;
}
——2021年01月01日(周五)——————————————————
——2021年01月02日(周六)——————————————————
新的一年:终结之时,亦是新的开始……
补上一篇关于树形DP的题解:
#树形DP
一、选课
想想这些课之间的关系网像一棵树一样,从一个没有先修课的课开始,往下连着那些完成它后的课程,如此一直往下……
做起来可以发现,在枚举留给某一棵树的节点进行转移时,“节点数”这一维很像背包里的“体积”这一维
所以这个题是一道“树形背包”的题。
#include<bits/stdc++.h>
using namespace std;
const int N = 310 , M = 610;
//h的初始化就是0,那些没有先修课的课,我们默认它们的“先修课”为0,这样最后输出f[0][m + 1]就可以了;也就是说,0这个节点也算一个节点。
int idx = 1, h[N], e[M], nxt[M];
int n, m;
//w是当前这门课的学分,sz是以当前节点为根的子树的节点数。
int w[N] , sz[N];
int f[N][N];
//用邻接矩阵来记录。
void add(int a, int b){
e[idx] = b, nxt[idx] = h[a], h[a] = idx ++;
}
//当前节点,它的父节点(只用来判断遍历的方向)。
void dfs(int x , int lst)
{
sz[x] = 1;
f[x][1] = w[x];
for(int i = h[x] , v ; v = e[i] , i ; i = nxt[i])
//先一个个走下去,然后再一个个地返回。
dfs(v , x) , sz[x] += sz[v];
//遍历所有的子树
for(int i = h[x] , v ; v = e[i] , i ; i = nxt[i])
{
//不要忽略了0这个节点)这里遍历的是留给整棵树的节点
for(int j = m + 1 ; j ; j--)
//这里是留给当前子树的节点
for(int k = 0 ; k < j ; k++)
f[x][j] = max(f[x][j] , f[x][j - k] + f[v][k]);
}
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
int pre, s; cin >> pre >> s;
//先修课做叶子
add(pre , i); w[i] = s;
}
dfs(0 , 0);
cout << f[0][m + 1];
return 0;
}
二、毛毛虫(第二遍)
按我的写法看,最难处理的是
当前子树的根节点
和当前根节点的父节点
了。
在储存以当前节点为根节点的子树 的总节点数
时我先忽略根节点
所以最后算答案时要加上被忽略的点(还有它的父节点
,因为父节点
在根节点
头上,储存时不会记录。
所以:如果当前根节点
不是祖宗节点
(整棵树来看最上面的根节点)的话,最后的结果还要加上2个节点,
如果是祖宗节点
,最后的结果就要加上1个节点。
还有一件事:
根据毛毛虫的定义,是一条主干加上它的所有分叉点,注意,是点,也就是每个延伸处的高度最多是1.
而对于我们枚举到得节点,我们首先要确定的就是它的主干部分。
有两种情况:
第一种:当前节点就是头结点。
那么只有一个延伸方向,也就是我们在任意一个
可延伸点接上一个
身体部分。
而我们f[n]数组储存的刚好就是这个形状的毛毛虫的数量(再加上头结点部分
第二种:当前节点不是头结点,而是一个连接点。
也就是说,当前节点连接着的任意两个
可延伸点上各自接上了一个
身体部分。
这个形状显然也是合法的,只是我们无法确定头和尾会是什么样子而已。
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10, M = 2 * N;
int n, m, ans = -1;
//以当前节点为根,不包括根节点的子树节点数
int f[N], sum[N];
int h[N], e[M], nxt[M], idx;
void add(int a, int b){
e[++ idx] = b, nxt[idx] = h[a], h[a] = idx;
}
void dp(int now, int dad)
{
// sum[now] = 1;
for(int i = h[now]; i != -1; i = nxt[i])
{
int son = e[i];
if(son == dad) continue;
dp(son, now);
sum[now] ++;
}
f[now] = sum[now];
for(int i = h[now];i != -1; i = nxt[i])
{
int son = e[i];
if(son == dad) continue;
//这里不要忘了父节点的存在,根节点要做讨论
if(now == 1)
ans = max(ans, f[son] + f[now] + 1);
else ans = max(ans, f[son] + f[now] + 2);
f[now] = max(f[now], f[son] + sum[now]);
}
if(now == 1)
ans = max(ans, f[now] + 1);
else ans = max(ans, f[now] + 2);
}
int main(){
memset(h, -1, sizeof h);
cin >> n >> m;
int a, b, c;
for(int i = 1; i <= m; i ++)
{
scanf("%d%d", &a, &b);
add(a, b); add(b, a);
}
dp(1, 1);
cout << ans;
return 0;
}