垃圾ACMer的暑假训练220709
12. 贪心
12.8 Calling
题意
有 T ( 1 ≤ T ≤ 1 e 5 ) T\ \ (1\leq T\leq 1\mathrm{e}5) T (1≤T≤1e5)组测试数据.有六种正方形纸片,其中边长为 i ( 1 ≤ i ≤ 6 ) i\ \ (1\leq i\leq 6) i (1≤i≤6)的正方形有 k i ( 0 ≤ k i ≤ 1 e 4 ) k_i\ \ (0\leq k_i\leq 1\mathrm{e}4) ki (0≤ki≤1e4)个.现需将它们放在至多 s ( 0 ≤ s ≤ 1 e 9 ) s\ \ (0\leq s\leq 1\mathrm{e}9) s (0≤s≤1e9)个边长为 6 6 6的正方形框内,每个框未必放满,每个框内的正方形边长未必相同.若能放下,输出"Yes",否则输出"No".
思路
用变量 n e e d need need表示放下所有纸片需要的框数的最小值,将其与 s s s比较,判断是否有解.
显然 6 × 6 , 5 × 5 , 4 × 4 6\times 6,5\times 5,4\times 4 6×6,5×5,4×4的纸片一个纸片需要一个框, 3 × 3 3\times 3 3×3的纸片一个框可放 4 4 4个, 2 × 2 2\times 2 2×2和 1 × 1 1\times 1 1×1的纸片插空放.
贪心,先放 6 × 6 , 5 × 5 , 4 × 4 , 3 × 3 6\times 6,5\times 5,4\times 4,3\times 3 6×6,5×5,4×4,3×3的纸片,若还剩下 d ≠ 0 d\neq 0 d=0个 3 × 3 3\times 3 3×3纸片,分三种情况:①若 d = 1 d=1 d=1,则还能放下 5 5 5个 2 × 2 2\times 2 2×2的纸片;②若 d = 2 d=2 d=2,则还能放下 3 3 3个 2 × 2 2\times 2 2×2的纸片;③若 d = 3 d=3 d=3,则还能放下 1 1 1个 2 × 2 2\times 2 2×2的纸片.用变量 p u t 2 put2 put2记录还能放的 2 × 2 2\times 2 2×2纸片数.
若还剩下 2 × 2 2\times 2 2×2的纸片,则开新的框放.边长为 2 × 6 2\times 6 2×6的纸片放完后用到的框的剩余面积即还能放的 1 × 1 1\times 1 1×1纸片数,将其与给定的纸片数比较,判断是否有解.
代码
const int MAXN = 7;
int s; // 6x6框数
int cnt[MAXN]; // cnt[i]表示边长为i的正方形的个数
int main() {
CaseT{
cin >> s;
for (int i = 1; i <= 6; i++) cin >> cnt[i];
int need = cnt[6] + cnt[5] + cnt[4] + ceil(cnt[3] / 4.0);
int put2 = cnt[4] * 5; // 还能放的2x2的个数
if (cnt[3] % 4 != 0) { // 3x3还有剩
if (cnt[3] % 4 == 1) put2 += 5;
else if (cnt[3] % 4 == 2) put2 += 3;
else if (cnt[3] % 4 == 3) put2 += 1;
}
if (cnt[2] > put2) need += ceil((cnt[2] - put2) / 9.0); // 还有2x2没放,开新的框
int put1 = 6 * 6 * need; // 剩余面积
for (int i = 2; i <= 6; i++) put1 -= i * i * cnt[i];
if (cnt[1] > put1) need += ceil((cnt[1] - put1) / 36.0); // 还有1x1没放,开新的框
cout << (need <= s ? "Yes" : "No") << endl;
}
}
1. 递推与递归
1.4 简单斐波那契
题意
输入整数 n ( 0 < n < 46 ) n\ \ (0<n<46) n (0<n<46),输出斐波那契数列的前 n n n项.如 n = 5 n=5 n=5时,前 5 5 5项为: 0 1 1 2 3 0\ 1\ 1\ 2\ 3 0 1 1 2 3.
代码I
const int MAXN = 50;
int n;
int fib[MAXN];
int main() {
cin >> n;
fib[1] = 0, fib[2] = 1;
for (int i = 3; i <= n; i++) fib[i] = fib[i - 1] + fib[i - 2];
for (int i = 1; i <= n; i++) cout << fib[i] << ' ';
}
思路
注意到 f i b [ n ] ( n ≥ 3 ) fib[n]\ \ (n\geq 3) fib[n] (n≥3)只与 f i b [ n − 1 ] fib[n-1] fib[n−1]和 f i b [ n − 2 ] fib[n-2] fib[n−2]有关,则可用变量 a = f i b [ n − 2 ] , b = f i b [ n − 1 ] a=fib[n-2],b=fib[n-1] a=fib[n−2],b=fib[n−1],下一步令 a = f i b [ n − 1 ] , b = f i b [ n ] a=fib[n-1],b=fib[n] a=fib[n−1],b=fib[n],滚动.
代码II:滚动优化空间
int main() {
int n; cin >> n;
int a = 0, b = 1;
for (int i = 0; i < n; i++) {
cout << a << ' ';
int c = a + b;
a = b, b = c;
}
}
1.5 费解的开关
题意
25 25 25盏灯排成 5 × 5 5\times 5 5×5的正方形,每盏灯有一开关,玩家可改变其状态.每一步,玩家可选择改变某一灯的状态,同时该灯上下左右的灯的状态也会改变.用 1 1 1表示灯亮, 0 0 0表示灯灭.给定游戏初始状态,判断 6 6 6步内是否可使所有灯亮.
有 t ( 1 ≤ t ≤ 500 ) t\ \ (1\leq t\leq 500) t (1≤t≤500)组测试数据,每组测试数据输入 5 × 5 5\times 5 5×5的矩阵表示初始状态,各组测试数据间用空行分隔.
若能在 6 6 6步内使所有灯亮,输出最小步数,否则输出 − 1 -1 −1.
思路
注意到最终每个开关的状态只与其及其上下左右的切换次数的奇偶有关,则最终结果与操作顺序无关.注意到每个灯至多切换一次状态.
枚举第一行的 2 5 = 32 2^{5}=32 25=32种操作,考虑第二行的操作.注意到此时能影响第一行的灯的状态的只有其正下方的第二行的灯,为使所有灯亮,则第一行灭的灯的下方的灯必须切换,第一行亮的灯的下方的灯必须不切换,即第二行的操作被第一行灯的状态唯一确定.同理下一行的状态被当前行的状态唯一确定.操作完第 5 5 5行后,第 1 ∼ 5 1\sim 5 1∼5行灯全亮,若此时第 6 6 6行灯全亮,则有解,更新最小步数;若此时第 6 6 6行灯不全亮,则无解.
代码
const int MAXN = 6;
char graph[MAXN][MAXN], backup[MAXN];
int dx[5] = { -1,0,1,0,0 }, dy[5] = { 0,1,0,-1,0 };
void turn(int x, int y) {
for (int i = 0; i < 5; i++) {
int curx = x + dx[i], cury = y + dy[i];
if (curx < 0 || curx >= 5 && cury < 0 || cury >= 5) continue;
graph[curx][cury] ^= 1;
}
}
int main() {
CaseT{
for (int i = 0; i < 5; i++) cin >> graph[i];
int ans = INF; // >6的数
for (int op = 0; op < 32; op++) { // 枚举第一行的状态
memcpy(backup, graph, so(graph));
int step = 0;
for (int i = 0; i < 5; i++) {
if (op >> i & 1) {
step++;
turn(0, i);
}
}
for (int i = 0; i < 4; i++) { // 下一行的操作由当前行完全确定
for (int j = 0; j < 5; j++) {
if (graph[i][j] == '0') {
step++;
turn(i + 1, j);
}
}
}
bool ok = true;
for (int i = 0; i < 5; i++) {
if (graph[4][i] == '0') {
ok = false;
break;
}
}
if (ok) ans = min(ans, step);
memcpy(graph, backup, so(graph));
}
if (ans > 6) ans = -1;
cout << ans << endl;
}
}
1.6 带分数
题意
100 100 100可表示为带分数的形式 100 = 3 + 69258 714 = 82 + 3546 197 100=3+\dfrac{69258}{714}=82+\dfrac{3546}{197} 100=3+71469258=82+1973546,其中带分数中 1 ∼ 9 1\sim 9 1∼9每个数字恰出现一次.类似这样的带分数, 100 100 100有 11 11 11种表示法.输入正整数 n ( 1 ≤ n < 1 e 6 ) n\ \ (1\leq n< 1\mathrm{e}6) n (1≤n<1e6),输出方案数.
思路
n = a + b c n=a+\dfrac{b}{c} n=a+cb,其中 a , b , c a,b,c a,b,c的数码中 1 ∼ 9 1\sim 9 1∼9恰出现一次.
暴力做法:枚举 1 ∼ 9 1\sim 9 1∼9的全排列,时间复杂度 O ( n ⋅ n ! ) O(n\cdot n!) O(n⋅n!),再枚举将哪段区间作为 a , b , c a,b,c a,b,c,即枚举隔板的位置,时间复杂度 C 8 2 C_8^2 C82,最后判断等式是否成立.总时间复杂度约 9 e 7 9\mathrm{e}7 9e7,但常数较小,可过.
考虑优化.注意到 1 ≤ n ≤ 1 e 6 1\leq n\leq 1\mathrm{e}6 1≤n≤1e6,则 1 ≤ a ≤ 1 e 6 1\leq a\leq 1\mathrm{e}6 1≤a≤1e6.又 c n = a c + b cn=ac+b cn=ac+b,则可对每个枚举的 a a a,再枚举 c c c,最后计算出 b b b.实现时,将 a a a的搜索树的叶子节点扩展成一棵 c c c的搜索树.
代码
const int MAXN = 10;
int n;
bool vis[MAXN]; // 记录每个数字是否被用过
int ans;
bool check(int a, int c) {
ll b = (ll)n * c - (ll)a * c;
if (!a || !b || !c) return false;
static bool backup[MAXN];
memcpy(backup, vis, so(vis));
while (b) {
int tmp = b % 10; b /= 10;
if (!tmp || backup[tmp]) return false;
backup[tmp] = true;
}
for (int i = 1; i <= 9; i++)
if (!backup[i]) return false;
return true;
}
void dfs_c(int used, int a, int c) { // 当前用了几个数字、a的值、当前c的值
if (used > 9) return;
if (check(a, c)) ans++;
for (int i = 1; i <= 9; i++) { // 枚举c的下一位
if (!vis[i]) {
vis[i] = true;
dfs_c(used + 1, a, c * 10 + i);
vis[i] = false;
}
}
}
void dfs_a(int used, int a) { // 当前用了几个数字、当前a的值
if (a >= n) return; // a=n时b=0
if (a) dfs_c(used, a, 0); // a非零时,对每个a枚举c
for (int i = 1; i <= 9; i++) { // 枚举a的下一位
if (!vis[i]) {
vis[i] = true;
dfs_a(used + 1, a * 10 + i);
vis[i] = false;
}
}
}
int main() {
cin >> n;
dfs_a(0, 0); // 当前用了0个数,当前a的值为0
cout << ans;
}
1.7 飞行员兄弟
题意
16 16 16个开关排成 4 × 4 4\times 4 4×4的矩阵,每个开关有闭合和断开两种状态,分别用 + + +和 − - −表示.每一步可切换开关 ( i , j ) ( 1 ≤ i , j ≤ 4 ) (i,j)\ \ (1\leq i,j\leq 4) (i,j) (1≤i,j≤4)的状态,同时第 i i i行和第 j j j列的开关状态都切换.求将所有开关断开的最小切换步数.
输入 4 × 4 4\times 4 4×4的矩阵表示开关初始状态,数据保证至少有一开关为闭合状态.
第一行输出最小切换步数 n n n,接下来 n n n行输出方案,每行输出两个数 i , j ( 1 ≤ i , j ≤ 4 ) i,j\ \ (1\leq i,j\leq 4) i,j (1≤i,j≤4)表示切换开关 ( i , j ) (i,j) (i,j)的状态.若存在多种开冰箱的方式,则按照优先级整体从上到下、同行从左到右打开.
思路
暴力枚举 2 16 = 65536 2^{16}=65536 216=65536种方案,有 16 16 16个开关,每个开关会改变 7 7 7个开关的状态,检查 16 16 16个开关是否都断开,若是,扫一遍记录方案,总时间复杂度 2 16 ( 16 × 7 + 16 + 16 ) ≈ 1 e 7 2^{16}(16\times 7+16+16)\approx 1\mathrm{e}7 216(16×7+16+16)≈1e7.
因要输出字典序最小的方案,当两方案的最小步数相等时,应先枚举二进制数中 1 1 1更靠前(低位)的方案.
代码I
const int MAXN = 5;
char graph[MAXN][MAXN], backup[MAXN][MAXN];
int get(int x, int y) { return x * 4 + y; }
void turn_one(int x, int y) {
graph[x][y] = graph[x][y] == '+' ? '-' : '+';
}
void turn_all(int x, int y) {
for (int i = 0; i < 4; i++) turn_one(x, i), turn_one(i, y);
turn_one(x, y); // (x,y)被改变两次
}
int main() {
for (int i = 0; i < 4; i++) cin >> graph[i];
vii ans;
for (int op = 0; op < 1 << 16; op++) { // 枚举每个开关的操作
vii tmp;
memcpy(backup, graph, so(backup));
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (op >> get(i, j) & 1) {
tmp.push_back({ i,j });
turn_all(i, j);
}
}
}
bool ok = true;
for (int i = 0; ok && i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (graph[i][j] == '+') {
ok = false;
break;
}
}
}
if (ok)
if (ans.empty() || ans.size() > tmp.size()) ans = tmp;
memcpy(graph, backup, so(graph));
}
cout << ans.size() << endl;
for (auto item : ans) cout << item.first + 1 << ' ' << item.second + 1 << endl;
}
代码II:位运算优化
const int MAXN = 5;
int graph[MAXN][MAXN];
int get(int x, int y) { return x * MAXN + y; }
int main() {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 4; k++)
graph[i][j] += (1 << get(i, k)) + (1 << get(k, j));
graph[i][j] -= 1 << get(i, j);
}
}
int state = 0;
for (int i = 0; i < 4; i++) {
string line; cin >> line;
for (int j = 0; j < 4; j++)
if (line[j] == '+') state += 1 << get(i, j);
}
vii ans;
for (int op = 0; op < 1 << 16; op++) { // 枚举每个开关的操作
int cur = state;
vii tmp;
for (int j = 0; j < 16; j++) {
if (op >> j & 1) {
int x = j / 4, y = j % 4;
cur ^= graph[x][y];
tmp.push_back({ x,y });
}
}
if (!cur && (ans.empty() || ans.size() > tmp.size())) ans = tmp;
}
cout << ans.size() << endl;
for (auto item : ans) cout << item.first + 1 << ' ' << item.second + 1 << endl;
}
1.8 翻硬币
题意
给定硬币的初始状态和目标状态,用 ∗ * ∗表示正面, o o o表示反面.每一步能翻转相邻两硬币,求最小翻转步数.
表示状态的字符串长度不超过 100 100 100,数据保证一定有解.
思路
显然至多需 50 50 50步.若用BFS,时间复杂度 5 0 99 50^{99} 5099,会TLE.
将硬币的正反看作灯泡的亮灭,在相邻两灯泡间放一个可反转它们的开关.从左往右考察初始状态与目标状态的当前位置是否相同,若不同,则必须按开关,否则必须不按开关.显然解是被唯一确定的.总时间复杂度 O ( n ) O(n) O(n).
代码
const int MAXN = 105;
string start, target;
void turn(int pos) {
start[pos] = start[pos] == '*' ? 'o' : '*';
}
int main() {
cin >> start >> target;
int ans = 0;
for (int i = 0; i < start.length() - 1; i++) {
if (start[i] != target[i]) {
turn(i), turn(i + 1);
ans++;
}
}
cout << ans;
}