9月22-26日小记

9月22日

晚17 : 30 - 21 : 30校艺术节,在机房待了一晚上,做了一些题,听了很多后摇。下面给出歌单。

  • Silent Roar

  • Hidden Path

  • New Years End

  • 水之湄

  • 彩虹山

  • Comforting Sounds

  • November

  • December

  • Farewell

  • 体育

  • 南方蝶道

  • (待补全)

1. P2016 战略游戏

题意简述:给定一棵树,可以选择一些节点,使得与这些节点直接相连的边被点亮;要使所有的边都被点亮,求最少选择的节点数。

对于每个节点,都有不选两个情况,所以可以用f[u][0/1]代表u子树内点亮所有边所需选择的最少节点数,且节点u被/不被选。

这道题很像舞会题,即:

  • 如果节点u被选,那么其子节点v可选可不选,即f[u][1] += min(f[v][0],f[v][1])

  • 如果节点u不被选,那么v必须选,即f[u][0] += f[v][1]

最终答案为min(f[root][0],f[root][1])

题面没有标明节点1一定为根,所以要遍历所有节点,找出根节点开始递归。

code:

#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=1505;
int f[N][2],n,root,has_father[N];
vector<int>t[N],lis;
void DFS(int u,int fa){
	f[u][0]=0,f[u][1]=1;
	for(auto&v:t[u]){
		if(v==fa)continue;
		DFS(v,u);
		f[u][0] += f[v][1];
		f[u][1] += min(f[v][0],f[v][1]);
	}
}
signed main(){
	cin.tie(0)->sync_with_stdio(0);
	memset(f,0x3f,sizeof(f));
	cin>>n;
	for(int i=1;i<=n;i++){
		int x,y,z;
		cin>>x>>y;
		lis.push_back(x);
		for(int j=1;j<=y;j++){
			cin>>z;
			has_father[z]=1;
			t[x].push_back(z);
		}
	}
	for(auto&u:lis){
		if(!has_father[u]){
			root=u;
			break;
		}
	}
	DFS(root,-1);
	cout<<min(f[root][0],f[root][1]);
	return 0;
}

2. P1273 有线电视网

题意简述:给定一个带边权的树,每个叶子节点有一个权值。对于每个叶子节点,它对答案的贡献等于其点权它到根的路径上的所有边权和的差值。求最大的叶子节点数,使得答案非负。

定义f[u][j]表示以u为根的子树,选取j个叶子节点,所获得的最大利润,也就是上面说的差值。

这里的利润是指:收入-成本,即叶子节点支付的钱数总和路径边权的差值。

当利润大于0时,则证明不亏本,所以可以作为答案使用。

值得注意的是,答案是第一个大于等于0f[u][j]中的j,而并不是f数组值,这启示我们答案所代表的量并不一定要作为状态值的定义。

状态转移方程是f[u][j]=max(f[u][j],f[v][k]+f[u][j-k]-g[u][v]),其中g[u][v]表示从uv的边权。

解释:对于状态f[u][j],它的转移肯定来源于它的儿子。

故考虑其儿子节点的状态f[v][k]与其余状态f[u][j-k]作加和。显然加和之后所形成的树的利润还需要减去一部分支出g[u][v]。再取最大值即可。

答案是当j倒序遍历时(因为要取最大的j),第一个大于等于0f[1][j]j值。

code:

#include <bits/stdc++.h>
using namespace std;
constexpr int N=3005;
int n,m,f[N][N],w[N][N],sum[N];
vector<int>t[N];
void DF$(int u,int fa){
    if(u>n-m){
        sum[u]=1;
        return;
    }
    for(auto&v:t[u]){
        if(v==fa)continue;
        DF$(v,u);
        sum[u]+=sum[v];
    }
}
void DFS(int u,int fa){
    for(auto&v:t[u]){
        if(v==fa)continue;
        DFS(v,u);
        for(int j=sum[u];j>=0;j--){
            for(int k=0;k<=min(j,sum[v]);k++){
                f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]-w[u][v]);
            }
        }
    }
}
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    memset(f,0xc0,sizeof(f));
    cin>>n>>m;
    for(int i=1;i<=n-m;i++){
        int k,a,c;
        cin>>k;
        for(int j=1;j<=k;j++){
            cin>>a>>c;
            t[i].push_back(a);
            t[a].push_back(i);
            w[i][a]=w[a][i]=c;
        }
    }
    for(int i=n-m+1;i<=n;i++){//用户终端编号
        int x;
        cin>>x;
        f[i][1]=x;
    }
    for(int i=1;i<=n;i++){
        f[i][0]=0;
    }
    DF$(1,-1);
    DFS(1,-1);
    for(int j=m;j>=0;j--){
        if(f[1][j]>=0){
            cout<<j;
            return 0;
        }
    }
    cout<<0;
    return 0;
}

3. P4084 [USACO17DEC] Barn Painting G

题意简述:给定一棵树,可以把节点染成0/1/2三种颜色,互相连通的两点的颜色不能相同,求有多少种染色方案。

f[u][c]表示以节点u为根的子树,把节点u染成颜色c的方案总数。

当某个节点被指定上色后,它染另外两种颜色的方案数为0,因为这种颜色已经被抢占,另外两种颜色上不了色。因此在递归过程中需要特判一下。

递归过程中,如果上述情况没发生,那么f[u][c]的值默认为1,因为自己涂一种颜色肯定算是一种方案。这就是边界。

对于u的每个子节点v,如果u染上了颜色c,那么v肯定不能染上颜色c(就设为颜色d吧)。u的每个子节点v都有f[v][d]种染色方案。

根据乘法原理,状态转移方程是:f[u][c] = f[u][c] * Σf[v][d],其中0<=d<=2 && d!=c

答案是Σf[1][c]。别忘了取模和开long long

code:

#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=1e5+5,mod=1e9+7;
int n,k,f[N][3];
vector<int>t[N];
void DFS(int u,int fa){
    bool flag=0;
    for(int c=0;c<3;c++){
        if(f[u][c]){//已经染色,其他染色方案就不可行了
            flag=1;
            break;
        }
    }
    if(!flag){
        for(int c=0;c<3;c++){
            f[u][c]=1;
        }
    }
    for(auto&v:t[u]){
        if(v==fa)continue;
        DFS(v,u);
        for(int c=0;c<3;c++){
            int sum=0;
            for(int d=0;d<3;d++){
                if(c==d)continue;
                sum=(sum+f[v][d])%mod;
            }
            f[u][c]=(f[u][c]*sum%mod)%mod;
        }
    }
}
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    cin>>n>>k;
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        t[u].push_back(v);
        t[v].push_back(u);
    }
    for(int i=1;i<=k;i++){
        int u,c;
        cin>>u>>c;
        f[u][c-1]=1;
    }
    DFS(1,-1);
    cout<<(f[1][0]+f[1][1]+f[1][2])%mod;
    return 0;
}

4. P2585 [ZJOI2006] 三色二叉树

还是刚才的思想,如果一个节点被染成了某种颜色,那么其子节点肯定不能染成与其相同的颜色。

假定0是绿色,那么后面的就简单了。

定义f[u][c]表示u子树内,将节点u染成颜色c时,能产生的最多绿色点数。

注意到这是一个二叉树,所以可以用t[N][2]这种方式存树。

状态转移方程为f[u][c]=max/min(f[l][d]+f[l][e],f[r][d]+f[r][e])+(c==0)

根据题意,c/d/e三种颜色互不相同。

答案为max/min(f[1][c])

code:

#include <bits/stdc++.h>
using namespace std;
constexpr int N=5e5+5;
string s;
int n,t[N][2],f[N][3],g[N][3],tot;
int build(){
    int now=++tot;
    if(s[now-1]=='2'){
        t[now][0]=build();
        t[now][1]=build();
    }
    else if(s[now-1]=='1'){
        t[now][0]=build();
    }
    return now;
}
void DFS(int u){
    int l=t[u][0],r=t[u][1];
    if(l)DFS(l);
    if(r)DFS(r);
    if(!l&&!r)f[u][0]=g[u][0]=1,f[u][1]=f[u][2]=g[u][1]=g[u][2]=0;
    f[u][0]=max(f[l][1]+f[r][2],f[r][1]+f[l][2])+1;
    f[u][1]=max(f[l][0]+f[r][2],f[r][0]+f[l][2]);
    f[u][2]=max(f[l][1]+f[r][0],f[r][1]+f[l][0]);
    g[u][0]=min(g[l][1]+g[r][2],g[r][1]+g[l][2])+1;
    g[u][1]=min(g[l][0]+g[r][2],g[r][0]+g[l][2]);
    g[u][2]=min(g[l][1]+g[r][0],g[r][1]+g[l][0]);
} 
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    cin>>s;
    n=s.length();
    DFS(build());
    cout<<max({f[1][0],f[1][1],f[1][2]})<<' '<<min({g[1][0],g[1][1],g[1][2]});
    return 0;
}

9月23日

1. P1040 [NOIP 2003 提高组] 加分二叉树

融合怪。其实并非树形DP,而是区间DP。

f[i][j]表示从节点i到节点j所构成的树所能获得的最大贡献。

对于上面这样的一棵树,可以选出一个根k,使得i<=k<=j,根据题中贡献的计算方式,有:f[i][j]=max(f[i][j],f[i][k-1]*f[k+1][j]+a[k])

在上面转移状态的过程中,需要记录从节点i到节点j所构成的树的root[i][j]k,以便后续的前序遍历输出。

最初,默认root[i][j]=i

边界为f[i][i]=a[i],且root[i][i]=i

注意:区间DP必须将区间长度len的循环写在循环最外层。

code:

#include <bits/stdc++.h>
#define int long long
#define debug cout<<"nyan";
using namespace std;
constexpr int N=33;
int n,a[N],f[N][N],root[N][N];
void DFS(int l,int r){
	if(l>r)return;
	cout<<root[l][r]<<' ';
	if(l==r)return;
	DFS(l,root[l][r]-1);
	DFS(root[l][r]+1,r);
}
signed main(){
	cin.tie(0)->sync_with_stdio(0);
	// memset(f,0x3f,sizeof(f));
	cin>>n;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			f[i][j]=1;
		}
	}
	for(int i=1;i<=n;i++){
		cin>>a[i];
		f[i][i]=a[i];
		root[i][i]=i;
	}
	for(int len=2;len<=n;len++){
		for(int i=1;i+len-1<=n;i++){
			int j=i+len-1;
			root[i][j]=i;
			for(int k=i;k<=j;k++){
				if(f[i][k-1]*f[k+1][j]+f[k][k]>f[i][j]){
					f[i][j]=f[i][k-1]*f[k+1][j]+a[k];
					root[i][j]=k;
				}
			}
		}
	}
	cout<<f[1][n]<<'\n';
	DFS(1,n);
	return 0;
}

2. P1613 跑路

倍增思想+图上DP+最短路,好题。

定义f[u][v][k]表示节点uv是否存在一条长度为1<<k的路径,作为之后最短路的判断逻辑使用。

根据倍增思想,考虑从节点u走到节点v经过了节点w,那么从uv一定存在一条长度为1<<k的路径,当且仅当存在一个节点w,使得u-w之间和w-v的路径长度均为1<<(k-1)。此时uv的想象距离为1

然后用Floyd跑最短路即可。dist初始值要赋最大值。

注意:Floyd最短路的断点一定要写在两端点循环的外面。

code:

#include <bits/stdc++.h>
#define int long long
#define debug cout<<"nyan";
using namespace std;
constexpr int N=55,M=1<<6;
int n,m;
bool f[N][N][M];
int dist[N][N];
signed main(){
	cin.tie(0)->sync_with_stdio(0);
	memset(dist,0x3f,sizeof(dist));
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;
		cin>>u>>v;
		dist[u][v]=1;//有向边
		f[u][v][0]=1;
	}
	for(int k=1;k<=M;k++){
		for(int u=1;u<=n;u++){
			for(int v=1;v<=n;v++){
				for(int w=1;w<=n;w++){
					if(f[u][w][k-1] && f[w][v][k-1]){
						f[u][v][k]=dist[u][v]=1;
					}
				}
			}
		}
	}
	for(int w=1;w<=n;w++){
		for(int u=1;u<=n;u++){
			for(int v=1;v<=n;v++){
				dist[u][v]=min(dist[u][v],dist[u][w]+dist[w][v]);
			}
		}
	}
	cout<<dist[1][n];
	return 0;
}

3. P4438 [HNOI/AHOI2018] 道路

好题。艺术节当天硬控我一小时多,还没做出来。

题干非常非常长,必须耐心读完。这里给出一些要点:

  • 每个非叶子节点都有两个子节点;

  • 对于每个非叶子节点,要么修它的左子边,要么修它的右子边,不能两者都修;

  • 每个叶子节点到根的贡献由参数abc共同决定。其实,可以把参数abc看作一种运算法则。

首先是输入。这个输入方式非常恶心,下面给出一种简洁的表达:

对于第u行(1<=u<n)而言,输入两个整数l,r

  • l>0 && r>0时,u的两个子节点是lr,且它们都是非叶子节点;

  • 否则,u的两个子节点是-l-r(也就是它们的绝对值),且它们都是叶子节点。

然后输入的就是参数,这里不赘述。

我们考虑到:不同的未修缮的左边/右边的数量,会导致对答案产生的不同贡献,所以我们将它们融入状态定义。

定义f[u][i][j]u子树中,当从u到根的路径上存在i条未修缮左边,j条未修缮右边时,u子树中所有叶子产生的最小贡献和。

分情况讨论:

  • u为叶子节点时:

    枚举ij的值,根据题中公式进行计算即可。

    f[u][i][j] = c[u]*(a[u]+i)*(b[u]+j)

  • u为非叶子节点时:

    考虑u的两个子节点lr,以及左边、右边LR

    • 如果修缮左边L,则多出一条未修缮的右边:

      f[u][i][j] = f[l][i][j]+f[r][i][j+1]

    • 如果修缮右边R,则多出一条未修缮的左边:

      f[u][i][j] = f[l][i+1][j]+f[r][i][j]

    • 上述两种情况取最小值。

    • 所以数组要赋初始最大值。

根节点1以上没有边,所以答案为f[1][0][0]

注意到对于每个节点u,有效的状态转移仅仅来源于u+1u+2两个节点,这样可以大大减少递归次数,避免RE。

code:

#include <bits/stdc++.h>
#define int long long
#define debug cout<<"nyan";
using namespace std;
constexpr int N=4e4+5,M=45;
int n,f[N][M][M],a[N],b[N],c[N],l[N],r[N];
void DFS(int u,int inu,int I,int J){
	if(inu<=0){
		for(int i=0;i<=I;i++){
			for(int j=0;j<=J;j++){
				f[u][i][j]=c[-inu]*(a[-inu]+i)*(b[-inu]+j);
			}
		}
	}
	else{
		DFS(u+1,l[inu],I+1,J);
		DFS(u+2,r[inu],I,J+1);
		for(int i=0;i<=I;i++){
			for(int j=0;j<=J;j++){
				f[u][i][j]=min(f[u+1][i][j]+f[u+2][i][j+1],f[u+1][i+1][j]+f[u+2][i][j]);
			}
		}
	}
}
signed main(){
	cin.tie(0)->sync_with_stdio(0);
	// memset(f,0x3f,sizeof(f));
	cin>>n;
	for(int u=1;u<n;u++){
		cin>>l[u]>>r[u];
	}
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i]>>c[i];
	}
	DFS(1,1,0,0);
	cout<<f[1][0][0];
	return 0;
}

9月24日

1. P3177 [HAOI2015] 树上染色

可以默认所有点刚开始都是白点,操作为:把某一点染成黑点。

为简便,将题中的k记为m

f[u][j]表示u子树内,染了j个点,所获得的最大收益。

考虑边u-v的流量,可以推出状态转移方程:f[u][j]=f[u][j-k]+f[v][k]+g[u][v]*k*(m-k)+g[u][v]*(siz[v]-k)*(n-m-siz[v]+k)

g[u][v]*k*(m-k)表示:染成黑色的点,两两之间经过边u-v产生的总贡献。

g[u][v]*(siz[v]-k)*(n-m-siz[v]+k)表示:染成白色的点,两两之间经过边u-v产生的总贡献(一个点不是黑就是白)。

答案显然是f[1][m]

code:

#include <bits/stdc++.h>
#define int long long
#define debug cout<<"nyan";
using namespace std;
constexpr int N=2005;
int n,m,g[N][N],siz[N],f[N][N];
vector<int>t[N];
void DFS(int u,int fa){
	siz[u]=1;
	for(auto v:t[u]){
		if(v==fa)continue;
		DFS(v,u);
		siz[u]+=siz[v];
		for(int j=max(m,siz[u]);j>=0;j--){
			for(int k=max(0ll,j-siz[u]+siz[v]);k<=min(j,siz[v]);k++){
				f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]+g[u][v]*k*(m-k)+g[u][v]*(siz[v]-k)*(n-m-siz[v]+k));
			}
		}
	}
}
signed main(){
	cin.tie(0)->sync_with_stdio(0);
	cin>>n>>m;
	for(int i=1;i<n;i++){
		int u,v,w;
		cin>>u>>v>>w;
		t[u].push_back(v);
		t[v].push_back(u);
		g[u][v]=g[v][u]=w;
	}
	DFS(1,-1);
	cout<<f[1][m];
	return 0;
}

2. P2607 [ZJOI2008] 骑士

基环树DP,属于树上DP/图上DP的一种。

引出概念:基环树是一种而非树。定义一个图是基环树,当且仅当它的点数等于边数,即图中存在且仅存在一个环。

由此可以发现基环树有一个很好的性质:对于某一基环树的环,将该环上相邻两点间的边断开,该基环树即会成为一颗树,其树根恰为被断开连接的两点中的任意一个。

因此,在进行基环树上的DP时,可以先找到它的环,断开环上的边,进行树上DP。

本题的样例很水,可以自己造一个。

注意到本题中可能出现多个基环树(即基环树森林)。那么这道题就需要对所有的基环树进行DP,累计求和得出答案。

对于每一个基环树断开环上的某边形成的树而言,对于节点u和其一个子节点v,存在舞会关系

舞会关系来源于“没有上司的舞会”,它是指,如果节点u被选择,节点v必须不能被选择;如果节点u被选择,节点v可选可不选。

同舞会题,可以定义一个f[u][0/1]数组,记录最大贡献。状态转移方程是:

f[u][0] += max(f[v][0],f[v][1])

f[u][1] += f[v][0]

答案为所有基环树f[root][0]的和。

code:

#include <bits/stdc++.h>
#define int long long
#define debug cout<<"nyan";
using namespace std;
constexpr int N=1e6+5;
vector<int>g[N];
int n,a[N],fa[N],vis[N],ans,f[N][2];
void DP(int u,int root,int fa){
	f[u][1]=a[u];
	f[u][0]=0;
	vis[u]=1;
	for(auto v:g[u]){
		if(v==root || v==fa)continue;
		DP(v,root,u);
		f[u][0] += max(f[v][0],f[v][1]);
		f[u][1] += f[v][0];
	}
}
void DFS(int u){
	vis[u]=1;
	int root=u;
	while(!vis[fa[root]]){
		root=fa[root];
		vis[root]=1;
	}
	DP(root,root,-1);
	int maxn=f[root][0];
	root=fa[root];
	DP(root,root,-1);
	maxn=max(maxn,f[root][0]);
	ans+=maxn;
}
signed main(){
	cin.tie(0)->sync_with_stdio(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		int x;
		cin>>a[i]>>x;
		g[x].push_back(i);
		fa[i]=x;
	}	
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			DFS(i);
		}
	}
	cout<<ans;
	return 0;
}

3. P1131 [ZJOI2007] 时态同步

树形DP,但有辅助数组。甚至可以认为是两个DP数组。

本题称:对于任何两个节点,都存在且仅存在一条通路,这启示我们本题中的图实际上是一棵树。

树有一个很好的性质:对于树上的任何一个节点,都可以将此节点作为根节点,重新建一颗新树。所以本题中的激发器,即是树的根。

我们的操作只能将边权加1而不能减。那么可以证明一个贪心:对于一列边权E,想对于所有的x∈E互相相等,最少操作次数即为Σ(max(E)-x)

因此可以定义tim[u]表示从u子树达到时态同步的操作次数最少时,从u到任意一个叶子节点需要的时间。

显然tim[u]=max(tim[v]+g[u][v]),其中vu的子节点。

定义f[u]表示u子树达到时态同步的最少操作次数。

f[u] += f[v]+(tim[u]-(tim[v]+g[u][v])),其中vu的子节点。

解释:(tim[v]+g[u][v])vu本来的权和,其与tim[u]的差值(tim[u]-(tim[v]+g[u][v]))正为uv的操作次数。

答案即是f[s]s为根,即触发器所在的编号)。

code:

#include <bits/stdc++.h>
#define int long long
#define debug cout<<"nyan";
using namespace std;
constexpr int N=5e5+5;
int n,s,tim[N],f[N];
vector<pair<int,int>>t[N];
void DFS(int u,int fa){
	for(auto&[v,w]:t[u]){
		if(v==fa)continue;
		DFS(v,u);
		tim[u]=max(tim[u],tim[v]+w);
	}
	for(auto&[v,w]:t[u]){
		if(v==fa)continue;
		// DFS(v,u);
		f[u]+=f[v]+(tim[u]-(tim[v]+w));
	}
}
signed main(){
	cin.tie(0)->sync_with_stdio(0);
	cin>>n>>s;
	for(int i=1;i<n;i++){
		int u,v,w;
		cin>>u>>v>>w;
		t[u].push_back(make_pair(v,w));
		t[v].push_back(make_pair(u,w));
	}
	DFS(s,-1);
	cout<<f[s];
	return 0;
}

9月25日

1. P3174 [HAOI2009] 毛毛虫

有点像有机化学里找最长碳链的问题。有机化学中分子结构的研究似乎涉及图论。

数据范围为3e5,启示我们不能开二维DP数组,最好开一维。

定义f[u]表示u子树内,以u为头的最长毛毛虫的节点数最大值。

定义deg[u]表示u节点的度数。

u为叶子节点时,f[u]=1,否则f[u]=f[v]+deg[u]-(u!=1)(因为根节点没有父亲,不用减去1)。

对于每个节点,考虑一个贪心:找出其子节点的最优解(f[v1])和次优解(f[v2]),加起来就一定是最优解。

因此有:

  • u没有子节点,ans=deg[u]+1

  • u有一个子节点,ans=f[v1]+deg[u]

  • u有两个子节点,ans=f[v1]+f[v2]+deg[u]-1

上述三种情况取最大值,输出ans即可。

code:

#include <bits/stdc++.h>
#define int long long
#define debug cout<<"nyan";
using namespace std;
constexpr int N=3e5+5;
int n,m,f[N],deg[N],ans;
vector<int>t[N];
void DFS(int u,int fa){
	int v1=0,v2=0,cnt=0;//cnt是儿子数
	for(auto v:t[u]){
		if(v==fa)continue;
		cnt++;
		DFS(v,u);
		if(f[v]>f[v1]){
			v2=v1,v1=v;
		}
		else if(f[v]>f[v2]){
			v2=v;
		}
	}
	if(cnt==0){
		f[u]=1;
		ans=max(ans,deg[u]+1);
	}
	else if(cnt==1){
		f[u]=f[v1]+deg[u]-(u!=1);
		ans=max(ans,f[v1]+deg[u]);
	}
	else{
		f[u]=f[v1]+deg[u]-(u!=1);
		ans=max(ans,f[v1]+f[v2]+deg[u]-1);
	}
}
signed main(){
	cin.tie(0)->sync_with_stdio(0);
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;
		cin>>u>>v;
		t[u].push_back(v);
		t[v].push_back(u);
		deg[u]++,deg[v]++;
	}
	DFS(1,-1);
	cout<<ans;
	return 0;
}
posted @ 2025-09-26 14:47  L-Coding  阅读(9)  评论(0)    收藏  举报