2022杭电多校联赛第八场 题解

比赛传送门
作者: fn


签到题

1004题 Quel’Thalas / 奎尔萨拉斯(幻方)

题目大意
有一个幻方,其中包含二维平面上的所有点,其坐标为 [ 0 , n ] [0,n] [0,n] 内的整数。
他能在平面上画几条直线。每条线将覆盖其上的所有点。这些线没有端点,并且在两个方向上延伸到无穷远。
还有一条特殊规则:他的线不能经过点 ( 0 , 0 ) (0,0) (0,0)

他需要画多少条线才能覆盖除 ( 0 , 0 ) (0,0) (0,0) 之外的幻方的所有点?

考察内容
签到

分析

斜着切就可以。答案是 2 n 2n 2n

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
signed main(){
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    int t;
    cin>>t;
    while(t--){
		int n;
		cin>>n;
        cout<<2*n<<endl;
	}
    return 0;
}

1001题 Theramore / 塞拉摩岛(翻转字符串)

题目大意
给定一个 01 序列,可以翻转奇数长度的区间任意次。求字典序最小的结果。

考察内容
思维

分析
每次翻转不能改变下标的奇偶性,所以统计奇偶下标的0和1的个数,按照字典序最小的情况放置即可。

#include<bits/stdc++.h>
#define ll long long
#define cer(x) cerr<<(#x)<<" = "<<(x)<<'\n'
#define endl '\n'
using namespace std;
const int N=1e5+5;
ll n,m,a[N];
string s;
char ch[N];

int main(){ 
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int t; cin>>t;
	while(t--){
		ll numodd[2]={0};
		ll numeven[2]={0};
		
		cin>>s;
		ll len=s.size();
		
		for(int i=0;i<len;i+=2){
			if(s[i]=='0'){
				numeven[0]++;
			}
			else{
				numeven[1]++;
			}
		}
		for(int i=1;i<len;i+=2){
			if(s[i]=='0'){
				numodd[0]++;
			}
			else{
				numodd[1]++;
			}
		}
		
		for(int i=len-1;i>=0;i--){
			if(i%2==1){
				if(numodd[1]>0){
					ch[i]='1';
					numodd[1]--;
				}
				else{
					ch[i]='0';
				}
			}
			else{
				if(numeven[1]>0){
					ch[i]='1';
					numeven[1]--;
				}
				else{
					ch[i]='0';
				}
			}
		}
		for(int i=0;i<=len-1;i++){
			cout<<ch[i];
		}
		cout<<endl;
	}
	return 0;
}

基本题

1011题 Stormwind / 暴风城(分割矩形)

题目大意

给定一个 n ∗ m n*m nm 的长方形,沿水平或竖直方向画若干条线,每条线的两端点都在长方形的边界上。要求这些线划分出的每个小长方形面积都 ≥ k ≥k k
求最多可以画几条线。

考察内容
贪心

分析
贪心策略:

  1. 让一边切的次数尽可能多,然后再去切另一边。
  2. 尽可能把每块切成一样的。

考虑两边(先切一边,后切另外一边),取两者的最大值即可。
时间复杂度 O ( 1 ) O(1) O(1)

#include<bits/stdc++.h>
#define ll long long
#define int long long
#define cer(x) cerr<<(#x)<<" = "<<(x)<<'\n'
#define endl '\n'
using namespace std;
ll n,m,k;

signed main(){ 
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int t; cin>>t;
	while(t--){
		cin>>n>>m>>k;
		
		if(n*m<k){ 
			cout<<0<<endl;
			continue;
		}
		
		ll ans=0;
		
		if(m>=k){ // 切成条 
			ll cnt=n-1;
			cnt+=m/k-1;
			ans=max(ans,cnt);
		}
		else{ // m<k
			ll d=(k+m-1)/m; 
			ll cnt=n/d-1;
			ans=max(ans,cnt);
		} 
		
		if(n>=k){ // 从另外一边切
			ll cnt=m-1;
			cnt+=n/k-1;
			ans=max(ans,cnt);
		}
		else{ // n<k
			ll d=(k+n-1)/n; 
			ll cnt=m/d-1;
			ans=max(ans,cnt);
		} 
		cout<<ans<<endl;
	}
	return 0;
}
/*
1
7 6 14

2


1
5 4 5

3

*/ 

1008题 Orgrimmar / 奥格瑞玛(树上最大分离集)

题目大意
求一棵树的最大分离集的大小。
分离集:一个顶点集,其中所有顶点的度数 ≤ 1 \leq1 1

考察内容
树形dp

分析
状态:
对于每个结点 i i i
f [ 2 ] [ i ] f[2][i] f[2][i] : 选 i i i 且下面有选的答案。
f [ 1 ] [ i ] f[1][i] f[1][i] : 选 i i i 且下面没选的答案。
f [ 0 ] [ i ] f[0][i] f[0][i] : 不选 i i i 时的答案。

转移:
跑一遍dfs。

void dfs(int x,int fa){
	ll sum0=0;
	ll sum=0;
	for(auto a1:g[x]){ // 遍历子树 
		if(a1==fa)continue;
		
		dfs(a1,x); 
		sum0+=f[0][a1]; // 下面没选
		sum+=max(f[2][a1],max(f[1][a1],f[0][a1]));
	}
	f[1][x]=sum0+1; // 选这个点,且下面没选 
	f[0][x]=sum; // 不选这个点 
	
	ll sum2=-1e18; 
	for(auto a1:g[x]){ // 遍历子树,子树可以选1个下面没选的 
		if(a1==fa)continue;
		
		sum2=max(sum2,sum0-f[0][a1]+f[1][a1]);
	}
	f[2][x]=sum2+1; 
}

完整代码:

#include<bits/stdc++.h>
#define ll long long
#define int long long 
#define cer(x) cerr<<(#x)<<" = "<<(x)<<'\n'
#define endl '\n'
using namespace std;
ll read(ll &n){
	char ch=' '; ll q=0,w=1;
	for(;(ch!='-')&&((ch<'0')||(ch>'9'));ch=getchar());
	if(ch=='-')w=-1,ch=getchar();
	for(;ch>='0'&&ch<='9';ch=getchar())q=q*10+ch-48;
	n=q*w; return n;
}
const int N=5e5+5;
ll n;
vector<ll> g[N];

ll f[3][N]; // f[2]:选i且下面有选的答案 f[1]:选i且下面没选的答案  f[0]:不选i时的答案 

void init(ll n){ // 初始化 
	for(int i=0;i<=n;i++){
		f[0][i]=f[1][i]=f[2][i]=0;  
		g[i].clear();
	}
}

void dfs(int x,int fa){
	ll sum0=0;
	ll sum=0;
	for(auto a1:g[x]){ // 遍历子树 
		if(a1==fa)continue;
		
		dfs(a1,x); 
		sum0+=f[0][a1]; // 下面没选
		sum+=max(f[2][a1],max(f[1][a1],f[0][a1]));
	}
	f[1][x]=sum0+1; // 选这个点,且下面没选 
	f[0][x]=sum; // 不选这个点 
	
	ll sum2=-1e18; 
	for(auto a1:g[x]){ // 遍历子树,子树可以选1个下面没选的 
		if(a1==fa)continue;
		
		sum2=max(sum2,sum0-f[0][a1]+f[1][a1]);
	}
	f[2][x]=sum2+1; 
}

signed main(){ // AC
    int size(512<<20);  // 512M,536870912
	__asm__ ( "movq %0, %%rsp\n"::"r"((char*)malloc(size)+size)); // 给栈扩容 

	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	int t; read(t);
	while(t--){
		read(n);
		init(n);
		for(int i=1;i<=n-1;i++){
			ll u0,v0;
			read(u0); read(v0);
			g[u0].push_back(v0);
			g[v0].push_back(u0);
		}
	
		int root=1;
		dfs(root,0);
		
		ll ans=max(f[0][root],f[1][root]);
		ans=max(ans,f[2][root]);
		
		cout<<ans<<endl;
	}
    exit(0);
}
/*
1
10
1 2
2 4
3 2
5 3
6 4
7 5
8 3
9 8
10 7


1
7
1 3
2 3
3 4
4 5
5 6
5 7

正确输出:
5

*/ 

进阶题

1007题 Darnassus / 达纳苏斯(最小生成树)

题目大意
给定一个排列 p p p ,把每个位置视为点,建一个无向图, i , j i,j i,j 之间的边权为 ∣ i − j ∣ ∗ ∣ p i − p j ∣ |i−j|∗|pi−pj| ijpipj
求这个图的最小生成树。

考察内容
最小生成树,Kruskal 算法,并查集

分析
1007题

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 50005;

template <typename T> inline void read(T &WOW) {
    T x = 0, flag = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-') flag = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - '0';
        ch = getchar();
    }
    WOW = flag * x;
}

int n, p[MAXN], pos[MAXN], ufs[MAXN];

int getf(int x) {
    return (ufs[x] == x)? x : ufs[x] = getf(ufs[x]);
}

struct Edge {
    int u, v, nxt;
} e[MAXN * 460];
int first[MAXN], eCnt;

inline void AddEdge(int w, int u, int v) {
    e[++eCnt].u = u;
    e[eCnt].v = v;
    e[eCnt].nxt = first[w];
    first[w] = eCnt;
}

void solve() {
    read(n);
    for (int i = 1; i <= n; ++i) {
        read(p[i]);
        pos[p[i]] = i;
        ufs[i] = i;
        first[i] = 0;
    }
    eCnt = 0;
    int m = sqrt(n);
    for (int i = 1; i <= n; ++i) {
        for (int j = i + 1; j <= i + m && j <= n; ++j) {
            int tmp = (j - i) * abs(p[j] - p[i]);
            if (tmp < n) {
                AddEdge(tmp, i, j);
            }
            tmp = (j - i) * abs(pos[j] - pos[i]);
            if (tmp < n) {
                AddEdge(tmp, pos[i], pos[j]);
            }
        }
    }
    ll ans = 0;
    int cnt = n - 1;
    for (int i = 1; i < n; ++i) {
        for (int j = first[i]; j; j = e[j].nxt) {
            int u = getf(e[j].u), v = getf(e[j].v);
            if (u == v) continue;
            ufs[u] = v;
            ans += i;
            --cnt;
        }
        if (cnt == 0) break;
    }
    printf("%lld\n", ans);
}

int main() {
    int T; read(T);
    while (T--) {
        solve();
    }
    return 0;
}

1005题 Ironforge / 铁炉堡(铁炉堡的地铁)

题目大意

给定一条 n n n 个顶点的链,每个点上有一个数,每条边上有一个质数。
带着一个空背包从某个点出发,走到一个点上可以把它上面数字的所有质因子放进背包,一条边如果背包里有那个质数就可以走。允许多次通过每个顶点和每条边。

m m m 次询问,每次回答点 x x x 是否可以到达点 y y y

2 ≤ n , m ≤ 2 × 1 0 5 2≤n,m≤2×10^5 2n,m2×105

考察内容
双指针,二分,质数筛,复杂度优化

分析

预处理每个点能走到的范围。

2 ≤ n , m ≤ 2 × 1 0 5 2≤n,m≤2×10^5 2n,m2×105 ,要做到 O ( n l o g n ) O(nlogn) O(nlogn)

两个关键的地方:

  1. 先从右往左枚举一遍只向右走的最大范围,再从左往右枚举向左右两边走的范围。

  2. 判断一个范围内是否存在某个质因子的方法:对每个质因子维护一个 vector 存它出现的所有位置,在 vector 上二分。

#include<bits/stdc++.h>
#define cer(x) cerr<<(#x)<<" = "<<(x)<<'\n'
using namespace std;
typedef long long ll;
const int MAXN = 200005;

template <typename T> inline void read(T &WOW) {
    T x = 0, flag = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-') flag = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - '0';
        ch = getchar();
    }
    WOW = flag * x;
}

int n, m, a[MAXN], b[MAXN];
int L[MAXN], R[MAXN]; // 往左右能走的范围 

int pr[MAXN], pcnt, vis[MAXN], low[MAXN];
vector<int> pos[MAXN];

bool Check(int p, int l, int r) { // 询问在区间[l,r]是否有质数p 
    if (!pos[p].size() || pos[p].back() < l) return 0;
    int x = *lower_bound(pos[p].begin(), pos[p].end(), l);
    return (x <= r);
}

void sieve() { // 筛质数
    n = 200000;
    for (int i = 2; i <= n; ++i) {
        if (!vis[i]) {
            pr[++pcnt] = i;
            low[i] = i;
        }
        for (int j = 1; j <= pcnt && i * pr[j] <= n; ++j) {
            vis[i * pr[j]] = 1;
            low[i * pr[j]] = pr[j];
            if (i % pr[j] == 0) break;
        }
    }
}

void solve() {
    read(n); read(m); // 读入数据 
    for (int i = 1; i <= n; ++i) {
        read(a[i]);
    }
    for (int i = 1; i < n; ++i) {
        read(b[i]);
    }
    for (int i = 0; i <= n; ++i){
    	pos[i].clear(); // 清空pos
    }
    
    for (int i = 1; i <= n; ++i) {
        int x = a[i];
        while (x > 1) {
            int nowp = low[x];
            while (nowp == low[x]) {
                x /= nowp;
            }
            pos[nowp].push_back(i);
        }
    }
    
	for (int i = n; i >= 1; --i) { // 先从右往左枚举
        R[i] = i;
        while (R[i] < n && Check(b[R[i]], i, R[i])) {
            R[i] = R[R[i] + 1];
        }
    }
    for (int i = 1; i <= n; ++i) { // 再从左往右枚举
        if (i > 1 && R[i - 1] >= i) {
            if (Check(b[i - 1], i, R[i])) {
                L[i] = L[i - 1];
                R[i] = R[i - 1];
            } 
			else {
                L[i] = i;
            }
        } 
		else {
            L[i] = i;
            bool flag = 1;
            while (flag) { // 上一轮有在更新 
            	flag = 0;
                while (R[i] < n && Check(b[R[i]], L[i], R[i])) { // 向右更新 
                    flag = 1;
                    R[i] = R[R[i] + 1];
                }
                while (L[i] > 1 && Check(b[L[i] - 1], L[i], R[i])) { // 向左更新 
                    flag = 1;
                    L[i] = L[L[i] - 1];
                }
            }
        }
    }

    for (int i = 1, x, y; i <= m; ++i) { // 处理询问 
        read(x); read(y);
        if (L[x] <= y && y <= R[x]) {
            puts("Yes");
        } else {
            puts("No");
        }
    }
}

int main() {
    sieve(); // 筛一下质数 
    int T; read(T);
    while (T--) {
        solve();
    }
    return 0;
}

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值