8月18日考试 题解(动态规划+并查集+分块+提交答案题)

考的比昨天好,至少做对了一道题。T3很多白给部分分没看,感觉巨亏。

T1 蓝蓝的棋盘

题目大意:给定一个长度为$n$的序列。两个人轮流移动棋子,棋子一开始在$0$。每次可以移动的范围为$[p+1,\min (p+m,n)]$。两个人都按最优策略走。最优策略指自己的分减去对方的分最大。求先手的人的分数减去后手的人的分数。

一开始没看懂样例。看明白以后大力DP,刚了两个小时也没做出来。发现正着DP有后效性,因为你现在的策略可能不是最优的,但可能影响到后面的策略。不如倒着DP。

设$f[i]$表示从$i$出发到$n$的最大差值。假设另一个人的位置在$j$。两个人的差值是相反的,即在$j$的差值对于$i$的是$-f[j]$;因为从$j$转移过来,所以还要加上$a[j]$。所以有转移方程:$f[i]=\max\limits_{i<j\leq \min (n,i+m)} (f[i],a[j]-f[j])$

发现是一个线性DP的式子,很容易想到用单调队列优化。然而我单调队列写挂了QAQ,于是用的线段树。常数大了一点。

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int inf=1e18;
int f[500005],n,m,a[500005];
int maxx[500005*4];
inline int read()
{
    int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
    while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
inline void update(int index,int l,int r,int pos,int x)
{
    if (l==r) {maxx[index]=x;return;}
    int mid=(l+r)>>1;
    if (pos<=mid) update(index*2,l,mid,pos,x);
    else update(index*2+1,mid+1,r,pos,x);
    maxx[index]=max(maxx[index*2],maxx[index*2+1]);
}
inline int query(int index,int l,int r,int ql,int qr)
{
    if (ql<=l&&r<=qr) return maxx[index];
    int mid=(l+r)>>1,res=-inf;
    if (ql<=mid) res=max(res,query(index*2,l,mid,ql,qr));
    if (qr>mid) res=max(res,query(index*2+1,mid+1,r,ql,qr));
    return res;
}
signed main()
{
    n=read();m=read();
    for (int i=1;i<=n;i++) a[i]=read();
    f[n]=0;f[n-1]=a[n];
    update(1,1,n,n,a[n]-f[n]);
    update(1,1,n,n-1,a[n-1]-f[n-1]);
    for (int i=n-2;i>=0;i--)
    {
        f[i]=query(1,1,n,i+1,min(i+m,n));
        update(1,1,n,i,a[i]-f[i]);
    }
    printf("%lld",f[0]);
    return 0;
}

T2 淘淘的集合

 给定$n$个集合$a[1],a[2],\cdots ,a[n]$,初始值为$0$。有4种操作:

1.合并$x,y$两个集合。如果$x,y$同属一个集合则忽略。

2.将$x$所在集合内所有数加上$y$。

3.$a[l]$到$a[r]$变为$0$。

4.求$\sum\limits_{i=l}^r a[i]$。

保证事件4的$\sum (r-l+1)\leq 10^7$。

暴力的$n^2$做法不难想到。暴力合并然后直接查询即可。上面的保证意味着我们可以暴力查询答案。

正解则是并查集+分块。并查集维护的是集合的合并:每次合并集合时采用启发式合并,可以证明复杂度是$n\log n$的。对于2操作,我们对每个集合打一个tag,合并的时候就把tag加到原数中去,查询就是原数的值加上tag的值。对于3操作我们分块维护:给每个位置打上时间戳标记,这样就变成了区间加。进行4操作时现在的值减去打上时间戳标记时的值就是答案。

对于1操作和2操作我们离线操作。时间复杂度$O(10^7+\sqrt n)$。

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=200005;
int val[maxn],tag[maxn],fa[maxn];//set
vector<int> v[maxn];
int a[maxn],tim[maxn],block,tot;//block
int n,m,op[maxn],opx[maxn],opy[maxn],ans[maxn];
struct node
{
    int id,t,x;
    bool operator < (node w) const{return t<w.t;}
}tmp[maxn*5];vector<node> g[maxn];
int cntq,lq;
struct Node
{
    int id,t,l,r;
    bool operator <(Node w) const{return t<w.t;}
}q[maxn];//lq2=cntq
inline int read()
{
    int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
    while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
inline int getpos(int x){return (x-1)/block+1;}
inline void build()
{
    block=sqrt(n);tot=n/block;
    if (n%block) tot++;
    for (int i=1;i<=tot;i++) tim[i]=-1;
}
inline void rebuild(int id)
{
    if (tim[id]==-1) return;
    for (int i=(id-1)*block+1;i<=min(id*block,n);i++) 
        a[i]=tim[id];
    tim[id]=-1;
}
inline void update(int l,int r,int v)
{
    int id;
    if (getpos(l)==getpos(r))
    {
        id=getpos(l);rebuild(id);
        for (int i=l;i<=r;i++) a[i]=v;
        return;
    }
    rebuild(id=getpos(l));
    for (int i=l;i<=min(id*block,n);i++) a[i]=v;
    rebuild(id=getpos(r));
    for (int i=(id-1)*block+1;i<=r;i++) a[i]=v;
    for (int i=getpos(l)+1;i<id;i++) tim[i]=v;
}
inline int ask(int x){return tim[getpos(x)]==-1?a[x]:tim[getpos(x)];}
inline int find(int x)
{
    if (x==fa[x]) return x;
    return fa[x]=find(fa[x]);
}
inline void merge(int x,int y)
{
    x=find(x),y=find(y);
    if (x==y) return;
    if (v[x].size()<v[y].size()) swap(x,y);fa[y]=x;
    for (int i=0;i<v[y].size();i++) val[v[y][i]]+=tag[y];tag[y]=0;
    for (int i=0;i<v[y].size();i++)
        val[v[y][i]]-=tag[x],v[x].push_back(v[y][i]);
    v[y].clear();
}
inline int query(int x){return val[x]+tag[find(x)];}
inline void add(int x,int v){x=find(x);tag[x]+=v;}
signed main()
{
    n=read();m=read();
    build();
    for (int i=1;i<=n;i++) fa[i]=i,v[i].push_back(i);
    for (int i=1;i<=m;i++)
    {
        op[i]=read(),opx[i]=read(),opy[i]=read();
        if (op[i]==3) update(opx[i],opy[i],i);
        if (op[i]==4)
        {
            cntq++;
            q[cntq].id=cntq;q[cntq].t=i;
            q[cntq].l=opx[i];q[cntq].r=opy[i];
            for (int j=opx[i];j<=opy[i];j++)
            {
                if (!ask(j)) continue;
                lq++;
                tmp[lq].id=cntq,tmp[lq].t=ask(j),tmp[lq].x=j;
                g[tmp[lq].t].push_back(tmp[lq]);
            }
        }
    }
    sort(q+1,q+cntq+1);
    int now=1;
    for (int i=1;i<=m;i++)
    {
        if (op[i]==1) merge(opx[i],opy[i]);
        if (op[i]==2) add(opx[i],opy[i]);
        for (int j=0;j<g[i].size();j++) 
            ans[g[i][j].id]-=query(g[i][j].x);
        while(now<=cntq&&q[now].t<=i)
        {
            for (int j=q[now].l;j<=q[now].r;j++)
                ans[q[now].id]+=query(j);
            now++;
        }
    }
    for (int i=1;i<=cntq;i++) printf("%lld\n",ans[i]);
    return 0;
}

T3 蓝蓝的程序

提交答案的SB题。

Subtask1:给定一个长度为$n$的序列。求$(\sum\limits_{i=1}^n a[i])+\max \limits_{1\leq i\leq n}(a[i])+\min \limits_{1\leq i\leq n}(a[i])$

硬搞即可。

Subtask2:给定一个矩阵。求二维前缀和之和。

考虑每个元素的贡献。类比二维树状数组,每个元素能产生$(n-i+1)*(n-j+1)$次贡献。$n^2$硬搞即可。

Subtask3:给定一个矩阵,求二维前缀和的前缀和之和。

还是考虑每个元素的贡献。设$f(n)=\frac{n(n+1)}{2}$,则每个元素能产生$f(n-i+1)*f(n-j+1)$次贡献。

Subtask4:直接输出$n$。很显然。

Subtask5:求$\sum\limits_{i=1}^n i$

高中数学必修五

Subtask6:求$\sum\limits_{i=1}^n i^2$

之前背过通项公式:$\frac{n(n+1)(2n+1)}{6}$

Subtask7:求$\sum\limits_{i=1}^n i^3$

$\frac{n^2(n+1)^2}{4}$

Subtask8:求$\sum\limits_{i=1}^n i^4$

通项公式:$\frac{n^8-1}{15}$。当然也可以拉格朗日插值做(我不会QAQ

Subtask9:给定一个$01$矩阵。问不含$0$的子矩阵个数。

单调栈。

Subtask10:给定一个$01$矩阵,问不含$0$的子矩阵的面积之和。

单调栈。上述两个子任务我都不会QAQ

上述子任务有70分是可做的,考试的时候没看这个题太TM亏了。

posted @ 2020-08-18 15:46  我亦如此向往  阅读(187)  评论(0编辑  收藏  举报