البحث في العمق
هذه خوارزمية تستكشف بشكل تكراري جيران رأس معين حتى تتم زيارة جميع الرؤوس. الرأس الابتدائي يسمى المصدر.
كيف يعمل DFS
يبدأ الاجتياز عند رأس مصدر \(u\) ويضع عليه علامة كمُزار، إذا لم تتم زيارة أحد جيران الرأس الحالي ننتقل إلى ذلك الرأس ونكرر العملية. إذا تمت زيارة جميع جيران الرأس الحالي فإننا نعود إلى الرأس الأب.
التعقيد الزمني لهذا الاجتياز هو \(O(N + M)\) حيث \(N\) هو عدد الرؤوس و \(M\) هو عدد الأحرف.
const int maxN = 200'001;
bool vis[maxN];
vector<int> adj[maxN]; // adjacency list
void dfs (int u) {
vis[u] = true;
for (int v : adj[u]) {
if (!vis[v]) dfs(v);
}
}Path Finding
معطى رسم بياني غير موجه \(G\) يتكون من \(N\) رأساً مرقمة من \(1\) إلى \(N\) و \(M\) حرفاً، تحقق مما إذا كان الرأس \(1\) يمكنه الوصول إلى الرأس \(N\) واطبع مساراً ممكناً.
Solution
إذا بدأنا اجتياز DFS على الرأس \(1\)، يمكننا فحص vis[N] لمعرفة ما إذا كان المسار موجوداً أم لا. هدفنا هو استرجاع المسار الذي اتخذه الاجتياز للوصول إلى الرأس \(N\).
لتحقيق ذلك، سنخزن مصفوفة \(p\) حيث \(p_i\) هو الرأس الذي أدى بـ DFS لاكتشاف الرأس \(i\). باستخدام هذا يمكننا تتبع المسار من \(N\) عائدين إلى \(1\).
const int maxN = 200'001;
int vis[maxN], p[maxN];
vector<int> adj[maxN];
void dfs (int u) {
vis[u] = true;
for (int v : adj[u]) {
if (!vis[v]) {
p[v] = u; // we discovered v through u
dfs(v);
}
}
}
int main () {
int n, m;
cin >> n >> m;
for (int i = 0 ; i < m ; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs(1);
if (!vis[n]) cout << "No";
else {
cout << "Yes\n";
vector<int> pth;
for (int i = n ; i != 1 ; i = p[i]) { // trace back loop
pth.push_back(i);
}
reverse(pth.begin(), pth.end());
cout << 1 << ' ';
for (int i : pth) cout << i << ' ';
}
}Building Teams
يوجد \(N\) تلميذاً في الصف، و \(M\) صداقة بينهم. مهمتك هي تقسيم التلاميذ إلى فريقين بحيث لا يكون أي تلميذين في فريق واحد صديقين. يمكنك اختيار أحجام الفرق بحرية.
Solution
إنها حيلة مفيدة أن تحاول تصور العلاقات الثنائية كرؤوس وأحرف.
يمكننا القول أن التلاميذ هم رؤوس والصداقة بين تلميذين تربطهم بحرف.
نلاحظ أنه يجب علينا تعيين لون لكل رأس (مثلاً إما أحمر أو أزرق) بحيث تكون نقاط نهاية كل حرف بألوان مختلفة. هذا يُعرف بـ التلوين الثنائي.
لأي تعيين صالح، إذا قلبنا لون كل رأس من أحمر إلى أزرق أو العكس، سيظل التعيين صالحاً، لذا يمكننا افتراض أن الرأس \(1\) دائماً أحمر.
من ذلك يمكننا ملاحظة أن جميع جيرانه يجب أن يكونوا أزرق وبشكل تكراري يمكننا تعيين ألوان لجميع الرؤوس.
إذا لونا رأساً بلون \(C\) ثم اكتشفنا أننا سبق ولونا أحد جيرانه \(C\) أيضاً فيجب علينا التوقف فوراً لأننا وجدنا تناقضاً.
قد يتكون الرسم البياني من عدة مكونات منفصلة لذا يجب علينا الحل لكل واحدة على حدة.
const int maxN = 200'001;
bool vis[maxN], clr[maxN], contradiction = false;
vector<int> adj[maxN];
void dfs (int u) {
vis[u] = true;
for (int v : adj[u]) {
if (vis[v]) {
if (clr[v] == clr[u]) contradiction = true;
} else {
clr[v] = !clr[u];
dfs(v);
}
}
}
int main () {
int n, m;
cin >> n >> m;
for (int i = 0 ; i < m ; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
for (int i = 1 ; i <= n ; i++) if (!vis[i]) dfs(i);
if (contradiction) cout << "IMPOSSIBLE";
else {
for (int i = 1 ; i <= n ; i++) cout << 1 + clr[i] << ' ';
}
}