习题11 Dyslexic Gollum UVA - 1633 (状态压缩)
题目链接
参考博客
因为要求
01
01
01串中不包含长度至少为
k
k
k的回文串,只要不包含长度为
k
k
k的回文串,就不包含长度大于
k
+
2
k+2
k+2的回文串(相当于在两边补上同样的字符),因此只要确保回文串长度无法达到
k
k
k和
k
+
1
k+1
k+1即可。
状态设计: 设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]为前
i
i
i个位置已经安排好,安排好的
i
i
i个中后
k
+
1
k+1
k+1个字符二进制数字为
j
j
j时,满足条件的字符串数量。每次我们向后扩展一个位置:前
i
−
1
i-1
i−1个位置安排好了,第
i
i
i个位置放下
0
0
0或
1
1
1之后,从
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j]转移过来,同时要求加入
0
0
0或
1
1
1都不能产生长度达到
k
k
k和
k
+
1
k+1
k+1的回文串。因为前面的都是满足要求的,即回文串长度不会超过
k
k
k,假设第
i
i
i位再放置
0
0
0,可能产生非法的情况只有两种:
- 回文串长度直接达到 k + 1 k+1 k+1,即当前 i − 1 i-1 i−1个位置安排好时,状态为 j j j的后 k − 1 k-1 k−1个字符构成回文串,而倒数第 k k k个字符恰好为 0 0 0,此时会构成"0+长度为 ( k − 1 ) (k-1) (k−1)的回文串+0"的长度为 k + 1 k+1 k+1的字符串。
- 回文串长度达到 k k k,即当前 i − 1 i-1 i−1个位置安排好时,状态为 j j j的后 k − 2 k-2 k−2个字符构成回文串,而倒数第 k − 1 k-1 k−1个字符恰好为 0 0 0,此时会构成"0+长度为 ( k − 2 ) (k-2) (k−2)的回文串+0"的长度为 k k k的字符串。
排除掉这两种情况(若枚举第
i
i
i位为
1
1
1也同理需要排除掉这两种情况),其他情况都直接从
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j]转移过来。
状态转移: 其实不算状态转移,而称为“刷表法”,就是用当前的状态去更新相关的状态。枚举前
i
−
1
i-1
i−1个的所有状态,然后将所有合法状态相加(即除了以上列出的两种非法情况)。
#include<bits/stdc++.h>
using namespace std;
const int N=410,K=1<<11,mod=1e9+7;
int dp[N][K];
int n,k;
bool check(int str,int len){
for(int i=0,j=len-1;i<j;i++,j--){
if((1&(str>>i))!=(1&(str>>j)))
return false;
}return true;
}
int main(){
int T;cin>>T;
while(T--){
cin>>n>>k;
if(k>n){
cout<<(1<<n)<<endl;continue;
}
if(k==1){
cout<<0<<endl;continue;
}
memset(dp,0,sizeof dp);
dp[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j< (1<<k+1);j++){
if(dp[i-1][j]){
bool judge1=1,judge0=1;
if(i>k&&check(j&((1<<k-1)-1),k-1)){
if(j&(1<<k-1))judge1=0;
else judge0=0;
}
if(i>=k&&check(j&((1<<k-2)-1),k-2)){
if(j&(1<<k-2))judge1=0;
else judge0=0;
}
int cur=(j&(1<<k)-1)<<1;
if(judge0)dp[i][cur]=(dp[i][cur]+dp[i-1][j])%mod;
cur++;
if(judge1)dp[i][cur]=(dp[i][cur]+dp[i-1][j])%mod;
}
}
}
int ans=0;
for(int i=0;i<( 1<<k+1);i++)
ans=(ans+dp[n][i])%mod;
cout<<ans<<endl;
}
}
或者可以像以下用 s t [ s ] st[s] st[s]数组存储每个状态 s s s是否合法,即不是回文串,然后再向后一位转移。
#include<bits/stdc++.h>
using namespace std;
const int N=410,K=1<<11,mod=1e9+7;
bool st[K];
int dp[N][K];
int n,k;
inline bool check(int str,int len){
for(int i=0,j=len-1;i<j;i++,j--)
if((1&(str>>i))!=(1&(str>>j)))
return true;
return false;
}
int main(){
int T;cin>>T;
while(T--){
cin>>n>>k;
if(k>n){cout<<(1<<n)<<endl;continue;}
if(k==1){cout<<0<<endl;continue;}
if(k==n){
int ans=0;
for(int i=0;i<1<<k;i++)
if(check(i,k))ans++;
cout<<ans<<endl;continue;
}
k++;
for(int i=0;i<1<<k;i++)
st[i]=check(i>>1,k-1)&check(i^(1<<k-1),k-1)&check(i,k);
memset(dp,0,sizeof dp);
for(int i=0;i< 1<<k;i++)
if(st[i])dp[k][i]=1;
for(int i=k+1;i<=n;i++){
for(int j=0;j< 1<<k;j++){
if(st[j]){
int t=((j&((1<<k-1)-1))<<1);
if(st[t])dp[i][t]=(dp[i][t]+dp[i-1][j])%mod;
t++;
if(st[t])dp[i][t]=(dp[i][t]+dp[i-1][j])%mod;
}
}
}
int ans=0;
for(int i=0;i< 1<<k;i++)
if(st[i])ans=(ans+dp[n][i])%mod;
cout<<ans<<endl;
}
}
习题12 Protecting Zonk UVA - 12093 (树形dp)
题目链接
参考题解
树形dp第一步:选定一个根,将无根树转化为有根树,之后设定
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示当前以
i
i
i为根的子树,且该子树状态为
j
j
j的最优解。
最难的就是对状态的设计,使得状态的转移可以覆盖所有情况。
这道题最简单的状态是3个:设当前节点为u,则u可以配置装置A;u配置装置B;u不配置任何装置。但是因为装置B的功能不只局限在父子中间,子节点B的可以扩展到祖父和父亲,父节点B可以扩展到儿子和孙子。因此应该细化状态,因为某条边被覆盖可能是被父亲,祖父,儿子和孙子。因为我们已经把无根树转化为有根树,只考虑当前节点u到孩子的连边的覆盖者,并不是我们不考虑节点u到父亲的连边,因为节点u到父亲的连边是在父亲那一层进行考虑的。
状态设计:
当u有配置A或B,u到孩子连边必然会被覆盖;否则该连边不是被u的子节点覆盖,就是被u的父节点覆盖。因此设计出以下状态:
- dp[u][0]:u无装置,u到孩子的连边被孩子覆盖
- dp[u][1]:u有装置A
- dp[u][2]:u有装置B
- dp[u][3]:u无装置,u到孩子的连边被父亲覆盖
状态转移:
设v为u的子节点们。
-
d
p
[
u
]
[
0
]
=
∑
m
i
n
(
d
p
(
v
,
1
,
2
)
dp[u][0]=\sum{min(dp(v,1,2)}
dp[u][0]=∑min(dp(v,1,2)
u被子节点覆盖,每个孩子应该配备A或B,因此无法从0和3转移来。 -
(
a
)
:
d
p
[
u
]
[
1
]
=
∑
m
i
n
(
d
p
(
v
,
0
,
1
,
2
)
+
c
1
(a):dp[u][1]=\sum{min(dp(v,0,1,2)}+c1
(a):dp[u][1]=∑min(dp(v,0,1,2)+c1
因为dp[u][1]表示u有装置A,而A只能覆盖u的相邻边,无法覆盖孩子和孙子的连边,因此无法从dp[v][3]转移来。
u配备装置A可以使得u的相邻边被覆盖,还有什么情况也可以使得u的相邻边被覆盖呢?就是u的某个孩子配置B!我们需要进行比较,即是”u配备A“花费更低还是“u的孩子配备B”花费更低。后者可以表示为:
( b ) : ∑ m i n ( d p ( v , 0 , 1 , 2 ) ) + m i n ( d p [ k ] [ 2 ] − m i n ( d p ( k , 0 , 1 , 2 ) ) (b):\sum{min(dp(v,0,1,2))} +min(dp[k][2]-min(dp(k,0,1,2)) (b):∑min(dp(v,0,1,2))+min(dp[k][2]−min(dp(k,0,1,2))
其中v表示u的所有子节点,k表示那个配备装置B的子节点,因为它已经配备了B,就排除其他原本可能取到的最小状态。
最后比较 d p [ u ] [ 1 ] dp[u][1] dp[u][1]在(a)(b)两式子中取小值。 - d p [ u ] [ 2 ] = ∑ m i n ( d p ( v , 0 , 1 , 2 , 3 ) + c 2 dp[u][2]=\sum{min(dp(v,0,1,2,3)}+c2 dp[u][2]=∑min(dp(v,0,1,2,3)+c2 有B就是王者,可以从子节点的每个状态转移而来。
- d p [ u ] [ 3 ] = ∑ m i n ( d p ( v , 0 , 1 , 2 ) dp[u][3]=\sum{min(dp(v,0,1,2)} dp[u][3]=∑min(dp(v,0,1,2)因为u与孩子节点被父亲覆盖,孩子有无配备A或B均可,但无法转移到3。
#include<bits/stdc++.h>
#define min3(a,b,c) min(a,min(b,c))
const int inf=0x3f3f3f3f;
using namespace std;
const int N=1e4+10;
vector<int>son[N];
int n,c1,c2;
int dp[N][4];
void dfs(int u,int fa){
dp[u][0]=dp[u][3]=0,dp[u][1]=0,dp[u][2]=c2;
int minB=inf;
for(int i=0;i<son[u].size();i++){
int v=son[u][i];
if(v==fa)continue;
dfs(v,u);
int minn=min3(dp[v][0],dp[v][1],dp[v][2]);
dp[u][0]+=min(dp[v][1],dp[v][2]);
dp[u][2]+=min(minn,dp[v][3]);
dp[u][3]+=minn;
minB=min(minB,dp[v][2]-minn);
}
dp[u][1]=dp[u][3]+min(c1,minB);
}
int main(){
while(cin>>n>>c1>>c2&&n){
for(int i=0;i<=n;i++)son[i].clear();
memset(dp,0x3f,sizeof dp);
for(int i=1;i<n;i++){
int x,y;cin>>x>>y;
son[x].push_back(y),son[y].push_back(x);
}
dfs(1,0);
cout<<min3(dp[1][0],dp[1][1],dp[1][2])<<endl;
}
return 0;
}