【JZOJ4004】【GDKOI2015】青蛙跳环(欧拉回路)

Problem

  有一个N(≤10000)颗石子围成的环,石子从0到N-1编号。从第i颗石子可以到第(i*2)%N颗石子和第(i*2+1)%N颗石子。求一条从0开始、最后回到0的、字典序最大的哈密顿回路。无解输出-1。

Solution

  首先考虑无解的情况。(通过打表发现)我们猜测N为奇数时会无解。其实证明也很简单:
  能到达点0的只有 N2 ⌊ N 2 ⌋ ,而能到达点N-1的则需要分类讨论:当N为奇数时,N-1为偶数,所以 N2=N12 ⌊ N 2 ⌋ = N − 1 2 ;当N为偶数时,N-1为奇数,所以 N2=N12+1 ⌊ N 2 ⌋ = ⌊ N − 1 2 ⌋ + 1 。综上,当N为奇数时,能到达点0和点N-1的均只有 N12 N − 1 2 ,但除0以外的每个点只能经过1次,所以不可能从点 N12 N − 1 2 到一遍点N-1再到一遍点0。所以当N为奇数时无解。
  那么考虑N为偶数的情况。对于点X(≤ N2 N 2 )而言,它的出边有两条:一条连向点(X*2)%N,一条连向(X*2+1)%N;而对于点X+ N2 N 2 而言,它的出边也有两条:一条连向点(X*2+N)%N=(X*2)%N,一条连向(X*2+1+N)%N=(X*2+1)%N。所以点X(≤ N2 N 2 )和点X+ N2 N 2 的出边是一样的。那么由于要求一条哈密顿回路,如果我们从点X走到点(X*2)%N,则我们必须从点X+ N2 N 2 走到点(X*2+1)%N,反之亦然。
  我们发现本图中有N个点,每个点有两条出边,所以本图有2N条边。由于要求哈密顿回路,所以我们必须在每个点的出边中只选择一条去走,也就是说只选择N条边。
  根据上面的分析,我们将点X和点X+ N2 N 2 合并为1个点,而此点则仍有两条出边:一条连向点(X*2)%N,一条连向(X*2+1)%N。这样,我们就把原图缩成了一个有 N2 N 2 个点、N条边的新图。而原来经过N条边的要求依然不变。所以题目被转化为求一条欧拉回路。
  那么我们只消码一个简洁的dfs即可优美地解决问题:一开始从点0出发,然后由于要求字典序最大,尽量地往编号大的点走,并将走过的边删掉(或标记一下);当我们在某个点上因其出边都被删掉无路可走时,我们就把这个点压进栈中(或数组里);最后从栈顶不断输出(逆序输出)。
  但是理解它可不像打它这么简单。我们可以举个栗子:当N为6时,我们构出来的新图如下图所示:
这里写图片描述
  由于点3、4、5与点0、1、2的出边相同,所以我们不必构出它们的出边;而当我们遍历到它们时,我们就把它们%一个 N2 N 2 ,但是在栈中的答案依然是它们原本的数。
  首先我们从0出发,到点1,再到点3,然后3%一个 N2 N 2 变成点0,但是由于不能再走边0 1,所以只能走边0 0,然后0的两条出边都被走过了,就把0压入栈中,弹出。
  回到点3(我们是从点3到点0的),和点0的情况一样,所以把3压入栈中,弹出。
  回到点1(我们刚才从1走到点3),到点2,再到点5,5%一个 N2 N 2 变成点2,再到点4,4%一个 N2 N 2 变成点1,而点1的两条出边也都被走过了,于是把点4压入栈中(因为我们此时在点4而非点1),弹出。
  回到点5(因为我们是从点5而非点2走到点4的),发现它的两条出边(去4的边和去5的边)都被走过,于是把点5压入栈中,弹出。
  回到点2,一样压入栈中弹出。最后点1、点0也一样。
  那么我们得到栈ans={0,3,4,5,2,1,0}。逆序输出则为:“0 1 2 5 4 3 0”。
  我们发现在此步骤中,我们在3的时候就已经遍历回点0了。但我们要求的是终点为0的一条欧拉回路,所以我们可以视为当时并没有去从点1走到点3,而是从点1走到了点2;而由于这种情况下点1肯定会遍历完它所有的出边,所以出边和它一样的点4肯定会无路可走,而且其他点均暂时不会碰到无路可走的情况,所以我们肯定是在点4时打破这个僵局,然后把4压入了栈,所以可以视为我们是从点4走到了点3。
  时间复杂度: O(n) O ( n )

Code

#include <cstdio>
#include <cstdlib>
#include <vector>
using namespace std;
#define N 10010
#define fo(i,a,b) for(i=a;i<=b;i++)
int i,n;
vector<int>side[N],ans;
void dfs(int x)
{
    int now=x;
    x%=n/2;
    while(!side[x].empty())
    {
        int y=side[x].back();
        side[x].pop_back();
        dfs(y);
    }
    ans.push_back(now%n);
}
int main()
{
    scanf("%d",&n);
    if(n&1)
    {
        printf("-1");
        exit(0);
    }
    fo(i,0,(n-1)/2)
    {
        side[i].push_back(2*i);
        side[i].push_back(2*i+1);
    }
    dfs(0);
    while(!ans.empty())
    {
        printf("%d ",ans.back());
        ans.pop_back();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值