一道背包的典型题,可惜考场上面总觉得是个结论题,于是就在一直打表,到最后喜提
给出个硬币,然后输出如果去掉第个硬币,最多能凑出多少种面值。
首先一眼就知道这道题肯定不是对每个硬币去掉的情况都跑一遍单独的算法,而是可以一趟就算出所有答案,或许会考虑这是一个,但考试时想到了背包,但是没有想到竟然可以对背包进行这样的操作,或者说我对的定义一开始就是错的,因为本题中对于一个面值,很容易觉得对于两个相同的面值,他们是可以在一定程度上互相替代的,所以我一直在想如何去解决去重的问题,但实际上正解并没有考虑这么多。
考虑对于两种面值相同但是编号不同的情况,如,我们认为定义为前组成面值的方案数,且我们认为两个是彼此独立的,即组成的方案有两种,那么转移方程就很轻松地写出来了:
这里的可能和正常的背包思路并不太一样,其实理解这个方程最好的方式就是手玩几组样例:
比如
这里一共会有种值:
在考虑时,程序给
在考虑时,程序给
在考虑时,程序给
在考虑时,程序给
这样就很清楚地看到,我们每引入一种币值,就能让对产生的贡献,即通过加入使得已有的面值变成,很明显的是,无论我们枚举硬币的顺序如何,只要枚举完了枚硬币,最后的答案是不会受枚举顺序影响的
然后我们需要正序枚举循环次,然后一定是倒叙枚举,最后求出总的方案数。
所以我们为什么要在这里把相同面值的币独立出来算呢?
首先当我们跑完这个之后,求出的方案数一定是把前枚硬币都考虑完了的方案数、
| 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]]; |
那么在去掉第枚硬币之后,我们需要对所有被这枚硬币贡献过的减去相应的贡献值。还是拿刚刚的样例手玩一下,发现一定是要正序减的,因为硬币的顺序对结果并不影响,我们不妨假设要去掉的硬币是最后一枚,那么我们现在想要得到的即是加入最后一枚硬币之前的所有值。
那么对于当前已经处理完了种硬币得到的序列中的任意一个值,我们想要把它还原到上一层对应位置的值,就应该用它当前的值减去上一层通过贡献它的值,这里强烈建议自己列个表理解一下贡献的方式。
剩下也没什么细节了
| #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]]; |
| } |
| } |
看错题了,喜提,就是一个枚举左端点然后暴力扩展的贪心算法,加一点小小的剪枝,应该就可以卡过水的数据,但是正解还是优先队列。
| #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; |
| } |
非常明显的一个贪心和的结合,但单调队列是我没想到的,属实高
首先非常明显的的是,对于一座任意高度的山,我们对他的一次轰击,应该是尽量使用能打到它的且威力最大的炮,如果对于两个炮,有,那么的存在就是没有意义的了。
考虑去掉每一个没有意义的炮,我们使用单调队列。先对输入的炮的信息按照升序排列(当相等的时候按照升序,保证所有没用的炮都能被去掉),然后往单调队列中加入第一个炮,对于之后的每一个炮,我们都之前单调队列中所有威力小于它的炮(此时一定是升序的),那么这样处理出来的一定是“当前最优的炮序列了”。我们使用反证法来证明,假设对于任意一个,当前处理出来的能打它的炮不是最优的,即还有的炮存在,根据我们的预处理方法,如果真的存在,那么当前炮是一定不会保留的,与实际矛盾了,所以当前是最优的。
说是其实也不是,就是一个简简单单的递推,但是的的确确用到了的思想。我们定义为把高度为的一座山轰平所需的最少弹药数,很容易想到要利用我们刚刚单调队列处理出来的炮序列来维护这个,假设当前对于打这座山最优的选择是炮,那么一定会在炮把当前这座山打到一定高度(即炮能打的高度)的时候,最优的选择变成了,因为的威力一定是比高的,这个贪心非常容易证明,就不用赘述了。还要注意的是当前这门炮最多把这座山打成的高度,这时最优选择就一定会变成了。
言下之意,对于每一个高度区间,我们只预处理区间内的值,就能保证我下一个区间计算的时候访问的值是一定被计算过的了
而转移就很简单了,对于当前高度,把它打到更低的高度区间所需的次数是
这个过程和几乎是一样的,就不多赘述了,懂了计算答案自然就能懂了,当然还有一些边界条件需要好好处理的
| #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; |
| } |
这也是一个手玩样例的题,但是同样也可以用淀粉质等高级算法(虽然我想到了 但是不会),你很容易就想到要用树形来解决,可是的转移确实是一个很大的问题。但是理解这道题的关键——是乘法分配率
考虑对于任意一个点为根的子树,利用淀粉质的思想,把边分成经过根节点和不经过根节点两类,对于经过根节点的我们可以直接计算,不经过根节点的我们递归进子树处理(这条路径就一定经过某个根节点),我们考虑如何把路径计算完全。
定义为以为根的树内,路径两个端点只分布于当前树的同一个子树中(其中一个为根节点)的路径方案,比如一棵以为根,为子节点的树,
首先对于一个节点,我们算的只是经过它的路径:
-
首先对于一个叶节点,他的总答案就是它本身,没有任何问题
-
对于一个规模稍微大一点的,以为根,有两个叶节点的树,答案是:
-
对于一个规模更大一点的,以为根,两个形如的子树为子树的树,答案是:
利用乘法分配律展开第三个答案,你会发现刚好对应每一条合法的路径,为什么呢?实际上乘法分配率就是一种对应匹配的过程,加上我们对的定义,我们一定能保证我们会把当前子树内的路径统计完全,全靠下面两句:
| ans=ans+dp[x]*dp[v]; |
| dp[x]=dp[x]+w[x]*dp[v]; |
手玩一下样例便能很好的体会这个过程,先上代码
| #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; |
| } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效