[dfs] aw1118. 分成互质组(dfs搜索顺序+dfs状态定义+最大团+好题)

1. 题目来源

链接:1118. 分成互质组

相似的搜索顺序:

2. 题目解析

当这个数据比较大的时候,要使用最大团算法。 其中的优化比较难,这个数据比较小,所以可以直接暴搜。


最大团思路:将 n 个数看成 n 个点,若两数不互质则将两点之间建立一条边,则问题将转化为如何将这些点尽可能分成少的组,使得组中各点没有边相连。


暴搜思路: 两种暴搜顺序,其实和 [dfs] aw843. n-皇后问题(模板题+经典) 的两种搜索顺序差不多:

  • 枚举每个数,考虑每个数放到哪个组里不错的题解,提供了两种方式。
  • 枚举每个组,考虑组内可以放那些元素。 本题采用的是这种方式枚举。
  • 优化1:
  • 针对当前数,有两种放入方式:
    • 能放到最后一组中。
    • 自己新开一组。
  • 其中第二种操作:自己新开一组一定可以被执行。第一种操作需要保证新加入数是和组内已有数是互质的。
  • 在此,为了优化效率,减少组的数量,可以证明,当第一种操作和第二种操作都能够被执行时,可以只执行第一种操作,而不执行第二种操作。
    • 如果能执行第一种操作,那么当前数一定和组内数是互质的。
    • 如果新开一组,该数也将和新开组的组内数字是互质的。
    • 所以,可以将当前数执行第一种操作,不会使答案变差。那么一定存在一个最优解,在它能同时做第一个操作、第二个操作时,尽量执行第一个操作,减少 dfs 层数。
  • 优化2:
  • 组内的元素是排列关系,并非是组合关系。即组 1 中元素为 {1, 2, 3}{3, 2, 1} 是一致的,与顺序无关。
  • 所以在针对每个组放置元素时,可以按照组合顺序枚举。 而不按照排列顺序枚举。
  • 在组合顺序枚举时,为了保证组内元素不重复,需要 dfs 时传入枚举的起始下标,让组内元素以下标递增。
  • 例如,当下标在 6 号点能够加入 a 组中,下一次搜索时,不用从头开始再次进行判断,因为前面 1~6 号点已经被判断过了。可以直接从 6+1 = 7 号点开始枚举,加快搜索速度。如,若 a 组有 {3 5 7} 那么排列型枚举需要枚举 3! = 6 次,而组合类型枚举按照下标递增顺序,只会枚举一次, 优化了 6 倍的时间。

具体逻辑看代码和注释就行了,本题确实是到好题。

每个组在考虑完之后,它组内的元素就已经固定下来了。

即当新开一个组时,一定是当前数无法放到前面的组中了,所以才会新开一个组


时间复杂度: O ( 指 数 级 ) O(指数级) O()

空间复杂度: O ( n 2 ) O(n^2) O(n2)


枚举每个组能放那些元素,很慢,700 ms 左右。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 15;

int n;
int p[N];
int group[N][N];
bool st[N];
int res = 1e9;      // 全局记录最小值,初始化为最大值

int gcd(int a, int b) {
    return b ? gcd(b, a % b) : a;
}

// 检查以 i 为下标的数能够添加进 group 组中
bool check(int group[], int gc, int i) {
    for (int j = 0; j < gc; j ++ ) 
        if (gcd(p[group[j]], p[i]) > 1)
            return false;
    
    return true;
}

// g:已经分的组的数量,gc:当前组存了多少数字,tc:已经有多少数字被分组了,
// start:当前组分到数的下标,组合形式,防止重复情况
void dfs(int g, int gc, int tc, int start) {
    if (g >= res) return ;
    if (tc == n) {
        res = min(res, g);
        return ;
    }

    bool flag = true;
    for (int i = start; i < n; i ++ ) {
        if (!st[i] && check(group[g], gc, i)) {
            st[i] = true;
            group[g][gc] = i;
            dfs(g, gc + 1, tc + 1, i + 1);
            st[i] = false;

            flag = false;
        }
    }

    // 当前组不能插入任何数,需要新开一个组
    if (flag) dfs(g + 1, 0, tc, 0);
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", p + i);

    // 当前是 1 个组,当前组内元素是 0 个,总共考虑了 0 个数,从下标 0 开始考虑选取情况
    // 最终会搜完所有情况,ans 会在全局记录最小组数,直接返回答案。
    dfs(1, 0, 0, 0);

    printf("%d\n", res);

    return 0;
}

枚举每个元素能够放到那个组中,快,40 ms

// 枚举每个数能够放到那个组里面
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 15;

int n;
int p[N];
vector<int> g[N];
bool st[N];
int res = 1e9, len;     // res 全局记录最大值,len 表示分了多少组

int gcd(int a, int b) {
    return b ? gcd(b, a % b) : a;
}

bool check(int x, int u) {
    for (int i = 0; i < g[u].size(); i ++ ) 
        if (gcd(g[u][i], x) > 1) 
            return false;
    
    return true;
}

// 枚举每个数放到哪个组中
void dfs(int u) {
    if (u == n) {
        res = min(res, len);
        return ;
    }
        
    // 枚举所有组,查看 a[u] 能否放到组中
    for (int i = 0; i < len; i ++ ) {
        if (check(p[u], i)) {
            g[i].push_back(p[u]);
            dfs(u + 1);
            g[i].pop_back();
        }
    }
    
    // 自己新开一组
    g[len ++ ].push_back(p[u]);     // len 表示下一个待加入的组的编号,所以是后置 ++ 
    dfs(u + 1);
    g[ -- len].pop_back();          // 恢复现场时,需要前置 -- ,直接将其恢复到上一个状态
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", p + i);
    
    dfs(0);
    
    printf("%d\n", res);
    
    return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值