可能是太懒了吧,想要先放一下数位dp,主要是因为目前的比赛中还未遇到过类似题目,所以先学了博弈论,博弈论在几次比赛中都有遇到并且也是cf的常驻嘉宾了。
首先说一下Nim游戏吧,这是最常见最经典的一类博弈论模型,通常变现的形式是有n堆石子,第i堆石子有ai个,alice和bob可以轮流从任何堆中取走若干个,哪一方无法操作则落败反之获胜。
该类问题的求解办法是将n堆石子数量异或起来,若异或和不为0则先手必胜,反之后手必胜,证明如下:若a1 ^ a2 ^ ....... ^ an = x != 0,那么x的二进制表示下最高位的1在第k个位置,那么必然存在一个ai的第k位为1,那么一定有ai ^ x < ai(因为ai和x在第k位为1,ai第k位会异或成0,并且第k位是x的最高位的1,那么x的更高位上都是0,因此ai ^ x必定小于ai),那么我们就可以从ai取走若干石子使得ai变成ai ^ x,那么新的x会变成x ^ ai ^ (xi ^ x) = y,那么y = 0,那么就是说异或和若不为0则一定有一种办法使得下一回合异或和为0,当异或和 = 0的时候,假设我们可以将ai变成ai'使得a1 ^ ....^ai' ^ ai+1 ^.....^an = 0,那么就有a1 ^ a2 ^ .... ^ ai ^ ai+1 ^ ..... ^ an == a1 ^ ....^ai' ^ ai+1 ^.....^an,可以得到ai == ai',说明没有取走石子这是不合法的,因此得证。
接下来是sg函数,对于一堆石头,每次可以从中取走{a1,a2....an}集合中的任意一个数量的石子,每一个sg函数相当于一个节点,它会有n个分支指向n个节点直到节点为0,这时候该节点的sg值为0,每一个节点的sg值 = mex(该节点所有后续节点的sg值),那么就可以得到一个性质,sg = 0的节点只能走到非0节点,sg != 0的节点必定可以走到一个sg = 0的节点,也就是说先手sg != 0的时候先手必定可以走到0节点那么留给后手的永远只有非0节点,等到先手走到终点的0结点的时候就赢了反之必败。
而当有多堆石子的时候这其实又变成了一个nim游戏,将n堆石子的sg函数异或起来即可。
该题需要注意的是每次放入两堆石子并不能分别存入set,而应该存入两者的异或和,因为本质上就是多了一堆石子。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll , ll> pii;
const int N = 200;
int n;
int f[N];
int sg(int x)
{
if(f[x] != -1)
{
return f[x];
}
set <int> s;
for(int i = 0 ; i < x ; i++)
{
for(int j = 0 ; j < x ; j++)
{
s.insert(sg(i) ^ sg(j));
}
}
for(int i = 0 ; ; i++)
{
if(!s.count(i))
{
f[x] = i;
return f[x];
}
}
}
int main()
{
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
memset(f , -1 , sizeof f);
int res = 0;
for(int i = 1 ; i <= n ; i++)
{
int x;
cin>>x;
res ^= sg(x);
}
if(res)
{
cout<<"Yes";
}else
{
cout<<"No";
}
}
分成两种情况来考虑
一种是石子数 > 1的堆,这些石子的总的操作次数b = 石子数 + 堆 - 1,有一个结论就是当b为奇数的时候该人获胜因为无论什么操作b都是由奇数 -> 偶数 -> 奇数 ...... -> 1。
接着再考虑石子数为1的堆a,这时候会有以下五种情况:
1.从a中取走一个石子 ==> (a - 1 , b)
2.从b中取走一个石子 ==> (a , b - 1)
3.合并a中的一对石子(a >= 2) ==> (a - 2 , b + 3(堆 + 1,石子数 + 2) / b + 2(原来b = 0,石子数 + 2))
4.合并b中的一对石子(b >= 2) ==> (a , b - 1)
5.合并a的一堆和b的一堆(a >= 1 && b >= 1) ==> (a - 1 , b + 1(堆-1 + 1,石子数 + 1))
接下来就是对这五种情况的转移进行记忆化搜索就行
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll , ll> pii;
const int N = 70,M = 51000;
int f[N][M];
int n;
int dp(int a,int b)
{
if(f[a][b] != -1)
{
return f[a][b];
}
if(a == 0)
{
f[a][b] = b % 2;
return f[a][b];
}
if(b == 1)
{
return dp(a + 1 , 0);
}
if(a && !dp(a - 1 , b))
{
f[a][b] = 1;
return 1;
}
if(b && !dp(a , b - 1))
{
f[a][b] = 1;
return 1;
}
if(a >= 2 && !dp(a - 2 , b + (b ? 3 : 2)))
{
f[a][b] = 1;
return 1;
}
if(a && b && !dp(a - 1 , b + 1))
{
f[a][b] = 1;
return 1;
}
return 0;
}
void work()
{
int a = 0,b = 0;
cin>>n;
for(int i = 1 ; i <= n ; i++)
{
int x;
cin>>x;
if(x == 1)
{
a++;
}else
{
if(b == 0)
{
b += x;
}else
{
b += x + 1;
}
}
}
if(dp(a , b))
{
cout<<"YES"<<"\n";
}else
{
cout<<"NO"<<"\n";
}
}
int main()
{
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int t;
cin>>t;
memset(f , -1 , sizeof f);
while(t--)
{
work();
}
}
该题还是比较好想的,容易发现删除第i个人的话,对前i-1个人的分配是不会有影响的,对[i + 1 ~ n]个人的话会产生偏移,并且如何偏移取决于第i个人,若ai > bi那么之后就会向着补a的方向反之向着补b的方向偏移,既然如此我们只需要维护三个前缀和即可,一个是删除第n + m + 1的时候的前缀和也就是前n + m个人的正常分配的前缀和,第二个是删除第一个ai > bi的前缀和,第三个是删除第一个ai < bi的前缀和,之后我们枚举删除的人对于第i个人我们的sum = sum1[i - 1] + (1.若ai > bi则加上sum2[n + m + 1] - sum2[i] 2.若ai < bi则加上sum3[n + m + 1] - sum3[i]若i是i + n + m + 1那么sum就是sum1[n + m].
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll , ll> pii;
const int N = 2e5 + 10;
int n,m;
ll a[N],b[N],sum1[N],sum2[N],sum3[N];
void work()
{
cin>>n>>m;
for(int i = 1 ; i <= n + m + 1 ; i++)
{
cin>>a[i];
}
for(int i = 1 ; i <= n + m + 1 ;i++)
{
cin>>b[i];
}
int x = 0,y = 0;
for(int i = 1 ; i <= n + m + 1 ; i++)
{
if(a[i] > b[i])
{
if(x < n)
{
x++;
sum1[i] = a[i];
}else
{
y++;
sum1[i] = b[i];
}
}else
{
if(y < m)
{
y++;
sum1[i] = b[i];
}else
{
x++;
sum1[i] = a[i];
}
}
sum1[i] += sum1[i - 1];
}
x = 0,y = 0;
bool f = 0;
for(int i = 1 ; i <= n + m + 1 ; i++)
{
if(a[i] > b[i])
{
if(!f)
{
f = 1;
sum2[i] = sum2[i - 1];
continue;
}
if(x < n)
{
x++;
sum2[i] = a[i];
}else
{
y++;
sum2[i] = b[i];
}
}else
{
if(y < m)
{
y++;
sum2[i] = b[i];
}else
{
x++;
sum2[i] = a[i];
}
}
sum2[i] += sum2[i - 1];
}
x = 0,y = 0;
f = 0;
for(int i = 1 ; i <= n + m + 1 ; i++)
{
if(a[i] > b[i])
{
if(x < n)
{
x++;
sum3[i] = a[i];
}else
{
y++;
sum3[i] = b[i];
}
}else
{
if(!f)
{
f = 1;
sum3[i] = sum3[i - 1];
continue;
}
if(y < m)
{
y++;
sum3[i] = b[i];
}else
{
x++;
sum3[i] = a[i];
}
}
sum3[i] += sum3[i - 1];
}
for(int i = 1 ; i <= n + m + 1 ; i++)
{
ll sum = 0;
if(a[i] > b[i])
{
sum = sum1[i - 1];
sum += sum2[n + m + 1] - sum2[i];
}else
{
sum = sum1[i - 1];
sum += sum3[n + m + 1] - sum3[i];
}
cout<<sum<<" ";
}
cout<<"\n";
}
int main()
{
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int t;
cin>>t;
while(t--)
{
work();
}
}
今天下午浪费了不少时间导致今天本来应该看2h英语课的没有时间了,明天还是学习web,今天马上把前几天背的20页英语单词复习一遍,明天算法开始刷洛谷题单一共大概31题,每天刷1 ~ 3题得话估计得要半个月了,每天还要写一道cf有时候还会有比赛占用时间估计得拖到一个月了,慢工出细活吧,dp能练好的话对各种比赛都帮助很大,就是线段树的复习和主席树,平衡树,ac自动机的学习估计要到年末了.......
今天的封面就是帅气的会长了,明天那一定得是真男人了