3 4 3 2 3 3 3 3 2 3
8 3HintFor 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)
这几天最烦这种,“姿势对着,就是不过”的题了。。。比赛的时候我想的算法是:“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和构建在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)
回到刚才多次乘法运算的问题,如果每一个都去乘,答案就会很慢很慢,因为这里相当于进行的是对一个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
原文地址:http://blog.csdn.net/polossk/article/details/38054581