3个 contest 的教训之傻逼 PM_pro
别急,别破防
Contest 1
Link
T1
思路
较详细。
考虑 DP,直接模拟决策过程,暴力。
设 \(f_{i}\) 表示以 \(a_{i}\) 为结尾的子序列的最大答案。
易得以下状态转移方程:
状态是 \(O(n)\) 的,但是转移也是 \(O(n)\) 的,太弱智了,我最多能想出来的样子。
期望得分:\(45\)。
考虑所有部分分:
$ a\le 511$,直接用 \(j\) 来表示选的数的值,状态为 \(f_{i,j}\)。
转移方程:
期望得分:\(20\)。
序列随机,考虑到不会有人卡我们,并且观察到样例大部分的最佳决策点都在 \(i\) 附近,所以取经验值 \(500\) 作为 \(j\) 离 \(i\) 的最大距离,状态转移同暴力 DP,只不过 \(j\) 只取 \(i\) 前 \(500\) 个点,可以考虑取更多,不超时就行。
期望得分:\(15\)。
考虑正解,如果我们想实现更快的转移,就要考虑一些冗余的状态。哪些状态是冗余的呢?考虑到对于 \(i>j\) 来说,很可能会出现 \(i\) 的所有决策包含选 \(j\) 的决策,因为 \(i\) 比 \(j\) 后转移,且转移考虑到了决策 \(j\),要利用啊。考虑对这一情况进行分析。
利用同样的过去转移,即目前正在转移 \(i\),对于一个 \(j\;(j\le i)\),\(a_{i}=a_{j}\),那么就有任意数 \(x\) 使得 \((a_{i}\land x)=(a_{j}\land x)\)。那么我们在考虑 \(i\) 的转移时,对于任意 \(x\),\(x\le j\) 的转移就可以直接用 \(j\) 的转移来代替,因为同样的情况在 \(j\) 转移时就已经考虑过。
这样的优化虽然不能真正的优化(条件太苛刻),但是可以启发我们。首先,我们知道,对于任意 \(p\) 进制数,第 \(x\) 位单位的权都大于 \(1\) 到 \(x-1\) 位的权和(大 \(1\))。这引导我们寻找 \(x\) 与 \(j\) 满足这种情况的时候。这样我们就可以排除无用决策点了。当 \(a_{x}\) 与 \(a_{j}\;(j\ge x)\) 两个数的最高位一致,两个数仅会有最高位前的数的差距,但是由于 \(j\) 在 \(x\) 之后,所以 \(j\) 至少都会与上一个点产生最高位权的答案贡献,于是,设最高位的权值为 \(k\),有以下式子:
这样,就知道了对于上述情况,\(j\) 点绝对优于 \(x\) 点。
所以,我们只需要枚举最高位,找到每个最高位最靠后的数进行转移就行。可以预处理 \(O(\log V)\) 解决。
这样,总时间复杂度是 \(O(n \log V)\) 的,可以过。
获得了 \(100\) 分。
Code(100 pts)
#include <bits/stdc++.h>
#define int long long
#define upp(a,x,y) for(int a=x;a<=y;a++)
#define dww(a,x,y) for(int a=x;a>=y;a--)
using namespace std;
const int N=1e6+5;
int a[N],f[N],last[N],n,ans;
int lowbit(int x) {return x&-x;}
signed main() {
	scanf("%lld",&n);upp(i,1,n) scanf("%lld",&a[i]);
	upp(i,2,n) {
		if(a[i-1]) last[(int)log2(a[i-1])]=i-1;
		for(int j=0;j<=45;j++) f[i]=max(f[i],f[last[j]]+(a[last[j]]&a[i]));
		ans=max(ans,f[i]);
	}
	printf("%lld",ans);
	return 0;
}
Contest 2
Link
A
思路
分类讨论,首先如果有两个以上的点直接无解,只有一个点肯定有解,如果有两个点,那么看两个点的差如果大于 \(1\) 就有解,否则无解。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e7+5;
int a[N];
signed main() {
    int tt;
    cin>>tt;
    while(tt--) {
        int n;
        cin>>n;
        for(int i=1;i<=n;i++) cin>>a[i];
        if(n==1) puts("YES");
        else {
            if(n==2) {
                if(a[2]-a[1]==1) puts("NO");
                else puts("YES");
            }
            else puts("NO");
        }
    }
    return 0;
}
B
思路
考虑两个区间的交,如果有交,那么交内的所有边都要去除,如果交的两边是可及的,也需要去除交的左右两边。
如果无交,直接去除一个区间的所经区间即可。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
signed main() {
	int tt;
	cin>>tt;
	while(tt--) {
		int x1,y1,x2,y2,ans=0;
		cin>>x1>>y1>>x2>>y2;
		if(max(x1,x2)<=min(y1,y2)) {
			ans=min(y1,y2)-max(x1,x2);
			ans+=(x1!=x2);ans+=(y1!=y2);
			cout<<ans<<endl;
		}
		else cout<<1<<endl;
	}
	return 0;
}
注意是 \(\le\)。
C
思路
考虑他们两个会采取的策略,通过他们的目的可以知道,他们实际上是希望自己拿的物品价值最高,所以是这样拿的 Alice 拿第一大,Bob 拿第二大,Alice 拿第三大......
考虑排序。
由于我们需要增加序列中的任意一些元素,总共增加 \(k\) 的值,使 Bob 拿到的尽可能大。所以考虑所有的 \(k\) 都给 Bob 拿的物品,即从编号为所有二的倍数的物品。但是!一旦我们通过增加一个物品使得它在物品价值中的排名改变,这个物品就会被 Alice 拿。所以 Bob 的值不能超过 Alice 的值,即将每个编号为二的倍数的物品的价值与编号比它小一的物品价值变为相等。(前提是 \(k\) 足够)于是只需要计算出原来未修改之前的分数,答案就是那个分数减去 \(k\) 与 \(0\) 取 \(\max\)。(代码写的是比较直观,未转化的过程)
Code
#include <bits/stdc++.H>
#define int long long
using namespace std;
const int N=1e6+5;
int a[N],n,k,ans,sum;
signed main() {
    int tt;
    cin>>tt;
    while(tt--) {
        cin>>n>>k;
        for(int i=1;i<=n;i++) cin>>a[i];
        sort(a+1,a+1+n);
        reverse(a+1,a+1+n);
        for(int i=1;i<=n;i+=2) {
            if(i==n) {
                ans+=a[i];
                break;
            }
            sum+=a[i]-a[i+1];
            ans+=a[i]-a[i+1];
        }
        cout<<ans-min(sum,k)<<endl;
        sum=0,ans=0;
    }
    return 0;
}
D
思路
我们研究颜色,发现最多只有四个不同的颜色,于是跳转时,我们的路程中只要有之前出现过的颜色(除入点),那么就可以省去入点,直接从之前出现过那个颜色的地方跳过来。于是我们得出了一个结论,路程中最多只会经过 \(4\) 个颜色。考虑起始点 \(x\) 与终点 \(y\),首先如果它们颜色有重合,那么一定可以直接跳,这样绝对是最短路。考虑它们颜色互不相同,如果想从 \(x\) 到 \(y\) 点,一定需要有一个点,既有 \(x\) 上的一个颜色,也有 \(y\) 上的一个颜色。所以考虑枚举 \(x\) 上的颜色与 \(y\) 上的颜色。这个点可以直接到 \(x\) 与 \(y\),这样颜色的点可能有很多个对吧,所以考虑找出一个点两个颜色固定,使它到 \(x\) 的距离加上它到 \(y\) 的距离最小。首先考虑它与 \(y\) 点在 \(x\) 的同一边,如果我们选 \(x\) 与 \(y\) 之间的点,那么再到 \(y\) 的距离都是一样的对吧,都是 \(x\) 到 \(y\) 的曼哈顿距离,考虑在 \(y\) 之后了,那么还要加上到 \(y\) 的两倍距离,这时离 \(y\) 最近的点就更优对吧(同时也最靠近 \(x\),因为所选点在 \(y\) 之后,\(x\) 与 \(y\) 在选点的同一边),如果有 \(x\) 与 \(y\) 之间的点,那么一定选那些点,优于 \(y\) 之后的点。所以考虑选 \(y\) 那边的点,只需要选择离 \(x\) 近的即可。考虑 \(y\) 对边的点,同样的,离 \(x\) 越远意味着离 \(y\) 越远(同上,所选点在 \(x\) 与 \(y\) 的同一边),所以考虑选离 \(x\) 最近的点。所以最后选离 \(x\) 最近的与 \(y\) 同边与对边的点,即 \(x\) 点最近的左右边点,取路程最少的即可。预处理即可。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5;
string ver[]={"BG","BR","BY","GR","GY","RY"};
int las[N][6],nex[N][6],a[N],n,q;
string putin[N];
signed main() {
	int tt;cin>>tt;
	while(tt--) {
		cin>>n>>q;
		for(int i=1;i<=n;i++) {
			cin>>putin[i];
			a[i]=find(ver,ver+6,putin[i])-ver;
		}
		for(int i=0;i<=n;i++)
          for(int j=0;j<6;j++) las[i][j]=-0x3f3f3f3f3f3f3f3f;
		for(int i=1;i<=n;i++) {
			for(int j=0;j<6;j++) las[i][j]=las[i-1][j];
			las[i][a[i]]=i;
		}
      	for(int i=1;i<=n+1;i++)
          for(int j=0;j<6;j++) nex[i][j]=0x3f3f3f3f3f3f3f3f;
		for(int i=n;i>=1;i--) {
			for(int j=0;j<6;j++) nex[i][j]=nex[i+1][j];
			nex[i][a[i]]=i;
		}
		while(q--) {
			int x,y;cin>>x>>y;
			bool flag=0;
			for(int i=0;i<2;i++)
				for(int j=0;j<2;j++)
					if(putin[x][i]==putin[y][j]) {
						flag=1;
						goto ok;
					}
			ok:
			if(flag) {
				cout<<abs(y-x)<<endl;
				continue;
			}
			int minn=1e9;
			for(int i=0;i<2;i++)
				for(int j=0;j<2;j++) {
					char xx=putin[x][i],yy=putin[y][j];
					if(xx>yy) swap(xx,yy);
					string s;s+=xx;s+=yy;
					int value=find(ver,ver+6,s)-ver;
					minn=min(minn,abs(x-las[x][value])+abs(y-las[x][value]));
					minn=min(minn,abs(x-nex[x][value])+abs(y-nex[x][value]));
				}
			cout<<(minn==(int)1000000000?-1:minn)<<endl;
		}
	}
	return 0;
} 
E
前置知识
思路
有一种天真的做法是求出所有的 SG 值,但是这样的时间复杂度是 \(O(V^2 \log V)\) 的。
但是,实际上,这可以启发我们,借用 CF 题解里的一个过程总结。
1.编写一个能简单计算 SG 值的解决方案;
2.运行它并生成 SG 函数的几个初值;
3.尝试观察一些模式,并利用这些模式制定更快的计算 SG 函数的方法;
4.在较大数字的 SG 值上验证您的模式,并/或尝试证明它们。
第三步可能是最困难的,生成的 SG 值太少可能会导致错误的结论。例如,SG 的前八个值是 \([1,0,2,0,3,0,4,0]\),考虑 \(x\) 是偶数,则可能导致假设 \(sg(x)=0\) 是偶数,考虑 \(x\) 是奇数,则可能导致假设 \(sg(x)=\frac{x+1}{2}\) 是奇数。然而,一旦计算出 \(sg(9)\),这种模式就被打破了,因为 \(sg(9)\) 不是 \(5\) ,而是 \(2\)。
现在,你已经会如何解决求解 \(SG\) 的值的问题了,你可以先关掉这个文章,自己试试看!
现在将讲解如何求解 SG 函数。
首先进行打表,找规律,表具体是这样的。
你同样可以自己对着这个表看看。
发现所有偶数的 SG 值都为 \(0\)。
考虑奇数,首先,是存在一个子序列从 \(1\) 开始一直升序的对吧,但是不是全部都是,所以思考,到底哪些才不属于递增子序列。发现 \(9、15、21\) 都不是,相信你已经知道了,就是合数奇数是不属于子序列的,但它们的值有什么规律呢?经过仔细思考后发现它们的值都是自己最小的质因数的 SG 值,若 \(x=p_{1}^{c_{1}} \cdot ... \cdot p_{k}^{c{k}}\),它的 SG 值即为:
现在问题来了,不可能对于每个 \(a_{i}\) 都分解质因数啊。此时我们突然想到,它的质因数 SG 值是随质因数大小递增的,我们只需要找出那个数的最小质因数即可。此时我们知道线性筛实际上是用一个合数最小的质因数来筛掉它的,于是在线性筛的过程中记录即可。真是完美又热血的组合技啊!
对于奇数非合数,它的值就是自己是第几个奇数非合数,同样是在线性筛记录即可。
时间复杂度 \(O(V + \sum_{i=1}^{q} n_{i})\)。
Code
#include <bits/stdc++.h>
using namespace std;
const int N=1e7+5,M=3e5+5;
int sg[N],pre[N],ni[N];
vector<int> primes;
bool st[N];
void init(int n) {
	ni[1]=1;
	for(int i=2;i<=n;i++) {
		if(!st[i]) primes.push_back(i),ni[i]=(int)primes.size();
		for(int j=0;primes[j]<=n/i;j++) {
			st[i*primes[j]]=1;
			pre[i*primes[j]]=primes[j];
			if(i%primes[j]==0) break;
		}
	}
}
signed main() {
	init((int)1e7);
	int tt;
	cin>>tt;
	while(tt--) {
		int n,sum=0;cin>>n;
		for(int i=1;i<=n;i++) {
			int x;cin>>x;
			if(x%2) {
				if(!st[x]) sum^=ni[x];
				else sum^=ni[pre[x]];
			}
		}
		if(!sum) cout<<"Bob"<<endl;
		else cout<<"Alice"<<endl;
	}
	return 0;
}
                    
                
                
            
        
浙公网安备 33010602011771号