有趣的问题
文件命名问题
给定n个文件名,第i个文件名记作namei,要求我们在文件系统中创建对应的n个文件。
文件系统的规则是这样的,如果我们要创建名字为x的文件,那么如果文件系统中已经存在同名文件了,我们就需要将新创建的文件名改成x(1),x(2),...
中第一个未被使用的名字。
要求判断每个文件在创建后的文件名是啥。
一个简单的做法,就是我们充当文件系统的角色,用哈希表维护所有出现的文件名,然后逐一去判断。下面给出代码:
String getName(String x, int index) {
String y = x;
if(index > 0) {
y += "(" + index + ")"
}
return fileSystem.contains(y) ? getName(x, index + 1) : y;
}
但是稍微思考一下就会发现,如果文件名都相同,那么时间复杂度会恶化到O(n2)。
实际上上面的问题我们可以O(n)解决,做法就是为每个名字x,都维护一个计数器C(x),表示目前已知x的最大编号,初始值为−1。
接下來,当我们处理文件名x的时候,如果C(x)=−1就尝试x,否则我们先尝试x(C(x)+1)
,如果文件系统存在了就尝试下一个。一直到找到第一个不存在的文件名x(t)。之后我们将C(x)更新为t。
下面我们证明上面算法的时间复杂度是O(n)。可以发现上面过程中可能会导致时间复杂度超过O(n)的部分就是上面的循环尝试x(C(x)+1),x(C(x)+2),...
的部分,这时候已经确认x存在于文件系统了。
考虑x(C(x)+1)
尝试失败的原因,这个文件只可能是因为我们首次创建了一个名字叫做x(C(x)+1)
的文件。同时发现x(C(x)+1)
这个文件的创建也只会使得x
的循环部分增加1。
因此我们知道最终只会创建最多n个名字,因此上面循环试错阶段总长度也是O(n)的,总的时间复杂度就是O(n)。
环上最小相邻元素差
给定n个数a1,…,an。我们将其重新排列组合,组成一个环b1,…,bn,其中b1与bn相邻。记这样的排列的费用为maxi|bi+1−bi|。找出一个排列使得费用最小。
一道牛客的题目。
对于n≤2的情况,这个问题是微不足道的。考虑n>2的情况。
我们先认为a1,…,an是递增的(如果不是,就先排个序)。之后我们考虑序列a1,a3,a5,…,a4,a2,即我们将下标为奇数的按原序排在前面,下标为偶数的按逆序排在后面。记c为这样排列的费用,我们证明c是最小的费用。
假设最优排列的费用t小于c,则我们可以找到一个下标i,满足ai+2−ai=c。我们将下标小于等于i的数记作第一类数,将ai+1记作第二类数,将小标大于等于i+2的数记作第三类数。我们可以发现在最优排列中第一类数和第三类数是不允许相邻的。但是由于第一类数和第三类数都是存在的,且第二类数仅有一个。我们从环上删除唯一的第二类数,这时候环变成一个链表,此时第一类数和第三类数一定会相邻。
牛吃饲料问题
一条直线上,在位置0处有n条牛,第i条牛需要吃ai单位草才能吃饱。现在有m个地方提供饲料,第i个地方的位置在坐标xi。每个饲料点同时只能喂养一头牛,每头牛可以有如下每秒钟可以做如下操作:
- 向左或向右移动1单位距离
- 如果处于某个饲料点,则可以吃1单位饲料
问最少花多少时间,才能把所有牛喂饱。其中1≤ai,xi≤109,1≤n,m≤105。
这道题来源于SRM739的HungryCowsMedium。
一开始我的想法是错误的,我认为最优解情况下一头牛只可能在一个饲料点吃草,这时候问题变成有m个背包,n个物品,判断是否有一种方案可以将所有物品装入到背包中。在背包数目多于2的情况下,问题是无法求解的。
但是实际上牛可以在多个位置吃草。比如a=[4,4,4]
,x=[2,4]
的情况下,答案是9。
我们可以发现如果答案是t,则位于xi的饲料点能提供的饲料量为max(0,t−xi)。我们按照这些草料产生的时间从大到小编号,第一份的编号为1。我们可以通过预先对xi进行排序,因此可以发现每个饲料点能提供的饲料量随着下标的增大而递减。
下面我们发现如果一头牛k如果在第i,j个饲料点都吃了草,且i<j,且记lk(i)为牛k在第i个位置吃的第一份草的编号,则rk(i)表示牛k在第i个位置吃的最后一份草的编号,可以发现rk(i)<lk(j)。
我们可以发现第i头牛只能吃前L(i)个位置的草料。这就给了我们一个贪心的思路,就是我们按牛的食量从大到小进行排序。而牛从第一个位置的草料开始吃,并优先吃编号较大的草料。
众数问题
题目1:给定n个数a1,a2,…,an,以及整数k,要求找出其中任意一个出现超过n/k次的元素,如果有多个,任意输出其中一个,否则输出−1。
我们可以不断从集合中删除k个不同的元素,直到不同的元素数目少于k,如果一个数出现超过n/k次,则其最终一定会被剩下来。这个过程可以O(kn)完成。
之后对每个出现的元素,考虑它们的出现次数即可。时间复杂度为O(kn),空间复杂度为O(k)。当然如果我们直接统计每个数出现的次数时间复杂度为O(n),空间复杂度为O(n)。
题目2:给定n个数a1,a2,…,an,记f(k)表示我们允许每次删除k个不同的元素,最多能执行多少次操作。要求输出f(1),f(2),…,f(n)。
一道题目。
我们先进行离散化,记ci表示整数i出现的次数。对于给定的k,我们可以设计一个贪心算法,就是每次选择出现次数最多的k个数各删除一个。这个算法跑一轮的时间复杂度为O(nlog2n),总的时间复杂度为O(n2log2n)。
我们发现如果一个数出现次数超过n/k,则我们每次都必定会选择它,换言之,问题实际上在问,在剩余的数中,我们每次可以选择k−1个不同的数删除,问最多执行多少次操作。
命题:对于任意n≥0,如果每个数的出现次数不超过n/k,那么总共可以执行操作⌊nk⌋次。
证明:通过归纳法证明。当n<k的时候,因为每个数出现次数不超过n/k,即每个数出现次数都是0,因此答案是0。对于n≥k,且n−1时命题成立,那么由于每个数出现次数都不超过n/k,因此出现次数最多的k个数,都至少出现了1次。我们将出现次数按从大到小排序,记做b1,b2,…,bm。我们选择最多的k个数执行一次操作后,接下来需要证明所有数次数不超过(n−k)/k=n/k−1。很显然对于b1−1,…,bk−1是满足条件的,我们再考虑bk+1。由两种情况,一种是bk+1<b1,这时候由于bk+1≤b1−1,因此所有数都满足条件。还有一种情况是bk+1=b1,若bk+1>n/k−1⇒bk+1≥n/k。则由于b1≥b2≥…≥bk+1,即∑k+1i=1bi≥(n/k)⋅(k+1)>n,这是不可能的。因此我们证明了对于任意非负整数n,命题都成立。
模运算下的比较
题目1:给定一个序列a0,a1,…,an−1,其中ai=y+ix(modm),x是给定的数,要求计算有多少个j,满足j<n−1且aj<aj+1。其中1≤n,m,x,y≤109。
如果x=0,则答案为0。否则我们可以通过统计有多少项j,满足aj≥aj+1来得到结果。由于x≠0,因此我们只需要考虑aj>aj+1的情况即可。
这个不是很好统计,但是如果我们能注意到此时一定有⌊(j+1)x+ym⌋=⌊jx+ym⌋+1,那么问题就迎刃而解了。
答案就是⌊(n−1)x+ym⌋。