https://codeforces.com/contest/1991
给定n,m,k
n * m 的白色矩阵
给定长度q的序列 H/V
如果H需要放入 1 * k的黑色横条
如果V需要放入 k * 1的黑色竖条
每次放入后,一行或一列全 黑,则同时删除对应行或列(变回白色)
问有没有方法 完成序列q,
如果有给出具体的方案,每次输出放置的左上角坐标
n,m 100, q 1000
2s
256mb
我的思路 想完美的消除 a个横 和 b个列, 那么形状 是 类似
那么对于 多行消除,需要 k个横 和 m-k竖
那么对于 多列消除,需要 k个竖 和 n-k横
想的是画二维(横个数,竖个数) 的坐标图,找边界
其实可以切割成 横(k,n-k,n),竖(k,m-k,m), 其中 n-k,m-k和k的大小关系不一定
然后这里其实是离线的,也就是我们可以知道最先达到哪个状态,但对于9宫格每个里面应该怎么摆,还没有具体的尝试
题解 hint1: 考虑 行只在最左k列放,竖只在最上k行放, 和我想的方向是一样的
hint2: Prioritize operations that will lead to a reset.
分成3部分,放置规则
横的B有空放B
竖的A有空放A
横的B放满了: - 如果A中有满的列对应的行 则放对应行 进行 消除,否则k * k中随便放一行
竖的A放满了: - 如果B中有满的行对应的列 则放对应列 进行 消除,否则k * k中随便放一列
首先 如果 k=n=m, 那么随便放直接消除
如果k=n or k=m, 那么 行列其中一个随便放直接消除,另一个负责k * k
所以下面 讨论是 k < n 且 k < m
就这样放,那么 我们[k*k,A,B]
有初始状态
[空,有剩余,有剩余]
那么 因为 只要有剩余 就不会放k * k, 所以 下一个状态只可能是
[空,放满,有剩余]
or [空,有剩余,放满]
因为行列对称,考虑[空,放满,有剩余]
往后
如果 放行
那么 [放了部分行 未满,放满,有剩余]
如果 放列
那么 [空,放满,放满]
状态 感觉 乱起来了
根据对称性,考虑 第二项是满 的情况
那么 对于 A/B 再 定义更细的状态 未满(放置进度x[0,n-k)个), 已满(消除进度x[0~k)个)
k * k
区域 状态 空, 行[i~j], 列[i~j]
[空, 满(x), 未(x)] + 行 = [行[1~1], 满(x), 未(x)]
[空, 满(x),未(x)] + 列 = [空,满(x),未(x+1)]
如果这时 列放满 状态会是 [空,满(x),满(0)]
[行[a~b], 满(x), 未(x)] + 行 = [行[a~b+1], 满(x), 未(x)]
(如果 这时k * k
全放满 则 所有行消除 [空,未(0),未(x)]
—–!!!!!!!!!!!!!
[行(a~b),满(x),未(x)] + 列 = [行(a~b),满(x),未(x+1)]
如果这时 列放满 状态会是 [空,满(x),满(b)]
[空, 满(x), 满(x)] + 行 = [空, 满(x), 满(x+1) 这里全消除则变成 未(0)]
[空, 满(x), 满(x)] + 列 = [空, 满(x+1) 这里全消除则变成 未(0), 满(x)]
看起来很美好
但还是有一个(上面很多惊叹号的!!!)需要仔细处理的情况
[空,未(0),未(0)] -> [空,未(0),满(0)] -> [+列(1-2),满(0),满(0)]=[空,满(2),满(0)] -> [+行,满(2),满(0)] = [空, 满(2), 未(0)] -> [+行, 满(2), 未(0)] = [列(1-2), 满(!!!!!! 所以满的状态还需要2个元素), 未(0)] (这里有神奇的变化)
代码 https://codeforces.com/contest/1991/submission/276109968
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 #include <bits/stdc++.h> using namespace std;#define rep(i,a,n) for(int i=(a);i<(int)(n);i++) int read () {int r;scanf ("%d" ,&r);return r;}const int NOTF = 0 ; const int ROW = 1 ;const int COL = 2 ;const int FULL = 3 ; using state = array<int ,3 >;char op[1010 ];int k;array<int ,2> calc (state &A,state &B,int n,state &kk,int row,int col) { array<int ,2> ret = {-1 ,-1 }; if (B[0 ] == NOTF) { ret = {(++B[1 ])+k,1 }; if (B[1 ] == n-k) { B={FULL,1 ,k}; if (kk[0 ] == col) { if (kk[1 ] == 1 ) { B={FULL,kk[2 ]+1 ,k}; }else { B={FULL,1 ,kk[1 ]-1 }; } kk={NOTF,0 ,0 }; } } }else { if (A[0 ] == NOTF){ if (kk[0 ] == NOTF) { ret = {1 ,1 }; kk = {row,1 ,1 }; }else { ret = {kk[2 ] == k ? --kk[1 ]:++kk[2 ],1 }; if (kk[2 ] - kk[1 ] + 1 == k) { assert (B[1 ] == 1 or B[2 ] == k); if (B[1 ] == 1 and B[2 ] == k){ kk={NOTF,0 ,0 }; }else if (B[1 ] == 1 ){ kk={col,B[2 ]+1 ,k}; }else if (B[2 ] == k){ kk={col,1 ,B[1 ]-1 }; } B={NOTF,0 ,0 }; } } }else { ret = {A[1 ]==1 ?A[2 ]--:A[1 ]++,1 }; if (A[1 ] > A[2 ]) A={NOTF,0 ,0 }; } } return ret; } void w () { int n=read (); int m=read (); k=read (); int q=read (); scanf ("%s" ,op); if (n==m and n == k){ rep (i,0 ,q) printf ("1 1\n" ); return ; }else if (n==k) { int p = 0 ; rep (i,0 ,q) { if (op[i] == 'H' ){ printf ("%d 1\n" ,++(p%=k)); }else { printf ("1 %d\n" ,k+1 ); } } return ; }else if (m==k) { int p = 0 ; rep (i,0 ,q) { if (op[i] == 'V' ){ printf ("1 %d\n" ,++(p%=k)); }else { printf ("%d 1\n" ,k+1 ); } } return ; }else if (k==1 ){ rep (i,0 ,q) printf ("%d 1\n" ,(i%n)+1 ); return ; } state kk = {NOTF,0 ,0 }; state A = {NOTF,0 ,0 }; state B = {NOTF,0 ,0 }; rep (i,0 ,q) { if (op[i] == 'H' ) { auto pos = calc (A,B,n,kk,ROW,COL); printf ("%d %d\n" ,pos[0 ],pos[1 ]); }else { auto pos = calc (B,A,m,kk,COL,ROW); printf ("%d %d\n" ,pos[1 ],pos[0 ]); } } } int main () { int t = read (); while (t--) w (); return 0 ; }
A,B 玩游戏,n堆石头, 第i堆$a_i$个
A先B后,每次操作:
选$k\in[1,\frac{n}{2}]$
删除其中$k$堆
剩余的$n-k$堆中选$k$堆,把每个选中的都分割成2堆 要求 分割后的$2k$堆石头 个数都是质数
无法移动的人输掉
n 2e5
ai 2e5
2s
256mb
我的思路 首先 如果一堆石头被选,要被分割,那么
<= 3不能被分割
=4的偶数,根据有限范围内的1+1=2定理,一定可以分割
奇数,那么 只可能尝试 v-2, 2
因此奇数被切割的次数是唯一的,而偶数被切割的次数,取决于,首次的选择
所以 f(奇数) = 它的切割次数,那么“分割”操作其实变成 选一些数,让它们的次数减1
f(偶数) = 1 + vector<pair<int,int>> ~= [f(),f()]
也就是可切割的方案对
而这个问题是n = 2e5 切割方案数 感觉需要n^2? 才能找到所有吗? 这个可以预计算吗,这个方案个数到底多还是少?
没啥想法
先不考虑偶数,或者激进一点,假设第一轮 结束后 只剩下奇数
那么 如何最优策略
而 例如1,2,3 这种不可拆分的数,虽然对于操作的第二步没有用,但是对于第一步还是很有用的,也就是
如果上一步是 [k个删除] [n-2k没动] [k个奇拆分]
因为总数 还是 n, 所以 可以拆分的 在 没动,和奇拆分出来的 v-2中
如果 当前 2 * 只可拆分1次的数个数 >= 可拆分的数,
那么 只需要 令k = min(n/2,只可拆分1次的数个数), 然后 就能胜利
所以 其实如果看成剩余次数的数组 arr=[...]
那么 其实选择 k
个数 变成0(原来也可以是0), 和选择k
个大于0的数减去1, 操作后全0则胜利
所以 1的个数 >= 其它非0的个数?
质数-2链应该很短,因为对于大于5的末位是5一定不是, x7,x9,(x+1)1,(x+1)3,!?
而注意到 x7是质数 mod3 = 1 or 2
x9=x7+2 也是质数那么 x7 mod 3 = 2, x9 mod 3 = 1
那么(x+1)1 一定不是质数
所以长度会更短!? 只能是1或者2?
题解 官方hint Hint 1. 考虑奇数如何切分,偶数呢? 这里我想到了奇数只能x-2,2
后来细想发现
这样一个数x mod 3 = 0 可能切割2次, x,x-2,x-4
这样一个数x mod 3 = 1 可能切割1次 x, x-2
这样一个数x mod 3 = 2 可能切割0次 x
那么一个偶数 切割是 不可行,或者 (01,01) 不多于4种方案,
因为如果切割出的是 mod 3那么只有3是质数,而3可切割0次,
Hint 2, 考虑简单版游戏:2堆石头,第一堆x个,第二堆1个. 判断所有必胜态
x是奇数 => (x,1)->(x-2,0)->(x-4,0), 所以判断它的最长质数链
-2 不是质数 输
-2是质数,-4 不是质数 赢
-2,-4是质数,-6 一定不是质数 输
x是偶数 =>
x mod3 = 0,
=6 => (0,0) 赢
6, 拆成 (mod 3 = 1(最多1次), mod 3 = 2(0次))
x mod3=1
x mod3=2
3+(x-3) = (0,0) 赢
(mod3=1,mod3=1)
换句说,要赢需要拆成两个不可再-2拆分的
Hint3 先计算奇数的结果,然后FFT计算出偶数的结果!!!!!
哦 好有道理,但这个性质需要先观察出前面的 只会是1,0
那么 只需要建立 a[i] = int(bool(i是质数且无法拆分)), i 是奇数
那么 对a[i]
自身卷积aa=a*a
aa
如果可行 那么对应的值 [1,2e5]
, 如果不可行则是0
Hint 4,对于“大多数情况”可以根据 输入的位置有多少是简化游戏中的获胜的状态,从而判断输赢
根据而上面的分析的结果还剩些什么
[0次奇败(2也属于这个),1次奇胜,2次奇败,0次偶败,1次偶胜,(0,1)偶败,(1,1)偶败]
奇数的话 获胜态有1次,偶数的话 切割了就是 两个不可操作的奇数
n=奇 如果 1次奇数和偶胜 的个数>=一半。有一个不可操作的,那么 必胜。
n=偶 如果 1次奇数和偶胜 的个数>=一半。那么 必胜。
因为这两种都是 只需要一次操作,对手就无法操作了
Hint5 对于hint4无法判断的,可以计算 有多少ai能切割成两个胜利奇数来判断
感觉hint4 我的判断的都还不完全。找ai能切割一样fft就能搞
官方solution
奇x只能切割成x-2,2 ✅
4可以切割成2,2,其它的都是 奇,奇 ✅
(x,1),称为 lose/win position
奇数 与能-2的次数有关
偶数
能切割成两个lose 则 是win, 所以这里是 (lose+质数) 做卷积
用fft算
2014 ICPC SWERC Problem C
https://archive.algo.is/icpc/swerc/2014/discussion.pdf
结论:
lose position: 不可切割 或者 切割中会产生一个win position !!!!!!!!!!!!!!!!!!!!!!!! 好有道理
win position: 能切割成2个 lose position
!? 这里连长度很短都还没有用???
如果所有位置都是losing position 那么后手胜利,因为不论先手如何操作,[A删][B拆][C不动]
, 那么 B拆就会有至少k个 不多于 2k个win position,那么始终可以变化回全losing position
因此 先手如果能把所有变成 losing position 那么就是先手胜利
这样如果是偶数长度
1 <= win position <= n/2,那么k=win position个数
win position > n/2, k= n/2
如果是奇数长度
1 <= win position <= n/2 那么k=win position个数
n > win position > n/2, k=n/2 因为至少一个bad position,而上面的操作可以覆盖完覆盖
全部都是 win position,那么无法一次全部变成 bad position
所以拆分后一定没有bad,如果一个win能拆成2个win,那么显然原来的是偶,拆后是奇(不可再拆成2个win)
如果 所有win在一次操作后都会产生一个lose,那么先手输
如果有[1,n-1]
个win在一次操作后会产生2个win, 那么 选一些 拆,选一些删除,让所有都是win,且所有win都不能拆成2个win
如果有n个都能拆成2个win,那么操作后要么有lose,要么全win,但是存在[1,n-1]
个能拆成2个win, 所以是输
综上
计算所有奇数的 win/lose
fft 计算所有偶数的win/lose
all lose => lose
len = 偶 => win
not all win => win
fft 计算所有偶可以拆 2win
[1,n-1]
个 2win win
0 or n 个2win lose
代码 https://codeforces.com/contest/1991/submission/279951917
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 #include <bits/stdc++.h> using namespace std;typedef long long ll;#define MOD 998244353 #define rep(i,a,n) for(ll i=(a);i<(ll)(n);i++) ll read () {ll r;scanf ("%lld" ,&r);return r;}namespace CMM{ const int _mod = 998244353 ; class modint { private : long long _v; public : modint () :_v(0 ) { } modint (long long _a) { _v = (_a < 0 )? _mod - ((-_a) % _mod) : (_a % _mod); } int val () const { return _v; } modint operator +()const { return *this ; } modint operator -()const { return { _mod - _v }; } modint operator +(const modint& rhs) const { return modint (*this ) += rhs; } modint operator -(const modint& rhs) const { return modint (*this ) -= rhs; } modint operator *(const modint& rhs) const { return modint (*this ) *= rhs; } modint operator /(const modint& rhs) const { return modint (*this ) /= rhs; } bool operator ==(const modint& rhs)const { return _v == rhs._v; } bool operator !=(const modint& rhs)const { return _v != rhs._v; } bool operator > (const modint& rhs)const { return _v > rhs._v; } bool operator >=(const modint& rhs)const { return _v >= rhs._v; } bool operator <=(const modint& rhs)const { return _v <= rhs._v; } bool operator < (const modint& rhs)const { return _v < rhs._v; } modint& operator +=(const modint& rhs) { (_v += rhs._v) %= _mod; return *this ; } modint& operator -=(const modint& rhs) { (_v += _mod - rhs._v) %= _mod; return *this ; } modint& operator *=(const modint& rhs) { _v = _v * rhs.val () % _mod; return *this ; } modint& operator /=(const modint& rhs) { _v = _v * rhs.inv ().val () % _mod ; return *this ; } modint pow (long long pwr) const { long long res (1 ) ; long long _b(_v); while (pwr) { if (pwr & 1 ) (res *= _b) %= _mod; (_b *= _b) %= _mod; pwr /= 2 ; } return res; } modint inv () const { assert (_v != 0 ); return pow (_mod - 2 ); } }; namespace NTT{ const int MAXPWR = 22 ; const int g = 3 ; const int invg = 332748118 ; int rev (int x, int len) { int ans = 0 ; while (len -- ){ ans <<= 1 ; ans |= x & 1 ; x >>= 1 ; } return ans; } inline int getlog2 (int n) { return 31 - __builtin_clz(n);} template <class T> T mypow (T a, long long k) { T res = 1 ; while (k) { if (k & 1 ) (res *= a); (a *= a); k >>= 1 ; } return res; } template <class mint> void NTT (std::vector<mint> &A, int flag = 1 ) { int n = A.size (); if (n == 1 ) return ; int lgn = getlog2 (n); for (int i=0 ;i<n;i++) { int j = rev (i, lgn); if (j > i) std::swap (A[i], A[j]); } for (int pwr=0 ;pwr<lgn;pwr++){ int m = 1 << pwr; mint gn = mypow <mint>(flag == 1 ? g : invg, (MOD - 1 ) / (m << 1 )); for (int k = 0 ; k < n; k += (m<<1 )) { mint gi = 1 ; for (int j=0 ;j<m;j++){ mint U = A[k + j]; mint T = A[k + j + m] * gi; A[k + j] = (U + T); A[k + j + m] = (U - T); gi *= gn; } } } if (flag == -1 ){ const mint INVSIZE = mint (n).inv (); for (int i=0 ;i<n;i++) (A[i] *= INVSIZE) ; } } template <class T> void INTT (std::vector<T> &A) { NTT <T>(A,-1 );} template <class T> std::vector<T> convolution (std::vector<T> v0, std::vector<T> v1) { assert (v0.size () < (1 <<MAXPWR)); assert (v1.size () < (1 <<MAXPWR)); int sz = v0.size () + v1.size (); if (sz == 0 )return {}; sz = 1 << (getlog2 (sz) + !!(sz & (sz-1 ))); v0.resize (sz,0 ); v1.resize (sz,0 ); NTT <T>(v0); NTT <T>(v1); std::vector<T> v2 (sz,0 ) ; for (int i=0 ;i<sz;i++) v2[i] = v0[i] * v1[i]; INTT <T>(v2); return v2; } template <class T> std::vector<T> poly_sq (std::vector<T> v0) { int sz = v0.size () * 2 ; if (sz == 0 )return {}; sz = 1 << (getlog2 (sz) + !!(sz & (sz-1 ))); v0.resize (sz,0 ); NTT <T>(v0); std::vector<T> v2 (sz,0 ) ; for (int i=0 ;i<sz;i++) v2[i] = v0[i] * v0[i]; INTT <T>(v2); return v2; } } }; using mint = CMM::modint;int n;const int N=200000 ;bool prime[N+10 ];int a[N+10 ];int win[N+10 ];int winwin[N+10 ]; void o (int idx) { printf ("%s\n" ,idx==0 ?"Alice" :"Bob" ); }void w () { n = read (); rep (i,0 ,n) a[i]=read (); int win_cnt = 0 ; rep (i,0 ,n) win_cnt+=win[a[i]]; if (win_cnt == 0 ) return o (1 ); if (n%2 ==0 ) return o (0 ); if (win_cnt != n) return o (0 ); int win2cnt = 0 ; rep (i,0 ,n) win2cnt+=winwin[a[i]]; if (win2cnt== 0 or win2cnt== n) return o (1 ); return o (0 ); } int main () { ios_base::sync_with_stdio (false ); cin.tie (0 ); rep (i,2 ,N+1 ) prime[i]=true ; rep (i,2 ,N+1 ) if (prime[i]) for (ll j=i*i;j<=N;j+=i) prime[j]=false ; for (int i=3 ;i<=N;i+=2 ) if (prime[i-2 ] and !win[i-2 ]) win[i]=true ; { vector<mint> tmp (N+1 ,0 ) ; rep (i,2 ,N+1 ) if (prime[i] and !win[i]) tmp[i] = 1 ; auto res = CMM::NTT::convolution (tmp,tmp); for (int i=4 ;i<=N;i+=2 ) win[i] = (res[i] != 0 ); } { vector<mint> tmp (N+1 ,0 ) ; rep (i,2 ,N+1 ) if (prime[i] and win[i]) tmp[i] = 1 ; auto res = CMM::NTT::convolution (tmp,tmp); for (int i=4 ;i<=N;i+=2 ) winwin[i] = (res[i] != 0 ); } int t = read (); while (t--) w (); return 0 ; }
总结 赛时 做完f只有40min了,没想出G/H
G: 评分2700
不过 其构造有难度 + 实现有难度 是我觉得再练习两年半也不一定能当场做出来的,赛后在有答案情况下,编写+调试也是花了一些时间
H: 评分3300
想了半天 才发现mod3带来的 奇数长度性质,不过题解其实没有用到
而是通过的 博弈论原理完成的,不过这个退化成(x,1)的问题是如何想到的??????????
不过博弈论,我的思路有点太过于使用这个 长度的,反而没用,而通过 [胜利状态]->[必定失败状态]->[总能 胜利状态]
的反复才是博弈论的常见
然后第一次 见到 这种中间状态 配合fft 来计算另一半的,虽然事后看很自然,但反过来想也是一种结果导向的反推方向