模拟31 考试总结
前面咕的太多了,来一篇吧
考试经过
开题发现都是序列题,十分不可做
T1思考各种贪心,各种hack,干了2h,最后全排列跑路。。。
T2没想太多,冲了个树状数组,这次没有RE,跑对了样例,T3在样例错的情况下成功被误导。。。
最终得分:10,几乎爆零
T1由于写了一般觉得读错题了,于是将20分暴力成功改错;T2爆零了,原因是没有想到\(战 神 三 问\)
你换行了吗
你取模了吗
你返回值了吗
真就一个\(int\)的\(add\)函数被我放那了……
我真傻,真的
T1.Game
首先把题读对:只管自己的得分,对面的不用管!
我们想如果不管字典序的最优答案:
对方每张牌,如果自己能把它吃掉,就选自己有的之中比他大的最小的一个,否则选择当前最小的手牌打出,一定有最大贡献
这样直接出来的是字典序最小,考虑能不能让字典序更大
简单思路是枚举当前打出的牌,对以后的牌按照以上的贪心验证,不难发现有单调性,所以可以二分当前打出的牌的点数,可是这样要\(n^2\)以上,肯定不行,关键在于怎么验证
然后神仙做法就来了:线段树
开一棵权值线段树,维护\(sum_a\),\(sum_b\),\(sum\)三个值
假设对方的牌是\(A\)集合,自己的是\(B\)集合,\(sum_a\)代表区间内对方牌的数量,\(sum_b\)同理,而\(sum\)就是在这个区间里我通过规划出牌顺序获得的最大收益
核心操作是\(pushup\)函数
inline void qi(int id)
{
int p=min(a[id*2].sa,a[id*2+1].sb);
a[id].sum=a[id*2].sum+a[id*2+1].sum+p;
a[id].sa=a[id*2].sa+a[id*2+1].sa-p;
a[id].sb=a[id*2].sb+a[id*2+1].sb-p;
}
会发现贡献的来源:我用大牌去打小牌
以及一个显然的性质:右区间所有数都比左边大
于是你的贡献就是用你右儿子里的牌去打左儿子里的对方牌,取最小值
牌打了就没了,于是你做了贡献之后左右合并的\(sum_a\),\(sum_b\)都要减去贡献值
很妙,不是吗?
这样每次只要查询1节点的\(sum\)就能得到当前贡献
然后考虑二分,方法很简单:每次在线段树里把对应的值减掉,看看答案是否变差,然后再加回来就行
再维护一个set代表当前手牌,看现在能不能把对面吃掉
如果能吃掉就在\(a_i+1\)和最大手牌之间二分,否则在1到\(a_i\)之间二分
不用担心会二分到不合法答案,因为他本来不在手牌里,一定不优
复杂度\(nlog^2n\),略微卡常,沈队有一个\(log\)做法,不过感觉只有大神适用。。。
#include <bits/stdc++.h>
using namespace std;
#define R register
#define gc if(++ip==ie)fread(ip=buf,1,SZ,stdin)
const int SZ=1<<19;
char buf[SZ],*ie=buf+SZ,*ip=ie-1;
inline int read(){
gc;while(*ip<'-')gc;
bool f=*ip=='-';if(f)gc;
int x=*ip&15;gc;
while(*ip>'-'){x*=10;x+=*ip&15;gc;}
return f?-x:x;
}
const int N=100050;
int b[N],c[N],ans;
multiset <int> s;
inline void de(int x)
{
s.erase(s.find(x));
}
struct Tree{
int sa,sb,sum;
}a[4*N];
inline void qi(int id)
{
int p=min(a[id*2].sa,a[id*2+1].sb);
a[id].sum=a[id*2].sum+a[id*2+1].sum+p;
a[id].sa=a[id*2].sa+a[id*2+1].sa-p;
a[id].sb=a[id*2].sb+a[id*2+1].sb-p;
}
inline void add(int id,int l,int r,int p,int v,int op)
{
if(l==r)
{
if(op==1)a[id].sa+=v;
if(op==2)a[id].sb+=v;
return;
}
int mid=(l+r)>>1;
if(p<=mid)add(id*2,l,mid,p,v,op);
else add(id*2+1,mid+1,r,p,v,op);
qi(id);
}
signed main()
{
int n;cin>>n;
for(int i=1;i<=n;i++)
{
b[i]=read();
add(1,1,n,b[i],1,1);
}
for(int i=1;i<=n;i++)
{
c[i]=read();
add(1,1,n,c[i],1,2);
s.insert(c[i]);
}
ans=a[1].sum;
for(R int i=1;i<=n;i++)
{
int l=b[i]+1,r=*s.rbegin(),ann=0;
if(l<r)
{
add(1,1,n,b[i],-1,1);
while(l<=r)
{
int mid=(l+r)>>1;
add(1,1,n,mid,-1,2);
if(a[1].sum+1==ans)l=mid+1,ann=mid;
else r=mid-1;
add(1,1,n,mid,1,2);
}
add(1,1,n,ann,-1,2);
printf("%d ",ann);
ans--;de(ann);
}
else if(l==r)
{
add(1,1,n,b[i],-1,1);
add(1,1,n,l,-1,2);
printf("%d ",l);
ans--;de(l);
}
else
{
l=1,r=b[i];
add(1,1,n,b[i],-1,1);
while(l<=r)
{
int mid=(l+r)>>1;
add(1,1,n,mid,-1,2);
if(a[1].sum==ans)l=mid+1,ann=mid;
else r=mid-1;
add(1,1,n,mid,1,2);
}
add(1,1,n,ann,-1,2);
printf("%d ",ann);
de(ann);
}
}
return 0;
}
T2.Time
其实很简单,发现对于最小值只能移到两边,肯定选代价小的方向移动
手模一下可以发现代价就是正着数和倒着数的逆序对数,于是一个树状数组求出来就行
每次取最小值更新答案,就没了
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=100050;
struct bit{
int b[N];
inline int lowbit(int x){return x&(-x);}
inline void add(int p,int x)
{
for(int i=p;i<=1e5+10;i+=lowbit(i))
b[i]+=x;
}
inline int get(int p)
{
int s=0;
for(int i=p;i;i-=lowbit(i))
s+=b[i];
return s;
}
inline int getlr(int l,int r){return get(r)-get(l-1);}
}x1,x2;
int a[N],s1[N],s2[N];
signed main()
{
int n;cin>>n;
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1;i<=n;i++)
{
int p=a[i];
s1[i]=x1.getlr(p+1,1e5);
x1.add(p,1);
}
for(int i=n;i>=1;i--)
{
int p=a[i];
s2[i]=x2.getlr(p+1,1e5);
x2.add(p,1);
}/**/
int ans=0;
for(int i=1;i<=n;i++)ans+=min(s1[i],s2[i]);
cout<<ans;
return 0;
}
T3.Cover
\(skyh\)大神曾经讲过:只有包含和不相交的区间一般可以转化成树上问题
的确如此,然而我们先要建树
对每个区间以左端点为第一关键字从小到大,右端点第二关键字从大到小排序,之后用一个类似单调栈实现:栈顶可以包含当前区间就建边,入栈,否则一直弹栈顶,记得先建一个虚点,包含整个区间。于是树有了,显然树形dp
考虑朴素dp:\(f_{i,j}\)代表在\(i\)的子树内,最多覆盖\(j\)层的最大贡献,就有
直接做时空双爆,考虑怎么优化
然后又是不会的东西了:维护差分表
首先你可以打表发现对于每个节点,在\(j\)递增的时候,\(f\)的差分序列单调不升
于是有一个思路:可以维护差分值,最后从大到小一直累加就是答案,省去\(j\)的一维
然后我们想怎么高效合并,由于差分值贡献只来自权值,而它又有单调性,可以用堆维护
开\(m\)个堆,每次转移把子树分别取堆顶相加到父亲的堆中,但这样还是相当与枚举\(j\),因为你要遍历子树中所有元素,所以我们再次考虑启发式合并
stl容器有一个很好的性质:对于\(swap\)操作,是交换指针,是\(O(1)\)的
所以我们预处理重儿子,每次直接用\(swap\)继承重儿子,剩下的暴力合并
具体是每次从父亲的堆中取出元素,从所有非重儿子(已经合并过了)中取堆顶相加,然后用栈什么的存起来,最后在压回去,还要把当前权值也压进去
复杂度\(nlog^2n\),稍微注意空间
代码很丑
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=300050;
struct ga{
int l,r,v,id;
}p[N];
inline bool cmp(ga aa,ga bb)
{
if(aa.l!=bb.l)return aa.l<bb.l;
return aa.r>bb.r;
}
inline bool cmp2(ga aa,ga bb)
{
return aa.id<bb.id;
}
struct node{
int from,to,next;
}a[2*N];
int head[N],mm=1;
inline void add(int x,int y)
{
a[mm].from=x;a[mm].to=y;
a[mm].next=head[x];head[x]=mm++;
}
inline bool pd(ga x,ga y)
{
if(x.l<=y.l&&x.r>=y.r)return 1;
return 0;
}
stack <ga> s;int n,m;
int size[N],son[N];
void dfs1(int x)
{
for(int i=head[x];i;i=a[i].next)
{
int y=a[i].to;
dfs1(y);
size[x]+=size[y];
if(size[y]>size[son[x]])son[x]=y;
}
size[x]++;
}
priority_queue <int> f[N];
stack <int> mem;
void dfs2(int x)
{
for(int i=head[x];i;i=a[i].next)
{
int y=a[i].to;
dfs2(y);
}
swap(f[x],f[son[x]]);
int sum=0;
while(1)
{
int ga=0;
for(int i=head[x];i;i=a[i].next)
{
int y=a[i].to;
if(y==son[x])continue;
if(f[y].size())ga+=f[y].top(),f[y].pop();
}
if(!ga)break;
if(f[x].empty())break;
mem.push(f[x].top()+ga);f[x].pop();sum++;
}
for(int i=1;i<=sum;i++)f[x].push(mem.top()),mem.pop();
f[x].push(p[x].v);
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)scanf("%lld%lld%lld",&p[i].l,&p[i].r,&p[i].v),p[i].id=i;
p[m+1].l=1;p[m+1].r=n;p[m+1].v=0;p[m+1].id=m+1;
sort(p+1,p+m+2,cmp);
s.push(p[1]);
for(int i=2;i<=m+1;i++)
{
while(!pd(s.top(),p[i]))s.pop();
add(s.top().id,p[i].id);
s.push(p[i]);
}
sort(p+1,p+m+2,cmp2);
dfs1(m+1);dfs2(m+1);
int ans=0;
for(int i=1;i<=m;i++)
{
if(f[m+1].size())ans+=f[m+1].top(),f[m+1].pop();
printf("%lld ",ans);
}
return 0;
}
还是觉得理解不太到位,这篇学长博客感觉分析更透彻
考试总结
1.首先是知识相关,线段树的奇妙用法又多了呢,还有怎么建树以及各种stl操作的相关实现 2.发现自己代码能力还是不太行,要慢慢练
3.还是懂得取舍吧,别再死磕了,这次心态比以前好,至少都冲上暴力了,但沙雕错误为啥还是这么多啊
4.要求稳,求稳,稳

浙公网安备 33010602011771号