Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
cliques.h
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14//
15// Maximal clique algorithms, based on the Bron-Kerbosch algorithm.
16// See http://en.wikipedia.org/wiki/Bron-Kerbosch_algorithm
17// and
18// C. Bron and J. Kerbosch, Joep, "Algorithm 457: finding all cliques of an
19// undirected graph", CACM 16 (9): 575-577, 1973.
20// http://dl.acm.org/citation.cfm?id=362367&bnc=1.
21//
22// Keywords: undirected graph, clique, clique cover, Bron, Kerbosch.
23
24#ifndef OR_TOOLS_GRAPH_CLIQUES_H_
25#define OR_TOOLS_GRAPH_CLIQUES_H_
26
27#include <cstddef>
28#include <cstdint>
29#include <functional>
30#include <limits>
31#include <numeric>
32#include <string>
33#include <vector>
34
35#include "absl/strings/str_cat.h"
40
41namespace operations_research {
42
43// Finds all maximal cliques, even of size 1, in the
44// graph described by the graph callback. graph->Run(i, j) indicates
45// if there is an arc between i and j.
46// This function takes ownership of 'callback' and deletes it after it has run.
47// If 'callback' returns true, then the search for cliques stops.
48void FindCliques(std::function<bool(int, int)> graph, int node_count,
49 std::function<bool(const std::vector<int>&)> callback);
50
51// Covers the maximum number of arcs of the graph with cliques. The graph
52// is described by the graph callback. graph->Run(i, j) indicates if
53// there is an arc between i and j.
54// This function takes ownership of 'callback' and deletes it after it has run.
55// It calls 'callback' upon each clique.
56// It ignores cliques of size 1.
57void CoverArcsByCliques(std::function<bool(int, int)> graph, int node_count,
58 std::function<bool(const std::vector<int>&)> callback);
59
60// Possible return values of the callback for reporting cliques. The returned
61// value determines whether the algorithm will continue the search.
62enum class CliqueResponse {
63 // The algorithm will continue searching for other maximal cliques.
65 // The algorithm will stop the search immediately. The search can be resumed
66 // by calling BronKerboschAlgorithm::Run (resp. RunIterations) again.
67 STOP
68};
69
70// The status value returned by BronKerboschAlgorithm::Run and
71// BronKerboschAlgorithm::RunIterations.
73 // The algorithm has enumerated all maximal cliques.
75 // The search algorithm was interrupted either because it reached the
76 // iteration limit or because the clique callback returned
77 // CliqueResponse::STOP.
79};
80
81// Implements the Bron-Kerbosch algorithm for finding maximal cliques.
82// The graph is represented as a callback that gets two nodes as its arguments
83// and it returns true if and only if there is an arc between the two nodes. The
84// cliques are reported back to the user using a second callback.
85//
86// Typical usage:
87// auto graph = [](int node1, int node2) { return true; };
88// auto on_clique = [](const std::vector<int>& clique) {
89// LOG(INFO) << "Clique!";
90// };
91//
92// BronKerboschAlgorithm<int> bron_kerbosch(graph, num_nodes, on_clique);
93// bron_kerbosch.Run();
94//
95// or:
96//
97// BronKerboschAlgorithm bron_kerbosch(graph, num_nodes, clique);
98// bron_kerbosch.RunIterations(kMaxNumIterations);
99//
100// This is a non-recursive implementation of the Bron-Kerbosch algorithm with
101// pivots as described in the paper by Bron and Kerbosch (1973) (the version 2
102// algorithm in the paper).
103// The basic idea of the algorithm is to incrementally build the cliques using
104// depth-first search. During the search, the algorithm maintains two sets of
105// candidates (nodes that are connected to all nodes in the current clique):
106// - the "not" set - these are candidates that were already visited by the
107// search and all the maximal cliques that contain them as a part of the
108// current clique were already reported.
109// - the actual candidates - these are candidates that were not visited yet, and
110// they can be added to the clique.
111// In each iteration, the algorithm does the first of the following actions that
112// applies:
113// A. If there are no actual candidates and there are candidates in the "not"
114// set, or if all actual candidates are connected to the same node in the
115// "not" set, the current clique can't be extended to a maximal clique that
116// was not already reported. Return from the recursive call and move the
117// selected candidate to the set "not".
118// B. If there are no candidates at all, it means that the current clique can't
119// be extended and that it is in fact a maximal clique. Report it to the user
120// and return from the recursive call. Move the selected candidate to the set
121// "not".
122// C. Otherwise, there are actual candidates, extend the current clique with one
123// of these candidates and process it recursively.
124//
125// To avoid unnecessary steps, the algorithm selects a pivot at each level of
126// the recursion to guide the selection of candidates added to the current
127// clique. The pivot can be either in the "not" set and among the actual
128// candidates. The algorithm tries to move the pivot and all actual candidates
129// connected to it to the set "not" as quickly as possible. This will fulfill
130// the conditions of step A, and the search algorithm will be able to leave the
131// current branch. Selecting a pivot that has the lowest number of disconnected
132// nodes among the candidates can reduce the running time significantly.
133//
134// The worst-case maximal depth of the recursion is equal to the number of nodes
135// in the graph, which makes the natural recursive implementation impractical
136// for nodes with more than a few thousands of nodes. To avoid the limitation,
137// this class simulates the recursion by maintaining a stack with the state at
138// each level of the recursion. The algorithm then runs in a loop. In each
139// iteration, the algorithm can do one or both of:
140// 1. Return to the previous recursion level (step A or B of the algorithm) by
141// removing the top state from the stack.
142// 2. Select the next candidate and enter the next recursion level (step C of
143// the algorithm) by adding a new state to the stack.
144//
145// The worst-case time complexity of the algorithm is O(3^(N/3)), and the memory
146// complexity is O(N^2), where N is the number of nodes in the graph.
147template <typename NodeIndex>
149 public:
150 // A callback called by the algorithm to test if there is an arc between a
151 // pair of nodes. The callback must return true if and only if there is an
152 // arc. Note that to function properly, the function must be symmetrical
153 // (represent an undirected graph).
154 using IsArcCallback = std::function<bool(NodeIndex, NodeIndex)>;
155 // A callback called by the algorithm to report a maximal clique to the user.
156 // The clique is returned as a list of nodes in the clique, in no particular
157 // order. The caller must make a copy of the vector if they want to keep the
158 // nodes.
159 //
160 // The return value of the callback controls how the algorithm continues after
161 // this clique. See the description of the values of 'CliqueResponse' for more
162 // details.
164 std::function<CliqueResponse(const std::vector<NodeIndex>&)>;
165
166 // Initializes the Bron-Kerbosch algorithm for the given graph and clique
167 // callback function.
169 CliqueCallback clique_callback)
170 : is_arc_(std::move(is_arc)),
171 clique_callback_(std::move(clique_callback)),
172 num_nodes_(num_nodes) {}
173
174 // Runs the Bron-Kerbosch algorithm for kint64max iterations. In practice,
175 // this is equivalent to running until completion or until the clique callback
176 // returns BronKerboschAlgorithmStatus::STOP. If the method returned because
177 // the search is finished, it will return COMPLETED; otherwise, it will return
178 // INTERRUPTED and it can be resumed by calling this method again.
180
181 // Runs at most 'max_num_iterations' iterations of the Bron-Kerbosch
182 // algorithm. When this function returns INTERRUPTED, there is still work to
183 // be done to process all the cliques in the graph. In such case the method
184 // can be called again and it will resume the work where the previous call had
185 // stopped. When it returns COMPLETED any subsequent call to the method will
186 // resume the search from the beginning.
187 BronKerboschAlgorithmStatus RunIterations(int64_t max_num_iterations);
188
189 // Runs at most 'max_num_iterations' iterations of the Bron-Kerbosch
190 // algorithm, until the time limit is exceeded or until all cliques are
191 // enumerated. When this function returns INTERRUPTED, there is still work to
192 // be done to process all the cliques in the graph. In such case the method
193 // can be called again and it will resume the work where the previous call had
194 // stopped. When it returns COMPLETED any subsequent call to the method will
195 // resume the search from the beginning.
196 BronKerboschAlgorithmStatus RunWithTimeLimit(int64_t max_num_iterations,
198
199 // Runs the Bron-Kerbosch algorithm for at most kint64max iterations, until
200 // the time limit is excceded or until all cliques are enumerated. In
201 // practice, running the algorithm for kint64max iterations is equivalent to
202 // running until completion or until the other stopping conditions apply. When
203 // this function returns INTERRUPTED, there is still work to be done to
204 // process all the cliques in the graph. In such case the method can be called
205 // again and it will resume the work where the previous call had stopped. When
206 // it returns COMPLETED any subsequent call to the method will resume the
207 // search from the beginning.
209 return RunWithTimeLimit(std::numeric_limits<int64_t>::max(), time_limit);
210 }
211
212 private:
213 DEFINE_INT_TYPE(CandidateIndex, ptrdiff_t);
214
215 // A data structure that maintains the variables of one "iteration" of the
216 // search algorithm. These are the variables that would normally be allocated
217 // on the stack in the recursive implementation.
218 //
219 // Note that most of the variables in the structure are explicitly left
220 // uninitialized by the constructor to avoid wasting resources on values that
221 // will be overwritten anyway. Most of the initialization is done in
222 // BronKerboschAlgorithm::InitializeState.
223 struct State {
224 State() {}
225 State(const State& other)
226 : pivot(other.pivot),
227 num_remaining_candidates(other.num_remaining_candidates),
228 candidates(other.candidates),
229 first_candidate_index(other.first_candidate_index),
230 candidate_for_recursion(other.candidate_for_recursion) {}
231
232 State& operator=(const State& other) {
233 pivot = other.pivot;
234 num_remaining_candidates = other.num_remaining_candidates;
235 candidates = other.candidates;
236 first_candidate_index = other.first_candidate_index;
237 candidate_for_recursion = other.candidate_for_recursion;
238 return *this;
239 }
240
241 // Moves the first candidate in the state to the "not" set. Assumes that the
242 // first candidate is also the pivot or a candidate disconnected from the
243 // pivot (as done by RunIteration).
244 inline void MoveFirstCandidateToNotSet() {
245 ++first_candidate_index;
246 --num_remaining_candidates;
247 }
248
249 // Creates a human-readable representation of the current state.
250 std::string DebugString() {
251 std::string buffer;
252 absl::StrAppend(&buffer, "pivot = ", pivot,
253 "\nnum_remaining_candidates = ", num_remaining_candidates,
254 "\ncandidates = [");
255 for (CandidateIndex i(0); i < candidates.size(); ++i) {
256 if (i > 0) buffer += ", ";
257 absl::StrAppend(&buffer, candidates[i]);
258 }
259 absl::StrAppend(
260 &buffer, "]\nfirst_candidate_index = ", first_candidate_index.value(),
261 "\ncandidate_for_recursion = ", candidate_for_recursion.value());
262 return buffer;
263 }
264
265 // The pivot node selected for the given level of the recursion.
266 NodeIndex pivot;
267 // The number of remaining candidates to be explored at the given level of
268 // the recursion; the number is computed as num_disconnected_nodes +
269 // pre_increment in the original algorithm.
270 int num_remaining_candidates;
271 // The list of nodes that are candidates for extending the current clique.
272 // This vector has the format proposed in the paper by Bron-Kerbosch; the
273 // first 'first_candidate_index' elements of the vector represent the
274 // "not" set of nodes that were already visited by the algorithm. The
275 // remaining elements are the actual candidates for extending the current
276 // clique.
277 // NOTE(user): We could store the delta between the iterations; however,
278 // we need to evaluate the impact this would have on the performance.
280 // The index of the first actual candidate in 'candidates'. This number is
281 // also the number of elements of the "not" set stored at the beginning of
282 // 'candidates'.
283 CandidateIndex first_candidate_index;
284
285 // The current position in candidates when looking for the pivot and/or the
286 // next candidate disconnected from the pivot.
287 CandidateIndex candidate_for_recursion;
288 };
289
290 // The deterministic time coefficients for the push and pop operations of the
291 // Bron-Kerbosch algorithm. The coefficients are set to match approximately
292 // the running time in seconds on a recent workstation on the random graph
293 // benchmark.
294 // NOTE(user): PushState is not the only source of complexity in the
295 // algorithm, but non-negative linear least squares produced zero coefficients
296 // for all other deterministic counters tested during the benchmarking. When
297 // we optimize the algorithm, we might need to add deterministic time to the
298 // other places that may produce complexity, namely InitializeState, PopState
299 // and SelectCandidateIndexForRecursion.
300 static const double kPushStateDeterministicTimeSecondsPerCandidate;
301
302 // Initializes the root state of the algorithm.
303 void Initialize();
304
305 // Removes the top state from the state stack. This is equivalent to returning
306 // in the recursive implementation of the algorithm.
307 void PopState();
308
309 // Adds a new state to the top of the stack, adding the node 'selected' to the
310 // current clique. This is equivalent to making a recurisve call in the
311 // recursive implementation of the algorithm.
312 void PushState(NodeIndex selected);
313
314 // Initializes the given state. Runs the pivot selection algorithm in the
315 // state.
316 void InitializeState(State* state);
317
318 // Returns true if (node1, node2) is an arc in the graph or if node1 == node2.
319 inline bool IsArc(NodeIndex node1, NodeIndex node2) const {
320 return node1 == node2 || is_arc_(node1, node2);
321 }
322
323 // Selects the next node for recursion. The selected node is either the pivot
324 // (if it is not in the set "not") or a node that is disconnected from the
325 // pivot.
326 CandidateIndex SelectCandidateIndexForRecursion(State* state);
327
328 // Returns a human-readable string representation of the clique.
329 std::string CliqueDebugString(const std::vector<NodeIndex>& clique);
330
331 // The callback called when the algorithm needs to determine if (node1, node2)
332 // is an arc in the graph.
333 IsArcCallback is_arc_;
334
335 // The callback called when the algorithm discovers a maximal clique. The
336 // return value of the callback controls how the algorithm proceeds with the
337 // clique search.
338 CliqueCallback clique_callback_;
339
340 // The number of nodes in the graph.
341 const NodeIndex num_nodes_;
342
343 // Contains the state of the aglorithm. The vector serves as an external stack
344 // for the recursive part of the algorithm - instead of using the C++ stack
345 // and natural recursion, it is implemented as a loop and new states are added
346 // to the top of the stack. The algorithm ends when the stack is empty.
347 std::vector<State> states_;
348
349 // A vector that receives the current clique found by the algorithm.
350 std::vector<NodeIndex> current_clique_;
351
352 // Set to true if the algorithm is active (it was not stopped by a the clique
353 // callback).
354 int64_t num_remaining_iterations_;
355
356 // The current time limit used by the solver. The time limit is assigned by
357 // the Run methods and it can be different for each call to run.
358 TimeLimit* time_limit_;
359};
360
361template <typename NodeIndex>
362void BronKerboschAlgorithm<NodeIndex>::InitializeState(State* state) {
363 DCHECK(state != nullptr);
364 const int num_candidates = state->candidates.size();
365 int num_disconnected_candidates = num_candidates;
366 state->pivot = 0;
367 CandidateIndex pivot_index(-1);
368 for (CandidateIndex pivot_candidate_index(0);
369 pivot_candidate_index < num_candidates &&
370 num_disconnected_candidates > 0;
371 ++pivot_candidate_index) {
372 const NodeIndex pivot_candidate = state->candidates[pivot_candidate_index];
373 int count = 0;
374 for (CandidateIndex i(state->first_candidate_index); i < num_candidates;
375 ++i) {
376 if (!IsArc(pivot_candidate, state->candidates[i])) {
377 ++count;
378 }
379 }
380 if (count < num_disconnected_candidates) {
381 pivot_index = pivot_candidate_index;
382 state->pivot = pivot_candidate;
383 num_disconnected_candidates = count;
384 }
385 }
386 state->num_remaining_candidates = num_disconnected_candidates;
387 if (pivot_index >= state->first_candidate_index) {
388 std::swap(state->candidates[pivot_index],
389 state->candidates[state->first_candidate_index]);
390 ++state->num_remaining_candidates;
391 }
392}
393
394template <typename NodeIndex>
395typename BronKerboschAlgorithm<NodeIndex>::CandidateIndex
396BronKerboschAlgorithm<NodeIndex>::SelectCandidateIndexForRecursion(
397 State* state) {
398 DCHECK(state != nullptr);
399 CandidateIndex disconnected_node_index =
400 std::max(state->first_candidate_index, state->candidate_for_recursion);
401 while (disconnected_node_index < state->candidates.size() &&
402 state->candidates[disconnected_node_index] != state->pivot &&
403 IsArc(state->pivot, state->candidates[disconnected_node_index])) {
404 ++disconnected_node_index;
405 }
406 state->candidate_for_recursion = disconnected_node_index;
407 return disconnected_node_index;
408}
409
410template <typename NodeIndex>
411void BronKerboschAlgorithm<NodeIndex>::Initialize() {
412 DCHECK(states_.empty());
413 states_.reserve(num_nodes_);
414 states_.emplace_back();
415
416 State* const root_state = &states_.back();
417 root_state->first_candidate_index = 0;
418 root_state->candidate_for_recursion = 0;
419 root_state->candidates.resize(num_nodes_, 0);
420 std::iota(root_state->candidates.begin(), root_state->candidates.end(), 0);
421 root_state->num_remaining_candidates = num_nodes_;
422 InitializeState(root_state);
423
424 DVLOG(2) << "Initialized";
425}
426
427template <typename NodeIndex>
428void BronKerboschAlgorithm<NodeIndex>::PopState() {
429 DCHECK(!states_.empty());
430 states_.pop_back();
431 if (!states_.empty()) {
432 State* const state = &states_.back();
433 current_clique_.pop_back();
434 state->MoveFirstCandidateToNotSet();
435 }
436}
437
438template <typename NodeIndex>
439std::string BronKerboschAlgorithm<NodeIndex>::CliqueDebugString(
440 const std::vector<NodeIndex>& clique) {
441 std::string message = "Clique: [ ";
442 for (const NodeIndex node : clique) {
443 absl::StrAppend(&message, node, " ");
444 }
445 message += "]";
446 return message;
447}
448
449template <typename NodeIndex>
450void BronKerboschAlgorithm<NodeIndex>::PushState(NodeIndex selected) {
451 DCHECK(!states_.empty());
452 DCHECK(time_limit_ != nullptr);
453 DVLOG(2) << "PushState: New depth = " << states_.size() + 1
454 << ", selected node = " << selected;
456
457 State* const previous_state = &states_.back();
458 const double deterministic_time =
459 kPushStateDeterministicTimeSecondsPerCandidate *
460 previous_state->candidates.size();
461 time_limit_->AdvanceDeterministicTime(deterministic_time, "PushState");
462
463 // Add all candidates from previous_state->candidates that are connected to
464 // 'selected' in the graph to the vector 'new_candidates', skipping the node
465 // 'selected'; this node is always at the position
466 // 'previous_state->first_candidate_index', so we can skip it by skipping the
467 // element at this particular index.
468 new_candidates.reserve(previous_state->candidates.size());
469 for (CandidateIndex i(0); i < previous_state->first_candidate_index; ++i) {
470 const NodeIndex candidate = previous_state->candidates[i];
471 if (IsArc(selected, candidate)) {
472 new_candidates.push_back(candidate);
473 }
474 }
475 const CandidateIndex new_first_candidate_index(new_candidates.size());
476 for (CandidateIndex i = previous_state->first_candidate_index + 1;
477 i < previous_state->candidates.size(); ++i) {
478 const NodeIndex candidate = previous_state->candidates[i];
479 if (IsArc(selected, candidate)) {
480 new_candidates.push_back(candidate);
481 }
482 }
483
484 current_clique_.push_back(selected);
485 if (new_candidates.empty()) {
486 // We've found a clique. Report it to the user, but do not push the state
487 // because it would be popped immediately anyway.
488 DVLOG(2) << CliqueDebugString(current_clique_);
489 const CliqueResponse response = clique_callback_(current_clique_);
490 if (response == CliqueResponse::STOP) {
491 // The number of remaining iterations will be decremented at the end of
492 // the loop in RunIterations; setting it to 0 here would make it -1 at
493 // the end of the main loop.
494 num_remaining_iterations_ = 1;
495 }
496 current_clique_.pop_back();
497 previous_state->MoveFirstCandidateToNotSet();
498 return;
499 }
500
501 // NOTE(user): The following line may invalidate previous_state (if the
502 // vector data was re-allocated in the process). We must avoid using
503 // previous_state below here.
504 states_.emplace_back();
505 State* const new_state = &states_.back();
506 new_state->candidates.swap(new_candidates);
507 new_state->first_candidate_index = new_first_candidate_index;
508
509 InitializeState(new_state);
510}
511
512template <typename NodeIndex>
514 int64_t max_num_iterations, TimeLimit* time_limit) {
515 CHECK(time_limit != nullptr);
516 time_limit_ = time_limit;
517 if (states_.empty()) {
518 Initialize();
519 }
520 for (num_remaining_iterations_ = max_num_iterations;
521 !states_.empty() && num_remaining_iterations_ > 0 &&
522 !time_limit->LimitReached();
523 --num_remaining_iterations_) {
524 State* const state = &states_.back();
525 DVLOG(2) << "Loop: " << states_.size() << " states, "
526 << state->num_remaining_candidates << " candidate to explore\n"
527 << state->DebugString();
528 if (state->num_remaining_candidates == 0) {
529 PopState();
530 continue;
531 }
532
533 const CandidateIndex selected_index =
534 SelectCandidateIndexForRecursion(state);
535 DVLOG(2) << "selected_index = " << selected_index;
536 const NodeIndex selected = state->candidates[selected_index];
537 DVLOG(2) << "Selected candidate = " << selected;
538
539 NodeIndex& f = state->candidates[state->first_candidate_index];
540 NodeIndex& s = state->candidates[selected_index];
541 std::swap(f, s);
542
543 PushState(selected);
544 }
545 time_limit_ = nullptr;
546 return states_.empty() ? BronKerboschAlgorithmStatus::COMPLETED
548}
549
550template <typename NodeIndex>
552 int64_t max_num_iterations) {
553 TimeLimit time_limit(std::numeric_limits<double>::infinity());
554 return RunWithTimeLimit(max_num_iterations, &time_limit);
555}
556
557template <typename NodeIndex>
559 return RunIterations(std::numeric_limits<int64_t>::max());
560}
561
562template <typename NodeIndex>
563const double BronKerboschAlgorithm<
564 NodeIndex>::kPushStateDeterministicTimeSecondsPerCandidate = 0.54663e-7;
565} // namespace operations_research
566
567#endif // OR_TOOLS_GRAPH_CLIQUES_H_
std::function< bool(NodeIndex, NodeIndex)> IsArcCallback
Definition cliques.h:154
BronKerboschAlgorithmStatus RunWithTimeLimit(TimeLimit *time_limit)
Definition cliques.h:208
BronKerboschAlgorithmStatus Run()
Definition cliques.h:558
std::function< CliqueResponse(const std::vector< NodeIndex > &)> CliqueCallback
Definition cliques.h:163
BronKerboschAlgorithm(IsArcCallback is_arc, NodeIndex num_nodes, CliqueCallback clique_callback)
Definition cliques.h:168
BronKerboschAlgorithmStatus RunIterations(int64_t max_num_iterations)
Definition cliques.h:551
BronKerboschAlgorithmStatus RunWithTimeLimit(int64_t max_num_iterations, TimeLimit *time_limit)
Definition cliques.h:513
void push_back(const value_type &val)
void reserve(size_type n)
GraphType graph
MPCallback * callback
#define DEFINE_INT_TYPE(int_type_name, value_type)
Definition int_type.h:167
time_limit
Definition solve.cc:22
In SWIG mode, we don't want anything besides these top-level includes.
void FindCliques(std::function< bool(int, int)> graph, int node_count, std::function< bool(const std::vector< int > &)> callback)
Definition cliques.cc:228
@ COMPLETED
The algorithm has enumerated all maximal cliques.
@ CONTINUE
The algorithm will continue searching for other maximal cliques.
void CoverArcsByCliques(std::function< bool(int, int)> graph, int node_count, std::function< bool(const std::vector< int > &)> callback)
Definition cliques.cc:242
STL namespace.
std::string message
Definition trace.cc:397