劳动节CF题总结

注:题意略

(vjudge上有中文翻译)

(可能会更新,因为我可能会再做点题)

感觉 CF 题对思维的训练意义很大,并且部分题对码力的训练也相当不错(WA自闭了)

CF1458E Nim Shortcuts

这就是那个WA自闭的题,最后都开始骗数据了

我们把状态 \((x,y)\) 看成平面直角坐标系上的点 \((x,y)\) 。那必输的点集相当于直线 \(y=x\) 上的点。我们记先手必输的点集为 \(S\)

考虑加入了某个“捷径” \((x_i,y_i)\),它会对 \(S\) 产生什么影响呢?

我们知道,有这样一个显然的结论

如果一个游戏状态的所有后继状态都是必胜状态,那么该状态为必输状态;

反之,只要有一个后继状态是必输状态,那么该状态为必胜状态

对于某个捷径 \((x_i,y_i)\),显然,它会让同一行、同一列的,x/y 坐标值比它大的所有点变成先手必胜。

显然,因为先手可以一步走到 \((x_i,y_i)\) 取胜

我们用 WPS 表格画出这个状态,红色格子表示先手必输,绿色格子表示先手必胜;红色格子里标 “S” 的表示它要么是 \((0,0)\) (最左上角),要么是一个捷径。

根据上面那个“显然的结论”,我们可以递推出整张图所有格子的胜负性

(我当时就是这么打草稿的)

我们发现,加入一个新的捷径以后,由于它强行让它所在那一行、列的后面变成了绿色,原本一条红色的斜线的一部分被迫偏移了,向下偏了一格。体现在坐标系上,即 x/y 坐标被迫 +1。

如果您有兴致,可以多加几个捷径画一画(像我一样),然后就会发现,多几个捷径情况还是差不多的,只是这条线可能会偏移不止一格(因为捷径很多)。

那现在我们大概知道这个“必输线”怎么搞了:默认斜着走,如果同一行/列的前面有捷径就偏移;这样走得到的轨迹就是“必输线”,即集合 \(S\)

这里我们判断它是否会“偏移”,要用 map 记下每一行的捷径中最小的 \(x\) 坐标,和每一列的捷径中最小的 \(y\) 坐标,如果比当前点的 x/y 坐标来的小,就要偏移一下。

由于坐标范围是 \(1e9\) 的,所以每次要 lower_bound 找一下,看跳到哪里,而不能一个一个跳。

如何存储这个红色的“必输线”呢?我们发现,它是由若干个斜率为 \(1\) 的线段组成的,只是截距(\(y-x\))不同。而且它至多只会有 \(n\) 段,因为一个捷径最多让它多一段,所以总共最多也不会超过 \(n\) 段。

于是我们可以按截距分类。对于每一种截距,我们存 \(x\) 的取值范围。范围可能是多段区间并起来,所以我们要用一个 vector 来存。截距的范围很广,但是很少,还要再来一个 map。设 node 表示一个区间,那么我们使用的数据结构为:map<int,vector<node> >。由于总共不超过 \(n\) 段,所有 vectorsize 的和也不会超过 \(n\)。这样预处理,时间复杂度 \(O(n\log n)\),空间复杂度 \(O(n)\)

我是在刘汝佳的蓝书中见到这样的阴间结构的

此处为啥不离散化:会影响直线方程的计算

那对于每次询问 \((x,y)\),把 \(y-x\) 的截距拿出来,然后得到一个 vector<node ,判断一下 \(x\) 是否在里面即可,这个东西也很好二分解决。于是每次询问就是 \(\log\) 的。注意判一下初始位置就在捷径的情况。

一个细节:每次跳的时候,先处理“偏移”,再开始跳,因为你可能从 \((1,1)\) 就开始偏移。

另一个细节:while 循环处理偏移的时候,不要先搞 \(x\) 再搞 \(y\),详情见代码。

一个注意:注意一下常数

代码:

#include <bits/stdc++.h>
using namespace std;
namespace Flandre_Scarlet
{
    #define N 100005
    #define INF 0x3f3f3f3f
    #define F(i,l,r) for(int i=l;i<=r;++i)
    #define D(i,r,l) for(int i=r;i>=l;--i)
    #define Fs(i,l,r,c) for(int i=l;i<=r;c)
    #define Ds(i,r,l,c) for(int i=r;i>=l;c)
    #define MEM(x,a) memset(x,a,sizeof(x))
    #define FK(x) MEM(x,0)
    #define Tra(i,u) for(int i=G.st(u),v=G.to(i);~i;i=G.nx(i),v=G.to(i))
    #define p_b push_back
    #define sz(a) ((int)a.size())
    #define all(a) a.begin(),a.end()
    #define iter(a,p) (a.begin()+p)
    #define PUT(a,n) F(i,1,n) printf("%d ",a[i]); puts("");
    int I() {char c=getchar(); int x=0; int f=1; while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar(); while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return ((f==1)?x:-x);}
    template <typename T> void Rd(T& arg){arg=I();}
    template <typename T,typename...Types> void Rd(T& arg,Types&...args){arg=I(); Rd(args...);}
    void RA(int *p,int n) {F(i,1,n) *p=I(),++p;}

    int n,m;
    struct node{int x,y;}; // node(x,y) 有多种含义, 有时代表一个点, 有时代表一个区间
    bool operator<(node a,node b) {return a.x<b.x or (a.x==b.x and a.y<b.y);}
    node s[N];
    void Input()
    {
        Rd(n,m);
        s[0]=(node){0,0}; 
        F(i,1,n) 
        {
            s[i]=(node){I(),I()};
            if (s[i].x<=5 and s[i].y<=5)
            {
                printf("(%d,%d)\n",s[i].x,s[i].y);
            }
        }
    }
    map<int,vector<node> > range;
    map<node,bool> have;
    map<int,int> firx,firy;
    // 先手败的点一定在若干条 y-x=k 的线段上
    // range[k]: 对于所有 y-x=k 的线段, x 的取值范围
    // 因为可能是若干个区间的并, 所以用 vector
    int xp[N],yp[N]; // 记录下所有的x,y坐标(方便跳)
    bool in_range(vector<node> &R,int p) // 判断 p 是否在 R 所代表的范围中
    {
        if (R.empty()) return false;
        if (R[0].x>p) return false;
        if (R.back().y<p) return false;
        node las=*(upper_bound(all(R),(node){p,INF})-1);
        // 最后一个左端点<=p的区间
        if (las.y>=p) return true;
        return false;
    }
    void Sakuya()
    {
        F(i,1,n) // 预处理: 每行第一个,每列第一个
        {
            xp[i]=s[i].x,yp[i]=s[i].y;
            have[s[i]]=1;
            if (firy.find(s[i].x)==firy.end())
            {
                firy[s[i].x]=s[i].y;
            }
            else
            {
                firy[s[i].x]=min(firy[s[i].x],s[i].y);
            }

            if (firx.find(s[i].y)==firx.end())
            {
                firx[s[i].y]=s[i].x;
            }
            else
            {
                firx[s[i].y]=min(firx[s[i].y],s[i].x);
            }
        }

        xp[n+1]=INF; yp[n+1]=INF; // 哨兵
        sort(xp+1,xp+n+1); sort(yp+1,yp+n+1);
        node cur=(node){0,0}; // 从 (0,0) 开始
        while(1) // 均摊 O(nlogn)
        {
            while(1) 
            {
                bool flag=0;
                if (firy.count(cur.x) and firy[cur.x]<=cur.y) ++cur.x,flag=1;
                if (firx.count(cur.y) and firx[cur.y]<=cur.x) ++cur.y,flag=1;
                if (!flag) break;
            }
            /*
            注:
            while(要偏x) ++cur.x;
            while(要偏y) ++cur.y;
            这样的写法是错误的, 因为处理完偏y之后, y坐标增加, 可能又有x要偏移了
            比如这样的数据 (test#6 的一部分)
            捷径: (1,0) (2,2) (3,0) (4,2) (5,2)
            */ 

            int b=cur.y-cur.x;
            if (!range.count(b))
            {
                range[b]=vector<node>();
            } // 初始化一下, 防止阴间问题
            int nx=*upper_bound(xp+1,xp+n+2,cur.x);
            int ny=*upper_bound(yp+1,yp+n+2,cur.y);
            if (nx>1e9 and ny>1e9) // 最后一段
            {
                range[b].p_b((node){cur.x,INF}); 
                // 只要我们不停下来, x坐标就会不断延伸, 所以说, 不要停下来啊 (无端)
                // 容易发现它确实是不断延伸的
                break;
            }
            else
            {
                int d=min(nx-cur.x,ny-cur.y);
                range[b].p_b((node){cur.x,cur.x+d-1});
                cur.x+=d; cur.y+=d;
            }
        }

        F(i,1,m)
        {
            int x=I(),y=I();
            if (have[(node){x,y}] or (x==0 and y==0))
            {
                puts("LOSE");
                continue;
            }
            int b=y-x;
            if (in_range(range[b],x))
            {
                puts("LOSE");
            }
            else
            {
                puts("WIN");
            }
        }
    }
    void IsMyWife()
    {
        Input();
        Sakuya();
    }
}
#undef int //long long
int main()
{
    freopen("in.txt","r",stdin);
    Flandre_Scarlet::IsMyWife();
    getchar();
    return 0;
}

CF1500D Tiles for Bathroom

有一个显然的思路,肯定是钦定一个角,求出最长能扩展的边长 \(L\),然后对答案数组 \([1,L]\) 做区间 \(+1\)

一开始我们可能会想到枚举左上角,然后像manacher一样继承一下右下角

但是我们发现它继承不起来,每次继承就要重新算一下出现次数的数组,并不能直接继承一部分。

(做到这我睡着了)

当你发现枚举一边不好用,那可以试试枚举另一边。(我醒来之后突然想到)考虑枚举右端点。注意到 \(q\) 很小,只要维护最近的 \(q\)颜色 即可(换句话说要对相同颜色的去个重)。这样就可以把上面,左边,左上三个位置的颜色(一共 \(3q\) 个)合并一下得到当前离的“最近”的 \(q\) 个颜色。

直观上感觉很对,然而位置的坐标有两个数,取什么样的“最近”?

回到原问题,我们要用正方形来盖住这些颜色。所以,“距离”就是"至少需要多少边长的正方形才能覆盖“。由于 \(x,y\) 两个坐标都要覆盖到,所以是 \(x,y\) 坐标的差取 \(\max\)。那其实就是切比雪夫距离。

然后我们要求“最长扩展长度”,其实可以求"最短不合法长度“然后-1。”最短不合法长度“就是第 \(q+1\) 近的颜色的切比雪夫距离。

直接求第 \(q\) 近的颜色会有问题,就比如第 \(q\) 近的颜色切比雪夫距离为 \(k\),但是可能 \(q+1\) 近的颜色切比雪夫距离也是 \(k\)。此时如果覆盖一个边长为 \(k\) 的正方形,那就会覆盖到第 \(q+1\) 近的那个,从而导致存在多于 \(q\) 种颜色。

其实样例 \(2\) 就是一个反例,如果直接取第 \(q+1\) 近的,那你第四个数会输出 \(1\)

然后就随便做了。复杂度是 \(O(n^2q\log q)\),稍微卡下常。

代码

#include <bits/stdc++.h>
using namespace std;
namespace Flandre_Scarlet
{
    #define N 1502
    #define F(i,l,r) for(int i=l;i<=r;++i)
    #define D(i,r,l) for(int i=r;i>=l;--i)
    #define Fs(i,l,r,c) for(int i=l;i<=r;c)
    #define Ds(i,r,l,c) for(int i=r;i>=l;c)
    #define MEM(x,a) memset(x,a,sizeof(x))
    #define FK(x) MEM(x,0)
    #define Tra(i,u) for(int i=G.st(u),v=G.to(i);~i;i=G.nx(i),v=G.to(i))
    #define p_b push_back
    #define sz(a) ((int)a.size())
    #define all(a) a.begin(),a.end()
    #define iter(a,p) (a.begin()+p)
    #define PUT(a,n) F(i,1,n) printf("%d ",a[i]); puts("");
    int I() {char c=getchar(); int x=0; int f=1; while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar(); while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return ((f==1)?x:-x);}
    template <typename T> void Rd(T& arg){arg=I();}
    template <typename T,typename...Types> void Rd(T& arg,Types&...args){arg=I(); Rd(args...);}
    void RA(int *p,int n) {F(i,1,n) *p=I(),++p;}
    int n,q;
    int a[N][N];
    void Input()
    {
        Rd(n,q);
        F(i,1,n) F(j,1,n)
        {
            a[i][j]=I();
        }
    }
    struct node
    {
        int x,y,d; // d: 切比雪夫距离
    };
    bool cmp_d(node a,node b) {return a.d<b.d;}
    vector<node> rec[N][N];
    node t[35]; // 这个t用vector会TLE
    int mxlen[N][N];
    inline int cheb(int x1,int y1,int x2,int y2) {return max(abs(x1-x2),abs(y1-y2))+1;}
    bool vis[N*N]; // 记下颜色, 每次用完要清理一下
    int cf[N]; // 差分数组
    void Sakuya()
    {
        FK(vis); FK(t);
        F(i,1,n) F(j,1,n)
        {
            node cur=(node){i,j,1};
            int tpos=0;
            t[++tpos]=cur;
            if (j>1) for(auto x:rec[i][j-1]) t[++tpos]=((node){x.x,x.y,cheb(x.x,x.y,i,j)});
            if (i>1) for(auto x:rec[i-1][j]) t[++tpos]=((node){x.x,x.y,cheb(x.x,x.y,i,j)});
            if (i>1 and j>1) for(auto x:rec[i-1][j-1]) t[++tpos]=((node){x.x,x.y,cheb(x.x,x.y,i,j)});
            sort(t+1,t+tpos+1,cmp_d);
            int tot=0;
            mxlen[i][j]=114514;
            // 注:如果颜色一共都没有 q+1 个,那么说明随便取都可以,此时的答案是 min(i,j)
            // 为了合并两种情况省去一些讨论, 我们令 mxlen 的初始值为 INF,然后最后和 min(i,j) 取 min
            // (这个INF臭死力)
            rec[i][j].clear();
            F(ii,1,tpos)
            {
                node x=t[ii];
                int c=a[x.x][x.y];
                if (!vis[c]) // 对颜色去重
                {
                    vis[c]=1;
                    rec[i][j].p_b(x);
                    ++tot;
                    if (tot==q+1) // 取到第 q+1 个
                    {
                        mxlen[i][j]=x.d-1;
                        break;
                    }
                }
            }
            mxlen[i][j]=min({mxlen[i][j],i,j});

            F(ii,1,tpos) 
            {
                node x=t[ii];
                vis[a[x.x][x.y]]=0;
            }
        }
        F(i,1,n) F(j,1,n)
        {
            ++cf[0]; --cf[mxlen[i][j]+1];
        }
        F(i,1,n) cf[i]+=cf[i-1];
        F(i,1,n) printf("%d\n",cf[i]);
    }
    void IsMyWife()
    {
        Input();
        Sakuya();
    }
}
#undef int //long long
int main()
{
    Flandre_Scarlet::IsMyWife();
    getchar();
    return 0;
}
posted @ 2021-05-02 21:35  Flandre-Zhu  阅读(70)  评论(0编辑  收藏  举报