Test 2022.10.14

今天是 水但爆零 专场

T1 硬币

一道背包的典型题,可惜考场上面总觉得是个结论题,于是就在一直打表,到最后喜提0pts

题意

给出n个硬币,然后输出如果去掉第i(i[1,n])个硬币,最多能凑出多少种面值。

分析

首先一眼就知道这道题肯定不是对每个硬币去掉的情况都跑一遍单独的算法,而是可以一趟就算出所有答案,或许会考虑这是一个,但考试时想到了背包,但是没有想到竟然可以对背包进行这样的操作,或者说我对dp的定义一开始就是错的,因为本题中对于一个面值,很容易觉得对于两个相同的面值,他们是可以在一定程度上互相替代的,所以我一直在想如何去解决去重的问题,但实际上正解并没有考虑这么多。

考虑对于两种面值相同但是编号不同的情况,如1,1,3,我们认为定义dp[j]为前组成面值j的方案数,且我们认为两个1,1是彼此独立的,即组成4的方案有两种,那么转移方程就很轻松地写出来了:

dp[j]=dp[j]+dp[ja[i]](i[1,n],ja[i]0)

这里的dp可能和正常的背包思路并不太一样,其实理解这个dp方程最好的方式就是手玩几组样例:

比如n=4,a[]=1,1,2,4

这里一共会有8dp值:

在考虑n=1,a[1]=1时,程序给dp[1](0)+dp[0](1)

在考虑n=2,a[2]=1时,程序给dp[2](0)+dp[1](1),dp[1](1)+dp[0](1)

在考虑n=3,a[3]=2时,程序给dp[4](0)+dp[2](1),dp[3](0)+dp[1](2),dp[2](1)+dp[0](1)

在考虑n=4,a[4]=4时,程序给dp[8](0)+dp[4](1),dp[7](0)+dp[3](2),dp[6](0)+dp[2](2),dp[5](0)+dp[1](2),dp[4](1)+dp[0](1)

这样就很清楚地看到,我们每引入一种币值k,就能让dp[i]dp[i+k]产生dp[i]的贡献,即通过加入k使得已有的面值i变成i+k,很明显的是,无论我们枚举硬币的顺序如何,只要枚举完了n枚硬币,最后的答案是不会受枚举顺序影响的

然后我们需要正序枚举i循环n次,然后一定是倒叙枚举j,最后求出总的方案数。

所以我们为什么要在这里把相同面值的币独立出来算呢?

首先当我们跑完这个dp之后,求出的方案数一定是把前n枚硬币都考虑完了的方案数、

for(int i=1;i<=n;++i)
for(int j=sum;j>=a[i];--j)
if(dp[j-a[i]])dp[j]+=dp[j-a[i]];

那么在去掉第i枚硬币之后,我们需要对所有被这枚硬币贡献过的dp减去相应的贡献值。还是拿刚刚的样例手玩一下,发现一定是要正序减的,因为硬币的顺序对结果并不影响,我们不妨假设要去掉的硬币是最后一枚,那么我们现在想要得到的即是加入最后一枚硬币之前的所有dp值。

那么对于当前已经处理完了n种硬币得到的dp序列中的任意一个dp值,我们想要把它还原到上一层对应位置的值,就应该用它当前的值减去上一层通过a[i]贡献它的值,这里强烈建议自己列个表理解一下贡献的方式。

剩下也没什么细节了

Code

#include<bits/stdc++.h>
#define R register
using namespace std;
int n,a[110],dp[300010];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch > '9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
int main()
{
R int sum=0;
n=read();
for(R int i=1;i<=n;++i)a[i]=read(),sum+=a[i];
dp[0]=1;
for(R int i=1;i<=n;++i)
for(R int j=sum;j>=a[i];--j)
if(dp[j-a[i]])dp[j]+=dp[j-a[i]];
for(R int i=1;i<=n;++i)
{
int ans=0;
for(R int j=a[i];j<=sum;++j) if(dp[j-a[i]])dp[j]-=dp[j-a[i]];
for(R int j =1;j<=sum;++j)if(dp[j])ans++;
printf("%d\n",ans);
for(R int j=sum;j>=a[i];--j)if(dp[j-a[i]])dp[j]+=dp[j-a[i]];
}
}

T2 序列

看错题了,喜提0pts,就是一个枚举左端点然后暴力扩展的O(n2)贪心算法,加一点小小的剪枝,应该就可以卡过水的数据,但是正解还是优先队列。

Code

T3 小Y的炮

非常明显的一个贪心和dp的结合,但单调队列是我没想到的,属实高

分析

首先非常明显的的是,对于一座任意高度的山,我们对他的一次轰击,应该是尽量使用能打到它的且威力最大的炮,如果对于两个炮i,j,有AiAjandDiDj,那么j的存在就是没有意义的了。

单调队列预处理

考虑去掉每一个没有意义的炮,我们使用单调队列。先对输入的炮的信息按照A升序排列(当A相等的时候按照D升序,保证所有没用的炮都能被去掉),然后往单调队列中加入第一个炮,对于之后的每一个炮,我们都popback之前单调队列中所有威力小于它的炮(此时A一定是升序的),那么这样处理出来的一定是“当前最优的炮序列了”。我们使用反证法来证明,假设对于任意一个H,当前处理出来的能打它的炮不是最优的,即还有AD的炮存在,根据我们的预处理方法,如果真的存在,那么当前炮是一定不会保留的,与实际矛盾了,所以当前是最优的。

dp预处理

说是dp其实也不是dp,就是一个简简单单的递推,但是的的确确用到了dp的思想。我们定义dp[H]为把高度为H的一座山轰平所需的最少弹药数,很容易想到要利用我们刚刚单调队列处理出来的炮序列来维护这个dp,假设当前对于打这座山最优的选择是炮p,那么一定会在炮p把当前这座山打到一定高度(即炮p1能打的高度)的时候,最优的选择变成了p1,因为p1的威力一定是比p高的,这个贪心非常容易证明,就不用赘述了。还要注意的是当前这门炮最多把这座山打成atk[p1].Aatk[p].D的高度,这时最优选择就一定会变成p1了。

言下之意,对于每一个高度区间,我们只预处理[atk[p].Aatk[p].D,atk[p].A]区间内的dp值,就能保证我下一个区间计算的时候访问的dp值是一定被计算过的了

而转移就很简单了,对于当前高度H,把它打到更低的高度区间所需的次数是t=(Hatk[i1].A)/(atk[i].D)

计算答案

这个过程和dp几乎是一样的,就不多赘述了,dp懂了计算答案自然就能懂了,当然还有一些边界条件需要好好处理的

Code

T4 统计损失

这也是一个手玩样例的题,但是同样也可以用淀粉质等高级算法(虽然我想到了 但是不会),你很容易就想到要用树形dp来解决,可是dp的转移确实是一个很大的问题。但是理解这道题dp的关键——是乘法分配率

分析

考虑对于任意一个点为根的子树,利用淀粉质的思想,把边分成经过根节点和不经过根节点两类,对于经过根节点的我们可以直接计算,不经过根节点的我们递归进子树处理(这条路径就一定经过某个根节点),我们考虑如何把路径计算完全。

定义dp[i]为以i为根的树内,路径两个端点只分布于当前树的同一个子树中(其中一个为根节点)的路径方案,比如一棵以x为根,y,z为子节点的树,dp[x]=x+x×y+x×z

首先对于一个节点,我们算的只是经过它的路径:

  • 首先对于一个叶节点x,他的总答案就是它本身valx,没有任何问题

  • 对于一个规模稍微大一点的,以x为根,有y,z两个叶节点的树,答案是:

    valx+valy+valz+valx×valy+valx×valz+valx×valy×valz

  • 对于一个规模更大一点的,以o为根,两个形如x,y,z的子树为子树的树,答案是:

    (valo×valx+valo×valx×valy+valo×valx×valz)×(vala+vala×valb+vala×valc)

利用乘法分配律展开第三个答案,你会发现刚好对应每一条合法的路径,为什么呢?实际上乘法分配率就是一种对应匹配的过程,加上我们对dp的定义,我们一定能保证我们会把当前子树内的dp路径统计完全,全靠下面两句:

ans=ans+dp[x]*dp[v];
dp[x]=dp[x]+w[x]*dp[v];

手玩一下样例便能很好的体会这个过程,先上代码

Code

posted @   Hanggoash  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
动态线条
动态线条end
点击右上角即可分享
微信分享提示