I think you should start with the data types that the language has built in like arrays and pointers, and when your students comprehend those, move on to classes and OO, then the STL.
The reason is that you can teach people to understand arrays without understanding much else besides variables and the underlying computer architecture, but you can't teach them to understand vector
without teaching them classes first. If you use the STL from the get go, your students will have to just live with not having a clue about how vector
works exactly. And then when you get to that point, they won't have a good enough grasp of pointers and arrays and things that you get from doing stuff like writing your own vector class, writing your own linked list class, etc. that will be necessary to appreciate and exploit its features. It annoys me when students say "what's that?" and teachers say "just ignore it, you'll learn it later."
And as Demian pointed out in the comments, deciphering the relatively cryptic messages you get from template errors is significantly harder than understanding the errors you may get from arrays/non-template constructs.
I do not feel the same way about cout
and printf
. Neither is any lower-level than the other, except that cout
uses operator overloading.
This may seem stupid, but I am absolutely fanatic about having people understand the very basic building blocks of everything before moving on to the abstractions. You shouldn't use smart pointers until you're proficient with raw pointers, no vectors before arrays, that sort of thing.
I often say this, but I'll say it again: it's better to teach students long division first and then let them use a calculator than to let them use a calculator then teach them long division afterwards.
As for books to teach beginners, see the master list of good C++ books.
I'll try to post some helpful hints, but with all due respect, this is Release Management 101, and if you really have a huge code base and a need for parallel work streams in the organization, you would do well to either read a book on it or hire somebody with more experience in this area.
Assuming this situation:
- The business needs new functionality that will take a long time to complete.
- The business needs various bugs fixed now.
- You have multiple developers who can work on different areas simultaneously.
- You cannot defer the development of the feature until after the bugs are fixed.
Make absolutely sure that point #4 is true, because if you can invalidate that assumption, your job suddenly gets a whole lot easier. I can't tell you much time I've saved in the past by simply not doing bug-fixing and feature-development at the same time. That said, it is sometimes an unfortunate necessity.
Formally, your options are:
Feature Branches: You create a branch where all of the new feature development will take place. Not recommended because they inherently break Continuous Integration. You can of course clone your "CI build" for the feature branch, but that's not actually CI because the code hasn't actually been integrated, it's just being built and maybe tested in isolation.
Feature Toggles: This is typically not done with an ifdef
but rather a configuration option. The ability to set this at runtime, not build time, is important because it makes testing and deployment a lot easier. Compile-time toggles require you to maintain two separate builds and two separate artifacts, but you can only promote one, a situation which tends to introduce ambiguity and process short-cutting.
Runtime feature toggles are partially recommended with some caveats:
It is important to minimize the surface area of the toggle. Ideally you would want the check to happen in only one place. Feature toggles that get too big for their britches are evil for the same reason that Singletons and other global state are. Try to check it just once, at application startup.
Too many feature toggles can become a spaghetti mess of their own over time, essentially creating a program-within-a-program. Feature Z depends on Feature Y which depends on Feature X but not Feature Q which is deprecated and breaks feature AB... etc. So in addition to minimizing the scope of individual toggles, you need to take steps to minimize the number of toggles and properly manage the dependencies between them. Even if there are no dependencies, you still need to run all end-to-end tests with each state that could plausibly be used in production, which quickly becomes a combinatorial explosion for the testers and release managers.
Parameterized Build or simply Multiple Targets: This is an incredibly useful tool/technique but I've never seen it used to build different feature sets and can't recommend that as an option here. You would normally use it to build for different platforms and/or perform certain one-off-tasks like running integration tests, generating documentation, etc. Using it as a poor-man's feature toggle has all of the potential drawbacks of a feature toggle and hides all of the complexity in your build scripts, which are generally the least flexible and most difficult to test of all your code.
Feature Abstraction, AKA Branch By Abstraction (a dangerous misnomer since it does not involve any actual branching): This means you encapsulate all of the interesting functionality of the new feature in a single class and use refactoring techniques (particularly Extract Interface) to encapsulate all of the existing functionality in the same interface. Then when you're ready to launch the feature, you just change the implementation.
This combines the advantages of (a) being in your trunk or mainline, (b) being fully covered by unit tests and possibly integration tests, and (c) not introducing any new global state to the application. This is much easier to do if your application uses dependency injection but not impossible if it doesn't. This is always the best choice if and when you are able to do it - it introduces the least overhead and the lowest risk, and can be combined very easily with options 2 or 3 above if you need to make the feature available on-demand to your testers.
Note that when using this option, once the feature has actually been launched, you should reevaluate whether the abstraction is still necessary and remove it if the overhead of maintaining it outweighs whatever benefits you're still getting.
You should choose options 4, 2, and 1 in descending order of preference. This is coming from someone with a whole lot of experience with all of the above options.
Final note: Fixing a bug or adding a new feature is not refactoring. Refactor does not mean "change", it refers to preserving the existing functionality exactly during some code change. So please don't use it to refer to your situation; what you're doing is the opposite of refactoring.
Best Answer
Key insight: there's no need to eagerly construct the adjacency list. In fact, there's no need to ever construct the adjacency list; you just need a way to find the next nodes. With this insight, we can construct an efficient version of the search.
Lazy edges
For any given node, finding indices i+1 and i-1 is trivial. Finding nodes that have the same value merely requires a lookup in the
sets
datastructure you've already created.The rest of your breadth first search still applies equally; you mark off nodes you've already seen to make sure you don't revisit them.
Grouped values
Note that there's really no reason to return to a value once you've left it. Any path that reaches an X-valued index can reach any other X-valued index on the next step. That means your maximum path is 19 (each value can occur at most twice for a 20 nodes i.e. 19 edges along the path).
However, it it also means you don't want the following to occur:
So you're doing quadratic work (each instance of a value iterates over each instance of the same value) and it's obviously pointless to boot. Once you process an X-valued index and thus visit all other X's, you know you'll never need to follow an X->X edge again. Easy fix? Just empty the list of indices by that value once you've processed one.
This new algorithm is linear time:
sets
is linear (each index is inserted once)If you really want, you can possibly shave off a bit of the constant factor here. After all, the final path will pass through each value once (and when it does, touching one or two indexes for that value). You can therefore construct a much, much smaller graph of how values can reach each other; solving that for the shortest paths is essentially free (at most 10 nodes, after all), and a shortest path through the indexes must be the shortest path through the values (proof left to reader, but note that any shortcut via i+1/i-1 costs at least a step, but can never win more than a step). Then, when you solve the bigger graph of indices, you don't need to bother taking any steps that conflict with what you already know of the solution; i.e. don't enqueue any indices that have a value other than your own or the "next" value along any shortest path in the value graph. (In essence, this optimization is A*).
Similarly, you could segment your queue by value and distance, and skip processing the rest of the current value with greater distance once you've found a way to the next.
Such trickery is almost certainly not worth it, but hey, it's possible :-).
Coding Style: I'd suggest naming your variables after what they represent. Your code is a little hard to follow because it contains things like
sets
(of what? why? - oh it's a way to lookup indices having a value), and a queueq
(again, what's this mean? oh it's the reachable set in order of distance). Give meaningful concepts (that you repeat in the code) likeadj[dq][i]
a name. It's the neighbor you're going to visit. This algorithm is so simple, it's eventually clear, but there's no reason it shouldn't be clear right away.TL;DR