暑训日记Day4 T2《立体几何》题解 (容斥、匈牙利算法二分图匹配)

T2 立体几何

第一次遇到这么可爱的出题人,其实这道题和二分图匹配没有半毛钱关系。
一定要看到最后,看蒟蒻第一次hack标程。

Background

小 P 是一个热爱几何的男孩子。

Description

OI 退役滚去高考的小 P 最近复习到了数学必修二第一章。他开始研究空间几何体 的三视图。他拿出了若干个大小相同的正方体小方块,将它们整齐地堆在一个 n × m 的 网格中,并且记录下了所形成立体图形的三视图。现在小 P 想要考考你,最多能取走 多少个小方块,使得存在一种重新安排小方块的方案,让三视图均不发生改变呢?

Format

Input

第一行一个正整数 T,表示数据组数。

接下来 T 行,每行两个正整数 n 和 m,分别表示网格的行数和列数。接下来 n 行, 每行 m 个整数 {ai, j},表示对应位置上小方块的数目。

Output

对于每组数据,输出一行一个整数,表示最多能取走的小方块数量。

Samples

输入数据1

2
5 5
1 4 0 5 2
2 1 2 0 1
0 2 3 4 4
0 3 0 3 1
1 2 2 1 1
2 3
50 20 3
20 10 3

输出数据1

9
30

输入数据2

见附件view2.in

输入数据2

见附件view2.ans

在这里插入图片描述
在这里插入图片描述

题解

原题解是这么说的:

• 好像数据造水了导致一堆不正确的做法获得了高分
• 锅++
• 子任务全是瞎放的
• 首先要满足俯视图,我们可以把所有有小方块的位置减到只剩一个
• 不难发现侧视与正视图上看到的是对应最高的小方块高度
• 若某行和某列最大值一样,我们可以只在他们相交的位置放置对应数量的小
方块
• 匈牙利算法跑二分图匹配即可

一开始看不懂没事,可以先看下面蒟蒻自己的做法,没有用二分图匹配,更简单一点。
看完下面的代码先别走,一定要看到最后,看蒟蒻第一次hack标程。

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int maxn = 102;
int a[maxn][maxn],d[maxn],n,m,hm[maxn],sm[maxn];
bool vis[maxn];
LL ans = 0;
bool dfs(int s){
	if(vis[s]) return 0;
	vis[s] = 1;
	for(int v = 1; v <= m; v++)
		if(a[s][v] && hm[s] == sm[v])
			if(!d[v] || dfs(d[v])){
        d[v] = s; return 1;								
			} 
	return 0;
}
int main(){
	int T;
	scanf("%d",&T);
	while(T--){
  	ans = 0;
	  scanf("%d%d",&n,&m);
	  memset(d,0,sizeof(d));
	  memset(hm,0,sizeof(hm));
	  memset(sm,0,sizeof(sm));
	  for(int i = 1;i <= n;i++)
		  for(int j = 1;j <= m;j++){
			  scanf("%d", &a[i][j]);
			  hm[i] = max(hm[i],a[i][j]);
			  sm[j] = max(sm[j],a[i][j]);
			  if(a[i][j] > 1) ans += a[i][j] - 1;
		  }
	  for(int i = 1;i <= n;i++)
	    if(hm[i] > 1)
		    ans -= hm[i] - 1;
	  for(int i = 1;i <= m;i++)
      if(sm[i] > 1)
		    ans -= sm[i] - 1;
	  for(int i = 1; i <= n; i++){
		  memset(vis,0,sizeof(vis));
		  if(dfs(i))	    
			  if(hm[i] > 1)
		      ans += hm[i] - 1;
	  }
	  printf("%lld\n", ans);		
	}
	return 0;
}

先解释一下原题解的意思。

我们先把所有方块取走,直至原来有方块的格子只剩下一个(保证俯视图)。
然而这时,我们的正视图以及侧视图与原来都不同了,于是我们需要补回去一些方块。
因为正视图侧视图看到的是纵向和横向的方块数最大值,我们只需要在读入时处理一下:“每个点横向与纵向方向上的方块数最大值”,分别用 h m hm hm s m sm sm数组记录。 ( h m − > 横 m a x , s m − > 竖 m a x ) (hm->横max,sm->竖max) (hm>maxsm>max)
在补方块时,我们只需要把每一行每一列的最大值补一次就行了。

然而,这不是最优的方案。因为在最开始,有可能一个点同时管辖了“横行与纵列上的方块数最大值”。即假设一个点的坐标是 ( i , j ) (i,j) (i,j) h m [ i ] = s m [ j ] hm[i] = sm[j] hm[i]=sm[j]
这时候我们就需要根据容斥原理,减去一次多补的方块。至于怎么减,原题目给了匈牙利算法做法。
之所以能把这个看做二分图匹配,原因在于,横行与纵列的方块数最大值聚集在一点的点就那么多个,在最优方案中,每个点就只有一个管辖横行或纵列的可能,我们只需要用横行最大或者纵列最大去匹配这些特别的点。

但 是!真的需要那么做吗???
一、怎么就需要二分图匹配了???绕那么大弯子干嘛。
二、最后的匈牙利跑二分图并不需要,这道题就是个简单的容斥。在最后计算行列最大值聚集在一个点上的情况时,只需要 O ( n 2 ) O(n^2) O(n2)地扫一遍就好了,扫到这种点就清空存储最大值的数组,在有些数据上比原题解程序还快(亲测)。
三、第一次见到这么可爱的出题人。

下面是我自己的最终程序:

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int maxn = 102;
int a[maxn][maxn],d[maxn],n,m,hm[maxn],sm[maxn];
bool vis[maxn];
LL ans = 0;
int main(){
	int T;
	scanf("%d",&T);
	while(T--){
  	ans = 0;
	  scanf("%d%d",&n,&m);
	  memset(d,0,sizeof(d));
	  memset(hm,0,sizeof(hm));
	  memset(sm,0,sizeof(sm));
	  for(int i = 1;i <= n;i++)
		  for(int j = 1;j <= m;j++){
			  scanf("%d", &a[i][j]);
			  hm[i] = max(hm[i],a[i][j]);
			  sm[j] = max(sm[j],a[i][j]);
			  if(a[i][j] > 1) ans += a[i][j] - 1;
		  }	  
	  for(int i = 1;i <= n;i++)
	    if(hm[i] > 1)
		  ans -= hm[i] - 1;
	  for(int i = 1;i <= m;i++)
	    if(sm[i] > 1)
		  ans -= sm[i] - 1;
	  for(int i = 1;i <= n;++i){
	  	for(int j = 1;j <= m;++j){
	  	  if(a[i][j] == 0) continue;
	  	  if(hm[i] == sm[j] && hm[i] > 0){
	  	  	ans += hm[i] - 1;
	  	  	hm[i] = 0;
	  	  	sm[j] = 0;
		  }
		}
	  }	
	  printf("%lld\n", ans);		
	}
	return 0;
}

在附一张hack前后对比图:
标程测评记录:请添加图片描述

hack后,蒟蒻的测评记录:
请添加图片描述
也不能说快得多了,反正是快了点,至少思路简单的多了。

看蒟蒻hack地那么辛苦。点个赞再走呗 Q A Q QAQ QAQ

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值