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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章