NewOJ Week 3题解

NewOJ周赛于2022年3月19日正式开始,比赛时间为每周六晚19:00-22:00。

比赛链接:http://oj.ecustacm.cn/contest.php?cid=1017

A 复制粘贴

题意: 最开始是 1 1 1行字符串,变成 n n n行最少需要复制粘贴多少次。

Tag: 思维题

难度:

来源: U V A   11636 UVA\ 11636 UVA 11636

思路: 简单发现规律:从 1 1 1开始的答案分别是: 0 , 1 , 2 , 2 , 3 , 3 , 3 , 3.... 0,1,2,2,3,3,3,3.... 0,1,2,2,3,3,3,3....

根据贪心的思想,每次复制粘贴所有行,那么经过 t t t次复制粘贴最多可以变成 2 t 2^t 2t行,对于数字 n n n,只需要判断 n n n在哪个 ( 2 t − 1 , 2 t ] (2^{t-1},2^t] 2t1,2t]区间即可。

答案等于数字 n − 1 n-1 n1可以除以 2 2 2的次数。

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int T;
    cin >> T;
    while(T--)
    {
        long long n, ans = 0;
        cin >> n;
        n--;
        while(n)n /= 2, ans++;
        cout<<ans<<endl;
    }
    return 0;
}

B Radical of N

题意: 多次询问,每次求 ∑ i = l r r a d ( i ) \sum_{i=l}^rrad(i) i=lrrad(i),其中 r a d ( i ) rad(i) rad(i)表示 i i i的素因子乘积, 1 ≤ l ≤ r ≤ 1000000 1\le l \le r \le 1000000 1lr1000000

Tag: 数论、埃氏筛

难度: ☆☆

来源: 原创

思路: 本题为多组询问区间和, i i i的范围 [ 1 , 1000000 ] [1,1000000] [1,1000000],考虑打表所有数字的 r a d rad rad值,而 [ l , r ] [l,r] [l,r]区间和可以转换成 r a d rad rad数组的前缀和 s u m [ r ] − s u m [ l − 1 ] sum[r]-sum[l-1] sum[r]sum[l1]。这样每次询问直接 O ( 1 ) O(1) O(1)输出结果即可。

对于 r a d ( n ) rad(n) rad(n)的求解:在素因子分解的过程中可以直接累乘,时间复杂度 O ( n ) O(\sqrt{n}) O(n ),所总的时间复杂度为 n n n\sqrt{n} nn ,在 n = 1000000 n=1000000 n=1000000时难以通过。

能否一次性求出所有 r a d rad rad值?

答案是可以的,先前做法是对于每个数字 n n n找存在哪些素因子,而数论中的埃氏筛法是对于每个素因子 x x x去找出现在哪些数字中,后者时间复杂度为 O ( n log ⁡ ( n ) ) O(n\log(n)) O(nlog(n))。本题直接在埃氏筛的基础上,顺便计算一下 r a d rad rad值即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e6 + 10;
bool not_prime[maxn];
ll rad[maxn], sum[maxn];

void init(int n)    ///埃氏筛的同时,求解出每个rad的值
{
    not_prime[1] = 1;
    for(int i = 1; i <= n; i++)
        rad[i] = 1;
    for(int i = 2; i <= n; i++)if(!not_prime[i])
    {
        rad[i] = i;
        for(int j = i + i; j <= n; j += i)///j是i倍数,其中i为素数
            rad[j] *= i, not_prime[j] = 1;
    }
    for(int i = 1; i <= n; i++)
        sum[i] = sum[i - 1] + rad[i];
}

int main()
{
    int n = 1000000;
    init(n);
    int T;
    cin >> T;
    while(T--)
    {
        int l, r;
        cin >> l >> r;
        cout<<sum[r] - sum[l - 1]<<endl;
    }
    return 0;
}

C 二进制中的1

题意: f ( i ) f(i) f(i)表示数字 i i i的素因子数目,求 ∏ i = 1 n f ( i ) \prod_{i=1}^{n}f(i) i=1nf(i)

Tag: 数位 D P DP DP

难度: ☆☆☆

来源: B Z O J   3209 BZOJ\ 3209 BZOJ 3209

思路: [ 1 , n ] [1,n] [1,n]的二进制表示中恰好有 x x x 1 1 1的数字数目,记为 s ( x ) s(x) s(x),则有:

∏ i = 1 n f ( i ) = ∏ x = 1 t o t x s ( x ) \prod_{i=1}^{n}f(i)=\prod_{x=1}^{tot}x^{s(x)} i=1nf(i)=x=1totxs(x)

相当于我们只需要计算出 s ( 1 ) , s ( 2 ) , . . . , s ( 50 ) s(1),s(2),...,s(50) s(1),s(2),...,s(50)即可,因为数字 n ≤ 1 0 15 n\le10^{15} n1015二进制最多不会超过50位。

[ 1 , n ] [1,n] [1,n]的二进制表示中恰好有 x x x 1 1 1的数字数目是一道经典的数位 D P DP DP问题。直接用 D F S DFS DFS+记忆化搜索进行求解: d p [ d e p t h ] [ s u m ] dp[depth][sum] dp[depth][sum]表示当前计算到第 d e p t h depth depth位,并且 1 1 1的个数为 s u m sum sum的方案数。

在搜索的过程中暴力枚举第 d e p t h depth depth位的取值即可。 d f s dfs dfs中的第三个参数 f l a g flag flag表示前面枚举的数字是否达到上限。 f l a g = T r u e flag=True flag=True时,当前位的取值上限与数字 n n n有关。 f l a g = F a l s e flag=False flag=False时,后续都不可能达到上限,因此答案与数字 n n n的值无关,对于这部分使用记忆化搜索记录答案。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 10000007;
///快速幂a^b % m
ll ksm(ll a, ll b, ll m)
{
    ll ans = 1 % m;
    a %= m;
    while(b)
    {
        if(b & 1)ans = ans * a % m;
        b >>= 1;
        a = a * a % m;
    }
    return ans;
}

int num[70];
ll dp[70][70];
//depth:当前第几位
//sum:当前有多少个1
//flag:当前是否达到上限
ll dfs(int depth, int sum, bool flag, int k)
{
    if(depth == 0)return sum == k;  //递归出口
    if(!flag && dp[depth][sum] != -1)
        return dp[depth][sum];
    int up = flag ? num[depth] : 1;
    dp[depth][sum] = 0;
    for(int i = 0; i <= up; i++)
        dp[depth][sum] += dfs(depth - 1, sum + (i == 1), flag && (i == up), k);
    return dp[depth][sum];
}

int main()
{
    ll n, ans = 1, tot = 0;
    cin >> n;
    while(n)num[++tot] = n % 2, n >>= 1;///将n进行二进制分解
    for(int i = 1; i <= tot; i++)///[1,n]中二进制中1的个数为i的方案数
    {
        memset(dp, -1, sizeof(dp));
        ll tmp = dfs(tot, 0, 1, i);
        ans = ans * ksm(i, tmp, MOD) % MOD;
    }
    cout<<ans<<endl;
    return 0;
}

D 最小生成树

题意: 平面上的 n ( n ≤ 1 0 5 ) n(n \le 10^5) n(n105)个点 { ( x i , y i ) ∣ i = 1 , . . . , n , 0 ≤ x i ≤ 1 0 6 , 0 ≤ y i ≤ 10 } \{(x_i,y_i)|i=1,...,n,0\le x_i\le10^6,0\le y_i \le10\} {(xi,yi)i=1,...,n,0xi106,0yi10},求最小生成树。

Tag: 贪心、最小生成树

难度: ☆☆☆

来源: U S A C O   2022   F e b USACO\ 2022\ Feb USACO 2022 Feb

思路: 最小生成树模板很简单,使用 K r u s k a l Kruskal Kruskal算法即可。核心在于无法连接 n 2 n^2 n2数量的边。

如何尽可能的减少边?

本题最特殊的地方在于 y i y_i yi的取值范围。基于 y y y坐标范围很小,对于某个点 ( x i , y i ) (x_i,y_i) (xi,yi)来说,所以考虑每一行分别往左和往右连接最近的点。

为什么这样连边可以保证求MST时不出错?

如下图所示,对于红点来说,只需要连接第一个绿点而不需要连第二个绿点,这是由于在 K r u s k a l Kruskal Kruskal算法进行的时候,首先按照边排序,而实线边长度均小于虚线边,因此虚线边本身就不会被 K r u s k a l Kruskal Kruskal算法选中,所以对每一行的点,只需要连接左边离自己最近的和右边离自己最近的即可。

在这里插入图片描述

#include<bits/stdc++.h>
#define Mul(a) ((long long)(a) * (a))
using namespace std;
const int maxn = 1e5 + 10;
typedef pair<int, int> Node;
int n, tot;///点、边
Node a[maxn];   ///n个点的坐标
struct edge
{
    int u, v;
    long long w;
    edge(){}
    bool operator<(const edge& a)const
    {
        return w < a.w;
    }
}e[maxn * 22];

void add_edge(int u, int v)//点u和点v连边
{
    tot++;
    e[tot].u = u;
    e[tot].v = v;
    e[tot].w = Mul(a[u].first - a[v].first) + Mul(a[u].second - a[v].second);
    //cout<<u<<" "<<v<<" "<<e[tot].w<<endl;
}
int f[maxn];//并查集
int Find(int x)
{
    return x == f[x] ? x : f[x] = Find(f[x]);
}
void kruskal()
{
    for(int i = 1; i <= n; i++)//并查集初始化
        f[i] = i;
    sort(e + 1, e + 1 + tot);//边排序
    long long ans = 0;
    for(int i = 1; i <= tot; i++)//从小到大遍历边
    {
        int u = Find(e[i].u), v = Find(e[i].v);
        if(u != v)f[u] = v, ans += e[i].w;
    }
    cout<<ans<<endl;
}
int Last[15];
int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)cin >> a[i].first >> a[i].second;
    //对点排序
    sort(a + 1, a + 1 + n);
    //每个点往每行的左边最近点连边
    for(int i = 0; i <= 10; i++)Last[i] = 0;
    for(int i = 1; i <= n; i++)
    {
        for(int y = 0; y <= 10; y++)if(Last[y])
            add_edge(i, Last[y]);
        Last[a[i].second] = i;
    }
    //每个点往每行的右边最近点连边
    for(int i = 0; i <= 10; i++)Last[i] = 0;
    for(int i = n; i >= 1; i--)
    {
        for(int y = 0; y <= 10; y++)if(Last[y])
            add_edge(i, Last[y]);
        Last[a[i].second] = i;
    }
    kruskal();
    return 0;
}

E 黑白边

题意: 给定一颗 n n n个节点的树,根节点为1,初始均为黑色边。两种操作: A   x   y A\ x\ y A x y把边 < x , y > <x,y> <x,y>修改为白色, W   x W\ x W x,询问 1 1 1 n n n的路径上黑色边数量。

Tag: d f s dfs dfs序、树状数组

难度: ☆☆☆

来源: P O I   2007 POI \ 2007 POI 2007

思路: 题目相当于每次修改树的边权,每次询问从根节点到其他节点的距离。

考虑预处理出 d f s dfs dfs序,因为树的 d f s dfs dfs序将问题变成单点修改区间查询

例如样例中的 d f s dfs dfs先序遍历序列:1 2 2 3 3 4 5 5 4 1,节点 i i i进入的 d f s dfs dfs序记为 L [ i ] L[i] L[i],出去的 d f s dfs dfs序记为 R [ i ] R[i] R[i]

将边的权值下放,即每个点 i i i表示其父亲到点 i i i这条边的边权,每个点进入的时候 + 1 +1 +1、出去的时候 − 1 -1 1,即在 L [ i ] L[i] L[i]处减 1 1 1、在 R [ i ] R[i] R[i]处减 1 1 1,可以得到数组 a a a

d f s dfs dfs 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10
节点编号 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 5 5 5 5 5 5 4 4 4 1 1 1
a a a 1 1 1 1 1 1 − 1 -1 1 1 1 1 − 1 -1 1 1 1 1 1 1 1 − 1 -1 1 − 1 -1 1 − 1 -1 1

可以发现,从根节点 1 1 1到节点 i i i的距离,等价于 a a a数组前缀和 [ 1 , L [ i ] ] − 1 [1,L[i]]-1 [1,L[i]]1。这是由于 d f s dfs dfs序使得一个子树正好是对应一个连续区间,当进入时 + 1 +1 +1、出去时 − 1 -1 1可以保证子树遍历结束总和为0。

对于修改操作: < x , y > <x,y> <x,y>,记 y y y为儿子,那么只需要把最开始 L [ y ] L[y] L[y] + 1 +1 +1 R [ y ] R[y] R[y] − 1 -1 1抵消掉即可,相当于把这条边的边权赋值为 0 0 0

#include<bits/stdc++.h>
#define lowbit(x) ((x) & (-x))
using namespace std;
const int maxn = 250000 + 10;
int n, tree[maxn * 2];
vector<int>G[maxn];
int read() {
  int x = 0, w = 1;
  char ch = 0;
  while (ch < '0' || ch > '9') {  // ch 不是数字时
    if (ch == '-') w = -1;        // 判断是否为负
    ch = getchar();               // 继续读入
  }
  while (ch >= '0' && ch <= '9') {  // ch 是数字时
    x = x * 10 + (ch - '0');  // 将新读入的数字’加’在 x 的后面
    // x 是 int 类型,char 类型的 ch 和 ’0’ 会被自动转为其对应的
    // ASCII 码,相当于将 ch 转化为对应数字
    // 此处也可以使用 (x<<3)+(x<<1) 的写法来代替 x*10
    ch = getchar();  // 继续读入
  }
  return x * w;  // 数字 * 正负号 = 实际数值
}
//树状数组模板
void add(int x, int d)
{
    while(x <= n + n)
        tree[x] += d, x += lowbit(x);
}
int sum(int x)
{
    int ans = 0;
    while(x)
        ans += tree[x], x -= lowbit(x);
    return ans;
}

//求dfs序
int l[maxn], r[maxn], cnt;
void dfs(int u)
{
    //cout<<u<<endl;
    l[u] = ++cnt;
    for(auto v : G[u])
        dfs(v);
    r[u] = ++cnt;
}

int main()
{
    n = read();
    for(int i = 1; i < n; i++)
    {
        int u, v;
        u = read();v = read();
        if(u > v)swap(u, v);
        G[u].push_back(v);
    }
    dfs(1);
    //for(int i = 1; i <= n; i++)
    //    cout<<i<<" "<<l[i]<<" "<<r[i]<<endl;
    for(int i = 1; i <= n; i++)
        add(l[i], 1), add(r[i], -1);
    int m;
    m = read();
    m = m + n - 1;
    while(m--)
    {
        char op;
        op = getchar();
        if(op == 'A')
        {
            int x, y;
            x = read();
            y = read();
            if(x > y)swap(x, y);
            add(l[y], -1);
            add(r[y], 1);
        }
        else
        {
            int x;
            x = read();
            printf("%d\n", sum(l[x]) - 1);
        }
    }
    return 0;
}
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

傅志凌

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

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

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

打赏作者

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

抵扣说明:

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

余额充值