2020 CCPC 秦皇岛 - K(树形dp+贪心),G(数论),E(尺取)

K. Kingdom’s Power

题意
给定一个树,可以从根节点发出无数个士兵。
但是每个时刻只能让树中的一个士兵移动。士兵每移动一个节点该节点就被占领。
问,占领所有节点至少需要多少时刻?

思路
如果只从根节点发出一个士兵跑完所有节点的话,因为最后遍历所有节点之后不必回来,所以其遍历的顺序一定是先遍历深度较小的子树,再遍历深度较大的子树,最终停留在深度最深的地方
那么就应该预处理出从每个节点能够到达的最深深度,将每个节点的所有子节点按照深度从小到大的顺序遍历

但是这道题可以从根节点无数次发出士兵,所以在上面的前提下,还应该考虑到下一个子节点时,是从上一个子节点所在子树的最长链末端回来,还是从根节点重新发出一个士兵

可以用 pre[] 数组记录从根节点 u 遍历的上一个子节点所在子树的最长链末端 t,然后到达一个点x时,取 dep[x]dep[t] - dep[u] + 1 的较小值,作为到达该节点的花费,答案累加。
(在树中,子节点和其祖先节点的距离是确定的,为其深度之差)

void dfs2(int x)
{
	pre[x] = x;
	for(auto tx : e[x])
	{
		sum += min(dep[tx], dep[pre[x]] - dep[x] + 1);
		dfs2(tx);
		pre[x] = pre[tx];
	}
}

首先将当前节点的 pre[x] 设为当前值,因为第一次遍历的节点不能从上一次遍历的子节点最底端转移,只能从其父节点过来。
在递归过一个子树的所有节点之后,将最后一个节点一层一层传递上来,最后传递到该子树的根节点,供下次遍历的子节点判断。
而这个传递的过程,就是在递归到下一节点 tx 之后,令 pre[x] = pre[tx]。此时,pre[tx] 存储的已经是到达最底端的节点编号,将其赋值为该子树根节点 x 的 pre[] 数组。
太妙了~

#include<bits/stdc++.h>
using namespace std;

#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define ll long long
#define endl '\n'

const int N = 2000010, mod = 1e9+7;
int T, n, m, k;
ll a[N], dep[N], maxa[N];
ll val[N], sum;
int pre[N];
vector<int> e[N];

bool cmp(int x, int y){
	return maxa[x] < maxa[y];
}

void dfs1(int x)
{
	for(auto tx : e[x])
	{
		dep[tx] = dep[x] + 1;
		maxa[tx] = dep[tx];
		dfs1(tx);
		maxa[x] = max(maxa[x], maxa[tx]);
	}
}

ll dfs2(int x)
{
	pre[x] = x;
	for(auto tx : e[x])
	{
		sum += min(dep[tx], dep[pre[x]] - dep[x] + 1);
		dfs2(tx);
		pre[x] = pre[tx];
	}
}

signed main(){
	Ios;
	cin >> T;
	for(int cs=1;cs<=T;cs ++)
	{
		cin >> n;
		for(int i=1;i<=n;i++)
			e[i].clear(), dep[i] = maxa[i] = val[i] = pre[i] = 0;
		
		for(int i=2;i<=n;i++)
		{
			int x; cin >> x;
			e[x].pb(i);
		}
		
		dfs1(1);
		
		for(int i=1;i<=n;i++)
			sort(e[i].begin(), e[i].end(), cmp);
		
		sum = 0;
		dfs2(1);
		printf("Case #%d: %lld\n", cs, sum);
	}
	
	return 0;
}

还有一种做法,没看懂,放一下吧

#include<bits/stdc++.h>
using namespace std;

#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define mem(a,b) memset(a,b,sizeof a)
#define ll long long
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'

map<int,int> mp;

const int N = 2000010, mod = 1e9+7;
int T, n, m, k;
ll a[N], dep[N], maxa[N];
ll val[N], sum;
vector<int> e[N];

bool cmp(int x, int y){
	return maxa[x] < maxa[y];
}

void dfs1(int x)
{
	for(auto tx : e[x])
	{
		dep[tx] = dep[x] + 1;
		maxa[tx] = dep[tx];
		dfs1(tx);
		maxa[x] = max(maxa[x], maxa[tx]);
	}
}

ll dfs2(int x, int dis) //返回值是自底向上的最短距离,dis表示从上个点到该点的最小步数 
{
	val[x] = dis;
	if(!e[x].size()) return 1;
	
	ll mina = dis;
	for(int i = 0; i < e[x].size(); i++)
	{
		int tx = e[x][i];
		mina = min(dep[x], dfs2(tx, mina + 1));
	}
	return mina + 1;
}

signed main(){
//	Ios;
	cin >> T;
	for(int cs=1;cs<=T;cs ++)
	{
		cin >> n;
		for(int i=1;i<=n;i++) e[i].clear(), dep[i] = maxa[i] = val[i] = 0;
		
		for(int i=2;i<=n;i++)
		{
			int x; cin >> x;
			e[x].pb(i);
		}
		
		dfs1(1);
		
		for(int i=1;i<=n;i++)
			sort(e[i].begin(), e[i].end(), cmp);
		
		sum = 0;
		dfs2(1, 0);
		for(int i=1;i<=n;i++)
		{
			cout << val[i] << " ";
			if(!e[i].size()) sum += val[i];
		}
		cout << endl;
//		printf("Case #%d: %lld\n", cs, sum);
	}
	
	return 0;
}

G. Good Number

题意
给定两个数 n 和 k,判断 1~n 中有多少数 x 满足: x x x % ⌊ x k ⌋ \lfloor \sqrt[k]{x} \rfloor kx = 0.
( 1 ≤ n , k ≤ 1 0 9 ) (1 \le n,k \le 10^9) (1n,k109)

思路
因为是取根号后下取整,所以如果 x 满足,x 是 t 的倍数并且 t k ≤ x < t k + 1 t^k ≤ x<t^{k+1} tkxtk+1,那么 x 就是合法。
所以可以遍历 t,遍历 t k t^k tk t k + 1 − 1 t^{k+1}-1 tk+11 中不超过 n 的 t 的倍数,个数每次+1。
但是一点点遍历倍数太慢了,有没有更快的方法得到区间 [ t k t^k tk, t k + 1 − 1 t^{k+1}-1 tk+11] 中 t 的倍数的个数呢?
两端点对于 t 的商相减。因为 t k t^k tk 一定是 t 的 倍数,所以区间中 t 的倍数个数为 r t − l t + 1 \frac rt - \frac lt + 1 trtl+1

此外要注意,当 k 很大的时候,t^k 会爆 longlong。
但是注意到,当 k = 32 时,2^32 就已经超过 n 最大值 1e9 了,当 t 遍历到 1 的时候右端点就超过 n 了,所以当 k≥32 时,1~n 中的所有数都是满足的。直接特判。

Code:

#include<bits/stdc++.h>
using namespace std;

const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N];
int ans;

int qmi(int x, int y)
{
	int ans = 1;
	while(y)
	{
		if(y & 1) ans = ans*x;
		y >>= 1;
		x = x*x;
	}
	return ans;
}

void pd(int n, int k)
{
	ans = 0;
	
	for(int i=1;;i++)
	{
		int x = qmi(i, k), y = min(n, qmi(i+1, k)-1);
		if(x > n) break;
		ans += y/i - x/i + 1;
	}
}

signed main(){
	scanf("%lld", &T);
	
	int k;
	for(int cs = 1;cs<=T;cs++)
	{
		scanf("%lld%lld", &n, &k);
		
		if(k==1 || k >= 32){ //特判一下较大数 
			printf("Case #%lld: %lld\n", cs, n);
			continue;
		}
		pd(n, k);
		printf("Case #%lld: %lld\n", cs, ans);
	}
	
	return 0;
}

经验

  • 大数要知道特判掉;
  • O(1) 求得一个区间中 x 倍数的个数;
  • 复习一遍快速幂。

E. Exam Results

题意
有 n 位同学,每位同学都有较高成绩 a i a_i ai 和 较低成绩 b i b_i bi
对于一次考试,每位同学都会得到两种成绩中的一种。
假设所有同学中最高分数为 x x x,那么得分不低于 x ⋅ p % x \cdot p\% xp% 的同学都会通过考试。
问,在所有得分方案中,通过考试的同学最多有多少个?

思路
将所有成绩从大到小排序后,遍历所有成绩作为最高成绩,然后尺取找到最后一个不低于其 %p 成绩的位置。
因为对于最高成绩从大到小遍历,其 %p 成绩的位置也是逐渐递减的。所以可以用一个指针不断后移,省掉一重循环,复杂度 O(n)。
设当前遍历的最高成绩的位置 iidx 为找到的最后一个不低于 %p 的位置。

  • 如果 idx+1 位置的成绩也是满足不低于 %p 的,那么 idx 就可以 +1,不断后移。
  • 对于新加进来的位置,如果该同学没有在区间中,那么 cnt++;
  • 将能够加进来的位置都加进来后,ans 和 cnt 取 max;
  • 接下来该遍历到下一个位置,那么就把当前位置从区间中去掉。如果该同学在区间中出现次数为1,那么去掉后区间中将没有该同学,所以要将 cnt–。

那么如何通过一个位置(成绩)来判断是哪位同学的成绩呢?
一开始想的是 map 标记成绩,但是成绩不是唯一的啊。
后面看题解发现,可以对每个成绩设置结构体,其元素为具体值和同学编号。 这样排过序之后也可以找到每个成绩是哪位同学的。

坑点:
所有的同学都应该有成绩,所以我们所枚举的最高成绩应该不小于所有同学较低成绩的最大值!

总复杂度O(nlogn)。

Code:

#include<bits/stdc++.h>
using namespace std;

#define ll long long

const int N = 1000010, mod = 1e9+7;
int T, n, m, p;
struct node{
	ll x;
	int pos;
}a[N];
int f[N];

bool cmp(node a, node b){
	return a.x > b.x;
}

signed main(){
	scanf("%d", &T);
	for(int cs=1;cs<=T;cs++)
	{
		scanf("%d%d", &n, &p);

		int mina = 0;
		for(int i=1;i<=n;i++){
			int x, y; scanf("%d%d", &x, &y);
			a[i].x = x, a[i].pos = i;
			a[n+i].x = y, a[n+i].pos = i;
			f[i] = 0;
			
			mina = max(mina, y); //
		}
		
		n *= 2;
		sort(a+1, a+n+1, cmp);
		
		int idx = 0, cnt = 0, ans = 0;
		for(int i=1;i<=n;i++)
		{
			if(a[i].x < mina) break; // 
			
			while(idx < n && a[idx+1].x*100ll >= a[i].x*p)
			{
				idx++;
				f[a[idx].pos] ++;
				if(f[a[idx].pos] == 1) cnt++;
			}
			
			ans = max(ans, cnt);
			
			f[a[i].pos]--;
			if(f[a[i].pos] == 0) cnt--;
		}
		
		printf("Case #%d: %d\n", cs, ans);
	}
	
	return 0;
}

经验
很好的一道题。一开始看到这题的时候一点思路都没有,总想着这两个成绩取一个该咋取。。
尺取的题目最近也没怎么做,算是一个很好的复习吧。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值