Placing a user in a binary tree - php

How can I find where to place users in my binary tree? considering the following binary tree:
1
/ \
2 3
/ \ / \
4 5 6 7
/ \ / \
8 9 10 11
How can I build a function to return where the next user should be placed?
The rule here is that a user must be placed up as high as possible in the tree under their sponsor with each node getting them evenly. So for example, for anyone coming in with 1 as their sponsor: the next user should go to 6, the one after to 7, then to 6, then to 7, then to 8.
If someone had a sponsor of 5, they would go under 10. Sponsor of 2, they would go under 8, etc.
I've been stuck for hours trying to figure this out, and built a recursive function but it would just go down the left side and land on 8 first (as it checks 1, then 2, then 4, then 8)
My model for the tree is pretty basic (if relevant), its a table with a self-relationship like so:
id | user_id | parent_id
So each user can only have 2 children (2 entries with their id as a parent_id)

Typically when you do recursion, you end up with exactly what you got, you fly down one side (in this case the leftmost) checking everything as you go and then end up finding something way down at the bottom of the tree.
What you want to do is use a First In First Out ("FIFO") stack. You start out by adding your tree to the stack and while there is anything on the stack and you still haven't found space, you loop. Each loop you get the first item off the stack and loop again to check each of it's children for space. If you find space, you set the child aside, if you didn't find space, you add the child to the end of the stack so we check the child's children on a later iteration. You keep doing this until you find some space. After checking all of the children nodes, if any space was found you compare the nodes set aside to see which has the most free space and that is your target. This lets you search horizontally by pulling stuff off the start of the stack and adding new things to the end. You end up checking each neighbor before the children.
Here is a sample that will return a reference to the first open node found:
<?php
$tree = [
1 => [
2 => [
4 => [
8 => [],
9 => [],
],
5 => [
10 => [],
11 => [],
],
],
3 => [
6 => [],
7 => [],
],
]
];
function &findNextLocation(&$tree, $maxChildren=2){
//shortcut, check the root tree first
if(count($tree) < $maxChildren){
return $tree;
}
//fifo stack, start with the whole tree.
//we use a FIFO stack here so that we can check all nodes horizontally
//before traversing down another level. This stores all the child nodes
//we still need to search.
$stack = array(&$tree);
//potential place with space
$out = null;
//go through and check everything
//loop while there is something on the stack and we haven't
//found space yet ($out is null).
//we check $out here so that we stop as soon as somewhere with
//space is found and assigned.
while(is_null($out) && !empty($stack)){
//get the first node from the tree
$branch = &$stack[0];
array_shift($stack);
//loop over every node at this branch and look for space
foreach($branch as $id=>&$node){
//if there is space, assign it to our output variable
if(count($node) < $maxChildren){
//check the number of open spaces compared to our out spaces
if(is_null($out) || count($out) > count($node)){
//better spot, assign this node
$out = &$node;
//if anyone has zero children, we can't find less than that so break
if(count($out) == 0){
break;
}
}
} else {
//space not found here, add to our stack so we check the children
$stack[] = &$node;
}
}
}
//not found
return $out;
}
print_r($tree);
//loop a few more times starting at our next number
for($i=12; $i<=20; $i++){
//get a reference to the open node
$node = &findNextLocation($tree);
//add this node as a child
$node[$i] = [];
//remove reference to the found node to prevent errors.
unset($node);
}
print_r($tree);
You can see a demo here: https://3v4l.org/X3BX4
The way you are describing this, it sounds like it is in an SQL table. You can use the above to find a position and insert a new value to an existing tree. Or if you add another column to the table that is depth meaning the distance from the root node, you can do it all in a query. The depth can't easily be calculated because it would require recursion which isn't really available with a procedure or user function. However, it is easy to update your current records by just parent depth+1. With a depth column, you can do something like this:
SELECT *
FROM tree_table as tt
WHERE
#where there is space (less than 2 children)
(SELECT COUNT(*)
FROM tree_table
WHERE parent_id=tt.id) < 2
ORDER BY
#distance from top of tree
depth,
#by free space
(SELECT COUNT(*)
FROM tree_table
WHERE parent_id=tt.id),
#leftmost
id
Demo here: http://sqlfiddle.com/#!9/5f6bfc/5

Related

Symfony/Sonata get next child (Sibling)

I'm looking for the cleanest way to get the next child sibling of an object (next child of the parent).
-- Parent Object
-- Child 1
-- Child 2 (<== Current object)
-- Child 3 (<== Required object)
-- Child 4
Let assume in this example that we are talking about pages (Sonata pages). Currently I have Page 2 (Child 2), and need the next page of the same parent (in this case child 3). In the case I have the last page (child 4), then I need the first child again.
One option would be to request the parent, then request all the childs, loop over all the childs and look for the current child. Then take the next child, or the first one in case there is no next one. But this seems like a lot of code, with a lot of ugly if logic and loops. So I'm wondering if there is some sort of pattern to solve this.
Eventually I came up with the next solution:
/**
* $siblings is an array containing all pages with the same parent.
* So it also includes the current page.
* First check if there are siblings: Check if the parent has more then 1 child
**/
if (count($siblings) != 1) {
// Find the current page in the array
for ($i = 0; $i < count($siblings); $i++) {
// If we're not at the end of the array: Return next sibling
if ($siblings{$i}->getId() === $page->getId() && $i+1 != count($siblings)) {
return $siblings{$i+1};
}
// If we're at the end: Return first sibling
if ($siblings{$i}->getId() === $page->getId() && $i+1 == count($siblings)) {
return $siblings{0};
}
}
}
This seems like a quite clean solution to tackle this problem. We don't have an excessive amount of loops and if logic, however the code is still readable.

PHP & Mysql tree navigation menu with unlimited sub categories

Im looking for a way to extract the data from the mysql database to create an unlimited subcategry menu.
The menu has about 1500 categories in it and they are stored into the database table (menu) in the following format :
category_id , category title , parent_id
i want to do 1 mysql query to put all the data into an array.
$menu_list =array();
$query = mysql_query("SELECT * FROM menu ORDER BY category_id ASC");
if(mysql_num_rows($query) != 0) {
while($row = mysql_fetch_array($query)){
$category_id = $row['category_id'];
$title = $row['category_title'];
$parent_id = $row[parent_id'];
$menu_list[] = array($category_id,$title,$parent_id);
}
}
That is the code that i am currently using to turn the data into the array.
I then need to loop through the array to build a menu up.
I will then hide all subcategories and expand with jquery ( I can do this bit no problem )
The problem i have is going through the array and displaying the data in the right order.
The menu has unlimited subcategories so it will probably need to be a recursive function to retrieve the data.
I have folllowed alot of the examples on here but then i get lost how to display the data..
i now need to end up with :
main item
sub cat
sub cat
sub cat
sub cat
main item
sub cat
main item
main item
sub cat
sub cat
sub cat
sub cat
sub cat
etc...
with each category in its own div
I will then need to be able to edit each category (ie title name and position if need be)
then if i change where a sub category is based a quick page refresh will reload the menu in the new order..
eventually i would like to make this into a drag and drop but that will be at a later date.
I hope this has explained it enough..
Thanks in advance..
So the problem has come back to haunt me again...
My method of ajax calling another mysql select was ok for the CMS section as it was only used from time to time.
But now we have got to the front end.
Each time the page is viewed the first 3 levels of the menu are pulled using numerous mysql requests.
As the categories are getting bigger this is now resulting in 1600+ categories being pulled numerous times each, and even at 1000 visits a day will result in more than a 1000000 sql requests a day.
So i think i will definately need to pull the categories into an array, and then recursively go through the array.
I have looked at the solutions above and they seem to work on paper but not in practise.
If anyone can attempt to give me another solution it would be appreciated..
just to recap in my db i have : id , category_name , parent_id
I am now using PDO with mysql and prepared statements for added security..
Thanks in advance..
Here is the code what you need
category_id AS id , category title AS kategori and parent_id AS kid
function countsubcat($pid)
{
$r=mysql_query("select count(kid) AS say from meskat where kid='$pid' limit 1");
$rw=mysql_fetch_array($r);
return $rw['say'];
}
function listmenu($pid = 0)
{
$res = mysql_query("select id,kategori,kid from meskat where kid='$pid'");
while($cat=mysql_fetch_array($res))
{
echo '<li>';
print''.$cat['kategori'].'';
if(countsubcat($cat['id'])>0)
{
print'<ul>';
listmenu($cat['id']);
print'</ul>';
}
echo '</li>';
}
}
echo '<ul>';
listmenu(0); //starting from base category
echo '</ul>';`
In MySQL, this will require many queries, one for each level plus one at a minimum.
you grab all the top-level categories, use them as root nodes for a forest (a group of trees).
You grab all of the categories that are children of any of them.
Add each of these categories as children to the parent node
If there are still children, go to step two, otherwise stop.
What you end up with a tree that you do a depth-first walk through to convert to some (presumably) html representation (e.g. nested lists).
There are some optimisations you can perform. If you sort the child levels by parent ID, then you can assume continuous blocks of children, meaning you don't have to do as many lookups to find the correct parent, just check for a parent id change.
If you maintain a list of the current lowest level of categories, indexed by ID, you can speed up forest creation.
If you overlap steps 2 and 4, then you only do n+1 queries (where n is the number of levels), so checking for more children is also grabbing the next level of children.
In terms of memory, this will use the memory required for the tree, plus lowest level lookup table and a current parent id.
The building algorithm it fairly fast, scaling linearly with the number of categories.
This method also successfully avoids corrupted sections of data, because it won't grab data that have cycles (no root node).
I also suggest using a prepared statement for the children query, since MySQL will compile and cache the query, it will speed up the operation by only sending new data across the wire.
If you don't understand some of the concepts, I suggest hitting up Wikipedia as I have tried to use standard terms for these.
Can you alter the table structure? In that case you could take a look at the Nested Set Model (link includes description and implementation details, scroll down to The Nested Set Model). Node insertion and removal becomes more complex, but allows you to retrieve the whole tree in just one query.
If you can assume that for any category category_id > parent_id is true, the following recursive functions to represent the menu as a multi-level array and render it as a nested HTML list would work:
$menu = array();
$query = mysql_query("SELECT category_id, category_title, parent_id FROM menu ORDER BY category_id ASC");
if(mysql_num_rows($query) != 0) {
while($row = mysql_fetch_assoc($query)) {
if(is_null($row['parent_id']))
$menu['children'][] = $row;
else
add_to_menu(&$menu,$row);
}
}
function add_to_menu($menu,$item) {
if(isset($menu['children'])) {
foreach($menu['children'] as &$child) {
if($item['parent_id'] == $child['category_id']) {
$child['children'][] = $item;
} else {
add_to_menu(&$child,$item);
}
}
}
}
function render_menu($menu) {
if(isset($menu['children'])) {
echo '<ul>';
foreach($menu['children'] as &$child) {
echo "<li>";
echo $child['category_id']." : ".$child['category_title'];
if(isset($child['children'])) {
render_menu(&$child);
}
echo "</li>";
}
echo '</ul>';
}
}
render_menu($menu);
So after various attempts at different solutions i actually have used jquery and a ajax load to retreive the next level.
With mine main categories all have a parent of 1 - shop i do a mysql scan for all categories with 1 as the parent id.
This will then display the main categories. Attached to each main category i have added a folder icon if they have any sub categories.
I have also inserted a blank div with an id of that category.
When the folder is clicked it does an ajax call to a php script to find all categories with that parent. These are appended to the parents_id div.
This inturn adds the category (with a css padding to indicate a sub category) and again a folder sign if it has sub categories as well as another blank div with the category_id.
You can continue this for as many categories and sub categories as the client requires.
I took it a stage further and added an edit icon to category/subcategory so categories can be moved or changed.
If you would like some sample code for this let me know...
Actually, you need very little code to build a tree.
You also only need one query.
Here is what I came up with (I use Drupal's database but it is explicit enough to understand):
$items = db_select('menu_items', 'mi')
->fields('mi')
->orderBy('position')
->execute()
->fetchAllAssoc('id', PDO::FETCH_ASSOC);
// Example of result. The ID as key is important for this to work.
$items = array(
3 => array('id' => 3, 'parent' => NULL, 'title' => 'Root', 'position' => 0),
4 => array('id' => 4, 'parent' => 3, 'title' => 'Sub', 'position' => 0),
5 => array('id' => 5, 'parent' => 4, 'title' => 'Sub sub', 'position' => 0),
6 => array('id' => 6, 'parent' => 4, 'title' => 'Sub sub', 'position' => 1),
);
// Create the nested structure. Note the & in front of $item.
foreach($items as &$item)
if($item['parent'])
$items[$item['parent']]['sub items'][$item['mid']] =& $item;
// Now remove the children from the root
foreach($items as $id => $item)
if($item['parent']) // This is a child
unset($items[$id])
At this point, all you need is a recursive function to display the menu:
function print_menu($items) {
echo '<ul>';
foreach($items as $item) {
echo '<li>';
echo '' . $item['title'] . '';
if(!empty($item['sub items']))
print_menu($item['sub items']);
echo '</li>';
}
echo '</ul>';
}

Given an array of values corresponding to the location of a leaf node, how would I access said node?

Note: I am unfamiliar with terminology regarding tree structures. Please forgive any oversights that may be a result of my ignorance!
Practical Example
Given an array as such:
Array
(
[0] => 0
[1] => 2
[2] => 8
[3] => 9
)
The tree node with the key "9" would be found at $tree[2][8][9] (with 0 being the root). Given the above array, how would I construct a statement in PHP that would access the leaf node?
Target Code
/*
Let's say I am given a $leafNodeID of 9, and I'd like to save some
data ($dataToSave) into said leaf node
*/
$leafNodeID = 9;
$dataToSave = array("name" => "foobar");
$tree_path = $this->findPathToRootNode($tree, $leafNodeID); // This returns the array found above.
${?????} = $dataToSave; // <-- Here be dragons
Thanks in advance!
Edit: For those wondering, my findPathToRootNode function just recursively finds the parent node, and saves it in the array format found above. If there's a better way to represent said data (especially if it solves my problem), that would be even better.
Edit: On a read through, it seems this question is less about trees, but rather how to access an array given its structure in a separate array. Tagging as such.
Make a self-targeting function. This should do the trick (untested)
function getLeaf($tree, $targetleaf, $depth = 0){
if (isset($targetleaf[$depth+1])
return getLeaf($tree[$targetleaf[$depth]], $targetleaf, $depth + 1)
else
return $tree[$depth];
}
With $tree being the data, $tree the path to the array, and $depth speaks for itself.
Call the function with
$leaf = getLeaf($tree,$targetleaf);

Linear Array with nodes randomly linked to other nodes in array, shortest path

INFO:
I have an Array of 100 nodes, [ 0 .. 99 ]. Each node can have an arbitrary number of linked nodes:
eg1, 0 links to 5, 10, 15, 20.
eg2, 1 links to 30, 40, 50.
eg3, etc..
All 100 nodes have at least one linked node, nodes do not know who links to them.
QUESTION:
How can I find the shortest link-path if provided with START and END.
eg. START=5, END=80, Link Path (example) : [5]->10->24->36->[80]?
I'm using Pascal and/or PHP, but understanding how is what I'm looking for [code helps too].
Plenty of reading/algorithms:
Shortest path problem. You effectively just have every edge ("link", as you called it) with an equal weight.
Do a Breadth First Traversal starting with the Start node and quit as soon as you find the end node.
Does this have cycles? i.e. is it a DAG?
If there aren't cycles:
List<Node> GetShortestPath(Node startNode, Node endNode)
{
//If this is the node you are looking for...
if (startNode.ReferenceEquals(endNode))
{
//return a list with just the end node
List<Nodes> result = new List<Nodes>();
result.Add(endNode);
return result;
}
List<Node> bestPath = null;
foreach(Node child in startNode.Children)
{
//get the shortest path from this child
List<Node> childPath = GetShortestPath(child, endNode);
if (childPath != null &&
( bestPath == null || childPath.Count < bestPath.Count))
{
bestPath = childPath;
}
}
bestPath.Insert(0, startNode);
return bestPath;
}
[Edit: Added an example for cycles]
If there can be cycles:
List<Node> GetShortestPath(Node startNode, Node endNode)
{
List<Node> nodesToExclude = new List<Node>();
return GetShortestPath(startNode, endNOde, nodesToExclude);
}
List<Node> GetShortestPath(Node startNode, Node endNode, List<Node> nodesToExclude)
{
nodesToExclude.Add(startNode);
List<Node> bestPath = null;
//If this is end node...
if (startNode.ReferenceEquals(endNode))
{
//return a list with just the child node
List<Nodes> result = new List<Nodes>();
result.Add(endNode);
return result;
}
foreach(Node child in startNode.Children)
{
if (!nodesToExclude.Contains(child))
{
//get the shortest path from this child
List<Node> childPath = GetShortestPath(child, endNode);
if (childPath != null &&
( bestPath == null || childPath.Count < bestPath.Count))
{
bestPath = childPath;
}
}
}
nodesToExclude.Remove(startNode);
bestPath.Insert(0, child);
return bestPath;
}
Two structures: a set and a list.
In the set, you store nodes you have already visited. This prevents you from following cycles.
The list is of objects containing: (1) a node, and (2) a pointer back to the node that found it.
Starting at the start node, add it to the set, add it to the list with a null back reference, and then add all the nodes it can reach to the list with back references to the index 0 in the list (the start node).
Then for each element in the list thereafter, up until you reach the end, do the following:
if it is in the set already skip it (you have already visited it) and move to the next item in the list.
otherwise, add it to the set, and add all nodes it can reach to the list with back references to the index you are 'looking at' to the end of the list. Then go to the next index in the list and repeat.
If at any point you reach the end node (optimally as you are adding it to the list - as opposed to visiting it in the list), track back through the back references to the start node and invert the path.
Example:
Given nodes 0 through 3, where
node0 --> node1
node0 --> node2
node1 --> node2
node2 --> node3
and node0 is START and node3 is END
SET = {}
LIST = []
Step 1 - add START:
SET = {node0}
LIST = [[node0, null]]
Step 2 - at index 0 of the list - add reachable nodes:
SET = {node0, node1, node2}
LIST = [[node0, null], [node1, 0], [node2, 0]]
Step 3 - at index 1 of the LIST - add reachable nodes:
node2 is already in the SET. skip adding reachable nodes to LIST.
SET = {node0, node1, node2}
LIST = [[node0, null], [node1, 0], [node2, 0]]
Step 4 - at index 2 of the LIST - add reachable nodes:
SET = {node0, node1, node2, node3}
LIST = [[node0, null], [node1, 0], [node2, 0], [node3, 2]]
Step 5 - reached END, now backtrack:
The END node (node3) inserted in the LIST has a back reference to index 2 in the list, which is node2. This has a back reference to index 0 in the list, which is node0 (START). Invert this and you get a shortest path of node0 --> node2 --> node3.
Is this a tree/graph or a forest? If it is a forest, the path may not be defined always. In case this is a tree/graph, try using Breadth-First-Search.
Think of it this way: say, you are out on a stealth mission to find cute chicks in your neighbourhood. You start at your own house and mark it as the START. You'd next go to knock on your closest neighbours, right? So, we'll do just that -- push all nodes connected to the start in a queue. Now, repeat the neighbour search for all the nodes in this queue. And keep doing this till you get your girl, err, the END.

Create SQL query that orders results by which condition they meet

This is for MySQL and PHP
I have a table that contains the following columns:
navigation_id (unsigned int primary key)
navigation_category (unsigned int)
navigation_path (varchar (256))
navigation_is_active (bool)
navigation_store_id (unsigned int index)
Data will be filled like:
1, 32, "4/32/", 1, 32
2, 33, "4/32/33/", 1, 32
3, 34, "4/32/33/34/", 1, 32
4, 35, "4/32/33/35/", 1, 32
5, 36, "4/32/33/36/", 1, 32
6, 37, "4/37/", 1, 32
... another group that is under the "4/37" node
... and so on
So this will represent a tree like structure. My goal is to write a SQL query that, given the store ID of 32 and category ID of 33, will return
First, a group of elements that are the parents of the category 33 (in this case 4 and 32)
Then, a group of elements that are a child of category 33 (in this case 34, 35, and 36)
Then the rest of the "root" categories under category 4 (in this case 37).
So the following query will return the correct results:
SELECT * FROM navigation
WHERE navigation_store_id = 32
AND (navigation_category IN (4, 32)
OR navigation_path LIKE "4/32/33/%/"
OR (navigation_path LIKE "4/%/"
AND navigation_category <> 32))
My problem is that I want to order the "groups" of categories in the order listed above (parents of 33 first, children of 33 second, and parents of the root node last). So if they meet the first condition, order them first, if they meet the second condition order them second and if they meet the third (and fourth) condition order them last.
You can see an example of how the category structure works at this site:
www.eanacortes.net
You may notice that it's fairly slow. The current way I am doing this I am using magento's original category table and executing three particularly slow queries on it; then putting the results together in PHP. Using this new table I am solving another issue that I have with magento but would also like to improve my performance at the same time. The best way I see this being accomplished is putting all three queries together and having PHP work less by having the results sorted properly.
Thanks
EDIT
Alright, it works great now. Cut it down from 4 seconds down to 500 MS. Great speed now :)
Here is my code in the Colleciton class:
function addCategoryFilter($cat)
{
$path = $cat->getPath();
$select = $this->getSelect();
$id = $cat->getId();
$root = Mage::app()->getStore()->getRootCategoryId();
$commaPath = implode(", ", explode("/", $path));
$where = new Zend_Db_Expr(
"(navigation_category IN ({$commaPath})
OR navigation_parent = {$id}
OR (navigation_parent = {$root}
AND navigation_category <> {$cat->getId()}))");
$order = new Zend_Db_Expr("
CASE
WHEN navigation_category IN ({$commaPath}) THEN 1
WHEN navigation_parent = {$id} THEN 2
ELSE 3
END, LENGTH(navigation_path), navigation_name");
$select->where($where)->order($order);
return $this;
}
Then I consume it with the following code found in my Category block:
// get our data
$navigation = Mage::getModel("navigation/navigation")->getCollection();
$navigation->
addStoreFilter(Mage::app()->getStore()->getId())->
addCategoryFilter($currentCat);
// put it in an array
$node = &$tree;
$navArray = array();
foreach ($navigation as $cat)
{
$navArray[] = $cat;
}
$navCount = count($navArray);
$i = 0;
// skip passed the root category
for (; $i < $navCount; $i++)
{
if ($navArray[$i]->getNavigationCategory() == $root)
{
$i++;
break;
}
}
// add the parents of the current category
for (; $i < $navCount; $i++)
{
$cat = $navArray[$i];
$node[] = array("cat" => $cat, "children" => array(),
"selected" => ($cat->getNavigationCategory() == $currentCat->getId()));
$node = &$node[0]["children"];
if ($cat->getNavigationCategory() == $currentCat->getId())
{
$i++;
break;
}
}
// add the children of the current category
for (; $i < $navCount; $i++)
{
$cat = $navArray[$i];
$path = explode("/", $cat->getNavigationPath());
if ($path[count($path) - 3] != $currentCat->getId())
{
break;
}
$node[] = array("cat" => $cat, "children" => array(),
"selected" => ($cat->getNavigationCategory() == $currentCat->getId()));
}
// add the children of the root category
for (; $i < $navCount; $i++)
{
$cat = $navArray[$i];
$tree[] = array("cat" => $cat, "children" => array(),
"selected" => ($cat->getNavigationCategory() == $currentCat->getId()));
}
return $tree;
If I could accept two answers I would accept the first and last one, and if I could accept an answer as "interesting/useful" I would do that with the second.
:)
A CASE expression should do the trick.
SELECT * FROM navigation
WHERE navigation_store_id = 32
AND (navigation_category IN (4, 32)
OR navigation_path LIKE "4/32/33/%/"
OR (navigation_path LIKE "4/%/"
AND navigation_category <> 32))
ORDER BY
CASE
WHEN navigation_category IN (4, 32) THEN 1
WHEN navigation_path LIKE "4/32/33/%/" THEN 2
ELSE 3
END, navigation_path
Try an additional derived column like "weight":
(untested)
(IF(criteriaA,1,0)) + (IF(criteriaB,1,0)) ... AS weight
....
ORDER BY weight
Each criteria increases the "weight" of the sort.
You could also set the weights distinctly by nesting IFs and giving the groups a particular integer to sort by like:
IF(criteriaA,0, IF(criteriaB,1, IF ... )) AS weight
Does MySQL have the UNION SQL keyword for combining queries? Your three queries have mainly non-overlapping criteria, so I suspect it's best to leave them as essentially separate queries, but combine them using UNION or UNION ALL. This will save 2 DB round-trips, and possibly make it easier for MySQL's query planner to "see" the best way to find each set of rows is.
By the way, your strategy of representing the tree by storing paths from root to tip is easy to follow but rather inefficient whenever you need to use a WHERE clause of the form navigation_path like '%XYZ' -- on all DBs I've seen, LIKE conditions must start with a non-wildcard to enable use of an index on that column. (In your example code snippet, you would need such a clause if you didn't already know that the root category was 4 (How did you know that by the way? From a separate, earlier query?))
How often do your categories change? If they don't change often, you can represent your tree using the "nested sets" method, described here, which enables much faster queries on things like "What categories are descendants/ancestors of a given category".

Categories