「アルゴリズム コンペティション: クイック 300 問」は 2024 年に出版される予定で、「アルゴリズム コンペティション」の補助問題集です。
すべての質問は、自作の OJ New Online Judgeに配置されます。
コードは C/C++、Java、Python の 3 つの言語で提供されており、トピックは主に中レベルから低レベルのトピックであり、初級レベルから上級レベルの学生に適しています。
「 キャンディーマッチング 」、リンク: http://oj.ecustacm.cn/problem.php?id=1735
質問の説明
[問題の説明] N 人の子供と M 個の異なるキャンディーが存在します。どの子供にも、一番好きなキャンディーと 2 番目に好きなキャンディーがあります。
いくつかのキャンディーが与えられると、子供たちはキャンディーを受け取るために列に並び、自分の好きなキャンディーがまだ残っている場合は一番好きなキャンディーを選択し、そうでない場合は 2 番目に好きなキャンディーを選択します。
どちらも存在しない場合、子供は泣き出すでしょう。
子どもたちを任意の順序で列に並べることができますが、泣く子どもの数が最小限になるようにしてください。
泣いている子供の最小数を見つけます。
[入力形式]入力形式
input の 1 行目には N と M が含まれます。(N, M ≤ 100000)
次に N 行あり、各 i 行には 2 つの数字 fi と si が含まれており、これらは i 番目の子供のお気に入りのキャンディーの番号と 2 番目に好きなキャンディーの番号を表します。
【出力形式】答えを表す数値を出力します。
【入力サンプル】
8 10
2 1
3 4
2 3
6 5
7 8
6 7
7 5
5 8
【出力サンプル】
1
答え
子どもたちはそれぞれ好きなキャンディーと 2 番目に好きなキャンディーを持っており、その 2 つのうち 1 つを選択することは、子供が 2 つのキャンディーをつなげることに相当します。子供とキャンディーをグラフとしてモデル化します。子供たちはグラフ上のエッジ、キャンディーはグラフ上の点となります。各ポイントを一致させるには各エッジが必要です。最大でいくつのポイントとエッジを一致させることができますか?
下の図のように例を描きます。絵の端は子供たち、点は子供たちが好きなキャンディーです。たとえば、エッジ {1-2} は、最初の子供が好きなキャンディー 1 と 2 です。サンプル データに基づいて、接続された 2 つのサブグラフを描画します。1 つのサブグラフは {1,2,3,4} で、これはツリーです。もう 1 つのサブグラフは、{5,6,7,8} で、これはリング グラフです。 。
分析して結論を下すのは簡単です: 接続されたサブグラフがツリーの場合、一致の数はエッジの数に等しいか、点の数から 1 を引いたものに等しくなります; 接続されたサブグラフが循環グラフの場合、一致の数はエッジの数に等しくなります。一致はポイントの数と同じです。たとえば、上の図では、左側のサブグラフは 3 つの子を満たすことができる 3 つのエッジを持つツリーであり、右側のサブグラフは 4 つの子を満足できる 4 つの点を持つリング グラフです。
変換後、この質問はそのようなグラフの接続性の問題です: (1) グラフを構築します; (2) 接続されたサブグラフがいくつあるかをクエリします; (3) 各サブグラフについて、それがツリーであるか循環グラフであるかを区別します。エッジと点の数を個別に数えます。
グラフの接続性は、BFS、DFS、およびユニオン ルックアップを使用してエンコードできます。以下は、コーディングを使用した比較的単純な Union-Find ソリューションです。
まず、点とエッジを読み取り、和集合ルックアップを使用してそれらを処理します。同じサブグラフに属する点については、それらのセットは同じです。同時に、リングを使用して、セットが循環グラフであるかどうかをマークします。
Union-find を使用して循環グラフを処理するにはどうすればよいですか? 2 つの点 u と v からなるエッジ uv を読み取ったときに、u と v が以前に読み込まれて処理されており、セットに属していることが判明した場合、エッジ uv が元の部分グラフを循環グラフに変換したことを意味します。
すべてのポイントとエッジを読み取って処理した後、上に示した 2 つのサブグラフは、以下の 2 つの和集合検索セットになります。結合検索セット 2 には点 {1, 2, 3, 4} が含まれ、結合検索セット 6 には点 {5, 6, 7, 8} が含まれます。
2 つの重要な点に注意してください:
(1) 集合内の各点が同じセットに属するように、和集合検索セットはパスで圧縮する必要があります。たとえば、{1、2、3、4} はすべて和集合検索セットに属します。 2.
(2) 各和集合がツリーであるか循環グラフであるかをマークする必要があります。次のコードは、リング パラメーターを使用して、点が循環グラフ上にあるかどうかをマークします。和集合内の点のリング マークが true である限り、和集合は循環グラフです。
最後のステップは、存在する共用体ルックアップ セットの数を検索し、各共用体ルックアップ セットに存在するキャンディーの一致の数をカウントすることです。これら 2 つのタスクは、O(nlogn) 回の計算のみで完了できます。
(1) すべての共用体検索セットをセットのサイズに従ってソートします (例: {1, 2, 3, 4}、{5, 6, 7, 8}、2 つの共用体検索セットの対応するセット) {2, 2 , 2, 2}、{6, 6, 6, 6} である場合、セットのサイズでソートした後、同じセット内の点はすべて一緒に配置されます。ソートの計算量はO(nlogn)です。
(2) 小さい集合から大きい集合まですべてたどり、同じ大きさの集合であれば集合に属します。このセット内のキャンディーの一致の数を数えます。計算量はO(n)となる。
【ポイント】グラフの接続性。
C++ コード
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
struct child {
int s; bool ring;} c[N]; //s: 并查集; ring:这个点是否在一个有环图上
bool cmp(struct child a, struct child b){
return a.s < b.s;} //按s排序
int find_set(int x){
//并查集:查询
if(c[x].s!=x) c[x].s=find_set(c[x].s); //路径压缩
return c[x].s;
}
int main(){
int n,m; cin>>n>>m;
for(int i=1;i<=m;i++) c[i].s = i,c[i], c[i].ring = false; //并查集初始化
for(int i=1;i<=n;i++) {
int u,v; cin>>u>>v; //读取一条边上的两个点
u = find_set(u); //查询它们的集
v = find_set(v);
if(u==v){
//已经是同一个集,说明这个集是一个有环图
c[u].ring = true; //标注这个集是一个有环图
continue; //已经在一个集中了,不用合并
}
c[v].s = c[u].s; //u、v还不在一个集中,进行并查集合并
}
for(int i=1;i<=m;i++)
find_set(i); //利用查询进行路径压缩,使同一个集的点的所属的集相同
sort(c+1,c+m+1,cmp); //对集排序,让同一个集的点排在一起
int tot = 0; //统计能满足多少小朋友
for(int i=2;i<=m;i++) {
//遍历有多少个集
bool Ring = false; //这个集是否为有环图,初始化为非环图
int point = 1; //统计这个集表示的连通子图内有多少个点
while(c[i].s == c[i-1].s) {
//如果两点的集s相同,说明它们属于同一个子图
if(c[i-1].ring || c[i].ring ) Ring = true; //这个集是一个有环图
point++; //统计这个集合的点的数量
i++; //遍历这个集
}
if(Ring==false) point--; //不是有环图,是一棵树
tot += point;
}
cout<<n-tot; //不能满足的小朋友人数
return 0;
}
Javaコード
import java.util.*;
public class Main {
static class Child {
int s;
boolean ring;
public Child(int s, boolean ring) {
this.s = s;
this.ring = ring;
}
}
static Child[] c;
static int n, m;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
m = scanner.nextInt();
int N = 100010;
c = new Child[N + 1];
for (int i = 1; i <= N; i++) c[i] = new Child(i, false);
for (int i = 1; i <= n; i++) {
int u = scanner.nextInt();
int v = scanner.nextInt();
u = findSet(u);
v = findSet(v);
if (u == v) {
c[u].ring = true;
continue;
}
c[v].s = c[u].s;
}
for (int i = 1; i <= m; i++) findSet(i);
Arrays.sort(c, 1, m + 1, new Comparator<Child>() {
public int compare(Child a, Child b) {
return a.s - b.s; }
});
int tot = 0;
for (int i = 2; i <= m; i++) {
boolean ring = false;
int point = 1;
while (c[i].s == c[i - 1].s) {
if (c[i - 1].ring || c[i].ring) ring = true;
point++;
i++;
}
if (!ring) point--;
tot += point;
}
System.out.println(n - tot);
}
static int findSet(int x) {
if (c[x].s != x) c[x].s = findSet(c[x].s);
return c[x].s;
}
}
Pythonコード
import sys
sys.setrecursionlimit(1000000)
import functools
N = 100010
class Child:
def __init__(self, s, ring):
self.s = s
self.ring = ring
def cmp(a, b): return a.s - b.s
def find_set(x):
if c[x].s != x: c[x].s = find_set(c[x].s)
return c[x].s
c = []
n, m = map(int, input().split())
for i in range(N): c.append(Child(i, False))
for i in range(1,n+1):
u, v = map(int, input().split())
u = find_set(u)
v = find_set(v)
if u == v:
c[u].ring = True
continue
c[v].s = c[u].s
for i in range(1, m + 1): find_set(i)
c[1:] = sorted(c[1:], key=functools.cmp_to_key(cmp))
tot = 0
i = 2
while i <= m:
Ring = False
point = 1
while c[i].s == c[i - 1].s:
if c[i - 1].ring or c[i].ring: Ring = True
point += 1
i += 1
if not Ring: point -= 1
tot += point
i += 1
print(n - tot)