2019牛客暑期多校训练营(第二场)F-Partition problem(搜索)

给出2*n个人,以及给出他们之间的关系数值,现在要求将这些人分成两拨,一拨有n个人,然后求

\sum_{i=1}^{2n}\sum_{j=i+1}^{2n}Wij (i And j Not Belong To One Team)这个最大

爆搜加剪枝。

比赛的时候演了自己一个多小时。。。

我以为那种直接在包含k个1的二进制间进行转移,上一个状态和这个状态只有两个位置的二进制不同,但是这种转移只是按照二进制的字典序从小到大排列的:(转移方法:)

//从n个数字中选择k个数字(k<=n);
int k;
int comb=(1<<k)-1;

while(comb<(1<<n)){
    int x=comb&-comb,y=comb+x;
    comb=((comb&~y)/x>>1)|y;
}

 

好的来说正解。

第一种、

我们要将这些人分为两拨,使得这两拨之间的连边的值加起来最大,那么可以转化为求两拨各自的集合内连边最小,于是我们依次考虑将编号为1~2*n的人加入两队列中,求这个最小值即可。

剪枝:最优性剪枝,如果搜索过程中出现大于等于答案的情况,剪掉。

           搜索顺序优化,我们将该点对其他点的影响按照从小到大的顺序排列。

复杂度为2^{2n}*n,对你没有看错就是这么高,可能是剪枝剪的好,也可能是数据弱,应该是数据弱了。

#include<bits/stdc++.h>
using namespace std;
int w[39][39];
typedef long long ll;
int n;

struct XXX{
    int i;
    ll x;
    bool operator <(const XXX& y)const{
        return x<y.x;
    }
}a[39],b[39];

struct Node{
    int a[59];
    int size;

    void init(){
        size=0;
    }

    void pop(){
        --size;
    }

    void Insert(int x){
        a[++size]=x;
    }

    ll jisuan(int id){
        ll res=0;
        for(int i=1;i<size;++i) res+=w[id][a[i]];
        return res;
    }
}v1,v2;

ll minn=1e18;

void dfs(int x,ll res){
    if(res>=minn) return ;
    if(x==n*2+1){
        minn=res;
        return ;
    }


    if(v1.size<n){
        v1.Insert(a[x].i);
        ll h=v1.jisuan(a[x].i);
        dfs(x+1,res+h);

        v1.pop();
    }
    if(v2.size<n){
        v2.Insert(a[x].i);
        ll h=v2.jisuan(a[x].i);
        dfs(x+1,res+h);
        v2.pop();
    }
}

int main(){
    //freopen("in.txt","r",stdin);
    scanf("%d",&n);
    for(int i=1;i<=n*2;++i)
        for(int j=1;j<=n*2;++j)
            scanf("%d",&w[i][j]);
    ll res=0;
    for(int i=1;i<=n*2;++i)
        for(int j=i+1;j<=n*2;++j)
            res+=w[i][j];
    for(int i=1;i<=2*n;++i)
        for(int j=1;j<=2*n;++j)
            a[i].i=i,a[i].x+=w[i][j];
    sort(a+1,a+1+n*2);
    /*
    int j=0;
    for(int i=1;i<=n;++i){
        b[++j]=a[i];
        b[++j]=a[n*2-i+1];
    }
    for(int i=1;i<=n*2;++i) a[i]=b[i];
    */
    dfs(1,0);
    printf("%lld\n",res-minn);

    return 0;
}

 

第二种、

我们考虑对于集合A,B是不是等价于集合B,A,所以我们一开始将1放入A中,其余都放入B中,然后按照顺序从前向后枚举编号,将之从B中弹出,加入到A中,一个元素从B中弹出就需要减去与原A中元素的连边,加上与B中其他元素的连边即可。

剪枝:可行性剪枝,如果将剩下的所有数字都加入到A中都无法凑够n个人说明不可行,剪掉。

时间复杂度为C\binom{N}{2*N}*2*N,方案数为组合数,算答案为2*n。

这里贴上出题人的标程:

#include <bits/stdc++.h>
using namespace std;
const int N = 18;
typedef long long LL;
int n, all;
LL v[N + N][N + N], ans;
//参数依次为:msk二进制下为1的位置为A中的元素,got为A中的元素个数,
//pre表示区间[0,pre]已被使用,cst为答案
void go(int msk, int got, int pre, LL cst) {
  if (got + got == n) {
    ans = max(ans, cst);
    return;
  }
  if (n - pre - 1 + got < n / 2) {
    return;
  }
  for (int nxt = pre + 1; nxt < n; ++ nxt) {
    LL nxt_cst = cst;
    for (int i = 0; i < n; ++i) {
      if ((msk >> i) & 1)
        nxt_cst -= v[nxt][i];//减掉与A中的连边;
      else
        nxt_cst += v[nxt][i];//加上与B中的连边;
    }
    go(msk | (1 << nxt), got + 1, nxt, nxt_cst);
  }
}
int main() {
  cin >> n;
  n <<= 1;
  for (int i = 0; i < n; ++i) {
    for (int j = 0; j < n; ++j) {
      cin >> v[i][j];
    }
  }
  all = (1 << n) - 1;
  LL cst = 0;
  for (int i = 0; i < n; ++i) {
    cst += v[0][i];
  }
  go((1 << 0), 1, 0, cst);
  printf("%lld\n", ans);
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值