9.26日志

可能是太懒了吧,想要先放一下数位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函数异或起来即可。

1.894. 拆分-Nim游戏 - AcWing题库

该题需要注意的是每次放入两堆石子并不能分别存入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";
    }
}

2.1321. 取石子 - AcWing题库

分成两种情况来考虑

一种是石子数 > 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();
	}    
}

3.Problem - C - Codeforces

该题还是比较好想的,容易发现删除第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自动机的学习估计要到年末了.......

今天的封面就是帅气的会长了,明天那一定得是真男人了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值