Codeforces #285 Div 1 简要题解

A. Misha and Forest

题目链接

http://codeforces.com/contest/504/problem/A

题目大意

给你一个无向森林里每个结点的度数,以及每个结点相连的点的编号的亦或和。要你求出这个无向森林里的每条边。

思路

由于是无向森林,初始时一定有度数为1的点,而且度数为1的点的亦或和就是唯一的与它相连的点的编号。而删去这个度数为1的点后,整个图还是一个无向森林。

于是我们可以不断重复上述操作,类似拓扑排序,用一个队列维护当前要删除的度数为1的点,如此反复即可得到答案。

但是还有很多细节需要注意,此题真的是个FST的好题。。。

代码

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <vector>
#include <map>

#define MAXV (1<<17)

using namespace std;

typedef pair<int,int> pr;

vector<pr>sol;
map<pr,bool>mp;
int degree[MAXV],xorsum[MAXV],n,m;
int q[MAXV],h=0,t=0;
bool vis[1<<17];

int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        scanf("%d%d",&degree[i],&xorsum[i]);
        if(degree[i]==1)
        {
            q[t++]=i;
            vis[i]=true;
        }
    }
    while(h<t)
    {
        int u=q[h++];
        int v=xorsum[u];
        if(!degree[u]) continue; //!!!!!
        degree[u]--;
        degree[v]--;
        xorsum[v]^=u;
        if(!mp.count(make_pair(u,v))&&!mp.count(make_pair(v,u))) sol.push_back(make_pair(u,v));
        mp[make_pair(u,v)]=true;
        if(degree[v]==1&&!vis[v])
        {
            q[t++]=v;
            vis[v]=true;
        }
    }
    printf("%d\n",sol.size());
    for(int i=0;i<sol.size();i++)
        printf("%d %d\n",sol[i].first,sol[i].second);
    return 0;
}

B. Misha and Permutations Summation

题目链接

http://codeforces.com/contest/504/problem/B

题目大意

给你两个长度为n的排列a与b,定义fA为排列a的字典序编号,求编号为(fA+fB)mod n!的排列,字典序编号是从0到n-1,排列里的数字范围为[0,n-1]

思路

对于一个长度为 n 的全排列A而言,它的字典序编号为

pre[A0](n1)!pre[A1](n2)!...pre[An1](0)!

其中 pre[x] 表示的是,全排列中在数字 x 左边,且数字大小比x小的数字个数。
我们可以从左到右扫一遍整个全排列,用一个平衡树 S 维护当前尚未被加入全排列的数字集合,在平衡树里查询数字x的排名即可得知 pre[x] ,由于 n 的范围很大,这里并不适合用一个int或long long int来代表排名,而应该用一个阶乘进制数来代表排名(阶乘进制数的概念请参阅http://en.wikipedia.org/wiki/Factorial_number_system)。

然后我们把排列a和排列 b 的字典序编号加起来,由于这两个字典序编号大小均小于n!,因此我们可以直接在阶乘进制数系下,将两个字典序编号按位相加后进位,并舍去高于 n 位的部分,只保留第1~ n 位,即可得到答案序列的字典序编号,然后我们再和之前类似地,从左到右扫一遍这个代表字典序编号的阶乘进制数(这个阶乘进制数的第i位就代表了 pre[i] ),用一个平衡树 S 维护当前尚未被加入全排列的数字集合,假如我们要求出排列里当前位的数字x,只需要在平衡树里查询第 pre[x]+1 大的数字即可得到 x

第一次尝试用pbds库里的平衡树做题,感觉非常爽!

代码

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>

#define MAXN 210000

using namespace std;
using namespace __gnu_pbds;

typedef tree<int,null_type,less<int>,rb_tree_tag,tree_order_statistics_node_update> bst;
int n,permua[MAXN],permub[MAXN],prec[MAXN]; //prec[i]=答案排列里,在第i个数字左边,但是又比第i个数字小d数字个数
bst A,B,C; //平衡树A和B里维护的是从左到右扫排列序列时,尚未被加入的数字集合

int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++) scanf("%d",&permua[i]);
    for(int i=0;i<n;i++) scanf("%d",&permub[i]);
    for(int i=0;i<n;i++)
    {
        A.insert(i);
        B.insert(i);
        C.insert(i);
    }
    for(int i=0;i<n;i++)
    {
        int a=A.order_of_key(permua[i]); //找出ai有多少个比它小的但是在其左边又尚未被用过的数字
        int b=B.order_of_key(permub[i]);
        prec[i]=a+b;
        A.erase(permua[i]);
        B.erase(permub[i]);
    }
    for(int i=n-1;i>=1;i--) //对变进制数prec[]进行进位
    {
        prec[i-1]+=prec[i]/(n-i);
        prec[i]%=(n-i);
    }
    prec[0]%=n;
    for(int i=0;i<n;i++)
    {
        int tmp=*C.find_by_order(prec[i]); //在平衡树C里查询第prec[i]+1大的数,就是答案排列里第i位的数
        printf("%d ",tmp);
        C.erase(tmp);
    }
    printf("\n");
    return 0;
}

C. Misha and Palindrome Degree

题目链接

http://codeforces.com/contest/504/problem/C

题目大意

给你一个字符串,定义一个区间[l,r]为操作区间,即将 [l,r] 内的元素以某种方式重新排列后,整个字符串变为回文串。问有多少个操作区间。

思路

首先我们可以把这个字符串左右两边相同的前缀和后缀去掉,这样的话,剩下的字符串里没有相同的前缀与后缀。

不妨设剩下的字符串在原串里的区间为 [L,R] 。显然 R=nL+1 ,相同的前缀和后缀的长度均为 L1 。在这个剩下的字符串(以下简称 S 串)里,所有的操作区间,要么左端点是字符串的开头,要么右端点是字符串的结尾。为了方便起见,下面我们只讨论如何统计左端点是字符串的开头的操作区间个数,而右端点是字符串的结尾的操作区间个数的统计,可以把 S 串翻转过来,和之前按照一模一样的做法即可统计出答案。

由于这个区间的合法性是单调的(即 [L,r] 为合法操作区间,那么 [L,R] 也为单调区间, LL,rR )。我们可以二分这个操作区间 [L,r] S 串里的右端点 r 最小值,问题变成判定性问题:操作区间为[L,r]是否是合法的。我们可以当作是:操作区间外的数字是固定不变的,而操作区间内的数字可以随意放置,数字 i tmpcnt[i]个,要想办法在放置完操作区间内的所有数字后, S 变成回文串。

我们可以在 O(n) 时间里先预处理每种数字 i S里的出现次数 tmpcnt[i] ,然后从左到右枚举串上的位置 i ,扫一遍S串。

1.i和n-i+1都在操作区间内,就无所谓了,先把这一对位置放到一边暂时不管
2.i在操作区间内,但是n-i+1不在操作区间内,则用一个s[n-i+1]的数字放在i位置上
3.n-i+1在操作区间内,但是i不在操作区间内,则用一个s[i]的数字放在n-i+1位置上
4.i和n-i+1均不在操作区间内,则要求s[i]必须和s[n-i+1]相同

如果在扫描过程中发现有数字不够的情况的话,就可以直接判定这个操作区间是无效的。

那么操作完了以后,对于1剩下的点对而言,显然是一定能随便找出2个相同的数字填满他们的。

实际上覆盖了 L 的操作区间的左端点是L的,而右端点是 >=r 的,因此,覆盖了 L 的操作区间的个数就是L(nr+1)。同样地,我们可以求出覆盖了 R 的操作区间个数。

但是答案是小于这两种操作区间个数之和的,因为两种操作区间的个数里有相同的部分,就是同时覆盖了L R 两点的操作区间个数,为L2,我们需要在最后的答案里减掉这部分。

代码

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>

#define MAXN 110000

using namespace std;

typedef long long int LL;

int n,s[MAXN],L,R;
int cnt[MAXN]; //cnt[i]=数字i的出现次数
int tmpcnt[MAXN]; //tmpcnt[i]=数字i在操作区间里的出现次数

bool check(int r) //操作区间为[L,r]是否可行
{
    memset(tmpcnt,0,sizeof(tmpcnt));
    for(int i=L;i<=r;i++) tmpcnt[s[i]]++;
    for(int i=1;i<n-i+1;i++)
    {
        bool LinRange=(i>=L&&i<=r); //true表明i是在操作区间内的
        bool RinRange=((n-i+1)>=L&&(n-i+1)<=r); //true表明n-i+1是在操作区间内的
        if(LinRange&&RinRange) continue; //i和n-i+1都在操作区间内,就无所谓了
        else if(LinRange) //i在操作区间内,但是n-i+1不在操作区间内,则用一个s[n-i+1]的数字放在i位置上
        {
            if(--tmpcnt[s[n-i+1]]<0) return false;
        }
        else if(RinRange) //n-i+1在操作区间内,但是i不在操作区间内,则用一个s[i]的数字放在n-i+1位置上
        {
            if(--tmpcnt[s[i]]<0) return false;
        }
        else //i和n-i+1均不在操作区间内,则要求s[i]必须和s[n-i+1]相同
        {
            if(s[i]!=s[n-i+1])
                return false;
        }
    }
    return true;
}

LL calc() //假设操作区间为[l,r],l<=L,求这样的区间个数
{
    int lowerBound=L,upperBound=n,ans=-1; //求出r的最小值ans
    while(lowerBound<=upperBound)
    {
        int mid=(lowerBound+upperBound)>>1;
        if(check(mid))
        {
            ans=mid;
            upperBound=mid-1;
        }
        else lowerBound=mid+1;
    }
    return (LL)(n-ans+1)*L; //r的取值在[ans,n]范围内,l的取值在[1,L]范围内,乘法原理得到答案
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&s[i]);
    L=1,R=n;
    while(L<=R)
    {
        if(s[L]==s[R])
        {
            L++;
            R--;
        }
        else break;
    }
    if(L>R)
    {
        printf("%I64d\n",(LL)n*(LL)(n-1)/2+n);
        return 0;
    }
    int numOfOdd=0; //出现次数为奇数次的数字个数
    for(int i=L;i<=R;i++)
        cnt[s[i]]++;
    for(int i=0;i<MAXN;i++)
        if(cnt[i]&1)
            numOfOdd++;
    if(numOfOdd>1)
    {
        printf("0\n");
        return 0;
    }
    LL ans=0;
    ans+=calc();
    reverse(s+1,s+n+1);
    ans+=calc();
    ans-=(LL)L*L;
    printf("%I64d\n",ans);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值