首页 > 其他分享 >「LOJ6734」图上的游戏

「LOJ6734」图上的游戏

时间:2023-05-14 19:01:11浏览次数:48  
标签:std 二分 游戏 int mid back 图上 vector LOJ6734

题目

点这里看题目。


有一张 \(n\) 个点 \(m\) 条边的无自环连通无向图,点编号为 \(0\sim n-1\),边编号为 \(0\sim m-1\)。

你可以执行不超过 \(3\times 10^4\) 次以下询问:

给出一个结点 \(u\) 和一个边的子集 \(S\),交互库会告诉你是否存在一条从 \(0\) 到 \(u\) 的、只经过 \(S\) 中的边的路径。

你需要猜出这张图,也就是猜出每条边连接的两个结点的编号。

所有数据满足 \(1\le n,m\le 600\)。

分析

Subtasks 1,2

\(n,m\le 25\)。

首先处理出一个边集 \(T\),该边集中包含一棵生成树。这很容易,只需要暴力地迭代就行了,复杂度为 \(O(n^2m)\)。可以顺便求出每条树边连接了哪两个点,相当于直接确定了生成树的形态。

然后处理非树边。从叶子开始往上处理每个结点 \(u\):每次删除 \(u\) 的父边,加入一条非树边(非树边不能指向自己子树)并检查连通性,复杂度为 \(O(n^2m)\)。这样可以确定跨子树非树边。

最后还会剩下祖孙非树边。这类树边的较深结点已经确定,可以直接二分出另一个端点。

这个貌似做复杂了,不过能过就行。

Subtask 3

\(m=n-1\),图的形态为以 \(0\) 为端点的一条链。

最开始想的是“拆边分治”,但是发现拆边不便于点集的划分。

那就反过来,每次拿出来一个点 \(u\),找出它的所有祖先边,这样边就自然被划分为了两个部分。再找出 \(u\) 的祖先和子树中的点,于是点集也被划分为了两个部分。两个部分为结构相似的子问题,递归下去即可。

随机选择 \(u\) 即可得到 \(O(n\log n)\) 的期望复杂度。

Subtask 5

对于 \(0\le i<n-1\),第 \(i\) 条边连接 \(i,i+1\)。

现在,每条非树边对应一个区间。一次询问可以告诉我们某个元素是否被某个区间集合内的任何一个区间包含。

若能对于每个区间找到它最靠左的元素,就可以二分出右端点。而从左往右枚举每一个元素,并二分出哪些区间包含它,就可以找到每个区间的最左元素。复杂度为 \(O(n+m\log m)\)。

Subtask 4

\(m=n-1\)。

可以随便找一个点 \(u\),然后通过二分找出 \(u\) 的所有祖先边。

然后不会了,就去看题解了。

为了保证复杂度不会过高,我们可以反复执行该二分操作,但是二分出一条边后就不再对它进行二分了。考察我们得到的东西:将同一个 \(u\) 二分出来的所有边染成一个颜色,这样就得到了题解中所谓的“链划分”的形式。

“链划分”仅仅将每一条边分到了一个链里面,但是没有进行点的划分;划分完点之后,我们还得接着确定链内部的顺序和链之间的连接点。

首先解决点的划分,即找出“每个点属于哪条链”。考虑枚举点 \(u\):

  • 如果 \(u\) 的祖先边中还存在没有被划分到某条链中的边(可以仅保留已经入链的边,检查连通性),那么就可以找出所有这样的祖先边,并生成一条新的链。

  • 否则,我们需要找到 \(u\) 在哪一条链。对于所在链的时间进行二分即可。

这样就可以 \(O(n\log n)\) 完成边的划分和点的划分。接下来,链内部的定序就是 Subtask 3 了,总共 \(O(n\log n)\)。

最后要完成链之间的连接,也就是找到链顶的父亲。注意到,如果按照链产生的时间处理的话,我们可以得到点的一个 DFS 序。这个顺序满足“\(u\) 的祖先一定出现在 \(u\) 之前”,所以按照 DFS 序二分即可。

Remark.

首先,这个做法可以看作是题解中链做法的拓展。

其次,合我胃口的思路是:

  • 因为询问涉及的是单点和边集,所以最多只能对于边集二分

  • 对于边集二分可以帮助我们找到所有祖先边,但是每个点都找一遍太浪费时间了;

  • 如果每条边仅允许被二分到一次,观察产生的东西的结构,就得到了“链划分”这么一个东西;

Subtask 6

编号 \(<n-1\) 的边构成一棵树。

用 Subtask 4 的方法,把树建出来先。

仍然考虑像 Subtask 5 一样,枚举点 \(u\),删掉它的父边,然后二分出哪些点连接了两个连通块。但是为了避免二分到子树内的边,也为了处理非祖孙非树边,我们需要限制二分范围:二分涉及到的边一定不能连接 \(u\) 的子树内非 \(u\) 的结点

这样,一条边至多被二分两次。如果一条边被二分到两次,则它是非祖孙非树边;否则,它是祖孙非树边,而我们的二分确定了它的一个端点,二分出另外一个即可。

Subtask 7

没有限制!!!没有限制!!!

现在需要攻克的难关,就是找到一棵生成树的边集!!!只需要找到边集就可以了!!!

Subtask 4 不是要二分祖先边吗?这个二分过程可以直接套过来:枚举每个点 \(u\),找到最长的一个编号前缀,使得删掉这个前缀恰好可以保证 \(u\) 能到 \(0\)(再多删一个就会爆炸)。可以发现,下一条边一定在某个生成树上。反复迭代即可找到一棵生成树。

这个过程可以和 Subtask 4 的“链划分”过程合在一起,所以并没有需要特别注意的部分。

交互次数上界在 \(5n\log n\),但实际上询问次数在 \(1.0\times 10^4\sim 1.5\times 10^4\),还是非常优秀的。

代码

#include <vector>
#include <random>
#include <utility>

#include "graph.h"

namespace Workplace {
    #define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
    #define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )

    const int MAXN = 605;

    std :: vector<int> chnEdg[MAXN], chnNod[MAXN];

    std :: pair<int, int> ans[MAXN];

    std :: vector<int> subtre[MAXN];
    int faE[MAXN], fath[MAXN];

    int ord[MAXN];
    int bel[MAXN], tot = 0;

    bool onTre[MAXN], vis[MAXN], solved[MAXN];

    int n, m;

    inline bool Query( const int &u, const std :: vector<int> &E ) {
        static std :: vector<int> s( m, 0 );
    
        rep( i, 0, m - 1 ) s[i] = 0;
        for( const int &e : E ) s[e] = 1;
        return query( u, s );
    }

    inline std :: vector<int> Slice( const std :: vector<int> &vec, const int &l, const int &r ) {
        return std :: vector<int> ( vec.begin() + l, vec.begin() + r );
    }

    inline std :: vector<int> operator + ( const std :: vector<int> &A, const std :: vector<int> &B ){
        std :: vector<int> C( A );
        C.insert( C.end(), B.begin(), B.end() );
        return C;
    }

    inline std :: vector<int>& operator += ( std :: vector<int> &A, const std :: vector<int> &B ) {
        return ( A.insert( A.end(), B.begin(), B.end() ), A );
    }

    void BuildChain( const std :: vector<int> &nod, const std :: vector<int> &edg ) {
        static std :: mt19937 rng( 1145141 );

        if( edg.empty() ) return ;
        if( edg.size() == 1u ) {
            ans[edg[0]] = { nod[0], nod[1] };
            faE[nod[1]] = edg[0], fath[nod[1]] = nod[0];

            ord[++ ord[0]] = nod[1];
            return ;
        }
        int V = nod.size(), E = edg.size();
        int p = std :: uniform_int_distribution<> ( 1, V - 1 ) ( rng );

        std :: vector<int> lNod, rNod, lEdg, rEdg, ance, qry;
        for( int u = nod[0] ; u ; ance.push_back( faE[u] ), u = fath[u] );
        rep( i, 0, E - 1 ) {
            qry = ance;
            rep( j, 0, E - 1 )
                if( i ^ j ) qry.push_back( edg[j] );
            ( Query( nod[p], qry ) ? rEdg : lEdg ).push_back( edg[i] );
        }
        qry = ance + lEdg, rNod.push_back( nod[p] );
        rep( i, 0, V - 1 )
            ( Query( nod[i], qry ) ? lNod : rNod ).push_back( nod[i] );
        BuildChain( lNod, lEdg );
        BuildChain( rNod, rEdg );
    }

    inline void BuildTree() {
        std :: vector<int> tre, qry;
        rep( i, 0, m - 1 ) onTre[i] = false;

        // form a spanning tree.
        // at the same time, construct a chain division.
        rep( i, 1, n - 1 ) {
            std :: vector<int> ance( tre ), avai, nw;
            rep( j, 0, m - 1 )
                if( ! onTre[j] )
                    avai.push_back( j );
            int num = avai.size();
            for( int lst = 0 ; lst < num ; ) {
                if( Query( i, ance ) ) break;

                int l = lst, r = num - 1, mid;
                while( l < r ) {
                    mid = ( l + r ) >> 1;
                    if( ! Query( i, ance + Slice( avai, mid + 1, num ) ) ) r = mid;
                    else l = mid + 1;
                }
                nw.push_back( avai[l] );
                ance.push_back( avai[l] );
                onTre[avai[l]] = true, lst = l + 1;
            }
            tre.swap( ance );
            if( ! nw.empty() ) {
                bel[i] = ++ tot;
                chnEdg[tot].swap( nw );
                chnNod[tot].push_back( i );
            } else {
                int l = 1, r = tot, mid;
                while( l < r ) {
                    mid = ( l + r ) >> 1, qry.clear();
                    rep( j, 1, mid ) qry += chnEdg[j];
                    if( Query( i, qry ) ) r = mid;
                    else l = mid + 1;
                }
                chnNod[bel[i] = l].push_back( i );
            }
        }
        // construct the spanning tree by the division.
        vis[ord[ord[0] = 1] = 0] = true;
        rep( i, 1, tot ) {
            int l = 1, r = ord[0], mid;
            while( l < r ) {
                mid = ( l + r ) >> 1, qry = chnEdg[i];
                rep( j, 2, mid ) qry.push_back( faE[ord[j]] );
                if( Query( chnNod[i][0], qry ) ) r = mid;
                else l = mid + 1;
            }
            std :: vector<int> nod;
            nod.push_back( ord[l] ), nod += chnNod[i];
            BuildChain( nod, chnEdg[i] );
        }
    }

    inline void FindRemaining() {
        rep( i, 0, n - 1 ) vis[i] = false;
        per( i, ord[0], 2 ) {
            int u = ord[i];
            std :: vector<int> tre, notTre;
            for( const int &v : subtre[u] ) vis[v] = true;
            rep( j, 0, m - 1 )
                if( onTre[j] ) {
                    if( j != faE[u] )
                        tre.push_back( j );
                } else {
                    if( ( ans[j]. first == -1 || ! vis[ans[j]. first] ) &&
                        ( ans[j].second == -1 || ! vis[ans[j].second] ) )
                        notTre.push_back( j );
                }
            for( const int &v : subtre[u] ) vis[v] = false;
            int num = notTre.size();
            for( int lst = 0 ; lst < num ; ) {
                if( ! Query( u, tre + Slice( notTre, lst, num ) ) ) break;

                int l = lst, r = num - 1, mid;
                while( l < r ) {
                    mid = ( l + r ) >> 1;
                    if( Query( u, tre + Slice( notTre, lst, mid + 1 ) ) ) r = mid;
                    else l = mid + 1;
                }
                int cur = notTre[l];
                ( ~ ans[cur].first ? ans[cur].second : ans[cur].first ) = u;
                lst = l + 1;
            }
        }
        rep( i, 0, m - 1 )
            if( ans[i].second == -1 ) {
                int u = ans[i].first;
                std :: vector<int> anceNod, anceEdg;
                for( int x = u ; x ; x = fath[x] )
                    anceNod.push_back( x ), anceEdg.push_back( faE[x] );
                anceNod.push_back( 0 );

                int num = anceEdg.size(), l = 0, r = num - 1, mid;
                while( l < r ) {
                    mid = ( l + r + 1 ) >> 1;
                    if( Query( u, Slice( anceEdg, mid + 1, num ) + ( std :: vector<int> ) { i } ) ) l = mid;
                    else r = mid - 1;
                }
                ans[i].second = anceNod[l + 1];
            }
    }

    inline std :: vector<std :: pair<int, int> > 
      Solve( const int &inputN, const int &inputM ) {
        n = inputN, m = inputM;

        rep( i, 0, m - 1 ) ans[i] = { -1, -1 };
        BuildTree();
        rep( i, 1, n - 1 ) 
            for( int u = fath[i] ; u ; u = fath[u] )
                subtre[u].push_back( i );
        FindRemaining();
        return std :: vector<std :: pair<int, int> > ( ans, ans + m );
    }

    #undef rep
    #undef per
}

std :: vector<std :: pair<int, int> > solve( int n, int m ) {
    return Workplace :: Solve( n, m );
}

标签:std,二分,游戏,int,mid,back,图上,vector,LOJ6734
From: https://www.cnblogs.com/crashed/p/17399894.html

相关文章

  • 有趣的前端小游戏
    今天搬了一天的家,状态不是很好,分享一个小游戏给大家吧~先介绍一下游戏规则:1、点击play后,游戏会提供4中不同的昆虫供玩家进行选择,这四种昆虫分别为:蟑螂、蛛蛛、蚊子和苍蝇。2、当玩家选择完昆虫后,游戏开始3、游戏会随机的生成玩家选择的昆虫到电脑屏幕,玩家利用鼠标点击到则得一分......
  • 【题解】Luogu[P8818] CSP-S 2022 策略游戏
    一道简单区间rmq分类讨论题,考场上最后5分钟想出来,没写出来,退役了……给定两个序列\(A_{1},\dots,A_{n}\);\(B_1,\dots,B_n\)规定\(C_{i,j}=A_i\timesB_i\)。题目说小L和小Q必定选择最优策略,而小L先选,小Q后选,小L要使得\(C_{i,j}\)尽可能大,小Q要使得\(C_{i,j}\)......
  • 550. 游戏玩法分析 IV
    【题目】Table:Activity+--------------+---------+|ColumnName |Type   |+--------------+---------+|player_id   |int    ||device_id   |int    ||event_date  |date   ||games_played|int    |+--------------+---------+......
  • 游戏APP用户行为分析
    游戏APP用户行为流程:数据导入install_table=pd.read_excel('./Data/游戏APP安装与注册信息.xlsx',sheet_name='安装信息')register_table=pd.read_excel('./Data/游戏APP安装与注册信息.xlsx',sheet_name='注册信息')数据概况print("安装信息:")print(install_......
  • 1218:取石子游戏
     #include<iostream>#include<cstdio>#include<cstring>usingnamespacestd;booljs(inta,intb){if(a<b)swap(a,b);if(a%b==0)returntrue;for(intk=a/b;k>=1;k--)if(!js((a-b*k),b))returntrue;ret......
  • 跳跃游戏V
    给你一个整数数组arr和一个整数darr存储着一些柱子的高度,整数d为你能跳的最远距离,可以选择往左跳和往右跳除此以外,跳跃途径中只能有更低的柱子存在你可以选择数组的任意下标开始跳跃,请你返回你最多可以访问多少个下标1.排序+动态规划classSolution{public: intmaxJu......
  • 跳跃游戏IV
    从0位置跳到末位置,每次可以往左跳、往右跳一格,或跳到有与该位置相同数值的地方,求最小跳跃次数1.广度优先搜索+哈希预处理+动态规划classSolution{public:vector<int>dp;//dp[i]表示到达i位置最小操作数intminJumps(vector<int>&arr){intn=arr.size......
  • 第四届CocoaChina游戏开发者大会 可谓群英荟萃
    2012年3月31日,第四届CocoaChina游戏开发者大会暨Cocos2D-X技术研讨会在北京剧场举行。CocoaChina成立于2008年,是整个华语地区第一个也是最大的苹果产品的中文开发社区网站。此前,由CocoaChina举办的开发者大会已成功举办过三届,且每届规模都在扩大,行业影响力与日俱增。从2009年起,每......
  • luogu P3345 [ZJOI2015]幻想乡战略游戏
    P3345[ZJOI2015]幻想乡战略游戏这道题还是比较有意思的,做了一个比较长的时间,但是点分树实在是太毒瘤了,所以记录一下线段树的做法。题面给一棵树,有边权,每次修改一个点的点权,修改完后输出所有点到这棵树的带权重心的贡献,即\(\sumdis_i\timesval_i\)题解考虑朴素的暴......
  • 2023-05-09:石子游戏中,爱丽丝和鲍勃轮流进行自己的回合,爱丽丝先开始 。 有 n 块石子排
    2023-05-09:石子游戏中,爱丽丝和鲍勃轮流进行自己的回合,爱丽丝先开始。有n块石子排成一排。每个玩家的回合中,可以从行中移除最左边的石头或最右边的石头,并获得与该行中剩余石头值之和相等的得分。当没有石头可移除时,得分较高者获胜。鲍勃发现他总是输掉游戏(可怜的鲍勃,他......