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] (2t−1,2t]区间即可。
答案等于数字 n − 1 n-1 n−1可以除以 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 1≤l≤r≤1000000。
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[l−1]。这样每次询问直接 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=1∏nf(i)=x=1∏totxs(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} n≤1015二进制最多不会超过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(n≤105)个点 { ( 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,0≤xi≤106,0≤yi≤10},求最小生成树。
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;
}
本文介绍了几道算法竞赛中的题目,涉及数论、贪心、数位DP和最小生成树等算法,如字符串复制粘贴次数、素因子乘积求解、二进制中1的个数和最小生成树构建,解析了思路与解决方案。
1152

被折叠的 条评论
为什么被折叠?



