题解
挑了几道想讲的题目。
D - Sticks
题意
给定
n
n
n根木棍,将这些木棍分组,使得每组的木棍长度和相等,问每组木棍长度和的最小值是多少。
n
≤
50
n\leq50
n≤50
思路
这题考虑用枚举每组木棍的长度和,然后判定是否可行。直接爆搜不剪枝通过不了这题,先来粗略估算一下单次判定的时间复杂度,假设现在64根木棍,10组,每根木棍可以放在任意一组,所以有
6
4
10
64^{10}
6410种放法,显然不能在1s内通过。
现在判定方案是否可行时采用一组一组填的方式,这种方式相比前面那种的方式的好处是如果现在某一组没填满就能及时不再尝试。仅此还是不能过掉这题,考虑如何剪枝,这题是一道蓝书上很好搜索剪枝的例题,要运用到多种剪枝方法才能通过这道题。
蓝书上把该题的剪枝分为两类:
- 优化搜索顺序
- 排除等效冗余
在搜索顺序方面每组先放长的比先放短的好,因为先放长的每组的剩余空间更小,搜索状态数也更少。
先考虑如何排除等效冗余。在前面的粗略估算中,其实重复的有很多,以下分类考虑:
- 假设现在某组有两根木棍,长度分别为 x , y x,y x,y,先放 x x x再放 y y y和先放 y y y再放 x x x是一样的,于是可以限制每组先放长的再放短的。
- 每根长度相同的木棍是等价的,于是在某时刻放了长度为 x x x的木棍发现失败后,下次不用再尝试长度为 x x x的木棍。
- 每一个没放木棍的组是等价的, 所以在某一时刻,在一个空组放了一根木棍发现失败后,这根木棍放其他的空组也一样会失败,可以直接剪掉。
- 如果存在某时刻,该组放了最后一个木棍 x x x填满该组,发现失败,可以直接剪掉,因为如果用其他若干木棍顶替 x x x填满该组,设有 x = a + b x=a+b x=a+b,用了 a a a和 b b b顶替 x x x,那么填 x x x剩下木棍 a , b a,b a,b及其他木棍,填 a , b a,b a,b剩下 x x x和其他木棍,这里两种填法的“其他木棍”是相同的,显然第一种接下来能达到的状态完全包含第二种,所以第一种失败第二种必然失败。
代码
//#include <bits/stdc++.h>
#include <cstdio>
#include <cctype>
#include <cstdlib>
#include <algorithm>
#include <iostream>
#include <cstring>
using namespace std;
#define fo(i, x, y) for (int i = (x); i <= (y); ++i)
#define fd(i, x, y) for (int i = (x); i >= (y); --i)
const int maxn = 64 + 5;
int n, tot, siz;
int a[maxn];
bool used[maxn];
int getint()
{
char ch;
int res = 0, p;
while (!isdigit(ch = getchar()) && (ch ^ '-'));
p = ch == '-'? ch = getchar(), -1 : 1;
while (isdigit(ch))
res = (res << 3) + (res << 1) + (ch ^ 48), ch = getchar();
return res * p;
}
bool dfs(int cnt, int len, int last)
{
if (cnt == tot) return true;
if (len == siz) return dfs(cnt + 1, 0, 0);
fo(i, last + 1, n)
if (!used[i] && len + a[i] <= siz)
{
used[i] = true;
if (dfs(cnt, len + a[i], i)) return true;
used[i] = false;
if (!len || a[i] + len == siz) return false;
while (i + 1 <= n && a[i + 1] == a[i]) i++;
}
return false;
}
void work()
{
int sum = 0;
fo(i, 1, n) a[i] = getint(), sum += a[i];
sort(a + 1, a + 1 + n, greater<int>());
fo(i, 1, sum)
if (!(sum % i))
{
siz = i; tot = sum / siz;
memset(used, 0, sizeof(used));
if (dfs(0, 0, 0))
{
printf("%d\n", i);
return;
}
}
}
int main()
{
while (~scanf("%d", &n) && n) work();
return 0;
}
E - Find The Multiple
题意
给定一个
n
n
n,找到一个
n
n
n的倍数
m
m
m,要求
m
m
m满足在十进制下每位是1或0。
n
≤
200
n\leq200
n≤200,保证
m
m
m的位数不超过100。
思路
这题的搜索做法是爆搜每一位是0还是1,在网上看到一个比较有趣的优化方法式,采用堆式存储,即10和11的父亲是1,100和101的父亲是10,即在父亲的末尾添上0/1,把
m
m
m看成是二进制,其对应的十进制节点关系就是,节点
x
x
x的父亲是
⌊
x
2
⌋
\lfloor \frac{x}{2}\rfloor
⌊2x⌋,根节点是
1
1
1。设当前搜索到的数
t
t
t看成二进制后对应的十进制数为
x
x
x,在搜索的时候用
a
x
a_x
ax记录
t
%
n
t\% n
t%n,则容易的到递推式
a
x
=
(
a
x
/
2
∗
10
+
t
%
10
)
%
n
a_x=(a_{x/2}*10+t \%10)\% n
ax=(ax/2∗10+t%10)%n,表达式的含义式在
x
x
x父亲的基础上加一位。
这题有一个比较容易想到的dp做法,即设
f
i
,
j
f_{i,j}
fi,j表示长度为
i
i
i,
%
n
=
j
\%n=j
%n=j的
m
m
m是否存在,转移式也很好写,根据第
i
i
i位填1还是0进行转移,转移方程看代码。
代码
//#include <bits/stdc++.h>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;
#define fo(i, x, y) for (int i = (x); i <= (y); ++i)
#define fd(i, x, y) for (int i = (x); i >= (y); --i)
const int maxn = 200 + 5;
int n;
int f[maxn][maxn][2], from[maxn][maxn][2];
int getint()
{
char ch;
int res = 0, p;
while (!isdigit(ch = getchar()) && (ch ^ '-'));
p = ch == '-'? ch = getchar(), -1 : 1;
while (isdigit(ch))
res = (res << 3) + (res << 1) + (ch ^ 48), ch = getchar();
return res * p;
}
void putans(int n, int t)
{
while (n)
{
printf(f[n][t][1]? "1" : "0");
t = f[n][t][1]? from[n][t][1] : from[n][t][0];
n--;
}
}
void work()
{
memset(f, 0, sizeof(f));
int p = 1;
f[0][0][0] = 1;
fo(i, 1, 100)
{
fo(j, 0, n - 1)
if (f[i - 1][j][0] || f[i - 1][j][1])
{
f[i][(j + p) % n][1] = f[i][j][0] = true;
from[i][(j + p) % n][1] = from[i][j][0] = j;
}
(p *= 10) %= n;
if (f[i][0][1])
{
putans(i, 0);
printf("\n");
return;
}
}
}
int main()
{
while (~scanf("%d", &n) && n) work();
return 0;
}
H - Following Orders
题意
给定若干个变量对之间的大小关系,输出所有可能所有变量大小关系。
变量用a-z表示。
思路
对于一对变量 x < y x<y x<y,连一条 x x x指向 y y y的边。那么入度为0的点就是最小的,一个拓扑序就是一个合法的方案,所以只要dfs,每次选不同的入度为0的点。
代码
//#include <bits/stdc++.h>
#include <cstdio>
#include <cstdlib>
#include <cctype>
#include <cstring>
using namespace std;
#define fo(i, x, y) for (int i = (x); i <= (y); ++i)
#define fd(i, x, y) for (int i = (x); i >= (y); --i)
const int maxl = 500 + 5;
int tot;
int cntin[30];
char ans[30];
char s1[maxl], s2[maxl];
bool v[30][30];
bool used[30];
int getint()
{
char ch;
int res = 0, p;
while (!isdigit(ch = getchar()) && (ch ^ '-'));
p = ch == '-'? ch = getchar(), -1 : 1;
while (isdigit(ch))
res = (res << 3) + (res << 1) + (ch ^ 48), ch = getchar();
return res * p;
}
void dfs(int k)
{
if (k > tot)
{
fo(i, 1, tot) printf("%c", ans[i]);
printf("\n");
return;
}
fo(i, 0, 25)
if (used[i] && !cntin[i])
{
ans[k] = i + 'a';
used[i] = 0;
fo(j, 0, 25)
if (v[i][j]) cntin[j]--;
dfs(k + 1);
used[i] = 1;
fo(j, 0, 25)
if (v[i][j]) cntin[j]++;
}
}
int main()
{
while (fgets(s1 + 1, sizeof(s1), stdin))
{
fgets(s2 + 1, sizeof(s2), stdin);
int len1 = strlen(s1 + 1), len2 = strlen(s2 + 1);
if (s1[len1] == '\n') len1--;
if (s2[len2] == '\n') len2--;
memset(v, 0, sizeof(v));
memset(used, 0, sizeof(used));
memset(cntin, 0, sizeof(cntin));
tot = len1 + 1 >> 1;
for (int i = 1; i <= len1; i += 2)
used[s1[i] - 'a'] = true;
for (int i = 1; i <= len2; i += 4)
{
int c1 = s2[i] - 'a', c2 = s2[i + 2] - 'a';
v[c1][c2] = true;
cntin[c2]++;
}
dfs(1);
printf("\n");
}
return 0;
}
知识总结
这个专题训练涉及到的知识有dfs/bfs,二分。
dfs/bfs主要用于图论搜索或搜索解:最优解/可行解。其难点在于剪枝,剪枝的依据是从题目得出的性质,一般剪枝能分为以下几类:
- 优化搜索顺序
- 排除等效冗余,即不同的搜索分枝是等效的话可以只搜索一条分支。
- 可行性剪枝
- 最优性剪枝
- 记忆化
二分则要求对象具有单调性,二分的对象可以是一个数组也可以是一段数值,二分数值即是二分答案,我比较习惯用的二分结构是
while(l + 1 < r)
{
int mid = l + r >> 1;
if (F(mid)) l = mid;
else r = mid;
}
二分有很多种写法,只要用熟其中一种就行。