2024.3.30 笔记

AcWing 372. 棋盘覆盖

设每个格子为 \((i,j)\)

\(i+j\) 为偶数和 \(i+j\) 为奇数的点的两个集合构成二分图的两个点集,和为偶数的边的四周全是和为奇数的点,满足二分图的性质,题目即求以和为偶数和奇数的点构成的二分图的最大匹配

const int dx[] = {0, 0, 1, -1};
const int dy[] = {1, -1, 0, 0};

int n, m;
int ans, match[M];
bool b[N][N], v[M];
vector<int> e[M];

bool dfs(int x) 
{
	for (unsigned int i = 0; i < e[x].size(); i++) 
	{
		int y = e[x][i];
		if (v[y]) continue;
		v[y] = 1;
		if (!match[y] || dfs(match[y])) 
		{
			match[y] = x;
			return 1;
		}
	}
	return 0;
}

signed main() 
{
	cin >> n >> m;
	while (m--) 
	{
		int x, y;
		cin >> x >> y;
		b[x][y] = 1;
	}
	
	for (rint i = 1; i <= n; i++)
	  for (rint j = 1; j <= n; j++)
		if (!b[i][j])
		  for (rint k = 0; k <= 3; k++) 
		  {
			int x = i + dx[k], y = j + dy[k];
			if (x >= 1 && x <= n && y >= 1 && y <= n && !b[x][y]) 
			{
				e[i * n + j].push_back(x * n + y);
				e[x * n + y].push_back(i * n + j);
			}
		  }
			
	memset(match, 0, sizeof match);
	for (rint i = 1; i <= n; i++)
		for (rint j = 1; j <= n; j++) 
		{
			if ((i ^ j) & 1) continue;
			memset(v, 0, sizeof v);
			ans += dfs(i * n + j);
		}
	cout << ans << endl;
	return 0;
}

AcWing 373. 車的放置

本题要求每行、每列只能放 \(1\) 个车,某个格子 \((i, j)\) 放了车,等于是占了第 \(i\) 行与第 \(j\) 行放车的名额。

因此我们可以把所有行、列看作节点,一共 \(n + m\) 个节点,如果格子 \((i, j)\) 没有被禁止,就在第 \(i\)

对应的节点与第 \(j\) 列对应的节点之间连无向边

对于行之间,每个车不可能同时放在两个行上,所以任意两行之间不可能有边,列同理。

要在互不冲突的前提下放置最多的车,就是求上述二分图的最大匹配数。

复杂度 \(O((N+M)*N*M)\)

int n, m, t;
int ans, match[N];
bool a[N][N], v[N];

bool dfs(int x) 
{
	// 不用邻接表, 直接枚举点
	for (rint y = 1; y <= m; y++) 
	{
		if (v[y] || a[x][y]) continue;
		v[y] = 1;
		if (!match[y] || dfs(match[y])) 
		{
			match[y] = x;
			return 1;
		}
	}
	return 0;
}

signed main() 
{
	cin >> n >> m >> t;
	for (rint i = 1; i <= t; i++) 
	{
		int x, y; 
		cin >> x >> y;
		a[x][y] = 1;
	}
	for (rint i = 1; i <= n; i++) 
	{
		memset(v, 0, sizeof v);
		if (dfs(i)) ans++;
	}
	cout << ans << endl;
}

AcWing 374. 导弹防御塔

时间较短时能击退所有入侵者,那么对于更长时间,显然也能击退入侵者,因此答案具有单调性,可以二分。

那么对于当前的二分值 \(mid\),就需要判断能否在 \(mid\) 秒之内击退所有入侵者。

已知发射预热时间 \(t1\)、冷却时间 \(t2\),我们容易计算出每座塔在 \(mid\) 分钟内最多能发射出多少枚导弹,记为 \(p\)

可以发现,本题是一个多重匹配问题,可以把入侵者作为二分图的左部节点,每座防御塔拆成 \(p\) 个导弹,作为二分图的右部节点。

最终会有 \(m\) 个左部节点、\(n * p\) 个右部节点

注意,因为塔和入侵者的坐标各不相同,所以从塔飞到入侵者的时间也可能不同,所以在连边时,还需要检查导弹是否有足够的时间飞到入侵者。因此,一座塔的 \(p\) 个导弹是不等价的,这个多重匹配问题必须用拆点来解决。如果 \(mid\) 时间内,第 \(i\) 个入侵者能被第 \(j\) 座塔的第 \(k\) 个导弹击中,那么就在第 \(i\) 个左部节点和第 \((j - 1) * p + k\) 个右部节点之间连一条无向边。

然后用匈牙利算法求最大匹配数,若左部节点都能找到匹配,说明 mid 时间内能击退入侵者,二分左区间,否则二分右区间。

int n, m;
double t1, t2, V;
pair<int, int> a[N], b[N]; 
//入侵者、塔的坐标
double dist[N][N]; 
//每个入侵者和塔之间的距离
bool v[N * N];
int match[N * N];
vector<int> e[N]; 
//e[i] 存储所有能打到第i个入侵者的导弹

double get_dist(int i, int j) 
//计算第i个入侵者和第j座塔之间的距离
{
    double x = a[i].x - b[j].x;
	double y = a[i].y - b[j].y;
    return sqrt(x * x + y * y);
}

bool dfs(int x) //匹配
{
    for (rint i = 0; i < e[x].size(); i++)
    {
        int y = e[x][i];
        if(v[y]) continue;
        v[y] = 1;
        if(!match[y] || dfs(match[y]))
        {
            match[y] = x;
            return 1;
        }
    }
    return false;
}

bool check()
//判断能否击退所有入侵者
{
    memset(match, 0, sizeof match); 
    for (rint i = 1; i <= m; i++) 
	//枚举所有入侵者
    {
        memset(v, 0, sizeof v); 
        if(!dfs(i)) return 0; 
		//只要有一个入侵者无法击退,则返回 0
    }
    return 1; 
	//到这说明所有入侵者都能击退,返回 1
}

signed main()
{
    cin >> n >> m >> t1 >> t2 >> V;
    t1 /= 60; //转化成分钟
    for (rint i = 1; i <= m; i++) cin >> a[i].x >> a[i].y; 
    for (rint i = 1; i <= n; i++) cin >> b[i].x >> b[i].y; 

    //计算所有入侵者和塔之间的距离
    for (rint i = 1; i <= m; i++)
        for (rint j = 1; j <= n; j++)
            dist[i][j] = get_dist(i, j);

    double l = 0, r = 1e9; //二分
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        int p = (mid + t2) / (t1 + t2); 
		//计算在mid时间内每座塔最多能发射多少枚导弹
        p = min(p, m); 
		//导弹数量最多只需要m枚
        for (rint i = 1; i <= m; i++) //枚举每个入侵者
        {
            e[i].clear(); 
            for (rint j = 1; j <= n; j++) //枚举所有塔
                for (rint k = 1; k <= p; k++) //枚举每一发导弹
                    //如果mid时间内第j座塔的第k发导弹能击退当前入侵者,连一条边
                    if (k * t1 + (k - 1) * t2 + dist[i][j] / V < mid - eps)
                        e[i].push_back((j - 1) * p + k); 
        }
        if (check()) r = mid; //如果mid时间内能击退所有敌人,尝试缩短时间
        else l = mid; //否则需要扩大时间
    }
    printf("%.6lf\n", r);

    return 0;
}

UVA1411 Ants

题目要求最后所有线段都不相交。

那么试想,如果第 \(i_1\) 个白点连第 \(j_1\) 个黑点,第 \(i_2\) 个白点连第 \(j_2\) 个黑点,并且线段 \((i_1, j_1)\) 和线段 \((i_2, j_2)\) 相交,
那么如果交换一下,让 \(i_1\)\(j_2\)\(i_2\)\(j_1\),这样两条线段就不会相交,根据三角形的性质可知,两条交换后的线段的长度
之和一定变小。

所以求所有线段的长度之和最小的方案,所有线段一定互不相交。

如果将所有白点作为左部节点,所有黑点作为右部节点,可以发现这是一个二分图,因此求的就是二分图的带权最小匹配。
由于每个白点都一定能对应一个黑点,因此是一个二分图的完备匹配,所以可以用 \(KM\) 算法。

\(KM\) 算法求的是二分图的带权的最大完备匹配,这里可以将所有边的权值取反,那么就转化为最大匹配了。

double w[N][N]; // 边权
double la[N], lb[N], upd[N]; // 左、右部点的顶标
bool va[N], vb[N]; // 访问标记:是否在交错树中
int match[N]; // 右部点匹配了哪一个左部点
int last[N]; // 右部点在交错树中的上一个右部点,用于倒推得到交错路
int n;
struct 
{
	int x, y;
} a[N], b[N];

bool dfs(int x, int father) 
{
	va[x] = 1;
	for (rint y = 1; y <= n; y++)
	{
		if (vb[y]) continue;
		if (fabs(la[x] + lb[y] - w[x][y]) < eps) 
		{ // 相等子图
			vb[y] = 1;
			last[y] = father;
			if (!match[y] || dfs(match[y], y))
			{
				match[y] = x;
				return 1;
			}
		} 
		else if (upd[y] > la[x] + lb[y] - w[x][y] + eps) 
		{
			upd[y] = la[x] + lb[y] - w[x][y];
			last[y] = father;
		}		
	}
	return 0;
}

void KM() 
{
	for (rint i = 1; i <= n; i++) 
	{
		la[i] = -INT_MAX;
		lb[i] = 0;
		for (rint j = 1; j <= n; j++)
			la[i] = max(la[i], w[i][j]);
	}
	for (rint i = 1; i <= n; i++) 
	{
		memset(va, 0, sizeof va);
		memset(vb, 0, sizeof vb);
		for (rint j = 1; j <= n; j++) 
		    upd[j] = INT_MAX;
		// 从右部点st匹配的左部点match[st]开始dfs,一开始假设有一条0-i的匹配
		int st = 0;
		match[0] = i;
		while (match[st]) 
		{ // 当到达一个非匹配点st时停止
			double delta = INT_MAX;
			if (dfs(match[st], st)) break;
			for (rint j = 1; j <= n; j++)
				if (!vb[j] && delta > upd[j]) 
				{
					delta = upd[j];
					st = j; // 下一次直接从最小边开始DFS
				}
			for (rint j = 1; j <= n; j++)
			{ // 修改顶标
				if (va[j]) la[j] -= delta;
				if (vb[j]) lb[j] += delta;
				else upd[j] -= delta;
			}
			vb[st] = true;
		}
		while (st) 
		{ // 倒推更新增广路
			match[st] = match[last[st]];
			st = last[st];
		}
	}
}

signed main() 
{
	cin >> n;
	for (rint i = 1; i <= n; i++) cin >> b[i].x >> b[i].y;
	for (rint i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
	for (rint i = 1; i <= n; i++)
		for (rint j = 1; j <= n; j++)
			w[i][j] = -sqrt((a[i].x - b[j].x) * (a[i].x - b[j].x) + (a[i].y - b[j].y) * (a[i].y - b[j].y));
	KM();
	for (rint i = 1; i <= n; i++) cout << match[i] << endl;
	return 0;
}

当然此题也存在费用流做法

int n, m, k, S, T;
int e[M], ne[M], f[M], h[N], idx;
double w[M];
int ans[N];
int incf[N], pre[N];
bool v[N];
double d[N];

void add(int a, int b, int c, double d) 
{
	e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx++;
	e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx++;
}
struct node 
{
	int x, y;
} a[N];

double get_dis(node a, node b) 
{
	double dx = a.x - b.x;
	double dy = a.y - b.y;
	return sqrt(dx * dx + dy * dy);
}

bool SPFA() 
{
	queue<int> q;
	fill(d, d + 2 * n + 2, 1e18);
	memset(v, 0, sizeof v);
	memset(incf, 0, sizeof incf);
	q.push(S);
	d[S] = 0;
	incf[S] = inf;
	while (!q.empty()) 
	{
		int x = q.front();
		q.pop();
		v[x] = 0;
		for (rint i = h[x] ; ~i; i = ne[i]) 
		{
			int y = e[i];
			double z = w[i];
			if (f[i] && d[y] > d[x] + z) 
			{
				d[y] = d[x] + z;
				incf[y] = min(incf[x], f[i]);
				pre[y] = i;
				if (!v[y]) 
				{
					q.push(y);
					v[y] = 1;
				}
			}
		}
	}
	return incf[T] > 0;
}

void EK() 
{
	while (SPFA()) 
	{
		int t = incf[T];
		for (rint i = T ; i != S ; i = e[pre[i] ^ 1]) 
		{
			f[pre[i]] -= t;
			f[pre[i] ^ 1] += t;
		}
	}
}

signed main() 
{
	cin >> n;
	S = 0, T = 2 * n + 1;
	memset(h, -1, sizeof h);
	for (rint i = 1; i <= n; i++) 
	{
		cin >> a[i].x >> a[i].y;
		//从源点向所有黑点(二分图左部)连容量为1费用为0的边
		add(S, i, 1, 0);
	}
	for (rint i = 1; i <= n; i++) 
	{
		cin >> a[i + n].x >> a[i + n].y;
		//从所有白点(二分图右部)向汇点连容量为1费用为0的边
		add(i + n, T, 1, 0);
	}
	for (rint i = 1; i <= n; i++) 
	{
		for (rint j = 1; j <= n; j++) 
		{
			//从左部点向右部点连容量为1费用为距离的边
			add(i, j + n, 1, get_dis(a[i], a[j + n]));
		}
	}
	EK();
	for (rint i = 1; i <= n; i++) 
	{
		for (rint j = h[i]; ~j; j = ne[j]) 
		{
			//残留网络中流量为0表示选择了该边
			if (!f[j]) 
			{
				cout << e[j] - n << endl;
				break;
			}
		}
	}
	return 0;
}

UVA1194 Machine Schedule

将每个任务看成一条边,连接的两台机器为两个点

做完所有的任务就是覆盖所有的边

刚开始每台机器模式均为 \(0\)

每转换一次模式相当于选择一个新的点

则问题转化为 求最小的点覆盖所有的边

在二分图中,最小点覆盖 == 最大匹配数

将每个任务在机器 \(A\) 上设置的模式与源点 \(S\) 连一条边,在机器 \(B\) 上设置的模式与汇点 \(T\) 连一条边,在将在机器 \(A\) 上设置的模式的点 与 在机器 \(B\) 上设置的模式的点连一条边,因为每个任务只需要完成一次,所以边的容量均为 \(1\)

void add(int a, int b)
{
    e[++idx] = b, ne[idx] = h[a], h[a] = idx;
}

bool dfs(int x)
{
    for (rint i = h[x]; i; i = ne[i])
    {
        int y = e[i];
        if (!vis[y])
        {
            vis[y] = 1;
            if (!match[y] || dfs(match[y]))
            {
                match[y] = x;
                return true;
            }
        }
    }
    return false;
}

signed main()
{
    int n;
    // while (~scanf("%lld", &n) and n != 0)
    while (114514)
    {
        cin >> n;
        if (n == 0)
        {
            break;
        }
        int m, k;
        cin >> m >> k;

        memset(e, 0, sizeof e);
        memset(ne, 0, sizeof ne);
        memset(h, 0, sizeof h);
        memset(match, 0, sizeof match);
        idx = 0;

        for (rint i = 1; i <= k; i++)
        {
            int a, b, fw;
            cin >> fw >> a >> b;
            if (a == 0 || b == 0)
            {
                continue;
            }
            add(a, b + n);
            add(b + n, a);
        }

        int ans = 0;

        for (rint i = 1; i <= n; i++)
        {
            memset(vis, 0, sizeof vis);
            if (dfs(i))
            {
                ans++;
            }
        }
        cout << ans << endl;
    }

    return 0;
}

P6062 Muddy Fields G

对于每块泥块都需要被横向或竖向的木板至少覆盖一次。

由于木板不能盖住干净的地面,所以横着的木板只能覆盖同一行若干个连续的泥块。
竖着的木板只能覆盖同一列若干个连续的泥块。我们把这些连续的泥块分别分成 行泥块 和 列泥块。

对于每个泥块,把它所在的行泥块和列泥块之间连一条边。

那么可以发现,行泥块就是左部节点,列泥块就是右部节点。我们只需要保证每条边至少有一个节点被覆盖即可。
因此求的就是二分图的最小点覆盖,等价于求最大匹配数,用匈牙利算法来解决。

const int N = 6e1 + 5;
const int M = 4e3 + 5;

int n, m, tot = 1;
int a[N][N][2], match[M], ans;
char s[N][N];
bool v[M];
vector<int> e[M];

bool dfs(int x) 
{
	for (unsigned int i = 0; i < e[x].size(); i++) 
	{
		int y = e[x][i];
		if (v[y]) continue;
		v[y] = 1;
		if (!match[y] || dfs(match[y])) 
		{
			match[y] = x;
			return 1;
		}
	}
	return 0;
}

signed main() 
{
	cin >> n >> m;
	for (rint i = 1; i <= n; i++) scanf("%s", s[i] + 1);
	
	for (rint i = 1; i <= n; i++)
	{
		for (rint j = 1; j <= m + 1; j++)
		{
			if (s[i][j] == '*') a[i][j][0] = tot;
			else ++tot;				
		}	
	}

	int t = tot;
	
	for (rint j = 1; j <= m; j++)
	{
		for (rint i = 1; i <= n + 1; i++)
		{
			if (s[i][j] == '*') a[i][j][1] = tot;
			else ++tot;				
		}
	}

	for (rint i = 1; i <= n; i++)
	{
		for (rint j = 1; j <= m; j++)
		{
			if (s[i][j] == '*') 
			{
				e[a[i][j][0]].push_back(a[i][j][1]);
				e[a[i][j][1]].push_back(a[i][j][0]);
			}			
		}
	}

	for (rint i = 1; i < t; i++)
	{
		memset(v, 0, sizeof v);
		if(dfs(i)) ans++;
	}
	
	cout << ans << endl;
	
	return 0;
}

AcWing 378. 骑士放置

最大独立集:在一个图中,选出最多的点,使得选出的点之间没有边。

最大团:在一个图中,选出最多的点,使得任意两点之间有边。

两者互补(补图:在一个图中,如果原来两点之间有边,则去掉该边,如果两点之间无边,则加上该边)

在二分图中,求最大独立集 \(<=>\) 求去掉最少的点将所有边破坏 \(<=>\) 找最小点覆盖 \(<=>\) 最大匹配

结论:设总点数是 \(n\),最大匹配是 \(m\),最大独立集是 \(n-m\)

该题中,将每个格子看成点,如果两个格子中放棋子之后可以攻击到则连一条边,任务就是找出最多的点互相攻击不到,既找出最多的点,使点之间没有边,即最大独立集问题,这里同样按照点的奇偶性染色,可以发现同样满足二分图的性质,所以最后的结果就是 \(n*m-\) 禁止放置点数 \(-\) 最大匹配数。

const int dx[] = {-2, -2, -1, -1, 1, 1, 2, 2};
const int dy[] = {-1, 1, -2, 2, -2, 2, -1, 1};

int n, m, t, ans;
int fx[N][N], fy[N][N];
bool a[N][N], v[N][N];

bool dfs(int x, int y) 
{
	for (rint i = 0; i < 8; i++) 
	{
		int nx = x + dx[i];
		int ny = y + dy[i];
		if (nx < 1 || ny < 1 || nx > n || ny > m || a[nx][ny]) continue;
		if (v[nx][ny]) continue;
		v[nx][ny] = 1;
		if (fx[nx][ny] == 0 || dfs(fx[nx][ny], fy[nx][ny])) 
		{
			fx[nx][ny] = x;
			fy[nx][ny] = y;
			return 1;
		}
	}
	return 0;
}

signed main() 
{
	cin >> n >> m >> t;
	for (rint i = 1; i <= t; i++) 
	{
		int x, y;
		cin >> x >> y;
		a[x][y] = 1;
	}
	for (rint i = 1; i <= n; i++) 
	{
		for (rint j = 1; j <= m; j++) 
		{
			if ((i + j) & 1 || a[i][j]) continue;
			memset(v, 0, sizeof v);
			if (dfs(i, j)) ans++;
		}
	}
	cout << n * m - t - ans << endl;
	return 0;
}

AcWing 379. 捉迷藏

考虑从路径上选点,要求每条路径只能选一个点,考虑可选路径和可选点数

记最小路径重复点覆盖为 \(cnt\),则可选路径 \(≤ cnt\),则可选点数 \(≤ cnt\)
下面构造一种选法,使点数可以等于 \(cnt\)

对于所有最小路径重复点覆盖中的路径,记终点集合为 \(E\),从 \(E\) 能到达的所有点记为 \(nxt(E)\)

\(E \cap nxt(E) = \varnothing\),则 \(E\) 内部两两之间不可相互到达,所以 \(E\) 可以作为一种方案,点数为 \(cnt\)

\(E \cap nxt(E) \neq \varnothing\),对 \(E\) 中的所有点 \(e_i\),让它一直往前走,直到 \(e_i \notin nxt(E)\) 时选择当前点,则最终选出的 \(cnt\) 个点可以作为一种方案

证明:对于任意 \(e_i \in E\),最多走到起点,就能找到 \(e_i \notin nxt(E)\)

假设存在 \(e_i \in E\),一直退到起点,路径上的点都属于 \(nxt(E)\),则起点可以被其它终点到达,把两条路径首尾相接可合并为一条路径,使总路径数变少,这与最小路径重复点覆盖的定义矛盾

#include <bits/stdc++.h>

#define rint register int
#define int long long
#define endl '\n'

using namespace std;

const int N = 3e2 + 5;

int n, m;
bool cl[N][N]; 
// 邻接矩阵
int match[N];
bool v[N], succ[N];
int hide[N]; 
// 藏身点集合

bool dfs(int x) 
{
	for (rint i = 1; i <= n; i++)
	{
		if (cl[x][i] && !v[i]) 
		{
			v[i] = 1;
			if (!match[i] || dfs(match[i])) 
			{
				match[i] = x;
				return 1;
			}
		}		
	}
	return 0;
}

signed main() 
{
	// 读入
	cin >> n >> m;
	for (rint i = 1; i <= m; i++) 
	{
		int x, y;
		cin >> x >> y;
		cl[x][y] = 1;
	}
	// Floyd 传递闭包
	for (rint i = 1; i <= n; i++) cl[i][i] = 1;
	for (rint k = 1; k <= n; k++)
		for (rint i = 1; i <= n; i++)
			for (rint j = 1; j <= n; j++)
				cl[i][j] |= cl[i][k] && cl[k][j];
	for (rint i = 1; i <= n; i++) cl[i][i] = 0;
	// 在拆点二分图上求最大匹配
	int ans = n;
	for (rint i = 1; i <= n; i++) 
	{
		memset(v, 0, sizeof v);
		ans -= dfs(i);
	}
	cout << ans << endl;
	// 构造方案,先把所有路径终点(左部非匹配点)作为藏身点
	for (rint i = 1; i <= n; i++) succ[match[i]] = 1;
	for (rint i = 1, k = 0; i <= n; i++)
		if (!succ[i]) 
		    hide[++k] = i;
	memset(v, 0, sizeof v);
	bool modify = 1;
	while (modify) 
	{
		modify = 0;
		// 求出 next(hide)
		for (rint i = 1; i <= ans; i++)
			for (rint j = 1; j <= n; j++)
				if (cl[hide[i]][j]) 
				    v[j] = 1;
		for (rint i = 1; i <= ans; i++)
			if (v[hide[i]]) 
			{
				modify = 1;
				// 不断向上移动
				while (v[hide[i]]) 
				    hide[i] = match[hide[i]];
			}
	}
	//for (rint i = 1; i <= ans; i++) cout << hide[i] << " ";
	//hide 为方案数
	return 0;
}
posted @ 2024-03-30 14:28  PassName  阅读(7)  评论(0编辑  收藏  举报