传智杯-省赛-第二场(B组)题解

补题链接

小苯点兵点将

思路:签到题,从lr遍历判断可以;也可以求出 [1,l-1] 中 3 倍数的个数,和 [1,𝑟]中 3 倍数的个数,作差判断结果是否大于等于 1

void solve()
{
    int l, r;
    cin >> l >> r;
    if (r/3-(l-1)/3 >= 1) cout << "YES" << endl;
    else cout << "NO" << endl;
}
void solve()
{
    int l, r;
    cin >> l >> r;
    for (int i = l; i <= r; i++){
        if (i% 3 == 0){
            cout << "YES" << endl;
            return;
        }
    }
    cout << "NO" << endl;
}

小苯的好数

思路:区间查询很容易想到前缀和来做,因此就是考怎么来判断是否是好数,这里a[i]为偶数时, 肯定是好数,主要就是判断奇数怎么是好数,这里正解是要证明的(但我太菜了,只能打表来观察了),得出哪些奇数是好数,哪些不是:接下来是打表函数:

void solve()
{
	for (int i = 1; i<=99; i++){
		if (i%2 == 1){
			int sum = 1;
			for (int j = 2; j<i; j++){
				if (i%j == 0) sum += j;
			}
			if (sum % 2 == 0) cout << i << endl;
		}
	}
}

观察出只有平方数的因子和才能是偶数,所以就可以使用前缀和来解决此题

void solve() {
    int n, q;
    cin >> n >> q;
    vector<int> s(n + 1);
    auto check = [&](int x) -> bool {
        if(x % 2 == 0) return 1;
        int sq = sqrt(x);
        return sq * sq == x;
    };
    for(int i = 1, x; i <= n; i++) {
        cin >> x;
        s[i] = s[i - 1] + check(x);
    }
    while(q -- ) {
        int l, r;
        cin >> l >> r;
        cout << s[r] - s[l - 1] << endl;
    }
}

小苯的ovo

思路:动态规划,有点像背包问题,这里要找出cost为修改次数,并通过动态转移。注意dp数组的初始化

void solve()
{
    int n, m, k;
    cin >> n >> k;
    vector<vector<int>> dp(n+1, vector<int>(k+1, -INF));
    // dp[i][k]表示前i个字符,最多修改k次的最大个数
    string s;
    cin >> s;
    s = " "+s;
    if (n < 3){
        cout << 0 << endl;
        return;
    }
    dp[0][0] = 0, dp[1][0] = 0, dp[2][0] = 0;
    for (int i = 3; i<=n; i++){
        int num = (s[i-2]!='o')+(s[i-1]!='v')+(s[i]!='o');
        dp[i] = dp[i-1];
        for (int j = num; j<=k; j++){
            dp[i][j] = max(dp[i][j], dp[i-3][j-num]+1);
        }
    }
    cout << *max_element(dp[n].begin(), dp[n].end()) << endl;
}

 小苯的水瓶

思路:看到取最值应该使用二分,这里注意check函数的编写,并主要need大于k+m时提前结束,或者开_i128,防止爆longlong。

void solve()
{
    int n, m, k;
    cin >> n >> m >> k;
    vi a(n);
    for (int i = 0; i<n; i++) cin >> a[i];
    sort(a.begin(), a.end());
    auto check = [&](int x)->bool
    {
        int xu=0, need = 0;
        for (int i = 0; i<n; i++){
            xu += max(0ll, a[i]-x);
            need += max(0ll, x-a[i]);
            if (need > k+m) return false;
        }
        return k+min(xu, m)-need >= 0;
    };
    int l = 0, r = 1e9;
    while (l+1 < r){
        int mid = (l+r)/2;
        if (check(mid)) l = mid;
        else r = mid;
    }
    cout << l << endl;
}

小苯的旅行计划

思路:因为m的值不是很大,所以我们可以来枚举要使用魔法的边来找最小值,需要记录每条边有多少个人的记录,这里应该用树上边差分来统计, 再在枚举时取最大值,思路还是比较简单,但代码实现上还是有点难度。

struct LCA{
    vector<vector<int>> f;
    vector<int> sum, dep;
    vector<vector<pair<int, int>>> g;
    LCA(int n){
        f.resize(20, vector<int>(n+1));
        dep.resize(n+1), sum.resize(n+1);
        g.resize(n+1);
    }
    void dfs(int u, int fa){
        dep[u] = dep[fa] + 1;
        f[0][u] = fa;
        for (int i = 1; i<20; i++){
            f[i][u] = f[i-1][f[i-1][u]];
        }
        for (auto [v, w] : g[u]){
            if (v == fa) continue;
            sum[v] = sum[u]+w;
            dfs(v, u);
        }
    }
    int find(int u, int v){
        if (dep[u] < dep[v]) swap(u, v);
        for (int i = 19; i>=0; i--){
            if (dep[f[i][u]] >= dep[v]) u = f[i][u];
        }
        if (u == v) return u;
        for (int i = 19; i>=0; i--){
            if (f[i][u] != f[i][v]){
                u = f[i][u];
                v = f[i][v];
            }
        }
        return f[0][u];
    }
};
struct node{
    int u, v, w;
};
void solve(){
    int n, m;
    cin >> n >> m;
    LCA lca(n);
    vector<node> edge(n+1);
    for (int i=1,u,v,w; i<=n-1; i++){
        cin >> u >> v >> w;
        lca.g[u].push_back(make_pair(v, w));
        lca.g[v].push_back(make_pair(u, w));
        edge.push_back({u, v, w});
    }
    lca.dfs(1, 0);
    int cost = 0;
    vector<int> s(n+1);
    for (int i = 0,a,b; i<m; i++){
        cin >> a >> b;
        int ll = lca.find(a, b);
        s[a]++, s[b]++, s[ll]-=2;
        cost += (lca.sum[a]+lca.sum[b]-2*lca.sum[ll]);
    }
    auto dfs2 = [&](auto &&dfs2, int u, int fa)->void
    {
        for (auto [v, w]: lca.g[u]){
            if (v == fa) continue;
            dfs2(dfs2, v, u);
            s[u] += s[v];
        }
    };
    dfs2(dfs2, 1, 0);
    int mx = 0;
    for (auto [u, v, w]:edge){
        if (lca.dep[u] < lca.dep[v]) swap(u, v);
        mx = max(mx, s[u]*w);
    }
    cout << cost - mx << endl;
}
struct Edge{
    int u, v, w;
};
void solve()
{
    int n, m, k;
    cin >> n >> m;
    vector<vector<pair<int,int>>> g(n+1);//记录连接边
    vector<Edge> edge;   //记录m条边
    for (int i = 1; i<=n-1; i++){
        int u, v, w;
        cin >> u >> v >> w;
        g[u].pb(mp(v,w));
        g[v].pb(mp(u,w));
        edge.pb({u,v,w});
    }
    vector<vector<int>> f(20, vector<int>(n+1)); //父节点
    vector<int> dep(n+1), sum(n+1);  //深度     树的前缀和(从根到叶递增)
    auto dfs1= [&](auto && dfs1, int u, int fa)->void //倍增预处理
    {
        dep[u] = dep[fa]+1;
        f[0][u] = fa;
        for (int j = 1; j<20; j++){
            f[j][u] = f[j-1][f[j-1][u]];
        }
        for (auto & [v,w] : g[u]){
            if (v == fa) continue;
            sum[v] = sum[u] + w;
            dfs1(dfs1, v, u);
        }
    };
    dfs1(dfs1, 1, 0);
    auto LCA = [&](int u, int v)->int   //求最近公共祖先
    {
        if (dep[u] < dep[v]) swap(u,v);
        for (int j = 19; j>=0; j--){
            if (dep[f[j][u]] >= dep[v]) u = f[j][u];
        }
        if (u == v) return u;
        for (int j = 19; j>=0; j--){
            if (f[j][u] != f[j][v]){
                u = f[j][u];
                v = f[j][v];
            }
        }
        return f[0][u];
    };
    int cost = 0;
    vector<int> s(n+1);
    for (int i = 0, a, b; i<m; i++){
        cin >> a >> b;
        int lca = LCA(a,b);
        s[a]++, s[b]++, s[lca]-=2;
        cost += (sum[a]+sum[b]-2*sum[lca]); // 算出原本的边权前缀和(不使用魔法时,全部的需要的花费)
    }
    auto dfs2 = [&](auto && dfs2, int u, int fa)->void  //利用差分求每条边经过的次数
    {
        for (auto [v,w]:g[u]){
            if (v == fa) continue;
            dfs2(dfs2, v, u);
            s[u]+=s[v];
        }
    };
    dfs2(dfs2, 1, 0);
    int mx = 0;
    for (auto [u,v,w] : edge){
        if (dep[u] < dep[v]) swap(u,v);   // 这里是因为边权是下降到点的,所以要用深度更深的那个点
        mx = max(mx, s[u]*w);
    }
    cout << cost-mx << endl;
}

小苯的奇怪最短路

思路:

考虑枚举答案中的最大边 w,那么问题变成了最小边如何求。

让我们考虑最小生成树的求法,在合并连通块的过程中,如果 (u,v)连通则我们直接跳过,否则连上边,并给总权加上 (u,v)的权。

在本题中实际上也类似,我们给每个连通块都维护一个当前连通块中的最小边 mn,接着正常执行

𝑘𝑟𝑢𝑠𝑘𝑎𝑙求最小生成树,如果当前 1 和 n 已经连通,则我们就对 1,n所在的连通块的最小边 mn 加上当前边 w 取 min即可,在过程中要一直维护每个连通块中最小边的权 mn

struct edge{
    int u, v, w;
    bool operator < (const edge&e) const{
        return w < e.w;
    }
};
struct ufSet{
    vector<int> fa,siz;
    // fa[N]用于存储每个元素的父节点信息
    // siz[N]用于存储每个集合的大小
    vector<int> mn;
    void init(int n){
        fa.resize(n+5),siz.resize(n+5);
        mn.resize(n+5);
        for(int i=1;i<=n;i++) fa[i]=i,siz[i]=1,mn[i]=INF;
    }
    int find(int x){
        if(x==fa[x]) return x;
        fa[x]=find(fa[x]);
        return fa[x];
    }
    bool mrg(int x,int y, int w){
        x=find(x),y=find(y);
        // if(x==y) return 0;
 
        if(siz[x]<siz[y]) swap(x,y);
        fa[y]=x,siz[x]+=siz[y];
        mn[x]=min({mn[x],w, mn[y]});
        return 1;
    }
}t;
void solve()
{
    int n, m, k;
    cin >> n >> m;
    t.init(n);
    vector<edge> e;
    for (int i = 0, u, v, w; i<m; i++){
        cin >> u >> v >> w;
        e.pb({u, v, w});
    }
    sort(e.begin(), e.end());
    int ans = INF;
    for (auto [u, v, w]: e){
        t.mrg(u, v, w);
        if (t.find(1) == t.find(n)){
            ans = min(ans, t.mn[t.find(1)]+w);
        }
    }
    if (ans > 1e17) ans = -1;
    cout << ans << endl;
}

小苯的矩阵变换

思路:(模拟建图,状压 𝑑𝑝dp 计数) 

我们将每个图看作是一个点,当图u能反转成图v时,将两图建一条无向边,其次记录能作为起点的点(即图中全为0的图)存入数组st,这样我们就将问题转换成对于k+1个点求起点开始有多少条哈密顿路径,通过状压dp来计数,并注意条件的判断。

#include <bits/stdc++.h>
#define int long long
using namespace std;
void solve(){
	int n, m, k;
	cin >> n >> m >> k;
	k++;
	vector<vector<string>> g(k,vector<string>(n+1)); //存图
	vector<int> st; //记录起点
	for (int i = 0; i<k; i++){
		bool white = 0;
		for (int j = 1; j<=n; j++){
			string s;
			cin >> s;
			s = " "+s;
			g[i][j]=s;
			for (int c = 1; c<=m; c++){
				white |= (s[c]-'0');
			}
		}
		if (!white) st.push_back(i); // 判断是否为起点
	}
	if (st.empty()){
		cout << 0 << endl;
		return ;
	}
	vector<vector<int>> adj(k); //记录无向边
	auto addEdge = [&](int u, int v)->bool
	{
		vector<vector<int>> mp(n+1, vector<int>(m+1)); //判断两图的差别
		int tx = 0, ty = 0, ex = 0, ey = 0;
		int cnt = 0;
		for (int i = 1; i<=n; i++){
			for (int j = 1; j<=m; j++){
				mp[i][j] = (g[u][i][j]-'0')^(g[v][i][j]-'0');
				if (mp[i][j]){
					if (tx == 0){
						tx = i, ty = j;
					}
					ex = i, ey = j;
					cnt++;
				}
			}
		}
		if (tx == ex && cnt == m){
			for (int i = 1; i<=m; i++){ //防止有可能该行上不足m个,但总图有m个
				if (!mp[tx][i]) return 0;
			}
			adj[u].push_back(v);
			adj[v].push_back(u);
			return 1;
		}
		else if (ty == ey && cnt == n){ //防止有可能该列上不足n个,但总图有n个
			for (int i = 1; i<=n; i++){
				if (!mp[i][ty]) return 0;
			}
			adj[u].push_back(v);
			adj[v].push_back(u);
			return 1;
		}
		return 0;
	};
	for (int i = 0; i<k; i++){
		for (int j = i+1; j<k; j++){
			addEdge(i, j);
		}
	}
	vector<vector<int>> dp(1<<k, vector<int>(k, 0)); //dp[i][j]计数,表示i二进制下以j为结尾的方案最大数
	for (auto s : st){
		dp[(1<<s)][s] = 1;  //点是从0-(k-1)编号的
	}
	// 状压范围1~[(1<<k)-1]
	for (int i = 1; i<(1<<k); i++){
		for (int j = 0; j<k; j++){ //枚举结尾
			if (!dp[i][j]) continue;  //去除之前不能达到该点的情况
			if (((i>>j)&1) == 0) continue; //状压判断
			for (auto nxt : adj[j]){
				if (((i>>nxt)&1) == 0){ //保证之前没访问过
					dp[i|(1<<nxt)][nxt] += dp[i][j];
				}
			}
		}
	}
	int ans = accumulate(dp[(1<<k)-1].begin(), dp[(1<<k)-1].end(), 0ll);
	cout << ans << endl;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	
	int t = 1;
	cin >> t;
	while (t--)
	{
		solve();
	}
	
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wirepuller_king

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值