CF1854 题解

发布时间 2023-09-04 22:14:35作者: The_Last_Candy

CF1854 题解

A

首先考虑只有非负的情况,次数完全可以接受 \(19\) 次,所以直接用 \(19\) 次做一次前缀和就可以保证单调不降了。

现在有了负数,考虑将负数变成正数,选出正数当中的最大值,然后用 \(a_i + a_i \to a_i\) 这样自增的方式让它的绝对值大于负数最大值,因为绝对值小于等于 \(20\) ,所以最多 \(5\) 次就可以达到。与其在一个负数上加很多次,显然不如先将正数加到 \(20\) 以上,再只加一次得到结果,但是我们只有 \(31\) 次,减去前缀和 \(19\) 次和这 \(5\) 次,我们还有 \(7\) 次,就是可以加 \(7\) 个负数,那么如果负数太多怎么办呢?

考虑到如果将整个数列取反,就相当于要构造一个不升序列,再 \(reverse\) 过来,就又变成了原问题,但是原来的正负数个数交换了,在上述情况 \(7\) 个负数以上的情况时,既然我们正数要自增 \(5\) 次,那么负数最大值一定大于正数最大值,所以我们翻转后无需自增,直接将新的负数加成正数就好了,次数是 \(19 + 12 = 31\) 次,这个 \(12\) 是因为上段情况不成立时原来的负数一定超过 \(8\) 个,那么新的负数一定小于等于 \(12\) 个,由此得证。

#include<bits/stdc++.h>
using namespace std;
int n,a[25],pa[25];
vector <pair<int,int> > pos;
inline void solve()
{
	int maxpos = 0,minneg = 0,maxi = 0;
	for(int i = 1;i <= n;i++) maxpos = max(maxpos,a[i]),minneg = min(minneg,a[i]);
	if(maxpos == 0 && minneg != 0)
	{
		for(int i = 1;i <= 32;i++) pos.push_back(make_pair(1,1));
		return;
	}
	for(int i = 1;i <= n;i++) if(maxpos == a[i]) maxi = i;
	while(maxpos < -minneg) pos.push_back(make_pair(maxi,maxi)),a[maxi] *= 2,maxpos *= 2;
	for(int i = 1;i <= n;i++) if(a[i] < 0) pos.push_back(make_pair(i,maxi)),a[i] += a[maxi];
	for(int i = 2;i <= n;i++) if(a[i] < a[i - 1]) pos.push_back(make_pair(i,i - 1)),a[i] += a[i - 1];
}
int main()
{
	int T;
	cin>>T;
	while(T--)
	{
		cin>>n;
		for(int i = 1;i <= n;i++) cin>>a[i];
		for(int i = 1;i <= n;i++) pa[i] = a[i];
		solve(); 
		if(pos.size() > 31) 
		{
			pos.clear();
			for(int i = 1;i <= n;i++) a[i] = -pa[i];
			reverse(a + 1,a + n + 1);
			solve();
			cout<<pos.size()<<endl;
			for(auto i : pos) cout<<n + 1 - i.first<<" "<<n + 1 - i.second<<endl;
		}
		else
		{
			cout<<pos.size()<<endl;
			for(auto i : pos) cout<<i.first<<" "<<i.second<<endl;
		}
		pos.clear();
	}
	return 0;
}

B

考虑到一个关键性质:得分和摸牌其实是同一个东西,再加上我们摸到的牌一定是一个区间 \([1,k]\) ,我们假设最后摸了 \(k\) 张牌,那么答案就是 \(\sum_{i = 1}^{k}a_i - (k - 1)\)。相当于摸了 \(k\) 张牌就一定有 \(k - 1\) 个值被拿来摸牌。

所以我们只需要知道能否有一种方案,使得最后恰好摸了 \(k\) 张牌即可,假设 \(dp_i\) 为能否摸到 \(i\) 张牌,每次枚举 \(a_i\) ,有转移: \(dp_j = dp_j\ |\ dp_{j - a_i}\)

我们注意到 \(dp\) 值是一个 \(bool\) 类型,和 \(10^5\) 的数据、 \(3s\) 的时长。

考虑用 \(bitset\) 优化,每次将 \(dp\) 数组整体左移 \(a_i\) 再或一下即可。注意就算超过了 \(n\) 也要减掉,所以数组大小开两倍。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
bitset <N * 2> dp,able;
long long a[N],s[N],n;
int main()
{
	cin>>n;
	for(int i = 1;i <= n;i++) cin>>a[i],s[i] = a[i] + s[i - 1];
	dp[1] = 1;able[1] = 1;
	for(int i = 1;i <= n;i++) 
	{
		dp |= (dp << a[i]);
		able[i + 1] = dp[i + 1];
		dp[i] = 0;
	}
	long long ans = 0;
	for(int i = 1;i <= n;i++) if(able[i]) ans = max(ans,s[i] - i + 1);
	for(int i = n + 1;i <= 2 * n;i++) if(dp[i]) ans = max(ans,s[n] - i + 1);
	cout<<ans;
	return 0;
}

C

期望 \(dp\) 题目,这题不好设状态,于是我们一个一个条件来。考虑没有“相撞”的答案,就相当于每个数到 \(m + 1\) 的距离之和。即 \(\sum_im + 1 - a_i\) 。由于有了相撞,我们多算了一些东西,考虑到相撞的概率只和 \(3\) 个元素有关:相撞的两个数和相撞位置,设数为 \(x,y\) ,位置为 \(z\) 。那么期望就要减去 \(p_{x,y,z} \times (m + 1 - z)\)

发现只有相邻的两个数字会相撞(不是的话可以等效)。所以考虑每一对数 \(a_i,a_{i + 1}\) 。设 \(f_{x,y}\) 表示两个数分别在 \(x,y\) 位置的概率,首先将 \(f_{a_i,a_{i + 1}}\) 设为 \(1\) 。然后由于只考虑两个数的移动,分别以 \(\frac 12\) 的概率将 \(f_{x,y}\) 转移到 \(f_{x + 1,y},f_{x,y + 1}\) 即可。从小到大枚举左端点,那么 \(f_{x,x}\) 就是两数在 \(x\) 处相撞的概率。时间复杂度 \(O(nm^2)\)

由于最后我们要将减掉的东西加起来,所以根据期望线性性,我们可以将每一对数一起处理,只用 \(dp\) 一遍,时间复杂度 \(O(m^2)\)

#include<bits/stdc++.h>
using namespace std;
const int N = 505,MOD = 1e9 + 7;
typedef long long ll;
ll a[N],n,m,dp[N][N];
inline ll ksm(ll base,ll pts)
{
	ll ret = 1;
	for(;pts > 0;pts >>= 1,base = base * base % MOD)
		if(pts & 1)
			ret = ret * base % MOD;
	return ret;
}
int main()
{
	cin>>n>>m;
	ll sig = 0;
	for(int i = 1;i <= n;i++) cin>>a[i],sig += (m + 1 - a[i]);
	memset(dp,0,sizeof(dp));
	for(int i = 1;i <= n - 1;i++) dp[a[i]][a[i + 1]] = 1;
	ll ans = sig;
	for(int i = 1;i <= m;i++)
	{
		ans = (ans - dp[i][i] * (m + 1 - i) % MOD + MOD) % MOD;
		for(int j = i + 1;j <= m;j++)
		{
			ll iv = ksm(2,MOD - 2);
			dp[i + 1][j] = (dp[i + 1][j] + iv * dp[i][j] % MOD) % MOD;
			dp[i][j + 1] = (dp[i][j + 1] + iv * dp[i][j] % MOD) % MOD;
		}
	}
	cout<<ans;
	return 0;
}

D

题意就是求出 \(1\) 所在树的所有节点,考虑如果找出环上的每一个点,就可以询问其他所有点,如果 \(x\)\(n\) 步可以到达环上的点,那么就在这棵树中。

首先,我们用一个二分法找到环上的第一个点,我们将已有的点集分为两部分,询问 \(1\) 是否可以走 \(n\) 步到达左半边,如果可以就取左半边,否则就取右半边。这样每次询问次数是 \(\lceil \log 500\rceil = 9\)

已知环上的一个点集 \(G\) ,我们有两种做法:

1.我们可以用上次询问的点 \(x\) ,观察它一步走到的是哪个点,这个点一定在环上,如果这个点已经被访问过,就说明已经找到了整个环,但是这样次数是 \(size \times 9\) 的,太多。

2.我们可以使用倍增,每次暴力扫描每个点,观察到目前点集 \(G\) 是否可以走 \(|G|\) 步到达。如果可以,要么是环上的前 \(|G|\) 个点,要么是树上的一些节点,但是树上的点最后还是要计入答案,所以不影响。这样的次数是 \(n - 1 + n - 2 + n - 4 + n - 8 + \dots\) ,还是太多。

我们发现,前一种方法适用于 \(|G|\) 很小的情况,后面适用于 \(|G|\) 很大的情况,于是整合这两个方法,就可以控制询问在 \(2000\) 次以内,计算得出第一种方法暴力找 \(63\) 个点时最优。最后询问每个点能否走 \(n\) 步到 \(G\) 即可。

#include<bits/stdc++.h>
using namespace std;
const int N = 2005,p = 63;
int vis[N],n;
vector <int> G;
inline int q(int u,int k,vector <int> v)
{
	cout<<"? "<<u<<" "<<k<<" "<<v.size()<<" ";
	for(auto in : v) cout<<in<<" ";
	cout<<endl;
	int ret;
	cin>>ret;
	return ret;
}
inline int ask1()
{
	vector <int> l,r,d;
	for(int i = 1;i <= n;i++) d.push_back(i);
	while(d.size() > 1)
	{
		l.clear();r.clear();
		for(int i = 0;i < d.size() / 2;i++) l.push_back(d[i]);
		for(int i = d.size() / 2;i < d.size();i++) r.push_back(d[i]);
		d.clear(); 
		if(q(1,n,l)) for(auto in : l) d.push_back(in);
		else for(auto in : r) d.push_back(in);
	} 
	return d[0];
}
inline int ask(int x)
{
	vector <int> l,r,d;
	for(int i = 1;i <= n;i++) d.push_back(i);
	while(d.size() > 1)
	{
		l.clear();r.clear();
		for(int i = 0;i < d.size() / 2;i++) l.push_back(d[i]);
		for(int i = d.size() / 2;i < d.size();i++) r.push_back(d[i]);
		d.clear();
		if(q(x,1,l)) for(auto in : l) d.push_back(in);
		else for(auto in : r) d.push_back(in);
	} 
	return d[0];
}
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	cin>>n;
	G.push_back(ask1());
	vis[G[0]] = 1;
	for(int i = 2;i <= p;i++) 
	{
		int ret = ask(G[i - 2]);
		if(vis[ret] == 1) break;
		else vis[ret] = 1,G.push_back(ret);
	}
	if(G.size() == p)
	{
		int shz = G.size();
		while(1)
		{
			for(int i = 1;i <= n;i++)
			{
				if(vis[i]) continue;
				if(q(i,shz,G)) G.push_back(i),vis[i] = 1;
			}	
			shz *= 2;
			if(shz > G.size()) break;
		}	
	}
	for(int i = 1;i <= n;i++)
	{
		if(vis[i]) continue;
		if(q(i,n,G)) G.push_back(i),vis[i] = 1;
	}
	cout<<"! "<<G.size()<<" ";
	for(auto i : G) cout<<i<<" ";
	cout<<endl;
	return 0;
}

E

考虑随机化,子序列中大于 \(30\) 的数最多只有一个,所以我们 \(rand\)\(30\) 个数,要求 \(\leq 30\) ,然后求出 \(f_i\) ,表示子序列和等于 \(i\) 时的情况数。然后将 \(31 - 60\) 按照 \(f_{60 - i}\) 从大到小排序,然后只要 \(m\) 还大于 \(f_{60 - p_i}\) ,就在序列末尾加上一个 \(p_i\) 。如果最后凑到了 \(0\) ,就输出方案,不然就全部重来。考虑最终方案有很多种,所以玄学地可以 \(AC\)

#include<bits/stdc++.h>
using namespace std;
const int N = 105;
typedef long long ll;
ll m,dp[N],a[N],p[N],n,top;
inline int qr(int l,int r)
{
	return l + rand() % (r - l + 1);
}
inline bool cmp(int x,int y){return dp[60 - x] > dp[60 - y];}
int main()
{
	srand(time(NULL));
	cin>>m;
	if(m <= 60)
	{
		cout<<m<<endl;
		for(int i = 1;i <= m;i++) cout<<"60 ";
		return 0;
	}
	for(int i = 1;i <= 30;i++) p[i] = 30 + i;
	while(1)
	{
		memset(dp,0,sizeof(dp));
		dp[0] = 1;
		n = 0;
		top = qr(2,30);
		for(int i = 1;i <= 60;i++)
		{
			a[++n] = rand() % top + 1;
			if(dp[60] + dp[60 - a[n]] > m) {n--;break;}
			for(int j = 60;j >= a[n];j--) dp[j] += dp[j - a[n]];
		}
		ll lft = m - dp[60];
		sort(p + 1,p + 31,cmp);
		for(int i = 1;i <= 30;i++)
			while(n < 60 && lft >= dp[60 - p[i]]) a[++n] = p[i],lft -= dp[60 - p[i]];
		if(!lft)
		{
			cout<<n<<endl;
			for(int i = 1;i <= n;i++) cout<<a[i]<<" ";
			return 0;
		}
	}
	return 0;
 } 

F

咕。