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] 是否是合法的。我们可以当作是:操作区间外的数字是固定不变的,而操作区间内的数字可以随意放置,数字itmpcnt[i] 个,要想办法在放置完操作区间内的所有数字后,S 变成回文串。

我们可以在O(n) 时间里先预处理每种数字iS 里的出现次数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 的操作区间个数。

但是答案是小于这两种操作区间个数之和的,因为两种操作区间的个数里有相同的部分,就是同时覆盖了LR 两点的操作区间个数,为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;
}
发布了378 篇原创文章 · 获赞 10 · 访问量 32万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章