题解 CF1369 D,E,F Codeforces Round #652 (Div. 2)
CF1369D TediousLee
考虑每一轮新增的节点。在第\(i\)轮(\(i\geq 3\)):
- 对于每个第\(i-1\)轮长出的节点,它现在没有儿子,所以它下面会新增\(1\)个第\(i\)轮的节点;
- 对于每个第\(i-2\)轮长出的节点,它现在恰好有一个儿子(也就是第\(i-1\)轮的节点),所以它下面会新增\(2\)个第\(i\)轮的节点。
所以,如果记\(f[i]\)表示第\(i\)轮新长出的节点数量,则:\(f[i]=f[i-1]+2\cdot f[i-2]\) (\(i\geq 3\))。边界是\(f[1]=f[2]=1\)。
现在我们已经能用\(f\)数组刻画出这棵树的样子了。考虑怎么求出最多覆盖的claw数量。
因为是在取模意义下运算,所以不能对数值比较大小,那就不太好DP。考虑贪心。
定义“一层”就是指同一轮加入的节点。以一个claw的根节点所在的层来代表它。我们先把最下面一层全取掉(也就是根节点在第\(n-2\)层的claw)。然后因为claw之间不能覆盖,所以下一个能取的是第\(n-5\)层。以此类推,后面能取的是:\(n-8\), \(n-11\) ... 层。所以每隔三层就能取一次,我们对\(f\)数组做一个隔\(3\)个的“前缀和”即可。
这个\(f\)数组和前缀和都能预处理出来。所以总时间复杂度\(O(n+t)\)。如果\(n\)特别大的话似乎也可以矩阵快速幂,复杂度就是\(O(t\log n)\)。
参考代码:
//problem:CF1369D TediousLee
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const int MOD=1e9+7;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int& x,int y){x=mod1(x+y);}
inline void sub(int& x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}
const int MAXN=2e6;
int f[MAXN+5],s[MAXN+5];
int main() {
s[1]=f[1]=1;
s[2]=f[2]=1;
for(int i=3;i<=MAXN;++i){
f[i]=mod1(f[i-1]+mod1(f[i-2]+f[i-2]));
s[i]=mod1(f[i]+s[i-3]);
}
int T;cin>>T;while(T--){
int n;cin>>n;
if(n<=2)cout<<0<<endl;
else cout<<(ll)mod1(f[n-2]+(n<5?0:s[n-5]))*4%MOD<<endl;
}
return 0;
}
CF1369E DeadLee
对于一种食物\(i\),设有\(s_i\)个人喜欢它。把食物分为\(s_i\leq w_i\)(充足的)和\(s_i>w_i\)(不足的)两类。
根据每个人喜欢的两种食物,也可以将人分为:(1) 喜欢的两种食物都充足;(2) 一种充足,一种不足;(3) 喜欢的两种食物都不足,这三类。
考虑一个人如果是第(1)或第(2)类,那他随便往后排都可以,因为他总能吃到一个充足的食物。
我们先把这些人丢到队伍最后去,然后“删掉”。考虑删掉这些人后,每种食物的“充足/不足”情况可能会变化,也就是有一些原本“不足”的食物,会因为这些人的离开而变得“充足”。于是我们又可以删掉一批满足条件的人。以此类推,直到所有人都被删掉,或者无法再删掉任何一个人(无解)。
朴素实现这个过程是\(O(m^2)\)的,因为每轮(最坏情况下)只有一个人被删掉。
考虑优化这个过程。发现一个人被删掉后,只有他喜欢的两个食物会发生变化。所以只需要更新这两个食物即可。具体实现时,把喜欢每种食物的人装在一个\(\texttt{set}\)里,便于删除。再开一个队列,里面装“已经变得充足的食物”,每次弹出队首,更新喜欢这种食物的人。
时间复杂度\(O((n+m)\log n)\)。
参考代码:
//problem:CF1369E DeadLee
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const int MAXN=1e5,MAXM=2e5;
int n,m,w[MAXN+5];
bool vis[MAXN+5];
struct Friend{int x,y;}a[MAXM+5];
set<int>s[MAXN+5];
int main() {
cin>>n>>m;
for(int i=1;i<=n;++i){
cin>>w[i];
}
for(int i=1;i<=m;++i){
cin>>a[i].x>>a[i].y;
s[a[i].x].insert(i);
s[a[i].y].insert(i);
}
queue<int>q;
for(int i=1;i<=n;++i){
if(SZ(s[i])<=w[i])vis[i]=1,q.push(i);
}
vector<int>ans;
while(!q.empty()){
int x=q.front();q.pop();
//cout<<x<<endl;
while(!s[x].empty()){
int i=*s[x].begin();
//cout<<"* "<<i<<endl;
assert(a[i].x==x||a[i].y==x);
int y=(a[i].x==x?a[i].y:a[i].x);
s[a[i].x].erase(i);
s[a[i].y].erase(i);
if(!vis[y] && SZ(s[y])<=w[y]){
vis[y]=1;q.push(y);
}
ans.pb(i);
}
}
if(SZ(ans)<m){
cout<<"DEAD"<<endl;return 0;
}
cout<<"ALIVE"<<endl;
for(int i=SZ(ans)-1;i>=0;--i)cout<<ans[i]<<" ";
cout<<endl;
return 0;
}
CF1369F BareLee
先只考虑一轮游戏。求出先手能否成为赢家,能否成为输家(不论对手怎么操作):分别记为:\(\text{win}(s,e),\text{lose}(s,e)\)。
求\(\text{win}(s,e)\):
-
当\(e\)是奇数时:如果\(s\)是奇数,则先手不能成为赢家,否则可以。
证明可以归纳。当\(s=e\)时显然正确(不可能赢)。当\(s<e\)时,如果对\(s'>s\)都成立:若\(s\)是奇数,则\(2s\)和\(s+1\)都是偶数(可以赢),所以\(s\)不能赢。若\(s\)是偶数,则\(s+1\)是奇数(不能赢)所以\(s\)可以赢。所以归纳假设正确。
-
当\(e\)是偶数时:
- 考虑如果\(2s>{e}\)。则没有\(\times2\)操作了,所以能赢当且仅当\(s\)是奇数。
- 否则如果\(4s>e\)(也就是\(\frac{e}{4}<s\leq\frac{e}{2}\)),则先手可以做一次\(\times2\)操作,此时\(s\)变成偶数,后手必败。所以这种情况下先手必胜。
- 否则,\(\text{win}(s,e)=\text{win}(s,\lfloor\frac{e}{4}\rfloor)\)。因为一旦\(s>\frac{e}{4}\),就是上一种情况,先手必胜,所以双方都不希望\(s>\frac{e}{4}\)。于是就等价于\(\text{win}(s,\lfloor\frac{e}{4}\rfloor)\)了。
求\(\text{lose}(s,e)\):
- 如果\(2s>e\),显然一次就可以直接输掉。
- 否则,这个结果等价于\(\text{win}(s,\lfloor\frac{e}{2}\rfloor)\),因为只要一旦\(s>\frac{e}{2}\),对手直接输掉。所以双方都不希望\(s>\frac{e}{2}\)。于是就等价于\(\text{win}(s,\lfloor\frac{e}{2}\rfloor)\)了。
现在我们已经能够在\(O(\log e)\)的时间里求出单轮先手能不能赢/输。
现在要回答这个多轮的问题。依次考虑每一轮。维护出“当前轮结束之后,Lee能否成为赢家,能否成为输家(不论对手怎么操作)”。
如果上一轮结束后,Lee既可以成为赢家,又可以成为输家,则答案就是\(1\ 1\)。因为他可以自己选择做先手或后手,所以无论后面的游戏是什么,都能达到想要的结果。
如果上一轮结束后,两者都不可以,则答案就是\(0\ 0\)。因为无论他想要的结果是什么,对手都有可能阻止他。
排除这两种情况后,如果上一轮可以输,说明这一轮只能做先手,我们按Lee为先手的情况,调用\(\text{win}\)/\(\text{lose}\)函数,求一下即可。否则,这一轮只能做后手,我们调用两个函数后把结果取反即可。
时间复杂度\(O(t\log e)\)。
参考代码:
//problem:CF1369F BareLee
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const int MAXN=1e5;
int n,can_win[MAXN+5],can_lose[MAXN+5];
struct game{ll s,e;}a[MAXN+5];
bool get_win(ll s,ll e){
assert(s<=e);
if(e%2==1){
if(s%2==1)return 0;
else return 1;
}
else{
if(s*2>e){
if(s%2==1)return 1;
else return 0;
}
else if(s*4>e){
return 1;
}
else{
return get_win(s,e/4);
}
}
}
bool get_lose(ll s,ll e){
if(s*2>e){
return 1;
}
else{
return get_win(s,e/2);
}
}
int main() {
cin>>n;
for(int i=1;i<=n;++i)cin>>a[i].s>>a[i].e;
can_lose[0]=1;
for(int i=1;i<=n;++i){
if(can_lose[i-1] && can_win[i-1]){
cout<<1<<" "<<1<<endl;
return 0;
}
if(!can_lose[i-1] && !can_win[i-1]){
cout<<0<<" "<<0<<endl;
return 0;
}
if(can_lose[i-1]){
//这一局是先手
can_win[i]=get_win(a[i].s,a[i].e);
can_lose[i]=get_lose(a[i].s,a[i].e);
}
else{
can_win[i]=(!get_win(a[i].s,a[i].e));
can_lose[i]=(!get_lose(a[i].s,a[i].e));
}
}
cout<<can_win[n]<<" "<<can_lose[n]<<endl;
return 0;
}