程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> HDU 4869 Turn the pokers 多校訓練第一場1009

HDU 4869 Turn the pokers 多校訓練第一場1009

編輯:C++入門知識

HDU 4869 Turn the pokers 多校訓練第一場1009


Turn the pokers

Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 282 Accepted Submission(s): 89


Problem Description During summer vacation,Alice stay at home for a long time, with nothing to do. She went out and bought m pokers, tending to play poker. But she hated the traditional gameplay. She wants to change. She puts these pokers face down, she decided to flip poker n times, and each time she can flip Xi pokers. She wanted to know how many the results does she get. Can you help her solve this problem?
Input The input consists of multiple test cases.
Each test case begins with a line containing two non-negative integers n and m(0 The next line contains n integers Xi(0<=Xi<=m).
Output Output the required answer modulo 1000000009 for each test case, one per line.
Sample Input
3 4
3 2 3
3 3
3 2 3

Sample Output
8
3

HintFor the second example:
0 express face down,1 express face up
Initial state 000
The first result:000->111->001->110
The second result:000->111->100->011
The third result:000->111->010->101
So, there are three kinds of results(110,011,101) 

Author FZU
Source 2014 Multi-University Training Contest 1

題解

這幾天最煩這種,“姿勢對著,就是不過”的題了。。。比賽的時候我想的算法是:“O(NlgN)排序 - O(N)計算答案區間 - O(N)對答案求和”。但是TLE了。。。後來又仔細的想了想,其實根本不需要排序,直接就能O(N)計算答案區間。然後重寫寫了份,糾結了半天的位運算處理奇偶性之後終於過了。

我慢慢一步一步說我的算法是怎麼來的。

抽象

這個模型非常的巧妙,因為是卡片的翻轉,那麼每個卡片就有兩種狀態(正面'1'和反面'0')。

那麼能不能把這個實際例子抽象出來呢?

當然是可以的了!

首先引入符號M(n, k):表示,n張卡片中,只有k張正面的所有的可能的集合。

其次我引入一種建立在M(n, k)中的元素上的二元運算,翻轉運算,用乘法(*)表示。

為什麼是二元運算呢?我們慢慢地考慮翻轉狀態。“從4張卡片中先翻轉3張。”這句話的意思是不是可以理解成“_M(4, 0) * _M(4, 3)”呢?我用前置的下劃線表示一個這個集合的元素。也就是說,這個表達式就是從沒有正面開始發生狀態轉移,結果我們暫時不考慮,狀態轉移的方法是通過“翻轉3張”這個操作。換句話說,一次乘法運算相當於一種累計翻轉。第一次翻轉的是0張,所以所有的答案是M(4, 0);第二次翻轉的是3張,所以所有的答案是M(4, 3)。兩次翻轉一累計,就是我們的答案了。

再次強調,一次乘法運算就相當於累計翻轉,當然多次的乘法運算(比如先翻3張,再翻2張,最後再翻3張)也可以看做多次的累計翻轉。即,針對一張特定的卡片,如果它翻轉了偶數次,就相當於沒有翻轉;同樣翻轉了奇數次的就相當於翻轉了一次。

然後這個運算的作用范圍是什麼呢?沒錯,是剛剛定義的符號M(n, k)的所有元素的全體,也就是:G = {x | x ∈ M(n, k), k = 0, 1, 2, 3, ..., n.}。

其次我們接著去關心這個運算的一些性質:(為了方便,以後稱這個運算為乘法)

首先這個運算滿足結合律。即,對任意的G中的元素a, b, c,a * b * c = a * (b * c)。這個不多提,獨立證明。其次這個運算有單位元e = M(n, 0)。即,對任意的G中的元素a, a * e = e * a = a。這個可以簡單地發現。再者這個運算有逆元a-1 = a。即,對任意的G中的元素a, a * a-1 = a-1 * a = e。因為一個翻轉只需再執行一次自己的翻轉就行了。最後這個運算有交換律。即,對任意的G中的元素a, b, c,a * b = b * a。因為翻轉的順序是不定的,可以先翻轉a張,再翻轉b張;也可以先翻轉b張,再翻轉a張。

所以,針對G和構建在G上的運算*就是一個阿貝爾群(交換群)。

但是這個代數系統顯然不夠好。我們應當接著去拓展他。

換句話說,剛才,我們僅僅研究兩個元素去做這個運算的性質,現在我們去研究兩個集合,去做這個運算的性質。當然,首先我們從結果入手,嘗試去尋找規律。

比如這次運算:M(n, x1) * M(n, x2),為了方便分析結果,我們假設x1 >= x2,會產生什麼樣的結果呢?

回歸剛才的定義,M(n, x1)表示有y1 := x1個正面,和y2 := (n - x1)個反面。那麼下一步要對x2個卡片進行翻轉。我們假設,它對i個上一次翻轉過的卡片進行再翻轉,對j個沒有翻轉過的卡片進行翻轉。這樣就有C(y1, i) + C(y2, j)種可能,同時當然還要滿足組合數的公式等等。這裡生成的結果都有什麼呢?假設最後的正面有z個,原本有y1個正面,先減少i個,在增加j個。又因為i + j = x2,x1 >= x2,所以有 x1 - x2 <= z <= min(n, (n - x1) + (n - x2))。而且,如果你是在草稿紙上寫了過程的話,你會發現,本來 z = y1 - i + j = x1 - i + (x2 - i) = x1 + x2 - 2 * i。也就是,z是一個公差為2的等差數列。

為了方便理解,我引入加法符號+,表示集合的並運算。

舉個具體的例子吧:

M(4, 3) * M(4, 2) = M(4, 1) + M(4, 3)

M(8, 5) * M(8, 3) = M(8, 2) + M(8, 4) + M(8, 6) + M(8, 8)

M(8, 4) * M(8, 6) = M(8, 3) + M(8, 5) + M(8, 7)

不知道大家看出一個規律沒有,就是關於結果序列的奇偶性。如果x1與x2同時是偶數,結果是一個奇數的序列;同時為奇數,結果是一個偶數序列;一奇一偶,結果是奇數序列。 還有一個規律,如果有多次的乘法運算,豈不是要對每一個生成的結果進行再一次乘法運算?沒錯的。這裡嚴格意義上,引入加法,就同時引入了分配率的概念,相當於建立了一個環。不過用處不大,只是方便理解而已。

回到剛才多次乘法運算的問題,如果每一個都去乘,答案就會很慢很慢,因為這裡相當於進行的是對一個N的數據展開成(N / 2)的一組新數據,會指數爆炸的。但是舉個例子,假設我計算的是這組乘法:

M(8, 4) * M(8, 6) * M(8, 3)

第一次乘法的運行結果時:

M(8, 4) * M(8, 6) = M(8, 3) + M(8, 5) + M(8, 7)

接著去逐項去乘:

M(8, 3) * M(8, 3) = M(8, 0) + M(8, 2) + M(8, 4) + M(8, 6)

M(8, 5) * M(8, 3) = M(8, 2) + M(8, 4) + M(8, 6) + M(8, 8)

M(8, 7) * M(8, 3) = M(8, 4) + M(8, 6)

可以看到,實際上的答案是M(8, 4) * M(8, 6) * M(8, 3) = M(8, 0) + M(8, 2) + M(8, 4) + M(8, 6) + M(8, 8)

換句話說,如果我們記錄上下界,豈不是很方便?這樣就不會一層一層的展開了。

所以問題就被我們抽象成:計算每一層的區間,同時考慮奇偶性。

計算區間並且記錄奇偶性

每一次我都對上一次的答案區間進行更新。其實更准確的說實際上是在檢查是否需要放大區間。特別判斷不在這個區間的x(相當於上文中的M(n, k)中的k)的情況,並且正確的賦值就行,也就是low = 0, high = n。其余的就判斷與當前的區間的邊界的距離,一個取小值,一個取大值。

當然不能忘記處理奇偶性。奇偶性和異或運算很類似,所以我是用異或搞的。

最後因為是一個公差為2的序列,但是我們只記錄了區間和奇偶性。所以應當根據奇偶性去判斷答案。

總體的時間復雜度就是O(N){計算區間} - O(N){計算答案}。

組合數的計算

組合數計算公式很簡單。C(n, m) = n! / (m! * (n - m)!)

模運算中,除法a / b,等價於a * b^-1,也就是乘上它的逆。所以我們在預處理時計算出逆元的階乘。

逆元的定義是,滿足a * b % p = 1的b,稱為a的逆元,有時記做b = inv(a)。計算方法很簡單。擴展歐幾裡得還記得不?ax + by = gcd(a, b)。因為相鄰的兩個數必然互素,那麼就可以寫成ax + by = 1。這個不定方程可以和一元線性同余方程互相轉化。所以通過擴展歐幾裡得可以很快的求得逆元。當然也可以直接通過遞推的解法來進行計算。

逆元計算好之後,就是查詢結果了,這個就是注意每次乘法都要取模就好。

代碼示例

ing/******************************************************************************
*       COPYRIGHT NOTICE
*       Copyright (c) 2014 All rights reserved
*       ----Stay Hungry Stay Foolish----
*
*       @author       : Shen
*       @name         : HDU 4869 Turn the pokers
*       @file         : G:\My Source Code\【ACM】比賽\0722 - MUTC[1]\I.cpp
*       @date         : 2014/07/22 13:52
*       @algorithm    : 群論,數論,組合
******************************************************************************/

#include 
#include 
#include 
using namespace std;
templateinline bool updateMin(T& a, T b){ return a > b ? a = b, 1: 0; }
templateinline bool updateMax(T& a, T b){ return a < b ? a = b, 1: 0; }

typedef long long int64;
typedef pair range; // 答案的范圍
// first -> LowerBound, second -> UpperBound

const int MaxM = 100005;
const int64 MOD  = 1000000009;
int64 inv[MaxM]; // 逆元,a * inv(a) % p = 1
int64 fac[MaxM]; // 階乘,1 * 2 * 3 * ...
int64 rfc[MaxM]; // 逆元階乘,inv(1) * inv(2) * inv(3) * ...

int n, m;
int x[MaxM];

void init()
{
    inv[0] = inv[1] = 1;
    fac[0] = fac[1] = 1;
    rfc[0] = rfc[1] = 1;
    for (int i = 2; i < MaxM; i++)
    {
        inv[i] = ((MOD - MOD / i) * inv[MOD % i]) % MOD;
        fac[i] = (fac[i - 1] * i) % MOD;
        rfc[i] = (rfc[i - 1] * inv[i]) % MOD;
    }
}

inline int64 c(int64 n, int64 k)
{
    return (fac[n] * rfc[k] % MOD) * rfc[n - k] % MOD;
}

inline bool cmp(int a, int b) { return a > b; }

range update(int x, range& cur, bool& isOdd)
{
    int low = 0, high = 0;
    int curl = cur.first, curh = cur.second;
    // update IsOdd)
    isOdd ^= (x % 2 == 1);
    // update Lower Bound
    if (curl <= x && x <= curh) low = 0;
    else
        low = min(abs(curl - x), abs(curh - x));
    // update Upper Bound
    x = n - x;
    if (curl <= x && x <= curh) high = n;
    else
        high = max(n - abs(curl - x), n - abs(curh - x));
    return make_pair(low, high);
}

void solve()
{
    for (int i = 0; i < m; i++)
        scanf("%d", &x[i]);
    range res = make_pair(0, 1);
    bool isOdd = 0;
    for (int i = 0; i < m; i++)
        res = update(x[i], res, isOdd);
    int64 ans = 0;
    for (int i = res.first; i <= res.second; i++)
        if ((i % 2 == 1) == isOdd)
            ans = (ans + c(n, i)) % MOD;
    cout << ans << endl;
}

int main()
{
    init();
    while (~scanf("%d%d", &m, &n)) solve();
    return 0;
}



  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved