1. 题目
T1 双色球计数
题目描述
题目描述
小明有两种颜色的球,当然你可以想象成任意自己喜欢的颜色,但为了方便题目描述我们先暂定为红色和蓝色。
目前有 (N) 个红色球和 (M) 个蓝色球,红球被编号为 (1 sim N),蓝球被编号为 (N+1sim N+M),每个球都是完全不同的。
小明不断地将这些小球排成一列,他发现把相同颜色的小球放在一起会影响美感,所以他倾向于不这么做。
现在问题来了,一共有多少种排列方式是不存在相邻的同色小球的呢。
答案可能很大,请对 (10^9+7) 取模。
输入格式
输入第一行,一个整数 (Q) ,表示测试数据组数。
对于每组数据,输入一行包含两个数字 (N) 和 (M) .
输出格式
输出共 (Q) 行,第 (i) 行的输出表示第 (i) 组测试数据的结果。
样例输入
4
6 7
8 9
6 8
5 8
样例输出
3628800
631321502
0
0
数据范围
- 对于 (30\%) 的数据,(1≤N,M≤10);
- 对于 (50\%) 的数据,(1≤N,M≤100);
- 对于 (100\%) 的数据,(1≤N,M≤10^6)。
Sol
大水题,下面把红球写作 R
,蓝球写作 B
。
显然当 (|n-m|>1) 时无解,输出 0
即可。
分类讨论:
- 当 (n=m) 时,有
RBRB ... RB
和BRBRBR ... BR
两种方案,方案数即为 (2n!m!)(摆R
是 (n!) 种,摆B
是 (m!) 种)。 - 当 (n
eq m) 时,不妨令 (m<n),即 (n=m+1),那么只有
RBRBRB ... RBR
这一种方案,方案数即为 (n!m!) .
炼金术
题目描述
小明在玩一款 MMORPG 游戏,在游戏中他扮演一个炼金术士,他刚解锁他的最强技能——当然是炼金术。
炼金术和你想的差不多,它将铅块以某种规则炼成金块(很合理)
小明研究了一番后发现炼金术的规则参考了古老的毕达哥拉斯学派,有 (4) 个数字扮演了重要的角色:(1) (2) (3) (5)
- (1) 是第一原则,万物之母,智慧
- (2) 是对立与否定,也是第一个偶数
- (4) 代表着万物,也是第一个奇数(与我们现在的奇数定义有所不同)
- (5) 是婚姻,是第一个奇数与第一个偶数的结合和
这四条法则对应到炼金术分为是
- 1:向炼金炉里投入 (A) 个铅块,在炼金炉里得到或失去一个金块(金块数量 (+ 1 / - 1))
- 2:向炼金炉里投入 (B) 个铅块,将炼金炉里金块的数量变为 (2) 倍
- 3:向炼金炉里投入 (C) 个铅块,将炼金炉里金块的数量变为 (3) 倍
- 5:向炼金炉里投入 (E) 个铅块,将炼金炉里金块的数量变为 (5) 倍
除此之外还有几条另外的约定
- 不可以向炼金炉里添加铅块以外的东西(很合理)
- 使用炼金术时必须要有炼金炉(很合理),每次开始炼金时,炼金炉里什么都没有(很合理,炼金的事前准备)
- 炼金炉是个一次性物品(很合理,理由如下),因为构造原因你不把炼金炉砸碎了是无法获得里面的金块的(这也很合理,请自行脑补一个合理的理由)
总结:你不可以向炼金炉里人为加入金块,也不能在中途从炼金炉里取出金块
现在小明只有一个炼金炉,他比较好奇恰好获得 (N) 个金块,至少需要多少铅块
输入格式
输入第一行,一个整数 (Q) ,表示测试数据组数
对于每组数据,输入一行包含五个数字 (N,A,B,C,E)
输出格式
输出共 (Q) 行,第 (i) 行的输出表示第 (i) 组测试数据的结果
样例输入
2
14 1 2 4 8
37 9 8 7 6
样例输出
8
47
数据范围
- 对于 (70\%) 的数据,(1≤N≤10^6);
- 对于 (100\%) 的数据,(1≤N≤10^{18}),(1≤A,B,C,E≤109);
数据保证答案不超过 unsigned long long
Sol
这个和 [USACO07OPEN]Catch That Cow S 很像,洛谷 P1588
70pts:
读懂题目意思后你会发现 (70) 分就是个搜索(广搜、深搜都行)
广搜:(你想叫他 dp 也行)
有两种搜索方法,一种是对金块搜,一种是对铅块搜
广搜状态是一个二元组(金块,铅块),代表一种方案
对金块搜:你第一次入队的方案可能不是最优解,如 (+1 imes 2 imes 2 +1) 到 (5) 金币比 (+1 imes 5) 优
所以你如果更新了某个 (dp_{ ext{金块}}) 的最优解,可能就要把它重新入队(参考 spfa)
对铅块搜:想象成一个 dij,你每次扩展铅块最小的 (( ext{金块}, ext{铅块}))
深搜:
就那么一种,dfs(金块数量 N)
然后思考 (N) 可能从什么地方转移过来
100pts:
从 dfs 的角度回过头思考这个问题
由于数据范围使我们无法接受 (O(n)) 的算法
我们要做的其实是尽量去优化我们的搜索过程
如果要杜绝 (O(n)),那么 dfs(n)
( o) dfs(n-1)
这条边是一定要剔除的,因为它是 (O(n)) 的罪魁祸首
如果没有这个转移,那么其他的转移的复杂度其实会很低(首先搜索树的树高会变成 (log) 级的)
所以我们要做的就是删除 (+1 -1) 这个转移,并将其并入其他转移中,然后用 map 代替数组做记忆化搜索即可
T3 地铁大亨
题目描述
题目描述
小明正在玩一款叫地铁大亨的游戏,在游戏里你每次都会来到一个新城市,你需要为这个新城市规划并建设铁路网络,然后依靠这些铁路牟利
假设这个城市有 (N) 个村庄,第 (i) 个村庄的坐标位于 ((x_i,y_i)) .
你规划的铁路网络首先要让城市中任意两个村庄之间都能靠铁路互相到达,其次你当然要将成本降低到最小。
铁路网络的成本主要用于向村民购买铺设铁路所用到的土地、清理预设铁路的沿线以及各式各样的补偿款。
直接上结论,在坐标 ((x_1,y_1)) 与 ((x_2,y_2)) 的村庄间铺设一条铁路的费用是 (min(|x1−x2|,|y1−y2|)) .
求这个城市铺设铁路网络的最低成本。
输入格式
输入第一行,一个整数 (N) ,表示该城市的村庄数。
接下来 (N) 行,每行包含一组正整数 (x_i,y_i),表示这些村庄的坐标。
输出格式
输出共一行,表示最低费用。
样例输入
3
1 5
3 9
7 8
样例输出
3
数据范围
- 对于 (30\%) 的数据,(2≤N≤10);
- 对于 (50\%) 的数据,(2≤N≤5000);
- 对于 (100\%) 的数据,(2≤N≤10^5),(0≤x_i,y_i≤10^9)。
Sol
这题显然是个最小生成树嘛,暴力求 MST 可以获得 50pts .
注意到对于横坐标依此递减的三个点 (a, b, c),边 ((a, c)) 至少在横坐标上是没有意义的
那么对于横坐标,有意义的边最多只有 (N - 1) 条,纵坐标同理
那么边的数量变成了 (O(n)) 级别,再暴力求 MST 即可获得 100pts .
T4 结束的派对
题目描述
题目描述
十一恰逢中秋,小明约了几个好朋友来家里打桌游,他们玩了整整一天一夜。
而在朋友走后,小明望着家里的一片狼藉,感到十分头疼。
没有人愿意一次又一次地去倒垃圾,小明注意到大部分的垃圾是各种外卖,被大大小小的塑料袋装着,而塑料袋一般没有装满。
那么处理方式相信大家都知道了,可以把原本两个袋子里的垃圾塞到一个袋子里,这样子袋子数量就减少了,只需要提几个袋子就可以把垃圾丢完。
比如袋子 (A) 的容量是 (3), 里面的垃圾体积是 (2),袋子 (B) 的容量是 (2),里面的垃圾体积是 (1)
你可以将两个袋子里的垃圾都转移到袋子 (A) 中,这样就可以只丢一袋垃圾,但是你不能把两个袋子里的垃圾都转移到 (B) 中,因为容量不够
同时小明把体积 (v) 的垃圾从一个袋子转移到另一个袋子需要的时间是 (v),垃圾可以最小拆分为 (1) 个体积单位(理解不了就想成是脏水)
小明现在想知道这些垃圾他最少能收拾成几袋(当然所有垃圾都要被丢掉),不如将答案记为 (Cnt)
小明还想知道收拾成 (Cnt) 袋最少要多长时间 (Time) .
输入格式
输入第一行,一个整数 (N) ,表示一开始有几袋垃圾
接下来 (N) 行,每行包含两个数字 (c_i) 与 (v_i),分别表示第 (i) 袋垃圾里有多少体积的垃圾,以及第 (i) 个袋子的容量大小
输出格式
输出共一行,(Cntquad Time)
样例输入
4
3 4
3 5
3 7
4 6
样例输出
2 6
样例解释
先把 ((3,4)) 倒入 ((3,7)),变成 ((6,7)) 再把 ((3,5)) 的 (3) 拆成 (1 + 2),一份倒入 ((6,7)),一份倒入 ((4,6)) 两袋,时间是 (3 + 1 + 2=6) .
数据范围
- 对于 (30\%) 的数据,(2≤N≤3);
- 对于 (50\%) 的数据,(2≤N≤20);
- 对于 (100\%) 的数据,(2≤N≤200),(1≤c_i≤v_i≤100)。
Sol
写点 if 可以获得 30pts(送)
写个暴力,如果用 (0) 表示最后某个袋子没有被用,(1) 表示用了,那么最后袋子的状态可以变成一个 01 串,你可以通过枚举这个 01 串来暴搜,对于一个给定的 01 串,第一问就是这个 01 串的 (1) 的个数,第二问就是这个 01 串中 (0) 号袋子们的 (c_i) 和,50pts .
下面说 100pts 做法:
你会发现第一个问其实是个贪心问题,你只要计算出一共有多少垃圾,然后按袋子容量从多到少去装就可以了
那么要解决的其实是第二问,进一步寻找规律发现,最节省时间的方案,显然有以下性质:
一个垃圾袋不是在往里丢垃圾,就是在往外倒垃圾,它不可能既倒又收。
那么答案就是求选择倒的垃圾袋的垃圾和最小是多少,或是求选择不倒的垃圾袋和最大多少。
那么问题变成一个变相的 01 背包,只不过 01 背包是决定一个物品拿或者不拿,而现在这个问题变成了这个垃圾袋是倒还是收,那么
用(f_{i,j}) 表示前 (i) 个垃圾袋都解决了,选择收的垃圾袋的总体积是 (j) 的最小袋数,
用 (g_{i,j}) 表示前 (i) 个垃圾袋都解决了,选择收的垃圾袋的总体积是 (j) 的这些垃圾的总体积即可。
算法 - 分治
1. 分治
分治,每次把一个问题分成几个子问题解决。
例子:排序算法
每次排序可以考虑将序列分成两份,把两份排序后再合并,即归并排序。
const int N=1048576;
int a[N],tmp[N];
void msort(int l,int r) // merge sort
{
int mid=(l+r)>>1;
if (l==r) return ;
msort(l,m); msort(m+1,r); // 分成两段
int i=l,j=m+1,k=l;
while ((i<=m)&&(j<=r)) // 合并
if (a[i]<=a[j]) tmp[k++]=a[i++];
else tmp[k++]=a[j++];
while (i<=m) tmp[k++]=a[i++];
while (i<=r) tmp[k++]=a[j++];
// for (int i=l;i<=r;i++) a[i]=tmp[i];
}
每次排序可以考虑先把序列分成两个小序列(我们叫做左子序列和右子序列),如果我们能做到左序列里的数字都小于等于右序列里的数,那么问题就变成了两个更小规模的数组的排序,即快速排序。
const int N=1048576;
int a[N];
void qsort(int l,int r) // quick sort
{
int x=a[(l+r)>>1]; // 基准数,让左子序列都 <x,右子序列都 >x
int i=l,j=r,tmp=233;
while (i<=j) // 有序
{
while (a[i]<x) ++i;
while (a[j]<x) --j;
while (i<=j){swap(a[i],a[j]); ++i; --j;}
}
if (l<j) qsort(l,j); // 分治
if (i<r) qsort(i,r);
}
Problem 1:瑞士轮(NOIp2011;洛谷 P1309)
(2N) 名编号为 (1sim 2N) 的选手共进行 (R) 轮比赛。每轮比赛开始前,以及所有比赛结束后,都会按照总分从高到低对选手进行一次排名。选手的总分为第一轮开始前的初始分数加上已参加过的所有比赛的得分和。总分相同的,约定编号较小的选手排名靠前。
每轮比赛的对阵安排与该轮比赛开始前的排名有关:第 (1) 名和第 (2) 名、第 (3) 名和第 (4) 名、(cdots)、第 (2K−1) 名和第 (2K) 名、(cdots) 、第 (2N−1) 名和第 (2N) 名,各进行一场比赛。每场比赛胜者得 (1) 分,负者得 (0) 分。也就是说除了首轮以外,其它轮比赛的安排均不能事先确定,而是要取决于选手在之前比赛中的表现。
现给定每个选手的初始分数及其实力值,试计算在 (R) 轮比赛过后,排名第 (Q) 的选手编号是多少。我们假设选手的实力值两两不同,且每场比赛中实力值较高的总能获胜。
模拟即可,注意对于较有序的序列归并排序要远快于快速排序。
快速选择算法:求一个数组中第 (K) 大的数字
算法思路:按快速排序的思路,但是每次与基准数比较就可以确定它在左右哪个序列里了,时间复杂度 (O(n)) .
const int N=1048576;
int a[N];
// STL 中有 nth_element 就是快速选择算法求 kth
void qselect(int l,int r,int k)
{ int x=a[(l+r)>>1];
int i=l,j=r,tmp=233;
while (i<=j)
{
while (a[i]<x) ++i;
while (a[j]<x) --j;
while (i<=j){swap(a[i],a[j]); ++i; --j;}
}
if (k<=j) qsort(l,j,k); // 左还是右?
if (i<=k) qsort(i,r,k);
}
芯片问题:
一共有 (n) 片芯片,芯片有好有坏。
我们不知道哪个是好芯片,哪个是坏芯片,但保证好芯片比坏芯片多。
我们可以用两片芯片互相测试对方是否是好芯片。
已知好芯片一定能返回正确的结果,
而坏芯片给出的结果是完全随机的。我们现在希望能找到一个好芯片。
做法:
偶数个:
分成两组,每组两个比较。
[好坏] 或者 [坏坏] 则把两块芯片都去掉;
如果结果是 [好好] 则任选其中一块去掉。
然后不断递归,最后剩下 (1) 个或 (2) 个即为好芯片。奇数个:
任取一个和其它所有芯片比较,如果有一半将此芯片判为好芯片。否则为坏芯片,去掉。
然后就变成偶数个的情况了。
逆序对问题:一共 (N) 个数,输出其逆序对个数(逆序对指 (i<j) 而 (a_i>a_j) 的有序对)
归并排序合并时统计一下即可。
2. 二分
二分就是每次把子问题分成 $dfrac 12 $ 的分治。
二分查找算法:给定一个长度为 (n) 的有序数组 (a) ,查询 (x) 是否在这 (n) 个数中 .
每次取中间的数 (mid),和 (x) 比较,因为 (a) 有序,所以可以确定 (x) 到底在 (mid) 左边还是 (mid) 右边,所以每次序列长度变为原长度的 (dfrac 12),复杂度 (O(log n)) .
const int N=1048576;
int a[N];
bool binary_search(int x)
{
int l=0,r=n-1,mid;
while (l<=r)
{
mid=(l+r)>>1;
if (a[mid]==x) return true;
if (a[mid]>x) r=m-1;
else l=m+1;
} return false;
}
二分算法在 OI 中最基本的两个运用情况:
- 求最值的最值:最大值的最小值 or 最小值的最大值
- 求满足某条件的最小 / 大值
我们将用两道题来具体分析这两个情况。
Problem 2:Monthly Expense(poj 3273)
给出 (N) 个月的开销,要求把 (N) 个月的开销按顺序划分成 (M) 个时间段,计算每一段的开销总和,求 (M) 个开销总和中最大值的最小值。
Sample Input:7 5 100 400 300 100 500 101 400
Sample Output:
500
二分最后的答案 (ans) .
每次的判断 (ans) 在左边还是右边就用贪心判定 (M) 个开销总和中最大值是否不超过 (m),即 (M) 个开销总和是否都不超过 (m) .
Problem 3:借教室(NOIp2012;洛谷 P1083;loj 2606)
在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。
面对海量租借教室的信息,我们自然希望编程解决这个问题。
我们需要处理接下来 (n) 天的借教室信息,其中第 (i) 天学校有 (t_i) 个教室可供租借。共有 (m) 份订单,每份订单用三个正整数描述,分别为 (d_j),(s_j),(t_j),表示某租借者需要从第 (s_j) 天到第 (t_j) 天租借教室(包括第 (s_j) 天和第 (t_j) 天),每天需要租借 (d_j) 个教室。
我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提供 (d_j) 个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。
借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第
(s_i) 天到第 (t_i) 天中有至少一天剩余的教室数量不足 (d_j) 个。现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改订单。
因为如果前一份订单都不满足,那么之后的所有订单都不用继续考虑;而如果后一份订单都满足,那么之前的所有订单一定都可以满足,符合局部舍弃性,所以可以二分订单数量。
check 的时候相当于一个区间加单点查询,差分即可。
Problem 4:自然数
你现在需要给小 A ( ext{Cnt}A) 个自然数,给小 B ( ext{Cnt}B) 个自然数。
但是给小 A 的自然数不能被 (x) 整除,
同理给小 B 的自然数不能被 (y) 整除。请问需要给的最大的那个自然数最小是多少?
二分答案。
check 的时候就判求分给两个人的数中,最大的数,能否 (le ans),即求分给两个人的数中,所有数能否 (le ans) .
也就是判能否把 (1sim ans) 的自然数,分给 A 和 B,满足 A 有 ( ext{Cnt}A) 个数,B 有 ( ext{Cnt}B) 个数。
令 (c_1=leftlfloordfrac nx ight floor),(c_2=leftlfloordfrac ny ight floor),(c_3=leftlfloordfrac n{xy} ight floor)。
先把 (c_1-c_3) 个数,分给 B,
再把 (c_2-c_3) 个数,分给 A。
剩下能自由分配的数只剩下 (ans - (c_1 + c_2 - c_3)) 个了。
所以判定即为 (M ge ext{cnt}A + ext{cnt}B)
Problem 5:给一个单峰函数,求峰顶。
三分板子。
对于一个单峰函数 (f),每次选取 (l,r) 中间的两个 ( t Lmid,Rmid) .
分类讨论:
- 当 (f( exttt{Lmid})>f( t Rmid)) 时,说明峰顶 (top) 在 (r) 左边,如「图 1」「图 2」。
- 当 (f( exttt{Lmid})le f( t Rmid)) 时,说明峰顶 (top) 在 (l) 右边,如「图 2」「图 3」。
const double eps=1e-8;
double f(double x){return x*x;} // y=x^2 函数
double search(double l,double r)
{
while (l+eps<r)
{
double mid1=(2*l+r)/3,mid2=(l+2*r)/3;
double f1=f(mid1),f2=f(mid2);
if (f1<f2) l=f1;
else r=f2;
} return l;
}
3. 倍增
倍增利用二进制来划分问题。
众所周知对于任意规模为 (n) 的问题,总能按二进制划分成一些规模为 (2^i) 的子问题。
Problem 6:求 (a^nmod p) .
快速幂板子。
设 (n=2^{alpha_1}+2^{alpha_2}+cdots+2^{alpha_r}) 且 (alpha_1 eq alpha_2 eqcdots eq alpha_r) .
则
直接把 (n) 二进制拆分即可。
typedef long long ll;
ll qpow(ll a,ll b,ll p) // quick pow
{
ll ans=1;
while (b)
{
if (b&1) ans=ans*a%p;
a=a*a%p; b>>=1;
} return ans;
}