NOIP2017_D2T3 列队

题目略(各大OJ上都有)

题解:

我们可以观察到一个规律,就是每次移动都只会改变一行和最后一列(最后一列是关键),于是我们就可以把这个方阵划分成这样

每个划分的区间只需要快速支持这两个操作:

  1. 从中查询一个位置的值并删除
  2. 向末尾添加一个值

因为最多只有q个添加操作,q有 3 × 1 0 5 3 \times 10^5 3×105而一个序列原本就有 3 × 1 0 5 3 \times 10^5 3×105这么长,用高级数据结构显然太浪费,动态开点的线段树就可以搞定,两个操作一次时间复杂度都是 l o g ( n + q ) log (n+q) log(n+q),新开的空间也是 l o g ( n + q ) log (n+q) log(n+q),而一次查询2个或4个操作就可以搞定。

总时间复杂度: O ( n l o g ( n + q ) ) O( nlog(n+q) ) O(nlog(n+q)),空间同

那么这个动态开点线段树怎么写呢:

把区间总范围设为 1 − ( m a x ( n , m ) + q ) 1-(max(n,m)+q) 1(max(n,m)+q),每个节点都设一个值cal表示节点区间包含的有效节点个数,叶节点再加一个val表示值,在这里,删除是指把此位置标为无效节点,并没有从序列中抹除,而2操作则是直接把最后一个有效点后的位置更新成要添加的值。

删除时直接在线段树上做二分,把叶节点cal改成0,上面的路径更新一下即可。

设一个Xi数组表示第i个序列的Xi位置还没有被插入过,每次插入时就可以精准定位,查完后Xi++,虽然不能保证和当前最后一个有效点紧挨着,但一定不会超出范围,这样效果是一样的。

下图展示了样例的第一个操作的过程:

有一步最为关键:如果一开始一个一个地更新初始方阵的每一个值,空间时间都要炸飞。可是不更新的话,前n或m-1个位置的值都不同,后q个位置又都是无效位置,没法操作。这时我们可以这样想,一个节点没有被建立,说明这个区间范围内没有被更新过,而我们知道节点所在序列,具体区间范围,又知道初始状态,那么我们就可以直接计算出有关这个节点的所有数据,而如果节点已被建立,直接使用节点数据即可。

还有就是此题卡常严重,非树状数组的童鞋们最好还是小心一点~~

代码:

#include <iostream>
#include <stdio.h>
#include <algorithm>
using namespace std;
const int N=3e5+5;
struct Node{
    Node* l;
    Node* r;
    int cal;
    long long val;
    Node(){
        cal=val=0;
        l=r=NULL;
    }
};
int n,m,q,Len,Xi[N];
Node* Root[N];

Node* NewNode(){
    return new Node();
}

inline int Cal(Node* o,int l,int r,int Limit)
{
    if(o!=NULL)
        return o->cal;
    r=min(Limit,r);
    return (l>r?0:r-l+1);
}

void update(Node* o,int l,int r,int k,long long val0,int Limit)
{
    o->cal++;
    if(l==r){
        o->val=val0;
        return;
    }
    int mid=(l+r)>>1,l_cal=Cal(o->l,l,mid,Limit);
    if(k<=mid)
    {
        if(o->l==NULL){
            o->l=NewNode();
            o->l->cal=l_cal;
        }
        update(o->l,l,mid,k,val0,Limit);
    }
    else
    {
        if(o->r==NULL){
            o->r=NewNode();
            o->r->cal=o->cal-1-l_cal;
        }
        update(o->r,mid+1,r,k,val0,Limit);
    }
}

long long query(Node* o,int l,int r,int k,int Limit,int id)
{
    o->cal--;
    if(l==r){
        if(!o->val)
        {
            long long id0=id,m0=m,l0=l;
            o->val=(id0?((id0-1ll)*m0+l0):(l0*m0));
        }
        return o->val;
    }
    int mid=(l+r)>>1,l_cal=Cal(o->l,l,mid,Limit);	
    if(k<=l_cal)
    {
        if(o->l==NULL){
            o->l=NewNode();
            o->l->cal=l_cal;
        }
        return query(o->l,l,mid,k,Limit,id);
    }
    else
    {
        if(o->r==NULL){
            o->r=NewNode();
            o->r->cal=o->cal+1-l_cal;
        }
        return query(o->r,mid+1,r,k-l_cal,Limit,id);
    }
}

int main()
{
    scanf("%d%d%d",&n,&m,&q);
    for(int i=0;i<=n;i++)
    {
        Root[i]=NewNode();
        Root[i]->cal=m-1;
        Xi[i]=m;
    }
    Root[0]->cal=n;
    Xi[0]=n+1;
    Len=max(n,m)+q;	
    while(q--)
    {
        int x,y;
        long long ans;
        scanf("%d%d",&x,&y);
        ans=query(Root[0],1,Len,x,n,0);
        if(y!=m)
        {
            long long ans1=query(Root[x],1,Len,y,m-1,x);
            update(Root[x],1,Len,Xi[x],ans,m-1);
            Xi[x]++;
            ans=ans1;
        }
        printf("%lld\n",ans);
        update(Root[0],1,Len,Xi[0],ans,n);
        Xi[0]++;
    }
    return 0;
}

你好!感谢你的提问。根据你的要求,我将为你解答关于NOIP2017普及t3问题的内容。 在这道题目中,我们需要解决一个关于棋盘的问题。题目描述了一个n×m的棋盘,每个格子上有一个非负整数。开始时,小明站在左上角的格子上,他可以向右或向下移动一步,每次移动的代价为目标格子上的数值。我们需要找到一条从左上角到右下角的路径,使得移动的总代价最小。 解决这个问题的一个常见的方法是使用动态规划(Dynamic Programming)。我们可以创建一个二维数组dp,其中dp[i][j]表示从起点到达坐标为(i, j)的格子时的最小代价。然后,我们可以按照从左上角到右下角的顺序依次计算dp数组的值。 具体的计算方法如下: 1. 首先,我们可以初始化dp数组的第一行和第一列,即dp[0][j]和dp[i][0],它们表示从起点到达第一行和第一列的格子时的最小代价。初始化的方法是累加前面的格子的代价。 2. 接下来,我们可以使用一个双重循环,从(1, 1)开始遍历整个棋盘。对于每个格子(i, j),我们可以选择从上方格子(i-1, j)或左方格子(i, j-1)中选择一个代价较小的路径,并加上当前格子的代价。即dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]。 3. 最后,当我们计算完dp数组的所有值后,dp[n-1][m-1]即为从起点到达右下角的格子时的最小代价。 这样,我们就可以得到从左上角到右下角的最小代价。希望能对你的问题有所帮助!如果你还有其他问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值