刷题周记(十一)——#树形DP:战略游戏、皇宫看守、有线电视网、括号树

——2021年01月03日(周日)——————————————————

干嘛去了呢??……

——2021年01月04日(周一)——————————————————

文化课真是修身养性……

——2021年01月05日(周二)——————————————————

高二考高考那三科,又放假,才放了三天,回来上了一天课他们又放……
假期与我无关就是了……还是颓机房好
先来道简单的树形DP题:

一、战略游戏

题目
有点像没有上司的舞会,主要分析每个节点选与不选两种情况:
对于每个节点来说:
如果选(1):那么下面的节点可选可不选;
如果不选(0):那么下面的节点必选。
没了,就是一道两个状态搞定的题目,相关的题目可以复习状态机里面的股票买卖I~VI

#include<bits/stdc++.h>
using namespace std;
const int N = 1510, M = 2 * N;
int 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;	
}

int dp[N][2];
void dfs(int now, int dad)
{
	dp[now][1] = 1, dp[now][0] = 0;
	for(int i = h[now]; i ; i = nxt[i])
	{
		int son = e[i];
		if(son == dad) continue;
		dfs(son, now);
		
		dp[now][1] += min(dp[son][0], dp[son][1]);
		dp[now][0] += dp[son][1];
	}
}
int main(){
	cin >> n;
	for(int i = 1; i <= n; i ++)
	{
		int a; cin >> a;
		int k; cin >> k;
		for(int j = 1; j <= k ; j ++)
		{
			int b; cin >> b;
			add(a, b); add(b, a);
		}
	}
	dfs(1, 1);
	cout << min(dp[1][0], dp[1][1]);
	return 0;
}

二、皇宫看守

题目

做了两个小时,不爽。
问题就在分析被son看着时候的价值,有一个巧妙之处可以省去一大堆功夫,可惜一开始我想不到。

#include<bits/stdc++.h>
using namespace std;
const int N = 1510, M = 2 * N;
int dp[N][3];
int n;

int h[N], e[N], nxt[N], idx, w[N];
void add(int a, int b)
{
	e[++ idx] = b, nxt[idx] = h[a], h[a] = idx;	
}

void dfs(int now)
{
    //一开始写错位置了hhh
    dp[now][1] = w[now];
    for(int i = h[now]; i ; i = nxt[i])
    {
        int son = e[i];
        dfs(son);
        
        //now由dad看着,son没人看,怎么办?son可以自理或者由son的son看,
        dp[now][0] += min(dp[son][1], dp[son][2]);
        //now自律,
        dp[now][1] += min(dp[son][0], min(dp[son][1], dp[son][2]));
    }
    /*
        由son来看,自己想了两个小时,最后还是在y总的视频里学到了一个美妙的写法:
        根据之前的分析,每次都要找到所有子树(除去枚举到的那个子树)的最小值,然后加上被枚举到的子树的自理价值
        所有子树(除去枚举到的那个子树)的最小值 其实 就是dp[now][0] - min(dp[son][1], dp[son][2]),有意思吧?
    */
    dp[now][2] = 1e6;
    for(int j = h[now];j ; j = nxt[j])
    {
        int son = e[j];
        dp[now][2] = min(dp[now][2], dp[son][1] + dp[now][0] - min(dp[son][1], dp[son][2]));
    }
}

bool st[N];
int main()
{
    cin >> n;
    int a, k, m;
    for(int i = 1; i <= n; i ++)
    {
        cin >> a;
        cin >> w[a] >> m;
        for(int j = 1; j <= m; j ++)
        {
            int b; cin >> b;
            add(a, b); 
            st[b] = 1;
        }
    }
    
    int root = 1;
    while(st[root]) ++ root;
    dfs(root);
    
    cout << min(dp[root][1], dp[root][2]);
    return 0;
}

——2021年01月06日(周三)——————————————————

一、有线电视网

七点半开始,九点结束……蒟蒻。
题目

#include<bits/stdc++.h>
using namespace std;
const int N = 3010, M = 2 * N;
int dp[N][N];
int n, m, w[N];

int h[N], e[N], nxt[N], idx, v[N];
void add(int a, int b, int vv)
{
	e[++ idx] = b, v[idx] = vv, nxt[idx] = h[a], h[a] = idx;	
}

int sum[N], sum_son[N];
void dfs(int now)
{
//	sum[now] = 1;
	//如果搜到了叶子 
	if(w[now])
	{
		dp[now][1] = w[now];
		sum_son[now] = 1;
		return ;
	}
	//不是叶子就往下搜 
	//这个初始化……卡了半小时 
	dp[now][0] = 0;
	for(int i = h[now]; i; i = nxt[i])
	{
		int son = e[i], vv = v[i];
		dfs(son);
		
		sum_son[now] += sum_son[son];
		//留给树的体积 
		for(int j = sum_son[now]; j >= 0; j --)
		{
			//留给当前子树的体积 
			for(int k = min(j, sum_son[son]); k >= 0; k --)
			{
				dp[now][j] = max(dp[now][j], dp[son][k] + dp[now][j - k] - vv);
			}
		} 
	}
}

int main()
{
	memset(dp, -0x3f, sizeof dp); 
    cin >> n >> m;
    //枚举中转站i 
    for(int i = 1; i <= n - m; i ++)
    {
    	int k; cin >> k;
    	for(int j = 1; j <= k; j ++)
		{
    		int b, vv; cin >> b >> vv;
			add(i, b, vv);	
    	}
    }
    for(int i = n - m + 1; i <= n; i ++)
    	cin >> w[i];
    
    dfs(1);
    for(int i = sum_son[1]; i >= 0; i --)
    {
    	if(dp[1][i] >= 0)
    	{
        	cout << i;
        	return 0;
    	}
    }
//    for(int i = 1; i <= n; i ++){
//    	for(int j = 1; j <= 3; j ++)
//    		printf("%d ",  dp[i][j]);
//    	puts("");
//	printf("%d ",  sum_son[i]);
//    }
//    cout << "0";
    return 0;
}

——2021年01月07日(周四)——————————————————

——2021年01月08日(周五)——————————————————

——2021年01月09日(周六)——————————————————

一、括号树

题目链接(ACwing)
题目链接(洛谷luogu)

想了两天,主要难点(对我)有如下:
一、转移方程到底是怎么来的?
首先我们定义两个数组:f[n]g[n];
f[u]代表从根节点到点u所经过的路径上的合法括号数;
g[n]代表从点u起往前数,所有的合法括号数,但是不能中断。
什么意思呢?
比如:()()(())中,
g[1] = 0;g[2] = 1;g[3] = 0;g[4] = 2;g[5] = 0;g[6] = 0;g[7] = 1;g[8] = 3;
f[1] = 0;f[2] = 1;f[3] = 1;f[4] = 3;f[5] = 3;f[6] = 3;f[7] = 4;f[8] = 7;
看下图;
想一想:我们每次加入一个),是不是会让对应的(所连接的合法的所有括号都延长一个长度,使得这些合法的情况变成新的合法的情况?
在这里插入图片描述

图注:每条横线表示不同的)加入后形成的新的***合法括号串***,不同的颜色代表不同的合成情况。
其中,紫色虚线的部分是可以忽略的,因为:
(())即“括号里面套其它括号”这种情况下,将最右边的)加入后,

  1. 只有外层的()会与前面相连的所有 合法括号串 组成新的 合法括号串
  2. 括号里面的任何情况都不会对产生新的***合法括号串***有帮助。

可以看到,对于每次新加入的),它会使得对应的(前面所有相连不间断的 合法括号串 都延长一个长度,形成新的合法括号串。
那么如果要加入一个),每次***合法括号串***都会在前一个括号的情况下,加上***前面所有相连不间断的合法括号串***的数量
也就是:f[u] = f[u - 1] + g[u];
g[u] = g[对应左括号的前面紧挨着的右括号] + 1是显然的

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 500010;
//dp为从根到当前u的所有合法括号串数。g为往前推不间断的合法括号串数。
ll dp[N], g[N];
int n, m, w[N];

char str[N];
int stck[N], top;
int p[N];

int h[N], e[N], nxt[N], idx, v[N];
void add(int a, int b)
{
	e[++ idx] = b, nxt[idx] = h[a], h[a] = idx;	
}

void dfs(int now)
{
    //左括号的时候直接加入,dp的值将被继承。g不用管。
	if(str[now] == '(')
	{
		stck[++ top] = now;
		dp[now] = dp[p[now]];
		for(int i = h[now]; i ; i = nxt[i]) dfs(e[i]);
		-- top;
	}
	
	else
	{
	    //如果还有可以匹配的左括号
		if(top)
		{
		    //取出栈顶的左括号
			int t = stck[top --];
			g[now] = g[p[t]] + 1;
			dp[now] = g[now] + dp[p[now]];
			
			for(int i = h[now]; i ; i = nxt[i]) dfs(e[i]);
			
			stck[++ top] = t;
		}
		//没有可以匹配的左括号;dp的值将被继承。g不用管。
		else
		{
			dp[now] = dp[p[now]];
			for(int i = h[now]; i ; i = nxt[i]) dfs(e[i]);
		}
	}
}

int main()
{
    cin >> n;
    scanf("%s", str + 1);
    for(int son = 2; son <= n; son ++)
    {
    	cin >> p[son];
    	add(p[son], son);
    }
    
    dfs(1);
    
    ll res = 0;
    for(int i = 1; i <= n; i ++)
    {
        res ^= (i * dp[i]);
        // printf("%d : %d\n", i, dp[i]);
    }
    cout << res;
    return 0;
}

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值