限于自己的水平可能题目出得多少还会有这样那样的不足,有些题目比完赛后交流的时候才知道多少有些“撞题”,还望大家见谅O(∩_∩)O~。
这次我出的题目分别是A、B、C、D、E、G,下面就和大家分享一下我自己的一些思路吧。
Problem A:A Simple Problem
这个题目是我去食堂吃饭的路上想到的,但后来也是“撞题”最严重的一个题。这个题目有一个trick,就是在判断最大值与最小值之差是否大于K时,如果用int型做减法就可能超出了int的表示范围了,因此可以在比较时先强制转换成long long int。
首先最朴素的O(N^2)的方法是这样的,枚举每个点作为区间的起点,然后依次向右扫描并更新这个起点的最大值和最小值,直到最大值和最小值之差比K大为止。
但是这样当区间比较长的时候,运算量就会很大,而其中大部分都是重复的计算,细想一下我们就会发现,如果固定i为左端点,枚举到右端点j时最大值和最小值比K大了,那么必然j位置的值比[i,j-1]的最大值还大且和最小值之差大于K,或者比最小值还小且比最大值之差小于-K。
我们不妨只讨论一下j位置的值比最大值还大且和最小值之差大于K的情况。那么对于枚举i右边的一些点为左端点时,比如i+1,如果[i+1,j]的最小值没有改观的话,那么一定不会得到更有的解,也就是说我们一直枚举i+1,i+2……直到[i+x,j](x>=1)范围内的最小值有所改观为止,才有可能得到更优的解。
因此这样就得到了一个看起来更好的办法,初始时i、j都在0位置,然后j向前移动直到[i,j]中最大值和最小值之差大于K为止,然后向前移动i直到[i,j]中最大值和最小值之差不大于K为止,然后如此反复。
这样乍看上去是O(N)的办法,但还有一个操作没有解决,就是我们如何能够快速得知任意时刻[i,j]范围内的最大值和最小值呢?从插入和删除的角度考虑,可以用一个最大堆和一个最小堆来实现,这样由于每个元素最多入两个堆各一次,出两个个堆各一次,整体是O(NlogN)的复杂度。但是,实际上[i,j]范围内的最大值和最小值也可以通过两个单调队列来维护,这样由于每个元素最多入两个队列各一次,出两个队列各一次,整体是O(N)的复杂度。
Problem B:Covered By A Square
交流的时候有个ACMer告诉我说这个题和POJ的2482有些像,我看了之后确实是的,不过我这个题要比POJ那个简单。如果我没记错的话,这个题应该是躺在床上的时候想到的。
由于正方形是任意的,那么位置就很多了,于是我们要尽量在保证不影响结果的情况下“约束”一下正方形的位置。想了一下之后就会发现,我们可以得到一个贪心的思路:如果是最优的解的话,那么一定存在一个正方形使得某一个或某一些点在其左边这条边上。因为如果没有点在左边这条边上,我们可以将正方形向右平移,同时是不会使解变得更差的。
这样正方形的位置就比较少了,这时我们又会发现,如果限定了正方形左边这条边所在的直线后,正方形是可以上下滑动的,因此我们可以将正方形从最下面的位置滑动到最上面,并不断统计正方形内的点数就一定可以找到最优解。
接下来就遇到了和一开始类似的问题,正方形滑动是连续的,这样就不好确定了,但我们可以用同样的贪心的思路,每次滑动必然要使上面那条边遇到一个点为止,这样正方形的滑动的尺度问题也就解决了。
到此为止大体的思路就有了,剩下的就是细节的处理了:
第一,如何确定正方形左边所在的直线的各个位置?把x坐标提取出来排个序(或者不排序也可以,只不过排序之后可以一个if来避免枚举相同的x坐标作为正方形左边所在直线的位置),然后依次枚举即可。
第二,如何确定正方形要滑到哪个位置就算遇到了一个点?由于是从下向上滑,那么如果原来的各个坐标都是对y有序的话,扫描一遍所有的点就必然会先遇到下面的点,再遇到上面的点,当然,如果某个点的x的坐标不在正方形左右两边所在的直线之间,就可以不理会这些点了。这样就可以维护一个点的队列,队列里面放的就是可以被当前这个正方形覆盖的点,每次添加完新的点并删掉离开正方形的点之后,统计一下队列里元素的数目并更新最优解即可。
Problem C:Generating Queue
这个题目由于一开始我没想到一种直接递归搜索的方式,所以没有针对这种暴力的搜索方式出数据,于是开始的时候有一部分人用这种特定顺序的递归搜索的方式水过了,后来又加强了数据。
这个题目是我过年前坐火车回家的途中想到的。
由于数据量比较大,所以暴力搜索是解决不了问题的,而跟暴力搜索比较接近的一种思路就是记忆化搜索了,或者递推形式的dp。向着这个方面想了之后,最后就会发现这个题目可以转化成区间dp。
用f[i][j]表示队列g中i到j这些元素是否可以由队列s中的前j-i+1个字符生成,如果是则f[i][j]为1,否则f[i][j]为0,状态转移方程为f[i][j]=(f[i+1][j]&&g[i]==s[j-i])||(f[i][j-1]&&g[j]==s[j-i])。
Problem D:Congruent Triangles
D题初步的思路就是对于这个三角形的若干形态,求出外接矩形的长和宽,然后O(1)的时间计算出每个形态的三角形有多少个,最后累加到一起即可。
容易想到的形态就是旋转90度、180度或者镜面对称,或者两者结合起来,但对于一些特殊的三角形,形态便会有更多情况。比如0 0 5 0 5 5这个三角形,当三个点位于0 0 4 3 1 7的时候也是可以的,位于0 0 3 4 -1 7的时候也是可以的,当然,坐标为负数可以通过平移变成正的。
这时就会想到把三角形绕一个点旋转,看有多少种不同的形态,同时我们会发现一个可喜的结论,就是如果不考虑等腰三角形,那么只要0-360度内旋转的角度不同,得到的三角形一定不同。而且绕一个点旋转就可以得到所有的情况了,再绕其他点旋转就会产生重复了。当然,还要考虑镜面对称之后的三角形的旋转。等腰的情况我们稍后再谈,先将一般的情况解决掉。
我们不妨设这个三角形是OAB,其中O是原点,然后绕将三角形绕O点旋转,当然,这样有时候点的坐标会是负数,但没关系,我们关注的只是三角形的形态和三角形外接矩形的大小,所以坐标值本身并不重要。这时如果我们把OA和OB一起旋转的话,就会发现同时需要判断A点和B点是否是格点,这样旋转的尺度就不好把握了。
于是我们不妨换另一种思路,先把OA可能的位置也就是A点可能的坐标求出来,再把OB可能的位置也就是B点可能的坐标求出来,然后枚举OA和OB的位置,去看枚举的OA和OB组成的三角形是否和给定的三角形全等即可。这样乍一看OA和OB可能的位置是O(N)的,于是算法是O(N^2)的,但实际上OA的位置是依赖于类似x^2+y^2=n(n代表一个任意整数,和题目的已知量并无关系)这样形式的方程的整数解的个数的,而根据一些定理就可以知道这个方程的整数解的数目是远远小于题目中的N的。于是我们付出得最多的时候就是用O(N)的时间去预处理OA和OB可能的位置的时候,而枚举、判断的时间是可以忽略不计的,因此算法整体的复杂度是O(N)的。
对于等腰三角形的情况,我们分析之后就会发现枚举的位置相当于都重复了一次,因此将计算得的结果除以2即可。由于格点三角形不存在是等边三角形的情况,所以就不需要考虑等边三角形的情况了。
由于时间的原因,这个题目没有经过太严格的测试,所以我也不敢担保这样去做就一定没有什么漏洞,于是就把代码贴在这里了,如果大家发现了其中的BUG还希望能帮我指出来。
#include<stdio.h>
#include<string.h>
#include<math.h>
#define MAXD 100010
struct point
{
int x, y;
}vec1[4 * MAXD], vec2[4 * MAXD];
int N, M;
long long int det(int x1, int y1, int x2, int y2)
{
return (long long int)x1 * y2 - (long long int)x2 * y1;
}
long long int mul(int x1, int y1, int x2, int y2)
{
return (long long int)x1 * x2 + (long long int)y1 * y2;
}
long long int sqr(int x)
{
return (long long int)x * x;
}
long long int labs(long long int x)
{
return x < 0 ? -x : x;
}
int getmin(int x, int y, int z)
{
int t = y < z ? y : z;
return x < t ? x : t;
}
int getmax(int x, int y, int z)
{
int t = y > z ? y : z;
return x > t ? x : t;
}
void prepare(point *p, int x0, int y0, int &n)
{
int i, j, k;
n = 0;
long long int a, b, mode = sqr(x0) + sqr(y0);
k = (int)sqrt((double)mode + 0.5);
for(a = 0; a <= k; a ++)
{
b = (long long int)sqrt(mode - a * a + 0.5);
if(a * a + b * b == mode && b != 0)
{
p[n].x = b, p[n].y = a;
++ n;
p[n].x = -a, p[n].y = b;
++ n;
p[n].x = -b, p[n].y = -a;
++ n;
p[n].x = a, p[n].y = -b;
++ n;
}
}
}
void calculate(point *p, point *q, int n1, int n2, long long int s, long long int m, long long int &ans)
{
int i, j, k, minx, miny, maxx, maxy;
for(i = 0; i < n1; i ++)
for(j = 0; j < n2; j ++)
{
if(mul(p[i].x, p[i].y, q[j].x, q[j].y) == m && det(p[i].x, p[i].y, q[j].x, q[j].y) == s)
{
minx = getmin(0, p[i].x, q[j].x);
maxx = getmax(0, p[i].x, q[j].x);
miny = getmin(0, p[i].y, q[j].y);
maxy = getmax(0, p[i].y, q[j].y);
if(maxx - minx <= N && maxy - miny <= M)
ans += (long long int)(N - maxx + minx + 1) * (M - maxy + miny + 1);
}
}
}
void deldup(int x1, int y1, int x2, int y2, int x3, int y3, long long int &ans)
{
long long int t1, t2, t3;
t1 = sqr(x2 - x1) + sqr(y2 - y1);
t2 = sqr(x3 - x1) + sqr(y3 - y1);
t3 = sqr(x3 - x2) + sqr(y3 - y2);
if(t1 == t2 || t2 == t3 || t3 == t1)
ans /= 2;
}
void solve()
{
int i, j, k, x1, y1, x2, y2, x3, y3, n1, n2;
long long int s, m, ans = 0;
scanf("%d%d%d%d%d%d", &x1, &y1, &x2, &y2, &x3, &y3);
prepare(vec1, x2 - x1, y2 - y1, n1);
prepare(vec2, x3 - x1, y3 - y1, n2);
s = labs(det(x2 - x1, y2 - y1, x3 - x1, y3 - y1));
m = mul(x2 - x1, y2 - y1, x3 - x1, y3 - y1);
calculate(vec1, vec2, n1, n2, s, m, ans);
calculate(vec2, vec1, n2, n1, s, m, ans);
deldup(x1, y1, x2, y2, x3, y3, ans);
printf("%lld\n", ans - 1);
}
int main()
{
while(scanf("%d%d", &N, &M) == 2)
{
solve();
}
return 0;
}
Problem G:Cross The River
这个题目可以用dp去做,而且dp的方式也不止一种。
我当时出完这个题目之后的一个思路就是用f[i][j]表示第i步到达第j个位置的时候所需要经过的最少的石墩数,这样状态转移方程就是f[i][j]=min{f[i-1][k]}(k<j且k和j相距不超过K)+(j==石墩),j==石墩的意思是如果j是石墩这个位置就是1,如果j是荷叶这个位置就是0。
如果不加优化的话,这样的复杂度应该是介于O(N^2)和O(N^3)之间。我确实不太会算这种算法的复杂度,因为如果发现第i步可以到达终点之后就直接break了,这样如果K小的话,i循环的次数会变大,k循环的次数会变小,而K大的话k循环的次数就会变大,i循环的次数又会变小。
上面的算法用单调队列优化之后可以达到最坏的复杂度是O(N^2)的效果。
此外,这个题还可以将最少步数和经过的最少的石墩数分别用两个数组记录,然后用类似求最长上升子序列的方式进行dp。
其他的题目可以参考下面的出题人的解题报告:
比赛题目链接(题目序号为1170-1178):http://acm.csu.edu.cn/OnlineJudge/problemset.php?page=2