码迷,mamicode.com
首页 > 其他好文 > 详细

HDU 4869 Turn the pokers 多校训练第一场1009

时间:2014-07-23 13:32:26      阅读:259      评论:0      收藏:0      [点我收藏+]

标签:acm   c++   hdu   

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<n,m<=100000). 
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
Hint
For 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
 


题解

这几天最烦这种,“姿势对着,就是不过”的题了。。。比赛的时候我想的算法是:“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.}。

其次我们接着去关心这个运算的一些性质:(为了方便,以后称这个运算为乘法)

  1. 首先这个运算满足结合律。即,对任意的G中的元素a, b, c,a * b * c = a * (b * c)。这个不多提,独立证明。
  2. 其次这个运算有单位元e = M(n, 0)。即,对任意的G中的元素a, a * e = e * a = a。这个可以简单地发现。
  3. 再者这个运算有逆元a-1 = a。即,对任意的G中的元素a, a * a-1 = a-1 * a = e。因为一个翻转只需再执行一次自己的翻转就行了。
  4. 最后这个运算有交换律。即,对任意的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。这个不定方程可以和一元线性同余方程互相转化。所以通过扩展欧几里得可以很快的求得逆元。当然也可以直接通过递推的解法来进行计算。

逆元计算好之后,就是查询结果了,这个就是注意每次乘法都要取模就好。

代码示例

/******************************************************************************
*       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 <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
template<class T>inline bool updateMin(T& a, T b){ return a > b ? a = b, 1: 0; }
template<class T>inline bool updateMax(T& a, T b){ return a < b ? a = b, 1: 0; }

typedef long long int64;
typedef pair<int, int> 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;
}



HDU 4869 Turn the pokers 多校训练第一场1009

标签:acm   c++   hdu   

原文地址:http://blog.csdn.net/polossk/article/details/38054581

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!