CSP-S 十一集训Day2
贪心
GDOI 2018
给定长度为n的a序列 a 的范围是[0,m-1] 然后每次可以选择一个区间+1, -1 然后 求 将所有的数字 在模m意义 下 变成那个0 所需要的最少次
这个题目 就是添加了取模意义下的最小次数。 不妨先思考一下。
然后考虑 不模 m的情况 这就是一道非常经典的问题 比如校oj的那道例题 数列差分问题 而这个题目要求我们 最后把所有的数字变成 0
然后我们考虑 对原序列进行差分 求出b序列 从第1项到第n项 我们只需要求出来 所有正数和p 和负数和q 那么最后的答案就是 max(p,,-q)
那么在取模意义下 我们不仅可以变成0 还可以把所有数字 变成m 所有现在考虑 将那些数字 变成0 哪些数字变成m 会使得整个序列修改次数最少
然后 显然 我们可以猜到一个贪心 就是对于 小的数字 肯定是尽量满足 变成0 较大的数字尽量变成m 所以我们 不妨 将b序列 进行从小到达排序
考虑找到一个位置 x st$b_1,b_2....b_x$ 全部调整为0 然后对于[x+1,n]的部分 变成 m 我们贪心的在做这个事情 会发现 这样寻找到的就是最小次数
也就是求$max(\sum_{i=1}^{x} b_i,\sum_{i=x+1}^{n} m-b_i)$ 最小
没找到题目链接qwq;
删数游戏:
键盘输入一个高精度的正整数N(不超过250位) ,去掉其中任意k个数字后剩下的数字按原左右次序将组成一个新的正整数。
编程对给定的N和k,寻找一种方案使得剩下的数字组成的新数最小。
存在一个贪心 从左到右便利 整个数字 那么当子串是单调递增的时候 删除最后一个数字 当下降的时候 删除第一个单调递减区间的第一个数字 这样的策略是
每次删除 我们都st 留下的数字尽可能的小
#include<bits/stdc++.h> using namespace std; char c[266]; int s,i,j; int main() { scanf("%s%d",c,&s); int len=strlen(c); while(s--) { for(i=0;i<=len-2;i++) { if(c[i]>c[i+1]) { for(j=i;j<=len-2;j++) c[j]=c[j+1]; break; } } len--; } i=0; while(i<=len-1&&c[i]=='0') i++; if(i==len) printf("0"); else { for(j=i;j<=len-1;j++) printf("%c",c[j]); } return 0; }
取数游戏 :给出n个正整数 需要把他们连在一排 组成一个最大的多位整数:
看到这题目样例 我们总是会猜出很多 假的贪心 所以这个时候 我们不妨考虑 两个数字a b
我们是能够比较出来 a放在b之前大 还是 b放在a之前最大 每两个数字 进行比较 我们就能 得到每个数字的相对大小 排序就行了
其实怎么贪心 真的不是看出来 我认为是通过简单地分析 然后证明推导出来的一种不会更差的策略
简单分享一下 怎么分析这种例题
对于 z 有两个儿子 x y 考虑在什么情况下 删除x 比删除 y更优秀 更不容易超过M
那么 删除x后 z的价值变成了 $a[z]+a[x]+son[z]+son[x]-1$...... ①
删除y后 z的价值变成了 $a[z]+a[y]+son[z]+son[y]-1$........②
当①<② 的时候 $a[x]+son[x]<a[y]+son[y]$.....③
所以我们 删除 x 而不删除 y 的条件就是③
那么我们就知道 贪心的时候要怎么排序了 然后考虑 能删除的时候我们一定要删除 而不是留着删除他的父亲
因为这样会让他的父亲的父亲 的权值增加 不利于我们删除 而删除当前的儿子 就不会产生这一影响
#include<bits/stdc++.h> using namespace std; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} x*=f; } const int N=2000010; int n,m,idx,x,ans,s[N],son[N],l[N],r[N],dfn[N]; inline bool mycmp(int x,int y) { return s[x]+son[x]<s[y]+son[y]; } inline void dfs(int x) { if(!son[x]) return ; for(int i=l[x];i<=r[x];i++) dfs(dfn[i]); sort(dfn+l[x],dfn+r[x]+1,mycmp); for(int i=l[x];i<=r[x];i++) { int y=dfn[i]; if(s[y]+s[x]+son[y]+son[x]-1<=m) { s[x]+=s[y]; son[x]+=son[y]-1; ans++; } else break; } } int main() { read(n); read(m); for(int i=1;i<=n;i++) { read(s[i]); } for(int i=1;i<=n;i++) { read(son[i]); l[i]=idx+1; r[i]=idx+son[i]; for(int j=1;j<=son[i];j++) { read(x); x++; dfn[++idx]=x; } } dfs(1); printf("%d\n",ans); return 0; }
color a tree
一颗树有 n 个节点,这些节点被标号为:1,2,3…n,每个节点 i 都有一个权值 A[i]。
现在要把这棵树的节点全部染色,染色的规则是:
根节点R可以随时被染色;对于其他节点,在被染色之前它的父亲节点必须已经染上了色。
每次染色的代价为T*A[i],其中T代表当前是第几次染色。
求把这棵树染色的最小总代价。
我觉得看到这个题目 就会存在一个假贪心 即类似于构造哈夫曼树的思路 让权值大的尽可能先取出来 但是显然会存在一些反例
当一些节点权值很小的节点连接这一些 权值很大 的节点 并且 有一个 权值很大的节点 没有儿子节点
此时 刚才得到思路就不正确了 所以 我们不妨从中考虑 一个比较好的性质 存在一个性质 满足 在树中 除了 根节点以外的权值最大的点
一定会在其父亲被染色之后 立即被染色 于是我们可以确定的是 树中权值最大的点 和他父亲的染色 是连续的 所以我们考虑 把这样的
节点 合并起来 新节点的 权值 就是两个节点权值的平均值 我们考虑 证明一下 这个贪心的正确性 假设 我们存在 三个权值 x y z的点
假设 已知$x y$ 的操作 是连续的 那么 此时存在两个染色方式 即
先染色 $x y$ 再染色 $z$ 此时代价是 $x+2y+3z$
或者 先染色 z 再染色 $x$ 和 $y$ 此时代价是$z+2x+3y$
我们比较两个代价的大小 把两个式子加上$(z-y)$ 然后 除以2 然后得到
代价1 $(x+y)/(2+2*x)$ 代价2 $z+2*((x+y)/2)$
这就恰好等同于 对$(x+y)/2$ 和 $z$ 的先后染色顺序
所以等同于 对$x y z$ 三个点染色 最优 次序 可以等同于$(x+y)/2 和 z$ 的顺序
所以对于多个合并的 点的权值 变成所有点的权值和除以该点包含的原始点数
根据先前提到的 性质 我们可以不断 弹出等效最大的点 p 然后让他与父节点fa 合并 所以对于合并前p 和 fa中的点的染色顺序是相对确定的
所以 我们将p第一个点 和 fa中的最后一个后紧接着被染色 并且把这个顺序保存下来 然后我们不断执行这样的操作
所以我们 最终将整个树变成了 一个点 而这个点的先后染色顺序 我们已经确定
按照这个顺序 求一下代价即可
#include<bits/stdc++.h> using namespace std; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} x*=f; } const int N=1100; int n,x,y,root,ans; struct gg { int siz,fa,v; double avg; }a[N]; inline int get() { double avg=0; int res=-1; for(int i=1;i<=n;i++) { if(i!=root&&a[i].avg>avg) { avg=a[i].avg; res=i; } } return res; } int main() { read(n); read(root); ans=0; memset(a,0,sizeof(&a)); for(int i=1;i<=n;i++) { read(a[i].v); a[i].avg=a[i].v; a[i].siz=1; ans+=a[i].v; } for(int i=0;i<n-1;i++) { read(x); read(y); a[y].fa=x; } for(int i=0;i<n-1;i++) { int p=get(); int father=a[p].fa; ans+=a[father].siz*a[p].v; a[p].avg=-1; for(int j=1;j<=n;j++) { if(a[j].fa==p) a[j].fa=father; } a[father].v+=a[p].v; a[father].siz+=a[p].siz; a[father].avg=(double)a[father].v/a[father].siz; } cout<<ans<<endl; return 0; }
二分
聪明的监制员 二分+前缀和
我们考虑到 产生贡献的产品 是和 W 有关的 所以我们不妨枚举一个 W 显然这个W 会使得 答案具有可二分性
因为是求一个绝对值 所以我们在二分的过程中 记录一下这个最小值 并且我们每次记录一下 满足条件的 个数的前缀和 以及 V的前缀和 就可以了
#include<bits/stdc++.h> using namespace std; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1;ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} x*=f; } typedef long long ll; const int N=210000; ll n,m,s,minn,maxx,w[N],v[N],sum[N],num[N],ans=1e18,res,lef[N],righ[N]; inline bool check(int x) { ll y=0; memset(num,0,sizeof(num)); memset(sum,0,sizeof(sum)); for(int i=1;i<=n;i++) { if(w[i]>=x) num[i]=num[i-1]+1,sum[i]=sum[i-1]+v[i]; else num[i]=num[i-1],sum[i]=sum[i-1]; } for(int i=1;i<=m;i++) { y+=(num[righ[i]]-num[lef[i]-1])*(sum[righ[i]]-sum[lef[i]-1]); } res=llabs(s-y); //cout<<y<<' '<<res<<endl; if(s>y) return false; else return true; } int main() { //freopen("1.in","r",stdin); read(n); read(m); read(s); minn=1e18+7,maxx=-1;//数据范围开到ll,minn初始值不够大 第二次wa for(int i=1;i<=n;i++) { read(w[i]); read(v[i]); minn=min(minn,w[i]); maxx=max(maxx,w[i]); } for(int i=1;i<=m;i++) { read(lef[i]); read(righ[i]); } int l=minn-2,r=maxx+2;//为了判断maxx也不合法的情况; while(l<=r) { int mid=(l+r)>>1; if(check(mid)) l=mid+1; else r=mid-1; if(res<ans) ans=res; } cout<<ans<<endl; return 0; }