【Study】AtCoder DP Contest 做题记录
Educational DP Contest
A - Frog 1
水题,设 为在第 个点的最小花费,容易得出
初始化 ,
B - Frog 2
同上,只是第 个是由 到 的点转移过来而已 ,复杂度是 , 无需优化。
注意数组不要越界。
C - Vacation
也是一道线性 。
设 为取第 个的 时的最大值,易得:
D - Knapsack 1
01背包问题。
设 为消耗 体积时可获得的最大价值,
01背包,倒序枚举。
E - Knapsack 2
也是一道背包问题,但发现体积非常大,没法根据体积来求最大价值。
仔细读题,发现价值不大,换个思路,也许可以根据价值来求最小体积。
设 为获得 价值时需要耗费的最小体积,则有:
初始化,要将 赋值为 ,
对于答案,我们可以从最大的价值开始枚举,当发现获得该价值时需要的体积小于等于提供的体积,则输出价值。
memset(f,INF,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++)
for(int j=MAXN;j>=v[i];j--)
f[j]=min(f[j],f[j-val[i]]+wei[i]);
for(int i=MAXN;i>=0;i--)
if(f[i]<=w)
{
printf("%d",i);
break;
}
(回顾上一道题,其实可以发现题目给的体积比较小,但是价值非常大。)
F - LCS
经典的最长公共子序列,只是这一道题要输出的时公共子序列。
设 为 串前 个字符, 串前 个字符的最长公共子序列,可以推出:
-
当 时,
-
否则
对于输出,可以设两个指针 和 分别扫 和 ,如果 , 则输出字符,并且 , ; 如果 , 说明 不在答案中, ;否则 。
for(int i=la-1;i>=0;i--)
for(int j=lb-1;j>=0;j--)
if(A[i]==B[j])
f[i][j]=max(f[i][j],f[i+1][j+1]+1);
else f[i][j]=max(f[i+1][j],f[i][j+1]);
int i=0,j=0;
while(i<la&&j<lb)
{
if(A[i]==B[j])
{
cout<<A[i];
i++;j++;
}
else if(f[i][j]==f[i+1][j]) i++;
else j++;
}
G - Longest Path
求最长的路径。
分析样例,发现最长的路径一定是从入度为 的点开始出发的,将这些点压入队列进行 。从队列里的点跑向其他点的时候将其他点的入度减 ,则剩下的图又变成了一个最长路径问题,继续将新的入度为 的点压入队列。
设 为到达第 个点时的最长路径,则
, 其中点 有一条到点 的边。
for(int i=1;i<=n;i++)
if(!ei[i])
q.push(i);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=Head[u];i;i=a[i].nex)
{
int v=a[i].t;
f[v] = max(f[u]+1,f[v]);
ans=max(ans,f[v]);
ei[v]--;
if(!ei[v]) q.push(v);
}
}
printf("%d",ans);
H - Grid 1
可以直接暴力递推,复杂度是 的。
设 为到达格 时的方案数,则有
-
如果格 不是障碍,
-
如果格 不是障碍,
初始化 , 答案为
I - Coins
基础的概率
设 为前 个硬币有 个硬币为正面时的概率,可以推出:
-
第 个硬币为正
-
第 个硬币为反
特别的 , ,
f[0][0]=1;
for(int i=1;i<=n;i++)
{
f[i][0]=f[i-1][0]*(1-p[i]);
for(int j=1;j<=i;j++)
f[i][j]=f[i-1][j]*(1-p[i])+f[i-1][j-1]*p[i];
}
for(int i=(n+1)/2;i<=n;i++)
ans+=f[n][i];
J - Sushi
期望 。
比较大,反正不能开一个 维的数组来记录。
注意到每个盘子的寿司不大于 ,因为每个盘子被选到的概率是相同的,所以我们只需要关心寿司数量为 , , 的盘子有多少个。
设 为 个盘子上有 个寿司, 格盘子上有 个寿司, 个盘子上有 个寿司,则
这个东西用记忆化搜索来写比较方便。
double Dfs(int x,int y,int z)
{
double &tmp=f[x][y][z];
if(x==0&&y==0&&z==0) return 0;
if(tmp) return tmp;
tmp=1.0*n/(x+y+z);
if(x)tmp+=Dfs(x-1,y,z)*x/(x+y+z);
if(y)tmp+=Dfs(x+1,y-1,z)*y/(x+y+z);
if(z)tmp+=Dfs(x,y+1,z-1)*z/(x+y+z);
return tmp;
}
K - Stones
一道博弈论,设 表示当取剩 个石头的时候,甲乙分别是赢或输,当值为 的时候表示赢。那么可以得出:
(当 为 ,此时 号取 个必赢,所以 为 )
可以用记忆化搜索实现
int Dfs(int k,int now)
{
if(f[k][now]) return f[k][now];
if(!k) return 0;
for(int i=1;i<=n;i++)
if(k>=a[i]) f[k][now]|=(Dfs(k-a[i],now^1)^1);
return f[k][now];
}
L - Deque
区间 好题。
设 为选手在区间 中可以取到的最大值。
考虑转移,当甲取最左边的值时,轮到乙取剩下的区间 ,可以取到的最大值为 , 则甲可以在 中取到的最大值就为 ,其中 表示区间 的和。
则
对于取最右边的值同理。
同样用记忆化搜索比较方便。
int Dfs(int l,int r)
{
if(f[l][r]) return f[l][r];
if(l==r) return f[l][r]=a[l];
return f[l][r]=max(a[l]+(s[r]-s[l])-Dfs(l+1,r),a[r]+(s[r-1]-s[l-1])-Dfs(l,r-1));
}
M - Candies
是一道好背包。
设 为前 个小孩已经取了 个糖果的方案数,容易写出:
初始化
但是这样时间复杂度是 ,想办法优化一个
可以发现, 是从 转移过来的,其中 ,而对于 是从 转移过来的,其中 , 那我们可以其实可以用一个 来维护这个区间的值,对于下一次枚举的时候只要删掉超出区间的一个,加入新进入区间的一个就行了。
(其实这样有点类似单调队列)
for(int i=1;i<=n;i++)
{
int sum=0;
for(int j=0;j<=k;j++)
{
sum+=f[i-1][j];
if(j>a[i]) sum-=f[i-1][j-a[i]-1];
f[i][j]=(sum+mod)%mod;
}
}
N - Slimes
石子合并问题。
设 为合并区间 需要的最小花费,则:
其中 , 表示区间 的和
memset(f,INF,sizeof(f));
for(int i=1;i<=n;i++) f[i][i]=0,s[i]=s[i-1]+a[i];
for(int len=2;len<=n;len++)
for(int l=1;l+len-1<=n;l++)
{
int r=l+len-1;
for(int k=l;k<r;k++)
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
}
printf("%lld",f[1][n]);
O - Matching
求二分图的最大匹配数的方案数。
发现 非常小,应该是一道状压 。
设 为匹配了 个男生,且匹配女生的状态为 , 不难写出:
其中 中 的个数要与 对应
初始化
for(int i=1;i<(1<<n);i++)
{
int qwq=0,aaa=i;
while(aaa)
{
if(aaa&1) qwq++;
aaa>>=1;
}
v[qwq].push_back(i);
}
f[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<v[i].size();j++)
{
int s=v[i][j];
for(int k=1;k<=n;k++)
{
if(!a[i][k]) continue;
if(!(s&(1<<(k-1)))) continue;
f[i][s]+=f[i-1][s^(1<<(k-1))];
f[i][s]%=mod;
}
}
printf("%lld",f[n][(1<<n)-1]);
P - Independent Set
简单的树形
设 为节点 染为黑/白色的方案数,可得:
-
若 点为白 ,
-
若 点为黑 ,
其中 表示 , 之间有连边。
初始化
void Dfs(int u,int fa)
{
f[u][1]=f[u][0]=1;
for(int i=Head[u];i;i=a[i].nex)
{
int v=a[i].t;
if(v==fa) continue;
Dfs(v,u);
f[u][0]*=((f[v][0]+f[v][1])%mod);f[u][0]%=mod;
f[u][1]*=f[v][0];f[u][1]%=mod;
}
}
void Work()
{
Dfs(1,0);
printf("%lld",(f[1][0]+f[1][1])%mod);
}
Q - Flowers
是一道变形的 ,设 为前 的最大价值,则有
其中
这样做的时间复杂度是 ,会超时,考虑优化。
对于第 朵花,实际转移过来的只有那些 的花朵,所以我们需要维护 的区间最值, 可以用线段树进行维护。
struct Node{
int l,r,m;
}t[MAXN<<2];
void Pushup(int x)
{
t[x].m=max(t[x<<1].m,t[x<<1|1].m);
}
#define mid ((l+r)>>1)
void Build(int x,int l,int r)
{
t[x].l=l;t[x].r=r;
if(l==r) return ;
Build(x<<1,l,mid);
Build(x<<1|1,mid+1,r);
Pushup(x);
}
#undef mid
void Updata(int x,int i,int v)
{
if(t[x].l>i||t[x].r<i) return ;
if(t[x].l==t[x].r){t[x].m=v;return ;}
Updata(x<<1,i,v);
Updata(x<<1|1,i,v);
Pushup(x);
}
int Query(int x,int l,int r)
{
if(t[x].l>r||t[x].r<l) return 0;
if(l<=t[x].l&&t[x].r<=r) return t[x].m;
return max(Query(x<<1,l,r),Query(x<<1|1,l,r));
}
void Work()
{
Build(1,1,n);
for(int i=1;i<=n;i++)
{
int qwq=Query(1,1,a[i]-1);
Updata(1,a[i],b[i]+qwq);
}
printf("%lld",t[1].m);
}
R - Walk
读题,发现 特别的大,考虑用矩阵快速幂。
设 为点 到点 的用了 步方案数。
对于原图 ,如果 ,则此时 就要乘上 ,这个东西就是矩阵乘法。我们不需要再在 上多个 维,将 与自身相乘的时候每个点的值所代表的步数都是同时增加的,我们只需要将 与自身乘 次就可以得到答案。又因为这里的 很大,需要用到快速幂。
其实和这道题是一模一样的 P2886 [USACO07NOV]Cow Relays G
struct M{
long long a[51][51];
M(){memset(a,0,sizeof(a));}
};
int n;
M Mul(M a,M b)
{
M awa;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int k=1;k<=n;k++)
awa.a[i][j]+=(a.a[i][k]*b.a[k][j]%mod),awa.a[i][j]%=mod;
return awa;
}
M Power(M a,long long k)
{
M ans;
for(int i=1;i<=n;i++)
ans.a[i][i]=1;
while(k)
{
if(k&1) ans=Mul(ans,a);
a=Mul(a,a);
k>>=1;
}
return ans;
}
M qwq;
long long k,ans;
void Work()
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
qwq.a[i][j]=Read();
qwq=Power(qwq,k);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
ans+=qwq.a[i][j],ans%=mod;
printf("%lld",ans);
}
S - Digit Sum
一道基础的数位 。
利用字符串输入 ,在将每一位的数提取出来进行数位 。
int num[MAXN],n,f[MAXN][2][105];
int Dfs(int pos,int lim,int qwq)
{
int &tmp=f[pos][lim][qwq];
if(~tmp) return tmp;
if(!pos) return tmp=(qwq==0);
tmp=0;
int bg=0,fn=lim?num[pos]:9;
for(int i=bg;i<=fn;i++)
tmp+=Dfs(pos-1,i==fn&&lim,(qwq+i)%n),tmp%=mod;
return tmp%mod;
}
int Solve(char *c)
{
memset(f,-1,sizeof(f));
int lc=strlen(c),pos=lc;
for(int i=lc;i>=1;i--) num[i]=c[lc-i]-48;
return Dfs(pos,1,0)-1;
}
对于记忆化搜索后得出的答案需要减去 ,因为对于 这个东西会对答案有贡献,需要减去。
T - Permutation
神仙区间 。
设 为前 个数的排列中最后一个数为 的方案数,则有:
-
如果是 ,则
-
如果是 ,则
对于 比较好理解,对于 ,为什么可以枚举到 呢,假设有一数列
,我们推 ,此时在该数列最后加上 ,但是又重复了一个 ,我们可以将原数列中大于等于 的数都给其加上 ,在最后插入 ,此时为 ,这样子就不会重复,并且这样的数列是满足原来的限制条件的。
对于上面的转移方程,其时间复杂度是 ,需要采取优化。
我们可以发现求 那一部分的时候是可以用前缀和优化的。假如对于 ,可以发现 的一部分答案已经储存在 中,此时就可以 。对于另一条转移方程式也是同理的。
f[1][1]=1;
for(int i=2;i<=n;i++)
{
f[i][1]=0;
if(s[i-2]=='>')
for(int j=2;j<=i;j++)
f[i][j]=(f[i-1][j-1]+f[i][j-1])%mod;
else
for(int j=i-1;j>=1;j--)
f[i][j]=(f[i-1][j]+f[i][j+1])%mod;
}
for(int i=1;i<=n;i++) ans+=f[n][i],ans%=mod;
printf("%d",ans);
U - Grouping
很小,应该是一道状压 。
设 为已选取成员的状态为 时可以获得的最大值,则可以推出:
其中 , 均为 的子集,并且
现在的问题就是要如何枚举子集。
for(int s1=s;s1;s1=(s1-1)&s)
{
int s2=s^s1;
...
}
这样就可以不重不漏枚举 中的每一个子集,并且可以保证 和 中不会有同个位置的 ,时间复杂度是 。
注意对于这一道题,要枚举的是真子集。
for(int s=1;s<(1<<n);s++)
{
for(int i=1;i<=n;i++)
{
if(!(s&(1<<(i-1)))) continue;
for(int j=1;j<=i;j++)
{
if(!(s&(1<<(j-1)))) continue;
f[s]+=a[i][j];
}
}
for(int qwq=((s-1)&s);qwq;qwq=((qwq-1)&s))
f[s]=max(f[s],f[qwq]+f[s^qwq]);
}
printf("%lld",f[(1<<n)-1]);
V - Subtree
毒瘤的树形 ,需要二次扫描和换根。
设 表示从根节点(固定点)出发,将 号节点染黑,满足条件的方案数。对于 的根节点为 子树, 节点有染和不染两种情况,那么可以推出:
其中 表示 , 之中有连边。
初始化
对于根节点是这样子,那么对于其他的节点就需要考虑换根。
设 表示以 为根节点是的方案数。
显然有
假设当前新的根节点为 ,那么此时 就不会对 有答案贡献,那么
并且 之中还应该要有 的贡献,
这样就基本完成了。
但是难受的是,这里面有除法,有取模,要写逆元,但模数不是质数,这里就不能用逆元处理。
我们可以在求 的时候 处理出 的前缀积和后缀积,在求 的时候就不需要除法,只需以要除掉的东西为分界,乘上其左边的前缀积与其右边的后缀积便好。
vector<int> a[MAXN],qwq[MAXN];
vector<int> pre[MAXN],suf[MAXN];
void Dfs(int u,int fa)
{
g[u]=1;
for(int i=0;i<a[u].size();i++)
{
int v=a[u][i];
if(v==fa) continue;
qwq[u].push_back(v);
Dfs(v,u);
g[u]*=(g[v]+1);g[u]%=m;
}
pre[u].push_back(1);
for(int i=0;i<qwq[u].size();i++)
pre[u].push_back(pre[u][i]*(g[qwq[u][i]]+1)%m);
suf[u].push_back(1);
for(int i=qwq[u].size()-1,j=0;i>=0;i--,j++)
suf[u].push_back(suf[u][j]*(g[qwq[u][i]]+1)%m);
reverse(suf[u].begin(),suf[u].end());
}
void Solve(int u,int gfa)
{
f[u]=g[u]*(gfa+1)%m;
for(int i=0;i<qwq[u].size();i++)
Solve(qwq[u][i],pre[u][i]*suf[u][i+1]%m*(gfa+1)%m);
}
void Work()
{
Dfs(1,0);
Solve(1,0);
for(int i=1;i<=n;i++)
printf("%lld\n",f[i]);
}
W - Intervals
难题qwq。
一般来说,对于这种题的套路,都是要先排序的,我们可以以 为非降序进行排序。
设 为目前在第 个位置,且第 个位置填 的最大价值,可以推出:
其中
这么打会超时,考虑优化。
其实对于上面的转移本质上就是一次对区间 内的 进行转移,再一起修改,对于区间的修改我们可以想到线段树。
当 不是某个区间的右端点时,我们先需要将其赋值为 ,其中 。虽然需要 ,但是我们目前区间仍未修改,则 ,不会对前面的最大值造成影响。
当 为某个区间的右端点时,此时 已经赋值为 ,可以对区间进行修改完成方程转移。
所以这也是我们首先要将区间按 非降序进行排列。
bool cmp(const qwq&a,const qwq&b){return a.r<b.r;}
struct Node{
int l,r,v,laz;
Node(){laz=0;v=-INF;}
}t[MAXN<<2];
void Pushup(int x){t[x].v=max(t[x<<1].v,t[x<<1|1].v);}
void Pushdown(int x)
{
if(t[x].laz)
{
t[x<<1].laz+=t[x].laz;
t[x<<1].v+=t[x].laz;
t[x<<1|1].laz+=t[x].laz;
t[x<<1|1].v+=t[x].laz;
t[x].laz=0;
}
}
#define mid ((l+r)>>1)
void Build(int x,int l,int r)
{
t[x].l=l;t[x].r=r;
if(l==r) return ;
Build(x<<1,l,mid);Build(x<<1|1,mid+1,r);
}
#undef mid
void Updata(int x,int l,int r,int v,int aaa)
{
if(t[x].l>r||t[x].r<l) return ;
if(l<=t[x].l&&t[x].r<=r)
{
if(aaa)
t[x].v+=v,t[x].laz+=v;
else
t[x].v=v;
return ;
}
Pushdown(x);
Updata(x<<1,l,r,v,aaa);Updata(x<<1|1,l,r,v,aaa);
Pushup(x);
}
int Query(int x,int l,int r)
{
if(t[x].l>r||t[x].r<l) return 0;
if(l<=t[x].l&&t[x].r<=r) return t[x].v;
Pushdown(x);
return max(Query(x<<1,l,r),Query(x<<1|1,l,r));
}
void Work()
{
sort(a+1,a+m+1,cmp);
Build(1,1,n);
int awa=1;
for(int i=1;i<=n;i++)
{
Updata(1,i,i,Query(1,1,i-1),0);
while(a[awa].r==i) Updata(1,a[awa].l,a[awa].r,a[awa].v,1),awa++;
}
printf("%lld",max(0ll,t[1].v));
}
X - Tower
能感觉是一道背包问题。
设 表示重量为 最大价值,为了保证当前重量满足物体承载重量,可以写出:
其中
然后发现发现过不了样例,感觉应该先对物品进行排序。
设两物品 和 ,前面的物体重量为 ,当 放在 上时 还能承受的重量为 ,反之, 还能承受的重量为 ,我们需要使物体还能承受的质量尽量大,不妨设 ,即我们希望物体 能放在物体 的下方,物体 能尽量晚点来转移到,化简上面不等式,可得 ,所以我们就需要先依照该条件对物品进行排序,再进行背包。
bool cmp(const Node&a,const Node&b){return a.w+a.s<b.w+b.s;}
void Work()
{
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)
for(int j=a[i].s;j>=0;j--)
f[j+a[i].w]=max(f[j+a[i].w],f[j]+a[i].v);
int ans=0;
for(int i=0;i<=a[n].s+a[n].w+1;i++)
ans=max(f[i],ans);
}
Y - Grid 2
好题。
读题发现 , 都很大,不能直接像前面那道题目一样直接 求方案数。考虑用数学做法。
由组合数学的知识可以得出,当两格之间无障碍时,格 到格 的路径总数一共有 或 条。
再运用容斥的思想,若两个格之间有一个障碍的时候,起点到终点的路径总数就应该等于 减去起点到障碍的方案数乘障碍到终点的方案数。
我们可以将 看做一个障碍,将障碍离起点排列一下,设 为 到达第 个障碍时的方案数,则可以得到:
这里面有除法,记得要求逆元。
struct qwq{
int x,y;
}a[MAXN];
bool cmp(const qwq&a,const qwq&b){if(a.x==b.x)return a.y<b.y;return a.x<b.x;}
int Power(int a,int k)
{
int ans=1;
while(k)
{
if(k&1) ans*=a,ans%=mod;
a*=a,a%=mod;
k>>=1;
}
return ans%mod;
}
int fac[MAXN];
void Init()
{
fac[0]=fac[1]=1;
for(int i=2;i<=MAXN-100;i++) fac[i]=fac[i-1]*i,fac[i]%=mod;
}
int C(int a,int b)
{
return fac[a]*Power(fac[a-b],mod-2)%mod*Power(fac[b],mod-2)%mod;
}
int f[MAXN],n,h,w;
void Work()
{
Init();
a[++n].x=h;a[n].y=w;
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)
{
f[i]=C(a[i].x+a[i].y-2,a[i].x-1);
for(int j=1;j<i;j++)
f[i]-=f[j]*C(a[i].x+a[i].y-a[j].x-a[j].y,a[i].x-a[j].x)%mod,f[i]+=mod,f[i]%=mod;
}
}
Z - Frog 3
设 为跳到第 个石头上的最小花费,可得:
时间复杂度是 ,需要优化。
显然这个式子可以斜率优化。
int h[MAXN],f[MAXN],n,c;
double X(int i){return h[i];}
double Y(int i){return f[i]+h[i]*h[i];}
double K(int i,int j){return (Y(j)-Y(i))/(X(j)-X(i));}
int q[MAXN],Head,Tail=1;
void Work()
{
n=Read();c=Read();
for(int i=1;i<=n;i++) h[i]=Read();
f[1]=0;q[++Head]=1;
for(int i=2;i<=n;i++)
{
while(Head<Tail&&h[i]*2>=K(q[Head],q[Head+1])) Head++;
f[i]=f[q[Head]]+(h[q[Head]]-h[i])*(h[q[Head]]-h[i])+c;
while(Head<Tail&&K(i,q[Tail])<=K(q[Tail],q[Tail-1])) Tail--;
q[++Tail]=i;
}
printf("%lld",f[n]);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通