一、トポロジカルソート#
1. 概念定義#
AOV ネットワーク#
例えば、あるコンピュータ専攻の学生が図 3-4 に示されたすべてのコースを修了しなければならないと仮定します。図からは、各コース間の前提条件と後続の関係が明確に示されています。例えば、コース C5 の前提コースは C2 で、後続コースは C4 と C6 です。このように、頂点が活動を表し、辺が活動間の前後関係を示す有向グラフを頂点活動ネットワーク(Activity On Vertex network)、略して AOV ネットと呼びます。
トポロジカル順序、DAG#
- グラフ内に V から W への有向パスがある場合、V は必ず W の前に配置されます。この条件を満たす頂点の列をトポロジカル順序と呼びます。
- トポロジカル順序を得るプロセスがトポロジカルソートです。
- AOV に合理的なトポロジカル順序がある場合、それは必ず有向非巡回グラフ(Directed Acyclic Graph, DAG)です。
2. トポロジカルソートの考え方#
トポロジカルソートの考え方は、毎回入次数が 0 の頂点を見つけて出力し、その頂点のすべての隣接点の入次数を 1 減らすことです。
入次数が 0 の頂点を見つけることが重要であり、毎回全てを探索するのは多くの時間と空間を消費するため、より賢いアルゴリズムは、入次数が 0 になる頂点を常にコンテナに入れることです。
擬似コードは以下の通りです。
void TopSort() {
int cnt = 0;
for(グラフ内の各頂点V)
if( Indegree[W] == 0)
Enqueue(V,Q);
while(!isEmpty(Q)) {
V = Dequeue(Q);
出力V,またはVの出力順序を記録,cnt++;
for(Vの各隣接点W)
if(--Indegree[W] == 0)
Enqueue(V,Q);
}
if(cnt != |V|)
Error("グラフにサイクルがあります");
}
テンプレートコード:
const int maxn = 1005;
int N,M;//頂点数、辺数(活動数)
int edge[maxn][maxn];
int mint[maxn];//各活動チェックポイントへの最短時間
int In[maxn];//各活動チェックポイントの入次数
void init() {
memset(edge, -1, sizeof(edge));
memset(mint, 0, sizeof(mint));
memset(In, 0, sizeof(In));
}
bool Topsort() {//トポロジカルソート
queue<int> q;
for(int i = 0; i < N; ++i) {
if(In[i] == 0)
q.push(i);
}
int cnt = 0;
while(!q.empty()) {
int v = q.front();
q.pop();
cnt++;
for(int i = 0; i < N; ++i) {
if(v == i || edge[v][i] == -1) continue;//vを始点とするすべての辺をチェック
In[i]--;
//その他の操作
if(In[i] == 0) q.push(i);
}
}
if(cnt != N) return false;
else return true;
}
例題#
08 - 図 8 どのくらい時間がかかりますか (25 点)
はトポロジカルソートの変形で、プログラムはそれほど複雑ではないので、挑戦することをお勧めします;
問題の意図、コード、考え方についてはブログを参照してください:
3. 実際の問題の解決#
关键路径问题#
AOE ネットワーク(Activity On Edge)ネットワーク#
- 一般的にプロジェクトの工程を計画するために使用されます
- AOE ネットワークでは、活動は辺上に表示され、頂点は 3 つの部分に分けられます:頂点番号、最早完了時間、最遅完了時間
最早完了時間を導出する —— mint [j] = max ( mint [ j ], mint [ i ]+edge [ i ][ j ])#
次に後ろから最遅完了時間を導出 —— maxt [i] = min ( maxt [ j ], maxt [ j ]-edge [ i ][ j ])#
これによりフロート時間を得る —— D [i][ j ] = maxt [ j ] - mint [ i ] - edge [ i ][ j ]#
そして、关键路径は絶対に遅延を許さない活動で構成されるパス、つまりフロート時間のないパスです。
例題#
08 - 図 8 どのくらい時間がかかりますか (25 点)
はトポロジカルソートの変形で、最早完了時間を求めます。
08 - 図 9 关键活动 (30 点)
关键路径を求めます。
コードと考え方についてはブログを参照してください:PTA データ構造問題集 第 8 週 —— グラフ(下)
二、簡単なソート#
1. 前提#
void X_Sort(ElementType A[], int N);
- 簡単のため、整数の昇順ソートについて議論します。
- N は正の整数です。
- 比較に基づくソート(> = < が定義されています)についてのみ議論します。
- 内部ソートについてのみ議論します。
- 安定性: 任意の 2 つの等しいデータのソート前後の相対位置は変わりません。
- どのソートも、どの状況でも最も良いパフォーマンスを示すわけではありません!
2. ソートアルゴリズム#
テスト問題:09 - ソート 1 ソート (25 点)
バブルソート#
それは、ソートする要素の列を繰り返し訪問し、隣接する 2 つの要素を比較し、順序(例えば、大から小、アルファベット順で Z から A)に誤りがあれば、それらを交換します。要素の訪問作業は、隣接要素の交換が必要なくなるまで、つまりその要素の列がソートされるまで繰り返されます。
このアルゴリズムの名前は、より小さな要素が交換を通じて徐々に「浮かんで」数列の頂点に達することに由来しています(昇順または降順に並べられます)。これは、炭酸飲料中の二酸化炭素の泡が最終的に上昇するのと同じです。したがって、「バブルソート」と呼ばれます。——百度百科からの引用
void Bubble_Sort(ll a[], int N) {
for(int P = N-1; P >= 0; P--) {
bool flag = false;
//一回のバブルソート 上から下に比較し、上が下より大きければ交換
for(int i = 0; i < P; ++i) {
if(a[i] > a[i+1]) {
swap(a[i], a[i+1]);
flag = true;
}
}
if(!flag) break; //一回の処理で既に整列されている場合、交換は発生しない
}
}
時間計算量#
最良の場合:順序、時間計算量 T = O (N)
最悪の場合:全逆順、時間計算量 T = O (N^2^)
長所と短所#
長所:簡単に書け、隣接要素を交換するだけで、単方向リストでも直接ソート可能で、安定(交換前後で等しい要素の位置は変わらない)。
短所:時間計算量が大きく、遅い!
テスト結果#
テスト結果は以下の通りで、3 つのサンプルが通過しませんでした。
挿入ソート#
挿入ソートは、一般に直接挿入ソートとも呼ばれます。少数の要素のソートに対しては、効果的なアルゴリズムです。挿入ソートは最も簡単なソート方法の一つで、その基本的な考え方は、1 つのレコードを既にソートされた順序表に挿入することによって、新しいレコード数が 1 増えたソートされた順序表を作成することです。実装プロセスでは、二重ループを使用し、外側のループは最初の要素を除くすべての要素に対して、内側のループは現在の要素の前にあるソートされた表で挿入位置を探し、移動を行います。
——百度百科からの引用
void Insertion_Sort(ll a[], int N) {
for(int P = 1; P < N; P++) {
ll t = a[P];//次のカードを引く
int i;
for(i = P; i > 0 && a[i-1] > t; --i)
a[i] = a[i-1]; //空き位置を移動 その前の要素が現在の要素より小さくなるまで
a[i] = t; //新しいカードを配置
}
}
時間計算量#
最良の場合:順序、時間計算量 T = O (N)
最悪の場合:全逆順、時間計算量 T = O (N^2^)
一般的な場合の時間計算量下界の計算:
隣接する 2 つの要素を交換することで、ちょうど 1 つの逆順対を消去します。
I 個の逆順対があると仮定します。
T(N,I) = O(N+I)
長所と短所#
長所:安定
短所:比較回数は一定ではなく、比較回数が少ないほど、挿入点以降のデータの移動が多くなり、特にデータの総量が多い場合に顕著です。
テスト結果#
テスト結果は以下の通りで、かなり良好です。
効率を向上させる方法#
以下の定理があります。
- 任意の N 個の異なる要素からなる列は、平均して N (N-1)/4 個の逆順対を持ちます。
- いかなる隣接する 2 つの要素を交換するだけでソートするアルゴリズムの平均時間計算量は O (N^2^) です。
したがって、アルゴリズムの効率を向上させるためには、
- 毎回 1 つ以上の逆順対を消去する必要があります!
- 毎回、離れた 2 つの要素を交換する必要があります!
シェルソート#
挿入ソートの簡単さを利用し、挿入ソートが隣接する 2 つの要素しか交換できないという欠点を克服します。
シェルソートは、レコードをインデックスの一定の増分でグループ化し、各グループに直接挿入ソートアルゴリズムを適用します。増分が徐々に減少するにつれて、各グループに含まれるキーワードが増え、増分が 1 になると、全ファイルがちょうど 1 つのグループに分けられ、アルゴリズムは終了します。
——百度百科からの引用
増分列 D~M~ >D~M-1~>…>D~1~ = 1
各 D~k~ に対して「D~k~ 間隔」ソートを行います(k=M,M-1,……,1)
注意:「D~k~ 間隔」ソートされた列は、「D~k-1~ 間隔」ソートを実行した後も「D~k~ 間隔」ソートされたままです!
シェル増分列の選択#
- 元のシェルソート増分列 D~M~ = N/2, D~k~ = D~k+1~ / 2
- 増分要素が互いに素でない場合、小さな増分は全く効果がない可能性があります!
- 増分要素が互いに素でない場合、小さな増分は全く効果がない可能性があります!
void Shell_Sort(ll a[], int N) {
for(int D = N/2; D > 0; D /= 2) { //シェル増分列
for(int P = D; P < N; ++P) { //挿入ソート
ll t = a[P];
int i;
for(i = P; i >= D && a[i-D] > t; i -= D)
a[i] = a[i-D];
a[i] = t;
}
}
}
- Hibbard 増分列
- D~k~ = 2^k^-1 —— 隣接要素が互いに素
- Sedgewick 増分列など
長所と短所#
長所:速く、データ移動が少ない!データ量が多い場合に適しています。
短所:異なる増分列の選択がアルゴリズムの計算量に差をもたらすため、増分列の選択は経験に基づく必要があり、不安定です。
テスト結果#
すべてのテストサンプルで 100ms を超えないことがわかります。これらのテストサンプルでは、速度は非常に理想的です。
選択ソート#
ヒープソートを紹介する前に、選択ソートを紹介します。古い友達です。
void Selection_Sort(ll a[], int N) {
for(int i = 0; i < N; ++i) {
int mini = 0;
ll ans = inf;
//iの後ろで最小の要素を探し、その位置をminiに代入
for(int j = i; j <= N-1; ++j) {
if(a[j] < ans) {
ans = a[j];
mini = j;
}
}
//未ソート部分の最小要素をソート済み部分の最後の位置に移動
swap(a[i], a[mini]);
}
}
時間計算量#
どのような場合でも計算量は O (N^2^) です。
テスト結果#
テスト結果は以下の通りで、すべて通過しましたが、後のいくつかのサンプルは時間がかかりました。
ヒープソート#
ここでは昇順に並べる例を示します。元の配列をインデックス 0 から始まる最大ヒープに調整し、最大ヒープの頂点と現在の最後の要素を交換(最大ヒープの頂点を削除するのと同じ)した後、調整します。
void swap(ll& x, ll& y) {
ll t = x;
x = y;
y = t;
}
void PercDown(ll a[], int N, int rt) {
//N個の要素の配列において、a[now]を根とする部分ヒープを最大ヒープに調整
int father, son;
ll tmp = a[rt];
for(father = rt; (father*2+1) < N; father = son) {
son = father * 2 + 1;//左の子
if(son != N-1 && a[son] < a[son+1]) //右の子が存在し、左の子より大きい
son++;
if(tmp >= a[son]) break;//配置すべき場所を見つける
else a[father] = a[son];//下にフィルタリング
}
a[father] = tmp;
}
inline void BuildHeap(ll a[], int N) {
for(int i = N/2-1; i >= 0; --i) {
PercDown(a, N, i);
}
}
void Heap_Sort(ll a[], int N) {
BuildHeap(a, N);
for(int i = N-1; i > 0; --i) {
swap(a[0], a[i]);//最大ヒープの頂点a[0]とa[i]を交換
PercDown(a, i, 0);//削除後に調整
}
}
時間計算量#
ヒープソートは最良の平均時間計算量を示します。
最良の場合 O (nlogn)
最悪の場合 O (nlogn)
平均時間計算量 O (nlogn)
長所と短所#
長所:速い!最悪の場合でも性能が優れており、使用する補助空間が少ない。
短所:不安定で、オブジェクトのソートには適していません。
テスト結果#
テスト結果は以下の通りで、シェルソートよりも優れているようです。
マージソート#
コアはソートされた部分列のマージです。ここでは再帰的実装のバージョンを示します〜非再帰的実装はここを見てくださいマージソートのループ実装
void Merge(ll a[], int s, int m, int e, ll tmp[]) {
//配列aの局所a[s,m]とa[m+1,e]をtmp配列にマージし、tmpをソートされた状態に保ちます。
//その後、再びa[s,m]にコピーします。時間計算量O(e-m+1)、すなわちO(n)です。
int pb = s;//pbはtmp配列のインデックス
int p1 = s, p2 = m+1;//p1は前半、p2は後半を指します
while (p1 <= m && p2 <= e) {
if (a[p1] < a[p2])
tmp[pb++] = a[p1++];
else
tmp[pb++] = a[p2++];
}
while(p1 <= m)
tmp[pb++] = a[p1++];
while(p2 <= e)
tmp[pb++] = a[p2++];
for (int i = 0; i < e-s+1; ++i)
a[s+i] = tmp[i];
}
void MergeSort(ll a[], int s, int e, ll tmp[]) {
if (s < e) {//s>=eの場合は何もしません
int m = s + (e-s)/2;
MergeSort(a, s, m, tmp);//前半をソート
MergeSort(a, m+1, e, tmp);//後半をソート
Merge(a, s, m, e, tmp);//マージ aのsからmとm+1からeの2つの配列をソートされた状態でマージ
}
}
時間計算量#
最良の場合 O (nlogn)
最悪の場合 O (nlogn)
平均時間計算量 O (nlogn)
長所と短所#
長所:安定、速い
短所:比較的多くの空間を占有します。
テスト結果#
テスト結果は以下の通りで、じっくり味わってください。
クイックソート#
- k = a [0] を設定し、k を適切な位置に移動させ、k より小さい要素はすべて k の左側に、k より大きい要素はすべて k の右側に配置します(O (n) 時間で完了)。
2.k の左側の部分をクイックソートします。
3.k の右側の部分をクイックソートします。
k は主元です。
void QuickSort(ll a[], int s, int e){//a[s,e]をクイックソート
if (s >= e)
return;
int k = a[s];
int i = s,j = e;
while (i != j) {
while (j > i && a[j] >= k) --j;
swap(a[i],a[j]);
while (i < j && a[i] <= k) ++i;
swap(a[i],a[j]);
}//処理が終わった後a[i] = k;
QuickSort(a, s, i-1);//左側の部分をクイックソート
QuickSort(a, i+1, e);//右側の部分をクイックソート
}
時間計算量#
最良の場合は毎回ちょうど中分割 O (nlogn)
最悪の場合 O (N^2^)
平均時間計算量 O (nlogn)
長所と短所#
長所:すべての内部ソートの中で最も速いアルゴリズムです。
短所:不安定で、最悪の場合の効率が低下します!
テスト結果#
テスト結果は以下の通りです。
表ソート#
データ量が多く、ソートする要素がオブジェクトで、移動にかかる時間が特に高い場合、間接ソートが必要です。
ポインタ配列を「表」として定義し、ソート待ちの要素を記録します。
時間計算量#
最良の場合は初期状態で整列されています。
最悪の場合は N/2 個の環があり、各環には 2 つの要素が含まれ、2 つの要素を交換するには 3 ステップが必要で、3N/2 回の要素移動が必要です。
T = O (m N)、m は各 A 要素のコピー時間です。
基数ソート(バケットソートの拡張)#
以前に説明したアルゴリズムはすべて比較が必要で、最悪の場合でも Nlogn がありますが、もっと速くなりませんか?
N 人の学生がいて、彼らの成績が 0〜100 の間の整数であると仮定します(したがって、M = 101 個の異なる成績値があります)。どのようにして線形時間で学生を成績順にソートしますか?
LSD 主位優先 MSD 次位優先
基数ソートのコードはこのブログを参考にしました。方法は非常に巧妙です:基数ソート
ll getMax(ll a[], int n) {//n個の要素のa配列から最大数を見つける
int maxx = a[0];
for(int i = 1; i < n; ++i) {
if(a[i] > maxx) maxx = a[i];
}
return maxx;
}
void radixsort(ll a[], int n, int exp) { //n個の要素の配列aを「ある位数」に従ってソート(バケットソート)、基数は10
ll tmp[maxn];
ll T[20] = {0}; //負数のある10進数 二十個のバケット
for(int i = 0; i < n; ++i) //Tにそのバケットにいくつの数があるかを格納
T[(a[i]/exp)%10 + 10]++;
for(int i = 1; i < 20; ++i) //Tの値をtmp内の位置にする
T[i] += T[i-1];
for(int i = n - 1; i >= 0; --i) {
int now = T[(a[i]/exp)%10 + 10];//現在の数が配置されるべき位置
tmp[now-1] = a[i];
T[(a[i]/exp)%10 + 10]--;
}
for(int i = 0; i < n; ++i)
a[i] = tmp[i]; //ソートされたtmpをaに代入
}
void Radix_Sort(ll a[], int n) {
ll maxnum = getMax(a, n);
for(int exp = 1; maxnum/exp > 0; exp *= 10)
radixsort(a, n, exp);
}
時間計算量#
N は待ちソート要素の個数、B はバケット数です。
O (P (N+B)) 一回の配分時間は O (N)、一回の収集時間の計算量は O (B)、合計で P 回の配分と収集を行います。
長所と短所#
長所:桁数が少なく、待ちソート列の最大桁数が特に大きくない場合に適しており、速い。
短所:空間を時間と交換します。
テスト結果#
テスト結果は以下の通りで、非常に速いです。