模拟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\)层的最大贡献,就有

\[f_{i,j}=max(\sum f{son_i,j},\sum f{son_i,j-1}+v_i) \]

直接做时空双爆,考虑怎么优化
然后又是不会的东西了:维护差分表
首先你可以打表发现对于每个节点,在\(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.要求稳,求稳,稳

posted @ 2021-08-06 20:48  D'A'T  阅读(47)  评论(0)    收藏  举报