【题解】2020 年电子科技大学 ACM-ICPC 暑假前集训 - 动态规划
A - 耀西藏蛋
\(f[i][j][k]\)表示从左到右遍历到了第\(i\)列,当前有\(j\)列内一个蛋也没有放,而当前有\(k\)行没有满足右侧埋蛋的需求
第\(i-1\)列向第\(i\)列转移时,统计第\(i\)列的:
左区间的右边界行数\(st\)
右区间的左边界行数\(ed\)
不属于左右边界的行数\(mid\)
转移时有三种决策:
- 向左区间\([0,i]\)埋入\(st\)个蛋
- 在左区间\([0,i-1]\)埋入\(st\)个蛋,同时在第\(i\)列向右区间埋入\(1\)个蛋
- 在左区间\([0,i-1]\)埋入\(st\)个蛋,同时在第\(i\)列向中间区间埋入\(1\)个蛋
对三种决策求和即可得到对应状态的方案数
一个比较巧妙的地方是,延迟左区间埋蛋决策的时间,但不提前或延后右区间埋蛋决策的时间
直到扫到区间边界时再决定埋在区间的哪个位置,同时由于边界的单调性,决策不会产生后效性
一开始的想法是把左区间按边界顺序依次决策,但是再怎么处理右区间就不会了
看来还是要学习一个,多找找规律
#include <cstdio>
typedef long long ll;
const ll N=101,M=301,mod=1e9+7;
ll n,m,a[N],b[N],f[M][M][N],ac[M],bc[M],acnt,bcnt,x,y,z,ans;
ll A(ll n,ll m){
ll t=1;
for (ll i=n-m+1;i<=n;i++) t=(t*i)%mod;
return t;
}
int main(){
scanf("%lld%lld",&n,&m);
for (ll i=1;i<=n;i++){
scanf("%lld%lld",&a[i],&b[i]);
b[i]=m+1-b[i];
ac[a[i]]++;bc[b[i]]++;
}
f[0][0][0]=1;
acnt=n,bcnt=0;
for (ll i=1;i<=m;i++){
bcnt+=bc[i];
for (ll j=0;j<=i;j++)
for (ll k=0;k<=n;k++){
x=0,y=0,z=0;
if (j-ac[i]>=0)
x=(f[i-1][j-ac[i]][k]*A(i-(j-ac[i]),ac[i]))%mod;
if (j-ac[i]-1>=0&&k-1>=0&&bcnt-(k-1)>=0)
y=((f[i-1][j-ac[i]-1][k-1]*A((i-1)-(j-ac[i]-1),ac[i]))%mod*(bcnt-(k-1)))%mod;
if (j-ac[i]-1>=0&&n-acnt-bcnt>=0)
z=((f[i-1][j-ac[i]-1][k]*A((i-1)-(j-ac[i]-1),ac[i]))%mod*(n-acnt-bcnt))%mod;
f[i][j][k]=(x+y+z)%mod;
}
acnt-=ac[i];
}
ans=0;
for (ll i=0;i<=m;i++) ans=(ans+f[m][i][n])%mod;
printf("%lld",ans);
}
B - 黑暗剑22近在咫尺!
C - 赛马
DP按递归顺序,换根
可以证明我们只需要关心包含所有标记节点的最小子树即可
如果出发点不在这个子树内,答案只需要加上从出发点进入这个子树的距离即可
换根DP需要记录,当前点向下走的最长路\(mx\)、向下走第一步和\(mx\)决策不同的最长路\(sec\)
由\(mx\)和\(sec\)可以得到第一步向上走的最长路\(upmax\)
那么从当前点出发的最长路就是\(max\{mx,upmax\}\)
找一个有标记的节点做根,这样特殊子树外的节点都是靠下的分支,容易进入子树的最短距离
同时,特殊子树内的点如果有父亲,父亲一定也是特殊子树内的点
子树中标记点数量为零,则该点不在特殊子树内
#include <cstdio>
#include <iostream>
using namespace std;
typedef long long ll;
const ll N=100001;
ll n,k,a,b,c,rt,ans,head[N],sz,flag[N],fa[N],dis[N],pre[N];
ll mx[N],maxid[N],sec[N],upmax[N],glmax[N],esum[N],fae[N];
struct E{
ll next,to,w;
}e[N*2];
void add(ll a,ll b,ll c){
sz++;
e[sz].next=head[a];
head[a]=sz;
e[sz].to=b;
e[sz].w=c;
}
void dfs1(ll x){
for (ll i=head[x];i;i=e[i].next){
ll v=e[i].to,w=e[i].w;
if (fa[x]!=v){
fae[v]=w;
fa[v]=x;
dfs1(v);
flag[x]+=flag[v];
if (flag[v]&&mx[v]+w>mx[x]){
mx[x]=mx[v]+w;
maxid[x]=v;
}
}
}
}
void dfs2(ll x){
if (flag[x]){
pre[x]=x;
dis[x]=0;
}else{
pre[x]=pre[fa[x]];
dis[x]=dis[fa[x]]+fae[x];
}
for (ll i=head[x];i;i=e[i].next){
ll v=e[i].to,w=e[i].w;
if (fa[x]!=v){
dfs2(v);
if (flag[v]){
esum[x]+=esum[v]+w;
}
if (flag[v]&&mx[v]+w>sec[x]&&v!=maxid[x]){
sec[x]=mx[v]+w;
}
}
}
}
void dfs3(ll x){
upmax[x]=fae[x]+max(upmax[fa[x]],x==maxid[fa[x]]?sec[fa[x]]:mx[fa[x]]);
glmax[x]=max(upmax[x],mx[x]);
for (ll i=head[x];i;i=e[i].next){
ll v=e[i].to,w=e[i].w;
if (fa[x]!=v) dfs3(v);
}
}
int main(){
scanf("%lld%lld",&n,&k);
for (ll i=1;i<n;i++){
scanf("%lld%lld%lld",&a,&b,&c);
add(a,b,c);add(b,a,c);
}
for (ll i=1;i<=k;i++){
scanf("%lld",&a);
flag[a]=1;
}
rt=a;
dfs1(rt);
dfs2(rt);
dfs3(rt);
for (ll i=1;i<=n;i++){
ans=dis[i]+esum[rt]*2-glmax[pre[i]];
printf("%lld\n",ans);
}
}
D - Gift For You
E - 拱坝老哥喜欢这个
F - 我是音乐小天才
DP按区间长度顺序
#include <cstdio>
#include <iostream>
#include <climits>
using namespace std;
int n,f[5000][5000];
char a[5001];
int main(){
scanf("%d",&n);
scanf("%s",a);
for (int i=2;i<=n;i++)
for (int j=0;j+i-1<=n-1;j++){
f[j][j+i-1]=INT_MAX;
if (a[j]==a[j+i-1]) f[j][j+i-1]=min(f[j][j+i-1],f[j+1][j+i-2]);
f[j][j+i-1]=min(f[j][j+i-1],f[j][j+i-2]+1);
f[j][j+i-1]=min(f[j][j+i-1],f[j+1][j+i-1]+1);
}
printf("%d",f[0][n-1]);
}
G - 君の名前は?唱:“达拉崩吧斑得贝迪卜多比鲁翁”
DP按物品顺序,二进制优化
#include <cstdio>
#include <iostream>
using namespace std;
typedef long long ll;
const ll N=10001,M=6;
ll n,m,c,v[N],w[N],d[N],x[M],y[M],z[M],f[N],ans,t;
int main(){
scanf("%lld%lld%lld",&n,&m,&c);
for (ll i=1;i<=n;i++) scanf("%lld%lld%lld",&v[i],&w[i],&d[i]);
for (ll i=1;i<=m;i++) scanf("%lld%lld%lld",&x[i],&y[i],&z[i]);
for (ll i=1;i<=n;i++)
for (ll j=0;j<20&&d[i];j++){
t=((1<<j)>d[i])?d[i]:(1<<j);
d[i]-=t;
for (ll k=c;k>=t*v[i];k--)
f[k]=max(f[k],f[k-t*v[i]]+t*w[i]);
}
for (ll i=1;i<=m;i++)
for (ll j=c;j>=0;j--)
for (ll k=0;k<=j;k++)
f[j]=max(f[j],f[j-k]+x[i]*k*k+y[i]*k+z[i]);
ans=f[c];
printf("%lld",ans);
}
H - 无聊的糖哥
DP按加入边权的大小顺序
可以证明最优的情况是边权从\(0\)到\(k-1\)连在一条长度为\(k\)的链上
对于大于等于\(k\)的边权无论赋值给哪条边,对答案都没有影响
为了避免处理把两条链拼接成一条链的情况,可以先枚举一个叶子节点作为根
即这个选取的叶子节点拉到最上,其他叶节点垂到最下
DFS时栈维护上到下的区间内的点
记忆化搜索,每个区间搜索一次,一共有\(n^2\)个区间
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
typedef long long ll;
const ll N=6001;
ll n,a,b,sta[N],sz,esz,siz[N],head[N],ans,f[N][N],cnt[N];
struct E{
ll to,next;
}e[N*2];
void add(ll a,ll b){
esz++;
e[esz].next=head[a];
head[a]=esz;
e[esz].to=b;
}
ll solve(ll l,ll r){
ll a=sta[l],b=sta[l+1],c=sta[r-1],d=sta[r];
if (l==r) return 0;
if (f[a][d]) return f[a][d];
return f[a][d]=max(solve(l+1,r),solve(l,r-1))+(n-siz[b])*siz[d];
}
void dfs1(ll x){
sta[++sz]=x;
siz[x]=1;
for (ll i=head[x];i;i=e[i].next){
ll v=e[i].to;
if (v!=sta[sz-1]){
dfs1(v);
siz[x]+=siz[v];
}
}
sz--;
}
void dfs2(ll x){
sta[++sz]=x;
if (siz[x]==1) ans=max(ans,solve(1,sz));
for (ll i=head[x];i;i=e[i].next){
ll v=e[i].to;
if (v!=sta[sz-1]){
dfs2(v);
}
}
sz--;
}
int main(){
scanf("%lld",&n);
for (ll i=1;i<n;i++){
scanf("%lld%lld",&a,&b);
add(a,b);add(b,a);
cnt[a]++;cnt[b]++;
}
for (ll i=1;i<=n;i++)
if (cnt[i]==1){
memset(siz,0,sizeof(siz));
sz=0;dfs1(i);
sz=0;dfs2(i);
}
printf("%lld",ans);
}
I - 已经没有什么好怕的了!
J - 狂风呼啸,此时站在你面前的是一队真正的特种兵Ⅰ
DP按已经排好的兵种数量顺序,状态压缩
#include <cstdio>
#include <iostream>
using namespace std;
const int N=100001,M=20;
int n,m,a[N],f[1<<M],dp[1<<M],cnt[M],rev,l,r,sum[N][M];
int main(){
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++){
scanf("%d",&a[i]);
a[i]--;
cnt[a[i]]++;
}
for (int i=1;i<=n;i++)
for (int j=0;j<m;j++)
sum[i][j]=sum[i-1][j]+(a[i]==j);
for (int i=0;i<(1<<m);i++)
for (int j=0;j<m;j++)
if (i&(1<<j)) f[i]+=cnt[j];
for (int i=0;i<(1<<m);i++)
for (int j=0;j<m;j++)
if (i&(1<<j)){
l=f[i-(1<<j)]+1,r=f[i];
dp[i]=max(dp[i],dp[i-(1<<j)]+sum[r][j]-sum[l-1][j]);
}
printf("%d",dp[(1<<m)-1]);
}
K - 狂风呼啸,此时站在你面前的是一队真正的特种兵Ⅱ
DP按已经排好的兵种数量顺序,状态压缩
#include <cstdio>
#include <iostream>
#include <climits>
using namespace std;
typedef long long ll;
const ll N=100001,M=20;
ll n,a[N],cnt[M],w[M][M],dp[1<<M],t;
int main(){
scanf("%lld",&n);
for (ll i=1;i<=n;i++){
scanf("%lld",&a[i]);
a[i]--;
}
for (ll i=1;i<=n;i++){
cnt[a[i]]++;
for (ll j=0;j<M;j++) w[j][a[i]]+=cnt[j];
}
for (ll i=1;i<(1<<M);i++)
{
dp[i]=1LL<<62;
for (ll j=0;j<M;j++)
if (i&(1<<j)){
t=0;
for (ll k=0;k<M;k++)
if ((i&(1<<k))&&(j!=k))
t+=w[j][k];
dp[i]=min(dp[i],dp[i-(1<<j)]+t);
}
}
printf("%lld",dp[(1<<M)-1]);
}
L - 保护团长
DP按序列顺序,减少冗余状态,矩阵快速幂
令平衡因子=左括号数-右扩号数
可以证明如果转移中平衡因子超过\(2n\),则可以通过调换使得平衡因子下降的序列在前边,所以平衡因子超过\(2n\)的状态不会对答案所求的最小代价造成影响
\(f[i][j]=min\{f[i-1][j-1]+a[i],f[i-1][j+1]+b[i]\}\)
这个线性的递推式可以使用矩阵乘法进行解决
我们只需要维护边长为\(2n+1\)的矩阵进行快速幂即可
#include <cstdio>
#include <iostream>
using namespace std;
typedef long long ll;
const ll N=21,inf=1LL<<62;
ll n,m,a[N],b[N];
struct Mat{
ll s[N*2][N*2];
ll* operator [](ll x){
return s[x];
}
friend Mat operator *(Mat a,Mat b){
Mat s;
for (ll i=0;i<=n*2;i++)
for (ll j=0;j<=n*2;j++)
s[i][j]=inf;
for (ll i=0;i<=n*2;i++)
for (ll j=0;j<=n*2;j++)
for (ll k=0;k<=n*2;k++)
if (a[i][k]!=inf&&b[k][j]!=inf)
s[i][j]=min(s[i][j],a[i][k]+b[k][j]);
return s;
}
Mat(){
for (ll i=0;i<N*2;i++)
for (ll j=0;j<N*2;j++)
s[i][j]=inf;
}
}c[N],sum;
Mat I(){
Mat s;
for (ll i=0;i<=n*2;i++)
for (ll j=0;j<=n*2;j++)
s[i][i]=(i==j);
return s;
}
Mat power(Mat a,ll b){
Mat s=I();
while(b){
if (b&1) s=s*a;
a=a*a;
b/=2;
}
return s;
}
void print(Mat a){
for (ll i=0;i<=n*2;i++){
for (ll j=0;j<=n*2;j++)
printf("%lld ",a[i][j]);
printf("\n");
}
printf("\n");
}
int main(){
scanf("%lld%lld",&n,&m);
for (ll i=0;i<n;i++) scanf("%lld",&a[i]);
for (ll i=0;i<n;i++) scanf("%lld",&b[i]);
for (ll i=0;i<n;i++)
for (ll j=0;j<n*2;j++)
c[i][j][j+1]=a[i],c[i][j+1][j]=b[i];
sum=I();
for (ll i=0;i<n;i++) sum=sum*c[i];
sum=power(sum,m);
printf("%lld",sum[0][0]);
}
M - 我来变身
N - 猛男修树
DP按树上递归顺序
#include <cstdio>
#include <cstring>
#include <climits>
#include <iostream>
using namespace std;
const int N=201;
int n,p,a,b,sz,head[N],f[N][N],vis[N],ans;
struct E{
int next,to;
}e[N*2];
void add(int a,int b){
sz++;
e[sz].next=head[a];
head[a]=sz;
e[sz].to=b;
}
void dfs1(int x){
vis[x]=1;
f[x][1]=0;
for (int i=head[x];i;i=e[i].next){
int v=e[i].to;
if (!vis[v]){
dfs1(v);
f[x][1]++;
}
}
}
void dfs2(int x){
vis[x]=1;
for (int i=head[x];i;i=e[i].next){
int v=e[i].to;
if (!vis[v]){
dfs2(v);
for (int j=n;j>=1;j--)
for (int k=1;k<=n;k++)
if (j+k<=n)
{
f[x][j+k]=min(f[x][j+k],f[x][j]+f[v][k]-1);
}
}
}
}
int main(){
scanf("%d%d",&n,&p);
for (int i=1;i<n;i++){
scanf("%d%d",&a,&b);
add(a,b);add(b,a);
}
memset(f,0x3f,sizeof(f));
dfs1(1);
memset(vis,0,sizeof(vis));
dfs2(1);
ans=f[1][p];
for (int i=2;i<=n;i++) ans=min(ans,f[i][p]+1);
printf("%d",ans);
}
O - 臭数
DP按数位顺序
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cmath>
#define ll long long
using namespace std;
const int N=1002,mod=1e9+7;
ll f[N][10][10],w[N],ans=0,pd,lc,rc;
char l[N],r[N],ten[N];
ll count(char *x){
if (x[0]==0) return 0;
int cnt=strlen(x);
for (int i=0;i<cnt;i++) w[cnt-i]=x[i]-'0';
return cnt;
}
ll check(char *x)
{
memset(f,0,sizeof(f));
ll cnt=count(x),s=0;pd=1;
if (cnt==0) return 0;
if (cnt==1) return w[cnt];
for (int j=0;j<=9;j++)
for (int k=1;k<=9;k++)
if (j!=k)
if (k<w[cnt]||(k==w[cnt]&&j<w[cnt-1])) f[cnt-1][j][k]=1;
if (w[cnt]==w[cnt-1]) pd=0;
for (int i=cnt-2;i>=1;i--)
{
for (int j=0;j<=9;j++)
{
for (int k=0;k<=9;k++)
{
for (int p=0;p<=9;p++)
{
if (j!=k&&k!=p&&j!=p)
{
if (k==w[i+1]&&p==w[i+2]&&j<w[i]) f[i][j][k]=(f[i][j][k]+pd)%mod;
f[i][j][k]=(f[i][j][k]+f[i+1][k][p])%mod;
}
}
}
}
if (w[i]==w[i+1]||w[i+1]==w[i+2]||w[i]==w[i+2]) pd=0;
}
for (int i=0;i<=9;i++)
for (int j=0;j<=9;j++)
s=(s+f[1][i][j])%mod;
s=(s+pd)%mod;
return s;
}
int main()
{
cin>>l>>r;
for (int i=0;i<strlen(l);i++) lc=(lc*10+l[i]-'0')%mod;
for (int i=0;i<strlen(r);i++) rc=(rc*10+r[i]-'0')%mod;
for (int i=0;i<N;i++) ten[i]='9';
for (int i=count(l);i<=count(r)-1;i++) ans=(ans+check(ten+N-i))%mod;
ans=(ans+check(r))%mod;
ans=(ans-check(l)+mod)%mod;
ans=(ans+pd)%mod;
cout<<(rc-lc+1-ans+2*mod)%mod<<endl;
}
P - 近战法师暴击好累
DP按区间长度顺序
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
const int N=501;
int n,w[N],f[N][N],a[N][N];
int main(){
scanf("%d",&n);
for (int i=1;i<=n;i++) scanf("%d",&w[i]);
memset(f,0x7f,sizeof(f));
for (int i=1;i<=n;i++){
f[i][i]=1;
a[i][i]=w[i];
}
for (int i=2;i<=n;i++)
for (int j=1;j+i-1<=n;j++)
for (int k=j;k<j+i-1;k++){
if (f[j][k]==1&&f[k+1][j+i-1]==1&&a[j][k]==a[k+1][j+i-1])
f[j][j+i-1]=1,a[j][j+i-1]=a[j][k]+1;
f[j][j+i-1]=min(f[j][j+i-1],f[j][k]+f[k+1][j+i-1]);
}
printf("%d",f[1][n]);
}
Q - 藤原货运
DP按时间顺序,单调栈优化
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <climits>
using namespace std;
typedef long long ll;
typedef pair<ll,ll> P;
const ll N=500001;
ll n,a,b,mn,sz,f[N],pt;
P s[N],p[N],w[N];
struct Stack{
ll l,r;
P t[N];
Stack(){
l=0,r=1;
}
void pop(ll x){
while(l<=r&&t[l].first<x) l++;
}
void push(ll x,ll v){
while(l<=r&&t[r].second>=v) r--;
t[++r]=P(x,v);
}
bool isempty(){
return l>r;
}
ll bottom(){
return t[l].second;
}
}sta;
int main(){
scanf("%lld",&n);
for (ll i=1;i<=n;i++){
scanf("%lld%lld",&a,&b);
s[i]=P(a,b);
}
for (ll i=1;i<=n;i++){
while(sz&&p[sz].second<=s[i].second) sz--;
p[++sz]=s[i];
}
mn=1LL<<62;
for (ll i=1;i<=sz;i++){
a=p[i].first,b=p[i].second;
f[i]=1LL<<62;
sta.push(f[i-1],f[i-1]+2*b);
sta.pop(a);
if (!sta.isempty()){
f[i]=min(f[i],sta.bottom());
}
w[i]=P(f[i-1],2*b);
while(pt<=i&&w[i].first<a) mn=min(mn,w[i].second),pt++;
f[i]=min(f[i],a+mn);
}
printf("%lld",f[sz]);
}
R - 君の名前は?唱:“达拉崩吧斑得贝迪卜多比鲁翁” Ⅱ
DP按物品顺序,二进制优化
#include <cstdio>
#include <iostream>
using namespace std;
typedef long long ll;
const ll N=10001,M=6;
ll n,m,c,v[N],w[N],d[N],x[M],y[M],z[M],f[N],ans,t;
int main(){
scanf("%lld%lld%lld",&n,&m,&c);
for (ll i=1;i<=n;i++) scanf("%lld%lld%lld",&v[i],&w[i],&d[i]);
for (ll i=1;i<=m;i++) scanf("%lld%lld%lld",&x[i],&y[i],&z[i]);
for (ll i=1;i<=n;i++)
for (ll j=0;j<20&&d[i];j++){
t=((1<<j)>d[i])?d[i]:(1<<j);
d[i]-=t;
for (ll k=c;k>=t*v[i];k--)
f[k]=max(f[k],f[k-t*v[i]]+t*w[i]);
}
for (ll i=1;i<=m;i++)
for (ll j=c;j>=0;j--)
for (ll k=0;k<=j;k++)
f[j]=max(f[j],f[j-k]+x[i]*k*k+y[i]*k+z[i]);
ans=f[c];
printf("%lld",ans);
}