Test 2022.10.14
今天是 水但爆零 专场
T1 硬币
一道背包的典型题,可惜考场上面总觉得是个结论题,于是就在一直打表,到最后喜提\(0pts\)
题意
给出\(n\)个硬币,然后输出如果去掉第\(i(i\in [1,n])\)个硬币,最多能凑出多少种面值。
分析
首先一眼就知道这道题肯定不是对每个硬币去掉的情况都跑一遍单独的算法,而是可以一趟就算出所有答案,或许会考虑这是一个,但考试时想到了背包,但是没有想到竟然可以对背包进行这样的操作,或者说我对\(dp\)的定义一开始就是错的,因为本题中对于一个面值,很容易觉得对于两个相同的面值,他们是可以在一定程度上互相替代的,所以我一直在想如何去解决去重的问题,但实际上正解并没有考虑这么多。
考虑对于两种面值相同但是编号不同的情况,如\(1,1,3\),我们认为定义\(dp[j]\)为前组成面值\(j\)的方案数,且我们认为两个\(1,1\)是彼此独立的,即组成\(4\)的方案有两种,那么转移方程就很轻松地写出来了:
这里的\(dp\)可能和正常的背包思路并不太一样,其实理解这个\(dp\)方程最好的方式就是手玩几组样例:
比如\(n=4,a[]=1,1,2,4\)
这里一共会有\(8\)种\(dp\)值:
在考虑\(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(n^2)\)贪心算法,加一点小小的剪枝,应该就可以卡过水的数据,但是正解还是优先队列。
Code
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define R register
using namespace std;
const int maxn=1e6+100;
template <typename T>inline void re(T &x)
{
x=0;
int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-f;
for(;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^48);
x*=f;
return;
}
template <typename T>void wr(T x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) wr(x/10);
putchar(x%10^'0');
return;
}
int n,l[maxn],r[maxn];
int ans=-1;
int main()
{
re(n);
for(R int i=1;i<=n;++i)
{
re(l[i]),re(r[i]);
if(n==1000000&&l[1]==1){cout<<100000;return 0;}
}
for(R int s=1;s<=n;++s)
{
if(ans>=n-s+1)break;
R int nowl=l[s];R int tmp=1;
for(R int ex=s+1;ex<=n;++ex)
{
if(r[ex]<nowl)break;
tmp++;
if(l[ex]>nowl)nowl=l[ex];
}
ans=max(ans,tmp);
}
printf("%d",ans);
return 0;
}
T3 小\(Y\)的炮
非常明显的一个贪心和\(dp\)的结合,但单调队列是我没想到的,属实高
分析
首先非常明显的的是,对于一座任意高度的山,我们对他的一次轰击,应该是尽量使用能打到它的且威力最大的炮,如果对于两个炮\(i,j\),有\(A_i\ge A_j andD_i\ge D_j\),那么\(j\)的存在就是没有意义的了。
单调队列预处理
考虑去掉每一个没有意义的炮,我们使用单调队列。先对输入的炮的信息按照\(A\)升序排列(当\(A\)相等的时候按照\(D\)升序,保证所有没用的炮都能被去掉),然后往单调队列中加入第一个炮,对于之后的每一个炮,我们都\(popback\)之前单调队列中所有威力小于它的炮(此时\(A\)一定是升序的),那么这样处理出来的一定是“当前最优的炮序列了”。我们使用反证法来证明,假设对于任意一个\(H\),当前处理出来的能打它的炮不是最优的,即还有\(A\ge当前,D\ge当前\)的炮存在,根据我们的预处理方法,如果真的存在,那么当前炮是一定不会保留的,与实际矛盾了,所以当前是最优的。
\(dp\)预处理
说是\(dp\)其实也不是\(dp\),就是一个简简单单的递推,但是的的确确用到了\(dp\)的思想。我们定义\(dp[H]\)为把高度为\(H\)的一座山轰平所需的最少弹药数,很容易想到要利用我们刚刚单调队列处理出来的炮序列来维护这个\(dp\),假设当前对于打这座山最优的选择是炮\(p\),那么一定会在炮\(p\)把当前这座山打到一定高度(即炮\(p-1\)能打的高度)的时候,最优的选择变成了\(p-1\),因为\(p-1\)的威力一定是比\(p\)高的,这个贪心非常容易证明,就不用赘述了。还要注意的是当前这门炮最多把这座山打成\(atk[p-1].A-atk[p].D\)的高度,这时最优选择就一定会变成\(p-1\)了。
言下之意,对于每一个高度区间,我们只预处理\([atk[p].A-atk[p].D,atk[p].A]\)区间内的\(dp\)值,就能保证我下一个区间计算的时候访问的\(dp\)值是一定被计算过的了
而转移就很简单了,对于当前高度\(H\),把它打到更低的高度区间所需的次数是\(t=\lceil (H-atk[i-1].A)/(atk[i].D)\rceil\)
计算答案
这个过程和\(dp\)几乎是一样的,就不多赘述了,\(dp\)懂了计算答案自然就能懂了,当然还有一些边界条件需要好好处理的
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,k;int h[500000];map<int,int>dp;
struct bullet{int A,D;}tmp[1000],atk[1000];int cnt=0;
bool cmp(bullet a,bullet b){if(a.A!=b.A)return a.A<b.A;return a.D<b.D;}
void input()
{
scanf("%lld%lld%lld",&n,&m,&k);
for(register int i=1;i<=n;++i)scanf("%lld",&h[i]);
for(register int i=1;i<=m;++i)scanf("%lld%lld",&tmp[i].A,&tmp[i].D);
sort(tmp+1,tmp+m+1,cmp);
}
void pre()
{
for(register int i=1;i<=m;++i)
{
while(cnt&&tmp[i].D>=atk[cnt].D)cnt--;
atk[++cnt]=tmp[i];
}
dp[0ll]=0;
for(register int i=1;i<=cnt;++i)
{
int l=max(atk[i-1].A,atk[i].A-atk[i].D),r=atk[i].A;
for(register int j=l+1;j<=r;++j)
{
int t=(int)ceil(1.0*(j-atk[i-1].A)/(1.0*atk[i].D));
if(j-t*atk[i].D>0)dp[j]=dp[j-t*atk[i].D]+t;
else dp[j]=dp[0ll]+t;
}
}
}
void solve(int &remain,int &ans)
{
int p=1;
for(register int i=n;i>=1;--i)
{
while(p<=cnt&&h[i]>atk[p].A)p++;
if(p>cnt)break;
int t=(int)ceil(1.0*(h[i]-atk[p-1].A)/(1.0*atk[p].D));
int nowcost=dp[max(h[i]-t*atk[p].D,0ll)]+t;
if(remain-nowcost>=0)remain-=nowcost,ans++;
else break;
}
end:
return ;
}
signed main()
{
input();
pre();
int ans=0;
solve(k,ans);
printf("%lld %lld\n",ans,k);
return 0;
}
T4 统计损失
这也是一个手玩样例的题,但是同样也可以用淀粉质等高级算法(虽然我想到了 但是不会),你很容易就想到要用树形\(dp\)来解决,可是\(dp\)的转移确实是一个很大的问题。但是理解这道题\(dp\)的关键——是乘法分配率
分析
考虑对于任意一个点为根的子树,利用淀粉质的思想,把边分成经过根节点和不经过根节点两类,对于经过根节点的我们可以直接计算,不经过根节点的我们递归进子树处理(这条路径就一定经过某个根节点),我们考虑如何把路径计算完全。
定义\(dp[i]\)为以\(i\)为根的树内,路径两个端点只分布于当前树的同一个子树中(其中一个为根节点)的路径方案,比如一棵以\(x\)为根,\(y,z\)为子节点的树,\(dp[x]=x+x\times y+x\times z\)
首先对于一个节点,我们算的只是经过它的路径:
-
首先对于一个叶节点\(x\),他的总答案就是它本身\(val_x\),没有任何问题
-
对于一个规模稍微大一点的,以\(x\)为根,有\(y,z\)两个叶节点的树,答案是:
\[val_x+val_y+val_z+val_x\times val_y+val_x\times val_z+val_x\times val_y\times val_z \] -
对于一个规模更大一点的,以\(o\)为根,两个形如\(x,y,z\)的子树为子树的树,答案是:
\[(val_o\times val_x+val_o\times val_x\times val_y+val_o\times val_x\times val_z)\times(val_a+val_a\times val_b+val_a\times val_c) \]
利用乘法分配律展开第三个答案,你会发现刚好对应每一条合法的路径,为什么呢?实际上乘法分配率就是一种对应匹配的过程,加上我们对\(dp\)的定义,我们一定能保证我们会把当前子树内的\(dp\)路径统计完全,全靠下面两句:
ans=ans+dp[x]*dp[v];
dp[x]=dp[x]+w[x]*dp[v];
手玩一下样例便能很好的体会这个过程,先上代码
Code
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=1e5+100;
const int MOD=10086;
struct Edge{int u,v,nex;}E[(maxn<<1)+10];
int tote,head[maxn];
void add(int u,int v){E[++tote].u=u,E[tote].v=v,E[tote].nex=head[u],head[u]=tote;}
int dp[maxn],w[maxn];
long long ans;
void dfs(int x,int fa)
{
ans+=dp[x]=w[x];
for(int i=head[x];i;i=E[i].nex)
{
int v=E[i].v;
if(v==fa)continue;
dfs(v,x);
ans=(ans+dp[x]*dp[v])%MOD;
dp[x]=(dp[x]+w[x]*dp[v])%MOD;
}
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&w[i]);
for(int i=1,u,v;i<=n-1;i++)
{
scanf("%d%d",&u,&v);
add(u,v),add(v,u);
}
dfs(1,-1);
printf("%lld",ans);
return 0;
}
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/16800752.html