上讲习题
AcWing 1075
本题每个数都可以变成下一个点,很像某个点向下一个点连边。一个数只有一个因子和,但是有可能多个数的因子和等于某个数,就像是一个点只有一个爹,但是会有多个儿子,所以这样建图是一棵树,每个点的爹就是自己的因子和。任意一个序列都对应树的一条路径,要求最长的序列就是求树的直径。求树的直径参考上讲的AcWing 1072。
#include<bits/stdc++.h>
using namespace std;
const int NN=50004;
int sum[NN],ans;
vector<int>g[NN];
int dfs(int u)
{
int maxx=0,maxy=0;
for(int i=0;i<g[u].size();i++)
{
int res=dfs(g[u][i])+1;
if(res>maxx)
{
maxy=maxx;
maxx=res;
}
else if(res>maxy)
maxy=res;
}
ans=max(ans,maxx+maxy);
return maxx;
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=2;j<=n/i;j++)
sum[i*j]+=i;
for(int i=1;i<=n;i++)
if(sum[i]<i)
g[sum[i]].push_back(i);
dfs(1);
printf("%d",ans);
return 0;
}
AcWing 1074
本题要求剪掉一些枝,可以分别给左右儿子分配一些保留的枝并加上自己的枝上的苹果。先预处理出所有的点的左右儿子,然后把连向父亲的边的苹果放在自己身上。注意,因为根节点没有父亲,然而留下的点会算它一个,所以留下的边数要加一。本题的状态有重复,需要记忆化。
#include<bits/stdc++.h>
using namespace std;
const int NN=104;
int a[NN],g[NN][NN],l[NN],r[NN],f[NN][NN],n,m;
void dfs(int u)
{
for(int i=1;i<=n;i++)
if(g[u][i]>=0)
{
l[u]=i;
a[i]=g[u][i];
g[u][i]=g[i][u]=-1;
dfs(i);
break;
}
for(int i=1;i<=n;i++)
if(g[u][i]>=0)
{
r[u]=i;
a[i]=g[u][i];
g[u][i]=g[i][u]=-1;
dfs(i);
break;
}
}
int dp(int u,int x)
{
int &d=f[u][x];
if(d>=0)
return d;
if(!x)
return d=0;
if(!l[u]&&!r[u])
return d=a[u];
for(int k=0;k<x;k++)
d=max(d,dp(l[u],k)+dp(r[u],x-k-1)+a[u]);
return d;
}
int main()
{
scanf("%d%d",&n,&m);
m++;
memset(g,0xaf,sizeof(g));
for(int i=1;i<n;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
g[u][v]=g[v][u]=w;
}
dfs(1);
memset(f,-1,sizeof(f));
printf("%d",dp(1,m));
return 0;
}
AcWing 1077
本题和上讲的AcWing 323非常像。但是我们研究一下发现,假设某个点不用,则所有儿子至少用一个。但是这种分析会少考虑一个情况:有可能父亲用了,那么儿子也可以一个都不用。于是我们就要分成三种状态: f u , ( 0 , 1 , 2 ) f_{u,(0,1,2)} fu,(0,1,2),分别表示父亲一定用了且自己一定没用、一定有一个儿子用了且自己一定没用、自己一定用了的情况下覆盖完本子树的最小代价。首先,第一种情况,那么自己的每个儿子可以选择用或者不用,而且因为自己一定不用,所以子节点不能选择父亲一定用了的情况, f u , 0 + = min ( f v , 1 , f v , 2 ) f_{u,0}+=\min(f_{v,1},f_{v,2}) fu,0+=min(fv,1,fv,2)。第二种情况,则所有子节点要至少有一种,同理,子节点也不能选择父亲一定用的情况, f u , 1 = min ( f v , 1 , f v , 2 ) f_{u,1}=\min(f_{v,1},f_{v,2}) fu,1=min(fv,1,fv,2)。但是这种情况如果更小的全是 f v , 1 f_{v,1} fv,1,则要找到一个变了之后差值最小的替换,即 m i n n = min ( f v , 2 − f v , 1 ) minn=\min(f_{v,2}-f_{v,1}) minn=min(fv,2−fv,1)。如果每个都是用 f v , 2 f_{v,2} fv,2更新的,那么 f u , 1 f_{u,1} fu,1就要加上 m i n n minn minn。考虑简化这个式子,发现如果用的 f v , 2 f_{v,2} fv,2更新,那么 f v , 2 = min ( f v , 1 , f v , 2 ) f_{v,2}=\min(f_{v,1},f_{v,2}) fv,2=min(fv,1,fv,2),带入刚才求 m i n n minn minn的式子刚好等于 0 0 0。遇到 0 0 0相当于不用加了,和想要的效果刚好相对应,则不用判断是否用了 f v , 2 f_{v,2} fv,2直接更新即可。第三种情况,则孩子可以用或者不用,则 f u , 2 = min ( f v , ( 0 , 1 , 2 ) ) f_{u,2}=\min(f_{v,(0,1,2)}) fu,2=min(fv,(0,1,2))。注意别忘了加上使用自己的代价。有一个问题:如何找根?方法很简单:找入度为零的即可。最后输出答案时根节点没有父亲,所以 a n s = min ( f r o o t , 1 , f r o o t , 2 ) ans=\min(f_{root,1},f_{root,2}) ans=min(froot,1,froot,2)。
#include<bits/stdc++.h>
using namespace std;
const int NN=2504;
struct node
{
int num,son[NN],money;
}a[NN];
int f[NN][3];
bool isson[NN];
int dp(int x,int fa)
{
int minn=2147483647;
f[x][2]=a[x].money;
for(int i=1;i<=a[x].num;i++)
{
int y=a[x].son[i];
dp(y,x);
f[x][0]+=min(f[y][1],f[y][2]);
f[x][1]+=min(f[y][1],f[y][2]);
f[x][2]+=min(f[y][2],min(f[y][1],f[y][0]));
minn=min(minn,f[y][2]-min(f[y][1],f[y][2]));
}
f[x][1]+=minn;
}
int main()
{
int n,root=1;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
scanf("%d%d",&a[x].money,&a[x].num);
for(int j=1;j<=a[x].num;j++)
{
scanf("%d",&a[x].son[j]);
isson[a[x].son[j]]=true;
}
}
while(isson[root])
root++;
dp(root,0);
printf("%d",min(f[root][1],f[root][2]));
return 0;
}
概念
数位 d p dp dp,就是一个构造数的 d p dp dp。这类问题一般求满足要求的数的个数。
方法
一般来说都是研究上边界这个数的每一位,从最高位开始。如果这一位填的是上边界,则要继续判断;如果填的不是上边界,则后面的可以随便填,直接计算并退出即可。因为要算随便填的方案数,所以可以初始化某一位开始,随便填且满足题目要求的方案数。
例题
AcWing 1081
不难发现,如果一个数填的不是 1 1 1或 0 0 0,那么就需要重复的数相加,一定不满足要求。则本题就是求把一个数拆成 b b b进制,每一位填 1 1 1或 0 0 0,刚好填 k k k个 1 1 1且数不超过 y y y的方案数。如果上界大于 0 0 0,那么这一位填 0 0 0后面的就可以随便填且要选 k k k位填 1 1 1, a n s + = C ( s i z e , k ) ans+=C(size,k) ans+=C(size,k)。如果上界还大于 1 1 1,那么这一位填 1 1 1后面也可以随便填,则 a n s + = C ( s i z e − 1 , k ) ans+=C(size-1,k) ans+=C(size−1,k),而且这一位不管填什么后面都随便填,那么已经把所有方案计算了,直接退出。如果上界这一位等于 1 1 1,那么这一位填了 1 1 1其他的就不能乱填,可是需要填的就少了,记 l a s t last last为前面填 1 1 1的个数,则 l a s t + + last++ last++,前面计算 C C C也要减去这些固定的。若上界等于 0 0 0,那么填了 0 0 0后也不能乱填,不能对答案有贡献。最后全部的都算完了后若已经固定了 k k k的每一位,即 l a s t = k last=k last=k,则答案 + 1 +1 +1。本题中,初始化从某一位开始随便填的方案数,就是初始化 C C C的值。
#include<bits/stdc++.h>
using namespace std;
const int NN=33;
int C[NN][NN],k,b;
int dp(int n)
{
vector<int>num;
while(n)
{
num.push_back(n%b);
n/=b;
}
int res=0,last=0;
for(int i=num.size()-1;i>=0;i--)
{
int x=num[i];
if(x)
{
res+=C[i][k-last];
if(x>1)
{
res+=C[i][k-last-1];
break;
}
else
{
last++;
if(last>k)
break;
}
}
if(!i&&last==k)
res++;
}
return res;
}
int main()
{
for(int i=0;i<NN;i++)
for(int j=0;j<=i;j++)
if(!j)
C[i][j]=1;
else
C[i][j]=C[i-1][j]+C[i-1][j-1];
int l,r;
scanf("%d%d%d%d",&l,&r,&k,&b);
printf("%d",dp(r)-dp(l-1));
return 0;
}
AcWing 1083
这个题每一位只要不是当前位的上界就可以随便填。但是题目要求两个数的差必须大于 2 2 2,则看一看上一位的边界与这一位填的数的差是否大于 2 2 2即可。如果在枚举当前位填边界的情况时,发现两位的边界的差小于 2 2 2,则这样填这个序列已经不满足要求了,直接退出即可。最后考虑后面随便填的方案数,设 f i , j f_{i,j} fi,j为有 i i i位且最高位是 j j j的数的个数。两位的差大于二即可转移,则 f i , j + = f i − 1 , k , ∣ j − k ∣ ≥ 2 f_{i,j}+=f_{i-1,k},|j-k|\ge2 fi,j+=fi−1,k,∣j−k∣≥2。注意,本题中如果有多个前导 0 0 0是会计算为不可取的方案,因为有两位都是 0 0 0 ,差小于 2 2 2。但是如果有多个前导 0 0 0,在本题中应当是可取的,所以要特殊判断。有多个前导 0 0 0相当于前几位都固定为 0 0 0,后面的随便填,因为最高位一定大于 0 0 0。最后,如果枚举完了,那么说明都填最高位是可以的,答案加一。注意, 0 0 0也是一个可行的数。
#include<bits/stdc++.h>
using namespace std;
const int NN=11;
int f[NN][NN];
int dp(int n)
{
if(!n)
return 1;
vector<int>num;
while(n)
{
num.push_back(n%10);
n/=10;
}
int res=0,last=-2;
for(int i=num.size()-1;i>=0;i--)
{
int x=num[i];
for(int j=i==num.size()-1;j<x;j++)
if(abs(j-last)>=2)
res+=f[i+1][j];
if(abs(x-last)<2)
break;
last=x;
if(!i)
res++;
}
for(int i=1;i<num.size();i++)
for(int j=1;j<=9;j++)
res+=f[i][j];
return res+1;
}
int main()
{
for(int i=1;i<NN;i++)
for(int j=0;j<=9;j++)
if(i==1)
f[i][j]=1;
else
for(int k=0;k<=9;k++)
if(abs(j-k)>=2)
f[i][j]+=f[i-1][k];
int l,r;
scanf("%d%d",&l,&r);
printf("%d",dp(r)-dp(l-1));
return 0;
}
AcWing 1084
本题是同样的思路,如果等于边界就继续枚举,反之就加上后面随便填的总方案数。考虑随便填的方案数,发现要求前面填边界的数的总和加上后面填的数的综合模 n n n为 0 0 0,所以可以把 l a s t last last设为前面填的边界的总和,并设 f i , j , k f_{i,j,k} fi,j,k表示有 i i i位,且最高位为 j j j,模 n n n为 k k k的数的个数。则每次 r e s + = f i , j , − l a s t res+=f_{i,j,-last} res+=fi,j,−last,因为后面模数填够 − l a s t -last −last,两个模数相加就是 0 0 0了。考虑状态转移,枚举数的第二位 x x x,则 f i , j , k + = f i − 1 , x , m o d ( k − x ) f_{i,j,k}+=f_{i-1,x,mod(k-x)} fi,j,k+=fi−1,x,mod(k−x),边界条件 f 1 , j , m o d ( j ) = 1 f_{1,j,mod(j)}=1 f1,j,mod(j)=1。
#include<bits/stdc++.h>
using namespace std;
const int NN=11;
int f[NN][NN][104],P;
int mod(int x)
{
return (x%P+P)%P;
}
int dp(int n)
{
if(!n)
return 1;
vector<int>num;
while(n)
{
num.push_back(n%10);
n/=10;
}
int res=0,last=0;
for(int i=num.size()-1;i>=0;i--)
{
int x=num[i];
for(int j=0;j<x;j++)
res+=f[i+1][j][mod(-last)];
last+=x;
if(!i&&!(last%P))
res++;
}
return res;
}
int main()
{
int l,r;
while(scanf("%d%d%d",&l,&r,&P)!=EOF)
{
memset(f,0,sizeof(f));
for(int i=0;i<=9;i++)
f[1][i][mod(i)]=1;
for(int i=2;i<NN;i++)
for(int j=0;j<=9;j++)
for(int k=0;k<P;k++)
for(int x=0;x<=9;x++)
f[i][j][k]+=f[i-1][x][mod(k-j)];
printf("%d\n",dp(r)-dp(l-1));
}
return 0;
}
习题
AcWing 1082
AcWing 1085
AcWing 1086
解析和代码在下一篇博客——单调队列优化 d p dp dp给出