"البرمجة الحية": كيف فعلت نصف نهائي ICPC الإقليمي في جامعة ITMO

في أوائل ديسمبر ، هو الدور نصف النهائي من بطولة العالم لبرمجة الطلاب ICPC. سنخبرك ما هي "التجارب" التي أجراها المشاركون ومن سيمثل منطقة شمال أوراسيا في الربيع ، في البطولة العالمية الرئيسية للمبرمجين الرياضيين.


icpcnews / Flickr / CC BY / Final ICPC-2017

خمسة عشر فائزا


جرت المسابقات ، التي شارك فيها أكثر من 300 فريق ، في وقت واحد في أربعة أماكن: في سان بطرسبرغ ، بارناول ، تبيليسي وألما آتا. استقبلت جامعة ITMO أكثر من مائة فريق من روسيا ودول البلطيق. قاتل المشاركون لكأس NEERC الشمالية أوراسيا والحق في الذهاب إلى نهائيات ICPC.

ما هو ICPC؟
هذه مسابقة فريق لطلاب الجامعة وطلاب الدراسات العليا في السنة الأولى من الدراسة. أقيمت البطولة لأكثر من أربعين عامًا. يتكون كل فريق من ثلاثة أشخاص ويحصل على جهاز كمبيوتر واحد لا يمكنه الوصول إلى الإنترنت. على هذا الجهاز ، يجب أن يحلوا معاً حوالي عشرة مهام. يتيح لك هذا النهج اختبار ليس فقط معرفة الطلاب ، ولكن أيضًا مهاراتهم في العمل الجماعي. يحصل الفائزون في الأولمبياد على جوائز نقدية ودعوات عمل من شركات تكنولوجيا المعلومات الكبرى.

أصبح فريق جامعة موسكو الحكومية هو البطل المطلق ، بعد أن حل إحدى عشرة مشكلة. لا أحد تمكن من القيام بذلك بعد الآن. وكان المركزان الثاني والثالث مشاركين من MIPT. يمكن مشاهدة تقدم "المعارك" على الهواء مباشرة. يوجد سجل على قناة ICPC على YouTube:


في المجموع ، تم اختيار 15 فريقًا في نهائي ICPC-2019 (يمكن الاطلاع على القائمة بأكملها هنا ) ، بما في ذلك شباب من جامعة ITMO. في أواخر شهر مارس ، سوف يذهبون إلى مدينة بورتو (البرتغال) للقتال من أجل لقب بطل العالم.

كيف ذهب الدور نصف النهائي؟


استخدم الطلاب لغات البرمجة Java أو C ++ أو Python أو Kotlin. جميع المهام المطلوبة الاهتمام والتركيز والمعرفة من خوارزميات مختلفة.

على سبيل المثال ، تم اقتراح المهمة التالية من قِبل الفائز مرتين في لجنة ICPC كجزء من فريق جامعة ITMO Gennady Korotkevich :

يوجد رسم بياني غير موجه. يتم تحديد المسافة بين رأستي u و v بعدد الحواف في أقصر مسار. أوجد مجموع d (u، v) لجميع أزواج القمم المضطربة.

أولاً ، يتم إدخال رقمين n و m (2 ≤ n ≤ 10 5 ؛ n-1 ≤ m ≤ n + 42) - عدد الرؤوس والحواف ، على التوالي ، يتم إدخالها في إدخال البرنامج. يتم ترقيم القمم من 1 إلى n . بعد ذلك ، يتم إدخال أسطر m بقيمتين صحيحتين: x i و y i (1 i x i ، y i ≤ n؛ x i ≠ y i ) - هذه هي نقاط النهاية للحافة ith. هناك حافة واحدة على الأقل بين أي زوج من القمم.

مثال البرنامج مع الحل (اقترح من قبل عضو لجنة التحكيم):

رمز C ++
#undef _GLIBCXX_DEBUG #include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(0); int n, m; cin >> n >> m; vector<set<int>> gs(n); for (int i = 0; i < m; i++) { int x, y; cin >> x >> y; x--; y--; gs[x].insert(y); gs[y].insert(x); } long long ans = 0; vector<int> weight(n, 1); set<pair<int,int>> s; for (int i = 0; i < n; i++) { s.emplace(gs[i].size(), i); } while (s.size() > 1) { int i = s.begin()->second; assert(!gs[i].empty()); if (gs[i].size() > 1) { break; } s.erase(s.begin()); int j = *gs[i].begin(); gs[i].clear(); ans += (long long) 2 * weight[i] * (n - weight[i]); weight[j] += weight[i]; auto it = gs[j].find(i); assert(it != gs[j].end()); s.erase({gs[j].size(), j}); gs[j].erase(it); s.emplace(gs[j].size(), j); } if (s.size() == 1) { cout << ans / 2 << '\n'; return 0; } vector<vector<int>> g(n); for (int i = 0; i < n; i++) { g[i] = vector<int>(gs[i].begin(), gs[i].end()); } vector<int> id(n, -1); int cnt = 0; for (int i = 0; i < n; i++) { if ((int) g[i].size() > 2) { id[i] = cnt++; } } if (cnt == 0) { for (int i = 0; i < n; i++) { if ((int) g[i].size() == 2) { id[i] = cnt++; break; } } assert(cnt > 0); } vector<int> rev_id(n, -1); for (int i = 0; i < n; i++) { if (id[i] != -1) { rev_id[id[i]] = i; } } vector<vector<vector<vector<int>>>> edges(cnt, vector<vector<vector<int>>>(cnt)); for (int i = 0; i < n; i++) { if (id[i] >= 0) { for (int j : g[i]) { if (id[j] >= 0) { edges[id[i]][id[j]].emplace_back(); } } } } for (int i = 0; i < n; i++) { if ((int) g[i].size() == 2 && id[i] == -1) { vector<int> edge; edge.push_back(weight[i]); id[i] = -2; vector<int> fin(2); for (int dir = 0; dir < 2; dir++) { int x = g[i][dir]; int px = i; while (id[x] == -1) { assert((int) g[x].size() == 2); edge.push_back(weight[x]); id[x] = -2; int nx = px ^ g[x][0] ^ g[x][1]; px = x; x = nx; } fin[dir] = x; reverse(edge.begin(), edge.end()); } edges[id[fin[1]]][id[fin[0]]].push_back(edge); } } vector<vector<int>> dist(cnt, vector<int>(cnt, n + 1)); for (int i = 0; i < cnt; i++) { dist[i][i] = 0; } for (int i = 0; i < cnt; i++) { for (int j = 0; j < cnt; j++) { for (auto &p : edges[i][j]) { dist[i][j] = dist[j][i] = min(dist[i][j], (int) p.size() + 1); } } } for (int k = 0; k < cnt; k++) { for (int i = 0; i < cnt; i++) { for (int j = 0; j < cnt; j++) { dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]); } } } vector<vector<vector<vector<long long>>>> edge_pref_sum(cnt, vector<vector<vector<long long>>>(cnt)); vector<vector<vector<vector<long long>>>> edge_pref_sum_by_pos(cnt, vector<vector<vector<long long>>>(cnt)); for (int i = 0; i < cnt; i++) { for (int j = 0; j < cnt; j++) { edge_pref_sum[i][j].resize(edges[i][j].size()); edge_pref_sum_by_pos[i][j].resize(edges[i][j].size()); for (int k = 0; k < (int) edges[i][j].size(); k++) { edge_pref_sum[i][j][k].resize(edges[i][j][k].size() + 1); edge_pref_sum_by_pos[i][j][k].resize(edges[i][j][k].size() + 1); for (int t = 0; t < (int) edges[i][j][k].size(); t++) { edge_pref_sum[i][j][k][t + 1] = edge_pref_sum[i][j][k][t] + edges[i][j][k][t]; edge_pref_sum_by_pos[i][j][k][t + 1] = edge_pref_sum_by_pos[i][j][k][t] + (long long) edges[i][j][k][t] * t; } } } } auto get = [&](int i, int j, int k, int from, int to, int coeff_from, int coeff_to) -> long long { if (from > to) { return 0; } assert(0 <= from && to <= (int) edges[i][j][k].size() - 1); long long ret = (edge_pref_sum[i][j][k][to + 1] - edge_pref_sum[i][j][k][from]) * coeff_from; if (coeff_from != coeff_to) { assert(abs(coeff_from - coeff_to) == to - from); long long other = edge_pref_sum_by_pos[i][j][k][to + 1] - edge_pref_sum_by_pos[i][j][k][from]; other -= (edge_pref_sum[i][j][k][to + 1] - edge_pref_sum[i][j][k][from]) * from; ret += other * (coeff_from < coeff_to ? 1 : -1); } return ret; }; for (int v = 0; v < cnt; v++) { long long w = weight[rev_id[v]]; for (int j = 0; j < cnt; j++) { ans += dist[v][j] * w * weight[rev_id[j]]; } for (int i = 0; i < cnt; i++) { for (int j = 0; j < cnt; j++) { for (int k = 0; k < (int) edges[i][j].size(); k++) { int x = dist[v][i]; int y = dist[v][j]; int cc = (y - x + (int) edges[i][j][k].size() + 1) / 2; cc = min(max(cc, 0), (int) edges[i][j][k].size()); ans += w * get(i, j, k, 0, cc - 1, x + 1, x + cc); ans += w * get(i, j, k, cc, (int) edges[i][j][k].size() - 1, y + ((int) edges[i][j][k].size() - cc), y + 1); } } } } vector<pair<int,int>> pairs; for (int i = 0; i < cnt; i++) { for (int j = 0; j < cnt; j++) { if (!edges[i][j].empty()) { pairs.emplace_back(i, j); } } } for (int ii = 0; ii < cnt; ii++) { for (int jj = 0; jj < cnt; jj++) { for (int kk = 0; kk < (int) edges[ii][jj].size(); kk++) { for (int tt = 0; tt < (int) edges[ii][jj][kk].size(); tt++) { long long w = edges[ii][jj][kk][tt]; for (int i = 0; i < cnt; i++) { int d1 = dist[ii][i] + tt + 1; int d2 = dist[jj][i] + (int) edges[ii][jj][kk].size() - tt; ans += w * weight[rev_id[i]] * min(d1, d2); } for (auto &p : pairs) { int i = p.first; int j = p.second; for (int k = 0; k < (int) edges[i][j].size(); k++) { if (i == ii && j == jj && k == kk) { int d1 = tt; int d2 = (int) edges[ii][jj][kk].size() - tt + dist[i][j] + 1; if (d1 <= d2) { ans += w * get(i, j, k, 0, tt, tt, 0); } else { int cut = (d1 - d2 + 1) / 2; ans += w * get(i, j, k, 0, cut - 1, d2, d2 + cut - 1); ans += w * get(i, j, k, cut, tt, tt - cut, 0); } int d3 = (int) edges[ii][jj][kk].size() - 1 - tt; int d4 = tt + 1 + dist[i][j] + 1; if (d3 <= d4) { ans += w * get(i, j, k, tt, (int) edges[i][j][k].size() - 1, 0, (int) edges[i][j][k].size() - 1 - tt); } else { int cut = (d3 - d4 + 1) / 2; ans += w * get(i, j, k, (int) edges[i][j][k].size() - cut, (int) edges[i][j][k].size() - 1, d4 + cut - 1, d4); ans += w * get(i, j, k, tt, (int) edges[i][j][k].size() - 1 - cut, 0, (int) edges[i][j][k].size() - 1 - cut - tt); } } else { int d1 = dist[ii][i] + tt + 1; int d2 = dist[jj][i] + (int) edges[ii][jj][kk].size() - tt; int d3 = dist[ii][j] + tt + 1; int d4 = dist[jj][j] + (int) edges[ii][jj][kk].size() - tt; int x = min(d1, d2); int y = min(d3, d4); int cc = (y - x + (int) edges[i][j][k].size() + 1) / 2; cc = min(max(cc, 0), (int) edges[i][j][k].size()); ans += w * get(i, j, k, 0, cc - 1, x + 1, x + cc); ans += w * get(i, j, k, cc, (int) edges[i][j][k].size() - 1, y + ((int) edges[i][j][k].size() - cc), y + 1); } } } } } } } assert(ans % 2 == 0); cout << ans / 2 << '\n'; return 0; } 


وهنا الكود الذي اقترحه أحد الفرق المشاركة كحل:

رمز C ++
 #include <bits/stdc++.h> #define ll long long #define ld long double using namespace std; int main() { int n,m; cin>>n>>m; int b[n+1][n+1]={}; int c[n+1][n+1]={}; int a1,b1; vector < vector <int> > v(n+1); vector <int> met(n+1); queue <int> q; for(int i=0;i<m;i++){ cin>>a1>>b1; v[a1].push_back(b1); v[b1].push_back(a1); c[a1][b1]=1; c[b1][a1]=1; } long long ans=0; for(int i=1;i<=n;i++){ q.push(i); met.clear(); met.resize(n+1); while(!q.empty()){ int frontt = q.front(); met[frontt]=1; for(int j=0;j<v[frontt].size();j++){ if(!met[v[frontt][j]]){ if(b[i][frontt]+1<b[i][v[frontt][j]] || b[i][v[frontt][j]]==0){ ans-=b[i][v[frontt][j]]; b[i][v[frontt][j]]=b[i][frontt]+1; ans+=b[i][v[frontt][j]]; } q.push(v[frontt][j]); met[v[frontt][j]]=1; } } q.pop(); } } cout<<ans/2; return 0; } 


يمكن العثور على تحليل الحل في المستند الرسمي على موقعنا ( صفحة 3 ).

مهمة أخرى هي "الشطرنج":

يتعلم إلما لعب الشطرنج ويعرف بالفعل أن الغراب يتحرك أفقياً أو رأسياً. أعطتها جدة إلما رقعة شطرنج 8 × 8 وطلبت منها إيجاد طريقة لتحريك الغراب من الخلية A1 إلى H8 في حركات ن . في هذه الحالة ، يجب أن تكون جميع الخلايا التي يتوقف عليها الرقم مختلفة. يتم توفير الإدخال بالقيمة n (2 ≤ n ≤ 63). يحتاج المشاركون إلى وضع قائمة بجميع الخلايا التي وضعتها Elma على الرخ.

فيما يلي مثال لحل هذه المشكلة الذي اقترحه أعضاء لجنة التحكيم:

كود جافا
 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.ArrayList; import java.util.StringTokenizer; public class easy_chess_va_wa { BufferedReader br; StringTokenizer st; PrintWriter out; public String nextToken() throws IOException { while (st == null || !st.hasMoreTokens()) { st = new StringTokenizer(br.readLine()); } return st.nextToken(); } public int nextInt() throws IOException { return Integer.parseInt(nextToken()); } public class Cell { int x, y; public Cell(int x, int y) { this.x = x; this.y = y; } public String toString() { return (char) ('a' + x) + "" + (y + 1); } } public void solve() throws IOException { int n = nextInt() + 1; ArrayList<Cell> cells = new ArrayList<>(); int k = Math.min(8 * 8, n + 4); if (k <= 8 * 7) { for (int i = 0; i < 7 && cells.size() < k; i++) { for (int j = 0; j < 8 && cells.size() < k; j++) { cells.add(new Cell(i % 2 == 0 ? j : 7 - j, i)); } } Cell last = cells.get(cells.size() - 1); if (last.x != 7) { cells.add(new Cell(last.x, 7)); } cells.add(new Cell(7, 7)); } else { for (int i = 0; i < 7; i++) { for (int j = 0; j < 8; j++) { cells.add(new Cell(i % 2 == 0 ? j : 7 - j, i)); } } for (int i = 0; i < 8; i++) { for (int j = 0; j < 2; j++) { cells.add(new Cell(i, i % 2 == 0 ? 7 - j : 6 + j)); } } Cell last = cells.get(cells.size() - 1); if (last.y != 7) { cells.add(new Cell(last.x, 7)); } cells.add(new Cell(7, 7)); } while (cells.size() > n) { cells.remove(1); } for (Cell cell : cells) { out.print(cell + " "); } } public void run() { try { br = new BufferedReader(new InputStreamReader(System.in)); out = new PrintWriter(System.out); solve(); out.close(); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { new easy_chess_va_wa().run(); } } 


يتم نشر قائمة المهام الكاملة على الموقع الرسمي للمسابقة . هناك يمكنك العثور على إجابات مع تحليل مفصل للحلول. يكمن الرمز الذي اقترحه المشاركون في أرشيف منفصل ، وهنا يمكنك العثور على جميع الحلول والاختبارات التي تم استخدامها للتحقق التلقائي.

خلال الأولمبياد ، أرسل المشاركون قراراتهم إلى خادم اختبار ، قام "بالتحقق" من الكود في مجموعة من الاختبارات المعدة مسبقًا. في حالة الانتهاء بنجاح من جميع الاختبارات ، وسجل المشاركون نقاط. خلاف ذلك ، تلقى الفريق ملاحظات حول الخطأ ويمكنه إجراء تعديلات على الكود.

وفقًا لقواعد ICPC ، يفوز الفريق الذي حل معظم المهام.

إذا سجل عدة مشاركين نفس العدد من النقاط دفعة واحدة ، فسيتم تحديد موقعهم في الترتيب حسب الوقت المحدد للعقوبة. يتم فرض غرامة جزائية لكل مشكلة تم حلها بشكل صحيح ، وهي تساوي الوقت المنقضي من بداية المسابقة حتى اجتاز الرمز جميع الاختبارات. علاوة على ذلك ، لكل محاولة فاشلة لتمرير المهمة ، تتم إضافة 20 دقيقة إلى العقوبة (فقط في حالة حل المشكلة في النهاية). لا يتم منح أي عقوبة إذا لم يقدم الفريق الحل الصحيح للمشكلة.


icpcnews / Flickr / CC BY / Final ICPC-2017

ما الذي سيتنافس عليه الفائزون؟


كما سبق وقلنا ، سيذهب بطل نصف النهائي - خمسة عشر فريقًا - إلى البرتغال ، إلى مدينة بورتو ، حيث سيقاتلون من أجل كأس العالم و 15000 دولار. الفرق التي ستقام من الأول إلى الرابع ستمنح ميداليات ذهبية. سيحصل المشاركون الذين "انتهوا" في المركزين الخامس والثامن على ميداليات فضية ومن البرونزية التاسعة إلى الثانية عشرة. كما يتم تقديم جوائز نقدية لهم.

أصبح فريق جامعة ITMO بطل ICPC لمدة سبع مرات (الأخيرة - في عام 2017). هذا رقم قياسي عالمي لم يتغلب عليه أحد حتى الآن: في المركز الثاني في عدد الألقاب البطولية هم أيضًا مواطنون ، جامعة سانت بطرسبرغ الحكومية ، بأربعة كؤوس ، وأقرب المنافسين الأجانب ، أمريكا ستانفورد وجامعة جاو تونغ الصينية - ثلاثة انتصارات. طوال سبع سنوات متتالية ، فازت الفرق الروسية نهائيات العالم. دعونا نأمل أن يظهر الشباب في ICPC 2019 نتيجة جيدة.

عن ماذا نكتب على حبري:

Source: https://habr.com/ru/post/ar433644/


All Articles