obscure code
c, a, mx, ct, og, mx = 0, 0, 0, [0], [0] * x, 0
Uggh, this is terrible in so many ways.
Please don't write code like that.
Find a classmate.
Show them this one line of code,
or all the code up to a given line of code,
and ask them to say out loud what the code does.
If they cannot offer a cogent explanation
related to the Business Domain (here, wizardry),
then go back and write new code.
Repeat, until your partner is able to
convince you that they understand the
technical idea you were trying to convey
in the source code.
The problem statement introduced two variables:
n
pages, and
- page index
i
(one-origin, like Fortran, different from python's zero-origin)
You get those variables for free.
The others, you're going to have to work for.
Perhaps og
denotes "original grimoire"?
If so, spell that out for us, with a longer identifier
or at least with a # comment
.
c,a,mx,ct,og,mx = ...
This code is so obscure that not even you can read it,
as we see mx
being introduced twice.
No one you show this code to knows what c
, a
, mx
, or ct
mean.
And neither will you in a month or two.
You owe it to the Gentle Reader to explain what's going on,
in sufficient detail that a maintenance engineer could
maintain this code (fix a bug, refactor, speed up, add a feature)
without needing to have a conversation with you.
The tuple unpack (the ,
comma notation) that you're
using here does not aid clear exposition.
If you really want to zero a bunch of variables in one line
(not necessarily a good approach to follow)
then at least say so clearly:
c = a = mx = 0
Now, as I'm parsing out the sequence of several variables,
at least I needn't juggle a few values to match up -- "is this
variable being set to a single-valued list? Is it being set to
a bunch of zeros? Or is it being set to a scalar?"
The line does just one thing, it sets stuff to zero.
be boring
x = int(input())
This is valid python syntax.
It's unclear how this line of code relates to the Problem Statement.
It would have been clearer if you either
(A.) adopted the contest's notation of n
, or
(B.) explained the "change of variables" to the Gentle Reader, who after
all has just finished reading the contest's explanatory material.
Writing code offers lots of room for creativity,
but that doesn't mean you should make up new names
for things that already have a name.
Prefer: n = int(input())
already visited graph nodes
# -1 is a marker for when the loop has already visited a page
t[j] = -1
That is a helpful comment, and I thank you for it.
Please continue to write such comments.
Or, write code which explains what the English sentence explains.
Rather than using an obscure
magic number
for this
sentinel,
we could explicitly name it:
VISITED = -1
...
t[j] = VISITED
Rather than overwrite the t
vector,
it might be more convenient to use this familiar idiom:
seen = set()
...
seen.add(j)
and then we can ask if k in seen:
to find out if node k
has already been visited.
algorithm
cycles
We are given a
graph,
and asked to find
cycles
in it, as well as paths which lead to a cycle.
In particular, we care about finding longer and longer paths,
with size bounded by n
.
The contest refers to path length as "spell length".
By the
pigeonhole principle
there must be at least one cycle, of length n
or smaller.
Consider this brief example:
2 --> 3 --> 1
^ |
| |
+-----------+
Page 1 takes us to page 2, and we soon are back at a visited page.
Max path length through a cycle is 3, and there are three such paths
or "spells", depending on where we start.
Consider this larger graph:
1 --> 2 --> 3 --> 4 --> 5 <-- 6 <-- 7 <-- 8 <-- 9
^ |
| |
+-----------+
Starting on either pages 1 or 2 will take us to page 3
and hence the cycle.
But it would be best to start on page 9, which brings
us to the cycle via page 5.
Once having exhausted the cycle, we're done.
There's more than one way to do
cycle detection.
Some ways use more memory than others.
For example the seen
set recommended above uses O(n) memory.
In contrast
Floyd's tortoise and hare
approach uses O(1) memory, at the cost of maybe taking
a little longer to complete, though still with maximum of
O(n) time complexity, even shorter if cycle lengths are << n.
For any given node, you want to identify
- whether it is a cycle node (part of a cycle), and
- what that cycle length is.
Then the problem boils down to finding longest tail (e.g. 9
above)
that leads into a longish cycle.
Let's see, how could we do that?
reverse links
The problem has inconveniently given us the forward link
from a given node to its successor node.
So 9
leads us down into a cycle.
But we might have first (wastefully!) tested 6
and 7
before that.
If we had the reverse link from a given node to its predecessor,
we could start at the cycle and efficiently chase
back from 5
to 6
and eventually 9
.
(We would also have to explore the 3 --> 2 --> 1 path --
them's the breaks.)
memory
The contest explains that n
is bounded by 3e5
,
and our datastructures must fit within a fixed memory budget.
Now, python list
s have a lot of things going for them;
they are certainly flexible and convenient.
But there's a lot of pointers in there, n
of them, in fact.
On a 64-bit machine, suppose it takes about 8 bytes
to store an int
value.
When we stick it in a list
, that costs another 8 bytes
for the list to point to the integer object.
Flexibility is all well and good, but we'll never store a str
nor a float
object, since the contest uses strictly int
page numbers.
An array
lets us efficiently allocate storage for n
integers,
saving a ton of storage for all those pointers:
from array import array
rev_link = array("I", range(n))
assert rev_link.itemsize == 4 # each element is a 32-bit unsigned integer
Since 3e5
is less than four billion,
we're confident each page number will fit within that allocation.
Also, the allowed memory budget turns out to be pretty generous;
we can allocate a little more than two hundred bytes per input node.
Iterate through the page numbers, recording reverse links, at O(n) cost.
A node (e.g. 5
) can have in-degree greater than one,
so we're probably back to using n
containers,
such as list
s, for this.
longest path
Now it's just a matter of doing a
DFS
outward from each cycle, in search of
long paths.
Choose a cycle node.
Use simple DFS, Kruskal,
Borůvka, or
Prim
to find paths leading outward, away from the cycle.
Identify the longest path(s) and report them.
memoize
Record “longest path from here” at each node.
For cycle nodes that’s the length of the cycle.
DFS will track its outbound path lengths and record max path length at each node it visits.
Notice that if there are C cycles to process, we could compute C max() assignments per node.