hiho难度1题目题解

hiho是个很不错的oj网站。定位应该是找工作的应届生。看起来很新颖,但实际上还是传统oj,只不过出题的由学校变成企业,讨论也很少,感觉几乎没什么人。它最吸引我的就是题目比较好,不像leetcode那样烂大街(leetcode的题目我并不怎么喜欢),很有acm的感觉;题目数量不多,但分类明确,有挑战有知识含量,省得我这种不是专门刷题的工作党到处找题做。缺点就是优点换个说法:太acm,不像leetcode那样专门为了企业招人;题目偏难,分类太过明确。

最近为了提高自己的修养,不烂在一个地方,重新开始刷oj。poj刷了一些,题目参差不齐,有点浪费时间;hiho就好多了。先把level1的都做完。这些题目难度不大,对我来说基本读完题就能有思路;但同时陷阱也不少,自己又好多年没写过这种模式的代码,不免好多时间都在找错误。虽然没专门做acm,但自己这几年写了不少代码,工程上考虑更多,也有些基础,所以做起来还算得心应手。这里先总结一下最简单的lv1。

所有代码都可以在这里找到。下面是每道题目的简单思路。

1039 字符消除

非常纯粹的一道模拟题。做的时候没有仔细看,以为插入字符之后可以自由选择消除顺序。那样的话会是一个很复杂的问题。 写了一会儿我开始怀疑这是个NP问题——只能搜索。第二天才发现原来过程是贪心的,有满足条件的就消除,然后再迭代。这样的话它就变成了一道很简单的题目。但是我还犯了一个想当然的错误,觉得添加一个相同的字符一定比其它的要优秀,当然自己也很快 想出了反例,比如AAABA -> ABAABA是最优解。

1051 补提交卡

很直接的思路——一定要按顺序填补。

1082 然而沼跃鱼早就看穿了一切

简单的字符串处理。

1094 Lost in the City

有的时候,最麻烦的处理反而最简单。所以我直接手写了八种匹配情况,看起来也不乱,跟着转就行。

bool check(int x, int y)
{
        if (a[x][y]==b[0][0] && a[x][y+1]==b[0][1] && a[x][y+2]==b[0][2] && 
        	 a[x+1][y]==b[1][0] && a[x+1][y+1]==b[1][1] && a[x+1][y+2]==b[1][2] &&
                a[x+2][y]==b[2][0] && a[x+2][y+1]==b[2][1] && a[x+2][y+2]==b[2][2])
                return true;
        if (a[x][y]==b[2][0] && a[x][y+1]==b[1][0] && a[x][y+2]==b[0][0] &&
                a[x+1][y]==b[2][1] && a[x+1][y+1]==b[1][1] && a[x+1][y+2]==b[0][1] &&
                a[x+2][y]==b[2][2] && a[x+2][y+1]==b[1][2] && a[x+2][y+2]==b[0][2])
                return true;
        if (a[x][y]==b[2][2] && a[x][y+1]==b[2][1] && a[x][y+2]==b[2][0] &&
                a[x+1][y]==b[1][2] && a[x+1][y+1]==b[1][1] && a[x+1][y+2]==b[1][0] &&
                a[x+2][y]==b[0][2] && a[x+2][y+1]==b[0][1] && a[x+2][y+2]==b[0][0])
                return true;
        if (a[x][y]==b[0][2] && a[x][y+1]==b[1][2] && a[x][y+2]==b[2][2] &&
                a[x+1][y]==b[0][1] && a[x+1][y+1]==b[1][1] && a[x+1][y+2]==b[2][1] &&
                a[x+2][y]==b[0][0] && a[x+2][y+1]==b[1][0] && a[x+2][y+2]==b[2][0])
                return true;
        return false;
}

1120 小Hi小Ho的惊天大作战:扫雷·三

这是第一道让我感到有些头晕的题目。估计是那天在家头脑不清醒。我总是找不到最好的方法来判断什么样的情况满足局面,怎么样把一个位置标记为有雷。第二天想明白了,先填充好,然后枚举一遍判断就行。数据量很小,直接搞一个10位bits。

1121 二分图一?二分图判定

二分图就很常用了,判断方法也很简单。BFS扫描一遍染色就可以。

1135 Magic Box

似乎是微软校招题。非常简单,我的做法是先把三个数排序,这样就只有三种差值情况了。

1137 Recruitment

这道题目让我感到很自豪,终于觉得自己就算在算法思路上,也是比高中强不少。很明显我们能想到背包问题,而M和F的设定就是扰乱视线,和最终的实现没什么关系,只要分开处理,然后合并答案就可以。也是个二维背包,写的时候还想了好久怎么处理顺序。最后也算自己想了清楚。

void packet(int n, int m, int x, Value &f, Person &a)
{
    f[0][0].value = 0;
	for (int now=1; now<=n; ++now) {
		for (int i=now; i>=1; --i){
			int temp;
			for (int j=m; j>=a[now].price; --j) {
				temp = f[j-a[now].price][i-1].value + a[now].value;
				if (temp >= 0 && temp > f[j][i].value) {
					f[j][i].value = temp;
					f[j][i].used = f[j-a[now].price][i-1].used;
					f[j][i].used.set(a[now].index);
				}
			}
		}
	}
}

// find answer
bitset<101> used;
for (int i=0; i<=B; ++i) {
	for (int j=0; j<=B-i; ++j) {
		if (f[i][X].value + m[j][Y].value > Ans) {
			Ans = f[i][X].value + m[j][Y].value;
			Cost = i + j;
			used = f[i][X].used | m[j][Y].used;
		}
	}
}

1138 Islands Travel

很明显的dijkstra+heap优化,当时偷了个懒,顺便回忆了迭代的spfa,没想到spfa这种奇葩算法也能通过数据。话说回来,hiho的数据并不算苛刻。

1143 骨牌覆盖问题·一

我能说什么呢,以前最爱给别人讲的就是幂运算+简单矩阵乘来算斐波那契数列。

#include <iostream>

using namespace std;

int main()
{
        int n;
        const int m = 19999997;
        long long a=0, b=1, c=1, d=1;
        long long x=1, y=0, z=0, q=1;
        long long t1, t2, t3, t4;
        cin >> n;
        while (n) {
                if (n%2) {
                        t1 = (a*x%m + c*y%m)%m;
                        t2 = (b*x%m + d*y%m)%m; 
                        t3 = (a*z%m + c*q%m)%m;
                        t4 = (b*z%m + d*q%m)%m;
                        x = t1, y = t2, z = t3, q = t4;
                }
                t1 = (a*a%m + b*c%m)%m;
                t2 = (a*b%m + b*d%m)%m;
                t3 = (a*c%m + c*d%m)%m;
                t4 = (b*c%m + d*d%m)%m;
                a = t1, b = t2, c = t3, d = t4;
                n /= 2;
        }
        cout << q << endl;
        return 0;
}

1148 2月29日

这道题让我感到挫败。因为我居然不太清楚闰年为什么是闰年,或者说我没有好好想一想。闰年显然是为了弥补历法和实际公转的某个差值,所以得过些年加一天补充一次,不然四季就会错乱,所以规定整100年时必须被400整除才算闰年。而我印象中却是过4年就是闰年。人类的认知,人类的寿命,跟宇宙比起来, 实在太渺小啊。

此外这里有个小技巧,(年份/4-年份/100+年份/400)是耶稣诞生以来的闰年数。我在代码里注释了N个自己犯的小错误,惭愧的很。

1152 Lucky Substrings

这个题目简单到离谱,然而不审题的后果就是错了也不知道为什么。我到现在还是想当然……

1175 拓扑排序·二

拓扑排序很有意思,我以前就对它的效率表示怀疑。当然,每个点最多进一次队列,所以确实是O(M)的。我以为value为0的边可以不用加入队列,实际上这是个大错误,入度存在,但是边没加进去,拓扑就不会成功。

1186 Coordinates

枚举就可以。注意for循环的上限,是x而不是x-1。

1197 Give My Text Back

这个题很有意思。这种题目的通过率往往很低,大家都容易少考虑很多情况。我还算比较有心得,只要一步一步考虑,思考当前位置应该发生什么,还是比较简单的。

1223 不等式

很容易想到枚举可能的数字,然后枚举等式,计数。但是数据可能是小数,所以很坑。我的做法是乘10,然后当做再枚举。每次做这种题我都觉得好多所谓“算法”和“思路”比赛,其实只是些小trick。做了当然有进步,但是进步真有多大呢?

1268 九宫

这个题和之前的1094有点像。我也是采用类似的方法,先把所有可能算出来。不过这次比较惨,上下颠倒的时候写错了一个数。

string start = "04923578160";

string turn_right(const string &s)
{
        string t = s;
        t[1] = s[7], t[2] = s[4], t[3] = s[1];
        t[4] = s[8], t[5] = s[5], t[6] = s[2];
        //                        t[7] = s[3] fuck!!!!
        t[7] = s[9], t[8] = s[6], t[9] = s[3];
        return t;
}

string left_to_right(const string &s)
{
        string t = s;
        for (int i=1; i<=3; ++i) {
                t[i] = s[i+6];
        }
        for (int i=7; i<=9; ++i) {
                t[i] = s[i-6];
        }
        return t;
}

string up_to_down(const string &s)
{
        string t = s;
        t[1] = s[3], t[3] = s[1];
        t[4] = s[6], t[6] = s[4];
        t[7] = s[9], t[9] = s[7];
        return t;
}

这么简单的手写都容易出错,所以把事情分成小块交给程序,保证程序的没一小部分都正确是很有必要的。

1272 买零食

因为最多可以买三样零食,所以枚举三层就可以。注意在第一层和第二层也需要计算,因为不一定非卖三样。同样,我也把数据乘10,这样方便计算是否被5整除。

1283 hiho密码

这就是最经典的二分了。但是要让所有人一下就想到二分并且写对,估计也不是那么容易。以前NOIP就因为二分写错,到现在不知道为什么。 这次换了种写法。

    int l = 0, r = n, mid = l+(n-l)/2;
	while (l < r) {
		if (check(mid)) {
			r = mid;
		} else {
			l = mid + 1;
		}
		mid = l+(r-l)/2;
	}

其实保证二分正确有一些基本原则:所有数据尤其是边界能被覆盖、不会死循环和走出二分之后能拿到正确的值。比如我这种写法,r一定是正确的值,而每一步r或l会有变化,同时l<r的设定保证一定能走出来。

1288 Font Size

这道题也可以用二分的思想解决——我们假定某个答案是X,然后判断它对不对。这得保证答案的有序性,比如x满足条件,那么x+1或者x-1一定满足。这样才可以二分。这道题就符合条件,最小的肯定满足,我们要找的是满足条件的最大的。所以二分可行。

    int r = min(w, h)+1, l = 1;
	while (l < r) {
		int mid = l+(r-l)/2;
		if (!check(mid)) {
			r = mid;
		} else {
			l = mid + 1;
		}
	}

可以注意到我这里做了少许变化,因为答案的有序性和上一题正好相反。可以想到,l是不满足条件的最小值,所以l-1是满足条件的最大值。

1296 数论三·约瑟夫问题

这个问题非常巧妙。以至于我不得不单独写一篇文章来说一下。

1297 数论四·扩展欧几里德

同样,扩展欧几里得的推导也很有意思。我也想单独写一篇文章来说这个问题,包括中国剩余定理。

1304 搜索一·24点

题目解释里给了不少提示:增加两种运算 反- 和 反/,就可以化成(((a b ) c) d ) 和 ((a b) (c d))这两种情况。只要枚举数字,再枚举运算符就可以。 这里我犯了个大错,枚举出三个运算符之后,计算情况一,当我发现被除数为0或者情况不满足时会continue,这样跳过了第二种情况的计算。 这个错误隐藏很深,发现之后不得不写个goto来简单处理。

1308 搜索二·骑士问题

这个题我用了状态压缩的思路来存储状态,把它们按8进制压缩。之后再进行BFS遍历。状态转移和判断条件都写得比较清晰,也注意了很多问题。也在写的过程中简化了不少代码。

inline bool valid(int x, int y)
{
    return (x >= 0 && x < 8 && y >=0 && y < 8);
}

inline bool ok(int status)
{
	return ((status & 077) == ((status >> 6) & 077)) && ((status & 077) == (status >> 12));
}

int find_ans(int start)
{
	queue<int> q;
	map<int, int> path;

	q.push(start);
	path.insert(make_pair(start, 0));

	while (!q.empty()) {
		int now = q.front();
		q.pop();
		int a[] = {now >> 12, (now >> 6) & 077, now & 077};
		int d = path[now];

		if (ok(now)) {
			return d;
		}

		for (int i=0; i<3; ++i) {
			for (int j=0; j<8; ++j) {
				int orig = a[i];
				int x = (orig >> 3) + mov[j][0], y = (orig & 07) + mov[j][1];
				a[i] = x*8 + y;
				int next = (a[0] << 12) + (a[1] << 6) + a[2];
				if (valid(x, y) && path.find(next) == path.end()) {
					if (ok(next)) {
						return d + 1;
					}
					q.push(next);
					path.insert(make_pair(next, d+1));
				}
				a[i] = orig;
			}
		}
	}
	return 1;
}

1321 搜索五·数独

数独和八皇后算是搜索和回溯的经典问题了。关键是判重,我这里定义了一个三维的bool数组,v[3][N][N],用来记录第i行的数字k、第i列的数字k和第i个小方块的数字k已经被占用。这样在搜索到i\j这个坐标时,可以尽可能少的知道该枚举哪个值。

bool find_ans(int x, int y, int (&a)[n][n], bool (&v)[3][N][N])
{
    if (x == n) {
		return true;
	}
	if (a[x][y] > 0) {
		if (y == n-1) {
			return find_ans(x + 1, 0, a, v);
		} else {
			return find_ans(x, y + 1, a, v);
		}
	}

	for (int i=1; i<=9; ++i) {
		if (!v[0][x][i] && !v[1][y][i] && !v[2][get_block(x,y)][i]) {
			a[x][y] = i;
			v[0][x][i] = true;
			v[1][y][i] = true;
			v[2][get_block(x,y)][i] = true;
			bool result = false;
			if (y == n-1) {
				result = find_ans(x + 1, 0, a, v);
			} else {
				result = find_ans(x, y + 1, a, v);
			}
			if (result) {
				return true;
			}
			a[x][y] = 0;
			v[0][x][i] = false;
			v[1][y][i] = false;
			v[2][get_block(x,y)][i] = false;
		}
	}
	return false;
}

此外还要注意一些边界问题。

1325 平衡树·Treap 1329 平衡树·Splay 1333 平衡树·Splay2 1337 平衡树·SBT

这四个题目可以算是平衡树的练习题。我也很久没写过了,所以把一些操作杂糅在了一起。基本操作还是splay,它也能应对这四道题目的所有数据。代码真的很丑。

1350 Binary Watch

这个“二进制表”很简单,枚举+统计二进制位的1个数。自己写或者用bit都可以。

1361 Playfair密码表

也是很单纯的模拟题。注意点也很少。

1365 图片排版

这道题是最后做的。虽然lv1评级,但是我觉得真需要一些思考。首先观察数据,虽然矩阵很多,但是宽度有限,最大才100。而只能去掉一个矩阵,因此顺序几乎是固定的。

我们这样想,如果去掉的是第i个矩阵,前i-1的排列方式已经知道,只要把后i+1个矩阵挨着前i-1放置就可以。大致定义解决问题的方程。 F[I][J]表示把第I个矩阵放到坐标J,J最大为M,M最大100,因此空间可以接受。但是从1~n计算有些困难,我当时就在这里卡了一会儿,这样的话情况会有太多,尤其是涉及到最有一个需要压缩。突然想到,如果倒着来,一切就又是已知的了。剩下的就是编码问题。

编码难度其实也不低。这里给出我做预处理的代码,完整代码github上也有。

    inline int cut(int left, int k, int &high)
    {
        if (left < k) {
		    high = static_cast<int>(ceil(static_cast<double>(left*high)/k));
		    return 0;
	    } else  {
		    return left - k;
	    }
    }

    for (int i=1; i<=n; ++i) {
		int high;
		cin >> a[i] >> b[i];
		f[i] = f[i-1];
		high = b[i];
		f[i].left = cut(f[i].left, a[i], high);
		if (high > f[i].highest) {
			f[i].highest = high;
		}
		if (f[i].left == 0) {
			f[i].left = m;
			f[i].high += f[i].highest;
			f[i].highest = 0;
		}
	}

	for (int i=1; i<=m; ++i) {
		int high = b[n];
		if (a[n]+i-1 >= m) {
			high = static_cast<int>(ceil(static_cast<double>((m-i+1)*high)/a[n]));
		}
		g[n][i].highest = high;
		if (i == 1) {
			g[n][i].high = high;
			g[n][i].highest = 0;
		}
	}

	for (int i=n-1; i>0; --i) {
		for (int j=m; j>=1; --j) {
			int high = b[i];
			if (a[i] + j - 1>= m) {
				// fuck! j is not i !!!!!!
				high = static_cast<int>(ceil(static_cast<double>((m-j+1)*high)/a[i]));
				g[i][j].highest = high;
				g[i][j].high = g[i+1][1].high;
			} else {
				g[i][j] = g[i+1][a[i]+j];
				if (high > g[i][j].highest) {
					g[i][j].highest = high;
				}
			}
			if (j == 1) {
				g[i][j].high += g[i][j].highest;
				g[i][j].highest = 0;
			}
		}
	}