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
时,则证明不亏本,所以可以作为答案使用。
值得注意的是,答案是第一个大于等于0
的f[u][j]
中的j
,而并不是f
数组值,这启示我们答案所代表的量并不一定要作为状态值的定义。
状态转移方程是f[u][j]=max(f[u][j],f[v][k]+f[u][j-k]-g[u][v])
,其中g[u][v]
表示从u
到v
的边权。
解释:对于状态f[u][j]
,它的转移肯定来源于它的儿子。
故考虑其儿子节点的状态f[v][k]
与其余状态f[u][j-k]
作加和。显然加和之后所形成的树的利润还需要减去一部分支出g[u][v]
。再取最大值即可。
答案是当j
倒序遍历时(因为要取最大的j
),第一个大于等于0
的f[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]
表示节点u
到v
是否存在一条长度为1<<k
的路径,作为之后最短路的判断逻辑使用。
根据倍增思想,考虑从节点u
走到节点v
经过了节点w
,那么从u
到v
一定存在一条长度为1<<k
的路径,当且仅当存在一个节点w
,使得u-w
之间和w-v
的路径长度均为1<<(k-1)
。此时u
到v
的想象距离为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] 道路
好题。艺术节当天硬控我一小时多,还没做出来。
题干非常非常长,必须耐心读完。这里给出一些要点:
-
每个非叶子节点都有两个子节点;
-
对于每个非叶子节点,要么修它的左子边,要么修它的右子边,不能两者都修;
-
每个叶子节点到根的贡献由参数
a
、b
、c
共同决定。其实,可以把参数a
、b
、c
看作一种运算法则。
首先是输入。这个输入方式非常恶心,下面给出一种简洁的表达:
对于第u
行(1<=u<n
)而言,输入两个整数l
,r
:
-
当
l>0 && r>0
时,u
的两个子节点是l
、r
,且它们都是非叶子节点; -
否则,
u
的两个子节点是-l
、-r
(也就是它们的绝对值),且它们都是叶子节点。
然后输入的就是参数,这里不赘述。
我们考虑到:不同的未修缮的左边/右边的数量,会导致对答案产生的不同贡献,所以我们将它们融入状态定义。
定义f[u][i][j]
在u
子树中,当从u
到根的路径上存在i
条未修缮左边,j条未修缮右边时,u
子树中所有叶子产生的最小贡献和。
分情况讨论:
-
当
u
为叶子节点时:枚举
i
、j
的值,根据题中公式进行计算即可。f[u][i][j] = c[u]*(a[u]+i)*(b[u]+j)
-
当
u
为非叶子节点时:考虑
u
的两个子节点l
、r
,以及左边、右边L
、R
。-
如果修缮左边
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+1
和u+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])
,其中v
为u
的子节点。
定义f[u]
表示u
子树达到时态同步的最少操作次数。
f[u] += f[v]+(tim[u]-(tim[v]+g[u][v]))
,其中v
为u
的子节点。
解释:(tim[v]+g[u][v])
为v
到u
本来的权和,其与tim[u]
的差值(tim[u]-(tim[v]+g[u][v]))
正为u
到v
的操作次数。
答案即是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;
}